watch_rs/
lib.rs

1use std::{
2    io::{stdout, Error, Result, Write},
3    process::Command,
4    time::{Duration, Instant},
5};
6
7use crossterm::{
8    cursor::*,
9    event::{poll, read, Event, KeyCode},
10    execute, queue,
11    style::*,
12    terminal::*,
13};
14
15/// Uses `crossterm` to watch a command and print its output.
16/// Allows the user to exit by pressing 'q' or 'Ctrl+C'.
17///
18/// # Arguments
19///
20/// * `command` - The command to watch.
21/// * `args` - The arguments to pass to the command.
22/// * `interval` - The interval in seconds between command executions.
23///
24/// # Errors
25///
26/// Returns a `std::io::Error` if the command fails to execute.
27///
28/// # Examples
29///
30/// ```
31/// use watch_rs::watch;
32///
33/// fn main() {
34///     if let Err(err) = watch("ls".to_string(), vec!["-l".to_string()], 1) {
35///         eprintln!("Error: {}", err);
36///     }
37/// }
38/// ```
39pub fn watch(command: String, args: Vec<String>, interval: u64) -> Result<()> {
40    let interval_duration: Duration = Duration::from_secs(interval);
41
42    let mut full_watch_command: String = command.to_owned();
43    full_watch_command.push_str(" ");
44    full_watch_command.push_str(args.join(" ").as_str());
45
46    let (program, command_arg): (&str, &str);
47    if cfg!(windows) {
48        program = "powershell";
49        command_arg = "-Command";
50    } else {
51        program = "sh";
52        command_arg = "-c";
53    }
54
55    const QUIT_MSG: &str = "Press 'q' or 'Ctrl+C' to exit";
56    let interval_msg = format!("Interval: {}s", interval);
57
58    enable_raw_mode()?;
59    execute!(stdout(), Hide, EnterAlternateScreen, EnableLineWrap)?;
60    'watchLoop: loop {
61        // Begin queueing updates
62        queue!(
63            stdout(),
64            Clear(ClearType::All),
65            MoveTo(0, 0),
66            Print("> "),
67            PrintStyledContent(full_watch_command.to_owned().rapid_blink()),
68            MoveToColumn(size().unwrap().0 - interval_msg.len() as u16),
69            PrintStyledContent(interval_msg.to_owned().bold()),
70            MoveToNextLine(2),
71        )?;
72        let output = Command::new(program)
73            .arg(command_arg)
74            .arg(&full_watch_command)
75            .output()?;
76
77        if !output.status.success() {
78            return Err(Error::other(format!(
79                "Command failed with exitCode: {}",
80                output.status.code().unwrap()
81            )));
82        }
83
84        let to_trim = String::from_utf8(output.stdout).expect("Get stdout");
85        let std_output = to_trim.trim();
86        let to_trim = String::from_utf8(output.stderr).expect("Get stderr");
87        let std_error = to_trim.trim();
88
89        // Print the output
90        queue!(
91            stdout(),
92            PrintStyledContent("Output:".bold().underlined()),
93            MoveToNextLine(1),
94            Print(std_output),
95            MoveToNextLine(1),
96        )?;
97        if !std_error.is_empty() {
98            queue!(
99                stdout(),
100                PrintStyledContent("StdErr:".bold().underlined()),
101                MoveToNextLine(1),
102                Print(std_error),
103                MoveToNextLine(1),
104            )?;
105        }
106        queue!(
107            stdout(),
108            MoveTo(size().unwrap().0 - QUIT_MSG.len() as u16, size().unwrap().1 - 1),
109            PrintStyledContent(QUIT_MSG.italic()),
110        )?;
111
112        // Flush updates
113        stdout().flush()?;
114
115        // Poll for keys/sleep
116        let start_time = Instant::now();
117        while start_time.elapsed() < interval_duration {
118            if poll(interval_duration - start_time.elapsed())? {
119                match read()? {
120                    Event::Key(event)
121                        if event.code == KeyCode::Char('q')
122                            || (event.code == KeyCode::Char('c')
123                                && event.modifiers == crossterm::event::KeyModifiers::CONTROL) =>
124                    {
125                        // Leave alternate screen and print output one more time before exit
126                        queue!(
127                            stdout(),
128                            LeaveAlternateScreen,
129                            Print("> "),
130                            Print(full_watch_command),
131                            MoveToNextLine(2),
132                            PrintStyledContent("Output:".bold().underlined()),
133                            MoveToNextLine(1),
134                            Print(std_output),
135                            MoveToNextLine(1),
136                        )?;
137                        if !std_error.is_empty() {
138                            queue!(
139                                stdout(),
140                                PrintStyledContent("StdErr:".bold().underlined()),
141                                MoveToNextLine(1),
142                                Print(std_error),
143                                MoveToNextLine(1),
144                            )?;
145                        }
146                        stdout().flush()?;
147                        break 'watchLoop;
148                    }
149                    _ => {}
150                }
151            }
152        }
153    }
154    execute!(stdout(), Show, DisableLineWrap)?;
155    disable_raw_mode()
156}