diff --git a/DESKTOP.md b/DESKTOP.md index f1556c3..d1cb5c1 100644 --- a/DESKTOP.md +++ b/DESKTOP.md @@ -5,6 +5,8 @@ Reviewed 2026-09-06 UTC on the physical laptop and against the pinned upstream s ## Design - Dark **One Ring** wallpaper, charcoal surfaces, parchment text and restrained old-gold accents. Terminal ANSI colors remain distinct; Neovim's theme and configuration are unchanged. +- **JetBrains Mono Nerd Font** throughout the system/UI defaults, with Noto emoji/CJK fallbacks. GTK, Qt's GTK integration, native desktop components and the lock screen share it; explicit website fonts and Tor Browser privacy settings are not overridden. +- **SDDM Astronaut** is styled with the same static wallpaper, gold accents and monospace typography. The left-side login form leaves the artwork visible, with no animated background or blur. Only the greeter's presentation changes: no host autologin, PAM/password edits or bootloader changes. - One continuous top bar: launcher/workspaces/help and active-window context on the left, date/time in the center, media, CPU/RAM/temperature, notification count, privacy and laptop status on the right. Details and actions are available without filling the bar with permanent buttons. - The bar's quick-settings panel owns audio, microphone, brightness, network/VPN, Bluetooth, power profiles and idle inhibition. Its **Actions** and **Health** buttons open the action palette and real system/update diagnostics. - SwayNC owns notification history, actions, DND and media—not a duplicate hardware settings dashboard. @@ -62,6 +64,8 @@ The native audit caught real issues that configuration evaluation did not: neste `desktop-test.py` exercises the disposable graphical VM using the same SDDM/UWSM workstation module as the laptop: session ownership, fonts, real PipeWire nodes, launcher geometry at 100%/150%, clipboard picker, recording container and no-audio default, notification ownership/inhibition, described help, and real wrong/correct-password PAM locking. Test credentials never reach the host. `desktop-actions-test.py` covers cancellation, untrusted input, byte-preserving clipboard behavior, lock failure handling, display timeout restoration and recording-inhibitor cleanup. +`nix build .#checks.x86_64-linux.appearance --out-link /tmp/workstation-appearance` checks generic font matching, emoji/CJK fallback and shared UI settings, then renders the actual packaged Qt6 SDDM greeter in test mode inside a disposable 1920×1080 Xvfb display. Its `greeter.png` and `greeter.log` are suitable for review without logging out, restarting SDDM or authenticating anyone. This is a theme rendering check, not a new real-password login test. + **Completed validation (2026-09-06):** flake evaluation, Nix formatting, generated Hyprland configuration, physical/AWS/shared-policy assertions, 125 CLI executable smoke checks, 14 desktop-action tests, 23 updater regressions, 22 manual-switch regressions (including lock retention through sudo), both host system builds, and the full SDDM/UWSM graphical/PAM/clipboard/recording VM check all passed on the refreshed inputs. Native clipboard, recording and rootless Podman checks passed too. The 150% six-result launcher and help screenshots were inspected visually; the transparent launcher click-catcher's IPC dimensions are not mistaken for the visible palette bounds. **Account/hardware boundaries:** diff --git a/README.md b/README.md index fd82eb6..5fbfbfc 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Flat, explicit NixOS modules with locked inputs. Required setup belongs here—n | `switch-system.sh`, `switch-test.py` | Same installed manual apply/preview command on both hosts, with host identity supplied by Nix | | `physical-test.nix`, `tools-test.nix`, `git-credentials-test.nix` | Physical/AWS safety, shared-policy assertions, offline tool/help smoke tests and disposable Git credential-cache checks | | `desktop-test.nix`, `desktop-test.py`, `audit-desktop.sh` | Disposable graphical/PAM/audio/scaling audit | -| `workstation.nix`, `nvidia.nix` | Shared local hardware/SDDM integration for laptop and VM; separate opt-in NVIDIA support | +| `workstation.nix`, `greeter-theme.nix`, `appearance-test.nix`, `nvidia.nix` | Local hardware/SDDM integration, One Ring login theme and isolated rendering/font check; separate opt-in NVIDIA support | | [DESKTOP.md](DESKTOP.md) | Live audit, wallpaper provenance, JaKooLit comparison and explicit feature-completion plan | | `agent-vm/`, `templates/agent/`, [AGENT-VM.md](AGENT-VM.md) | Reusable rootless DSH microVM, shared project toolchain, live RW cwd/config/credentials/skills and access/networking guide | @@ -33,6 +33,7 @@ Flat, explicit NixOS modules with locked inputs. Required setup belongs here—n - The physical host uses the locally established `dev` password; mutable users preserve it. No plaintext password, private key or password hash is embedded in this repo/store. - At the user's request, **Plasma and the previously managed `kbot` account are removed**. This configuration does not delete `/home/kbot`; retained files and old generations are not erased. Home Manager only manages `dev`. - SDDM exposes **Hyprland (uwsm-managed)** only. Plain Hyprland is deliberately hidden because it does not start the session-bound bar, wallpaper, idle and polkit services. UWSM is the single session owner. +- The login greeter uses **SDDM Astronaut**, customized to the One Ring wallpaper, charcoal/parchment/gold palette, JetBrains Mono, a minimal left-hand form and matching cursor. This is a Qt6 login theme, not Plasma. PAM, passwords, session selection and the bootloader are unchanged; host autologin remains disabled. - Both checkouts are **dev-owned**, including Git metadata: `/etc/nix` on the laptop and `/etc/nixos` on EC2. Existing paths are retained to avoid moving deployed repositories. NixOS maintains ownership without following symlinks into the Nix store; the laptop's original `/etc/nixos` files stay untouched. - On EC2, AWS boot/storage/networking and SSM/SSH recovery remain intact; no physical greeter, Wi-Fi/Bluetooth or battery services are added. User settings, locale/timezone, packages and update policy come from the same modules. The graphical VM now uses the same SDDM/UWSM integration as the laptop. - The SSH key in `dev-authorized-keys` is public. Account/cloud/browser/VPN credentials and vaults remain user-controlled. @@ -63,6 +64,8 @@ Flat, explicit NixOS modules with locked inputs. Required setup belongs here—n | `direnv allow` | Explicitly approve a project's `.envrc`; no automatic trust whitelist | | `tmux`, `zellij`, `lazygit` | Persistent terminal workspaces and Git UI; not auto-started or forced | +**JetBrains Mono Nerd Font** is the shared system default for monospace, sans-serif and serif requests, GTK/Qt application UI, bar, launchers, notifications, dialogs and lock/login screens. The family is declared once in `tools.nix`; UI sizes remain 11pt where appropriate and Kitty stays 12pt. Noto fonts preserve emoji and international-character coverage. App/site-specific embedded fonts and Tor Browser's privacy defaults are not forcibly overridden; the Linux text console still uses its bitmap font. + Kitty uses an opaque charcoal background, 12pt JetBrains Mono and compact padding. Font zoom remains `Ctrl-Shift-+` / `Ctrl-Shift--`; reset with `Ctrl-Shift-Backspace`. Semantic ANSI colors remain distinguishable from the gold UI accent. Neovim's own theme is unchanged. The **informative top bar** includes workspaces/window context, clock, CPU/RAM/host temperature, media, notification count, privacy and laptop status. Click its right-hand status area for audio, microphone, brightness, network/Bluetooth, idle inhibition and power-profile controls. **Actions** and **Health** open the searchable action palette and real system/update diagnostics. Notification history is a separate compact drawer, not another settings dashboard. diff --git a/anyrun.css b/anyrun.css index ae3e44c..1b6a850 100644 --- a/anyrun.css +++ b/anyrun.css @@ -1,4 +1,4 @@ -* { font-family: Inter, sans-serif; font-size: 16px; } +* { font-family: "@font@", monospace; font-size: 16px; } window { background: transparent; } box.main { padding: 12px; diff --git a/appearance-test.nix b/appearance-test.nix new file mode 100644 index 0000000..22e01cb --- /dev/null +++ b/appearance-test.nix @@ -0,0 +1,79 @@ +# Render the actual packaged greeter in an isolated X server, never live SDDM. +{ config, pkgs }: +let + inherit (pkgs) lib; + font = "JetBrainsMono Nerd Font"; + hm = config.home-manager.users.dev; + theme = import ./greeter-theme.nix { inherit pkgs font; }; + sddm = config.services.displayManager.sddm.package.override { + extraPackages = config.services.displayManager.sddm.extraPackages; + }; + fontConfig = pkgs.makeFontsConf { + fontDirectories = config.fonts.packages; + impureFontDirectories = [ ]; + includes = [ "${config.environment.etc.fonts.source}/conf.d" ]; + }; +in +assert lib.all (family: builtins.head config.fonts.fontconfig.defaultFonts.${family} == font) [ + "sansSerif" + "serif" + "monospace" +]; +assert hm.gtk.font.name == font; +assert hm.qt.platformTheme.name == "gtk3"; +assert hm.programs.kitty.font.name == font; +assert hm.programs.ashell.settings.appearance.font_name == font; +assert hm.dconf.settings."org/gnome/desktop/interface".font-name == "${font} 11"; +assert lib.all (label: label.font_family == font) hm.programs.hyprlock.settings.label; +assert lib.hasInfix font hm.programs.anyrun.extraCss; +assert lib.hasInfix font hm.services.swaync.style; +assert config.services.displayManager.sddm.theme == "sddm-astronaut-theme"; +assert config.services.displayManager.sddm.settings.Theme.Font == font; +assert !config.services.displayManager.autoLogin.enable; +assert !config.services.desktopManager.plasma6.enable; +pkgs.runCommand "desktop-appearance-check" + { + nativeBuildInputs = with pkgs; [ + fontconfig + xvfb-run + xdotool + imagemagick + gnugrep + ]; + } + '' + export HOME="$TMPDIR/home" XDG_CACHE_HOME="$TMPDIR/cache" XDG_RUNTIME_DIR="$TMPDIR/runtime" + mkdir -m 700 -p "$HOME" "$XDG_CACHE_HOME" "$XDG_RUNTIME_DIR" "$out" + export FONTCONFIG_FILE=${fontConfig} + for family in sans-serif serif monospace; do + fc-match --format='%{family}' "$family" | grep -Fq '${font}' + done + fc-match --format='%{family}' emoji | grep -Fq 'Noto Color Emoji' + fc-match --format='%{family}' ':charset=4e00' | grep -Fq 'Noto Sans CJK' + test -r ${theme}/share/sddm/themes/sddm-astronaut-theme/Backgrounds/one-ring.jpg + export QT_QUICK_BACKEND=software LIBGL_ALWAYS_SOFTWARE=1 QT_QPA_PLATFORM=xcb + xvfb-run -a -s '-screen 0 1920x1080x24' ${pkgs.runtimeShell} -euc ' + ${sddm}/bin/sddm-greeter-qt6 --test-mode \ + --theme ${theme}/share/sddm/themes/sddm-astronaut-theme > "$out/greeter.log" 2>&1 & + pid=$! + trap "kill $pid 2>/dev/null || true" EXIT + for attempt in $(seq 1 40); do + kill -0 "$pid" + if xdotool search --onlyvisible --pid "$pid" > /dev/null 2>&1; then break; fi + sleep 0.5 + done + sleep 3 + kill -0 "$pid" + xdotool search --onlyvisible --pid "$pid" > /dev/null + magick import -window root "$out/greeter.png" + # Check the focused password field and enabled-button appearance, without login. + window=$(xdotool search --onlyvisible --pid "$pid" | head -n 1) + xdotool windowfocus --sync "$window" + xdotool type --clearmodifiers "preview-only" + sleep 1 + magick import -window root "$out/greeter-password.png" + if grep -Ei "(module .* is not installed|failed to load|is not a type|ReferenceError|TypeError|cannot assign)" "$out/greeter.log"; then + exit 1 + fi + ' + '' diff --git a/apps.nix b/apps.nix index 0f17ed2..f2528af 100644 --- a/apps.nix +++ b/apps.nix @@ -131,7 +131,7 @@ in programs.zathura = { enable = true; options = { - font = "Inter 12"; + font = "${builtins.head config.fonts.fontconfig.defaultFonts.monospace} 12"; adjust-open = "best-fit"; zoom-step = 10; recolor = false; # Preserve actual document colors; Ctrl-R toggles recolor. diff --git a/desktop-test.py b/desktop-test.py index 9e6ff5f..f60962a 100644 --- a/desktop-test.py +++ b/desktop-test.py @@ -31,7 +31,7 @@ def screenshot(name): # Capture through Wayland; QEMU's framebuffer dump cannot read VirGL surfaces. path = "/tmp/" + name + ".png" session("grim " + shlex.quote(path)) - machine.copy_from_vm(path) + machine.copy_from_machine(path) def launch(name, command): @@ -58,10 +58,21 @@ except Exception: raise wait_layer("ashell-main-layer") +# A headless GPU's preferred mode is not necessarily the requested test size. +# Fix both mode and scale before calling screenshots a 100% geometry audit. +output = json.loads(session("hyprctl -j monitors"))[0]["name"] +session("hyprctl eval " + shlex.quote( + 'hl.monitor({output=' + json.dumps(output) + ',mode="1920x1080@60",position="0x0",scale=1})' +)) +machine.wait_until_succeeds(in_session( + "hyprctl -j monitors | jq -e '.[0] | .width == 1920 and .height == 1080 and .scale == 1'" +)) screenshot("startup") assert session("hyprctl configerrors").strip() in ("", "ok") -assert "JetBrainsMono" in user("fc-match 'JetBrainsMono Nerd Font'") -assert "Inter" in user("fc-match Inter") +for family in ["sans-serif", "serif", "monospace"]: + assert "JetBrainsMono" in user("fc-match " + shlex.quote(family)) +assert "Noto Color Emoji" in user("fc-match --format='%{family}' emoji") +assert "JetBrainsMono Nerd Font 11" in user("dconf read /org/gnome/desktop/interface/font-name") # Pi's real --version is checked natively; avoid costly Node startup under TCG. user("test -x /run/current-system/sw/bin/pi") assert "zsh" in user("getent passwd dev") @@ -133,8 +144,6 @@ session("swaync-client -t -sw") machine.sleep(1) screenshot("notification-100") -monitors = json.loads(session("hyprctl -j monitors")) -output = monitors[0]["name"] session("hyprctl eval " + shlex.quote( 'hl.monitor({output=' + json.dumps(output) + ',mode="1920x1080@60",position="0x0",scale=1.5})' )) @@ -204,5 +213,6 @@ user("test ! -e /etc/profiles/per-user/dev/share/applications/element-desktop.de user("grep -Eq 'fade_on_empty *= *false' ~/.config/hypr/hyprlock.conf") assert "libapplications.so" in user("cat ~/.config/anyrun/config.ron") session("hyprctl clients") + machine.succeed("journalctl -b -p err --no-pager > /tmp/desktop-errors.log") -machine.copy_from_vm("/tmp/desktop-errors.log") +machine.copy_from_machine("/tmp/desktop-errors.log") diff --git a/desktop.nix b/desktop.nix index 451ffae..517f092 100644 --- a/desktop.nix +++ b/desktop.nix @@ -7,11 +7,15 @@ let c = import ./colors.nix; + font = builtins.head config.fonts.fontconfig.defaultFonts.monospace; + styleTokens = c // { + inherit font; + }; renderColors = text: builtins.replaceStrings (map (name: "@${name}@") ( - builtins.attrNames c - )) (builtins.attrValues c) text; + builtins.attrNames styleTokens + )) (builtins.attrValues styleTokens) text; rgb = color: "rgb(${lib.removePrefix "#" color})"; wallpaper = import ./wallpaper.nix { inherit pkgs; }; help = pkgs.writeShellApplication { @@ -137,17 +141,21 @@ in ]; services.udev.packages = [ pkgs.brightnessctl ]; fonts.packages = with pkgs; [ - inter noto-fonts noto-fonts-cjk-sans noto-fonts-color-emoji ]; fonts.fontconfig.defaultFonts = { + # Use the code font for generic UI/document families too, retaining + # international glyph fallbacks rather than replacing missing characters. sansSerif = [ - "Inter" + font "Noto Sans" ]; - serif = [ "Noto Serif" ]; + serif = [ + font + "Noto Serif" + ]; emoji = [ "Noto Color Emoji" ]; }; @@ -169,7 +177,7 @@ in gtk = { enable = true; font = { - name = "Inter"; + name = font; size = 11; }; theme = { @@ -186,14 +194,19 @@ in qt = { enable = true; platformTheme.name = "gtk3"; + # GTK integration also supplies the same font to Qt 5/6 applications. # Let Home Manager provide BOTH Qt 5 and Qt 6 style plugins. style.name = "adwaita-dark"; }; - dconf.settings."org/gnome/desktop/interface" = { - color-scheme = "prefer-dark"; - font-name = "Inter 11"; - monospace-font-name = "JetBrainsMono Nerd Font 12"; - accent-color = "yellow"; + dconf.settings = { + "org/gnome/desktop/interface" = { + color-scheme = "prefer-dark"; + font-name = "${font} 11"; + document-font-name = "${font} 11"; + monospace-font-name = "${font} 12"; + accent-color = "yellow"; + }; + "org/gnome/desktop/wm/preferences".titlebar-font = "${font} Bold 11"; }; wayland.windowManager.hyprland = { @@ -331,7 +344,7 @@ in audio_indicator_format = "IconAndPercentage"; }; appearance = { - font_name = "Inter"; + font_name = font; scale_factor = 1.15; style = "Solid"; opacity = 1.0; @@ -401,7 +414,7 @@ in enable = true; settings = { main = { - font = "Inter:size=11"; + font = "${font}:size=11"; terminal = "kitty"; "launch-prefix" = "uwsm app --"; "line-height" = 22; @@ -524,7 +537,7 @@ in monitor = ""; text = "cmd[update:1000] date +'%H:%M'"; font_size = 64; - font_family = "Inter"; + font_family = font; color = rgb c.text; position = "0,100"; halign = "center"; @@ -534,7 +547,7 @@ in monitor = ""; text = "$USER"; font_size = 18; - font_family = "Inter"; + font_family = font; color = rgb c.muted; position = "0,0"; halign = "center"; @@ -551,7 +564,7 @@ in outline_thickness = 2; dots_center = true; fade_on_empty = false; - font_family = "Inter"; + font_family = font; rounding = 6; inner_color = rgb c.surface; outer_color = rgb c.accent; diff --git a/flake.nix b/flake.nix index 946f5d6..185b6b3 100644 --- a/flake.nix +++ b/flake.nix @@ -63,6 +63,10 @@ in { desktop = import ./desktop-test.nix { inherit inputs pkgs; }; + appearance = import ./appearance-test.nix { + inherit pkgs; + config = inputs.self.nixosConfigurations.nixos.config; + }; git-credentials = import ./git-credentials-test.nix { inherit pkgs; config = inputs.self.nixosConfigurations.nixos.config; diff --git a/greeter-theme.nix b/greeter-theme.nix new file mode 100644 index 0000000..2ea51e6 --- /dev/null +++ b/greeter-theme.nix @@ -0,0 +1,65 @@ +# Packaged Qt6 theme, styled with the same wallpaper/palette as the desktop. +# No runtime downloader, custom QML, Plasma desktop, or authentication changes. +{ pkgs, font }: +let + c = import ./colors.nix; + wallpaper = import ./wallpaper.nix { inherit pkgs; }; +in +(pkgs.sddm-astronaut.override { + embeddedTheme = "black_hole"; + themeConfig = { + Font = font; + FontSize = "11"; + HeaderText = "Welcome back"; + HourFormat = "HH:mm"; + DateFormat = "dddd d MMMM"; + Background = "Backgrounds/one-ring.jpg"; + DimBackground = "0.15"; + CropBackground = "true"; + FormPosition = "left"; + HaveFormBackground = "true"; + PartialBlur = "false"; + FullBlur = "false"; + RoundCorners = "10"; + UseRealName = "false"; + HideCompletePassword = "true"; + AllowEmptyPassword = "false"; + HeaderTextColor = c.accent; + DateTextColor = c.muted; + TimeTextColor = c.text; + FormBackgroundColor = c.background; + BackgroundColor = c.background; + DimBackgroundColor = c.background; + LoginFieldBackgroundColor = c.surface; + PasswordFieldBackgroundColor = c.surface; + LoginFieldTextColor = c.text; + PasswordFieldTextColor = c.text; + PlaceholderTextColor = c.muted; + WarningColor = c.red; + LoginButtonTextColor = c.background; + LoginButtonBackgroundColor = c.accent; + UserIconColor = c.text; + PasswordIconColor = c.text; + SystemButtonsIconsColor = c.text; + SessionButtonTextColor = c.text; + VirtualKeyboardButtonTextColor = c.text; + DropdownTextColor = c.text; + DropdownSelectedBackgroundColor = c.selection; + DropdownBackgroundColor = c.surface; + HighlightTextColor = c.background; + HighlightBackgroundColor = c.accent; + HighlightBorderColor = c.accent; + HoverUserIconColor = c.accent; + HoverPasswordIconColor = c.accent; + HoverSystemButtonsIconsColor = c.accent; + HoverSessionButtonTextColor = c.accent; + HoverVirtualKeyboardButtonTextColor = c.accent; + }; +}).overrideAttrs + (old: { + postInstall = (old.postInstall or "") + '' + # The theme resolves background paths relative to its own directory. + chmod u+w "$out/share/sddm/themes/sddm-astronaut-theme/Backgrounds" + ln -s ${wallpaper} "$out/share/sddm/themes/sddm-astronaut-theme/Backgrounds/one-ring.jpg" + ''; + }) diff --git a/swaync.css b/swaync.css index 34613af..4ba4b88 100644 --- a/swaync.css +++ b/swaync.css @@ -1,4 +1,4 @@ -* { font-family: Inter, sans-serif; font-size: 14px; color: @text@; } +* { font-family: "@font@", monospace; font-size: 14px; color: @text@; } .control-center { background: @background@; border: 1px solid @border@; diff --git a/tools.nix b/tools.nix index c0c2e59..2f0c51b 100644 --- a/tools.nix +++ b/tools.nix @@ -2,6 +2,7 @@ let c = import ./colors.nix; + font = "JetBrainsMono Nerd Font"; # Standalone tools from the fast-moving pin, NOT an overlay of the system's # Python/GCC/libraries. Desktop, drivers and NixOS services remain coherent. latest = import inputs.nixpkgs-latest { @@ -229,7 +230,8 @@ in ]); fonts.packages = [ pkgs.nerd-fonts.jetbrains-mono ]; - fonts.fontconfig.defaultFonts.monospace = [ "JetBrainsMono Nerd Font" ]; + # Desktop/greeter consumers derive their font from this shared default. + fonts.fontconfig.defaultFonts.monospace = [ font ]; home-manager.users.dev = { config, ... }: { programs.git = { @@ -409,7 +411,7 @@ in programs.kitty = { enable = true; font = { - name = "JetBrainsMono Nerd Font"; + name = font; size = 12; }; settings = { diff --git a/workstation.nix b/workstation.nix index abf9138..8545270 100644 --- a/workstation.nix +++ b/workstation.nix @@ -7,6 +7,8 @@ ... }: let + font = builtins.head config.fonts.fontconfig.defaultFonts.monospace; + greeterTheme = import ./greeter-theme.nix { inherit pkgs font; }; managedHyprlandSession = pkgs.runCommand "hyprland-managed-session" { @@ -28,7 +30,17 @@ in services.xserver.enable = true; services.displayManager = { - sddm.enable = true; + sddm = { + enable = true; + package = pkgs.kdePackages.sddm; # Qt6 runtime, not the Plasma desktop. + theme = "sddm-astronaut-theme"; + extraPackages = [ greeterTheme ]; # Carries the theme's Qt6 QML dependencies. + settings.Theme = { + Font = font; + CursorTheme = "Bibata-Modern-Ice"; + CursorSize = 24; + }; + }; defaultSession = "hyprland-uwsm"; # Plain Hyprland bypasses the UWSM-owned bar/idle/polkit services. sessionPackages = lib.mkForce [ managedHyprlandSession ]; @@ -38,7 +50,11 @@ in services.gvfs.enable = true; services.fwupd.enable = true; home-manager.users.dev.services.udiskie.enable = true; - environment.systemPackages = [ pkgs.networkmanagerapplet ]; + environment.systemPackages = [ + pkgs.networkmanagerapplet + greeterTheme + pkgs.bibata-cursors + ]; hardware.bluetooth = { enable = true;