Author SHA1 Message Date
Gabriel Brown 09e5f1ad6b Fix external monitor brightness OSD 2026-08-18 08:15:01 -04:00
4 changed files with 299 additions and 808 deletions
+139 -4
View File
@@ -2,6 +2,8 @@
set -u set -u
readonly PANAMA_OSD_SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
strict_delivery() { strict_delivery() {
[[ ${PANAMA_OSD_STRICT:-false} == true || ${PANAMA_OSD_STRICT:-false} == 1 ]] [[ ${PANAMA_OSD_STRICT:-false} == true || ${PANAMA_OSD_STRICT:-false} == 1 ]]
} }
@@ -67,18 +69,151 @@ adjust_microphone() {
show_volume "$target" microphone show_volume "$target" microphone
} }
brightness_percent() {
local output="$1" percent
percent="$(awk -F, 'NR == 1 { value=$5; gsub(/%/, "", value); print value }' <<<"$output")"
[[ $percent =~ ^[0-9]+$ ]] || return 1
printf '%s\n' "$percent"
}
brightness_error() {
local detail="$1" label="External brightness unavailable"
if [[ $detail == *udev* || $detail == *accessible* || $detail == *permission* ]]; then
label="Brightness needs permission"
fi
show_message dialog-warning-symbolic "$label" || true
if command -v notify-send >/dev/null 2>&1; then
notify-send --app-name=Panama --icon=display-brightness-symbolic \
"Brightness unavailable" "$detail" >/dev/null 2>&1 || true
fi
}
discover_ddc_bus() {
local helper="$1" cache_file="$2" list_json focused selected error bus connector
command -v jq >/dev/null 2>&1 || {
brightness_error "jq is required to discover DDC/CI displays."
return 1
}
list_json="$("$helper" list 2>/dev/null)" || {
brightness_error "The external brightness helper could not inspect connected displays."
return 1
}
if ! jq -e 'type == "object" and (.displays | type == "array")' >/dev/null 2>&1 <<<"$list_json"; then
brightness_error "The external brightness helper returned invalid display information."
return 1
fi
error="$(jq -r '.error // empty' <<<"$list_json")"
if [[ -n $error ]]; then
brightness_error "$error"
return 1
fi
focused="$(hyprctl -j monitors 2>/dev/null \
| jq -r '.[] | select(.focused == true) | .name' 2>/dev/null \
| head -n1)"
selected="$(jq -r --arg connector "$focused" '
([.displays[] | select(.connector == $connector)][0] // .displays[0] // empty)
| [.bus, .connector]
| @tsv
' <<<"$list_json")"
IFS=$'\t' read -r bus connector <<<"$selected"
if [[ ! $bus =~ ^[0-9]+$ ]]; then
brightness_error "No connected monitor exposes DDC/CI brightness control."
return 1
fi
umask 077
printf '%s\t%s\n' "$bus" "$connector" >"$cache_file"
printf '%s\n' "$bus"
}
adjust_ddc_brightness() {
local action="$1" step="$2"
local helper="${PANAMA_OSD_BRIGHTNESS_HELPER:-$PANAMA_OSD_SCRIPT_DIR/panama-brightness}"
local runtime_dir="${PANAMA_OSD_RUNTIME_DIR:-${XDG_RUNTIME_DIR:-/tmp}/panama-osd-${UID}}"
local cache_file="$runtime_dir/brightness-bus" lock_file="$runtime_dir/brightness.lock"
local bus="" connector="" current target lock_fd
[[ -x $helper ]] || {
brightness_error "The external brightness helper is not installed."
return 0
}
mkdir -p "$runtime_dir" || return 0
chmod 700 "$runtime_dir" 2>/dev/null || true
exec {lock_fd}>"$lock_file" || return 0
# DDC transactions on one I2C bus cannot safely overlap. A short wait also
# sheds an excessive key-repeat backlog instead of replaying it seconds later.
flock -w 2 "$lock_fd" || return 0
if [[ -r $cache_file ]]; then
IFS=$'\t' read -r bus connector <"$cache_file" || true
[[ $bus =~ ^[0-9]+$ ]] || bus=""
fi
if [[ -n $bus ]]; then
current="$("$helper" get "$bus" 2>/dev/null)" || current=""
if [[ ! $current =~ ^[0-9]+$ ]]; then
: >"$cache_file"
bus=""
fi
fi
if [[ -z $bus ]]; then
bus="$(discover_ddc_bus "$helper" "$cache_file")" || return 0
current="$("$helper" get "$bus" 2>/dev/null)" || current=""
fi
if [[ ! $current =~ ^[0-9]+$ ]]; then
brightness_error "The selected monitor stopped responding over DDC/CI."
return 0
fi
if [[ $action == up ]]; then
target=$(( current + step ))
else
target=$(( current - step ))
fi
(( target > 100 )) && target=100
(( target < 0 )) && target=0
if ! "$helper" set "$bus" "$target" >/dev/null 2>&1; then
brightness_error "The selected monitor did not accept the brightness change."
return 0
fi
show_progress brightness "$target" "${target}%"
}
adjust_brightness() { adjust_brightness() {
local action="${1:-}" step="${2:-5}" output percent local action="${1:-}" step="${2:-5}" output percent
[[ $step =~ ^[0-9]+$ ]] || {
printf 'Usage: panama-osd brightness up|down [step]\n' >&2
return 2
}
case "$action" in case "$action" in
up) brightnessctl -e4 -n2 set "${step}%+" >/dev/null || return ;; up|down) ;;
down) brightnessctl -e4 -n2 set "${step}%-" >/dev/null || return ;;
*) printf 'Usage: panama-osd brightness up|down [step]\n' >&2; return 2 ;; *) printf 'Usage: panama-osd brightness up|down [step]\n' >&2; return 2 ;;
esac esac
# Laptop panels expose a kernel backlight class and remain the fastest,
# most reliable path. Desktops fall through to DDC/CI monitor control.
output="$(brightnessctl -m -c backlight 2>/dev/null)" || output=""
if percent="$(brightness_percent "$output")"; then
if [[ $action == up ]]; then
brightnessctl -e4 -n2 -c backlight set "${step}%+" >/dev/null || return 0
else
brightnessctl -e4 -n2 -c backlight set "${step}%-" >/dev/null || return 0
fi
output="$(brightnessctl -m -c backlight 2>/dev/null)" || return 0 output="$(brightnessctl -m -c backlight 2>/dev/null)" || return 0
percent="$(awk -F, 'NR == 1 { value=$5; gsub(/%/, "", value); print value }' <<<"$output")" percent="$(brightness_percent "$output")" || return 0
[[ $percent =~ ^[0-9]+$ ]] || return 0
show_progress brightness "$percent" "${percent}%" show_progress brightness "$percent" "${percent}%"
return
fi
adjust_ddc_brightness "$action" "$step"
} }
media_action() { media_action() {
@@ -1,566 +0,0 @@
# Panama Health & Recovery Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Build a quiet, trustworthy System Health surface that diagnoses Panama-owned desktop functionality, exposes redacted reports, and offers only narrow allow-listed repairs.
**Architecture:** An executable Python helper, `panama-doctor`, is the only operating-system boundary and emits one deterministic JSON schema. A `Health.qml` singleton owns accepted snapshots, scan generations, repair state, and report copying; Settings, the bar, IPC, and Vicinae consume that typed state without constructing commands.
**Tech Stack:** Python 3 standard library, Bash contract tests, Quickshell/QML, QtQuick, Hyprland IPC, Vicinae script commands, Prism design tokens.
**Spec:** `docs/superpowers/specs/2026-08-18-panama-health-recovery-design.md`
## Global Constraints
- Healthy background scans are silent: no notifications, Signal Glass events, animations, or permanent bar ornament.
- Allowed statuses are exactly `ok`, `warning`, `error`, and `unconfigured`; overall status is `healthy`, `warning`, or `error`.
- Optional integrations that have never been configured are `unconfigured`, never warnings.
- The helper never reads or reports secret values, clipboard contents, notification bodies, calendar event data, SSIDs, addresses, or arbitrary command output.
- The helper never installs packages, invokes `sudo`, deletes user data, rewrites arbitrary configuration, or repairs services Panama does not own.
- Probe-derived values may populate observations only; check IDs, groups, titles, actions, commands, and arguments are authored constants.
- All process launches use argument arrays. UI text and report content never become commands.
- Preserve the last valid snapshot on helper failure or malformed JSON.
- Repairs are judged by a fresh observed scan, not by process exit status alone.
- Do not run a state-changing live repair without a genuinely degraded disposable target or explicit user approval.
## File map and stable interfaces
- `config/dot/quickshell/scripts/panama-doctor`: Python CLI and sole diagnostic/repair OS boundary.
- `config/dot/quickshell/services/Health.qml`: snapshot state machine, scan/repair processes, report copy, and fixture seams.
- `config/dot/quickshell/health-harness.qml`: deterministic IPC harness for generations, malformed data, coalescing, and repairs.
- `config/dot/quickshell/modules/settings/HealthPage.qml`: System Health page composition.
- `config/dot/quickshell/modules/settings/HealthSummary.qml`: stable-height summary hero and primary controls.
- `config/dot/quickshell/modules/settings/HealthCheckRow.qml`: one accessible check row with one action.
- `config/dot/quickshell/modules/bar/HealthIndicator.qml`: degraded-only bar entry point.
- `config/local/share/vicinae/scripts/check-system-health.sh`: searchable launcher command.
- `tests/quickshell/fixtures/doctor/`: isolated command, config, state, and runtime fixtures containing no real workstation data.
- `tests/quickshell/panama-doctor-contract.sh`: schema, status, redaction, timeout, ordering, and repair allow-list contract.
- `tests/quickshell/health-service-contract.sh`: QML state-machine contract.
- `tests/quickshell/health-ui-contract.sh`: Settings, footer, report, indicator, IPC, and Vicinae integration contract.
The helper's authored check order is:
```text
desktop.hyprland
desktop.quickshell
desktop.notifications
desktop.portals
desktop.hyprpaper
desktop.hypridle
desktop.vicinae
input.pipewire
input.clipboard
input.wallpaper
input.capture
input.ocr
input.brightness
integration.nextcloud
integration.rustdesk
integration.kdeconnect
integration.bluebubbles
integration.home-assistant
integration.calendar
panama.runtime-links
panama.vicinae-commands
panama.selected-terminal
panama.selected-launcher
panama.processes
panama.caffeine
```
Only these repair IDs are executable in release one:
```text
desktop.hyprpaper -> systemctl --user restart hyprpaper.service
desktop.hypridle -> systemctl --user restart hypridle.service
desktop.vicinae -> systemctl --user restart vicinae.service
desktop.quickshell -> panama-action restart-shell (confirmation required)
panama.runtime-links -> recreate only known Panama-owned broken symlinks
panama.vicinae-commands -> setup/scripts/link-vicinae-scripts
panama.caffeine -> release duplicate Panama/Caffeine inhibitor PIDs only
```
---
### Task 1: Prism health mocks and visual approval
**Files:**
- Create outside tracked source: `.superpowers/mocks/system-health/index.html`
- Create outside tracked source: `.superpowers/mocks/system-health/panama.css`
- Create outside tracked source: `.superpowers/mocks/system-health/mock.js`
- Create outside tracked source: `.superpowers/mocks/system-health/a-ledger.html`
- Create outside tracked source: `.superpowers/mocks/system-health/b-focus.html`
- Create outside tracked source: `.superpowers/mocks/system-health/c-compact.html`
**Interfaces:**
- Consumes: the existing 272 px Settings sidebar, 48 px titlebar, Tokyo Night Moon Prism tokens, and the approved information architecture.
- Produces: one approved visual composition for healthy, warning, checking, and error states plus the degraded-only bar indicator.
- [ ] **Step 1: Build three static compositions from real copy**
Use the same warning fixture in all three: `Vicinae` is stopped with action `Restart Vicinae`; `External monitor brightness` needs permission with action `View setup instructions`; `BlueBubbles` is `Not set up`. Keep every variant inside the real Settings geometry and include the footer and bar indicator.
```text
A — Diagnostic ledger: one restrained amber issue rail beside calm grouped rows.
B — Focus card: issues receive the visual focus; healthy groups collapse into quieter ledgers below.
C — Compact matrix: dense two-column group cards with the same issue-first ordering.
```
- [ ] **Step 2: Serve and visually inspect the mocks**
Run:
```bash
python3 -m http.server 52780 --directory .superpowers/mocks/system-health
```
Expected: all three variants render at `http://localhost:52780`, keyboard focus is visible, no element overflows at 1360x900, and reduced-motion mode has no continuous animation.
- [ ] **Step 3: Capture the approved direction in the plan**
Add a short `Approved visual: <variant>` note beneath this task after user selection. Production UI work in Task 4 must reproduce that composition using existing QML tokens rather than copying browser-only effects.
### Task 2: Deterministic read-only doctor
**Files:**
- Create: `config/dot/quickshell/scripts/panama-doctor`
- Create: `tests/quickshell/panama-doctor-contract.sh`
- Create: `tests/quickshell/fixtures/doctor/bin/systemctl`
- Create: `tests/quickshell/fixtures/doctor/bin/pgrep`
- Create: `tests/quickshell/fixtures/doctor/bin/busctl`
- Create: `tests/quickshell/fixtures/doctor/bin/qs`
- Create: `tests/quickshell/fixtures/doctor/bin/vicinae`
- Create: `tests/quickshell/fixtures/doctor/bin/systemd-inhibit`
**Interfaces:**
- Consumes: `PANAMA_DOCTOR_ROOT`, `PANAMA_DOCTOR_HOME`, `PANAMA_DOCTOR_CONFIG_HOME`, `PANAMA_DOCTOR_STATE_HOME`, `PANAMA_DOCTOR_RUNTIME_DIR`, `PANAMA_DOCTOR_PATH`, and `PANAMA_DOCTOR_TIMEOUT` test seams; production defaults resolve from the real process environment.
- Produces: `panama-doctor --json`, `panama-doctor --summary`, and a versioned schema with `summary` plus the 25 ordered check objects listed above.
- [ ] **Step 1: Write the failing schema and redaction contract**
The contract must create an isolated home, tracked source tree, runtime tree, and fake command directory, then assert:
```bash
snapshot="$($doctor --json)"
jq -e '.schemaVersion == 1
and (.generatedAt | type == "string")
and (.summary.status | IN("healthy", "warning", "error"))
and (.context.session | IN("hyprland", "other"))
and (.context.versions | type == "array")
and ([.checks[].id] | length == 25)
and ([.checks[].id] | unique | length == 25)
and ([.checks[].status] | all(IN("ok", "warning", "error", "unconfigured")))' <<<"$snapshot"
[[ "$(jq -r '.checks[].id' <<<"$snapshot")" == "$expected_order" ]]
! grep -Fq 'fixture-secret-token' <<<"$snapshot"
! grep -Fq 'fixture clipboard body' <<<"$snapshot"
! grep -Fq 'AA:BB:CC:DD:EE:FF' <<<"$snapshot"
```
Cover a healthy required service, a missing required executable, an unconfigured optional integration, a configured-but-stopped integration, an inaccessible DDC bus, a timed-out probe, duplicate Caffeine inhibitors, malformed probe output, and concise `--summary` output.
- [ ] **Step 2: Run the contract and verify the helper is absent**
Run: `tests/quickshell/panama-doctor-contract.sh`
Expected: FAIL because `config/dot/quickshell/scripts/panama-doctor` does not exist.
- [ ] **Step 3: Implement authored checks and concurrent bounded probes**
Use Python standard-library types and deterministic assembly:
```python
@dataclass(frozen=True)
class Action:
kind: Literal["repair", "open", "instructions"]
label: str
confirm: bool = False
@dataclass(frozen=True)
class Check:
id: str
group: Literal["desktop-foundation", "input-media", "integrations", "panama-tools"]
title: str
status: Literal["ok", "warning", "error", "unconfigured"]
detail: str
action: Action | None = None
```
Run independent probes through `ThreadPoolExecutor(max_workers=8)`. Every subprocess call must use a constant argument tuple, `capture_output=True`, `text=True`, and the configured timeout. Convert timeout, non-zero status, and parse failure into a check result. Assemble checks by the authored ID tuple after futures settle; never emit completion order.
Configuration checks may inspect existence and file type only. Home Assistant is configured when both expected variable names are present, but their values are never retained. Calendar is configured from enabled EDS source count only; event commands are never called. BlueBubbles is configured from the Flatpak installation check. DDC uses only `panama-brightness list` and retains display count plus its authored error classification, never connector names.
The top-level `context` contains only an authored session class and an ordered array of parsed Hyprland, Quickshell, Fedora, and Panama revision versions. `panama.processes` counts only exact authored process names and flags duplicate Quickshell, Vicinae, Hyprpaper, or Hypridle instances without exposing command lines. Integration actions are authored too: Nextcloud, RustDesk, KDE Connect, and BlueBubbles may offer their exact Open action; Home Assistant and calendar failures route to `home-phone` and `datetime`; never derive an application or page name from probe output.
- [ ] **Step 4: Run the doctor contract**
Run: `tests/quickshell/panama-doctor-contract.sh`
Expected: `panama doctor contract: PASS`.
- [ ] **Step 5: Commit the read-only engine**
```bash
git add config/dot/quickshell/scripts/panama-doctor tests/quickshell/panama-doctor-contract.sh tests/quickshell/fixtures/doctor
git commit -m "Add Panama system health diagnostics"
```
### Task 3: Health singleton and typed IPC state machine
**Files:**
- Create: `config/dot/quickshell/services/Health.qml`
- Create: `config/dot/quickshell/health-harness.qml`
- Create: `tests/quickshell/health-service-contract.sh`
- Modify: `config/dot/quickshell/shell.qml`
**Interfaces:**
- Consumes: `panama-doctor --json` and `panama-doctor --repair CHECK_ID --json`.
- Produces: `Health.snapshot`, `Health.checks`, `Health.summary`, `Health.status`, `Health.actionable`, `Health.busy`, `Health.diagnosticUnavailable`, `Health.lastError`, `Health.repairingId`, `Health.lastRepair`, `Health.lastCopyResult`, `Health.refresh()`, `Health.repair(id, external)`, `Health.copyReport()`, and IPC target `health` with `refresh`, `status`, `open`, and `repair(id)`.
- [ ] **Step 1: Write the failing QML state contract**
The harness exposes fixture methods that call the real singleton's pure consumption seams:
```qml
function accept(text: string, generation: int): bool { return Health.consumeSnapshot(text, generation); }
function queue(): void { Health.refresh(); Health.refresh(); }
function status(): string { return JSON.stringify(Health.diagnostics()); }
```
Assert that a valid warning snapshot is accepted, an older generation is ignored, malformed JSON preserves the prior checks and marks the engine unavailable, two refreshes while running schedule exactly one follow-up, a valid repair triggers one rescan, and unknown/non-repairable IDs start no process.
- [ ] **Step 2: Run the contract and verify it fails**
Run: `tests/quickshell/health-service-contract.sh`
Expected: FAIL because `Health.qml` and the harness do not exist.
- [ ] **Step 3: Implement the singleton state machine**
Define the stable state shape:
```qml
property var snapshot: ({})
property var checks: []
property var summary: ({ status: "healthy", healthy: 0, warnings: 0, errors: 0, unconfigured: 0 })
property string status: "healthy"
property bool diagnosticUnavailable: false
property bool queuedRefresh: false
property int generation: 0
property int acceptedGeneration: 0
property string repairingId: ""
property var lastRepair: ({})
property string lastCopyResult: ""
readonly property bool actionable: status === "warning" || status === "error"
readonly property bool busy: scanProcess.running || repairProcess.running
```
Use `Process.exec([root.helperPath, "--json"])`; attach the current generation to the collector before launch. `consumeSnapshot(text, generation)` validates schema version, summary keys, context shape, unique IDs, groups, statuses, titles, details, and action shapes before replacing state. A 2200 ms one-shot startup timer requests the initial scan. A running scan sets `queuedRefresh`; exit consumes at most one queued follow-up. `copyReport()` sends only `JSON.stringify(root.snapshot, null, 2)` to `wl-copy` through a `Process` stdin buffer and writes success or failure to `lastCopyResult` without touching the clipboard service's history model.
The `health` IPC `status()` returns only the already-redacted summary, busy flags, generation, and check IDs/statuses. `open()` calls `ShellState.openSettings("services")` then refreshes. IPC `repair(id)` calls `Health.repair(id, true)` and returns a Boolean acceptance result; Settings calls `Health.repair(id, false)`. A failed externally-originated repair uses an argument-array `notify-send` process with the existing `Panama action failed` title, while Settings failures remain inline.
- [ ] **Step 4: Run service and IPC contracts**
Run:
```bash
tests/quickshell/health-service-contract.sh
tests/quickshell/settings-window-contract.sh
```
Expected: both PASS.
- [ ] **Step 5: Commit the service layer**
```bash
git add config/dot/quickshell/services/Health.qml config/dot/quickshell/health-harness.qml tests/quickshell/health-service-contract.sh config/dot/quickshell/shell.qml
git commit -m "Add Panama health state service"
```
### Task 4: Approved System Health Settings page
**Files:**
- Create: `config/dot/quickshell/modules/settings/HealthPage.qml`
- Create: `config/dot/quickshell/modules/settings/HealthSummary.qml`
- Create: `config/dot/quickshell/modules/settings/HealthCheckRow.qml`
- Modify: `config/dot/quickshell/modules/settings/SettingsShell.qml`
- Modify: `config/dot/quickshell/modules/settings/SettingsSidebar.qml`
- Modify: `config/dot/quickshell/services/SettingsSearch.qml`
- Delete: `config/dot/quickshell/modules/settings/ServicesPage.qml`
- Create: `tests/quickshell/health-ui-contract.sh`
- Modify: `tests/quickshell/settings-pages-contract.sh`
- Modify: `tests/quickshell/settings-search-contract.sh`
**Interfaces:**
- Consumes: all read-only state and methods from `Health.qml`; route remains the stable internal name `services`.
- Produces: System Health summary, issues-first cards, four grouped ledgers, live clickable sidebar footer, Copy Report feedback, and confirmation requests for disruptive repairs.
- [ ] **Step 1: Write failing Settings and accessibility assertions**
Assert static structure and fixture-rendered state:
```bash
rg -Fq 'label: "System Health"' config/dot/quickshell/modules/settings/SettingsSidebar.qml
rg -Fq 'onClicked: Health.copyReport()' config/dot/quickshell/modules/settings/HealthSummary.qml
rg -Fq 'onTapped: root.pageRequested("services")' config/dot/quickshell/modules/settings/SettingsSidebar.qml
rg -Fq 'text: "Checking…"' config/dot/quickshell/modules/settings/HealthSummary.qml
rg -Fq 'Health.refresh()' config/dot/quickshell/modules/settings/HealthPage.qml
```
The runtime harness must prove warning rows appear before healthy groups, unconfigured is visible as `Not set up`, every status has text in addition to color, refresh preserves row geometry, keyboard focus reaches both hero actions and row actions, and a Quickshell-restart repair opens a confirmation sheet.
- [ ] **Step 2: Run UI contracts and verify failure**
Run:
```bash
tests/quickshell/health-ui-contract.sh
tests/quickshell/settings-pages-contract.sh
tests/quickshell/settings-search-contract.sh
```
Expected: FAIL because the approved Health components are absent.
- [ ] **Step 3: Implement the approved composition**
Use `SettingsPage`, `SettingsCard`, `SettingsButton`, `Theme`, and `PrismEdge`. Keep the summary hero stable at 126 px and each check row at least 62 px. Derive labels exactly:
```qml
function statusLabel(status: string): string {
if (status === "ok") return "Healthy";
if (status === "warning") return "Needs attention";
if (status === "error") return "Action required";
return "Not set up";
}
```
`HealthPage.Component.onCompleted` calls `Health.refresh()`. Issues are checks with `warning` or `error`. Group cards preserve helper order. Each row exposes at most one action. `open` actions route to exact Settings pages; `instructions` actions reveal authored inline instructions; `repair` actions call `Health.repair(id, false)` after confirmation only when `action.confirm === true`.
The sidebar footer is a 54 px `TapHandler` target with status text derived from Health, not a hardcoded string. It opens `services`; when no scan has completed it says `Checking Panama desktop`. A malformed or failed doctor run retains the last rows, changes only the hero to `Health check unavailable`, and exposes one bounded `Retry` action. The page ends with the approved boundary note and an `Open GNOME Settings` action for networking, printers, users, and other Fedora-owned areas.
- [ ] **Step 4: Run UI contracts and inspect the rendered fixture**
Run:
```bash
tests/quickshell/health-ui-contract.sh
tests/quickshell/settings-pages-contract.sh
tests/quickshell/settings-search-contract.sh
```
Expected: all PASS with zero QML warnings.
- [ ] **Step 5: Commit the Settings experience**
```bash
git add config/dot/quickshell/modules/settings config/dot/quickshell/services/SettingsSearch.qml tests/quickshell/health-ui-contract.sh tests/quickshell/settings-pages-contract.sh tests/quickshell/settings-search-contract.sh
git commit -m "Build the System Health settings page"
```
### Task 5: Quiet bar indicator and launcher entry point
**Files:**
- Create: `config/dot/quickshell/modules/bar/HealthIndicator.qml`
- Modify: `config/dot/quickshell/modules/bar/Bar.qml`
- Create: `config/local/share/vicinae/scripts/check-system-health.sh`
- Modify: `config/dot/quickshell/scripts/panama-action`
- Modify: `tests/quickshell/health-ui-contract.sh`
- Modify: `tests/quickshell/panama-action-contract.sh`
- Modify: `tests/quickshell/panama-commands-contract.sh`
**Interfaces:**
- Consumes: `Health.actionable`, `Health.status`, and `Health.summary`; existing `panama-action` dispatcher and Settings IPC.
- Produces: one degraded-only bar affordance and Vicinae command `Panama: Check System Health`.
- [ ] **Step 1: Extend contracts before production files**
Assert the indicator is absent for healthy/unconfigured-only fixtures, visible amber for warnings, visible red for errors, includes a textual accessible label, and opens `services`. Extend command fixtures so:
```text
panama-action health -> qs ipc call health open
check-system-health.sh title -> Panama: Check System Health
check-system-health.sh exec -> $HOME/.config/quickshell/scripts/panama-action health
```
- [ ] **Step 2: Run focused tests and verify failure**
Run:
```bash
tests/quickshell/health-ui-contract.sh
tests/quickshell/panama-action-contract.sh
tests/quickshell/panama-commands-contract.sh
```
Expected: FAIL on the missing indicator and command.
- [ ] **Step 3: Implement the quiet entry points**
Place `HealthIndicator` in the right-side bar row before `ActivityIndicator`. It has no reserved width while hidden, no animation, and one compact shield/wrench glyph with an issue-count tooltip or accessible description. Use `Theme.warn` only for warnings and `Theme.danger` only for errors. Clicking calls `ShellState.openSettings("services")` and `Health.refresh()`.
Add this dispatcher case and usage token:
```bash
health) qs ipc call health open ;;
```
Create a Vicinae script with schema version 1, silent mode, Panama Settings icon, keywords `health`, `doctor`, `repair`, `services`, and the stable `panama-action health` execution path.
- [ ] **Step 4: Run focused tests**
Run the three commands from Step 2.
Expected: all PASS; command count increases from 17 to 18.
- [ ] **Step 5: Commit the entry points**
```bash
git add config/dot/quickshell/modules/bar config/local/share/vicinae/scripts/check-system-health.sh config/dot/quickshell/scripts/panama-action tests/quickshell
git commit -m "Add quiet System Health entry points"
```
### Task 6: Allow-listed repairs and observed recovery
**Files:**
- Modify: `config/dot/quickshell/scripts/panama-doctor`
- Modify: `tests/quickshell/panama-doctor-contract.sh`
- Modify: `config/dot/quickshell/services/Health.qml`
- Modify: `tests/quickshell/health-service-contract.sh`
- Modify: `config/dot/quickshell/modules/settings/HealthCheckRow.qml`
- Modify: `tests/quickshell/health-ui-contract.sh`
**Interfaces:**
- Consumes: the fixed repair matrix in this plan and current accepted checks from `Health.qml`.
- Produces: `panama-doctor --repair CHECK_ID --json` result `{schemaVersion, checkId, accepted, exitCode, message}`, inline repair state, and one post-repair scan.
- [ ] **Step 1: Add exact repair-command tests**
For every repair ID, use fake commands and isolated paths to assert the exact argv. Assert all of these are rejected before any process or filesystem write:
```text
unknown.check
integration.home-assistant
input.brightness
desktop.notifications
../../escape
desktop.vicinae;touch injected
```
For runtime links, fixtures must prove only these link names are eligible: `hypr`, `quickshell`, `uwsm`, and `vicinae`; a regular user-owned directory is reported but never replaced. For Caffeine, only duplicate rows with application `Panama`, current UID, reason `Caffeine`, and mode `block` may yield numeric PIDs; leave one valid inhibitor alive and release extras.
- [ ] **Step 2: Run repair contracts and verify failure**
Run:
```bash
tests/quickshell/panama-doctor-contract.sh
tests/quickshell/health-service-contract.sh
```
Expected: FAIL because `--repair` is not implemented.
- [ ] **Step 3: Implement the authored repair registry**
Represent commands as immutable constant tuples or dedicated functions:
```python
REPAIR_COMMANDS = {
"desktop.hyprpaper": ("systemctl", "--user", "restart", "hyprpaper.service"),
"desktop.hypridle": ("systemctl", "--user", "restart", "hypridle.service"),
"desktop.vicinae": ("systemctl", "--user", "restart", "vicinae.service"),
"desktop.quickshell": ("panama-action", "restart-shell"),
}
```
Handle runtime links, Vicinae command linking, and duplicate inhibitors in dedicated functions that accept no caller-controlled path or command. Return JSON on every known failure. Unknown IDs exit 2 with `accepted: false` and do not invoke any runner.
`Health.repair(id, external)` requires the ID to exist in the current snapshot with `action.kind === "repair"`, records `repairingId`, runs the helper with an argument array, parses the result, clears the busy row, and requests exactly one fresh scan. Keep the row degraded until that scan reports recovery.
- [ ] **Step 4: Run repair and UI contracts**
Run:
```bash
tests/quickshell/panama-doctor-contract.sh
tests/quickshell/health-service-contract.sh
tests/quickshell/health-ui-contract.sh
```
Expected: all PASS.
- [ ] **Step 5: Commit repairs**
```bash
git add config/dot/quickshell/scripts/panama-doctor config/dot/quickshell/services/Health.qml config/dot/quickshell/modules/settings/HealthCheckRow.qml tests/quickshell
git commit -m "Add bounded Panama recovery actions"
```
### Task 7: Full verification, live read-only audit, and documentation
**Files:**
- Modify: `config/dot/hypr/DESKTOP-PARITY.md`
- Modify: `config/dot/quickshell/modules/settings/README.md`
- Modify: `docs/superpowers/plans/2026-08-18-panama-health-recovery.md`
**Interfaces:**
- Consumes: the complete feature and existing regression suite.
- Produces: current user documentation, a redacted live health snapshot, and final verification evidence.
- [ ] **Step 1: Document boundaries and entry points**
Document `Panama: Check System Health`, Settings → System Health, the degraded-only bar indicator, `panama-doctor --summary`, the no-`sudo`/no-package-install boundary, and the fact that GNOME/Fedora tools remain responsible for generic system configuration.
- [ ] **Step 2: Run syntax, focused, and full contracts**
Run:
```bash
python3 -m py_compile config/dot/quickshell/scripts/panama-doctor
bash -n config/dot/quickshell/scripts/panama-action
tests/quickshell/panama-doctor-contract.sh
tests/quickshell/health-service-contract.sh
tests/quickshell/health-ui-contract.sh
for test in tests/quickshell/*contract.sh; do "$test"; done
for test in tests/hypr/*contract.sh; do "$test"; done
```
Expected: every command exits 0; Quickshell tests report 58 contracts after the three new contracts land.
- [ ] **Step 3: Run a redacted live read-only comparison**
Run:
```bash
config/dot/quickshell/scripts/panama-doctor --json >"$(mktemp)"
config/dot/quickshell/scripts/panama-doctor --summary
systemctl --user is-active hyprpaper.service hypridle.service vicinae.service pipewire.service
qs ipc call health refresh
qs ipc call health status | jq '{status, busy, checks: [.checks[] | {id, status}]}'
```
Expected: helper and direct service states agree. Do not print details from integrations; copied and IPC reports contain only redacted authored observations.
- [ ] **Step 4: Reload and inspect the live shell**
Run:
```bash
qs reload
sleep 4
journalctl --user --since '-2 minutes' --no-pager | rg -i 'quickshell|qml|panama' | tail -200
```
Expected: the shell returns, System Health opens, the healthy state is silent, and there are no new QML errors or binding-loop warnings. Do not invoke a repair during this step.
- [ ] **Step 5: Final diff and commit**
Run:
```bash
git diff --check
git status --short
git diff --stat origin/main...HEAD
git add config/dot/hypr/DESKTOP-PARITY.md config/dot/quickshell/modules/settings/README.md docs/superpowers/plans/2026-08-18-panama-health-recovery.md
git commit -m "Document Panama health and recovery"
```
Expected: only intentional Health & Recovery files are present and no workstation-specific values appear in the diff.
@@ -1,235 +0,0 @@
# Panama Health & Recovery Design
## Purpose
Panama Health & Recovery makes the desktop explain itself. It verifies the
local services, dependencies, links, and integrations that Panama relies on,
then presents useful recovery actions without asking the user to read logs or
diagnose a collection of unrelated Linux processes.
The feature is intentionally quiet. A healthy desktop produces no notification,
banner, or permanent bar ornament. Problems appear in Panama Settings and, when
actionable, as one restrained bar indicator. User-initiated repairs receive
immediate Prism OSD or inline feedback.
## Product boundaries
The first release covers Panama-owned or Panama-integrated functionality:
- Hyprland, Quickshell, the notification server, XDG desktop portals, PipeWire,
Vicinae, the clipboard watcher, wallpaper, idle policy, and Panama's runtime
configuration links.
- The Panama command collection, screenshot and OCR dependencies, DDC
brightness support, and the currently selected terminal and launcher.
- Nextcloud, RustDesk, KDE Connect, BlueBubbles, Home Assistant, calendar
aggregation, and the configured autostart entries.
- Orphaned Panama processes and inhibitors, including duplicate Caffeine locks.
- Versions and non-sensitive diagnostic context needed for a useful copied
report.
It does not become a package manager, a generic system monitor, or a replacement
for Fedora's troubleshooting tools. It never installs packages, invokes `sudo`,
deletes user data, rewrites arbitrary configuration, or repairs services Panama
does not own.
An optional integration that has never been configured is neutral **Not set
up**, not a warning. A configured integration that cannot operate is degraded.
This distinction prevents the health UI from pressuring the user to enable
features they do not want.
## Information architecture
The existing **Startup & Services** destination becomes **System Health**. This
avoids two pages reporting the same background services. Its existing Open and
Refresh actions remain available through the richer health rows.
The page has four levels:
1. A compact summary hero: **Healthy**, **Needs attention**, or **Action
required**, the last completed scan time, Refresh, and Copy Report.
2. An issues-first section shown only when one or more checks are degraded.
3. Grouped cards for Desktop Foundation, Input & Media, Integrations, and Panama
Tools. Healthy rows remain visible but visually quiet.
4. A short boundary note linking to GNOME or Fedora tools for system areas Panama
does not own.
Each row contains a stable title, one-sentence observation, status label, and at
most one primary action. Actions use concrete language such as **Restart
Vicinae**, **Repair command link**, **Open Home settings**, or **View setup
instructions**. There is no generic Fix Everything button.
The Settings sidebar's existing health footer becomes real and clickable. It
shows the aggregate state and opens System Health. The top bar gains a small
`HealthIndicator` only while an actionable warning or error exists; clicking it
opens the same page. Background scans never publish Signal Glass events or
desktop notifications.
## Diagnostic engine
`config/dot/quickshell/scripts/panama-doctor` is the single operating-system
boundary. It supports:
- `panama-doctor --json` for a complete versioned snapshot.
- `panama-doctor --summary` for a concise human-readable installer or terminal
result.
- `panama-doctor --repair CHECK_ID --json` for an explicitly allow-listed repair.
The helper emits one schema:
```json
{
"schemaVersion": 1,
"generatedAt": "2026-08-18T12:00:00Z",
"summary": {
"status": "warning",
"healthy": 18,
"warnings": 1,
"errors": 0,
"unconfigured": 2
},
"checks": [
{
"id": "launcher.panama-commands",
"group": "panama-tools",
"title": "Panama Commands",
"status": "warning",
"detail": "16 of 17 commands are loaded",
"action": {
"kind": "repair",
"label": "Repair command link"
}
}
]
}
```
Allowed statuses are `ok`, `warning`, `error`, and `unconfigured`. Check IDs,
group IDs, titles, and repair mappings are authored constants. Probe output may
populate observations but can never become a command or executable argument.
Checks run concurrently where doing so is safe, with short per-probe timeouts.
A failed or timed-out probe yields a check result rather than aborting the whole
snapshot. Output order is deterministic so tests, copied reports, and visual
rows do not jump between scans.
No secrets are read. The report may state whether a Home Assistant URL or token
is configured, but never includes either value. It excludes clipboard contents,
notification bodies, calendar event data, SSIDs, device addresses, environment
values, file contents, and command output that has not been explicitly parsed.
## Quickshell state and refresh model
`services/Health.qml` owns the latest accepted snapshot, aggregate severity,
busy state, last scan time, and the result of the most recent repair. It invokes
`panama-doctor` with argument arrays through `Process`; UI components never
construct shell commands.
Health performs one delayed scan after the shell reaches a stable startup state.
It scans again when the System Health page is opened, when the user presses
Refresh, and after a repair settles. There is no periodic polling loop while the
desktop is idle. Services that already expose event-driven state remain the
authoritative source for their own interactive controls; Health is a diagnostic
snapshot, not a competing live service model.
Every scan receives a monotonically increasing generation. Late output from an
older scan is discarded. A malformed snapshot leaves the last valid result in
place, marks the diagnostic engine unavailable, and offers a bounded Retry.
The shell exposes a typed `health` IPC target with `refresh`, `status`, `open`,
and `repair(id)` operations. Vicinae gains **Panama: Check System Health**, which
opens the page and requests a fresh scan through the existing `panama-action`
dispatcher.
## Repair policy
Repairs are narrow, reversible, and attached to one check. The first release may:
- Restart Panama's user services such as Vicinae, Hyprpaper, or Hypridle.
- Recreate Panama-owned symlinks when their destination is known and tracked.
- Reload Vicinae's Panama command collection.
- Release duplicate user-owned inhibitors whose metadata identifies Panama and
Caffeine.
- Restart Quickshell through the verified `panama-action restart-shell` path.
- Open the exact Panama Settings page required to finish credentials or entity
selection.
Restarting a working service is not presented as a repair. Repairs that interrupt
visible desktop chrome require a confirmation sheet in Settings. Navigation and
setup actions do not. Package installation, privileged service changes, display
mode writes, and destructive cleanup are never automatic; the UI shows concise
instructions instead.
After a repair, Health rescans and judges success from the observed result. A
zero exit status alone never turns a row green. Failure remains inline on the
affected row and also produces the existing Panama action-failure notification
when the action originated outside Settings.
## Visual language and interaction
System Health uses the established Settings cards and Prism tokens. Healthy
states use a small muted green dot and subdued **Healthy** copy. Warnings use
amber; red is reserved for functionality that is configured, required, and
currently broken. `unconfigured` rows use neutral gray.
The summary hero does not use a decorative gauge, percentage score, pulse,
shimmer, or animated gradient. A desktop is not “82% healthy.” The headline and
issue count are more understandable and do not create false precision.
Rows keep their height stable while refreshing. The previous snapshot remains
visible with a quiet **Checking…** label rather than replacing the page with a
spinner. Keyboard focus order reaches Refresh, Copy Report, issue rows, repair
actions, and external handoffs. Status is always expressed in text as well as
color.
Before production components are edited, the page and degraded bar indicator
will be shown in several static mocks using the existing Settings geometry. The
chosen mock must preserve this information architecture and Panama's current
Prism language rather than introduce a new visual system.
## Failure handling
- Missing required executables become actionable check results.
- Missing optional applications remain neutral until configured.
- A doctor crash, timeout, or malformed JSON does not clear the last good
snapshot or crash Quickshell.
- Concurrent refresh requests coalesce into one follow-up scan.
- A repair request for an unknown or non-repairable ID is rejected before any
process starts.
- Copy Report uses only the already-redacted snapshot and reports clipboard
failure inline.
- If the Settings window is closed during a scan or repair, the process may
finish; reopening the page shows the settled result.
## Verification
- Run the real helper against isolated fake command, config, state, and runtime
directories and prove every status transition deterministically.
- Validate the JSON schema, stable check IDs, deterministic ordering, and
uniqueness of each ID.
- Prove unconfigured integrations remain neutral while configured failures are
degraded.
- Prove reports contain no fixture secrets, clipboard text, calendar data,
addresses, or unparsed environment values.
- Exercise every repair through the allow-list, assert its exact command, and
prove unknown IDs cannot execute anything.
- Test scan generations, malformed snapshots, refresh coalescing, repair
rescans, and preservation of the last valid state in a Quickshell harness.
- Verify Settings routing, search entries, the live sidebar footer, and the
degraded-only bar indicator without QML warnings.
- Validate the Vicinae command and typed IPC surface.
- Run a read-only doctor scan on the real workstation and compare key results to
direct service checks. State-changing live repair tests require an actually
degraded disposable target or explicit user approval.
- Restart the live shell, inspect the fresh log, and visually review healthy,
warning, error, unconfigured, refreshing, and repair-result states.
## Delivery slices
1. Diagnostic schema, read-only probes, redaction, and contract tests.
2. `Health.qml`, typed IPC, startup/manual refresh, and fixture harness.
3. System Health Settings page, live sidebar footer, search, and report copy.
4. Degraded-only bar indicator and Vicinae command.
5. Allow-listed repairs, confirmations, post-repair verification, and live audit.
The slices are one feature and land together. Their order keeps the UI backed by
real diagnostics from its first production render.
+158 -1
View File
@@ -26,10 +26,62 @@ printf 'brightnessctl' >>"$OSD_TEST_LOG"
printf ' <%s>' "$@" >>"$OSD_TEST_LOG" printf ' <%s>' "$@" >>"$OSD_TEST_LOG"
printf '\n' >>"$OSD_TEST_LOG" printf '\n' >>"$OSD_TEST_LOG"
if [[ " $* " == *" -m "* && " $* " != *" set "* ]]; then if [[ " $* " == *" -m "* && " $* " != *" set "* ]]; then
[[ ${BACKLIGHT_AVAILABLE:-true} == true ]] || exit 1
printf '%s\n' "${BRIGHTNESS_OUTPUT:-intel_backlight,backlight,500,1000,50%}" printf '%s\n' "${BRIGHTNESS_OUTPUT:-intel_backlight,backlight,500,1000,50%}"
fi fi
SH SH
cat >"$scratch/bin/panama-brightness" <<'SH'
#!/bin/bash
printf 'panama-brightness' >>"$OSD_TEST_LOG"
printf ' <%s>' "$@" >>"$OSD_TEST_LOG"
printf '\n' >>"$OSD_TEST_LOG"
case "${1:-}" in
list)
if [[ -n ${DDC_LIST_JSON:-} ]]; then
printf '%s\n' "$DDC_LIST_JSON"
else
printf '%s\n' '{"displays":[],"error":"No displays"}'
fi
;;
get)
[[ ${DDC_FAIL_GET_BUS:-} != "${2:-}" ]] || exit 1
if [[ -s $OSD_DDC_STATE ]]; then
cat "$OSD_DDC_STATE"
else
printf '%s\n' "${DDC_GET_VALUE:-40}"
fi
;;
set)
if [[ -n ${DDC_SET_DELAY:-} ]]; then
if ! mkdir "$OSD_DDC_PROBE" 2>/dev/null; then
printf 'ddc-overlap\n' >>"$OSD_TEST_LOG"
fi
sleep "$DDC_SET_DELAY"
rmdir "$OSD_DDC_PROBE" 2>/dev/null || true
fi
printf '%s\n' "${3:-0}" >"$OSD_DDC_STATE"
;;
*) exit 2 ;;
esac
SH
cat >"$scratch/bin/hyprctl" <<'SH'
#!/bin/bash
printf 'hyprctl' >>"$OSD_TEST_LOG"
printf ' <%s>' "$@" >>"$OSD_TEST_LOG"
printf '\n' >>"$OSD_TEST_LOG"
printf '[{"name":"%s","focused":true}]\n' "${FOCUSED_MONITOR:-DP-2}"
SH
cat >"$scratch/bin/notify-send" <<'SH'
#!/bin/bash
printf 'notify-send' >>"$OSD_TEST_LOG"
printf ' <%s>' "$@" >>"$OSD_TEST_LOG"
printf '\n' >>"$OSD_TEST_LOG"
SH
cat >"$scratch/bin/playerctl" <<'SH' cat >"$scratch/bin/playerctl" <<'SH'
#!/bin/bash #!/bin/bash
printf 'playerctl' >>"$OSD_TEST_LOG" printf 'playerctl' >>"$OSD_TEST_LOG"
@@ -53,9 +105,21 @@ SH
chmod +x "$scratch/bin/"* chmod +x "$scratch/bin/"*
run_helper() { run_helper() {
local runtime="${OSD_RUNTIME_DIR:-$scratch/runtime-default}"
mkdir -p "$runtime"
PATH="$scratch/bin:$PATH" OSD_TEST_LOG="$log" \ PATH="$scratch/bin:$PATH" OSD_TEST_LOG="$log" \
OSD_TEST_FAIL_QS="${OSD_TEST_FAIL_QS:-false}" \ OSD_TEST_FAIL_QS="${OSD_TEST_FAIL_QS:-false}" \
PANAMA_OSD_STRICT="${PANAMA_OSD_STRICT:-false}" \ PANAMA_OSD_STRICT="${PANAMA_OSD_STRICT:-false}" \
PANAMA_OSD_BRIGHTNESS_HELPER="$scratch/bin/panama-brightness" \
PANAMA_OSD_RUNTIME_DIR="$runtime" \
OSD_DDC_STATE="$runtime/ddc-state" \
OSD_DDC_PROBE="$runtime/ddc-probe" \
BACKLIGHT_AVAILABLE="${BACKLIGHT_AVAILABLE:-true}" \
DDC_LIST_JSON="${DDC_LIST_JSON:-}" \
DDC_GET_VALUE="${DDC_GET_VALUE:-40}" \
DDC_FAIL_GET_BUS="${DDC_FAIL_GET_BUS:-}" \
DDC_SET_DELAY="${DDC_SET_DELAY:-}" \
FOCUSED_MONITOR="${FOCUSED_MONITOR:-DP-2}" \
"$helper" "$@" "$helper" "$@"
} }
@@ -86,9 +150,102 @@ assert_line 'qs <ipc> <call> <osd> <progress> <microphone-muted> <72> <100> <Mut
: >"$log" : >"$log"
run_helper brightness up 5 run_helper brightness up 5
assert_line 'brightnessctl <-e4> <-n2> <set> <5%+>'
assert_line 'brightnessctl <-m> <-c> <backlight>' assert_line 'brightnessctl <-m> <-c> <backlight>'
assert_line 'brightnessctl <-e4> <-n2> <-c> <backlight> <set> <5%+>'
assert_line 'qs <ipc> <call> <osd> <progress> <brightness> <50> <100> <50%>' assert_line 'qs <ipc> <call> <osd> <progress> <brightness> <50> <100> <50%>'
if grep -Fq 'panama-brightness' "$log"; then
printf 'osd helper contract: DDC fallback ran despite a native backlight\n' >&2
exit 1
fi
: >"$log"
OSD_RUNTIME_DIR="$scratch/runtime-ddc" \
BACKLIGHT_AVAILABLE=false \
DDC_LIST_JSON='{"displays":[{"bus":3,"connector":"HDMI-A-1","value":35},{"bus":5,"connector":"DP-2","value":40}],"error":""}' \
run_helper brightness up 5
assert_line 'hyprctl <-j> <monitors>'
assert_line 'panama-brightness <list>'
assert_line 'panama-brightness <get> <5>'
assert_line 'panama-brightness <set> <5> <45>'
assert_line 'qs <ipc> <call> <osd> <progress> <brightness> <45> <100> <45%>'
# A cached bus avoids the expensive display scan on subsequent key presses.
: >"$log"
OSD_RUNTIME_DIR="$scratch/runtime-ddc" \
BACKLIGHT_AVAILABLE=false \
DDC_LIST_JSON='{"displays":[{"bus":3,"connector":"HDMI-A-1","value":35},{"bus":5,"connector":"DP-2","value":45}],"error":""}' \
run_helper brightness down 5
assert_line 'panama-brightness <get> <5>'
assert_line 'panama-brightness <set> <5> <40>'
assert_line 'qs <ipc> <call> <osd> <progress> <brightness> <40> <100> <40%>'
if grep -Fq 'panama-brightness <list>' "$log"; then
printf 'osd helper contract: cached DDC bus triggered another display scan\n' >&2
exit 1
fi
# A disconnected cached monitor is discarded and rediscovered once.
mkdir -p "$scratch/runtime-ddc-stale"
printf '9\tDP-9\n' >"$scratch/runtime-ddc-stale/brightness-bus"
: >"$log"
OSD_RUNTIME_DIR="$scratch/runtime-ddc-stale" \
BACKLIGHT_AVAILABLE=false \
DDC_FAIL_GET_BUS=9 \
DDC_LIST_JSON='{"displays":[{"bus":5,"connector":"DP-2","value":40}],"error":""}' \
run_helper brightness up 5
assert_line 'panama-brightness <get> <9>'
assert_line 'panama-brightness <list>'
assert_line 'panama-brightness <get> <5>'
assert_line 'panama-brightness <set> <5> <45>'
# If the focused output is not DDC-capable, use the first discovered display.
: >"$log"
OSD_RUNTIME_DIR="$scratch/runtime-ddc-first" \
BACKLIGHT_AVAILABLE=false \
FOCUSED_MONITOR='eDP-1' \
DDC_GET_VALUE=35 \
DDC_LIST_JSON='{"displays":[{"bus":3,"connector":"HDMI-A-1","value":35},{"bus":5,"connector":"DP-2","value":40}],"error":""}' \
run_helper brightness down 10
assert_line 'panama-brightness <get> <3>'
assert_line 'panama-brightness <set> <3> <25>'
assert_line 'qs <ipc> <call> <osd> <progress> <brightness> <25> <100> <25%>'
# Permission and discovery errors must be visible, never masquerade as 0%.
: >"$log"
OSD_RUNTIME_DIR="$scratch/runtime-ddc-error" \
BACKLIGHT_AVAILABLE=false \
DDC_LIST_JSON='{"displays":[],"error":"Run sudo udevadm control --reload-rules && sudo udevadm trigger --subsystem-match=i2c-dev --subsystem-match=drm"}' \
run_helper brightness up 5
assert_line 'qs <ipc> <call> <osd> <message> <dialog-warning-symbolic> <Brightness needs permission>'
assert_line 'notify-send <--app-name=Panama> <--icon=display-brightness-symbolic> <Brightness unavailable> <Run sudo udevadm control --reload-rules && sudo udevadm trigger --subsystem-match=i2c-dev --subsystem-match=drm>'
if grep -Fq 'osd> <progress> <brightness>' "$log"; then
printf 'osd helper contract: unavailable brightness rendered a false percentage\n' >&2
exit 1
fi
# Separate key-repeat processes must not overlap their DDC transactions.
: >"$log"
OSD_RUNTIME_DIR="$scratch/runtime-ddc-lock" \
BACKLIGHT_AVAILABLE=false \
DDC_SET_DELAY=0.15 \
DDC_LIST_JSON='{"displays":[{"bus":5,"connector":"DP-2","value":40}],"error":""}' \
run_helper brightness up 5 &
first_pid=$!
OSD_RUNTIME_DIR="$scratch/runtime-ddc-lock" \
BACKLIGHT_AVAILABLE=false \
DDC_SET_DELAY=0.15 \
DDC_LIST_JSON='{"displays":[{"bus":5,"connector":"DP-2","value":40}],"error":""}' \
run_helper brightness up 5 &
second_pid=$!
wait "$first_pid"
wait "$second_pid"
if grep -Fqx 'ddc-overlap' "$log"; then
printf 'osd helper contract: concurrent DDC transactions overlapped\n' >&2
exit 1
fi
if [[ $(<"$scratch/runtime-ddc-lock/ddc-state") != 50 ]]; then
printf 'osd helper contract: serialized key repeats did not both apply\n' >&2
exit 1
fi
: >"$log" : >"$log"
run_helper media next run_helper media next