Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 61 additions & 2 deletions docs/UX-standards/launcher-standard.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,13 @@ open_browser() {

=== Error Handling

Stderr-only `err()` is the right default for TUI invocation, but when a
launcher is started from a desktop entry there is no terminal for the
user to see — every failure looks identical (a brief flash and nothing
else). The reference helper `launcher/gui-error.sh` surfaces errors via
the platform's dialog ladder AND stderr, so the failure is visible no
matter how the launcher was started.

[source,bash]
----
# Always provide actionable feedback
Expand All @@ -124,13 +131,65 @@ err() {
echo "[$APP_NAME] Try: check $LOG_FILE for details" >&2
}

# Example usage
# Source the shared GUI-error helper (reference impl of [error-visibility]
# from launcher-standard.a2ml). Falls back to err() if not available so
# every launcher works even without the shared helper on PATH.
if [ -r "$(hp_resolve_desktop_tools gui-error.sh 2>/dev/null)" ]; then
# shellcheck disable=SC1090
. "$(hp_resolve_desktop_tools gui-error.sh)"
else
hp_gui_error() { err "$2"; } # graceful degradation
fi

# Example usage — fails LOUDLY whether GUI or TUI
if ! start_server; then
err "Failed to start server"
hp_gui_error "$APP_NAME failed to start" \
"Check log: $LOG_FILE\n\nOverride wait timeout: WAIT_FOR_URL_TIMEOUT_SECONDS=<N>"
exit 1
fi
----

NOTE: `hp_gui_error` writes to stderr unconditionally per
`[error-visibility].always-also-to-stderr = true`. Set `NO_GUI_ERROR=1`
to suppress the dialog attempt (useful in CI).

=== Soft-Attach (optional ecosystem integrations)

A "soft-attach" tool is one the launcher calls IF it is installed, and
silently skips otherwise. The estate ships three by default
(`feedback-o-tron`, `hypatia`, `panic-attack`) — see
`[soft-attach].tools` in `launcher-standard.a2ml` for the live list.

Downstream launchers SHOULD source `launcher/soft-attach.sh` rather
than re-implementing the if-installed-then-invoke pattern, so behaviour
stays consistent across the estate.

[source,bash]
----
# Source the shared soft-attach helper. Graceful degradation: if the
# helper is not on the resolution ladder, every soft-attach call
# becomes a silent no-op.
if [ -r "$(hp_resolve_desktop_tools soft-attach.sh 2>/dev/null)" ]; then
# shellcheck disable=SC1090
. "$(hp_resolve_desktop_tools soft-attach.sh)"
else
hp_soft_attach_event() { :; }
hp_soft_attach_run() { :; }
fi

# Call sites: typically wired into start_server() on failure path
on_start_failed() {
hp_soft_attach_event "feedback-o-tron" "launcher:start_failed" \
--app "$APP_NAME" --log "$LOG_FILE"
hp_soft_attach_run "hypatia diagnose --app $APP_NAME --log $LOG_FILE"
hp_soft_attach_run "panic-attack assail $REPO_DIR"
}
----

Note that template substitution (`{app-name}`, `{log-file}`,
`{repo-dir}` in the a2ml) is the launcher's responsibility — interpolate
before passing the command line to `hp_soft_attach_run`.

== Standard Modes

=== Required Modes
Expand Down
69 changes: 69 additions & 0 deletions launcher/gui-error.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: PMPL-1.0-or-later
# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
#
# gui-error.sh — reference implementation of [error-visibility] from
# launcher/launcher-standard.a2ml.
#
# When the launcher runs in a GUI context (no TTY + DISPLAY or
# WAYLAND_DISPLAY set), errors written only to stderr disappear: the
# user sees a brief terminal flash and nothing else. This script
# surfaces such errors via a graphical dialog AND stderr.
#
# Downstream launchers SHOULD source this script and call
# hp_gui_error "Title" "message"
# rather than re-implementing the dialog ladder.
#
# Dialog ladder (matches [error-visibility].gui-dialog-chain):
# 1. kdialog — KDE Plasma
# 2. zenity — GNOME / Cinnamon / Xfce
# 3. notify-send — libnotify (less prominent than a dialog, but standard)
# 4. xmessage — X11 last resort
#
# Returns 0 if any dialog succeeded, non-zero if all failed. stderr is
# always written regardless (per [error-visibility].always-also-to-stderr).
#
# Env overrides:
# NO_GUI_ERROR=1 — suppress the dialog attempt; stderr only.
# Useful in CI / scripted invocation.

hp_gui_error() {
local title="${1:-Error}"
local message="${2:-}"

# Always write to stderr regardless of dialog outcome.
printf '[%s] %s\n' "${title}" "${message}" >&2

# Skip dialogs when we have a TTY (the user will see stderr fine)
# or when explicitly suppressed.
if [[ -t 2 ]] || [[ -n "${NO_GUI_ERROR:-}" ]]; then
return 0
fi

# GUI requires a display.
if [[ -z "${DISPLAY:-}" ]] && [[ -z "${WAYLAND_DISPLAY:-}" ]]; then
return 1
fi

# Try the ladder; first present + successful wins.
if command -v kdialog >/dev/null 2>&1; then
kdialog --title "${title}" --error "${message}" >/dev/null 2>&1 && return 0
fi
if command -v zenity >/dev/null 2>&1; then
zenity --title="${title}" --error --text="${message}" >/dev/null 2>&1 && return 0
fi
if command -v notify-send >/dev/null 2>&1; then
notify-send -u critical "${title}" "${message}" >/dev/null 2>&1 && return 0
fi
if command -v xmessage >/dev/null 2>&1; then
xmessage -title "${title}" -center "${message}" >/dev/null 2>&1 && return 0
fi

return 1
}

# CLI mode (not sourced): forward args to hp_gui_error.
# ./gui-error.sh "Launcher failed" "Server died — see ~/.local/state/app/server.log"
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
hp_gui_error "$@"
fi
33 changes: 28 additions & 5 deletions launcher/launcher-standard.a2ml
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,18 @@ startup-command-search = [
[error-visibility]
# When the launcher runs in a GUI context (no tty + DISPLAY/WAYLAND_DISPLAY set),
# errors must be visible to the user via a GUI dialog, not only to stderr.
gui-dialog-chain = ["kdialog", "zenity", "notify-send", "xmessage"]
#
# Reference implementation: launcher/gui-error.sh — exposes
# `hp_gui_error "title" "message"`. Downstream launchers SHOULD source
# this rather than re-implement the dialog ladder, so the spec stays
# consistent across the estate.
#
# Env override: NO_GUI_ERROR=1 suppresses the dialog attempt (stderr
# still gets the message). Useful in CI / scripted invocation.
reference-impl = "launcher/gui-error.sh"
gui-dialog-chain = ["kdialog", "zenity", "notify-send", "xmessage"]
always-also-to-stderr = true
suppress-env-var = "NO_GUI_ERROR"

[integration.linux]
apps-dir = "$HOME/.local/share/applications"
Expand Down Expand Up @@ -126,11 +136,24 @@ preserve = [
]

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

[a2ml-metadata-block]
Expand Down
80 changes: 80 additions & 0 deletions launcher/soft-attach.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: PMPL-1.0-or-later
# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
#
# soft-attach.sh — reference implementation of [soft-attach] from
# launcher/launcher-standard.a2ml.
#
# Soft-attach = optional ecosystem integrations that the launcher invokes
# IF they are installed, and silently skips otherwise. Downstream
# launchers SHOULD source this script rather than re-implementing the
# "if-installed-then-invoke" pattern, so the spec stays consistent
# across the estate.
#
# All primitives are non-fatal — a missing or failing soft-attach tool
# never breaks the launcher (per the §soft-attach spec: "called if
# present, silently skipped if absent").
#
# Primitives:
#
# hp_soft_attach_present "command"
# Returns 0 if the command is on PATH, 1 otherwise. Building
# block; rarely called directly.
#
# hp_soft_attach_run "command-line"
# If the first token of command-line is on PATH, runs the whole
# line via `bash -c`. Otherwise silent no-op. Suitable for
# [soft-attach].tools entries that use `command = "..."`.
# Template substitution ({app-name}, {log-file}, {repo-dir}) is
# the CALLER's responsibility — substitute before passing in.
#
# hp_soft_attach_event "tool" "event-name" [extra args...]
# If `tool` is on PATH, invokes `tool emit event-name [args]`.
# The `emit` verb is the soft-attach convention for event-style
# integrations (e.g. feedback-o-tron). Tools that use a different
# verb should be called via hp_soft_attach_run with the full
# command line.
#
# Recommended call sites:
# - on launcher start failure: emit start_failed event to feedback-o-tron;
# run hypatia diagnose; run panic-attack assail.
# - on --integ failure: same pattern.
# See the comprehensive-launcher-template.sh for the full hook layout.

hp_soft_attach_present() {
command -v "${1:?soft-attach: command required}" >/dev/null 2>&1
}

hp_soft_attach_run() {
local cmd_line="${1:?soft-attach: command line required}"
local first_token
first_token=$(printf '%s' "${cmd_line}" | awk '{print $1}')
if hp_soft_attach_present "${first_token}"; then
bash -c "${cmd_line}" || true
fi
}

hp_soft_attach_event() {
local tool="${1:?soft-attach: tool required}"
local event="${2:?soft-attach: event-name required}"
shift 2
if hp_soft_attach_present "${tool}"; then
"${tool}" emit "${event}" "$@" || true
fi
}

# CLI mode (not sourced): provide a thin wrapper for ad-hoc invocation.
# ./soft-attach.sh run "hypatia diagnose --app foo --log /tmp/foo.log"
# ./soft-attach.sh event feedback-o-tron launcher:start_failed
# ./soft-attach.sh present hypatia
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
case "${1:-}" in
run) shift; hp_soft_attach_run "$@" ;;
event) shift; hp_soft_attach_event "$@" ;;
present) shift; hp_soft_attach_present "$@" ;;
*)
printf 'usage: %s {run|event|present} ...\n' "${0##*/}" >&2
exit 64 # EX_USAGE
;;
esac
fi
Loading