Rebuild Displays around the canvas, and let the transaction keep color

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
Gabriel Brown
2026-08-24 10:44:11 -04:00
parent 1f694b00b6
commit 9bc68ba358
29 changed files with 3065 additions and 388 deletions
+315 -35
View File
@@ -1,6 +1,7 @@
pragma Singleton
// Display configuration: resolution, refresh rate, scale, and rotation.
// Display configuration: resolution, refresh rate, scale, rotation, position,
// colour management, variable refresh rate, and mirroring.
//
// 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
@@ -27,7 +28,14 @@ Singleton {
id: root
// [{ name, description, width, height, refreshRate, scale, transform,
// x, y, primary, currentFormat, colorPreset, bitdepth, sdrBrightness,
// sdrSaturation, mirrorOf, vrr,
// modes: [{ label, mode, width, height, refresh }] }]
//
// bitdepth, sdrBrightness and sdrSaturation are 0 when the compositor does
// not report them, which is not the same as a value: 0 is outside every
// valid range, so verification skips a field it cannot read rather than
// treating "unknown" as a mismatch.
property var monitors: []
// Quickshell.screens is the topology authority. The override is only the
// isolated harness model; normal sessions always observe Quickshell.
@@ -77,6 +85,33 @@ Singleton {
{ value: 3, label: "Portrait (flipped)" }
]
// Colour management presets Hyprland accepts as `cm`. "auto" is a policy,
// not a state: the compositor resolves it to a concrete preset and reports
// that one back, so it is never verified against readback.
readonly property var colorProfiles: [
{ value: "auto", label: "Automatic" },
{ value: "srgb", label: "sRGB" },
{ value: "wide", label: "Wide gamut" },
{ value: "hdr", label: "HDR" }
]
// Per-display override of the global VRR policy. -1 means the display has
// no opinion and follows misc.vrr, which is expressed by leaving `vrr` out
// of the monitor rule entirely. 3 (fullscreen games only) is deliberately
// absent: it belongs to the global policy, not to one display.
readonly property var vrrModes: [
{ value: -1, label: "Follow gaming policy" },
{ value: 0, label: "Off" },
{ value: 1, label: "Always on" },
{ value: 2, label: "Fullscreen only" }
]
readonly property var bitdepths: [8, 10]
readonly property real sdrBrightnessMin: 0.8
readonly property real sdrBrightnessMax: 2.0
readonly property real sdrSaturationMin: 0.8
readonly property real sdrSaturationMax: 1.2
// 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.
@@ -197,7 +232,8 @@ Singleton {
|| Math.abs(entry.scale - record.scale) >= 0.001
|| entry.transform !== record.transform
|| (entry.x !== undefined && entry.x !== record.x)
|| (entry.y !== undefined && entry.y !== record.y))
|| (entry.y !== undefined && entry.y !== record.y)
|| root.storedFieldsDiffer(entry, record))
changed = true;
record.mode = entry.mode;
record.width = parts.width;
@@ -208,6 +244,7 @@ Singleton {
if (entry.x !== undefined) record.x = entry.x;
if (entry.y !== undefined) record.y = entry.y;
record.primary = entry.primary === true;
root.overlayStoredFields(record, entry);
}
if (!changed)
@@ -313,6 +350,11 @@ Singleton {
primary: monitor.name === primaryName,
currentFormat: monitor.currentFormat ?? "",
colorPreset: monitor.colorManagementPreset ?? "",
bitdepth: root.formatBitdepth(monitor.currentFormat ?? ""),
sdrBrightness: Number.isFinite(monitor.sdrBrightness) ? monitor.sdrBrightness : 0,
sdrSaturation: Number.isFinite(monitor.sdrSaturation) ? monitor.sdrSaturation : 0,
// Hyprland reports "none" for an output that is not mirroring.
mirrorOf: (monitor.mirrorOf ?? "none") === "none" ? "" : String(monitor.mirrorOf),
vrr: monitor.vrr === true,
modes: modes
};
@@ -384,6 +426,55 @@ Singleton {
return root.monitors.find(monitor => monitor.name === name) ?? null;
}
// The framebuffer format is the only honest report of the bit depth in
// effect: asking for 10-bit and getting it are different things, and a
// panel that cannot carry the link rate quietly stays at 8. Formats outside
// this map are read as "unknown", never as a mismatch.
function formatBitdepth(format: string): int {
if (format === "XRGB8888")
return 8;
if (format === "XRGB2101010")
return 10;
return 0;
}
function persistedDisplays(): var {
const stored = DesktopPreferences.get("displays");
return stored && typeof stored === "object" ? stored : {};
}
// The stored entry for an output, or null when nothing valid is stored.
function savedEntry(output: string): var {
const entry = root.persistedDisplays()[output];
return root.isPersistedLayoutEntry(entry) ? entry : null;
}
// Whether a stored entry asks for something the live record does not
// already have. A field the entry does not carry is not a difference: it
// predates that field, and the compositor's current value stands.
function storedFieldsDiffer(entry: var, record: var): bool {
return (entry.vrrMode !== undefined && entry.vrrMode !== record.vrrMode)
|| (entry.colorProfile !== undefined && entry.colorProfile !== record.colorProfile)
|| (entry.bitdepth !== undefined && entry.bitdepth !== record.bitdepth)
|| (entry.mirrorOf !== undefined && entry.mirrorOf !== record.mirrorOf)
|| (entry.sdrBrightness !== undefined
&& Math.abs(entry.sdrBrightness - record.sdrBrightness) >= 0.001)
|| (entry.sdrSaturation !== undefined
&& Math.abs(entry.sdrSaturation - record.sdrSaturation) >= 0.001);
}
function overlayStoredFields(record: var, entry: var): void {
for (const field of ["vrrMode", "colorProfile", "bitdepth",
"sdrBrightness", "sdrSaturation", "mirrorOf"]) {
if (entry[field] !== undefined)
record[field] = entry[field];
}
}
// Everything past `primary` is optional so that arrangements stored before
// the colour and mirror fields existed still load. Present but invalid is
// not optional: a half-written record is one Panama refuses rather than
// guesses at, exactly as it treats a half-written position.
function isPersistedLayoutEntry(entry: var): bool {
return !!entry && typeof entry === "object"
&& root.modeParts(entry.mode) !== null
@@ -392,7 +483,14 @@ Singleton {
&& 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";
&& typeof entry.primary === "boolean"
&& (entry.vrrMode === undefined || DisplayLayout.validVrrMode(entry.vrrMode))
&& (entry.colorProfile === undefined || DisplayLayout.validColorProfile(entry.colorProfile))
&& (entry.bitdepth === undefined || DisplayLayout.validBitdepth(entry.bitdepth))
&& (entry.sdrBrightness === undefined || DisplayLayout.validSdrBrightness(entry.sdrBrightness))
&& (entry.sdrSaturation === undefined || DisplayLayout.validSdrSaturation(entry.sdrSaturation))
&& (entry.mirrorOf === undefined || (typeof entry.mirrorOf === "string"
&& (entry.mirrorOf === "" || /^[A-Za-z0-9_.-]+$/.test(entry.mirrorOf))));
}
function modeParts(mode: string): var {
@@ -429,19 +527,49 @@ Singleton {
choices[0]);
}
// The complete state of every connected display, read from the compositor.
//
// Every field is read live, because a Hyprland monitor rule replaces the
// previous rule for that output wholesale: a change to one field that did
// not carry the others would drop them back to compositor defaults. That is
// the clobber this reads against.
//
// vrrMode is the exception. The compositor's `vrr` readback is whether
// variable refresh is active right now, not which policy was configured, so
// it comes from the stored record and defaults to -1 (follow the global
// policy). Reverting to a record with -1 is still correct: the rule pushed
// for it carries no `vrr` key, and the display falls back to misc.vrr.
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
}));
return root.monitors.map(monitor => {
const saved = root.savedEntry(monitor.name);
const live = DisplayLayout.validColorProfile(monitor.colorPreset)
? monitor.colorPreset : "auto";
return {
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,
vrrMode: saved && DisplayLayout.validVrrMode(saved.vrrMode) ? saved.vrrMode : -1,
// A display set to "auto" reads back as the preset auto chose.
// Keeping the stored policy stops one apply from pinning the
// display to whatever automatic happened to pick today.
colorProfile: saved && saved.colorProfile === "auto" ? "auto" : live,
bitdepth: monitor.bitdepth !== 0
? monitor.bitdepth
: (saved && DisplayLayout.validBitdepth(saved.bitdepth) ? saved.bitdepth : 8),
sdrBrightness: DisplayLayout.validSdrBrightness(monitor.sdrBrightness)
? monitor.sdrBrightness : 1.0,
sdrSaturation: DisplayLayout.validSdrSaturation(monitor.sdrSaturation)
? monitor.sdrSaturation : 1.0,
mirrorOf: typeof monitor.mirrorOf === "string" ? monitor.mirrorOf : ""
};
});
}
function matchesLayout(monitors: var, layout: var): bool {
@@ -459,13 +587,72 @@ Singleton {
|| 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)
|| monitor.transform !== requested.transform)
return false;
const mirrorOf = typeof requested.mirrorOf === "string" ? requested.mirrorOf : "";
if (root.readbackMirror(monitor) !== mirrorOf)
return false;
// A mirror is placed by the compositor on top of what it copies, so
// its coordinates are not ours to assert. Every other display still
// has to land exactly where it was asked to.
if (mirrorOf === "" && (monitor.x !== requested.x || monitor.y !== requested.y))
return false;
if (!root.matchesColor(monitor, requested))
return false;
}
return true;
}
// Readback accessors. They take either a parsed record from root.monitors
// or a raw `hyprctl -j monitors` object, because verification is also
// exercised against captured compositor output, and a comparison that only
// understood one of the two shapes would pass for the wrong reason.
function readbackMirror(monitor: var): string {
const value = typeof monitor.mirrorOf === "string" ? monitor.mirrorOf : "";
return value === "none" ? "" : value;
}
function readbackPreset(monitor: var): string {
if (typeof monitor.colorPreset === "string" && monitor.colorPreset !== "")
return monitor.colorPreset;
return typeof monitor.colorManagementPreset === "string"
? monitor.colorManagementPreset : "";
}
function readbackBitdepth(monitor: var): int {
if (Number.isInteger(monitor.bitdepth))
return monitor.bitdepth;
return root.formatBitdepth(String(monitor.currentFormat ?? ""));
}
// The colour half of the readback comparison. Each field is asserted only
// where the compositor reports something to compare against; vrrMode is
// absent by design, since `vrr` reads back live state rather than policy.
function matchesColor(monitor: var, requested: var): bool {
// "auto" is resolved by the compositor into srgb or wide before it is
// reported, so there is no value it could equal. It is applied, and the
// preset it resolved to is what the page shows.
const preset = root.readbackPreset(monitor);
if (requested.colorProfile !== undefined && requested.colorProfile !== "auto"
&& preset !== "" && preset !== requested.colorProfile)
return false;
const bitdepth = root.readbackBitdepth(monitor);
if (requested.bitdepth !== undefined && bitdepth !== 0
&& bitdepth !== requested.bitdepth)
return false;
if (requested.sdrBrightness !== undefined
&& DisplayLayout.validSdrBrightness(monitor.sdrBrightness)
&& Math.abs(monitor.sdrBrightness - requested.sdrBrightness) >= 0.01)
return false;
if (requested.sdrSaturation !== undefined
&& DisplayLayout.validSdrSaturation(monitor.sdrSaturation)
&& Math.abs(monitor.sdrSaturation - requested.sdrSaturation) >= 0.01)
return false;
return true;
}
function modeIsCurrent(monitor: var, candidate: var): bool {
return !!monitor && !!candidate
&& monitor.width === candidate.width
@@ -483,34 +670,66 @@ Singleton {
return layout.every(record => {
const monitor = root.monitorNamed(record.name);
const parts = root.modeParts(record.mode);
const mirrorOf = typeof record.mirrorOf === "string" ? record.mirrorOf : "";
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);
&& root.transforms.some(candidate => candidate.value === record.transform)
// DisplayLayout.validate already refuses chains and a mirroring
// primary; this is the connected-hardware half of the same rule.
&& (mirrorOf === "" || !!root.monitorNamed(mirrorOf));
});
}
// 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 {
// Their edit is cloned into the current layout so every output's position,
// colour and mirror state participates in apply, verification, and
// rollback. `partial` carries only the fields being changed; anything it
// leaves out keeps the value currentLayout just read from the compositor.
function applyRecord(output: string, partial: var): 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.";
if (!record) {
root.lastError = "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;
const changes = (partial && typeof partial === "object") ? partial : {};
if (changes.mode !== undefined) {
const parts = root.modeParts(changes.mode);
if (!parts) {
root.lastError = "That display does not offer that mode.";
return false;
}
record.mode = changes.mode;
record.width = parts.width;
record.height = parts.height;
record.refreshRate = parts.refresh;
}
for (const field of ["scale", "transform", "vrrMode", "colorProfile",
"bitdepth", "sdrBrightness", "sdrSaturation", "mirrorOf"]) {
if (changes[field] !== undefined)
record[field] = changes[field];
}
// The arrangement is anchored on the primary display, and a mirror has
// no position of its own to anchor to. Said plainly here rather than
// left to the layout validator's one generic message.
if (record.primary === true && typeof record.mirrorOf === "string"
&& record.mirrorOf !== "") {
root.lastError = "The primary display cannot mirror another display. Make a different display primary first.";
return false;
}
return root.applyLayout(layout);
}
// The original four-field entry point, kept so existing callers and the
// harness keep working.
function apply(output: string, mode: string, scale: real, transform: int): bool {
return root.applyRecord(output, { mode: mode, scale: scale, transform: transform });
}
// 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 {
@@ -554,19 +773,66 @@ Singleton {
function makePrimary(output: string): bool {
const layout = root.currentLayout();
if (!layout.some(record => record.name === output)) {
const target = layout.find(record => record.name === output);
if (!target) {
root.lastError = "That display is not connected.";
return false;
}
// Refused rather than silently un-mirrored: the arrangement is anchored
// on the primary, and a mirror has no position of its own. Turning the
// mirror off is a change to what is on screen, and the user makes it.
if (typeof target.mirrorOf === "string" && target.mirrorOf !== "") {
root.lastError = "A mirrored display cannot be the primary. Set it back to an extended display first.";
return false;
}
for (const record of layout)
record.primary = record.name === output;
return root.applyLayout(DisplayLayout.normalize(layout));
}
// A monitor rule replaces the previous rule for that output entirely, so
// every field the record knows is emitted every time. Omission is not
// "leave it alone", it is "go back to the compositor's default", which is
// exactly what the omission rules below rely on:
//
// * vrr is left out for vrrMode -1, which returns the display to the
// global misc.vrr policy. Emitting the key at all IS the override.
// * mirror is left out unless the record names a target.
// * sdrbrightness / sdrsaturation are left out at their neutral 1.0.
// Naming the neutral value pins the display to it, which is not what
// "no opinion" means.
// * position is "auto" for a mirror, whose place is the compositor's to
// choose and ours to read back, never to ask for.
function monitorRule(record: var): string {
const mirrorOf = typeof record.mirrorOf === "string" ? record.mirrorOf : "";
const fields = [
`output = "${record.name}"`,
`mode = "${record.mode}"`,
mirrorOf === ""
? `position = "${record.x}x${record.y}"`
: `position = "auto"`,
`scale = ${record.scale}`,
`transform = ${record.transform}`
];
if (mirrorOf !== "")
fields.push(`mirror = "${mirrorOf}"`);
if (DisplayLayout.validVrrMode(record.vrrMode) && record.vrrMode >= 0)
fields.push(`vrr = ${record.vrrMode}`);
if (DisplayLayout.validBitdepth(record.bitdepth))
fields.push(`bitdepth = ${record.bitdepth}`);
if (DisplayLayout.validColorProfile(record.colorProfile))
fields.push(`cm = "${record.colorProfile}"`);
if (DisplayLayout.validSdrBrightness(record.sdrBrightness)
&& Math.abs(record.sdrBrightness - 1.0) >= 0.001)
fields.push(`sdrbrightness = ${record.sdrBrightness}`);
if (DisplayLayout.validSdrSaturation(record.sdrSaturation)
&& Math.abs(record.sdrSaturation - 1.0) >= 0.001)
fields.push(`sdrsaturation = ${record.sdrSaturation}`);
return `hl.monitor({ ${fields.join(", ")} })`;
}
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("; ");
const payload = layout.map(record => root.monitorRule(record)).join("; ");
runner.exec(["hyprctl", "eval", payload]);
}
@@ -587,7 +853,13 @@ Singleton {
transform: record.transform,
x: record.x,
y: record.y,
primary: record.primary
primary: record.primary,
vrrMode: record.vrrMode,
colorProfile: record.colorProfile,
bitdepth: record.bitdepth,
sdrBrightness: record.sdrBrightness,
sdrSaturation: record.sdrSaturation,
mirrorOf: record.mirrorOf
};
}
if (!DesktopPreferences.set("displays", next)) {
@@ -637,6 +909,14 @@ Singleton {
const previous = (root.pendingPreviousLayout || [])
.filter(record => connected[record.name])
.map(record => Object.assign({}, record));
// A mirror whose target was unplugged has nothing left to copy, and a
// rule pointing at a missing output is one the compositor ignores
// silently. Give it back its own picture instead.
for (const record of previous) {
if (typeof record.mirrorOf === "string" && record.mirrorOf !== ""
&& !connected[record.mirrorOf])
record.mirrorOf = "";
}
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;