Skip to main content

ite_cli/
runner.rs

1//! Executes `sh` keybindings with `$path` / `$relpath` in the environment.
2
3use std::ffi::OsStr;
4use std::io::IsTerminal;
5use std::process::{Command, ExitStatus, Stdio};
6
7/// Run `cmd` through `sh -c`. The focused node's action values are exported as
8/// `$path` and `$relpath`.
9///
10/// Foreground commands inherit the terminal and their exit status is returned;
11/// background commands are detached from stdio and `None` is returned.
12pub fn run_shell(
13    cmd: &str,
14    path: &OsStr,
15    relpath: &OsStr,
16    bg: bool,
17) -> std::io::Result<Option<ExitStatus>> {
18    let mut command = Command::new("sh");
19    command
20        .arg("-c")
21        .arg(cmd)
22        .env("path", path)
23        .env("relpath", relpath);
24    if bg {
25        command
26            .stdin(Stdio::null())
27            .stdout(Stdio::null())
28            .stderr(Stdio::null())
29            .spawn()?;
30        Ok(None)
31    } else {
32        // When JSON was piped in, fd 0 is an exhausted pipe; hand interactive
33        // commands the terminal instead, as fzf's execute action does.
34        if !std::io::stdin().is_terminal()
35            && let Ok(tty) = std::fs::File::open("/dev/tty")
36        {
37            command.stdin(Stdio::from(tty));
38        }
39        command.status().map(Some)
40    }
41}
42
43#[cfg(test)]
44mod tests {
45    use super::*;
46
47    #[test]
48    fn foreground_command_sees_path_env_vars() {
49        let dir = tempfile::tempdir().unwrap();
50        let out = dir.path().join("out");
51        let status = run_shell(
52            &format!(
53                "printf '%s\\n%s' \"$path\" \"$relpath\" > {}",
54                out.display()
55            ),
56            OsStr::new("/abs/some/file.txt"),
57            OsStr::new("some/file.txt"),
58            false,
59        )
60        .unwrap()
61        .expect("foreground returns a status");
62        assert!(status.success());
63        assert_eq!(
64            std::fs::read_to_string(out).unwrap(),
65            "/abs/some/file.txt\nsome/file.txt"
66        );
67    }
68
69    #[test]
70    fn foreground_reports_failure_status() {
71        let status = run_shell("exit 3", OsStr::new("/x"), OsStr::new("x"), false)
72            .unwrap()
73            .unwrap();
74        assert_eq!(status.code(), Some(3));
75    }
76
77    #[test]
78    fn background_command_detaches() {
79        let dir = tempfile::tempdir().unwrap();
80        let out = dir.path().join("out");
81        let result = run_shell(
82            &format!("echo \"$relpath\" > {}", out.display()),
83            OsStr::new("/abs/f"),
84            OsStr::new("f"),
85            true,
86        )
87        .unwrap();
88        assert!(result.is_none());
89        // The detached process still runs to completion.
90        for _ in 0..50 {
91            if out.exists() {
92                break;
93            }
94            std::thread::sleep(std::time::Duration::from_millis(10));
95        }
96        assert_eq!(std::fs::read_to_string(out).unwrap().trim(), "f");
97    }
98}