fix(docking): make clamshell hotplug safe and discoverable
All checks were successful
Check / eval (push) Successful in 3m32s
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:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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"; }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}";
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user