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#[derive(Debug, Clone)]
12pub struct SBStateConfig {
13 pub task_name_width: TaskNameWidth,
16
17 pub refresh_rate: Duration,
22
23 pub silent: bool,
26
27 pub grow_if_no_progress: bool,
30}
31
32#[derive(Debug, Clone)]
33pub enum TaskNameWidth {
34 Min(f32),
36
37 Max(f32),
39
40 ExactRatio(f32),
42
43 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#[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 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}