sleep_progress/
lib.rs

1//! `sleep-progress` is a clone of GNU sleep with an optional progress bar.
2//!
3//! The arguments are compatible with the original sleep but you can add `--progress` or `-p` to display a progress bar with an ETA.
4//!
5//! It can be use as a replacement for GNU sleep: `alias sleep=sleep-progress` .
6//!
7//! WARNING: the displayed ETA may not be as accurate as the sleep delay.
8//!
9//! ```text
10//! Usage: sleep-progress [OPTIONS] <NUMBER>...
11//!
12//! Arguments:
13//!   <NUMBER>...  Pause  for  NUMBER seconds.
14//!                SUFFIX may be 's' for seconds (the default), 'm' for minutes, 'h' for hours or 'd' for days.
15//!                NUMBER need not be an integer.
16//!                Given two or more arguments, pause for the amount of time specified by the sum of their values
17//!
18//! Options:
19//!   -p, --progress  Display the sleep indicator
20//!   -h, --help      Print help information
21//!   -V, --version   Print version information
22//! ```
23//!
24//! ## Installation
25//!
26//! ### Binaries
27//!
28//! Download the binary for your architecture from
29//! <https://github.com/djedi23/sleep-progress.rs/releases>
30//!
31//! ### From cargo
32//!
33//! Run:
34//! ``` bash
35//! cargo install sleep-progress
36//! ```
37//!
38//! ### From source
39//!
40//! Run:
41//! ``` bash
42//! git clone https://github.com/djedi23/sleep-progress.rs.git
43//! cd sleep-progress.rs
44//! cargo install --path .
45//! ```
46
47use chrono::Utc;
48use clap::Parser;
49use dateparser::DateTimeUtc;
50use miette::{miette, Diagnostic, Result};
51use thiserror::Error;
52
53#[derive(Error, Debug, Diagnostic)]
54#[error("invalid time interval '{origin}'")]
55#[diagnostic(
56  code(invalid::time),
57  help("Try `sleep-progress --help` for more informations.")
58)]
59pub(crate) struct InvalidTimeInterval {
60  origin: String,
61}
62
63#[derive(Parser, Debug)]
64#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
65#[command(author, version, about, long_about = None)]
66#[doc(hidden)]
67pub struct Args {
68  /// Pause  for  NUMBER seconds.  SUFFIX may be 's' for seconds (the default), 'm' for minutes, 'h' for hours or 'd' for days.  NUMBER need not be an integer.  Given two or more arguments, pause for the amount of time specified by the sum of their values.
69  #[arg(required_unless_present("timespec"))]
70  number: Vec<String>,
71
72  /// Sleep until this timestamp.
73  #[arg(short('u'), long("until"), conflicts_with("number"))]
74  pub timespec: Option<DateTimeUtc>,
75
76  /// Display the sleep indicator
77  #[arg(short, long)]
78  pub progress: bool,
79}
80
81#[doc(hidden)]
82pub fn parse_interval(args: &Args) -> Result<u64> {
83  match &args.timespec {
84    Some(timespec) => {
85      let millis = (timespec.0 - Utc::now()).num_milliseconds();
86      if millis < 0 {
87        Err(miette!(
88          "Can't wait a past time: {}",
89          timespec.0.to_rfc2822()
90        ))
91      } else {
92        Ok(millis as u64)
93      }
94    }
95    None => {
96      let mut sum = 0.0;
97      for duration_spec in args.number.iter() {
98        let (value, multipliers) = if let Some(seconds) = duration_spec.strip_suffix('s') {
99          (seconds, 1000.0)
100        } else if let Some(minutes) = duration_spec.strip_suffix('m') {
101          (minutes, 60.0 * 1000.0)
102        } else if let Some(hours) = duration_spec.strip_suffix('h') {
103          (hours, 60.0 * 60.0 * 1000.0)
104        } else if let Some(days) = duration_spec.strip_suffix('d') {
105          (days, 24.0 * 60.0 * 60.0 * 1000.0)
106        } else {
107          (duration_spec.as_str(), 1000.0)
108        };
109        sum += multipliers
110          * value.parse::<f64>().map_err(|_| InvalidTimeInterval {
111            origin: duration_spec.to_string(),
112          })?
113      }
114      Ok(sum.round() as u64)
115    }
116  }
117}
118
119#[cfg(test)]
120mod tests {
121  use super::*;
122
123  #[test]
124  fn parse_help() {
125    let result = Args::try_parse_from([" ", "-h"]);
126    assert!(result.is_err());
127  }
128
129  #[test]
130  fn parse_long_help() {
131    let result = Args::try_parse_from([" ", "--help"]);
132    assert!(result.is_err());
133  }
134
135  #[test]
136  fn parse_unknown_args() {
137    let result = Args::try_parse_from([" ", "-t"]);
138    //    dbg!(&result);
139    assert!(result.is_err());
140  }
141
142  #[test]
143  fn parse_cli_arg() {
144    let result = Args::try_parse_from([" ", "34"]);
145    //    dbg!(&result);
146    assert!(result.is_ok());
147  }
148
149  #[test]
150  fn parse_cli_args() {
151    let result = Args::try_parse_from([" ", "34", "6.4"]);
152    //    dbg!(&result);
153    assert!(result.is_ok());
154  }
155  #[test]
156  fn parse_cli_arg_progress() {
157    let result = Args::try_parse_from([" ", "34", "-p"]);
158    //    dbg!(&result);
159    assert!(result.is_ok());
160  }
161
162  #[test]
163  fn parse_cli_args_progress() {
164    let result = Args::try_parse_from([" ", "34", "6.4", "-p"]);
165    //    dbg!(&result);
166    assert!(result.is_ok());
167  }
168
169  #[test]
170  fn parse_cli_arg_unknown_args() {
171    let result = Args::try_parse_from([" ", "34", " ", "-t"]);
172    //dbg!(&result);
173    assert!(result.is_err());
174  }
175
176  #[test]
177  fn parse_cli_args_unknown_args() {
178    let result = Args::try_parse_from([" ", "34", "6.4", " ", "-t"]);
179    //    dbg!(&result);
180    assert!(result.is_err());
181  }
182  #[test]
183  fn parse_cli_arg_progress_unknown_args() {
184    let result = Args::try_parse_from([" ", "34", "-p", " ", "-t"]);
185    //    dbg!(&result);
186    assert!(result.is_err());
187  }
188
189  #[test]
190  fn parse_cli_args_progress_unknown_args() {
191    let result = Args::try_parse_from([" ", "34", "6.4", "-p", " ", "-t"]);
192    //    dbg!(&result);
193    assert!(result.is_err());
194  }
195
196  #[test]
197  fn parse_interval_1() {
198    let result = parse_interval(&Args {
199      number: vec!["1".into()],
200      timespec: None,
201      progress: false,
202    });
203    assert_eq!(result.ok(), Some(1000));
204  }
205
206  #[test]
207  fn parse_interval_1p() {
208    let result = parse_interval(&Args {
209      number: vec!["1".into()],
210      timespec: None,
211      progress: true,
212    });
213    assert_eq!(result.ok(), Some(1000));
214  }
215
216  #[test]
217  fn parse_interval_0_5() {
218    let result = parse_interval(&Args {
219      number: vec!["0.5".into()],
220      timespec: None,
221      progress: false,
222    });
223    assert_eq!(result.ok(), Some(500));
224  }
225
226  #[test]
227  fn parse_interval_1s() {
228    let result = parse_interval(&Args {
229      number: vec!["1s".into()],
230      timespec: None,
231      progress: false,
232    });
233    assert_eq!(result.ok(), Some(1000));
234  }
235
236  #[test]
237  fn parse_interval_1m() {
238    let result = parse_interval(&Args {
239      number: vec!["1m".into()],
240      timespec: None,
241      progress: false,
242    });
243    assert_eq!(result.ok(), Some(60000));
244  }
245
246  #[test]
247  fn parse_interval_1h() {
248    let result = parse_interval(&Args {
249      number: vec!["1h".into()],
250      timespec: None,
251      progress: false,
252    });
253    assert_eq!(result.ok(), Some(3600000));
254  }
255
256  #[test]
257  fn parse_interval_1d() {
258    let result = parse_interval(&Args {
259      number: vec!["1d".into()],
260      timespec: None,
261      progress: false,
262    });
263    assert_eq!(result.ok(), Some(86400000));
264  }
265
266  #[test]
267  fn parse_interval_multiple() {
268    let result = parse_interval(&Args {
269      number: vec![
270        "1.023".into(),
271        "1s".into(),
272        "1m".into(),
273        "1h".into(),
274        "1d".into(),
275      ],
276      timespec: None,
277      progress: false,
278    });
279    assert_eq!(result.ok(), Some(90062023));
280  }
281
282  #[test]
283  fn parse_interval_err() {
284    let result = parse_interval(&Args {
285      number: vec!["1z".into()],
286      timespec: None,
287      progress: false,
288    });
289
290    assert_eq!(
291      result.err().unwrap().to_string(),
292      "invalid time interval '1z'"
293    );
294  }
295
296  #[test]
297  fn parse_interval_err_2() {
298    let result = parse_interval(&Args {
299      number: vec![
300        "1".into(),
301        "2".into(),
302        "3e".into(),
303        "4".into(),
304        "5".into(),
305        "6".into(),
306      ],
307      timespec: None,
308      progress: false,
309    });
310
311    assert_eq!(
312      result.err().unwrap().to_string(),
313      "invalid time interval '3e'"
314    );
315  }
316
317  #[test]
318  fn parse_interval_err_3() {
319    let result = parse_interval(&Args {
320      number: vec!["one".into()],
321      timespec: None,
322      progress: false,
323    });
324
325    assert_eq!(
326      result.err().unwrap().to_string(),
327      "invalid time interval 'one'"
328    );
329  }
330}