max / alloy
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
8 files changed,
+286 insertions,
-90 deletions
| @@ -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 |
| @@ -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 |