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 ({}). Stopping the status bar.", e);
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 = format!(
84"{save_cursor}{last_line}{clear}{restore_scroll_region}{restore_cursor}",
85 save_cursor = SAVE_CURSOR,
86 last_line = LAST_LINE,
87 clear = CLEAR,
88 restore_scroll_region = RESTORE_SCROLL_REGION,
89 restore_cursor = RESTORE_CURSOR,
90 );
9192 std::io::stderr().write_all(to_print.as_bytes()).unwrap();
93let _ = std::io::stderr().flush();
94 }
9596/// Stop and remove the status bar.
97pub fn stop(self) {
98// will be stopped in the drop handler
99}
100101pub fn status(&self) -> &Arc<Status<T>> {
102&self.state
103 }
104}
105106impl<T: 'static + StatusBarState> std::ops::Drop for StatusBar<T> {
107fn drop(&mut self) {
108self.stop_flag
109 .swap(true, std::sync::atomic::Ordering::Relaxed);
110if let Some(handle) = self.thread.take() {
111if let Err(e) = handle.join() {
112log::warn!("Progress bar thread did not exit cleanly: {:?}", e);
113 }
114 }
115 }
116}
117118pub struct StatusPrinter<T: 'static + StatusBarState> {
119 state: Arc<Status<T>>,
120 stop_sender: Option<std::sync::mpsc::Sender<()>>,
121 thread: Option<std::thread::JoinHandle<()>>,
122}
123124impl<T: 'static + StatusBarState> StatusPrinter<T> {
125/// Create and start printing the status.
126pub fn new(state: T) -> Self {
127let state = Arc::new(Status::new(state));
128let (stop_sender, stop_receiver) = std::sync::mpsc::channel();
129130Self {
131 state: Arc::clone(&state),
132 stop_sender: Some(stop_sender),
133 thread: Some(std::thread::spawn(move || {
134Self::print_loop(state, stop_receiver);
135 })),
136 }
137 }
138139fn print_loop(state: Arc<Status<T>>, stop_receiver: std::sync::mpsc::Receiver<()>) {
140let print_interval = Duration::from_secs(60);
141142loop {
143match stop_receiver.recv_timeout(print_interval) {
144// the sender disconnects to signal that we should stop
145Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break,
146Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {}
147Ok(()) => unreachable!(),
148 }
149150// We want to write everything in as few write() syscalls as possible. Note that
151 // if we were to use eprint! with a format string like "{}{}", eprint! would
152 // always make at least two write() syscalls, which we wouldn't want.
153let to_write = format!("Progress: {}\n", *state.inner.read().unwrap());
154 std::io::stderr().write_all(to_write.as_bytes()).unwrap();
155let _ = std::io::stderr().flush();
156 }
157 }
158159/// Stop printing the status.
160pub fn stop(self) {
161// will be stopped in the drop handler
162}
163164pub fn status(&self) -> &Arc<Status<T>> {
165&self.state
166 }
167}
168169impl<T: 'static + StatusBarState> std::ops::Drop for StatusPrinter<T> {
170fn drop(&mut self) {
171// drop the sender to disconnect it
172self.stop_sender.take();
173if let Some(handle) = self.thread.take() {
174if let Err(e) = handle.join() {
175log::warn!("Progress thread did not exit cleanly: {:?}", e);
176 }
177 }
178 }
179}
180181/// The status bar's internal state.
182#[derive(Debug)]
183pub struct Status<T> {
184// we wrap an RwLock to hide the implementation details, for example we might want to replace
185 // this with a faster-writing lock in the future
186inner: RwLock<T>,
187}
188189impl<T> Status<T> {
190fn new(inner: T) -> Self {
191Self {
192 inner: RwLock::new(inner),
193 }
194 }
195196/// Update the status bar's internal state. The status will be shown to the user the next time
197 /// that the status bar redraws.
198pub fn update(&self, f: impl FnOnce(&mut T)) {
199 f(&mut *self.inner.write().unwrap())
200 }
201}
202203nix::ioctl_read_bad!(_tiocgwinsz, libc::TIOCGWINSZ, libc::winsize);
204205fn tiocgwinsz() -> nix::Result<libc::winsize> {
206let mut win_size: libc::winsize = unsafe { std::mem::zeroed() };
207unsafe { _tiocgwinsz(0, &mut win_size)? };
208Ok(win_size)
209}