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
28#[derive(Debug, Clone)]
29pub enum TaskNameWidth {
30    // Take up at least x% of the screen, growing if necessary
31    Min(f32),
32
33    // Take up at most x% of the screen, shrinking if possible
34    Max(f32),
35
36    // Take up exactly x% of the screen
37    ExactRatio(f32),
38
39    // Take up exactly x chars of the screen
40    ExactChars(usize),
41}
42
43impl Default for SBStateConfig {
44    fn default() -> Self {
45        Self {
46            silent: false,
47            refresh_rate: Duration::from_millis(30),
48            task_name_width: TaskNameWidth::Max(0.5),
49        }
50    }
51}
52
53// Used internally to pipe commands over an mpsc channel.
54#[derive(Debug, PartialEq, Eq, Clone)]
55pub(crate) enum TaskEvent {
56    AddTask(TaskId, Option<String>, Status),
57    SetTaskDisplayName(TaskId, String),
58    UpdateTask(TaskId, Status),
59    DeleteTask(TaskId),
60    AddSubTask(TaskId, TaskId, Option<String>, Status),
61    UpdateSubTask(TaskId, TaskId, Status),
62}
63
64impl SBState {
65    pub fn new(config: SBStateConfig) -> Self {
66        let (sender, receiver) = std::sync::mpsc::channel::<TaskEvent>();
67
68        std::thread::spawn(move || -> ! {
69            let mut internal_state = InternalState::default();
70            let mut should_refresh_display = true;
71            let mut old_width = 0;
72            let mut old_height = 0;
73            loop {
74                for event in receiver.try_iter() {
75                    should_refresh_display = true;
76                    match event {
77                        TaskEvent::AddTask(key, maybe_display_name, status) => {
78                            internal_state.add_task(key.make_weak(), maybe_display_name, status);
79                        }
80                        TaskEvent::SetTaskDisplayName(key, display_name) => {
81                            internal_state.set_display_name(key.make_weak(), display_name);
82                        }
83                        TaskEvent::UpdateTask(key, status) => {
84                            internal_state.update_task(key.make_weak(), status);
85                        }
86                        TaskEvent::DeleteTask(key) => {
87                            internal_state.delete_task(key.make_weak());
88                        }
89                        TaskEvent::AddSubTask(key, subkey, maybe_display_name, status) => {
90                            internal_state.add_subtask(
91                                key.make_weak(),
92                                subkey,
93                                maybe_display_name,
94                                status,
95                            );
96                        }
97                        TaskEvent::UpdateSubTask(key, subkey, new_status) => {
98                            internal_state.update_subtask(key.make_weak(), subkey, new_status);
99                        }
100                    }
101                }
102
103                if let Ok((width, height)) = termion::terminal_size() {
104                    if width != old_width || height != old_height {
105                        old_height = height;
106                        old_width = width;
107                        should_refresh_display = true;
108                    }
109
110                    if !config.silent && should_refresh_display {
111                        // Reset the display
112                        print!("{}", termion::clear::All);
113                        print!("{}", termion::cursor::Goto(0, 1));
114
115                        internal_state.clear_old_entries(
116                            std::time::Duration::from_secs(10),
117                            &[Status::Error, Status::Info],
118                        );
119
120                        let num_finished = match internal_state.task_map.get(&Status::Finished) {
121                            Some(v) => v.len(),
122                            None => 0,
123                        };
124
125                        println!(
126                            "Finished tasks: {} / {}",
127                            format!("{}", num_finished).bright_green(),
128                            internal_state.get_total(),
129                        );
130
131                        let width = width as usize;
132                        let task_name_fit = match config.task_name_width {
133                            TaskNameWidth::Min(max) => {
134                                ColumnFit::MIN((max.min(1.0) * width as f32) as usize)
135                            }
136                            TaskNameWidth::Max(max) => {
137                                ColumnFit::MAX((max.min(1.0) * width as f32) as usize)
138                            }
139                            TaskNameWidth::ExactRatio(max) => {
140                                ColumnFit::EXACT((max.min(1.0) * width as f32) as usize)
141                            }
142                            TaskNameWidth::ExactChars(max) => ColumnFit::EXACT(max.min(width)),
143                        };
144
145                        internal_state.print_list(
146                            Status::Info,
147                            10,
148                            |f: &str| f.into(),
149                            width,
150                            ColumnFit::EXACT(width),
151                        );
152                        internal_state.print_list(
153                            Status::Started,
154                            10,
155                            |f: &str| f.bright_green(),
156                            width,
157                            task_name_fit,
158                        );
159                        internal_state.print_list(
160                            Status::Queued,
161                            10,
162                            |f: &str| f.bright_yellow(),
163                            width,
164                            task_name_fit,
165                        );
166                        internal_state.print_list(
167                            Status::Error,
168                            10,
169                            |f: &str| f.bright_red(),
170                            width,
171                            task_name_fit,
172                        );
173                    }
174                }
175
176                std::thread::sleep(config.refresh_rate);
177                should_refresh_display = false;
178            }
179        });
180
181        Self { sender }
182    }
183
184    pub fn error<S: ToString>(&self, display_name: S) {
185        let task_id = TaskId::new();
186        self.sender
187            .send(TaskEvent::AddTask(
188                task_id.clone(),
189                Some(display_name.to_string()),
190                Status::Error,
191            ))
192            .unwrap();
193    }
194
195    pub fn info<S: ToString>(&self, display_name: S) {
196        let task_id = TaskId::new();
197        self.sender
198            .send(TaskEvent::AddTask(
199                task_id.clone(),
200                Some(display_name.to_string()),
201                Status::Info,
202            ))
203            .unwrap();
204    }
205
206    pub fn add_task<S: ToString>(&self, display_name: S, status: Status) -> TaskId {
207        let task_id = TaskId::new_with_sender(self.sender.clone());
208        self.sender
209            .send(TaskEvent::AddTask(
210                task_id.clone(),
211                Some(display_name.to_string()),
212                status,
213            ))
214            .unwrap();
215        return task_id;
216    }
217
218    pub fn set_task_display_name(&self, task_id: &TaskId, display_name: String) {
219        self.sender
220            .send(TaskEvent::SetTaskDisplayName(task_id.clone(), display_name))
221            .unwrap();
222    }
223
224    pub fn delete_task(&self, task_id: &TaskId) {
225        self.sender
226            .send(TaskEvent::DeleteTask(task_id.clone()))
227            .unwrap();
228    }
229
230    pub fn update_task(&self, task_id: &TaskId, new_status: Status) {
231        self.sender
232            .send(TaskEvent::UpdateTask(task_id.clone(), new_status))
233            .unwrap();
234    }
235
236    pub fn add_subtask(&self, task_id: &TaskId, status: Status) -> TaskId {
237        let sub_task_id = TaskId::new();
238        self.sender
239            .send(TaskEvent::AddSubTask(
240                task_id.clone(),
241                sub_task_id.clone(),
242                None,
243                status,
244            ))
245            .unwrap();
246        return sub_task_id;
247    }
248
249    pub fn update_subtask(&self, task_id: &TaskId, sub_task_id: &TaskId, status: Status) {
250        self.sender
251            .send(TaskEvent::UpdateSubTask(
252                task_id.clone(),
253                sub_task_id.clone(),
254                status,
255            ))
256            .unwrap();
257    }
258}