text_loading_animation/
lib.rs

1/// Contains a function that helps you to display a simple loading animation on your screen.
2
3use std::thread::sleep;
4use std::time::Duration;
5use std::io::{stdout, Write, stderr};
6
7/// Animation steps per seconds / character changes per second.
8const ANIMATION_STEPS_PER_SECOND: u64 = 4;
9/// Timeout after the next animation step is printed
10const ANIMATION_SLEEP_TIMEOUT: u64 = 1000 / ANIMATION_STEPS_PER_SECOND;
11
12/// The chars for the animation (the loading spinner).
13static ANIMATION_STATES: [char; 4] = ['—', '\\', '|', '/'];
14
15/// The destination stream where the loading animation should be written to.
16#[derive(Debug)]
17pub enum Destination {
18    /// Print to STDOUT
19    STDOUT,
20    /// Print to STDERR
21    STDERR,
22}
23
24/// Describes the semantics of the return value of the progress function.
25#[derive(Debug)]
26pub enum ProgressFnKind {
27    /// Progress report function returns percentage from 0 to 100.
28    PERCENT,
29    /// Progress report function returns total progress in processed elements from total.
30    PROGRESS
31}
32
33/// Displays a loading animation on stdout. From and To are both values that are not necessarily
34/// percent. They describe to problem range, e.g. copy files 1000 to 2000.
35///
36/// One must provide a function that returns the progress in percent at any given time.
37pub fn show_loading_animation(from: usize,
38                              to: usize,
39                              target: Destination,
40                              progress_fn: &dyn Fn() -> usize,
41                              fn_kind: ProgressFnKind) {
42    assert!(from <= to);
43
44    let mut animation_step = 0;
45    loop {
46        let (curr_percent, curr_progress) = get_curr_prog_n_perc(progress_fn, &fn_kind, from, to);
47
48        // current char of the loading animation
49        let indicator = ANIMATION_STATES[animation_step];
50        print_line(indicator, curr_progress, to, curr_percent, &target);
51
52        // break when we're done
53        if curr_progress >= to { break; }
54
55        // switch to next animation step / next char
56        animation_step = (animation_step + 1) % ANIMATION_STATES.len();
57
58        sleep(Duration::from_millis(ANIMATION_SLEEP_TIMEOUT));
59    }
60}
61
62
63/// Like show_loading_animation() but callable from C.
64/// C declaration looks like this:
65///
66///     /** The target stream for the loading animation. */
67///     enum target { TARGET_STDOUT, TARGET_STDERR };
68///     /** The semantic type of the progress function. */
69///     enum fn_kind { PERCENTAGE_FN, PROGRESS_FN };
70///     typedef unsigned long long usize_t; // 64 bit / usize on 64 bit machine
71///     extern void show_loading_animation_ffi(usize_t, usize_t, int, usize_t (*prog_fn)(), int);
72#[no_mangle]
73// pub not necessary here; this way this symbol is not visible to Rust but only to C
74fn show_loading_animation_ffi(from: usize,
75                              to: usize,
76                              target: Destination,
77                              progress_in_percentage_fn: extern "C" fn() -> usize,
78                              fn_kind: ProgressFnKind) {
79    let fn_closure = || { progress_in_percentage_fn() };
80    show_loading_animation(from, to, target, &fn_closure, fn_kind);
81}
82
83/// Calculates the current percentage and total current progress of the workload for
84/// the loading animation.
85fn get_curr_prog_n_perc(fnc: &dyn Fn() -> usize,
86                        fn_kind: &ProgressFnKind,
87                        from: usize,
88                        to: usize) -> (usize, usize) {
89    let mut percentage: usize;
90    let mut total_current_progress: usize;
91    if let ProgressFnKind::PERCENT = fn_kind {
92        percentage = fnc();
93        if percentage > 100 {
94            percentage = 100;
95        }
96        total_current_progress = calc_progress(from, to, percentage);
97    } else {
98        total_current_progress = fnc();
99        if total_current_progress > to {
100            total_current_progress = to;
101        }
102        percentage = calc_percentage(from, to, total_current_progress);
103    }
104    (percentage, total_current_progress)
105}
106
107// Prints a single line of the loading animation. Resets the previous line on each iteration by '\r'.
108fn print_line(indicator: char, curr_progress: usize, to: usize, curr_percent: usize, target: &Destination) {
109    // msg to be printed; \r is important to reset the current line
110    let msg = format!("\r [{}] {}/{} ({}%)", indicator, curr_progress, to, curr_percent);
111
112    if let Destination::STDOUT = &target {
113        stdout().write_all(msg.as_bytes()).unwrap();
114        // it's important to flush
115        stdout().flush().unwrap();
116    } else {
117        stderr().write_all(msg.as_bytes()).unwrap();
118        // it's important to flush
119        stderr().flush().unwrap();
120    }
121}
122
123/// Calculates the progress from the given percentage.
124fn calc_progress(from: usize, to: usize, percent: usize) -> usize {
125    // not necessary; usize assert!(0 <= percent);
126    assert!(percent <= 100);
127    let normalized = (to - from) as f64;
128    let progress = (percent as f64) / 100_f64;
129    let progress = (progress * normalized) as usize;
130    let progress = progress + from;
131    progress
132}
133
134/// Calculates the percentage of the given progress.
135fn calc_percentage(from: usize, to: usize, progress: usize) -> usize {
136    assert!(from <= progress);
137    assert!(progress <= to);
138    let normalized_to = to - from;
139    if normalized_to == 0 { return 100 }
140
141    let normalized_to = normalized_to as f64;
142    let normalized_progress = (progress - from) as f64;
143    ((normalized_progress / normalized_to) * 100_f64) as usize
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn test_calc_progress() {
152        assert_eq!(0, calc_progress(0, 100, 0));
153        assert_eq!(5, calc_progress(0, 100, 5));
154        assert_eq!(100, calc_progress(0, 100, 100));
155
156        assert_eq!(0 + 10, calc_progress(10, 20, 0));
157        assert_eq!(1 + 10, calc_progress(10, 20, 10));
158        assert_eq!(10 + 10, calc_progress(10, 20, 100));
159
160        assert_eq!(0, calc_progress(0, 0, 0));
161    }
162
163    #[test]
164    #[should_panic]
165    fn test_calc_progress_panic_1() {
166        calc_progress(0, 100, 101);
167    }
168
169    #[test]
170    fn test_calc_percentage() {
171        assert_eq!(0, calc_percentage(0, 100, 0));
172        assert_eq!(20, calc_percentage(0, 100, 20));
173        assert_eq!(100, calc_percentage(0, 100, 100));
174
175        assert_eq!(0, calc_percentage(10, 20, 10));
176        assert_eq!(50, calc_percentage(10, 20, 15));
177        assert_eq!(100, calc_percentage(10, 20, 20));
178
179        // 0 from 0 is like 100 percent
180        assert_eq!(100, calc_percentage(0, 0, 0));
181    }
182
183    #[test]
184    #[should_panic]
185    fn test_calc_percentage_panic_1() {
186        calc_percentage(20, 100, 19);
187    }
188
189    #[test]
190    #[should_panic]
191    fn test_calc_percentage_panic_2() {
192        calc_percentage(20, 100, 101);
193    }
194}