Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 8 additions & 30 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,36 +1,14 @@
[package]
name = "trx-cli"
version = "0.1.12"
edition = "2024"
description = "A Modern Cross-Platform Package Manager TUI"
license = "MIT"
repository = "https://github.com/pie-314/trx"
homepage = "https://github.com/pie-314/trx"

[[bin]]
name = "trx"
path = "src/main.rs"
version = "0.1.0"
edition = "2021"

[dependencies]
color-eyre = "0.6.5"
ratatui = "0.29.0"
textwrap = "0.16.2"
lazy_static = "1.4"
reqwest = { version = "0.12", features = ["blocking", "json"] }
tokio = { version = "1.0", features = ["full"] }
async-trait = "0.1"
log = "0.4"
quick-xml = "0.28"
serde = { version = "1.0", features = ["derive"] }
rayon = "1.8"
serde_json = "1.0"
urlencoding = "2.1"
flate2 = "1.1.5"
tar = "0.4.44"
toml = "0.8"
directories = "5.0"
tempfile = "3.10"
self-replace = "1.5"

[profile.release]
opt-level = "z" # Optimize for size
lto = true # Enable Link Time Optimization
codegen-units = 1 # Reduce parallel codegen units to increase optimizations
panic = "abort" # Remove panic unwinding to save space
strip = true # Strip symbols from binary
reqwest = { version = "0.11", features = ["json"] }
thiserror = "1.0"
306 changes: 46 additions & 260 deletions src/managers/mod.rs
Original file line number Diff line number Diff line change
@@ -1,270 +1,56 @@
pub mod apt;
pub mod arch;
pub mod brew;
pub mod dnf;
pub mod pacman;
pub mod yay;
pub mod zypper;

use ratatui::DefaultTerminal;
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex};
use async_trait::async_trait;

#[derive(Debug, Clone, PartialEq)]
pub struct Package {
pub provider: String,
/// Represents information about a package
#[derive(Debug, Clone, Default)]
pub struct PackageInfo {
pub name: String,
pub version: String,
pub summary: String,
pub description: String,
pub score: f64,
pub arch: String,
pub repository: String,
pub status: String,
pub kind: String,
}

/// Trait that all package managers must implement
#[async_trait]
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 update_packages(
&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>>;
}

pub struct CombinedManager {
managers: Vec<Box<dyn PackageManager>>,
}

impl PackageManager for CombinedManager {
fn name(&self) -> &str {
"Multiple Managers"
}

fn search(&self, query: &str) -> Vec<Package> {
let mut all = Vec::new();
for m in &self.managers {
all.extend(m.search(query));
}
all.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal));
all
}

fn get_installed(&self) -> HashSet<String> {
let mut all = HashSet::new();
for m in &self.managers {
all.extend(m.get_installed());
}
all
}

fn get_installed_details(&self) -> Vec<Package> {
let mut all = Vec::new();
for m in &self.managers {
all.extend(m.get_installed_details());
}
all
}

fn get_updates(&self) -> Vec<Package> {
let mut all = Vec::new();
for m in &self.managers {
all.extend(m.get_updates());
}
all
}

fn get_details(&self, pkg: &str, provider: &str) -> Option<HashMap<String, String>> {
for m in &self.managers {
// Check if this manager's name/prefix matches the provider
if provider.to_lowercase().contains(&m.name().to_lowercase())
|| m.name().to_lowercase().contains(&provider.to_lowercase())
{
return m.get_details(pkg, provider);
}
}
// Fallback: try all
for m in &self.managers {
if let Some(details) = m.get_details(pkg, provider) {
return Some(details);
}
}
None
}

fn install(
&self,
terminal: &mut DefaultTerminal,
pkgs: &HashSet<String>,
) -> Result<(), Box<dyn std::error::Error>> {
for m in &self.managers {
// This is tricky; we'd need to know which package belongs to which manager.
// For now, let's assume the manager's internal logic handles its own packages.
m.install(terminal, pkgs)?;
}
Ok(())
}

fn remove(
&self,
terminal: &mut DefaultTerminal,
pkgs: &HashSet<String>,
) -> Result<(), Box<dyn std::error::Error>> {
for m in &self.managers {
m.remove(terminal, pkgs)?;
}
Ok(())
}

fn update_packages(
&self,
terminal: &mut DefaultTerminal,
pkgs: &HashSet<String>,
) -> Result<(), Box<dyn std::error::Error>> {
// Partition `pkgs` by manager: only send a package to the backend that
// owns it (determined by intersecting with each manager's installed set).
// This prevents backends from attempting to upgrade packages they don't manage.
for m in &self.managers {
let installed = m.get_installed();
let manager_pkgs: HashSet<String> =
pkgs.iter().filter(|p| installed.contains(*p)).cloned().collect();
if !manager_pkgs.is_empty() {
m.update_packages(terminal, &manager_pkgs)?;
}
}
Ok(())
}

fn system_upgrade(
&self,
terminal: &mut DefaultTerminal,
) -> Result<(), Box<dyn std::error::Error>> {
for m in &self.managers {
m.system_upgrade(terminal)?;
}
Ok(())
}

fn refresh_databases(
&self,
terminal: &mut DefaultTerminal,
) -> Result<(), Box<dyn std::error::Error>> {
for m in &self.managers {
m.refresh_databases(terminal)?;
}
Ok(())
}
}

pub fn get_available_managers() -> Vec<String> {
let mut available = Vec::new();

if std::process::Command::new("pacman").arg("--version").output().is_ok() {
available.push("pacman".to_string());
available.push("yay".to_string());
}

if std::process::Command::new("brew").arg("--version").output().is_ok() {
available.push("brew".to_string());
}

if std::process::Command::new("apt").arg("--version").output().is_ok() {
available.push("apt".to_string());
}

available
}

pub fn get_system_manager(config: &crate::config::Config) -> Box<dyn PackageManager> {
let mut managers: Vec<Box<dyn PackageManager>> = Vec::new();
let enabled = &config.settings.enabled_managers;
let available = get_available_managers();

if available.contains(&"brew".to_string()) && enabled.contains(&"brew".to_string()) {
managers.push(Box::new(brew::BrewManager));
}

if (available.contains(&"pacman".to_string()) || available.contains(&"yay".to_string()))
&& (enabled.contains(&"pacman".to_string()) || enabled.contains(&"yay".to_string()))
{
managers.push(Box::new(arch::ArchManager::new(config.aur_helper.clone())));
}

if available.contains(&"apt".to_string()) && enabled.contains(&"apt".to_string()) {
managers.push(Box::new(apt::AptManager));
}

if managers.is_empty() {
if available.contains(&"pacman".to_string()) {
return Box::new(arch::ArchManager::new(config.aur_helper.clone()));
}
return Box::new(apt::AptManager);
}

if managers.len() == 1 {
return managers.pop().unwrap();
}

Box::new(CombinedManager { managers })
}

//makes a DETAILS_CACHE which is global
lazy_static::lazy_static! {
pub static ref DETAILS_CACHE: Arc<Mutex<HashMap<String, HashMap<String, String>>>> =
Arc::new(Mutex::new(HashMap::new()));
pub static ref SEARCH_CACHE: Arc<Mutex<HashMap<String, Vec<Package>>>> =
Arc::new(Mutex::new(HashMap::new()));
}

pub fn parse_alternating_lines(lines: &[&str], manager: String, query: &str) -> Vec<Package> {
let mut res = Vec::new();
let mut i = 0;

while i + 1 < lines.len() {
let first_line = lines[i];
let second_line = lines[i + 1];

let parts: Vec<&str> = first_line.split_whitespace().collect();

if parts.len() >= 2 {
let package = parts[0].to_string();
let version = parts[1].to_string();
let description = second_line.trim().to_string();

let package_name = package.split('/').next_back().unwrap_or(&package).to_string();
let score = crate::fuzzy::fuzzy_match(query, &package_name);

res.push(Package {
provider: manager.clone(),
name: package,
version,
description,
score,
});
}

i += 2;
}

res.retain(|p| p.score > 0.01);
res.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal));

res
}
/// Search for packages matching the query
async fn search(&self, query: &str) -> Result<Vec<PackageInfo>, String>;

/// Install a package
async fn install(&self, package: &str) -> Result<(), String>;

/// Remove a package
async fn remove(&self, package: &str) -> Result<(), String>;

/// Update packages. If package is None, update all packages
async fn update(&self, package: Option<&str>) -> Result<Vec<PackageInfo>, String>;

/// List all installed packages
async fn list_installed(&self) -> Result<Vec<PackageInfo>, String>;

/// Refresh package repositories
async fn refresh(&self) -> Result<(), String>;

/// Get detailed information about a package
async fn info(&self, package: &str) -> Result<PackageInfo, String>;

/// Check if a package is installed
async fn is_installed(&self, package: &str) -> Result<bool, String>;

/// Add a repository
async fn add_repository(&self, name: &str, url: &str) -> Result<(), String>;

/// Remove a repository
async fn remove_repository(&self, name: &str) -> Result<(), String>;

/// List all configured repositories
async fn list_repositories(&self) -> Result<Vec<String>, String>;
}
Loading