use std::collections::HashMap;
use crossterm::event::{KeyCode, KeyModifiers};
use ratatui::style::Color;
#[derive(Clone, Copy, Debug)]
pub struct Theme {
pub base: Color,
pub mantle: Color,
pub crust: Color,
pub surface1: Color,
pub surface2: Color,
pub overlay1: Color,
pub overlay2: Color,
pub text: Color,
pub subtext0: Color,
pub subtext1: Color,
pub sapphire: Color,
pub mauve: Color,
pub green: Color,
pub yellow: Color,
pub red: Color,
pub lavender: Color,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PackageMarker {
FullLine,
Front,
End,
}
#[derive(Clone, Debug)]
#[allow(clippy::struct_excessive_bools)]
pub struct Settings {
pub layout_left_pct: u16,
pub layout_center_pct: u16,
pub layout_right_pct: u16,
pub main_pane_order: [crate::state::MainVerticalPane; 3],
pub vertical_min_results: u16,
pub vertical_max_results: u16,
pub vertical_min_middle: u16,
pub vertical_max_middle: u16,
pub vertical_min_package_info: u16,
pub app_dry_run_default: bool,
pub keymap: KeyMap,
pub sort_mode: crate::state::SortMode,
pub clipboard_suffix: String,
pub show_recent_pane: bool,
pub show_install_pane: bool,
pub show_keybinds_footer: bool,
pub selected_countries: String,
pub mirror_count: u16,
pub virustotal_api_key: String,
pub scan_do_clamav: bool,
pub scan_do_trivy: bool,
pub scan_do_semgrep: bool,
pub scan_do_shellcheck: bool,
pub scan_do_virustotal: bool,
pub scan_do_custom: bool,
pub scan_do_sleuth: bool,
pub pkgbuild_shellcheck_exclude: String,
pub pkgbuild_checks_show_raw_output: bool,
pub start_in_news: bool,
pub news_filter_show_arch_news: bool,
pub news_filter_show_advisories: bool,
pub news_filter_show_pkg_updates: bool,
pub news_filter_show_aur_updates: bool,
pub news_filter_show_aur_comments: bool,
pub news_filter_installed_only: bool,
pub news_max_age_days: Option<u32>,
pub startup_news_configured: bool,
pub startup_news_show_arch_news: bool,
pub startup_news_show_advisories: bool,
pub startup_news_show_aur_updates: bool,
pub startup_news_show_aur_comments: bool,
pub startup_news_show_pkg_updates: bool,
pub startup_news_max_age_days: Option<u32>,
pub news_cache_ttl_days: u32,
pub package_marker: PackageMarker,
pub news_read_symbol: String,
pub news_unread_symbol: String,
pub preferred_terminal: String,
pub skip_preflight: bool,
pub locale: String,
pub search_startup_mode: bool,
pub fuzzy_search: bool,
pub updates_refresh_interval: u64,
pub installed_packages_mode: crate::state::InstalledPackagesMode,
pub get_announcement: bool,
pub use_passwordless_sudo: bool,
pub privilege_mode: crate::logic::privilege::PrivilegeMode,
pub auth_mode: crate::logic::privilege::AuthMode,
pub use_terminal_theme: bool,
pub aur_vote_enabled: bool,
pub aur_vote_ssh_timeout_seconds: u32,
pub aur_vote_ssh_command: String,
pub results_filter_toggles: HashMap<String, bool>,
}
impl Default for Settings {
fn default() -> Self {
Self {
layout_left_pct: 20,
layout_center_pct: 60,
layout_right_pct: 20,
main_pane_order: crate::state::DEFAULT_MAIN_PANE_ORDER,
vertical_min_results: 3,
vertical_max_results: 17,
vertical_min_middle: 3,
vertical_max_middle: 5,
vertical_min_package_info: 3,
app_dry_run_default: false,
keymap: KeyMap::default(),
sort_mode: crate::state::SortMode::RepoThenName,
clipboard_suffix: "Check PKGBUILD and source for suspicious and malicious activities"
.to_string(),
show_recent_pane: true,
show_install_pane: true,
show_keybinds_footer: true,
selected_countries: "Worldwide".to_string(),
mirror_count: 20,
virustotal_api_key: String::new(),
scan_do_clamav: true,
scan_do_trivy: true,
scan_do_semgrep: true,
scan_do_shellcheck: true,
scan_do_virustotal: true,
scan_do_custom: true,
scan_do_sleuth: true,
pkgbuild_shellcheck_exclude: "SC2034, SC2164, SC2148, SC2154".to_string(),
pkgbuild_checks_show_raw_output: false,
start_in_news: false,
news_filter_show_arch_news: true,
news_filter_show_advisories: true,
news_filter_show_pkg_updates: true,
news_filter_show_aur_updates: true,
news_filter_show_aur_comments: true,
news_filter_installed_only: false,
news_max_age_days: Some(30),
startup_news_configured: false,
startup_news_show_arch_news: true,
startup_news_show_advisories: true,
startup_news_show_aur_updates: true,
startup_news_show_aur_comments: true,
startup_news_show_pkg_updates: true,
startup_news_max_age_days: Some(7),
news_cache_ttl_days: 7,
package_marker: PackageMarker::Front,
news_read_symbol: "✓".to_string(),
news_unread_symbol: "∘".to_string(),
preferred_terminal: String::new(),
skip_preflight: false,
locale: String::new(), search_startup_mode: false, fuzzy_search: false, updates_refresh_interval: 30, installed_packages_mode: crate::state::InstalledPackagesMode::LeafOnly,
get_announcement: true, use_passwordless_sudo: false, privilege_mode: crate::logic::privilege::PrivilegeMode::Auto, auth_mode: crate::logic::privilege::AuthMode::Prompt, use_terminal_theme: false, aur_vote_enabled: true, aur_vote_ssh_timeout_seconds: 10,
aur_vote_ssh_command: "ssh".to_string(),
results_filter_toggles: HashMap::new(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct KeyChord {
pub code: KeyCode,
pub mods: KeyModifiers,
}
impl KeyChord {
#[must_use]
pub fn label(&self) -> String {
let mut parts: Vec<&'static str> = Vec::new();
if self.mods.contains(KeyModifiers::CONTROL) {
parts.push("Ctrl");
}
if self.mods.contains(KeyModifiers::ALT) {
parts.push("Alt");
}
if self.mods.contains(KeyModifiers::SHIFT) {
parts.push("Shift");
}
if self.mods.contains(KeyModifiers::SUPER) {
parts.push("Super");
}
let key = match self.code {
KeyCode::Char(ch) => {
let up = ch.to_ascii_uppercase();
if up == ' ' {
"Space".to_string()
} else {
up.to_string()
}
}
KeyCode::Enter => "Enter".to_string(),
KeyCode::Esc => "Esc".to_string(),
KeyCode::Backspace => "Backspace".to_string(),
KeyCode::Tab => "Tab".to_string(),
KeyCode::BackTab => "Shift+Tab".to_string(),
KeyCode::Delete => "Del".to_string(),
KeyCode::Insert => "Ins".to_string(),
KeyCode::Home => "Home".to_string(),
KeyCode::End => "End".to_string(),
KeyCode::PageUp => "PgUp".to_string(),
KeyCode::PageDown => "PgDn".to_string(),
KeyCode::Up => "↑".to_string(),
KeyCode::Down => "↓".to_string(),
KeyCode::Left => "←".to_string(),
KeyCode::Right => "→".to_string(),
KeyCode::F(n) => format!("F{n}"),
_ => "?".to_string(),
};
if parts.is_empty() || matches!(self.code, KeyCode::BackTab) {
key
} else {
format!("{}+{key}", parts.join("+"))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn theme_keychord_label_variants() {
let kc = KeyChord {
code: KeyCode::Char('r'),
mods: KeyModifiers::CONTROL,
};
assert_eq!(kc.label(), "Ctrl+R");
let kc2 = KeyChord {
code: KeyCode::Char(' '),
mods: KeyModifiers::empty(),
};
assert_eq!(kc2.label(), "Space");
let kc3 = KeyChord {
code: KeyCode::F(5),
mods: KeyModifiers::empty(),
};
assert_eq!(kc3.label(), "F5");
let kc4 = KeyChord {
code: KeyCode::BackTab,
mods: KeyModifiers::SHIFT,
};
assert_eq!(kc4.label(), "Shift+Tab");
let kc5 = KeyChord {
code: KeyCode::Left,
mods: KeyModifiers::empty(),
};
assert_eq!(kc5.label(), "←");
let kc6 = KeyChord {
code: KeyCode::Char('x'),
mods: KeyModifiers::ALT | KeyModifiers::SHIFT,
};
assert_eq!(kc6.label(), "Alt+Shift+X");
}
}
#[derive(Clone, Debug)]
pub struct KeyMap {
pub help_overlay: Vec<KeyChord>,
pub reload_config: Vec<KeyChord>,
pub exit: Vec<KeyChord>,
pub show_pkgbuild: Vec<KeyChord>,
pub comments_toggle: Vec<KeyChord>,
pub run_pkgbuild_checks: Vec<KeyChord>,
pub cycle_pkgbuild_sections: Vec<KeyChord>,
pub change_sort: Vec<KeyChord>,
pub pane_next: Vec<KeyChord>,
pub pane_left: Vec<KeyChord>,
pub pane_right: Vec<KeyChord>,
pub config_menu_toggle: Vec<KeyChord>,
pub options_menu_toggle: Vec<KeyChord>,
pub panels_menu_toggle: Vec<KeyChord>,
pub search_move_up: Vec<KeyChord>,
pub search_move_down: Vec<KeyChord>,
pub search_page_up: Vec<KeyChord>,
pub search_page_down: Vec<KeyChord>,
pub search_add: Vec<KeyChord>,
pub search_install: Vec<KeyChord>,
pub search_focus_left: Vec<KeyChord>,
pub search_focus_right: Vec<KeyChord>,
pub search_backspace: Vec<KeyChord>,
pub search_insert_clear: Vec<KeyChord>,
pub search_normal_toggle: Vec<KeyChord>,
pub search_normal_insert: Vec<KeyChord>,
pub search_normal_select_left: Vec<KeyChord>,
pub search_normal_select_right: Vec<KeyChord>,
pub search_normal_delete: Vec<KeyChord>,
pub search_normal_clear: Vec<KeyChord>,
pub search_normal_open_status: Vec<KeyChord>,
pub search_normal_import: Vec<KeyChord>,
pub search_normal_export: Vec<KeyChord>,
pub search_normal_updates: Vec<KeyChord>,
pub toggle_fuzzy: Vec<KeyChord>,
pub recent_move_up: Vec<KeyChord>,
pub recent_move_down: Vec<KeyChord>,
pub recent_find: Vec<KeyChord>,
pub recent_use: Vec<KeyChord>,
pub recent_add: Vec<KeyChord>,
pub recent_to_search: Vec<KeyChord>,
pub recent_focus_right: Vec<KeyChord>,
pub recent_remove: Vec<KeyChord>,
pub recent_clear: Vec<KeyChord>,
pub install_move_up: Vec<KeyChord>,
pub install_move_down: Vec<KeyChord>,
pub install_confirm: Vec<KeyChord>,
pub install_remove: Vec<KeyChord>,
pub install_clear: Vec<KeyChord>,
pub install_find: Vec<KeyChord>,
pub install_to_search: Vec<KeyChord>,
pub install_focus_left: Vec<KeyChord>,
pub news_mark_read: Vec<KeyChord>,
pub news_mark_all_read: Vec<KeyChord>,
pub news_mark_read_feed: Vec<KeyChord>,
pub news_mark_unread_feed: Vec<KeyChord>,
pub news_toggle_read_feed: Vec<KeyChord>,
}
type GlobalKeys = (
Vec<KeyChord>,
Vec<KeyChord>,
Vec<KeyChord>,
Vec<KeyChord>,
Vec<KeyChord>,
Vec<KeyChord>,
Vec<KeyChord>,
Vec<KeyChord>,
Vec<KeyChord>,
Vec<KeyChord>,
);
type SearchKeys = (
Vec<KeyChord>,
Vec<KeyChord>,
Vec<KeyChord>,
Vec<KeyChord>,
Vec<KeyChord>,
Vec<KeyChord>,
Vec<KeyChord>,
Vec<KeyChord>,
Vec<KeyChord>,
Vec<KeyChord>,
);
type SearchNormalKeys = (
Vec<KeyChord>,
Vec<KeyChord>,
Vec<KeyChord>,
Vec<KeyChord>,
Vec<KeyChord>,
Vec<KeyChord>,
Vec<KeyChord>,
Vec<KeyChord>,
Vec<KeyChord>,
Vec<KeyChord>,
);
type RecentKeys = (
Vec<KeyChord>,
Vec<KeyChord>,
Vec<KeyChord>,
Vec<KeyChord>,
Vec<KeyChord>,
Vec<KeyChord>,
Vec<KeyChord>,
Vec<KeyChord>,
Vec<KeyChord>,
);
type InstallKeys = (
Vec<KeyChord>,
Vec<KeyChord>,
Vec<KeyChord>,
Vec<KeyChord>,
Vec<KeyChord>,
Vec<KeyChord>,
Vec<KeyChord>,
Vec<KeyChord>,
);
fn default_global_keys(none: KeyModifiers, ctrl: KeyModifiers) -> GlobalKeys {
use KeyCode::{BackTab, Char, Left, Right, Tab};
(
vec![
KeyChord {
code: KeyCode::F(1),
mods: none,
},
KeyChord {
code: Char('?'),
mods: none,
},
],
vec![KeyChord {
code: Char('r'),
mods: ctrl,
}],
vec![KeyChord {
code: Char('c'),
mods: ctrl,
}],
vec![KeyChord {
code: Char('x'),
mods: ctrl,
}],
vec![KeyChord {
code: Char('t'),
mods: ctrl,
}],
vec![KeyChord {
code: Char('k'),
mods: ctrl,
}],
vec![KeyChord {
code: BackTab,
mods: none,
}],
vec![KeyChord {
code: Tab,
mods: none,
}],
vec![KeyChord {
code: Left,
mods: none,
}],
vec![KeyChord {
code: Right,
mods: none,
}],
)
}
fn default_dropdown_keys(shift: KeyModifiers) -> (Vec<KeyChord>, Vec<KeyChord>, Vec<KeyChord>) {
use KeyCode::Char;
(
vec![KeyChord {
code: Char('c'),
mods: shift,
}],
vec![KeyChord {
code: Char('o'),
mods: shift,
}],
vec![KeyChord {
code: Char('p'),
mods: shift,
}],
)
}
fn default_search_keys(none: KeyModifiers) -> SearchKeys {
use KeyCode::{Backspace, Char, Down, Enter, Left, PageDown, PageUp, Right, Up};
(
vec![KeyChord {
code: Up,
mods: none,
}],
vec![KeyChord {
code: Down,
mods: none,
}],
vec![KeyChord {
code: PageUp,
mods: none,
}],
vec![KeyChord {
code: PageDown,
mods: none,
}],
vec![KeyChord {
code: Char(' '),
mods: none,
}],
vec![KeyChord {
code: Enter,
mods: none,
}],
vec![KeyChord {
code: Left,
mods: none,
}],
vec![KeyChord {
code: Right,
mods: none,
}],
vec![KeyChord {
code: Backspace,
mods: none,
}],
vec![KeyChord {
code: KeyCode::Delete,
mods: KeyModifiers::SHIFT,
}],
)
}
fn default_search_normal_keys(none: KeyModifiers, shift: KeyModifiers) -> SearchNormalKeys {
use KeyCode::{Char, Delete, Esc};
(
vec![KeyChord {
code: Esc,
mods: none,
}],
vec![KeyChord {
code: Char('i'),
mods: none,
}],
vec![KeyChord {
code: Char('h'),
mods: none,
}],
vec![KeyChord {
code: Char('l'),
mods: none,
}],
vec![KeyChord {
code: Char('d'),
mods: none,
}],
vec![KeyChord {
code: Delete,
mods: shift,
}],
vec![KeyChord {
code: Char('s'),
mods: shift,
}],
vec![KeyChord {
code: Char('i'),
mods: shift,
}],
vec![KeyChord {
code: Char('e'),
mods: shift,
}],
vec![KeyChord {
code: Char('u'),
mods: shift,
}],
)
}
fn default_recent_keys(none: KeyModifiers, shift: KeyModifiers) -> RecentKeys {
use KeyCode::{Char, Delete, Down, Enter, Esc, Right, Up};
(
vec![
KeyChord {
code: Char('k'),
mods: none,
},
KeyChord {
code: Up,
mods: none,
},
],
vec![
KeyChord {
code: Char('j'),
mods: none,
},
KeyChord {
code: Down,
mods: none,
},
],
vec![KeyChord {
code: Char('/'),
mods: none,
}],
vec![KeyChord {
code: Enter,
mods: none,
}],
vec![KeyChord {
code: Char(' '),
mods: none,
}],
vec![KeyChord {
code: Esc,
mods: none,
}],
vec![KeyChord {
code: Right,
mods: none,
}],
vec![
KeyChord {
code: Char('d'),
mods: none,
},
KeyChord {
code: Delete,
mods: none,
},
],
vec![KeyChord {
code: Delete,
mods: shift,
}],
)
}
fn default_install_keys(none: KeyModifiers, shift: KeyModifiers) -> InstallKeys {
use KeyCode::{Char, Delete, Down, Enter, Esc, Left, Up};
(
vec![
KeyChord {
code: Char('k'),
mods: none,
},
KeyChord {
code: Up,
mods: none,
},
],
vec![
KeyChord {
code: Char('j'),
mods: none,
},
KeyChord {
code: Down,
mods: none,
},
],
vec![KeyChord {
code: Enter,
mods: none,
}],
vec![
KeyChord {
code: Delete,
mods: none,
},
KeyChord {
code: Char('d'),
mods: none,
},
],
vec![KeyChord {
code: Delete,
mods: shift,
}],
vec![KeyChord {
code: Char('/'),
mods: none,
}],
vec![KeyChord {
code: Esc,
mods: none,
}],
vec![KeyChord {
code: Left,
mods: none,
}],
)
}
fn default_news_keys(none: KeyModifiers, ctrl: KeyModifiers) -> (Vec<KeyChord>, Vec<KeyChord>) {
use KeyCode::Char;
(
vec![KeyChord {
code: Char('r'),
mods: none,
}],
vec![KeyChord {
code: Char('r'),
mods: ctrl,
}],
)
}
fn default_news_feed_keys(none: KeyModifiers) -> (Vec<KeyChord>, Vec<KeyChord>, Vec<KeyChord>) {
use KeyCode::Char;
(
vec![KeyChord {
code: Char('r'),
mods: none,
}],
vec![KeyChord {
code: Char('u'),
mods: none,
}],
vec![KeyChord {
code: Char('t'),
mods: none,
}],
)
}
fn build_default_keymap() -> KeyMap {
let none = KeyModifiers::empty();
let ctrl = KeyModifiers::CONTROL;
let shift = KeyModifiers::SHIFT;
let global = default_global_keys(none, ctrl);
let dropdown = default_dropdown_keys(shift);
let search = default_search_keys(none);
let search_normal = default_search_normal_keys(none, shift);
let recent = default_recent_keys(none, shift);
let install = default_install_keys(none, shift);
let news = default_news_keys(none, ctrl);
let news_feed = default_news_feed_keys(none);
KeyMap {
help_overlay: global.0,
reload_config: global.1,
exit: global.2,
show_pkgbuild: global.3,
comments_toggle: global.4,
run_pkgbuild_checks: global.5,
cycle_pkgbuild_sections: vec![KeyChord {
code: KeyCode::Char('d'),
mods: ctrl,
}],
change_sort: global.6,
pane_next: global.7,
pane_left: global.8,
pane_right: global.9,
config_menu_toggle: dropdown.0,
options_menu_toggle: dropdown.1,
panels_menu_toggle: dropdown.2,
search_move_up: search.0,
search_move_down: search.1,
search_page_up: search.2,
search_page_down: search.3,
search_add: search.4,
search_install: search.5,
search_focus_left: search.6,
search_focus_right: search.7,
search_backspace: search.8,
search_insert_clear: search.9,
search_normal_toggle: search_normal.0,
search_normal_insert: search_normal.1,
search_normal_select_left: search_normal.2,
search_normal_select_right: search_normal.3,
search_normal_delete: search_normal.4,
search_normal_clear: search_normal.5,
search_normal_open_status: search_normal.6,
search_normal_import: search_normal.7,
search_normal_export: search_normal.8,
search_normal_updates: search_normal.9,
toggle_fuzzy: vec![KeyChord {
code: KeyCode::Char('f'),
mods: ctrl,
}],
recent_move_up: recent.0,
recent_move_down: recent.1,
recent_find: recent.2,
recent_use: recent.3,
recent_add: recent.4,
recent_to_search: recent.5,
recent_focus_right: recent.6,
recent_remove: recent.7,
recent_clear: recent.8,
install_move_up: install.0,
install_move_down: install.1,
install_confirm: install.2,
install_remove: install.3,
install_clear: install.4,
install_find: install.5,
install_to_search: install.6,
install_focus_left: install.7,
news_mark_read: news.0,
news_mark_all_read: news.1,
news_mark_read_feed: news_feed.0,
news_mark_unread_feed: news_feed.1,
news_toggle_read_feed: news_feed.2,
}
}
impl Default for KeyMap {
fn default() -> Self {
build_default_keymap()
}
}