Give a reconnected display the arrangement it had
hypr/monitors.lua applies the stored per-output entries when the compositor reads its config, and never again. A monitor plugged in an hour later got the compositor's automatic placement instead of the position, scale and rotation this machine was told to use, and the only way back was to open Settings and apply it again. Docking should not cost you your desk. Deliberately not a confirmed transaction. applyLayout arms a fifteen second countdown because it is about to show you something you might not be able to undo; this restores a layout you already confirmed, on hardware you already had, and a countdown would be asking you to re-approve your own decision every time you sat down. It refuses rather than guesses when the stored mode is one the connected panel does not offer -- DP-1 on one dock is not DP-1 on another -- and when the surviving layout would name no primary. Both land on the compositor's automatic placement plus a toast that opens the Displays page, which is recoverable; silence would not be. That toast needed a new open-settings verb in StatusEvents, whose page name goes through ShellState's existing allow-list. The decision is split from the action as plannedRestore so it can be tested without driving a real compositor, and the harness sets topology and stored arrangement in one call because a real query landing between two would replace the fixture. Both fixtures travel base64: qs ipc call splits a JSON array of several objects into one argument per object, so a two-monitor fixture was arriving as an extra argument.
This commit is contained in:
@@ -374,7 +374,67 @@ run ipc call displays-test forget >/dev/null
|
||||
sleep 0.6
|
||||
[[ "$(status | jq -r .overridden)" == "false" ]] || fail 'forget did not clear the stored display setting'
|
||||
|
||||
# ── Restoring an arrangement when a display comes back ───────────────────────
|
||||
#
|
||||
# hypr/monitors.lua applies stored per-output entries when the compositor reads
|
||||
# its config, and never again. A monitor plugged in an hour later used to get
|
||||
# the compositor's automatic placement instead of the arrangement this machine
|
||||
# was told to use, and the only way back was to open Settings and apply it
|
||||
# again. Docking should not cost you your desk.
|
||||
#
|
||||
# Driven through plannedRestore, which is the decision with no side effects --
|
||||
# applying a real layout here would drive the live compositor.
|
||||
#
|
||||
# Three cases, and the last two matter most: a wrong answer is a screen you
|
||||
# cannot see well enough to fix, so it refuses rather than guesses.
|
||||
|
||||
# Single-line and space-free: the IPC call splits its arguments on whitespace,
|
||||
# so a pretty-printed fixture arrives as several arguments instead of one.
|
||||
two_screens='[{"name":"DP-2","width":1920,"height":1080,"refreshRate":60.0,"scale":1.0,"transform":0,"x":0,"y":0,"availableModes":["[email protected]","[email protected]"]},{"name":"HDMI-A-1","width":1920,"height":1080,"refreshRate":60.0,"scale":1.0,"transform":0,"x":1920,"y":0,"availableModes":["[email protected]"]}]'
|
||||
|
||||
# base64 because `qs ipc call` splits a JSON array of several objects into one
|
||||
# argument per object, which makes a two-monitor fixture look like an extra
|
||||
# argument and the call is refused for arity.
|
||||
b64() { printf '%s' "$1" | base64 -w0; }
|
||||
plan() { run ipc call displays-test restorePlan "$(b64 "$1")" "$(b64 "$2")"; }
|
||||
|
||||
# Nothing stored: the compositor's placement stands.
|
||||
result="$(plan "$two_screens" '{}')"
|
||||
jq -e '.action == "none"' <<<"$result" >/dev/null \
|
||||
|| fail "with nothing stored, Panama wanted to change the arrangement: $result"
|
||||
|
||||
# Stored and already correct: still nothing to do, so a reconnect does not
|
||||
# push a layout the compositor is already showing.
|
||||
result="$(plan "$two_screens" '{"DP-2":{"mode":"[email protected]","scale":1,"transform":0,"x":0,"y":0,"primary":true}}')"
|
||||
jq -e '.action == "none"' <<<"$result" >/dev/null \
|
||||
|| fail "an arrangement that already matches was re-applied anyway: $result"
|
||||
|
||||
# Stored and different: restore it, with the stored scale and position.
|
||||
result="$(plan "$two_screens" '{"DP-2":{"mode":"[email protected]","scale":1,"transform":0,"x":0,"y":0,"primary":true}}')"
|
||||
jq -e '.action == "apply" and (.layout[] | select(.name == "DP-2") | .mode == "[email protected]")' \
|
||||
<<<"$result" >/dev/null \
|
||||
|| fail "a stored arrangement was not restored when the display reconnected: $result"
|
||||
|
||||
# A mode this panel does not offer. Same connector, different hardware -- DP-1
|
||||
# on one dock is not DP-1 on another. It must refuse rather than ask the
|
||||
# compositor for a mode that does not exist.
|
||||
result="$(plan "$two_screens" '{"DP-2":{"mode":"[email protected]","scale":1,"transform":0,"x":0,"y":0,"primary":true}}')"
|
||||
jq -e '.action == "refuse"' <<<"$result" >/dev/null \
|
||||
|| fail "a stored mode the connected panel does not offer was requested anyway: $result"
|
||||
|
||||
# Undocked: the stored primary is gone. The layout that survives must still
|
||||
# name exactly one primary, or DisplayLayout.validate refuses it outright and
|
||||
# the machine keeps whatever it happens to have.
|
||||
one_screen='[{"name":"DP-2","width":1920,"height":1080,"refreshRate":60.0,"scale":1.0,"transform":0,"x":0,"y":0,"availableModes":["[email protected]","[email protected]"]}]'
|
||||
result="$(plan "$one_screen" '{"DP-2":{"mode":"[email protected]","scale":1,"transform":0,"x":0,"y":0,"primary":false},"HDMI-A-1":{"mode":"[email protected]","scale":1,"transform":0,"x":1920,"y":0,"primary":true}}')"
|
||||
jq -e '.action == "apply" and ([.layout[] | select(.primary)] | length == 1)' <<<"$result" >/dev/null \
|
||||
|| fail "after undocking, the restored layout did not name exactly one primary: $result"
|
||||
jq -e '[.layout[] | .name] == ["DP-2"]' <<<"$result" >/dev/null \
|
||||
|| fail "the restored layout mentions a display that is not connected: $result"
|
||||
|
||||
|
||||
restore_display || fail 'the final cleanup could not restore and verify the original display'
|
||||
|
||||
original_mode=""
|
||||
stop_harness
|
||||
trap - EXIT
|
||||
|
||||
Reference in New Issue
Block a user