1#[allow(unused_imports)]
2use std::process::Command;
3
4use crate::state::modal::CascadeMode;
5
6#[must_use]
19pub fn check_config_directories(package_name: &str, home: &str) -> Vec<std::path::PathBuf> {
20 use std::path::PathBuf;
21 let mut found_dirs = Vec::new();
22
23 let home_pkg_dir = PathBuf::from(home).join(package_name);
25 if home_pkg_dir.exists() && home_pkg_dir.is_dir() {
26 found_dirs.push(home_pkg_dir);
27 }
28
29 let config_pkg_dir = PathBuf::from(home).join(".config").join(package_name);
31 if config_pkg_dir.exists() && config_pkg_dir.is_dir() {
32 found_dirs.push(config_pkg_dir);
33 }
34
35 found_dirs
36}
37
38#[cfg(not(target_os = "windows"))]
39use super::utils::{choose_terminal_index_prefer_path, command_on_path, shell_single_quote};
40
41#[cfg(not(target_os = "windows"))]
42fn configure_terminal_env(cmd: &mut Command, term: &str) {
55 if term == "konsole" && std::env::var_os("WAYLAND_DISPLAY").is_some() {
56 cmd.env("QT_LOGGING_RULES", "qt.qpa.wayland.textinput=false");
57 }
58 if term == "gnome-console" || term == "kgx" {
59 cmd.env("GSK_RENDERER", "cairo");
60 cmd.env("LIBGL_ALWAYS_SOFTWARE", "1");
61 }
62}
63
64#[cfg(not(target_os = "windows"))]
65fn configure_test_env(cmd: &mut Command) {
76 if let Ok(p) = std::env::var("PACSEA_TEST_OUT") {
77 if let Some(parent) = std::path::Path::new(&p).parent() {
78 let _ = std::fs::create_dir_all(parent);
79 }
80 cmd.env("PACSEA_TEST_OUT", p);
81 }
82}
83
84#[cfg(not(target_os = "windows"))]
85struct SpawnContext<'a> {
90 names_str: &'a str,
92 names_len: usize,
94 dry_run: bool,
96 cascade_mode: CascadeMode,
98}
99
100#[cfg(not(target_os = "windows"))]
101fn try_spawn_terminal(
118 term: &str,
119 args: &[&str],
120 needs_xfce_command: bool,
121 cmd_str: &str,
122 ctx: &SpawnContext<'_>,
123) -> bool {
124 let mut cmd = Command::new(term);
125 if needs_xfce_command && term == "xfce4-terminal" {
126 let quoted = shell_single_quote(cmd_str);
127 cmd.arg("--command").arg(format!("bash -lc {quoted}"));
128 } else {
129 cmd.args(args.iter().copied()).arg(cmd_str);
130 }
131 configure_test_env(&mut cmd);
132 configure_terminal_env(&mut cmd, term);
133
134 match cmd.spawn() {
135 Ok(_) => {
136 tracing::info!(
137 terminal = %term,
138 names = %ctx.names_str,
139 total = ctx.names_len,
140 dry_run = ctx.dry_run,
141 mode = ?ctx.cascade_mode,
142 "launched terminal for removal"
143 );
144 true
145 }
146 Err(e) => {
147 tracing::warn!(
148 terminal = %term,
149 error = %e,
150 names = %ctx.names_str,
151 "failed to spawn terminal, trying next"
152 );
153 false
154 }
155 }
156}
157
158#[cfg(not(target_os = "windows"))]
159pub fn spawn_remove_all(names: &[String], dry_run: bool, cascade_mode: CascadeMode) {
171 #[cfg(test)]
173 if std::env::var("PACSEA_TEST_OUT").is_err() {
174 return;
175 }
176
177 let names_str = names.join(" ");
178 tracing::info!(
179 names = %names_str,
180 total = names.len(),
181 dry_run = dry_run,
182 mode = ?cascade_mode,
183 "spawning removal"
184 );
185 let flag = cascade_mode.flag();
186 let hold_tail = "; echo; echo 'Finished.'; echo 'Press any key to close...'; read -rn1 -s _ || (echo; echo 'Press Ctrl+C to close'; sleep infinity)";
187 let tool = match crate::logic::privilege::active_tool() {
188 Ok(t) => t,
189 Err(err) => {
190 tracing::error!(error = %err, "privilege tool resolution failed for removal");
191 return;
192 }
193 };
194 let base = format!("pacman {flag} --noconfirm {}", names.join(" "));
195 let cmd_str = if dry_run {
196 let cmd = format!(
197 "{}{hold_tail}",
198 crate::logic::privilege::build_privilege_command(tool, &base)
199 );
200 let quoted = shell_single_quote(&cmd);
201 format!("echo DRY RUN: {quoted}")
202 } else {
203 format!(
204 "{}{hold_tail}",
205 crate::logic::privilege::build_privilege_command(tool, &base)
206 )
207 };
208
209 let terms_gnome_first: &[(&str, &[&str], bool)] = &[
210 ("gnome-terminal", &["--", "bash", "-lc"], false),
211 ("gnome-console", &["--", "bash", "-lc"], false),
212 ("kgx", &["--", "bash", "-lc"], false),
213 ("alacritty", &["-e", "bash", "-lc"], false),
214 ("ghostty", &["-e", "bash", "-lc"], false),
215 ("kitty", &["bash", "-lc"], false),
216 ("konsole", &["-e", "bash", "-lc"], false),
217 ("xterm", &["-hold", "-e", "bash", "-lc"], false),
218 ("xfce4-terminal", &[], true),
219 ("tilix", &["--", "bash", "-lc"], false),
220 ("mate-terminal", &["--", "bash", "-lc"], false),
221 ];
222 let terms_default: &[(&str, &[&str], bool)] = &[
223 ("alacritty", &["-e", "bash", "-lc"], false),
224 ("ghostty", &["-e", "bash", "-lc"], false),
225 ("kitty", &["bash", "-lc"], false),
226 ("konsole", &["-e", "bash", "-lc"], false),
227 ("gnome-terminal", &["--", "bash", "-lc"], false),
228 ("gnome-console", &["--", "bash", "-lc"], false),
229 ("kgx", &["--", "bash", "-lc"], false),
230 ("xterm", &["-hold", "-e", "bash", "-lc"], false),
231 ("xfce4-terminal", &[], true),
232 ("tilix", &["--", "bash", "-lc"], false),
233 ("mate-terminal", &["--", "bash", "-lc"], false),
234 ];
235
236 let is_gnome = std::env::var("XDG_CURRENT_DESKTOP")
237 .ok()
238 .is_some_and(|v| v.to_uppercase().contains("GNOME"));
239 let terms = if is_gnome {
240 terms_gnome_first
241 } else {
242 terms_default
243 };
244
245 let ctx = SpawnContext {
246 names_str: &names_str,
247 names_len: names.len(),
248 dry_run,
249 cascade_mode,
250 };
251
252 let mut launched = choose_terminal_index_prefer_path(terms).is_some_and(|idx| {
253 let (term, args, needs_xfce_command) = terms[idx];
254 try_spawn_terminal(term, args, needs_xfce_command, &cmd_str, &ctx)
255 });
256
257 if !launched {
258 for (term, args, needs_xfce_command) in terms {
259 if command_on_path(term) {
260 launched = try_spawn_terminal(term, args, *needs_xfce_command, &cmd_str, &ctx);
261 if launched {
262 break;
263 }
264 }
265 }
266 }
267
268 if !launched {
269 let res = Command::new("bash").args(["-lc", &cmd_str]).spawn();
270 if let Err(e) = res {
271 tracing::error!(error = %e, names = %names_str, "failed to spawn bash to run removal command");
272 } else {
273 tracing::info!(
274 names = %names_str,
275 total = names.len(),
276 dry_run = dry_run,
277 mode = ?cascade_mode,
278 "launched bash for removal"
279 );
280 }
281 }
282}
283
284#[cfg(target_os = "windows")]
285#[allow(unused_variables, clippy::missing_const_for_fn)]
300pub fn spawn_remove_all(names: &[String], dry_run: bool, cascade_mode: CascadeMode) {
301 #[cfg(not(test))]
302 {
303 let mut names = names.to_vec();
304 if names.is_empty() {
305 names.push("nothing".into());
306 }
307 let names_str = names.join(" ");
308 let msg = if dry_run {
309 format!("DRY RUN: Would remove packages: {names_str}")
310 } else {
311 format!("Cannot remove packages on Windows: {names_str}")
312 };
313
314 if dry_run && super::utils::is_powershell_available() {
316 let escaped_msg = msg.replace('\'', "''");
318 let powershell_cmd = format!(
319 "Write-Host '{escaped_msg}' -ForegroundColor Yellow; Write-Host ''; Write-Host 'Press any key to close...'; $null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')"
320 );
321 let _ = std::process::Command::new("powershell.exe")
322 .args(["-NoProfile", "-Command", &powershell_cmd])
323 .spawn();
324 } else {
325 let _ = std::process::Command::new("cmd")
326 .args([
327 "/C",
328 "start",
329 "Pacsea Remove",
330 "cmd",
331 "/K",
332 &super::utils::cmd_echo_command(&msg),
333 ])
334 .spawn();
335 }
336 }
337}
338
339#[cfg(test)]
340mod tests {
341 #[test]
342 #[cfg(unix)]
343 fn remove_all_uses_gnome_terminal_double_dash() {
355 use std::fs;
356 use std::os::unix::fs::PermissionsExt;
357 use std::path::PathBuf;
358
359 let mut dir: PathBuf = std::env::temp_dir();
360 dir.push(format!(
361 "pacsea_test_remove_gnome_{}_{}",
362 std::process::id(),
363 std::time::SystemTime::now()
364 .duration_since(std::time::UNIX_EPOCH)
365 .expect("System time is before UNIX epoch")
366 .as_nanos()
367 ));
368 let _ = fs::create_dir_all(&dir);
369 let mut out_path = dir.clone();
370 out_path.push("args.txt");
371 let mut term_path = dir.clone();
372 term_path.push("gnome-terminal");
373 let script = "#!/bin/sh\n: > \"$PACSEA_TEST_OUT\"\nfor a in \"$@\"; do printf '%s\n' \"$a\" >> \"$PACSEA_TEST_OUT\"; done\n";
374 fs::write(&term_path, script.as_bytes()).expect("failed to write test terminal script");
375 let mut perms = fs::metadata(&term_path)
376 .expect("failed to read test terminal script metadata")
377 .permissions();
378 perms.set_mode(0o755);
379 fs::set_permissions(&term_path, perms)
380 .expect("failed to set test terminal script permissions");
381
382 let orig_path = std::env::var_os("PATH");
383 unsafe {
384 std::env::set_var("PATH", dir.display().to_string());
385 std::env::set_var("PACSEA_TEST_OUT", out_path.display().to_string());
386 }
387
388 let names = vec!["ripgrep".to_string(), "fd".to_string()];
389 super::spawn_remove_all(
390 &names,
391 true,
392 crate::state::modal::CascadeMode::CascadeWithConfigs,
393 );
394 std::thread::sleep(std::time::Duration::from_millis(50));
395
396 let body = fs::read_to_string(&out_path).expect("fake terminal args file written");
397 let lines: Vec<&str> = body.lines().collect();
398 assert!(lines.len() >= 3, "expected at least 3 args, got: {body}");
399 assert_eq!(lines[0], "--");
400 assert_eq!(lines[1], "bash");
401 assert_eq!(lines[2], "-lc");
402
403 unsafe {
404 if let Some(v) = orig_path {
405 std::env::set_var("PATH", v);
406 } else {
407 std::env::remove_var("PATH");
408 }
409 std::env::remove_var("PACSEA_TEST_OUT");
410 }
411 }
412}