Skip to main content

dkdc_sh/
lib.rs

1//! Shell utilities for tmux, git, and command management.
2//!
3//! Minimal, synchronous shell abstractions. No async runtime required.
4
5use std::fmt;
6use std::path::PathBuf;
7use std::process::Command;
8
9pub mod git;
10pub mod tmux;
11
12/// Shell operation errors.
13#[derive(Debug)]
14pub enum Error {
15    CommandNotFound(String),
16    CommandFailed { cmd: String, detail: String },
17    Tmux(String),
18    Io(std::io::Error),
19}
20
21impl fmt::Display for Error {
22    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23        match self {
24            Error::CommandNotFound(cmd) => write!(f, "command not found: {cmd}"),
25            Error::CommandFailed { cmd, detail } => write!(f, "command failed: {cmd} — {detail}"),
26            Error::Tmux(msg) => write!(f, "tmux error: {msg}"),
27            Error::Io(err) => write!(f, "io error: {err}"),
28        }
29    }
30}
31
32impl std::error::Error for Error {
33    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
34        match self {
35            Error::Io(err) => Some(err),
36            _ => None,
37        }
38    }
39}
40
41impl From<std::io::Error> for Error {
42    fn from(err: std::io::Error) -> Self {
43        Error::Io(err)
44    }
45}
46
47/// Check if a command exists in PATH.
48pub fn which(cmd: &str) -> Option<PathBuf> {
49    ::which::which(cmd).ok()
50}
51
52/// Require a command to exist, returning an error if not found.
53pub fn require(cmd: &str) -> Result<PathBuf, Error> {
54    ::which::which(cmd).map_err(|_| Error::CommandNotFound(cmd.to_string()))
55}
56
57/// Run a command and return its stdout.
58pub fn run(program: &str, args: &[&str]) -> Result<String, Error> {
59    run_with_env(program, args, &[])
60}
61
62/// Run a command with extra environment variables and return its stdout.
63pub fn run_with_env(program: &str, args: &[&str], env: &[(&str, &str)]) -> Result<String, Error> {
64    require(program)?;
65
66    let mut command = Command::new(program);
67    command.args(args);
68    for (k, v) in env {
69        command.env(k, v);
70    }
71    let output = command.output()?;
72
73    if !output.status.success() {
74        let stderr = String::from_utf8_lossy(&output.stderr).to_string();
75        return Err(Error::CommandFailed {
76            cmd: format!("{program} {}", args.first().unwrap_or(&"")),
77            detail: stderr,
78        });
79    }
80
81    Ok(String::from_utf8_lossy(&output.stdout).to_string())
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn test_which_exists() {
90        assert!(which("ls").is_some());
91    }
92
93    #[test]
94    fn test_which_not_exists() {
95        assert!(which("nonexistent_command_12345").is_none());
96    }
97
98    #[test]
99    fn test_require_exists() {
100        assert!(require("ls").is_ok());
101    }
102
103    #[test]
104    fn test_require_not_exists() {
105        let result = require("nonexistent_command_12345");
106        assert!(result.is_err());
107        assert!(matches!(result, Err(Error::CommandNotFound(_))));
108    }
109
110    #[test]
111    fn test_run() {
112        let output = run("echo", &["hello"]).unwrap();
113        assert_eq!(output.trim(), "hello");
114    }
115
116    #[test]
117    fn test_run_not_found() {
118        let result = run("nonexistent_command_12345", &[]);
119        assert!(matches!(result, Err(Error::CommandNotFound(_))));
120    }
121
122    #[test]
123    fn test_has_session_nonexistent() {
124        assert!(!tmux::has_session("dkdc_test_nonexistent_12345"));
125    }
126}