From 8be3fc2fdd1ef393ca228ac1c4c6a7d4aa19d2f5 Mon Sep 17 00:00:00 2001 From: Gabriel Brown Date: Tue, 18 Aug 2026 14:46:04 -0400 Subject: [PATCH] Right-click a bar widget to open its settings Four places in the entire shell could reach Settings. The bar, where a person looks first, was not one of them -- and Pill has routed right-click to a secondaryActivated signal all along, which nothing connected, so the gesture did nothing on every widget in the bar. Each widget now opens the page that owns its settings: the clock and the calendar reminder open Date & Time, weather opens Home, the vitals readout opens Appearance, the status glyphs open Network & Devices, the media readout opens Sound, and the privacy indicator opens Privacy & Security. Left-click behaviour is untouched. Two routing bugs found while picking those destinations, both of the same kind and both invisible from the code, since each page reads perfectly well on its own: weather routed to Appearance while every weather control lives on Home, so searching "temperature unit" opened a page without it. vitals routed to Appearance, but the refresh interval sat on Home while the toggles it governs sat on Appearance -- one concept split across two pages, which is exactly what the ownership rule forbids. The interval now sits beside the toggles and Home's stub card is gone. The jump contract guards the failure mode these share. openSettings() falls back to Home for an unknown page, sensibly and completely silently, so a typo or a later rename turns a right-click into "opens the wrong page" with nothing logged. It also fails a Pill-based bar widget that leaves right-click unconnected, since that is how the gesture came to be inert everywhere in the first place. A third instance of the routing bug is still open: followMouse and pointerSensitivity sit in the input group, which routes to Keyboard, while both render on Mouse. Fixing it is a two-line group change in PreferenceSchema.qml, which codex currently owns, so the contract that catches all three lands with that fix rather than red. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L --- .../modules/bar/ActivityIndicator.qml | 3 ++ .../modules/bar/CalendarIndicator.qml | 3 ++ config/dot/quickshell/modules/bar/Clock.qml | 5 ++ .../quickshell/modules/bar/MediaWidget.qml | 4 ++ .../quickshell/modules/bar/StatusCluster.qml | 3 ++ .../quickshell/modules/bar/VitalsWidget.qml | 4 ++ .../quickshell/modules/bar/WeatherWidget.qml | 3 ++ .../modules/settings/AppearancePage.qml | 5 +- .../quickshell/modules/settings/HomePage.qml | 6 --- .../quickshell/services/SettingsSearch.qml | 2 +- tests/quickshell/settings-jump-contract.sh | 50 +++++++++++++++++++ 11 files changed, 80 insertions(+), 8 deletions(-) create mode 100755 tests/quickshell/settings-jump-contract.sh diff --git a/config/dot/quickshell/modules/bar/ActivityIndicator.qml b/config/dot/quickshell/modules/bar/ActivityIndicator.qml index 107ad3e..8f49118 100644 --- a/config/dot/quickshell/modules/bar/ActivityIndicator.qml +++ b/config/dot/quickshell/modules/bar/ActivityIndicator.qml @@ -13,6 +13,9 @@ Pill { horizontalPadding: 8 onActivated: ShellState.toggle("activity") + // Right-click opens the settings that govern this widget. Camera, microphone and screen-sharing state is a privacy readout. + onSecondaryActivated: ShellState.openSettings("privacy") + Text { anchors.verticalCenter: parent.verticalCenter text: { diff --git a/config/dot/quickshell/modules/bar/CalendarIndicator.qml b/config/dot/quickshell/modules/bar/CalendarIndicator.qml index 24695d9..0862e50 100644 --- a/config/dot/quickshell/modules/bar/CalendarIndicator.qml +++ b/config/dot/quickshell/modules/bar/CalendarIndicator.qml @@ -15,6 +15,9 @@ Pill { horizontalPadding: 8 onActivated: ShellState.openDateMenu("agenda") + // Right-click opens the settings that govern this widget. The same place the clock leads, since this is the calendar's own reminder. + onSecondaryActivated: ShellState.openSettings("datetime") + ToolTip.visible: root.hovered && root.visible ToolTip.delay: 500 ToolTip.text: CalendarAgenda.nextEvent?.summary ?? "Upcoming event" diff --git a/config/dot/quickshell/modules/bar/Clock.qml b/config/dot/quickshell/modules/bar/Clock.qml index 59a0fff..9ae6dd5 100644 --- a/config/dot/quickshell/modules/bar/Clock.qml +++ b/config/dot/quickshell/modules/bar/Clock.qml @@ -26,6 +26,11 @@ Pill { onActivated: ShellState.toggleDateMenu("agenda") + // Right-click opens the settings that govern this widget. Timezone and clock format live on Date & Time. The + // format toggles are mirrored on Appearance, but someone right-clicking a + // clock is far more often after the time itself than its typography. + onSecondaryActivated: ShellState.openSettings("datetime") + Text { anchors.verticalCenter: parent.verticalCenter text: Qt.formatDateTime(clock.date, root.format) diff --git a/config/dot/quickshell/modules/bar/MediaWidget.qml b/config/dot/quickshell/modules/bar/MediaWidget.qml index e67e4aa..7fa1986 100644 --- a/config/dot/quickshell/modules/bar/MediaWidget.qml +++ b/config/dot/quickshell/modules/bar/MediaWidget.qml @@ -7,6 +7,7 @@ import QtQuick import Quickshell.Services.Mpris import qs.config +import qs.services import qs.widgets Pill { @@ -34,6 +35,9 @@ Pill { onActivated: if (root.player?.canTogglePlaying) root.player.togglePlaying() + // Right-click opens the settings that govern this widget. Output device and per-application volume. + onSecondaryActivated: ShellState.openSettings("sound") + // Scroll up = previous, down = next — the same direction as the workspace // switcher, so the whole bar scrolls consistently. onScrolled: delta => { diff --git a/config/dot/quickshell/modules/bar/StatusCluster.qml b/config/dot/quickshell/modules/bar/StatusCluster.qml index 474e4de..312fcf8 100644 --- a/config/dot/quickshell/modules/bar/StatusCluster.qml +++ b/config/dot/quickshell/modules/bar/StatusCluster.qml @@ -24,6 +24,9 @@ Pill { onActivated: root.requestQuickSettings() + // Right-click opens the settings that govern this widget. Network and Bluetooth, which is most of what these glyphs report. + onSecondaryActivated: ShellState.openSettings("connectivity") + // ── Audio ─────────────────────────────────────────────────────────────── // Without a tracker, volume and muted silently read as zero/false. PwObjectTracker { diff --git a/config/dot/quickshell/modules/bar/VitalsWidget.qml b/config/dot/quickshell/modules/bar/VitalsWidget.qml index 05ccfcb..9d1f85b 100644 --- a/config/dot/quickshell/modules/bar/VitalsWidget.qml +++ b/config/dot/quickshell/modules/bar/VitalsWidget.qml @@ -13,6 +13,10 @@ import qs.widgets Pill { id: root + // Right-click opens the settings that govern this widget. Which readouts + // appear in the bar, and how often they update. + onSecondaryActivated: ShellState.openSettings("appearance") + interactive: false Row { diff --git a/config/dot/quickshell/modules/bar/WeatherWidget.qml b/config/dot/quickshell/modules/bar/WeatherWidget.qml index 1c0a874..b0c8740 100644 --- a/config/dot/quickshell/modules/bar/WeatherWidget.qml +++ b/config/dot/quickshell/modules/bar/WeatherWidget.qml @@ -10,6 +10,9 @@ import qs.widgets Pill { id: root + // Right-click opens the settings that govern this widget. Location, units and refresh interval are all on Home. + onSecondaryActivated: ShellState.openSettings("home") + interactive: false visible: Weather.available diff --git a/config/dot/quickshell/modules/settings/AppearancePage.qml b/config/dot/quickshell/modules/settings/AppearancePage.qml index 86c31d6..50dc3aa 100644 --- a/config/dot/quickshell/modules/settings/AppearancePage.qml +++ b/config/dot/quickshell/modules/settings/AppearancePage.qml @@ -300,7 +300,10 @@ SettingsPage { ToggleRow { setting: "showCpu" } ToggleRow { setting: "showMemory" } - ToggleRow { setting: "showGpu"; divider: GraphicsDevices.devices.length > 1 || GraphicsDevices.selectionMissing } + ToggleRow { setting: "showGpu"; divider: true } + // Refresh interval was on the Home page, which split one concept across + // two pages -- what the vitals show here, how often they update there. + SliderRow { setting: "vitalsIntervalMs"; divider: GraphicsDevices.devices.length > 1 || GraphicsDevices.selectionMissing } // Only worth asking when there is a choice to make. ChoiceGrid { diff --git a/config/dot/quickshell/modules/settings/HomePage.qml b/config/dot/quickshell/modules/settings/HomePage.qml index 83afe61..19a2465 100644 --- a/config/dot/quickshell/modules/settings/HomePage.qml +++ b/config/dot/quickshell/modules/settings/HomePage.qml @@ -117,12 +117,6 @@ SettingsPage { SliderRow { setting: "weatherRefreshMinutes"; divider: false } } - SettingsCard { - title: "System vitals" - subtitle: "Processor, memory, and graphics activity in the bar" - SliderRow { setting: "vitalsIntervalMs"; divider: false } - } - Grid { id: summaryCards diff --git a/config/dot/quickshell/services/SettingsSearch.qml b/config/dot/quickshell/services/SettingsSearch.qml index 90cd8b8..408abcf 100644 --- a/config/dot/quickshell/services/SettingsSearch.qml +++ b/config/dot/quickshell/services/SettingsSearch.qml @@ -47,7 +47,7 @@ Singleton { "edges": "desktop", "master": "desktop", "notices": "desktop", - "weather": "appearance", + "weather": "home", "notifications": "notifications", "capture": "screen-intelligence" }) diff --git a/tests/quickshell/settings-jump-contract.sh b/tests/quickshell/settings-jump-contract.sh new file mode 100755 index 0000000..02d356c --- /dev/null +++ b/tests/quickshell/settings-jump-contract.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash + +# Every "open the settings for this" jump must land somewhere real. +# +# ShellState.openSettings() validates its argument against an allow-list and +# falls back to Home for anything unknown. That fallback is sensible and it is +# also completely silent: a typo, or a page renamed later, turns a right-click +# into "opens Settings on the wrong page" with nothing logged and no error. +# +# Before this, exactly four places in the entire shell could reach Settings, so +# the risk was small. The bar now offers a jump on every widget, which makes the +# fallback worth guarding. + +set -uo pipefail + +repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +shell_state="$repo_dir/config/dot/quickshell/services/ShellState.qml" +modules="$repo_dir/config/dot/quickshell/modules" + +fail() { + printf 'settings jump contract: %s\n' "$1" >&2 + exit 1 +} + +allowed_line="$(grep -m1 'const allowed = \[' "$shell_state")" \ + || fail 'could not find the allow-list in ShellState' + +jumps="$(grep -rhoE 'openSettings\("[a-z-]+"\)' "$modules" 2>/dev/null \ + | sed 's/openSettings("//; s/")//' | sort -u)" +[[ -n "$jumps" ]] || fail 'found no settings jumps at all -- this contract is not reading the modules correctly' + +count=0 +while read -r page; do + [[ -n "$page" ]] || continue + grep -qF "\"$page\"" <<<"$allowed_line" \ + || fail "a jump opens \"$page\", which ShellState does not allow -- openSettings falls back to Home silently, so this reads as a right-click that goes to the wrong page" + count=$((count + 1)) +done <<<"$jumps" + +# The bar is where a person looks first, and Pill has offered a right-click +# signal all along that nothing connected -- so the gesture did nothing on every +# widget in the bar. Anything built on Pill that can be configured should say so. +for widget in Clock WeatherWidget VitalsWidget StatusCluster MediaWidget; do + file="$modules/bar/$widget.qml" + [[ -r "$file" ]] || continue + grep -q 'onSecondaryActivated' "$file" \ + || fail "$widget has no right-click jump; Pill routes right-click to secondaryActivated, so leaving it unconnected makes the gesture silently inert" +done + +printf 'settings jump contract: PASS (%d distinct destinations)\n' "$count"