Skip to main content

pfx_editor_toml/
host.rs

1use std::fmt;
2use std::io::{self, ErrorKind};
3use std::path::Path;
4use std::process::{Command, Stdio};
5
6pub const NO_EDITOR: &str = "set $VISUAL or $EDITOR to open the file";
7
8#[derive(Clone, Debug, PartialEq, Eq)]
9pub enum NoHead {
10    NoGit,
11    NotRepo,
12    Untracked,
13    Failed(String),
14}
15
16impl fmt::Display for NoHead {
17    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18        match self {
19            NoHead::NoGit => f.write_str("git is not on PATH"),
20            NoHead::NotRepo => f.write_str("the file is not in a git repository"),
21            NoHead::Untracked => f.write_str("git HEAD has no such file"),
22            NoHead::Failed(why) => write!(f, "git HEAD cannot be read: {why}"),
23        }
24    }
25}
26
27pub trait Head {
28    fn show(&self, file: &Path) -> Result<String, NoHead>;
29}
30
31#[derive(Clone, Copy, Debug, Default)]
32pub struct Git;
33
34fn git(dir: &Path) -> Command {
35    let mut command = Command::new("git");
36    command.current_dir(dir).stdin(Stdio::null());
37    command
38}
39
40fn spawned(error: io::Error) -> NoHead {
41    if error.kind() == ErrorKind::NotFound {
42        NoHead::NoGit
43    } else {
44        NoHead::Failed(error.to_string())
45    }
46}
47
48impl Head for Git {
49    fn show(&self, file: &Path) -> Result<String, NoHead> {
50        let dir = file
51            .parent()
52            .filter(|dir| !dir.as_os_str().is_empty())
53            .unwrap_or(Path::new("."));
54        let name = file.file_name().ok_or(NoHead::Untracked)?;
55        if !dir.is_dir() {
56            return Err(NoHead::NotRepo);
57        }
58        let shown = git(dir)
59            .arg("show")
60            .arg(format!("HEAD:./{}", name.to_string_lossy()))
61            .output()
62            .map_err(spawned)?;
63        if shown.status.success() {
64            return String::from_utf8(shown.stdout)
65                .map_err(|_| NoHead::Failed("the file is not UTF-8".into()));
66        }
67        let inside = git(dir)
68            .args(["rev-parse", "--is-inside-work-tree"])
69            .output()
70            .map_err(spawned)?;
71        Err(if inside.status.success() {
72            NoHead::Untracked
73        } else {
74            NoHead::NotRepo
75        })
76    }
77}
78
79pub trait Opener {
80    fn command(&self) -> Option<String>;
81    fn open(&self, command: &str, file: &Path) -> io::Result<()>;
82}
83
84#[derive(Clone, Copy, Debug, Default)]
85pub struct Process;
86
87pub fn pick(visual: Option<String>, editor: Option<String>) -> Option<String> {
88    [visual, editor]
89        .into_iter()
90        .flatten()
91        .find(|value| !value.trim().is_empty())
92}
93
94impl Opener for Process {
95    fn command(&self) -> Option<String> {
96        pick(std::env::var("VISUAL").ok(), std::env::var("EDITOR").ok())
97    }
98
99    fn open(&self, command: &str, file: &Path) -> io::Result<()> {
100        #[cfg(unix)]
101        let mut run = {
102            let mut run = Command::new("sh");
103            run.arg("-c")
104                .arg(format!("{command} \"$1\""))
105                .arg("sh")
106                .arg(file);
107            run
108        };
109        #[cfg(windows)]
110        let mut run = {
111            let mut run = Command::new("cmd");
112            run.arg("/C").arg(command).arg(file);
113            run
114        };
115        let mut child = run
116            .stdin(Stdio::null())
117            .stdout(Stdio::null())
118            .stderr(Stdio::null())
119            .spawn()?;
120        std::thread::spawn(move || {
121            let _ = child.wait();
122        });
123        Ok(())
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn visual_wins_over_editor_and_blanks_are_skipped() {
133        let some = |text: &str| Some(text.to_string());
134        assert_eq!(pick(some("code -w"), some("vi")), some("code -w"));
135        assert_eq!(pick(None, some("vi")), some("vi"));
136        assert_eq!(pick(some("  "), some("vi")), some("vi"));
137        assert_eq!(pick(some(""), None), None);
138        assert_eq!(pick(None, None), None);
139    }
140
141    #[test]
142    fn a_missing_folder_is_not_a_repository() {
143        let gone = Path::new("tmp/no-such-folder/scene.toml");
144        assert_eq!(Git.show(gone), Err(NoHead::NotRepo));
145    }
146
147    #[test]
148    fn the_reasons_read_plainly() {
149        assert_eq!(NoHead::NoGit.to_string(), "git is not on PATH");
150        assert!(NoHead::NotRepo.to_string().contains("repository"));
151    }
152}