diff --git a/config/dot/quickshell/config/PreferenceSchema.qml b/config/dot/quickshell/config/PreferenceSchema.qml index 4772613..858f90a 100644 --- a/config/dot/quickshell/config/PreferenceSchema.qml +++ b/config/dot/quickshell/config/PreferenceSchema.qml @@ -374,7 +374,13 @@ Singleton { key: "touchpadTapToClick", type: "bool", def: true, group: "touchpad", label: "Tap to click", detail: "A tap counts as a click without pressing down", - hypr: { path: ["input", "touchpad", "tap-to-click"], option: "input:touchpad:tap-to-click", readAs: "bool" } + // The Lua config key and the hyprctl option name genuinely differ + // here: hl.config wants input.touchpad.tap_to_click, getoption + // answers to input:touchpad:tap-to-click. Using either spelling for + // both fails -- a hyphen is not a Lua identifier, and the + // underscored name is not a known option to getoption. This is what + // the two separate fields are for. + hypr: { path: ["input", "touchpad", "tap_to_click"], option: "input:touchpad:tap-to-click", readAs: "bool" } }, { key: "touchpadNaturalScroll", type: "bool", def: true, group: "touchpad", diff --git a/config/dot/quickshell/modules/settings/AboutPage.qml b/config/dot/quickshell/modules/settings/AboutPage.qml index d648c56..30b266e 100644 --- a/config/dot/quickshell/modules/settings/AboutPage.qml +++ b/config/dot/quickshell/modules/settings/AboutPage.qml @@ -4,9 +4,18 @@ import qs.config import qs.services SettingsPage { + id: root + title: "About Panama" lede: "A curated Hyprland desktop built around focus, speed, and good taste." + Component.onCompleted: { + if (!MachineInfo.scanned) + MachineInfo.refresh(); + if (GraphicsDevices.devices.length === 0) + GraphicsDevices.refresh(); + } + SettingsCard { title: "Panama Desktop" subtitle: "Tokyo Night Moon · Prism glass · native tiling" @@ -31,6 +40,52 @@ SettingsPage { } } + // What GNOME's About panel answers and this page did not: what am I running + // on. The rows come from MachineInfo, except graphics, which is joined from + // GraphicsDevices rather than read a second time -- two readouts of the same + // hardware are two things that can disagree. + SettingsCard { + title: "This machine" + subtitle: "Hardware and system, as the kernel reports it." + + Repeater { + model: MachineInfo.facts + + TextRow { + id: machineRow + required property var modelData + required property int index + + label: machineRow.modelData.label + value: machineRow.modelData.value + } + } + + Repeater { + model: GraphicsDevices.devices + + TextRow { + id: gpuRow + required property var modelData + required property int index + + label: GraphicsDevices.devices.length > 1 + ? "Graphics " + (gpuRow.index + 1) + : "Graphics" + value: gpuRow.modelData.name + divider: gpuRow.index < GraphicsDevices.devices.length - 1 + } + } + + TextRow { + visible: MachineInfo.scanned && MachineInfo.facts.length === 0 + label: "Hardware" + detail: "The system did not report anything readable" + value: "Unavailable" + divider: false + } + } + SettingsCard { title: "Design principles" diff --git a/config/dot/quickshell/modules/settings/RegionPage.qml b/config/dot/quickshell/modules/settings/RegionPage.qml new file mode 100644 index 0000000..b86812a --- /dev/null +++ b/config/dot/quickshell/modules/settings/RegionPage.qml @@ -0,0 +1,75 @@ +// Region & Language. +// +// GNOME keeps language and formats under System; the setting itself is the +// machine's locale, which localectl owns. Panama does not store a copy of it -- +// there is exactly one system locale and localectl is where it lives, so a +// preference here would be a second source of truth that drifts the moment +// anything else changes it. +// +// Keyboard layout is deliberately not repeated here even though GNOME groups it +// with region. It is a compositor setting that applies instantly, it lives on +// the Keyboard page with the rest of the typing settings, and showing it twice +// invites the two views to disagree. + +import QtQuick +import qs.config +import qs.services + +SettingsPage { + id: root + + title: "Region & Language" + lede: SystemLocale.pendingRestart + ? "Your new language applies to programs started after you sign out and back in." + : "The language and regional formats this machine uses." + + Component.onCompleted: if (SystemLocale.locales.length === 0) SystemLocale.refresh() + + SettingsCard { + title: "Language" + subtitle: "Changing this needs your password, and takes effect for programs started afterwards." + + TextRow { + label: "Current language" + detail: SystemLocale.pendingRestart + ? "Chosen, but not in use until you sign out and back in" + : "Used by programs that ask the system what language to speak" + value: SystemLocale.currentLabel || "Reading…" + } + + SearchPicker { + width: parent.width + items: SystemLocale.locales + current: SystemLocale.current + placeholder: "Search languages and regions" + emptyText: SystemLocale.scanning ? "Reading installed locales…" : "No locales are installed" + onPicked: value => SystemLocale.set(value) + } + } + + SettingsCard { + visible: SystemLocale.lastError !== "" + title: "Language problem" + subtitle: SystemLocale.lastError + } + + SettingsCard { + title: "Formats" + subtitle: "Dates, times, and numbers follow the language above. Panama's own clock formatting is on the Appearance page." + + ActionRow { + label: "Clock and date display" + detail: "How Panama itself shows the time" + action: "Open appearance" + onTriggered: ShellState.openSettings("appearance") + } + + ActionRow { + label: "Regional formats" + detail: "Separate per-category formats (LC_TIME, LC_NUMERIC) are owned by Fedora" + action: "Open system" + divider: false + onTriggered: SystemSettings.openGnomePanel("system", "region") + } + } +} diff --git a/config/dot/quickshell/modules/settings/SearchPicker.qml b/config/dot/quickshell/modules/settings/SearchPicker.qml new file mode 100644 index 0000000..6fa4922 --- /dev/null +++ b/config/dot/quickshell/modules/settings/SearchPicker.qml @@ -0,0 +1,94 @@ +// A searchable list of choices, for settings with far too many values to put in +// a dropdown and no useful preview to show. +// +// SearchPicker { +// items: Locale.locales // [{ value, label, detail }] +// current: Locale.current +// placeholder: "Search languages" +// onPicked: value => Locale.set(value) +// } +// +// FontPicker is the same idea specialised: it draws each candidate in its own +// family, which is the whole reason it is a list rather than a text field. This +// one is for choices whose only useful preview is their name. + +import QtQuick +import qs.config +// SearchField lives with the clipboard module, which is where it was first +// needed; FontPicker imports it from the same place. +import qs.modules.clipboard + +Column { + id: root + + // [{ value, label, detail }] + property var items: [] + property string current: "" + property string emptyText: "Nothing to choose from" + property string placeholder: "Search" + + signal picked(string value) + + spacing: 0 + + // Capped and filtered rather than listing everything: 327 locales inside a + // page that is already scrolling is worse than a search box. + readonly property var matches: { + const needle = filter.text.trim().toLowerCase(); + const list = root.items.filter(item => + needle === "" + ? true + : (String(item.label).toLowerCase().indexOf(needle) >= 0 + || String(item.detail).toLowerCase().indexOf(needle) >= 0)); + + // The current choice stays visible while browsing, so it is always + // clear what would be replaced. + const selected = list.find(item => item.value === root.current); + if (needle === "" && selected !== undefined) + return [selected].concat(list.filter(item => item.value !== root.current)).slice(0, 12); + return list.slice(0, 12); + } + + SearchField { + id: filter + width: parent.width + placeholder: root.placeholder + } + + Repeater { + model: root.matches + + SettingRow { + id: candidate + + required property var modelData + required property int index + + readonly property bool selected: candidate.modelData.value === root.current + + label: candidate.modelData.label + detail: candidate.selected ? "Currently in use" : candidate.modelData.detail + controlWidth: 120 + divider: candidate.index < root.matches.length - 1 + activatable: !candidate.selected + onActivated: root.picked(candidate.modelData.value) + + Text { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + visible: candidate.selected + text: "✓" + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSize + 2 + color: Theme.accent + } + } + } + + SettingRow { + width: parent.width + visible: root.matches.length === 0 + label: root.emptyText + divider: false + } +} diff --git a/config/dot/quickshell/modules/settings/ServicesPage.qml b/config/dot/quickshell/modules/settings/ServicesPage.qml index c6e9bd4..ea4129e 100644 --- a/config/dot/quickshell/modules/settings/ServicesPage.qml +++ b/config/dot/quickshell/modules/settings/ServicesPage.qml @@ -120,14 +120,40 @@ SettingsPage { SettingsCard { title: "Fedora system settings" - subtitle: "These remain owned by trusted system services and GNOME's mature panels." + subtitle: "Panels Panama does not own, because they configure system services rather than the desktop. Each row opens the panel that actually owns it. Printers and online accounts live with the rest of the network hardware, on Network & Devices." + + // This was one row listing five subjects and opening the network panel + // regardless. Naming a panel and then not opening it is worse than not + // offering it: it looks like a broken button rather than a deliberate + // hand-off, and someone looking for printers had to know to navigate + // once GNOME Settings appeared on the wrong page. + ActionRow { + label: "Users" + detail: "Accounts, passwords, and automatic login" + action: "Open users" + onTriggered: SystemSettings.openGnomePanel("system", "users") + } ActionRow { - label: "Network, Bluetooth, printers, users, and accounts" - detail: "GNOME Settings remains searchable from the launcher too" + label: "Sharing" + detail: "Remote desktop, media sharing, and remote login" + action: "Open sharing" + onTriggered: SystemSettings.openGnomePanel("sharing") + } + + ActionRow { + label: "Colour profiles" + detail: "ICC profiles for displays, printers, and scanners" + action: "Open colour" + onTriggered: SystemSettings.openGnomePanel("color") + } + + ActionRow { + label: "Digital wellbeing" + detail: "Screen time and break reminders" + action: "Open wellbeing" divider: false - action: "Open network" - onTriggered: SystemSettings.openGnomePanel("network") + onTriggered: SystemSettings.openGnomePanel("wellbeing") } } } diff --git a/config/dot/quickshell/modules/settings/SettingsShell.qml b/config/dot/quickshell/modules/settings/SettingsShell.qml index 033cdf6..10ac867 100644 --- a/config/dot/quickshell/modules/settings/SettingsShell.qml +++ b/config/dot/quickshell/modules/settings/SettingsShell.qml @@ -101,6 +101,7 @@ Rectangle { case "shortcuts": return shortcutsPage; case "mouse": return mousePage; case "privacy": return privacyPage; + case "region": return regionPage; case "accessibility": return accessibilityPage; case "power": return powerPage; case "datetime": return dateTimePage; @@ -157,6 +158,7 @@ Rectangle { Component { id: shortcutsPage; ShortcutsPage {} } Component { id: mousePage; MousePage {} } Component { id: privacyPage; PrivacyPage {} } + Component { id: regionPage; RegionPage {} } Component { id: servicesPage; ServicesPage {} } Component { id: aboutPage; AboutPage {} } diff --git a/config/dot/quickshell/modules/settings/SettingsSidebar.qml b/config/dot/quickshell/modules/settings/SettingsSidebar.qml index d82a99c..a7593cb 100644 --- a/config/dot/quickshell/modules/settings/SettingsSidebar.qml +++ b/config/dot/quickshell/modules/settings/SettingsSidebar.qml @@ -34,6 +34,7 @@ Rectangle { { page: "shortcuts", label: "Keyboard", icon: "\u{F030C}" }, { page: "mouse", label: "Mouse & Touchpad", icon: "\u{F037D}" }, { page: "privacy", label: "Privacy & Security", icon: "\u{F0483}" }, + { page: "region", label: "Region & Language", icon: "\u{F0AC2}" }, { page: "accessibility", label: "Accessibility", icon: "\u{F0208}" }, { page: "power", label: "Power & Lock", icon: "\u{F0425}" }, { page: "datetime", label: "Date & Time", icon: "\u{F0954}" }, diff --git a/config/dot/quickshell/modules/settings/qmldir b/config/dot/quickshell/modules/settings/qmldir index 464b268..99bbf5c 100644 --- a/config/dot/quickshell/modules/settings/qmldir +++ b/config/dot/quickshell/modules/settings/qmldir @@ -49,3 +49,5 @@ FontPicker 1.0 FontPicker.qml MousePage 1.0 MousePage.qml TextEntryRow 1.0 TextEntryRow.qml PrivacyPage 1.0 PrivacyPage.qml +RegionPage 1.0 RegionPage.qml +SearchPicker 1.0 SearchPicker.qml diff --git a/config/dot/quickshell/scripts/panama-about b/config/dot/quickshell/scripts/panama-about new file mode 100755 index 0000000..a902afe --- /dev/null +++ b/config/dot/quickshell/scripts/panama-about @@ -0,0 +1,81 @@ +#!/usr/bin/env bash + +# What this machine is, as JSON: {label, value} pairs in display order. +# +# GNOME's About panel answers "what am I running on" in one screen -- model, +# processor, memory, disk, OS, kernel, windowing system. Panama's About listed +# only its own component versions, which answers a different and much narrower +# question. +# +# Graphics is deliberately absent: GraphicsDevices already enumerates GPUs for +# the vitals readout, and naming them again here would be a second source of +# truth that could disagree with the first. The page joins the two. +# +# Anything unreadable is omitted rather than reported as "Unknown". These are +# facts about hardware, and a row saying "Processor: Unknown" is noise where +# simply not having the row is not. + +set -uo pipefail + +facts=() + +emit() { + [[ -n "${2:-}" ]] || return 0 + facts+=("$(jq -cn --arg label "$1" --arg value "$2" '{label: $label, value: $value}')") +} + +# ── Machine ────────────────────────────────────────────────────────────────── +# DMI strings are frequently placeholders ("To Be Filled By O.E.M.", "Default +# string"). Those are worse than nothing, so they are filtered out. +dmi() { + local value + value="$(cat "/sys/class/dmi/id/$1" 2>/dev/null)" || return 0 + case "$value" in + ""|"To Be Filled By O.E.M."*|"Default string"|"System Product Name"|"Unknown"|"None") return 0 ;; + esac + printf '%s' "$value" +} + +vendor="$(dmi sys_vendor)" +product="$(dmi product_name)" +if [[ -n "$vendor" && -n "$product" ]]; then + emit "Model" "$vendor $product" +else + emit "Model" "${product:-$vendor}" +fi + +emit "Hostname" "$(hostnamectl hostname 2>/dev/null || hostname 2>/dev/null)" + +# ── Processor ──────────────────────────────────────────────────────────────── +cpu="$(awk -F': ' '/^model name/ { print $2; exit }' /proc/cpuinfo 2>/dev/null)" +threads="$(nproc 2>/dev/null)" +if [[ -n "$cpu" ]]; then + # The marketing name usually already says "6-Core", so the thread count is + # the part that adds information. + [[ -n "$threads" ]] && cpu="$cpu ($threads threads)" + emit "Processor" "$cpu" +fi + +# ── Memory ─────────────────────────────────────────────────────────────────── +# Reported as the kernel sees it, which is a little under the sticker figure +# because firmware and integrated graphics reserve some before Linux starts. +emit "Memory" "$(awk '/^MemTotal:/ { printf "%.1f GiB", $2 / 1048576 }' /proc/meminfo 2>/dev/null)" + +# ── Storage ────────────────────────────────────────────────────────────────── +read -r size used avail <<<"$(df -h --output=size,used,avail / 2>/dev/null | tail -1)" +[[ -n "${size:-}" ]] && emit "Disk" "$avail free of $size" + +# ── Software ───────────────────────────────────────────────────────────────── +if [[ -r /etc/os-release ]]; then + # Sourced in a subshell so the variables cannot leak into this script. + os="$( . /etc/os-release 2>/dev/null && printf '%s' "${PRETTY_NAME:-$NAME}" )" + emit "Operating system" "$os" +fi +emit "Kernel" "$(uname -r 2>/dev/null)" + +case "${XDG_SESSION_TYPE:-}" in + wayland) emit "Windowing system" "Wayland" ;; + x11) emit "Windowing system" "X11" ;; +esac + +printf '[%s]\n' "$(IFS=,; printf '%s' "${facts[*]}")" diff --git a/config/dot/quickshell/scripts/panama-locale b/config/dot/quickshell/scripts/panama-locale new file mode 100755 index 0000000..cd8c8ee --- /dev/null +++ b/config/dot/quickshell/scripts/panama-locale @@ -0,0 +1,98 @@ +#!/usr/bin/env bash + +# System locale, via localectl. +# +# panama-locale list -> [{value, label, detail}] +# panama-locale get -> the current LANG, e.g. en_US.UTF-8 +# panama-locale set +# +# Locale codes are not names. "pt_BR.UTF-8" tells you what it means only if you +# already know, which defeats the point of a picker, so codes are resolved +# against the iso-codes database into "Portuguese (Brazil)" the way GNOME does. +# The code stays visible as the row's detail, because it is what actually gets +# written and someone choosing between two Spanish variants needs to see it. +# +# The join happens in a single jq pass. Doing it per locale meant 327 jq +# invocations, which took long enough to be visible when opening the page. +# +# Setting the locale is a privileged operation: localectl goes through polkit, +# which prompts. It also only takes effect for programs started afterwards, so +# the caller is responsible for saying a sign-out is needed -- this script does +# not pretend the running session changed. + +set -uo pipefail + +readonly ISO_LANG=/usr/share/iso-codes/json/iso_639-2.json +readonly ISO_COUNTRY=/usr/share/iso-codes/json/iso_3166-1.json + +cmd_get() { + localectl status 2>/dev/null \ + | awk -F'LANG=' '/System Locale:/ { print $2; exit }' \ + | tr -d '[:space:]' +} + +cmd_list() { + local locales + locales="$(localectl list-locales 2>/dev/null)" || locales="" + if [[ -z "$locales" ]]; then + printf '[]\n' + return 0 + fi + + # Without iso-codes installed the codes are still perfectly usable; they + # just do not get friendly names. That is a degraded list, not a failure. + if [[ ! -r "$ISO_LANG" || ! -r "$ISO_COUNTRY" ]]; then + jq -Rn --rawfile raw /dev/stdin \ + '[$raw | split("\n")[] | select(length > 0) | {value: ., label: ., detail: ""}]' \ + <<<"$locales" + return 0 + fi + + jq -Rn \ + --slurpfile languages "$ISO_LANG" \ + --slurpfile countries "$ISO_COUNTRY" \ + --rawfile raw /dev/stdin ' + # alpha_2 -> name, for both databases. Languages without a two-letter + # code cannot appear in a locale name, so they are simply absent. + ($languages[0]["639-2"] | map(select(.alpha_2)) | INDEX(.alpha_2) | map_values(.name)) as $lang + | ($countries[0]["3166-1"] | INDEX(.alpha_2) | map_values(.name)) as $country + | [ $raw + | split("\n")[] + | select(length > 0) + | . as $value + # en_US.UTF-8 -> ["en", "US"]; the codeset and any @modifier are + # not part of the human name. + | ($value | split(".")[0] | split("@")[0] | split("_")) as $parts + | ($lang[$parts[0]] // $parts[0]) as $language + | (if ($parts | length) > 1 then $country[$parts[1]] else null end) as $region + | { + value: $value, + label: (if $region then "\($language) (\($region))" else $language end), + detail: $value + } + ] + | sort_by(.label) + ' <<<"$locales" +} + +cmd_set() { + local locale="${1:-}" + # Constrained rather than passed through: this reaches a privileged + # command, and the set of legal locale names is narrow and well known. + [[ "$locale" =~ ^[a-zA-Z0-9_@.-]+$ ]] || { + printf 'panama-locale: refusing a locale name with unexpected characters\n' >&2 + return 2 + } + localectl list-locales 2>/dev/null | grep -qxF "$locale" || { + printf 'panama-locale: %s is not an installed locale\n' "$locale" >&2 + return 2 + } + localectl set-locale "LANG=$locale" +} + +case "${1:-list}" in + list) cmd_list ;; + get) cmd_get ;; + set) shift; cmd_set "${1:-}" ;; + *) printf 'usage: panama-locale [list|get|set ]\n' >&2; exit 2 ;; +esac diff --git a/config/dot/quickshell/services/MachineInfo.qml b/config/dot/quickshell/services/MachineInfo.qml new file mode 100644 index 0000000..4c2f736 --- /dev/null +++ b/config/dot/quickshell/services/MachineInfo.qml @@ -0,0 +1,47 @@ +pragma Singleton + +// What this machine is: model, processor, memory, disk, OS, kernel. +// +// Read once, on demand. None of it changes while the desktop is running except +// free disk space, and About is not a monitor -- the vitals readout on the Home +// page is where live figures belong. +// +// Graphics is not here. GraphicsDevices already enumerates GPUs for the vitals +// readout, and naming them again would be a second source of truth that could +// disagree with the first; the About page joins the two instead. + +import Quickshell +import Quickshell.Io +import QtQuick + +Singleton { + id: root + + readonly property string helperPath: Quickshell.shellDir + "/scripts/panama-about" + + // [{ label, value }] in display order. + property var facts: [] + property bool scanned: false + + function refresh(): void { + if (!query.running) + query.running = true; + } + + Process { + id: query + command: [root.helperPath] + stdout: StdioCollector { + onStreamFinished: { + try { + const parsed = JSON.parse(this.text); + root.facts = Array.isArray(parsed) ? parsed : []; + } catch (error) { + root.facts = []; + console.warn("MachineInfo: could not parse helper output:", error); + } + root.scanned = true; + } + } + } +} diff --git a/config/dot/quickshell/services/ShellState.qml b/config/dot/quickshell/services/ShellState.qml index 5a84298..b785047 100644 --- a/config/dot/quickshell/services/ShellState.qml +++ b/config/dot/quickshell/services/ShellState.qml @@ -92,7 +92,7 @@ Singleton { } function openSettings(page: string): void { - const allowed = ["home", "appearance", "displays", "connectivity", "home-phone", "desktop", "sound", "notifications", "screen-intelligence", "shortcuts", "mouse", "privacy", "accessibility", "power", "datetime", "applications", "services", "about"]; + const allowed = ["home", "appearance", "displays", "connectivity", "home-phone", "desktop", "sound", "notifications", "screen-intelligence", "shortcuts", "mouse", "privacy", "region", "accessibility", "power", "datetime", "applications", "services", "about"]; root.settingsPage = allowed.indexOf(page) >= 0 ? page : "home"; DesktopPreferences.set("lastPage", root.settingsPage); root.settingsOpen = true; diff --git a/config/dot/quickshell/services/SystemLocale.qml b/config/dot/quickshell/services/SystemLocale.qml new file mode 100644 index 0000000..6a5b7b7 --- /dev/null +++ b/config/dot/quickshell/services/SystemLocale.qml @@ -0,0 +1,99 @@ +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; + } + + function set(value: string): void { + if (value === root.current) + return; + 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."; + } + } + } +} diff --git a/config/dot/quickshell/services/SystemSettings.qml b/config/dot/quickshell/services/SystemSettings.qml index 67261fd..1f4c803 100644 --- a/config/dot/quickshell/services/SystemSettings.qml +++ b/config/dot/quickshell/services/SystemSettings.qml @@ -497,18 +497,35 @@ Singleton { ].indexOf(panel) >= 0; } - function openGnomePanel(panel: string): bool { + // `subpage` reaches the panels GNOME 50 nests under System -- users, + // about, datetime, region -- which its own desktop entries open as + // `gnome-control-center system users`. Without it, a row labelled "Users" + // lands on System's front page and leaves the user to navigate, which is + // most of the way to a broken button. + function openGnomePanel(panel: string, subpage: string): bool { if (!root.isGnomePanelAllowed(panel)) { root.lastError = "That GNOME Settings panel is not available."; return false; } + const command = ["gnome-control-center", panel]; + if (subpage !== undefined && subpage !== "" && root.isGnomeSubpageAllowed(panel, subpage)) + command.push(subpage); Quickshell.execDetached({ - command: ["gnome-control-center", panel], + command: command, environment: { "XDG_CURRENT_DESKTOP": "GNOME" } }); return true; } + // Only System nests panels, and only these. Read off the Exec lines of the + // gnome-*-panel desktop entries rather than guessed, for the same reason + // the panel list above was. + function isGnomeSubpageAllowed(panel: string, subpage: string): bool { + if (panel !== "system") + return false; + return ["users", "about", "datetime", "region", "remote-desktop"].indexOf(subpage) >= 0; + } + function openApplication(id: string): bool { const commands = { "nextcloud": ["nextcloud"],