diff --git a/flake.nix b/flake.nix
index 8df64cd..825a676 100644
--- a/flake.nix
+++ b/flake.nix
@@ -148,6 +148,7 @@
specialArgs = { inherit username; };
modules = [
self.nixosModules.nomarchy
+ { nomarchy.system.stateFile = ./theme-state.json; }
./hosts/default/configuration.nix
];
};
@@ -251,6 +252,20 @@
({ lib, ... }: {
nomarchy.system.snapper.enable = true;
nomarchy.system.greeter.autoLogin = username;
+ # The installer writes these (keyboard-everywhere
+ # incl. the LUKS prompt) — mirror them or the
+ # initrd/vconsole pins don't match an offline
+ # install's rebuild set.
+ console.useXkbConfig = true;
+ boot.initrd.systemd.enable = true;
+ # nixos-generate-config keys microcode off
+ # enableRedistributableFirmware (now on): the
+ # real machine enables exactly one vendor —
+ # pin both, or the offline install builds
+ # amd-ucode/intel-ucode from source (found the
+ # hard way: the 2700s regression hang).
+ hardware.cpu.amd.updateMicrocode = true;
+ hardware.cpu.intel.updateMicrocode = true;
swapDevices = [{ device = "/swap/swapfile"; }];
boot.resumeDevice = "/dev/disk/by-uuid/00000000-0000-0000-0000-000000000000";
boot.kernelParams = [ "resume_offset=1" ];
@@ -298,6 +313,11 @@
representativeInstall.config.system.build.etc.inputDerivation
representativeInstall.config.system.build.initialRamdisk.inputDerivation
template.homeConfigurations.${username}.activationPackage.inputDerivation
+ # home-files COPIES user-dbus-services instead of
+ # referencing it (same story as the self pin above) —
+ # a custom-username install rebuilds home-files and
+ # needs its direct inputs present.
+ template.homeConfigurations.${username}.config.home-files.inputDerivation
] ++ [
# A theme switch re-renders stylix's base16 templates
# (gtk.css, kvantum, …) on the machine: the renderer and
@@ -328,6 +348,18 @@
# tries to BUILD remarshal, whose test closure pulls
# matplotlib/ffmpeg/x265 (the ~1900-drv cascade).
pkgs.remarshal
+ # system-path/initrd-bin-env are buildEnvs rebuilt per
+ # machine; their shared builder.pl is build-time-only
+ # (never in any runtime closure) — an empty env's
+ # inputDerivation retains it.
+ (pkgs.buildEnv { name = "nomarchy-pin-buildenv"; paths = [ ]; }).inputDerivation
+ # /etc/dbus-1 reassembly wants this setup hook.
+ pkgs.findXMLCatalogs
+ # nixos-generate-config keys microcode off
+ # enableRedistributableFirmware (now on): exactly one
+ # vendor per real machine, so pin both ucode cpios.
+ pkgs.microcode-amd
+ pkgs.microcode-intel
# …and one representative disko script: its tool closure
# (file, which, wrapper hooks, …) isn't otherwise on the
diff --git a/hosts/live.nix b/hosts/live.nix
index d3f0c8d..ac769a0 100644
--- a/hosts/live.nix
+++ b/hosts/live.nix
@@ -16,6 +16,11 @@
# Ghostty silently falls back to the wrong font (seen on the
# Latitude 5410). Normal priority (100) beats the override.
fonts.fontconfig.enable = true;
+
+ # No boot splash on the install medium: the installer ISO boots its
+ # own initrd path (squashfs), and visibility of boot messages is
+ # worth more than polish here. Installed systems get the splash.
+ nomarchy.system.plymouth.enable = false;
xdg.icons.enable = true;
xdg.mime.enable = true;
xdg.autostart.enable = true;
@@ -85,6 +90,12 @@
# ── Live-session desktop tweaks ─────────────────────────────────────
home-manager.users.${username} = {
+ # No idle lock/suspend on the install medium: an offline install
+ # runs 20-30 min unattended, and hypridle would blank the display
+ # then SUSPEND the machine mid-install (it did — the install-hung
+ # regression). Installed systems keep idle management.
+ nomarchy.idle.enable = false;
+
wayland.windowManager.hyprland.settings = {
# QEMU (and some panels) report a tiny "preferred" mode; ask for
# the highest resolution instead.
diff --git a/lib.nix b/lib.nix
index 07e442d..ff9746c 100644
--- a/lib.nix
+++ b/lib.nix
@@ -65,6 +65,9 @@
# (`nomarchy-theme-sync apply` → `home-manager switch`) works
# out of the box — same pinned input as the desktop modules.
{ environment.systemPackages = [ home-manager.packages.${system}.home-manager ]; }
+ # System-side theme consumers (Plymouth splash background)
+ # read the same JSON the desktop does.
+ { nomarchy.system.stateFile = src + "/theme-state.json"; }
]
++ hardwareModules
++ [
diff --git a/modules/home/fuzzel.nix b/modules/home/fuzzel.nix
new file mode 100644
index 0000000..b0f126b
--- /dev/null
+++ b/modules/home/fuzzel.nix
@@ -0,0 +1,82 @@
+# Fuzzel — launcher, dmenu renderer for the menu system, themed from
+# theme-state.json like every other surface. Also home of nomarchy-menu,
+# the dispatcher script the menu roadmap grows out of (first module:
+# power).
+{ config, lib, pkgs, ... }:
+
+let
+ cfg = config.nomarchy;
+ t = cfg.theme;
+ c = t.colors;
+
+ # fuzzel.ini colors are rrggbbaa, no leading #.
+ hexa = color: alpha: "${lib.removePrefix "#" color}${alpha}";
+
+ nomarchy-menu = pkgs.writeShellScriptBin "nomarchy-menu" ''
+ # Nomarchy menu dispatcher — thin presentation layer over
+ # `fuzzel --dmenu`; actions delegate to systemctl/hyprctl/
+ # nomarchy-theme-sync. The icons are nf-md glyphs (any shipped
+ # Nerd Font carries them).
+ case "''${1:-}" in
+ power)
+ choice=$(printf '%s\n' \
+ " Logout" \
+ " Suspend" \
+ " Hibernate" \
+ " Reboot" \
+ " Shutdown" \
+ | fuzzel --dmenu --prompt "power " --lines 5 --width 24) || exit 0
+ case "$choice" in
+ *Logout) hyprctl dispatch exit ;;
+ *Suspend) systemctl suspend ;;
+ *Hibernate) systemctl hibernate ;;
+ *Reboot) systemctl reboot ;;
+ *Shutdown) systemctl poweroff ;;
+ esac ;;
+ *)
+ echo "usage: nomarchy-menu power" >&2
+ exit 64 ;;
+ esac
+ '';
+in
+{
+ config = lib.mkIf cfg.fuzzel.enable {
+ home.packages = [ nomarchy-menu ];
+
+ programs.fuzzel = {
+ enable = true;
+ settings = {
+ main = {
+ # UI font first; the mono Nerd Font follows so menu icons
+ # (nf-md glyphs) resolve — fcft falls back per glyph.
+ font = "${t.fonts.ui}:size=${toString (t.fonts.size + 1)}, ${t.fonts.mono}:size=${toString (t.fonts.size + 1)}";
+ terminal = cfg.terminal;
+ layer = "overlay";
+ lines = 12;
+ width = 40;
+ horizontal-pad = 24;
+ vertical-pad = 16;
+ inner-pad = 8;
+ };
+
+ colors = {
+ background = hexa c.base "f2";
+ text = hexa c.text "ff";
+ prompt = hexa c.subtext "ff";
+ placeholder = hexa c.muted "ff";
+ input = hexa c.text "ff";
+ match = hexa c.accent "ff";
+ selection = hexa c.surface "ff";
+ selection-text = hexa c.text "ff";
+ selection-match = hexa c.accent "ff";
+ border = hexa c.accent "ee";
+ };
+
+ border = {
+ width = t.ui.borderSize;
+ radius = t.ui.rounding;
+ };
+ };
+ };
+ };
+}
diff --git a/modules/home/idle.nix b/modules/home/idle.nix
new file mode 100644
index 0000000..db4b173
--- /dev/null
+++ b/modules/home/idle.nix
@@ -0,0 +1,72 @@
+# hyprlock + hypridle — screen locking and idle management, themed from
+# theme-state.json. One concern, one file: hypridle drives WHEN (idle
+# lock, display off, suspend, lock-before-sleep), hyprlock is the
+# themed lock screen itself (also behind the power menu's Lock entry).
+{ config, lib, ... }:
+
+let
+ cfg = config.nomarchy;
+ t = cfg.theme;
+ c = t.colors;
+ inherit (config.nomarchy.lib) rgb;
+in
+{
+ config = lib.mkIf cfg.idle.enable {
+ programs.hyprlock = {
+ enable = true;
+ settings = {
+ general.hide_cursor = true;
+
+ background = [{
+ monitor = "";
+ color = rgb c.base;
+ }];
+
+ input-field = [{
+ monitor = "";
+ size = "300, 50";
+ outline_thickness = t.ui.borderSize;
+ dots_size = 0.25;
+ outer_color = rgb c.accent;
+ inner_color = rgb c.surface;
+ font_color = rgb c.text;
+ check_color = rgb c.warn;
+ fail_color = rgb c.bad;
+ rounding = t.ui.rounding;
+ placeholder_text = "password…";
+ }];
+
+ label = [{
+ monitor = "";
+ text = "$TIME";
+ color = rgb c.text;
+ font_size = 64;
+ font_family = t.fonts.ui;
+ position = "0, 120";
+ halign = "center";
+ valign = "center";
+ }];
+ };
+ };
+
+ services.hypridle = {
+ enable = true;
+ settings = {
+ general = {
+ lock_cmd = "pidof hyprlock || hyprlock";
+ before_sleep_cmd = "loginctl lock-session";
+ after_sleep_cmd = "hyprctl dispatch dpms on";
+ };
+ listener = [
+ { timeout = 300; on-timeout = "loginctl lock-session"; }
+ {
+ timeout = 600;
+ on-timeout = "hyprctl dispatch dpms off";
+ on-resume = "hyprctl dispatch dpms on";
+ }
+ { timeout = 1800; on-timeout = "systemctl suspend"; }
+ ];
+ };
+ };
+ };
+}
diff --git a/modules/home/swaync.nix b/modules/home/swaync.nix
new file mode 100644
index 0000000..ed2cba9
--- /dev/null
+++ b/modules/home/swaync.nix
@@ -0,0 +1,92 @@
+# swaync — notification daemon + control centre, themed from
+# theme-state.json. Until this shipped, nothing rendered notify-send at
+# all: the theme-switch progress toasts, the CLI's font warnings and the
+# live ISO's welcome message were all invisible.
+{ config, lib, ... }:
+
+let
+ cfg = config.nomarchy;
+ t = cfg.theme;
+ c = t.colors;
+ r = toString t.ui.rounding;
+in
+{
+ config = lib.mkIf cfg.swaync.enable {
+ services.swaync = {
+ enable = true;
+
+ settings = {
+ positionX = "right";
+ positionY = "top";
+ control-center-margin-top = t.ui.gapsOut;
+ control-center-margin-right = t.ui.gapsOut;
+ control-center-width = 420;
+ notification-window-width = 400;
+ notification-icon-size = 48;
+ timeout = 6;
+ timeout-low = 3;
+ timeout-critical = 0; # critical stays until dismissed
+ };
+
+ style = ''
+ /* Palette baked from theme-state.json */
+ @define-color base ${c.base};
+ @define-color surface ${c.surface};
+ @define-color text ${c.text};
+ @define-color subtext ${c.subtext};
+ @define-color accent ${c.accent};
+ @define-color bad ${c.bad};
+
+ .notification {
+ background: alpha(@base, 0.95);
+ border: ${toString t.ui.borderSize}px solid alpha(@accent, 0.4);
+ border-radius: ${r}px;
+ color: @text;
+ }
+
+ .notification-content .summary {
+ color: @text;
+ font-weight: bold;
+ }
+
+ .notification-content .body {
+ color: @subtext;
+ }
+
+ .notification.critical {
+ border-color: @bad;
+ }
+
+ .control-center {
+ background: alpha(@base, 0.95);
+ border: ${toString t.ui.borderSize}px solid alpha(@accent, 0.4);
+ border-radius: ${r}px;
+ color: @text;
+ }
+
+ .control-center .notification-row:focus,
+ .control-center .notification-row:hover {
+ background: alpha(@surface, 0.6);
+ border-radius: ${r}px;
+ }
+
+ .widget-title {
+ color: @text;
+ font-weight: bold;
+ }
+
+ .widget-title > button {
+ background: @surface;
+ color: @text;
+ border: none;
+ border-radius: ${r}px;
+ }
+
+ .widget-title > button:hover {
+ background: @accent;
+ color: @base;
+ }
+ '';
+ };
+ };
+}
diff --git a/modules/nixos/default.nix b/modules/nixos/default.nix
index 4ef0cda..4beaa52 100644
--- a/modules/nixos/default.nix
+++ b/modules/nixos/default.nix
@@ -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 = {
diff --git a/modules/nixos/options.nix b/modules/nixos/options.nix
index 2b93139..9ed548e 100644
--- a/modules/nixos/options.nix
+++ b/modules/nixos/options.nix
@@ -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; };
diff --git a/modules/nixos/plymouth.nix b/modules/nixos/plymouth.nix
new file mode 100644
index 0000000..36b9161
--- /dev/null
+++ b/modules/nixos/plymouth.nix
@@ -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.0–1.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"
+ ];
+ };
+}
diff --git a/modules/nixos/plymouth/bullet.png b/modules/nixos/plymouth/bullet.png
new file mode 100644
index 0000000..62249b3
Binary files /dev/null and b/modules/nixos/plymouth/bullet.png differ
diff --git a/modules/nixos/plymouth/entry.png b/modules/nixos/plymouth/entry.png
new file mode 100644
index 0000000..5c78917
Binary files /dev/null and b/modules/nixos/plymouth/entry.png differ
diff --git a/modules/nixos/plymouth/lock.png b/modules/nixos/plymouth/lock.png
new file mode 100644
index 0000000..3046de1
Binary files /dev/null and b/modules/nixos/plymouth/lock.png differ
diff --git a/modules/nixos/plymouth/logo.png b/modules/nixos/plymouth/logo.png
new file mode 100644
index 0000000..dbcde56
Binary files /dev/null and b/modules/nixos/plymouth/logo.png differ
diff --git a/modules/nixos/plymouth/nomarchy.plymouth b/modules/nixos/plymouth/nomarchy.plymouth
new file mode 100644
index 0000000..4ade91c
--- /dev/null
+++ b/modules/nixos/plymouth/nomarchy.plymouth
@@ -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
diff --git a/modules/nixos/plymouth/nomarchy.script b/modules/nixos/plymouth/nomarchy.script
new file mode 100644
index 0000000..216dac9
--- /dev/null
+++ b/modules/nixos/plymouth/nomarchy.script
@@ -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();
+ }
diff --git a/modules/nixos/plymouth/progress_bar.png b/modules/nixos/plymouth/progress_bar.png
new file mode 100644
index 0000000..dbb9fd7
Binary files /dev/null and b/modules/nixos/plymouth/progress_bar.png differ
diff --git a/modules/nixos/plymouth/progress_box.png b/modules/nixos/plymouth/progress_box.png
new file mode 100644
index 0000000..6a263f2
Binary files /dev/null and b/modules/nixos/plymouth/progress_box.png differ
diff --git a/themes/summer-day/waybar.jsonc b/themes/summer-day/waybar.jsonc
new file mode 100644
index 0000000..a45b8f5
--- /dev/null
+++ b/themes/summer-day/waybar.jsonc
@@ -0,0 +1,92 @@
+{
+ "margin-top": 0,
+ "margin-left": 120,
+ "margin-bottom": 0,
+ "margin-right": 120,
+ "height": 60,
+ "layer": "top",
+ "position": "top",
+ "spacing": 15,
+ "reload_style_on_change": true,
+
+ "modules-left": ["custom/launcher", "clock", "clock#date"],
+ "modules-center": ["hyprland/workspaces"],
+ "modules-right": ["pulseaudio", "network", "battery", "tray", "custom/powermenu"],
+
+ "hyprland/workspaces": {
+ "disable-scroll": true,
+ "all-outputs": true,
+ "on-click": "activate",
+ "on-scroll-up": "hyprctl dispatch workspace e+1",
+ "on-scroll-down": "hyprctl dispatch workspace e-1",
+ "persistent-workspaces": {
+ "1": [], "2": [], "3": [], "4": [], "5": [],
+ "6": [], "7": [], "8": [], "9": [], "10": []
+ }
+ },
+
+ "custom/launcher": {
+ "interval": "once",
+ "format": "",
+ "on-click": "nomarchy-menu",
+ "tooltip-format": "Nomarchy menu"
+ },
+
+ "pulseaudio": {
+ "format": "{icon} {volume}%",
+ "format-bluetooth": "{icon} {volume}%",
+ "format-bluetooth-muted": " muted",
+ "format-muted": " muted",
+ "format-icons": {
+ "headphone": "",
+ "hands-free": "",
+ "headset": "",
+ "phone": "",
+ "portable": "",
+ "car": "",
+ "default": ["", "", ""]
+ },
+ "on-click": "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle",
+ "tooltip-format": "Playing at {volume}%"
+ },
+
+ "network": {
+ "format-wifi": " {signalStrength}%",
+ "format-ethernet": "",
+ "format-disconnected": "",
+ "on-click": "sh -c '$TERMINAL -e nmtui'",
+ "tooltip-format": "{ipaddr} via {gwaddr}"
+ },
+
+ "battery": {
+ "interval": 60,
+ "states": { "warning": 30, "critical": 15 },
+ "max-length": 10,
+ "format": "{icon} {capacity}%",
+ "format-charging": " {capacity}%",
+ "format-plugged": " {capacity}%",
+ "format-full": " 100%",
+ "format-icons": ["", "", "", "", ""],
+ "on-click": "nomarchy-menu power",
+ "tooltip-format": "Battery status"
+ },
+
+ "clock": {
+ "format": " {:%H:%M}",
+ "tooltip": true,
+ "tooltip-format": "{calendar}"
+ },
+
+ "clock#date": {
+ "format": " {:%A, %B %d, %Y}",
+ "tooltip": false
+ },
+
+ "tray": { "spacing": 8 },
+
+ "custom/powermenu": {
+ "format": "",
+ "on-click": "nomarchy-menu power",
+ "tooltip": false
+ }
+}
diff --git a/themes/summer-night/waybar.jsonc b/themes/summer-night/waybar.jsonc
new file mode 100644
index 0000000..e5a1430
--- /dev/null
+++ b/themes/summer-night/waybar.jsonc
@@ -0,0 +1,99 @@
+{
+ "margin-top": 0,
+ "margin-left": 120,
+ "margin-bottom": 0,
+ "margin-right": 120,
+ "height": 61,
+ "layer": "top",
+ "position": "top",
+ "spacing": 15,
+ "reload_style_on_change": true,
+
+ "modules-left": ["custom/nomarchy", "clock", "clock#date"],
+ "modules-center": ["hyprland/workspaces"],
+ "modules-right": ["idle_inhibitor", "pulseaudio", "network", "battery", "tray", "custom/powermenu"],
+
+ "hyprland/workspaces": {
+ "disable-scroll": true,
+ "all-outputs": true,
+ "on-click": "activate",
+ "on-scroll-up": "hyprctl dispatch workspace r+1",
+ "on-scroll-down": "hyprctl dispatch workspace r-1",
+ "persistent-workspaces": {
+ "1": [], "2": [], "3": [], "4": [], "5": [],
+ "6": [], "7": [], "8": [], "9": [], "10": []
+ }
+ },
+
+ "custom/nomarchy": {
+ "interval": "once",
+ "format": "",
+ "on-click": "nomarchy-menu",
+ "tooltip-format": "Nomarchy menu"
+ },
+
+ "idle_inhibitor": {
+ "format": "{icon}",
+ "format-icons": {
+ "activated": " ",
+ "deactivated": " "
+ }
+ },
+
+ "pulseaudio": {
+ "scroll-step": 5,
+ "format": "{icon} {volume}%",
+ "format-bluetooth": "{icon} {volume}%",
+ "format-bluetooth-muted": " {icon}",
+ "format-muted": " muted",
+ "format-icons": {
+ "headphone": "",
+ "hands-free": "",
+ "headset": "",
+ "phone": "",
+ "portable": "",
+ "car": "",
+ "default": ["", "", ""]
+ },
+ "on-click": "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle",
+ "tooltip-format": "{desc} | {volume}%"
+ },
+
+ "network": {
+ "format-wifi": " {essid}",
+ "format-ethernet": "",
+ "format-disconnected": "",
+ "on-click": "sh -c '$TERMINAL -e nmtui'",
+ "tooltip-format": "{ipaddr} via {gwaddr}"
+ },
+
+ "battery": {
+ "interval": 30,
+ "states": { "warning": 25, "critical": 10 },
+ "format": "{icon} {capacity}%",
+ "format-charging": " {capacity}%",
+ "format-plugged": " {capacity}%",
+ "format-icons": ["", "", "", "", ""],
+ "on-click": "nomarchy-menu power",
+ "tooltip-format": "Battery status"
+ },
+
+ "clock": {
+ "format": " {:%H:%M}",
+ "tooltip": true,
+ "tooltip-format": "{calendar}"
+ },
+
+ "clock#date": {
+ "format": " {:%A, %B %d, %Y}",
+ "tooltip": false
+ },
+
+ "tray": { "spacing": 8 },
+
+ "custom/powermenu": {
+ "format": "",
+ "on-click": "nomarchy-menu power",
+ "tooltip": false
+ }
+}