Skip to main content

ytil_sys/
lib.rs

1//! System helpers: args, paths, symlinks, permissions, clipboard.
2#![feature(exit_status_error)]
3
4use std::process::Command;
5use std::str::FromStr;
6use std::thread::JoinHandle;
7
8use owo_colors::OwoColorize as _;
9pub use pico_args;
10use rootcause::prelude::ResultExt;
11use rootcause::report;
12use ytil_cmd::CmdExt as _;
13pub use ytil_macros::main;
14
15pub mod cli;
16pub mod dir;
17pub mod file;
18pub mod lsof;
19pub mod rm;
20
21/// Runs `f` and, on error, prints the report in bold red to stderr then exits with code 1.
22pub fn run(f: impl FnOnce() -> rootcause::Result<()>) {
23    if let Err(err) = f() {
24        eprintln!("{}", format!("{err:?}").red().bold());
25        std::process::exit(1);
26    }
27}
28
29/// Joins a thread handle and returns the result.
30///
31/// # Errors
32/// - Task panicked or returned an error.
33pub fn join<T>(join_handle: JoinHandle<rootcause::Result<T>>) -> Result<T, rootcause::Report> {
34    join_handle
35        .join()
36        .map_err(|err| report!("error joining handle").attach(format!("error={err:#?}")))?
37}
38
39/// Opens the given argument using the system's default app (`open` on macOS).
40///
41/// # Errors
42/// - `open` command fails.
43pub fn open(arg: &str) -> rootcause::Result<()> {
44    let cmd = "open";
45    Command::new("sh")
46        .arg("-c")
47        .arg(format!("{cmd} '{arg}'"))
48        .status()
49        .context("error running cmd")
50        .attach_with(|| format!("cmd={cmd:?} arg={arg:?}"))?
51        .exit_ok()
52        .context("error cmd exit not ok")
53        .attach_with(|| format!("cmd={cmd:?} arg={arg:?}"))?;
54    Ok(())
55}
56
57pub struct SysInfo {
58    pub os: Os,
59    pub arch: Arch,
60}
61
62impl SysInfo {
63    /// Retrieves system information via `uname -mo`.
64    ///
65    /// # Errors
66    /// - If `uname -mo` command fails.
67    /// - If `uname -mo` output is unexpected.
68    pub fn get() -> rootcause::Result<Self> {
69        let output = Command::new("uname")
70            .arg("-mo")
71            .exec()
72            .context("error running cmd")
73            .attach(r#"cmd="uname" arg="-mo""#)?;
74        let s = ytil_cmd::extract_success_output(&output)?;
75        Self::from_str(s.as_str())
76    }
77}
78
79impl FromStr for SysInfo {
80    type Err = rootcause::Report;
81
82    fn from_str(output: &str) -> Result<Self, Self::Err> {
83        let mut os_arch = output.split_ascii_whitespace();
84
85        let os = os_arch
86            .next()
87            .ok_or_else(|| report!("error missing os part in uname output"))
88            .attach_with(|| format!("output={output:?}"))
89            .and_then(Os::from_str)?;
90        let arch = os_arch
91            .next()
92            .ok_or_else(|| report!("error missing arch part in uname output"))
93            .attach_with(|| format!("output={output:?}"))
94            .and_then(Arch::from_str)?;
95
96        Ok(Self { os, arch })
97    }
98}
99
100#[cfg_attr(test, derive(Debug, Eq, PartialEq))]
101pub enum Os {
102    MacOs,
103    Linux,
104}
105
106impl FromStr for Os {
107    type Err = rootcause::Report;
108
109    fn from_str(value: &str) -> Result<Self, Self::Err> {
110        match value.to_lowercase().as_str() {
111            "darwin" => Ok(Self::MacOs),
112            "linux" => Ok(Self::Linux),
113            normalized_value => Err(report!("error unknown normalized os value")
114                .attach(format!("normalized_value={normalized_value:?} value={value:?}"))),
115        }
116    }
117}
118
119#[cfg_attr(test, derive(Debug, Eq, PartialEq))]
120pub enum Arch {
121    Arm,
122    X86,
123}
124
125impl FromStr for Arch {
126    type Err = rootcause::Report;
127
128    fn from_str(value: &str) -> Result<Self, Self::Err> {
129        match value.to_lowercase().as_str() {
130            "x86_64" => Ok(Self::X86),
131            "arm64" => Ok(Self::Arm),
132            normalized_value => Err(report!("error unknown normalized arch value")
133                .attach(format!("value={value:?} normalized_value={normalized_value:?}"))),
134        }
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use rstest::rstest;
141
142    use super::*;
143
144    #[rstest]
145    #[case("x86_64", Arch::X86)]
146    #[case("arm64", Arch::Arm)]
147    #[case("X86_64", Arch::X86)]
148    #[case("ARM64", Arch::Arm)]
149    fn arch_from_str_when_valid_input_returns_expected_arch(#[case] input: &str, #[case] expected: Arch) {
150        let result = Arch::from_str(input);
151        assert2::assert!(let Ok(arch) = result);
152        pretty_assertions::assert_eq!(arch, expected);
153    }
154
155    #[test]
156    fn arch_from_str_when_unknown_input_returns_error_with_message() {
157        let result = Arch::from_str("unknown");
158        assert2::assert!(let Err(err) = result);
159        assert!(err.to_string().contains("error unknown normalized arch value"));
160    }
161
162    #[rstest]
163    #[case("darwin", Os::MacOs)]
164    #[case("linux", Os::Linux)]
165    #[case("DARWIN", Os::MacOs)]
166    #[case("LINUX", Os::Linux)]
167    fn os_from_str_when_valid_input_returns_expected_os(#[case] input: &str, #[case] expected: Os) {
168        let result = Os::from_str(input);
169        assert2::assert!(let Ok(os) = result);
170        pretty_assertions::assert_eq!(os, expected);
171    }
172
173    #[test]
174    fn os_from_str_when_unknown_input_returns_error_with_message() {
175        let result = Os::from_str("unknown");
176        assert2::assert!(let Err(err) = result);
177        assert!(err.to_string().contains("error unknown normalized os value"));
178    }
179}