Review everything shipped this weekend, and fix what the reviewers caught

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
Gabriel Brown
2026-08-24 13:29:42 -04:00
parent ffce48964e
commit 07db1068f1
42 changed files with 959 additions and 219 deletions
+82 -9
View File
@@ -97,8 +97,18 @@ Singleton {
if (root.transaction === null)
return;
if (exitCode !== 0) {
root.lastError = "Hyprpaper did not apply that background.";
root.transaction = null;
// Coming back from a video, hyprpaper's service is being
// started again underneath us and may not have its socket yet.
// That is a "not ready", not a "no", so the handoff is retried
// a few times before it counts as a failure.
if (root.pendingStillPolicy !== null
&& root.stillHandoffAttempts < root.stillHandoffLimit) {
stillAfterVideo.restart();
return;
}
root.lastError = "Hyprpaper did not apply that background.";
root.abandonStillHandoff();
root.schedulePendingHotplug();
return;
}
@@ -116,6 +126,34 @@ Singleton {
onExited: (exitCode, exitStatus) => root.finishVerification(exitCode)
}
// `busy` is the gate on every apply path, on refreshActive and on the
// slideshow timer, and a transaction is the only thing that clears it. So a
// hyprctl that never returns does not fail a wallpaper change — it takes the
// whole wallpaper surface out of service for the rest of the session, with
// no error to say why. Nothing here is a ten-second operation.
Timer {
id: transactionWatchdog
interval: 10000
onTriggered: {
if (root.transaction === null)
return;
root.transaction = null;
root.abandonStillHandoff();
// Killing the stuck child is part of the release: `busy` counts the
// processes too, so leaving one running would keep the latch shut.
applyProcess.running = false;
verifyProcess.running = false;
root.lastError = "Hyprpaper stopped responding while setting that background.";
}
}
onTransactionChanged: {
if (root.transaction === null)
transactionWatchdog.stop();
else
transactionWatchdog.restart();
}
Component.onCompleted: {
root.rescan();
root.refreshActive();
@@ -253,11 +291,17 @@ Singleton {
const normalized = root.normalizePolicy(policy);
if (normalized === null) {
root.lastError = "That wallpaper policy is not valid.";
// Nothing will reach schedulePendingHotplug from here, and a flag
// left standing turns the next unrelated apply into a surprise
// hotplug reapply. The signature change that armed it will arm it
// again if a display really did move.
root.pendingHotplugReapply = false;
return false;
}
const outputs = root.outputNames();
if (outputs.length === 0) {
root.lastError = "No display to set a wallpaper on.";
root.pendingHotplugReapply = false;
return false;
}
const expected = WallpaperPolicy.effectiveMap(
@@ -266,6 +310,7 @@ Singleton {
if (Object.keys(expected).length !== outputs.length
|| Object.values(expected).some(path => path === "")) {
root.lastError = "That wallpaper policy is not valid.";
root.pendingHotplugReapply = false;
return false;
}
root.lastError = "";
@@ -318,6 +363,7 @@ Singleton {
if (!matches) {
root.lastError = "Hyprpaper did not confirm that background.";
root.transaction = null;
root.abandonStillHandoff();
root.schedulePendingHotplug();
return;
}
@@ -325,6 +371,7 @@ Singleton {
const completed = root.transaction;
root.activeByOutput = observed;
root.transaction = null;
root.abandonStillHandoff();
root.lastError = "";
if (completed.automatic) {
root.slideshowPath = completed.policy.slideshowPath;
@@ -413,28 +460,53 @@ Singleton {
policy.mode = "single";
policy.globalPath = effectivePath;
policy.slideshowPath = effectivePath;
// Returning from a video: stop mpvpaper first, then give hyprpaper's
// service a beat to come back before the transaction talks to it.
// Returning from a video: stop mpvpaper first, then offer the policy to
// hyprpaper once its service is back, retrying while it is still coming
// up rather than betting on one long wait (see attemptStillHandoff).
if (VideoWallpaper.active) {
VideoWallpaper.stop();
root.pendingStillPolicy = policy;
root.stillHandoffAttempts = 0;
stillAfterVideo.restart();
return true;
}
return root.applyPolicy(policy, true, false);
}
// The still policy waiting for hyprpaper to come back, and how many times it
// has been offered to it. A single blind wait used to stand in for this: it
// was long enough to be felt on every switch and still short enough to lose
// whenever systemd was busy, and losing meant the desktop kept the video's
// last frame with an error in Settings and no wallpaper behind it.
property var pendingStillPolicy: null
property int stillHandoffAttempts: 0
readonly property int stillHandoffLimit: 3
Timer {
id: stillAfterVideo
interval: 900
onTriggered: {
if (root.pendingStillPolicy) {
root.applyPolicy(root.pendingStillPolicy, true, false);
root.pendingStillPolicy = null;
}
interval: 300
onTriggered: root.attemptStillHandoff()
}
function attemptStillHandoff(): void {
if (root.pendingStillPolicy === null)
return;
// The previous attempt's verification may still be winding down.
if (root.busy) {
stillAfterVideo.restart();
return;
}
root.stillHandoffAttempts += 1;
if (!root.applyPolicy(root.pendingStillPolicy, true, false)) {
// Rejected before hyprpaper was ever asked — retrying an invalid
// policy would only fail identically three more times.
root.abandonStillHandoff();
}
}
function abandonStillHandoff(): void {
root.pendingStillPolicy = null;
root.stillHandoffAttempts = 0;
}
function setMode(mode: string): bool {
@@ -458,6 +530,7 @@ Singleton {
DesktopPreferences.set("videoWallpaperPath", "");
VideoWallpaper.stop();
root.pendingStillPolicy = policy;
root.stillHandoffAttempts = 0;
stillAfterVideo.restart();
return true;
}