cli_status_board/
state.rs

1use crate::{Status, TaskId, column::ColumnFit, internal_state::InternalState};
2use colored::Colorize;
3use std::{sync::mpsc::Sender, time::Duration};
4
5#[derive(Debug, Clone)]
6pub struct SBState {
7    sender: Sender<TaskEvent>,
8}
9
10// Configuration for the status board.
11#[derive(Debug, Clone)]
12pub struct SBStateConfig {
13    // Defines how we should render the task name.
14    // If unset then we'll restrict it to 50% of the available screen.
15    pub task_name_width: TaskNameWidth,
16
17    // Custom refresh rate for the status board. Defaults to 30 ms.
18    // Since this only actually rerenders when the terminal size has
19    // changed or there's a pending event this tends to be fine, but
20    // you can turn this down for better performance.
21    pub refresh_rate: Duration,
22
23    // If true then the status board won't actually render anything.
24    // Not terribly useful, apart from testing.
25    pub silent: bool,
26
27    // In the case that a line doesn't have any progress, should we grow it?
28    // Default to true
29    pub grow_if_no_progress: bool,
30}
31
32#[derive(Debug, Clone)]
33pub enum TaskNameWidth {
34    // Take up at least x% of the screen, growing if necessary
35    Min(f32),
36
37    // Take up at most x% of the screen, shrinking if possible
38    Max(f32),
39
40    // Take up exactly x% of the screen
41    ExactRatio(f32),
42
43    // Take up exactly x chars of the screen
44    ExactChars(usize),
45}
46
47impl Default for SBStateConfig {
48    fn default() -> Self {
49        Self {
50            silent: false,
51            refresh_rate: Duration::from_millis(30),
52            task_name_width: TaskNameWidth::Max(0.5),
53            grow_if_no_progress: true,
54        }
55    }
56}
57
58// Used internally to pipe commands over an mpsc channel.
59#[derive(Debug, PartialEq, Eq, Clone)]
60pub(crate) enum TaskEvent {
61    AddTask(TaskId, Option<String>, Status),
62    SetTaskDisplayName(TaskId, String),
63    UpdateTask(TaskId, Status),
64    DeleteTask(TaskId),
65    AddSubTask(TaskId, TaskId, Option<String>, Status),
66    UpdateSubTask(TaskId, TaskId, Status),
67}
68
69impl SBState {
70    pub fn new(config: SBStateConfig) -> Self {
71        let (sender, receiver) = std::sync::mpsc::channel::<TaskEvent>();
72
73        std::thread::spawn(move || -> ! {
74            let mut internal_state = InternalState::default();
75            let mut should_refresh_display = true;
76            let mut old_width = 0;
77            let mut old_height = 0;
78            loop {
79                for event in receiver.try_iter() {
80                    should_refresh_display = true;
81                    match event {
82                        TaskEvent::AddTask(key, maybe_display_name, status) => {
83                            internal_state.add_task(key.make_weak(), maybe_display_name, status);
84                        }
85                        TaskEvent::SetTaskDisplayName(key, display_name) => {
86                            internal_state.set_display_name(key.make_weak(), display_name);
87                        }
88                        TaskEvent::UpdateTask(key, status) => {
89                            internal_state.update_task(key.make_weak(), status);
90                        }
91                        TaskEvent::DeleteTask(key) => {
92                            internal_state.delete_task(key.make_weak());
93                        }
94                        TaskEvent::AddSubTask(key, subkey, maybe_display_name, status) => {
95                            internal_state.add_subtask(
96                                key.make_weak(),
97                                subkey,
98                                maybe_display_name,
99                                status,
100                            );
101                        }
102                        TaskEvent::UpdateSubTask(key, subkey, new_status) => {
103                            internal_state.update_subtask(key.make_weak(), subkey, new_status);
104                        }
105                    }
106                }
107
108                if let Ok((width, height)) = termion::terminal_size() {
109                    if width != old_width || height != old_height {
110                        old_height = height;
111                        old_width = width;
112                        should_refresh_display = true;
113                    }
114
115                    if !config.silent && should_refresh_display {
116                        // Reset the display
117                        print!("{}", termion::clear::All);
118                        print!("{}", termion::cursor::Goto(0, 1));
119
120                        internal_state.clear_old_entries(
121                            std::time::Duration::from_secs(10),
122                            &[Status::Error, Status::Info],
123                        );
124
125                        let num_finished = match internal_state.task_map.get(&Status::Finished) {
126                            Some(v) => v.len(),
127                            None => 0,
128                        };
129
130                        println!(
131                            "Finished tasks: {} / {}",
132                            format!("{}", num_finished).bright_green(),
133                            internal_state.get_total(),
134                        );
135
136                        let width = width as usize;
137                        let task_name_fit = match config.task_name_width {
138                            TaskNameWidth::Min(max) => {
139                                ColumnFit::MIN((max.min(1.0) * width as f32) as usize)
140                            }
141                            TaskNameWidth::Max(max) => {
142                                ColumnFit::MAX((max.min(1.0) * width as f32) as usize)
143                            }
144                            TaskNameWidth::ExactRatio(max) => {
145                                ColumnFit::EXACT((max.min(1.0) * width as f32) as usize)
146                            }
147                            TaskNameWidth::ExactChars(max) => ColumnFit::EXACT(max.min(width)),
148                        };
149
150                        internal_state.print_list(
151                            Status::Info,
152                            10,
153                            |f: &str| f.into(),
154                            width,
155                            ColumnFit::EXACT(width),
156                            &config,
157                        );
158                        internal_state.print_list(
159                            Status::Started,
160                            10,
161                            |f: &str| f.bright_green(),
162                            width,
163                            task_name_fit,
164                            &config,
165                        );
166                        internal_state.print_list(
167                            Status::Queued,
168                            10,
169                            |f: &str| f.bright_yellow(),
170                            width,
171                            task_name_fit,
172                            &config,
173                        );
174                        internal_state.print_list(
175                            Status::Error,
176                            10,
177                            |f: &str| f.bright_red(),
178                            width,
179                            task_name_fit,
180                            &config,
181                        );
182                    }
183                }
184
185                std::thread::sleep(config.refresh_rate);
186                should_refresh_display = false;
187            }
188        });
189
190        Self { sender }
191    }
192
193    pub fn error<S: ToString>(&self, display_name: S) {
194        let task_id = TaskId::new();
195        self.sender
196            .send(TaskEvent::AddTask(
197                task_id.clone(),
198                Some(display_name.to_string()),
199                Status::Error,
200            ))
201            .unwrap();
202    }
203
204    pub fn info<S: ToString>(&self, display_name: S) {
205        let task_id = TaskId::new();
206        self.sender
207            .send(TaskEvent::AddTask(
208                task_id.clone(),
209                Some(display_name.to_string()),
210                Status::Info,
211            ))
212            .unwrap();
213    }
214
215    pub fn add_task<S: ToString>(&self, display_name: S, status: Status) -> TaskId {
216        let task_id = TaskId::new_with_sender(self.sender.clone());
217        self.sender
218            .send(TaskEvent::AddTask(
219                task_id.clone(),
220                Some(display_name.to_string()),
221                status,
222            ))
223            .unwrap();
224        return task_id;
225    }
226
227    pub fn set_task_display_name(&self, task_id: &TaskId, display_name: String) {
228        self.sender
229            .send(TaskEvent::SetTaskDisplayName(task_id.clone(), display_name))
230            .unwrap();
231    }
232
233    pub fn delete_task(&self, task_id: &TaskId) {
234        self.sender
235            .send(TaskEvent::DeleteTask(task_id.clone()))
236            .unwrap();
237    }
238
239    pub fn update_task(&self, task_id: &TaskId, new_status: Status) {
240        self.sender
241            .send(TaskEvent::UpdateTask(task_id.clone(), new_status))
242            .unwrap();
243    }
244
245    pub fn add_subtask(&self, task_id: &TaskId, status: Status) -> TaskId {
246        let sub_task_id = TaskId::new();
247        self.sender
248            .send(TaskEvent::AddSubTask(
249                task_id.clone(),
250                sub_task_id.clone(),
251                None,
252                status,
253            ))
254            .unwrap();
255        return sub_task_id;
256    }
257
258    pub fn update_subtask(&self, task_id: &TaskId, sub_task_id: &TaskId, status: Status) {
259        self.sender
260            .send(TaskEvent::UpdateSubTask(
261                task_id.clone(),
262                sub_task_id.clone(),
263                status,
264            ))
265            .unwrap();
266    }
267}