pacsea/theme/
types.rs

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