| 71 |
71 |
|
use crate::cli::{CommandLog, Invocation, Secret};
|
| 72 |
72 |
|
use crate::field::TextField;
|
| 73 |
73 |
|
use crate::run::{Sequence, Stage};
|
| 74 |
|
- |
use crate::shell::{Confirm, Flow, TICK, View, block_title};
|
|
74 |
+ |
use crate::shell::{Confirm, Flow, TICK, View, block_title, truncate};
|
| 75 |
75 |
|
use crate::wizard::Steps;
|
| 76 |
76 |
|
|
| 77 |
77 |
|
/// The questions, in the order they are asked.
|
| 959 |
959 |
|
format!("{bytes} B")
|
| 960 |
960 |
|
}
|
| 961 |
961 |
|
|
|
962 |
+ |
/// The fixed-width columns of a disk row, before any styling.
|
|
963 |
+ |
///
|
|
964 |
+ |
/// Split out from [`InstallView::row`] because the widths are the whole point
|
|
965 |
+ |
/// of the row and styling needs a `Theme`, which a test cannot cheaply build.
|
|
966 |
+ |
///
|
|
967 |
+ |
/// Every column is clipped as well as padded. A width in a format spec is a
|
|
968 |
+ |
/// minimum, not a maximum, so a model string longer than its column pushes
|
|
969 |
+ |
/// every column after it along, and the disk the user is about to erase is then
|
|
970 |
+ |
/// identified by columns that have moved. Vendors ship forty-character model
|
|
971 |
+ |
/// strings, and this list is the last thing read before a command carrying
|
|
972 |
+ |
/// `--wipe`.
|
|
973 |
+ |
fn row_columns(disk: &Disk) -> [String; 4] {
|
|
974 |
+ |
/// Clipped to one char short of `width` so a full column still has a gap
|
|
975 |
+ |
/// after it, then padded back out. Same shape the audio list uses.
|
|
976 |
+ |
fn column(text: &str, width: usize) -> String {
|
|
977 |
+ |
format!("{:<width$}", truncate(text, width - 1))
|
|
978 |
+ |
}
|
|
979 |
+ |
|
|
980 |
+ |
[
|
|
981 |
+ |
column(&disk.name, 14),
|
|
982 |
+ |
// Right-aligned: sizes are compared down the column, and the digits
|
|
983 |
+ |
// only line up if the units do.
|
|
984 |
+ |
format!("{:>10} ", truncate(&format_size(disk.size), 10)),
|
|
985 |
+ |
column(disk.model_or_dash(), 24),
|
|
986 |
+ |
column(&disk.attachment(), 16),
|
|
987 |
+ |
]
|
|
988 |
+ |
}
|
|
989 |
+ |
|
| 962 |
990 |
|
// ---- backend ----
|
| 963 |
991 |
|
|
| 964 |
992 |
|
/// A source of disks.
|
| 1588 |
1616 |
|
frame.render_widget(Paragraph::new(lines), area);
|
| 1589 |
1617 |
|
}
|
| 1590 |
1618 |
|
|
|
1619 |
+ |
/// One disk, as a row: the columns from [`row_columns`], styled.
|
| 1591 |
1620 |
|
fn row<'a>(&self, theme: &Theme, disk: &'a Disk) -> Line<'a> {
|
| 1592 |
1621 |
|
let (status, severity) = match disk.blocker() {
|
| 1593 |
1622 |
|
Some(blocked) => (blocked.label(), Severity::Warn),
|
| 1594 |
1623 |
|
None => ("", Severity::Info),
|
| 1595 |
1624 |
|
};
|
| 1596 |
1625 |
|
|
| 1597 |
|
- |
let model = disk.model_or_dash();
|
|
1626 |
+ |
let [name, size, model, attachment] = row_columns(disk);
|
| 1598 |
1627 |
|
|
| 1599 |
1628 |
|
Line::from(vec![
|
| 1600 |
|
- |
text::bold(theme, format!("{:<14}", disk.name)),
|
| 1601 |
|
- |
text::primary(theme, format!("{:>10} ", format_size(disk.size))),
|
| 1602 |
|
- |
text::secondary(theme, format!("{model:<24}")),
|
| 1603 |
|
- |
text::muted(theme, format!("{:<16}", disk.attachment())),
|
|
1629 |
+ |
text::bold(theme, name),
|
|
1630 |
+ |
text::primary(theme, size),
|
|
1631 |
+ |
text::secondary(theme, model),
|
|
1632 |
+ |
text::muted(theme, attachment),
|
| 1604 |
1633 |
|
Span::styled(status.to_string(), severity.style(theme)),
|
| 1605 |
1634 |
|
])
|
| 1606 |
1635 |
|
}
|
| 1955 |
1984 |
|
assert_eq!(format_size(0), "0 B");
|
| 1956 |
1985 |
|
}
|
| 1957 |
1986 |
|
|
|
1987 |
+ |
// The columns are how the user tells one disk from another, so a long
|
|
1988 |
+ |
// field has to lose its own tail rather than shift everything after it.
|
|
1989 |
+ |
#[test]
|
|
1990 |
+ |
fn a_long_field_does_not_shift_the_columns_after_it() {
|
|
1991 |
+ |
let mut disk = disks().into_iter().find(|d| d.name == "sda").expect("sda");
|
|
1992 |
+ |
let short: Vec<usize> = row_columns(&disk)
|
|
1993 |
+ |
.iter()
|
|
1994 |
+ |
.map(|column| column.chars().count())
|
|
1995 |
+ |
.collect();
|
|
1996 |
+ |
|
|
1997 |
+ |
disk.model = Some("Seagate FireCuda 530 ZP4000GM30013 Heatsink".into());
|
|
1998 |
+ |
let widths: Vec<usize> = row_columns(&disk)
|
|
1999 |
+ |
.iter()
|
|
2000 |
+ |
.map(|column| column.chars().count())
|
|
2001 |
+ |
.collect();
|
|
2002 |
+ |
|
|
2003 |
+ |
assert_eq!(widths, short);
|
|
2004 |
+ |
assert_eq!(widths, [14, 12, 24, 16]);
|
|
2005 |
+ |
}
|
|
2006 |
+ |
|
|
2007 |
+ |
// Clipping counts chars, so a multibyte model neither panics nor spills.
|
|
2008 |
+ |
#[test]
|
|
2009 |
+ |
fn a_multibyte_model_is_clipped_by_chars() {
|
|
2010 |
+ |
let mut disk = disks().into_iter().find(|d| d.name == "sda").expect("sda");
|
|
2011 |
+ |
disk.model = Some("Königsberg Überspeicher 2000 Pro".into());
|
|
2012 |
+ |
|
|
2013 |
+ |
let [_, _, model, _] = row_columns(&disk);
|
|
2014 |
+ |
assert_eq!(model.chars().count(), 24);
|
|
2015 |
+ |
assert!(model.trim_end().ends_with('…'), "{model}");
|
|
2016 |
+ |
}
|
|
2017 |
+ |
|
| 1958 |
2018 |
|
// ---- the step ----
|
| 1959 |
2019 |
|
|
| 1960 |
2020 |
|
/// A view over the real fixture's disks, built directly rather than
|