Some checks failed
Check / eval (push) Has been cancelled
The default desktop showed a red Hyprland config-error banner on every boot, occluding the Waybar. Root cause: Hyprland 0.53 rewrote window rules — the `windowrulev2` keyword is a hard error and the old rule-first order (`float, class:^…$`) no longer parses, so the float rules were not applying either. Confirmed against the Hyprland 0.55.4 source (`handleWindowrule`): each comma field is now `<keyword> <value>`, so effects carry a value and matchers take a `match:` prefix. Migrated the 7-rule block to `float 1, match:class ^…$` form. Verified: V0 nix flake check --no-build; built the generated hyprland.conf; V2 theme-shot re-render — banner gone, themed Waybar renders. Journal + MEMORY gotcha added. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
559 lines
26 KiB
Nix
559 lines
26 KiB
Nix
# 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;
|
|
|
|
# nomarchy-theme-sync — the in-flake state writer the keyboard watcher uses
|
|
# to remember per-device layouts (same path night-light's toggle takes).
|
|
sync = lib.getExe config.nomarchy.package;
|
|
|
|
# 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"
|
|
'';
|
|
|
|
# Launch-or-focus (nomarchy.launchOrFocus, item 17): focus the app's
|
|
# window if one exists (case-insensitive class match), launch it
|
|
# otherwise. Self-gates: an uninstalled command notifies instead of
|
|
# failing silently, so a bind survives the app being removed from the
|
|
# suite. rofi.nix renders the same entries into the SUPER+? cheatsheet.
|
|
focusOrLaunch = pkgs.writeShellScriptBin "nomarchy-focus-or-launch" ''
|
|
class="''${1:-}"; shift || true
|
|
[ -n "$class" ] && [ $# -gt 0 ] || {
|
|
echo "usage: nomarchy-focus-or-launch <class> <command...>" >&2; exit 64
|
|
}
|
|
if hyprctl clients -j 2>/dev/null \
|
|
| ${pkgs.jq}/bin/jq -e --arg c "$class" \
|
|
'any(.[]; (.class | ascii_downcase) == ($c | ascii_downcase))' >/dev/null; then
|
|
exec hyprctl dispatch focuswindow "class:(?i)^$class\$"
|
|
fi
|
|
command -v "$1" >/dev/null 2>&1 \
|
|
|| { notify-send "Launch" "$1 is not installed."; exit 0; }
|
|
exec "$@"
|
|
'';
|
|
lofEntries = map
|
|
(e: e // {
|
|
command = if e.command == "" then lib.toLower e.class else e.command;
|
|
})
|
|
config.nomarchy.launchOrFocus;
|
|
lofBinds = map
|
|
(e: "${e.mods}, ${e.key}, exec, ${focusOrLaunch}/bin/nomarchy-focus-or-launch ${e.class} ${e.command}")
|
|
lofEntries;
|
|
|
|
# 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}";
|
|
|
|
# Monitor-rule composition (rendering + the display-profile/resolution
|
|
# overlays) lives in ./monitor-rules.nix — pure, so
|
|
# checks.display-profiles unit-tests the overlay semantics directly.
|
|
# The layering story (profiles replace whole-by-name, menu resolution
|
|
# picks apply field-level, disable-guard) is documented there.
|
|
monitorLib = import ./monitor-rules.nix { inherit lib; };
|
|
monitorRule = monitorLib.rule;
|
|
|
|
# The night-light pattern: nomarchy-display-profile applies rules live
|
|
# via hyprctl and writes settings.displayProfile with --no-switch;
|
|
# resolve bakes the same choice here at the next rebuild.
|
|
# Profiles normalized to { monitors; workspaces; } — the option also
|
|
# accepts the original bare-list-of-monitors shape.
|
|
profiles = lib.mapAttrs
|
|
(_: p: if builtins.isList p then { monitors = p; workspaces = { }; } else p)
|
|
config.nomarchy.displayProfiles;
|
|
resolvedMonitors = monitorLib.resolve {
|
|
base = config.nomarchy.monitors;
|
|
profiles = lib.mapAttrs (_: p: p.monitors) profiles;
|
|
active = config.nomarchy.settings.displayProfile or "";
|
|
resOverrides = config.nomarchy.settings.monitors;
|
|
};
|
|
|
|
# The active profile's workspace→output pins, baked as `workspace`
|
|
# rules below (same rebuild path as the monitor overlay; the applier
|
|
# covers the instant half). Same junk-state guard as resolve.
|
|
activeProfile =
|
|
let a = config.nomarchy.settings.displayProfile or "";
|
|
in profiles.${if builtins.isString a then a else ""} or { workspaces = { }; };
|
|
profileWorkspaceRules =
|
|
lib.mapAttrsToList monitorLib.workspaceRule activeProfile.workspaces;
|
|
|
|
# Profile applier — on PATH only when profiles are declared (the menu
|
|
# row self-gates on it). apply: the profile's rules live via hyprctl +
|
|
# the in-flake state write; base: clear the state, then hyprctl reload
|
|
# re-applies the generated config's rules.
|
|
displayProfileTool = pkgs.writeShellScriptBin "nomarchy-display-profile" ''
|
|
case "''${1:-}" in
|
|
list)
|
|
${lib.concatMapStringsSep "\n" (n: " echo ${lib.escapeShellArg n}") (lib.attrNames profiles)} ;;
|
|
active)
|
|
cur=$(${sync} get settings.displayProfile 2>/dev/null) || cur=
|
|
echo "''${cur:-none}" ;;
|
|
apply)
|
|
case "''${2:-}" in
|
|
${lib.concatStringsSep "\n" (lib.mapAttrsToList
|
|
(name: p:
|
|
" ${lib.escapeShellArg name})\n"
|
|
+ lib.concatMapStringsSep "\n"
|
|
(m: " hyprctl keyword monitor ${lib.escapeShellArg (monitorRule m)} >/dev/null 2>&1")
|
|
p.monitors
|
|
# Workspace pins: the keyword sets the session rule, the dispatch
|
|
# moves an already-open workspace over. Pins from a previously
|
|
# applied profile linger until reload/rebuild (hyprctl can only
|
|
# add) — harmless, a rule naming an absent output is inert.
|
|
+ lib.concatStrings (lib.mapAttrsToList
|
|
(ws: out:
|
|
"\n hyprctl keyword workspace ${lib.escapeShellArg (monitorLib.workspaceRule ws out)} >/dev/null 2>&1"
|
|
+ "\n hyprctl dispatch moveworkspacetomonitor ${lib.escapeShellArg ws} ${lib.escapeShellArg out} >/dev/null 2>&1")
|
|
p.workspaces)
|
|
+ " ;;")
|
|
profiles)}
|
|
*) echo "unknown profile '$2' — try: nomarchy-display-profile list" >&2; exit 64 ;;
|
|
esac
|
|
${sync} --quiet set settings.displayProfile "$2" --no-switch
|
|
notify-send "Display profile" "Applied: $2" ;;
|
|
base)
|
|
${sync} --quiet set settings.displayProfile "" --no-switch
|
|
hyprctl reload >/dev/null 2>&1
|
|
notify-send "Display profile" "Base layout restored." ;;
|
|
match)
|
|
# Which profile fits these connected outputs? (args = output names,
|
|
# e.g. `match $(hyprctl monitors all -j | jq -r '.[].name')`.)
|
|
# An exact set match wins; else the largest profile whose named
|
|
# outputs are all connected; any tie prints nothing (exit 1) — the
|
|
# auto-switch watcher must never flap between ambiguous layouts.
|
|
shift
|
|
[ "$#" -gt 0 ] || exit 1
|
|
con=" $* " ncon=$#
|
|
exact= exactdup= best= bestn=0 dup=
|
|
while IFS=: read -r p names; do
|
|
[ -n "$p" ] || continue
|
|
n=0 ok=1
|
|
for o in $names; do
|
|
case "$con" in *" $o "*) n=$((n+1)) ;; *) ok=; break ;; esac
|
|
done
|
|
[ -n "$ok" ] || continue
|
|
if [ "$n" -eq "$ncon" ]; then
|
|
if [ -n "$exact" ]; then exactdup=1; else exact=$p; fi
|
|
fi
|
|
if [ "$n" -gt "$bestn" ]; then best=$p bestn=$n dup=
|
|
elif [ "$n" -eq "$bestn" ] && [ "$n" -gt 0 ]; then dup=1
|
|
fi
|
|
done < <(printf '%s\n' ${lib.concatMapStringsSep " " lib.escapeShellArg
|
|
(lib.mapAttrsToList
|
|
(name: p: "${name}:${lib.concatMapStringsSep " " (m: m.name) p.monitors}")
|
|
profiles)})
|
|
if [ -n "$exact" ] && [ -z "$exactdup" ]; then echo "$exact"; exit 0; fi
|
|
if [ -n "$best" ] && [ -z "$dup" ]; then echo "$best"; exit 0; fi
|
|
exit 1 ;;
|
|
*)
|
|
echo "usage: nomarchy-display-profile [list|active|apply <name>|base|match <output>...]" >&2
|
|
exit 64 ;;
|
|
esac
|
|
'';
|
|
|
|
# Hotplug auto-switch (opt-in via the menu's Auto-switch row →
|
|
# settings.displayProfileAuto, read LIVE on every output change so the
|
|
# toggle is instant, no rebuild). Polls like the keyboard watcher —
|
|
# exec-once, not a systemd unit (graphical-session-bound units raced
|
|
# Hyprland's IPC on relogin; see the waybar note below). Only reacts to
|
|
# CHANGES after session start: the baked config already encodes the
|
|
# last choice, and login must not fight a deliberate manual pick.
|
|
# `match` picks deterministically (exact set, else unambiguous largest
|
|
# subset); applying via the tool persists the concrete profile, so the
|
|
# next rebuild bakes what auto chose.
|
|
displayProfileWatch = pkgs.writeShellScriptBin "nomarchy-display-profile-watch" ''
|
|
set -u
|
|
auto_on() {
|
|
v=$(${sync} get settings.displayProfileAuto 2>/dev/null) || v=false
|
|
case "$v" in true|True) return 0 ;; *) return 1 ;; esac
|
|
}
|
|
outputs() { hyprctl monitors all -j 2>/dev/null | jq -r '.[].name' | sort; }
|
|
|
|
check_monitors() {
|
|
cur=$(outputs)
|
|
[ "$cur" = "$1" ] && return
|
|
[ -n "$cur" ] || return
|
|
auto_on || return
|
|
|
|
target=$(nomarchy-display-profile match $cur) || target="base"
|
|
|
|
if [ "$target" = "base" ]; then
|
|
[ "$(nomarchy-display-profile active)" = "none" ] && return
|
|
nomarchy-display-profile base
|
|
else
|
|
[ "$target" = "$(nomarchy-display-profile active)" ] && return
|
|
nomarchy-display-profile apply "$target"
|
|
fi
|
|
echo "$cur"
|
|
}
|
|
|
|
prev=$(outputs)
|
|
|
|
# Listen to Hyprland's IPC socket for instant hotplug reaction
|
|
${pkgs.socat}/bin/socat -U - UNIX-CONNECT:$XDG_RUNTIME_DIR/hypr/$HYPRLAND_INSTANCE_SIGNATURE/.socket2.sock | while read -r line; do
|
|
case "$line" in
|
|
monitoradded*|monitorremoved*)
|
|
res=$(check_monitors "$prev")
|
|
[ -n "$res" ] && prev="$res"
|
|
;;
|
|
esac
|
|
done
|
|
'';
|
|
|
|
# Candidate layouts offered by the new-keyboard picker: the session
|
|
# layout(s) (nomarchy.keyboard.layout, comma-split for a multi-layout
|
|
# session) plus the extra nomarchy.keyboard.layouts pool. The pool is
|
|
# deliberately NOT merged into input.kb_layout — those candidates are for
|
|
# *external* keyboards and get applied per-device by the watcher's apply().
|
|
# Loading them onto the session keyboard is what let a global switch flip
|
|
# the built-in board to the wrong layout.
|
|
pickerLayouts = lib.unique (
|
|
(lib.splitString "," config.nomarchy.keyboard.layout)
|
|
++ config.nomarchy.keyboard.layouts
|
|
);
|
|
kbAutoSwitch = config.nomarchy.keyboard.layouts != [ ];
|
|
|
|
# On a keyboard that connects after login (not declared in
|
|
# keyboard.devices, not already remembered), ask for a layout and persist it
|
|
# per-device, re-applying silently on later reconnects. Stateful runtime
|
|
# piece — the complement to the declarative keyboard.devices. Reliable
|
|
# primitives only: poll hyprctl devices, apply with a per-device
|
|
# `device[<name>]:kb_layout` keyword — the runtime twin of the declarative
|
|
# device blocks, so it isolates that one keyboard and never disturbs the
|
|
# built-in board (switchxkblayout flipped the *shared* layout, which leaked
|
|
# onto the laptop and stuck after unplug).
|
|
#
|
|
# The remembered choices live in the git-tracked in-flake state
|
|
# (settings.keyboard.devices), NOT ~/.local/state — read live from the
|
|
# working tree (so a pick this session is honoured at once) and written
|
|
# instantly with `--no-switch` (no rebuild). A later rebuild graduates them
|
|
# into nomarchy.keyboard.devices (generated device{} blocks), after which the
|
|
# watcher treats them as declared and steps back. NOTE: needs an on-hardware
|
|
# test (hotplug isn't verifiable in CI).
|
|
keyboardWatch = pkgs.writeShellScriptBin "nomarchy-keyboard-watch" ''
|
|
set -u
|
|
layouts="${lib.concatStringsSep " " pickerLayouts}"
|
|
declared="${lib.concatStringsSep " " (builtins.attrNames config.nomarchy.keyboard.devices)}"
|
|
sync=${sync}
|
|
|
|
apply() { hyprctl keyword "device[$1]:kb_layout" "$2" >/dev/null 2>&1; }
|
|
keyboards() { hyprctl devices -j | jq -r '.keyboards[].name'; }
|
|
is_declared() { case " $declared " in *" $1 "*) return 0 ;; *) return 1 ;; esac; }
|
|
|
|
# The remembered map (device-name -> layout) from the LIVE in-flake state;
|
|
# an absent key (sparse state) reads as an empty object.
|
|
saved_map() { "$sync" get settings.keyboard.devices 2>/dev/null || echo '{}'; }
|
|
saved_for() { saved_map | jq -r --arg k "$1" '.[$k] // empty'; }
|
|
|
|
# Persist a pick INSTANTLY: merge it into the map and write the whole object
|
|
# back at the dot-free parent path, so a device name containing a dot can't
|
|
# corrupt the dotted set-path. --no-switch = write only, no rebuild.
|
|
remember() {
|
|
map=$(saved_map | jq -c --arg k "$1" --arg v "$2" '. + {($k): $v}') || return
|
|
"$sync" --quiet set settings.keyboard.devices "$map" --no-switch
|
|
}
|
|
|
|
# Startup: re-apply remembered layouts, never prompt — so the built-in
|
|
# keyboard just stays on the session default.
|
|
prev=" "
|
|
for kb in $(keyboards); do
|
|
prev="$prev$kb "
|
|
is_declared "$kb" && continue
|
|
s=$(saved_for "$kb"); [ -n "$s" ] && apply "$kb" "$s"
|
|
done
|
|
|
|
# Watch for keyboards that connect later.
|
|
while :; do
|
|
sleep 3
|
|
cur=" "
|
|
for kb in $(keyboards); do
|
|
cur="$cur$kb "
|
|
case "$prev" in *" $kb "*) continue ;; esac
|
|
is_declared "$kb" && continue
|
|
s=$(saved_for "$kb")
|
|
if [ -n "$s" ]; then
|
|
apply "$kb" "$s"
|
|
else
|
|
choice=$(printf '%s\n' $layouts | rofi -dmenu -p "Layout · $kb")
|
|
[ -n "$choice" ] && { remember "$kb" "$choice"; apply "$kb" "$choice"; }
|
|
fi
|
|
done
|
|
prev="$cur"
|
|
done
|
|
'';
|
|
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 resolvedMonitors);
|
|
|
|
# The active display profile's workspace→output pins (a list, so
|
|
# downstream `workspace = [...]` rules concatenate).
|
|
workspace = profileWorkspaceRules;
|
|
|
|
# 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"
|
|
# Polkit authentication agent — without one, EVERY pkexec/polkit
|
|
# prompt in the session fails silently (btrfs-assistant-launcher
|
|
# was the discovery case). hyprpolkitagent is Hyprland's own Qt
|
|
# agent, so the prompt is Stylix-themed like every other Qt
|
|
# surface. Its binary lives in libexec (not bin), hence the path.
|
|
"${pkgs.hyprpolkitagent}/libexec/hyprpolkitagent"
|
|
# Waybar is launched here rather than as a systemd user service: bound
|
|
# to graphical-session.target the unit raced Hyprland's IPC on relogin
|
|
# and landed in `failed`; exec-once only fires once the compositor is
|
|
# up. Via the nomarchy-waybar supervisor (waybar.nix): a crash — or
|
|
# the clean pkill a theme switch now does — respawns the bar instead
|
|
# of orphaning the session bar-less.
|
|
] ++ lib.optional config.nomarchy.waybar.enable "nomarchy-waybar"
|
|
++ lib.optional kbAutoSwitch "${keyboardWatch}/bin/nomarchy-keyboard-watch"
|
|
++ lib.optional (profiles != { }) "${displayProfileWatch}/bin/nomarchy-display-profile-watch";
|
|
|
|
# ── Theme-driven look ──────────────────────────────────────────
|
|
# These flow from theme-state.json and stay at NORMAL priority:
|
|
# change them with `nomarchy-theme-sync set ui.<key>` (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). Only the
|
|
# session layout(s) land here; the keyboard.layouts pool is applied
|
|
# per-device to external boards, never onto the session keyboard.
|
|
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;
|
|
# The backdrop when no wallpaper is painted (awww still starting,
|
|
# a slow first paint, or a wallpaper-less setup): the theme's base
|
|
# instead of compositor black — on light themes the black flash
|
|
# reads as a glitch (item 28c, seen in the capture harness).
|
|
background_color = rgb c.base;
|
|
};
|
|
|
|
# Window rules — float + center small config/utility dialogs that
|
|
# tile awkwardly. Normal-priority list like `bind`/`exec-once` below,
|
|
# so a downstream `windowrule = [...]` concatenates rather than
|
|
# replaces. Syntax is the post-0.53 rule rewrite (hyprlang legacy
|
|
# parser, `handleWindowrule`): each comma field is `<keyword>
|
|
# <value>`, so effects carry a value (`float 1`, `center 1`, `size
|
|
# W H`) and matchers take a `match:` prefix (`match:class ^…$`).
|
|
# The old rule-first form (`float, class:^…$`) and the
|
|
# `windowrulev2` key are both hard errors now (red config-error
|
|
# banner on every session start — seen in the capture harness).
|
|
# Class regexes use `(?i)` and tolerate the XWayland
|
|
# `.…-wrapped` binary-name form.
|
|
#
|
|
# Conservative set on purpose (item 41): only windows the distro
|
|
# actually ships, each an unambiguous dialog. Deliberately NOT
|
|
# floated yet — needs on-hardware class verification: the polkit auth
|
|
# prompt (hyprpolkitagent exposes no discoverable class) and GTK
|
|
# file-chooser portals. A dead rule is harmless, but guessing a class
|
|
# is worse than leaving it for a verified pass.
|
|
windowrule = [
|
|
# Audio mixer (item 35): right-click the Waybar volume module.
|
|
"float 1, match:class ^(com\\.saivert\\.pwvucontrol|org\\.pulseaudio\\.pavucontrol|pavucontrol)$"
|
|
"center 1, match:class ^(com\\.saivert\\.pwvucontrol|org\\.pulseaudio\\.pavucontrol|pavucontrol)$"
|
|
|
|
# Bluetooth manager (blueman) + CUPS printer admin.
|
|
"float 1, match:class (?i)^(\\.?blueman-(manager|adapters)(-wrapped)?|\\.?system-config-printer(-wrapped)?)$"
|
|
"center 1, match:class (?i)^(\\.?blueman-(manager|adapters)(-wrapped)?|\\.?system-config-printer(-wrapped)?)$"
|
|
|
|
# Calendar popup (item 42): the Waybar clock's on-click runs
|
|
# nomarchy-calendar → calcurse in a ghostty window tagged with a
|
|
# distinct --class, so it floats centered instead of tiling.
|
|
"float 1, match:class ^(com\\.nomarchy\\.calendar)$"
|
|
"size 60% 65%, match:class ^(com\\.nomarchy\\.calendar)$"
|
|
"center 1, match:class ^(com\\.nomarchy\\.calendar)$"
|
|
];
|
|
|
|
# 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. The layout-cycle bind only
|
|
# exists when there is something to cycle (same comma condition
|
|
# the Waybar language indicator gates on).
|
|
bind = map mkBind (keybinds.binds
|
|
++ lib.optionals (lib.hasInfix "," config.nomarchy.keyboard.layout)
|
|
keybinds.multiLayoutBinds)
|
|
++ workspaceBinds
|
|
++ lofBinds;
|
|
|
|
# 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"
|
|
];
|
|
};
|
|
};
|
|
|
|
# Runtime-remembered per-device layouts (settings.keyboard.devices, written
|
|
# by the watcher into the in-flake state) graduate into the declarative
|
|
# keyboard.devices: each becomes a generated Hyprland device{} block —
|
|
# reproducible and applied natively on hotplug — after which the watcher sees
|
|
# it as declared and steps back. mkDefault so a hand-written
|
|
# keyboard.devices.<name> in home.nix still wins for the same keyboard.
|
|
nomarchy.keyboard.devices = lib.mapAttrs
|
|
(_name: layout: { layout = lib.mkDefault layout; })
|
|
config.nomarchy.settings.keyboard.devices;
|
|
|
|
# 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 ]
|
|
# The new-keyboard watcher on PATH (when enabled) so it's discoverable and
|
|
# runnable by name for debugging — Hyprland still exec-once's it by store path.
|
|
++ lib.optional (config.nomarchy.hyprland.enable && kbAutoSwitch) keyboardWatch
|
|
# The display-profile switcher (the menu's Profiles row gates on it)
|
|
# + its hotplug watcher, on PATH for debugging like the keyboard one.
|
|
++ lib.optionals (config.nomarchy.hyprland.enable && profiles != { })
|
|
[ displayProfileTool displayProfileWatch ];
|
|
}
|