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 cmd_str = if dry_run {
188 let cmd = format!(
189 "sudo pacman {flag} --noconfirm {n}{hold}",
190 flag = flag,
191 n = names.join(" "),
192 hold = hold_tail
193 );
194 let quoted = shell_single_quote(&cmd);
195 format!("echo DRY RUN: {quoted}")
196 } else {
197 format!(
198 "sudo pacman {flag} --noconfirm {n}{hold}",
199 flag = flag,
200 n = names.join(" "),
201 hold = hold_tail
202 )
203 };
204
205 let terms_gnome_first: &[(&str, &[&str], bool)] = &[
206 ("gnome-terminal", &["--", "bash", "-lc"], false),
207 ("gnome-console", &["--", "bash", "-lc"], false),
208 ("kgx", &["--", "bash", "-lc"], false),
209 ("alacritty", &["-e", "bash", "-lc"], false),
210 ("ghostty", &["-e", "bash", "-lc"], false),
211 ("kitty", &["bash", "-lc"], false),
212 ("xterm", &["-hold", "-e", "bash", "-lc"], false),
213 ("konsole", &["-e", "bash", "-lc"], false),
214 ("xfce4-terminal", &[], true),
215 ("tilix", &["--", "bash", "-lc"], false),
216 ("mate-terminal", &["--", "bash", "-lc"], false),
217 ];
218 let terms_default: &[(&str, &[&str], bool)] = &[
219 ("alacritty", &["-e", "bash", "-lc"], false),
220 ("ghostty", &["-e", "bash", "-lc"], false),
221 ("kitty", &["bash", "-lc"], false),
222 ("xterm", &["-hold", "-e", "bash", "-lc"], false),
223 ("gnome-terminal", &["--", "bash", "-lc"], false),
224 ("gnome-console", &["--", "bash", "-lc"], false),
225 ("kgx", &["--", "bash", "-lc"], false),
226 ("konsole", &["-e", "bash", "-lc"], false),
227 ("xfce4-terminal", &[], true),
228 ("tilix", &["--", "bash", "-lc"], false),
229 ("mate-terminal", &["--", "bash", "-lc"], false),
230 ];
231
232 let is_gnome = std::env::var("XDG_CURRENT_DESKTOP")
233 .ok()
234 .is_some_and(|v| v.to_uppercase().contains("GNOME"));
235 let terms = if is_gnome {
236 terms_gnome_first
237 } else {
238 terms_default
239 };
240
241 let ctx = SpawnContext {
242 names_str: &names_str,
243 names_len: names.len(),
244 dry_run,
245 cascade_mode,
246 };
247
248 let mut launched = choose_terminal_index_prefer_path(terms).is_some_and(|idx| {
249 let (term, args, needs_xfce_command) = terms[idx];
250 try_spawn_terminal(term, args, needs_xfce_command, &cmd_str, &ctx)
251 });
252
253 if !launched {
254 for (term, args, needs_xfce_command) in terms {
255 if command_on_path(term) {
256 launched = try_spawn_terminal(term, args, *needs_xfce_command, &cmd_str, &ctx);
257 if launched {
258 break;
259 }
260 }
261 }
262 }
263
264 if !launched {
265 let res = Command::new("bash").args(["-lc", &cmd_str]).spawn();
266 if let Err(e) = res {
267 tracing::error!(error = %e, names = %names_str, "failed to spawn bash to run removal command");
268 } else {
269 tracing::info!(
270 names = %names_str,
271 total = names.len(),
272 dry_run = dry_run,
273 mode = ?cascade_mode,
274 "launched bash for removal"
275 );
276 }
277 }
278}
279
280#[cfg(target_os = "windows")]
281#[allow(unused_variables, clippy::missing_const_for_fn)]
296pub fn spawn_remove_all(names: &[String], dry_run: bool, cascade_mode: CascadeMode) {
297 #[cfg(not(test))]
298 {
299 let mut names = names.to_vec();
300 if names.is_empty() {
301 names.push("nothing".into());
302 }
303 let names_str = names.join(" ");
304 let msg = if dry_run {
305 format!("DRY RUN: Would remove packages: {names_str}")
306 } else {
307 format!("Cannot remove packages on Windows: {names_str}")
308 };
309
310 if dry_run && super::utils::is_powershell_available() {
312 let escaped_msg = msg.replace('\'', "''");
314 let powershell_cmd = format!(
315 "Write-Host '{escaped_msg}' -ForegroundColor Yellow; Write-Host ''; Write-Host 'Press any key to close...'; $null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')"
316 );
317 let _ = std::process::Command::new("powershell.exe")
318 .args(["-NoProfile", "-Command", &powershell_cmd])
319 .spawn();
320 } else {
321 let _ = std::process::Command::new("cmd")
322 .args([
323 "/C",
324 "start",
325 "Pacsea Remove",
326 "cmd",
327 "/K",
328 &format!("echo {msg}"),
329 ])
330 .spawn();
331 }
332 }
333}
334
335#[cfg(test)]
336mod tests {
337 #[test]
338 #[cfg(unix)]
339 fn remove_all_uses_gnome_terminal_double_dash() {
351 use std::fs;
352 use std::os::unix::fs::PermissionsExt;
353 use std::path::PathBuf;
354
355 let mut dir: PathBuf = std::env::temp_dir();
356 dir.push(format!(
357 "pacsea_test_remove_gnome_{}_{}",
358 std::process::id(),
359 std::time::SystemTime::now()
360 .duration_since(std::time::UNIX_EPOCH)
361 .expect("System time is before UNIX epoch")
362 .as_nanos()
363 ));
364 let _ = fs::create_dir_all(&dir);
365 let mut out_path = dir.clone();
366 out_path.push("args.txt");
367 let mut term_path = dir.clone();
368 term_path.push("gnome-terminal");
369 let script = "#!/bin/sh\n: > \"$PACSEA_TEST_OUT\"\nfor a in \"$@\"; do printf '%s\n' \"$a\" >> \"$PACSEA_TEST_OUT\"; done\n";
370 fs::write(&term_path, script.as_bytes()).expect("failed to write test terminal script");
371 let mut perms = fs::metadata(&term_path)
372 .expect("failed to read test terminal script metadata")
373 .permissions();
374 perms.set_mode(0o755);
375 fs::set_permissions(&term_path, perms)
376 .expect("failed to set test terminal script permissions");
377
378 let orig_path = std::env::var_os("PATH");
379 unsafe {
380 std::env::set_var("PATH", dir.display().to_string());
381 std::env::set_var("PACSEA_TEST_OUT", out_path.display().to_string());
382 }
383
384 let names = vec!["ripgrep".to_string(), "fd".to_string()];
385 super::spawn_remove_all(
386 &names,
387 true,
388 crate::state::modal::CascadeMode::CascadeWithConfigs,
389 );
390 std::thread::sleep(std::time::Duration::from_millis(50));
391
392 let body = fs::read_to_string(&out_path).expect("fake terminal args file written");
393 let lines: Vec<&str> = body.lines().collect();
394 assert!(lines.len() >= 3, "expected at least 3 args, got: {body}");
395 assert_eq!(lines[0], "--");
396 assert_eq!(lines[1], "bash");
397 assert_eq!(lines[2], "-lc");
398
399 unsafe {
400 if let Some(v) = orig_path {
401 std::env::set_var("PATH", v);
402 } else {
403 std::env::remove_var("PATH");
404 }
405 std::env::remove_var("PACSEA_TEST_OUT");
406 }
407 }
408}