| 22 |
22 |
|
//! works at, and every one of them had been hand-rolled at least twice in this
|
| 23 |
23 |
|
//! tree before it was lifted.
|
| 24 |
24 |
|
//!
|
|
25 |
+ |
//! [`activity`] and [`awaiting`] joined them in 0.35.0, out of wiki
|
|
26 |
+ |
//! `loading-and-progress-standard`. They are the one pair here that arrived
|
|
27 |
+ |
//! before their second consumer rather than after it: nothing in the tree drew
|
|
28 |
+ |
//! a wait at all, on any surface, which is why the crate that had the vocabulary
|
|
29 |
+ |
//! for one had never been asked for the drawing.
|
|
30 |
+ |
//!
|
| 25 |
31 |
|
//! # What these take, and what they leave alone
|
| 26 |
32 |
|
//!
|
| 27 |
33 |
|
//! Each takes a `makeover-layout` description, a [`PieceStyle`], and whatever
|
| 44 |
50 |
|
//! nothing here places anything relative to anything else, because the moment
|
| 45 |
51 |
|
//! it did it would be a layout engine with one consumer's flow baked into it.
|
| 46 |
52 |
|
|
| 47 |
|
- |
use makeover_layout::{Act, Field, FieldKind, Figure, Heading, Meter, Token, Tone};
|
|
53 |
+ |
use makeover_layout::{Act, Awaiting, Field, FieldKind, Figure, Heading, Meter, Token, Tone};
|
| 48 |
54 |
|
use ratatui::buffer::Buffer;
|
| 49 |
55 |
|
use ratatui::layout::Rect;
|
| 50 |
56 |
|
use ratatui::style::{Modifier, Style};
|
| 51 |
57 |
|
use ratatui::text::{Line, Span};
|
| 52 |
58 |
|
|
| 53 |
59 |
|
use crate::text;
|
|
60 |
+ |
use std::time::Duration;
|
| 54 |
61 |
|
|
| 55 |
62 |
|
/// The colours and marks the drawings below use.
|
| 56 |
63 |
|
///
|
| 290 |
297 |
|
}
|
| 291 |
298 |
|
}
|
| 292 |
299 |
|
|
|
300 |
+ |
/// What a host can see about a wait that is running.
|
|
301 |
+ |
///
|
|
302 |
+ |
/// Neither half is derivable from a description, which is why both are here and
|
|
303 |
+ |
/// not on [`Awaiting`]. That type says how big the payload is; how much of it
|
|
304 |
+ |
/// has landed is a fact about a transfer in flight, and only whoever is running
|
|
305 |
+ |
/// the transfer knows it.
|
|
306 |
+ |
///
|
|
307 |
+ |
/// The same shape `makeover-immediate` carries, deliberately: a wait is one
|
|
308 |
+ |
/// reading on every surface and the two renderers should not disagree about
|
|
309 |
+ |
/// what a host owes them.
|
|
310 |
+ |
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
|
311 |
+ |
pub struct Progress {
|
|
312 |
+ |
/// How much has arrived, in whatever unit the description counted.
|
|
313 |
+ |
pub delivered: Option<u64>,
|
|
314 |
+ |
/// How long the wait has lasted so far.
|
|
315 |
+ |
///
|
|
316 |
+ |
/// The one time value a wait may show. See [`awaiting`] for the three it
|
|
317 |
+ |
/// may not.
|
|
318 |
+ |
pub elapsed: Option<Duration>,
|
|
319 |
+ |
}
|
|
320 |
+ |
|
|
321 |
+ |
/// The activity mark: one cell, lit or dark.
|
|
322 |
+ |
///
|
|
323 |
+ |
/// Rule 2 of wiki `loading-and-progress-standard`, and the surface the metaphor
|
|
324 |
+ |
/// came from. A hard-disk light is one cell that blinks, and a terminal draws
|
|
325 |
+ |
/// that with no metaphor in the way — where a webview needs a keyframe and egui
|
|
326 |
+ |
/// needs a repaint schedule, this is a character.
|
|
327 |
+ |
///
|
|
328 |
+ |
/// The two glyphs are [`PieceStyle::meter_full`] and
|
|
329 |
+ |
/// [`PieceStyle::meter_empty`], not a third pair. A bar's filled cell and a lit
|
|
330 |
+ |
/// mark are the same statement in the same alphabet, and a terminal that had to
|
|
331 |
+ |
/// render two vocabularies of "on" would be saying there are two kinds of on.
|
|
332 |
+ |
///
|
|
333 |
+ |
/// **Dark, not absent.** A mark that is drawn half the time is a hole in the
|
|
334 |
+ |
/// line, and the line reflows around it or the reader loses where to look. It
|
|
335 |
+ |
/// occupies its cell either way.
|
|
336 |
+ |
///
|
|
337 |
+ |
/// `lit` is the caller's: this module holds no clock. [`crate::activity_lit`]
|
|
338 |
+ |
/// is the one place the phase is worked out from the cadence, so a caller
|
|
339 |
+ |
/// should reach for that rather than dividing by 500 itself.
|
|
340 |
+ |
#[must_use]
|
|
341 |
+ |
pub fn activity(style: &PieceStyle, lit: bool) -> Span<'static> {
|
|
342 |
+ |
if lit {
|
|
343 |
+ |
Span::styled(style.meter_full.to_string(), style.action)
|
|
344 |
+ |
} else {
|
|
345 |
+ |
Span::styled(style.meter_empty.to_string(), style.muted)
|
|
346 |
+ |
}
|
|
347 |
+ |
}
|
|
348 |
+ |
|
|
349 |
+ |
/// A wait as one line, drawn from what is actually known about it.
|
|
350 |
+ |
///
|
|
351 |
+ |
/// [`Awaiting::is_determinate`] is the first branch and there is a second the
|
|
352 |
+ |
/// description cannot answer: whether anything is watching the transfer. A bar
|
|
353 |
+ |
/// wants a total and a numerator both, so a described amount with no
|
|
354 |
+ |
/// [`Progress::delivered`] beside it draws the mark and the size it is waiting
|
|
355 |
+ |
/// on, rather than an empty trough implying somebody is counting.
|
|
356 |
+ |
///
|
|
357 |
+ |
/// So three drawings for three states, which is the point:
|
|
358 |
+ |
///
|
|
359 |
+ |
/// ```text
|
|
360 |
+ |
/// unmeasured # a blinking cell
|
|
361 |
+ |
/// measured, nothing watching # 41943040 the cell, and how much there is
|
|
362 |
+ |
/// measured and observed ####------ 17825792/41943040 4s
|
|
363 |
+ |
/// ```
|
|
364 |
+ |
///
|
|
365 |
+ |
/// **What the bar may not do**, from rule 1 of the standard and from
|
|
366 |
+ |
/// [`Awaiting`]'s own docs: what is done over what there is, plus the time it
|
|
367 |
+ |
/// has taken. Never a remaining time, an arrival time, or a rate extrapolated
|
|
368 |
+ |
/// forward. A prediction is wrong the moment the transfer stalls, and being
|
|
369 |
+ |
/// confidently wrong is worse than being honestly indeterminate.
|
|
370 |
+ |
///
|
|
371 |
+ |
/// The numbers are raw. The unit is the app's — bytes for an upload, rows for
|
|
372 |
+ |
/// an import — and a renderer that formatted one as a file size would be
|
|
373 |
+ |
/// dressing up a quantity it was deliberately not told about.
|
|
374 |
+ |
#[must_use]
|
|
375 |
+ |
pub fn awaiting(
|
|
376 |
+ |
style: &PieceStyle,
|
|
377 |
+ |
awaiting: Awaiting,
|
|
378 |
+ |
progress: Progress,
|
|
379 |
+ |
lit: bool,
|
|
380 |
+ |
) -> Line<'static> {
|
|
381 |
+ |
let Some(total) = awaiting.amount else {
|
|
382 |
+ |
return Line::from(vec![activity(style, lit)]);
|
|
383 |
+ |
};
|
|
384 |
+ |
let Some(done) = progress.delivered else {
|
|
385 |
+ |
return Line::from(vec![
|
|
386 |
+ |
activity(style, lit),
|
|
387 |
+ |
Span::styled(format!(" {total}"), style.muted),
|
|
388 |
+ |
]);
|
|
389 |
+ |
};
|
|
390 |
+ |
let cells = u32::from(style.meter_cells);
|
|
391 |
+ |
// In cells rather than in floating point, the way `meter` does it: a
|
|
392 |
+ |
// terminal's bar has ten states and rounding through an f64 to reach one of
|
|
393 |
+ |
// ten is arithmetic nobody needs. Saturating rather than wrapping, because
|
|
394 |
+ |
// a transfer that over-delivers is a real case and a panicking bar is not
|
|
395 |
+ |
// the way to report it.
|
|
396 |
+ |
let filled = u32::try_from(
|
|
397 |
+ |
done.saturating_mul(u64::from(cells))
|
|
398 |
+ |
.checked_div(total)
|
|
399 |
+ |
.unwrap_or(0),
|
|
400 |
+ |
)
|
|
401 |
+ |
.unwrap_or(cells)
|
|
402 |
+ |
.min(cells);
|
|
403 |
+ |
let bar = format!(
|
|
404 |
+ |
"{}{}",
|
|
405 |
+ |
style.meter_full.to_string().repeat(filled as usize),
|
|
406 |
+ |
style
|
|
407 |
+ |
.meter_empty
|
|
408 |
+ |
.to_string()
|
|
409 |
+ |
.repeat((cells - filled) as usize)
|
|
410 |
+ |
);
|
|
411 |
+ |
let reading = match progress.elapsed {
|
|
412 |
+ |
Some(elapsed) => format!(" {done}/{total} {}s", elapsed.as_secs()),
|
|
413 |
+ |
None => format!(" {done}/{total}"),
|
|
414 |
+ |
};
|
|
415 |
+ |
Line::from(vec![
|
|
416 |
+ |
Span::styled(bar, style.action),
|
|
417 |
+ |
Span::styled(reading, style.muted),
|
|
418 |
+ |
])
|
|
419 |
+ |
}
|
|
420 |
+ |
|
| 293 |
421 |
|
/// A proportion as one line: the bar, then the reading beside it.
|
| 294 |
422 |
|
///
|
| 295 |
423 |
|
/// The reading is built here from the two numbers and the noun rather than
|
| 1299 |
1427 |
|
assert_eq!(painted.bg, None);
|
| 1300 |
1428 |
|
}
|
| 1301 |
1429 |
|
}
|
|
1430 |
+ |
|
|
1431 |
+ |
#[test]
|
|
1432 |
+ |
fn the_three_states_of_a_wait_are_three_drawings() {
|
|
1433 |
+ |
// The whole done condition of `5db1e0ed`: a measured wait and an
|
|
1434 |
+ |
// unmeasured one stopped being the same line.
|
|
1435 |
+ |
let style = PieceStyle::default();
|
|
1436 |
+ |
let bare = awaiting(&style, Awaiting::unmeasured(), Progress::default(), true);
|
|
1437 |
+ |
let sized = awaiting(&style, Awaiting::of(41_943_040), Progress::default(), true);
|
|
1438 |
+ |
let watched = awaiting(
|
|
1439 |
+ |
&style,
|
|
1440 |
+ |
Awaiting::of(40),
|
|
1441 |
+ |
Progress {
|
|
1442 |
+ |
delivered: Some(20),
|
|
1443 |
+ |
elapsed: Some(Duration::from_secs(4)),
|
|
1444 |
+ |
},
|
|
1445 |
+ |
true,
|
|
1446 |
+ |
);
|
|
1447 |
+ |
let read = |line: &Line<'_>| {
|
|
1448 |
+ |
line.spans
|
|
1449 |
+ |
.iter()
|
|
1450 |
+ |
.map(|s| s.content.to_string())
|
|
1451 |
+ |
.collect::<String>()
|
|
1452 |
+ |
};
|
|
1453 |
+ |
assert_eq!(read(&bare), "#");
|
|
1454 |
+ |
assert_eq!(read(&sized), "# 41943040");
|
|
1455 |
+ |
assert_eq!(read(&watched), "#####----- 20/40 4s");
|
|
1456 |
+ |
}
|
|
1457 |
+ |
|
|
1458 |
+ |
#[test]
|
|
1459 |
+ |
fn a_dark_mark_still_occupies_its_cell() {
|
|
1460 |
+ |
// Not absent. A line that reflowed every half second would move the
|
|
1461 |
+ |
// content beside it, and the reader would lose where to look.
|
|
1462 |
+ |
let style = PieceStyle::default();
|
|
1463 |
+ |
assert_eq!(activity(&style, true).content.chars().count(), 1);
|
|
1464 |
+ |
assert_eq!(activity(&style, false).content.chars().count(), 1);
|
|
1465 |
+ |
}
|
|
1466 |
+ |
|
|
1467 |
+ |
#[test]
|
|
1468 |
+ |
fn an_over_delivered_wait_clamps_and_does_not_panic() {
|
|
1469 |
+ |
// A transfer can hand over more than the size it announced, and the
|
|
1470 |
+ |
// bar has ten cells whatever happens.
|
|
1471 |
+ |
let style = PieceStyle::default();
|
|
1472 |
+ |
let over = awaiting(
|
|
1473 |
+ |
&style,
|
|
1474 |
+ |
Awaiting::of(4),
|
|
1475 |
+ |
Progress {
|
|
1476 |
+ |
delivered: Some(9),
|
|
1477 |
+ |
elapsed: None,
|
|
1478 |
+ |
},
|
|
1479 |
+ |
true,
|
|
1480 |
+ |
);
|
|
1481 |
+ |
assert!(over.spans[0].content.chars().all(|c| c == '#'));
|
|
1482 |
+ |
assert_eq!(over.spans[0].content.chars().count(), 10);
|
|
1483 |
+ |
// A zero payload is no payload rather than a finished one.
|
|
1484 |
+ |
let empty = awaiting(
|
|
1485 |
+ |
&style,
|
|
1486 |
+ |
Awaiting::of(0),
|
|
1487 |
+ |
Progress {
|
|
1488 |
+ |
delivered: Some(9),
|
|
1489 |
+ |
elapsed: None,
|
|
1490 |
+ |
},
|
|
1491 |
+ |
true,
|
|
1492 |
+ |
);
|
|
1493 |
+ |
assert!(empty.spans[0].content.starts_with('-'));
|
|
1494 |
+ |
}
|
| 1302 |
1495 |
|
}
|