Skip to main content

max / alloy

Bring the lockscreen and the image viewer under the generator swaylock and imv were the two files the first templating pass left in etc/skel, and the reason was the same for both: neither can read `#rrggbb`. swaylock takes bare `rrggbbaa` with the alpha always written, imv takes bare `rrggbb`, and every skelgen expression resolved to a `#` and six digits. So both stayed pinned to Akari Dawn, which on the lock path meant locking a night session flashed a light screen at you. So skelgen grows output formats: hex_bare, hex_alpha, and rgba for decimal channels. The last of those closes a gap zathura's own comment had already diagnosed and specified, leaving no color in the tree written by hand. Day renders are value-identical to the files they replace. The literal guard could not have caught any of this: it looked for a `#` and a seven-character word, so bare hex was invisible to it. It now checks both spellings, verified by putting imv's old literal back and watching it fail. Also: eleven empty directories from the first pass were still on disk, untracked, which made every stale docs link resolve on this machine and nowhere else. Gone, and the nine links they were hiding now point into templates/. TOKENS.md described mode selection as an OSC background query, which the console deliberately does not do, and called `alloy theme` deferred after it shipped.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-30 03:12 UTC
Signed with PGP, not checked
Commit: 9bc820e0c22f062555e24f0280dfdc7691a023a8
Parent: 7b17509
8 files changed, +286 insertions, -90 deletions
M docs/STACK.md +9 -9
@@ -132,7 +132,7 @@
132 132 - [`etc/firefox/policies/policies.json`](../etc/firefox/policies/policies.json): enterprise policy. Pins **uBlock Origin** as a force-installed, update-locked extension. Disables telemetry, Pocket, studies, sponsored tiles, new-tab feed, formfill, and password saving at the policy layer (higher-precedence than user prefs).
133 133 - [`usr/lib64/firefox/mozilla.cfg`](../usr/lib64/firefox/mozilla.cfg): system-wide default prefs, loaded via Firefox autoconfig. Configures compact UI density, blank new tab, DuckDuckGo suggestions off, quiet scroll, and enables `toolkit.legacyUserProfileCustomizations.stylesheets` so `userChrome.css` gets read. Every entry uses `defaultPref()` so users can still override in `about:config`.
134 134 - [`usr/lib64/firefox/defaults/pref/autoconfig.js`](../usr/lib64/firefox/defaults/pref/autoconfig.js): one-line pointer telling Firefox to load `mozilla.cfg` at startup.
135 - - [`etc/skel/.mozilla/firefox/profiles.ini`](../etc/skel/.mozilla/firefox/profiles.ini) + [`etc/skel/.mozilla/firefox/alloy.default/chrome/userChrome.css`](../etc/skel/.mozilla/firefox/alloy.default/chrome/userChrome.css): first-launch profile seed. `profiles.ini` names a fixed-path profile (`alloy.default/`) so the pre-seeded `chrome/userChrome.css` (Helium-style ultra-compact horizontal, Alloy light-mode tokens from [TOKENS.md](TOKENS.md)) lands under it. Firefox opens the seeded profile on first launch instead of generating a random-suffix one.
135 + - [`etc/skel/.mozilla/firefox/profiles.ini`](../etc/skel/.mozilla/firefox/profiles.ini) + [`etc/skel/.mozilla/firefox/alloy.default/chrome/userChrome.css`](../templates/etc/skel/.mozilla/firefox/alloy.default/chrome/userChrome.css.in): first-launch profile seed. `profiles.ini` names a fixed-path profile (`alloy.default/`) so the pre-seeded `chrome/userChrome.css` (Helium-style ultra-compact horizontal, Alloy light-mode tokens from [TOKENS.md](TOKENS.md)) lands under it. Firefox opens the seeded profile on first launch instead of generating a random-suffix one.
136 136
137 137 Runs all modern sites (uBlock Origin blocks ads, never JS). Horizontal tabs only, per firm preference.
138 138
@@ -206,13 +206,13 @@
206 206
207 207 ### Video: **mpv**
208 208
209 - C, mature, keyboard-driven, no chrome by default, extensible via Lua. Handles local files, streams, and YouTube via yt-dlp. Zero visual identity out of the box, which is exactly what a video player should be. Config at [`etc/skel/.config/mpv/`](../etc/skel/.config/mpv/): hardware decode, no OSC/OSD chatter, screenshots to `~/Pictures/Screenshots`, yt-dlp capped at 1080p.
209 + C, mature, keyboard-driven, no chrome by default, extensible via Lua. Handles local files, streams, and YouTube via yt-dlp. Zero visual identity out of the box, which is exactly what a video player should be. Config at [`templates/etc/skel/.config/mpv/`](../templates/etc/skel/.config/mpv/): hardware decode, no OSC/OSD chatter, screenshots to `~/Pictures/Screenshots`, yt-dlp capped at 1080p.
210 210
211 211 Rejected: VLC (retained-mode GTK UI, oversized surface), celluloid (adds retained UI on top of mpv, defeating the point), Haruna (Qt/KDE-shaped). No production-quality Rust video player exists.
212 212
213 213 ### Images: **imv**
214 214
215 - C, Wayland-native, tiny, dedicated to viewing. Config at [`etc/skel/.config/imv/`](../etc/skel/.config/imv/): Alloy warm-cream background, overlay hidden by default, vi-like binds inherited from imv defaults.
215 + C, Wayland-native, tiny, dedicated to viewing. Config at [`templates/etc/skel/.config/imv/`](../templates/etc/skel/.config/imv/): the theme's page tone behind the image, overlay hidden by default, vi-like binds inherited from imv defaults.
216 216
217 217 Rejected: oculante (Rust and Alloy-toolkit-aligned via egui, but its RAW/EXIF/crop feature set drifts toward "photo tool" territory that overlaps GIMP/darktable's job; wrong scope for a base-image image viewer), swayimg (newer, less mature), nomacs (Qt, heavy), feh (X11).
218 218
@@ -220,7 +220,7 @@
220 220
221 221 ### PDF: **zathura**
222 222
223 - C, vim-like keybinds, MuPDF backend, tiny, extensible to djvu/ps/epub via plugins. Config at [`etc/skel/.config/zathura/`](../etc/skel/.config/zathura/): Alloy palette on chrome, `i` toggles recolor for reading dark PDFs on the cream background, statusbar-only chrome.
223 + C, vim-like keybinds, MuPDF backend, tiny, extensible to djvu/ps/epub via plugins. Config at [`templates/etc/skel/.config/zathura/`](../templates/etc/skel/.config/zathura/): Alloy palette on chrome, statusbar-only chrome, and `i` toggles recolor. Recolor is off in the day render, where the page already agrees with the chrome, and on in the night one, where an unrecolored PDF is a floodlight in the middle of a dark session.
224 224
225 225 Rejected: sioyek (research-oriented, C++/Qt, great for papers but too specialized for base image; users who read lots of academic PDFs install it themselves), evince/okular (retained-mode, mouse-driven), mupdf viewer (even smaller than zathura but no config surface worth naming). No production-quality Rust PDF viewer exists.
226 226
@@ -232,7 +232,7 @@
232 232
233 233 ### Editor: **helix**
234 234
235 - Rust, modal (selection-first grammar, Kakoune-shaped), LSP + tree-sitter + formatters batteries-included, TOML config, zero-config to be useful. Config at [`etc/skel/.config/helix/`](../etc/skel/.config/helix/): editor UX in `config.toml`, Akari Dawn theme shipped verbatim at `themes/akari-dawn.toml` (with `akari-night.toml` alongside for dark mode) from Shu Kutsuzawa's [Akari](https://github.com/cappyzawa/akari-theme) (MIT).
235 + Rust, modal (selection-first grammar, Kakoune-shaped), LSP + tree-sitter + formatters batteries-included, TOML config, zero-config to be useful. Config at [`templates/etc/skel/.config/helix/`](../templates/etc/skel/.config/helix/): editor UX in `config.toml`, Akari Dawn theme shipped verbatim at `themes/akari-dawn.toml` (with `akari-night.toml` alongside for dark mode) from Shu Kutsuzawa's [Akari](https://github.com/cappyzawa/akari-theme) (MIT).
236 236
237 237 `EDITOR=hx` is set by `etc/skel/.config/nushell/env.nu`; git commit messages, `crontab -e`, and every other `$EDITOR`-respecting tool routes to helix.
238 238
@@ -246,7 +246,7 @@
246 246
247 247 ### Monitor: **bottom** (`btm`)
248 248
249 - Rust, ratatui-based, TOML config. Config at [`etc/skel/.config/bottom/`](../etc/skel/.config/bottom/): Alloy palette on graphs and borders, click disabled (keyboard-only), CPU widget as the default focus.
249 + Rust, ratatui-based, TOML config. Config at [`templates/etc/skel/.config/bottom/`](../templates/etc/skel/.config/bottom/): Alloy palette on graphs and borders, click disabled (keyboard-only), CPU widget as the default focus.
250 250
251 251 Same ratatui toolkit family as the forthcoming `alloy_tui` crate, so bottom and `alloy console` read as design siblings.
252 252
@@ -336,7 +336,7 @@
336 336
337 337 **adw-gtk3 (base) + Alloy palette patch (layered).** Install `adw-gtk3-theme` from Fedora as the base, then override libadwaita's ~20 named color tokens (`window_bg_color`, `accent_color`, `card_bg_color`, etc.) with the Alloy light-mode ramp via `~/.config/gtk-{3.0,4.0}/gtk.css`.
338 338
339 - Config at [`etc/skel/.config/gtk-3.0/`](../etc/skel/.config/gtk-3.0/) and [`etc/skel/.config/gtk-4.0/`](../etc/skel/.config/gtk-4.0/) with matching `gtk.css` and `settings.ini` per version.
339 + Config at [`templates/etc/skel/.config/gtk-3.0/`](../templates/etc/skel/.config/gtk-3.0/) and [`templates/etc/skel/.config/gtk-4.0/`](../templates/etc/skel/.config/gtk-4.0/) with matching `gtk.css` and `settings.ini` per version.
340 340
341 341 **What this covers:** any GTK 3 or GTK 4 app that consumes libadwaita's named tokens: swayosd (GTK-rendered overlays), Firefox's system dialogs, xdg-desktop-portal-gtk dialogs, etc.
342 342
@@ -371,7 +371,7 @@
371 371 Applied at:
372 372 - Rio, all five `fonts.*.family` entries: `IosevkaTerm Nerd Font Mono`.
373 373 - fontconfig `monospace` alias: `IosevkaTerm Nerd Font Mono`, for everything else that asks for mono.
374 - - `etc/skel/.config/mpv/mpv.conf`, `etc/skel/.config/imv/config`, the sway bar and title font, mako, swaylock, satty: the plain `IosevkaTerm Nerd Font`, all pango.
374 + - `templates/etc/skel/.config/mpv/mpv.conf.in`, `templates/etc/skel/.config/imv/config.in`, the sway bar and title font, mako, swaylock, satty: the plain `IosevkaTerm Nerd Font`, all pango.
375 375
376 376 Rejected: JetBrains Mono (wider characters cost columns per line), Fira Code (older, ligatures on by default), Cascadia Code (Microsoft-shaped, fine but less Alloy-neutral), Berkeley Mono (paid, can't ship).
377 377
@@ -401,7 +401,7 @@
401 401
402 402 **greetd + tuigreet.** Rust, minimal, ratatui-rendered. greetd is the daemon that owns VT1; tuigreet is the ratatui client that prompts for user/password and execs [`alloy-session`](../usr/bin/alloy-session) on successful auth, which applies the chosen day/night skeleton and execs `sway`. Config at [`etc/greetd/`](../etc/greetd/): `/etc/greetd/config.toml` sets up VT1 with tuigreet and a `--theme` written in ANSI color names.
403 403
404 - **The palette reaches it through the console, not through `--theme`.** tuigreet 0.9.1 parses only the sixteen ANSI names; a hex value is ignored rather than rejected, and the affected element falls back to a default, so `prompt=#8a4530` renders red. This doc claimed an Akari greeter for a while on the strength of a `--theme` that could not deliver one. What delivers it is [`usr/lib/bootc/kargs.d/10-alloy.toml`](../usr/lib/bootc/kargs.d/10-alloy.toml), which sets the Linux console's 16-color table via `vt.default_red/grn/blu`. tuigreet then asks for `white` and gets `surface.page`. The same substitution themes every VT, including the debug shell and any getty, and it costs one boot-time kernel argument rather than a patch to tuigreet.
404 + **The palette reaches it through the console, not through `--theme`.** tuigreet 0.9.1 parses only the sixteen ANSI names; a hex value is ignored rather than rejected, and the affected element falls back to a default, so `prompt=#8a4530` renders red. This doc claimed an Akari greeter for a while on the strength of a `--theme` that could not deliver one. What delivers it is [`usr/lib/bootc/kargs.d/10-alloy.toml`](../templates/usr/lib/bootc/kargs.d/10-alloy.toml.in), which sets the Linux console's 16-color table via `vt.default_red/grn/blu`. tuigreet then asks for `white` and gets `surface.page`. The same substitution themes every VT, including the debug shell and any getty, and it costs one boot-time kernel argument rather than a patch to tuigreet.
405 405
406 406 Same ratatui toolkit family as `alloy console` and `bottom`; the login screen reads as a design sibling to the rest of the Alloy chrome.
407 407
M docs/TOKENS.md +4 -2
@@ -45,7 +45,9 @@
45 45 six = "#305858"
46 46 ```
47 47
48 - Alloy's default light theme is **[Akari Dawn](https://makenot.work/git/max/makeover/tree/main/themes/akari-dawn.toml)**; the default dark is **[Akari Night](https://makenot.work/git/max/makeover/tree/main/themes/akari-night.toml)**. Both are based on Shu Kutsuzawa's [Akari](https://github.com/cappyzawa/akari-theme) (MIT). Users pick a mode at startup (`--theme` flag or `$COLORFGBG` / OSC background query); users can also drop any makeover `.toml` into `~/.config/alloy/themes/` and pass its name to swap.
48 + Alloy's default light theme is **[Akari Dawn](https://makenot.work/git/max/makeover/tree/main/themes/akari-dawn.toml)**; the default dark is **[Akari Night](https://makenot.work/git/max/makeover/tree/main/themes/akari-night.toml)**. Both are based on Shu Kutsuzawa's [Akari](https://github.com/cappyzawa/akari-theme) (MIT). A mode is picked in `alloy settings`, which writes two things: the console's own theme key, effective immediately, and `~/.config/alloy/mode`, which every other themed config follows at the next login via `alloy theme apply`. A console told nothing follows what it is drawn on instead, asking `$COLORFGBG` first and the mode file second. `--theme` overrides either for one run. Users can also drop any makeover `.toml` into `~/.config/alloy/themes/` and name it.
49 +
50 + There is no OSC background query. It is the only other way to learn a terminal's background, and it means writing an escape sequence and waiting for a reply before the first frame, which is not worth the stall.
49 51
50 52 **Why makeover as the storage format:** the future user-facing theme catalog (a website of downloadable `.toml` files) is only useful if every make-family app renders any downloaded theme the same way. Sharing the format across apps buys that with zero adapter code.
51 53
@@ -129,7 +131,7 @@
129 131 - **Calibrated-display verification** on real hardware in real terminals (truecolor and 256-color). Pre-1.0 gate.
130 132 - **256-color downgrade table** for terminals that report no truecolor.
131 133 - **Figlet / Nerd-Font recipes** for the large numeric readouts a display font would otherwise carry.
132 - - **`alloy theme <name>` subcommand** (see [CONSOLE.md](CONSOLE.md)), which wraps theme swap. The console reads themes at startup today, via `--theme` or a guess from `$COLORFGBG`; swapping in place without a relaunch is what remains.
134 + - **Swapping the desktop's mode without a re-login.** `alloy theme apply` ships and the settings row writes the mode file, but the configs it lays down are read once by programs already running, so the switch lands at the next login. The console itself re-themes immediately.
133 135
134 136 ## How this file is consumed
135 137
@@ -14,6 +14,12 @@
14 14 //! here. Same rule docs/TOKENS.md states for the console itself: no hex in the
15 15 //! source.
16 16 //!
17 + //! Seventeen was the first pass. swaylock and imv followed later, and they were
18 + //! left behind the first time for one reason: neither can read `#rrggbb`, so
19 + //! there was no expression to write in place of their literals until
20 + //! [`eval_format`] existed. `etc/skel/` now holds only the files that carry no
21 + //! color at all.
22 + //!
17 23 //! # Template syntax
18 24 //!
19 25 //! A template is the target file with its color literals replaced by `@{ }`
@@ -33,6 +39,17 @@
33 39 //! label = "@{readable_on(status.danger)}"
34 40 //! ```
35 41 //!
42 + //! A color renders as `#rrggbb`, which is what all but three of the consumers
43 + //! read. The rest take an output format, and those are the outermost call rather
44 + //! than something a color function composes over, because their result is text
45 + //! and not a color (see [`eval_format`]).
46 + //!
47 + //! ```text
48 + //! background = "@{hex_bare(surface.page)}" e4ded6
49 + //! color = "@{hex_alpha(surface.page, 1.0)}" e4ded6ff
50 + //! highlight = "@{rgba(status.warning, 0.35)}" rgba(176, 120, 64, 0.35)
51 + //! ```
52 + //!
36 53 //! An optional first-line directive picks which theme the file renders against;
37 54 //! without one it takes the default. The directive line is stripped from the
38 55 //! output.
@@ -484,9 +501,80 @@
484 501 "meta.is_dark" => return Ok((palette.variant() == "dark").to_string()),
485 502 _ => {}
486 503 }
504 + if let Some(formatted) = eval_format(expr, palette)? {
505 + return Ok(formatted);
506 + }
487 507 Ok(eval_color(expr, palette)?.to_hex())
488 508 }
489 509
510 + /// The shapes a color takes that are not `#rrggbb`.
511 + ///
512 + /// Every color expression resolves to an [`Rgb`], and most config files want it
513 + /// written the CSS way, so [`eval`] ends in `to_hex`. Three consumers in the
514 + /// tree cannot read that, and each was the reason a file stayed hand-written:
515 + ///
516 + /// - **swaylock** wants `rrggbbaa`, bare and with an alpha. It has no `#` form
517 + /// at all, so its config could not be templated until there was a function
518 + /// here. It is on the lock path, which made it the worst file in the tree to
519 + /// leave in one polarity: locking a night session flashed a light screen.
520 + /// - **imv** wants bare `rrggbb`.
521 + /// - **zathura** wants `rgba(r, g, b, a)` with the channels in decimal, for the
522 + /// two search-highlight colors that need to let the glyph under them through.
523 + ///
524 + /// Returns `None` for anything that is not one of these, so [`eval_color`] keeps
525 + /// reporting unknown functions and malformed calls. That is also why a missing
526 + /// closing paren is passed along rather than diagnosed here: one place should own
527 + /// that message.
528 + fn eval_format(expr: &str, palette: &Palette) -> Result<Option<String>> {
529 + let expr = expr.trim();
530 + let Some(open) = expr.find('(') else {
531 + return Ok(None);
532 + };
533 + if !expr.ends_with(')') {
534 + return Ok(None);
535 + }
536 + let name = expr[..open].trim();
537 + if !matches!(name, "hex_bare" | "hex_alpha" | "rgba") {
538 + return Ok(None);
539 + }
540 +
541 + let args = split_args(&expr[open + 1..expr.len() - 1])?;
542 + let color = eval_color(
543 + args.first()
544 + .ok_or_else(|| anyhow!("`{name}` wants a color in position 0"))?,
545 + palette,
546 + )?;
547 + // Bare, not `#rrggbb` minus a character: `to_hex` stays the one place that
548 + // decides the digits and their case.
549 + let bare = || color.to_hex().trim_start_matches('#').to_string();
550 +
551 + // Out of range is an error rather than a clamp. An alpha is written by hand
552 + // in the template, so a 35 meant as a percentage is a typo worth a build
553 + // failure, not a value to quietly read as opaque.
554 + let alpha = |i: usize| -> Result<f32> {
555 + let arg = args
556 + .get(i)
557 + .ok_or_else(|| anyhow!("`{name}` wants an alpha in position {i}"))?;
558 + let value: f32 = arg
559 + .trim()
560 + .parse()
561 + .with_context(|| format!("`{arg}` is not a number"))?;
562 + if !(0.0..=1.0).contains(&value) {
563 + bail!("`{name}` wants an alpha in 0.0..=1.0, not `{arg}`");
564 + }
565 + Ok(value)
566 + };
567 +
568 + let formatted = match name {
569 + "hex_bare" => bare(),
570 + "hex_alpha" => format!("{}{:02x}", bare(), (alpha(1)? * 255.0).round() as u8),
571 + // The alpha is written through as the template gave it, so what a
572 + // reader sees in the config is what the template says.
573 + _ => format!("rgba({}, {}, {}, {})", color.r, color.g, color.b, alpha(1)?),
574 + };
575 + Ok(Some(formatted))
576 + }
577 +
490 578 /// The color grammar: a token path, or a call over other colors.
491 579 fn eval_color(expr: &str, palette: &Palette) -> Result<Rgb> {
492 580 let expr = expr.trim();
@@ -625,6 +713,89 @@
625 713 }
626 714 }
627 715
716 + // ---- output formats ----
717 +
718 + // The three shapes that are not `#rrggbb`, against the same color, so the
719 + // digits are visibly the same color written three ways.
720 + #[test]
721 + fn a_color_renders_in_every_shape_its_consumer_can_read() {
722 + let p = dawn();
723 + let hex = p.get("surface.page").unwrap().to_hex();
724 + let bare = hex.trim_start_matches('#');
725 +
726 + assert_eq!(render("@{surface.page}", &p).unwrap(), hex);
727 + assert_eq!(render("@{hex_bare(surface.page)}", &p).unwrap(), bare);
728 + assert_eq!(
729 + render("@{hex_alpha(surface.page, 1.0)}", &p).unwrap(),
730 + format!("{bare}ff"),
731 + "swaylock's opaque suffix",
732 + );
733 + }
734 +
735 + // Alpha is two hex digits for swaylock and a decimal for zathura, from the
736 + // same written value, which is the whole reason there are two functions.
737 + #[test]
738 + fn an_alpha_is_written_the_way_its_consumer_spells_it() {
739 + let p = dawn();
740 + let (r, g, b) = p.get("status.warning").unwrap().tuple();
741 +
742 + assert_eq!(
743 + render("@{rgba(status.warning, 0.35)}", &p).unwrap(),
744 + format!("rgba({r}, {g}, {b}, 0.35)"),
745 + );
746 + // 0.35 * 255 = 89.25, so the round lands on 89 = 0x59.
747 + assert_eq!(
748 + render("@{hex_alpha(status.warning, 0.35)}", &p).unwrap(),
749 + format!("{r:02x}{g:02x}{b:02x}59"),
750 + );
751 + // Fully transparent is a real value, and `00` must not be mistaken for
752 + // a failure to write an alpha at all.
753 + assert!(
754 + render("@{hex_alpha(status.warning, 0.0)}", &p)
755 + .unwrap()
756 + .ends_with("00")
757 + );
758 + }
759 +
760 + // An output format wraps a color expression, so a composed tone can still
761 + // reach a consumer that cannot read `#rrggbb`.
762 + #[test]
763 + fn an_output_format_takes_a_whole_expression_not_only_a_token() {
764 + let p = dawn();
765 + let want = makeover::mix(
766 + p.get("surface.page").unwrap(),
767 + p.get("surface.sunken").unwrap(),
768 + 0.5,
769 + );
770 + assert_eq!(
771 + render("@{hex_bare(mix(surface.page, surface.sunken, 0.5))}", &p).unwrap(),
772 + want.to_hex().trim_start_matches('#'),
773 + );
774 + }
775 +
776 + // A percentage written where a fraction belongs is a typo, and reading it as
777 + // opaque would ship a lockscreen nobody could see through the wrong alpha.
778 + #[test]
779 + fn an_alpha_outside_the_unit_range_is_an_error() {
780 + let p = dawn();
781 + for bad in ["35", "-0.5", "255"] {
782 + assert!(
783 + render(&format!("@{{hex_alpha(surface.page, {bad})}}"), &p).is_err(),
784 + "alpha `{bad}` was accepted",
785 + );
786 + }
787 + assert!(render("@{rgba(surface.page, 1.5)}", &p).is_err());
788 + }
789 +
790 + // The formats are additions to the grammar, not a new way to typo past it.
791 + #[test]
792 + fn an_output_format_still_reports_a_bad_inner_expression() {
793 + let p = dawn();
794 + assert!(render("@{hex_bare(no.such.token)}", &p).is_err());
795 + assert!(render("@{hex_alpha(surface.page)}", &p).is_err());
796 + assert!(render("@{hex_bare()}", &p).is_err());
797 + }
798 +
628 799 #[test]
629 800 fn the_vt_table_is_sixteen_bytes_per_channel() {
630 801 let p = dawn();
@@ -82,6 +82,15 @@
82 82 /// The whole point of the tree is that colors come from the theme. A literal is
83 83 /// not always wrong — a comment can quote one, and the Helix header does — so
84 84 /// this checks the lines that assign rather than every line.
85 + ///
86 + /// Both spellings, because one of them was a blind spot the size of the
87 + /// lockscreen: swaylock and imv take their colors *bare*, with no `#`, so this
88 + /// test would have passed their literals even once the files became templates.
89 + /// `#`-prefixed
90 + /// is checked against the whole line and bare against the code before any
91 + /// comment, which is not an inconsistency: `#` is both the hex prefix and the
92 + /// comment character, so a `#rrggbb` value *is* everything after a `#` and
93 + /// cutting there would throw the value away.
85 94 #[test]
86 95 fn no_template_assigns_a_hex_literal() {
87 96 for path in templates() {
@@ -93,14 +102,24 @@
93 102 if is_comment || !code.contains('=') {
94 103 continue;
95 104 }
96 - let hex = line.split_whitespace().find(|w| {
105 + let word = |w: &str| {
97 106 w.trim_matches(|c: char| !c.is_ascii_alphanumeric() && c != '#')
98 - .len()
99 - == 7
100 - && w.contains('#')
107 + .to_string()
108 + };
109 + let prefixed = line
110 + .split_whitespace()
111 + .find(|w| word(w).len() == 7 && w.contains('#'));
112 + // Six or eight digits, `rrggbb` and swaylock's `rrggbbaa`. At least
113 + // one a-f, so a plain decimal that happens to be six digits long is
114 + // not a color: `indicator-radius=90` is fine, and so would be
115 + // `font-size=100000`.
116 + let bare = code.split_whitespace().map(word).find(|w| {
117 + matches!(w.len(), 6 | 8)
118 + && w.bytes().all(|b| b.is_ascii_hexdigit())
119 + && w.bytes().any(|b| b.is_ascii_alphabetic())
101 120 });
102 121 assert!(
103 - hex.is_none(),
122 + prefixed.is_none() && bare.is_none(),
104 123 "{}:{}: hex literal in an assignment: {line}",
105 124 path.display(),
106 125 n + 1
@@ -34,16 +34,14 @@
34 34 # -------------------------------------------------------------------
35 35 # Selection and search
36 36 # -------------------------------------------------------------------
37 - # These two are the only colors in the tree still written by hand, and they are
38 - # Akari Dawn's amber and oxblood in both renders. A search hit has to let the
39 - # glyph under it through, so it needs an alpha, and zathura wants that as
40 - # `rgba(r, g, b, a)` with the channels in decimal. Every skelgen expression
41 - # resolves to `#rrggbb`, so there is nothing to write here that would follow
42 - # the theme. Amber-at-0.35 does stay legible over the night render's recolored
43 - # page, which is why this is a gap rather than a bug; closing it means a
44 - # decimal-channel expression in skelgen.
45 - set highlight-color "rgba(176, 120, 64,0.35)" # accent-warn at low alpha
46 - set highlight-active-color "rgba(106, 40, 40,0.5)" # accent-error at mid alpha
37 + # A search hit has to let the glyph under it through, so these two need an
38 + # alpha, and zathura wants that as `rgba(r, g, b, a)` with the channels in
39 + # decimal. These were the last two colors in the tree written by hand, pinned to
40 + # Akari Dawn's amber and oxblood in both renders, because every skelgen
41 + # expression resolved to `#rrggbb` and there was nothing to write that would
42 + # follow the theme. `rgba` is that expression.
43 + set highlight-color "@{rgba(status.warning, 0.35)}" # accent-warn at low alpha
44 + set highlight-active-color "@{rgba(status.danger, 0.5)}" # accent-error at mid alpha
47 45 set selection-clipboard clipboard
48 46
49 47 # -------------------------------------------------------------------
@@ -1,15 +1,15 @@
1 + @{! variants = default, night }
1 2 # Alloy imv configuration
2 3 #
3 - # Keyboard-driven image viewer. Alloy light-mode palette applied to
4 - # background and overlay text. Overlay hidden by default (press "o"
5 - # to toggle).
4 + # Keyboard-driven image viewer. Overlay hidden by default (press "o" to
5 + # toggle). imv takes bare `rrggbb` with no `#`, which is what `hex_bare` is for.
6 6
7 7 [options]
8 - background = e4ded6 # Alloy surface (letterbox / transparent-image background)
8 + background = @{hex_bare(surface.page)} # letterbox / transparent-image background
9 9 overlay = false # start with overlay hidden
10 10 overlay_font = IosevkaTerm Nerd Font:11
11 - overlay_text_color = 1a1816 # text-primary
12 - overlay_background_color = ede7de # surface-raised
11 + overlay_text_color = @{hex_bare(content.primary)}
12 + overlay_background_color = @{hex_bare(surface.raised)}
13 13 overlay_position_bottom = true
14 14
15 15 initial_pan = 50 50