1use std::{fmt, sync::atomic::Ordering, time::Duration};
2
3use tui::{
4 buffer::Buffer,
5 layout::Rect,
6 style::{Color, Modifier, Style},
7};
8use tui_react::fill_background;
9
10use crate::{
11 progress::{self, Key, Step, Task, Value},
12 render::tui::{
13 draw::State,
14 utils::{
15 block_width, draw_text_nowrap_fn, draw_text_with_ellipsis_nowrap, rect, sanitize_offset,
16 GraphemeCountWriter, VERTICAL_LINE,
17 },
18 InterruptDrawInfo,
19 },
20 time::format_now_datetime_seconds,
21 unit, Throughput,
22};
23
24const MIN_TREE_WIDTH: u16 = 20;
25
26pub fn pane(entries: &[(Key, progress::Task)], mut bound: Rect, buf: &mut Buffer, state: &mut State) {
27 state.task_offset = sanitize_offset(state.task_offset, entries.len(), bound.height);
28 let needs_overflow_line =
29 if entries.len() > bound.height as usize || (state.task_offset).min(entries.len() as u16) > 0 {
30 bound.height = bound.height.saturating_sub(1);
31 true
32 } else {
33 false
34 };
35 state.task_offset = sanitize_offset(state.task_offset, entries.len(), bound.height);
36
37 if entries.is_empty() {
38 return;
39 }
40
41 let initial_column_width = bound.width / 3;
42 let desired_max_tree_draw_width = *state.next_tree_column_width.as_ref().unwrap_or(&initial_column_width);
43 {
44 if initial_column_width >= MIN_TREE_WIDTH {
45 let tree_bound = Rect {
46 width: desired_max_tree_draw_width,
47 ..bound
48 };
49 let computed = draw_tree(entries, buf, tree_bound, state.task_offset);
50 state.last_tree_column_width = Some(computed);
51 } else {
52 state.last_tree_column_width = Some(0);
53 };
54 }
55
56 {
57 if let Some(tp) = state.throughput.as_mut() {
58 tp.update_elapsed();
59 }
60
61 let progress_area = rect::offset_x(bound, desired_max_tree_draw_width);
62 draw_progress(
63 entries,
64 buf,
65 progress_area,
66 state.task_offset,
67 state.throughput.as_mut(),
68 );
69
70 if let Some(tp) = state.throughput.as_mut() {
71 tp.reconcile(entries);
72 }
73 }
74
75 if needs_overflow_line {
76 let overflow_rect = Rect {
77 y: bound.height + 1,
78 height: 1,
79 ..bound
80 };
81 draw_overflow(
82 entries,
83 buf,
84 overflow_rect,
85 desired_max_tree_draw_width,
86 bound.height,
87 state.task_offset,
88 );
89 }
90}
91
92pub(crate) fn headline(
93 entries: &[(Key, Task)],
94 interrupt_mode: InterruptDrawInfo,
95 duration_per_frame: Duration,
96 buf: &mut Buffer,
97 bound: Rect,
98) {
99 let (num_running_tasks, num_blocked_tasks, num_groups) = entries.iter().fold(
100 (0, 0, 0),
101 |(mut running, mut blocked, mut groups), (_key, Task { progress, .. })| {
102 match progress.as_ref().map(|p| p.state) {
103 Some(progress::State::Running) => running += 1,
104 Some(progress::State::Blocked(_, _)) | Some(progress::State::Halted(_, _)) => blocked += 1,
105 None => groups += 1,
106 }
107 (running, blocked, groups)
108 },
109 );
110 let text = format!(
111 " {} {} {:3} running + {:3} blocked + {:3} groups = {} ",
112 match interrupt_mode {
113 InterruptDrawInfo::Instantly => "'q' or CTRL+c to quit",
114 InterruptDrawInfo::Deferred(interrupt_requested) => {
115 if interrupt_requested {
116 "interrupt requested - please wait"
117 } else {
118 "cannot interrupt current operation"
119 }
120 }
121 },
122 if duration_per_frame > Duration::from_secs(1) {
123 format!(
124 " Every {}s → {}",
125 duration_per_frame.as_secs(),
126 format_now_datetime_seconds()
127 )
128 } else {
129 "".into()
130 },
131 num_running_tasks,
132 num_blocked_tasks,
133 num_groups,
134 entries.len()
135 );
136
137 let bold = Style::default().add_modifier(Modifier::BOLD);
138 draw_text_with_ellipsis_nowrap(rect::snap_to_right(bound, block_width(&text) + 1), buf, text, bold);
139}
140
141struct ProgressFormat<'a>(&'a Option<Value>, u16, Option<unit::display::Throughput>);
142
143impl fmt::Display for ProgressFormat<'_> {
144 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145 match self.0 {
146 Some(p) => match p.unit.as_ref() {
147 Some(unit) => write!(
148 f,
149 "{}",
150 unit.display(p.step.load(Ordering::SeqCst), p.done_at, self.2.clone())
151 ),
152 None => match p.done_at {
153 Some(done_at) => write!(f, "{}/{}", p.step.load(Ordering::SeqCst), done_at),
154 None => write!(f, "{}", p.step.load(Ordering::SeqCst)),
155 },
156 },
157 None => write!(f, "{:─<width$}", '─', width = self.1 as usize),
158 }
159 }
160}
161
162fn has_child(entries: &[(Key, Task)], index: usize) -> bool {
163 entries
164 .get(index + 1)
165 .and_then(|(other_key, other_val)| {
166 entries.get(index).map(|(cur_key, _)| {
167 cur_key.shares_parent_with(other_key, cur_key.level()) && other_val.progress.is_some()
168 })
169 })
170 .unwrap_or(false)
171}
172
173pub fn draw_progress(
174 entries: &[(Key, Task)],
175 buf: &mut Buffer,
176 bound: Rect,
177 offset: u16,
178 mut throughput: Option<&mut Throughput>,
179) {
180 let title_spacing = 2u16 + 1; let max_progress_label_width = entries
182 .iter()
183 .skip(offset as usize)
184 .take(bound.height as usize)
185 .map(|(_, Task { progress, .. })| progress)
186 .fold(0, |state, progress| match progress {
187 progress @ Some(_) => {
188 use std::io::Write;
189 let mut w = GraphemeCountWriter::default();
190 write!(w, "{}", ProgressFormat(progress, 0, None)).expect("never fails");
191 state.max(w.0)
192 }
193 None => state,
194 });
195
196 for (
197 line,
198 (
199 entry_index,
200 (
201 key,
202 Task {
203 progress,
204 name: title,
205 id: _,
206 },
207 ),
208 ),
209 ) in entries
210 .iter()
211 .enumerate()
212 .skip(offset as usize)
213 .take(bound.height as usize)
214 .enumerate()
215 {
216 let throughput = throughput
217 .as_mut()
218 .and_then(|tp| tp.update_and_get(key, progress.as_ref()));
219 let line_bound = rect::line_bound(bound, line);
220 let progress_text = format!(
221 " {progress}",
222 progress = ProgressFormat(
223 progress,
224 if has_child(entries, entry_index) {
225 bound.width.saturating_sub(title_spacing)
226 } else {
227 0
228 },
229 throughput
230 )
231 );
232
233 draw_text_with_ellipsis_nowrap(line_bound, buf, VERTICAL_LINE, None);
234
235 let tree_prefix = level_prefix(entries, entry_index);
236 let progress_rect = rect::offset_x(line_bound, block_width(&tree_prefix));
237 draw_text_with_ellipsis_nowrap(line_bound, buf, tree_prefix, None);
238 match progress
239 .as_ref()
240 .map(|p| (p.fraction(), p.state, p.step.load(Ordering::SeqCst)))
241 {
242 Some((Some(fraction), state, _step)) => {
243 let mut progress_text = progress_text;
244 add_block_eta(state, &mut progress_text);
245 let (bound, style) = draw_progress_bar_fn(buf, progress_rect, fraction, |fraction| match state {
246 progress::State::Blocked(_, _) => Color::Red,
247 progress::State::Halted(_, _) => Color::LightRed,
248 progress::State::Running => {
249 if fraction >= 0.8 {
250 Color::Green
251 } else {
252 Color::Yellow
253 }
254 }
255 });
256 let style_fn = move |_t: &str, x: u16, _y: u16| {
257 if x < bound.right() {
258 style
259 } else {
260 Style::default()
261 }
262 };
263 draw_text_nowrap_fn(progress_rect, buf, progress_text, style_fn);
264 }
265 Some((None, state, step)) => {
266 let mut progress_text = progress_text;
267 add_block_eta(state, &mut progress_text);
268 draw_text_with_ellipsis_nowrap(progress_rect, buf, progress_text, None);
269 let bar_rect = rect::offset_x(line_bound, max_progress_label_width as u16);
270 draw_spinner(
271 buf,
272 bar_rect,
273 step,
274 line,
275 match state {
276 progress::State::Blocked(_, _) => Color::Red,
277 progress::State::Halted(_, _) => Color::LightRed,
278 progress::State::Running => Color::White,
279 },
280 );
281 }
282 None => {
283 let bold = Style::default().add_modifier(Modifier::BOLD);
284 draw_text_nowrap_fn(progress_rect, buf, progress_text, |_, _, _| Style::default());
285 draw_text_with_ellipsis_nowrap(progress_rect, buf, format!(" {} ", title), bold);
286 }
287 }
288 }
289}
290
291fn add_block_eta(state: progress::State, progress_text: &mut String) {
292 match state {
293 progress::State::Blocked(reason, maybe_eta) | progress::State::Halted(reason, maybe_eta) => {
294 progress_text.push_str(" [");
295 progress_text.push_str(reason);
296 progress_text.push(']');
297 if let Some(eta) = maybe_eta {
298 let eta = jiff::Timestamp::try_from(eta).expect("reasonable system time");
299 let now = jiff::Timestamp::now();
300 if eta > now {
301 use std::fmt::Write;
302 write!(
303 progress_text,
304 " → {:#} to {}",
305 eta.duration_since(now),
306 if let progress::State::Blocked(_, _) = state {
307 "unblock"
308 } else {
309 "continue"
310 }
311 )
312 .expect("in-memory writes never fail");
313 }
314 }
315 }
316 progress::State::Running => {}
317 }
318}
319
320fn draw_spinner(buf: &mut Buffer, bound: Rect, step: Step, seed: usize, color: Color) {
321 if bound.width == 0 {
322 return;
323 }
324 let x = bound.x + ((step + seed) % bound.width as usize) as u16;
325 let width = 5;
326 let bound = rect::intersect(Rect { x, width, ..bound }, bound);
327 tui_react::fill_background(bound, buf, color);
328}
329
330fn draw_progress_bar_fn(
331 buf: &mut Buffer,
332 bound: Rect,
333 fraction: f32,
334 style: impl FnOnce(f32) -> Color,
335) -> (Rect, Style) {
336 if bound.width == 0 {
337 return (Rect::default(), Style::default());
338 }
339 let mut fractional_progress_rect = Rect {
340 width: ((bound.width as f32 * fraction).floor() as u16).min(bound.width),
341 ..bound
342 };
343 let color = style(fraction);
344 for y in fractional_progress_rect.top()..fractional_progress_rect.bottom() {
345 for x in fractional_progress_rect.left()..fractional_progress_rect.right() {
346 let cell = buf.get_mut(x, y);
347 cell.set_fg(color);
348 cell.set_symbol(tui::symbols::block::FULL);
349 }
350 }
351 if fractional_progress_rect.width < bound.width {
352 static BLOCK_SECTIONS: [&str; 9] = [
353 " ",
354 tui::symbols::block::ONE_EIGHTH,
355 tui::symbols::block::ONE_QUARTER,
356 tui::symbols::block::THREE_EIGHTHS,
357 tui::symbols::block::HALF,
358 tui::symbols::block::FIVE_EIGHTHS,
359 tui::symbols::block::THREE_QUARTERS,
360 tui::symbols::block::SEVEN_EIGHTHS,
361 tui::symbols::block::FULL,
362 ];
363 let index = ((((bound.width as f32 * fraction) - fractional_progress_rect.width as f32) * 8f32).round()
365 as usize)
366 % BLOCK_SECTIONS.len();
367 let cell = buf.get_mut(fractional_progress_rect.right(), bound.y);
368 cell.set_symbol(BLOCK_SECTIONS[index]);
369 cell.set_fg(color);
370 fractional_progress_rect.width += 1;
371 }
372 (fractional_progress_rect, Style::default().bg(color).fg(Color::Black))
373}
374
375pub fn draw_tree(entries: &[(Key, Task)], buf: &mut Buffer, bound: Rect, offset: u16) -> u16 {
376 let mut max_prefix_len = 0;
377 for (line, (entry_index, entry)) in entries
378 .iter()
379 .enumerate()
380 .skip(offset as usize)
381 .take(bound.height as usize)
382 .enumerate()
383 {
384 let mut line_bound = rect::line_bound(bound, line);
385 line_bound.x = line_bound.x.saturating_sub(1);
386 line_bound.width = line_bound.width.saturating_sub(1);
387 let tree_prefix = format!("{} {} ", level_prefix(entries, entry_index), entry.1.name);
388 max_prefix_len = max_prefix_len.max(block_width(&tree_prefix));
389
390 let style = if entry.1.progress.is_none() {
391 Style::default().add_modifier(Modifier::BOLD).into()
392 } else {
393 None
394 };
395 draw_text_with_ellipsis_nowrap(line_bound, buf, tree_prefix, style);
396 }
397 max_prefix_len
398}
399
400fn level_prefix(entries: &[(Key, Task)], entry_index: usize) -> String {
401 let adj = Key::adjacency(entries, entry_index);
402 let key = entries[entry_index].0;
403 let key_level = key.level();
404 let is_orphan = adj.level() != key_level;
405 let mut buf = String::with_capacity(key_level as usize);
406 for level in 1..=key_level {
407 use crate::progress::key::SiblingLocation::*;
408 let is_child_level = level == key_level;
409 if level != 1 {
410 buf.push(' ');
411 }
412 if level == 1 && is_child_level {
413 buf.push(match adj[level] {
414 AboveAndBelow | Above => '├',
415 NotFound | Below => '│',
416 });
417 } else {
418 let c = if is_child_level {
419 match adj[level] {
420 NotFound => {
421 if is_orphan {
422 ' '
423 } else {
424 '·'
425 }
426 }
427 Above => '└',
428 Below => '┌',
429 AboveAndBelow => '├',
430 }
431 } else {
432 match adj[level] {
433 NotFound => {
434 if level == 1 {
435 '│'
436 } else if is_orphan {
437 '·'
438 } else {
439 ' '
440 }
441 }
442 Above => '└',
443 Below => '┌',
444 AboveAndBelow => '│',
445 }
446 };
447 buf.push(c)
448 }
449 }
450 buf
451}
452
453pub fn draw_overflow(
454 entries: &[(Key, Task)],
455 buf: &mut Buffer,
456 bound: Rect,
457 label_offset: u16,
458 num_entries_on_display: u16,
459 offset: u16,
460) {
461 let (count, mut progress_fraction) = entries
462 .iter()
463 .take(offset as usize)
464 .chain(entries.iter().skip((offset + num_entries_on_display) as usize))
465 .fold((0usize, 0f32), |(count, progress_fraction), (_key, value)| {
466 let progress = value.progress.as_ref().and_then(|p| p.fraction()).unwrap_or_default();
467 (count + 1, progress_fraction + progress)
468 });
469 progress_fraction /= count as f32;
470 let label = format!(
471 "{} …{} skipped and {} more",
472 if label_offset == 0 { "" } else { VERTICAL_LINE },
473 offset,
474 entries
475 .len()
476 .saturating_sub((offset + num_entries_on_display + 1) as usize)
477 );
478 let (progress_rect, style) = draw_progress_bar_fn(buf, bound, progress_fraction, |_| Color::Green);
479
480 let bg_color = Color::Red;
481 fill_background(rect::offset_x(bound, progress_rect.right() - 1), buf, bg_color);
482 let color_text_according_to_progress = move |_g: &str, x: u16, _y: u16| {
483 if x < progress_rect.right() {
484 style
485 } else {
486 style.bg(bg_color)
487 }
488 };
489 draw_text_nowrap_fn(
490 rect::offset_x(bound, label_offset),
491 buf,
492 label,
493 color_text_according_to_progress,
494 );
495 let help_text = "⇊ = d|↓ = j|⇈ = u|↑ = k ";
496 draw_text_nowrap_fn(
497 rect::snap_to_right(bound, block_width(help_text)),
498 buf,
499 help_text,
500 color_text_according_to_progress,
501 );
502}