Skip to main content

max / alloy_tui

image: single stage, wlr portal, pinned fonts; add idle lock and zellij Session: - swayidle drives the lock (5 min lock, 10 min displays off, always before sleep). swaylock and swayidle were both missing from the image even though a fully themed swaylock config has shipped in etc/skel since v0, so Mod+Ctrl+L was dead. - zellij added alongside rio, bound to Mod+Shift+Return. It is a multiplexer, not an emulator, so it runs as $term -e zellij rather than replacing $term. Build: - xdg-desktop-portal-gnome to -wlr. The GNOME backend drives screencast through gnome-shell, which this image removes, so it was the wrong backend for a sway session. Also 34 MiB and 12 packages lighter. - Fonts move ahead of the package list, with unzip and fontconfig in their own layer. Behind it, every package edit re-fetched both archives. - Font sources pinned (nerd-fonts v3.4.0, atkinson at a commit since it publishes no tags). Unpinned, those layers change content without the Containerfile changing, so a cache hit stopped meaning same bytes. - wl-screenrec dropped, which removes the last cargo stage: 912 packages and an ffmpeg-sys bindgen run for one binary. It is packaged in neither Fedora, Terra, nor ublue-os/staging (verified, and the Containerfile comment claiming otherwise is corrected). The pick stands; only the packaging cost is refused. Return condition and the cargo install workaround are in STACK.md.
Co-Authored-By
Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-19 16:11 UTC
Signed with PGP, not checked
Commit: 12613d02741ad859be0c72e7475181fa0e75c619
Parent: e470459
4 files changed, +83 insertions, -81 deletions
M Containerfile +62 -64
@@ -4,32 +4,13 @@
4 4 # See docs/IMAGE.md for the composition strategy and rationale.
5 5 #
6 6 # Layer order optimizes rebuild speed: repos first (rarely change),
7 - # then package installs (change with STACK.md), then config tree
8 - # (changes most often, so lives at the tail).
7 + # then fonts (large downloads, pinned), then package installs (change
8 + # with STACK.md), then config tree (changes most often, so lives at
9 + # the tail).
9 10 #
10 - # Multi-stage: two cargo-install stages produce Rust binaries not
11 - # packaged in Fedora, then the final bootc stage COPYs them in.
12 - # Keeps the Rust toolchain out of the runtime image.
13 -
14 - # =====================================================================
15 - # rust-build: wl-screenrec
16 - # =====================================================================
17 - # Small crate not packaged in Fedora. Its own stage keeps the Rust
18 - # toolchain out of the runtime image. clang-devel is for wl-screenrec's
19 - # ffmpeg-sys bindgen.
20 - FROM registry.fedoraproject.org/fedora:42 AS rust-build
21 - RUN dnf install -y \
22 - rust cargo git \
23 - gcc make pkgconf-pkg-config \
24 - clang-devel \
25 - wayland-devel libdrm-devel \
26 - ffmpeg-free-devel \
27 - openssl-devel \
28 - && dnf clean all
29 - ENV CARGO_HOME=/build/cargo \
30 - CARGO_TARGET_DIR=/build/target
31 - # wl-screenrec is on crates.io.
32 - RUN cargo install --locked --root /build/out wl-screenrec
11 + # Single-stage. The image carries no cargo-built binaries: everything
12 + # ships from a repo. Screen recording is the one gap (see the deferral
13 + # note in docs/STACK.md#screen-recorder).
33 14
34 15 # =====================================================================
35 16 # Runtime image — the bootable container itself.
@@ -56,10 +37,13 @@
56 37 RUN dnf install -y 'dnf5-command(copr)'
57 38
58 39 # ublue-os/staging is the community-maintained COPR that packages
59 - # most of the Wayland / Rust ecosystem for atomic Fedora derivatives
60 - # — anyrun, swayosd, cliphist, wl-screenrec, ironbar, and friends.
40 + # much of the Wayland / Rust ecosystem for atomic Fedora derivatives.
61 41 # We're not using their base image, but their COPR is a legitimate
62 42 # adoption (same relationship the rest of Alloy has to Fedora repos).
43 + #
44 + # Verified 2026-07-19: it does NOT carry wl-screenrec, which is why
45 + # that one needed a cargo stage and is now deferred. Check before
46 + # assuming a Rust Wayland tool is here.
63 47 RUN dnf copr enable -y ublue-os/staging
64 48
65 49 # nushell binary (Terra has crates only)
@@ -71,25 +55,64 @@
71 55 # satty (screenshot annotator) is expected in ublue-os/staging;
72 56 # if it turns out to need its own COPR, add here.
73 57
58 + # =====================================================================
59 + # Fonts pulled from upstream releases (not cleanly packaged in Fedora):
60 + # - IosevkaTerm Nerd Font (mono, TUI-safe: no ligatures, has NF glyphs)
61 + # - Atkinson Hyperlegible (sans, high-legibility)
62 + #
63 + # These sit ahead of the package list on purpose. They are the largest
64 + # downloads in the build, and the package list is the line that changes
65 + # most often; behind it, every package edit re-fetched both archives.
66 + # The unzip/fontconfig they need comes from its own small layer rather
67 + # than from the main install, which is what lets the split work.
68 + # =====================================================================
69 + # Both sources are pinned. Unpinned (`releases/latest`, branch `main`)
70 + # these layers change content without the Containerfile changing, so a
71 + # cache hit stops meaning "same bytes" and two builds of the same commit
72 + # can ship different fonts. Bump deliberately.
73 + #
74 + # Atkinson Hyperlegible publishes no tags, so it pins to a commit.
75 + ARG NERD_FONTS_VERSION=v3.4.0
76 + ARG ATKINSON_COMMIT=1cb311624b2ddf88e9e37873999d165a8cd28b46
77 +
78 + RUN dnf install -y unzip fontconfig && dnf clean all
79 + RUN mkdir -p /usr/share/fonts/iosevkaterm-nerd \
80 + /usr/share/fonts/atkinson-hyperlegible && \
81 + curl -fsSL -o /tmp/iosevkaterm-nf.zip \
82 + "https://github.com/ryanoasis/nerd-fonts/releases/download/${NERD_FONTS_VERSION}/IosevkaTerm.zip" && \
83 + unzip -o /tmp/iosevkaterm-nf.zip -d /usr/share/fonts/iosevkaterm-nerd && \
84 + rm /tmp/iosevkaterm-nf.zip && \
85 + curl -fsSL -o /tmp/atkinson-hyperlegible.tar.gz \
86 + "https://github.com/googlefonts/atkinson-hyperlegible/archive/${ATKINSON_COMMIT}.tar.gz" && \
87 + tar -xzf /tmp/atkinson-hyperlegible.tar.gz -C /tmp && \
88 + find /tmp/atkinson-hyperlegible-* -name '*.ttf' -exec cp {} /usr/share/fonts/atkinson-hyperlegible/ \; && \
89 + rm -rf /tmp/atkinson-hyperlegible.tar.gz /tmp/atkinson-hyperlegible-* && \
90 + fc-cache -fv
91 +
74 92 # =====================================================================
75 93 # Package additions — full Alloy stack per docs/STACK.md
76 94 #
77 95 # Sources noted per group. Anything absent from both Fedora main and
78 - # Terra is downloaded directly (Nerd Fonts). The `flatpak` client is
79 - # installed so users can pull ungoogled-chromium and other on-demand
80 - # apps from Flathub post-install; no Flatpaks are provisioned at
81 - # build or first-boot time.
96 + # Terra is downloaded directly (Nerd Fonts, in the layer above). The
97 + # `flatpak` client is installed so users can pull ungoogled-chromium and
98 + # other on-demand apps from Flathub post-install; no Flatpaks are
99 + # provisioned at build or first-boot time.
82 100 # =====================================================================
83 101 RUN dnf install -y \
84 102 # Compositor and Wayland session (Fedora main)
85 103 sway \
86 - xdg-desktop-portal xdg-desktop-portal-gtk xdg-desktop-portal-gnome \
104 + # Portals: -wlr is the wlroots backend sway needs for screencast.
105 + # -gnome was the wrong backend here (it drives screencast through
106 + # gnome-shell, which Alloy removes below). -gtk stays for the file
107 + # chooser.
108 + xdg-desktop-portal xdg-desktop-portal-gtk xdg-desktop-portal-wlr \
87 109 # Notifications, screenshot annotate, wallpaper (bar = sway's built-in swaybar)
88 110 mako \
89 111 satty \
90 112 swww \
91 - # Terminal, editor, shell, prompt
113 + # Terminal, multiplexer, editor, shell, prompt
92 114 rio \
115 + zellij \
93 116 helix \
94 117 nushell \
95 118 starship \
@@ -106,6 +129,12 @@
106 129 dua-cli \
107 130 # Wayland session glue
108 131 cliphist \
132 + # Lock + idle. swaylock is the adopted lock surface per
133 + # docs/STACK.md#lock. A session-lock surface is graphical and
134 + # cannot be a TUI, so Alloy adopts rather than authors it. The
135 + # themed config in etc/skel had no package behind it until now.
136 + swaylock \
137 + swayidle \
109 138 swayosd \
110 139 playerctl \
111 140 gammastep \
@@ -125,39 +154,8 @@
125 154 firefox \
126 155 # Flatpak client (user-installed apps on demand; see docs/STACK.md)
127 156 flatpak \
128 - # Font tools
129 - unzip fontconfig \
130 - # Runtime libs for the cargo-built binary copied in below:
131 - # wl-screenrec → ffmpeg-free + libdrm
132 - ffmpeg-free libdrm \
133 157 && dnf clean all
134 158
135 - # =====================================================================
136 - # Fonts pulled from upstream releases (not cleanly packaged in Fedora):
137 - # - IosevkaTerm Nerd Font (mono, TUI-safe: no ligatures, has NF glyphs)
138 - # - Atkinson Hyperlegible (sans, high-legibility)
139 - # =====================================================================
140 - RUN mkdir -p /usr/share/fonts/iosevkaterm-nerd \
141 - /usr/share/fonts/atkinson-hyperlegible && \
142 - curl -fsSL -o /tmp/iosevkaterm-nf.zip \
143 - https://github.com/ryanoasis/nerd-fonts/releases/latest/download/IosevkaTerm.zip && \
144 - unzip -o /tmp/iosevkaterm-nf.zip -d /usr/share/fonts/iosevkaterm-nerd && \
145 - rm /tmp/iosevkaterm-nf.zip && \
146 - curl -fsSL -o /tmp/atkinson-hyperlegible.tar.gz \
147 - https://github.com/googlefonts/atkinson-hyperlegible/archive/refs/heads/main.tar.gz && \
148 - tar -xzf /tmp/atkinson-hyperlegible.tar.gz -C /tmp && \
149 - find /tmp/atkinson-hyperlegible-main -name '*.ttf' -exec cp {} /usr/share/fonts/atkinson-hyperlegible/ \; && \
150 - rm -rf /tmp/atkinson-hyperlegible.tar.gz /tmp/atkinson-hyperlegible-main && \
151 - fc-cache -fv
152 -
153 - # =====================================================================
154 - # Rust binaries not packaged in Fedora — pulled from the multi-stage
155 - # builds above. Landing under /usr/local/bin keeps them out of dnf's
156 - # view of /usr/bin, which is what the atomic tooling expects for
157 - # hand-installed binaries.
158 - # =====================================================================
159 - COPY --from=rust-build /build/out/bin/wl-screenrec /usr/local/bin/wl-screenrec
160 -
161 159 # =====================================================================
162 160 # Package removals — stock desktop pieces Alloy replaces
163 161 # =====================================================================
M docs/STACK.md +3 -1
@@ -241,10 +241,12 @@
241 241
242 242 ### Screen recorder: **wl-screenrec**
243 243
244 - Rust, hardware-accelerated (VAAPI), small. CLI-driven — no config file. Nu function `screenrec` in `etc/skel/.config/nushell/aliases.nu` wraps the common invocations (region-select via slurp, timestamped output to `~/Videos/Screenrecords/`).
244 + Rust, hardware-accelerated (VAAPI), small. CLI-driven, no config file.
245 245
246 246 Rejected: wf-recorder (C++, older, no hardware accel by default), OBS (way overscope for base image — power users install).
247 247
248 + **Not in the v0 image (deferred 2026-07-19).** wl-screenrec is packaged in neither Fedora main nor Terra, so shipping it meant a dedicated `cargo install` build stage that pulled a Rust toolchain, clang, and the ffmpeg headers (912 packages) into every cold build for one binary. The pick stands; only the packaging cost is being refused. It returns when a repo carries it, or when there is a second cargo-built binary to share the stage's cost. Until then the image has no screen recorder and the `screenrec` Nu function is removed from `etc/skel/.config/nushell/aliases.nu`. Users who want it now: `cargo install wl-screenrec`.
249 +
248 250 ### Volume/brightness OSD: **swayosd**
249 251
250 252 Rust, systemd user daemon, GTK-rendered overlays for volume/brightness/caps-lock/num-lock. Config at [`etc/skel/.config/swayosd/`](../etc/skel/.config/swayosd/) — Alloy palette CSS with amber `accent-warn` progress bar.
@@ -39,23 +39,12 @@
39 39 alias top = btm # bottom replaces top
40 40
41 41 # -------------------------------------------------------------------
42 - # Screen recording — wl-screenrec wrapper with region-select and
43 - # an autogenerated path under ~/Videos/Screenrecords/
42 + # Screen recording is not in the image. wl-screenrec is the picked tool
43 + # but is not packaged for Fedora, and building it pulled a whole Rust
44 + # toolchain stage into the build for one binary. Deferred until it is
45 + # packaged; see docs/STACK.md#screen-recorder. The screenrec wrapper
46 + # that lived here comes back with it.
44 47 # -------------------------------------------------------------------
45 - def screenrec [
46 - --output(-o): string # explicit output name (e.g. "eDP-1"); default: prompt region via slurp
47 - ] {
48 - let dir = ($env.HOME | path join "Videos" "Screenrecords")
49 - let file = ($dir | path join $"screenrec-((date now | format date '%Y%m%d-%H%M%S')).mp4")
50 - mkdir $dir
51 - if ($output != null) {
52 - wl-screenrec --output $output --filename $file
53 - } else {
54 - let geom = (slurp | str trim)
55 - wl-screenrec --geometry $geom --filename $file
56 - }
57 - print $"saved: ($file)"
58 - }
59 48
60 49 # -------------------------------------------------------------------
61 50 # Alloy itself (once alloy console is built and installed)
@@ -88,10 +88,23 @@
88 88 # swayosd-server backs the volume/brightness/caps overlays.
89 89 exec swayosd-server
90 90
91 + # Idle: dim to lock at 5 minutes, displays off at 10, and always lock
92 + # before suspend so a closed lid never resumes into a live session.
93 + # -w makes swayidle wait for the lock to come up before sleeping.
94 + # Lock styling comes from ~/.config/swaylock/config (it sets daemonize).
95 + exec swayidle -w \
96 + timeout 300 'swaylock -f' \
97 + timeout 600 'swaymsg "output * power off"' \
98 + resume 'swaymsg "output * power on"' \
99 + before-sleep 'swaylock -f'
100 +
91 101 # -------------------------------------------------------------------
92 102 # Binds — session
93 103 # -------------------------------------------------------------------
94 104 bindsym $mod+Return exec $term
105 + # Multiplexed terminal. zellij is not a Wayland client; it runs inside
106 + # $term, so this is the emulator with the multiplexer as its command.
107 + bindsym $mod+Shift+Return exec $term -e zellij
95 108 # Launcher removed with anyrun. Bind a TUI launcher here when chosen,
96 109 # e.g. bindsym $mod+d exec $term -e <picker>
97 110 # bindsym $mod+d exec ...