# Hyprland — fully driven by config.nomarchy.theme (theme-state.json). # Gaps, borders and colors are baked from the JSON at eval time; theme # changes arrive via `home-manager switch`. { config, lib, pkgs, ... }: let t = config.nomarchy.theme; c = t.colors; inherit (config.nomarchy.lib) rgb rgba; # swayosd's `--input-volume mute-toggle` draws the OSD but never flips the # source mute (verified on hardware: wpctl toggles the state, swayosd's # input-mute path is a no-op — unlike its working output-mute path). Do the # real toggle with wpctl, then re-render the same icon + volume-bar OSD the # native input path would, reading the resulting state back from wpctl: # --custom-progress is the mic level (0.0-1.0), --custom-icon the # muted/active glyph. Pure bash builtins for parsing so the script needs no # PATH beyond the two pinned tools. micMute = pkgs.writeShellScript "nomarchy-mic-mute" '' ${pkgs.wireplumber}/bin/wpctl set-mute @DEFAULT_SOURCE@ toggle out=$(${pkgs.wireplumber}/bin/wpctl get-volume @DEFAULT_SOURCE@) set -- $out # "Volume: 0.85 [MUTED]" -> $2 is the level case "$out" in *MUTED*) icon=microphone-sensitivity-muted-symbolic ;; *) icon=microphone-sensitivity-high-symbolic ;; esac ${pkgs.swayosd}/bin/swayosd-client --custom-icon "$icon" --custom-progress "$2" ''; # SUPER+1..9 / SUPER+SHIFT+1..9 workspace binds, generated. workspaceBinds = builtins.concatLists (builtins.genList (i: let ws = toString (i + 1); in [ "$mod, ${ws}, workspace, ${ws}" "$mod SHIFT, ${ws}, movetoworkspace, ${ws}" ]) 9); # The keyboard binds — single source shared with the SUPER+? cheatsheet # (rofi.nix renders the same list). Edit them in ./keybinds.nix. keybinds = import ./keybinds.nix; mkBind = b: "${b.mods}, ${b.key}, ${b.action}"; # A nomarchy.monitors entry -> a Hyprland `monitor` rule. Unset optional # fields are omitted; `resolution = "disable"` collapses to the short form. monitorRule = m: if m.resolution == "disable" then "${m.name}, disable" else lib.concatStringsSep ", " ( [ m.name m.resolution (toString m.position) (toString m.scale) ] ++ lib.optionals (m.transform != null) [ "transform" (toString m.transform) ] ++ lib.optionals (m.mirror != null) [ "mirror" m.mirror ] ++ lib.optionals (m.bitdepth != null) [ "bitdepth" (toString m.bitdepth) ] ++ lib.optionals (m.vrr != null) [ "vrr" (toString m.vrr) ] ++ lib.optional (m.extra != "") m.extra ); in { wayland.windowManager.hyprland = lib.mkIf config.nomarchy.hyprland.enable { enable = true; # The binary and portal come from the NixOS module # (programs.hyprland.enable); HM only manages configuration. package = null; portalPackage = null; # HM defaults stateVersion >= 26.05 to the new Lua config and would # render these hyprlang-style settings as broken `hl.$mod(...)` Lua # (Hyprland then boots into emergency mode with no binds). configType = "hyprlang"; settings = { "$mod" = "SUPER"; "$terminal" = config.nomarchy.terminal; # `,preferred,auto,1` is the wildcard fallback for any output not in # nomarchy.monitors; the generated rules are appended and win by name, # and Hyprland applies them on hotplug. mkDefault so a downstream # `monitor = [...]` replaces the lot (the live ISO uses mkForce too). monitor = lib.mkDefault ([ ",preferred,auto,1" ] ++ map monitorRule config.nomarchy.monitors); # exec-once is a list at normal priority, so downstream additions # CONCATENATE (your autostart runs alongside ours) rather than # replace. Same for the bind* lists below. exec-once = [ # nixpkgs' swww is awww (renamed fork); the binary is awww-daemon. "awww-daemon" # Paint the wallpaper as soon as the session is up (waits for # the daemon internally). "nomarchy-theme-sync wallpaper" ]; # ── Theme-driven look ────────────────────────────────────────── # These flow from theme-state.json and stay at NORMAL priority: # change them with `nomarchy-theme-sync set ui.` (the intended # path), or lib.mkForce in home.nix to hardcode against the theme. # The non-theme knobs around them are lib.mkDefault — override # those with a plain home.nix assignment. See docs/OVERRIDES.md. general = { gaps_in = t.ui.gapsIn; gaps_out = t.ui.gapsOut; border_size = t.ui.borderSize; # Solid borders from theme.border (per-theme, palette-resolved) — # matches the legacy identity themes, which never used a gradient. "col.active_border" = rgb t.border.active; "col.inactive_border" = rgb t.border.inactive; layout = lib.mkDefault "dwindle"; resize_on_border = lib.mkDefault true; }; decoration = { rounding = t.ui.rounding; active_opacity = t.ui.activeOpacity; inactive_opacity = t.ui.inactiveOpacity; blur = { enabled = t.ui.blur; size = lib.mkDefault 8; passes = lib.mkDefault 2; popups = lib.mkDefault true; }; shadow = { enabled = t.ui.shadow; range = lib.mkDefault 20; render_power = lib.mkDefault 3; color = rgba c.mantle "aa"; }; }; # ── Behaviour (mkDefault → plain home.nix assignment overrides) ── animations = { enabled = lib.mkDefault true; bezier = lib.mkDefault [ "smooth, 0.25, 0.1, 0.25, 1.0" "snappy, 0.6, 0.0, 0.1, 1.0" ]; animation = lib.mkDefault [ "windows, 1, 4, snappy, popin 85%" "border, 1, 8, smooth" "fade, 1, 5, smooth" "workspaces, 1, 4, snappy, slidefade 15%" ]; }; input = { # kb_layout/variant come from nomarchy.keyboard.* — set that # option (the installer writes it; it also drives tty/LUKS). kb_layout = config.nomarchy.keyboard.layout; kb_variant = config.nomarchy.keyboard.variant; follow_mouse = lib.mkDefault 1; touchpad.natural_scroll = lib.mkDefault true; }; # Per-device keyboard layouts (nomarchy.keyboard.devices): each # overrides the session kb_layout for one named keyboard — e.g. an # external board that's physically a different layout than the laptop's # built-in one. Hyprland applies them whenever the device connects. device = lib.mapAttrsToList (name: d: { inherit name; kb_layout = d.layout; kb_variant = d.variant; }) config.nomarchy.keyboard.devices; # dwindle:pseudotile was removed in Hyprland 0.55 (the `pseudo` # dispatcher still exists); setting it aborts config parsing. dwindle.preserve_split = lib.mkDefault true; # Hyprland 0.51+ replaced gestures:workspace_swipe with the # gesture keyword — without this, touchpads have NO gestures. gesture = lib.mkDefault [ "3, horizontal, workspace" "4, pinch, fullscreen" ]; misc = { disable_hyprland_logo = lib.mkDefault true; disable_splash_rendering = lib.mkDefault true; force_default_wallpaper = lib.mkDefault 0; }; # Rendered from ./keybinds.nix (the cheatsheet reads the same list), # plus the generated per-workspace binds. Like exec-once above this # stays a normal-priority list, so a downstream `bind = [...]` # concatenates rather than replaces. bind = map mkBind keybinds.binds ++ workspaceBinds; # Media keys via swayosd-client: it performs the action AND shows # the on-screen display (the nomarchy.osd module). e = repeat, # l = also on a locked screen — the standard flags for hardware keys. bindel = [ ", XF86AudioRaiseVolume, exec, swayosd-client --output-volume raise" ", XF86AudioLowerVolume, exec, swayosd-client --output-volume lower" ", XF86MonBrightnessUp, exec, swayosd-client --brightness raise" ", XF86MonBrightnessDown, exec, swayosd-client --brightness lower" ]; bindl = [ ", XF86AudioMute, exec, swayosd-client --output-volume mute-toggle" # swayosd's input mute-toggle is broken on 0.2.1 (see micMute above). ", XF86AudioMicMute, exec, ${micMute}" ", XF86AudioPlay, exec, playerctl play-pause" ", XF86AudioPause, exec, playerctl play-pause" ", XF86AudioNext, exec, playerctl next" ", XF86AudioPrev, exec, playerctl previous" ]; bindm = [ "$mod, mouse:272, movewindow" "$mod, mouse:273, resizewindow" ]; }; }; # nwg-displays: interactive monitor arranger (applies live via hyprctl and # writes a config file). A helper to find values for nomarchy.monitors — # the declarative config stays the source of truth (we don't source its # output). Gated on its toggle; pointless without the Hyprland session. home.packages = lib.optionals (config.nomarchy.hyprland.enable && config.nomarchy.displays.enable) [ pkgs.nwg-displays ]; }