Skip to content

Latest commit

 

History

History
732 lines (586 loc) · 24.8 KB

File metadata and controls

732 lines (586 loc) · 24.8 KB

UX Standards - Comprehensive Launcher Standard

Overview

This document defines the Comprehensive Launcher Standard for the hyperpolymath ecosystem. It extends the E-Grade principles with specific implementation patterns, code templates, and troubleshooting guides for creating robust, reliable application launchers.

Standard Launcher Template

The reference implementation is provided in comprehensive-launcher-template.sh.

Key Features

# Standardized structure for all launchers
APP_NAME="MyApp"              # Application name
REPO_DIR="/path/to/repo"       # Repository directory
COMMAND="command to run"      # Startup command
URL="http://localhost:PORT"     # Web URL (if applicable)
# PID file in XDG_RUNTIME_DIR (mode 0700, user-scoped) — falls back to
# $TMPDIR (macOS) then /tmp (last resort). See §Best Practices > Security.
PID_FILE="${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/${APP_NAME}-server.pid"
# Log file in XDG_STATE_HOME (defaults to $HOME/.local/state per spec).
LOG_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/${APP_NAME}"
LOG_FILE="${LOG_DIR}/server.log"
mkdir -p "$LOG_DIR"
MODE="${1:---auto}"            # Default mode

Required Functions

log()          # User feedback (stdout)
err()          # Error messages (stderr)
is_running()   # Check if process is running
wait_for_server() # Active waiting with timeout
start_server() # Robust server startup
stop_server()  # Clean shutdown
open_browser() # Browser launching (if web app)

Implementation Patterns

Process Management

# Start process with nohup to prevent termination
nohup $COMMAND >"$LOG_FILE" 2>&1 &
echo $! > "$PID_FILE"

# Check if process is still running
is_running() {
  [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null
}

# Clean shutdown
stop_server() {
  if is_running; then
    kill "$(cat "$PID_FILE")" 2>/dev/null || true
    rm -f "$PID_FILE"
  fi
}

Server Readiness Checking

wait_for_server() {
  # All three values are env-overridable per [runtime] in launcher-standard.a2ml.
  local max_wait="${1:-${WAIT_FOR_URL_TIMEOUT_SECONDS:-15}}"
  local poll_interval="${WAIT_FOR_URL_POLL_INTERVAL:-1}"
  local per_request_timeout=2   # curl --max-time; caps each probe so a
                                # hung server can't eat the whole budget
  local waited=0

  while [ "$waited" -lt "$max_wait" ]; do
    if curl -fsS --max-time "$per_request_timeout" "$URL" >/dev/null 2>&1; then
      return 0  # Success
    fi
    sleep "$poll_interval"
    waited=$((waited + poll_interval))
  done

  return 1  # Timeout
}

# Usage — pass nothing so the env-var/constant chain applies.
if ! wait_for_server; then
  err "Server did not start within ${WAIT_FOR_URL_TIMEOUT_SECONDS:-15}s"
  err "Check log: $LOG_FILE"
  err "Override with WAIT_FOR_URL_TIMEOUT_SECONDS=<N> if the host needs longer"
  return 1
fi

Browser Launching

Resolution order, per [browser-launch] in launcher-standard.a2ml:

  1. $BROWSER env var (de-facto Unix convention; user/operator override)

  2. Platform-specific ladder, dispatched via uname -s:

    • macOS: open

    • Linux: if WSL (detected via /proc/version containing "microsoft"), prefer wslview so the URL opens in the Windows-side default browser. Otherwise: xdg-openfirefoxchromium.

    • Windows (Git Bash / MSYS / Cygwin): start (cmd builtin)

  3. Fallback: print "Open manually: $URL" so the user is never silently left without a URL.

open_browser() {
  if ! is_running; then
    err "Server is not running"
    return 1
  fi

  # 1. $BROWSER override (canonical Unix convention)
  if [[ -n "${BROWSER:-}" ]]; then
    "$BROWSER" "$URL" &
    return $?
  fi

  # 2. Platform-specific ladder
  case "$(uname -s)" in
    Darwin*)
      open "$URL" &
      ;;
    Linux*)
      # WSL detection: /proc/version mentions "microsoft" under WSL1/2
      if grep -qi microsoft /proc/version 2>/dev/null && \
         command -v wslview >/dev/null 2>&1; then
        wslview "$URL" &
      elif command -v xdg-open >/dev/null 2>&1; then
        xdg-open "$URL" &
      elif command -v firefox >/dev/null 2>&1; then
        firefox "$URL" &
      elif command -v chromium >/dev/null 2>&1; then
        chromium "$URL" &
      else
        log "Open manually: $URL"
      fi
      ;;
    MINGW*|MSYS*|CYGWIN*)
      # Git Bash / MSYS / Cygwin all carry `start` from cmd
      start "$URL" &
      ;;
    *)
      log "Open manually: $URL"
      ;;
  esac
}

Error Handling

Stderr-only err() is the right default for TUI invocation, but when a launcher is started from a desktop entry there is no terminal for the user to see — every failure looks identical (a brief flash and nothing else). The reference helper launcher/gui-error.sh surfaces errors via the platform’s dialog ladder AND stderr, so the failure is visible no matter how the launcher was started.

# Always provide actionable feedback
err() {
  echo "[$APP_NAME] ERROR: $1" >&2
  echo "[$APP_NAME] Try: check $LOG_FILE for details" >&2
}

# Source the shared GUI-error helper (reference impl of [error-visibility]
# from launcher-standard.a2ml). Falls back to err() if not available so
# every launcher works even without the shared helper on PATH.
if [ -r "$(hp_resolve_desktop_tools gui-error.sh 2>/dev/null)" ]; then
  # shellcheck disable=SC1090
  . "$(hp_resolve_desktop_tools gui-error.sh)"
else
  hp_gui_error() { err "$2"; }   # graceful degradation
fi

# Example usage — fails LOUDLY whether GUI or TUI
if ! start_server; then
  hp_gui_error "$APP_NAME failed to start" \
               "Check log: $LOG_FILE\n\nOverride wait timeout: WAIT_FOR_URL_TIMEOUT_SECONDS=<N>"
  exit 1
fi
Note
hp_gui_error writes to stderr unconditionally per [error-visibility].always-also-to-stderr = true. Set NO_GUI_ERROR=1 to suppress the dialog attempt (useful in CI).

Soft-Attach (optional ecosystem integrations)

A "soft-attach" tool is one the launcher calls IF it is installed, and silently skips otherwise. The estate ships three by default (feedback-o-tron, hypatia, panic-attack) — see [soft-attach].tools in launcher-standard.a2ml for the live list.

Downstream launchers SHOULD source launcher/soft-attach.sh rather than re-implementing the if-installed-then-invoke pattern, so behaviour stays consistent across the estate.

# Source the shared soft-attach helper. Graceful degradation: if the
# helper is not on the resolution ladder, every soft-attach call
# becomes a silent no-op.
if [ -r "$(hp_resolve_desktop_tools soft-attach.sh 2>/dev/null)" ]; then
  # shellcheck disable=SC1090
  . "$(hp_resolve_desktop_tools soft-attach.sh)"
else
  hp_soft_attach_event() { :; }
  hp_soft_attach_run()   { :; }
fi

# Call sites: typically wired into start_server() on failure path
on_start_failed() {
  hp_soft_attach_event "feedback-o-tron" "launcher:start_failed" \
      --app "$APP_NAME" --log "$LOG_FILE"
  hp_soft_attach_run "hypatia diagnose --app $APP_NAME --log $LOG_FILE"
  hp_soft_attach_run "panic-attack assail $REPO_DIR"
}

Note that template substitution ({app-name}, {log-file}, {repo-dir} in the a2ml) is the launcher’s responsibility — interpolate before passing the command line to hp_soft_attach_run.

Standard Modes

Required Modes

Mode Purpose

--start

Start the server/application without opening browser

--stop

Stop the running server/application

--status

Show current status (running/stopped, URL if applicable)

--auto (default)

Start server and open browser (for web apps)

--browser / --web

Alias for --auto (start and open browser)

--integ

System integration. Install the launcher as a first-class application: Start Menu / Applications folder entry, Desktop shortcut, icon, and the launcher itself copied to a stable location on PATH. Cross-platform (see §System Integration Modes below). Idempotent; --integ --force reinstalls without prompting.

--disinteg

System dis-integration. Undo everything --integ installed: Start Menu entry, Desktop shortcut, icon, and the installed launcher. Also stops the running server. Leaves user config in ~/.config/<app>/ and logs in place so reinstall is seamless. Idempotent.

--help / -h

Print usage text, including a description of every mode, the files the launcher reads/writes, and the detected platform.

--version / -V

Print the launcher’s version on a single machine-greppable first line, then exit 0. Format: <app-name> <version> (<build-sha-short>) [<platform>] (e.g. aerie-launcher 0.4.2 (a1b2c3d) [linux-x86_64]). Additional lines (build date, runtime versions) MAY follow. Required so field bug reports can quote a specific build — a launcher without --version produces unactionable issues.

Note
--integ and --disinteg replace the earlier pattern of separate install.sh / uninstall.sh scripts from LM-LA-LIFECYCLE-STANDARD. Having install, uninstall, and runtime in a single script means users have one thing to run and one thing to remember, which is the whole point of the E-Grade principle. The LM-LA-LIFECYCLE-STANDARD remains authoritative for what must happen during install and uninstall; this standard specifies where those operations are triggered from (the launcher itself).

Mode Implementation

case "$MODE" in
  --start)
    start_server
    ;;

  --stop)
    stop_server
    ;;

  --status)
    if is_running; then
      log "Server running (PID: $(cat "$PID_FILE"))"
      [ -n "$URL" ] && log "URL: $URL"
    else
      log "Server not running"
    fi
    ;;

  --browser|--web|--auto|*)
    start_server && open_browser
    ;;
esac

System Integration Modes (--integ / --disinteg)

The --integ mode installs the launcher as a first-class desktop application on the host OS. It is the single entry point for "make this a real app on my system". The --disinteg mode is its exact inverse.

Design principles

  1. Idempotent. --integ run twice is fine. --disinteg on an unintegrated system is a friendly no-op, not an error.

  2. Cross-platform. The same launcher script works on Linux, macOS, and Windows (via Git Bash or WSL). Platform detection is via uname -s.

  3. Detectable. --integ detects existing installations and prompts before overwriting, unless --force is passed as a second argument.

  4. Reversible. --disinteg removes everything --integ installed and leaves user config and logs alone so reinstall is seamless.

  5. No elevated privileges. All paths are user-level. No sudo, no system-wide directories. Reinstall per user, not per machine.

  6. Launcher copy, not symlink. --integ copies the launcher script to a stable location on PATH rather than symlinking, so moving or renaming the source repo does not break the Start Menu entry.

What --integ creates

Platform Path Purpose

Linux

~/.local/share/applications/<app>.desktop

Start Menu / app launcher entry

Linux

~/Desktop/<app>.desktop

Desktop shortcut (marked trusted via gio if available, so KDE doesn’t prompt)

Linux

~/.local/share/icons/hicolor/256x256/apps/<app>.png

Icon used by the Start Menu entry

Linux

~/.local/bin/<app>-launcher

Copy of the launcher script on PATH

macOS

~/Applications/<App>.app/ (minimal bundle: Info.plist, Contents/MacOS/<app>)

Launchpad + Spotlight entry. Not codesigned.

macOS

~/Desktop/<App>.command

Double-clickable shortcut

macOS

~/.local/bin/<app>-launcher

Copy of the launcher script on PATH

Windows (Git Bash / WSL)

%APPDATA%\Microsoft\Windows\Start Menu\Programs\<App>.lnk

Start Menu entry. Created via PowerShell WScript.Shell. Falls back to .bat if PowerShell is not reachable.

Windows (Git Bash / WSL)

~/Desktop/<App>.lnk

Desktop shortcut

Windows (Git Bash / WSL)

~/.local/bin/<app>-launcher.sh

Copy of the launcher script

What --disinteg removes

Everything --integ created, plus:

  • The PID file ($XDG_RUNTIME_DIR/<app>-server.pid, or the resolved equivalent — see §Best Practices > Security)

  • Any .bat fallback shortcuts written when PowerShell wasn’t available

It deliberately does not remove:

  • ~/.config/<app>/ — user preferences survive reinstall

  • $XDG_STATE_HOME/<app>/ (defaults to $HOME/.local/state/<app>/) — logs stay for post-mortem

  • The source repository at REPO_DIR

The removal instructions for those are printed after --disinteg so the user can run them manually if they really want a clean wipe.

Reference implementation

See comprehensive-launcher-template.sh for the do_integ() / do_disinteg() functions and the platform-dispatch pattern. Existing launchers should be updated to include these modes; the pattern is drop-in because the integration paths are all globals set by platform detection at the top of the script.

Desktop File Standard

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.

[Desktop Entry]
Type=Application
Name=Application Name
# The Exec path MUST be the absolute path resolved at install time via
# the [resolution].desktop-tools-search ladder (see §Canonical location).
# The freedesktop spec does NOT expand environment variables in Exec=
# lines, so `--integ` is responsible for picking the host-correct path.
# The /var/mnt/eclipse/... value below is one possible resolution — on
# hosts using $HOME/developer/repos or $HOME/dev/repos the resolved path
# will differ.
Exec=/var/mnt/eclipse/repos/.desktop-tools/keepopen.sh "AppName" "/path/to/repo" "GUI_CMD" "TUI_CMD" "$HOME/.local/state/app/server.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 — these can bypass keepopen because
# they are invoked from an already-open context menu; no fallback needed.
Actions=stop;status;

[Desktop Action stop]
Name=Stop Server
Exec=/path/to/launcher.sh --stop

[Desktop Action status]
Name=Server Status
Exec=/path/to/launcher.sh --status
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 (which need a stable absolute path) a symlink or copy is deployed inside a .desktop-tools/ directory whose location varies by host. Consumers MUST resolve that location via the search ladder declared in launcher/launcher-standard.a2ml [resolution].desktop-tools-search:

  1. $HP_DESKTOP_TOOLS — explicit override

  2. $HP_ESTATE_ROOT/.desktop-tools — estate-root convention

  3. $XDG_DATA_HOME/hyperpolymath/.desktop-tools — XDG default

  4. /var/mnt/eclipse/repos/.desktop-tools — legacy eclipse-mount layout

  5. $HOME/developer/repos/.desktop-tools — alt: $HOME/developer/repos

  6. $HOME/dev/repos/.desktop-tools — alt: $HOME/dev/repos

First existing path wins. A reference shell implementation lives at launcher/resolve-desktop-tools.sh (sourceable; provides hp_resolve_desktop_tools and hp_resolve_standard). Downstream launchers SHOULD use the reference impl rather than re-implementing the ladder.

launch-scaffolder copies the same script into its baked-in standards so regenerated launchers stay in sync. The legacy single hard-coded path (/var/mnt/eclipse/repos/.desktop-tools/keepopen.sh) remains in the a2ml as deployed-symlink for compatibility with pre-resolution consumers; new code MUST use the ladder.

Calling convention

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:

"aerie-launcher.sh --auto && tail -f $HOME/.local/state/aerie/server.log"

The three stages

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

# BAD: konsole wrapping (pre-keepopen era)
Exec=konsole -e /path/to/launcher.sh --auto
Terminal=true

# BAD: konsole --hold with no cwd / no login shell
Exec=konsole --background-mode --hold --qwindowtitle Title -e wrapper.sh launcher.sh

# BAD: launcher without keepopen — flashes a terminal on failure with no feedback
Exec=launcher.sh --auto

Troubleshooting Guide

Common Issues and Solutions

Symptom Likely Cause Solution

Black terminal window

Terminal wrapping in desktop file

Remove konsole wrapping, set Terminal=false

"Cannot connect to server"

Server not running or wrong URL

Check is_server_running() function and URL

Browser doesn’t open

Server not ready when browser launches

Increase wait_for_server timeout

Process dies immediately

Missing nohup or process management

Add nohup and proper PID tracking

Multiple instances running

No PID file checking

Implement is_running() check before start

Port already in use

Previous instance not cleaned up

Add kill before starting new instance

Debugging Checklist

  1. Check log file: tail -f "$LOG_FILE" (resolves to $XDG_STATE_HOME/<app>/server.log)

  2. Verify process: ps aux | grep app-name

  3. Test URL manually: curl -v http://localhost:PORT

  4. Check PID file: cat "$PID_FILE" (resolves to $XDG_RUNTIME_DIR/<app>-server.pid)

  5. Test browser opening: xdg-open http://localhost:PORT

  6. Verify dependencies: command -v required-command

  7. Check port availability: ss -tlnp | grep PORT

Migration Guide

From Old Konsole-Wrapped Launchers

# OLD (bad)
Exec=konsole --background-mode --hold --qwindowtitle Title -e wrapper.sh launcher.sh
Terminal=true

# NEW (good)
Exec=/path/to/launcher.sh --auto
Terminal=false

From Simple Pass-Through Scripts

# OLD (minimal)
#!/bin/bash
exec upstream-launcher "$@"

# NEW (robust)
#!/bin/bash
# Standardized launcher with process management
APP_NAME="AppName"
# ... include standard template ...

Compliance Checklist

[ ] 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 [ ] Add proper error handling and user feedback [ ] Provide clear success/failure messages [ ] Log to XDG state dir ($XDG_STATE_HOME/<app>/server.log, defaults to $HOME/.local/state/<app>/server.log); never use /tmp/<app>.log [ ] Handle browser launch failures gracefully [ ] Provide manual fallback instructions [ ] Implement --start, --stop, --status modes [ ] Test desktop launching without terminal [ ] Verify browser opens automatically [ ] Test error conditions (port in use, missing deps)

Best Practices

Logging

  • Log to $XDG_STATE_HOME/<app>/server.log (defaults to $HOME/.local/state/<app>/server.log per XDG spec). Per-user, survives reboot, not world-writable.

  • Never log to /tmp/<app>.log. Predictable names in a world-writable dir are a symlink-attack target on shared hosts — see §Best Practices > Security.

  • Include timestamps for long-running processes

  • Rotate logs if they grow large

  • Provide log location in error messages

User Experience

  • Provide immediate feedback when launching

  • Show progress during server startup

  • Clear success message when ready

  • Actionable error messages with next steps

  • Never leave user wondering what happened

Performance

  • Use reasonable timeouts (10-20 seconds typical)

  • Don’t block indefinitely

  • Provide feedback during waiting

  • Optimize startup sequence

Security

  • PID files MUST go in $XDG_RUNTIME_DIR (Linux) / $TMPDIR (macOS), not /tmp. $XDG_RUNTIME_DIR is mode 0700 and user-scoped per the XDG Base Directory spec; $TMPDIR on macOS is /var/folders/…​/T (per-user). /tmp is world-writable: an attacker on a shared host can pre-create /tmp/<app>-server.pid containing their own PID, after which the launcher’s is_running() returns true and stop_server() will kill <attacker-pid> — DoS or signal-handling abuse vector. The fallback ladder ${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}} exists only as a last resort for hosts that set neither (rare).

  • Log files MUST go in $XDG_STATE_HOME for the same reason — never /tmp/<app>.log.

  • Use predictable but unique PID file names within the chosen dir (not in /tmp).

  • Clean up PID files on exit

  • Don’t log sensitive information

  • Validate URLs before opening

Reference Implementations

Burble Launcher

link:../../../../.desktop-tools/burble-launcher.sh[role=include]

IDApTIK Launcher

link:../../../../.desktop-tools/idaptik-launcher.sh[role=include]

PanLL Launcher

link:../../../../.desktop-tools/panll-launcher.sh[role=include]

See Also