From ad21005c473e79e47d70833cd9ee2f77b887cd71 Mon Sep 17 00:00:00 2001 From: Gene Liverman Date: Sun, 19 Jul 2026 07:54:34 -0400 Subject: [PATCH 1/2] feat(genebean): extract chromium-kiosk service module kiosk-gene-desk and kiosk-entryway each ran an identical cage + chromium kiosk pattern (a writeShellScript "kiosk.sh" doing wlr-randr then exec'ing chromium, plus a systemd.services.cage-tty1.wants block), and each host's home-gene.nix carried a near-identical programs.chromium.commandLineArgs list - differing only in the dashboard URL, whether the screen is rotated, one extra chromium flag (--hide-scrollbars, kiosk-entryway only), and the wifi interface name used in the cage-tty1 dependency (wlan0 vs wlp3s0). Adds genebean.services.chromium-kiosk under services/ (not programs/) on both the home-manager and NixOS sides, matching this repo's existing split - services/ is for things that manage a services.*/systemd-unit- backed daemon on both sides (flatpak, tailscale), which is what this module's NixOS half does (services.cage, systemd.services.cage-tty1). Following the same tailscale.nix/flatpak.nix convention, the home-manager module owns the options (dashboardUrl, extraCommandLineArgs, wlrRandrOutput, rotate) and the NixOS module reads them back via config.home-manager.users.${username}.genebean.services.chromium-kiosk - so a host only specifies these values once, in its home-.nix, not duplicated across two files. The wifi interface for cage-tty1.wants is read from the already-declared networking.wireless.interfaces list rather than becoming a third option naming the same thing. fbcon console rotation (kiosk-gene-desk's boot.kernelParams, Pi-specific and independent of cage's own GUI rotation per its existing comment) and the zramSwap/users.users.${username} blocks stay host-specific - genuinely separate concerns, not chromium/cage-specific ones this module should own. Pure refactor, not a behavior change: verified both hosts' evaluated services.cage.program script content and systemd.services.cage-tty1.wants are byte-for-byte identical to before (kiosk-gene-desk built locally; kiosk-entryway's derivation inspected directly, since it's x86_64-linux and this workstation's linux-builder VM is aarch64-linux only). --- modules/genebean/home/default.nix | 1 + .../genebean/home/services/chromium-kiosk.nix | 53 +++++++++++++++++++ modules/genebean/nixos/default.nix | 1 + .../nixos/services/chromium-kiosk.nix | 33 ++++++++++++ .../hosts/nixos/kiosk-entryway/default.nix | 26 --------- .../hosts/nixos/kiosk-entryway/home-gene.nix | 22 ++------ .../hosts/nixos/kiosk-gene-desk/default.nix | 23 -------- .../hosts/nixos/kiosk-gene-desk/home-gene.nix | 24 +++------ 8 files changed, 99 insertions(+), 84 deletions(-) create mode 100644 modules/genebean/home/services/chromium-kiosk.nix create mode 100644 modules/genebean/nixos/services/chromium-kiosk.nix diff --git a/modules/genebean/home/default.nix b/modules/genebean/home/default.nix index ea0ab66b..2109669e 100644 --- a/modules/genebean/home/default.nix +++ b/modules/genebean/home/default.nix @@ -54,6 +54,7 @@ ./programs/zoom.nix ./programs/wezterm ./programs/zsh.nix + ./services/chromium-kiosk.nix ./services/flatpak.nix ./services/tailscale.nix ]; diff --git a/modules/genebean/home/services/chromium-kiosk.nix b/modules/genebean/home/services/chromium-kiosk.nix new file mode 100644 index 00000000..c4f267c6 --- /dev/null +++ b/modules/genebean/home/services/chromium-kiosk.nix @@ -0,0 +1,53 @@ +{ config, lib, ... }: +let + cfg = config.genebean.services.chromium-kiosk; +in +{ + options.genebean.services.chromium-kiosk = { + enable = lib.mkEnableOption "chromium kiosk pointed at a Home Assistant dashboard"; + + dashboardUrl = lib.mkOption { + type = lib.types.str; + description = "URL chromium opens in --app/--kiosk mode."; + example = "http://192.168.22.22:8123/kiosk-entryway/immich?kiosk"; + }; + + extraCommandLineArgs = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Extra chromium flags appended after the common kiosk set."; + example = [ "--hide-scrollbars" ]; + }; + + rotate = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "wlr-randr --transform value (e.g. \"90\", \"flipped-90\"). null = no --transform flag."; + }; + + wlrRandrOutput = lib.mkOption { + type = lib.types.str; + default = "HDMI-A-1"; + description = "wlr-randr output name cage's compositor targets."; + }; + }; + + config = lib.mkIf cfg.enable { + programs.chromium = { + enable = true; + commandLineArgs = [ + "--app=${cfg.dashboardUrl}" + "--kiosk" + "--noerrdialogs" + "--disable-infobars" + "--no-first-run" + "--ozone-platform=wayland" + "--enable-features=OverlayScrollbar" + "--start-maximized" + "--force-dark-mode" + "--hide-crash-restore-bubble" + ] + ++ cfg.extraCommandLineArgs; + }; + }; +} diff --git a/modules/genebean/nixos/default.nix b/modules/genebean/nixos/default.nix index cc7cae62..408cb247 100644 --- a/modules/genebean/nixos/default.nix +++ b/modules/genebean/nixos/default.nix @@ -9,6 +9,7 @@ ./programs/onepassword.nix ./programs/thunderbird.nix ./programs/xfce4-terminal.nix + ./services/chromium-kiosk.nix ./services/flatpak.nix ./services/tailscale.nix ]; diff --git a/modules/genebean/nixos/services/chromium-kiosk.nix b/modules/genebean/nixos/services/chromium-kiosk.nix new file mode 100644 index 00000000..8718a73b --- /dev/null +++ b/modules/genebean/nixos/services/chromium-kiosk.nix @@ -0,0 +1,33 @@ +{ + config, + lib, + pkgs, + username, + ... +}: +let + cfg = config.home-manager.users.${username}.genebean.services.chromium-kiosk; + wifiInterface = lib.elemAt config.networking.wireless.interfaces 0; +in +{ + config = lib.mkIf cfg.enable { + environment.systemPackages = [ pkgs.wlr-randr ]; + + services.cage = { + enable = true; + environment.WLR_LIBINPUT_NO_DEVICES = "1"; # boot up even if no mouse/keyboard connected + program = pkgs.writeShellScript "kiosk.sh" '' + WAYLAND_DISPLAY=wayland-0 wlr-randr --output ${cfg.wlrRandrOutput}${ + lib.optionalString (cfg.rotate != null) " --transform ${cfg.rotate}" + } + /etc/profiles/per-user/${username}/bin/chromium-browser + ''; + user = username; + }; + + systemd.services.cage-tty1.wants = [ + "wpa_supplicant-${wifiInterface}.service" + "network-online.target" + ]; + }; +} diff --git a/modules/hosts/nixos/kiosk-entryway/default.nix b/modules/hosts/nixos/kiosk-entryway/default.nix index ff2099ad..332299c0 100644 --- a/modules/hosts/nixos/kiosk-entryway/default.nix +++ b/modules/hosts/nixos/kiosk-entryway/default.nix @@ -19,10 +19,6 @@ "ext4" ]; - environment.systemPackages = with pkgs; [ - wlr-randr - ]; - fonts = { fontconfig = { enable = true; @@ -60,21 +56,6 @@ ]; services = { - cage = - let - kioskProgram = pkgs.writeShellScript "kiosk.sh" '' - WAYLAND_DISPLAY=wayland-0 wlr-randr --output HDMI-A-1 - /etc/profiles/per-user/gene/bin/chromium-browser - ''; - in - { - enable = true; - program = kioskProgram; - user = "gene"; - environment = { - WLR_LIBINPUT_NO_DEVICES = "1"; # boot up even if no mouse/keyboard connected - }; - }; prometheus.exporters.node = { enable = true; enabledCollectors = [ @@ -107,13 +88,6 @@ }; }; - systemd.services.cage-tty1 = { - wants = [ - "wpa_supplicant-wlp3s0.service" - "network-online.target" - ]; - }; - users.users.${username} = { isNormalUser = true; description = "Gene Liverman"; diff --git a/modules/hosts/nixos/kiosk-entryway/home-gene.nix b/modules/hosts/nixos/kiosk-entryway/home-gene.nix index 8ff51f92..d453d701 100644 --- a/modules/hosts/nixos/kiosk-entryway/home-gene.nix +++ b/modules/hosts/nixos/kiosk-entryway/home-gene.nix @@ -1,23 +1,9 @@ { home.stateVersion = "24.11"; - programs = { - chromium = { - enable = true; - commandLineArgs = [ - "--app=http://192.168.22.22:8123/kiosk-entryway/immich?kiosk" - "--kiosk" - "--noerrdialogs" - "--disable-infobars" - "--no-first-run" - "--ozone-platform=wayland" - "--enable-features=OverlayScrollbar" - "--start-maximized" - "--force-dark-mode" - "--hide-crash-restore-bubble" - "--hide-scrollbars" - ]; - }; + genebean.services.chromium-kiosk = { + enable = true; + dashboardUrl = "http://192.168.22.22:8123/kiosk-entryway/immich?kiosk"; + extraCommandLineArgs = [ "--hide-scrollbars" ]; }; - } diff --git a/modules/hosts/nixos/kiosk-gene-desk/default.nix b/modules/hosts/nixos/kiosk-gene-desk/default.nix index ac031dd1..2f0043ed 100644 --- a/modules/hosts/nixos/kiosk-gene-desk/default.nix +++ b/modules/hosts/nixos/kiosk-gene-desk/default.nix @@ -35,7 +35,6 @@ environment.systemPackages = with pkgs; [ libraspberrypi raspberrypi-eeprom - wlr-randr ]; hardware = { @@ -86,21 +85,6 @@ ]; services = { - cage = - let - kioskProgram = pkgs.writeShellScript "kiosk.sh" '' - WAYLAND_DISPLAY=wayland-0 wlr-randr --output HDMI-A-1 --transform 90 - /etc/profiles/per-user/gene/bin/chromium-browser - ''; - in - { - enable = true; - program = kioskProgram; - user = "gene"; - environment = { - WLR_LIBINPUT_NO_DEVICES = "1"; # boot up even if no mouse/keyboard connected - }; - }; prometheus.exporters.node = { enable = true; inherit (config.genebean.ports.node-exporter) port; @@ -182,13 +166,6 @@ }; }; - systemd.services.cage-tty1 = { - wants = [ - "wpa_supplicant-wlan0.service" - "network-online.target" - ]; - }; - users.users.${username} = { isNormalUser = true; description = "Gene Liverman"; diff --git a/modules/hosts/nixos/kiosk-gene-desk/home-gene.nix b/modules/hosts/nixos/kiosk-gene-desk/home-gene.nix index 8410f739..e56125e9 100644 --- a/modules/hosts/nixos/kiosk-gene-desk/home-gene.nix +++ b/modules/hosts/nixos/kiosk-gene-desk/home-gene.nix @@ -1,25 +1,15 @@ { home.stateVersion = "24.11"; - genebean.services.tailscale.enable = true; - - programs = { - chromium = { + genebean.services = { + chromium-kiosk = { enable = true; - commandLineArgs = [ - "--app=http://192.168.22.22:8123/kiosk-gene-desk/immich?kiosk" - "--kiosk" - "--noerrdialogs" - "--disable-infobars" - "--no-first-run" - "--ozone-platform=wayland" - "--enable-features=OverlayScrollbar" - "--start-maximized" - "--force-dark-mode" - "--hide-crash-restore-bubble" - ]; + dashboardUrl = "http://192.168.22.22:8123/kiosk-gene-desk/immich?kiosk"; + rotate = "90"; }; - zsh.history.path = "/tmp/zsh_history_gene"; # needed becaues of read only fs + tailscale.enable = true; }; + programs.zsh.history.path = "/tmp/zsh_history_gene"; # needed becaues of read only fs + } From 786481ceab8160e9a5be4f5c7620e30aefb8c5bf Mon Sep 17 00:00:00 2001 From: Gene Liverman Date: Sun, 19 Jul 2026 08:27:18 -0400 Subject: [PATCH 2/2] fix(genebean): restart cage-tty1 reliably on config changes Two things needed fixing together, both found on hardware (kiosk-entryway) while validating this actually works: Upstream's cage module sets restartIfChanged = false, presumably to avoid yanking an interactive desktop session out from under someone mid `nixos-rebuild switch`. That caution doesn't apply to a kiosk - without this override, a config change (e.g. a new dashboard URL) requires a manual `systemctl restart cage-tty1` or a reboot to actually take effect, since the switch itself silently leaves the old chromium/cage instance running. But turning restartIfChanged on alone made things worse, not better: NixOS's activation reactivates TTY-related units on every `switch` regardless of whether anything relevant changed, and (re)starts getty@tty1.service even though it shows "disabled" - cage-tty1's existing Conflicts=getty@tty1.service (set by upstream's cage module) then kills it as a side effect, and nothing re-triggers it afterward (WantedBy=graphical.target doesn't get re-evaluated once that target's already reached), leaving the kiosk dark until a manual restart. This happens independently of restartIfChanged and made the failure mode worse once enabled, since now there were two competing stop/start cycles racing in the same transaction. Masking getty@tty1 outright - the standard practice for a TTY a compositor owns exclusively - removes the conflict at its source instead of just reacting to it, except getty@.service's own upstream unit carries Alias=autovt@%i.service, so masking the getty@tty1 name alone doesn't stop systemd resolving the same underlying unit via its autovt@tty1 alias instead - confirmed on hardware, masking only getty@tty1 still left autovt@tty1.service starting at the exact moment cage-tty1 died. Both names need masking. Validated with two consecutive `nixos-rebuild switch` runs against kiosk-entryway (one that actually changed cage-tty1's unit, one no-op) - cage-tty1 restarted cleanly on the first and was left completely untouched on the second, matching expected behavior both times. --- .../nixos/services/chromium-kiosk.nix | 44 +++++++++++++++++-- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/modules/genebean/nixos/services/chromium-kiosk.nix b/modules/genebean/nixos/services/chromium-kiosk.nix index 8718a73b..6d3a2364 100644 --- a/modules/genebean/nixos/services/chromium-kiosk.nix +++ b/modules/genebean/nixos/services/chromium-kiosk.nix @@ -25,9 +25,45 @@ in user = username; }; - systemd.services.cage-tty1.wants = [ - "wpa_supplicant-${wifiInterface}.service" - "network-online.target" - ]; + systemd.services = { + # cage-tty1 already Conflicts=getty@tty1.service (set by upstream's + # cage module) to stop both fighting over the same TTY, but that + # alone doesn't prevent getty@tty1 from being reintroduced later. + # Confirmed on hardware: NixOS's activation reactivates TTY-related + # units on every `switch` and (re)starts getty@tty1.service + # regardless of it showing "disabled" - the Conflicts= then kills + # cage-tty1 as a side effect, and nothing re-triggers it afterward + # (WantedBy=graphical.target doesn't get re-evaluated once that + # target's already reached), leaving the kiosk dark until a manual + # restart. Masking getty@tty1 outright - the standard practice for + # a TTY a compositor owns exclusively - removes the conflict at its + # source... except getty@.service's own upstream unit carries + # Alias=autovt@%i.service, so masking the getty@tty1 name alone + # doesn't stop systemd resolving the SAME underlying unit via its + # autovt@tty1 alias instead - confirmed on hardware, masking only + # getty@tty1 still left autovt@tty1.service starting at the exact + # moment cage-tty1 died. Both names need masking. + "autovt@tty1".enable = false; + + cage-tty1 = { + # Upstream's cage module sets restartIfChanged = false, presumably + # to avoid yanking an interactive desktop session out from under + # someone mid `nixos-rebuild switch`. That caution doesn't apply to + # a kiosk - confirmed on hardware that without this override, a + # config change (e.g. a new dashboard URL) requires a manual + # `systemctl restart cage-tty1` (or a reboot) to actually take + # effect, since the switch itself silently leaves the old + # chromium/cage instance running. + restartIfChanged = lib.mkForce true; + wants = [ + "wpa_supplicant-${wifiInterface}.service" + "network-online.target" + ]; + }; + + # Masked alongside its autovt@tty1 alias above - see that entry's + # comment for why both names are needed. + "getty@tty1".enable = false; + }; }; }