diff --git a/docs/UX-standards/launcher-standard.adoc b/docs/UX-standards/launcher-standard.adoc index 28f5f910..9ace12eb 100644 --- a/docs/UX-standards/launcher-standard.adoc +++ b/docs/UX-standards/launcher-standard.adoc @@ -305,18 +305,25 @@ platform detection at the top of the script. === Required Format +Every `.desktop` file's primary `Exec=` line MUST go through `keepopen.sh` +(see §Fallback Ladder below). This ensures that a user who double-clicks +a desktop icon always lands somewhere useful — even when every upstream +hook is broken — instead of seeing a terminal flash on-and-off with no +feedback. + [source,ini] ---- [Desktop Entry] Type=Application Name=Application Name -Exec=/path/to/launcher.sh --auto -Terminal=false # Critical for web/GUI apps +Exec=/var/mnt/eclipse/repos/.desktop-tools/keepopen.sh "AppName" "/path/to/repo" "GUI_CMD" "TUI_CMD" "/tmp/app.log" +Terminal=true # keepopen needs a terminal for its loud banners and shell fallback Icon=/path/to/icon.png Categories=Category; StartupNotify=true -# Actions for additional functionality +# Actions for additional functionality — these can bypass keepopen because +# they are invoked from an already-open context menu; no fallback needed. Actions=stop;status; [Desktop Action stop] @@ -328,19 +335,112 @@ Name=Server Status Exec=/path/to/launcher.sh --status ---- -=== Forbidden Patterns +NOTE: The old "pure-GUI apps use `Terminal=false`" advice is superseded by +`keepopen.sh`. A pure-GUI `Exec=` still flashes a terminal for ~1 second +when the GUI launch succeeds, but in exchange every failure mode becomes +*visible* instead of silent. The tradeoff favours debuggability. + +== Fallback Ladder (`keepopen.sh`) + +=== Why this exists + +Desktop launchers fail in three predictable ways, and users must be able +to see which one happened: + +1. The GUI binary is missing, broken, or the URL is unreachable. +2. The TUI/CLI fallback is also broken. +3. The repo itself is missing or unbuildable. + +Without a visible fallback ladder, all three failures look identical to +the user: a terminal flashes for half a second, then the desktop is quiet. +`keepopen.sh` turns each failure into a LOUD, labelled banner and, when +all else fails, drops the user into an interactive shell at the repo +root so they can fix whatever is broken. + +=== Canonical location + +The single source of truth is: + + developer-ecosystem/standards/launcher/keepopen.sh + +For desktop files to reference a stable absolute path, a symlink is +deployed at: + + /var/mnt/eclipse/repos/.desktop-tools/keepopen.sh → ↑ + +`launch-scaffolder` copies the same script into its baked-in standards +so regenerated launchers stay in sync. + +=== Calling convention + +[source,bash] +---- +keepopen.sh APP_NAME REPO_DIR "GUI_CMD" "TUI_CMD" [LOG_FILE] +---- + +* `APP_NAME`: short label; used in banners and the `[keepopen:${APP_NAME}]` prefix. +* `REPO_DIR`: absolute path to the app's repository root — where the final shell fallback lands. +* `GUI_CMD`: shell command for the primary GUI path. Pass `""` to skip this stage. +* `TUI_CMD`: shell command for the TUI fallback. Pass `""` to skip this stage. +* `LOG_FILE` (optional): log file path, shown in banners so the user knows where to look. + +Each `*_CMD` is executed via `bash -c`, so pipelines and shell quoting work. +For launchers that daemonise their payload and exit 0 immediately, chain +a `tail -f LOG` after the launcher so the terminal stays open: + +[source,bash] +---- +"aerie-launcher.sh --auto && tail -f /tmp/aerie.log" +---- + +=== The three stages + +[cols="1,3,3"] +|=== +| Stage | On success | On failure + +| **1. GUI** +| `keepopen` exits 0 silently. +| Yellow banner titled `FALLBACK 1/2 — GUI FAILED (exit N)` listing the + GUI cmd, log file, and a human-readable hint. Proceeds to stage 2. + +| **2. TUI** +| `keepopen` exits 0 silently. +| Red banner titled `FALLBACK 2/2 — TUI ALSO FAILED (exit N)` listing + both commands and the repo path. Proceeds to stage 3. + +| **3. Shell at repo root** +| `exec bash -l` inside `REPO_DIR` — user can investigate, run `just --list`, etc. +| If `REPO_DIR` does not exist, a red warning prints and the shell starts in `$PWD`. +|=== + +The banners are intentionally loud and ugly. Visibility beats aesthetics — +the point is that a broken launcher should *look* broken, not silently fail. + +=== Forbidden patterns + +* Do **not** use `read -rp 'Press Enter to close...'` as a terminal-keepalive. + It loses the user's context. Use `keepopen.sh`'s shell fallback instead, + which drops them into the repo root where they can actually debug. +* Do **not** wrap `Exec=` in `konsole --hold` — the `--hold` terminal has + no working directory and no login-shell environment, so the user can't + easily switch to investigating. +* Do **not** skip `keepopen.sh` for "simple" apps. Simple apps break too, + and the symptom is identical to the flashy ones without the wrapper. + +=== Forbidden (legacy) patterns [source,ini] ---- -# BAD: Terminal wrapping +# BAD: konsole wrapping (pre-keepopen era) Exec=konsole -e /path/to/launcher.sh --auto Terminal=true -# BAD: Complex wrapping +# BAD: konsole --hold with no cwd / no login shell Exec=konsole --background-mode --hold --qwindowtitle Title -e wrapper.sh launcher.sh -# BAD: No error handling -Exec=launcher.sh +# BAD: launcher without keepopen — flashes a terminal on failure with no feedback +Exec=launcher.sh --auto ---- == Troubleshooting Guide @@ -418,8 +518,12 @@ APP_NAME="AppName" == Compliance Checklist -[ ] Remove all terminal wrapping from desktop files -[ ] Set `Terminal=false` for GUI/web applications +[ ] Primary `Exec=` goes through `keepopen.sh` with non-empty GUI and/or TUI commands +[ ] `GUI_CMD` for daemon-style launchers chains `&& tail -f LOG` so the terminal stays open on success +[ ] `REPO_DIR` argument points to a real, existing repository root +[ ] `LOG_FILE` argument is passed when the launcher writes one, so banners can cite it +[ ] Remove all pre-keepopen konsole wrapping from desktop files +[ ] `Terminal=true` on the primary Exec (keepopen needs a terminal for banners + shell fallback) [ ] Implement `nohup` for background processes [ ] Add PID file tracking and cleanup [ ] Implement `wait_for_server()` with reasonable timeout diff --git a/launcher/keepopen.sh b/launcher/keepopen.sh new file mode 100755 index 00000000..b3049d14 --- /dev/null +++ b/launcher/keepopen.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: PMPL-1.0-or-later +# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# keepopen.sh — standard desktop launcher fallback ladder. +# +# Canonical location: developer-ecosystem/standards/launcher/keepopen.sh +# Deployed copy: .desktop-tools/keepopen.sh (symlinked) +# Documented in: standards/docs/UX-standards/launcher-standard.adoc §Fallback Ladder +# +# Its job is to turn a possibly-broken launcher into something that ALWAYS +# lands the user somewhere useful — even when every upstream hook fails. +# +# Usage: +# keepopen.sh APP_NAME REPO_DIR "GUI_CMD" "TUI_CMD" [LOG_FILE] +# +# Fallback ladder (each fallback shows a LOUD banner so the failure is +# visible — the point is that the user CAN see a tool is broken): +# +# 1. GUI_CMD — primary path. Silent on success. If it fails ↓ +# 2. TUI_CMD — loud yellow banner, then fallback. If it fails ↓ +# 3. bash -l — loud red banner, then cd into REPO_DIR and drop into +# an interactive login shell. Never just "press enter +# to close" — the user lands in the repo so they can +# actually fix the thing that's broken. +# +# Each CMD is evaluated as `bash -c "$cmd"`, so pipelines and shell quoting +# work normally. Pass an empty string to skip a stage (e.g. an app with no +# GUI can use `""` for GUI_CMD and go straight to the TUI banner → TUI). +# +# Banners are intentionally loud and ugly — visibility beats aesthetics. + +set -u + +APP_NAME="${1:?keepopen: APP_NAME required (arg 1)}" +REPO_DIR="${2:?keepopen: REPO_DIR required (arg 2)}" +GUI_CMD="${3:?keepopen: GUI_CMD required (arg 3) — pass '' if not applicable}" +TUI_CMD="${4:?keepopen: TUI_CMD required (arg 4) — pass '' if not applicable}" +LOG_FILE="${5:-}" + +C_RED=$'\033[1;31m' +C_YEL=$'\033[1;33m' +C_CYA=$'\033[1;36m' +C_GRN=$'\033[1;32m' +C_BOLD=$'\033[1m' +C_RST=$'\033[0m' + +banner() { + # $1 = colour; $2 = title; remaining args = body lines. + local colour="$1"; shift + local title="$1"; shift + echo + echo "${colour}${C_BOLD}================================================================${C_RST}" + echo "${colour}${C_BOLD} ${title}${C_RST}" + echo "${colour}${C_BOLD}================================================================${C_RST}" + local line + for line in "$@"; do + [[ -z "${line}" ]] && { echo; continue; } + echo " ${colour}${line}${C_RST}" + done + echo +} + +# ----------------------------------------------------------------------------- +# STAGE 1 — GUI +# ----------------------------------------------------------------------------- + +gui_exit=0 +if [[ -n "${GUI_CMD}" ]]; then + echo "${C_CYA}[keepopen:${APP_NAME}] GUI → ${GUI_CMD}${C_RST}" + bash -c "${GUI_CMD}" + gui_exit=$? + if [[ ${gui_exit} -eq 0 ]]; then + exit 0 + fi + banner "${C_YEL}" "FALLBACK 1/2 — GUI FAILED (exit ${gui_exit})" \ + "APP : ${APP_NAME}" \ + "GUI cmd : ${GUI_CMD}" \ + "${LOG_FILE:+LOG FILE: ${LOG_FILE}}" \ + "" \ + "The primary GUI path exited non-zero." \ + "Something needs fixing. Falling back to the TUI path." \ + "(If this keeps happening, edit the .desktop file or the" \ + "keepopen invocation to point at a working GUI command.)" +else + banner "${C_YEL}" "STAGE 1/2 SKIPPED — NO GUI CONFIGURED" \ + "APP : ${APP_NAME}" \ + "" \ + "This app was launched with no GUI command. Going straight to TUI." +fi + +# ----------------------------------------------------------------------------- +# STAGE 2 — TUI +# ----------------------------------------------------------------------------- + +tui_exit=0 +if [[ -n "${TUI_CMD}" ]]; then + echo "${C_CYA}[keepopen:${APP_NAME}] TUI → ${TUI_CMD}${C_RST}" + bash -c "${TUI_CMD}" + tui_exit=$? + if [[ ${tui_exit} -eq 0 ]]; then + exit 0 + fi + banner "${C_RED}" "FALLBACK 2/2 — TUI ALSO FAILED (exit ${tui_exit})" \ + "APP : ${APP_NAME}" \ + "GUI cmd : ${GUI_CMD:-}" \ + "TUI cmd : ${TUI_CMD}" \ + "${LOG_FILE:+LOG FILE: ${LOG_FILE}}" \ + "REPO : ${REPO_DIR}" \ + "" \ + "BOTH the GUI and the TUI paths failed." \ + "Something needs fixing — you are being dropped into a shell" \ + "at the repo root so you can investigate, not just closed out." +else + banner "${C_RED}" "STAGE 2/2 SKIPPED — NO TUI CONFIGURED" \ + "APP : ${APP_NAME}" \ + "REPO: ${REPO_DIR}" \ + "" \ + "No TUI command was provided either. Dropping into a shell at the repo root." +fi + +# ----------------------------------------------------------------------------- +# STAGE 3 — interactive shell at repo root (final fallback) +# ----------------------------------------------------------------------------- + +if [[ -d "${REPO_DIR}" ]]; then + cd "${REPO_DIR}" || true + echo "${C_GRN}[keepopen:${APP_NAME}] Dropping into bash at ${REPO_DIR}${C_RST}" +else + echo "${C_RED}[keepopen:${APP_NAME}] REPO_DIR does not exist: ${REPO_DIR}${C_RST}" >&2 + echo "${C_RED}[keepopen:${APP_NAME}] Staying in ${PWD} instead.${C_RST}" >&2 +fi + +cat <