Skip to content

Commit 48851f9

Browse files
hyperpolymathclaude
andcommitted
feat(nqc/launcher): migrate to launch-scaffolder (process kind)
Adds nqc/nqc.launcher.a2ml and the scaffolder-generated nqc/nqc-launcher.sh inside the nextgen-databases monorepo. Replaces the hand-written launcher from ~/.desktop-tools/ (archived). The nqc launcher shells out to the pre-installed ~/.bin/nqc binary with --gui. Desktop shortcut (NextGen-Query-Client.desktop) repointed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4774c56 commit 48851f9

2 files changed

Lines changed: 422 additions & 0 deletions

File tree

nqc/nqc-launcher.sh

Lines changed: 394 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,394 @@
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

Comments
 (0)