|
| 1 | + |
| 2 | +#!/usr/bin/env bash |
| 3 | +# SPDX-License-Identifier: PMPL-1.0-or-later |
| 4 | +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk> |
| 5 | +# |
| 6 | +# @a2ml-metadata begin |
| 7 | +# ( |
| 8 | +# id = "nqc-launcher" |
| 9 | +# type = "launcher" |
| 10 | +# version = "1.0.0" |
| 11 | +# app-name = "nqc" |
| 12 | +# app-display = "NQC" |
| 13 | +# app-url = "" |
| 14 | +# runtime-kind = "process" |
| 15 | +# standards-compliance = [ |
| 16 | +# "launcher-standard.adoc" |
| 17 | +# "LM-LA-LIFECYCLE-STANDARD.adoc" |
| 18 | +# "cross-platform-system-integration-modes" |
| 19 | +# ] |
| 20 | +# standard-spec-version = "0.1.0" |
| 21 | +# generator = "launch-scaffolder" |
| 22 | +# ) |
| 23 | +# @a2ml-metadata end |
| 24 | +# |
| 25 | +# ============================================================================ |
| 26 | +# nqc-launcher.sh — NQC |
| 27 | +# ============================================================================ |
| 28 | +# GENERATED by launch-scaffolder from nqc.launcher.a2ml. |
| 29 | +# Do NOT edit this file directly — your edits will be overwritten on the next |
| 30 | +# `launch-scaffolder realign`. Put per-app tweaks in the [exceptions] block of |
| 31 | +# the .launcher.a2ml config instead. |
| 32 | +# ============================================================================ |
| 33 | + |
| 34 | +set -euo pipefail |
| 35 | + |
| 36 | +# ---------------------------------------------------------------------------- |
| 37 | +# CONFIGURATION |
| 38 | +# ---------------------------------------------------------------------------- |
| 39 | + |
| 40 | +APP_NAME="nqc" |
| 41 | +APP_DISPLAY="NQC" |
| 42 | +APP_DESC="Non-Quantum Computing — hyperpolymath database client" |
| 43 | +APP_CATEGORIES="Development;Database;" |
| 44 | +APP_GENERIC_NAME="NQC" |
| 45 | +RUNTIME_KIND="process" |
| 46 | + |
| 47 | +REPO_DIR="/var/mnt/eclipse/repos/nextgen-databases/nqc" |
| 48 | +ICON_SOURCE="/var/mnt/eclipse/repos/nextgen-databases/nqc/assets/icon-256.png" |
| 49 | + |
| 50 | +URL="" |
| 51 | + |
| 52 | + |
| 53 | +PID_FILE="/tmp/nqc.pid" |
| 54 | +LOG_FILE="/tmp/nqc.log" |
| 55 | + |
| 56 | +# Explicit argv from [runtime].command |
| 57 | +START_COMMAND=(/home/hyper/.bin/nqc --gui ) |
| 58 | + |
| 59 | +MODE="${1:---start}" |
| 60 | +FORCE="false" |
| 61 | +[[ "${2:-}" == "--force" ]] && FORCE="true" |
| 62 | + |
| 63 | +# ---------------------------------------------------------------------------- |
| 64 | +# LOGGING HELPERS |
| 65 | +# ---------------------------------------------------------------------------- |
| 66 | + |
| 67 | +log() { echo -e "\033[0;32m[$APP_DISPLAY]\033[0m $1"; } |
| 68 | +warn() { echo -e "\033[0;33m[$APP_DISPLAY]\033[0m $1" >&2; } |
| 69 | +err() { echo -e "\033[0;31m[$APP_DISPLAY]\033[0m ERROR: $1" >&2; } |
| 70 | + |
| 71 | +is_gui_context() { |
| 72 | + [ ! -t 2 ] && { [ -n "${DISPLAY:-}" ] || [ -n "${WAYLAND_DISPLAY:-}" ]; } |
| 73 | +} |
| 74 | + |
| 75 | +gui_error() { |
| 76 | + local title="$1" |
| 77 | + local body="$2" |
| 78 | + err "$title" |
| 79 | + echo "$body" | sed 's/^/ /' >&2 |
| 80 | + if is_gui_context; then |
| 81 | + if command -v kdialog >/dev/null 2>&1; then kdialog --title "$APP_DISPLAY: $title" --error "$body" 2>/dev/null & |
| 82 | + elif command -v zenity >/dev/null 2>&1; then zenity --error --title="$APP_DISPLAY: $title" --text="$body" --width=500 2>/dev/null & |
| 83 | + elif command -v notify-send >/dev/null 2>&1; then notify-send --urgency=critical --icon=dialog-error "$APP_DISPLAY: $title" "$body" 2>/dev/null & |
| 84 | + elif command -v xmessage >/dev/null 2>&1; then xmessage -center "$APP_DISPLAY: $title\n\n$body" 2>/dev/null & |
| 85 | + fi |
| 86 | + fi |
| 87 | +} |
| 88 | + |
| 89 | +# ---------------------------------------------------------------------------- |
| 90 | +# PLATFORM DETECTION |
| 91 | +# ---------------------------------------------------------------------------- |
| 92 | + |
| 93 | +detect_platform() { |
| 94 | + case "$(uname -s)" in |
| 95 | + Linux*) echo "linux" ;; |
| 96 | + Darwin*) echo "macos" ;; |
| 97 | + CYGWIN*|MINGW*|MSYS*|Windows_NT) echo "windows" ;; |
| 98 | + *) echo "unknown" ;; |
| 99 | + esac |
| 100 | +} |
| 101 | +PLATFORM="$(detect_platform)" |
| 102 | + |
| 103 | +case "$PLATFORM" in |
| 104 | + linux) |
| 105 | + APPS_DIR="$HOME/.local/share/applications" |
| 106 | + ICON_DIR="$HOME/.local/share/icons/hicolor/256x256/apps" |
| 107 | + DESKTOP_SHORTCUT_DIR="$HOME/Desktop" |
| 108 | + BIN_DIR="$HOME/.local/bin" |
| 109 | + DESKTOP_FILE_TARGET="$APPS_DIR/${APP_NAME}.desktop" |
| 110 | + DESKTOP_SHORTCUT_TARGET="$DESKTOP_SHORTCUT_DIR/${APP_NAME}.desktop" |
| 111 | + ICON_TARGET="$ICON_DIR/${APP_NAME}.png" |
| 112 | + LAUNCHER_TARGET="$BIN_DIR/${APP_NAME}-launcher" |
| 113 | + ;; |
| 114 | + macos) |
| 115 | + APPS_DIR="$HOME/Applications" |
| 116 | + DESKTOP_SHORTCUT_DIR="$HOME/Desktop" |
| 117 | + BIN_DIR="$HOME/.local/bin" |
| 118 | + DESKTOP_FILE_TARGET="$APPS_DIR/${APP_DISPLAY}.app" |
| 119 | + DESKTOP_SHORTCUT_TARGET="$DESKTOP_SHORTCUT_DIR/${APP_DISPLAY}.command" |
| 120 | + ICON_TARGET="$APPS_DIR/${APP_DISPLAY}.app/Contents/Resources/icon.png" |
| 121 | + LAUNCHER_TARGET="$BIN_DIR/${APP_NAME}-launcher" |
| 122 | + ;; |
| 123 | + windows) |
| 124 | + APPDATA_DIR="${APPDATA:-$HOME/AppData/Roaming}" |
| 125 | + START_MENU_DIR="$APPDATA_DIR/Microsoft/Windows/Start Menu/Programs" |
| 126 | + DESKTOP_SHORTCUT_DIR="$HOME/Desktop" |
| 127 | + BIN_DIR="$HOME/.local/bin" |
| 128 | + DESKTOP_FILE_TARGET="$START_MENU_DIR/${APP_DISPLAY}.lnk" |
| 129 | + DESKTOP_SHORTCUT_TARGET="$DESKTOP_SHORTCUT_DIR/${APP_DISPLAY}.lnk" |
| 130 | + ICON_TARGET="$BIN_DIR/${APP_NAME}.ico" |
| 131 | + LAUNCHER_TARGET="$BIN_DIR/${APP_NAME}-launcher.sh" |
| 132 | + ;; |
| 133 | + *) |
| 134 | + APPS_DIR=""; DESKTOP_SHORTCUT_DIR=""; BIN_DIR="$HOME/.local/bin" |
| 135 | + DESKTOP_FILE_TARGET=""; DESKTOP_SHORTCUT_TARGET=""; ICON_TARGET="" |
| 136 | + LAUNCHER_TARGET="$BIN_DIR/${APP_NAME}-launcher" |
| 137 | + ;; |
| 138 | +esac |
| 139 | + |
| 140 | +# ---------------------------------------------------------------------------- |
| 141 | +# PROCESS MANAGEMENT |
| 142 | +# ---------------------------------------------------------------------------- |
| 143 | + |
| 144 | +is_running() { |
| 145 | + [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null |
| 146 | +} |
| 147 | + |
| 148 | +clear_stale_pid() { |
| 149 | + if [ -f "$PID_FILE" ] && ! kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then |
| 150 | + rm -f "$PID_FILE" |
| 151 | + fi |
| 152 | +} |
| 153 | + |
| 154 | + |
| 155 | + |
| 156 | +start_server() { |
| 157 | + clear_stale_pid |
| 158 | +if is_running; then |
| 159 | + log "Already running (PID $(cat "$PID_FILE"))" |
| 160 | + return 0 |
| 161 | + fi |
| 162 | + |
| 163 | + if [ -z "${START_COMMAND:-}" ] || { [ ! "${START_COMMAND:0:1}" = "(" ] && [ ! -x "${START_COMMAND%% *}" ] && ! command -v "${START_COMMAND%% *}" >/dev/null 2>&1; }; then |
| 164 | + if [ -z "${START_COMMAND:-}" ]; then |
| 165 | + gui_error "No startup command found" \ |
| 166 | +"$APP_DISPLAY is installed, but I don't know how to start it. Check the |
| 167 | +[runtime].startup-command-search or [runtime].command in nqc.launcher.a2ml." |
| 168 | + return 1 |
| 169 | + fi |
| 170 | + fi |
| 171 | + |
| 172 | + log "Starting $APP_DISPLAY..." |
| 173 | + cd "$REPO_DIR" 2>/dev/null || true |
| 174 | +nohup "${START_COMMAND[@]}" >"$LOG_FILE" 2>&1 & |
| 175 | + echo $! > "$PID_FILE" |
| 176 | + |
| 177 | + sleep 0.2 |
| 178 | + if ! kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then |
| 179 | + gui_error "Process exited immediately" "Check $LOG_FILE" |
| 180 | + rm -f "$PID_FILE" |
| 181 | + return 1 |
| 182 | + fi |
| 183 | + |
| 184 | +log "Started (PID $(cat "$PID_FILE"))" |
| 185 | + return 0 |
| 186 | +} |
| 187 | + |
| 188 | +stop_server() { |
| 189 | +if ! is_running; then |
| 190 | + log "No running instance found" |
| 191 | + return 0 |
| 192 | + fi |
| 193 | + log "Stopping $APP_DISPLAY..." |
| 194 | + kill "$(cat "$PID_FILE")" 2>/dev/null || true |
| 195 | + rm -f "$PID_FILE" |
| 196 | + log "Stopped" |
| 197 | +} |
| 198 | + |
| 199 | +open_browser() { |
| 200 | + if [ -z "$URL" ]; then |
| 201 | + log "No URL configured for $APP_DISPLAY" |
| 202 | + return 1 |
| 203 | + fi |
| 204 | + |
| 205 | + log "Opening $URL..." |
| 206 | + case "$PLATFORM" in |
| 207 | + linux) |
| 208 | + if command -v xdg-open >/dev/null 2>&1; then xdg-open "$URL" & |
| 209 | + elif command -v firefox >/dev/null 2>&1; then firefox "$URL" & |
| 210 | + elif command -v chromium >/dev/null 2>&1; then chromium "$URL" & |
| 211 | + else warn "No browser found — open manually: $URL"; fi ;; |
| 212 | + macos) open "$URL" 2>/dev/null || warn "Open manually: $URL" ;; |
| 213 | + windows) |
| 214 | + if command -v start >/dev/null 2>&1; then start "$URL" |
| 215 | + elif command -v cygstart >/dev/null 2>&1; then cygstart "$URL" |
| 216 | + else warn "Open manually: $URL"; fi ;; |
| 217 | + *) warn "Unknown platform — open manually: $URL" ;; |
| 218 | + esac |
| 219 | +} |
| 220 | + |
| 221 | +# ---------------------------------------------------------------------------- |
| 222 | +# SYSTEM INTEGRATION — --integ / --disinteg |
| 223 | +# ---------------------------------------------------------------------------- |
| 224 | + |
| 225 | +already_integrated() { |
| 226 | + [ -f "$DESKTOP_FILE_TARGET" ] || [ -f "$LAUNCHER_TARGET" ] |
| 227 | +} |
| 228 | + |
| 229 | +write_linux_desktop_file() { |
| 230 | + local target="$1" |
| 231 | + local icon_name |
| 232 | + if [ -f "$ICON_TARGET" ]; then |
| 233 | + icon_name="$APP_NAME" |
| 234 | + else |
| 235 | + icon_name="package-x-generic" |
| 236 | + fi |
| 237 | + cat > "$target" <<EOF |
| 238 | +[Desktop Entry] |
| 239 | +Type=Application |
| 240 | +Version=1.0 |
| 241 | +Name=$APP_DISPLAY |
| 242 | +GenericName=$APP_GENERIC_NAME |
| 243 | +Comment=$APP_DESC |
| 244 | +Exec=$LAUNCHER_TARGET --start |
| 245 | +Icon=$icon_name |
| 246 | +Terminal=false |
| 247 | +Categories=$APP_CATEGORIES |
| 248 | +StartupNotify=true |
| 249 | +StartupWMClass=$APP_NAME |
| 250 | +Actions=stop;status; |
| 251 | +
|
| 252 | +[Desktop Action stop] |
| 253 | +Name=Stop |
| 254 | +Exec=$LAUNCHER_TARGET --stop |
| 255 | +
|
| 256 | +[Desktop Action status] |
| 257 | +Name=Status |
| 258 | +Exec=$LAUNCHER_TARGET --status |
| 259 | +EOF |
| 260 | + chmod 444 "$target" |
| 261 | +} |
| 262 | + |
| 263 | +do_integ_linux() { |
| 264 | + mkdir -p "$APPS_DIR" "$ICON_DIR" "$BIN_DIR" "$DESKTOP_SHORTCUT_DIR" |
| 265 | + local script_path="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")" |
| 266 | + cp "$script_path" "$LAUNCHER_TARGET" |
| 267 | + chmod +x "$LAUNCHER_TARGET" |
| 268 | + log " + launcher: $LAUNCHER_TARGET" |
| 269 | + |
| 270 | + if [ -n "$ICON_SOURCE" ] && [ -f "$ICON_SOURCE" ]; then |
| 271 | + cp "$ICON_SOURCE" "$ICON_TARGET" |
| 272 | + log " + icon: $ICON_TARGET" |
| 273 | + else |
| 274 | + log " · no custom icon — using system fallback (package-x-generic)" |
| 275 | + fi |
| 276 | + |
| 277 | + write_linux_desktop_file "$DESKTOP_FILE_TARGET" |
| 278 | + log " + menu: $DESKTOP_FILE_TARGET" |
| 279 | + write_linux_desktop_file "$DESKTOP_SHORTCUT_TARGET" |
| 280 | + log " + desktop: $DESKTOP_SHORTCUT_TARGET" |
| 281 | + |
| 282 | + command -v update-desktop-database >/dev/null 2>&1 && \ |
| 283 | + update-desktop-database "$APPS_DIR" 2>/dev/null || true |
| 284 | + if command -v gio >/dev/null 2>&1; then |
| 285 | + gio set "$DESKTOP_FILE_TARGET" "metadata::trusted" true 2>/dev/null || true |
| 286 | + gio set "$DESKTOP_SHORTCUT_TARGET" "metadata::trusted" true 2>/dev/null || true |
| 287 | + fi |
| 288 | + if [ -x "/var/mnt/eclipse/repos/.desktop-tools/verify-desktop-integrity.sh" ]; then |
| 289 | + /var/mnt/eclipse/repos/.desktop-tools/verify-desktop-integrity.sh --generate 2>/dev/null \ |
| 290 | + && log " + integrity hashes generated" \ |
| 291 | + || log " · integrity hash generation failed (non-fatal)" |
| 292 | + fi |
| 293 | +} |
| 294 | + |
| 295 | +do_integ() { |
| 296 | + if already_integrated && [ "$FORCE" != "true" ]; then |
| 297 | + warn "$APP_DISPLAY is already integrated." |
| 298 | + read -rp "Reinstall? [y/N] " confirm |
| 299 | + [[ ! "$confirm" =~ ^[Yy]$ ]] && { log "Nothing changed."; return 0; } |
| 300 | + fi |
| 301 | + log "Integrating $APP_DISPLAY with the $PLATFORM desktop..." |
| 302 | + case "$PLATFORM" in |
| 303 | + linux) do_integ_linux ;; |
| 304 | + *) err "Only Linux integration is implemented in the generated template so far."; return 1 ;; |
| 305 | + esac |
| 306 | + log "✓ $APP_DISPLAY is now in your menu and on your Desktop." |
| 307 | +} |
| 308 | + |
| 309 | +do_disinteg() { |
| 310 | + log "Removing $APP_DISPLAY system integration..." |
| 311 | +if is_running; then |
| 312 | + log " • stopping running server" |
| 313 | + stop_server |
| 314 | + fi |
| 315 | + local removed_anything="false" |
| 316 | + local targets=( |
| 317 | + "$DESKTOP_FILE_TARGET" |
| 318 | + "$DESKTOP_SHORTCUT_TARGET" |
| 319 | + "$ICON_TARGET" |
| 320 | + "$LAUNCHER_TARGET" |
| 321 | + ) |
| 322 | + for t in "${targets[@]}"; do |
| 323 | + [ -z "$t" ] && continue |
| 324 | + if [ -e "$t" ] || [ -L "$t" ]; then |
| 325 | + rm -rf "$t" |
| 326 | + log " - removed $t" |
| 327 | + removed_anything="true" |
| 328 | + fi |
| 329 | + done |
| 330 | + [ "$PLATFORM" = "linux" ] && command -v update-desktop-database >/dev/null 2>&1 && \ |
| 331 | + update-desktop-database "$APPS_DIR" 2>/dev/null || true |
| 332 | + rm -f "$PID_FILE" |
| 333 | + if [ "$removed_anything" = "true" ]; then |
| 334 | + log "✓ $APP_DISPLAY removed from your system." |
| 335 | + else |
| 336 | + log "Nothing to remove — $APP_DISPLAY was not integrated here." |
| 337 | + fi |
| 338 | +} |
| 339 | + |
| 340 | +show_help() { |
| 341 | + cat <<EOF |
| 342 | +$APP_DISPLAY launcher — $APP_DESC |
| 343 | +
|
| 344 | +Usage: $0 [MODE] [--force] |
| 345 | +
|
| 346 | +Runtime modes: |
| 347 | +
|
| 348 | + --start Start the process (default) |
| 349 | + --stop Stop the process |
| 350 | + --status Show running status |
| 351 | + --auto Alias for --start |
| 352 | +
|
| 353 | +System integration: |
| 354 | + --integ Install desktop entry + shortcut + icon (idempotent; --force to reinstall) |
| 355 | + --disinteg Remove everything --integ installed (idempotent) |
| 356 | +
|
| 357 | +Misc: |
| 358 | + --help This text |
| 359 | +
|
| 360 | +Detected platform: $PLATFORM |
| 361 | +Runtime kind: $RUNTIME_KIND |
| 362 | +Repo: $REPO_DIR |
| 363 | +EOF |
| 364 | +} |
| 365 | + |
| 366 | +# ---------------------------------------------------------------------------- |
| 367 | +# MAIN SWITCH |
| 368 | +# ---------------------------------------------------------------------------- |
| 369 | + |
| 370 | +case "$MODE" in |
| 371 | + --start) start_server ;; |
| 372 | + --stop) stop_server ;; |
| 373 | + --status) |
| 374 | + if is_running; then |
| 375 | + log "Running (PID $(cat "$PID_FILE"))${URL:+ — $URL}" |
| 376 | + else |
| 377 | + log "Not running${URL:+ — $URL}" |
| 378 | + fi |
| 379 | + ;; |
| 380 | + --browser|--web) |
| 381 | +log "$APP_DISPLAY has no URL — --browser is not applicable" |
| 382 | + ;; |
| 383 | + --auto) |
| 384 | +start_server |
| 385 | + ;; |
| 386 | + --integ) do_integ ;; |
| 387 | + --disinteg) do_disinteg ;; |
| 388 | + --help|-h) show_help ;; |
| 389 | + *) |
| 390 | + err "Unknown mode: $MODE" |
| 391 | + show_help |
| 392 | + exit 2 |
| 393 | + ;; |
| 394 | +esac |
0 commit comments