Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 114 additions & 10 deletions docs/UX-standards/launcher-standard.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
144 changes: 144 additions & 0 deletions launcher/keepopen.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: PMPL-1.0-or-later
# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
#
# 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:-<none>}" \
"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 <<EOF

${C_YEL}${C_BOLD}Hints:${C_RST}
- You are in this shell because the launcher's GUI/TUI paths failed.
- ${LOG_FILE:+Check the log file: ${LOG_FILE}}
- Type 'exit' or Ctrl-D to close this window.
- Type 'ls', 'cat README.adoc', or 'just --list' to investigate.

EOF

exec bash -l
23 changes: 21 additions & 2 deletions launcher/launcher-standard.a2ml
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,33 @@
# - standards/docs/UX-standards/LM-LA-LIFECYCLE-STANDARD.adoc (install/uninstall)

[spec]
version = "0.1.0"
date = "2026-04-10"
version = "0.2.0"
date = "2026-04-17"
compliance = [
"launcher-standard.adoc",
"LM-LA-LIFECYCLE-STANDARD.adoc",
"cross-platform-system-integration-modes",
"fallback-ladder-keepopen",
]

[fallback-ladder]
# keepopen.sh wraps every primary desktop-file Exec line. It turns launcher
# failures from invisible flashes into loud, labelled banners, and lands the
# user in an interactive shell at the repo root if everything fails.
#
# See launcher-standard.adoc §Fallback Ladder for the prose version.
wrapper = "keepopen.sh"
canonical-path = "developer-ecosystem/standards/launcher/keepopen.sh"
deployed-symlink = "/var/mnt/eclipse/repos/.desktop-tools/keepopen.sh"
calling-convention = "keepopen.sh APP_NAME REPO_DIR \"GUI_CMD\" \"TUI_CMD\" [LOG_FILE]"
stages = [
{ name = "gui", colour = "yellow", on-failure = "show-banner-then-try-tui" },
{ name = "tui", colour = "red", on-failure = "show-banner-then-drop-to-shell" },
{ name = "shell", colour = "green", behaviour = "exec-bash-login-at-repo-dir" },
]
banner-visibility = "loud" # Intentionally ugly — visibility beats aesthetics.
final-shell = "bash -l at REPO_DIR (never 'press enter to close')"

[required-modes]
# These are the modes every compliant launcher MUST implement.
runtime = ["--start", "--stop", "--status", "--auto", "--browser"]
Expand Down
Loading