use crate::state::AppState;
#[allow(clippy::missing_const_for_fn)]
fn is_tool_installed(pkg: &str, bin: &str) -> bool {
crate::index::is_installed(pkg) || crate::install::command_on_path(bin)
}
fn create_optional_dep_row(
app: &AppState,
category_key: &str,
label_suffix: &str,
package: String,
installed: bool,
note: Option<String>,
) -> crate::state::types::OptionalDepRow {
crate::state::types::OptionalDepRow {
label: format!("{}: {label_suffix}", crate::i18n::t(app, category_key)),
package,
installed,
selectable: !installed,
note,
}
}
fn find_first_installed_candidate<'a>(
candidates: &'a [(&'a str, &'a str)],
) -> Option<(&'a str, &'a str)> {
for (bin, pkg) in candidates {
if is_tool_installed(pkg, bin) {
return Some((*bin, *pkg));
}
}
None
}
fn is_helix_installed(pkg: &str, bin: &str) -> bool {
if pkg == "helix" {
is_tool_installed(pkg, bin) || is_tool_installed(pkg, "hx")
} else {
is_tool_installed(pkg, bin)
}
}
fn is_emacs_installed(pkg: &str, bin: &str) -> bool {
if pkg == "emacs" {
is_tool_installed(pkg, bin) || is_tool_installed(pkg, "emacsclient")
} else {
is_tool_installed(pkg, bin)
}
}
fn is_editor_installed(pkg: &str, bin: &str) -> bool {
if pkg == "helix" {
is_helix_installed(pkg, bin)
} else if pkg == "emacs" {
is_emacs_installed(pkg, bin)
} else {
is_tool_installed(pkg, bin)
}
}
fn build_editor_rows(app: &AppState, rows: &mut Vec<crate::state::types::OptionalDepRow>) {
let editor_candidates: &[(&str, &str)] = &[
("nvim", "neovim"),
("vim", "vim"),
("hx", "helix"),
("helix", "helix"),
("emacsclient", "emacs"),
("emacs", "emacs"),
("nano", "nano"),
];
if let Some((bin, pkg)) = find_first_installed_candidate(editor_candidates) {
let installed = is_editor_installed(pkg, bin);
rows.push(create_optional_dep_row(
app,
"app.optional_deps.categories.editor",
bin,
pkg.to_string(),
installed,
None,
));
} else {
let mut seen = std::collections::HashSet::new();
for (bin, pkg) in editor_candidates {
if seen.insert(*pkg) {
let installed = is_editor_installed(pkg, bin);
rows.push(create_optional_dep_row(
app,
"app.optional_deps.categories.editor",
bin,
(*pkg).to_string(),
installed,
None,
));
}
}
}
}
fn build_terminal_rows(app: &AppState, rows: &mut Vec<crate::state::types::OptionalDepRow>) {
let term_candidates: &[(&str, &str)] = &[
("alacritty", "alacritty"),
("ghostty", "ghostty"),
("kitty", "kitty"),
("xterm", "xterm"),
("gnome-terminal", "gnome-terminal"),
("konsole", "konsole"),
("xfce4-terminal", "xfce4-terminal"),
("tilix", "tilix"),
("mate-terminal", "mate-terminal"),
];
if let Some((bin, pkg)) = find_first_installed_candidate(term_candidates) {
let installed = is_tool_installed(pkg, bin);
rows.push(create_optional_dep_row(
app,
"app.optional_deps.categories.terminal",
bin,
pkg.to_string(),
installed,
None,
));
} else {
for (bin, pkg) in term_candidates {
let installed = is_tool_installed(pkg, bin);
rows.push(create_optional_dep_row(
app,
"app.optional_deps.categories.terminal",
bin,
(*pkg).to_string(),
installed,
None,
));
}
}
}
fn is_kde_session() -> bool {
std::env::var("KDE_FULL_SESSION").is_ok()
|| std::env::var("XDG_CURRENT_DESKTOP").ok().is_some_and(|v| {
let u = v.to_uppercase();
u.contains("KDE") || u.contains("PLASMA")
})
|| crate::install::command_on_path("klipper")
}
fn build_clipboard_rows(app: &AppState, rows: &mut Vec<crate::state::types::OptionalDepRow>) {
if is_kde_session() {
let pkg = "plasma-workspace";
let installed = is_tool_installed(pkg, "klipper");
rows.push(create_optional_dep_row(
app,
"app.optional_deps.categories.clipboard",
"Klipper (KDE)",
pkg.to_string(),
installed,
Some("KDE Plasma".to_string()),
));
} else if std::env::var("WAYLAND_DISPLAY").is_ok() {
let pkg = "wl-clipboard";
let installed = is_tool_installed(pkg, "wl-copy");
rows.push(create_optional_dep_row(
app,
"app.optional_deps.categories.clipboard",
"wl-clipboard",
pkg.to_string(),
installed,
Some("Wayland".to_string()),
));
} else {
let pkg = "xclip";
let installed = is_tool_installed(pkg, "xclip");
rows.push(create_optional_dep_row(
app,
"app.optional_deps.categories.clipboard",
"xclip",
pkg.to_string(),
installed,
Some("X11".to_string()),
));
}
}
fn build_mirror_rows(rows: &mut Vec<crate::state::types::OptionalDepRow>) {
let os_release = std::fs::read_to_string("/etc/os-release").unwrap_or_default();
let manjaro = os_release.contains("Manjaro");
let artix = os_release.contains("Artix");
if manjaro {
let pkg = "pacman-mirrors";
let installed = crate::index::is_installed(pkg);
rows.push(crate::state::types::OptionalDepRow {
label: "Mirrors: pacman-mirrors".to_string(),
package: pkg.to_string(),
installed,
selectable: !installed,
note: Some("Manjaro".to_string()),
});
} else if artix {
let pkg = "rate-mirrors";
let installed = is_tool_installed(pkg, "rate-mirrors");
rows.push(crate::state::types::OptionalDepRow {
label: "Mirrors: rate mirrors".to_string(),
package: pkg.to_string(),
installed,
selectable: !installed,
note: Some("Artix".to_string()),
});
} else {
let pkg = "reflector";
let installed = crate::index::is_installed(pkg);
rows.push(crate::state::types::OptionalDepRow {
label: "Mirrors: reflector".to_string(),
package: pkg.to_string(),
installed,
selectable: !installed,
note: None,
});
}
}
fn build_aur_helper_rows(app: &AppState, rows: &mut Vec<crate::state::types::OptionalDepRow>) {
let paru_inst = is_tool_installed("paru", "paru");
let yay_inst = is_tool_installed("yay", "yay");
if paru_inst {
rows.push(create_optional_dep_row(
app,
"app.optional_deps.categories.aur_helper",
"paru",
"paru".to_string(),
true,
None,
));
} else if yay_inst {
rows.push(create_optional_dep_row(
app,
"app.optional_deps.categories.aur_helper",
"yay",
"yay".to_string(),
true,
None,
));
} else {
let note = Some("Install via git clone + makepkg -si".to_string());
rows.push(create_optional_dep_row(
app,
"app.optional_deps.categories.aur_helper",
"paru",
"paru".to_string(),
false,
note.clone(),
));
rows.push(create_optional_dep_row(
app,
"app.optional_deps.categories.aur_helper",
"yay",
"yay".to_string(),
false,
note,
));
}
}
fn build_security_scanner_rows(
app: &AppState,
rows: &mut Vec<crate::state::types::OptionalDepRow>,
) {
let installed = is_tool_installed("clamav", "clamscan");
rows.push(create_optional_dep_row(
app,
"app.optional_deps.categories.security",
"clamav",
"clamav".to_string(),
installed,
None,
));
let installed = is_tool_installed("trivy", "trivy");
rows.push(create_optional_dep_row(
app,
"app.optional_deps.categories.security",
"trivy",
"trivy".to_string(),
installed,
None,
));
let installed = is_tool_installed("semgrep-bin", "semgrep");
rows.push(create_optional_dep_row(
app,
"app.optional_deps.categories.security",
"semgrep-bin",
"semgrep-bin".to_string(),
installed,
Some("AUR".to_string()),
));
let installed = is_tool_installed("shellcheck", "shellcheck");
rows.push(create_optional_dep_row(
app,
"app.optional_deps.categories.security",
"shellcheck",
"shellcheck".to_string(),
installed,
None,
));
let vt_key_present = !crate::theme::settings().virustotal_api_key.is_empty();
rows.push(create_optional_dep_row(
app,
"app.optional_deps.categories.security",
"VirusTotal API",
"virustotal-setup".to_string(),
vt_key_present,
Some("Setup".to_string()),
));
let sleuth_installed = {
let onpath = crate::install::command_on_path("aur-sleuth");
let home = std::env::var("HOME").ok();
let user_local = home.as_deref().is_some_and(|h| {
std::path::Path::new(h)
.join(".local/bin/aur-sleuth")
.exists()
});
let system_local = std::path::Path::new("/usr/local/bin/aur-sleuth").exists();
onpath || user_local || system_local
};
rows.push(create_optional_dep_row(
app,
"app.optional_deps.categories.security",
"aur-sleuth",
"aur-sleuth-setup".to_string(),
sleuth_installed,
Some("Setup".to_string()),
));
let ssh_setup_ready = crate::logic::ssh_setup::is_aur_ssh_setup_configured();
rows.push(create_optional_dep_row(
app,
"app.optional_deps.categories.security",
"AUR SSH setup",
"aur-ssh-setup".to_string(),
ssh_setup_ready,
Some("Setup".to_string()),
));
let active_tool = crate::logic::privilege::active_tool().ok();
if matches!(
active_tool,
Some(crate::logic::privilege::PrivilegeTool::Sudo)
) {
let sudo_ts_configured =
crate::logic::sudo_timestamp_setup::pacsea_sudo_timestamp_drop_in_present();
rows.push({
let label = crate::i18n::t(app, "app.optional_deps.items.sudo_timestamp_setup");
create_optional_dep_row(
app,
"app.optional_deps.categories.privilege",
&label,
"sudo-timestamp-setup".to_string(),
sudo_ts_configured,
Some(if sudo_ts_configured {
"Configured".to_string()
} else {
"Setup".to_string()
}),
)
});
}
if matches!(
active_tool,
Some(crate::logic::privilege::PrivilegeTool::Doas)
) {
let doas_persist_configured =
crate::logic::doas_persist_setup::pacsea_doas_persist_configured();
rows.push({
let label = crate::i18n::t(app, "app.optional_deps.items.doas_persist_setup");
create_optional_dep_row(
app,
"app.optional_deps.categories.privilege",
&label,
"doas-persist-setup".to_string(),
doas_persist_configured,
Some(if doas_persist_configured {
"Configured".to_string()
} else {
"Setup".to_string()
}),
)
});
}
}
fn build_downgrade_rows(app: &AppState, rows: &mut Vec<crate::state::types::OptionalDepRow>) {
let installed = is_tool_installed("downgrade", "downgrade");
rows.push(create_optional_dep_row(
app,
"app.optional_deps.categories.downgrade",
"downgrade",
"downgrade".to_string(),
installed,
None,
));
}
pub fn build_optional_deps_rows(app: &AppState) -> Vec<crate::state::types::OptionalDepRow> {
let mut rows: Vec<crate::state::types::OptionalDepRow> = Vec::new();
build_editor_rows(app, &mut rows);
build_terminal_rows(app, &mut rows);
build_clipboard_rows(app, &mut rows);
build_mirror_rows(&mut rows);
build_aur_helper_rows(app, &mut rows);
build_security_scanner_rows(app, &mut rows);
build_downgrade_rows(app, &mut rows);
rows
}