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 })) }); } // The extended record — vrr override, colour profile, bit depth, SDR // trim, mirroring — merged one field at a time, which is how every // control on the page changes it. Base64 for the reason restorePlan // documents above: `qs ipc call` splits JSON that looks like an array // of objects into one argument per object. function applyRecordFixture(output: string, patchB64: string): bool { return Displays.applyRecord(output, JSON.parse(Qt.atob(patchB64))); } // matchesLayout as a pure function. The readback carve-outs — a // mirrored output's position, a framebuffer format Panama does not // recognise — are decisions about what NOT to assert, and proving them // through a compositor would mean owning a compositor that mirrors. function layoutMatch(monitorsB64: string, layoutB64: string): bool { return Displays.matchesLayout(JSON.parse(Qt.atob(monitorsB64)), JSON.parse(Qt.atob(layoutB64))); } // A settings.json written before any of the new fields existed is the // common case on every machine that has this installed today. function persistedEntryValid(entryB64: string): bool { return Displays.isPersistedLayoutEntry(JSON.parse(Qt.atob(entryB64))); } function expireApplyVerification(): void { Displays.verificationTimedOut(); } function expireRevertVerification(): void { Displays.revertVerificationTimedOut(); } function refreshIdentityFixture(): string { const modes = Displays.normalizeModes([ "1920x1080@59.94Hz", "1920x1080@60.00Hz" ]); 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: ["4500x3000@60.00Hz"] }, { name: "HDMI-A-1", description: "Second", width: 2560, height: 1440, refreshRate: 60, scale: 1, transform: 0, x: 3140, y: 80, availableModes: ["2560x1440@60.00Hz"] } ]), 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); } } }