pragma Singleton // Display configuration: resolution, refresh rate, scale, and rotation. // // This is the only page in Panama Settings where a wrong value can leave you // unable to SEE the screen well enough to undo it. A mode the display cannot // show, or a scale that makes everything unreadable, is not recoverable through // the same UI that caused it. // // So a change is never applied irreversibly. It is applied, then reverted // automatically after a countdown unless confirmed -- the same contract every // desktop uses for this one setting, and for the same reason. Confirming is // what writes it to the settings store; letting the countdown run leaves // nothing behind. // // Applied with `hyprctl eval` and hl.monitor{}. As everywhere else in Panama, // success means the value was read back from the compositor and matched, never // that a command exited zero. import Quickshell import Quickshell.Io import QtQuick import qs.config import "DisplayLayout.js" as DisplayLayout Singleton { id: root // [{ name, description, width, height, refreshRate, scale, transform, // modes: [{ label, mode, width, height, refresh }] }] property var monitors: [] property string lastError: "" // Set while a change is applied but not yet confirmed. property var pendingPreviousLayout: null property var pendingRequestedLayout: null property bool pendingVerified: false property bool revertQueued: false property var revertExpectedLayout: null property string revertReason: "" property bool revertVerificationActive: false property int operationGeneration: 0 property int revertGeneration: -1 property bool externalChangeBlocked: false property int secondsLeft: 0 property bool identifying: false readonly property bool awaitingConfirmation: root.pendingRequestedLayout !== null readonly property bool canConfirm: root.awaitingConfirmation && root.pendingVerified && !root.busy readonly property bool busy: query.running || applyRun.running || revertRun.running || root.revertExpectedLayout !== null readonly property int confirmSeconds: 15 readonly property var transforms: [ { value: 0, label: "Landscape" }, { value: 1, label: "Portrait" }, { value: 2, label: "Landscape (flipped)" }, { value: 3, label: "Portrait (flipped)" } ] // Scales that divide this desktop's common resolutions into whole pixels. // Hyprland rejects a fractional scale that does not, and the message it // gives is not something to put in front of a user. readonly property var scales: [1.0, 1.25, 1.5, 1.75, 2.0, 2.5, 3.0] Process { id: query property int generation: 0 command: ["hyprctl", "-j", "monitors"] stdout: StdioCollector { onStreamFinished: root.parse(this.text, query.generation) } onExited: (exitCode, exitStatus) => { if (exitCode !== 0) root.lastError = "Could not read the connected displays."; if (root.revertQueued && !applyRun.running && root.awaitingConfirmation) root.performRevert(); } } Process { id: applyRun onExited: (exitCode, exitStatus) => { if (!root.awaitingConfirmation) return; if (root.revertQueued) { if (!query.running) root.performRevert(); return; } if (exitCode !== 0) { root.revertWithMessage("The display rejected that change and Panama restored the previous setting."); return; } verifyTimer.attempts = 0; verifyTimer.ticks = 0; verifyTimer.restart(); } } Process { id: revertRun onExited: (exitCode, exitStatus) => { // Exit status is advisory only. Hyprland's Lua bridge can report // success without applying a value, so exact readback decides. root.revertVerificationActive = true; revertVerifyTimer.attempts = 0; revertVerifyTimer.ticks = 0; revertVerifyTimer.restart(); } } Component.onCompleted: root.refresh() function refresh(): bool { if (!query.running) { query.generation = root.operationGeneration; query.running = true; return true; } return false; } function identify(): void { root.identifying = true; identifyTimer.restart(); } function parse(text: string, generation: int): void { try { const raw = JSON.parse(text); const stored = DesktopPreferences.get("displays"); const persisted = stored && typeof stored === "object" ? stored : {}; const persistedPrimaries = raw.filter(monitor => { const entry = persisted[monitor.name ?? ""]; return root.isPersistedLayoutEntry(entry) && entry.primary === true; }); const origin = raw.find(monitor => monitor.x === 0 && monitor.y === 0); const primaryName = persistedPrimaries.length === 1 ? persistedPrimaries[0].name : (origin?.name ?? raw[0]?.name ?? ""); root.monitors = raw.map(monitor => { const modes = root.normaliseModes(monitor.availableModes ?? []); const width = monitor.width ?? 0; const height = monitor.height ?? 0; const refreshRate = monitor.refreshRate ?? 0; const current = modes .filter(mode => mode.width === width && mode.height === height) .sort((left, right) => Math.abs(left.refresh - refreshRate) - Math.abs(right.refresh - refreshRate))[0]; return { name: monitor.name ?? "", description: monitor.description ?? monitor.model ?? "Display", width: width, height: height, refreshRate: refreshRate, mode: current?.mode ?? `${width}x${height}@${refreshRate}`, scale: monitor.scale ?? 1, transform: monitor.transform ?? 0, x: Number.isInteger(monitor.x) ? monitor.x : 0, y: Number.isInteger(monitor.y) ? monitor.y : 0, primary: monitor.name === primaryName, currentFormat: monitor.currentFormat ?? "", colorPreset: monitor.colorManagementPreset ?? "", vrr: monitor.vrr === true, modes: modes }; }); if (root.awaitingConfirmation && root.pendingRequestedLayout && generation === root.operationGeneration && root.matchesLayout(root.monitors, root.pendingRequestedLayout)) { root.pendingVerified = true; verifyTimer.stop(); root.lastError = ""; } else if (root.revertVerificationActive && generation === root.revertGeneration && root.revertExpectedLayout && root.matchesLayout(root.monitors, root.revertExpectedLayout)) { revertVerifyTimer.stop(); root.revertVerificationActive = false; root.revertGeneration = -1; root.revertExpectedLayout = null; if (root.revertReason === "") root.lastError = ""; else root.lastError = root.revertReason; root.revertReason = ""; } else if (!root.awaitingConfirmation && !root.revertExpectedLayout && ( root.lastError === "Could not read the connected displays." || root.lastError === "The display list could not be read.")) { root.lastError = ""; } } catch (error) { root.lastError = "The display list could not be read."; } } // "4500x3000@60.00Hz" -> a sortable record. The compositor reports the same // resolution at distinct rates such as 60.00 and 59.94. Those identities // remain separate because confirmation and recovery must read back the // exact mode the user chose, even when their rounded labels look similar. function normaliseModes(raw: var): var { const seen = {}; const out = []; for (const entry of raw) { const match = String(entry).match(/^(\d+)x(\d+)@([\d.]+)Hz$/); if (!match) continue; const width = Number(match[1]); const height = Number(match[2]); const refreshText = match[3]; const refresh = Number(refreshText); const roundedRefresh = Math.round(refresh); const key = `${width}x${height}@${refreshText}`; if (seen[key]) continue; seen[key] = true; out.push({ label: `${width} × ${height}`, refreshLabel: Math.abs(refresh - roundedRefresh) < 0.005 ? `${roundedRefresh} Hz` : `${refresh.toFixed(2)} Hz`, mode: `${width}x${height}@${refreshText}`, width: width, height: height, refresh: refresh }); } return out.sort((a, b) => (b.width * b.height) - (a.width * a.height) || b.refresh - a.refresh); } function monitorNamed(name: string): var { return root.monitors.find(monitor => monitor.name === name) ?? null; } function isPersistedLayoutEntry(entry: var): bool { return !!entry && typeof entry === "object" && root.modeParts(entry.mode) !== null && Number.isFinite(entry.scale) && entry.scale > 0 && Number.isInteger(entry.transform) && entry.transform >= 0 && entry.transform <= 3 && Number.isInteger(entry.x) && entry.x >= -100000 && entry.x <= 100000 && Number.isInteger(entry.y) && entry.y >= -100000 && entry.y <= 100000 && typeof entry.primary === "boolean"; } function modeParts(mode: string): var { const match = String(mode).match(/^(\d+)x(\d+)@(\d+(?:\.\d+)?)$/); if (!match) return null; return { width: Number(match[1]), height: Number(match[2]), refresh: Number(match[3]) }; } function isScaleClean(mode: string, scale: real): bool { const parts = root.modeParts(mode); if (!parts || root.scales.indexOf(scale) < 0 || !Number.isFinite(scale) || scale <= 0) return false; const logicalWidth = parts.width / scale; const logicalHeight = parts.height / scale; return Math.abs(logicalWidth - Math.round(logicalWidth)) < 0.0001 && Math.abs(logicalHeight - Math.round(logicalHeight)) < 0.0001; } function scalesForMode(mode: string): var { return root.scales.filter(scale => root.isScaleClean(mode, scale)); } function nearestCleanScale(mode: string, preferred: real): real { const choices = root.scalesForMode(mode); if (choices.length === 0) return 1.0; return choices.reduce((best, candidate) => Math.abs(candidate - preferred) < Math.abs(best - preferred) ? candidate : best, choices[0]); } function currentLayout(): var { return root.monitors.map(monitor => ({ name: monitor.name, width: monitor.width, height: monitor.height, refreshRate: monitor.refreshRate, mode: monitor.mode, scale: monitor.scale, transform: monitor.transform, x: monitor.x, y: monitor.y, primary: monitor.primary === true })); } function matchesLayout(monitors: var, layout: var): bool { if (!Array.isArray(monitors) || !Array.isArray(layout) || monitors.length !== layout.length) return false; const expected = Array.from(layout).sort((a, b) => a.name.localeCompare(b.name)); const actual = Array.from(monitors).sort((a, b) => a.name.localeCompare(b.name)); for (let index = 0; index < expected.length; index++) { const requested = expected[index]; const monitor = actual[index]; const parts = root.modeParts(requested.mode); if (!parts || monitor.name !== requested.name || monitor.width !== parts.width || monitor.height !== parts.height || Math.abs(monitor.refreshRate - parts.refresh) >= 0.01 || Math.abs(monitor.scale - requested.scale) >= 0.001 || monitor.transform !== requested.transform || monitor.x !== requested.x || monitor.y !== requested.y) return false; } return true; } function modeIsCurrent(monitor: var, candidate: var): bool { return !!monitor && !!candidate && monitor.width === candidate.width && monitor.height === candidate.height && Math.abs(monitor.refreshRate - candidate.refresh) < 0.01; } function validRequestedLayout(layout: var): bool { if (!DisplayLayout.validate(layout) || layout.length !== root.monitors.length) return false; const currentNames = root.monitors.map(monitor => monitor.name).sort(); const requestedNames = layout.map(record => record.name).sort(); if (JSON.stringify(currentNames) !== JSON.stringify(requestedNames)) return false; return layout.every(record => { const monitor = root.monitorNamed(record.name); const parts = root.modeParts(record.mode); return !!monitor && !!parts && record.width === parts.width && record.height === parts.height && monitor.modes.some(candidate => candidate.mode === record.mode) && root.isScaleClean(record.mode, record.scale) && root.transforms.some(candidate => candidate.value === record.transform); }); } // One-field controls remain callers of the complete-layout transaction. // Their edit is cloned into the current layout so every output's position // participates in apply, verification, and rollback. function apply(output: string, mode: string, scale: real, transform: int): bool { const layout = root.currentLayout(); const record = layout.find(candidate => candidate.name === output); const parts = root.modeParts(mode); if (!record || !parts) { root.lastError = record ? "That display does not offer that mode." : "That display is not connected."; return false; } record.mode = mode; record.width = parts.width; record.height = parts.height; record.refreshRate = parts.refresh; record.scale = scale; record.transform = transform; return root.applyLayout(layout); } // Applies immediately and starts the countdown. Nothing is stored yet: the // complete connected layout is only written by confirm(). function applyLayout(layout: var, protectedOperation: bool): bool { if (root.externalChangeBlocked && protectedOperation !== true) { root.lastError = "Wait for Settings to finish restoring before changing a display."; return false; } if (root.busy) { root.lastError = "Wait for the current display operation to finish."; return false; } if (root.awaitingConfirmation) { root.lastError = "Finish the current display change first."; return false; } const normalized = DisplayLayout.normalize(layout); if (!root.validRequestedLayout(normalized)) { root.lastError = "That complete display layout is not valid for the connected displays."; return false; } root.pendingPreviousLayout = root.currentLayout(); root.operationGeneration++; root.pendingRequestedLayout = normalized; root.pendingVerified = false; root.revertQueued = false; root.secondsLeft = root.confirmSeconds; root.lastError = ""; countdown.restart(); root.pushLayout(normalized, applyRun); return true; } // Settings restore holds the external-change lock while it proves a // snapshot. This narrow entry point authorizes that one transaction while // keeping every user-facing control blocked until restore settles. function applyProtectedLayout(layout: var): bool { return root.applyLayout(layout, true); } function makePrimary(output: string): bool { const layout = root.currentLayout(); if (!layout.some(record => record.name === output)) { root.lastError = "That display is not connected."; return false; } for (const record of layout) record.primary = record.name === output; return root.applyLayout(DisplayLayout.normalize(layout)); } function pushLayout(layout: var, runner: var): void { const payload = layout.map(record => `hl.monitor({ output = "${record.name}", mode = "${record.mode}", position = "${record.x}x${record.y}", scale = ${record.scale}, transform = ${record.transform} })` ).join("; "); runner.exec(["hyprctl", "eval", payload]); } function confirm(): bool { if (!root.canConfirm || !root.matchesLayout(root.monitors, root.pendingRequestedLayout)) { if (root.awaitingConfirmation) root.lastError = "Wait for the display to finish applying before keeping it."; return false; } const stored = DesktopPreferences.get("displays"); const next = Object.assign({}, (stored && typeof stored === "object") ? stored : {}); for (const record of root.pendingRequestedLayout) { next[record.name] = { mode: record.mode, scale: record.scale, transform: record.transform, x: record.x, y: record.y, primary: record.primary }; } if (!DesktopPreferences.set("displays", next)) { root.lastError = "That display setting could not be saved. Revert it and try again."; return false; } root.clearPending(); root.lastError = ""; return true; } function clearPending(): void { countdown.stop(); verifyTimer.stop(); root.pendingPreviousLayout = null; root.pendingRequestedLayout = null; root.pendingVerified = false; root.revertQueued = false; root.secondsLeft = 0; } function revert(): void { root.revertWithMessage(""); } function revertWithMessage(message: string): void { if (!root.awaitingConfirmation) return; countdown.stop(); verifyTimer.stop(); root.pendingVerified = false; root.revertReason = message; if (message !== "") root.lastError = message; if (applyRun.running || query.running) { root.revertQueued = true; return; } root.performRevert(); } function performRevert(): void { const connected = {}; for (const monitor of root.monitors) connected[monitor.name] = true; const previous = (root.pendingPreviousLayout || []) .filter(record => connected[record.name]) .map(record => Object.assign({}, record)); if (previous.length > 0 && !previous.some(record => record.primary)) { const origin = previous.find(record => record.x === 0 && record.y === 0); (origin || previous[0]).primary = true; } root.operationGeneration++; root.revertGeneration = root.operationGeneration; root.revertExpectedLayout = previous.length > 0 ? previous : null; root.revertVerificationActive = false; root.clearPending(); if (previous.length > 0) root.pushLayout(previous, revertRun); else root.lastError = root.revertReason; } // Clears any stored override for an output so it returns to the value // shipped in hypr/monitors.lua on the next start. function forget(output: string): void { const stored = DesktopPreferences.get("displays"); if (!stored || typeof stored !== "object" || stored[output] === undefined) return; const next = Object.assign({}, stored); delete next[output]; DesktopPreferences.set("displays", next); } function isOverridden(output: string): bool { const stored = DesktopPreferences.get("displays"); return !!(stored && typeof stored === "object" && stored[output] !== undefined); } function verificationTimedOut(): void { root.revertWithMessage("The display did not apply that setting, so Panama restored the previous one."); } function revertVerificationTimedOut(): void { revertVerifyTimer.stop(); root.revertVerificationActive = false; root.revertGeneration = -1; root.revertExpectedLayout = null; root.revertReason = ""; root.lastError = "The previous display setting could not be verified. Open Displays and restore it manually."; } Timer { id: identifyTimer interval: 3000 repeat: false onTriggered: root.identifying = false } Timer { id: verifyTimer property int attempts: 0 property int ticks: 0 interval: 120 repeat: true onTriggered: { ticks++; if (ticks > 50) { root.verificationTimedOut(); return; } if (root.refresh()) attempts++; } } Timer { id: revertVerifyTimer property int attempts: 0 property int ticks: 0 interval: 120 repeat: true onTriggered: { ticks++; if (ticks > 50) { root.revertVerificationTimedOut(); return; } if (root.refresh()) attempts++; } } Timer { id: countdown interval: 1000 repeat: true onTriggered: { root.secondsLeft -= 1; if (root.secondsLeft <= 0) root.revert(); } } }