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) = match build_install_command(item, password, dry_run) {
145 Ok(v) => v,
146 Err(err) => {
147 tracing::error!(error = %err, names = %item.name, "privilege tool resolution failed for install");
148 return;
149 }
150 };
151 let src = match item.source {
152 Source::Official { .. } => "official",
153 Source::Aur => "aur",
154 };
155 tracing::info!(
156 names = %item.name,
157 total = 1,
158 aur_count = usize::from(src == "aur"),
159 official_count = usize::from(src == "official"),
160 dry_run = dry_run,
161 uses_sudo,
162 "spawning install"
163 );
164
165 let terms = get_terminal_preferences();
166
167 let mut launched = choose_terminal_index_prefer_path(terms).is_some_and(|idx| {
169 let (term, args, needs_xfce_command) = terms[idx];
170 try_spawn_terminal(
171 term,
172 args,
173 needs_xfce_command,
174 &cmd_str,
175 &item.name,
176 src,
177 dry_run,
178 )
179 });
180
181 if !launched {
183 for (term, args, needs_xfce_command) in terms {
184 if command_on_path(term) {
185 launched = try_spawn_terminal(
186 term,
187 args,
188 *needs_xfce_command,
189 &cmd_str,
190 &item.name,
191 src,
192 dry_run,
193 );
194 if launched {
195 break;
196 }
197 }
198 }
199 }
200
201 if !launched {
203 let res = Command::new("bash").args(["-lc", &cmd_str]).spawn();
204 if let Err(e) = res {
205 tracing::error!(error = %e, names = %item.name, "failed to spawn bash to run install command");
206 } else {
207 tracing::info!(
208 names = %item.name,
209 total = 1,
210 aur_count = usize::from(src == "aur"),
211 official_count = usize::from(src == "official"),
212 dry_run = dry_run,
213 "launched bash for install"
214 );
215 }
216 }
217
218 if !dry_run && let Err(e) = log_installed(std::slice::from_ref(&item.name)) {
219 tracing::warn!(error = %e, names = %item.name, "failed to write install audit log");
220 }
221}
222
223#[cfg(all(test, not(target_os = "windows")))]
224mod tests {
225 #[test]
226 fn install_single_uses_gnome_terminal_double_dash() {
238 use std::fs;
239 use std::os::unix::fs::PermissionsExt;
240 use std::path::PathBuf;
241
242 let mut dir: PathBuf = std::env::temp_dir();
243 dir.push(format!(
244 "pacsea_test_inst_single_gnome_{}_{}",
245 std::process::id(),
246 std::time::SystemTime::now()
247 .duration_since(std::time::UNIX_EPOCH)
248 .expect("System time is before UNIX epoch")
249 .as_nanos()
250 ));
251 fs::create_dir_all(&dir).expect("Failed to create test directory");
252 let mut out_path = dir.clone();
253 out_path.push("args.txt");
254 let mut term_path = dir.clone();
255 term_path.push("gnome-terminal");
256 let script = "#!/bin/sh\n: > \"$PACSEA_TEST_OUT\"\nfor a in \"$@\"; do printf '%s\n' \"$a\" >> \"$PACSEA_TEST_OUT\"; done\n";
257 fs::write(&term_path, script.as_bytes()).expect("Failed to write test terminal script");
258 let mut perms = fs::metadata(&term_path)
259 .expect("Failed to read test terminal script metadata")
260 .permissions();
261 perms.set_mode(0o755);
262 fs::set_permissions(&term_path, perms)
263 .expect("Failed to set test terminal script permissions");
264
265 let orig_path = std::env::var_os("PATH");
266 unsafe {
267 std::env::set_var("PATH", dir.display().to_string());
268 std::env::set_var("PACSEA_TEST_OUT", out_path.display().to_string());
269 }
270
271 let pkg = crate::state::PackageItem {
272 name: "ripgrep".into(),
273 version: "1".into(),
274 description: String::new(),
275 source: crate::state::Source::Official {
276 repo: "extra".into(),
277 arch: "x86_64".into(),
278 },
279 popularity: None,
280 out_of_date: None,
281 orphaned: false,
282 };
283 super::spawn_install(&pkg, None, true);
284 std::thread::sleep(std::time::Duration::from_millis(50));
285
286 let body = fs::read_to_string(&out_path).expect("fake terminal args file written");
287 let lines: Vec<&str> = body.lines().collect();
288 assert!(lines.len() >= 3, "expected at least 3 args, got: {body}");
289 assert_eq!(lines[0], "--");
290 assert_eq!(lines[1], "bash");
291 assert_eq!(lines[2], "-lc");
292
293 unsafe {
294 if let Some(v) = orig_path {
295 std::env::set_var("PATH", v);
296 } else {
297 std::env::remove_var("PATH");
298 }
299 std::env::remove_var("PACSEA_TEST_OUT");
300 }
301 }
302}
303
304#[cfg(target_os = "windows")]
305#[allow(unused_variables, clippy::missing_const_for_fn)]
320pub fn spawn_install(item: &PackageItem, password: Option<&str>, dry_run: bool) {
321 #[cfg(not(test))]
322 {
323 let (cmd_str, _uses_sudo) = match build_install_command(item, password, dry_run) {
324 Ok(v) => v,
325 Err(err) => {
326 tracing::error!(error = %err, "privilege tool resolution failed for install");
327 return;
328 }
329 };
330
331 if dry_run && super::utils::is_powershell_available() {
332 let powershell_cmd = format!(
334 "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')",
335 item.name,
336 cmd_str.replace('\'', "''")
337 );
338 let _ = Command::new("powershell.exe")
339 .args(["-NoProfile", "-Command", &powershell_cmd])
340 .spawn();
341 } else {
342 let _ = Command::new("cmd")
343 .args(["/C", "start", "Pacsea Install", "cmd", "/K", &cmd_str])
344 .spawn();
345 }
346
347 if !dry_run {
348 let _ = super::logging::log_installed(std::slice::from_ref(&item.name));
349 }
350 }
351}