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:
@@ -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}";
|
||||
|
||||
Reference in New Issue
Block a user