waterui_cli/
utils.rs

1//! Utility functions for the CLI.
2
3use std::{
4    io,
5    path::{Path, PathBuf},
6    process::Output,
7    process::Stdio,
8    sync::atomic::{AtomicBool, Ordering},
9};
10
11use color_eyre::eyre;
12use smol::{process::Command, unblock};
13
14/// Locate an executable in the system's PATH.
15///
16/// Return the path to the executable if found.
17///
18/// # Errors
19/// - If the executable is not found in the PATH.
20pub(crate) async fn which(name: &'static str) -> Result<PathBuf, which::Error> {
21    unblock(move || which::which(name)).await
22}
23
24/// Enable or disable standard output for command executions.
25///
26/// By default, standard output is disabled.
27static STD_OUTPUT: AtomicBool = AtomicBool::new(false);
28
29/// Enable or disable standard output for command executions.
30pub fn set_std_output(enabled: bool) {
31    STD_OUTPUT.store(enabled, std::sync::atomic::Ordering::SeqCst);
32}
33
34// Warn: You will lose stdout/stderr piping if you modify this function!
35pub(crate) fn command(command: &mut Command) -> &mut Command {
36    command
37        .kill_on_drop(true)
38        .stdout(if STD_OUTPUT.load(Ordering::SeqCst) {
39            Stdio::inherit()
40        } else {
41            Stdio::piped()
42        })
43        .stderr(if STD_OUTPUT.load(Ordering::SeqCst) {
44            Stdio::inherit()
45        } else {
46            Stdio::piped()
47        })
48}
49
50/// Run a command and capture its output regardless of exit status.
51///
52/// When `STD_OUTPUT` is enabled, also prints to terminal.
53///
54/// # Errors
55/// - If the command fails to execute.
56pub(crate) async fn run_command_output(
57    name: &str,
58    args: impl IntoIterator<Item = &str>,
59) -> eyre::Result<Output> {
60    let result = Command::new(name)
61        .args(args)
62        .kill_on_drop(true)
63        .stdout(Stdio::piped())
64        .stderr(Stdio::piped())
65        .output()
66        .await?;
67
68    // If STD_OUTPUT is enabled, also print to terminal
69    if STD_OUTPUT.load(Ordering::SeqCst) {
70        use std::io::Write;
71        let _ = std::io::stdout().write_all(&result.stdout);
72        let _ = std::io::stderr().write_all(&result.stderr);
73    }
74
75    Ok(result)
76}
77
78/// Run a command with the specified name and arguments.
79///
80/// Always captures output. When `STD_OUTPUT` is enabled, also prints to terminal.
81///
82/// Return the standard output as a `String` if successful.
83/// # Errors
84/// - If the command fails to execute or returns a non-zero exit status.
85pub(crate) async fn run_command(
86    name: &str,
87    args: impl IntoIterator<Item = &str>,
88) -> eyre::Result<String> {
89    let result = run_command_output(name, args).await?;
90
91    if result.status.success() {
92        Ok(String::from_utf8_lossy(&result.stdout).to_string())
93    } else {
94        Err(eyre::eyre!(
95            "Command {} failed with status {}",
96            name,
97            result.status
98        ))
99    }
100}
101
102/// Parse whitespace-separated u32 values (e.g., process IDs).
103pub(crate) fn parse_whitespace_separated_u32s(input: &str) -> Vec<u32> {
104    input
105        .split_whitespace()
106        .filter_map(|part| part.parse::<u32>().ok())
107        .collect()
108}
109
110/// Async file copy using reflink when available, falling back to regular copy.
111///
112/// This is more efficient than regular copy on filesystems that support reflinks (APFS, Btrfs).
113///
114/// # Errors
115/// - If the copy operation fails.
116pub async fn copy_file(from: impl AsRef<Path>, to: impl AsRef<Path>) -> io::Result<()> {
117    let from = from.as_ref().to_path_buf();
118    let to = to.as_ref().to_path_buf();
119    unblock(move || reflink::reflink_or_copy(from, to).map(|_| ())).await
120}
121
122#[cfg(test)]
123mod tests {
124    use super::parse_whitespace_separated_u32s;
125
126    #[test]
127    fn parses_pidof_output_with_multiple_pids() {
128        let parsed = parse_whitespace_separated_u32s("123 456\n");
129        assert_eq!(parsed, vec![123, 456]);
130    }
131
132    #[test]
133    fn ignores_non_numeric_tokens() {
134        let parsed = parse_whitespace_separated_u32s("foo 42 bar\n");
135        assert_eq!(parsed, vec![42]);
136    }
137}