Skip to content

Commit dbfc169

Browse files
launcher: add keepopen.sh fallback ladder to launcher standard (#30)
## Summary - Add `launcher/keepopen.sh` — universal wrapper with a three-stage fallback ladder (GUI → TUI → bash at repo root) - Document the ladder in `docs/UX-standards/launcher-standard.adoc` (new §Fallback Ladder, updated Compliance Checklist, forbidden patterns) - Bump machine spec `launcher/launcher-standard.a2ml` 0.1.0 → 0.2.0, add `[fallback-ladder]` block ## Motivation Desktop launchers were flashing terminal windows on-and-off with no feedback when they failed. The user literally could not see *which* failure happened — missing binary, bad config, unreachable URL, or broken repo. keepopen turns each failure into a LOUD, labelled banner and drops to an interactive shell at the repo root as the final fallback so the user can investigate. ## Test plan - [x] keepopen.sh smoke-tested with deliberately-broken GUI/TUI commands (exit 42, 7, 9 correctly captured) - [x] Missing REPO_DIR handled with red warning + stays in \$PWD - [x] `desktop-file-validate` passes on all 11 rewired user-facing .desktop files - [ ] (follow-up) `launch-scaffolder realign` run to regenerate dependent launchers — tracked in separate PRs per app repo 🤖 Generated with [Claude Code](https://claude.com/claude-code)
2 parents 5a320dd + 79d8df9 commit dbfc169

3 files changed

Lines changed: 279 additions & 12 deletions

File tree

docs/UX-standards/launcher-standard.adoc

Lines changed: 114 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -305,18 +305,25 @@ platform detection at the top of the script.
305305

306306
=== Required Format
307307

308+
Every `.desktop` file's primary `Exec=` line MUST go through `keepopen.sh`
309+
(see §Fallback Ladder below). This ensures that a user who double-clicks
310+
a desktop icon always lands somewhere useful — even when every upstream
311+
hook is broken — instead of seeing a terminal flash on-and-off with no
312+
feedback.
313+
308314
[source,ini]
309315
----
310316
[Desktop Entry]
311317
Type=Application
312318
Name=Application Name
313-
Exec=/path/to/launcher.sh --auto
314-
Terminal=false # Critical for web/GUI apps
319+
Exec=/var/mnt/eclipse/repos/.desktop-tools/keepopen.sh "AppName" "/path/to/repo" "GUI_CMD" "TUI_CMD" "/tmp/app.log"
320+
Terminal=true # keepopen needs a terminal for its loud banners and shell fallback
315321
Icon=/path/to/icon.png
316322
Categories=Category;
317323
StartupNotify=true
318324
319-
# Actions for additional functionality
325+
# Actions for additional functionality — these can bypass keepopen because
326+
# they are invoked from an already-open context menu; no fallback needed.
320327
Actions=stop;status;
321328
322329
[Desktop Action stop]
@@ -328,19 +335,112 @@ Name=Server Status
328335
Exec=/path/to/launcher.sh --status
329336
----
330337

331-
=== Forbidden Patterns
338+
NOTE: The old "pure-GUI apps use `Terminal=false`" advice is superseded by
339+
`keepopen.sh`. A pure-GUI `Exec=` still flashes a terminal for ~1 second
340+
when the GUI launch succeeds, but in exchange every failure mode becomes
341+
*visible* instead of silent. The tradeoff favours debuggability.
342+
343+
== Fallback Ladder (`keepopen.sh`)
344+
345+
=== Why this exists
346+
347+
Desktop launchers fail in three predictable ways, and users must be able
348+
to see which one happened:
349+
350+
1. The GUI binary is missing, broken, or the URL is unreachable.
351+
2. The TUI/CLI fallback is also broken.
352+
3. The repo itself is missing or unbuildable.
353+
354+
Without a visible fallback ladder, all three failures look identical to
355+
the user: a terminal flashes for half a second, then the desktop is quiet.
356+
`keepopen.sh` turns each failure into a LOUD, labelled banner and, when
357+
all else fails, drops the user into an interactive shell at the repo
358+
root so they can fix whatever is broken.
359+
360+
=== Canonical location
361+
362+
The single source of truth is:
363+
364+
developer-ecosystem/standards/launcher/keepopen.sh
365+
366+
For desktop files to reference a stable absolute path, a symlink is
367+
deployed at:
368+
369+
/var/mnt/eclipse/repos/.desktop-tools/keepopen.sh → ↑
370+
371+
`launch-scaffolder` copies the same script into its baked-in standards
372+
so regenerated launchers stay in sync.
373+
374+
=== Calling convention
375+
376+
[source,bash]
377+
----
378+
keepopen.sh APP_NAME REPO_DIR "GUI_CMD" "TUI_CMD" [LOG_FILE]
379+
----
380+
381+
* `APP_NAME`: short label; used in banners and the `[keepopen:${APP_NAME}]` prefix.
382+
* `REPO_DIR`: absolute path to the app's repository root — where the final shell fallback lands.
383+
* `GUI_CMD`: shell command for the primary GUI path. Pass `""` to skip this stage.
384+
* `TUI_CMD`: shell command for the TUI fallback. Pass `""` to skip this stage.
385+
* `LOG_FILE` (optional): log file path, shown in banners so the user knows where to look.
386+
387+
Each `*_CMD` is executed via `bash -c`, so pipelines and shell quoting work.
388+
For launchers that daemonise their payload and exit 0 immediately, chain
389+
a `tail -f LOG` after the launcher so the terminal stays open:
390+
391+
[source,bash]
392+
----
393+
"aerie-launcher.sh --auto && tail -f /tmp/aerie.log"
394+
----
395+
396+
=== The three stages
397+
398+
[cols="1,3,3"]
399+
|===
400+
| Stage | On success | On failure
401+
402+
| **1. GUI**
403+
| `keepopen` exits 0 silently.
404+
| Yellow banner titled `FALLBACK 1/2 — GUI FAILED (exit N)` listing the
405+
GUI cmd, log file, and a human-readable hint. Proceeds to stage 2.
406+
407+
| **2. TUI**
408+
| `keepopen` exits 0 silently.
409+
| Red banner titled `FALLBACK 2/2 — TUI ALSO FAILED (exit N)` listing
410+
both commands and the repo path. Proceeds to stage 3.
411+
412+
| **3. Shell at repo root**
413+
| `exec bash -l` inside `REPO_DIR` — user can investigate, run `just --list`, etc.
414+
| If `REPO_DIR` does not exist, a red warning prints and the shell starts in `$PWD`.
415+
|===
416+
417+
The banners are intentionally loud and ugly. Visibility beats aesthetics —
418+
the point is that a broken launcher should *look* broken, not silently fail.
419+
420+
=== Forbidden patterns
421+
422+
* Do **not** use `read -rp 'Press Enter to close...'` as a terminal-keepalive.
423+
It loses the user's context. Use `keepopen.sh`'s shell fallback instead,
424+
which drops them into the repo root where they can actually debug.
425+
* Do **not** wrap `Exec=` in `konsole --hold` — the `--hold` terminal has
426+
no working directory and no login-shell environment, so the user can't
427+
easily switch to investigating.
428+
* Do **not** skip `keepopen.sh` for "simple" apps. Simple apps break too,
429+
and the symptom is identical to the flashy ones without the wrapper.
430+
431+
=== Forbidden (legacy) patterns
332432

333433
[source,ini]
334434
----
335-
# BAD: Terminal wrapping
435+
# BAD: konsole wrapping (pre-keepopen era)
336436
Exec=konsole -e /path/to/launcher.sh --auto
337437
Terminal=true
338438
339-
# BAD: Complex wrapping
439+
# BAD: konsole --hold with no cwd / no login shell
340440
Exec=konsole --background-mode --hold --qwindowtitle Title -e wrapper.sh launcher.sh
341441
342-
# BAD: No error handling
343-
Exec=launcher.sh
442+
# BAD: launcher without keepopen — flashes a terminal on failure with no feedback
443+
Exec=launcher.sh --auto
344444
----
345445

346446
== Troubleshooting Guide
@@ -418,8 +518,12 @@ APP_NAME="AppName"
418518

419519
== Compliance Checklist
420520

421-
[ ] Remove all terminal wrapping from desktop files
422-
[ ] Set `Terminal=false` for GUI/web applications
521+
[ ] Primary `Exec=` goes through `keepopen.sh` with non-empty GUI and/or TUI commands
522+
[ ] `GUI_CMD` for daemon-style launchers chains `&& tail -f LOG` so the terminal stays open on success
523+
[ ] `REPO_DIR` argument points to a real, existing repository root
524+
[ ] `LOG_FILE` argument is passed when the launcher writes one, so banners can cite it
525+
[ ] Remove all pre-keepopen konsole wrapping from desktop files
526+
[ ] `Terminal=true` on the primary Exec (keepopen needs a terminal for banners + shell fallback)
423527
[ ] Implement `nohup` for background processes
424528
[ ] Add PID file tracking and cleanup
425529
[ ] Implement `wait_for_server()` with reasonable timeout

launcher/keepopen.sh

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
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+
# keepopen.sh — standard desktop launcher fallback ladder.
6+
#
7+
# Canonical location: developer-ecosystem/standards/launcher/keepopen.sh
8+
# Deployed copy: .desktop-tools/keepopen.sh (symlinked)
9+
# Documented in: standards/docs/UX-standards/launcher-standard.adoc §Fallback Ladder
10+
#
11+
# Its job is to turn a possibly-broken launcher into something that ALWAYS
12+
# lands the user somewhere useful — even when every upstream hook fails.
13+
#
14+
# Usage:
15+
# keepopen.sh APP_NAME REPO_DIR "GUI_CMD" "TUI_CMD" [LOG_FILE]
16+
#
17+
# Fallback ladder (each fallback shows a LOUD banner so the failure is
18+
# visible — the point is that the user CAN see a tool is broken):
19+
#
20+
# 1. GUI_CMD — primary path. Silent on success. If it fails ↓
21+
# 2. TUI_CMD — loud yellow banner, then fallback. If it fails ↓
22+
# 3. bash -l — loud red banner, then cd into REPO_DIR and drop into
23+
# an interactive login shell. Never just "press enter
24+
# to close" — the user lands in the repo so they can
25+
# actually fix the thing that's broken.
26+
#
27+
# Each CMD is evaluated as `bash -c "$cmd"`, so pipelines and shell quoting
28+
# work normally. Pass an empty string to skip a stage (e.g. an app with no
29+
# GUI can use `""` for GUI_CMD and go straight to the TUI banner → TUI).
30+
#
31+
# Banners are intentionally loud and ugly — visibility beats aesthetics.
32+
33+
set -u
34+
35+
APP_NAME="${1:?keepopen: APP_NAME required (arg 1)}"
36+
REPO_DIR="${2:?keepopen: REPO_DIR required (arg 2)}"
37+
GUI_CMD="${3:?keepopen: GUI_CMD required (arg 3) — pass '' if not applicable}"
38+
TUI_CMD="${4:?keepopen: TUI_CMD required (arg 4) — pass '' if not applicable}"
39+
LOG_FILE="${5:-}"
40+
41+
C_RED=$'\033[1;31m'
42+
C_YEL=$'\033[1;33m'
43+
C_CYA=$'\033[1;36m'
44+
C_GRN=$'\033[1;32m'
45+
C_BOLD=$'\033[1m'
46+
C_RST=$'\033[0m'
47+
48+
banner() {
49+
# $1 = colour; $2 = title; remaining args = body lines.
50+
local colour="$1"; shift
51+
local title="$1"; shift
52+
echo
53+
echo "${colour}${C_BOLD}================================================================${C_RST}"
54+
echo "${colour}${C_BOLD} ${title}${C_RST}"
55+
echo "${colour}${C_BOLD}================================================================${C_RST}"
56+
local line
57+
for line in "$@"; do
58+
[[ -z "${line}" ]] && { echo; continue; }
59+
echo " ${colour}${line}${C_RST}"
60+
done
61+
echo
62+
}
63+
64+
# -----------------------------------------------------------------------------
65+
# STAGE 1 — GUI
66+
# -----------------------------------------------------------------------------
67+
68+
gui_exit=0
69+
if [[ -n "${GUI_CMD}" ]]; then
70+
echo "${C_CYA}[keepopen:${APP_NAME}] GUI → ${GUI_CMD}${C_RST}"
71+
bash -c "${GUI_CMD}"
72+
gui_exit=$?
73+
if [[ ${gui_exit} -eq 0 ]]; then
74+
exit 0
75+
fi
76+
banner "${C_YEL}" "FALLBACK 1/2 — GUI FAILED (exit ${gui_exit})" \
77+
"APP : ${APP_NAME}" \
78+
"GUI cmd : ${GUI_CMD}" \
79+
"${LOG_FILE:+LOG FILE: ${LOG_FILE}}" \
80+
"" \
81+
"The primary GUI path exited non-zero." \
82+
"Something needs fixing. Falling back to the TUI path." \
83+
"(If this keeps happening, edit the .desktop file or the" \
84+
"keepopen invocation to point at a working GUI command.)"
85+
else
86+
banner "${C_YEL}" "STAGE 1/2 SKIPPED — NO GUI CONFIGURED" \
87+
"APP : ${APP_NAME}" \
88+
"" \
89+
"This app was launched with no GUI command. Going straight to TUI."
90+
fi
91+
92+
# -----------------------------------------------------------------------------
93+
# STAGE 2 — TUI
94+
# -----------------------------------------------------------------------------
95+
96+
tui_exit=0
97+
if [[ -n "${TUI_CMD}" ]]; then
98+
echo "${C_CYA}[keepopen:${APP_NAME}] TUI → ${TUI_CMD}${C_RST}"
99+
bash -c "${TUI_CMD}"
100+
tui_exit=$?
101+
if [[ ${tui_exit} -eq 0 ]]; then
102+
exit 0
103+
fi
104+
banner "${C_RED}" "FALLBACK 2/2 — TUI ALSO FAILED (exit ${tui_exit})" \
105+
"APP : ${APP_NAME}" \
106+
"GUI cmd : ${GUI_CMD:-<none>}" \
107+
"TUI cmd : ${TUI_CMD}" \
108+
"${LOG_FILE:+LOG FILE: ${LOG_FILE}}" \
109+
"REPO : ${REPO_DIR}" \
110+
"" \
111+
"BOTH the GUI and the TUI paths failed." \
112+
"Something needs fixing — you are being dropped into a shell" \
113+
"at the repo root so you can investigate, not just closed out."
114+
else
115+
banner "${C_RED}" "STAGE 2/2 SKIPPED — NO TUI CONFIGURED" \
116+
"APP : ${APP_NAME}" \
117+
"REPO: ${REPO_DIR}" \
118+
"" \
119+
"No TUI command was provided either. Dropping into a shell at the repo root."
120+
fi
121+
122+
# -----------------------------------------------------------------------------
123+
# STAGE 3 — interactive shell at repo root (final fallback)
124+
# -----------------------------------------------------------------------------
125+
126+
if [[ -d "${REPO_DIR}" ]]; then
127+
cd "${REPO_DIR}" || true
128+
echo "${C_GRN}[keepopen:${APP_NAME}] Dropping into bash at ${REPO_DIR}${C_RST}"
129+
else
130+
echo "${C_RED}[keepopen:${APP_NAME}] REPO_DIR does not exist: ${REPO_DIR}${C_RST}" >&2
131+
echo "${C_RED}[keepopen:${APP_NAME}] Staying in ${PWD} instead.${C_RST}" >&2
132+
fi
133+
134+
cat <<EOF
135+
136+
${C_YEL}${C_BOLD}Hints:${C_RST}
137+
- You are in this shell because the launcher's GUI/TUI paths failed.
138+
- ${LOG_FILE:+Check the log file: ${LOG_FILE}}
139+
- Type 'exit' or Ctrl-D to close this window.
140+
- Type 'ls', 'cat README.adoc', or 'just --list' to investigate.
141+
142+
EOF
143+
144+
exec bash -l

launcher/launcher-standard.a2ml

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,33 @@
1515
# - standards/docs/UX-standards/LM-LA-LIFECYCLE-STANDARD.adoc (install/uninstall)
1616

1717
[spec]
18-
version = "0.1.0"
19-
date = "2026-04-10"
18+
version = "0.2.0"
19+
date = "2026-04-17"
2020
compliance = [
2121
"launcher-standard.adoc",
2222
"LM-LA-LIFECYCLE-STANDARD.adoc",
2323
"cross-platform-system-integration-modes",
24+
"fallback-ladder-keepopen",
2425
]
2526

27+
[fallback-ladder]
28+
# keepopen.sh wraps every primary desktop-file Exec line. It turns launcher
29+
# failures from invisible flashes into loud, labelled banners, and lands the
30+
# user in an interactive shell at the repo root if everything fails.
31+
#
32+
# See launcher-standard.adoc §Fallback Ladder for the prose version.
33+
wrapper = "keepopen.sh"
34+
canonical-path = "developer-ecosystem/standards/launcher/keepopen.sh"
35+
deployed-symlink = "/var/mnt/eclipse/repos/.desktop-tools/keepopen.sh"
36+
calling-convention = "keepopen.sh APP_NAME REPO_DIR \"GUI_CMD\" \"TUI_CMD\" [LOG_FILE]"
37+
stages = [
38+
{ name = "gui", colour = "yellow", on-failure = "show-banner-then-try-tui" },
39+
{ name = "tui", colour = "red", on-failure = "show-banner-then-drop-to-shell" },
40+
{ name = "shell", colour = "green", behaviour = "exec-bash-login-at-repo-dir" },
41+
]
42+
banner-visibility = "loud" # Intentionally ugly — visibility beats aesthetics.
43+
final-shell = "bash -l at REPO_DIR (never 'press enter to close')"
44+
2645
[required-modes]
2746
# These are the modes every compliant launcher MUST implement.
2847
runtime = ["--start", "--stop", "--status", "--auto", "--browser"]

0 commit comments

Comments
 (0)