pacsea/events/preflight/keys/
action_keys.rs1use std::collections::HashMap;
4
5use crate::state::AppState;
6use crate::state::modal::ServiceRestartDecision;
7
8use super::context::{EnterOrSpaceContext, PreflightKeyContext};
9use super::tab_handlers::handle_enter_or_space;
10use crate::events::preflight::modal::close_preflight_modal;
11
12pub(super) fn handle_esc_key(app: &mut AppState) -> bool {
23 let service_info = if let crate::state::Modal::Preflight { service_info, .. } = &app.modal {
24 service_info.clone()
25 } else {
26 Vec::new()
27 };
28 close_preflight_modal(app, &service_info);
29 false
31}
32
33pub(super) fn handle_enter_key(app: &mut AppState) -> bool {
44 let should_close = if let crate::state::Modal::Preflight {
45 tab,
46 items,
47 dependency_info,
48 dep_selected,
49 dep_tree_expanded,
50 file_info,
51 file_selected,
52 file_tree_expanded,
53 sandbox_info,
54 sandbox_selected,
55 sandbox_tree_expanded,
56 selected_optdepends,
57 service_info,
58 service_selected,
59 ..
60 } = &mut app.modal
61 {
62 handle_enter_or_space(EnterOrSpaceContext {
63 tab,
64 items,
65 dependency_info,
66 dep_selected: *dep_selected,
67 dep_tree_expanded,
68 file_info,
69 file_selected: *file_selected,
70 file_tree_expanded,
71 sandbox_info,
72 sandbox_selected: *sandbox_selected,
73 sandbox_tree_expanded,
74 selected_optdepends,
75 service_info,
76 service_selected: *service_selected,
77 })
78 } else {
79 false
80 };
81
82 if should_close {
83 let (items_clone, action_clone, header_chips_clone, cascade_mode) =
86 if let crate::state::Modal::Preflight {
87 items,
88 action,
89 header_chips,
90 cascade_mode,
91 ..
92 } = &app.modal
93 {
94 (items.clone(), *action, header_chips.clone(), *cascade_mode)
95 } else {
96 let service_info =
98 if let crate::state::Modal::Preflight { service_info, .. } = &app.modal {
99 service_info.clone()
100 } else {
101 Vec::new()
102 };
103 close_preflight_modal(app, &service_info);
104 return false;
105 };
106
107 let service_info = if let crate::state::Modal::Preflight { service_info, .. } = &app.modal {
109 service_info.clone()
110 } else {
111 Vec::new()
112 };
113 close_preflight_modal(app, &service_info);
114
115 match action_clone {
117 crate::state::PreflightAction::Install => {
118 use super::command_keys;
119 command_keys::handle_proceed_install(app, items_clone, header_chips_clone);
120 }
121 crate::state::PreflightAction::Remove => {
122 use super::command_keys;
123 command_keys::handle_proceed_remove(
124 app,
125 items_clone,
126 cascade_mode,
127 header_chips_clone,
128 );
129 }
130 crate::state::PreflightAction::Downgrade => {
131 let username = std::env::var("USER").unwrap_or_else(|_| "user".to_string());
134 if let Some(lockout_msg) =
135 crate::logic::faillock::get_lockout_message_if_locked(&username, app)
136 {
137 app.modal = crate::state::Modal::Alert {
139 message: lockout_msg,
140 };
141 return false;
142 }
143 let settings = crate::theme::settings();
144 if crate::logic::password::should_use_interactive_auth_handoff(&settings) {
145 crate::events::spawn_downgrade_in_terminal(app, &items_clone);
146 } else {
147 app.modal = crate::state::Modal::PasswordPrompt {
148 purpose: crate::state::modal::PasswordPurpose::Downgrade,
149 items: items_clone,
150 input: crate::state::SecureString::default(),
151 cursor: 0,
152 error: None,
153 };
154 app.pending_exec_header_chips = Some(header_chips_clone);
155 }
156 }
157 }
158 return false;
160 }
161 false
162}
163
164#[allow(clippy::needless_pass_by_value)] pub fn start_execution(
177 app: &mut AppState,
178 items: &[crate::state::PackageItem],
179 action: crate::state::PreflightAction,
180 header_chips: crate::state::modal::PreflightHeaderChips,
181 password: Option<crate::state::SecureString>,
182) {
183 use crate::install::ExecutorRequest;
184
185 if !app.dry_run
190 && let Some(message) = crate::events::guardrails::db_lock_alert_message(app)
191 {
192 tracing::warn!("[Preflight] Blocking execution: pacman database is locked");
193 app.modal = crate::state::Modal::Alert { message };
194 return;
195 }
196
197 tracing::debug!(
198 action = ?action,
199 item_count = items.len(),
200 header_chips = ?header_chips,
201 has_password = password.is_some(),
202 "[Preflight] Transitioning modal: Preflight -> PreflightExec"
203 );
204
205 app.modal = crate::state::Modal::PreflightExec {
207 items: items.to_vec(),
208 action,
209 tab: crate::state::PreflightTab::Summary,
210 verbose: false,
211 log_lines: Vec::new(),
212 abortable: false,
213 header_chips,
214 success: None,
215 };
216
217 app.pending_executor_request = Some(match action {
219 crate::state::PreflightAction::Install => ExecutorRequest::Install {
220 items: items.to_vec(),
221 password,
222 dry_run: app.dry_run,
223 },
224 crate::state::PreflightAction::Remove => {
225 let names: Vec<String> = items.iter().map(|p| p.name.clone()).collect();
226 ExecutorRequest::Remove {
227 names,
228 password,
229 cascade: app.remove_cascade_mode,
230 dry_run: app.dry_run,
231 }
232 }
233 crate::state::PreflightAction::Downgrade => {
234 let names: Vec<String> = items.iter().map(|p| p.name.clone()).collect();
235 ExecutorRequest::Downgrade {
236 names,
237 password,
238 dry_run: app.dry_run,
239 }
240 }
241 });
242}
243
244pub(super) fn handle_space_key(ctx: &mut PreflightKeyContext<'_>) -> bool {
252 handle_enter_or_space(EnterOrSpaceContext {
253 tab: ctx.tab,
254 items: ctx.items,
255 dependency_info: ctx.dependency_info,
256 dep_selected: *ctx.dep_selected,
257 dep_tree_expanded: ctx.dep_tree_expanded,
258 file_info: ctx.file_info,
259 file_selected: *ctx.file_selected,
260 file_tree_expanded: ctx.file_tree_expanded,
261 sandbox_info: ctx.sandbox_info,
262 sandbox_selected: *ctx.sandbox_selected,
263 sandbox_tree_expanded: ctx.sandbox_tree_expanded,
264 selected_optdepends: ctx.selected_optdepends,
265 service_info: ctx.service_info,
266 service_selected: *ctx.service_selected,
267 });
268 false
269}
270
271pub(super) fn handle_shift_r_key(app: &mut AppState) -> bool {
279 tracing::info!("Shift+R pressed: Re-running all preflight analyses");
280
281 let (items, action) = if let crate::state::Modal::Preflight { items, action, .. } = &app.modal {
282 (items.clone(), *action)
283 } else {
284 return false;
285 };
286
287 if let crate::state::Modal::Preflight {
289 dependency_info,
290 deps_error,
291 file_info,
292 files_error,
293 service_info,
294 services_error,
295 services_loaded,
296 sandbox_info,
297 sandbox_error,
298 sandbox_loaded,
299 summary,
300 dep_selected,
301 file_selected,
302 service_selected,
303 sandbox_selected,
304 dep_tree_expanded,
305 file_tree_expanded,
306 sandbox_tree_expanded,
307 ..
308 } = &mut app.modal
309 {
310 *dependency_info = Vec::new();
311 *deps_error = None;
312 *file_info = Vec::new();
313 *files_error = None;
314 *service_info = Vec::new();
315 *services_error = None;
316 *services_loaded = false;
317 *sandbox_info = Vec::new();
318 *sandbox_error = None;
319 *sandbox_loaded = false;
320 *summary = None;
321
322 *dep_selected = 0;
323 *file_selected = 0;
324 *service_selected = 0;
325 *sandbox_selected = 0;
326
327 dep_tree_expanded.clear();
328 file_tree_expanded.clear();
329 sandbox_tree_expanded.clear();
330 }
331
332 app.preflight_cancelled
334 .store(false, std::sync::atomic::Ordering::Relaxed);
335
336 app.preflight_summary_items = Some((items.clone(), action));
338 app.preflight_summary_resolving = true;
339
340 if matches!(action, crate::state::PreflightAction::Install) {
341 app.preflight_deps_items = Some((items.clone(), crate::state::PreflightAction::Install));
342 app.preflight_deps_resolving = true;
343
344 app.preflight_files_items = Some(items.clone());
345 app.preflight_files_resolving = true;
346
347 app.preflight_services_items = Some(items.clone());
348 app.preflight_services_resolving = true;
349
350 let aur_items: Vec<_> = items
352 .iter()
353 .filter(|p| matches!(p.source, crate::state::Source::Aur))
354 .cloned()
355 .collect();
356 if aur_items.is_empty() {
357 app.preflight_sandbox_items = None;
358 app.preflight_sandbox_resolving = false;
359 if let crate::state::Modal::Preflight { sandbox_loaded, .. } = &mut app.modal {
360 *sandbox_loaded = true;
361 }
362 } else {
363 app.preflight_sandbox_items = Some(aur_items);
364 app.preflight_sandbox_resolving = true;
365 }
366 }
367
368 app.toast_message = Some("Re-running all preflight analyses...".to_string());
369 app.toast_expires_at = Some(std::time::Instant::now() + std::time::Duration::from_secs(3));
370 false
371}
372
373pub(super) fn handle_r_key(ctx: &mut PreflightKeyContext<'_>) -> bool {
381 if *ctx.tab == crate::state::PreflightTab::Services && !ctx.service_info.is_empty() {
382 if *ctx.service_selected >= ctx.service_info.len() {
384 *ctx.service_selected = ctx.service_info.len().saturating_sub(1);
385 }
386 if let Some(service) = ctx.service_info.get_mut(*ctx.service_selected) {
387 service.restart_decision = ServiceRestartDecision::Restart;
388 }
389 } else if *ctx.tab == crate::state::PreflightTab::Deps
390 && matches!(*ctx.action, crate::state::PreflightAction::Install)
391 {
392 *ctx.deps_error = None;
394 *ctx.dependency_info = crate::logic::deps::resolve_dependencies(ctx.items);
395 *ctx.dep_selected = 0;
396 } else if *ctx.tab == crate::state::PreflightTab::Files {
397 *ctx.files_error = None;
399 *ctx.file_info = crate::logic::files::resolve_file_changes(ctx.items, *ctx.action);
400 *ctx.file_selected = 0;
401 } else if *ctx.tab == crate::state::PreflightTab::Services {
402 *ctx.services_error = None;
404 *ctx.services_loaded = false;
405 *ctx.service_info = crate::logic::services::resolve_service_impacts(ctx.items, *ctx.action);
406 *ctx.service_selected = 0;
407 *ctx.services_loaded = true;
408 }
409 false
410}
411
412pub(super) fn handle_d_key(ctx: &mut PreflightKeyContext<'_>) -> bool {
420 if *ctx.tab == crate::state::PreflightTab::Services && !ctx.service_info.is_empty() {
421 if *ctx.service_selected >= ctx.service_info.len() {
422 *ctx.service_selected = ctx.service_info.len().saturating_sub(1);
423 }
424 if let Some(service) = ctx.service_info.get_mut(*ctx.service_selected) {
425 service.restart_decision = ServiceRestartDecision::Defer;
426 }
427 }
428 false
429}
430
431pub(super) fn handle_a_key(ctx: &mut PreflightKeyContext<'_>) -> bool {
439 if *ctx.tab == crate::state::PreflightTab::Deps && !ctx.dependency_info.is_empty() {
440 let mut grouped: HashMap<String, Vec<&crate::state::modal::DependencyInfo>> =
441 HashMap::new();
442 for dep in ctx.dependency_info.iter() {
443 for req_by in &dep.required_by {
444 grouped.entry(req_by.clone()).or_default().push(dep);
445 }
446 }
447
448 let all_expanded = ctx
449 .items
450 .iter()
451 .all(|p| ctx.dep_tree_expanded.contains(&p.name));
452 if all_expanded {
453 ctx.dep_tree_expanded.clear();
455 } else {
456 for pkg_name in ctx.items.iter().map(|p| &p.name) {
458 ctx.dep_tree_expanded.insert(pkg_name.clone());
459 }
460 }
461 } else if *ctx.tab == crate::state::PreflightTab::Files && !ctx.file_info.is_empty() {
462 let all_expanded = ctx
464 .file_info
465 .iter()
466 .filter(|p| !p.files.is_empty())
467 .all(|p| ctx.file_tree_expanded.contains(&p.name));
468 if all_expanded {
469 ctx.file_tree_expanded.clear();
471 } else {
472 for pkg_info in ctx.file_info.iter() {
474 if !pkg_info.files.is_empty() {
475 ctx.file_tree_expanded.insert(pkg_info.name.clone());
476 }
477 }
478 }
479 }
480 false
481}