diff --git a/docs/UX-standards/launcher-standard.adoc b/docs/UX-standards/launcher-standard.adoc index 9ace12eb..5deb234d 100644 --- a/docs/UX-standards/launcher-standard.adoc +++ b/docs/UX-standards/launcher-standard.adoc @@ -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 @@ -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=" 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 diff --git a/launcher/gui-error.sh b/launcher/gui-error.sh new file mode 100755 index 00000000..42a863a5 --- /dev/null +++ b/launcher/gui-error.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# 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 diff --git a/launcher/launcher-standard.a2ml b/launcher/launcher-standard.a2ml index 6075d9ec..59f25a57 100644 --- a/launcher/launcher-standard.a2ml +++ b/launcher/launcher-standard.a2ml @@ -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" @@ -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] diff --git a/launcher/soft-attach.sh b/launcher/soft-attach.sh new file mode 100755 index 00000000..04bdc3a1 --- /dev/null +++ b/launcher/soft-attach.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# 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