fix(docking): make clamshell hotplug safe and discoverable
All checks were successful
Check / eval (push) Successful in 3m32s

Real dock QA exposed pointer-bound menus, an absent default rescue watcher, the unsafe zero-output panel-off path, missing keyboard/audio hotplug coverage, and unpainted new outputs.

Route Rofi to the focused output; replace panel disable with safe workspace migration; always run and reconnect the display watcher and repaint hotplugged outputs; expose a default per-device keyboard picker; handle audio card availability transitions.

Verified: V1 — nix flake check --no-build; checks.docking-ux real Home Manager artifact build; option-docs/template-sot; Nix parse, py_compile, and git diff --check. V2 unavailable: no readable KVM. V3 pending in agent/HARDWARE-QUEUE.md.
This commit is contained in:
2026-07-13 11:53:01 +01:00
parent 16846786e2
commit 7ebfab4bd4
15 changed files with 357 additions and 171 deletions

View File

@@ -46,28 +46,47 @@ let
"Output ''${2:-$1}" 2>/dev/null || true
}
# Return dock-class sinks whose current port is usable. PipeWire often
# creates the HDMI/DP node before the cable is plugged and later flips its
# card/port availability, so presence alone is not a hotplug signal.
dock_sinks() {
$PACTL --format=json list sinks \
| $JQ -r --arg re '^(${dockSinkRe})$' '
.[]
| select(.name | test($re))
| select(
((.ports // []) | length) == 0
or any(.ports[]?; (.availability // "unknown") != "not available")
)
| "\(.name)\t\(.description)"'
}
adopt_first_dock() {
dock_sinks | while IFS="$TAB" read -r name desc; do
[ -n "$name" ] || continue
switch_to "$name" "$desc"
break
done
}
# Startup sweep (login/relogin while already docked): adopt a dock
# sink only when the current default is NOT one a service restart
# must not override a manual in-dock pick.
if ! is_dock "$($PACTL get-default-sink)"; then
$PACTL --format=json list sinks \
| $JQ -r '.[] | "\(.name)\t\(.description)"' \
| while IFS="$TAB" read -r name desc; do
if is_dock "$name"; then switch_to "$name" "$desc"; break; fi
done
adopt_first_dock
fi
# Event loop: `pactl subscribe` prints "Event 'new' on sink #NN".
# Event loop: handle both a genuinely new sink and the common case where
# an existing HDMI/DP card changes profile/port availability. Deliberately
# ignore ordinary sink "change" events (volume/mute) and server-default
# changes, so a manual speaker pick while docked remains sticky.
LC_ALL=C $PACTL subscribe | while read -r _ ev _ kind id; do
[ "$ev" = "'new'" ] && [ "$kind" = "sink" ] || continue
id=''${id#\#}
case "$id" in *[!0-9]*|"") continue ;; esac
sleep 0.5 # let the node's description/routes settle
line=$($PACTL --format=json list sinks | $JQ -r --argjson i "$id" \
'.[] | select(.index == $i) | "\(.name)\t\(.description)"')
[ -n "$line" ] || continue
name=''${line%%"$TAB"*}; desc=''${line#*"$TAB"}
if is_dock "$name"; then switch_to "$name" "$desc"; fi
case "$ev:$kind" in
"'new':sink"|"'new':card"|"'change':card") ;;
*) continue ;;
esac
sleep 0.75 # let PipeWire finish the profile/route transition
adopt_first_dock
done
'';
in

View File

@@ -215,13 +215,21 @@ ${lib.concatStringsSep "\n" (lib.mapAttrsToList
case "$v" in true|True) return 0 ;; *) return 1 ;; esac
}
outputs() { hyprctl monitors all -j 2>/dev/null | jq -r '.[].name' | sort; }
paint_outputs() {
# awww does not inherit the current image when Hyprland adds an output.
# Let the new output settle, then re-apply the current wallpaper to all
# outputs. Backgrounded so profile matching never waits on animation.
sleep 0.5
${sync} --quiet wallpaper >/dev/null 2>&1 || true
}
# Blackout rescue the session must never end up with ZERO active
# outputs. Hyprland does NOT re-enable a soft-disabled monitor when
# the last active one disconnects (VM-verified, tools/
# monitor-fallback.nix): menu "Laptop screen off" + sudden undock
# left a dead session. On every monitorremoved, if nothing is
# active, re-enable whatever is disabled (preferred/auto the
# monitor-fallback.nix): the former panel-off menu action (and still-valid
# named profiles that disable eDP) + sudden undock left a dead session.
# On every monitorremoved, if nothing is active, re-enable whatever is
# disabled (preferred/auto the
# declared rule returns at the next reload/relogin). Independent of
# the profile auto-switch and its settings gate: this is a safety
# invariant, not a layout preference.
@@ -237,17 +245,23 @@ ${lib.concatStringsSep "\n" (lib.mapAttrsToList
# ~0.5s of the last output going while it persisted, so shrinking
# the window is the priority; the keyword is idempotent and the
# retries cover a first attempt racing the removal teardown.
tries=0
while [ "$(active_count)" = 0 ] && [ "$tries" -lt 4 ]; do
hyprctl monitors all -j 2>/dev/null | jq -r '.[] | select(.disabled) | .name' \
| while read -r m; do
[ -n "$m" ] || continue
hyprctl keyword monitor "$m,preferred,auto,auto" >/dev/null 2>&1
done
tries=0 rescued=
# monitorremoved can arrive just before the departing output disappears
# from `hyprctl monitors`; keep a short bounded watch instead of deciding
# from that racy first snapshot. The first check is still immediate.
while [ "$tries" -lt 8 ]; do
if [ "$(active_count)" = 0 ]; then
rescued=1
hyprctl monitors all -j 2>/dev/null | jq -r '.[] | select(.disabled) | .name' \
| while read -r m; do
[ -n "$m" ] || continue
hyprctl keyword monitor "$m,preferred,auto,auto" >/dev/null 2>&1
done
fi
tries=$((tries+1))
sleep 1
sleep 0.25
done
if [ "$tries" -gt 0 ] && [ "$(active_count)" != 0 ]; then
if [ -n "$rescued" ] && [ "$(active_count)" != 0 ]; then
notify-send "Display" "Screen re-enabled the active output disappeared." 2>/dev/null || true
fi
}
@@ -257,6 +271,7 @@ ${lib.concatStringsSep "\n" (lib.mapAttrsToList
[ "$cur" = "$1" ] && return
[ -n "$cur" ] || return
auto_on || return
command -v nomarchy-display-profile >/dev/null 2>&1 || return
target=$(nomarchy-display-profile match $cur) || target="base"
@@ -271,20 +286,27 @@ ${lib.concatStringsSep "\n" (lib.mapAttrsToList
}
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
monitorremoved*)
rescue_blackout
res=$(check_monitors "$prev")
[ -n "$res" ] && prev="$res"
;;
monitoradded*)
res=$(check_monitors "$prev")
[ -n "$res" ] && prev="$res"
;;
esac
# Listen to Hyprland's IPC socket for instant hotplug reaction. Reconnect
# after a compositor reload/socket hiccup instead of silently losing the
# safety listener for the rest of the session.
socket="$XDG_RUNTIME_DIR/hypr/$HYPRLAND_INSTANCE_SIGNATURE/.socket2.sock"
while :; do
${pkgs.socat}/bin/socat -U - UNIX-CONNECT:"$socket" | while read -r line; do
case "$line" in
monitorremoved*)
rescue_blackout
res=$(check_monitors "$prev")
[ -n "$res" ] && prev="$res"
;;
monitoradded*)
paint_outputs &
res=$(check_monitors "$prev")
[ -n "$res" ] && prev="$res"
;;
esac
done
sleep 1
done
'';
@@ -299,7 +321,67 @@ ${lib.concatStringsSep "\n" (lib.mapAttrsToList
(lib.splitString "," config.nomarchy.keyboard.layout)
++ config.nomarchy.keyboard.layouts
);
kbAutoSwitch = config.nomarchy.keyboard.layouts != [ ];
# Per-device keyboard helper shared by the hotplug watcher and the manual
# System Keyboard menu. The configured candidates are listed first, then
# every XKB layout known to the system, so the feature works on a default
# install instead of silently disappearing until home.nix is edited.
keyboardTool = pkgs.writeShellScriptBin "nomarchy-keyboard-layout" ''
set -u
sync=${sync}
layouts() {
printf '%s\n' ${lib.concatMapStringsSep " " lib.escapeShellArg pickerLayouts}
${pkgs.gawk}/bin/awk '
/^! layout/ { in_layouts=1; next }
/^!/ && in_layouts { exit }
in_layouts && NF { print $1 }
' ${pkgs.xkeyboard_config}/share/X11/xkb/rules/base.lst
}
saved_map() { "$sync" get settings.keyboard.devices 2>/dev/null || echo '{}'; }
saved_for() { saved_map | jq -r --arg k "$1" '.[$k] // empty'; }
apply_runtime() { hyprctl keyword "device[$1]:kb_layout" "$2" >/dev/null 2>&1; }
case "''${1:-}" in
layouts)
layouts | ${pkgs.gawk}/bin/awk 'NF && !seen[$0]++' ;;
devices)
hyprctl devices -j 2>/dev/null \
| jq -r '.keyboards[] | "\(.name)\t\(.active_keymap // \"unknown\")"' ;;
saved)
[ -n "''${2:-}" ] || exit 64
saved_for "$2" ;;
restore)
[ -n "''${2:-}" ] || exit 64
layout=$(saved_for "$2")
[ -n "$layout" ] && apply_runtime "$2" "$layout" ;;
apply)
[ -n "''${2:-}" ] && [ -n "''${3:-}" ] || exit 64
layouts | ${pkgs.gnugrep}/bin/grep -Fxq "$3" \
|| { notify-send "Keyboard" "Unknown XKB layout: $3"; exit 64; }
if apply_runtime "$2" "$3"; then
map=$(saved_map | jq -c --arg k "$2" --arg v "$3" '. + {($k): $v}') || exit 1
if "$sync" --quiet set settings.keyboard.devices "$map" --no-switch; then
notify-send "Keyboard" "$2 $3"
else
notify-send "Keyboard" "$2 $3 for this session, but the choice could not be saved."
exit 1
fi
else
notify-send "Keyboard" "Could not apply $3 to $2."
exit 1
fi ;;
forget)
[ -n "''${2:-}" ] || exit 64
map=$(saved_map | jq -c --arg k "$2" 'del(.[$k])') || exit 1
"$sync" --quiet set settings.keyboard.devices "$map" --no-switch || exit 1
apply_runtime "$2" ${lib.escapeShellArg config.nomarchy.keyboard.layout} || true
notify-send "Keyboard" "$2 now follows the session layout." ;;
*)
echo "usage: nomarchy-keyboard-layout [layouts|devices|saved <device>|restore <device>|apply <device> <layout>|forget <device>]" >&2
exit 64 ;;
esac
'';
# On a keyboard that connects after login (not declared in
# keyboard.devices, not already remembered), ask for a layout and persist it
@@ -320,34 +402,18 @@ ${lib.concatStringsSep "\n" (lib.mapAttrsToList
# 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; }
tool=${keyboardTool}/bin/nomarchy-keyboard-layout
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"
"$tool" restore "$kb" || true
done
# Watch for keyboards that connect later.
@@ -358,12 +424,13 @@ ${lib.concatStringsSep "\n" (lib.mapAttrsToList
cur="$cur$kb "
case "$prev" in *" $kb "*) continue ;; esac
is_declared "$kb" && continue
s=$(saved_for "$kb")
s=$("$tool" saved "$kb")
if [ -n "$s" ]; then
apply "$kb" "$s"
"$tool" restore "$kb" || true
else
choice=$(printf '%s\n' $layouts | rofi -dmenu -p "Layout · $kb")
[ -n "$choice" ] && { remember "$kb" "$choice"; apply "$kb" "$choice"; }
choice=$("$tool" layouts \
| rofi -monitor -1 -dmenu -i -p "Keyboard layout · $kb (e.g. us, gb, pt, br)")
[ -n "$choice" ] && "$tool" apply "$kb" "$choice"
fi
done
prev="$cur"
@@ -420,8 +487,10 @@ in
# 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";
++ [
"${keyboardWatch}/bin/nomarchy-keyboard-watch"
"${displayProfileWatch}/bin/nomarchy-display-profile-watch"
];
# ── Theme-driven look ──────────────────────────────────────────
# These flow from theme-state.json and stay at NORMAL priority:
@@ -636,11 +705,10 @@ in
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 ];
# Runtime hardware watchers/helpers stay on PATH for manual recovery and
# debugging. The display-profile switcher itself remains gated so the
# menu does not advertise an empty Profiles submenu.
++ lib.optionals config.nomarchy.hyprland.enable
([ keyboardTool keyboardWatch displayProfileWatch ]
++ lib.optional (profiles != { }) displayProfileTool);
}

View File

@@ -49,6 +49,7 @@
{ mods = "$mod CTRL"; key = "E"; action = "exec, nomarchy-menu emoji"; desc = "Emoji picker"; }
{ mods = "$mod CTRL"; key = "N"; action = "exec, nomarchy-menu network"; desc = "Network (networkmanager_dmenu)"; }
{ mods = "$mod CTRL"; key = "B"; action = "exec, nomarchy-menu bluetooth"; desc = "Bluetooth"; }
{ mods = "$mod CTRL"; key = "K"; action = "exec, nomarchy-menu keyboard"; desc = "Keyboard layout"; }
{ mods = "$mod CTRL"; key = "S"; action = "exec, nomarchy-menu capture"; desc = "Screenshot / capture"; }
{ mods = "$mod CTRL"; key = "P"; action = "exec, nomarchy-menu colorpicker"; desc = "Color picker ( clipboard)"; }
{ mods = "$mod CTRL"; key = "A"; action = "exec, nomarchy-menu ask"; desc = "Ask Claude"; }

View File

@@ -128,13 +128,13 @@ in
default = [ ];
example = [ "de" "fr" ];
description = ''
Extra candidate layouts offered by the interactive new-keyboard
picker. When non-empty, a small watcher runs in the session: when a
keyboard connects that isn't covered by nomarchy.keyboard.devices and
hasn't been chosen before, it pops a rofi picker (these layouts plus
the primary nomarchy.keyboard.layout), applies the choice to that
keyboard only (a per-device kb_layout, so the built-in board is left
alone), and remembers it in the git-tracked in-flake state
Extra candidate layouts shown first by the interactive new-keyboard
picker (all installed XKB layouts remain searchable). A small watcher
always runs in the Hyprland session: when a keyboard connects that
isn't covered by nomarchy.keyboard.devices and hasn't been chosen
before, it pops a rofi picker, applies the choice to that keyboard only
(a per-device kb_layout, so the built-in board is left alone), and
remembers it in the git-tracked in-flake state
(settings.keyboard.devices, not ~/.local/state) re-applied
automatically on later reconnects and across reboots. Each remembered
choice graduates into nomarchy.keyboard.devices on the next rebuild

View File

@@ -236,6 +236,7 @@ let
# one arrow everywhere. Always matched EXACTLY (" Back") so it can't
# collide with clipboard or filename content.
BACK=" Back"
TAB=$(printf '\t')
back() { printf '%s\n' "$BACK"; }
# dmenu mode is case-sensitive by default (dmenu compatibility), even
@@ -515,7 +516,12 @@ ${themeRows}
sinks=$(${pkgs.pulseaudio}/bin/pactl --format=json list sinks 2>/dev/null || true)
cursink=$(${pkgs.pulseaudio}/bin/pactl get-default-sink 2>/dev/null || true)
dockname=$(printf '%s' "$sinks" | jq -r --arg cur "$cursink" --arg re '${dockSinkRe}' \
'[.[] | select((.name | test("^(" + $re + ")$")) and .name != $cur)][0].name // empty')
'[.[]
| select((.name | test("^(" + $re + ")$")) and .name != $cur)
| select(
((.ports // []) | length) == 0
or any(.ports[]?; (.availability // "unknown") != "not available")
)][0].name // empty')
dockdesc=
[ -n "$dockname" ] && dockdesc=$(printf '%s' "$sinks" \
| jq -r --arg n "$dockname" '[.[] | select(.name == $n)][0].description // empty')
@@ -534,6 +540,33 @@ ${themeRows}
*Input*) exec rofi-pulse-select source ;;
esac ;;
keyboard)
command -v nomarchy-keyboard-layout >/dev/null 2>&1 \
|| { notify-send "Keyboard" "Per-device layout helper is unavailable."; exit 0; }
devices=$(nomarchy-keyboard-layout devices)
[ -n "$devices" ] \
|| { notify-send "Keyboard" "No keyboards reported by Hyprland."; exit 0; }
picked=$( {
printf '%s\n' "$devices" | while IFS="$TAB" read -r dev active; do
printf '%s · %s\n' "$dev" "$active"
done
back
} | rofi_menu -p "Keyboard") || exit 0
[ "$picked" = "$BACK" ] && exec "$0" system
device=''${picked%% ·*}
saved=$(nomarchy-keyboard-layout saved "$device")
layout=$( {
[ -n "$saved" ] && printf 'Use session default · forget %s\n' "$saved"
nomarchy-keyboard-layout layouts
back
} | rofi_menu -p "Layout · $device (now: ''${saved:-session default})") || exit 0
case "$layout" in
"$BACK") exec "$0" keyboard ;;
"Use session default"*) exec nomarchy-keyboard-layout forget "$device" ;;
"") exit 0 ;;
*) exec nomarchy-keyboard-layout apply "$device" "$layout" ;;
esac ;;
display)
# Per-output resolution picker. Applies the chosen mode INSTANTLY via
# `hyprctl keyword monitor` keeping the output's current position and
@@ -566,11 +599,12 @@ ${themeRows}
if [ -n "$profs" ] || [ "$nActive" -gt 1 ] || [ -n "$offmon" ]; then
name=$( {
[ -n "$profs" ] && printf 'Profiles \n'
# Docked rows self-gated: never offer to disable the only
# active display; the re-enable row appears whenever an
# output is soft-off (so an undocked laptop can recover too).
# Dock mode deliberately keeps the internal panel logically
# active as the unplug fallback. Soft-disabling it can leave
# Hyprland with zero outputs and kill the whole session before a
# user-space rescue runs. Move its workspaces and focus instead.
[ "$nActive" -gt 1 ] && [ -n "$internal" ] && [ -n "$external" ] \
&& printf 'Laptop screen off · everything %s\n' "$external"
&& printf 'Dock mode · everything %s (safe unplug)\n' "$external"
[ -n "$offmon" ] && printf 'Screen on · %s\n' "$offmon"
[ "$nActive" -gt 1 ] && printf 'Move workspace · next monitor\n'
[ "$nActive" -eq 2 ] && printf 'Swap workspaces · %s %s\n' \
@@ -582,16 +616,16 @@ ${themeRows}
[ "$name" = "$BACK" ] && exec "$0" system
[ "$name" = "Profiles " ] && exec "$0" display-profile
case "$name" in
"Laptop screen off"*)
# Live-only on purpose: Hyprland moves the panel's
# workspaces to the remaining output on disable; persisting
# a disable could black-screen an undocked boot, so the
# declared rule returns at the next relogin/switch.
if hyprctl keyword monitor "$internal,disable" >/dev/null 2>&1; then
notify-send "Display" "Laptop screen off workspaces moved to $external (this session; turn it back on from this menu)."
else
notify-send "Display" "Could not disable $internal."
fi
"Dock mode"*)
moved=0
while read -r ws; do
[ -n "$ws" ] || continue
hyprctl dispatch moveworkspacetomonitor "$ws" "$external" >/dev/null 2>&1 \
&& moved=$((moved+1))
done < <(hyprctl workspaces -j | jq -r --arg m "$internal" \
'.[] | select(.monitor == $m and .id > 0) | .id')
hyprctl dispatch focusmonitor "$external" >/dev/null 2>&1
notify-send "Display" "Dock mode $moved workspace(s) moved to $external; laptop panel kept as the unplug fallback."
exit 0 ;;
"Screen on"*)
# preferred/auto/auto is good enough live; the declared
@@ -1106,6 +1140,8 @@ ${themeRows}
[ -e "''${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/pulse/native" ] \
&& row "Audio" audio-volume-high
row "Display" video-display
command -v nomarchy-keyboard-layout >/dev/null 2>&1 \
&& row "Keyboard${menuHint "keyboard"}" preferences-desktop-keyboard
command -v system-config-printer >/dev/null 2>&1 && row "Printers" printer
row "Do Not Disturb${menuHint "dnd"}" notification-disabled
if [ "$(nomarchy-theme-sync get settings.autoTimezone 2>/dev/null)" = true ]
@@ -1145,6 +1181,7 @@ ${themeRows}
*Bluetooth*) exec "$0" bluetooth ;;
*Audio*) exec "$0" audio ;;
*Display*) exec "$0" display ;;
*Keyboard*) exec "$0" keyboard ;;
*Printers*) exec "$0" printers ;;
*"Do Not Disturb"*) exec "$0" dnd ;;
*"Auto timezone"*) exec "$0" autotimezone ;;
@@ -1262,7 +1299,7 @@ ${themeRows}
esac ;;
*)
echo "usage: nomarchy-menu [lookfeel|tools|system|power|power-profile|powermgmt|batterylimit|theme|clipboard|calc|files|emoji|web|network|bluetooth|audio|display|display-profile|printers|capture|colorpicker|keybinds|ask|dnd|nightlight|autotimezone|autocommit|vpn|snapshot|doctor|firmware|fingerprint|controlcenter|rollback|whatchanged]" >&2
echo "usage: nomarchy-menu [lookfeel|tools|system|power|power-profile|powermgmt|batterylimit|theme|clipboard|calc|files|emoji|web|network|bluetooth|audio|display|display-profile|keyboard|printers|capture|colorpicker|keybinds|ask|dnd|nightlight|autotimezone|autocommit|vpn|snapshot|doctor|firmware|fingerprint|controlcenter|rollback|whatchanged]" >&2
exit 64 ;;
esac
'';
@@ -1477,6 +1514,10 @@ in
extraConfig = {
modi = "drun,run";
# Rofi defaults to the output containing the mouse pointer (-5).
# After docking with the lid shut that pointer can remain on the hidden
# laptop workspace while keyboard focus is on the external display.
monitor = -1; # Hyprland's currently focused output
show-icons = true; # app icons via the theme's icon set
icon-theme = t.iconTheme; # resolved in theme.nix (Papirus-*/per-theme)
drun-display-format = "{name}";

View File

@@ -11,8 +11,9 @@
# history is generation history.
#
# The one runtime exception is the wallpaper (swww is imperative by
# nature): applied at session start, after every switch (hook below),
# and cycled instantly with `nomarchy-theme-sync bg next`.
# nature): applied at session start, after every switch (hook below), and
# after output hotplug (hyprland.nix); cycled instantly with
# `nomarchy-theme-sync bg next`.
{ config, lib, pkgs, ... }:
let