Skip to main content

pacsea/state/app_state/
mod.rs

1//! Central `AppState` container, split out from the monolithic module.
2
3use lru::LruCache;
4use ratatui::widgets::ListState;
5use std::{
6    collections::HashMap, collections::HashSet, collections::VecDeque, path::PathBuf, time::Instant,
7};
8
9use crate::sources::VoteAction;
10use crate::state::modal::{
11    CascadeMode, Modal, PreflightAction, RepoOverlapApplyPending, RepositoriesModalResume,
12    ServiceImpact,
13};
14use crate::state::types::{
15    AppMode, ArchStatusColor, Focus, InstalledPackagesMode, NewsFeedItem, NewsReadFilter,
16    NewsSortMode, PackageDetails, PackageItem, RightPaneFocus, SortMode,
17};
18use crate::theme::KeyMap;
19
20mod constants;
21mod default_impl;
22mod defaults;
23mod defaults_cache;
24mod methods;
25
26#[cfg(test)]
27mod tests;
28
29pub use constants::{FileSyncResult, RECENT_CAPACITY, recent_capacity};
30
31/// What: UI-facing live vote-state for an AUR package.
32///
33/// Details:
34/// - `Unknown`: no live check requested yet.
35/// - `Loading`: background check is currently in flight.
36/// - `Voted`: current user has voted for the package.
37/// - `NotVoted`: current user has not voted for the package.
38/// - `Error`: last check failed with a short user-facing reason.
39#[derive(Clone, Debug, PartialEq, Eq)]
40pub enum AurVoteStateUi {
41    /// No vote-state has been requested yet.
42    Unknown,
43    /// Vote-state request is currently running in background.
44    Loading,
45    /// Current user has voted for the package.
46    Voted,
47    /// Current user has not voted for the package.
48    NotVoted,
49    /// Vote-state request failed.
50    Error(String),
51}
52
53/// What: Execution status for PKGBUILD static checks in the preview panel.
54#[derive(Clone, Copy, Debug, PartialEq, Eq)]
55pub enum PkgbuildCheckStatus {
56    /// No checks have been requested yet.
57    Idle,
58    /// Checks are currently running in a background worker.
59    Running,
60    /// Checks completed and data is available for rendering.
61    Complete,
62}
63
64/// What: Supported static checker tool names for PKGBUILD preview checks.
65#[derive(Clone, Copy, Debug, PartialEq, Eq)]
66pub enum PkgbuildCheckTool {
67    /// `shellcheck` output.
68    Shellcheck,
69    /// `namcap` output.
70    Namcap,
71}
72
73/// What: Severity of a parsed PKGBUILD check finding.
74#[derive(Clone, Copy, Debug, PartialEq, Eq)]
75pub enum PkgbuildCheckSeverity {
76    /// High-confidence error that should be fixed.
77    Error,
78    /// Warning that likely needs manual review.
79    Warning,
80    /// Informational note from checker output.
81    Info,
82}
83
84/// What: Parsed finding line for PKGBUILD static check output.
85#[derive(Clone, Debug, PartialEq, Eq)]
86pub struct PkgbuildCheckFinding {
87    /// Tool that produced the finding.
88    pub tool: PkgbuildCheckTool,
89    /// Parsed severity level.
90    pub severity: PkgbuildCheckSeverity,
91    /// Optional line number in PKGBUILD, if parseable.
92    pub line: Option<u32>,
93    /// User-facing message to show in the findings list.
94    pub message: String,
95}
96
97/// What: Raw execution result for an individual PKGBUILD checker tool.
98#[derive(Clone, Debug, PartialEq, Eq)]
99pub struct PkgbuildToolRawResult {
100    /// Tool name.
101    pub tool: PkgbuildCheckTool,
102    /// Whether the tool binary was available on `PATH`.
103    pub available: bool,
104    /// Exit code when executed.
105    pub exit_code: Option<i32>,
106    /// Whether execution timed out.
107    pub timed_out: bool,
108    /// Exact command string executed (or would be executed in dry-run).
109    pub command: String,
110    /// Captured stdout.
111    pub stdout: String,
112    /// Captured stderr.
113    pub stderr: String,
114}
115
116/// Global application state shared by the event, networking, and UI layers.
117///
118/// This structure is mutated frequently in response to input and background
119/// updates. Certain subsets are persisted to disk to preserve user context
120/// across runs (e.g., recent searches, details cache, install list).
121#[derive(Debug)]
122#[allow(clippy::struct_excessive_bools)]
123pub struct AppState {
124    /// Current top-level mode (package management vs news feed).
125    pub app_mode: AppMode,
126    /// Current search input text.
127    pub input: String,
128    /// Current search results, most relevant first.
129    pub results: Vec<PackageItem>,
130    /// Unfiltered results as last received from the search worker.
131    pub all_results: Vec<PackageItem>,
132    /// Backup of results when toggling to installed-only view.
133    pub results_backup_for_toggle: Option<Vec<PackageItem>>,
134    /// Index into `results` that is currently highlighted.
135    pub selected: usize,
136    /// Details for the currently highlighted result.
137    pub details: PackageDetails,
138    /// List selection state for the search results list.
139    pub list_state: ListState,
140    /// Active modal dialog, if any.
141    pub modal: Modal,
142    /// Previous modal state (used to restore when closing help/alert modals).
143    pub previous_modal: Option<Modal>,
144    /// If `true`, show install steps without executing side effects.
145    pub dry_run: bool,
146    // Recent searches
147    /// Previously executed queries stored as an LRU cache (keyed case-insensitively).
148    pub recent: LruCache<String, String>,
149    /// List selection state for the Recent pane.
150    pub history_state: ListState,
151    /// Which pane is currently focused.
152    pub focus: Focus,
153    /// Timestamp of the last input edit, used for debouncing or throttling.
154    pub last_input_change: Instant,
155    /// Last value persisted for the input field, to avoid redundant writes.
156    pub last_saved_value: Option<String>,
157    // Persisted recent searches
158    /// Path where recent searches are persisted as JSON.
159    pub recent_path: PathBuf,
160    /// Dirty flag indicating `recent` needs to be saved.
161    pub recent_dirty: bool,
162
163    // Search coordination
164    /// Identifier of the latest query whose results are being displayed.
165    pub latest_query_id: u64,
166    /// Next query identifier to allocate.
167    pub next_query_id: u64,
168    // Search result cache
169    /// Cached search query text (None if cache is empty or invalid).
170    pub search_cache_query: Option<String>,
171    /// Whether fuzzy mode was used for cached query.
172    pub search_cache_fuzzy: bool,
173    /// Cached search results (None if cache is empty or invalid).
174    pub search_cache_results: Option<Vec<PackageItem>>,
175    // Details cache
176    /// Cache of details keyed by package name.
177    pub details_cache: HashMap<String, PackageDetails>,
178    /// Path where the details cache is persisted as JSON.
179    pub cache_path: PathBuf,
180    /// Dirty flag indicating `details_cache` needs to be saved.
181    pub cache_dirty: bool,
182
183    // News read/unread tracking (persisted)
184    /// Set of Arch news item URLs the user has marked as read.
185    pub news_read_urls: std::collections::HashSet<String>,
186    /// Path where the read news URLs are persisted as JSON.
187    pub news_read_path: PathBuf,
188    /// Dirty flag indicating `news_read_urls` needs to be saved.
189    pub news_read_dirty: bool,
190    /// Set of news feed item IDs the user has marked as read.
191    pub news_read_ids: std::collections::HashSet<String>,
192    /// Path where the read news IDs are persisted as JSON.
193    pub news_read_ids_path: PathBuf,
194    /// Dirty flag indicating `news_read_ids` needs to be saved.
195    pub news_read_ids_dirty: bool,
196    /// News feed items currently loaded.
197    pub news_items: Vec<NewsFeedItem>,
198    /// Filtered/sorted news results shown in the UI.
199    pub news_results: Vec<NewsFeedItem>,
200    /// Whether the news feed is currently loading.
201    pub news_loading: bool,
202    /// Whether news are ready to be viewed (loading complete and news available).
203    pub news_ready: bool,
204    /// Selected index within news results.
205    pub news_selected: usize,
206    /// List state for news results pane.
207    pub news_list_state: ListState,
208    /// News search input text.
209    pub news_search_input: String,
210    /// Caret position within news search input.
211    pub news_search_caret: usize,
212    /// Selection anchor within news search input.
213    pub news_search_select_anchor: Option<usize>,
214    /// LRU cache of recent news searches (case-insensitive key).
215    pub news_recent: LruCache<String, String>,
216    /// Path where news recent searches are persisted.
217    pub news_recent_path: PathBuf,
218    /// Dirty flag indicating `news_recent` needs to be saved.
219    pub news_recent_dirty: bool,
220    /// Pending news search awaiting debounce before saving to history.
221    pub news_history_pending: Option<String>,
222    /// Timestamp when the pending news search was last updated.
223    pub news_history_pending_at: Option<std::time::Instant>,
224    /// Last news search saved to history (prevents duplicate saves).
225    pub news_history_last_saved: Option<String>,
226    /// Whether to show Arch news items.
227    pub news_filter_show_arch_news: bool,
228    /// Whether to show security advisories.
229    pub news_filter_show_advisories: bool,
230    /// Whether to show installed package update items.
231    pub news_filter_show_pkg_updates: bool,
232    /// Whether to show AUR package update items.
233    pub news_filter_show_aur_updates: bool,
234    /// Whether to show AUR comment items.
235    pub news_filter_show_aur_comments: bool,
236    /// Whether to restrict advisories to installed packages.
237    pub news_filter_installed_only: bool,
238    /// Read/unread filter for the News Feed list.
239    pub news_filter_read_status: NewsReadFilter,
240    /// Clickable rectangle for Arch news filter chip in news title.
241    pub news_filter_arch_rect: Option<(u16, u16, u16, u16)>,
242    /// Clickable rectangle for security advisory filter chip in news title.
243    pub news_filter_advisory_rect: Option<(u16, u16, u16, u16)>,
244    /// Clickable rectangle for installed-only advisory filter chip in news title.
245    pub news_filter_installed_rect: Option<(u16, u16, u16, u16)>,
246    /// Clickable rectangle for installed update filter chip in news title.
247    pub news_filter_updates_rect: Option<(u16, u16, u16, u16)>,
248    /// Clickable rectangle for AUR update filter chip in news title.
249    pub news_filter_aur_updates_rect: Option<(u16, u16, u16, u16)>,
250    /// Clickable rectangle for AUR comment filter chip in news title.
251    pub news_filter_aur_comments_rect: Option<(u16, u16, u16, u16)>,
252    /// Clickable rectangle for read/unread filter chip in news title.
253    pub news_filter_read_rect: Option<(u16, u16, u16, u16)>,
254    /// Maximum age of news items in days (None = unlimited).
255    pub news_max_age_days: Option<u32>,
256    /// Whether to show the news history pane in News mode.
257    pub show_news_history_pane: bool,
258    /// Whether to show the news bookmarks pane in News mode.
259    pub show_news_bookmarks_pane: bool,
260    /// Sort mode for news results.
261    pub news_sort_mode: NewsSortMode,
262    /// Saved news/bookmarked items with cached content.
263    pub news_bookmarks: Vec<crate::state::types::NewsBookmark>,
264    /// Path where news bookmarks are persisted.
265    pub news_bookmarks_path: PathBuf,
266    /// Dirty flag indicating `news_bookmarks` needs to be saved.
267    pub news_bookmarks_dirty: bool,
268    /// Cache of fetched news article content (URL -> content).
269    pub news_content_cache: std::collections::HashMap<String, String>,
270    /// Path where the news content cache is persisted.
271    pub news_content_cache_path: PathBuf,
272    /// Dirty flag indicating `news_content_cache` needs to be saved.
273    pub news_content_cache_dirty: bool,
274    /// Currently displayed news content (for the selected item).
275    pub news_content: Option<String>,
276    /// Whether news content is currently being fetched.
277    pub news_content_loading: bool,
278    /// When the current news content load started (for timeout/logging).
279    pub news_content_loading_since: Option<std::time::Instant>,
280    /// Debounce timer for news content requests - tracks when user selected current item.
281    /// Only requests content after 0.5 seconds of staying on the same item.
282    pub news_content_debounce_timer: Option<std::time::Instant>,
283    /// Scroll offset for news content details.
284    pub news_content_scroll: u16,
285    /// Path where the cached news feed is persisted.
286    pub news_feed_path: PathBuf,
287    /// Last-seen versions for installed packages (dedup for update feed items).
288    pub news_seen_pkg_versions: HashMap<String, String>,
289    /// Path where last-seen package versions are persisted.
290    pub news_seen_pkg_versions_path: PathBuf,
291    /// Dirty flag indicating `news_seen_pkg_versions` needs to be saved.
292    pub news_seen_pkg_versions_dirty: bool,
293    /// Last-seen AUR comment identifiers per installed package.
294    pub news_seen_aur_comments: HashMap<String, String>,
295    /// Path where last-seen AUR comments are persisted.
296    pub news_seen_aur_comments_path: PathBuf,
297    /// Dirty flag indicating `news_seen_aur_comments` needs to be saved.
298    pub news_seen_aur_comments_dirty: bool,
299
300    // Announcement read tracking (persisted)
301    /// Set of announcement IDs the user has marked as read.
302    /// Tracks both version strings (e.g., "v0.6.0") and remote announcement IDs.
303    pub announcements_read_ids: std::collections::HashSet<String>,
304    /// Path where the read announcement IDs are persisted as JSON.
305    pub announcement_read_path: PathBuf,
306    /// Dirty flag indicating `announcements_read_ids` needs to be saved.
307    pub announcement_dirty: bool,
308
309    // Last startup tracking (for incremental updates)
310    /// Timestamp of the previous TUI startup (format: `YYYYMMDD:HHMMSS`).
311    /// Used to determine what news/updates need fresh fetching vs cached data.
312    pub last_startup_timestamp: Option<String>,
313    /// Path where the last startup timestamp is persisted.
314    pub last_startup_path: PathBuf,
315
316    // Install list pane
317    /// Packages selected for installation.
318    pub install_list: Vec<PackageItem>,
319    /// List selection state for the Install pane.
320    pub install_state: ListState,
321    /// Separate list of packages selected for removal (active in installed-only mode).
322    pub remove_list: Vec<PackageItem>,
323    /// List selection state for the Remove pane.
324    pub remove_state: ListState,
325    /// Separate list of packages selected for downgrade (shown in installed-only mode).
326    pub downgrade_list: Vec<PackageItem>,
327    /// List selection state for the Downgrade pane.
328    pub downgrade_state: ListState,
329    // Persisted install list
330    /// Path where the install list is persisted as JSON.
331    pub install_path: PathBuf,
332    /// Dirty flag indicating `install_list` needs to be saved.
333    pub install_dirty: bool,
334    /// Timestamp of the most recent change to the install list for throttling disk writes.
335    pub last_install_change: Option<Instant>,
336    /// `HashSet` of package names in install list for O(1) membership checking.
337    pub install_list_names: HashSet<String>,
338    /// `HashSet` of package names in remove list for O(1) membership checking.
339    pub remove_list_names: HashSet<String>,
340    /// `HashSet` of package names in downgrade list for O(1) membership checking.
341    pub downgrade_list_names: HashSet<String>,
342
343    // Visibility toggles for middle row panes
344    /// Whether the Recent pane is visible in the middle row.
345    pub show_recent_pane: bool,
346    /// Whether the Install/Remove pane is visible in the middle row.
347    pub show_install_pane: bool,
348    /// Whether to show the keybindings footer in the details pane.
349    pub show_keybinds_footer: bool,
350
351    // In-pane search (for Recent/Install panes)
352    /// Optional, transient find pattern used by pane-local search ("/").
353    pub pane_find: Option<String>,
354
355    /// Whether Search pane is in Normal mode (Vim-like navigation) instead of Insert mode.
356    pub search_normal_mode: bool,
357
358    /// Whether fuzzy search is enabled (fzf-style matching) instead of normal substring search.
359    pub fuzzy_search_enabled: bool,
360
361    /// Caret position (in characters) within the `Search` input.
362    /// Always clamped to the range 0..=`input.chars().count()`.
363    pub search_caret: usize,
364    /// Selection anchor (in characters) for the Search input when selecting text.
365    /// When `None`, no selection is active. When `Some(i)`, the selected range is
366    /// between `min(i, search_caret)` and `max(i, search_caret)` (exclusive upper bound).
367    pub search_select_anchor: Option<usize>,
368
369    // Official package index persistence
370    /// Path to the persisted official package index used for fast offline lookups.
371    pub official_index_path: PathBuf,
372
373    // Loading indicator for official index generation
374    /// Whether the application is currently generating the official index.
375    pub loading_index: bool,
376
377    // Track which package's details the UI is focused on
378    /// Name of the package whose details are being emphasized in the UI, if any.
379    pub details_focus: Option<String>,
380
381    // Ring prefetch debounce state
382    /// Smooth scrolling accumulator for prefetch heuristics.
383    pub scroll_moves: u32,
384    /// Timestamp at which to resume ring prefetching, if paused.
385    pub ring_resume_at: Option<Instant>,
386    /// Whether a ring prefetch is needed soon.
387    pub need_ring_prefetch: bool,
388
389    // Clickable URL button rectangle (x, y, w, h) in terminal cells
390    /// Rectangle of the clickable URL button in terminal cell coordinates.
391    pub url_button_rect: Option<(u16, u16, u16, u16)>,
392
393    // VirusTotal API setup modal clickable URL rectangle
394    /// Rectangle of the clickable `VirusTotal` API URL in the setup modal (x, y, w, h).
395    pub vt_url_rect: Option<(u16, u16, u16, u16)>,
396
397    // Install pane bottom action (Import)
398    /// Clickable rectangle for the Install pane bottom "Import" button (x, y, w, h).
399    pub install_import_rect: Option<(u16, u16, u16, u16)>,
400    /// Clickable rectangle for the Install pane bottom "Export" button (x, y, w, h).
401    pub install_export_rect: Option<(u16, u16, u16, u16)>,
402
403    // Arch status label (middle row footer)
404    /// Latest fetched status message from `status.archlinux.org`.
405    pub arch_status_text: String,
406    /// Clickable rectangle for the status label (x, y, w, h).
407    pub arch_status_rect: Option<(u16, u16, u16, u16)>,
408    /// Optional status color indicator (e.g., operational vs. current incident).
409    pub arch_status_color: ArchStatusColor,
410
411    // Package updates available
412    /// Number of available package updates, if checked.
413    pub updates_count: Option<usize>,
414    /// Sorted list of package names with available updates.
415    pub updates_list: Vec<String>,
416    /// Clickable rectangle for the updates button (x, y, w, h).
417    pub updates_button_rect: Option<(u16, u16, u16, u16)>,
418    /// Clickable rectangle for the news button in News mode (x, y, w, h).
419    pub news_button_rect: Option<(u16, u16, u16, u16)>,
420    /// Whether updates check is currently in progress.
421    pub updates_loading: bool,
422    /// Whether the last completed update check used an authoritative official-repo source (`None` before first result).
423    pub updates_last_check_authoritative: Option<bool>,
424    /// Flag to trigger refresh of updates list after package installation/update.
425    pub refresh_updates: bool,
426    /// Flag to indicate that Updates modal should open after refresh completes.
427    pub pending_updates_modal: bool,
428
429    // Faillock lockout status
430    /// Whether the user account is currently locked out.
431    pub faillock_locked: bool,
432    /// Timestamp when the lockout will expire (if locked).
433    pub faillock_lockout_until: Option<std::time::SystemTime>,
434    /// Remaining lockout time in minutes (if locked).
435    pub faillock_remaining_minutes: Option<u32>,
436
437    // Clickable PKGBUILD button rectangle and viewer state
438    /// Rectangle of the clickable "Show PKGBUILD" in terminal cell coordinates.
439    pub pkgb_button_rect: Option<(u16, u16, u16, u16)>,
440    /// Rectangle of the clickable "Copy PKGBUILD" button in PKGBUILD title.
441    pub pkgb_check_button_rect: Option<(u16, u16, u16, u16)>,
442    /// Rectangle of the clickable "Reload PKGBUILD" button in PKGBUILD title.
443    pub pkgb_reload_button_rect: Option<(u16, u16, u16, u16)>,
444    /// Whether the PKGBUILD viewer is visible (details pane split in half).
445    pub pkgb_visible: bool,
446    /// The fetched PKGBUILD text when available.
447    pub pkgb_text: Option<String>,
448    /// Name of the package that the PKGBUILD is currently for.
449    pub pkgb_package_name: Option<String>,
450    /// Timestamp when PKGBUILD reload was last requested (for debouncing).
451    pub pkgb_reload_requested_at: Option<Instant>,
452    /// Name of the package for which PKGBUILD reload was requested (for debouncing).
453    pub pkgb_reload_requested_for: Option<String>,
454    /// Scroll offset (lines) for the PKGBUILD viewer.
455    pub pkgb_scroll: u16,
456    /// Active subsection for `Ctrl+D` rotation: 0 = PKGBUILD body, 1 = `ShellCheck`, 2 = `Namcap`.
457    pub pkgb_section_cycle: u8,
458    /// Content rectangle of the PKGBUILD viewer (x, y, w, h) when visible.
459    pub pkgb_rect: Option<(u16, u16, u16, u16)>,
460    /// Rectangle of the clickable "Run checks" button in PKGBUILD title.
461    pub pkgb_run_checks_button_rect: Option<(u16, u16, u16, u16)>,
462    /// Current status of PKGBUILD checks in preview panel.
463    pub pkgb_check_status: PkgbuildCheckStatus,
464    /// Parsed findings from latest PKGBUILD check run.
465    pub pkgb_check_findings: Vec<PkgbuildCheckFinding>,
466    /// Raw per-tool outputs from latest PKGBUILD check run.
467    pub pkgb_check_raw_results: Vec<PkgbuildToolRawResult>,
468    /// Missing tool hints shown when ShellCheck/namcap are unavailable.
469    pub pkgb_check_missing_tools: Vec<String>,
470    /// Whether raw output panel is expanded in PKGBUILD preview.
471    pub pkgb_check_show_raw_output: bool,
472    /// Scroll offset for parsed findings list.
473    pub pkgb_check_scroll: u16,
474    /// Scroll offset for raw output panel.
475    pub pkgb_check_raw_scroll: u16,
476    /// Last package name for which checks were run.
477    pub pkgb_check_last_package_name: Option<String>,
478    /// Last completion timestamp for checks.
479    pub pkgb_check_last_run_at: Option<Instant>,
480    /// Last error text for check execution path.
481    pub pkgb_check_last_error: Option<String>,
482
483    // AUR comments viewer state
484    /// Rectangle of the clickable "Show comments" / "Hide comments" button in terminal cell coordinates.
485    pub comments_button_rect: Option<(u16, u16, u16, u16)>,
486    /// Whether the comments viewer is visible (details pane split).
487    pub comments_visible: bool,
488    /// The fetched comments data when available.
489    pub comments: Vec<crate::state::types::AurComment>,
490    /// Name of the package that the comments are currently for.
491    pub comments_package_name: Option<String>,
492    /// Timestamp when comments were last fetched (for cache invalidation).
493    pub comments_fetched_at: Option<Instant>,
494    /// Scroll offset (lines) for the comments viewer.
495    pub comments_scroll: u16,
496    /// Content rectangle of the comments viewer (x, y, w, h) when visible.
497    pub comments_rect: Option<(u16, u16, u16, u16)>,
498    /// Whether comments are currently being fetched.
499    pub comments_loading: bool,
500    /// Error message if comments fetch failed.
501    pub comments_error: Option<String>,
502    /// URLs in comments with their screen positions for click detection.
503    /// Vector of (`x`, `y`, `width`, `url_string`) tuples.
504    pub comments_urls: Vec<(u16, u16, u16, String)>,
505    /// Author names in comments with their screen positions for click detection.
506    /// Vector of (`x`, `y`, `width`, `username`) tuples.
507    pub comments_authors: Vec<(u16, u16, u16, String)>,
508    /// Dates in comments with their screen positions and URLs for click detection.
509    /// Vector of (`x`, `y`, `width`, `url_string`) tuples.
510    pub comments_dates: Vec<(u16, u16, u16, String)>,
511
512    // Transient toast message (bottom-right)
513    /// Optional short-lived info message rendered at the bottom-right corner.
514    pub toast_message: Option<String>,
515    /// Deadline (Instant) after which the toast is automatically hidden.
516    pub toast_expires_at: Option<Instant>,
517
518    // User settings loaded at startup
519    /// Left pane width percentage.
520    pub layout_left_pct: u16,
521    /// Center pane width percentage.
522    pub layout_center_pct: u16,
523    /// Right pane width percentage.
524    pub layout_right_pct: u16,
525    /// Top-to-bottom order of the main vertical stack (results, middle, package info).
526    pub main_pane_order: [crate::state::MainVerticalPane; 3],
527    /// Min/max row counts for vertical layout (semantic per pane, not screen slot).
528    pub vertical_layout_limits: crate::state::VerticalLayoutLimits,
529    /// Resolved key bindings from user settings
530    pub keymap: KeyMap,
531    // Internationalization (i18n)
532    /// Resolved locale code (e.g., "de-DE", "en-US")
533    pub locale: String,
534    /// Translation map for the current locale
535    pub translations: crate::i18n::translations::TranslationMap,
536    /// Fallback translation map (English) for missing keys
537    pub translations_fallback: crate::i18n::translations::TranslationMap,
538
539    // Mouse hit-test rectangles for panes
540    /// Inner content rectangle of the Results list (x, y, w, h).
541    pub results_rect: Option<(u16, u16, u16, u16)>,
542    /// Inner content rectangle of the Package Info details pane (x, y, w, h).
543    pub details_rect: Option<(u16, u16, u16, u16)>,
544    /// Scroll offset (lines) for the Package Info details pane.
545    pub details_scroll: u16,
546    /// Inner content rectangle of the Recent pane list (x, y, w, h).
547    pub recent_rect: Option<(u16, u16, u16, u16)>,
548    /// Inner content rectangle of the Install pane list (x, y, w, h).
549    pub install_rect: Option<(u16, u16, u16, u16)>,
550    /// Inner content rectangle of the Downgrade subpane when visible.
551    pub downgrade_rect: Option<(u16, u16, u16, u16)>,
552    /// Whether mouse capture is temporarily disabled to allow text selection in details.
553    pub mouse_disabled_in_details: bool,
554    /// Last observed mouse position (column, row) in terminal cells.
555    pub last_mouse_pos: Option<(u16, u16)>,
556    /// Whether global terminal mouse capture is currently enabled.
557    pub mouse_capture_enabled: bool,
558
559    // News modal mouse hit-testing
560    /// Outer rectangle of the News modal (including borders) when visible.
561    pub news_rect: Option<(u16, u16, u16, u16)>,
562    /// Inner list rectangle for clickable news rows.
563    pub news_list_rect: Option<(u16, u16, u16, u16)>,
564
565    // Announcement modal mouse hit-testing
566    /// Outer rectangle of the Announcement modal (including borders) when visible.
567    pub announcement_rect: Option<(u16, u16, u16, u16)>,
568    /// URLs in announcement content with their screen positions for click detection.
569    /// Vector of (`x`, `y`, `width`, `url_string`) tuples.
570    pub announcement_urls: Vec<(u16, u16, u16, String)>,
571    /// Pending remote announcements to show after current announcement is dismissed.
572    pub pending_announcements: Vec<crate::announcements::RemoteAnnouncement>,
573    /// Pending news to show after all announcements are dismissed.
574    pub pending_news: Option<Vec<crate::state::NewsItem>>,
575    /// Startup setup steps queued from first-run setup selector.
576    pub pending_startup_setup_steps: VecDeque<crate::state::modal::StartupSetupTask>,
577    /// Flag to trigger startup news fetch after `NewsSetup` is completed.
578    pub trigger_startup_news_fetch: bool,
579    /// Session-scoped latch to avoid repeatedly showing long-run auth preflight warning text.
580    pub long_run_auth_preflight_warned: bool,
581
582    // Updates modal mouse hit-testing
583    /// Outer rectangle of the Updates modal (including borders) when visible.
584    pub updates_modal_rect: Option<(u16, u16, u16, u16)>,
585    /// Clickable rectangle for the `Wizard` button in the Optional Deps modal.
586    pub optional_deps_wizard_rect: Option<(u16, u16, u16, u16)>,
587    /// Outer rectangle of the Optional Deps modal for wheel hit-testing.
588    pub optional_deps_modal_rect: Option<(u16, u16, u16, u16)>,
589    /// Outer rectangle of the System Update modal for wheel hit-testing.
590    pub system_update_modal_rect: Option<(u16, u16, u16, u16)>,
591    /// Outer rectangle of the Repositories modal for wheel hit-testing.
592    pub repositories_modal_rect: Option<(u16, u16, u16, u16)>,
593    /// Clickable row for copying the SSH public key in the AUR SSH setup modal.
594    pub ssh_setup_copy_key_rect: Option<(u16, u16, u16, u16)>,
595    /// Inner content rectangle for scrollable updates list.
596    pub updates_modal_content_rect: Option<(u16, u16, u16, u16)>,
597    /// Per-entry starting rendered line indices for the updates modal content.
598    ///
599    /// Each value maps an entry index to its first rendered line in the wrapped pane output.
600    pub updates_modal_entry_line_starts: Vec<u16>,
601    /// Total rendered line count across all updates entries after wrapping.
602    pub updates_modal_total_lines: u16,
603    /// Timestamp when `g` was pressed in Updates modal awaiting chord completion.
604    pub updates_modal_pending_g_at: Option<Instant>,
605
606    // Help modal scroll and hit-testing
607    /// Scroll offset (lines) for the Help modal content.
608    pub help_scroll: u16,
609    /// Inner content rectangle of the Help modal (x, y, w, h) for hit-testing.
610    pub help_rect: Option<(u16, u16, u16, u16)>,
611
612    // Preflight modal mouse hit-testing
613    /// Clickable rectangles for preflight tabs (x, y, w, h) - Summary, Deps, Files, Services, Sandbox.
614    pub preflight_tab_rects: [Option<(u16, u16, u16, u16)>; 5],
615    /// Inner content rectangle of the preflight modal (x, y, w, h) for hit-testing package groups.
616    pub preflight_content_rect: Option<(u16, u16, u16, u16)>,
617
618    // Results sorting UI
619    /// Current sort mode for results.
620    pub sort_mode: SortMode,
621    /// Filter mode for installed packages (leaf only vs all explicit).
622    pub installed_packages_mode: InstalledPackagesMode,
623    /// Whether the sort dropdown is currently visible.
624    pub sort_menu_open: bool,
625    /// Clickable rectangle for the sort button in the Results title (x, y, w, h).
626    pub sort_button_rect: Option<(u16, u16, u16, u16)>,
627    /// Clickable rectangle for the news age toggle button (x, y, w, h).
628    pub news_age_button_rect: Option<(u16, u16, u16, u16)>,
629    /// Inner content rectangle of the sort dropdown menu when visible (x, y, w, h).
630    pub sort_menu_rect: Option<(u16, u16, u16, u16)>,
631    /// Deadline after which the sort dropdown auto-closes.
632    pub sort_menu_auto_close_at: Option<Instant>,
633    // Sort result caching for O(1) sort mode switching
634    /// Cached sort order for `RepoThenName` mode (indices into `results`).
635    pub sort_cache_repo_name: Option<Vec<usize>>,
636    /// Cached sort order for `AurPopularityThenOfficial` mode (indices into `results`).
637    pub sort_cache_aur_popularity: Option<Vec<usize>>,
638    /// Signature of results used to validate caches (order-insensitive hash of names).
639    pub sort_cache_signature: Option<u64>,
640
641    // Results options UI (top-right dropdown)
642    /// Whether the options dropdown is currently visible.
643    pub options_menu_open: bool,
644    /// Clickable rectangle for the options button in the Results title (x, y, w, h).
645    pub options_button_rect: Option<(u16, u16, u16, u16)>,
646    /// Inner content rectangle of the options dropdown menu when visible (x, y, w, h).
647    pub options_menu_rect: Option<(u16, u16, u16, u16)>,
648
649    // Panels dropdown UI (left of Options)
650    /// Whether the panels dropdown is currently visible.
651    pub panels_menu_open: bool,
652    /// Clickable rectangle for the panels button in the Results title (x, y, w, h).
653    pub panels_button_rect: Option<(u16, u16, u16, u16)>,
654    /// Inner content rectangle of the panels dropdown menu when visible (x, y, w, h).
655    pub panels_menu_rect: Option<(u16, u16, u16, u16)>,
656
657    // Config/Lists dropdown UI (left of Panels)
658    /// Whether the Config/Lists dropdown is currently visible.
659    pub config_menu_open: bool,
660    /// Clickable rectangle for the Config/Lists button in the Results title (x, y, w, h).
661    pub config_button_rect: Option<(u16, u16, u16, u16)>,
662    /// Inner content rectangle of the Config/Lists dropdown menu when visible (x, y, w, h).
663    pub config_menu_rect: Option<(u16, u16, u16, u16)>,
664
665    // Artix filter dropdown UI (when specific repo filters are hidden)
666    /// Whether the Artix filter dropdown is currently visible.
667    pub artix_filter_menu_open: bool,
668    /// Inner content rectangle of the Artix filter dropdown menu when visible (x, y, w, h).
669    pub artix_filter_menu_rect: Option<(u16, u16, u16, u16)>,
670
671    /// Whether the custom `repos.conf` results-filter dropdown is visible.
672    pub custom_repos_filter_menu_open: bool,
673    /// Inner hit-test rect for the custom repos filter dropdown when visible.
674    pub custom_repos_filter_menu_rect: Option<(u16, u16, u16, u16)>,
675
676    // Collapsed menu dropdown UI (when window is too narrow for all three buttons)
677    /// Whether the collapsed menu dropdown is currently visible.
678    pub collapsed_menu_open: bool,
679    /// Clickable rectangle for the collapsed menu button in the Results title (x, y, w, h).
680    pub collapsed_menu_button_rect: Option<(u16, u16, u16, u16)>,
681    /// Inner content rectangle of the collapsed menu dropdown when visible (x, y, w, h).
682    pub collapsed_menu_rect: Option<(u16, u16, u16, u16)>,
683
684    /// Whether Results is currently showing only explicitly installed packages.
685    pub installed_only_mode: bool,
686    /// Which right subpane is focused when installed-only mode splits the pane.
687    pub right_pane_focus: RightPaneFocus,
688    /// Visual marker style for packages added to lists (user preference cached at startup).
689    pub package_marker: crate::theme::PackageMarker,
690
691    // Results filters UI
692    /// Whether to include AUR packages in the Results view.
693    pub results_filter_show_aur: bool,
694    /// Whether to include packages from the `core` repo in the Results view.
695    pub results_filter_show_core: bool,
696    /// Whether to include packages from the `extra` repo in the Results view.
697    pub results_filter_show_extra: bool,
698    /// Whether to include packages from the `multilib` repo in the Results view.
699    pub results_filter_show_multilib: bool,
700    /// Whether to include packages from the `eos` repo in the Results view.
701    pub results_filter_show_eos: bool,
702    /// Whether to include packages from `cachyos*` repos in the Results view.
703    pub results_filter_show_cachyos: bool,
704    /// Whether to include packages from Artix Linux repos in the Results view.
705    pub results_filter_show_artix: bool,
706    /// Whether to include packages from Artix omniverse repo in the Results view.
707    pub results_filter_show_artix_omniverse: bool,
708    /// Whether to include packages from Artix universe repo in the Results view.
709    pub results_filter_show_artix_universe: bool,
710    /// Whether to include packages from Artix lib32 repo in the Results view.
711    pub results_filter_show_artix_lib32: bool,
712    /// Whether to include packages from Artix galaxy repo in the Results view.
713    pub results_filter_show_artix_galaxy: bool,
714    /// Whether to include packages from Artix world repo in the Results view.
715    pub results_filter_show_artix_world: bool,
716    /// Whether to include packages from Artix system repo in the Results view.
717    pub results_filter_show_artix_system: bool,
718    /// Whether to include packages from the `blackarch` repo in the Results view.
719    pub results_filter_show_blackarch: bool,
720    /// Whether to include packages labeled as `manjaro` in the Results view.
721    pub results_filter_show_manjaro: bool,
722    /// Lowercase pacman `[repo]` name → canonical `results_filter` id from `repos.conf`.
723    pub repo_results_filter_by_name: HashMap<String, String>,
724    /// Per dynamic filter id (canonical), whether search results include packages from mapped repos.
725    pub results_filter_dynamic: HashMap<String, bool>,
726    /// Clickable rectangle for the AUR filter toggle in the Results title (x, y, w, h).
727    pub results_filter_aur_rect: Option<(u16, u16, u16, u16)>,
728    /// Clickable rectangle for the core filter toggle in the Results title (x, y, w, h).
729    pub results_filter_core_rect: Option<(u16, u16, u16, u16)>,
730    /// Clickable rectangle for the extra filter toggle in the Results title (x, y, w, h).
731    pub results_filter_extra_rect: Option<(u16, u16, u16, u16)>,
732    /// Clickable rectangle for the multilib filter toggle in the Results title (x, y, w, h).
733    pub results_filter_multilib_rect: Option<(u16, u16, u16, u16)>,
734    /// Clickable rectangle for the EOS filter toggle in the Results title (x, y, w, h).
735    pub results_filter_eos_rect: Option<(u16, u16, u16, u16)>,
736    /// Clickable rectangle for the `CachyOS` filter toggle in the Results title (x, y, w, h).
737    pub results_filter_cachyos_rect: Option<(u16, u16, u16, u16)>,
738    /// Clickable rectangle for the Artix filter toggle in the Results title (x, y, w, h).
739    pub results_filter_artix_rect: Option<(u16, u16, u16, u16)>,
740    /// Clickable rectangle for the Artix omniverse filter toggle in the Results title (x, y, w, h).
741    pub results_filter_artix_omniverse_rect: Option<(u16, u16, u16, u16)>,
742    /// Clickable rectangle for the Artix universe filter toggle in the Results title (x, y, w, h).
743    pub results_filter_artix_universe_rect: Option<(u16, u16, u16, u16)>,
744    /// Clickable rectangle for the Artix lib32 filter toggle in the Results title (x, y, w, h).
745    pub results_filter_artix_lib32_rect: Option<(u16, u16, u16, u16)>,
746    /// Clickable rectangle for the Artix galaxy filter toggle in the Results title (x, y, w, h).
747    pub results_filter_artix_galaxy_rect: Option<(u16, u16, u16, u16)>,
748    /// Clickable rectangle for the Artix world filter toggle in the Results title (x, y, w, h).
749    pub results_filter_artix_world_rect: Option<(u16, u16, u16, u16)>,
750    /// Clickable rectangle for the Artix system filter toggle in the Results title (x, y, w, h).
751    pub results_filter_artix_system_rect: Option<(u16, u16, u16, u16)>,
752    /// Clickable rectangle for the `BlackArch` filter toggle in the Results title (x, y, w, h).
753    pub results_filter_blackarch_rect: Option<(u16, u16, u16, u16)>,
754    /// Clickable rectangle for the Manjaro filter toggle in the Results title (x, y, w, h).
755    pub results_filter_manjaro_rect: Option<(u16, u16, u16, u16)>,
756    /// Clickable rectangle for the custom `repos.conf` filter dropdown chip (x, y, w, h).
757    pub results_filter_custom_repos_rect: Option<(u16, u16, u16, u16)>,
758    /// Clickable rectangle for the fuzzy search mode indicator in the Search title (x, y, w, h).
759    pub fuzzy_indicator_rect: Option<(u16, u16, u16, u16)>,
760
761    // Background refresh of installed/explicit caches after package mutations
762    /// If `Some`, keep polling pacman/yay to refresh installed/explicit caches until this time.
763    pub refresh_installed_until: Option<Instant>,
764    /// Next scheduled time to poll caches while `refresh_installed_until` is active.
765    pub next_installed_refresh_at: Option<Instant>,
766
767    // Pending installs to detect completion and clear Install list
768    /// Names of packages we just triggered to install; when all appear installed, clear Install list.
769    pub pending_install_names: Option<Vec<String>>,
770
771    // Pending removals to detect completion and log
772    /// Names of packages we just triggered to remove; when all disappear, append to removed log.
773    pub pending_remove_names: Option<Vec<String>>,
774
775    // Dependency resolution cache for install list
776    /// Cached resolved dependencies for the current install list (updated in background).
777    pub install_list_deps: Vec<crate::state::modal::DependencyInfo>,
778    /// Reverse dependency summary for the current remove preflight modal (populated on demand).
779    pub remove_preflight_summary: Vec<crate::state::modal::ReverseRootSummary>,
780    /// Selected cascade removal mode for upcoming removals.
781    pub remove_cascade_mode: CascadeMode,
782    /// Whether dependency resolution is currently in progress.
783    pub deps_resolving: bool,
784    /// Path where the dependency cache is persisted as JSON.
785    pub deps_cache_path: PathBuf,
786    /// Dirty flag indicating `install_list_deps` needs to be saved.
787    pub deps_cache_dirty: bool,
788
789    // File resolution cache for install list
790    /// Cached resolved file changes for the current install list (updated in background).
791    pub install_list_files: Vec<crate::state::modal::PackageFileInfo>,
792    /// Whether file resolution is currently in progress.
793    pub files_resolving: bool,
794    /// Path where the file cache is persisted as JSON.
795    pub files_cache_path: PathBuf,
796    /// Dirty flag indicating `install_list_files` needs to be saved.
797    pub files_cache_dirty: bool,
798
799    // Service impact cache for install list
800    /// Cached resolved service impacts for the current install list (updated in background).
801    pub install_list_services: Vec<crate::state::modal::ServiceImpact>,
802    /// Whether service impact resolution is currently in progress.
803    pub services_resolving: bool,
804    /// Path where the service cache is persisted as JSON.
805    pub services_cache_path: PathBuf,
806    /// Dirty flag indicating `install_list_services` needs to be saved.
807    pub services_cache_dirty: bool,
808    /// Flag requesting that the runtime schedule service impact resolution for the active Preflight modal.
809    pub service_resolve_now: bool,
810    /// Identifier of the active service impact resolution request, if any.
811    pub active_service_request: Option<u64>,
812    /// Monotonic counter used to tag service impact resolution requests.
813    pub next_service_request_id: u64,
814    /// Signature of the package set currently queued for service impact resolution.
815    pub services_pending_signature: Option<(PreflightAction, Vec<String>)>,
816    /// Service restart decisions captured during the Preflight Services tab.
817    pub pending_service_plan: Vec<ServiceImpact>,
818
819    // Sandbox analysis cache for install list
820    /// Cached resolved sandbox information for the current install list (updated in background).
821    pub install_list_sandbox: Vec<crate::logic::sandbox::SandboxInfo>,
822    /// Whether sandbox resolution is currently in progress.
823    pub sandbox_resolving: bool,
824    /// Path where the sandbox cache is persisted as JSON.
825    pub sandbox_cache_path: PathBuf,
826    /// Dirty flag indicating `install_list_sandbox` needs to be saved.
827    pub sandbox_cache_dirty: bool,
828
829    // Preflight modal background resolution requests
830    /// Packages to resolve for preflight summary computation.
831    pub preflight_summary_items: Option<(Vec<PackageItem>, crate::state::modal::PreflightAction)>,
832    /// Packages to resolve for preflight dependency analysis (with action for forward/reverse).
833    pub preflight_deps_items: Option<(Vec<PackageItem>, crate::state::modal::PreflightAction)>,
834    /// Packages to resolve for preflight file analysis.
835    pub preflight_files_items: Option<Vec<PackageItem>>,
836    /// Packages to resolve for preflight service analysis.
837    pub preflight_services_items: Option<Vec<PackageItem>>,
838    /// AUR packages to resolve for preflight sandbox analysis (subset only).
839    pub preflight_sandbox_items: Option<Vec<PackageItem>>,
840    /// Whether preflight summary computation is in progress.
841    pub preflight_summary_resolving: bool,
842    /// Whether preflight dependency resolution is in progress.
843    pub preflight_deps_resolving: bool,
844    /// Whether preflight file resolution is in progress.
845    pub preflight_files_resolving: bool,
846    /// Whether preflight service resolution is in progress.
847    pub preflight_services_resolving: bool,
848    /// Whether preflight sandbox resolution is in progress.
849    pub preflight_sandbox_resolving: bool,
850    /// Last preflight dependency log state to suppress duplicate tick logs.
851    pub last_logged_preflight_deps_state: Option<(usize, bool, bool)>,
852    /// Cancellation flag for preflight operations (set to true when modal closes).
853    pub preflight_cancelled: std::sync::Arc<std::sync::atomic::AtomicBool>,
854
855    // Executor integration
856    /// Pending AUR vote intent (pkgbase and action) awaiting user confirmation.
857    pub pending_aur_vote_intent: Option<(String, VoteAction)>,
858    /// Pending AUR vote request (pkgbase and action) to be sent by the runtime tick handler.
859    pub pending_aur_vote_request: Option<(String, VoteAction)>,
860    /// Live AUR vote-state cache keyed by pkgbase/package name.
861    pub aur_vote_state_by_pkgbase: HashMap<String, AurVoteStateUi>,
862    /// Path where persisted AUR vote-state cache is stored as JSON.
863    pub aur_vote_state_path: PathBuf,
864    /// Dirty flag indicating `aur_vote_state_by_pkgbase` needs to be saved.
865    pub aur_vote_state_dirty: bool,
866    /// Whether live AUR vote-state lookup is available in current runtime session.
867    ///
868    /// Details:
869    /// - Set to `false` after first unsupported `list-votes` response to avoid repeatedly
870    ///   replacing stable cached states with transient `Loading`/`Unknown`.
871    pub aur_vote_state_lookup_supported: bool,
872    /// Pending AUR vote-state check request (pkgbase) to be sent by the runtime tick handler.
873    pub pending_aur_vote_state_request: Option<String>,
874    /// Pending executor request to be sent when `PreflightExec` modal is ready.
875    pub pending_executor_request: Option<crate::install::ExecutorRequest>,
876    /// Pending post-summary computation request (items and success flag to compute summary for).
877    pub pending_post_summary_items: Option<(Vec<PackageItem>, Option<bool>)>,
878    /// Header chips to use when transitioning to `PreflightExec` modal.
879    pub pending_exec_header_chips: Option<crate::state::modal::PreflightHeaderChips>,
880    /// Custom command to execute after password prompt (for special packages like paru/yay/semgrep-bin).
881    pub pending_custom_command: Option<String>,
882    /// Update commands to execute after password prompt (for system update).
883    pub pending_update_commands: Option<Vec<String>>,
884    /// Repo apply commands after password prompt (custom `repos.conf` apply).
885    pub pending_repo_apply_commands: Option<Vec<String>>,
886    /// Summary lines to seed `PreflightExec` when starting a repo apply.
887    pub pending_repo_apply_summary: Option<Vec<String>>,
888    /// Pending foreign∩sync overlap check after a successful full repository apply.
889    pub pending_repo_apply_overlap_check: Option<RepoOverlapApplyPending>,
890    /// Reopen the Repositories modal with a rescanned pacman view after repo apply completes.
891    pub pending_repositories_modal_resume: Option<RepositoriesModalResume>,
892    /// Privileged shell commands for foreign→sync migration (`PasswordPurpose::RepoForeignMigrate`).
893    pub pending_foreign_migrate_commands: Option<Vec<String>>,
894    /// Summary lines for foreign→sync migration preflight log.
895    pub pending_foreign_migrate_summary: Option<Vec<String>>,
896    /// Skips the next AUR-vs-repo duplicate-results warning (after user continues once).
897    pub skip_aur_repo_dup_warning_once: bool,
898    /// AUR update command to execute conditionally if pacman fails (for system update).
899    pub pending_aur_update_command: Option<String>,
900    /// Password obtained from password prompt, stored temporarily for reinstall confirmation flow.
901    pub pending_executor_password: Option<crate::state::SecureString>,
902    /// File database sync result from background thread (checked in tick handler).
903    pub pending_file_sync_result: Option<FileSyncResult>,
904    /// Background AUR SSH validation result handle for Optional Deps status refresh.
905    pub pending_aur_ssh_help_check_result: Option<std::sync::Arc<std::sync::Mutex<Option<bool>>>>,
906    /// Latest AUR SSH help validation result (`Some(true/false)`) from the background check.
907    pub aur_ssh_help_ready: Option<bool>,
908}