Harden display apply and recovery
This commit is contained in:
@@ -33,10 +33,16 @@ Singleton {
|
||||
// Set while a change is applied but not yet confirmed.
|
||||
property string pendingOutput: ""
|
||||
property var pendingPrevious: null
|
||||
property var pendingRequested: null
|
||||
property bool pendingVerified: false
|
||||
property bool revertQueued: false
|
||||
property int secondsLeft: 0
|
||||
|
||||
readonly property bool awaitingConfirmation: root.pendingOutput !== ""
|
||||
readonly property bool busy: query.running || applyRun.running
|
||||
readonly property bool canConfirm: root.awaitingConfirmation
|
||||
&& root.pendingVerified
|
||||
&& !root.busy
|
||||
readonly property bool busy: query.running || applyRun.running || revertRun.running
|
||||
|
||||
readonly property int confirmSeconds: 15
|
||||
|
||||
@@ -66,7 +72,29 @@ Singleton {
|
||||
|
||||
Process {
|
||||
id: applyRun
|
||||
onExited: (exitCode, exitStatus) => root.refresh()
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
if (!root.awaitingConfirmation)
|
||||
return;
|
||||
if (root.revertQueued) {
|
||||
root.performRevert();
|
||||
return;
|
||||
}
|
||||
if (exitCode !== 0) {
|
||||
root.revertWithMessage("The display rejected that change and Panama restored the previous setting.");
|
||||
return;
|
||||
}
|
||||
verifyTimer.attempts = 0;
|
||||
verifyTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: revertRun
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
if (exitCode !== 0)
|
||||
root.lastError = "The previous display setting could not be restored automatically.";
|
||||
root.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: root.refresh()
|
||||
@@ -89,7 +117,14 @@ Singleton {
|
||||
transform: monitor.transform ?? 0,
|
||||
modes: root.normaliseModes(monitor.availableModes ?? [])
|
||||
}));
|
||||
root.lastError = "";
|
||||
if (root.awaitingConfirmation && root.pendingRequested
|
||||
&& root.matchesRequest(root.monitorNamed(root.pendingOutput), root.pendingRequested)) {
|
||||
root.pendingVerified = true;
|
||||
verifyTimer.stop();
|
||||
root.lastError = "";
|
||||
} else if (!root.awaitingConfirmation) {
|
||||
root.lastError = "";
|
||||
}
|
||||
} catch (error) {
|
||||
root.lastError = "The display list could not be read.";
|
||||
}
|
||||
@@ -129,6 +164,52 @@ Singleton {
|
||||
return root.monitors.find(monitor => monitor.name === name) ?? null;
|
||||
}
|
||||
|
||||
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 matchesRequest(monitor: var, requested: var): bool {
|
||||
if (!monitor || !requested || monitor.name !== requested.output)
|
||||
return false;
|
||||
const parts = root.modeParts(requested.mode);
|
||||
return !!parts
|
||||
&& monitor.width === parts.width
|
||||
&& monitor.height === parts.height
|
||||
&& Math.abs(monitor.refreshRate - parts.refresh) < 0.6
|
||||
&& Math.abs(monitor.scale - requested.scale) < 0.001
|
||||
&& monitor.transform === requested.transform;
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -145,8 +226,8 @@ Singleton {
|
||||
root.lastError = "That display does not offer that mode.";
|
||||
return false;
|
||||
}
|
||||
if (root.scales.indexOf(scale) < 0) {
|
||||
root.lastError = "That scale is not one Panama offers.";
|
||||
if (!root.isScaleClean(mode, scale)) {
|
||||
root.lastError = "That scale does not divide this resolution cleanly.";
|
||||
return false;
|
||||
}
|
||||
if (!root.transforms.some(candidate => candidate.value === transform)) {
|
||||
@@ -160,8 +241,17 @@ Singleton {
|
||||
scale: monitor.scale,
|
||||
transform: monitor.transform
|
||||
};
|
||||
root.pendingRequested = {
|
||||
output: output,
|
||||
mode: mode,
|
||||
scale: scale,
|
||||
transform: transform
|
||||
};
|
||||
root.pendingOutput = output;
|
||||
root.pendingVerified = false;
|
||||
root.revertQueued = false;
|
||||
root.secondsLeft = root.confirmSeconds;
|
||||
root.lastError = "";
|
||||
countdown.restart();
|
||||
|
||||
root.push(output, mode, scale, transform);
|
||||
@@ -175,39 +265,68 @@ Singleton {
|
||||
`hl.monitor({ output = "${output}", mode = "${mode}", scale = ${scale}, transform = ${transform} })`]);
|
||||
}
|
||||
|
||||
function confirm(): void {
|
||||
if (!root.awaitingConfirmation)
|
||||
return;
|
||||
const monitor = root.monitorNamed(root.pendingOutput);
|
||||
countdown.stop();
|
||||
|
||||
if (monitor) {
|
||||
const stored = DesktopPreferences.get("displays");
|
||||
const next = Object.assign({}, (stored && typeof stored === "object") ? stored : {});
|
||||
next[root.pendingOutput] = {
|
||||
mode: `${monitor.width}x${monitor.height}@${Math.round(monitor.refreshRate)}`,
|
||||
scale: monitor.scale,
|
||||
transform: monitor.transform
|
||||
};
|
||||
DesktopPreferences.set("displays", next);
|
||||
function confirm(): bool {
|
||||
if (!root.canConfirm || !root.matchesRequest(
|
||||
root.monitorNamed(root.pendingOutput), root.pendingRequested)) {
|
||||
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 : {});
|
||||
next[root.pendingOutput] = {
|
||||
mode: root.pendingRequested.mode,
|
||||
scale: root.pendingRequested.scale,
|
||||
transform: root.pendingRequested.transform
|
||||
};
|
||||
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.pendingOutput = "";
|
||||
root.pendingPrevious = null;
|
||||
root.pendingRequested = null;
|
||||
root.pendingVerified = false;
|
||||
root.revertQueued = false;
|
||||
root.secondsLeft = 0;
|
||||
root.lastError = "";
|
||||
}
|
||||
|
||||
function revert(): void {
|
||||
root.revertWithMessage("");
|
||||
}
|
||||
|
||||
function revertWithMessage(message: string): void {
|
||||
if (!root.awaitingConfirmation)
|
||||
return;
|
||||
const previous = root.pendingPrevious;
|
||||
countdown.stop();
|
||||
root.pendingOutput = "";
|
||||
root.pendingPrevious = null;
|
||||
root.secondsLeft = 0;
|
||||
if (previous)
|
||||
root.push(previous.output, previous.mode, previous.scale, previous.transform);
|
||||
verifyTimer.stop();
|
||||
root.pendingVerified = false;
|
||||
if (message !== "")
|
||||
root.lastError = message;
|
||||
if (applyRun.running) {
|
||||
root.revertQueued = true;
|
||||
return;
|
||||
}
|
||||
root.performRevert();
|
||||
}
|
||||
|
||||
function performRevert(): void {
|
||||
const previous = root.pendingPrevious;
|
||||
root.clearPending();
|
||||
if (previous) {
|
||||
revertRun.exec(["hyprctl", "eval",
|
||||
`hl.monitor({ output = "${previous.output}", mode = "${previous.mode}", scale = ${previous.scale}, transform = ${previous.transform} })`]);
|
||||
}
|
||||
}
|
||||
|
||||
// Clears any stored override for an output so it returns to the value
|
||||
@@ -226,6 +345,21 @@ Singleton {
|
||||
return !!(stored && typeof stored === "object" && stored[output] !== undefined);
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: verifyTimer
|
||||
property int attempts: 0
|
||||
interval: 120
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
attempts++;
|
||||
if (attempts > 25) {
|
||||
root.revertWithMessage("The display did not apply that setting, so Panama restored the previous one.");
|
||||
return;
|
||||
}
|
||||
root.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: countdown
|
||||
interval: 1000
|
||||
|
||||
Reference in New Issue
Block a user