1use std::io::Write;
2use std::sync::atomic::AtomicBool;
3use std::sync::{Arc, RwLock};
4use std::time::Duration;
56const SAVE_CURSOR: &str = "\u{1B}[s";
7const RESTORE_CURSOR: &str = "\u{1B}[u";
8const NEXT_LINE: &str = "\u{1B}[1E";
9const PREV_LINE: &str = "\u{1B}[1F";
10const CLEAR: &str = "\u{1B}[K";
11const RESTORE_SCROLL_REGION: &str = "\u{1B}[r";
12const LAST_LINE: &str = "\u{1B}[9999H";
1314pub trait StatusBarState: std::fmt::Display + std::marker::Send + std::marker::Sync {}
15impl<T> StatusBarState for T where T: std::fmt::Display + std::marker::Send + std::marker::Sync {}
1617pub struct StatusBar<T: 'static + StatusBarState> {
18 state: Arc<Status<T>>,
19 stop_flag: Arc<AtomicBool>,
20 thread: Option<std::thread::JoinHandle<()>>,
21}
2223impl<T: 'static + StatusBarState> StatusBar<T> {
24/// Create and start drawing the status bar.
25pub fn new(state: T, redraw_interval: Duration) -> Self {
26let state = Arc::new(Status::new(state));
27let stop_flag = Arc::new(AtomicBool::new(false));
2829Self {
30 state: Arc::clone(&state),
31 stop_flag: Arc::clone(&stop_flag),
32 thread: Some(std::thread::spawn(move || {
33Self::redraw_loop(state, stop_flag, redraw_interval);
34 })),
35 }
36 }
3738fn redraw_loop(state: Arc<Status<T>>, stop_flag: Arc<AtomicBool>, redraw_interval: Duration) {
39// we re-draw the status bar every interval, even if the state hasn't changed, since the
40 // terminal might have been resized and the scroll region might have been reset
41while !stop_flag.load(std::sync::atomic::Ordering::Acquire) {
42// the window size might change during the simulation, so we re-check it each time
43let rows = match tiocgwinsz() {
44Ok(x) => x.ws_row,
45Err(e) => {
46log::error!("Status bar ioctl failed ({e}). Stopping the status bar.");
47break;
48 }
49 };
5051if rows > 1 {
52#[rustfmt::skip]
53let to_print = [
54// Restore the scroll region since some terminals handle scroll regions
55 // differently. For example, when using '{next_line}' some terminals will
56 // allow the cursor to move outside of the scroll region, and others don't.
57SAVE_CURSOR, RESTORE_SCROLL_REGION, RESTORE_CURSOR,
58// This will scroll the buffer up only if the cursor is on the last row.
59SAVE_CURSOR, "\n", RESTORE_CURSOR,
60// This will move the cursor up only if the cursor is on the last row (to
61 // match the previous scroll behaviour).
62NEXT_LINE, PREV_LINE,
63// The cursor is currently at the correct location, so save it for later.
64SAVE_CURSOR,
65// Set the scroll region to include all rows but the last.
66&format!("\u{1B}[1;{}r", rows - 1),
67// Move to the last row and write the message.
68LAST_LINE, &format!("{}", *state.inner.read().unwrap()), CLEAR,
69// Restore the cursor position.
70RESTORE_CURSOR,
71 ]
72 .join("");
7374// We want to write everything in as few write() syscalls as possible. Note that
75 // if we were to use eprint! with a format string like "{}{}", eprint! would
76 // always make at least two write() syscalls, which we wouldn't want.
77std::io::stderr().write_all(to_print.as_bytes()).unwrap();
78let _ = std::io::stderr().flush();
79 }
80 std::thread::sleep(redraw_interval);
81 }
8283let to_print =
84format!("{SAVE_CURSOR}{LAST_LINE}{CLEAR}{RESTORE_SCROLL_REGION}{RESTORE_CURSOR}");
8586 std::io::stderr().write_all(to_print.as_bytes()).unwrap();
87let _ = std::io::stderr().flush();
88 }
8990/// Stop and remove the status bar.
91pub fn stop(self) {
92// will be stopped in the drop handler
93}
9495pub fn status(&self) -> &Arc<Status<T>> {
96&self.state
97 }
98}
99100impl<T: 'static + StatusBarState> std::ops::Drop for StatusBar<T> {
101fn drop(&mut self) {
102self.stop_flag
103 .swap(true, std::sync::atomic::Ordering::Relaxed);
104if let Some(handle) = self.thread.take() {
105if let Err(e) = handle.join() {
106log::warn!("Progress bar thread did not exit cleanly: {e:?}");
107 }
108 }
109 }
110}
111112pub struct StatusPrinter<T: 'static + StatusBarState> {
113 state: Arc<Status<T>>,
114 stop_sender: Option<std::sync::mpsc::Sender<()>>,
115 thread: Option<std::thread::JoinHandle<()>>,
116}
117118impl<T: 'static + StatusBarState> StatusPrinter<T> {
119/// Create and start printing the status.
120pub fn new(state: T) -> Self {
121let state = Arc::new(Status::new(state));
122let (stop_sender, stop_receiver) = std::sync::mpsc::channel();
123124Self {
125 state: Arc::clone(&state),
126 stop_sender: Some(stop_sender),
127 thread: Some(std::thread::spawn(move || {
128Self::print_loop(state, stop_receiver);
129 })),
130 }
131 }
132133fn print_loop(state: Arc<Status<T>>, stop_receiver: std::sync::mpsc::Receiver<()>) {
134let print_interval = Duration::from_secs(60);
135136loop {
137match stop_receiver.recv_timeout(print_interval) {
138// the sender disconnects to signal that we should stop
139Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break,
140Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {}
141Ok(()) => unreachable!(),
142 }
143144// We want to write everything in as few write() syscalls as possible. Note that
145 // if we were to use eprint! with a format string like "{}{}", eprint! would
146 // always make at least two write() syscalls, which we wouldn't want.
147let to_write = format!("Progress: {}\n", *state.inner.read().unwrap());
148 std::io::stderr().write_all(to_write.as_bytes()).unwrap();
149let _ = std::io::stderr().flush();
150 }
151 }
152153/// Stop printing the status.
154pub fn stop(self) {
155// will be stopped in the drop handler
156}
157158pub fn status(&self) -> &Arc<Status<T>> {
159&self.state
160 }
161}
162163impl<T: 'static + StatusBarState> std::ops::Drop for StatusPrinter<T> {
164fn drop(&mut self) {
165// drop the sender to disconnect it
166self.stop_sender.take();
167if let Some(handle) = self.thread.take() {
168if let Err(e) = handle.join() {
169log::warn!("Progress thread did not exit cleanly: {e:?}");
170 }
171 }
172 }
173}
174175/// The status bar's internal state.
176#[derive(Debug)]
177pub struct Status<T> {
178// we wrap an RwLock to hide the implementation details, for example we might want to replace
179 // this with a faster-writing lock in the future
180inner: RwLock<T>,
181}
182183impl<T> Status<T> {
184fn new(inner: T) -> Self {
185Self {
186 inner: RwLock::new(inner),
187 }
188 }
189190/// Update the status bar's internal state. The status will be shown to the user the next time
191 /// that the status bar redraws.
192pub fn update(&self, f: impl FnOnce(&mut T)) {
193 f(&mut *self.inner.write().unwrap())
194 }
195}
196197nix::ioctl_read_bad!(_tiocgwinsz, libc::TIOCGWINSZ, libc::winsize);
198199fn tiocgwinsz() -> nix::Result<libc::winsize> {
200let mut win_size: libc::winsize = unsafe { std::mem::zeroed() };
201unsafe { _tiocgwinsz(0, &mut win_size)? };
202Ok(win_size)
203}