Verify display restoration and expose outputs

This commit is contained in:
Gabriel Brown
2026-08-18 02:55:43 -04:00
parent 67674d297e
commit efc53435a2
4 changed files with 124 additions and 30 deletions
+4 -3
View File
@@ -16,7 +16,8 @@ ShellRoot {
name: monitor ? monitor.name : "",
width: monitor ? monitor.width : 0,
height: monitor ? monitor.height : 0,
refresh: monitor ? Math.round(monitor.refreshRate) : 0,
refresh: monitor ? monitor.refreshRate : 0,
mode: monitor ? monitor.mode : "",
scale: monitor ? monitor.scale : 0,
transform: monitor ? monitor.transform : -1,
modes: monitor ? monitor.modes.length : 0,
@@ -31,14 +32,14 @@ ShellRoot {
function applyScale(scale: real): bool {
const monitor = Displays.monitors[0];
if (!monitor) return false;
const mode = monitor.width + "x" + monitor.height + "@" + Math.round(monitor.refreshRate);
const mode = monitor.mode;
return Displays.apply(monitor.name, mode, scale, monitor.transform);
}
function applyBad(kind: string): bool {
const monitor = Displays.monitors[0];
if (!monitor) return false;
const mode = monitor.width + "x" + monitor.height + "@" + Math.round(monitor.refreshRate);
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") {
@@ -20,11 +20,24 @@ SettingsPage {
title: "Displays"
lede: SystemSettings.monitorDescription || "Reading the active display…"
readonly property var monitor: Displays.monitors.length > 0 ? Displays.monitors[0] : null
property string selectedOutput: ""
readonly property var monitor: Displays.monitorNamed(root.selectedOutput)
?? (Displays.monitors.length > 0 ? Displays.monitors[0] : null)
readonly property string currentMode: root.monitor
? `${root.monitor.width}x${root.monitor.height}@${Math.round(root.monitor.refreshRate)}`
? root.monitor.mode
: ""
function syncSelectedOutput(): void {
if (!Displays.monitorNamed(root.selectedOutput))
root.selectedOutput = Displays.monitors.length > 0 ? Displays.monitors[0].name : "";
}
Component.onCompleted: root.syncSelectedOutput()
Connections {
target: Displays
function onMonitorsChanged(): void { root.syncSelectedOutput(); }
}
// The confirmation sits above everything, because while it is counting down
// it is the only thing that matters on this page.
header: Component {
@@ -86,6 +99,25 @@ SettingsPage {
}
}
SettingsCard {
visible: Displays.monitors.length > 1
title: "Connected display"
subtitle: "Choose the output whose resolution, scale, and rotation you want to adjust."
ChoiceGrid {
width: parent.width
label: "Display"
options: Displays.monitors.map(monitor => ({
value: monitor.name,
label: monitor.description || monitor.name
}))
current: root.monitor ? root.monitor.name : ""
enabled: !Displays.awaitingConfirmation && !Displays.busy
divider: false
onPicked: value => root.selectedOutput = value
}
}
SettingsCard {
title: root.monitor ? root.monitor.name : (SystemSettings.monitorName || "Active display")
subtitle: root.monitor
+75 -23
View File
@@ -36,6 +36,8 @@ Singleton {
property var pendingRequested: null
property bool pendingVerified: false
property bool revertQueued: false
property var revertExpected: null
property string revertReason: ""
property int secondsLeft: 0
readonly property bool awaitingConfirmation: root.pendingOutput !== ""
@@ -43,6 +45,7 @@ Singleton {
&& root.pendingVerified
&& !root.busy
readonly property bool busy: query.running || applyRun.running || revertRun.running
|| root.revertExpected !== null
readonly property int confirmSeconds: 15
@@ -91,9 +94,10 @@ Singleton {
Process {
id: revertRun
onExited: (exitCode, exitStatus) => {
if (exitCode !== 0)
root.lastError = "The previous display setting could not be restored automatically.";
root.refresh();
// Exit status is advisory only. Hyprland's Lua bridge can report
// success without applying a value, so exact readback decides.
revertVerifyTimer.attempts = 0;
revertVerifyTimer.restart();
}
}
@@ -107,22 +111,42 @@ Singleton {
function parse(text: string): void {
try {
const raw = JSON.parse(text);
root.monitors = raw.map(monitor => ({
name: monitor.name ?? "",
description: monitor.description ?? monitor.model ?? "Display",
width: monitor.width ?? 0,
height: monitor.height ?? 0,
refreshRate: monitor.refreshRate ?? 0,
scale: monitor.scale ?? 1,
transform: monitor.transform ?? 0,
modes: root.normaliseModes(monitor.availableModes ?? [])
}));
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,
modes: modes
};
});
if (root.awaitingConfirmation && root.pendingRequested
&& root.matchesRequest(root.monitorNamed(root.pendingOutput), root.pendingRequested)) {
root.pendingVerified = true;
verifyTimer.stop();
root.lastError = "";
} else if (!root.awaitingConfirmation && (
} else if (root.revertExpected
&& root.matchesRequest(root.monitorNamed(root.revertExpected.output), root.revertExpected)) {
revertVerifyTimer.stop();
root.revertExpected = null;
if (root.revertReason === "")
root.lastError = "";
else
root.lastError = root.revertReason;
root.revertReason = "";
} else if (!root.awaitingConfirmation && !root.revertExpected && (
root.lastError === "Could not read the connected displays."
|| root.lastError === "The display list could not be read.")) {
root.lastError = "";
@@ -133,9 +157,9 @@ Singleton {
}
// "[email protected]" -> a sortable record. The compositor reports the same
// resolution several times at refresh rates that differ only in rounding
// (60.00 and 59.94), which as a list of buttons is noise rather than
// choice, so equal rounded pairs collapse to one.
// 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 = [];
@@ -145,15 +169,19 @@ Singleton {
continue;
const width = Number(match[1]);
const height = Number(match[2]);
const refresh = Math.round(Number(match[3]));
const key = `${width}x${height}@${refresh}`;
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: `${refresh} Hz`,
mode: key,
refreshLabel: Math.abs(refresh - roundedRefresh) < 0.005
? `${roundedRefresh} Hz`
: `${refresh.toFixed(2)} Hz`,
mode: `${width}x${height}@${refreshText}`,
width: width,
height: height,
refresh: refresh
@@ -207,7 +235,7 @@ Singleton {
return !!parts
&& monitor.width === parts.width
&& monitor.height === parts.height
&& Math.abs(monitor.refreshRate - parts.refresh) < 0.6
&& Math.abs(monitor.refreshRate - parts.refresh) < 0.01
&& Math.abs(monitor.scale - requested.scale) < 0.001
&& monitor.transform === requested.transform;
}
@@ -215,6 +243,10 @@ Singleton {
// Applies immediately and starts the countdown. Nothing is stored yet: the
// settings file is only written by confirm().
function apply(output: string, mode: string, scale: real, transform: int): bool {
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;
@@ -239,7 +271,7 @@ Singleton {
root.pendingPrevious = {
output: output,
mode: `${monitor.width}x${monitor.height}@${Math.round(monitor.refreshRate)}`,
mode: monitor.mode,
scale: monitor.scale,
transform: monitor.transform
};
@@ -313,6 +345,7 @@ Singleton {
countdown.stop();
verifyTimer.stop();
root.pendingVerified = false;
root.revertReason = message;
if (message !== "")
root.lastError = message;
if (applyRun.running) {
@@ -324,6 +357,7 @@ Singleton {
function performRevert(): void {
const previous = root.pendingPrevious;
root.revertExpected = previous;
root.clearPending();
if (previous) {
revertRun.exec(["hyprctl", "eval",
@@ -362,6 +396,24 @@ Singleton {
}
}
Timer {
id: revertVerifyTimer
property int attempts: 0
interval: 120
repeat: true
onTriggered: {
attempts++;
if (attempts > 25) {
stop();
root.revertExpected = null;
root.revertReason = "";
root.lastError = "The previous display setting could not be verified. Open Displays and restore it manually.";
return;
}
root.refresh();
}
}
Timer {
id: countdown
interval: 1000