Skip to main content

max / alloy

install, cli, run: clip the disk columns and hold the ring bounds Three of the same shape as the two fixes before: a bound declared in one place and enforced by something written independently. The disk list padded its columns but never clipped them. A width in a format spec is a minimum, so a model string past 24 characters shifted every column after it, on the list read immediately before a command carrying --wipe. The columns move to row_columns, which clips through shell::truncate the way the audio list already does, and which a test can reach without building a Theme. push_line and CommandLog::record trimmed on len() == CAPACITY. That holds the bound only while every append comes through the one function, which is a fact about today's callers rather than about the buffer. One append elsewhere and neither would ever trim again.
Author: Max Johnson <me@maxj.phd> · 2026-07-22 21:56 UTC
Signed with PGP, not checked
Commit: 82ef53c5994a1e96a5fa0527d1250e355e7d898a
Parent: 82e348e
3 files changed, +106 insertions, -8 deletions
@@ -60,7 +60,10 @@
60 60 if self.muted {
61 61 return;
62 62 }
63 - if self.entries.len() == LOG_CAPACITY {
63 + // `>=` rather than `==`: an equality test holds the bound only while
64 + // every push comes through here, which is a fact about the callers
65 + // rather than about the log.
66 + while self.entries.len() >= LOG_CAPACITY {
64 67 self.entries.pop_front();
65 68 }
66 69 self.entries.push_back(LogEntry::new(command, outcome));
@@ -573,6 +576,22 @@
573 576 );
574 577 }
575 578
579 + // The bound belongs to the log, not to the one function that appends to it
580 + // today. An equality test enforces it only while that stays true.
581 + #[test]
582 + fn a_log_already_over_capacity_is_trimmed_back_to_it() {
583 + let mut log = CommandLog::new();
584 + for i in 0..LOG_CAPACITY + 5 {
585 + log.entries
586 + .push_back(LogEntry::new(format!("cmd {i}"), Severity::Healthy));
587 + }
588 +
589 + log.record("newest", Severity::Healthy);
590 +
591 + assert_eq!(log.entries().len(), LOG_CAPACITY);
592 + assert_eq!(log.entries()[LOG_CAPACITY - 1].command, "newest");
593 + }
594 +
576 595 #[test]
577 596 fn quiet_suppresses_recording_and_restores_after() {
578 597 let mut log = CommandLog::new();
@@ -71,7 +71,7 @@
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,6 +959,34 @@
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,19 +1616,20 @@
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,6 +1984,37 @@
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
@@ -242,8 +242,13 @@
242 242 }
243 243
244 244 /// Append a line, holding the buffer to [`SCROLLBACK`].
245 + ///
246 + /// `>=` rather than `==`: an equality test holds the bound only while every
247 + /// append in the program comes through here, which is a fact about the callers
248 + /// rather than about the buffer. One append somewhere else and this stops
249 + /// trimming for good.
245 250 fn push_line(into: &mut Vec<String>, line: String) {
246 - if into.len() == SCROLLBACK {
251 + while into.len() >= SCROLLBACK {
247 252 into.remove(0);
248 253 }
249 254 into.push(line);
@@ -778,6 +783,20 @@
778 783 assert!(sequence.outcome().expect("stopped").is_err());
779 784 }
780 785
786 + // The bound is the buffer's, not the caller's. An equality test enforces
787 + // it only while every append comes through push_line, which is a fact
788 + // about today's callers rather than about the buffer, and one append
789 + // elsewhere would leave it never trimming again.
790 + #[test]
791 + fn a_buffer_already_over_the_bound_is_trimmed_back_to_it() {
792 + let mut lines: Vec<String> = (0..SCROLLBACK + 5).map(|i| i.to_string()).collect();
793 +
794 + push_line(&mut lines, "newest".into());
795 +
796 + assert_eq!(lines.len(), SCROLLBACK);
797 + assert_eq!(lines.last().expect("appended"), "newest");
798 + }
799 +
781 800 #[test]
782 801 fn progress_counts_completed_commands() {
783 802 let mut log = CommandLog::new();