fix(autoswitch): WorkspacesChanged output-set diff; unit paths; deploy symlinks
- niri 26.04 has no hotplug event: OutputConfigChanged is only an ACK (niri-ipc). Watch WorkspacesChanged, diff sorted output set, apply on change. Proven live: eDP-1 off/on -> 3 output-set transitions, each matched + applied + reloaded. - unit: %h/.local/bin/desk-autoswitch.sh, quoted Environment, drop User/Group (user unit); add desk-autoswitch.sh bin symlink - desk.sh: silence validate/load-config/notify stdout - deploy.sh: deploy repo symlinks (find -type l), so .local/bin links land - docs: event semantics corrected (displays diagram, log)
This commit is contained in:
@@ -112,8 +112,8 @@ widget_order = [
|
|||||||
[lockscreen_widgets.widget."lockscreen-login-box@eDP-1"]
|
[lockscreen_widgets.widget."lockscreen-login-box@eDP-1"]
|
||||||
box_height = 196.0
|
box_height = 196.0
|
||||||
box_width = 810.0
|
box_width = 810.0
|
||||||
cx = 960.0
|
cx = 960.000244140625
|
||||||
cy = 852.5
|
cy = 852.500244140625
|
||||||
output = "eDP-1"
|
output = "eDP-1"
|
||||||
placement_height = 1080.0
|
placement_height = 1080.0
|
||||||
placement_width = 1920.0
|
placement_width = 1920.0
|
||||||
|
|||||||
@@ -5,15 +5,13 @@ ConditionHost=blankpad
|
|||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
User=%u
|
ExecStart=%h/.local/bin/desk-autoswitch.sh watch
|
||||||
Group=%u
|
|
||||||
ExecStart=/usr/bin/env bash %h/.omp/scripts/desk-autoswitch.sh watch
|
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=5
|
RestartSec=5
|
||||||
StandardOutput=journal
|
StandardOutput=journal
|
||||||
StandardError=journal
|
StandardError=journal
|
||||||
Environment=DESK_NIRI=/usr/bin/niri
|
Environment=DESK_NIRI=/usr/bin/niri
|
||||||
Environment=DESK_NOTIFY=noctalia msg notification-show
|
Environment="DESK_NOTIFY=noctalia msg notification-show"
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=default.target
|
WantedBy=default.target
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
../../../../scripts/desk-autoswitch.sh
|
||||||
+1
-1
@@ -15,7 +15,7 @@ timestamp: 2026-08-28
|
|||||||
|
|
||||||
Branch `blankpad-de-redesign` (worktree `wt/de-redesign-20261009`). Full sweep + redesign of the niri/noctalia/Handy stack on blankpad.
|
Branch `blankpad-de-redesign` (worktree `wt/de-redesign-20261009`). Full sweep + redesign of the niri/noctalia/Handy stack on blankpad.
|
||||||
|
|
||||||
* **Desk profiles.** `cfg/display.kdl` retired to a symlink; per-desk profiles `desk-1-home.kdl` / `desk-2-portable.kdl` declare monitors by **EDID long name** (`// requires:` lines) — connector names reshuffle between docks, identity travels. `scripts/desk.sh` = list/current/switch/cycle/match/capture; apply = relink → `niri validate` (rollback on failure) → `load-config-file` → noctalia notification. `scripts/desk-autoswitch.sh` + `blankpad-desk-autoswitch.service` watch the `niri msg --json event-stream` for `OutputsChanged` and hot-apply the matching profile (debounced 1.5 s). PM1601 EDID still uncaptured (panel disconnected) — connector-keyed placeholder, `desk.sh capture` pending.
|
* **Desk profiles.** `cfg/display.kdl` retired to a symlink; per-desk profiles `desk-1-home.kdl` / `desk-2-portable.kdl` declare monitors by **EDID long name** (`// requires:` lines) — connector names reshuffle between docks, identity travels. `scripts/desk.sh` = list/current/switch/cycle/match/capture; apply = relink → `niri validate` (rollback on failure) → `load-config-file` → noctalia notification. `scripts/desk-autoswitch.sh` + `blankpad-desk-autoswitch.service` watch `niri msg --json event-stream`: niri 26.04 has **no hotplug event** (`OutputConfigChanged` is only an ACK that an IPC/config output change was Applied, per niri-ipc); the daemon diffs the sorted output set carried by `WorkspacesChanged` and hot-applies the matching profile (debounced 1.5 s). Proven live with an eDP-1 off/on cycle: `[DP-2 eDP-1] → [DP-2] → [DP-2 eDP-1]`, apply + reload each time. PM1601 EDID still uncaptured (panel disconnected) — connector-keyed placeholder, `desk.sh capture` pending.
|
||||||
* **Keybinds.** `swap-window-left/right` on Mod+CTRL+H/L; move-column demoted to Mod+CTRL+Alt+H/L; 7 column-width presets 12.5–87.5 %; Print trio (full / gui / screen) via noctalia screenshot IPC; launcher Mod+D; Alt+Tab → noctalia window-switcher; Mod+V → clipboard panel (id verified live); Mod+Shift+D → desk cycle; Mod+Shift+Space → `handy --toggle-post-process`; lid-close → `noctalia msg session lock-and-suspend` (pairs with logind `HandleLidSwitch=ignore`). Cheat sheet now GENERATED: `scripts/gen-keybinds.sh` → `omp-docs/tools/niri/keybindings.md` (119 bindings).
|
* **Keybinds.** `swap-window-left/right` on Mod+CTRL+H/L; move-column demoted to Mod+CTRL+Alt+H/L; 7 column-width presets 12.5–87.5 %; Print trio (full / gui / screen) via noctalia screenshot IPC; launcher Mod+D; Alt+Tab → noctalia window-switcher; Mod+V → clipboard panel (id verified live); Mod+Shift+D → desk cycle; Mod+Shift+Space → `handy --toggle-post-process`; lid-close → `noctalia msg session lock-and-suspend` (pairs with logind `HandleLidSwitch=ignore`). Cheat sheet now GENERATED: `scripts/gen-keybinds.sh` → `omp-docs/tools/niri/keybindings.md` (119 bindings).
|
||||||
* **Noctalia.** `config.toml` + `settings.toml` (lives in `~/.local/state/`, not `~/.config/`) tracked; `taskbar` widget added to bar center, applied live via `noctalia msg config-reload`. Docs corrected to the v5 `noctalia msg` API.
|
* **Noctalia.** `config.toml` + `settings.toml` (lives in `~/.local/state/`, not `~/.config/`) tracked; `taskbar` widget added to bar center, applied live via `noctalia msg config-reload`. Docs corrected to the v5 `noctalia msg` API.
|
||||||
* **Handy.** Self-autostart killed (`~/.config/autostart/Handy.desktop` → `.AGENTTRASH`; `autostart_enabled=false`); niri owns autostart (`spawn-at-startup "handy" "--start-hidden"`). Fleet post-processing wired into the live store: provider `custom` → `http://192.168.0.143:7771/v1`, model `frank-secondary`, prompt `default_improve_transcriptions`. **Open breaker:** frank-secondary answers in `reasoning_content` and leaves `content` empty; per-request cure `chat_template_kwargs.enable_thinking=false` verified, but Handy cannot send it and the server is shared with fleet compaction — dedicated no-think port proposed. 0.9.6→0.9.8 upgrade is operator-owned (`yay -Syu handy-bin`).
|
* **Handy.** Self-autostart killed (`~/.config/autostart/Handy.desktop` → `.AGENTTRASH`; `autostart_enabled=false`); niri owns autostart (`spawn-at-startup "handy" "--start-hidden"`). Fleet post-processing wired into the live store: provider `custom` → `http://192.168.0.143:7771/v1`, model `frank-secondary`, prompt `default_improve_transcriptions`. **Open breaker:** frank-secondary answers in `reasoning_content` and leaves `content` empty; per-request cure `chat_template_kwargs.enable_thinking=false` verified, but Handy cannot send it and the server is shared with fleet compaction — dedicated no-think port proposed. 0.9.6→0.9.8 upgrade is operator-owned (`yay -Syu handy-bin`).
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ the ultrawide wins when present and the laptop panel is the fallback.
|
|||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
graph LR
|
graph LR
|
||||||
P[plug / unplug] --> E[niri event-stream<br/>OutputsChanged]
|
P[plug / unplug] --> E[niri event-stream<br/>WorkspacesChanged → output-set diff]
|
||||||
E --> M[desk-autoswitch.sh<br/>debounced matcher]
|
E --> M[desk-autoswitch.sh<br/>debounced matcher]
|
||||||
M --> R{profile requires<br/>subset of connected?}
|
M --> R{profile requires<br/>subset of connected?}
|
||||||
R -->|best match| A[desk.sh apply:<br/>validate - relink - reload - notify]
|
R -->|best match| A[desk.sh apply:<br/>validate - relink - reload - notify]
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# scripts/desk-autoswitch.sh — hotplug daemon: apply the matching desk profile
|
# scripts/desk-autoswitch.sh — hotplug daemon: apply the matching desk profile
|
||||||
# whenever niri reports an OutputsChanged event.
|
# when the SET OF OUTPUTS changes.
|
||||||
|
#
|
||||||
|
# niri has no dedicated hotplug event (verified on 26.04):
|
||||||
|
# - WorkspacesChanged carries every workspace's `output`, so the sorted
|
||||||
|
# unique set of outputs changes exactly on connect/disconnect.
|
||||||
|
# - OutputConfigChanged is only an ACK that an IPC/config output change was
|
||||||
|
# Applied (niri-ipc), NOT a hotplug signal.
|
||||||
#
|
#
|
||||||
# Usage: desk-autoswitch.sh watch
|
# Usage: desk-autoswitch.sh watch
|
||||||
#
|
#
|
||||||
@@ -8,7 +14,7 @@
|
|||||||
# DESK_AUTO_HOSTNAME only run on this hostname (default: blankpad)
|
# DESK_AUTO_HOSTNAME only run on this hostname (default: blankpad)
|
||||||
# DESK_SCRIPT path to desk.sh (default ~/.local/bin/desk.sh)
|
# DESK_SCRIPT path to desk.sh (default ~/.local/bin/desk.sh)
|
||||||
# DESK_NIRI niri binary (default niri)
|
# DESK_NIRI niri binary (default niri)
|
||||||
# DESK_DEBOUNCE_S debounce window in seconds (default 1.5)
|
# DESK_DEBOUNCE_S min seconds between applies (default 1.5)
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -17,8 +23,10 @@ DESK="${DESK_SCRIPT:-$HOME/.local/bin/desk.sh}"
|
|||||||
NIRI="${DESK_NIRI:-niri}"
|
NIRI="${DESK_NIRI:-niri}"
|
||||||
DEBOUNCE="${DESK_DEBOUNCE_S:-1.5}"
|
DEBOUNCE="${DESK_DEBOUNCE_S:-1.5}"
|
||||||
|
|
||||||
usage() {
|
usage() { sed -n '2,18p' "$0" | sed 's/^# \{0,1\}//'; }
|
||||||
sed -n '2,12p' "$0" | sed 's/^# \{0,1\}//'
|
|
||||||
|
outputs_from_event() { # $1 = one JSON event line
|
||||||
|
jq -r '[.WorkspacesChanged.workspaces[]?.output // empty] | unique | join(" ")' <<<"$1" 2>/dev/null || true
|
||||||
}
|
}
|
||||||
|
|
||||||
watch_mode() {
|
watch_mode() {
|
||||||
@@ -26,16 +34,20 @@ watch_mode() {
|
|||||||
echo "desk-autoswitch: initial match"
|
echo "desk-autoswitch: initial match"
|
||||||
"$DESK" match --apply || true
|
"$DESK" match --apply || true
|
||||||
|
|
||||||
local last=0 now
|
local seen="" last_seen="" last=0 now
|
||||||
# One JSON event per line; reconnect forever (niri restart drops the stream).
|
# One JSON event per line; reconnect forever (niri restart drops the stream).
|
||||||
while "$NIRI" msg --json event-stream 2>/dev/null | while IFS= read -r line; do
|
while "$NIRI" msg --json event-stream 2>/dev/null | while IFS= read -r line; do
|
||||||
case "$line" in
|
case "$line" in
|
||||||
*OutputsChanged*)
|
*WorkspacesChanged*)
|
||||||
|
seen="$(outputs_from_event "$line")"
|
||||||
|
[ -n "$seen" ] || continue
|
||||||
|
[ "$seen" = "$last_seen" ] && continue
|
||||||
|
last_seen="$seen"
|
||||||
now="$(date +%s.%N)"
|
now="$(date +%s.%N)"
|
||||||
# swallow bursts inside the debounce window
|
# swallow bursts inside the debounce window
|
||||||
if awk -v l="$last" -v n="$now" -v d="$DEBOUNCE" 'BEGIN { exit !(n - l >= d) }'; then
|
if awk -v l="$last" -v n="$now" -v d="$DEBOUNCE" 'BEGIN { exit !(n - l >= d) }'; then
|
||||||
last="$now"
|
last="$now"
|
||||||
echo "desk-autoswitch: OutputsChanged"
|
echo "desk-autoswitch: outputs changed [$seen]"
|
||||||
"$DESK" match --apply || true
|
"$DESK" match --apply || true
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
|
|||||||
+1
-1
@@ -61,7 +61,7 @@ requires() {
|
|||||||
|
|
||||||
notify() {
|
notify() {
|
||||||
# shellcheck disable=SC2086 # word splitting of the prefix is intended
|
# shellcheck disable=SC2086 # word splitting of the prefix is intended
|
||||||
$NOTIFY "Desk" "$1" 2>/dev/null || true
|
$NOTIFY "Desk" "$1" >/dev/null 2>/dev/null || true
|
||||||
}
|
}
|
||||||
|
|
||||||
# Apply a profile: relink, validate live config, roll back on failure,
|
# Apply a profile: relink, validate live config, roll back on failure,
|
||||||
|
|||||||
Reference in New Issue
Block a user