diff --git a/src/managers/apt.rs b/src/managers/apt.rs index 834b98b..e8a480d 100644 --- a/src/managers/apt.rs +++ b/src/managers/apt.rs @@ -140,10 +140,17 @@ impl PackageManager for AptManager { fn install( &self, terminal: &mut DefaultTerminal, - pkgs: &HashSet, + pkgs: &HashSet<(String, String)>, ) -> Result<(), Box> { + let names: HashSet = 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(()) @@ -152,10 +159,17 @@ impl PackageManager for AptManager { fn remove( &self, terminal: &mut DefaultTerminal, - pkgs: &HashSet, + pkgs: &HashSet<(String, String)>, ) -> Result<(), Box> { + let names: HashSet = 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(()) @@ -164,13 +178,17 @@ impl PackageManager for AptManager { fn update_packages( &self, terminal: &mut DefaultTerminal, - pkgs: &HashSet, + pkgs: &HashSet<(String, String)>, ) -> Result<(), Box> { - if pkgs.is_empty() { + let names: HashSet = 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(()) diff --git a/src/managers/arch.rs b/src/managers/arch.rs index e6dc618..9df86f7 100644 --- a/src/managers/arch.rs +++ b/src/managers/arch.rs @@ -103,15 +103,22 @@ impl PackageManager for ArchManager { fn install( &self, terminal: &mut DefaultTerminal, - pkgs: &HashSet, + pkgs: &HashSet<(String, String)>, ) -> Result<(), Box> { + let names: HashSet = 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(()) } @@ -119,27 +126,38 @@ impl PackageManager for ArchManager { fn remove( &self, terminal: &mut DefaultTerminal, - pkgs: &HashSet, + pkgs: &HashSet<(String, String)>, ) -> Result<(), Box> { - pacman::pacman_remove(terminal, pkgs) + let names: HashSet = 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, + pkgs: &HashSet<(String, String)>, ) -> Result<(), Box> { - if pkgs.is_empty() { + let names: HashSet = 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(()) } diff --git a/src/managers/brew.rs b/src/managers/brew.rs index ce4557b..2eaa3e8 100644 --- a/src/managers/brew.rs +++ b/src/managers/brew.rs @@ -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(), @@ -220,10 +220,17 @@ impl PackageManager for BrewManager { fn install( &self, terminal: &mut DefaultTerminal, - pkgs: &HashSet, + pkgs: &HashSet<(String, String)>, ) -> Result<(), Box> { + let names: HashSet = 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(()) @@ -232,10 +239,17 @@ impl PackageManager for BrewManager { fn remove( &self, terminal: &mut DefaultTerminal, - pkgs: &HashSet, + pkgs: &HashSet<(String, String)>, ) -> Result<(), Box> { + let names: HashSet = 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(()) @@ -244,13 +258,17 @@ impl PackageManager for BrewManager { fn update_packages( &self, terminal: &mut DefaultTerminal, - pkgs: &HashSet, + pkgs: &HashSet<(String, String)>, ) -> Result<(), Box> { - if pkgs.is_empty() { + let names: HashSet = 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(()) diff --git a/src/managers/mod.rs b/src/managers/mod.rs index 73eded9..9a3b90e 100644 --- a/src/managers/mod.rs +++ b/src/managers/mod.rs @@ -27,17 +27,17 @@ pub trait PackageManager: Send + Sync { fn install( &self, terminal: &mut DefaultTerminal, - pkgs: &HashSet, + pkgs: &HashSet<(String, String)>, ) -> Result<(), Box>; fn remove( &self, terminal: &mut DefaultTerminal, - pkgs: &HashSet, + pkgs: &HashSet<(String, String)>, ) -> Result<(), Box>; fn update_packages( &self, terminal: &mut DefaultTerminal, - pkgs: &HashSet, + pkgs: &HashSet<(String, String)>, ) -> Result<(), Box>; fn system_upgrade( &self, @@ -93,10 +93,7 @@ impl PackageManager for CombinedManager { fn get_details(&self, pkg: &str, provider: &str) -> Option> { 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); } } @@ -112,12 +109,17 @@ impl PackageManager for CombinedManager { fn install( &self, terminal: &mut DefaultTerminal, - pkgs: &HashSet, + pkgs: &HashSet<(String, String)>, ) -> Result<(), Box> { 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(()) } @@ -125,10 +127,17 @@ impl PackageManager for CombinedManager { fn remove( &self, terminal: &mut DefaultTerminal, - pkgs: &HashSet, + pkgs: &HashSet<(String, String)>, ) -> Result<(), Box> { 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(()) } @@ -136,15 +145,14 @@ impl PackageManager for CombinedManager { fn update_packages( &self, terminal: &mut DefaultTerminal, - pkgs: &HashSet, + pkgs: &HashSet<(String, String)>, ) -> Result<(), Box> { - // 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 = - 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)?; } @@ -268,3 +276,18 @@ 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 { + p_lower.contains(&m_lower) || m_lower.contains(&p_lower) + } +} + diff --git a/src/ui/app.rs b/src/ui/app.rs index d89b46f..65264e3 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -39,7 +39,7 @@ pub struct App { pub current_tab: Tab, pub packages: Vec, pub checked: Vec, - pub selected_names: HashSet, + pub selected_names: HashSet<(String, String)>, pub installed_packages: HashSet, pub selected: usize, pub list_state: ListState, @@ -273,9 +273,9 @@ impl App { } let mut to_remove = HashSet::new(); - for name in &self.selected_names { + for (name, provider) in &self.selected_names { if self.installed_packages.contains(name) { - to_remove.insert(name.clone()); + to_remove.insert((name.clone(), provider.clone())); } } @@ -291,15 +291,15 @@ impl App { terminal: &mut DefaultTerminal, ) -> Result<(), Box> { // Prefer explicitly checked packages; fall back to the highlighted item. - let mut to_update: HashSet = self.selected_names.clone(); + let mut to_update: HashSet<(String, String)> = self.selected_names.clone(); if to_update.is_empty() && let Some(pkg) = self.packages.get(self.selected) { - to_update.insert(pkg.name.clone()); + to_update.insert((pkg.name.clone(), pkg.provider.clone())); } // Only update packages that are actually installed; avoids invoking // backend-specific upgrade commands on packages the system doesn't own. - to_update.retain(|name| self.installed_packages.contains(name)); + to_update.retain(|(name, _)| self.installed_packages.contains(name)); if !to_update.is_empty() { self.manager.update_packages(terminal, &to_update)?; } @@ -545,7 +545,7 @@ impl App { self.checked = self .packages .iter() - .map(|p| self.selected_names.contains(&p.name)) + .map(|p| self.selected_names.contains(&(p.name.clone(), p.provider.clone()))) .collect(); self.selected = 0; @@ -659,18 +659,21 @@ impl App { } else if is_key(key.code, &keys.install) { let _ = self.run_command(terminal); self.installed_packages = self.manager.get_installed(); + self.selected_names.retain(|(name, _)| !self.installed_packages.contains(name)); if let Tab::Installed = self.current_tab { self.reset_tab_state(); } } else if is_key(key.code, &keys.remove) { let _ = self.run_remove_command(terminal); self.installed_packages = self.manager.get_installed(); + self.selected_names.retain(|(name, _)| self.installed_packages.contains(name)); if let Tab::Installed = self.current_tab { self.reset_tab_state(); } } else if is_key(key.code, &keys.update) { let _ = self.run_update_command(terminal); self.installed_packages = self.manager.get_installed(); + self.selected_names.retain(|(_, provider)| !provider.contains("/update")); // Reset both Updates and Installed tabs so their lists // reflect the post-update state immediately. match self.current_tab { @@ -696,12 +699,13 @@ impl App { } else if !self.packages.is_empty() { let pkg = &self.packages[self.selected]; let name = pkg.name.clone(); + let provider = pkg.provider.clone(); let is_checked = !self.checked[self.selected]; self.checked[self.selected] = is_checked; if is_checked { - self.selected_names.insert(name); + self.selected_names.insert((name, provider)); } else { - self.selected_names.remove(&name); + self.selected_names.remove(&(name, provider)); } } } else if is_key(key.code, &keys.search_edit) { @@ -874,7 +878,36 @@ impl App { // Tab switching if mouse_event.row >= 1 && mouse_event.row <= 3 { let col = mouse_event.column.saturating_sub(1); - let tab_titles = ["Search", "Installed", "Updates", "Settings"]; + + let search_count = self.selected_names.iter() + .filter(|(name, provider)| !provider.contains("/update") && !self.installed_packages.contains(name)) + .count(); + let installed_count = self.selected_names.iter() + .filter(|(name, provider)| !provider.contains("/update") && self.installed_packages.contains(name)) + .count(); + let updates_count = self.selected_names.iter() + .filter(|(_, provider)| provider.contains("/update")) + .count(); + + let search_title = if search_count > 0 { + format!("Search [{}]", search_count) + } else { + "Search".to_string() + }; + + let installed_title = if installed_count > 0 { + format!("Installed [{}]", installed_count) + } else { + "Installed".to_string() + }; + + let updates_title = if updates_count > 0 { + format!("Updates [{}]", updates_count) + } else { + "Updates".to_string() + }; + + let tab_titles = [search_title, installed_title, updates_title, "Settings".to_string()]; let mut current_x = 0; for (i, title) in tab_titles.iter().enumerate() { let width = title.len() as u16; @@ -941,12 +974,13 @@ impl App { if mouse_event.column < 5 { let name = self.packages[real_idx].name.clone(); + let provider = self.packages[real_idx].provider.clone(); let is_checked = !self.checked[real_idx]; self.checked[real_idx] = is_checked; if is_checked { - self.selected_names.insert(name); + self.selected_names.insert((name, provider)); } else { - self.selected_names.remove(&name); + self.selected_names.remove(&(name, provider)); } } } diff --git a/src/ui/draw.rs b/src/ui/draw.rs index c55c00d..69efe18 100644 --- a/src/ui/draw.rs +++ b/src/ui/draw.rs @@ -202,7 +202,41 @@ fn draw_tabs(frame: &mut Frame, app: &App, area: Rect, theme: &crate::config::Th let highlight_color = app.config.get_color(&theme.highlight_color); let border_type = get_border_type(&app.config.settings.border_style); - let tab_titles = vec!["Search", "Installed", "Updates", "Settings"]; + let search_count = app.selected_names.iter() + .filter(|(name, provider)| !provider.contains("/update") && !app.installed_packages.contains(name)) + .count(); + let installed_count = app.selected_names.iter() + .filter(|(name, provider)| !provider.contains("/update") && app.installed_packages.contains(name)) + .count(); + let updates_count = app.selected_names.iter() + .filter(|(_, provider)| provider.contains("/update")) + .count(); + + let search_title = if search_count > 0 { + format!("Search [{}]", search_count) + } else { + "Search".to_string() + }; + + let installed_title = if installed_count > 0 { + format!("Installed [{}]", installed_count) + } else { + "Installed".to_string() + }; + + let updates_title = if updates_count > 0 { + format!("Updates [{}]", updates_count) + } else { + "Updates".to_string() + }; + + let tab_titles = vec![ + search_title, + installed_title, + updates_title, + "Settings".to_string(), + ]; + let tabs = ratatui::widgets::Tabs::new(tab_titles) .block( Block::bordered()