Introduction

TRX logo

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

FeatureDetail
SpeedFuzzy search with sub-50 ms results
Keyboard-firstVim-inspired navigation; no mouse needed
Unified interfaceSame keybindings across all package managers
Non-blockingAll I/O on OS threads — UI never freezes
Self-updatingChecks GitHub releases on startup
ExtensiblePluggable backend trait — add a new PM in one file

Supported Platforms

Package ManagerPlatformStatus
PacmanArch Linux✅ Implemented
yay (AUR)Arch Linux✅ Implemented
APTDebian / Ubuntu✅ Implemented
HomebrewmacOS✅ Implemented
dnf / yumFedora / RHEL🔜 Planned
zypperopenSUSE🔜 Planned
winget / scoopWindows🔜 Planned

Installation

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 (rustup is 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:

OSArchitecture
Linuxx86_64
macOSx86_64
macOSaarch64 (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:

TabDescription
SearchFuzzy-search all available packages
InstalledBrowse packages currently installed on the system
UpdatesPackages with a newer version available

Keybindings

Global

KeyAction
TabSwitch to next tab
Shift+TabSwitch to previous tab
?Toggle help overlay
q / EscQuit TRX (or exit current mode)
KeyAction
/ kMove selection up
/ jMove selection down

Package Operations

KeyAction
SpaceToggle package selection
iInstall all selected packages
xRemove all selected packages
UFull system upgrade
RRefresh package databases

Search Tab

KeyAction
eEnter search / editing mode
EscExit search mode (return to normal navigation)

Workflow Example

  1. Press e to enter search mode.
  2. Type a package name (e.g. ripgrep). Results appear within 50 ms as you type.
  3. Use / j to move through results. The details panel on the right updates automatically.
  4. Press Space to select one or more packages.
  5. Press i to 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 string
  • current_tab — which of the three tabs is active
  • packages — the currently displayed list
  • checked / selected_names — multi-selection state
  • installed_packagesHashSet<String> fetched once on startup
  • details_state — sidebar content (Empty | Loading | Success | Error)
  • loading — drives the spinner in the header
  • managerArc<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:

  1. Poll keyboard — via crossterm::event::poll with a short timeout so the loop never blocks long.
  2. Drain channelstry_recv on result_rx and details_rx (non-blocking).
  3. Render — call draw_ui to produce the next terminal frame.

Startup Sequence

  1. Parse CLI flags (--version, --help).
  2. Call updater::check_for_updates() — if a newer release exists, self-update and exit.
  3. Initialise the ratatui terminal (ratatui::init).
  4. Load Config from the TOML file (or write defaults).
  5. Call managers::get_system_manager(&config) to select the correct backend.
  6. Create the mpsc channels and construct App.
  7. Enter App::run() — the main event loop.
  8. 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:

ChannelProducerConsumerPayload
result_rxSearch / list-load threadsApp::run(String, Vec<Package>) — a tag plus a list of packages
details_rxDetails-fetch threadsApp::runDetailsState

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:

  1. Disable raw mode — so the child process receives normal terminal I/O.
  2. Leave alternate screen — the TUI disappears; the package manager's output is printed normally.
  3. Run the command — via std::process::Command.
  4. Wait for Enter — the user can review the output.
  5. 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 layerdraw_ui and its helpers take &mut App (for ListState selection) 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:

ConditionBonus
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:

ConditionPenalty
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:

  1. OS == "macos"BrewManager
  2. pacman --version succeeds → ArchManager (Pacman + optional AUR helper)
  3. apt --version succeeds → AptManager
  4. Fallback → ArchManager (with default yay AUR 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

BackendSource filePlatform
Pacmansrc/managers/pacman.rsArch Linux
AUR / yaysrc/managers/yay.rsArch Linux
APTsrc/managers/apt.rsDebian / Ubuntu
Homebrewsrc/managers/brew.rsmacOS

Arch Linux — Pacman

The Pacman backend is implemented across two files:

  • src/managers/pacman.rs — low-level wrappers around the pacman CLI
  • src/managers/arch.rsArchManager struct that implements PackageManager by 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.


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

KeyCommand
Usudo pacman -Syu
Rsudo 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

OperationCommand
Installsudo apt install <packages>
Removesudo apt remove <packages>

Both operations hand control of the terminal to the interactive APT output via execute_external_command.


System Upgrade & Refresh

KeyCommand
Usudo apt upgrade
Rsudo 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:

  • name
  • desc
  • homepage
  • versions.stable
  • installed (list of installed versions)

Install / Remove

OperationCommand
Installbrew install <packages>
Removebrew uninstall <packages>

Both hand control of the terminal to Homebrew's output via execute_external_command.


System Upgrade & Refresh

KeyCommand
Ubrew upgrade
Rbrew 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:

  1. Create a new file in src/managers/
  2. Implement the PackageManager trait
  3. 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_lines if the package manager outputs results in the standard name version\n description format.
  • Always check DETAILS_CACHE at the top of get_details to avoid redundant subprocess calls.
  • Call execute_external_command for any operation that produces interactive output (confirmations, progress bars, etc.).
  • Make the struct Send + Sync — the PackageManager trait bound requires it. Zero-size structs and structs with only owned data satisfy this automatically.
  • Run cargo clippy before 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:

PlatformPath
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_CACHE entries 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

AreaExamples
Backend IntegrationsNew package managers (dnf, zypper, winget)
TUI ImprovementsNew widgets, themes, layout changes
Fuzzy SearchBetter scoring heuristics, performance
PerformanceCaching, parallel execution
Bug FixesReproduce, isolate, and fix issues
DocumentationImprove this site, README, examples

Pull Request Workflow

  1. Fork the repository and create a feature branch:
    git checkout -b feat/dnf-backend
    
  2. Make your changes.
  3. Run cargo fmt and cargo clippy (zero warnings preferred).
  4. Run cargo test.
  5. Commit using conventional commit messages (see Coding Guidelines).
  6. 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 + mpsc channels 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 + mpsc pattern.
  • 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_details implementation should check and populate DETAILS_CACHE to avoid redundant subprocess calls.
  • Pure rendering. The draw_ui function 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

ToolInstallPurpose
rustfmtrustup component add rustfmtCode formatting
clippyrustup component add clippyLinting
cargo-expandcargo install cargo-expandMacro 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

FeatureDescription
dnf / yum backendFedora and RHEL support via dnf
zypper backendopenSUSE support
winget / scoop backendWindows support

Planned

FeatureDescription
Configurable keybindingsRemap any key via config.toml
Pluggable themesColour scheme selection and custom themes via config
Transaction historyLog of installs/removes with rollback support
Batch / scripting modeNon-interactive mode for CI and shell scripts
Dependency graph visualiserVisual view of package dependency trees
Metadata cachingPersist DETAILS_CACHE across sessions for faster repeated searches
Plugin systemLoad custom backends and widgets from shared libraries

Completed

FeatureVersion
Pacman backendv0.1.0
AUR (yay) backendv0.1.0
APT backendv0.1.0
Homebrew backendv0.1.0
Self-updating mechanismv0.1.2
Binary releases via GitHub Actionsv0.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.