1#[allow(unused_imports)]
2use std::process::Command;
3
4use crate::state::PackageItem;
5#[cfg(not(target_os = "windows"))]
6use crate::state::Source;
7
8#[cfg(not(target_os = "windows"))]
9use super::command::build_install_command;
10#[cfg(all(target_os = "windows", not(test)))]
11use super::command::build_install_command;
12#[cfg(not(target_os = "windows"))]
13use super::logging::log_installed;
14#[cfg(not(target_os = "windows"))]
15use super::utils::{choose_terminal_index_prefer_path, command_on_path, shell_single_quote};
16
17#[cfg(not(target_os = "windows"))]
18fn try_spawn_terminal(
35 term: &str,
36 args: &[&str],
37 needs_xfce_command: bool,
38 cmd_str: &str,
39 item_name: &str,
40 src: &str,
41 dry_run: bool,
42) -> bool {
43 let mut cmd = Command::new(term);
44 if needs_xfce_command && term == "xfce4-terminal" {
45 let quoted = shell_single_quote(cmd_str);
46 cmd.arg("--command").arg(format!("bash -lc {quoted}"));
47 } else {
48 cmd.args(args.iter().copied()).arg(cmd_str);
49 }
50 if let Ok(p) = std::env::var("PACSEA_TEST_OUT") {
51 if let Some(parent) = std::path::Path::new(&p).parent() {
52 let _ = std::fs::create_dir_all(parent);
53 }
54 cmd.env("PACSEA_TEST_OUT", p);
55 }
56 match cmd.spawn() {
57 Ok(_) => {
58 tracing::info!(
59 terminal = %term,
60 names = %item_name,
61 total = 1,
62 aur_count = usize::from(src == "aur"),
63 official_count = usize::from(src == "official"),
64 dry_run,
65 "launched terminal for install"
66 );
67 true
68 }
69 Err(e) => {
70 tracing::warn!(
71 terminal = %term,
72 error = %e,
73 names = %item_name,
74 "failed to spawn terminal, trying next"
75 );
76 false
77 }
78 }
79}
80
81#[cfg(not(target_os = "windows"))]
82fn get_terminal_preferences() -> &'static [(&'static str, &'static [&'static str], bool)] {
93 let is_gnome = std::env::var("XDG_CURRENT_DESKTOP")
94 .ok()
95 .is_some_and(|v| v.to_uppercase().contains("GNOME"));
96 if is_gnome {
97 &[
98 ("gnome-terminal", &["--", "bash", "-lc"], false),
99 ("gnome-console", &["--", "bash", "-lc"], false),
100 ("kgx", &["--", "bash", "-lc"], false),
101 ("alacritty", &["-e", "bash", "-lc"], false),
102 ("kitty", &["bash", "-lc"], false),
103 ("xterm", &["-hold", "-e", "bash", "-lc"], false),
104 ("konsole", &["-e", "bash", "-lc"], false),
105 ("xfce4-terminal", &[], true),
106 ("tilix", &["--", "bash", "-lc"], false),
107 ("mate-terminal", &["--", "bash", "-lc"], false),
108 ]
109 } else {
110 &[
111 ("alacritty", &["-e", "bash", "-lc"], false),
112 ("kitty", &["bash", "-lc"], false),
113 ("xterm", &["-hold", "-e", "bash", "-lc"], false),
114 ("gnome-terminal", &["--", "bash", "-lc"], false),
115 ("gnome-console", &["--", "bash", "-lc"], false),
116 ("kgx", &["--", "bash", "-lc"], false),
117 ("konsole", &["-e", "bash", "-lc"], false),
118 ("xfce4-terminal", &[], true),
119 ("tilix", &["--", "bash", "-lc"], false),
120 ("mate-terminal", &["--", "bash", "-lc"], false),
121 ]
122 }
123}
124
125#[cfg(not(target_os = "windows"))]
126pub fn spawn_install(item: &PackageItem, password: Option<&str>, dry_run: bool) {
138 #[cfg(test)]
140 if std::env::var("PACSEA_TEST_OUT").is_err() {
141 return;
142 }
143
144 let (cmd_str, uses_sudo) = build_install_command(item, password, dry_run);
145 let src = match item.source {
146 Source::Official { .. } => "official",
147 Source::Aur => "aur",
148 };
149 tracing::info!(
150 names = %item.name,
151 total = 1,
152 aur_count = usize::from(src == "aur"),
153 official_count = usize::from(src == "official"),
154 dry_run = dry_run,
155 uses_sudo,
156 "spawning install"
157 );
158
159 let terms = get_terminal_preferences();
160
161 let mut launched = choose_terminal_index_prefer_path(terms).is_some_and(|idx| {
163 let (term, args, needs_xfce_command) = terms[idx];
164 try_spawn_terminal(
165 term,
166 args,
167 needs_xfce_command,
168 &cmd_str,
169 &item.name,
170 src,
171 dry_run,
172 )
173 });
174
175 if !launched {
177 for (term, args, needs_xfce_command) in terms {
178 if command_on_path(term) {
179 launched = try_spawn_terminal(
180 term,
181 args,
182 *needs_xfce_command,
183 &cmd_str,
184 &item.name,
185 src,
186 dry_run,
187 );
188 if launched {
189 break;
190 }
191 }
192 }
193 }
194
195 if !launched {
197 let res = Command::new("bash").args(["-lc", &cmd_str]).spawn();
198 if let Err(e) = res {
199 tracing::error!(error = %e, names = %item.name, "failed to spawn bash to run install command");
200 } else {
201 tracing::info!(
202 names = %item.name,
203 total = 1,
204 aur_count = usize::from(src == "aur"),
205 official_count = usize::from(src == "official"),
206 dry_run = dry_run,
207 "launched bash for install"
208 );
209 }
210 }
211
212 if !dry_run && let Err(e) = log_installed(std::slice::from_ref(&item.name)) {
213 tracing::warn!(error = %e, names = %item.name, "failed to write install audit log");
214 }
215}
216
217#[cfg(all(test, not(target_os = "windows")))]
218mod tests {
219 #[test]
220 fn install_single_uses_gnome_terminal_double_dash() {
232 use std::fs;
233 use std::os::unix::fs::PermissionsExt;
234 use std::path::PathBuf;
235
236 let mut dir: PathBuf = std::env::temp_dir();
237 dir.push(format!(
238 "pacsea_test_inst_single_gnome_{}_{}",
239 std::process::id(),
240 std::time::SystemTime::now()
241 .duration_since(std::time::UNIX_EPOCH)
242 .expect("System time is before UNIX epoch")
243 .as_nanos()
244 ));
245 fs::create_dir_all(&dir).expect("Failed to create test directory");
246 let mut out_path = dir.clone();
247 out_path.push("args.txt");
248 let mut term_path = dir.clone();
249 term_path.push("gnome-terminal");
250 let script = "#!/bin/sh\n: > \"$PACSEA_TEST_OUT\"\nfor a in \"$@\"; do printf '%s\n' \"$a\" >> \"$PACSEA_TEST_OUT\"; done\n";
251 fs::write(&term_path, script.as_bytes()).expect("Failed to write test terminal script");
252 let mut perms = fs::metadata(&term_path)
253 .expect("Failed to read test terminal script metadata")
254 .permissions();
255 perms.set_mode(0o755);
256 fs::set_permissions(&term_path, perms)
257 .expect("Failed to set test terminal script permissions");
258
259 let orig_path = std::env::var_os("PATH");
260 unsafe {
261 std::env::set_var("PATH", dir.display().to_string());
262 std::env::set_var("PACSEA_TEST_OUT", out_path.display().to_string());
263 }
264
265 let pkg = crate::state::PackageItem {
266 name: "ripgrep".into(),
267 version: "1".into(),
268 description: String::new(),
269 source: crate::state::Source::Official {
270 repo: "extra".into(),
271 arch: "x86_64".into(),
272 },
273 popularity: None,
274 out_of_date: None,
275 orphaned: false,
276 };
277 super::spawn_install(&pkg, None, true);
278 std::thread::sleep(std::time::Duration::from_millis(50));
279
280 let body = fs::read_to_string(&out_path).expect("fake terminal args file written");
281 let lines: Vec<&str> = body.lines().collect();
282 assert!(lines.len() >= 3, "expected at least 3 args, got: {body}");
283 assert_eq!(lines[0], "--");
284 assert_eq!(lines[1], "bash");
285 assert_eq!(lines[2], "-lc");
286
287 unsafe {
288 if let Some(v) = orig_path {
289 std::env::set_var("PATH", v);
290 } else {
291 std::env::remove_var("PATH");
292 }
293 std::env::remove_var("PACSEA_TEST_OUT");
294 }
295 }
296}
297
298#[cfg(target_os = "windows")]
299#[allow(unused_variables, clippy::missing_const_for_fn)]
314pub fn spawn_install(item: &PackageItem, password: Option<&str>, dry_run: bool) {
315 #[cfg(not(test))]
316 {
317 let (cmd_str, _uses_sudo) = build_install_command(item, password, dry_run);
318
319 if dry_run && super::utils::is_powershell_available() {
320 let powershell_cmd = format!(
322 "Write-Host 'DRY RUN: Simulating install of {}' -ForegroundColor Yellow; Write-Host 'Command: {}' -ForegroundColor Cyan; Write-Host ''; Write-Host 'Press any key to close...'; $null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')",
323 item.name,
324 cmd_str.replace('\'', "''")
325 );
326 let _ = Command::new("powershell.exe")
327 .args(["-NoProfile", "-Command", &powershell_cmd])
328 .spawn();
329 } else {
330 let _ = Command::new("cmd")
331 .args(["/C", "start", "Pacsea Install", "cmd", "/K", &cmd_str])
332 .spawn();
333 }
334
335 if !dry_run {
336 let _ = super::logging::log_installed(std::slice::from_ref(&item.name));
337 }
338 }
339}