cli_progress/
lib.rs

1//! Dynamic progress and process display library
2//!
3//! To use this, construct a [CLIDisplayManager], whose output can be changed with the [CLIDisplayManager::modify] function,
4//! and drop it on completion.
5//!
6//! While the [CLIDisplayManager] is in use, no other [CLIDisplayManager] should be active,
7//! however stdout can still be used through the [erasing_println] macro during [modify](CLIDisplayManager::modify) calls and it will appear in front of the displayed progress/process.
8//!
9//! Currently there are three types of displays:
10//!
11//! - [Just text](CLIDisplayNodeType::Message)
12//! - [Text with a progress spinner at the end](CLIDisplayNodeType::SpinningMessage)
13//! - [A progress bar whose progress can be set through an `Arc<AtomicU8>`](CLIDisplayNodeType::ProgressBar)
14//!
15//! Example with progress bars:
16//! `cargo run --example progress`
17//! ```
18#![doc = include_str!("../examples/progress.rs")]
19//! ```
20
21#![deny(missing_docs)]
22
23use std::{
24    borrow::Cow,
25    io::{Write, stdout},
26    mem::forget,
27    ops::Neg,
28    sync::{
29        Arc, Condvar, Mutex, RwLock,
30        atomic::{AtomicBool, AtomicU8, AtomicUsize, Ordering::*},
31    },
32    thread::{Builder, JoinHandle},
33    time::Duration,
34};
35
36const CURSOR_HIDE: &str = "\x1B[?25l";
37const CURSOR_SHOW: &str = "\x1B[?25h";
38const ERASE_LINE: &str = "\x1b[2K\r";
39const CURSOR_UP: &str = "\x1b[1A";
40
41#[doc(hidden)]
42pub const _ERASE_LINE: &str = ERASE_LINE;
43
44struct CursorHideGuard;
45
46impl CursorHideGuard {
47    fn new() -> Self {
48        print!("{}", CURSOR_HIDE);
49        let _ = stdout().flush();
50        CursorHideGuard
51    }
52}
53
54impl Drop for CursorHideGuard {
55    fn drop(&mut self) {
56        print!("{}", CURSOR_SHOW);
57        let _ = stdout().flush();
58    }
59}
60
61/// This is the core struct of the library.
62/// Everything is managed here.
63/// Create this with the initial root item and a refresh rate and drop it when done.
64pub struct CLIDisplayManager {
65    root: Arc<RwLock<CLIDisplayNode>>,
66    cv: Arc<Condvar>,
67    mutex: Arc<Mutex<()>>,
68    self_handle: Option<JoinHandle<()>>,
69    stop: Arc<AtomicBool>,
70    tick_counter: Arc<AtomicUsize>,
71    _cursor_visibility_guard: CursorHideGuard,
72}
73
74impl CLIDisplayManager {
75    /// Creates a [CLIDisplayManager] with a specified root node and a tick rate at which it updates
76    pub fn new(root_node: CLIDisplayNodeType, tick_rate: u32) -> Self {
77        let _ = enable_ansi_support::enable_ansi_support();
78
79        let mut clidm = Self {
80            root: RwLock::new(CLIDisplayNode::new(root_node)).into(),
81            cv: Condvar::new().into(),
82            mutex: Mutex::new(()).into(),
83            self_handle: None,
84            stop: AtomicBool::new(false).into(),
85            tick_counter: AtomicUsize::new(0).into(),
86            _cursor_visibility_guard: CursorHideGuard::new(),
87        };
88
89        let stop = clidm.stop.clone();
90        let cv = clidm.cv.clone();
91        let mutex = clidm.mutex.clone();
92        let root = clidm.root.clone();
93        let tick_counter = clidm.tick_counter.clone();
94
95        clidm.self_handle.replace(
96            Builder::new()
97                .name("CLIDisplayManagerThread".to_string())
98                .spawn(move || {
99                    let mut guard = mutex
100                        .lock()
101                        .expect("Poisoned mutex in CLIDisplayManagerThread!!!");
102
103                    let node = root
104                        .read()
105                        .expect("Poisoned rwlock in CLIDisplayManagerThread!!!");
106                    node.display(0, tick_counter.load(Relaxed), true);
107                    node.go_back();
108                    drop(node);
109
110                    while !stop.load(Relaxed) {
111                        let node = root
112                            .read()
113                            .expect("Poisoned rwlock in CLIDisplayManagerThread!!!");
114                        node.display(0, tick_counter.load(Relaxed), true);
115                        node.go_back();
116                        drop(node);
117                        print!("\r");
118
119                        tick_counter.fetch_add(1, Relaxed);
120                        if tick_rate != 0 {
121                            guard = cv
122                                .wait_timeout(guard, Duration::from_secs(1) / tick_rate)
123                                .expect("Poisoned condition variable in CLIDisplayManagerThread!!!")
124                                .0;
125                        } else {
126                            guard = cv.wait(guard).expect(
127                                "Poisoned condition variable in CLIDisplayManagerThread!!!",
128                            );
129                        }
130                    }
131                })
132                .unwrap(),
133        );
134
135        clidm
136    }
137
138    /// Modifies a [CLIDisplayManager]s output through a [CLIModificationElement] handle that gets passed to a callback
139    pub fn modify<F: FnOnce(&mut CLIModificationElement) -> ()>(&mut self, f: F) {
140        let guard = self.mutex.lock();
141
142        let mut modification_element = CLIModificationElement {
143            root_node: &self.root,
144            additions: 0,
145        };
146
147        f(&mut modification_element);
148
149        let removed_lines = modification_element.additions.neg().max(0);
150
151        drop(modification_element);
152
153        let node = self
154            .root
155            .read()
156            .expect("Poisoned rwlock in CLIDisplayManagerThread!!!");
157        node.display(0, self.tick_counter.load(Relaxed), true);
158
159        for i in 1..=removed_lines {
160            print!("{}", ERASE_LINE);
161
162            if i != removed_lines {
163                println!("");
164            }
165        }
166
167        for _ in 1..removed_lines {
168            print!("{}", CURSOR_UP);
169        }
170
171        node.go_back();
172        drop(node);
173        print!("\r");
174        let _ = stdout().flush();
175
176        drop(guard);
177    }
178}
179
180impl Drop for CLIDisplayManager {
181    fn drop(&mut self) {
182        self.stop.store(true, Relaxed);
183
184        self.cv.notify_all();
185
186        self.self_handle.take().unwrap().join().unwrap();
187    }
188}
189
190/// This is the struct through which the output of a [CLIDisplayManager] can be changed.
191pub struct CLIModificationElement<'a> {
192    root_node: &'a RwLock<CLIDisplayNode>,
193    additions: isize,
194}
195
196impl<'a> CLIModificationElement<'a> {
197    /// Removes the last displayed item
198    pub fn pop(&mut self) {
199        self.additions -= 1;
200
201        let mut node = self
202            .root_node
203            .write()
204            .expect("Poisoned rwlock in CLIModificationElement!!!");
205
206        if node.sub_nodes.len() == 0 {
207            self.additions += 1;
208            return;
209        }
210
211        let mut mapped_node = &mut *node;
212
213        while mapped_node.sub_nodes.last().unwrap().sub_nodes.len() != 0 {
214            mapped_node = mapped_node.sub_nodes.last_mut().unwrap();
215        }
216
217        forget(mapped_node.sub_nodes.pop());
218    }
219
220    /// Adds another parallel task or subtask if only the root node is present
221    pub fn push(&mut self, node_type: CLIDisplayNodeType) {
222        self.additions += 1;
223
224        let mut node = self
225            .root_node
226            .write()
227            .expect("Poisoned rwlock in CLIModificationElement!!!");
228
229        if node.sub_nodes.len() == 0 {
230            drop(node);
231
232            self.additions -= 1;
233            return Self::make_sub(self, node_type);
234        }
235
236        let mut mapped_node = &mut *node;
237
238        while mapped_node.sub_nodes.last().unwrap().sub_nodes.len() != 0 {
239            mapped_node = mapped_node.sub_nodes.last_mut().unwrap();
240        }
241
242        mapped_node.sub_nodes.push(CLIDisplayNode::new(node_type));
243    }
244
245    /// Makes a new subtask for the current task
246    pub fn make_sub(&mut self, node_type: CLIDisplayNodeType) {
247        self.additions += 1;
248
249        let mut node = self
250            .root_node
251            .write()
252            .expect("Poisoned rwlock in CLIModificationElement!!!");
253
254        let mut last_node = &mut *node;
255
256        while last_node.sub_nodes.len() != 0 {
257            last_node = last_node.sub_nodes.last_mut().unwrap();
258        }
259
260        last_node.sub_nodes.push(CLIDisplayNode::new(node_type));
261    }
262
263    /// Replaces the root node with a different one
264    pub fn replace_root(&mut self, node_type: CLIDisplayNodeType) {
265        self.root_node
266            .write()
267            .expect("Poisoned rwlock in CLIModificationElement!!!")
268            .node_type = node_type;
269    }
270}
271
272struct CLIDisplayNode {
273    node_type: CLIDisplayNodeType,
274    sub_nodes: Vec<CLIDisplayNode>,
275}
276
277impl CLIDisplayNode {
278    fn new(node_type: CLIDisplayNodeType) -> Self {
279        Self {
280            node_type,
281            sub_nodes: Vec::new(),
282        }
283    }
284
285    fn display(&self, depth: usize, tick_counter: usize, last: bool) {
286        print!("{}", ERASE_LINE);
287        if depth != 0 {
288            for _ in 1..depth {
289                print!("  ");
290            }
291
292            if last {
293                print!("\u{2514}\u{2574}");
294            } else {
295                print!("\u{251C}\u{2574}");
296            }
297        }
298
299        self.node_type.display(tick_counter);
300
301        for (index, sub_node) in self.sub_nodes.iter().enumerate() {
302            sub_node.display(depth + 1, tick_counter, index + 1 == self.sub_nodes.len());
303        }
304    }
305
306    fn go_back(&self) {
307        for sub_node in self.sub_nodes.iter() {
308            sub_node.go_back();
309        }
310
311        print!("{}", CURSOR_UP);
312    }
313}
314
315impl Drop for CLIDisplayNode {
316    fn drop(&mut self) {
317        println!("");
318    }
319}
320
321/// All possible display node types.
322pub enum CLIDisplayNodeType {
323    /// Just text
324    Message(Cow<'static, str>),
325    /// Text with an animated spinner at the end
326    SpinningMessage(Cow<'static, str>),
327    /// A controllable progress bar
328    ProgressBar(Arc<AtomicU8>),
329}
330
331impl CLIDisplayNodeType {
332    fn display(&self, tick_counter: usize) {
333        match self {
334            CLIDisplayNodeType::Message(cow) => println!("{}", cow),
335            CLIDisplayNodeType::SpinningMessage(cow) => {
336                println!("{} {}", cow, "/-\\|".chars().nth(tick_counter % 4).unwrap())
337            }
338            CLIDisplayNodeType::ProgressBar(progress) => {
339                let mut lock = stdout().lock();
340                let progress = (progress.load(Relaxed) / 5).clamp(0, 20);
341
342                let _ = write!(lock, "[");
343
344                for _ in 0..progress {
345                    let _ = write!(lock, "#");
346                }
347
348                if progress != 20 {
349                    let _ = write!(lock, "{}", "/-\\|".chars().nth(tick_counter % 4).unwrap());
350                }
351
352                for _ in progress..19 {
353                    let _ = write!(lock, " ");
354                }
355
356                let _ = writeln!(lock, "]");
357            }
358        }
359    }
360}
361
362/// This macro can be used in modify calls to add lines to stdout without interrupting the [CLIDisplayManager]
363#[macro_export]
364macro_rules! erasing_println {
365    ($me:ident) => {{
366        let _: &mut $crate::CLIModificationElement = $me;
367        print!("{}\n", $crate::_ERASE_LINE)
368    }};
369    ($me:ident, $($arg:tt)*) => {{
370        let _: &mut $crate::CLIModificationElement = $me;
371        print!("{}{}\n", $crate::_ERASE_LINE, format_args!($($arg)*));
372    }};
373}