Skip to main content

pacsea/install/
direct.rs

1//! Direct install/remove operations using integrated processes (bypassing preflight).
2
3use crate::state::{AppState, PackageItem, modal::CascadeMode};
4
5/// What: Show one-time long-run auth preflight guidance when readiness is at risk.
6fn maybe_show_long_run_auth_preflight(app: &mut AppState) {
7    if app.long_run_auth_preflight_warned {
8        return;
9    }
10    let settings = crate::theme::settings();
11    let readiness = crate::logic::long_run_auth::evaluate_long_run_auth_readiness(&settings);
12    if readiness.should_warn {
13        app.long_run_auth_preflight_warned = true;
14        app.toast_message = Some(crate::logic::long_run_auth::build_long_run_warning_message(
15            app,
16        ));
17        app.toast_expires_at = Some(std::time::Instant::now() + std::time::Duration::from_secs(8));
18    }
19}
20
21/// What: Start integrated install process for a single package (bypassing preflight).
22///
23/// Inputs:
24/// - `app`: Mutable application state
25/// - `item`: Package to install
26/// - `dry_run`: Whether to run in dry-run mode
27///
28/// Output:
29/// - If passwordless sudo is available: Proceeds directly to `PreflightExec` modal.
30/// - If passwordless sudo is not available: Transitions to `PasswordPrompt` modal.
31///
32/// Details:
33/// - Checks for passwordless sudo first to skip password prompt if available.
34/// - Both official packages (sudo pacman) and AUR packages (paru/yay need sudo for final step)
35///   require sudo, but password may not be needed if passwordless sudo is configured.
36/// - Uses `ExecutorRequest::Install` for execution.
37pub fn start_integrated_install(app: &mut AppState, item: &PackageItem, dry_run: bool) {
38    use crate::events::start_execution;
39    use crate::state::modal::PreflightHeaderChips;
40
41    app.dry_run = dry_run;
42    maybe_show_long_run_auth_preflight(app);
43    let items = vec![item.clone()];
44    let header_chips = PreflightHeaderChips::default();
45
46    // Check faillock status before proceeding
47    let username = std::env::var("USER").unwrap_or_else(|_| "user".to_string());
48    if let Some(lockout_msg) = crate::logic::faillock::get_lockout_message_if_locked(&username, app)
49    {
50        // User is locked out - show warning
51        app.modal = crate::state::Modal::Alert {
52            message: lockout_msg,
53        };
54        return;
55    }
56
57    let settings = crate::theme::settings();
58    if crate::logic::password::should_use_interactive_auth_handoff(&settings) {
59        match crate::events::try_interactive_auth_handoff() {
60            Ok(true) => start_execution(
61                app,
62                &items,
63                crate::state::PreflightAction::Install,
64                header_chips,
65                None,
66            ),
67            Ok(false) => {
68                app.modal = crate::state::Modal::Alert {
69                    message: crate::i18n::t(app, "app.errors.authentication_failed"),
70                };
71            }
72            Err(e) => {
73                app.modal = crate::state::Modal::Alert { message: e };
74            }
75        }
76    } else if crate::logic::password::resolve_auth_mode(&settings)
77        == crate::logic::privilege::AuthMode::PasswordlessOnly
78        && crate::logic::password::should_use_passwordless_sudo(&settings)
79    {
80        start_execution(
81            app,
82            &items,
83            crate::state::PreflightAction::Install,
84            header_chips,
85            None,
86        );
87    } else {
88        app.modal = crate::state::Modal::PasswordPrompt {
89            purpose: crate::state::modal::PasswordPurpose::Install,
90            items,
91            input: crate::state::SecureString::default(),
92            cursor: 0,
93            error: None,
94        };
95        app.pending_exec_header_chips = Some(header_chips);
96    }
97}
98
99/// What: Start integrated install process for multiple packages (bypassing preflight).
100///
101/// Inputs:
102/// - `app`: Mutable application state
103/// - `items`: Packages to install
104/// - `dry_run`: Whether to run in dry-run mode
105///
106/// Output:
107/// - If passwordless sudo is available: Proceeds directly to `PreflightExec` modal.
108/// - If passwordless sudo is not available: Transitions to `PasswordPrompt` modal.
109///
110/// Details:
111/// - Checks for passwordless sudo first to skip password prompt if available.
112/// - Both official packages (sudo pacman) and AUR packages (paru/yay need sudo for final step)
113///   require sudo, but password may not be needed if passwordless sudo is configured.
114/// - Uses `ExecutorRequest::Install` for execution.
115pub fn start_integrated_install_all(app: &mut AppState, items: &[PackageItem], dry_run: bool) {
116    use crate::events::start_execution;
117    use crate::state::modal::PreflightHeaderChips;
118
119    app.dry_run = dry_run;
120    maybe_show_long_run_auth_preflight(app);
121    let items_vec = items.to_vec();
122    let header_chips = PreflightHeaderChips::default();
123
124    // Check faillock status before proceeding
125    let username = std::env::var("USER").unwrap_or_else(|_| "user".to_string());
126    if let Some(lockout_msg) = crate::logic::faillock::get_lockout_message_if_locked(&username, app)
127    {
128        // User is locked out - show warning
129        app.modal = crate::state::Modal::Alert {
130            message: lockout_msg,
131        };
132        return;
133    }
134
135    let settings = crate::theme::settings();
136    if crate::logic::password::should_use_interactive_auth_handoff(&settings) {
137        match crate::events::try_interactive_auth_handoff() {
138            Ok(true) => start_execution(
139                app,
140                &items_vec,
141                crate::state::PreflightAction::Install,
142                header_chips,
143                None,
144            ),
145            Ok(false) => {
146                app.modal = crate::state::Modal::Alert {
147                    message: crate::i18n::t(app, "app.errors.authentication_failed"),
148                };
149            }
150            Err(e) => {
151                app.modal = crate::state::Modal::Alert { message: e };
152            }
153        }
154    } else if crate::logic::password::resolve_auth_mode(&settings)
155        == crate::logic::privilege::AuthMode::PasswordlessOnly
156        && crate::logic::password::should_use_passwordless_sudo(&settings)
157    {
158        start_execution(
159            app,
160            &items_vec,
161            crate::state::PreflightAction::Install,
162            header_chips,
163            None,
164        );
165    } else {
166        app.modal = crate::state::Modal::PasswordPrompt {
167            purpose: crate::state::modal::PasswordPurpose::Install,
168            items: items_vec,
169            input: crate::state::SecureString::default(),
170            cursor: 0,
171            error: None,
172        };
173        app.pending_exec_header_chips = Some(header_chips);
174    }
175}
176
177/// What: Start integrated remove process (bypassing preflight).
178///
179/// Inputs:
180/// - `app`: Mutable application state
181/// - `names`: Package names to remove
182/// - `dry_run`: Whether to run in dry-run mode
183/// - `cascade_mode`: Cascade removal mode
184///
185/// Output:
186/// - In interactive mode: Proceeds directly to `PreflightExec` without password modal.
187/// - Otherwise: Transitions to `PasswordPrompt`.
188///
189/// Details:
190/// - Remove operations always need privilege escalation.
191/// - In `auth_mode = interactive`, Pacsea performs terminal handoff auth and then starts
192///   execution without collecting a password in-app.
193/// - Outside interactive mode, remove keeps using the in-app password prompt.
194/// - Uses `ExecutorRequest::Remove` for execution.
195pub fn start_integrated_remove_all(
196    app: &mut AppState,
197    names: &[String],
198    dry_run: bool,
199    cascade_mode: CascadeMode,
200) {
201    use crate::events::start_execution;
202    use crate::state::modal::PreflightHeaderChips;
203
204    app.dry_run = dry_run;
205    maybe_show_long_run_auth_preflight(app);
206    app.remove_cascade_mode = cascade_mode;
207
208    // Convert names to PackageItem for password prompt (we only need names, so create minimal items)
209    let items: Vec<PackageItem> = names
210        .iter()
211        .map(|name| PackageItem {
212            name: name.clone(),
213            version: String::new(),
214            description: String::new(),
215            source: crate::state::Source::Official {
216                repo: String::new(),
217                arch: String::new(),
218            },
219            popularity: None,
220            out_of_date: None,
221            orphaned: false,
222        })
223        .collect();
224
225    // Remove operations always need sudo (pacman -R requires sudo regardless of package source).
226    // Remove is intentionally never passwordless for safety (see docstring).
227    // Check faillock status before showing password prompt
228    let username = std::env::var("USER").unwrap_or_else(|_| "user".to_string());
229    if let Some(lockout_msg) = crate::logic::faillock::get_lockout_message_if_locked(&username, app)
230    {
231        // User is locked out - show warning and don't show password prompt
232        app.modal = crate::state::Modal::Alert {
233            message: lockout_msg,
234        };
235        return;
236    }
237    let header_chips = PreflightHeaderChips::default();
238    let settings = crate::theme::settings();
239    if crate::logic::password::should_use_interactive_auth_handoff(&settings) {
240        match crate::events::try_interactive_auth_handoff() {
241            Ok(true) => {
242                start_execution(
243                    app,
244                    &items,
245                    crate::state::PreflightAction::Remove,
246                    header_chips,
247                    None,
248                );
249            }
250            Ok(false) => {
251                app.modal = crate::state::Modal::Alert {
252                    message: crate::i18n::t(app, "app.errors.authentication_failed"),
253                };
254            }
255            Err(e) => {
256                app.modal = crate::state::Modal::Alert { message: e };
257            }
258        }
259        return;
260    }
261
262    app.modal = crate::state::Modal::PasswordPrompt {
263        purpose: crate::state::modal::PasswordPurpose::Remove,
264        items,
265        input: crate::state::SecureString::default(),
266        cursor: 0,
267        error: None,
268    };
269    app.pending_exec_header_chips = Some(header_chips);
270}
271
272#[cfg(test)]
273mod tests {
274    use super::maybe_show_long_run_auth_preflight;
275
276    #[test]
277    fn preflight_warning_is_latched_once_per_session() {
278        let _guard = crate::global_test_mutex_lock();
279        unsafe {
280            std::env::set_var("PACSEA_INTEGRATION_TEST", "1");
281            std::env::set_var("PACSEA_TEST_PRIVILEGE_AVAILABLE", "none");
282        }
283        let mut app = crate::state::AppState::default();
284        assert!(!app.long_run_auth_preflight_warned);
285
286        maybe_show_long_run_auth_preflight(&mut app);
287        let first_toast = app.toast_message.clone();
288        assert!(app.long_run_auth_preflight_warned);
289        assert!(first_toast.is_some());
290
291        maybe_show_long_run_auth_preflight(&mut app);
292        assert_eq!(app.toast_message, first_toast);
293
294        unsafe {
295            std::env::remove_var("PACSEA_TEST_PRIVILEGE_AVAILABLE");
296            std::env::remove_var("PACSEA_INTEGRATION_TEST");
297        }
298    }
299}