1use std::fmt;
6use std::path::PathBuf;
7use std::process::Command;
8
9pub mod editor;
10pub mod git;
11pub mod tmux;
12
13#[derive(Debug)]
15pub enum Error {
16 CommandNotFound(String),
17 CommandFailed { cmd: String, detail: String },
18 Tmux(String),
19 Io(std::io::Error),
20}
21
22impl fmt::Display for Error {
23 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24 match self {
25 Error::CommandNotFound(cmd) => write!(f, "command not found: {cmd}"),
26 Error::CommandFailed { cmd, detail } => write!(f, "command failed: {cmd} — {detail}"),
27 Error::Tmux(msg) => write!(f, "tmux error: {msg}"),
28 Error::Io(err) => write!(f, "io error: {err}"),
29 }
30 }
31}
32
33impl std::error::Error for Error {
34 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
35 match self {
36 Error::Io(err) => Some(err),
37 _ => None,
38 }
39 }
40}
41
42impl From<std::io::Error> for Error {
43 fn from(err: std::io::Error) -> Self {
44 Error::Io(err)
45 }
46}
47
48pub fn which(cmd: &str) -> Option<PathBuf> {
50 ::which::which(cmd).ok()
51}
52
53pub fn require(cmd: &str) -> Result<PathBuf, Error> {
55 ::which::which(cmd).map_err(|_| Error::CommandNotFound(cmd.to_string()))
56}
57
58pub fn run(program: &str, args: &[&str]) -> Result<String, Error> {
60 run_with_env(program, args, &[])
61}
62
63pub fn run_with_env(program: &str, args: &[&str], env: &[(&str, &str)]) -> Result<String, Error> {
65 require(program)?;
66
67 let mut command = Command::new(program);
68 command.args(args);
69 for (k, v) in env {
70 command.env(k, v);
71 }
72 let output = command.output()?;
73
74 if !output.status.success() {
75 let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
76 return Err(Error::CommandFailed {
77 cmd: format!("{program} {}", args.first().unwrap_or(&"")),
78 detail: stderr,
79 });
80 }
81
82 Ok(String::from_utf8_lossy(&output.stdout).into_owned())
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 #[test]
90 fn test_which_exists() {
91 assert!(which("ls").is_some());
92 }
93
94 #[test]
95 fn test_which_not_exists() {
96 assert!(which("nonexistent_command_12345").is_none());
97 }
98
99 #[test]
100 fn test_require_exists() {
101 assert!(require("ls").is_ok());
102 }
103
104 #[test]
105 fn test_require_not_exists() {
106 let result = require("nonexistent_command_12345");
107 assert!(result.is_err());
108 assert!(matches!(result, Err(Error::CommandNotFound(_))));
109 }
110
111 #[test]
112 fn test_run() {
113 let output = run("echo", &["hello"]).unwrap();
114 assert_eq!(output.trim(), "hello");
115 }
116
117 #[test]
118 fn test_run_not_found() {
119 let result = run("nonexistent_command_12345", &[]);
120 assert!(matches!(result, Err(Error::CommandNotFound(_))));
121 }
122
123 #[test]
124 fn test_run_failed_command() {
125 let result = run("ls", &["/nonexistent_path_12345"]);
126 assert!(matches!(result, Err(Error::CommandFailed { .. })));
127 }
128
129 #[test]
130 fn test_run_with_env() {
131 let output = run_with_env("env", &[], &[("DKDC_SH_TEST_VAR", "hello123")]).unwrap();
132 assert!(output.contains("DKDC_SH_TEST_VAR=hello123"));
133 }
134
135 #[test]
136 fn test_error_display() {
137 let err = Error::CommandNotFound("foo".to_string());
138 assert_eq!(err.to_string(), "command not found: foo");
139
140 let err = Error::CommandFailed {
141 cmd: "bar".to_string(),
142 detail: "oops".to_string(),
143 };
144 assert!(err.to_string().contains("bar"));
145
146 let err = Error::Tmux("bad".to_string());
147 assert!(err.to_string().contains("bad"));
148 }
149}