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
This commit is contained in:
Gabriel Brown
2026-08-18 14:46:04 -04:00
parent 2d69ce7648
commit 8be3fc2fdd
11 changed files with 80 additions and 8 deletions
@@ -13,6 +13,9 @@ Pill {
horizontalPadding: 8 horizontalPadding: 8
onActivated: ShellState.toggle("activity") 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 { Text {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: { text: {
@@ -15,6 +15,9 @@ Pill {
horizontalPadding: 8 horizontalPadding: 8
onActivated: ShellState.openDateMenu("agenda") 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.visible: root.hovered && root.visible
ToolTip.delay: 500 ToolTip.delay: 500
ToolTip.text: CalendarAgenda.nextEvent?.summary ?? "Upcoming event" ToolTip.text: CalendarAgenda.nextEvent?.summary ?? "Upcoming event"
@@ -26,6 +26,11 @@ Pill {
onActivated: ShellState.toggleDateMenu("agenda") 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 { Text {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: Qt.formatDateTime(clock.date, root.format) text: Qt.formatDateTime(clock.date, root.format)
@@ -7,6 +7,7 @@
import QtQuick import QtQuick
import Quickshell.Services.Mpris import Quickshell.Services.Mpris
import qs.config import qs.config
import qs.services
import qs.widgets import qs.widgets
Pill { Pill {
@@ -34,6 +35,9 @@ Pill {
onActivated: if (root.player?.canTogglePlaying) onActivated: if (root.player?.canTogglePlaying)
root.player.togglePlaying() 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 // Scroll up = previous, down = next — the same direction as the workspace
// switcher, so the whole bar scrolls consistently. // switcher, so the whole bar scrolls consistently.
onScrolled: delta => { onScrolled: delta => {
@@ -24,6 +24,9 @@ Pill {
onActivated: root.requestQuickSettings() 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 ─────────────────────────────────────────────────────────────── // ── Audio ───────────────────────────────────────────────────────────────
// Without a tracker, volume and muted silently read as zero/false. // Without a tracker, volume and muted silently read as zero/false.
PwObjectTracker { PwObjectTracker {
@@ -13,6 +13,10 @@ import qs.widgets
Pill { Pill {
id: root 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 interactive: false
Row { Row {
@@ -10,6 +10,9 @@ import qs.widgets
Pill { Pill {
id: root 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 interactive: false
visible: Weather.available visible: Weather.available
@@ -300,7 +300,10 @@ SettingsPage {
ToggleRow { setting: "showCpu" } ToggleRow { setting: "showCpu" }
ToggleRow { setting: "showMemory" } 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. // Only worth asking when there is a choice to make.
ChoiceGrid { ChoiceGrid {
@@ -117,12 +117,6 @@ SettingsPage {
SliderRow { setting: "weatherRefreshMinutes"; divider: false } SliderRow { setting: "weatherRefreshMinutes"; divider: false }
} }
SettingsCard {
title: "System vitals"
subtitle: "Processor, memory, and graphics activity in the bar"
SliderRow { setting: "vitalsIntervalMs"; divider: false }
}
Grid { Grid {
id: summaryCards id: summaryCards
@@ -47,7 +47,7 @@ Singleton {
"edges": "desktop", "edges": "desktop",
"master": "desktop", "master": "desktop",
"notices": "desktop", "notices": "desktop",
"weather": "appearance", "weather": "home",
"notifications": "notifications", "notifications": "notifications",
"capture": "screen-intelligence" "capture": "screen-intelligence"
}) })
+50
View File
@@ -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"