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
126 lines
4.3 KiB
QML
126 lines
4.3 KiB
QML
pragma Singleton
|
|
|
|
// The system locale.
|
|
//
|
|
// Named SystemLocale, not Locale: QML has a built-in Locale value type, and a
|
|
// singleton of that name is silently shadowed by it. Every binding then reads
|
|
// properties off the wrong thing and the page renders empty with only
|
|
// "Cannot read property of undefined" to show for it.
|
|
//
|
|
// Changing it is privileged: localectl goes through polkit, which prompts
|
|
// (hyprpolkitagent serves that in this session). It also only applies to
|
|
// programs started afterwards, so `pendingRestart` goes true once a change is
|
|
// accepted and the page says a sign-out is needed. Reporting the new locale as
|
|
// simply "in effect" would be wrong -- almost nothing on screen would be using
|
|
// it yet.
|
|
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import QtQuick
|
|
|
|
Singleton {
|
|
id: root
|
|
|
|
readonly property string helperPath: Quickshell.shellDir + "/scripts/panama-locale"
|
|
|
|
// [{ value, label, detail }]
|
|
property var locales: []
|
|
property string current: ""
|
|
property bool scanning: false
|
|
property string lastError: ""
|
|
|
|
// True once a change has been accepted but the session has not restarted,
|
|
// so the UI can stop claiming the new locale is already in use.
|
|
property bool pendingRestart: false
|
|
|
|
readonly property string currentLabel: {
|
|
const match = root.locales.find(locale => locale.value === root.current);
|
|
return match ? match.label : root.current;
|
|
}
|
|
|
|
function refresh(): void {
|
|
if (root.scanning)
|
|
return;
|
|
root.scanning = true;
|
|
readCurrent.running = true;
|
|
list.running = true;
|
|
}
|
|
|
|
// A picker click while a change is still applying is queued rather than
|
|
// fired directly: assigning `running = true` to an already-running
|
|
// Process is a no-op, so a second set() here would otherwise be silently
|
|
// dropped and apply.onExited would then adopt the second value as if it
|
|
// had actually been applied. requestedValue always holds the latest
|
|
// request; apply.onExited re-fires with it once the in-flight apply
|
|
// settles, mirroring Brightness.qml's pending-write queue.
|
|
property string requestedValue: ""
|
|
|
|
function set(value: string): void {
|
|
if (value === root.current)
|
|
return;
|
|
root.requestedValue = value;
|
|
if (!apply.running)
|
|
root._applyValue(value);
|
|
}
|
|
|
|
function _applyValue(value: string): void {
|
|
root.requestedValue = "";
|
|
apply.command = [root.helperPath, "set", value];
|
|
apply.pendingValue = value;
|
|
apply.running = true;
|
|
}
|
|
|
|
Process {
|
|
id: list
|
|
command: [root.helperPath, "list"]
|
|
stdout: StdioCollector {
|
|
onStreamFinished: {
|
|
try {
|
|
const parsed = JSON.parse(this.text);
|
|
root.locales = Array.isArray(parsed) ? parsed : [];
|
|
} catch (error) {
|
|
root.locales = [];
|
|
console.warn("SystemLocale: could not parse the locale list:", error);
|
|
}
|
|
root.scanning = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: readCurrent
|
|
command: [root.helperPath, "get"]
|
|
stdout: StdioCollector {
|
|
onStreamFinished: root.current = this.text.trim()
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: apply
|
|
|
|
property string pendingValue: ""
|
|
|
|
// A refused change -- polkit dismissed, or an unknown locale -- must not
|
|
// move the UI. The value is only adopted on a zero exit.
|
|
onExited: code => {
|
|
if (code === 0) {
|
|
root.current = apply.pendingValue;
|
|
root.pendingRestart = true;
|
|
root.lastError = "";
|
|
} else {
|
|
root.lastError = "The system did not accept that language. It may have needed a password.";
|
|
}
|
|
|
|
// A set() call that arrived while this apply was running only
|
|
// queued itself in requestedValue (see the comment above). Fire
|
|
// it now if it still names something other than what was just
|
|
// applied, so the UI never settles on a locale the system was
|
|
// never actually asked for.
|
|
if (root.requestedValue !== "" && root.requestedValue !== apply.pendingValue)
|
|
root._applyValue(root.requestedValue);
|
|
else
|
|
root.requestedValue = "";
|
|
}
|
|
}
|
|
}
|