| 53 |
53 |
|
//!
|
| 54 |
54 |
|
//! <!-- wiki: alloy-console -->
|
| 55 |
55 |
|
|
|
56 |
+ |
use std::time::Duration;
|
|
57 |
+ |
|
| 56 |
58 |
|
use alloy_tui::{AlloyBlock, AlloyList, Hint, Severity, Theme, hint, text};
|
| 57 |
59 |
|
use anyhow::{Context, Result};
|
| 58 |
60 |
|
use ratatui::Frame;
|
| 69 |
71 |
|
use crate::cli::{CommandLog, Invocation, Secret};
|
| 70 |
72 |
|
use crate::field::TextField;
|
| 71 |
73 |
|
use crate::run::{Sequence, Stage};
|
| 72 |
|
- |
use crate::shell::{Confirm, Flow, View, block_title};
|
|
74 |
+ |
use crate::shell::{Confirm, Flow, TICK, View, block_title};
|
| 73 |
75 |
|
use crate::wizard::Steps;
|
| 74 |
76 |
|
|
| 75 |
77 |
|
/// The questions, in the order they are asked.
|
| 244 |
246 |
|
/// mountpoint used throughout.
|
| 245 |
247 |
|
const TARGET_MOUNT: &str = "/mnt/alloy-target";
|
| 246 |
248 |
|
|
|
249 |
+ |
/// The activity light's lit and dim glyphs.
|
|
250 |
+ |
///
|
|
251 |
+ |
/// Block elements from CP437, which the Linux console's default font carries —
|
|
252 |
+ |
/// the same family as the box-drawing the rest of the UI already uses. A
|
|
253 |
+ |
/// brighter and a fainter block rather than a glyph and a blank, so the light
|
|
254 |
+ |
/// blinks in place instead of flickering away.
|
|
255 |
+ |
const LIGHT_ON: char = '█';
|
|
256 |
+ |
const LIGHT_OFF: char = '░';
|
|
257 |
+ |
|
|
258 |
+ |
/// How often the run screen redraws, which is how fast the light blinks.
|
|
259 |
+ |
///
|
|
260 |
+ |
/// Short enough that the light reads as blinking rather than as a screen that
|
|
261 |
+ |
/// changed once, long enough that polling the running command a few times a
|
|
262 |
+ |
/// second stays cheap. In effect only while the install runs; every other
|
|
263 |
+ |
/// step ticks at the shell's default second.
|
|
264 |
+ |
const BLINK_INTERVAL: Duration = Duration::from_millis(400);
|
|
265 |
+ |
|
|
266 |
+ |
/// The activity light's glyph for a given blink count: lit on even, dim on odd.
|
|
267 |
+ |
fn blink_glyph(blink: usize) -> char {
|
|
268 |
+ |
if blink % 2 == 0 { LIGHT_ON } else { LIGHT_OFF }
|
|
269 |
+ |
}
|
|
270 |
+ |
|
| 247 |
271 |
|
/// Find the ostree deployment directory beneath a mounted target root.
|
| 248 |
272 |
|
///
|
| 249 |
273 |
|
/// **This is why the earlier design was wrong, not merely unverified.** An
|
| 1145 |
1169 |
|
/// happens after the last answer. While it is `Some` it owns the screen:
|
| 1146 |
1170 |
|
/// there is nothing to ask and nothing to go back to.
|
| 1147 |
1171 |
|
running: Option<Sequence>,
|
|
1172 |
+ |
/// Counts up while the install runs, to blink the activity light.
|
|
1173 |
+ |
///
|
|
1174 |
+ |
/// Some steps — settling udev, importing 86 layers — print nothing for a
|
|
1175 |
+ |
/// second or more, and a screen that has not changed reads as a hang. The
|
|
1176 |
+ |
/// light blinks off this counter so a silent step still visibly works.
|
|
1177 |
+ |
blink: usize,
|
| 1148 |
1178 |
|
}
|
| 1149 |
1179 |
|
|
| 1150 |
1180 |
|
impl InstallView {
|
| 1167 |
1197 |
|
answers: Answers::default(),
|
| 1168 |
1198 |
|
error: None,
|
| 1169 |
1199 |
|
running: None,
|
|
1200 |
+ |
blink: 0,
|
| 1170 |
1201 |
|
};
|
| 1171 |
1202 |
|
view.refresh(log);
|
| 1172 |
1203 |
|
view
|
| 1438 |
1469 |
|
let (done, total) = sequence.progress();
|
| 1439 |
1470 |
|
|
| 1440 |
1471 |
|
let status = match sequence.outcome() {
|
| 1441 |
|
- |
None => Line::from(text::muted(
|
| 1442 |
|
- |
theme,
|
| 1443 |
|
- |
format!("Installing. Step {} of {total}.", done + 1),
|
| 1444 |
|
- |
)),
|
|
1472 |
+ |
None => Line::from(vec![
|
|
1473 |
+ |
text::muted(theme, format!("Installing. Step {} of {total}. ", done + 1)),
|
|
1474 |
+ |
self.activity_light(theme),
|
|
1475 |
+ |
]),
|
| 1445 |
1476 |
|
Some(Ok(())) => Line::from(Span::styled(
|
| 1446 |
1477 |
|
"Installation finished. Reboot to start Alloy.".to_string(),
|
| 1447 |
1478 |
|
Severity::Healthy.style(theme),
|
| 1467 |
1498 |
|
frame.render_widget(Paragraph::new(lines), area);
|
| 1468 |
1499 |
|
}
|
| 1469 |
1500 |
|
|
|
1501 |
+ |
/// The activity light, lit or dim by the blink counter.
|
|
1502 |
+ |
///
|
|
1503 |
+ |
/// A solid block that blinks against a faint one, so the cell keeps its
|
|
1504 |
+ |
/// place rather than flickering in and out. On, then off, one tick each.
|
|
1505 |
+ |
fn activity_light(&self, theme: &Theme) -> Span<'static> {
|
|
1506 |
+ |
match blink_glyph(self.blink) {
|
|
1507 |
+ |
LIGHT_ON => text::primary(theme, LIGHT_ON.to_string()),
|
|
1508 |
+ |
glyph => text::muted(theme, glyph.to_string()),
|
|
1509 |
+ |
}
|
|
1510 |
+ |
}
|
|
1511 |
+ |
|
| 1470 |
1512 |
|
/// Raise the wipe confirmation.
|
| 1471 |
1513 |
|
///
|
| 1472 |
1514 |
|
/// Two gates of different kinds, on purpose. The summary is the one you
|
| 1667 |
1709 |
|
self.running = Some(Sequence::new(self.plan()));
|
| 1668 |
1710 |
|
}
|
| 1669 |
1711 |
|
|
| 1670 |
|
- |
/// Drive the running install.
|
|
1712 |
+ |
/// Drive the running install and advance the activity light.
|
| 1671 |
1713 |
|
///
|
| 1672 |
|
- |
/// This is the tick the shell already calls once a second, so the run
|
| 1673 |
|
- |
/// screen costs nothing on any other step and needs no timer of its own.
|
|
1714 |
+ |
/// The shell calls this every [`tick_interval`](Self::tick_interval), which
|
|
1715 |
+ |
/// the run screen shortens so the light blinks. On any other step there is
|
|
1716 |
+ |
/// no sequence, the interval is the default second, and this does nothing.
|
| 1674 |
1717 |
|
fn tick(&mut self, log: &mut CommandLog) {
|
| 1675 |
|
- |
if let Some(sequence) = &mut self.running {
|
| 1676 |
|
- |
sequence.poll(log);
|
|
1718 |
+ |
let Some(sequence) = &mut self.running else {
|
|
1719 |
+ |
return;
|
|
1720 |
+ |
};
|
|
1721 |
+ |
sequence.poll(log);
|
|
1722 |
+ |
// Freeze the light once the run is over: a finished or failed install
|
|
1723 |
+ |
// is not still working, and its status line says which.
|
|
1724 |
+ |
if !sequence.is_done() {
|
|
1725 |
+ |
self.blink = self.blink.wrapping_add(1);
|
|
1726 |
+ |
}
|
|
1727 |
+ |
}
|
|
1728 |
+ |
|
|
1729 |
+ |
/// Blink fast while installing, idle at the shell's default otherwise.
|
|
1730 |
+ |
fn tick_interval(&self) -> Duration {
|
|
1731 |
+ |
match &self.running {
|
|
1732 |
+ |
Some(sequence) if !sequence.is_done() => BLINK_INTERVAL,
|
|
1733 |
+ |
_ => TICK,
|
| 1677 |
1734 |
|
}
|
| 1678 |
1735 |
|
}
|
| 1679 |
1736 |
|
|
| 1900 |
1957 |
|
answers: Answers::default(),
|
| 1901 |
1958 |
|
error: None,
|
| 1902 |
1959 |
|
running: None,
|
|
1960 |
+ |
blink: 0,
|
| 1903 |
1961 |
|
};
|
| 1904 |
1962 |
|
view.cursor.resize(view.disks.len());
|
| 1905 |
1963 |
|
(view, CommandLog::new())
|
| 2812 |
2870 |
|
assert_eq!(view.step(), Step::Summary, "a key moved the wizard");
|
| 2813 |
2871 |
|
}
|
| 2814 |
2872 |
|
|
|
2873 |
+ |
// The light alternates every tick, and off is a different glyph, not a
|
|
2874 |
+ |
// gap: the cell keeps its place so the light blinks rather than flickers.
|
|
2875 |
+ |
#[test]
|
|
2876 |
+ |
fn the_activity_light_blinks() {
|
|
2877 |
+ |
assert_eq!(blink_glyph(0), LIGHT_ON);
|
|
2878 |
+ |
assert_eq!(blink_glyph(1), LIGHT_OFF);
|
|
2879 |
+ |
assert_eq!(blink_glyph(2), LIGHT_ON);
|
|
2880 |
+ |
assert_ne!(LIGHT_ON, LIGHT_OFF);
|
|
2881 |
+ |
}
|
|
2882 |
+ |
|
|
2883 |
+ |
// A queued install redraws fast enough to blink; every other step waits
|
|
2884 |
+ |
// the shell's default second, so an idle wizard costs no busy-looping.
|
|
2885 |
+ |
#[test]
|
|
2886 |
+ |
fn the_run_screen_ticks_faster_than_an_idle_step() {
|
|
2887 |
+ |
let (mut view, mut log) = at_summary();
|
|
2888 |
+ |
assert_eq!(view.tick_interval(), TICK, "an idle step should not spin");
|
|
2889 |
+ |
|
|
2890 |
+ |
view.confirmed(&mut log);
|
|
2891 |
+ |
assert_eq!(
|
|
2892 |
+ |
view.tick_interval(),
|
|
2893 |
+ |
BLINK_INTERVAL,
|
|
2894 |
+ |
"a running install should redraw to blink"
|
|
2895 |
+ |
);
|
|
2896 |
+ |
assert!(BLINK_INTERVAL < TICK);
|
|
2897 |
+ |
}
|
|
2898 |
+ |
|
| 2815 |
2899 |
|
#[test]
|
| 2816 |
2900 |
|
fn the_title_names_the_backend_and_the_position() {
|
| 2817 |
2901 |
|
let (view, _log) = view();
|