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 tracing::debug!(
189 action = ?action,
190 item_count = items.len(),
191 header_chips = ?header_chips,
192 has_password = password.is_some(),
193 "[Preflight] Transitioning modal: Preflight -> PreflightExec"
194 );
195
196 app.modal = crate::state::Modal::PreflightExec {
198 items: items.to_vec(),
199 action,
200 tab: crate::state::PreflightTab::Summary,
201 verbose: false,
202 log_lines: Vec::new(),
203 abortable: false,
204 header_chips,
205 success: None,
206 };
207
208 app.pending_executor_request = Some(match action {
210 crate::state::PreflightAction::Install => ExecutorRequest::Install {
211 items: items.to_vec(),
212 password,
213 dry_run: app.dry_run,
214 },
215 crate::state::PreflightAction::Remove => {
216 let names: Vec<String> = items.iter().map(|p| p.name.clone()).collect();
217 ExecutorRequest::Remove {
218 names,
219 password,
220 cascade: app.remove_cascade_mode,
221 dry_run: app.dry_run,
222 }
223 }
224 crate::state::PreflightAction::Downgrade => {
225 let names: Vec<String> = items.iter().map(|p| p.name.clone()).collect();
226 ExecutorRequest::Downgrade {
227 names,
228 password,
229 dry_run: app.dry_run,
230 }
231 }
232 });
233}
234
235pub(super) fn handle_space_key(ctx: &mut PreflightKeyContext<'_>) -> bool {
243 handle_enter_or_space(EnterOrSpaceContext {
244 tab: ctx.tab,
245 items: ctx.items,
246 dependency_info: ctx.dependency_info,
247 dep_selected: *ctx.dep_selected,
248 dep_tree_expanded: ctx.dep_tree_expanded,
249 file_info: ctx.file_info,
250 file_selected: *ctx.file_selected,
251 file_tree_expanded: ctx.file_tree_expanded,
252 sandbox_info: ctx.sandbox_info,
253 sandbox_selected: *ctx.sandbox_selected,
254 sandbox_tree_expanded: ctx.sandbox_tree_expanded,
255 selected_optdepends: ctx.selected_optdepends,
256 service_info: ctx.service_info,
257 service_selected: *ctx.service_selected,
258 });
259 false
260}
261
262pub(super) fn handle_shift_r_key(app: &mut AppState) -> bool {
270 tracing::info!("Shift+R pressed: Re-running all preflight analyses");
271
272 let (items, action) = if let crate::state::Modal::Preflight { items, action, .. } = &app.modal {
273 (items.clone(), *action)
274 } else {
275 return false;
276 };
277
278 if let crate::state::Modal::Preflight {
280 dependency_info,
281 deps_error,
282 file_info,
283 files_error,
284 service_info,
285 services_error,
286 services_loaded,
287 sandbox_info,
288 sandbox_error,
289 sandbox_loaded,
290 summary,
291 dep_selected,
292 file_selected,
293 service_selected,
294 sandbox_selected,
295 dep_tree_expanded,
296 file_tree_expanded,
297 sandbox_tree_expanded,
298 ..
299 } = &mut app.modal
300 {
301 *dependency_info = Vec::new();
302 *deps_error = None;
303 *file_info = Vec::new();
304 *files_error = None;
305 *service_info = Vec::new();
306 *services_error = None;
307 *services_loaded = false;
308 *sandbox_info = Vec::new();
309 *sandbox_error = None;
310 *sandbox_loaded = false;
311 *summary = None;
312
313 *dep_selected = 0;
314 *file_selected = 0;
315 *service_selected = 0;
316 *sandbox_selected = 0;
317
318 dep_tree_expanded.clear();
319 file_tree_expanded.clear();
320 sandbox_tree_expanded.clear();
321 }
322
323 app.preflight_cancelled
325 .store(false, std::sync::atomic::Ordering::Relaxed);
326
327 app.preflight_summary_items = Some((items.clone(), action));
329 app.preflight_summary_resolving = true;
330
331 if matches!(action, crate::state::PreflightAction::Install) {
332 app.preflight_deps_items = Some((items.clone(), crate::state::PreflightAction::Install));
333 app.preflight_deps_resolving = true;
334
335 app.preflight_files_items = Some(items.clone());
336 app.preflight_files_resolving = true;
337
338 app.preflight_services_items = Some(items.clone());
339 app.preflight_services_resolving = true;
340
341 let aur_items: Vec<_> = items
343 .iter()
344 .filter(|p| matches!(p.source, crate::state::Source::Aur))
345 .cloned()
346 .collect();
347 if aur_items.is_empty() {
348 app.preflight_sandbox_items = None;
349 app.preflight_sandbox_resolving = false;
350 if let crate::state::Modal::Preflight { sandbox_loaded, .. } = &mut app.modal {
351 *sandbox_loaded = true;
352 }
353 } else {
354 app.preflight_sandbox_items = Some(aur_items);
355 app.preflight_sandbox_resolving = true;
356 }
357 }
358
359 app.toast_message = Some("Re-running all preflight analyses...".to_string());
360 app.toast_expires_at = Some(std::time::Instant::now() + std::time::Duration::from_secs(3));
361 false
362}
363
364pub(super) fn handle_r_key(ctx: &mut PreflightKeyContext<'_>) -> bool {
372 if *ctx.tab == crate::state::PreflightTab::Services && !ctx.service_info.is_empty() {
373 if *ctx.service_selected >= ctx.service_info.len() {
375 *ctx.service_selected = ctx.service_info.len().saturating_sub(1);
376 }
377 if let Some(service) = ctx.service_info.get_mut(*ctx.service_selected) {
378 service.restart_decision = ServiceRestartDecision::Restart;
379 }
380 } else if *ctx.tab == crate::state::PreflightTab::Deps
381 && matches!(*ctx.action, crate::state::PreflightAction::Install)
382 {
383 *ctx.deps_error = None;
385 *ctx.dependency_info = crate::logic::deps::resolve_dependencies(ctx.items);
386 *ctx.dep_selected = 0;
387 } else if *ctx.tab == crate::state::PreflightTab::Files {
388 *ctx.files_error = None;
390 *ctx.file_info = crate::logic::files::resolve_file_changes(ctx.items, *ctx.action);
391 *ctx.file_selected = 0;
392 } else if *ctx.tab == crate::state::PreflightTab::Services {
393 *ctx.services_error = None;
395 *ctx.services_loaded = false;
396 *ctx.service_info = crate::logic::services::resolve_service_impacts(ctx.items, *ctx.action);
397 *ctx.service_selected = 0;
398 *ctx.services_loaded = true;
399 }
400 false
401}
402
403pub(super) fn handle_d_key(ctx: &mut PreflightKeyContext<'_>) -> bool {
411 if *ctx.tab == crate::state::PreflightTab::Services && !ctx.service_info.is_empty() {
412 if *ctx.service_selected >= ctx.service_info.len() {
413 *ctx.service_selected = ctx.service_info.len().saturating_sub(1);
414 }
415 if let Some(service) = ctx.service_info.get_mut(*ctx.service_selected) {
416 service.restart_decision = ServiceRestartDecision::Defer;
417 }
418 }
419 false
420}
421
422pub(super) fn handle_a_key(ctx: &mut PreflightKeyContext<'_>) -> bool {
430 if *ctx.tab == crate::state::PreflightTab::Deps && !ctx.dependency_info.is_empty() {
431 let mut grouped: HashMap<String, Vec<&crate::state::modal::DependencyInfo>> =
432 HashMap::new();
433 for dep in ctx.dependency_info.iter() {
434 for req_by in &dep.required_by {
435 grouped.entry(req_by.clone()).or_default().push(dep);
436 }
437 }
438
439 let all_expanded = ctx
440 .items
441 .iter()
442 .all(|p| ctx.dep_tree_expanded.contains(&p.name));
443 if all_expanded {
444 ctx.dep_tree_expanded.clear();
446 } else {
447 for pkg_name in ctx.items.iter().map(|p| &p.name) {
449 ctx.dep_tree_expanded.insert(pkg_name.clone());
450 }
451 }
452 } else if *ctx.tab == crate::state::PreflightTab::Files && !ctx.file_info.is_empty() {
453 let all_expanded = ctx
455 .file_info
456 .iter()
457 .filter(|p| !p.files.is_empty())
458 .all(|p| ctx.file_tree_expanded.contains(&p.name));
459 if all_expanded {
460 ctx.file_tree_expanded.clear();
462 } else {
463 for pkg_info in ctx.file_info.iter() {
465 if !pkg_info.files.is_empty() {
466 ctx.file_tree_expanded.insert(pkg_info.name.clone());
467 }
468 }
469 }
470 }
471 false
472}