ytil_sys/
lib.rs

1//! Provide cohesive system helpers: args, paths, symlinks, permissions, atomic copy, clipboard.
2//!
3//! Offer small utilities for CLI tools: joining thread handles, building home-relative paths,
4//! manipulating filesystem entries (chmod, symlinks, atomic copy) and clipboard integration.
5#![feature(exit_status_error)]
6
7use std::process::Command;
8use std::str::FromStr;
9use std::thread::JoinHandle;
10
11use color_eyre::eyre;
12use color_eyre::eyre::Context;
13use color_eyre::eyre::bail;
14use color_eyre::eyre::eyre;
15pub use pico_args;
16use ytil_cmd::CmdExt as _;
17
18pub mod cli;
19pub mod dir;
20pub mod file;
21pub mod lsof;
22pub mod rm;
23
24/// Joins a thread handle and returns the result, handling join errors as [`eyre::Error`].
25/// Awaits a `JoinHandle` and unwraps the inner `Result`.
26///
27/// # Errors
28/// - The task panicked.
29/// - The task returned an error.
30pub fn join<T>(join_handle: JoinHandle<color_eyre::Result<T>>) -> Result<T, eyre::Error> {
31    join_handle
32        .join()
33        .map_err(|err| eyre!("error joining handle | error={err:#?}"))?
34}
35
36/// Opens the given argument using the system's default app ("open").
37///
38/// # Rationale
39/// The argument passed to the "open" command is naively wrapped with ''
40/// to avoid failures in case of URLs with & or other shell sensitive
41/// characters.
42///
43/// # Errors
44/// - If the `open` command exits with a non-zero status.
45pub fn open(arg: &str) -> color_eyre::Result<()> {
46    let cmd = "open";
47    Command::new("sh")
48        .arg("-c")
49        .arg(format!("{cmd} '{arg}'"))
50        .status()
51        .wrap_err_with(|| eyre!("error running cmd | cmd={cmd:?} arg={arg:?}"))?
52        .exit_ok()
53        .wrap_err_with(|| eyre!("error cmd exit not ok | 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() -> color_eyre::Result<Self> {
69        Command::new("uname")
70            .arg("-mo")
71            .exec()
72            .wrap_err_with(|| eyre!(r#"error running cmd | cmd="uname" arg="-mo""#))
73            .and_then(|s| ytil_cmd::extract_success_output(&s))
74            .and_then(|f| Self::from_str(f.as_str()))
75    }
76}
77
78impl FromStr for SysInfo {
79    type Err = color_eyre::eyre::Error;
80
81    fn from_str(output: &str) -> Result<Self, Self::Err> {
82        let mut os_arch = output.split_ascii_whitespace();
83
84        let os = os_arch
85            .next()
86            .ok_or_else(|| eyre!("error missing os part in uname output | output={output:?}"))
87            .and_then(Os::from_str)?;
88        let arch = os_arch
89            .next()
90            .ok_or_else(|| eyre!("error missing arch part in uname output | output={output:?}"))
91            .and_then(Arch::from_str)?;
92
93        Ok(Self { os, arch })
94    }
95}
96
97#[cfg_attr(test, derive(Debug, Eq, PartialEq))]
98pub enum Os {
99    MacOs,
100    Linux,
101}
102
103impl FromStr for Os {
104    type Err = color_eyre::eyre::Error;
105
106    fn from_str(value: &str) -> Result<Self, Self::Err> {
107        match value.to_lowercase().as_str() {
108            "darwin" => Ok(Self::MacOs),
109            "linux" => Ok(Self::Linux),
110            normalized_value => {
111                bail!("error unknown normalized arch value | normalized_value={normalized_value:?} value={value:?} ")
112            }
113        }
114    }
115}
116
117#[cfg_attr(test, derive(Debug, Eq, PartialEq))]
118pub enum Arch {
119    Arm,
120    X86,
121}
122
123impl FromStr for Arch {
124    type Err = color_eyre::eyre::Error;
125
126    fn from_str(value: &str) -> Result<Self, Self::Err> {
127        match value.to_lowercase().as_str() {
128            "x86_64" => Ok(Self::X86),
129            "arm64" => Ok(Self::Arm),
130            normalized_value => {
131                bail!(
132                    "error unknown normalized arch value | value={value:?} normalized_value={normalized_value:?} value={value:?} "
133                )
134            }
135        }
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use rstest::rstest;
142
143    use super::*;
144
145    #[rstest]
146    #[case("x86_64", Arch::X86)]
147    #[case("arm64", Arch::Arm)]
148    #[case("X86_64", Arch::X86)]
149    #[case("ARM64", Arch::Arm)]
150    fn arch_from_str_when_valid_input_returns_expected_arch(#[case] input: &str, #[case] expected: Arch) {
151        let result = Arch::from_str(input);
152        assert2::let_assert!(Ok(arch) = result);
153        pretty_assertions::assert_eq!(arch, expected);
154    }
155
156    #[test]
157    fn arch_from_str_when_unknown_input_returns_error_with_message() {
158        let result = Arch::from_str("unknown");
159        assert2::let_assert!(Err(err) = result);
160        assert!(err.to_string().contains("error unknown normalized arch value"));
161    }
162
163    #[rstest]
164    #[case("darwin", Os::MacOs)]
165    #[case("linux", Os::Linux)]
166    #[case("DARWIN", Os::MacOs)]
167    #[case("LINUX", Os::Linux)]
168    fn os_from_str_when_valid_input_returns_expected_os(#[case] input: &str, #[case] expected: Os) {
169        let result = Os::from_str(input);
170        assert2::let_assert!(Ok(os) = result);
171        pretty_assertions::assert_eq!(os, expected);
172    }
173
174    #[test]
175    fn os_from_str_when_unknown_input_returns_error_with_message() {
176        let result = Os::from_str("unknown");
177        assert2::let_assert!(Err(err) = result);
178        assert!(err.to_string().contains("error unknown normalized arch value"));
179    }
180}