The first run of the suite on a laptop found five contracts asserting the desktop instead of the code. settings-system pinned DP-2 at 4500x3000 in XRGB2101010; it now asks Hyprland what is actually primary. ssh-keys hardcoded id_ed25519; it now uses whichever key exists. switcher's live half stepped a session with one window, which step() deliberately refuses. displays raced the service's revert readback -- the compositor looks restored while verification still holds busy, so an immediate apply was refused with its error already cleared; the harness now exposes settled and the contract waits for it. declared-dependencies gets an OPTIONAL list for docker: the aliases serve machines that run Docker deliberately, Panama's runtime is rootless podman, and a missing docker fails by naming the command, which is loud enough. Claude-Session: https://claude.ai/code/session_01Epx9ZC1gwm81K3jm9x9CKh
187 lines
8.1 KiB
QML
187 lines
8.1 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,
|
|
// The compositor being visually restored is not the service
|
|
// being done: revert verification keeps its own readback
|
|
// running for a few ticks, and busy blocks a new apply until
|
|
// it settles. A caller that only watched `awaiting` raced
|
|
// this and got a refusal with no error text.
|
|
settled: !Displays.busy && !Displays.revertVerificationActive
|
|
&& !Displays.awaitingConfirmation
|
|
});
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|