use ratatui::{
style::{Modifier, Style},
text::{Line, Span},
widgets::ListItem,
};
use crate::state::{AppState, PackageItem, Source};
use crate::theme::{PackageMarker, Theme};
pub struct PackageListStatus {
pub in_install: bool,
pub in_remove: bool,
pub in_downgrade: bool,
}
pub fn check_package_in_lists(package: &PackageItem, app: &AppState) -> PackageListStatus {
let in_install = app
.install_list
.iter()
.any(|it| it.name.eq_ignore_ascii_case(&package.name));
let in_remove = app
.remove_list
.iter()
.any(|it| it.name.eq_ignore_ascii_case(&package.name));
let in_downgrade = app
.downgrade_list
.iter()
.any(|it| it.name.eq_ignore_ascii_case(&package.name));
PackageListStatus {
in_install,
in_remove,
in_downgrade,
}
}
pub fn determine_source_label_and_color(
source: &Source,
package_name: &str,
app: &AppState,
theme: &Theme,
) -> (String, ratatui::style::Color) {
match source {
Source::Official { repo, .. } => {
let owner = app
.details_cache
.get(package_name)
.map(|d| d.owner.clone())
.unwrap_or_default();
let label = crate::logic::distro::label_for_official(repo, package_name, &owner);
let repo_lower = repo.to_lowercase();
let color = if app.repo_results_filter_by_name.contains_key(&repo_lower) {
theme.mauve
} else if label == "EOS"
|| label == "CachyOS"
|| label == "Artix"
|| label == "OMNI"
|| label == "UNI"
|| label == "LIB32"
|| label == "GALAXY"
|| label == "WORLD"
|| label == "SYSTEM"
|| label == "Manjaro"
{
theme.sapphire
} else {
theme.green
};
(label, color)
}
Source::Aur => ("AUR".to_string(), theme.yellow),
}
}
pub fn build_package_marker_item(
segs: Vec<Span<'static>>,
marker_type: PackageMarker,
label: &str,
color: ratatui::style::Color,
in_install: bool,
theme: &Theme,
) -> ListItem<'static> {
match marker_type {
PackageMarker::FullLine => {
let mut item = ListItem::new(Line::from(segs));
let bgc = if in_install {
if let ratatui::style::Color::Rgb(r, g, b) = color {
ratatui::style::Color::Rgb(
u8::try_from((u16::from(r) * 85) / 100).unwrap_or(255),
u8::try_from((u16::from(g) * 85) / 100).unwrap_or(255),
u8::try_from((u16::from(b) * 85) / 100).unwrap_or(255),
)
} else {
color
}
} else {
color
};
item = item.style(Style::default().fg(theme.crust).bg(bgc));
item
}
PackageMarker::Front => {
let mut new_segs: Vec<Span> = Vec::new();
new_segs.push(Span::styled(
label.to_string(),
Style::default()
.fg(theme.crust)
.bg(color)
.add_modifier(Modifier::BOLD),
));
new_segs.push(Span::raw(" "));
new_segs.extend(segs);
ListItem::new(Line::from(new_segs))
}
PackageMarker::End => {
let mut new_segs = segs.clone();
new_segs.push(Span::raw(" "));
new_segs.push(Span::styled(
label.to_string(),
Style::default()
.fg(theme.crust)
.bg(color)
.add_modifier(Modifier::BOLD),
));
ListItem::new(Line::from(new_segs))
}
}
}
fn aur_vote_state_spans(
package: &PackageItem,
app: &AppState,
theme: &Theme,
) -> Option<Vec<Span<'static>>> {
if !matches!(package.source, Source::Aur) {
return None;
}
let state = app
.aur_vote_state_by_pkgbase
.get(&package.name)
.unwrap_or(&crate::state::app_state::AurVoteStateUi::Unknown);
let (label, color, bold) = match state {
crate::state::app_state::AurVoteStateUi::Unknown => return None,
crate::state::app_state::AurVoteStateUi::Loading => {
("[Vote: loading]", theme.overlay1, false)
}
crate::state::app_state::AurVoteStateUi::Voted => ("[Vote: voted]", theme.green, true),
crate::state::app_state::AurVoteStateUi::NotVoted => {
("[Vote: not voted]", theme.overlay1, false)
}
crate::state::app_state::AurVoteStateUi::Error(_) => ("[Vote: error]", theme.red, true),
};
let style = if bold {
Style::default().fg(color).add_modifier(Modifier::BOLD)
} else {
Style::default().fg(color)
};
Some(vec![
Span::raw(" "),
Span::styled(label.to_string(), style),
])
}
pub fn build_list_item(
package: &PackageItem,
app: &AppState,
theme: &Theme,
prefs: &crate::theme::Settings,
in_viewport: bool,
) -> ListItem<'static> {
if !in_viewport {
return ListItem::new(Line::raw(""));
}
let (src, color) = determine_source_label_and_color(&package.source, &package.name, app, theme);
let desc = if package.description.is_empty() {
app.details_cache
.get(&package.name)
.map(|d| d.description.clone())
.unwrap_or_default()
} else {
package.description.clone()
};
let installed = crate::index::is_installed(&package.name);
let mut segs: Vec<Span<'static>> = Vec::new();
if let Some(pop) = package.popularity {
segs.push(Span::styled(
format!("Pop: {pop:.2} "),
Style::default().fg(theme.overlay1),
));
}
segs.push(Span::styled(format!("{src} "), Style::default().fg(color)));
if matches!(package.source, Source::Aur) {
if package.out_of_date.is_some() {
segs.push(Span::styled(
"[OOD] ",
Style::default().fg(theme.red).add_modifier(Modifier::BOLD),
));
}
if package.orphaned {
segs.push(Span::styled(
"[ORPHAN] ",
Style::default().fg(theme.red).add_modifier(Modifier::BOLD),
));
}
}
segs.push(Span::styled(
package.name.clone(),
Style::default().fg(theme.text).add_modifier(Modifier::BOLD),
));
segs.push(Span::styled(
format!(" {}", package.version),
Style::default().fg(theme.overlay1),
));
if !desc.is_empty() {
segs.push(Span::raw(" - "));
segs.push(Span::styled(desc, Style::default().fg(theme.overlay2)));
}
if installed {
segs.push(Span::raw(" "));
segs.push(Span::styled(
"[Installed]",
Style::default()
.fg(theme.green)
.add_modifier(Modifier::BOLD),
));
}
if let Some(vote_spans) = aur_vote_state_spans(package, app, theme) {
segs.extend(vote_spans);
}
let list_status = check_package_in_lists(package, app);
if list_status.in_install || list_status.in_remove || list_status.in_downgrade {
let (label, marker_color) = if list_status.in_remove {
("[-]", theme.red)
} else if list_status.in_downgrade {
("[↓]", theme.yellow)
} else {
("[+]", theme.green)
};
build_package_marker_item(
segs,
prefs.package_marker,
label,
marker_color,
list_status.in_install,
theme,
)
} else {
ListItem::new(Line::from(segs))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_check_package_in_lists() {
let mut app = crate::state::AppState::default();
let package = PackageItem {
name: "test-pkg".to_string(),
version: "1.0".to_string(),
description: String::new(),
source: Source::Aur,
popularity: None,
out_of_date: None,
orphaned: false,
};
let status = check_package_in_lists(&package, &app);
assert!(!status.in_install);
assert!(!status.in_remove);
assert!(!status.in_downgrade);
app.install_list.push(crate::state::PackageItem {
name: "TEST-PKG".to_string(), version: "1.0".to_string(),
description: String::new(),
source: Source::Aur,
popularity: None,
out_of_date: None,
orphaned: false,
});
let status = check_package_in_lists(&package, &app);
assert!(status.in_install);
assert!(!status.in_remove);
assert!(!status.in_downgrade);
}
#[test]
fn test_determine_source_label_and_color_aur() {
let app = crate::state::AppState::default();
let theme = crate::theme::theme();
let source = Source::Aur;
let (label, color) = determine_source_label_and_color(&source, "test-pkg", &app, &theme);
assert_eq!(label, "AUR");
assert_eq!(color, theme.yellow);
}
#[test]
fn test_determine_source_label_and_color_official() {
let app = crate::state::AppState::default();
let theme = crate::theme::theme();
let source = Source::Official {
repo: "core".to_string(),
arch: "x86_64".to_string(),
};
let (label, color) = determine_source_label_and_color(&source, "test-pkg", &app, &theme);
assert_eq!(label, "core");
assert_eq!(color, theme.green);
}
#[test]
fn test_determine_source_label_and_color_dynamic_repo_is_mauve() {
let mut app = crate::state::AppState::default();
app.repo_results_filter_by_name
.insert("core".to_string(), "my-core".to_string());
let theme = crate::theme::theme();
let source = Source::Official {
repo: "core".to_string(),
arch: "x86_64".to_string(),
};
let (label, color) = determine_source_label_and_color(&source, "test-pkg", &app, &theme);
assert_eq!(label, "core");
assert_eq!(color, theme.mauve);
}
#[test]
fn test_build_list_item_not_in_viewport() {
let app = crate::state::AppState::default();
let theme = crate::theme::theme();
let prefs = crate::theme::settings();
let package = PackageItem {
name: "test-pkg".to_string(),
version: "1.0".to_string(),
description: "Test description".to_string(),
source: Source::Aur,
popularity: None,
out_of_date: None,
orphaned: false,
};
let item = build_list_item(&package, &app, &theme, &prefs, false);
let _ = item;
}
#[test]
fn test_build_list_item_in_viewport() {
let app = crate::state::AppState::default();
let theme = crate::theme::theme();
let prefs = crate::theme::settings();
let package = PackageItem {
name: "test-pkg".to_string(),
version: "1.0".to_string(),
description: "Test description".to_string(),
source: Source::Aur,
popularity: Some(1.5),
out_of_date: None,
orphaned: false,
};
let item = build_list_item(&package, &app, &theme, &prefs, true);
let _ = item;
}
}