Skip to main content

max / alloy

Write the console palette in the format setvtrgb actually reads alloy-vtrgb.service has failed on every boot of every Alloy image ever built, and the palette it exists to apply has never once been applied. Found by booting the first server image, where that service is the only themed surface there is: setvtrgb: Error: /usr/share/alloy/vtrgb: Insufficient number of fields in line 1. kbd's setvtrgb parses each line comma-delimited. skelgen joined with spaces, inherited from the vtrgb.py it replaced, so the file was rejected at parse time before a console was ever opened. The sibling that renders the same sixteen colors for the kernel cmdline has always joined with commas, which is why the kernel half worked and this half did not. Three things carried the mistake and all three are corrected: the doc comment described the format as whitespace-separated, the test pinned the retired script's output including its spaces, and the build guard split on awk's default whitespace, so it validated the wrong property and passed on a file setvtrgb refused. The guard now splits on commas and rejects whitespace, and the test asserts the shape rather than a literal, so changing the values cannot reintroduce the separator. Verified both ways against the kbd in the built image: the space form dies at parsing, the comma form parses and reaches the console.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-04 00:00 UTC
Signed with PGP, not checked
Commit: 3b2ea825dde694532152ff06a56412b25521b57e
Parent: 722b1a9
2 files changed, +51 insertions, -11 deletions
M Containerfile +6 -2
@@ -243,8 +243,12 @@
243 243 [ "$rendered" -eq "$((templates + variants))" ] \
244 244 || { echo "skelgen wrote $rendered files; $templates templates of which $variants render twice should have produced $((templates + variants))" >&2; exit 1; }; \
245 245 for table in /staged-skel/usr/share/alloy/vtrgb /staged-skel/usr/share/alloy/vtrgb.night; do \
246 - awk 'NF != 16 { exit 1 } END { if (NR != 3) exit 1 }' "$table" \
247 - || { echo "$table is not three rows of sixteen values; setvtrgb would reject it" >&2; exit 1; }; \
246 + : "commas, and no whitespace: setvtrgb parses each line comma-delimited"; \
247 + : "and rejects anything else before it opens a console. This guard split"; \
248 + : "on whitespace until 2026-08-03 and so passed a file setvtrgb refused,"; \
249 + : "which is why the palette never once applied on any image."; \
250 + awk -F, 'NF != 16 { exit 1 } /[[:space:]]/ { exit 1 } END { if (NR != 3) exit 1 }' "$table" \
251 + || { echo "$table is not three rows of sixteen comma-separated values; setvtrgb would reject it" >&2; exit 1; }; \
248 252 done; \
249 253 for night in $(find /staged-skel/etc/skel -type f -name '*.night'); do \
250 254 rel="${night#/staged-skel/etc/skel/}"; \
@@ -223,9 +223,20 @@
223 223 }
224 224
225 225 /// The whole `setvtrgb` table, in the file format that tool reads: three
226 - /// whitespace-separated lines of sixteen decimal values, all reds then all
226 + /// lines of sixteen comma-separated decimal values, all reds then all
227 227 /// greens then all blues.
228 228 ///
229 + /// **Comma, not whitespace.** kbd's `setvtrgb` parses each line with a
230 + /// comma-delimited scan and rejects anything else with "Insufficient number
231 + /// of fields", which is a parse failure before it ever opens a console.
232 + /// This emitted spaces from the day it was written, inherited from the
233 + /// `vtrgb.py` it replaced, so `alloy-vtrgb.service` failed on every boot of
234 + /// every image and the console palette has never once been applied. Found
235 + /// 2026-08-03 by booting a server image, where that service is the only
236 + /// themed surface there is. Verified both ways against the shipped kbd: the
237 + /// space form dies at parsing, the comma form parses and reaches the
238 + /// console.
239 + ///
229 240 /// The kernel cmdline (`vt.default_*`, via [`Palette::vt_channel`]) and
230 241 /// this file are the same sixteen colors applied twice — once by the kernel
231 242 /// before userspace, once by `alloy-vtrgb.service` after. They were
@@ -246,7 +257,7 @@
246 257 .to_string()
247 258 })
248 259 .collect();
249 - out.push_str(&row.join(" "));
260 + out.push_str(&row.join(","));
250 261 out.push('\n');
251 262 }
252 263 Ok(out)
@@ -748,17 +759,42 @@
748 759 assert_eq!(p.ansi(15).unwrap().to_hex(), "#f0ece4");
749 760 }
750 761
751 - // The table skelgen emits has to be the one vtrgb.py emitted, because the
752 - // greeter's look was tuned against it. This is that script's output for
753 - // Akari Dawn, captured before it was deleted.
762 + // The values are the ones vtrgb.py emitted, because the greeter's look was
763 + // tuned against them. The separator is not: that script wrote spaces, this
764 + // inherited them, and `setvtrgb` reads commas, so the table it produced was
765 + // rejected at parse time on every boot for as long as either existed. The
766 + // test that used to live here pinned the whole string including the
767 + // spaces, which is how a bug gets a guard pointed the wrong way.
754 768 #[test]
755 - fn the_vtrgb_table_matches_what_the_retired_script_produced() {
756 - let want = "26 106 58 176 48 128 48 237 81 138 58 176 48 128 48 240\n\
757 - 24 40 88 120 64 96 88 231 75 69 88 120 64 96 88 236\n\
758 - 22 40 48 64 80 128 88 222 69 48 48 64 80 128 88 228\n";
769 + fn the_vtrgb_table_carries_the_tuned_values_in_the_format_setvtrgb_reads() {
770 + let want = "26,106,58,176,48,128,48,237,81,138,58,176,48,128,48,240\n\
771 + 24,40,88,120,64,96,88,231,75,69,88,120,64,96,88,236\n\
772 + 22,40,48,64,80,128,88,222,69,48,48,64,80,128,88,228\n";
759 773 assert_eq!(dawn().vtrgb_table().unwrap(), want);
760 774 }
761 775
776 + // The property behind that literal, stated so a future edit to the values
777 + // cannot quietly reintroduce the separator bug: three lines, sixteen
778 + // comma-separated fields each, every field a decimal byte. This is
779 + // `setvtrgb`'s documented format and the whole of what its parser accepts.
780 + #[test]
781 + fn every_vtrgb_line_is_sixteen_comma_separated_bytes() {
782 + for palette in [dawn(), night()] {
783 + let table = palette.vtrgb_table().unwrap();
784 + let lines: Vec<&str> = table.lines().collect();
785 + assert_eq!(lines.len(), 3, "{table}");
786 + for line in lines {
787 + assert!(!line.contains(' '), "a space would fail the parse: {line}");
788 + let fields: Vec<&str> = line.split(',').collect();
789 + assert_eq!(fields.len(), 16, "{line}");
790 + assert!(
791 + fields.iter().all(|f| f.parse::<u8>().is_ok()),
792 + "every field is a decimal byte: {line}"
793 + );
794 + }
795 + }
796 + }
797 +
762 798 fn night() -> Palette {
763 799 let dir = makeover::bundled_themes_dir().expect("makeover bundles its themes");
764 800 let theme =