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}