1#![doc = include_str!("../examples/progress.rs")]
19#![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
61pub 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 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 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
190pub struct CLIModificationElement<'a> {
192 root_node: &'a RwLock<CLIDisplayNode>,
193 additions: isize,
194}
195
196impl<'a> CLIModificationElement<'a> {
197 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 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 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 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
321pub enum CLIDisplayNodeType {
323 Message(Cow<'static, str>),
325 SpinningMessage(Cow<'static, str>),
327 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#[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}