Close desktop safety gaps

This commit is contained in:
Gabriel Brown
2026-08-20 22:00:26 -04:00
parent c4733e0624
commit c4642919f7
13 changed files with 194 additions and 35 deletions
@@ -29,8 +29,24 @@ Singleton {
// [{ name, description, width, height, refreshRate, scale, transform,
// modes: [{ label, mode, width, height, refresh }] }]
property var monitors: []
// Quickshell.screens is the topology authority. The override is only the
// isolated harness model; normal sessions always observe Quickshell.
property var screenOverride: null
property string lastError: ""
readonly property var screenModel: Array.isArray(root.screenOverride)
? root.screenOverride : Quickshell.screens
readonly property string screenSignature: root.screenModel
.map(screen => typeof screen === "string" ? screen : screen.name)
.filter(name => !!name)
.sort()
.join("|")
readonly property var primaryFirstMonitors: root.monitors.slice().sort((left, right) => {
if (left.primary !== right.primary)
return left.primary ? -1 : 1;
return left.name.localeCompare(right.name);
})
// Set while a change is applied but not yet confirmed.
property var pendingPreviousLayout: null
property var pendingRequestedLayout: null
@@ -113,6 +129,17 @@ Singleton {
Component.onCompleted: root.refresh()
onScreenSignatureChanged: root.reconcileTopology()
// A hotplug can invalidate the unconfirmed layout while the confirmation
// is visible. Read the current compositor layout first; the normal queued
// rollback then filters out any output that has disappeared.
function reconcileTopology(): void {
root.refresh();
if (root.awaitingConfirmation)
root.revertWithMessage("A display was connected or disconnected, so Panama restored the previous setting.");
}
function refresh(): bool {
if (!query.running) {
query.generation = root.operationGeneration;
+18 -7
View File
@@ -45,9 +45,10 @@ Singleton {
readonly property string outputSignature: root.outputNames().slice().sort().join("|")
property var outputNames: function() {
if (Array.isArray(root.outputOverride))
return root.outputOverride.slice();
return Quickshell.screens.map(screen => screen.name).filter(name => !!name);
const outputs = Array.isArray(root.outputOverride)
? root.outputOverride.slice()
: Quickshell.screens.map(screen => screen.name).filter(name => !!name);
return root.primaryFirstOutputs(outputs);
}
readonly property var searchRoots: [
@@ -59,10 +60,8 @@ Singleton {
Process {
id: scan
command: ["bash", "-lc",
"find " + root.searchRoots.map(dir => `'${dir}'`).join(" ")
+ " -maxdepth 2 -type f \\( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.webp' \\)"
+ " -printf '%T@ %p\\n' 2>/dev/null | sort -rn | cut -d' ' -f2- | head -60"]
command: [Quickshell.shellDir + "/scripts/panama-wallpaper-scan"]
.concat(root.searchRoots)
stdout: StdioCollector {
onStreamFinished: {
root.available = this.text.split("\n")
@@ -164,6 +163,18 @@ Singleton {
activeQuery.running = true;
}
// The saved primary role anchors Panama's per-monitor choices. Keep the
// compositor's remaining order stable so a reconnect does not reshuffle
// the rest of the picker unnecessarily.
function primaryFirstOutputs(outputs: var): var {
const stored = DesktopPreferences.get("displays");
const layouts = stored && typeof stored === "object" ? stored : {};
const primary = outputs.find(output => layouts[output]?.primary === true);
return primary === undefined
? outputs
: [primary].concat(outputs.filter(output => output !== primary));
}
function candidates(extra: var): var {
const result = [];
const discovered = Array.isArray(root.candidateOverride)