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.
180 lines
7.6 KiB
QML
180 lines
7.6 KiB
QML
import Quickshell
|
|
import Quickshell.Io
|
|
import QtQuick
|
|
|
|
import qs.config
|
|
import qs.services
|
|
|
|
ShellRoot {
|
|
// The isolated screen model begins with both fixture outputs so changing
|
|
// it below exercises the same reactive topology path as a real hotplug.
|
|
Component.onCompleted: Displays.screenOverride = ["DP-2", "HDMI-A-1"]
|
|
|
|
IpcHandler {
|
|
target: "displays-test"
|
|
|
|
function status(): string {
|
|
const monitor = Displays.monitors.length > 0 ? Displays.monitors[0] : null;
|
|
return JSON.stringify({
|
|
count: Displays.monitors.length,
|
|
name: monitor ? monitor.name : "",
|
|
width: monitor ? monitor.width : 0,
|
|
height: monitor ? monitor.height : 0,
|
|
refresh: monitor ? monitor.refreshRate : 0,
|
|
mode: monitor ? monitor.mode : "",
|
|
scale: monitor ? monitor.scale : 0,
|
|
transform: monitor ? monitor.transform : -1,
|
|
x: monitor ? monitor.x : 0,
|
|
y: monitor ? monitor.y : 0,
|
|
primary: monitor ? monitor.primary : false,
|
|
modes: monitor ? monitor.modes.length : 0,
|
|
awaiting: Displays.awaitingConfirmation,
|
|
canConfirm: Displays.canConfirm,
|
|
secondsLeft: Displays.secondsLeft,
|
|
lastError: Displays.lastError,
|
|
overridden: monitor ? Displays.isOverridden(monitor.name) : false
|
|
});
|
|
}
|
|
|
|
function applyScale(scale: real): bool {
|
|
const monitor = Displays.monitors[0];
|
|
if (!monitor) return false;
|
|
const mode = monitor.mode;
|
|
return Displays.apply(monitor.name, mode, scale, monitor.transform);
|
|
}
|
|
|
|
function transactionStatus(): string {
|
|
return JSON.stringify({
|
|
layout: Displays.currentLayout(),
|
|
primaryFirst: Displays.primaryFirstMonitors.map(monitor => monitor.name),
|
|
pending: Displays.pendingRequestedLayout,
|
|
previous: Displays.pendingPreviousLayout,
|
|
reverting: Displays.revertExpectedLayout,
|
|
awaiting: Displays.awaitingConfirmation,
|
|
canConfirm: Displays.canConfirm,
|
|
busy: Displays.busy,
|
|
generation: Displays.operationGeneration,
|
|
revertGeneration: Displays.revertGeneration,
|
|
lastError: Displays.lastError
|
|
});
|
|
}
|
|
|
|
function applyLayoutFixture(secondX: int, secondY: int): bool {
|
|
const layout = Displays.currentLayout();
|
|
if (layout.length !== 2) return false;
|
|
layout[0].x = 0;
|
|
layout[0].y = 0;
|
|
layout[0].primary = true;
|
|
layout[1].x = secondX;
|
|
layout[1].y = secondY;
|
|
layout[1].primary = false;
|
|
return Displays.applyLayout(layout);
|
|
}
|
|
|
|
function makePrimaryFixture(output: string): bool {
|
|
return Displays.makePrimary(output);
|
|
}
|
|
|
|
function injectReadback(text: string, generation: int): void {
|
|
Displays.parse(text, generation);
|
|
}
|
|
|
|
// The restore-on-reconnect decision, without applying anything. The
|
|
// caller injects a topology with injectReadback first, sets the stored
|
|
// arrangement here, and reads back what Panama would do about it.
|
|
// Both arguments are base64. `qs ipc call` splits an argument that
|
|
// looks like a JSON array of several objects into one argument per
|
|
// object, so a two-monitor fixture arrives as two arguments and the
|
|
// call is rejected for arity. Encoding sidesteps the parsing entirely.
|
|
//
|
|
// Topology and stored arrangement are set in one call on purpose: a
|
|
// real compositor query landing between two calls would replace the
|
|
// injected topology, and the answer would be about this machine's
|
|
// actual monitor instead of the fixture.
|
|
function restorePlan(readbackB64: string, storedB64: string): string {
|
|
Displays.parse(Qt.atob(readbackB64), 0);
|
|
DesktopPreferences.set("displays", JSON.parse(Qt.atob(storedB64)));
|
|
const plan = Displays.plannedRestore();
|
|
return JSON.stringify({
|
|
action: plan.action,
|
|
layout: (plan.layout ?? []).map(record => ({
|
|
name: record.name, mode: record.mode, scale: record.scale,
|
|
transform: record.transform, x: record.x, y: record.y,
|
|
primary: record.primary
|
|
}))
|
|
});
|
|
}
|
|
|
|
function expireApplyVerification(): void {
|
|
Displays.verificationTimedOut();
|
|
}
|
|
|
|
function expireRevertVerification(): void {
|
|
Displays.revertVerificationTimedOut();
|
|
}
|
|
|
|
function refreshIdentityFixture(): string {
|
|
const modes = Displays.normalizeModes([
|
|
"[email protected]",
|
|
"[email protected]"
|
|
]);
|
|
const monitor = { width: 1920, height: 1080, refreshRate: 59.94 };
|
|
return JSON.stringify({
|
|
count: modes.length,
|
|
modes: modes.map(mode => mode.mode),
|
|
selected: modes.filter(mode => Displays.modeIsCurrent(monitor, mode)).map(mode => mode.mode)
|
|
});
|
|
}
|
|
|
|
function positionFixture(): string {
|
|
const previous = Displays.monitors;
|
|
Displays.parse(JSON.stringify([
|
|
{
|
|
name: "DP-2", description: "Primary", width: 4500, height: 3000,
|
|
refreshRate: 60, scale: 1.5, transform: 0, x: 140, y: 80,
|
|
availableModes: ["[email protected]"]
|
|
},
|
|
{
|
|
name: "HDMI-A-1", description: "Second", width: 2560, height: 1440,
|
|
refreshRate: 60, scale: 1, transform: 0, x: 3140, y: 80,
|
|
availableModes: ["[email protected]"]
|
|
}
|
|
]), Displays.operationGeneration);
|
|
const result = JSON.stringify(Displays.monitors.map(monitor => ({
|
|
name: monitor.name,
|
|
x: monitor.x,
|
|
y: monitor.y,
|
|
primary: monitor.primary
|
|
})));
|
|
Displays.monitors = previous;
|
|
return result;
|
|
}
|
|
|
|
function applyBad(kind: string): bool {
|
|
const monitor = Displays.monitors[0];
|
|
if (!monitor) return false;
|
|
const mode = monitor.mode;
|
|
if (kind === "mode") return Displays.apply(monitor.name, "9999x9999@240", monitor.scale, monitor.transform);
|
|
if (kind === "scale") return Displays.apply(monitor.name, mode, 1.37, monitor.transform);
|
|
if (kind === "dirtyScale") {
|
|
const dirty = Displays.scales.find(scale => !Displays.isScaleClean(mode, scale));
|
|
return dirty === undefined ? false : Displays.apply(monitor.name, mode, dirty, monitor.transform);
|
|
}
|
|
if (kind === "transform") return Displays.apply(monitor.name, mode, monitor.scale, 9);
|
|
if (kind === "output") return Displays.apply("NOPE-1", mode, monitor.scale, monitor.transform);
|
|
return false;
|
|
}
|
|
|
|
function confirmChange(): bool { return Displays.confirm(); }
|
|
function revertChange(): void { Displays.revert(); }
|
|
function forget(): void {
|
|
const monitor = Displays.monitors[0];
|
|
if (monitor) Displays.forget(monitor.name);
|
|
}
|
|
function refresh(): void { Displays.refresh(); }
|
|
function setScreenModel(names: string): void {
|
|
Displays.screenOverride = JSON.parse(names);
|
|
}
|
|
}
|
|
}
|