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.