Introduction
TRX is a fast, keyboard-driven terminal UI (TUI) package manager written in Rust. It gives you a unified, keyboard-first interface for searching, inspecting, and managing packages — whether you're on macOS with Homebrew, Arch Linux with Pacman + AUR, or Debian/Ubuntu with APT.
Search 50,000+ packages in under 50 ms. Install, remove, and update without leaving your terminal.
No daemon. No config file required. Just run trx.
Highlights
| Feature | Detail |
|---|---|
| Speed | Fuzzy search with sub-50 ms results |
| Keyboard-first | Vim-inspired navigation; no mouse needed |
| Unified interface | Same keybindings across all package managers |
| Non-blocking | All I/O on OS threads — UI never freezes |
| Self-updating | Checks GitHub releases on startup |
| Extensible | Pluggable backend trait — add a new PM in one file |
Supported Platforms
| Package Manager | Platform | Status |
|---|---|---|
| Pacman | Arch Linux | ✅ Implemented |
| yay (AUR) | Arch Linux | ✅ Implemented |
| APT | Debian / Ubuntu | ✅ Implemented |
| Homebrew | macOS | ✅ Implemented |
| dnf / yum | Fedora / RHEL | 🔜 Planned |
| zypper | openSUSE | 🔜 Planned |
| winget / scoop | Windows | 🔜 Planned |
Quick Links
- Installation — get TRX running in 30 seconds
- Usage — keybindings and daily workflow
- Architecture — how TRX is structured internally
- Adding a Backend — extend TRX to support a new package manager
Installation
One-liner (Recommended)
The quickest way to install TRX on any supported platform:
curl -fsSL https://trx.pidev.tech/install.sh | sh
This script detects your OS and architecture, downloads the appropriate pre-built binary from GitHub Releases, and places it in /usr/local/bin.
Cargo
If you have a Rust toolchain installed, you can install directly from crates.io:
cargo install trx-cli
The binary is named trx.
Build from Source
git clone https://github.com/pie-314/trx.git
cd trx
cargo build --release
sudo cp target/release/trx /usr/local/bin/
Requirements:
- Rust 1.70 or later (
rustupis the easiest way to get it) - A terminal with Unicode and truecolor support (most modern terminals qualify)
- The package manager for your platform must be installed and available on
$PATH
Self-updating
TRX checks the GitHub Releases API on every startup. If a newer version is found, it downloads and replaces the running binary automatically, then exits with a prompt to restart.
Supported auto-update targets:
| OS | Architecture |
|---|---|
| Linux | x86_64 |
| macOS | x86_64 |
| macOS | aarch64 (Apple Silicon) |
Verify Installation
trx --version
trx --help
Usage
Start TRX by simply running:
trx
TRX will detect your system's package manager automatically and open the TUI.
Tabs
TRX has three tabs, cycled with Tab / Shift+Tab:
| Tab | Description |
|---|---|
| Search | Fuzzy-search all available packages |
| Installed | Browse packages currently installed on the system |
| Updates | Packages with a newer version available |
Keybindings
Global
| Key | Action |
|---|---|
Tab | Switch to next tab |
Shift+Tab | Switch to previous tab |
? | Toggle help overlay |
q / Esc | Quit TRX (or exit current mode) |
Navigation
| Key | Action |
|---|---|
↑ / k | Move selection up |
↓ / j | Move selection down |
Package Operations
| Key | Action |
|---|---|
Space | Toggle package selection |
i | Install all selected packages |
x | Remove all selected packages |
U | Full system upgrade |
R | Refresh package databases |
Search Tab
| Key | Action |
|---|---|
e | Enter search / editing mode |
Esc | Exit search mode (return to normal navigation) |
Workflow Example
- Press
eto enter search mode. - Type a package name (e.g.
ripgrep). Results appear within 50 ms as you type. - Use
↓/jto move through results. The details panel on the right updates automatically. - Press
Spaceto select one or more packages. - Press
ito install the selection.
Command-line Options
TRX accepts a small set of flags when called from the command line (before the TUI starts):
trx [OPTIONS]
Options:
-v, --version Print version information
-h, --help Print help information
Architecture Overview
TRX is split into a small set of focused modules. Each module has a single responsibility and communicates with the others through well-defined interfaces.
src/
├── main.rs # Entry point, terminal setup, execute_external_command helper
├── config.rs # TOML configuration loading
├── updater.rs # GitHub release polling and binary self-replacement
├── ui/
│ ├── mod.rs
│ ├── app.rs # App state, event loop, channel polling
│ ├── draw.rs # ratatui rendering logic
│ └── input.rs # InputMode enum, character-level editing, debounce state
├── managers/
│ ├── mod.rs # PackageManager trait, Package struct, parse_alternating_lines, DETAILS_CACHE
│ ├── arch.rs # ArchManager — delegates to pacman.rs and yay.rs
│ ├── pacman.rs # Pacman system-call wrapper
│ ├── yay.rs # yay/AUR system-call wrapper
│ ├── apt.rs # AptManager
│ └── brew.rs # BrewManager
└── fuzzy/
└── mod.rs # Scoring-based fuzzy match engine
Key Data Structures
Package
Defined in src/managers/mod.rs, this is the universal package representation passed through all layers of the application:
#![allow(unused)] fn main() { pub struct Package { pub provider: String, // e.g. "pacman", "aur", "apt", "brew" pub name: String, // full name, possibly prefixed: "core/ripgrep" pub version: String, pub description: String, pub score: f64, // fuzzy match score used for ranking } }
App
Defined in src/ui/app.rs. Holds all runtime state:
input— current search stringcurrent_tab— which of the three tabs is activepackages— the currently displayed listchecked/selected_names— multi-selection stateinstalled_packages—HashSet<String>fetched once on startupdetails_state— sidebar content (Empty | Loading | Success | Error)loading— drives the spinner in the headermanager—Arc<Box<dyn PackageManager>>shared across spawned threads
Module Interactions
keyboard event
│
▼
App::run() ──── spawns thread ──► PackageManager::search()
│ │
│◄── result_rx (mpsc) ◄───────────────┘
│
▼
draw_ui() (ratatui frame render)
The event loop in App::run does three things every iteration:
- Poll keyboard — via
crossterm::event::pollwith a short timeout so the loop never blocks long. - Drain channels —
try_recvonresult_rxanddetails_rx(non-blocking). - Render — call
draw_uito produce the next terminal frame.
Startup Sequence
- Parse CLI flags (
--version,--help). - Call
updater::check_for_updates()— if a newer release exists, self-update and exit. - Initialise the ratatui terminal (
ratatui::init). - Load
Configfrom the TOML file (or write defaults). - Call
managers::get_system_manager(&config)to select the correct backend. - Create the
mpscchannels and constructApp. - Enter
App::run()— the main event loop. - On exit, restore the terminal (
ratatui::restore).
Concurrency Model
TRX deliberately avoids an async runtime. All background work is done with OS threads and std::sync::mpsc channels. This keeps the dependency tree small, makes the code easy to reason about, and avoids the overhead of an executor in a single-user TUI.
Channel Architecture
Two channels flow into the main event loop:
| Channel | Producer | Consumer | Payload |
|---|---|---|---|
result_rx | Search / list-load threads | App::run | (String, Vec<Package>) — a tag plus a list of packages |
details_rx | Details-fetch threads | App::run | DetailsState |
The tag in result_rx lets the event loop distinguish between results for Search, Installed ("__INSTALLED__"), and Updates ("__UPDATES__"). Stale results (where the tag no longer matches the current UI state) are discarded.
Search Flow
User types a character
│
(50 ms debounce)
│
App spawns OS thread
│
▼
PackageManager::search(&query) ← runs system command, parses output, scores results
│
result_tx.send((query, packages))
│
Main loop: result_rx.try_recv()
│
App updates packages list + triggers details fetch
The 50 ms debounce is implemented in input.rs / app.rs: last_input_time is refreshed on every keystroke. check_and_execute_search is called each frame and only fires a thread when Instant::now() - last_input_time >= 50ms and the query has changed.
Details Fetch Flow
Whenever the selected row changes (navigation or new search results), trigger_details_fetch spawns a thread that calls PackageManager::get_details. Results arrive on details_rx and update the sidebar.
A global DETAILS_CACHE (Arc<Mutex<HashMap<String, HashMap<String, String>>>>) prevents redundant system calls for packages that have been inspected before.
External Command Execution
When the user triggers an install (i), remove (x), upgrade (U), or refresh (R), TRX must hand control of the terminal to the package manager's interactive output. This is handled by execute_external_command in main.rs:
- Disable raw mode — so the child process receives normal terminal I/O.
- Leave alternate screen — the TUI disappears; the package manager's output is printed normally.
- Run the command — via
std::process::Command. - Wait for Enter — the user can review the output.
- Re-enter alternate screen and raw mode — TRX redraws the TUI.
Thread Safety
manager is wrapped in Arc<Box<dyn PackageManager>> (where PackageManager: Send + Sync). Cloning the Arc into a spawned thread is the only synchronisation needed for backend calls.
UI & Rendering
TRX's TUI is built on ratatui, a Rust library for building terminal UIs with a retained-mode, declarative rendering model.
Layout
draw_ui in src/ui/draw.rs computes the full layout on every frame:
┌─────────────────────────────────────────┐
│ Help header (1 line) │
├─────────────────────────────────────────┤
│ Tab bar (3 lines) │
├───────────────────────┬─────────────────┤
│ │ │
│ Package list │ Details panel │
│ + search input │ │
│ (Search tab only) │ │
│ │ │
├─────────────────────────────────────────┤
│ Status bar (1 line) │
└─────────────────────────────────────────┘
The split between the list and details panel is responsive:
- Terminal width ≥ 100 columns → 50 / 50 horizontal split
- Terminal width < 100 columns → 60 / 40 split (still horizontal but tighter)
Input Modes
InputMode in src/ui/input.rs is a simple two-variant enum:
#![allow(unused)] fn main() { pub enum InputMode { Normal, Editing, } }
- Normal — navigation, package operations, tab switching.
- Editing — the search input bar is focused; characters are routed to
App::enter_char/App::delete_char.
The cursor is hidden in Normal mode and shown at the current character position in Editing mode.
Spinner
While a background search or list-load is in progress, App::loading is true. The draw layer reads App::spinner_tick (incremented each frame) to index into a Braille spinner sequence:
#![allow(unused)] fn main() { const SPINNERS: [&str; 10] = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; }
Help Overlay
Pressing ? sets App::show_help = true. draw_ui renders a centred popup over the main content using the centered_rect helper, which computes a rectangle as a percentage of the terminal area. The overlay is cleared with the ratatui Clear widget before drawing the help text on top.
Performance
- Minimal redraws — the event loop uses a short poll timeout (not a busy-wait), so frames are only rendered when there is user input or a channel message.
- No double-buffer diffing overhead — ratatui handles diffing internally; TRX simply calls
terminal.draw(|frame| draw_ui(frame, app))each iteration. - Pure functions in draw layer —
draw_uiand its helpers take&mut App(forListStateselection) but do not mutate business logic, keeping rendering predictable and testable.
Fuzzy Search Engine
The fuzzy search engine lives in src/fuzzy/mod.rs. It is intentionally self-contained — no external crates — and is optimised for the substring-heavy patterns typical in package names.
Public API
#![allow(unused)] fn main() { /// Returns a score in [0.0, ∞). Returns 0.0 when there is no fuzzy match. pub fn fuzzy_match(query: &str, target: &str) -> f64; /// Returns the character indices in `target` that match `query` in order, /// or `None` if no such sequence exists. pub fn fuzzy_get_indexes(query: &[char], target: &[char]) -> Option<Vec<usize>>; /// Computes the final score given the matched positions. pub fn calculate_score(query: &[char], target: &[char], indices: &[usize]) -> f64; }
Matching Algorithm
fuzzy_get_indexes performs a greedy left-to-right scan: for each character in query it finds the first remaining position in target that matches (case-insensitively). If any query character cannot be matched, None is returned and the package is excluded from results.
Scoring
calculate_score is inspired by the VS Code fuzzy finder algorithm. The score rewards:
| Condition | Bonus |
|---|---|
| Every matched character | +1.0 |
| Consecutive run of matches | +1.0 + 0.3 × run length |
| Match at position 0 (start of name) | +4.0 |
Match after a separator (-, _, /, ., ) | +2.5 |
And penalises gaps between matched characters:
| Condition | Penalty |
|---|---|
| Gap of n characters between consecutive matches | −0.15 × n |
The raw score is then normalised by target_length * 0.15 + 1.0 to prevent long package names from dominating.
Integration
fuzzy_match is called from parse_alternating_lines in src/managers/mod.rs for every package returned by a backend. Packages with a score ≤ 0.01 are dropped, and the remainder are sorted descending by score before being sent to the UI.
Each backend's search method may also pass the query directly to the underlying tool (e.g. pacman -Ss <query>), so the fuzzy layer acts as a re-ranking step on top of the backend's own filtering, not a replacement for it.
Package Manager Backends
TRX abstracts all package manager interaction behind the PackageManager trait defined in src/managers/mod.rs. The correct backend is selected at runtime in get_system_manager.
PackageManager Trait
#![allow(unused)] fn main() { pub trait PackageManager: Send + Sync { fn name(&self) -> &str; fn search(&self, query: &str) -> Vec<Package>; fn get_installed(&self) -> HashSet<String>; fn get_installed_details(&self) -> Vec<Package>; fn get_updates(&self) -> Vec<Package>; fn get_details(&self, pkg: &str, provider: &str) -> Option<HashMap<String, String>>; fn install( &self, terminal: &mut DefaultTerminal, pkgs: &HashSet<String>, ) -> Result<(), Box<dyn std::error::Error>>; fn remove( &self, terminal: &mut DefaultTerminal, pkgs: &HashSet<String>, ) -> Result<(), Box<dyn std::error::Error>>; fn system_upgrade( &self, terminal: &mut DefaultTerminal, ) -> Result<(), Box<dyn std::error::Error>>; fn refresh_databases( &self, terminal: &mut DefaultTerminal, ) -> Result<(), Box<dyn std::error::Error>>; } }
terminal is passed into mutating operations so that the backend can call execute_external_command — which temporarily hands the terminal back to the underlying package manager's interactive output.
Backend Selection
get_system_manager in src/managers/mod.rs uses a simple priority order:
OS == "macos"→BrewManagerpacman --versionsucceeds →ArchManager(Pacman + optional AUR helper)apt --versionsucceeds →AptManager- Fallback →
ArchManager(with defaultyayAUR helper)
Shared Utilities
parse_alternating_lines
Many package manager CLI tools output results in alternating-line format:
<name> <version> [flags...]
<description>
<name> <version> ...
<description>
parse_alternating_lines parses this format, calls fuzzy_match on each package name, drops scores ≤ 0.01, and returns results sorted by score.
DETAILS_CACHE
A global Arc<Mutex<HashMap<String, HashMap<String, String>>>> used by all backends to cache detail lookups (the get_details call). This avoids repeated subprocess invocations when the user scrolls back to a previously inspected package.
Supported Backends
| Backend | Source file | Platform |
|---|---|---|
| Pacman | src/managers/pacman.rs | Arch Linux |
| AUR / yay | src/managers/yay.rs | Arch Linux |
| APT | src/managers/apt.rs | Debian / Ubuntu |
| Homebrew | src/managers/brew.rs | macOS |
Arch Linux — Pacman
The Pacman backend is implemented across two files:
src/managers/pacman.rs— low-level wrappers around thepacmanCLIsrc/managers/arch.rs—ArchManagerstruct that implementsPackageManagerby composing Pacman and the AUR helper
ArchManager
ArchManager is constructed with an aur_helper string (default: "yay", configurable in config.toml):
#![allow(unused)] fn main() { pub struct ArchManager { pub aur_helper: String, } }
It implements PackageManager by delegating to pacman::* and yay::* functions.
Search
ArchManager::search merges results from both pacman -Ss and the AUR helper, sorts by fuzzy score, and truncates to 50 results:
#![allow(unused)] fn main() { fn search(&self, query: &str) -> Vec<Package> { let mut all = pacman::search_pacman(query); all.extend(yay::search_aur(query, &self.aur_helper)); all.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(Equal)); all.truncate(50); all } }
search_pacman calls pacman -Ss <query> and parses the alternating-line output via parse_alternating_lines.
Package Details
pacman_info first tries pacman -Si <pkg> (remote package info), then falls back to pacman -Qi <pkg> (locally installed info). The colon-separated key-value output is parsed into a HashMap<String, String>.
Results are cached in DETAILS_CACHE to avoid repeated subprocess calls.
Installed Packages
get_installed_packages() runs pacman -Q and returns a HashSet<String> of package names. get_installed_packages_details() runs pacman -Q and constructs Package structs from the name-version pairs.
Updates
get_updates() runs pacman -Qu to list packages with newer versions available and returns them as Vec<Package>.
Install / Remove
pacman -S <packages> # install
pacman -Rns <packages> # remove with unused dependencies
Both require sudo and hand control of the terminal to the interactive pacman output via execute_external_command.
System Upgrade & Refresh
| Key | Command |
|---|---|
U | sudo pacman -Syu |
R | sudo pacman -Sy |
Arch Linux — AUR (yay)
The AUR backend in src/managers/yay.rs wraps an AUR helper (default: yay) using the same interface as the Pacman backend. The AUR helper is configurable — see Configuration.
AUR Helper Configuration
The helper name is read from Config::aur_helper (default: "yay"). Any helper that accepts yay-compatible CLI flags should work:
# ~/.config/trx/config.toml
aur_helper = "paru"
Search
search_aur calls <helper> -Ss <query> and parses the alternating-line output via parse_alternating_lines. Packages receive the provider string "aur".
Package Details
aur_details runs <helper> -Si <pkg> and parses the colon-separated output into a HashMap. The Maintainer, URL, Votes, and Popularity fields typically appear in AUR results.
Install
aur_install runs <helper> -S <packages> via execute_external_command, handing control of the terminal to the helper's interactive output (which typically presents a PKGBUILD review step).
Provider Routing
When the user selects packages for install, ArchManager::install inspects each package name:
#![allow(unused)] fn main() { for name in pkgs { if name.starts_with("aur/") { yay::aur_install(terminal, &[name])?; } else { pacman::pacman_install(terminal, &[name])?; } } }
Packages prefixed with aur/ go to the AUR helper; all others go to pacman.
Debian / Ubuntu — APT
The APT backend is implemented in src/managers/apt.rs as AptManager (a zero-size struct, since APT requires no runtime state).
Search
AptManager::search calls apt-cache search <query>, which outputs one package per line in name - description format:
ripgrep - recursively searches directories for a regex pattern
The name is extracted, fuzzy-scored against the query, and packages with score ≤ 0.01 are dropped. Note that apt-cache search does not return version numbers, so Package::version is empty for search results.
Installed Packages
get_installed runs dpkg-query -W -f='${Package}\n' and returns a HashSet<String>.
Package Details
get_details runs apt-cache show <pkg> and parses the colon-separated RFC 822-style output into a HashMap<String, String>. Keys include Package, Version, Description, Depends, Homepage, and others.
Install / Remove
| Operation | Command |
|---|---|
| Install | sudo apt install <packages> |
| Remove | sudo apt remove <packages> |
Both operations hand control of the terminal to the interactive APT output via execute_external_command.
System Upgrade & Refresh
| Key | Command |
|---|---|
U | sudo apt upgrade |
R | sudo apt update |
Updates
get_updates parses the output of apt list --upgradable to build a list of packages with newer versions available.
macOS — Homebrew
The Homebrew backend is implemented in src/managers/brew.rs as BrewManager (a zero-size struct).
Search
BrewManager::search calls brew search <query>, which returns one formula or cask name per line. Because brew search does not return descriptions or versions, both fields are left empty in the initial search results — they are populated lazily when the user selects a package and get_details is called.
Installed Packages
get_installed calls brew list --formula and returns a HashSet<String> of formula names.
Note: Casks (
brew list --cask) are not yet included in the installed list. This is a known limitation tracked in the roadmap.
Package Details
get_details calls brew info --json=v2 <pkg> and parses the JSON response. Key fields surfaced in the TUI sidebar include:
namedeschomepageversions.stableinstalled(list of installed versions)
Install / Remove
| Operation | Command |
|---|---|
| Install | brew install <packages> |
| Remove | brew uninstall <packages> |
Both hand control of the terminal to Homebrew's output via execute_external_command.
System Upgrade & Refresh
| Key | Command |
|---|---|
U | brew upgrade |
R | brew update |
Updates
get_updates runs brew outdated --formula and constructs a list of formulas with newer versions available.
Adding a New Backend
TRX is designed to make adding a new package manager straightforward. You only need to:
- Create a new file in
src/managers/ - Implement the
PackageManagertrait - Register the backend in
get_system_manager
Step 1 — Create the backend file
src/managers/dnf.rs # example: Fedora/RHEL dnf
Add the module to src/managers/mod.rs:
#![allow(unused)] fn main() { pub mod dnf; }
Step 2 — Implement PackageManager
Below is a minimal skeleton. All methods must be implemented (the trait has no default implementations):
#![allow(unused)] fn main() { use crate::managers::{Package, PackageManager}; use ratatui::DefaultTerminal; use std::collections::{HashMap, HashSet}; use std::process::Command; pub struct DnfManager; impl PackageManager for DnfManager { fn name(&self) -> &str { "DNF (Fedora/RHEL)" } fn search(&self, query: &str) -> Vec<Package> { if query.is_empty() { return Vec::new(); } let output = Command::new("dnf") .args(["search", query]) .output() .ok(); // Parse output and return Vec<Package>. // Use parse_alternating_lines if the format fits, // or write a custom parser. todo!() } fn get_installed(&self) -> HashSet<String> { let output = Command::new("dnf") .args(["list", "--installed"]) .output() .ok(); // Parse and return package names. todo!() } fn get_installed_details(&self) -> Vec<Package> { todo!() } fn get_updates(&self) -> Vec<Package> { let output = Command::new("dnf") .args(["list", "--upgrades"]) .output() .ok(); todo!() } fn get_details(&self, pkg: &str, _provider: &str) -> Option<HashMap<String, String>> { // Check DETAILS_CACHE first. { let cache = crate::managers::DETAILS_CACHE.lock().unwrap(); if let Some(cached) = cache.get(pkg) { return Some(cached.clone()); } } let output = Command::new("dnf") .args(["info", pkg]) .output() .ok()?; // Parse colon-separated key: value lines and store in DETAILS_CACHE. todo!() } fn install( &self, terminal: &mut DefaultTerminal, pkgs: &HashSet<String>, ) -> Result<(), Box<dyn std::error::Error>> { let names: Vec<&str> = pkgs.iter().map(String::as_str).collect(); let mut args = vec!["dnf", "install"]; args.extend(names); crate::execute_external_command(terminal, "sudo", &args) } fn remove( &self, terminal: &mut DefaultTerminal, pkgs: &HashSet<String>, ) -> Result<(), Box<dyn std::error::Error>> { let names: Vec<&str> = pkgs.iter().map(String::as_str).collect(); let mut args = vec!["dnf", "remove"]; args.extend(names); crate::execute_external_command(terminal, "sudo", &args) } fn system_upgrade( &self, terminal: &mut DefaultTerminal, ) -> Result<(), Box<dyn std::error::Error>> { crate::execute_external_command(terminal, "sudo", &["dnf", "upgrade"]) } fn refresh_databases( &self, terminal: &mut DefaultTerminal, ) -> Result<(), Box<dyn std::error::Error>> { crate::execute_external_command(terminal, "sudo", &["dnf", "check-update"]) } } }
Step 3 — Register the backend
In src/managers/mod.rs, add a detection branch in get_system_manager:
#![allow(unused)] fn main() { pub fn get_system_manager(config: &crate::config::Config) -> Box<dyn PackageManager> { if std::env::consts::OS == "macos" { return Box::new(brew::BrewManager); } if std::process::Command::new("pacman").arg("--version").output().is_ok() { return Box::new(arch::ArchManager::new(config.aur_helper.clone())); } if std::process::Command::new("apt").arg("--version").output().is_ok() { return Box::new(apt::AptManager); } // Add your backend here: if std::process::Command::new("dnf").arg("--version").output().is_ok() { return Box::new(dnf::DnfManager); } Box::new(arch::ArchManager::new(config.aur_helper.clone())) } }
Tips
- Use
parse_alternating_linesif the package manager outputs results in the standardname version\n descriptionformat. - Always check
DETAILS_CACHEat the top ofget_detailsto avoid redundant subprocess calls. - Call
execute_external_commandfor any operation that produces interactive output (confirmations, progress bars, etc.). - Make the struct
Send + Sync— thePackageManagertrait bound requires it. Zero-size structs and structs with only owned data satisfy this automatically. - Run
cargo clippybefore opening a PR — Clippy warnings should ideally be zero.
Configuration
TRX reads a single TOML configuration file on startup. If the file does not exist, it is created automatically with default values.
Location
The config file follows the XDG Base Directory convention via the directories crate:
| Platform | Path |
|---|---|
| Linux | $XDG_CONFIG_HOME/trx/config.toml (usually ~/.config/trx/config.toml) |
| macOS | ~/Library/Application Support/trx/config.toml |
| Windows | %APPDATA%\trx\config.toml |
Options
# ~/.config/trx/config.toml
# The AUR helper to use on Arch Linux systems.
# Any helper with yay-compatible CLI flags works (e.g. "paru", "aura").
# Default: "yay"
aur_helper = "yay"
Future Options
The following options are planned for future releases:
- Keybindings — remap any key to a different action
- Theme — colour scheme selection (dark / light / custom)
- Search limit — maximum number of results per tab
- Metadata cache TTL — how long
DETAILS_CACHEentries remain valid
See the Roadmap for status.
Contributing Guide
Contributions to TRX are welcome — whether that's a bug fix, a new backend, a UI improvement, or documentation. This page provides an overview. For full details, see CONTRIBUTING.md in the repository.
Getting Started
git clone https://github.com/pie-314/trx.git
cd trx
cargo build # ensure the project compiles
cargo run # smoke test in your terminal
cargo test # run the test suite
Contribution Areas
| Area | Examples |
|---|---|
| Backend Integrations | New package managers (dnf, zypper, winget) |
| TUI Improvements | New widgets, themes, layout changes |
| Fuzzy Search | Better scoring heuristics, performance |
| Performance | Caching, parallel execution |
| Bug Fixes | Reproduce, isolate, and fix issues |
| Documentation | Improve this site, README, examples |
Pull Request Workflow
- Fork the repository and create a feature branch:
git checkout -b feat/dnf-backend - Make your changes.
- Run
cargo fmtandcargo clippy(zero warnings preferred). - Run
cargo test. - Commit using conventional commit messages (see Coding Guidelines).
- Open a PR describing what changed, why, and how it was tested.
Issues
Before filing an issue, check if it already exists. When reporting a bug, include:
- Steps to reproduce
- Platform (OS, package manager version)
- TRX version (
trx --version) - Relevant terminal output or screenshots
Common issue labels: good first issue, help wanted, backend, tui, fuzzy, performance.
Coding Guidelines
Code Style
TRX follows standard Rust idioms. Before committing, always run:
cargo fmt
cargo clippy
Clippy warnings should ideally be zero. Rustfmt is non-negotiable.
Architecture Principles
- Keep the UI thread non-blocking. Never call blocking I/O on the main loop thread. Use
std::thread::spawn+mpscchannels for any subprocess call. - Prefer OS threads over async. TRX intentionally does not use Tokio or async/await. New features should follow the existing
std::thread+mpscpattern. - Return structured errors. Use
Box<dyn std::error::Error>for backend method errors, matching the trait signature. Do not panic on expected failure paths. - Cache detail lookups. Any
get_detailsimplementation should check and populateDETAILS_CACHEto avoid redundant subprocess calls. - Pure rendering. The
draw_uifunction and its helpers should read state but not mutate business logic. Keep rendering predictable.
Commit Messages
Use Conventional Commits:
feat(dnf): implement DnfManager backend
fix(ui): prevent double redraw on search
docs: update architecture overview
refactor(fuzzy): simplify gap penalty calculation
Scope tokens: ui, fuzzy, pacman, apt, brew, aur, config, updater, docs.
Development Tools
| Tool | Install | Purpose |
|---|---|---|
rustfmt | rustup component add rustfmt | Code formatting |
clippy | rustup component add clippy | Linting |
cargo-expand | cargo install cargo-expand | Macro debugging |
Environment Requirements
- Rust 1.70+
- A terminal supporting Unicode and truecolor
- The package manager for your platform installed and on
$PATH
Roadmap
This page tracks planned and in-progress features for TRX.
In Progress / Near-term
| Feature | Description |
|---|---|
| dnf / yum backend | Fedora and RHEL support via dnf |
| zypper backend | openSUSE support |
| winget / scoop backend | Windows support |
Planned
| Feature | Description |
|---|---|
| Configurable keybindings | Remap any key via config.toml |
| Pluggable themes | Colour scheme selection and custom themes via config |
| Transaction history | Log of installs/removes with rollback support |
| Batch / scripting mode | Non-interactive mode for CI and shell scripts |
| Dependency graph visualiser | Visual view of package dependency trees |
| Metadata caching | Persist DETAILS_CACHE across sessions for faster repeated searches |
| Plugin system | Load custom backends and widgets from shared libraries |
Completed
| Feature | Version |
|---|---|
| Pacman backend | v0.1.0 |
| AUR (yay) backend | v0.1.0 |
| APT backend | v0.1.0 |
| Homebrew backend | v0.1.0 |
| Self-updating mechanism | v0.1.2 |
| Binary releases via GitHub Actions | v0.1.4 |
Contributing to the Roadmap
If you want to work on a planned feature, open a GitHub Discussion or comment on the relevant issue. See the Contributing Guide for how to get started.