1use anyhow::{bail, Context, Result};
2use std::path::Path;
3use std::process::{Command, Stdio};
4
5pub fn open_in_editor(path: &Path, command: &str) -> Result<()> {
9 let path_str = path
10 .to_str()
11 .context("Workspace path contains non-UTF-8 characters")?;
12
13 let cmd_str = if command.contains(" . ") || command.ends_with(" .") || command == "." {
15 command.replacen(" .", &format!(" {path_str}"), 1)
16 } else {
17 format!("{command} {path_str}")
18 };
19
20 run_shell_command(&cmd_str)
21 .with_context(|| format!("Failed to open editor with command: {cmd_str}"))
22}
23
24fn run_shell_command(cmd: &str) -> Result<()> {
26 let mut parts = shlex_split(cmd);
27 if parts.is_empty() {
28 bail!("Empty command");
29 }
30 let program = parts.remove(0);
31 Command::new(&program)
32 .args(&parts)
33 .env("PATH", augmented_path())
34 .stdin(Stdio::null())
35 .stdout(Stdio::null())
36 .stderr(Stdio::null())
37 .spawn()
38 .with_context(|| format!("Failed to spawn {program}"))?;
39 Ok(())
40}
41
42fn augmented_path() -> String {
45 let current = std::env::var("PATH").unwrap_or_default();
46 let extras = [
47 "/usr/local/bin",
48 "/opt/homebrew/bin",
49 "/opt/homebrew/sbin",
50 ];
51 let mut parts: Vec<&str> = extras.iter().copied().collect();
52 for p in current.split(':').filter(|s| !s.is_empty()) {
53 if !parts.contains(&p) {
54 parts.push(p);
55 }
56 }
57 parts.join(":")
58}
59
60fn shlex_split(s: &str) -> Vec<String> {
62 let mut parts = Vec::new();
63 let mut current = String::new();
64 let mut in_quotes = false;
65 let mut chars = s.chars().peekable();
66
67 while let Some(c) = chars.next() {
68 match c {
69 '"' => in_quotes = !in_quotes,
70 ' ' | '\t' if !in_quotes => {
71 if !current.is_empty() {
72 parts.push(current.clone());
73 current.clear();
74 }
75 }
76 _ => current.push(c),
77 }
78 }
79 if !current.is_empty() {
80 parts.push(current);
81 }
82 parts
83}