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
32 changes: 25 additions & 7 deletions src/managers/apt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,17 @@ impl PackageManager for AptManager {
fn install(
&self,
terminal: &mut DefaultTerminal,
pkgs: &HashSet<String>,
pkgs: &HashSet<(String, String)>,
) -> Result<(), Box<dyn std::error::Error>> {
let names: HashSet<String> = pkgs.iter()
.filter(|(_, provider)| super::manager_handles_provider(self.name(), provider))
.map(|(name, _)| name.clone())
.collect();
if names.is_empty() {
return Ok(());
}
let mut args = vec!["apt", "install", "-y"];
let pkg_refs: Vec<&str> = pkgs.iter().map(|s| s.as_str()).collect();
let pkg_refs: Vec<&str> = names.iter().map(|s| s.as_str()).collect();
args.extend(pkg_refs);
crate::execute_external_command(terminal, "sudo", &args)?;
Ok(())
Expand All @@ -152,10 +159,17 @@ impl PackageManager for AptManager {
fn remove(
&self,
terminal: &mut DefaultTerminal,
pkgs: &HashSet<String>,
pkgs: &HashSet<(String, String)>,
) -> Result<(), Box<dyn std::error::Error>> {
let names: HashSet<String> = pkgs.iter()
.filter(|(_, provider)| super::manager_handles_provider(self.name(), provider))
.map(|(name, _)| name.clone())
.collect();
if names.is_empty() {
return Ok(());
}
let mut args = vec!["apt", "remove", "-y"];
let pkg_refs: Vec<&str> = pkgs.iter().map(|s| s.as_str()).collect();
let pkg_refs: Vec<&str> = names.iter().map(|s| s.as_str()).collect();
args.extend(pkg_refs);
crate::execute_external_command(terminal, "sudo", &args)?;
Ok(())
Expand All @@ -164,13 +178,17 @@ impl PackageManager for AptManager {
fn update_packages(
&self,
terminal: &mut DefaultTerminal,
pkgs: &HashSet<String>,
pkgs: &HashSet<(String, String)>,
) -> Result<(), Box<dyn std::error::Error>> {
if pkgs.is_empty() {
let names: HashSet<String> = pkgs.iter()
.filter(|(_, provider)| super::manager_handles_provider(self.name(), provider))
.map(|(name, _)| name.clone())
.collect();
if names.is_empty() {
return Ok(());
}
let mut args = vec!["apt", "install", "--only-upgrade", "-y"];
let pkg_refs: Vec<&str> = pkgs.iter().map(|s| s.as_str()).collect();
let pkg_refs: Vec<&str> = names.iter().map(|s| s.as_str()).collect();
args.extend(pkg_refs);
crate::execute_external_command(terminal, "sudo", &args)?;
Ok(())
Expand Down
36 changes: 27 additions & 9 deletions src/managers/arch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,43 +103,61 @@ impl PackageManager for ArchManager {
fn install(
&self,
terminal: &mut DefaultTerminal,
pkgs: &HashSet<String>,
pkgs: &HashSet<(String, String)>,
) -> Result<(), Box<dyn std::error::Error>> {
let names: HashSet<String> = pkgs.iter()
.filter(|(_, provider)| super::manager_handles_provider(self.name(), provider))
.map(|(name, _)| name.clone())
.collect();
if names.is_empty() {
return Ok(());
}
if !self.aur_helper.is_empty() {
// yay (or configured AUR helper) can transparently install both
// official-repo and AUR packages; aur_install strips any "aur/" prefix.
yay::aur_install(terminal, pkgs, &self.aur_helper)?;
yay::aur_install(terminal, &names, &self.aur_helper)?;
} else {
// No AUR helper available — install via pacman only.
pacman::pacman_install(terminal, pkgs)?;
pacman::pacman_install(terminal, &names)?;
}
Ok(())
}

fn remove(
&self,
terminal: &mut DefaultTerminal,
pkgs: &HashSet<String>,
pkgs: &HashSet<(String, String)>,
) -> Result<(), Box<dyn std::error::Error>> {
pacman::pacman_remove(terminal, pkgs)
let names: HashSet<String> = pkgs.iter()
.filter(|(_, provider)| super::manager_handles_provider(self.name(), provider))
.map(|(name, _)| name.clone())
.collect();
if names.is_empty() {
return Ok(());
}
pacman::pacman_remove(terminal, &names)
}

fn update_packages(
&self,
terminal: &mut DefaultTerminal,
pkgs: &HashSet<String>,
pkgs: &HashSet<(String, String)>,
) -> Result<(), Box<dyn std::error::Error>> {
if pkgs.is_empty() {
let names: HashSet<String> = pkgs.iter()
.filter(|(_, provider)| super::manager_handles_provider(self.name(), provider))
.map(|(name, _)| name.clone())
.collect();
if names.is_empty() {
return Ok(());
}
if !self.aur_helper.is_empty() {
// When an AUR helper is available, route everything through it —
// it handles both official-repo and AUR packages transparently.
// aur_install already strips any "aur/" prefix before invoking the helper.
yay::aur_install(terminal, pkgs, &self.aur_helper)?;
yay::aur_install(terminal, &names, &self.aur_helper)?;
} else {
// No AUR helper; treat all selected packages as official-repo.
pacman::pacman_install(terminal, pkgs)?;
pacman::pacman_install(terminal, &names)?;
}
Ok(())
}
Expand Down
34 changes: 26 additions & 8 deletions src/managers/brew.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ impl PackageManager for BrewManager {
let name = parts.next()?.trim().to_string();
let version_info = parts.next().unwrap_or("").trim().to_string();
Some(Package {
provider: "brew".to_string(),
provider: "brew/update".to_string(),
name,
version: version_info,
description: String::new(),
Expand Down Expand Up @@ -220,10 +220,17 @@ impl PackageManager for BrewManager {
fn install(
&self,
terminal: &mut DefaultTerminal,
pkgs: &HashSet<String>,
pkgs: &HashSet<(String, String)>,
) -> Result<(), Box<dyn std::error::Error>> {
let names: HashSet<String> = pkgs.iter()
.filter(|(_, provider)| super::manager_handles_provider(self.name(), provider))
.map(|(name, _)| name.clone())
.collect();
if names.is_empty() {
return Ok(());
}
let mut args = vec!["install"];
let pkg_refs: Vec<&str> = pkgs.iter().map(|s| s.as_str()).collect();
let pkg_refs: Vec<&str> = names.iter().map(|s| s.as_str()).collect();
args.extend(pkg_refs);
crate::execute_external_command(terminal, "brew", &args)?;
Ok(())
Expand All @@ -232,10 +239,17 @@ impl PackageManager for BrewManager {
fn remove(
&self,
terminal: &mut DefaultTerminal,
pkgs: &HashSet<String>,
pkgs: &HashSet<(String, String)>,
) -> Result<(), Box<dyn std::error::Error>> {
let names: HashSet<String> = pkgs.iter()
.filter(|(_, provider)| super::manager_handles_provider(self.name(), provider))
.map(|(name, _)| name.clone())
.collect();
if names.is_empty() {
return Ok(());
}
let mut args = vec!["uninstall"];
let pkg_refs: Vec<&str> = pkgs.iter().map(|s| s.as_str()).collect();
let pkg_refs: Vec<&str> = names.iter().map(|s| s.as_str()).collect();
args.extend(pkg_refs);
crate::execute_external_command(terminal, "brew", &args)?;
Ok(())
Expand All @@ -244,13 +258,17 @@ impl PackageManager for BrewManager {
fn update_packages(
&self,
terminal: &mut DefaultTerminal,
pkgs: &HashSet<String>,
pkgs: &HashSet<(String, String)>,
) -> Result<(), Box<dyn std::error::Error>> {
if pkgs.is_empty() {
let names: HashSet<String> = pkgs.iter()
.filter(|(_, provider)| super::manager_handles_provider(self.name(), provider))
.map(|(name, _)| name.clone())
.collect();
if names.is_empty() {
return Ok(());
}
let mut args = vec!["upgrade"];
let pkg_refs: Vec<&str> = pkgs.iter().map(|s| s.as_str()).collect();
let pkg_refs: Vec<&str> = names.iter().map(|s| s.as_str()).collect();
args.extend(pkg_refs);
crate::execute_external_command(terminal, "brew", &args)?;
Ok(())
Expand Down
74 changes: 54 additions & 20 deletions src/managers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pub mod arch;
pub mod brew;
pub mod pacman;
pub mod yay;
pub mod zypper;

use ratatui::DefaultTerminal;
use std::collections::{HashMap, HashSet};
Expand All @@ -27,17 +28,17 @@ pub trait PackageManager: Send + Sync {
fn install(
&self,
terminal: &mut DefaultTerminal,
pkgs: &HashSet<String>,
pkgs: &HashSet<(String, String)>,
) -> Result<(), Box<dyn std::error::Error>>;
fn remove(
&self,
terminal: &mut DefaultTerminal,
pkgs: &HashSet<String>,
pkgs: &HashSet<(String, String)>,
) -> Result<(), Box<dyn std::error::Error>>;
fn update_packages(
&self,
terminal: &mut DefaultTerminal,
pkgs: &HashSet<String>,
pkgs: &HashSet<(String, String)>,
) -> Result<(), Box<dyn std::error::Error>>;
fn system_upgrade(
&self,
Expand Down Expand Up @@ -93,10 +94,7 @@ impl PackageManager for CombinedManager {

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())
{
if manager_handles_provider(m.name(), provider) {
return m.get_details(pkg, provider);
}
}
Expand All @@ -112,39 +110,50 @@ impl PackageManager for CombinedManager {
fn install(
&self,
terminal: &mut DefaultTerminal,
pkgs: &HashSet<String>,
pkgs: &HashSet<(String, 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)?;
let manager_pkgs: HashSet<(String, String)> = pkgs
.iter()
.filter(|(_, provider)| manager_handles_provider(m.name(), provider))
.cloned()
.collect();
if !manager_pkgs.is_empty() {
m.install(terminal, &manager_pkgs)?;
}
}
Ok(())
}

fn remove(
&self,
terminal: &mut DefaultTerminal,
pkgs: &HashSet<String>,
pkgs: &HashSet<(String, String)>,
) -> Result<(), Box<dyn std::error::Error>> {
for m in &self.managers {
m.remove(terminal, pkgs)?;
let manager_pkgs: HashSet<(String, String)> = pkgs
.iter()
.filter(|(_, provider)| manager_handles_provider(m.name(), provider))
.cloned()
.collect();
if !manager_pkgs.is_empty() {
m.remove(terminal, &manager_pkgs)?;
}
}
Ok(())
}

fn update_packages(
&self,
terminal: &mut DefaultTerminal,
pkgs: &HashSet<String>,
pkgs: &HashSet<(String, 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();
let manager_pkgs: HashSet<(String, String)> = pkgs
.iter()
.filter(|(_, provider)| manager_handles_provider(m.name(), provider))
.cloned()
.collect();
if !manager_pkgs.is_empty() {
m.update_packages(terminal, &manager_pkgs)?;
}
Expand Down Expand Up @@ -189,6 +198,10 @@ pub fn get_available_managers() -> Vec<String> {
available.push("apt".to_string());
}

if std::process::Command::new("which").arg("zypper").output().map(|o| o.status.success()).unwrap_or(false) {
available.push("zypper".to_string());
}

available
}

Expand All @@ -211,6 +224,10 @@ pub fn get_system_manager(config: &crate::config::Config) -> Box<dyn PackageMana
managers.push(Box::new(apt::AptManager));
}

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

if managers.is_empty() {
if available.contains(&"pacman".to_string()) {
return Box::new(arch::ArchManager::new(config.aur_helper.clone()));
Expand Down Expand Up @@ -268,3 +285,20 @@ pub fn parse_alternating_lines(lines: &[&str], manager: String, query: &str) ->

res
}

pub fn manager_handles_provider(manager_name: &str, provider: &str) -> bool {
let m_lower = manager_name.to_lowercase();
let p_lower = provider.to_lowercase();
if p_lower.contains("pacman") || p_lower.contains("aur") || p_lower.contains("yay") {
m_lower.contains("arch") || m_lower.contains("pacman") || m_lower.contains("yay")
} else if p_lower.contains("brew") || p_lower.contains("homebrew") {
m_lower.contains("brew")
} else if p_lower.contains("apt") {
m_lower.contains("apt") || m_lower.contains("debian") || m_lower.contains("ubuntu")
} else if p_lower.contains("zypper") || p_lower.contains("opensuse") || p_lower.contains("suse") {
m_lower.contains("zypper") || m_lower.contains("opensuse") || m_lower.contains("suse")
} else {
p_lower.contains(&m_lower) || m_lower.contains(&p_lower)
}
}

Loading