Skip to main content

pacsea/app/runtime/
mod.rs

1use ratatui::{Terminal, backend::CrosstermBackend};
2
3use crate::logic::send_query;
4use crate::state::AppState;
5
6use super::terminal::{restore_terminal, setup_terminal};
7
8/// Background worker management and spawning.
9mod background;
10/// Channel definitions for runtime communication.
11mod channels;
12/// Cleanup operations on application exit.
13mod cleanup;
14/// Main event loop implementation.
15mod event_loop;
16/// Event handlers for different event types.
17mod handlers;
18/// Application state initialization module.
19pub mod init;
20/// Tick handler for periodic UI updates.
21mod tick_handler;
22/// Background worker implementations.
23mod workers;
24
25use background::{Channels, spawn_auxiliary_workers, spawn_event_thread};
26use cleanup::cleanup_on_exit;
27use event_loop::run_event_loop;
28use init::{initialize_app_state, run_startup_config_preflight, trigger_initial_resolutions};
29
30/// Result type alias for runtime operations.
31type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
32
33/// What: Best-effort terminal restore guard for non-headless runtime sessions.
34///
35/// Inputs:
36/// - `active`: Whether terminal restore should run on drop.
37///
38/// Output:
39/// - On drop, attempts to restore terminal modes when active.
40///
41/// Details:
42/// - Prevents leaked raw mode / mouse capture when `run()` returns early with `?`.
43/// - Uses best-effort restore and logs failures without changing the original return path.
44struct TerminalRestoreGuard {
45    /// Whether drop should attempt terminal restoration.
46    active: bool,
47}
48
49impl TerminalRestoreGuard {
50    /// What: Create a new terminal restore guard.
51    ///
52    /// Inputs:
53    /// - `active`: Enables or disables restore-on-drop behavior.
54    ///
55    /// Output:
56    /// - New `TerminalRestoreGuard`.
57    ///
58    /// Details:
59    /// - `active` should be `true` only after successful terminal setup in non-headless mode.
60    const fn new(active: bool) -> Self {
61        Self { active }
62    }
63
64    /// What: Disable restore-on-drop after explicit terminal restoration.
65    ///
66    /// Inputs:
67    /// - None.
68    ///
69    /// Output:
70    /// - Guard no longer restores terminal in `Drop`.
71    ///
72    /// Details:
73    /// - Prevents duplicate restoration attempts on normal exit path.
74    const fn disarm(&mut self) {
75        self.active = false;
76    }
77}
78
79impl Drop for TerminalRestoreGuard {
80    /// What: Restore terminal modes when the guard goes out of scope.
81    ///
82    /// Inputs:
83    /// - `self`: Guard state.
84    ///
85    /// Output:
86    /// - No return value; logs on restore failure.
87    ///
88    /// Details:
89    /// - This runs during unwinding and early-return paths.
90    fn drop(&mut self) {
91        if self.active
92            && let Err(err) = restore_terminal()
93        {
94            tracing::warn!(error = %err, "failed to restore terminal from drop guard");
95        }
96    }
97}
98
99/// What: Run the Pacsea TUI application end-to-end.
100///
101/// This function initializes terminal and state, spawns background workers
102/// (index, search, details, status/news), drives the event loop, persists
103/// caches, and restores the terminal on exit.
104///
105/// Inputs:
106/// - `dry_run_flag`: When `true`, install/remove/downgrade actions are displayed but not executed
107///   (overrides the config default for the session).
108///
109/// Output:
110/// - `Ok(())` when the UI exits cleanly; `Err` on unrecoverable terminal or runtime errors.
111///
112/// # Errors
113/// - Returns `Err` when terminal setup fails (e.g., unable to initialize terminal backend)
114/// - Returns `Err` when terminal restoration fails on exit
115/// - Returns `Err` when critical runtime errors occur during initialization or event loop execution
116///
117/// Details:
118/// - Config/state: Migrates legacy configs, loads settings (layout, keymap, sort), and reads
119///   persisted files (details cache, recent queries, install list, on-disk official index).
120/// - Background tasks: Spawns channels and tasks for batched details fetch, AUR/official search,
121///   PKGBUILD retrieval, official index refresh/enrichment, Arch status text, and Arch news.
122/// - Event loop: Renders UI frames and handles keyboard, mouse, tick, and channel messages to
123///   update results, details, ring-prefetch, PKGBUILD viewer, installed-only mode, and modals.
124/// - Persistence: Debounces and periodically writes recent, details cache, and install list.
125/// - Cleanup: Flushes pending writes and restores terminal modes before returning.
126pub async fn run(dry_run_flag: bool) -> Result<()> {
127    let headless = std::env::var("PACSEA_TEST_HEADLESS").ok().as_deref() == Some("1");
128
129    // Migrate legacy configs, fill missing keys, and return the resolved settings snapshot.
130    let prefs = run_startup_config_preflight();
131
132    // Force theme resolution BEFORE terminal setup.
133    // This is important because theme resolution may query terminal colors via OSC 10/11,
134    // which must happen before mouse capture is enabled to avoid input conflicts.
135    let _ = crate::theme::theme();
136
137    if !headless {
138        setup_terminal()?;
139    }
140    let mut terminal_restore_guard = TerminalRestoreGuard::new(!headless);
141    let mut terminal = if headless {
142        None
143    } else {
144        Some(Terminal::new(CrosstermBackend::new(std::io::stdout()))?)
145    };
146
147    let mut app = AppState::default();
148
149    // Initialize application state (loads settings, caches, etc.)
150    let init_flags = initialize_app_state(&mut app, dry_run_flag, headless, &prefs);
151
152    // Create channels and spawn background workers
153    let mut channels = Channels::new(app.official_index_path.clone());
154
155    // Get updates refresh interval from settings (minimum 60s per requirement)
156    let updates_refresh_interval = crate::theme::settings().updates_refresh_interval.max(60);
157
158    // Spawn auxiliary workers (status, news, tick, index updates)
159    spawn_auxiliary_workers(
160        headless,
161        &channels.status_tx,
162        &channels.news_tx,
163        &channels.news_feed_tx,
164        &channels.news_incremental_tx,
165        &channels.announcement_tx,
166        &channels.tick_tx,
167        &app.news_read_ids,
168        &app.news_read_urls,
169        &app.news_seen_pkg_versions,
170        &app.news_seen_aur_comments,
171        &app.official_index_path,
172        &channels.net_err_tx,
173        &channels.index_notify_tx,
174        &channels.updates_tx,
175        updates_refresh_interval,
176        app.installed_packages_mode,
177        crate::theme::settings().get_announcement,
178        app.last_startup_timestamp.as_deref(),
179    );
180
181    // Spawn event reading thread
182    spawn_event_thread(
183        headless,
184        channels.event_tx.clone(),
185        channels.event_thread_cancelled.clone(),
186    );
187
188    // Trigger initial background resolutions if caches were missing/invalid
189    trigger_initial_resolutions(
190        &mut app,
191        &init_flags,
192        &channels.deps_req_tx,
193        &channels.files_req_tx,
194        &channels.services_req_tx,
195        &channels.sandbox_req_tx,
196    );
197
198    // Send initial query
199    send_query(&mut app, &channels.query_tx);
200
201    // Main event loop
202    run_event_loop(&mut terminal, &mut app, &mut channels).await;
203
204    // Cleanup on exit - this resets flags and flushes caches
205    cleanup_on_exit(&mut app, &channels);
206
207    // Drop channels to close request channels and stop workers from accepting new work
208    drop(channels);
209
210    // Restore terminal so user sees prompt
211    if !headless {
212        restore_terminal()?;
213        terminal_restore_guard.disarm();
214    }
215
216    // Force immediate process exit to avoid waiting for background blocking tasks
217    // This is necessary because spawn_blocking tasks cannot be cancelled and would
218    // otherwise keep the tokio runtime alive until they complete
219    std::process::exit(0);
220}