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"