Skip to main content

podbox/
editor.rs

1use std::path::{Path, PathBuf};
2
3use anyhow::Result;
4
5/// A resolved editor — binary path and any extra arguments needed.
6pub struct Editor {
7    pub bin: PathBuf,
8    pub args: Vec<String>,
9}
10
11/// Resolve the editor using the priority chain:
12/// `$PODBOX_EDITOR` > `$VISUAL` > `$EDITOR` > nvim > neovim > hx > helix > code > nano > vi.
13pub fn resolve() -> Result<Editor> {
14    let env_editor = std::env::var("PODBOX_EDITOR").ok();
15    let env_visual = std::env::var("VISUAL").ok();
16    let env_editor_generic = std::env::var("EDITOR").ok();
17
18    let candidates: Vec<(&str, Option<&str>)> = vec![
19        ("$PODBOX_EDITOR", env_editor.as_deref()),
20        ("$VISUAL", env_visual.as_deref()),
21        ("$EDITOR", env_editor_generic.as_deref()),
22        ("nvim", None),
23        ("neovim", None),
24        ("hx", None),
25        ("helix", None),
26        ("code", None),
27        ("nano", None),
28        ("vi", None),
29    ];
30
31    for (name, env_val) in &candidates {
32        let path = match env_val {
33            Some(val) => {
34                let parts = shell_words::split(val).unwrap_or_else(|_| vec![val.to_string()]);
35                let bin_part = parts.first().map(|s| s.as_str()).unwrap_or(val);
36                let p = PathBuf::from(bin_part);
37                if p.is_absolute() && p.exists() {
38                    p
39                } else {
40                    match which::which(bin_part) {
41                        Ok(p) => p,
42                        Err(_) => continue,
43                    }
44                }
45            }
46            None => match which::which(name) {
47                Ok(p) => p,
48                Err(_) => continue,
49            },
50        };
51
52        let mut args = editor_args(name);
53        if let Some(val) = env_val {
54            if let Ok(words) = shell_words::split(val) {
55                args.extend(words.into_iter().skip(1));
56            }
57        }
58        let editor = Editor { bin: path, args };
59        return Ok(editor);
60    }
61
62    anyhow::bail!(
63        "no editor found.\n\
64         Set $VISUAL, $EDITOR, or $PODBOX_EDITOR to your preferred editor.\n\
65         Example: export EDITOR=nano"
66    );
67}
68
69/// Return extra args needed for the given editor binary name.
70/// VS Code (`code` / `code-insiders`) needs `--wait`.
71fn editor_args(name: &str) -> Vec<String> {
72    let base = Path::new(name)
73        .file_name()
74        .map(|s| s.to_string_lossy().to_string())
75        .unwrap_or_else(|| name.to_string());
76    if base == "code" || base == "code-insiders" {
77        vec!["--wait".into()]
78    } else {
79        vec![]
80    }
81}
82
83/// Open the given file in the editor. Blocks until the editor exits.
84pub fn open(editor: &Editor, path: &Path) -> Result<()> {
85    let status = std::process::Command::new(&editor.bin)
86        .args(&editor.args)
87        .arg(path)
88        .status()?;
89
90    if !status.success() {
91        let code = status.code().unwrap_or(-1);
92        anyhow::bail!("editor exited with code {}", code);
93    }
94    Ok(())
95}