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
150 changes: 94 additions & 56 deletions src/managers/apt.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,62 @@
use crate::managers::{Package, PackageManager};
use ratatui::DefaultTerminal;
use rayon::prelude::*;
use std::collections::{HashMap, HashSet};
use std::process::Command;

pub struct AptManager;

fn parse_search_line(query: &str, line: &str) -> Option<Package> {
let parts: Vec<&str> = line.splitn(2, " - ").collect();
if parts.is_empty() {
return None;
}

let name = parts[0].trim().to_string();
if name.is_empty() {
return None;
}

let description = parts.get(1).unwrap_or(&"").trim().to_string();
let score = crate::fuzzy::fuzzy_match(query, &name);

if score <= 0.01 {
return None;
}

Some(Package { provider: "apt".to_string(), name, version: String::new(), description, score })
}

fn parse_installed_detail_line(line: &str) -> Option<Package> {
let parts: Vec<&str> = line.split('\t').collect();
if parts.len() < 2 {
return None;
}

Some(Package {
provider: "apt/local".to_string(),
name: parts[0].to_string(),
version: parts[1].to_string(),
description: parts.get(2).unwrap_or(&"").to_string(),
score: 1.0,
})
}

fn parse_update_line(line: &str) -> Option<Package> {
let parts: Vec<&str> = line.split('/').collect();
if parts.is_empty() || parts[0].is_empty() {
return None;
}

Some(Package {
provider: "apt/update".to_string(),
name: parts[0].to_string(),
version: "Update available".to_string(),
description: String::new(),
score: 1.0,
})
}

impl PackageManager for AptManager {
fn name(&self) -> &str {
"APT (Debian/Ubuntu)"
Expand All @@ -18,27 +70,8 @@ impl PackageManager for AptManager {

if let Some(output) = output {
let stdout = String::from_utf8_lossy(&output.stdout);
stdout
.lines()
.filter_map(|line| {
let parts: Vec<&str> = line.splitn(2, " - ").collect();
if !parts.is_empty() {
let name = parts[0].trim().to_string();
let desc = parts.get(1).unwrap_or(&"").trim().to_string();
let score = crate::fuzzy::fuzzy_match(query, &name);
Some(Package {
provider: "apt".to_string(),
name,
version: "".to_string(),
description: desc,
score,
})
} else {
None
}
})
.filter(|p| p.score > 0.01)
.collect()
let lines: Vec<&str> = stdout.lines().collect();
lines.par_iter().filter_map(|line| parse_search_line(query, line)).collect()
} else {
Vec::new()
}
Expand All @@ -63,23 +96,8 @@ impl PackageManager for AptManager {

if let Some(output) = output {
let stdout = String::from_utf8_lossy(&output.stdout);
stdout
.lines()
.filter_map(|line| {
let parts: Vec<&str> = line.split('\t').collect();
if parts.len() >= 2 {
Some(Package {
provider: "apt/local".to_string(),
name: parts[0].to_string(),
version: parts[1].to_string(),
description: parts.get(2).unwrap_or(&"").to_string(),
score: 1.0,
})
} else {
None
}
})
.collect()
let lines: Vec<&str> = stdout.lines().collect();
lines.par_iter().filter_map(|line| parse_installed_detail_line(line)).collect()
} else {
Vec::new()
}
Expand All @@ -90,24 +108,8 @@ impl PackageManager for AptManager {

if let Some(output) = output {
let stdout = String::from_utf8_lossy(&output.stdout);
stdout
.lines()
.skip(1)
.filter_map(|line| {
let parts: Vec<&str> = line.split('/').collect();
if !parts.is_empty() {
Some(Package {
provider: "apt/update".to_string(),
name: parts[0].to_string(),
version: "Update available".to_string(),
description: "".to_string(),
score: 1.0,
})
} else {
None
}
})
.collect()
let lines: Vec<&str> = stdout.lines().skip(1).collect();
lines.par_iter().filter_map(|line| parse_update_line(line)).collect()
} else {
Vec::new()
}
Expand Down Expand Up @@ -192,3 +194,39 @@ impl PackageManager for AptManager {
Ok(())
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn parses_apt_search_lines_including_empty_descriptions() {
let with_description = parse_search_line("curl", "curl - command line tool").unwrap();
assert_eq!(with_description.provider, "apt");
assert_eq!(with_description.name, "curl");
assert_eq!(with_description.description, "command line tool");

let without_description = parse_search_line("curl", "curl").unwrap();
assert_eq!(without_description.name, "curl");
assert_eq!(without_description.description, "");
}

#[test]
fn rejects_empty_apt_search_names() {
assert!(parse_search_line("curl", " - missing package").is_none());
}

#[test]
fn parses_apt_detail_and_update_lines() {
let detail = parse_installed_detail_line("curl\t8.5.0\ttransfer tool").unwrap();
assert_eq!(detail.provider, "apt/local");
assert_eq!(detail.name, "curl");
assert_eq!(detail.version, "8.5.0");
assert_eq!(detail.description, "transfer tool");

let update = parse_update_line("curl/stable 8.6.0 amd64 [upgradable]").unwrap();
assert_eq!(update.provider, "apt/update");
assert_eq!(update.name, "curl");
assert_eq!(update.version, "Update available");
}
}
60 changes: 39 additions & 21 deletions src/managers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub mod pacman;
pub mod yay;

use ratatui::DefaultTerminal;
use rayon::prelude::*;
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex};

Expand Down Expand Up @@ -234,37 +235,54 @@ lazy_static::lazy_static! {
}

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();
let mut res: Vec<Package> = lines
.par_chunks(2)
.filter_map(|chunk| {
let [first_line, second_line] = chunk else {
return None;
};

let parts: Vec<&str> = first_line.split_whitespace().collect();
if parts.len() < 2 {
return None;
}

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;
}
Some(Package { provider: manager.clone(), name: package, version, description, score })
})
.collect();

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
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn parses_alternating_package_output_in_parallel_safe_chunks() {
let lines = [
"extra/ripgrep 14.1.1-1",
" fast recursive search",
"core/grep 3.11-1",
" GNU grep",
"dangling-line-without-description 1.0.0",
];

let packages = parse_alternating_lines(&lines, "pacman".to_string(), "ripgrep");

assert_eq!(packages.len(), 1);
assert_eq!(packages[0].provider, "pacman");
assert_eq!(packages[0].name, "extra/ripgrep");
assert_eq!(packages[0].version, "14.1.1-1");
assert_eq!(packages[0].description, "fast recursive search");
}
}
16 changes: 10 additions & 6 deletions src/managers/pacman.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use super::Package;
use crate::execute_external_command;
use ratatui::DefaultTerminal;
use rayon::prelude::*;
use std::collections::{HashMap, HashSet};

pub fn search_pacman(search_word: &str) -> Vec<Package> {
Expand Down Expand Up @@ -129,8 +130,9 @@ pub fn get_installed_packages() -> HashSet<String> {

if let Some(output) = output {
let stdout = String::from_utf8_lossy(&output.stdout);
stdout
.lines()
let lines: Vec<&str> = stdout.lines().collect();
lines
.par_iter()
.filter_map(|line| line.split_whitespace().next().map(|s| s.to_string()))
.collect()
} else {
Expand All @@ -143,8 +145,9 @@ pub fn get_installed_packages_details() -> Vec<Package> {

if let Some(output) = output {
let stdout = String::from_utf8_lossy(&output.stdout);
stdout
.lines()
let lines: Vec<&str> = stdout.lines().collect();
lines
.par_iter()
.filter_map(|line| {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 2 {
Expand All @@ -170,8 +173,9 @@ pub fn get_updates() -> Vec<Package> {

if let Some(output) = output {
let stdout = String::from_utf8_lossy(&output.stdout);
stdout
.lines()
let lines: Vec<&str> = stdout.lines().collect();
lines
.par_iter()
.filter_map(|line| {
// format: pkgname oldver -> newver
let parts: Vec<&str> = line.split_whitespace().collect();
Expand Down
Loading