Skip to main content

worktree_io/
opener.rs

1use anyhow::{bail, Context, Result};
2use std::path::Path;
3use std::process::Command;
4
5/// Open the workspace path in the configured editor.
6/// `command` is a shell template, e.g. `"code ."` or `"nvim ."`.
7/// The trailing `.` (or any `.`) in the command is replaced by the actual path.
8pub 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    // Replace standalone `.` tokens with the actual path, or append it
14    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
24/// Split a command string on whitespace and run it.
25fn 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        .spawn()
34        .with_context(|| format!("Failed to spawn {program}"))?;
35    Ok(())
36}
37
38/// Very simple whitespace-based command splitter that respects double-quoted strings.
39fn shlex_split(s: &str) -> Vec<String> {
40    let mut parts = Vec::new();
41    let mut current = String::new();
42    let mut in_quotes = false;
43    let mut chars = s.chars().peekable();
44
45    while let Some(c) = chars.next() {
46        match c {
47            '"' => in_quotes = !in_quotes,
48            ' ' | '\t' if !in_quotes => {
49                if !current.is_empty() {
50                    parts.push(current.clone());
51                    current.clear();
52                }
53            }
54            _ => current.push(c),
55        }
56    }
57    if !current.is_empty() {
58        parts.push(current);
59    }
60    parts
61}