Skip to content

Commit 4bc34e6

Browse files
feat(launcher-standard): reference impls for soft-attach + gui-dialog-chain (#179)
## Summary The a2ml declared `[soft-attach]` and `[error-visibility]` contracts but provided no reference implementations. Every downstream launcher had to re-implement the "if-installed-then-invoke" pattern and the GUI dialog ladder — guaranteed drift, and a common reason downstream launchers either skip these features (silent failures stay silent) or implement them inconsistently. This adds two sourceable bash helpers in `launcher/`, mirroring the contract semantics from the a2ml, plus prose with graceful-degradation usage patterns. ## `launcher/gui-error.sh` `hp_gui_error "title" "message"`: | Condition | Behaviour | |-----------|-----------| | Always | Write `[title] message` to stderr | | stderr is TTY or `NO_GUI_ERROR=1` | stderr only, return 0 | | no `\$DISPLAY` and no `\$WAYLAND_DISPLAY` | return 1 (cannot show GUI) | | Else | Try `kdialog → zenity → notify-send → xmessage`; first success wins | Mirrors `[error-visibility].gui-dialog-chain` exactly. Verified locally: `NO_GUI_ERROR=1 ./launcher/gui-error.sh "T" "M"` → stderr "[T] M", exit 0. ## `launcher/soft-attach.sh` Three primitives mirroring the three shapes in `[soft-attach].tools`: \`\`\`bash hp_soft_attach_present "command" # 0 if on PATH hp_soft_attach_run "command line" # run if first token present, silent no-op + 0 if missing hp_soft_attach_event "tool" "event-name" [args] # `tool emit event-name args` if present, silent no-op if missing \`\`\` All non-fatal — missing tools never break the launcher (per the §soft-attach spec: *"called if present, silently skipped if absent"*). CLI mode for ad-hoc use: \`\`\` ./soft-attach.sh run "hypatia diagnose --app foo" ./soft-attach.sh event feedback-o-tron launcher:start_failed ./soft-attach.sh present hypatia \`\`\` ## a2ml contract additions `[error-visibility]`: - `reference-impl = "launcher/gui-error.sh"` (pointer) - `suppress-env-var = "NO_GUI_ERROR"` (formalises the override name) `[soft-attach]`: - `reference-impl = "launcher/soft-attach.sh"` (pointer) - Each `tools` entry now carries explicit `style` (`"event" | "command"`) and `trigger` (e.g. `"on-start-failed"`) so launchers know **WHEN** to invoke each tool, not just **HOW**. Previously only `feedback-o-tron` had an explicit failure trigger. ## Prose additions (`launcher-standard.adoc`) - §Error Handling: rewritten with `hp_gui_error` integration, graceful degradation pattern, and a NOTE on stderr-always behaviour - §Soft-Attach (new subsection): documents the three primitives, the graceful-degradation source pattern, and an example `on_start_failed` hook wiring all three default tools ## Test plan - [x] a2ml parses (python tomllib); new fields round-trip cleanly - [x] Both helpers pass `bash -n` - [x] `hp_gui_error` writes stderr + respects `NO_GUI_ERROR` - [x] `hp_soft_attach_present` returns 0/1 correctly - [x] `hp_soft_attach_run` runs installed, silently skips missing - [x] `hp_soft_attach_event` silently skips missing tool - [ ] Manual dialog test (deferred — requires KDE/GNOME desktop; logic matches well-documented invocation conventions per each dialog's man page) - [ ] Lock-step gate (#172) goes green on first push (both files in diff) ## Coordination Independent of #170, #171, #172, #173, #175, #176, #177 — no file overlap. Builds on the resolution ladder shipped in #171 (`hp_resolve_desktop_tools` is referenced in the prose examples) so the new helpers are findable wherever `.desktop-tools/` resolves. Both work standalone or via the ladder. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ca28251 commit 4bc34e6

4 files changed

Lines changed: 238 additions & 7 deletions

File tree

docs/UX-standards/launcher-standard.adoc

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,13 @@ open_browser() {
121121

122122
=== Error Handling
123123

124+
Stderr-only `err()` is the right default for TUI invocation, but when a
125+
launcher is started from a desktop entry there is no terminal for the
126+
user to see — every failure looks identical (a brief flash and nothing
127+
else). The reference helper `launcher/gui-error.sh` surfaces errors via
128+
the platform's dialog ladder AND stderr, so the failure is visible no
129+
matter how the launcher was started.
130+
124131
[source,bash]
125132
----
126133
# Always provide actionable feedback
@@ -129,13 +136,65 @@ err() {
129136
echo "[$APP_NAME] Try: check $LOG_FILE for details" >&2
130137
}
131138
132-
# Example usage
139+
# Source the shared GUI-error helper (reference impl of [error-visibility]
140+
# from launcher-standard.a2ml). Falls back to err() if not available so
141+
# every launcher works even without the shared helper on PATH.
142+
if [ -r "$(hp_resolve_desktop_tools gui-error.sh 2>/dev/null)" ]; then
143+
# shellcheck disable=SC1090
144+
. "$(hp_resolve_desktop_tools gui-error.sh)"
145+
else
146+
hp_gui_error() { err "$2"; } # graceful degradation
147+
fi
148+
149+
# Example usage — fails LOUDLY whether GUI or TUI
133150
if ! start_server; then
134-
err "Failed to start server"
151+
hp_gui_error "$APP_NAME failed to start" \
152+
"Check log: $LOG_FILE\n\nOverride wait timeout: WAIT_FOR_URL_TIMEOUT_SECONDS=<N>"
135153
exit 1
136154
fi
137155
----
138156

157+
NOTE: `hp_gui_error` writes to stderr unconditionally per
158+
`[error-visibility].always-also-to-stderr = true`. Set `NO_GUI_ERROR=1`
159+
to suppress the dialog attempt (useful in CI).
160+
161+
=== Soft-Attach (optional ecosystem integrations)
162+
163+
A "soft-attach" tool is one the launcher calls IF it is installed, and
164+
silently skips otherwise. The estate ships three by default
165+
(`feedback-o-tron`, `hypatia`, `panic-attack`) — see
166+
`[soft-attach].tools` in `launcher-standard.a2ml` for the live list.
167+
168+
Downstream launchers SHOULD source `launcher/soft-attach.sh` rather
169+
than re-implementing the if-installed-then-invoke pattern, so behaviour
170+
stays consistent across the estate.
171+
172+
[source,bash]
173+
----
174+
# Source the shared soft-attach helper. Graceful degradation: if the
175+
# helper is not on the resolution ladder, every soft-attach call
176+
# becomes a silent no-op.
177+
if [ -r "$(hp_resolve_desktop_tools soft-attach.sh 2>/dev/null)" ]; then
178+
# shellcheck disable=SC1090
179+
. "$(hp_resolve_desktop_tools soft-attach.sh)"
180+
else
181+
hp_soft_attach_event() { :; }
182+
hp_soft_attach_run() { :; }
183+
fi
184+
185+
# Call sites: typically wired into start_server() on failure path
186+
on_start_failed() {
187+
hp_soft_attach_event "feedback-o-tron" "launcher:start_failed" \
188+
--app "$APP_NAME" --log "$LOG_FILE"
189+
hp_soft_attach_run "hypatia diagnose --app $APP_NAME --log $LOG_FILE"
190+
hp_soft_attach_run "panic-attack assail $REPO_DIR"
191+
}
192+
----
193+
194+
Note that template substitution (`{app-name}`, `{log-file}`,
195+
`{repo-dir}` in the a2ml) is the launcher's responsibility — interpolate
196+
before passing the command line to `hp_soft_attach_run`.
197+
139198
== Standard Modes
140199

141200
=== Required Modes

launcher/gui-error.sh

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#!/usr/bin/env bash
2+
# SPDX-License-Identifier: PMPL-1.0-or-later
3+
# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
4+
#
5+
# gui-error.sh — reference implementation of [error-visibility] from
6+
# launcher/launcher-standard.a2ml.
7+
#
8+
# When the launcher runs in a GUI context (no TTY + DISPLAY or
9+
# WAYLAND_DISPLAY set), errors written only to stderr disappear: the
10+
# user sees a brief terminal flash and nothing else. This script
11+
# surfaces such errors via a graphical dialog AND stderr.
12+
#
13+
# Downstream launchers SHOULD source this script and call
14+
# hp_gui_error "Title" "message"
15+
# rather than re-implementing the dialog ladder.
16+
#
17+
# Dialog ladder (matches [error-visibility].gui-dialog-chain):
18+
# 1. kdialog — KDE Plasma
19+
# 2. zenity — GNOME / Cinnamon / Xfce
20+
# 3. notify-send — libnotify (less prominent than a dialog, but standard)
21+
# 4. xmessage — X11 last resort
22+
#
23+
# Returns 0 if any dialog succeeded, non-zero if all failed. stderr is
24+
# always written regardless (per [error-visibility].always-also-to-stderr).
25+
#
26+
# Env overrides:
27+
# NO_GUI_ERROR=1 — suppress the dialog attempt; stderr only.
28+
# Useful in CI / scripted invocation.
29+
30+
hp_gui_error() {
31+
local title="${1:-Error}"
32+
local message="${2:-}"
33+
34+
# Always write to stderr regardless of dialog outcome.
35+
printf '[%s] %s\n' "${title}" "${message}" >&2
36+
37+
# Skip dialogs when we have a TTY (the user will see stderr fine)
38+
# or when explicitly suppressed.
39+
if [[ -t 2 ]] || [[ -n "${NO_GUI_ERROR:-}" ]]; then
40+
return 0
41+
fi
42+
43+
# GUI requires a display.
44+
if [[ -z "${DISPLAY:-}" ]] && [[ -z "${WAYLAND_DISPLAY:-}" ]]; then
45+
return 1
46+
fi
47+
48+
# Try the ladder; first present + successful wins.
49+
if command -v kdialog >/dev/null 2>&1; then
50+
kdialog --title "${title}" --error "${message}" >/dev/null 2>&1 && return 0
51+
fi
52+
if command -v zenity >/dev/null 2>&1; then
53+
zenity --title="${title}" --error --text="${message}" >/dev/null 2>&1 && return 0
54+
fi
55+
if command -v notify-send >/dev/null 2>&1; then
56+
notify-send -u critical "${title}" "${message}" >/dev/null 2>&1 && return 0
57+
fi
58+
if command -v xmessage >/dev/null 2>&1; then
59+
xmessage -title "${title}" -center "${message}" >/dev/null 2>&1 && return 0
60+
fi
61+
62+
return 1
63+
}
64+
65+
# CLI mode (not sourced): forward args to hp_gui_error.
66+
# ./gui-error.sh "Launcher failed" "Server died — see ~/.local/state/app/server.log"
67+
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
68+
hp_gui_error "$@"
69+
fi

launcher/launcher-standard.a2ml

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,18 @@ startup-command-search = [
9393
[error-visibility]
9494
# When the launcher runs in a GUI context (no tty + DISPLAY/WAYLAND_DISPLAY set),
9595
# errors must be visible to the user via a GUI dialog, not only to stderr.
96-
gui-dialog-chain = ["kdialog", "zenity", "notify-send", "xmessage"]
96+
#
97+
# Reference implementation: launcher/gui-error.sh — exposes
98+
# `hp_gui_error "title" "message"`. Downstream launchers SHOULD source
99+
# this rather than re-implement the dialog ladder, so the spec stays
100+
# consistent across the estate.
101+
#
102+
# Env override: NO_GUI_ERROR=1 suppresses the dialog attempt (stderr
103+
# still gets the message). Useful in CI / scripted invocation.
104+
reference-impl = "launcher/gui-error.sh"
105+
gui-dialog-chain = ["kdialog", "zenity", "notify-send", "xmessage"]
97106
always-also-to-stderr = true
107+
suppress-env-var = "NO_GUI_ERROR"
98108

99109
[integration.linux]
100110
apps-dir = "$HOME/.local/share/applications"
@@ -149,11 +159,24 @@ preserve = [
149159
]
150160

151161
[soft-attach]
152-
# Optional integrations — called if present, silently skipped if absent.
162+
# Optional ecosystem integrations — called if present, silently skipped
163+
# if absent. Each tool entry carries an explicit `trigger` naming the
164+
# hook point at which the launcher should invoke it.
165+
#
166+
# Reference implementation: launcher/soft-attach.sh — exposes three
167+
# primitives:
168+
# hp_soft_attach_present "command" → 0 if on PATH
169+
# hp_soft_attach_run "command line" → run if first token present
170+
# hp_soft_attach_event "tool" "event" [...] → invoke `tool emit event ...`
171+
# Downstream launchers SHOULD source this rather than re-implement.
172+
#
173+
# Trigger values: "on-start-failed", "on-start-succeeded", "on-integ-failed",
174+
# "on-user-request". Additional triggers may be added as the lifecycle grows.
175+
reference-impl = "launcher/soft-attach.sh"
153176
tools = [
154-
{ name = "feedback-o-tron", event-on-failure = "launcher:start_failed" },
155-
{ name = "hypatia", command = "hypatia diagnose --app {app-name} --log {log-file}" },
156-
{ name = "panic-attack", command = "panic-attack assail {repo-dir}" },
177+
{ name = "feedback-o-tron", style = "event", trigger = "on-start-failed", event = "launcher:start_failed" },
178+
{ name = "hypatia", style = "command", trigger = "on-start-failed", command = "hypatia diagnose --app {app-name} --log {log-file}" },
179+
{ name = "panic-attack", style = "command", trigger = "on-start-failed", command = "panic-attack assail {repo-dir}" },
157180
]
158181

159182
[a2ml-metadata-block]

launcher/soft-attach.sh

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
#!/usr/bin/env bash
2+
# SPDX-License-Identifier: PMPL-1.0-or-later
3+
# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
4+
#
5+
# soft-attach.sh — reference implementation of [soft-attach] from
6+
# launcher/launcher-standard.a2ml.
7+
#
8+
# Soft-attach = optional ecosystem integrations that the launcher invokes
9+
# IF they are installed, and silently skips otherwise. Downstream
10+
# launchers SHOULD source this script rather than re-implementing the
11+
# "if-installed-then-invoke" pattern, so the spec stays consistent
12+
# across the estate.
13+
#
14+
# All primitives are non-fatal — a missing or failing soft-attach tool
15+
# never breaks the launcher (per the §soft-attach spec: "called if
16+
# present, silently skipped if absent").
17+
#
18+
# Primitives:
19+
#
20+
# hp_soft_attach_present "command"
21+
# Returns 0 if the command is on PATH, 1 otherwise. Building
22+
# block; rarely called directly.
23+
#
24+
# hp_soft_attach_run "command-line"
25+
# If the first token of command-line is on PATH, runs the whole
26+
# line via `bash -c`. Otherwise silent no-op. Suitable for
27+
# [soft-attach].tools entries that use `command = "..."`.
28+
# Template substitution ({app-name}, {log-file}, {repo-dir}) is
29+
# the CALLER's responsibility — substitute before passing in.
30+
#
31+
# hp_soft_attach_event "tool" "event-name" [extra args...]
32+
# If `tool` is on PATH, invokes `tool emit event-name [args]`.
33+
# The `emit` verb is the soft-attach convention for event-style
34+
# integrations (e.g. feedback-o-tron). Tools that use a different
35+
# verb should be called via hp_soft_attach_run with the full
36+
# command line.
37+
#
38+
# Recommended call sites:
39+
# - on launcher start failure: emit start_failed event to feedback-o-tron;
40+
# run hypatia diagnose; run panic-attack assail.
41+
# - on --integ failure: same pattern.
42+
# See the comprehensive-launcher-template.sh for the full hook layout.
43+
44+
hp_soft_attach_present() {
45+
command -v "${1:?soft-attach: command required}" >/dev/null 2>&1
46+
}
47+
48+
hp_soft_attach_run() {
49+
local cmd_line="${1:?soft-attach: command line required}"
50+
local first_token
51+
first_token=$(printf '%s' "${cmd_line}" | awk '{print $1}')
52+
if hp_soft_attach_present "${first_token}"; then
53+
bash -c "${cmd_line}" || true
54+
fi
55+
}
56+
57+
hp_soft_attach_event() {
58+
local tool="${1:?soft-attach: tool required}"
59+
local event="${2:?soft-attach: event-name required}"
60+
shift 2
61+
if hp_soft_attach_present "${tool}"; then
62+
"${tool}" emit "${event}" "$@" || true
63+
fi
64+
}
65+
66+
# CLI mode (not sourced): provide a thin wrapper for ad-hoc invocation.
67+
# ./soft-attach.sh run "hypatia diagnose --app foo --log /tmp/foo.log"
68+
# ./soft-attach.sh event feedback-o-tron launcher:start_failed
69+
# ./soft-attach.sh present hypatia
70+
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
71+
case "${1:-}" in
72+
run) shift; hp_soft_attach_run "$@" ;;
73+
event) shift; hp_soft_attach_event "$@" ;;
74+
present) shift; hp_soft_attach_present "$@" ;;
75+
*)
76+
printf 'usage: %s {run|event|present} ...\n' "${0##*/}" >&2
77+
exit 64 # EX_USAGE
78+
;;
79+
esac
80+
fi

0 commit comments

Comments
 (0)