A Process's exited and streamFinished signals aren't guaranteed to fire in order, and several services decided an outcome on whichever fired first: KdeConnect could report a successful file transfer as failed if exited landed before the real stdout payload; Clipboard could present a failed history query as an empty-but-healthy one; Brightness could strand the last queued write of a drag; SoundFeedback and SystemLocale could drop or misapply a rapid second toggle/click because re-arming an already-running Process is a no-op. All five now wait for both signals and let the authoritative one decide, matching the pattern HomeAssistantConfig.qml already used correctly. Health's "copy report" never enabled stdin, so it copied nothing while claiming success. Capture announced every recording as saved regardless of the recorder's actual exit code. Connectivity never restarted Bluetooth discovery when the adapter was enabled from an already-open page. CalendarAgenda left the UI in "loading" forever if its helper died at startup, and the helper itself could crash unguarded instead of reporting unavailable. Geocoding silently dropped a query typed while the previous one was still in flight. Notifs leaked tracked-but-undisplayed notifications under Do Not Disturb, and dismissAll() skipped them. Claude-Session: https://claude.ai/code/session_01E6TJUAh41HaP25MVHWkhRZ
123 lines
3.9 KiB
QML
123 lines
3.9 KiB
QML
pragma Singleton
|
|
|
|
// GNOME and GTK applications already honour these desktop sound preferences.
|
|
// Panama controls the same durable keys so moving between sessions does not
|
|
// create two competing notions of whether event feedback is enabled.
|
|
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import QtQuick
|
|
|
|
Singleton {
|
|
id: root
|
|
|
|
property bool eventSounds: true
|
|
property bool inputFeedback: false
|
|
property string lastError: ""
|
|
readonly property bool busy: eventRead.running || inputRead.running
|
|
|| eventWrite.running || inputWrite.running
|
|
|
|
function parsedBoolean(text: string, fallback: bool): bool {
|
|
const value = text.trim();
|
|
if (value === "true")
|
|
return true;
|
|
if (value === "false")
|
|
return false;
|
|
return fallback;
|
|
}
|
|
|
|
function refresh(): void {
|
|
if (!eventRead.running)
|
|
eventRead.running = true;
|
|
if (!inputRead.running)
|
|
inputRead.running = true;
|
|
}
|
|
|
|
function setEventSounds(enabled: bool): void {
|
|
root.eventSounds = enabled;
|
|
root._writeEventSounds();
|
|
}
|
|
|
|
function setInputFeedback(enabled: bool): void {
|
|
root.inputFeedback = enabled;
|
|
root._writeInputFeedback();
|
|
}
|
|
|
|
// Assigning `running = true` to a Process that is already running is a
|
|
// no-op, not a queue -- so a toggle that lands mid-write would otherwise
|
|
// be dropped silently. eventWrite.onExited re-checks root.eventSounds
|
|
// against what was actually written and calls this again if they still
|
|
// disagree.
|
|
function _writeEventSounds(): void {
|
|
if (eventWrite.running)
|
|
return;
|
|
eventWrite.writtenValue = root.eventSounds;
|
|
eventWrite.command = ["gsettings", "set", "org.gnome.desktop.sound", "event-sounds", String(root.eventSounds)];
|
|
eventWrite.running = true;
|
|
}
|
|
|
|
function _writeInputFeedback(): void {
|
|
if (inputWrite.running)
|
|
return;
|
|
inputWrite.writtenValue = root.inputFeedback;
|
|
inputWrite.command = ["gsettings", "set", "org.gnome.desktop.sound", "input-feedback-sounds", String(root.inputFeedback)];
|
|
inputWrite.running = true;
|
|
}
|
|
|
|
Process {
|
|
id: eventRead
|
|
command: ["gsettings", "get", "org.gnome.desktop.sound", "event-sounds"]
|
|
stdout: StdioCollector {
|
|
onStreamFinished: root.eventSounds = root.parsedBoolean(this.text, root.eventSounds)
|
|
}
|
|
onExited: (code, status) => {
|
|
if (code !== 0)
|
|
root.lastError = "Event sound preferences could not be read.";
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: inputRead
|
|
command: ["gsettings", "get", "org.gnome.desktop.sound", "input-feedback-sounds"]
|
|
stdout: StdioCollector {
|
|
onStreamFinished: root.inputFeedback = root.parsedBoolean(this.text, root.inputFeedback)
|
|
}
|
|
onExited: (code, status) => {
|
|
if (code !== 0)
|
|
root.lastError = "Input feedback preferences could not be read.";
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: eventWrite
|
|
property bool writtenValue: true
|
|
onExited: (code, status) => {
|
|
if (code !== 0) {
|
|
root.lastError = "Event sound preferences could not be changed.";
|
|
root.refresh();
|
|
} else {
|
|
root.lastError = "";
|
|
}
|
|
if (root.eventSounds !== eventWrite.writtenValue)
|
|
root._writeEventSounds();
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: inputWrite
|
|
property bool writtenValue: false
|
|
onExited: (code, status) => {
|
|
if (code !== 0) {
|
|
root.lastError = "Input feedback preferences could not be changed.";
|
|
root.refresh();
|
|
} else {
|
|
root.lastError = "";
|
|
}
|
|
if (root.inputFeedback !== inputWrite.writtenValue)
|
|
root._writeInputFeedback();
|
|
}
|
|
}
|
|
|
|
Component.onCompleted: root.refresh()
|
|
}
|