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 = ""; } } }