feat(system): Plymouth splash, distroName, allowUnfree, offline-pin hardening

- Plymouth boot splash ported from the legacy branch (modules/nixos/
  plymouth/): logo + eased progress + LUKS entry, background tinted from
  theme-state.json via the new nomarchy.system.stateFile (wired by
  mkFlake/lib.nix; null → Tokyo Night fallback). Default on; OFF on the
  live ISO (boot-message visibility on the install medium). Pulls
  boot.initrd.systemd, which also drives the keyboard-at-LUKS feature.
- system.nixos.distroName = "Nomarchy" (os-release PRETTY_NAME,
  systemd-boot entries, ISO menu label). distroId left "nixos" (feeds
  DEFAULT_HOSTNAME + upstream isNixos checks — roadmapped).
- nixpkgs.config.allowUnfree distro-wide (here + both import-nixpkgs
  sites) — unblocks claude-code for the menu's ask-Claude module.
- systemd-boot.configurationLimit = 10 so entries don't fill the ESP.
- Live ISO: nomarchy.idle.enable = false — hypridle was suspending the
  VM mid-install (the install-hung regression); installed systems keep it.
- flake.nix offline pins (verified 0-leak via a foreign-identity
  gap-analysis probe): the repo's own standalone HM gen + inputDerivations,
  mustache-go + stdenv (stylix re-renders base16 per switch), microcode-amd/
  intel (enableRedistributableFirmware activated updateMicrocode →
  source-build cascade), buildEnv's builder.pl, findXMLCatalogs, and the
  representativeInstall mirror (xkb/initrd-systemd/microcode).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Bernardo Magri
2026-06-13 07:37:32 +01:00
parent 1ba1eeed64
commit eda8461304
19 changed files with 906 additions and 1 deletions

View File

@@ -11,9 +11,15 @@ let
cfg = config.nomarchy.system;
in
{
imports = [ ./options.nix ];
imports = [ ./options.nix ./plymouth.nix ];
config = {
# The safe half of distro branding: distroName flows into
# /etc/os-release PRETTY_NAME, systemd-boot entry titles and the
# ISO boot-menu label. distroId stays "nixos" on purpose — it feeds
# DEFAULT_HOSTNAME and upstream isNixos checks (see roadmap).
system.nixos.distroName = lib.mkDefault "Nomarchy";
# Unfree allowed distro-wide: pragmatic-desktop territory (claude-code
# for the menu's ask-Claude module, vendor drivers, …). The custom
# nixpkgs-config type can't carry a nested mkDefault; disagree with
@@ -114,6 +120,9 @@ in
inter
noto-fonts
noto-fonts-color-emoji
# The legacy-ported waybar identities (summer-day/night) use
# FontAwesome codepoints in their layouts and list it in css.
font-awesome
];
fontconfig.defaultFonts = {
monospace = lib.mkDefault [ "JetBrainsMono Nerd Font" ];
@@ -125,6 +134,31 @@ in
# ── Essential packages ───────────────────────────────────────────
environment.systemPackages = with pkgs; [
nomarchy-theme-sync # provided by overlays.default
# Friendly wrappers for the two rebuild paths (README §3). Run as
# your user: `nix flake update` must NOT run as root (libgit2
# refuses the user-owned flake repo) — sudo happens inside, only
# for the system switch.
(pkgs.writeShellScriptBin "sys-update" ''
set -e
if [ "$(id -u)" -eq 0 ]; then
echo "sys-update: run as your normal user (it sudos the rebuild itself)" >&2
exit 1
fi
flake="''${NOMARCHY_PATH:-$HOME/.nomarchy}"
echo "sys-update: updating flake inputs in $flake"
nix flake update --flake "$flake"
if command -v nixos-rebuild-snap >/dev/null 2>&1; then
sudo nixos-rebuild-snap "$@" # BTRFS snapshot first
else
sudo nixos-rebuild switch --flake "$flake#default" "$@"
fi
'')
(pkgs.writeShellScriptBin "home-update" ''
set -e
exec home-manager switch --flake "''${NOMARCHY_PATH:-$HOME/.nomarchy}" "$@"
'')
git
vim
wget
@@ -152,6 +186,9 @@ in
nixos-rebuild switch --flake /etc/nixos#default "$@"
'');
# Don't let boot entries fill the ESP over the years.
boot.loader.systemd-boot.configurationLimit = lib.mkDefault 10;
# ── Nix itself ───────────────────────────────────────────────────
nix = {
settings = {

View File

@@ -21,6 +21,22 @@
'';
};
plymouth.enable = lib.mkEnableOption ''
the Nomarchy Plymouth boot splash (logo + progress + LUKS prompt),
background-tinted from theme-state.json via nomarchy.system.stateFile.
Recolors on system rebuilds theme switches don't touch the initrd'' // { default = true; };
stateFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
example = lib.literalExpression "./theme-state.json";
description = ''
theme-state.json for the system-side consumers (currently the
Plymouth splash background). lib.mkFlake wires it automatically
from your flake; null falls back to the Tokyo Night base color.
'';
};
audio.enable = lib.mkEnableOption "the Pipewire audio stack" // { default = true; };
bluetooth.enable = lib.mkEnableOption "Bluetooth support with blueman" // { default = true; };

View File

@@ -0,0 +1,87 @@
# Plymouth boot splash — Nomarchy-branded (ported from the legacy
# iteration), background tinted from the same theme-state.json that
# drives the desktop (nomarchy.system.stateFile, wired automatically by
# lib.mkFlake). One caveat by design: theme switches are Home
# Manager-only and never touch the initrd, so the splash follows the
# theme as of the last SYSTEM rebuild (`sys-update`), not the last
# `nomarchy-theme-sync apply`.
{ config, lib, pkgs, ... }:
let
cfg = config.nomarchy.system;
state =
if cfg.stateFile != null
then builtins.fromJSON (builtins.readFile cfg.stateFile)
else { };
base = lib.removePrefix "#" ((state.colors or { }).base or "#1a1b26");
# Plymouth's Window.SetBackgroundTopColor takes three floats in
# 0.01.0; the .plymouth metadata's ConsoleLogBackgroundColor takes a
# 0xRRGGBB hex. Nix has no float math: multiply, integer-divide, pad.
byteToFloat = n:
let
thousandths = (n * 1000) / 255;
s = toString thousandths;
padded =
if lib.stringLength s == 1 then "00${s}"
else if lib.stringLength s == 2 then "0${s}"
else s;
in "0.${padded}";
channel = off: byteToFloat (lib.fromHexString (lib.substring off 2 base));
nomarchy-plymouth = pkgs.stdenv.mkDerivation {
pname = "nomarchy-plymouth";
version = "1.0";
src = ./plymouth;
installPhase = ''
mkdir -p $out/share/plymouth/themes/nomarchy
cp * $out/share/plymouth/themes/nomarchy/
# Point the .plymouth metadata into the store
sed -i "s|/usr/share/plymouth/themes/nomarchy|$out/share/plymouth/themes/nomarchy|g" \
$out/share/plymouth/themes/nomarchy/nomarchy.plymouth
# Bake the theme's base color: RGB floats for the script's
# Window.SetBackground* calls, 0xRRGGBB for ConsoleLogBackground.
sed -i \
-e 's|@BG_R@|${channel 0}|g' \
-e 's|@BG_G@|${channel 2}|g' \
-e 's|@BG_B@|${channel 4}|g' \
$out/share/plymouth/themes/nomarchy/nomarchy.script
sed -i 's|@BG_HEX@|${base}|g' \
$out/share/plymouth/themes/nomarchy/nomarchy.plymouth
'';
};
in
{
config = lib.mkIf cfg.plymouth.enable {
# Plymouth wants the systemd initrd (also what applies the keyboard
# layout to the LUKS prompt) and a quiet console around it.
boot.initrd.systemd.enable = lib.mkDefault true;
boot.initrd.verbose = lib.mkDefault false;
console.earlySetup = lib.mkDefault true;
boot.consoleLogLevel = lib.mkDefault 0;
boot.plymouth = {
enable = lib.mkDefault true;
themePackages = lib.mkDefault [ nomarchy-plymouth ];
theme = lib.mkDefault "nomarchy";
};
# Not mkDefault: kernelParams is a list other modules add to at
# normal priority — a mkDefault def would be dropped entirely,
# losing the quiet/splash boot. These merge with the rest.
boot.kernelParams = [
"quiet"
"splash"
"loglevel=3"
"rd.systemd.show_status=false"
"rd.udev.log_level=3"
"udev.log_priority=3"
"boot.shell_on_fail"
];
};
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 694 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -0,0 +1,11 @@
[Plymouth Theme]
Name=Nomarchy
Description=Nomarchy splash screen.
ModuleName=script
[script]
ImageDir=/usr/share/plymouth/themes/nomarchy
ScriptFile=/usr/share/plymouth/themes/nomarchy/nomarchy.script
ConsoleLogBackgroundColor=0x@BG_HEX@
MonospaceFont=Cantarell 11
Font=Cantarell 11

View File

@@ -0,0 +1,271 @@
# Nomarchy Plymouth Theme Script
Window.SetBackgroundTopColor(@BG_R@, @BG_G@, @BG_B@);
Window.SetBackgroundBottomColor(@BG_R@, @BG_G@, @BG_B@);
logo.image = Image("logo.png");
# Calculate scale factor to make logo ~15% of screen height
logo_scale_factor = (Window.GetHeight() * 0.15) / logo.image.GetHeight();
logo_width = logo.image.GetWidth() * logo_scale_factor;
logo_height = logo.image.GetHeight() * logo_scale_factor;
logo.image = logo.image.Scale(logo_width, logo_height);
logo.sprite = Sprite(logo.image);
logo.sprite.SetX (Window.GetWidth() / 2 - logo.image.GetWidth() / 2);
logo.sprite.SetY (Window.GetHeight() / 2 - logo.image.GetHeight() / 2);
logo.sprite.SetOpacity (1);
# Use these to adjust the progress bar timing
global.fake_progress_limit = 0.7; # Target percentage for fake progress (0.0 to 1.0)
global.fake_progress_duration = 15.0; # Duration in seconds to reach limit
# Progress bar animation variables
global.fake_progress = 0.0;
global.real_progress = 0.0;
global.fake_progress_active = 0; # 0 / 1 boolean
global.animation_frame = 0;
global.fake_progress_start_time = 0; # Track when fake progress started
global.password_shown = 0; # Track if password dialog has been shown
global.max_progress = 0.0; # Track the maximum progress reached to prevent backwards movement
fun refresh_callback ()
{
global.animation_frame++;
# Animate fake progress to limit over time with easing
if (global.fake_progress_active == 1)
{
# Calculate elapsed time since start
elapsed_time = global.animation_frame / 50.0; # Convert frames to seconds (50 FPS)
# Calculate linear progress ratio (0 to 1) based on time
time_ratio = elapsed_time / global.fake_progress_duration;
if (time_ratio > 1.0)
time_ratio = 1.0;
# Apply easing curve: ease-out quadratic
# Formula: 1 - (1 - x)^2
eased_ratio = 1 - ((1 - time_ratio) * (1 - time_ratio));
# Calculate fake progress based on eased ratio
global.fake_progress = eased_ratio * global.fake_progress_limit;
# Update progress bar with fake progress
update_progress_bar(global.fake_progress);
}
}
Plymouth.SetRefreshFunction (refresh_callback);
#----------------------------------------- Helper Functions --------------------------------
fun update_progress_bar(progress)
{
# Only update if progress is moving forward
if (progress > global.max_progress)
{
global.max_progress = progress;
width = Math.Int(progress_bar.original_image.GetWidth() * progress);
if (width < 1) width = 1; # Ensure minimum width of 1 pixel
progress_bar.image = progress_bar.original_image.Scale(width, progress_bar.original_image.GetHeight());
progress_bar.sprite.SetImage(progress_bar.image);
}
}
fun show_progress_bar()
{
progress_box.sprite.SetOpacity(1);
progress_bar.sprite.SetOpacity(1);
}
fun hide_progress_bar()
{
progress_box.sprite.SetOpacity(0);
progress_bar.sprite.SetOpacity(0);
}
fun show_password_dialog()
{
lock.sprite.SetOpacity(1);
entry.sprite.SetOpacity(1);
}
fun hide_password_dialog()
{
lock.sprite.SetOpacity(0);
entry.sprite.SetOpacity(0);
for (index = 0; bullet.sprites[index]; index++)
bullet.sprites[index].SetOpacity(0);
}
fun start_fake_progress()
{
# Don't reset if we already have progress
if (global.max_progress == 0.0)
{
global.fake_progress = 0.0;
global.real_progress = 0.0;
update_progress_bar(0.0);
}
global.fake_progress_active = 1;
global.animation_frame = 0;
}
fun stop_fake_progress()
{
global.fake_progress_active = 0;
}
#----------------------------------------- Dialogue --------------------------------
lock.image = Image("lock.png");
entry.image = Image("entry.png");
bullet.image = Image("bullet.png");
entry.sprite = Sprite(entry.image);
entry.x = Window.GetWidth()/2 - entry.image.GetWidth() / 2;
entry.y = logo.sprite.GetY() + logo.image.GetHeight() + 40;
entry.sprite.SetPosition(entry.x, entry.y, 10001);
entry.sprite.SetOpacity(0);
# Scale lock to be slightly shorter than entry field height
# Original lock is 84x96, entry height determines scale
lock_height = entry.image.GetHeight() * 0.8;
lock_scale = lock_height / 96;
lock_width = 84 * lock_scale;
scaled_lock = lock.image.Scale(lock_width, lock_height);
lock.sprite = Sprite(scaled_lock);
lock.x = entry.x - lock_width - 15;
lock.y = entry.y + entry.image.GetHeight()/2 - lock_height/2;
lock.sprite.SetPosition(lock.x, lock.y, 10001);
lock.sprite.SetOpacity(0);
# Bullet array
bullet.sprites = [];
fun display_normal_callback ()
{
hide_password_dialog();
# Get current mode
mode = Plymouth.GetMode();
# Only show progress bar for boot and resume modes
if (mode == "boot" || mode == "resume")
{
show_progress_bar();
start_fake_progress();
}
}
fun display_password_callback (prompt, bullets)
{
global.password_shown = 1; # Mark that password dialog has been shown
# Reset progress when password dialog appears
stop_fake_progress();
hide_progress_bar();
global.max_progress = 0.0;
global.fake_progress = 0.0;
global.real_progress = 0.0;
show_password_dialog();
# Clear all bullets first
for (index = 0; bullet.sprites[index]; index++)
bullet.sprites[index].SetOpacity(0);
# Create and show bullets for current password (max 21)
max_bullets = 21;
bullets_to_show = bullets;
if (bullets_to_show > max_bullets)
bullets_to_show = max_bullets;
for (index = 0; index < bullets_to_show; index++)
{
if (!bullet.sprites[index])
{
# Scale bullet image to 7x7 pixels
scaled_bullet = bullet.image.Scale(7, 7);
bullet.sprites[index] = Sprite(scaled_bullet);
bullet.x = entry.x + 20 + index * (7 + 5);
bullet.y = entry.y + entry.image.GetHeight() / 2 - 3.5;
bullet.sprites[index].SetPosition(bullet.x, bullet.y, 10002);
}
bullet.sprites[index].SetOpacity(1);
}
}
Plymouth.SetDisplayNormalFunction(display_normal_callback);
Plymouth.SetDisplayPasswordFunction(display_password_callback);
#----------------------------------------- Progress Bar --------------------------------
progress_box.image = Image("progress_box.png");
progress_box.sprite = Sprite(progress_box.image);
progress_box.x = Window.GetWidth() / 2 - progress_box.image.GetWidth() / 2;
progress_box.y = entry.y + entry.image.GetHeight() / 2 - progress_box.image.GetHeight() / 2;
progress_box.sprite.SetPosition(progress_box.x, progress_box.y, 0);
progress_box.sprite.SetOpacity(0);
progress_bar.original_image = Image("progress_bar.png");
progress_bar.sprite = Sprite();
progress_bar.image = progress_bar.original_image.Scale(1, progress_bar.original_image.GetHeight());
progress_bar.x = Window.GetWidth() / 2 - progress_bar.original_image.GetWidth() / 2;
progress_bar.y = progress_box.y + (progress_box.image.GetHeight() - progress_bar.original_image.GetHeight()) / 2;
progress_bar.sprite.SetPosition(progress_bar.x, progress_bar.y, 1);
progress_bar.sprite.SetOpacity(0);
fun progress_callback (duration, progress)
{
global.real_progress = progress;
# If real progress is above limit, stop fake progress and use real progress
if (progress > global.fake_progress_limit)
{
stop_fake_progress();
update_progress_bar(progress);
}
}
Plymouth.SetBootProgressFunction(progress_callback);
#----------------------------------------- Quit --------------------------------
fun quit_callback ()
{
logo.sprite.SetOpacity (1);
}
Plymouth.SetQuitFunction(quit_callback);
#----------------------------------------- Message --------------------------------
message_sprite = Sprite();
message_sprite.SetPosition(10, 10, 10000);
fun display_message_callback (text)
{
my_image = Image.Text(text, 1, 1, 1);
message_sprite.SetImage(my_image);
}
fun hide_message_callback (text)
{
message_sprite.SetOpacity(0);
}
Plymouth.SetDisplayMessageFunction (display_message_callback);
Plymouth.SetHideMessageFunction (hide_message_callback);
# Initialize progress bar immediately for normal boots
if (Plymouth.GetMode() == "boot" || Plymouth.GetMode() == "resume")
{
show_progress_bar();
start_fake_progress();
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 B