Verify display restoration and expose outputs
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user