Skip to main content

pacsea/theme/
types.rs

1use std::collections::HashMap;
2
3use crossterm::event::{KeyCode, KeyModifiers};
4use ratatui::style::Color;
5
6/// Application theme palette used by rendering code.
7///
8/// All colors are provided as [`ratatui::style::Color`] and are suitable for
9/// direct use with widgets and styles.
10#[derive(Clone, Copy, Debug)]
11pub struct Theme {
12    /// Primary background color for the canvas.
13    pub base: Color,
14    /// Slightly lighter background layer used behind panels.
15    pub mantle: Color,
16    /// Darkest background shade for deep contrast areas.
17    pub crust: Color,
18    /// Subtle surface color for component backgrounds (level 1).
19    pub surface1: Color,
20    /// Subtle surface color for component backgrounds (level 2).
21    pub surface2: Color,
22    /// Muted overlay line/border color (primary).
23    pub overlay1: Color,
24    /// Muted overlay line/border color (secondary).
25    pub overlay2: Color,
26    /// Primary foreground text color.
27    pub text: Color,
28    /// Secondary text for less prominent content.
29    pub subtext0: Color,
30    /// Tertiary text for captions and low-emphasis content.
31    pub subtext1: Color,
32    /// Accent color commonly used for selection and interactive highlights.
33    pub sapphire: Color,
34    /// Accent color for emphasized headings or selections.
35    pub mauve: Color,
36    /// Success/positive state color.
37    pub green: Color,
38    /// Warning/attention state color.
39    pub yellow: Color,
40    /// Error/danger state color.
41    pub red: Color,
42    /// Accent color for subtle emphasis and borders.
43    pub lavender: Color,
44}
45
46/// User-configurable application settings parsed from `pacsea.conf`.
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
48pub enum PackageMarker {
49    /// Color the entire line for the marked package.
50    FullLine,
51    /// Add a marker at the front of the line.
52    Front,
53    /// Add a marker at the end of the line.
54    End,
55}
56
57/// User-configurable application settings parsed from `pacsea.conf`.
58#[derive(Clone, Debug)]
59#[allow(clippy::struct_excessive_bools)]
60pub struct Settings {
61    /// Percentage width allocated to the Recent pane (left column).
62    pub layout_left_pct: u16,
63    /// Percentage width allocated to the Search pane (center column).
64    pub layout_center_pct: u16,
65    /// Percentage width allocated to the Install pane (right column).
66    pub layout_right_pct: u16,
67    /// Vertical order of the main stack: results list, middle search row, package info (each once).
68    pub main_pane_order: [crate::state::MainVerticalPane; 3],
69    /// Minimum terminal rows for the results list band.
70    pub vertical_min_results: u16,
71    /// Maximum terminal rows for the results list band.
72    pub vertical_max_results: u16,
73    /// Minimum terminal rows for the middle (search) row.
74    pub vertical_min_middle: u16,
75    /// Maximum terminal rows for the middle row.
76    pub vertical_max_middle: u16,
77    /// Minimum terminal rows for package info when that band is shown.
78    pub vertical_min_package_info: u16,
79    /// Default value for the application's dry-run mode on startup.
80    /// This can be toggled via the `--dry-run` CLI flag.
81    pub app_dry_run_default: bool,
82    /// Configurable key bindings parsed from `pacsea.conf`
83    pub keymap: KeyMap,
84    /// Initial sort mode for results list.
85    pub sort_mode: crate::state::SortMode,
86    /// Text appended when copying PKGBUILD to clipboard.
87    pub clipboard_suffix: String,
88    /// Whether the Search history pane should be shown on startup.
89    pub show_recent_pane: bool,
90    /// Whether the Install/Remove pane should be shown on startup.
91    pub show_install_pane: bool,
92    /// Whether the keybinds footer should be shown on startup.
93    pub show_keybinds_footer: bool,
94    /// Selected countries used when updating mirrors (comma-separated or multiple).
95    pub selected_countries: String,
96    /// Number of mirrors to fetch/rank when updating.
97    pub mirror_count: u16,
98    /// Preferred AUR helper for CLI operations: "auto" (detect, paru preferred), "paru", or "yay".
99    pub aur_helper: String,
100    /// `VirusTotal` API key for security scanning.
101    pub virustotal_api_key: String,
102    /// Whether to run `ClamAV` scan on AUR packages.
103    pub scan_do_clamav: bool,
104    /// Whether to run `Trivy` scan on AUR packages.
105    pub scan_do_trivy: bool,
106    /// Whether to run `Semgrep` scan on AUR packages.
107    pub scan_do_semgrep: bool,
108    /// Whether to run `ShellCheck` scan on AUR packages.
109    pub scan_do_shellcheck: bool,
110    /// Whether to run `VirusTotal` scan on AUR packages.
111    pub scan_do_virustotal: bool,
112    /// Whether to run custom scan on AUR packages.
113    pub scan_do_custom: bool,
114    /// Whether to run Sleuth scan on AUR packages.
115    pub scan_do_sleuth: bool,
116    /// Comma-separated `ShellCheck` rule IDs to pass as `--exclude` when running PKGBUILD static checks.
117    /// Empty means `ShellCheck` runs without `--exclude`.
118    pub pkgbuild_shellcheck_exclude: String,
119    /// When true, the PKGBUILD details pane may show the expandable `Raw output:` block from static checks.
120    /// When false, findings and status still show; raw command output is hidden. Defaults to false.
121    pub pkgbuild_checks_show_raw_output: bool,
122    /// Whether to start the app in News mode (true) or Package mode (false).
123    pub start_in_news: bool,
124    /// Whether to show Arch news items in the News view.
125    pub news_filter_show_arch_news: bool,
126    /// Whether to show security advisories in the News view.
127    pub news_filter_show_advisories: bool,
128    /// Whether to show installed package update items in the News view.
129    pub news_filter_show_pkg_updates: bool,
130    /// Whether to show AUR package update items in the News view.
131    pub news_filter_show_aur_updates: bool,
132    /// Whether to show installed AUR comment items in the News view.
133    pub news_filter_show_aur_comments: bool,
134    /// Whether to restrict advisories to installed packages in the News view.
135    pub news_filter_installed_only: bool,
136    /// Maximum age of news items in days (None = unlimited).
137    pub news_max_age_days: Option<u32>,
138    /// Whether startup news popup setup has been completed.
139    pub startup_news_configured: bool,
140    /// Whether to show Arch news in startup news popup.
141    pub startup_news_show_arch_news: bool,
142    /// Whether to show security advisories in startup news popup.
143    pub startup_news_show_advisories: bool,
144    /// Whether to show AUR updates in startup news popup.
145    pub startup_news_show_aur_updates: bool,
146    /// Whether to show AUR comments in startup news popup.
147    pub startup_news_show_aur_comments: bool,
148    /// Whether to show official package updates in startup news popup.
149    pub startup_news_show_pkg_updates: bool,
150    /// Maximum age of news items in days for startup news popup (None = unlimited).
151    pub startup_news_max_age_days: Option<u32>,
152    /// How many days to keep Arch news and advisories cached on disk.
153    /// Default is 7 days. Helps reduce network requests on startup.
154    pub news_cache_ttl_days: u32,
155    /// Visual marker style for packages added to Install/Remove/Downgrade lists.
156    pub package_marker: PackageMarker,
157    /// Symbol used to mark a news item as read in the News modal.
158    pub news_read_symbol: String,
159    /// Symbol used to mark a news item as unread in the News modal.
160    pub news_unread_symbol: String,
161    /// Preferred terminal binary name to spawn for shell commands (e.g., "alacritty", "kitty", "gnome-terminal").
162    /// When empty, Pacsea auto-detects from available terminals.
163    pub preferred_terminal: String,
164    /// When true, skip the Preflight modal and execute actions directly (install/remove/downgrade).
165    /// Defaults to false to preserve the safer, review-first workflow.
166    pub skip_preflight: bool,
167    /// Locale code for translations (e.g., "de-DE", "en-US").
168    /// Empty string means auto-detect from system locale.
169    pub locale: String,
170    /// Search input mode on startup.
171    /// When false, starts in insert mode (default).
172    /// When true, starts in normal mode.
173    pub search_startup_mode: bool,
174    /// Whether fuzzy search is enabled by default on startup.
175    /// When false, uses normal substring search (default).
176    /// When true, uses fuzzy matching (fzf-style).
177    pub fuzzy_search: bool,
178    /// Refresh interval in seconds for pacman -Qu and AUR helper checks.
179    /// Default is 30 seconds. Set to a higher value to reduce resource usage on slow systems.
180    pub updates_refresh_interval: u64,
181    /// Filter mode for installed packages display.
182    /// `LeafOnly` shows explicitly installed packages with no dependents.
183    /// `AllExplicit` shows all explicitly installed packages.
184    pub installed_packages_mode: crate::state::InstalledPackagesMode,
185    /// Whether to fetch remote announcements from GitHub Gist.
186    /// If `true`, fetches announcements from the configured Gist URL.
187    /// If `false`, remote announcements are disabled (version announcements still show).
188    pub get_announcement: bool,
189    /// Whether to use passwordless sudo for install operations when available.
190    /// If `false` (default), password prompt is always shown even if passwordless sudo is configured.
191    /// If `true`, passwordless sudo is used when available, skipping the password prompt.
192    /// This acts as an additional safety barrier requiring explicit opt-in.
193    pub use_passwordless_sudo: bool,
194    /// Privilege escalation tool selection mode.
195    /// Controls which tool (sudo/doas) is used for privileged operations.
196    /// `Auto` (default): prefer doas if available, fall back to sudo.
197    /// `Sudo`: always use sudo. `Doas`: always use doas.
198    pub privilege_mode: crate::logic::privilege::PrivilegeMode,
199    /// Authentication mode for privilege escalation.
200    /// Controls how Pacsea handles password/authentication before privileged operations.
201    /// `Prompt` (default): Pacsea captures password only for stdin-capable tools (sudo).
202    /// For doas, prompt mode is coerced to `Interactive` because doas cannot read stdin passwords.
203    /// `PasswordlessOnly`: Skip prompt only when `{tool} -n true` succeeds.
204    /// `Interactive`: Let the privilege tool handle auth directly (fingerprint via PAM, etc.).
205    pub auth_mode: crate::logic::privilege::AuthMode,
206    /// Whether to use the terminal's theme colors instead of theme.conf.
207    /// If `true`, Pacsea queries the terminal for foreground/background colors via OSC 10/11.
208    /// If `false` (default), uses theme.conf colors.
209    /// Note: Terminal theme is also used automatically when theme.conf is missing/invalid
210    /// and the terminal is on the supported list (alacritty, kitty, konsole, ghostty, xterm,
211    /// gnome-terminal, xfce4-terminal, tilix, mate-terminal, wezterm-gui, `WezTerm`).
212    pub use_terminal_theme: bool,
213    /// Whether AUR voting via SSH is enabled.
214    /// Requires an SSH key uploaded to the user's AUR account.
215    pub aur_vote_enabled: bool,
216    /// SSH connect timeout in seconds for AUR vote commands.
217    pub aur_vote_ssh_timeout_seconds: u32,
218    /// SSH binary path or name for AUR vote commands.
219    /// Defaults to `"ssh"`. Override for non-standard SSH setups.
220    pub aur_vote_ssh_command: String,
221    /// Dynamic results-list toggles from `repos.conf` filter ids (canonical keys, see `repos` module).
222    ///
223    /// Keys match canonical `results_filter` tokens from repos.conf (e.g. `vendor_pkgs` for `results_filter_show_vendor_pkgs`).
224    pub results_filter_toggles: HashMap<String, bool>,
225}
226
227impl Default for Settings {
228    /// What: Provide the built-in baseline configuration for Pacsea settings.
229    ///
230    /// Inputs:
231    /// - None.
232    ///
233    /// Output:
234    /// - Returns a `Settings` instance populated with Pacsea's preferred defaults.
235    ///
236    /// Details:
237    /// - Sets balanced pane layout percentages and enables all panes by default.
238    /// - Enables all scan types and uses Catppuccin-inspired news glyphs.
239    fn default() -> Self {
240        Self {
241            layout_left_pct: 20,
242            layout_center_pct: 60,
243            layout_right_pct: 20,
244            main_pane_order: crate::state::DEFAULT_MAIN_PANE_ORDER,
245            vertical_min_results: 3,
246            vertical_max_results: 17,
247            vertical_min_middle: 3,
248            vertical_max_middle: 5,
249            vertical_min_package_info: 3,
250            app_dry_run_default: false,
251            keymap: KeyMap::default(),
252            sort_mode: crate::state::SortMode::RepoThenName,
253            clipboard_suffix: "Check PKGBUILD and source for suspicious and malicious activities"
254                .to_string(),
255            show_recent_pane: true,
256            show_install_pane: true,
257            show_keybinds_footer: true,
258            selected_countries: "Worldwide".to_string(),
259            mirror_count: 20,
260            aur_helper: "auto".to_string(),
261            virustotal_api_key: String::new(),
262            scan_do_clamav: true,
263            scan_do_trivy: true,
264            scan_do_semgrep: true,
265            scan_do_shellcheck: true,
266            scan_do_virustotal: true,
267            scan_do_custom: true,
268            scan_do_sleuth: true,
269            pkgbuild_shellcheck_exclude: "SC2034, SC2164, SC2148, SC2154".to_string(),
270            pkgbuild_checks_show_raw_output: false,
271            start_in_news: false,
272            news_filter_show_arch_news: true,
273            news_filter_show_advisories: true,
274            news_filter_show_pkg_updates: true,
275            news_filter_show_aur_updates: true,
276            news_filter_show_aur_comments: true,
277            news_filter_installed_only: false,
278            news_max_age_days: Some(30),
279            startup_news_configured: false,
280            startup_news_show_arch_news: true,
281            startup_news_show_advisories: true,
282            startup_news_show_aur_updates: true,
283            startup_news_show_aur_comments: true,
284            startup_news_show_pkg_updates: true,
285            startup_news_max_age_days: Some(7),
286            news_cache_ttl_days: 7,
287            package_marker: PackageMarker::Front,
288            news_read_symbol: "✓".to_string(),
289            news_unread_symbol: "∘".to_string(),
290            preferred_terminal: String::new(),
291            skip_preflight: false,
292            locale: String::new(),        // Empty means auto-detect from system
293            search_startup_mode: false,   // Default to insert mode
294            fuzzy_search: false,          // Default to normal substring search
295            updates_refresh_interval: 30, // Default to 30 seconds
296            installed_packages_mode: crate::state::InstalledPackagesMode::LeafOnly,
297            get_announcement: true, // Default to fetching remote announcements
298            use_passwordless_sudo: false, // Default to always showing password prompt (safety barrier)
299            privilege_mode: crate::logic::privilege::PrivilegeMode::Auto, // Default to auto-detect (prefer doas, fallback sudo)
300            auth_mode: crate::logic::privilege::AuthMode::Prompt, // Default to Pacsea password modal
301            use_terminal_theme: false, // Default to using theme.conf colors
302            aur_vote_enabled: true,    // Enabled by default; requires SSH key configured on AUR
303            aur_vote_ssh_timeout_seconds: 10,
304            aur_vote_ssh_command: "ssh".to_string(),
305            results_filter_toggles: HashMap::new(),
306        }
307    }
308}
309
310/// A single keyboard chord (modifiers + key).
311#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
312pub struct KeyChord {
313    /// The key code (e.g., Char('a'), Enter, Esc).
314    pub code: KeyCode,
315    /// The modifier keys (e.g., Ctrl, Shift, Alt).
316    pub mods: KeyModifiers,
317}
318
319impl KeyChord {
320    /// Return a short display label such as "Ctrl+R", "F1", "Shift+Del", "+/ ?".
321    #[must_use]
322    pub fn label(&self) -> String {
323        let mut parts: Vec<&'static str> = Vec::new();
324        if self.mods.contains(KeyModifiers::CONTROL) {
325            parts.push("Ctrl");
326        }
327        if self.mods.contains(KeyModifiers::ALT) {
328            parts.push("Alt");
329        }
330        if self.mods.contains(KeyModifiers::SHIFT) {
331            parts.push("Shift");
332        }
333        if self.mods.contains(KeyModifiers::SUPER) {
334            parts.push("Super");
335        }
336        let key = match self.code {
337            KeyCode::Char(ch) => {
338                // Show uppercase character for display
339                let up = ch.to_ascii_uppercase();
340                if up == ' ' {
341                    "Space".to_string()
342                } else {
343                    up.to_string()
344                }
345            }
346            KeyCode::Enter => "Enter".to_string(),
347            KeyCode::Esc => "Esc".to_string(),
348            KeyCode::Backspace => "Backspace".to_string(),
349            KeyCode::Tab => "Tab".to_string(),
350            KeyCode::BackTab => "Shift+Tab".to_string(),
351            KeyCode::Delete => "Del".to_string(),
352            KeyCode::Insert => "Ins".to_string(),
353            KeyCode::Home => "Home".to_string(),
354            KeyCode::End => "End".to_string(),
355            KeyCode::PageUp => "PgUp".to_string(),
356            KeyCode::PageDown => "PgDn".to_string(),
357            KeyCode::Up => "↑".to_string(),
358            KeyCode::Down => "↓".to_string(),
359            KeyCode::Left => "←".to_string(),
360            KeyCode::Right => "→".to_string(),
361            KeyCode::F(n) => format!("F{n}"),
362            _ => "?".to_string(),
363        };
364        if parts.is_empty() || matches!(self.code, KeyCode::BackTab) {
365            key
366        } else {
367            format!("{}+{key}", parts.join("+"))
368        }
369    }
370}
371
372#[cfg(test)]
373mod tests {
374    use super::*;
375
376    #[test]
377    /// What: Ensure `KeyChord::label` renders user-facing text for modifier and key combinations.
378    ///
379    /// Inputs:
380    /// - Sample chords covering control characters, space, function keys, Shift+BackTab, arrow keys, and multi-modifier chords.
381    ///
382    /// Output:
383    /// - Labels such as `"Ctrl+R"`, `"Space"`, `"F5"`, `"Shift+Tab"`, arrow glyphs, and combined modifier strings.
384    ///
385    /// Details:
386    /// - Protects the formatting logic that appears in keybinding help overlays.
387    fn theme_keychord_label_variants() {
388        let kc = KeyChord {
389            code: KeyCode::Char('r'),
390            mods: KeyModifiers::CONTROL,
391        };
392        assert_eq!(kc.label(), "Ctrl+R");
393
394        let kc2 = KeyChord {
395            code: KeyCode::Char(' '),
396            mods: KeyModifiers::empty(),
397        };
398        assert_eq!(kc2.label(), "Space");
399
400        let kc3 = KeyChord {
401            code: KeyCode::F(5),
402            mods: KeyModifiers::empty(),
403        };
404        assert_eq!(kc3.label(), "F5");
405
406        let kc4 = KeyChord {
407            code: KeyCode::BackTab,
408            mods: KeyModifiers::SHIFT,
409        };
410        assert_eq!(kc4.label(), "Shift+Tab");
411
412        let kc5 = KeyChord {
413            code: KeyCode::Left,
414            mods: KeyModifiers::empty(),
415        };
416        assert_eq!(kc5.label(), "←");
417
418        let kc6 = KeyChord {
419            code: KeyCode::Char('x'),
420            mods: KeyModifiers::ALT | KeyModifiers::SHIFT,
421        };
422        assert_eq!(kc6.label(), "Alt+Shift+X");
423    }
424}
425
426/// Application key bindings.
427/// Each action can have multiple chords.
428#[derive(Clone, Debug)]
429pub struct KeyMap {
430    // Global
431    /// Key chords to show help overlay.
432    pub help_overlay: Vec<KeyChord>,
433    /// Key chords to reload configuration.
434    pub reload_config: Vec<KeyChord>,
435    /// Key chords to exit the application.
436    pub exit: Vec<KeyChord>,
437    /// Global: Show/Hide PKGBUILD viewer
438    pub show_pkgbuild: Vec<KeyChord>,
439    /// Global: Show/Hide AUR comments viewer
440    pub comments_toggle: Vec<KeyChord>,
441    /// Global: Run PKGBUILD static checks in preview panel.
442    pub run_pkgbuild_checks: Vec<KeyChord>,
443    /// Global: Cycle PKGBUILD pane between body, `ShellCheck`, and `Namcap` sections.
444    pub cycle_pkgbuild_sections: Vec<KeyChord>,
445    /// Global: Change results sorting mode
446    pub change_sort: Vec<KeyChord>,
447    /// Key chords to move to next pane.
448    pub pane_next: Vec<KeyChord>,
449    /// Key chords to move focus left.
450    pub pane_left: Vec<KeyChord>,
451    /// Key chords to move focus right.
452    pub pane_right: Vec<KeyChord>,
453    /// Global: Toggle Config/Lists dropdown
454    pub config_menu_toggle: Vec<KeyChord>,
455    /// Global: Toggle Options dropdown
456    pub options_menu_toggle: Vec<KeyChord>,
457    /// Global: Toggle Panels dropdown
458    pub panels_menu_toggle: Vec<KeyChord>,
459
460    // Search
461    /// Key chords to move selection up in search results.
462    pub search_move_up: Vec<KeyChord>,
463    /// Key chords to move selection down in search results.
464    pub search_move_down: Vec<KeyChord>,
465    /// Key chords to page up in search results.
466    pub search_page_up: Vec<KeyChord>,
467    /// Key chords to page down in search results.
468    pub search_page_down: Vec<KeyChord>,
469    /// Key chords to add package to install list.
470    pub search_add: Vec<KeyChord>,
471    /// Key chords to install selected package.
472    pub search_install: Vec<KeyChord>,
473    /// Key chords to move focus left from search pane.
474    pub search_focus_left: Vec<KeyChord>,
475    /// Key chords to move focus right from search pane.
476    pub search_focus_right: Vec<KeyChord>,
477    /// Key chords for backspace in search input.
478    pub search_backspace: Vec<KeyChord>,
479    /// Insert mode: clear entire search input (default: Shift+Del)
480    pub search_insert_clear: Vec<KeyChord>,
481
482    // Search normal mode
483    /// Toggle Search normal mode on/off (works from both insert/normal)
484    pub search_normal_toggle: Vec<KeyChord>,
485    /// Enter insert mode while in Search normal mode
486    pub search_normal_insert: Vec<KeyChord>,
487    /// Normal mode: extend selection to the left (default: h)
488    pub search_normal_select_left: Vec<KeyChord>,
489    /// Normal mode: extend selection to the right (default: l)
490    pub search_normal_select_right: Vec<KeyChord>,
491    /// Normal mode: delete selected text (default: d)
492    pub search_normal_delete: Vec<KeyChord>,
493    /// Normal mode: clear entire search input (default: Shift+Del)
494    pub search_normal_clear: Vec<KeyChord>,
495    /// Normal mode: open Arch status page in browser (default: Shift+S)
496    pub search_normal_open_status: Vec<KeyChord>,
497    /// Normal mode: trigger Import packages dialog
498    pub search_normal_import: Vec<KeyChord>,
499    /// Normal mode: trigger Export Install list
500    pub search_normal_export: Vec<KeyChord>,
501    /// Normal mode: open Available Updates window
502    pub search_normal_updates: Vec<KeyChord>,
503    /// Toggle fuzzy search mode on/off
504    pub toggle_fuzzy: Vec<KeyChord>,
505
506    // Recent
507    /// Key chords to move selection up in recent queries.
508    pub recent_move_up: Vec<KeyChord>,
509    /// Key chords to move selection down in recent queries.
510    pub recent_move_down: Vec<KeyChord>,
511    /// Key chords to find/search in recent queries.
512    pub recent_find: Vec<KeyChord>,
513    /// Key chords to use selected recent query.
514    pub recent_use: Vec<KeyChord>,
515    /// Key chords to add package from recent to install list.
516    pub recent_add: Vec<KeyChord>,
517    /// Key chords to move focus from recent to search pane.
518    pub recent_to_search: Vec<KeyChord>,
519    /// Key chords to move focus right from recent pane.
520    pub recent_focus_right: Vec<KeyChord>,
521    /// Remove one entry from Recent
522    pub recent_remove: Vec<KeyChord>,
523    /// Clear all entries in Recent
524    pub recent_clear: Vec<KeyChord>,
525
526    // Install
527    /// Key chords to move selection up in install list.
528    pub install_move_up: Vec<KeyChord>,
529    /// Key chords to move selection down in install list.
530    pub install_move_down: Vec<KeyChord>,
531    /// Key chords to confirm and execute install/remove operation.
532    pub install_confirm: Vec<KeyChord>,
533    /// Key chords to remove item from install list.
534    pub install_remove: Vec<KeyChord>,
535    /// Key chords to clear install list.
536    pub install_clear: Vec<KeyChord>,
537    /// Key chords to find/search in install list.
538    pub install_find: Vec<KeyChord>,
539    /// Key chords to move focus from install to search pane.
540    pub install_to_search: Vec<KeyChord>,
541    /// Key chords to move focus left from install pane.
542    pub install_focus_left: Vec<KeyChord>,
543
544    // News modal
545    /// Mark currently listed News items as read (without opening URL)
546    pub news_mark_read: Vec<KeyChord>,
547    /// Mark all listed News items as read
548    pub news_mark_all_read: Vec<KeyChord>,
549    /// Mark selected News Feed item as read.
550    pub news_mark_read_feed: Vec<KeyChord>,
551    /// Mark selected News Feed item as unread.
552    pub news_mark_unread_feed: Vec<KeyChord>,
553    /// Toggle read/unread for selected News Feed item.
554    pub news_toggle_read_feed: Vec<KeyChord>,
555}
556
557/// Type alias for global key bindings tuple.
558///
559/// Contains 10 `Vec<KeyChord>` for `help_overlay`, `reload_config`, `exit`, `show_pkgbuild`, `comments_toggle`, `run_pkgbuild_checks`, `change_sort`, and pane navigation keys.
560type GlobalKeys = (
561    Vec<KeyChord>,
562    Vec<KeyChord>,
563    Vec<KeyChord>,
564    Vec<KeyChord>,
565    Vec<KeyChord>,
566    Vec<KeyChord>,
567    Vec<KeyChord>,
568    Vec<KeyChord>,
569    Vec<KeyChord>,
570    Vec<KeyChord>,
571);
572
573/// Type alias for search key bindings tuple.
574///
575/// Contains 10 `Vec<KeyChord>` for search navigation, actions, and focus keys.
576type SearchKeys = (
577    Vec<KeyChord>,
578    Vec<KeyChord>,
579    Vec<KeyChord>,
580    Vec<KeyChord>,
581    Vec<KeyChord>,
582    Vec<KeyChord>,
583    Vec<KeyChord>,
584    Vec<KeyChord>,
585    Vec<KeyChord>,
586    Vec<KeyChord>,
587);
588
589/// Type alias for search normal mode key bindings tuple.
590///
591/// Contains 10 `Vec<KeyChord>` for Vim-like normal mode search keys.
592type SearchNormalKeys = (
593    Vec<KeyChord>,
594    Vec<KeyChord>,
595    Vec<KeyChord>,
596    Vec<KeyChord>,
597    Vec<KeyChord>,
598    Vec<KeyChord>,
599    Vec<KeyChord>,
600    Vec<KeyChord>,
601    Vec<KeyChord>,
602    Vec<KeyChord>,
603);
604
605/// Type alias for recent pane key bindings tuple.
606///
607/// Contains 9 `Vec<KeyChord>` for recent pane navigation and action keys.
608type RecentKeys = (
609    Vec<KeyChord>,
610    Vec<KeyChord>,
611    Vec<KeyChord>,
612    Vec<KeyChord>,
613    Vec<KeyChord>,
614    Vec<KeyChord>,
615    Vec<KeyChord>,
616    Vec<KeyChord>,
617    Vec<KeyChord>,
618);
619
620/// Type alias for install list key bindings tuple.
621///
622/// Contains 8 `Vec<KeyChord>` for install list navigation and action keys.
623type InstallKeys = (
624    Vec<KeyChord>,
625    Vec<KeyChord>,
626    Vec<KeyChord>,
627    Vec<KeyChord>,
628    Vec<KeyChord>,
629    Vec<KeyChord>,
630    Vec<KeyChord>,
631    Vec<KeyChord>,
632);
633
634/// What: Create default global key bindings.
635///
636/// Inputs:
637/// - `none`: Empty key modifiers
638/// - `ctrl`: Control modifier
639///
640/// Output:
641/// - Tuple of global key binding vectors
642///
643/// Details:
644/// - Returns `help_overlay`, `reload_config`, `exit`, `show_pkgbuild`, `change_sort`, and pane navigation keys.
645fn default_global_keys(none: KeyModifiers, ctrl: KeyModifiers) -> GlobalKeys {
646    use KeyCode::{BackTab, Char, Left, Right, Tab};
647    (
648        vec![
649            KeyChord {
650                code: KeyCode::F(1),
651                mods: none,
652            },
653            KeyChord {
654                code: Char('?'),
655                mods: none,
656            },
657        ],
658        vec![KeyChord {
659            code: Char('r'),
660            mods: ctrl,
661        }],
662        vec![KeyChord {
663            code: Char('c'),
664            mods: ctrl,
665        }],
666        vec![KeyChord {
667            code: Char('x'),
668            mods: ctrl,
669        }],
670        vec![KeyChord {
671            code: Char('t'),
672            mods: ctrl,
673        }],
674        vec![KeyChord {
675            code: Char('k'),
676            mods: ctrl,
677        }],
678        vec![KeyChord {
679            code: BackTab,
680            mods: none,
681        }],
682        vec![KeyChord {
683            code: Tab,
684            mods: none,
685        }],
686        vec![KeyChord {
687            code: Left,
688            mods: none,
689        }],
690        vec![KeyChord {
691            code: Right,
692            mods: none,
693        }],
694    )
695}
696
697/// What: Create default dropdown toggle key bindings.
698///
699/// Inputs:
700/// - `shift`: Shift modifier
701///
702/// Output:
703/// - Tuple of dropdown toggle key binding vectors
704///
705/// Details:
706/// - Returns `config_menu_toggle`, `options_menu_toggle`, and `panels_menu_toggle` keys.
707fn default_dropdown_keys(shift: KeyModifiers) -> (Vec<KeyChord>, Vec<KeyChord>, Vec<KeyChord>) {
708    use KeyCode::Char;
709    (
710        vec![KeyChord {
711            code: Char('c'),
712            mods: shift,
713        }],
714        vec![KeyChord {
715            code: Char('o'),
716            mods: shift,
717        }],
718        vec![KeyChord {
719            code: Char('p'),
720            mods: shift,
721        }],
722    )
723}
724
725/// What: Create default search key bindings.
726///
727/// Inputs:
728/// - `none`: Empty key modifiers
729///
730/// Output:
731/// - Tuple of search key binding vectors
732///
733/// Details:
734/// - Returns all search-related key bindings for navigation, actions, and focus.
735fn default_search_keys(none: KeyModifiers) -> SearchKeys {
736    use KeyCode::{Backspace, Char, Down, Enter, Left, PageDown, PageUp, Right, Up};
737    (
738        vec![KeyChord {
739            code: Up,
740            mods: none,
741        }],
742        vec![KeyChord {
743            code: Down,
744            mods: none,
745        }],
746        vec![KeyChord {
747            code: PageUp,
748            mods: none,
749        }],
750        vec![KeyChord {
751            code: PageDown,
752            mods: none,
753        }],
754        vec![KeyChord {
755            code: Char(' '),
756            mods: none,
757        }],
758        vec![KeyChord {
759            code: Enter,
760            mods: none,
761        }],
762        vec![KeyChord {
763            code: Left,
764            mods: none,
765        }],
766        vec![KeyChord {
767            code: Right,
768            mods: none,
769        }],
770        vec![KeyChord {
771            code: Backspace,
772            mods: none,
773        }],
774        vec![KeyChord {
775            code: KeyCode::Delete,
776            mods: KeyModifiers::SHIFT,
777        }],
778    )
779}
780
781/// What: Create default search normal mode key bindings.
782///
783/// Inputs:
784/// - `none`: Empty key modifiers
785/// - `shift`: Shift modifier
786///
787/// Output:
788/// - Tuple of search normal mode key binding vectors
789///
790/// Details:
791/// - Returns all Vim-like normal mode key bindings for search.
792fn default_search_normal_keys(none: KeyModifiers, shift: KeyModifiers) -> SearchNormalKeys {
793    use KeyCode::{Char, Delete, Esc};
794    (
795        vec![KeyChord {
796            code: Esc,
797            mods: none,
798        }],
799        vec![KeyChord {
800            code: Char('i'),
801            mods: none,
802        }],
803        vec![KeyChord {
804            code: Char('h'),
805            mods: none,
806        }],
807        vec![KeyChord {
808            code: Char('l'),
809            mods: none,
810        }],
811        vec![KeyChord {
812            code: Char('d'),
813            mods: none,
814        }],
815        vec![KeyChord {
816            code: Delete,
817            mods: shift,
818        }],
819        vec![KeyChord {
820            code: Char('s'),
821            mods: shift,
822        }],
823        vec![KeyChord {
824            code: Char('i'),
825            mods: shift,
826        }],
827        vec![KeyChord {
828            code: Char('e'),
829            mods: shift,
830        }],
831        vec![KeyChord {
832            code: Char('u'),
833            mods: shift,
834        }],
835    )
836}
837
838/// What: Create default recent pane key bindings.
839///
840/// Inputs:
841/// - `none`: Empty key modifiers
842/// - `shift`: Shift modifier
843///
844/// Output:
845/// - Tuple of recent pane key binding vectors
846///
847/// Details:
848/// - Returns all recent pane navigation and action key bindings.
849fn default_recent_keys(none: KeyModifiers, shift: KeyModifiers) -> RecentKeys {
850    use KeyCode::{Char, Delete, Down, Enter, Esc, Right, Up};
851    (
852        vec![
853            KeyChord {
854                code: Char('k'),
855                mods: none,
856            },
857            KeyChord {
858                code: Up,
859                mods: none,
860            },
861        ],
862        vec![
863            KeyChord {
864                code: Char('j'),
865                mods: none,
866            },
867            KeyChord {
868                code: Down,
869                mods: none,
870            },
871        ],
872        vec![KeyChord {
873            code: Char('/'),
874            mods: none,
875        }],
876        vec![KeyChord {
877            code: Enter,
878            mods: none,
879        }],
880        vec![KeyChord {
881            code: Char(' '),
882            mods: none,
883        }],
884        vec![KeyChord {
885            code: Esc,
886            mods: none,
887        }],
888        vec![KeyChord {
889            code: Right,
890            mods: none,
891        }],
892        vec![
893            KeyChord {
894                code: Char('d'),
895                mods: none,
896            },
897            KeyChord {
898                code: Delete,
899                mods: none,
900            },
901        ],
902        vec![KeyChord {
903            code: Delete,
904            mods: shift,
905        }],
906    )
907}
908
909/// What: Create default install list key bindings.
910///
911/// Inputs:
912/// - `none`: Empty key modifiers
913/// - `shift`: Shift modifier
914///
915/// Output:
916/// - Tuple of install list key binding vectors
917///
918/// Details:
919/// - Returns all install list navigation and action key bindings.
920fn default_install_keys(none: KeyModifiers, shift: KeyModifiers) -> InstallKeys {
921    use KeyCode::{Char, Delete, Down, Enter, Esc, Left, Up};
922    (
923        vec![
924            KeyChord {
925                code: Char('k'),
926                mods: none,
927            },
928            KeyChord {
929                code: Up,
930                mods: none,
931            },
932        ],
933        vec![
934            KeyChord {
935                code: Char('j'),
936                mods: none,
937            },
938            KeyChord {
939                code: Down,
940                mods: none,
941            },
942        ],
943        vec![KeyChord {
944            code: Enter,
945            mods: none,
946        }],
947        vec![
948            KeyChord {
949                code: Delete,
950                mods: none,
951            },
952            KeyChord {
953                code: Char('d'),
954                mods: none,
955            },
956        ],
957        vec![KeyChord {
958            code: Delete,
959            mods: shift,
960        }],
961        vec![KeyChord {
962            code: Char('/'),
963            mods: none,
964        }],
965        vec![KeyChord {
966            code: Esc,
967            mods: none,
968        }],
969        vec![KeyChord {
970            code: Left,
971            mods: none,
972        }],
973    )
974}
975
976/// What: Create default news modal key bindings.
977///
978/// Inputs:
979/// - `none`: Empty key modifiers
980/// - `ctrl`: Control modifier
981///
982/// Output:
983/// - Tuple of news modal key binding vectors
984///
985/// Details:
986/// - Returns `news_mark_read` and `news_mark_all_read` key bindings.
987fn default_news_keys(none: KeyModifiers, ctrl: KeyModifiers) -> (Vec<KeyChord>, Vec<KeyChord>) {
988    use KeyCode::Char;
989    (
990        vec![KeyChord {
991            code: Char('r'),
992            mods: none,
993        }],
994        vec![KeyChord {
995            code: Char('r'),
996            mods: ctrl,
997        }],
998    )
999}
1000
1001/// What: Create default News Feed key bindings.
1002///
1003/// Inputs:
1004/// - `none`: Empty key modifiers
1005///
1006/// Output:
1007/// - Tuple of news feed key binding vectors
1008///
1009/// Details:
1010/// - Returns `news_mark_read_feed`, `news_mark_unread_feed`, and `news_toggle_read_feed`.
1011fn default_news_feed_keys(none: KeyModifiers) -> (Vec<KeyChord>, Vec<KeyChord>, Vec<KeyChord>) {
1012    use KeyCode::Char;
1013    (
1014        vec![KeyChord {
1015            code: Char('r'),
1016            mods: none,
1017        }],
1018        vec![KeyChord {
1019            code: Char('u'),
1020            mods: none,
1021        }],
1022        vec![KeyChord {
1023            code: Char('t'),
1024            mods: none,
1025        }],
1026    )
1027}
1028
1029/// What: Build the default `KeyMap` by constructing it from helper functions.
1030///
1031/// Inputs:
1032/// - None (uses internal modifier constants).
1033///
1034/// Output:
1035/// - Returns a fully constructed `KeyMap` with all default key bindings.
1036///
1037/// Details:
1038/// - Consolidates all key binding construction to reduce data flow complexity.
1039/// - All key bindings are constructed inline within the struct initialization.
1040fn build_default_keymap() -> KeyMap {
1041    let none = KeyModifiers::empty();
1042    let ctrl = KeyModifiers::CONTROL;
1043    let shift = KeyModifiers::SHIFT;
1044
1045    let global = default_global_keys(none, ctrl);
1046    let dropdown = default_dropdown_keys(shift);
1047    let search = default_search_keys(none);
1048    let search_normal = default_search_normal_keys(none, shift);
1049    let recent = default_recent_keys(none, shift);
1050    let install = default_install_keys(none, shift);
1051    let news = default_news_keys(none, ctrl);
1052    let news_feed = default_news_feed_keys(none);
1053
1054    KeyMap {
1055        help_overlay: global.0,
1056        reload_config: global.1,
1057        exit: global.2,
1058        show_pkgbuild: global.3,
1059        comments_toggle: global.4,
1060        run_pkgbuild_checks: global.5,
1061        cycle_pkgbuild_sections: vec![KeyChord {
1062            code: KeyCode::Char('d'),
1063            mods: ctrl,
1064        }],
1065        change_sort: global.6,
1066        pane_next: global.7,
1067        pane_left: global.8,
1068        pane_right: global.9,
1069        config_menu_toggle: dropdown.0,
1070        options_menu_toggle: dropdown.1,
1071        panels_menu_toggle: dropdown.2,
1072        search_move_up: search.0,
1073        search_move_down: search.1,
1074        search_page_up: search.2,
1075        search_page_down: search.3,
1076        search_add: search.4,
1077        search_install: search.5,
1078        search_focus_left: search.6,
1079        search_focus_right: search.7,
1080        search_backspace: search.8,
1081        search_insert_clear: search.9,
1082        search_normal_toggle: search_normal.0,
1083        search_normal_insert: search_normal.1,
1084        search_normal_select_left: search_normal.2,
1085        search_normal_select_right: search_normal.3,
1086        search_normal_delete: search_normal.4,
1087        search_normal_clear: search_normal.5,
1088        search_normal_open_status: search_normal.6,
1089        search_normal_import: search_normal.7,
1090        search_normal_export: search_normal.8,
1091        search_normal_updates: search_normal.9,
1092        toggle_fuzzy: vec![KeyChord {
1093            code: KeyCode::Char('f'),
1094            mods: ctrl,
1095        }],
1096        recent_move_up: recent.0,
1097        recent_move_down: recent.1,
1098        recent_find: recent.2,
1099        recent_use: recent.3,
1100        recent_add: recent.4,
1101        recent_to_search: recent.5,
1102        recent_focus_right: recent.6,
1103        recent_remove: recent.7,
1104        recent_clear: recent.8,
1105        install_move_up: install.0,
1106        install_move_down: install.1,
1107        install_confirm: install.2,
1108        install_remove: install.3,
1109        install_clear: install.4,
1110        install_find: install.5,
1111        install_to_search: install.6,
1112        install_focus_left: install.7,
1113        news_mark_read: news.0,
1114        news_mark_all_read: news.1,
1115        news_mark_read_feed: news_feed.0,
1116        news_mark_unread_feed: news_feed.1,
1117        news_toggle_read_feed: news_feed.2,
1118    }
1119}
1120
1121impl Default for KeyMap {
1122    /// What: Supply the default key bindings for Pacsea interactions.
1123    ///
1124    /// Inputs:
1125    /// - None.
1126    ///
1127    /// Output:
1128    /// - Returns a `KeyMap` prefilling chord vectors for global, search, recent, install, and news actions.
1129    ///
1130    /// Details:
1131    /// - Encodes human-friendly defaults such as `F1` for help and `Ctrl+R` to reload the configuration.
1132    /// - Provides multiple bindings for certain actions (e.g., `F1` and `?` for help).
1133    /// - Delegates to `build_default_keymap()` to reduce data flow complexity.
1134    fn default() -> Self {
1135        build_default_keymap()
1136    }
1137}