Make the weather location and graphics device choosable

The last two values that could only be changed by editing a file.

Weather was pinned to hardcoded coordinates, so the card could not be
pointed anywhere else. It is a location search now, not latitude and
longitude fields: nobody knows their own coordinates, and a control that
demands them is one nobody uses. Open-Meteo's geocoding endpoint needs
no key, the same reason the forecast already uses them. Only the search
term leaves the machine -- the stored place name is a label -- and
coordinates are rounded to four decimals, far finer than a weather
reading resolves and coarse enough to keep a precise home location out
of the settings file.

The graphics readout was hardcoded to card1. This machine has two amdgpu
cards, discrete and integrated, so that was right only by luck, and the
path is meaningless on any other machine. GPUs are enumerated with a
readable name from lspci, since sysfs exposes only numeric ids, and the
picker appears only when there is more than one to choose between. A
stored path the machine does not have is refused and reported rather
than silently measuring nothing.

Also merges the per-application notification rules UI. Its three commits
were believed integrated but the page half was not actually in the tree:
main had the service side in Notifs.qml and zero references to
setAppRule in NotificationsPage. Ancestry is not content.

Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
This commit is contained in:
Gabriel Brown
2026-08-18 06:38:46 -04:00
parent 9f9515ffe0
commit d18fe51553
15 changed files with 850 additions and 5 deletions
+94
View File
@@ -0,0 +1,94 @@
#!/bin/bash
set -euo pipefail
repo_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
helper="$repo_dir/config/dot/quickshell/scripts/panama-osd"
scratch="$(mktemp -d)"
trap 'rm -rf "$scratch"' EXIT
mkdir -p "$scratch/bin"
log="$scratch/calls"
cat >"$scratch/bin/wpctl" <<'SH'
#!/bin/bash
printf 'wpctl' >>"$OSD_TEST_LOG"
printf ' <%s>' "$@" >>"$OSD_TEST_LOG"
printf '\n' >>"$OSD_TEST_LOG"
if [[ $1 == "get-volume" ]]; then
printf '%s\n' "${WPCTL_OUTPUT:-Volume: 0.58}"
fi
SH
cat >"$scratch/bin/brightnessctl" <<'SH'
#!/bin/bash
printf 'brightnessctl' >>"$OSD_TEST_LOG"
printf ' <%s>' "$@" >>"$OSD_TEST_LOG"
printf '\n' >>"$OSD_TEST_LOG"
if [[ " $* " == *" -m "* && " $* " != *" set "* ]]; then
printf '%s\n' "${BRIGHTNESS_OUTPUT:-intel_backlight,backlight,500,1000,50%}"
fi
SH
cat >"$scratch/bin/playerctl" <<'SH'
#!/bin/bash
printf 'playerctl' >>"$OSD_TEST_LOG"
printf ' <%s>' "$@" >>"$OSD_TEST_LOG"
printf '\n' >>"$OSD_TEST_LOG"
if [[ $1 == "metadata" ]]; then
printf '%s\n' "${PLAYER_OUTPUT:-Horizon — Tycho}"
elif [[ $1 == "status" ]]; then
printf '%s\n' "${PLAYER_STATUS:-Playing}"
fi
SH
cat >"$scratch/bin/qs" <<'SH'
#!/bin/bash
printf 'qs' >>"$OSD_TEST_LOG"
printf ' <%s>' "$@" >>"$OSD_TEST_LOG"
printf '\n' >>"$OSD_TEST_LOG"
SH
chmod +x "$scratch/bin/"*
run_helper() {
PATH="$scratch/bin:$PATH" OSD_TEST_LOG="$log" "$helper" "$@"
}
assert_line() {
local expected="$1"
grep -Fqx -- "$expected" "$log" || {
printf 'osd helper contract: missing call\n%s\nactual:\n' "$expected" >&2
cat "$log" >&2
exit 1
}
}
: >"$log"
run_helper volume up 6
assert_line 'wpctl <set-volume> <-l> <1> <@DEFAULT_AUDIO_SINK@> <6%+>'
assert_line 'wpctl <get-volume> <@DEFAULT_AUDIO_SINK@>'
assert_line 'qs <ipc> <call> <osd> <progress> <volume> <58> <100> <58%>'
: >"$log"
WPCTL_OUTPUT='Volume: 0.58 [MUTED]' run_helper volume toggle
assert_line 'wpctl <set-mute> <@DEFAULT_AUDIO_SINK@> <toggle>'
assert_line 'qs <ipc> <call> <osd> <progress> <volume-muted> <58> <100> <Muted>'
: >"$log"
WPCTL_OUTPUT='Volume: 0.72 [MUTED]' run_helper microphone toggle
assert_line 'wpctl <set-mute> <@DEFAULT_AUDIO_SOURCE@> <toggle>'
assert_line 'qs <ipc> <call> <osd> <progress> <microphone-muted> <72> <100> <Muted>'
: >"$log"
run_helper brightness up 5
assert_line 'brightnessctl <-e4> <-n2> <set> <5%+>'
assert_line 'brightnessctl <-m> <-c> <backlight>'
assert_line 'qs <ipc> <call> <osd> <progress> <brightness> <50> <100> <50%>'
: >"$log"
run_helper media next
assert_line 'playerctl <next>'
assert_line 'qs <ipc> <call> <osd> <message> <media-next> <Horizon — Tycho>'
printf 'osd helper contract: PASS\n'
+62
View File
@@ -0,0 +1,62 @@
#!/bin/bash
set -euo pipefail
repo_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
model="$repo_dir/config/dot/quickshell/modules/osd/OsdModel.js"
node - "$model" <<'JS'
const assert = require('node:assert/strict')
const model = require(process.argv[2])
assert.equal(model.iconFor('volume', 0), 'audio-volume-muted-symbolic')
assert.equal(model.iconFor('volume', 0.2), 'audio-volume-low-symbolic')
assert.equal(model.iconFor('volume', 0.5), 'audio-volume-medium-symbolic')
assert.equal(model.iconFor('volume', 0.9), 'audio-volume-high-symbolic')
assert.equal(model.iconFor('microphone-muted', 0.7), 'microphone-sensitivity-muted-symbolic')
assert.equal(model.iconFor('brightness', 0.4), 'display-brightness-symbolic')
assert.deepEqual(
model.progressState('volume', 140, 100, '', 900),
{
kind: 'volume',
value: 100,
maximum: 100,
ratio: 1,
label: '100%',
icon: 'audio-volume-high-symbolic',
duration: 900,
progress: true
}
)
assert.deepEqual(
model.progressState('volume-muted', 43, 100, 'Muted', -50),
{
kind: 'volume-muted',
value: 43,
maximum: 100,
ratio: 0.43,
label: 'Muted',
icon: 'audio-volume-muted-symbolic',
duration: 0,
progress: true
}
)
assert.deepEqual(
model.messageState('media-next', 'Glass Beams', 'invalid'),
{
kind: 'media-next',
value: 0,
maximum: 100,
ratio: 0,
label: 'Glass Beams',
icon: 'media-skip-forward-symbolic',
duration: 1400,
progress: false
}
)
console.log('osd model contract: PASS')
JS
+58
View File
@@ -0,0 +1,58 @@
#!/bin/bash
set -euo pipefail
repo_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
qs_dir="$repo_dir/config/dot/quickshell"
osd="$qs_dir/modules/osd/Osd.qml"
state="$qs_dir/services/OsdState.qml"
shell_file="$qs_dir/shell.qml"
keybinds="$repo_dir/config/dot/hypr/keybinds.lua"
fail() {
printf 'osd ui contract: %s\n' "$1" >&2
exit 1
}
[[ -f "$osd" ]] || fail 'Prism OSD surface is missing'
[[ -f "$state" ]] || fail 'OSD presentation state is missing'
[[ -f "$qs_dir/modules/osd/qmldir" ]] || fail 'OSD module manifest is missing'
rg -Fq 'import qs.modules.osd' "$shell_file" || fail 'shell does not import the OSD module'
[[ "$(rg -c '^[[:space:]]*Osd \{\}' "$shell_file")" -eq 1 ]] \
|| fail 'shell does not create exactly one OSD per screen'
rg -Fq 'target: "osd"' "$shell_file" || fail 'OSD IPC target is missing'
rg -Fq 'OsdState.progress(kind, value, maximum, label)' "$shell_file" \
|| fail 'progress IPC is not wired to OSD state'
rg -Fq 'OsdState.message(kind, label)' "$shell_file" \
|| fail 'message IPC is not wired to OSD state'
rg -Fq 'WlrLayershell.namespace: "qs-popover-osd"' "$osd" \
|| fail 'OSD does not use the existing Prism blur namespace'
rg -Fq 'WlrLayershell.keyboardFocus: WlrKeyboardFocus.None' "$osd" \
|| fail 'OSD may steal keyboard focus'
rg -Fq 'mask: Region {}' "$osd" || fail 'OSD may intercept pointer input'
rg -Fq 'PrismEdge {' "$osd" || fail 'OSD is missing the Prism signature edge'
rg -Fq 'font.features: Theme.tabularFigures' "$osd" \
|| fail 'changing percentages do not use tabular figures'
rg -Fq '$HOME/.config/quickshell/scripts/panama-osd' "$keybinds" \
|| fail 'keybinds do not use the deployed Panama OSD helper'
for action in \
'volume up 6' \
'volume down 6' \
'volume toggle' \
'microphone toggle' \
'volume up 1' \
'volume down 1' \
'media play-pause' \
'media next' \
'media previous' \
'media stop' \
'brightness up 5' \
'brightness down 5'; do
rg -Fq "osd(\"$action\")" "$keybinds" \
|| fail "keybind is not routed through panama-osd $action"
done
printf 'osd ui contract: PASS\n'