Skip to content

Commit 79d8df9

Browse files
hyperpolymathclaude
andcommitted
launcher: add keepopen.sh fallback ladder to launcher standard
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) <noreply@anthropic.com>
1 parent 5a320dd commit 79d8df9

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)