cli_status_board/
state.rs

1use crate::{Status, TaskId, 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    // If true then the status board won't actually render anything.
14    // Not terribly useful, apart from testing.
15    pub silent: bool,
16
17    // Custom refresh rate for the status board. Defaults to 250 ms.
18    pub refresh_rate: Duration,
19
20    // If set then we'll only show the first n characters of a task's name.
21    // If unset then we'll restrict it to 1/2 of the available screen.
22    // If the task's name is too long we'll truncate with a '...'.
23    pub max_task_name_width: Option<usize>,
24}
25
26impl Default for SBStateConfig {
27    fn default() -> Self {
28        Self {
29            silent: false,
30            refresh_rate: Duration::from_millis(250),
31            max_task_name_width: None,
32        }
33    }
34}
35
36// Used internally to pipe commands over an mpsc channel.
37#[derive(Debug, PartialEq, Eq, Clone)]
38pub(crate) enum TaskEvent {
39    AddTask(TaskId, Option<String>, Status),
40    SetTaskDisplayName(TaskId, String),
41    UpdateTask(TaskId, Status),
42    DeleteTask(TaskId),
43    AddSubTask(TaskId, TaskId, Option<String>, Status),
44    UpdateSubTask(TaskId, TaskId, Status),
45}
46
47impl SBState {
48    pub fn new(config: SBStateConfig) -> Self {
49        let (sender, receiver) = std::sync::mpsc::channel::<TaskEvent>();
50
51        std::thread::spawn(move || -> ! {
52            let mut internal_state = InternalState::default();
53            let mut should_refresh_display = true;
54            loop {
55                for event in receiver.try_iter() {
56                    should_refresh_display = true;
57                    match event {
58                        TaskEvent::AddTask(key, maybe_display_name, status) => {
59                            internal_state.add_task(key.make_weak(), maybe_display_name, status);
60                        }
61                        TaskEvent::SetTaskDisplayName(key, display_name) => {
62                            internal_state.set_display_name(key.make_weak(), display_name);
63                        }
64                        TaskEvent::UpdateTask(key, status) => {
65                            internal_state.update_task(key.make_weak(), status);
66                        }
67                        TaskEvent::DeleteTask(key) => {
68                            internal_state.delete_task(key.make_weak());
69                        }
70                        TaskEvent::AddSubTask(key, subkey, maybe_display_name, status) => {
71                            internal_state.add_subtask(
72                                key.make_weak(),
73                                subkey,
74                                maybe_display_name,
75                                status,
76                            );
77                        }
78                        TaskEvent::UpdateSubTask(key, subkey, new_status) => {
79                            internal_state.update_subtask(key.make_weak(), subkey, new_status);
80                        }
81                    }
82                }
83
84                if !config.silent && should_refresh_display {
85                    // Reset the display
86                    print!("{}", termion::clear::All);
87                    print!("{}", termion::cursor::Goto(0, 1));
88
89                    internal_state.clear_old_entries(
90                        std::time::Duration::from_secs(10),
91                        &[Status::Error, Status::Info],
92                    );
93
94                    let num_finished = match internal_state.task_map.get(&Status::Finished) {
95                        Some(v) => v.len(),
96                        None => 0,
97                    };
98
99                    println!(
100                        "Finished tasks: {} / {}",
101                        format!("{}", num_finished).bright_green(),
102                        internal_state.get_total(),
103                    );
104
105                    if let Ok((width, _height)) = termion::terminal_size() {
106                        let width = width as usize;
107                        let max_task_name_width =
108                            config.max_task_name_width.unwrap_or(width / 2).min(width);
109                        internal_state.print_simple(Status::Info, 10, |f: &str| f.into(), width);
110                        internal_state.print_complex(
111                            Status::Started,
112                            10,
113                            |f: &str| f.bright_green(),
114                            width,
115                            max_task_name_width,
116                        );
117                        internal_state.print_complex(
118                            Status::Queued,
119                            10,
120                            |f: &str| f.bright_yellow(),
121                            width,
122                            max_task_name_width,
123                        );
124                        internal_state.print_complex(
125                            Status::Error,
126                            10,
127                            |f: &str| f.bright_red(),
128                            width,
129                            max_task_name_width,
130                        );
131                    }
132                }
133
134                std::thread::sleep(config.refresh_rate);
135                should_refresh_display = false;
136            }
137        });
138
139        Self { sender }
140    }
141
142    pub fn error<S: ToString>(&self, display_name: S) {
143        let task_id = TaskId::new();
144        self.sender
145            .send(TaskEvent::AddTask(
146                task_id.clone(),
147                Some(display_name.to_string()),
148                Status::Error,
149            ))
150            .unwrap();
151    }
152
153    pub fn info<S: ToString>(&self, display_name: S) {
154        let task_id = TaskId::new();
155        self.sender
156            .send(TaskEvent::AddTask(
157                task_id.clone(),
158                Some(display_name.to_string()),
159                Status::Info,
160            ))
161            .unwrap();
162    }
163
164    pub fn add_task<S: ToString>(&self, display_name: S, status: Status) -> TaskId {
165        let task_id = TaskId::new_with_sender(self.sender.clone());
166        self.sender
167            .send(TaskEvent::AddTask(
168                task_id.clone(),
169                Some(display_name.to_string()),
170                status,
171            ))
172            .unwrap();
173        return task_id;
174    }
175
176    pub fn set_task_display_name(&self, task_id: &TaskId, display_name: String) {
177        self.sender
178            .send(TaskEvent::SetTaskDisplayName(task_id.clone(), display_name))
179            .unwrap();
180    }
181
182    pub fn delete_task(&self, task_id: &TaskId) {
183        self.sender
184            .send(TaskEvent::DeleteTask(task_id.clone()))
185            .unwrap();
186    }
187
188    pub fn update_task(&self, task_id: &TaskId, new_status: Status) {
189        self.sender
190            .send(TaskEvent::UpdateTask(task_id.clone(), new_status))
191            .unwrap();
192    }
193
194    pub fn add_subtask(&self, task_id: &TaskId, status: Status) -> TaskId {
195        let sub_task_id = TaskId::new();
196        self.sender
197            .send(TaskEvent::AddSubTask(
198                task_id.clone(),
199                sub_task_id.clone(),
200                None,
201                status,
202            ))
203            .unwrap();
204        return sub_task_id;
205    }
206
207    pub fn update_subtask(&self, task_id: &TaskId, sub_task_id: &TaskId, status: Status) {
208        self.sender
209            .send(TaskEvent::UpdateSubTask(
210                task_id.clone(),
211                sub_task_id.clone(),
212                status,
213            ))
214            .unwrap();
215    }
216}