Author SHA1 Message Date
Gabriel Brown f63b683979 Design Panama Health and Recovery 2026-08-18 07:52:25 -04:00
8 changed files with 274 additions and 896 deletions
@@ -1,44 +1,24 @@
// Brightness, from whichever source this machine actually has. // Backlight slider, via brightnessctl.
// //
// Two exist and they are not interchangeable: // This machine drives an external DisplayPort monitor and has no backlight
// // class device at all (brightnessctl only reports keyboard/NIC LEDs), so the
// The kernel backlight class, driven by brightnessctl. Laptop panels have it; // row removes itself rather than sitting there as a dead control. Probed once
// this desktop does not -- brightnessctl reports only keyboard and NIC LEDs. // at startup — backlight devices do not appear and disappear.
//
// DDC/CI, the channel the buttons on a monitor's bezel drive. That is the
// only brightness an external display has, and it is per-monitor.
//
// A machine may have neither, either, or both, so this renders a row per source
// found and removes itself entirely when there are none, rather than sitting
// there as a dead control.
//
// Connector labels appear only when there is more than one row. A single
// slider needs no explanation of which screen it dims.
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import qs.widgets import qs.widgets
import qs.config import qs.config
import qs.services
Item { Item {
id: root id: root
property bool backlightAvailable: false property bool available: false
property real backlightValue: 0 property real value: 0
readonly property int rowCount: (root.backlightAvailable ? 1 : 0) + Brightness.displays.length visible: root.available
readonly property bool labelled: root.rowCount > 1 implicitHeight: root.available ? 32 : 0
visible: root.rowCount > 0
implicitHeight: rows.implicitHeight
// Probing I2C takes on the order of a second, so it waits until the panel
// is actually on screen rather than running at shell startup. Monitors do
// not come and go, so once is enough.
onVisibleChanged: if (visible && !Brightness.scanned) Brightness.refresh()
Component.onCompleted: if (root.visible && !Brightness.scanned) Brightness.refresh()
// `-m` is the machine-readable form: name,class,current,percent,max // `-m` is the machine-readable form: name,class,current,percent,max
Process { Process {
@@ -54,85 +34,18 @@ Item {
const fields = line.split(","); const fields = line.split(",");
if (fields.length < 5 || fields[1] !== "backlight") if (fields.length < 5 || fields[1] !== "backlight")
continue; continue;
root.backlightAvailable = true; root.available = true;
root.backlightValue = parseInt(fields[3]) / 100; root.value = parseInt(fields[3]) / 100;
return; return;
} }
} }
function applyBacklight(v: real): void { function apply(v: real): void {
root.backlightValue = v; root.value = v;
// Never go fully dark: a 0% backlight looks like a broken shell. // Never go fully dark: a 0% backlight looks like a broken shell.
Quickshell.execDetached(["brightnessctl", "-c", "backlight", "-q", "set", Math.max(1, Math.round(v * 100)) + "%"]); Quickshell.execDetached(["brightnessctl", "-c", "backlight", "-q", "set", Math.max(1, Math.round(v * 100)) + "%"]);
} }
Column {
id: rows
anchors.left: parent.left
anchors.right: parent.right
spacing: 4
BrightnessRow {
width: rows.width
visible: root.backlightAvailable
label: "Built-in"
value: root.backlightValue
onMoved: v => root.applyBacklight(v)
}
Repeater {
model: Brightness.displays
BrightnessRow {
required property var modelData
width: rows.width
// Hyprland already knows what each output is called, so the
// name comes from there rather than from a second source that
// could disagree with the Displays page. The connector is the
// fallback, so a display is never an unlabelled slider.
label: Displays.monitorNamed(modelData.connector)?.description || modelData.connector
value: modelData.value / 100
onMoved: v => Brightness.set(modelData.bus, Math.round(v * 100))
}
}
}
component BrightnessRow: Item {
id: row
property string label: ""
property real value: 0
signal moved(real value)
implicitHeight: caption.height + control.height
Text {
id: caption
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: 6
anchors.rightMargin: 6
anchors.top: parent.top
visible: root.labelled
height: visible ? implicitHeight + 2 : 0
text: row.label
color: Theme.fgDim
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
elide: Text.ElideRight
}
// The slider and its glyph share one strip so the two stay aligned
// whether or not a caption sits above them. Anchoring the glyph to
// both a caption and a centre line instead would conflict, and an
// anchor set to undefined is not released.
Item {
id: control
anchors.left: parent.left
anchors.right: parent.right
anchors.top: caption.bottom
height: 32
// ValueSlider draws its own leading icon, but symbolic icons need // ValueSlider draws its own leading icon, but symbolic icons need
// recolouring to be visible — see ThemedIcon. // recolouring to be visible — see ThemedIcon.
ThemedIcon { ThemedIcon {
@@ -150,9 +63,7 @@ Item {
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: 32 anchors.rightMargin: 32
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
value: row.value value: root.value
onMoved: v => row.moved(v) onMoved: v => root.apply(v)
}
}
} }
} }
@@ -1,7 +1,7 @@
// Displays. // Displays.
// //
// Resolution, refresh rate, scale, and rotation, plus panel brightness and the // Resolution, refresh rate, scale, and rotation, plus the gaming display
// gaming display policy that was already here. // policy that was already here.
// //
// Every geometry change goes through an apply-then-confirm countdown. This is // Every geometry change goes through an apply-then-confirm countdown. This is
// the one page where a wrong value can leave the screen unreadable or blank, // the one page where a wrong value can leave the screen unreadable or blank,
@@ -32,14 +32,7 @@ SettingsPage {
root.selectedOutput = Displays.monitors.length > 0 ? Displays.monitors[0].name : ""; root.selectedOutput = Displays.monitors.length > 0 ? Displays.monitors[0].name : "";
} }
// Probing I2C for DDC-capable monitors takes on the order of a second, so Component.onCompleted: root.syncSelectedOutput()
// it runs when this page is opened rather than at shell startup. Monitors
// do not appear while you are looking at a settings page, so once is enough.
Component.onCompleted: {
root.syncSelectedOutput();
if (!Brightness.scanned)
Brightness.refresh();
}
Connections { Connections {
target: Displays target: Displays
function onMonitorsChanged(): void { root.syncSelectedOutput(); } function onMonitorsChanged(): void { root.syncSelectedOutput(); }
@@ -207,45 +200,6 @@ SettingsPage {
SliderRow { setting: "nightLightTemperature"; divider: false } SliderRow { setting: "nightLightTemperature"; divider: false }
} }
// Panel brightness, over DDC/CI.
//
// This is hardware state rather than a stored preference: the monitor
// remembers it, the bezel buttons change it behind Panama's back, and
// writing it into settings.json would mean restoring a value the panel had
// already moved on from. So there is no schema key here and no SliderRow --
// the rows read and write the display directly.
SettingsCard {
visible: Brightness.available || Brightness.lastError !== ""
title: "Brightness"
subtitle: Brightness.available
? "Sent to the monitor over DDC/CI, the same channel its buttons use."
: Brightness.lastError
Repeater {
model: Brightness.displays
SettingRow {
id: brightnessRow
required property var modelData
required property int index
label: Displays.monitorNamed(modelData.connector)?.description || modelData.connector
detail: modelData.connector ? modelData.connector + " · " + modelData.value + "%"
: modelData.value + "%"
divider: brightnessRow.index < Brightness.displays.length - 1
controlWidth: 190
ValueSlider {
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
width: parent.width
value: brightnessRow.modelData.value / 100
onMoved: v => Brightness.set(brightnessRow.modelData.bus, Math.round(v * 100))
}
}
}
}
SettingsCard { SettingsCard {
title: "Gaming display policy" title: "Gaming display policy"
subtitle: "Applied immediately and restored when Panama starts." subtitle: "Applied immediately and restored when Panama starts."
@@ -1,125 +0,0 @@
#!/usr/bin/env bash
# External monitor brightness over DDC/CI.
#
# A desktop with no backlight class device has no brightness control at all --
# brightnessctl only sees keyboard and NIC LEDs. The panel itself still has a
# brightness setting, reachable over the monitor's DDC/CI channel (VCP feature
# 0x10), which is what the buttons on the bezel drive.
#
# Usage:
# panama-brightness list -> {"displays":[...],"error":""}
# panama-brightness get <bus> -> integer percent
# panama-brightness set <bus> <pct> -> applies, prints nothing
#
# Displays are enumerated from sysfs rather than from `ddcutil detect`. The
# kernel publishes the connector-to-I2C-bus mapping directly, as
# /sys/class/drm/<card>-<connector>/ddc, along with whether anything is plugged
# in. That is better than parsing detect output in three ways: the format is
# stable where detect's brief output is undocumented, the connector name comes
# out exactly as Hyprland and the Displays page already spell it (DP-2), and
# only connectors with a monitor attached get probed -- one bus on this machine
# instead of fourteen, which is the difference between a fast scan and a slow
# one, since each probe of an empty bus waits for a timeout.
#
# No model name is reported. Hyprland already knows the human-readable
# description of every output, so the UI joins on the connector name rather than
# having two sources of truth for what a monitor is called.
set -uo pipefail
readonly VCP_BRIGHTNESS=0x10
# Test seams. The contract needs to exercise enumeration and parsing on a
# machine whose real monitors it must not touch, so both roots this script
# reads are overridable. Nothing sets them in normal use.
readonly DRM_ROOT="${PANAMA_BRIGHTNESS_DRM_ROOT:-/sys/class/drm}"
readonly DEV_ROOT="${PANAMA_BRIGHTNESS_DEV_ROOT:-/dev}"
emit_error() {
printf '{"displays":[],"error":%s}\n' "$(jq -Rn --arg e "$1" '$e')"
exit 0
}
command -v ddcutil >/dev/null 2>&1 || emit_error 'ddcutil is not installed'
# Reading a VCP value needs read/write access to the monitor's I2C bus. The
# udev rule ddcutil ships grants that to the seat user through uaccess, but only
# to devices created after the rule was installed -- so a machine that installed
# ddcutil without rebooting has the rule in place and no access to show for it.
# That is by far the most likely reason for an empty list, and it is fixable in
# one command, so say so rather than reporting "no displays".
has_accessible_bus() {
local dev
for dev in "$DEV_ROOT"/i2c-*; do
[[ -r "$dev" && -w "$dev" ]] && return 0
done
return 1
}
cmd_list() {
has_accessible_bus || emit_error 'no I2C bus is accessible. ddcutil ships a udev rule that grants this, but only to devices created after it was installed. Run: sudo udevadm control --reload-rules && sudo udevadm trigger --subsystem-match=i2c-dev --subsystem-match=drm'
local rows=() connector bus value path
for path in "$DRM_ROOT"/card*-*; do
[[ -e "$path/ddc" ]] || continue
[[ "$(cat "$path/status" 2>/dev/null)" == "connected" ]] || continue
# card1-DP-2 -> DP-2, the name Hyprland uses.
connector="$(basename "$path")"
connector="${connector#card*-}"
bus="$(basename "$(readlink -f "$path/ddc")")"
bus="${bus#i2c-}"
[[ "$bus" =~ ^[0-9]+$ ]] || continue
# A monitor that does not implement 0x10 is not an error; it simply
# cannot be controlled, and is left out rather than shown as a slider
# that does nothing.
value="$(cmd_get "$bus")" || continue
[[ -n "$value" ]] || continue
rows+=("$(jq -cn \
--argjson bus "$bus" \
--arg connector "$connector" \
--argjson value "$value" \
'{bus: $bus, connector: $connector, value: $value}')")
done
if [[ ${#rows[@]} -eq 0 ]]; then
emit_error 'no connected monitor reports DDC/CI brightness. Some panels implement it only when "DDC/CI" is enabled in their on-screen menu.'
fi
printf '{"displays":[%s],"error":""}\n' "$(IFS=,; printf '%s' "${rows[*]}")"
}
# Prints the current brightness as a whole percent, or nothing when the display
# cannot report it. `getvcp --brief` is documented as machine readable and
# answers "VCP 10 C <current> <max>"; the max is almost always 100 but is not
# guaranteed to be, so it is read rather than assumed.
cmd_get() {
local bus="$1" out current max
out="$(timeout 10 ddcutil --bus "$bus" getvcp "$VCP_BRIGHTNESS" --brief 2>/dev/null)" || return 1
read -r _ _ _ current max <<<"$out"
[[ "$current" =~ ^[0-9]+$ && "$max" =~ ^[0-9]+$ && "$max" -gt 0 ]] || return 1
printf '%s' "$(( current * 100 / max ))"
}
cmd_set() {
local bus="$1" percent="$2" max out
[[ "$percent" =~ ^[0-9]+$ ]] || return 1
(( percent > 100 )) && percent=100
out="$(timeout 10 ddcutil --bus "$bus" getvcp "$VCP_BRIGHTNESS" --brief 2>/dev/null)" || return 1
read -r _ _ _ _ max <<<"$out"
[[ "$max" =~ ^[0-9]+$ && "$max" -gt 0 ]] || max=100
timeout 10 ddcutil --bus "$bus" setvcp "$VCP_BRIGHTNESS" "$(( percent * max / 100 ))" >/dev/null 2>&1
}
case "${1:-list}" in
list) cmd_list ;;
get) cmd_get "${2:?bus required}" ;;
set) cmd_set "${2:?bus required}" "${3:?percent required}" ;;
*) printf 'usage: panama-brightness [list|get <bus>|set <bus> <percent>]\n' >&2; exit 2 ;;
esac
+4 -139
View File
@@ -2,8 +2,6 @@
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 ]]
} }
@@ -69,151 +67,18 @@ 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|down) ;; up) brightnessctl -e4 -n2 set "${step}%+" >/dev/null || return ;;
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="$(brightness_percent "$output")" || return 0 percent="$(awk -F, 'NR == 1 { value=$5; gsub(/%/, "", value); print value }' <<<"$output")"
[[ $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,163 +0,0 @@
pragma Singleton
// Panel brightness for external monitors, over DDC/CI.
//
// brightnessctl covers laptop panels through the kernel's backlight class. A
// desktop driving a DisplayPort monitor has no such device, so it has no
// brightness control at all -- the only way to dim the screen is the buttons on
// the bezel. DDC/CI is the channel those buttons drive, and monitors expose it
// over the same I2C lines that carry EDID.
//
// Two things shape everything here:
//
// Detection is slow. Probing every I2C bus takes on the order of a second,
// which is far too slow to sit in front of a settings page opening. It runs
// once, on demand, and afterwards each display is addressed by its bus number
// directly.
//
// Writes are slow AND rate-limited by the monitor's firmware. A slider drag
// emits values continuously; sending each one produces a queue the panel
// works through seconds after the user let go, and some monitors drop or
// garble writes that arrive too fast. So `value` updates immediately for the
// UI and the hardware write is debounced, with only the latest value sent.
//
// Displays are keyed by DRM connector name (DP-2) so they line up with what
// Hyprland, the Displays page, and the monitor list already call them.
import Quickshell
import Quickshell.Io
import QtQuick
Singleton {
id: root
readonly property string helperPath: Quickshell.shellDir + "/scripts/panama-brightness"
// [{ bus, connector, model, value }] where value is 0..100.
property var displays: []
property bool scanning: false
// Empty when everything is fine. Carries the helper's explanation
// otherwise -- most usefully the udev command that grants I2C access,
// which is the difference between "brightness is unavailable" and
// "brightness is one command away".
property string lastError: ""
readonly property bool available: root.displays.length > 0
// True once a scan has completed, however it went. Lets the UI tell "not
// looked yet" apart from "looked and found nothing", which otherwise render
// identically and leave a permanently empty panel with no explanation.
property bool scanned: false
// Pending writes, keyed by bus. A monitor being dragged accumulates exactly
// one entry no matter how many values the slider emits.
property var pending: ({})
function refresh(): void {
if (root.scanning)
return;
root.scanning = true;
scan.running = true;
}
function displayFor(connector: string): var {
return root.displays.find(display => display.connector === connector) ?? null;
}
// Sets brightness for one display. The stored value moves at once so the
// slider tracks the pointer; the hardware follows when the drag settles.
function set(bus: int, percent: int): void {
const clamped = Math.max(0, Math.min(100, Math.round(percent)));
root.displays = root.displays.map(display =>
display.bus === bus ? Object.assign({}, display, { value: clamped }) : display);
const next = Object.assign({}, root.pending);
next[String(bus)] = clamped;
root.pending = next;
writeDebounce.restart();
}
Process {
id: scan
command: [root.helperPath, "list"]
stdout: StdioCollector {
onStreamFinished: {
try {
const parsed = JSON.parse(this.text);
root.displays = Array.isArray(parsed.displays) ? parsed.displays : [];
root.lastError = String(parsed.error ?? "");
} catch (error) {
root.displays = [];
root.lastError = "Could not read the brightness helper's output.";
console.warn("Brightness: could not parse helper output:", error);
}
root.scanning = false;
root.scanned = true;
}
}
}
// Long enough that a drag produces one write rather than dozens, short
// enough that a single click still feels immediate.
Timer {
id: writeDebounce
interval: 120
onTriggered: root.pump()
}
// Writes run one at a time, and each is read back.
//
// Serial because DDC/CI is a bus protocol with no arbitration: two ddcutil
// processes talking to the same monitor interleave their exchanges and both
// can come back with garbage. Read back because a write is not a promise --
// panels clamp to their own range, ignore values while waking from standby,
// and drop writes that arrive too quickly. Without the read the slider shows
// what Panama asked for rather than what the monitor did, which is the same
// class of lie as trusting `hyprctl keyword` to have applied something.
property int writingBus: -1
function pump(): void {
if (writer.running || reader.running)
return;
for (const bus in root.pending) {
const value = root.pending[bus];
const remaining = Object.assign({}, root.pending);
delete remaining[bus];
root.pending = remaining;
root.writingBus = parseInt(bus);
writer.command = [root.helperPath, "set", bus, String(value)];
writer.running = true;
return;
}
}
Process {
id: writer
onExited: {
reader.command = [root.helperPath, "get", String(root.writingBus)];
reader.running = true;
}
}
Process {
id: reader
stdout: StdioCollector {
onStreamFinished: {
const actual = parseInt(this.text.trim());
if (!isNaN(actual)) {
root.displays = root.displays.map(display =>
display.bus === root.writingBus
? Object.assign({}, display, { value: actual })
: display);
}
root.writingBus = -1;
// Anything queued while this write was in flight goes now.
root.pump();
}
}
}
}
@@ -0,0 +1,235 @@
# 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.
@@ -1,142 +0,0 @@
#!/usr/bin/env bash
# panama-brightness enumerates monitors from sysfs and speaks DDC/CI to them.
#
# The parts worth pinning down are the ones that decide whether a slider appears
# at all, and whether it appears attached to the right screen:
#
# * only connectors with something plugged in are probed, because probing an
# empty bus costs a timeout each and there are fourteen of them here;
# * a panel that cannot report brightness is omitted rather than shown as a
# control that does nothing;
# * the connector name matches what Hyprland calls the output, since the UI
# joins on it to get the monitor's description;
# * no I2C access produces the udev command that fixes it, not "no displays".
#
# Runs entirely against fixtures. Real monitors are never touched: both the
# sysfs root and the device root are redirected, and ddcutil is replaced on PATH.
set -uo pipefail
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
helper="$repo_dir/config/dot/quickshell/scripts/panama-brightness"
fail() {
printf 'brightness helper contract: %s\n' "$1" >&2
exit 1
}
fixture="$(mktemp -d /tmp/panama-brightness.XXXXXX)"
trap 'rm -rf "$fixture"' EXIT
mkdir -p "$fixture/drm" "$fixture/dev" "$fixture/bin" "$fixture/i2c"
# Two connectors with a monitor, two without. DP-2 answers DDC; DP-3 is
# connected but does not implement brightness. HDMI-A-1 and DP-1 are empty and
# must never be probed at all.
make_connector() {
local name="$1" bus="$2" status="$3"
mkdir -p "$fixture/drm/$name"
printf '%s\n' "$status" >"$fixture/drm/$name/status"
mkdir -p "$fixture/i2c/i2c-$bus"
ln -sfn "$fixture/i2c/i2c-$bus" "$fixture/drm/$name/ddc"
}
make_connector card1-DP-1 4 disconnected
make_connector card1-DP-2 5 connected
make_connector card1-DP-3 6 connected
make_connector card1-HDMI-A-1 7 disconnected
# has_accessible_bus only needs one readable/writable node to exist.
touch "$fixture/dev/i2c-5"
# Stub ddcutil. Records every bus it is asked about so the test can prove the
# disconnected ones were skipped. Bus 6 refuses, standing in for a panel without
# VCP 0x10.
#
# Bus 5 reports its brightness out of 200 rather than 100. Most panels do use
# 100, which is exactly the problem: with a maximum of 100 the scaling
# arithmetic is the identity, so a helper that ignored the reported maximum
# entirely would pass every assertion. 200 makes reads and writes that skip the
# conversion visibly wrong.
cat >"$fixture/bin/ddcutil" <<'STUB'
#!/usr/bin/env bash
bus=""
args=("$@")
for ((i = 0; i < ${#args[@]}; i++)); do
[[ "${args[$i]}" == "--bus" ]] && bus="${args[$((i + 1))]}"
done
printf '%s\n' "$bus" >>"$DDCUTIL_PROBE_LOG"
for arg in "$@"; do
if [[ "$arg" == "setvcp" ]]; then
printf 'set %s %s\n' "$bus" "${args[-1]}" >>"$DDCUTIL_SET_LOG"
exit 0
fi
done
case "$bus" in
5) printf 'VCP 10 C 120 200\n'; exit 0 ;;
*) exit 1 ;;
esac
STUB
chmod +x "$fixture/bin/ddcutil"
export DDCUTIL_PROBE_LOG="$fixture/probes.log"
export DDCUTIL_SET_LOG="$fixture/sets.log"
: >"$DDCUTIL_PROBE_LOG"
: >"$DDCUTIL_SET_LOG"
run_helper() {
PATH="$fixture/bin:$PATH" \
PANAMA_BRIGHTNESS_DRM_ROOT="$fixture/drm" \
PANAMA_BRIGHTNESS_DEV_ROOT="$fixture/dev" \
"$helper" "$@"
}
# ── Enumeration ──────────────────────────────────────────────────────────────
listing="$(run_helper list)"
jq -e . >/dev/null 2>&1 <<<"$listing" || fail "list did not emit JSON: $listing"
[[ "$(jq -r '.displays | length' <<<"$listing")" == "1" ]] \
|| fail "expected exactly one controllable display, got: $listing"
[[ "$(jq -r '.displays[0].connector' <<<"$listing")" == "DP-2" ]] \
|| fail "the connector name must match Hyprland's output name: $listing"
[[ "$(jq -r '.displays[0].bus' <<<"$listing")" == "5" ]] \
|| fail "the display was mapped to the wrong I2C bus: $listing"
# 120 of a maximum of 200 is 60%.
[[ "$(jq -r '.displays[0].value' <<<"$listing")" == "60" ]] \
|| fail "brightness was not read as a percent of the reported maximum: $listing"
[[ "$(jq -r '.error' <<<"$listing")" == "" ]] \
|| fail "a successful listing must not carry an error: $listing"
# A connected panel that cannot report brightness is dropped, not listed.
jq -e '.displays | map(.connector) | index("DP-3") == null' >/dev/null <<<"$listing" \
|| fail 'a display without VCP 0x10 was listed as controllable'
# ── Disconnected connectors are never probed ─────────────────────────────────
if grep -qxE '4|7' "$DDCUTIL_PROBE_LOG"; then
fail "a disconnected connector was probed -- each empty bus costs a timeout: $(tr '\n' ' ' <"$DDCUTIL_PROBE_LOG")"
fi
# ── Writes scale to the reported maximum ─────────────────────────────────────
run_helper set 5 40
[[ "$(tail -1 "$DDCUTIL_SET_LOG")" == "set 5 80" ]] \
|| fail "set did not scale to the display's maximum: $(cat "$DDCUTIL_SET_LOG")"
run_helper set 5 500
[[ "$(tail -1 "$DDCUTIL_SET_LOG")" == "set 5 200" ]] \
|| fail "an out-of-range percent was not clamped: $(cat "$DDCUTIL_SET_LOG")"
# ── No I2C access explains itself ────────────────────────────────────────────
rm -f "$fixture/dev"/i2c-*
denied="$(run_helper list)"
[[ "$(jq -r '.displays | length' <<<"$denied")" == "0" ]] \
|| fail "displays were reported without I2C access: $denied"
grep -q 'udevadm' <<<"$(jq -r '.error' <<<"$denied")" \
|| fail "the no-access error must name the command that fixes it, got: $(jq -r '.error' <<<"$denied")"
printf 'brightness helper contract: PASS\n'
+1 -158
View File
@@ -26,62 +26,10 @@ 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"
@@ -105,21 +53,9 @@ 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" "$@"
} }
@@ -150,102 +86,9 @@ 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