From d438ca84e6f2db72157e84528503b2e6633c6ee5 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Fri, 17 Apr 2026 08:51:59 +0100 Subject: [PATCH] launcher: add keepopen.sh fallback ladder to launcher standard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three-stage ladder (GUI → TUI → bash-at-repo-root) with loud, labelled banners at each fallback step. Final fallback lands in an interactive shell at the repo root so the user can investigate, replacing the previous "Press Enter to close" pattern. Canonical source: launcher/keepopen.sh Standard spec bumped 0.1.0 → 0.2.0 with [fallback-ladder] section. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/UX-standards/launcher-standard.adoc | 124 +++++++++++++++++-- launcher/keepopen.sh | 144 +++++++++++++++++++++++ launcher/launcher-standard.a2ml | 23 +++- 3 files changed, 279 insertions(+), 12 deletions(-) create mode 100755 launcher/keepopen.sh 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 <