Skip to main content

worktree_io/opener/
mod.rs

1/// Return only the editors/terminals available on the current system.
2pub mod available_entries;
3mod editor;
4/// Unified table of all supported editors and terminals.
5pub mod entries;
6mod is_available;
7mod shell;
8mod terminal;
9#[cfg(windows)]
10mod wt;
11
12pub use editor::resolve_editor_command;
13pub use shell::augmented_path;
14
15use anyhow::{Context, Result};
16use std::path::Path;
17
18/// Check whether a macOS application bundle is installed.
19#[cfg(target_os = "macos")]
20fn app_exists(name: &str) -> bool {
21    std::path::Path::new(&format!("/Applications/{name}.app")).exists()
22        || std::path::Path::new(&format!("/System/Applications/{name}.app")).exists()
23}
24
25/// For the IDE case: find an available terminal app and run `init_script` inside it.
26fn open_hook_in_auto_terminal(path: &Path, init_script: &str) -> Result<bool> {
27    #[cfg(windows)]
28    if which::which("wt").is_ok() && terminal::try_terminal_with_init(path, "wt", init_script)? {
29        return Ok(true);
30    }
31    if which::which("tmux").is_ok() && terminal::try_terminal_with_init(path, "tmux", init_script)?
32    {
33        return Ok(true);
34    }
35    #[cfg(target_os = "macos")]
36    {
37        let candidates: &[(&str, &str)] = &[
38            ("iTerm", "open -a iTerm ."),
39            ("Warp", "open -a Warp ."),
40            ("Ghostty", "open -a Ghostty ."),
41            ("Terminal", "open -a Terminal ."),
42        ];
43        for &(app, cmd) in candidates {
44            // LLVM_COV_EXCL_START
45            if app_exists(app) && terminal::try_terminal_with_init(path, cmd, init_script)? {
46                return Ok(true);
47            }
48            // LLVM_COV_EXCL_STOP
49        }
50    }
51    Ok(false)
52}
53
54/// Open `path` with `command` and run `init_script` inside the resulting window.
55///
56/// # Errors
57///
58/// Returns an error if the editor or terminal command fails to spawn.
59pub fn open_with_hook(path: &Path, cmd: &str, init: &str, background: bool) -> Result<bool> {
60    if terminal::try_terminal_with_init(path, cmd, init)? {
61        return Ok(true);
62    }
63    open_in_editor(path, cmd, background)?;
64    open_hook_in_auto_terminal(path, init)
65}
66
67/// Open the workspace path in the configured editor.
68///
69/// # Errors
70///
71/// Returns an error if the workspace path is not valid UTF-8 or the editor
72/// command fails to spawn.
73pub fn open_in_editor(path: &Path, command: &str, background: bool) -> Result<()> {
74    let path_str = path
75        .to_str()
76        .context("Workspace path contains non-UTF-8 characters")?;
77    let cmd_str = if command.contains(" . ") || command.ends_with(" .") || command == "." {
78        command.replacen(" .", &format!(" {path_str}"), 1)
79    } else {
80        format!("{command} {path_str}")
81    };
82
83    shell::run_shell_command(&cmd_str, background)
84        .with_context(|| format!("Failed to open editor with command: {cmd_str}"))
85}
86
87/// Open `path` with `cmd`; uses terminal-specific logic if `cmd` is a known terminal,
88/// otherwise opens as an editor.
89/// # Errors
90/// Returns an error if the spawn or editor command fails.
91pub fn open_editor_or_terminal(path: &Path, cmd: &str, background: bool) -> Result<()> {
92    if !terminal::try_terminal_with_init(path, cmd, "")? {
93        open_in_editor(path, cmd, background)?;
94    }
95    Ok(())
96}
97
98#[cfg(test)]
99#[path = "opener_tests.rs"]
100mod tests;