Files
Panama/config/dot/quickshell/services/DefaultApps.qml
T
Gabriel Brown b743f44c5b Make changing a default application actually take effect
Three bugs, all silent, all in the same feature.

The write always worked. What failed was the refresh after it. That
refresh is called from the mutation's own onExited handler and was
guarded on `busy`, a binding over both processes -- and a binding hands
back its cached value until the change notification feeding it has been
delivered, which inside that handler has not happened yet. So `busy`
read true, refresh returned immediately, and the page kept showing the
old application with no error anywhere. Guards now read the Process
objects directly, where the value is current, and a refresh is no longer
blocked by the mutation that asked for it.

The service also kept its own list of which roles it would accept. It
stayed at seven when the helper and the page grew documents, text and
archives, so choosing a PDF viewer set an error and changed nothing.
It is derived from the snapshot now.

And category matching never worked. DesktopEntries returns a QML list,
for which Array.isArray is false, so the code stringified it into
"Network,WebBrowser" and split on ";" alone -- one token matching no
category. Browsers still appeared because their generic name contains
"web browser" and the terms fallback carried the role by itself.
Archives matched nothing at all, so that row could only ever offer the
application it already had.

The harness that should have caught the first bug passed while it was
live: it set each role to the value it already had and asserted no error
appeared, and the bug produces no error. It now changes a role to a
genuinely different application, requires the service to observe the new
value, and changes it back -- with the contract restoring the original
from the outside however the run ends.

Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
2026-08-19 11:59:53 -04:00

137 lines
5.1 KiB
QML

pragma Singleton
// Freedesktop default handlers and session autostart entries.
//
// The helper owns parsing and atomic desktop-file writes. This singleton keeps
// the QML side typed and reactive, and every external command crosses Process
// as an argument array.
import Quickshell
import Quickshell.Io
import QtQuick
Singleton {
id: root
property var handlers: ({})
property var autostartEntries: []
property var luaAutostartEntries: []
property string lastError: ""
// For the UI, which wants one answer to "is anything happening".
//
// Guards inside this file do NOT use it. `busy` is a binding, and a binding
// hands back its cached value until the change notification that feeds it
// has been delivered. Inside a process's own onExited handler that has not
// happened yet, so `busy` still reads true there -- which silently turned
// the refresh after every successful write into a no-op. The write landed
// and the settings page never noticed, which looks exactly like a settings
// page that cannot change anything. Guards below read the Process objects
// directly, where the value is current.
readonly property bool busy: snapshotProcess.running || mutationProcess.running
readonly property string helper: Quickshell.shellDir + "/scripts/panama-default-apps"
// Derived from the snapshot, never restated. This was a hardcoded list of
// seven, and when the helper and the page grew documents, text and archives
// it stayed at seven -- so choosing a PDF viewer set lastError and did
// nothing, which reads exactly like a settings page that does not work.
//
// The snapshot already reports one handler per role the helper supports, so
// that IS the list. An empty one means no snapshot has landed yet; the
// helper validates the role itself and reports a failure, so there is
// nothing for this guard to add before then.
readonly property var supportedRoles: Object.keys(root.handlers ?? ({}))
Process {
id: snapshotProcess
stdout: StdioCollector {
onStreamFinished: root.applySnapshot(this.text)
}
onExited: (exitCode, exitStatus) => {
if (exitCode !== 0)
root.lastError = "Default applications could not be read. Try refreshing."
}
}
Process {
id: mutationProcess
onExited: (exitCode, exitStatus) => {
if (exitCode !== 0) {
root.lastError = "That application setting could not be changed."
return;
}
root.refresh();
}
}
function applySnapshot(text: string): void {
try {
const payload = JSON.parse(text);
root.handlers = payload.handlers ?? ({});
root.autostartEntries = payload.autostartEntries ?? [];
root.luaAutostartEntries = payload.luaAutostartEntries ?? [];
root.lastError = "";
} catch (error) {
root.lastError = "Default applications returned an unreadable response."
}
}
function refresh(): void {
// Only a snapshot already in flight is a reason not to start another.
// A mutation finishing is the single best reason TO refresh.
if (snapshotProcess.running)
return;
root.lastError = "";
snapshotProcess.exec([root.helper, "snapshot"]);
}
function knownDesktopId(desktopId: string): bool {
if (!/^[A-Za-z0-9][A-Za-z0-9._+-]*\.desktop$/.test(desktopId))
return false;
const entries = DesktopEntries.applications.values;
return entries.some(entry => {
const entryId = String(entry.id ?? "");
return entryId === desktopId || entryId + ".desktop" === desktopId;
});
}
function setDefault(role: string, desktopId: string): void {
if (mutationProcess.running)
return;
const roleIsKnown = root.supportedRoles.length === 0
|| root.supportedRoles.includes(role);
if (!roleIsKnown || !root.knownDesktopId(desktopId)) {
root.lastError = "Choose an application from the available list."
return;
}
root.lastError = "";
mutationProcess.exec([root.helper, "set-default", role, desktopId]);
}
function setAutostart(desktopId: string, enabled: bool): void {
if (mutationProcess.running)
return;
const known = root.autostartEntries.some(entry => entry.id === desktopId);
if (!known) {
root.lastError = "That user autostart entry is no longer available."
return;
}
root.lastError = "";
mutationProcess.exec([root.helper, "set-autostart", desktopId, String(enabled)]);
}
function addAutostart(desktopId: string): void {
if (mutationProcess.running)
return;
if (!root.knownDesktopId(desktopId)) {
root.lastError = "Choose an installed application.";
return;
}
root.lastError = "";
mutationProcess.exec([root.helper, "add-autostart", desktopId]);
}
Component.onCompleted: root.refresh()
}