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