Skip to main content

worktree_io/opener/
mod.rs

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