Skip to main content

worktree_io/opener/
mod.rs

1mod editor;
2mod shell;
3mod terminal;
4
5pub use editor::resolve_editor_command;
6pub use shell::augmented_path;
7
8use anyhow::{Context, Result};
9use std::path::Path;
10
11/// Check whether a macOS application bundle is installed.
12fn app_exists(name: &str) -> bool {
13    std::path::Path::new(&format!("/Applications/{name}.app")).exists()
14        || std::path::Path::new(&format!("/System/Applications/{name}.app")).exists()
15}
16
17/// For the IDE case: find an available terminal app and run `init_script` inside it.
18fn open_hook_in_auto_terminal(path: &Path, init_script: &str) -> Result<bool> {
19    let candidates: &[(&str, &str)] = &[
20        ("iTerm", "open -a iTerm ."),
21        ("Warp", "open -a Warp ."),
22        ("Ghostty", "open -a Ghostty ."),
23        ("Terminal", "open -a Terminal ."),
24    ];
25    for &(app, cmd) in candidates {
26        // LLVM_COV_EXCL_START
27        if app_exists(app) && terminal::try_terminal_with_init(path, cmd, init_script)? {
28            return Ok(true);
29        }
30        // LLVM_COV_EXCL_STOP
31    }
32    Ok(false)
33}
34
35/// Open `path` with `command` and run `init_script` inside the resulting window.
36///
37/// # Errors
38///
39/// Returns an error if the editor or terminal command fails to spawn.
40pub fn open_with_hook(path: &Path, command: &str, init_script: &str) -> Result<bool> {
41    if terminal::try_terminal_with_init(path, command, init_script)? {
42        // LLVM_COV_EXCL_START
43        return Ok(true);
44        // LLVM_COV_EXCL_STOP
45    }
46    open_in_editor(path, command)?;
47    open_hook_in_auto_terminal(path, init_script)
48}
49
50/// Open the workspace path in the configured editor.
51///
52/// # Errors
53///
54/// Returns an error if the workspace path is not valid UTF-8 or the editor
55/// command fails to spawn.
56pub fn open_in_editor(path: &Path, command: &str) -> Result<()> {
57    let path_str = path
58        .to_str()
59        .context("Workspace path contains non-UTF-8 characters")?;
60
61    let cmd_str = if command.contains(" . ") || command.ends_with(" .") || command == "." {
62        command.replacen(" .", &format!(" {path_str}"), 1)
63    } else {
64        format!("{command} {path_str}")
65    };
66
67    shell::run_shell_command(&cmd_str)
68        .with_context(|| format!("Failed to open editor with command: {cmd_str}"))
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74    #[test]
75    fn test_app_exists_nonexistent() {
76        assert!(!app_exists("__NoSuchApp__"));
77    }
78    #[test]
79    fn test_open_with_hook_ide_no_terminal() {
80        let p = std::path::Path::new("/tmp");
81        // "code ." is not a terminal, and no /Applications/iTerm.app etc in CI
82        let _ = open_with_hook(p, "echo .", "true");
83    }
84    #[test]
85    fn test_open_in_editor_dot_substitution() {
86        let p = std::path::Path::new("/tmp/myproject");
87        open_in_editor(p, "echo .").unwrap();
88    }
89    #[test]
90    fn test_open_in_editor_no_dot() {
91        let p = std::path::Path::new("/tmp/myproject");
92        open_in_editor(p, "echo").unwrap();
93    }
94}