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
28#[derive(Debug, Clone)]
29pub enum TaskNameWidth {
30 Min(f32),
32
33 Max(f32),
35
36 ExactRatio(f32),
38
39 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#[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 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}