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 app.modal = crate::state::Modal::PasswordPrompt {
145 purpose: crate::state::modal::PasswordPurpose::Downgrade,
146 items: items_clone,
147 input: String::new(),
148 cursor: 0,
149 error: None,
150 };
151 app.pending_exec_header_chips = Some(header_chips_clone);
152 }
153 }
154 return false;
156 }
157 false
158}
159
160#[allow(clippy::needless_pass_by_value)] pub fn start_execution(
173 app: &mut AppState,
174 items: &[crate::state::PackageItem],
175 action: crate::state::PreflightAction,
176 header_chips: crate::state::modal::PreflightHeaderChips,
177 password: Option<String>,
178) {
179 use crate::install::ExecutorRequest;
180
181 tracing::debug!(
185 action = ?action,
186 item_count = items.len(),
187 header_chips = ?header_chips,
188 has_password = password.is_some(),
189 "[Preflight] Transitioning modal: Preflight -> PreflightExec"
190 );
191
192 app.modal = crate::state::Modal::PreflightExec {
194 items: items.to_vec(),
195 action,
196 tab: crate::state::PreflightTab::Summary,
197 verbose: false,
198 log_lines: Vec::new(),
199 abortable: false,
200 header_chips,
201 success: None,
202 };
203
204 app.pending_executor_request = Some(match action {
206 crate::state::PreflightAction::Install => ExecutorRequest::Install {
207 items: items.to_vec(),
208 password,
209 dry_run: app.dry_run,
210 },
211 crate::state::PreflightAction::Remove => {
212 let names: Vec<String> = items.iter().map(|p| p.name.clone()).collect();
213 ExecutorRequest::Remove {
214 names,
215 password,
216 cascade: app.remove_cascade_mode,
217 dry_run: app.dry_run,
218 }
219 }
220 crate::state::PreflightAction::Downgrade => {
221 let names: Vec<String> = items.iter().map(|p| p.name.clone()).collect();
222 ExecutorRequest::Downgrade {
223 names,
224 password,
225 dry_run: app.dry_run,
226 }
227 }
228 });
229}
230
231pub(super) fn handle_space_key(ctx: &mut PreflightKeyContext<'_>) -> bool {
239 handle_enter_or_space(EnterOrSpaceContext {
240 tab: ctx.tab,
241 items: ctx.items,
242 dependency_info: ctx.dependency_info,
243 dep_selected: *ctx.dep_selected,
244 dep_tree_expanded: ctx.dep_tree_expanded,
245 file_info: ctx.file_info,
246 file_selected: *ctx.file_selected,
247 file_tree_expanded: ctx.file_tree_expanded,
248 sandbox_info: ctx.sandbox_info,
249 sandbox_selected: *ctx.sandbox_selected,
250 sandbox_tree_expanded: ctx.sandbox_tree_expanded,
251 selected_optdepends: ctx.selected_optdepends,
252 service_info: ctx.service_info,
253 service_selected: *ctx.service_selected,
254 });
255 false
256}
257
258pub(super) fn handle_shift_r_key(app: &mut AppState) -> bool {
266 tracing::info!("Shift+R pressed: Re-running all preflight analyses");
267
268 let (items, action) = if let crate::state::Modal::Preflight { items, action, .. } = &app.modal {
269 (items.clone(), *action)
270 } else {
271 return false;
272 };
273
274 if let crate::state::Modal::Preflight {
276 dependency_info,
277 deps_error,
278 file_info,
279 files_error,
280 service_info,
281 services_error,
282 services_loaded,
283 sandbox_info,
284 sandbox_error,
285 sandbox_loaded,
286 summary,
287 dep_selected,
288 file_selected,
289 service_selected,
290 sandbox_selected,
291 dep_tree_expanded,
292 file_tree_expanded,
293 sandbox_tree_expanded,
294 ..
295 } = &mut app.modal
296 {
297 *dependency_info = Vec::new();
298 *deps_error = None;
299 *file_info = Vec::new();
300 *files_error = None;
301 *service_info = Vec::new();
302 *services_error = None;
303 *services_loaded = false;
304 *sandbox_info = Vec::new();
305 *sandbox_error = None;
306 *sandbox_loaded = false;
307 *summary = None;
308
309 *dep_selected = 0;
310 *file_selected = 0;
311 *service_selected = 0;
312 *sandbox_selected = 0;
313
314 dep_tree_expanded.clear();
315 file_tree_expanded.clear();
316 sandbox_tree_expanded.clear();
317 }
318
319 app.preflight_cancelled
321 .store(false, std::sync::atomic::Ordering::Relaxed);
322
323 app.preflight_summary_items = Some((items.clone(), action));
325 app.preflight_summary_resolving = true;
326
327 if matches!(action, crate::state::PreflightAction::Install) {
328 app.preflight_deps_items = Some((items.clone(), crate::state::PreflightAction::Install));
329 app.preflight_deps_resolving = true;
330
331 app.preflight_files_items = Some(items.clone());
332 app.preflight_files_resolving = true;
333
334 app.preflight_services_items = Some(items.clone());
335 app.preflight_services_resolving = true;
336
337 let aur_items: Vec<_> = items
339 .iter()
340 .filter(|p| matches!(p.source, crate::state::Source::Aur))
341 .cloned()
342 .collect();
343 if aur_items.is_empty() {
344 app.preflight_sandbox_items = None;
345 app.preflight_sandbox_resolving = false;
346 if let crate::state::Modal::Preflight { sandbox_loaded, .. } = &mut app.modal {
347 *sandbox_loaded = true;
348 }
349 } else {
350 app.preflight_sandbox_items = Some(aur_items);
351 app.preflight_sandbox_resolving = true;
352 }
353 }
354
355 app.toast_message = Some("Re-running all preflight analyses...".to_string());
356 app.toast_expires_at = Some(std::time::Instant::now() + std::time::Duration::from_secs(3));
357 false
358}
359
360pub(super) fn handle_r_key(ctx: &mut PreflightKeyContext<'_>) -> bool {
368 if *ctx.tab == crate::state::PreflightTab::Services && !ctx.service_info.is_empty() {
369 if *ctx.service_selected >= ctx.service_info.len() {
371 *ctx.service_selected = ctx.service_info.len().saturating_sub(1);
372 }
373 if let Some(service) = ctx.service_info.get_mut(*ctx.service_selected) {
374 service.restart_decision = ServiceRestartDecision::Restart;
375 }
376 } else if *ctx.tab == crate::state::PreflightTab::Deps
377 && matches!(*ctx.action, crate::state::PreflightAction::Install)
378 {
379 *ctx.deps_error = None;
381 *ctx.dependency_info = crate::logic::deps::resolve_dependencies(ctx.items);
382 *ctx.dep_selected = 0;
383 } else if *ctx.tab == crate::state::PreflightTab::Files {
384 *ctx.files_error = None;
386 *ctx.file_info = crate::logic::files::resolve_file_changes(ctx.items, *ctx.action);
387 *ctx.file_selected = 0;
388 } else if *ctx.tab == crate::state::PreflightTab::Services {
389 *ctx.services_error = None;
391 *ctx.services_loaded = false;
392 *ctx.service_info = crate::logic::services::resolve_service_impacts(ctx.items, *ctx.action);
393 *ctx.service_selected = 0;
394 *ctx.services_loaded = true;
395 }
396 false
397}
398
399pub(super) fn handle_d_key(ctx: &mut PreflightKeyContext<'_>) -> bool {
407 if *ctx.tab == crate::state::PreflightTab::Services && !ctx.service_info.is_empty() {
408 if *ctx.service_selected >= ctx.service_info.len() {
409 *ctx.service_selected = ctx.service_info.len().saturating_sub(1);
410 }
411 if let Some(service) = ctx.service_info.get_mut(*ctx.service_selected) {
412 service.restart_decision = ServiceRestartDecision::Defer;
413 }
414 }
415 false
416}
417
418pub(super) fn handle_a_key(ctx: &mut PreflightKeyContext<'_>) -> bool {
426 if *ctx.tab == crate::state::PreflightTab::Deps && !ctx.dependency_info.is_empty() {
427 let mut grouped: HashMap<String, Vec<&crate::state::modal::DependencyInfo>> =
428 HashMap::new();
429 for dep in ctx.dependency_info.iter() {
430 for req_by in &dep.required_by {
431 grouped.entry(req_by.clone()).or_default().push(dep);
432 }
433 }
434
435 let all_expanded = ctx
436 .items
437 .iter()
438 .all(|p| ctx.dep_tree_expanded.contains(&p.name));
439 if all_expanded {
440 ctx.dep_tree_expanded.clear();
442 } else {
443 for pkg_name in ctx.items.iter().map(|p| &p.name) {
445 ctx.dep_tree_expanded.insert(pkg_name.clone());
446 }
447 }
448 } else if *ctx.tab == crate::state::PreflightTab::Files && !ctx.file_info.is_empty() {
449 let all_expanded = ctx
451 .file_info
452 .iter()
453 .filter(|p| !p.files.is_empty())
454 .all(|p| ctx.file_tree_expanded.contains(&p.name));
455 if all_expanded {
456 ctx.file_tree_expanded.clear();
458 } else {
459 for pkg_info in ctx.file_info.iter() {
461 if !pkg_info.files.is_empty() {
462 ctx.file_tree_expanded.insert(pkg_info.name.clone());
463 }
464 }
465 }
466 }
467 false
468}