Files
lime/scripts/run-xephyr-dev
2026-10-02 16:35:17 -05:00

192 lines
5.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# Start a disposable nested X11 server plus Lime's safe live-development
# runtime. Run this from the normal graphical session, never from inside the
# Xephyr session it creates.
#
# Usage:
# run-xephyr-dev [--config PATH | --no-config] [--display :99]
# [--screen 1600x1000] [--repl-port 4005]
set -euo pipefail
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
repo_dir="$(cd -- "$script_dir/.." && pwd)"
config_home="${XDG_CONFIG_HOME:-${HOME}/.config}"
default_config_file="$config_home/lime/config.lisp"
display_name=":99"
screen_size="1600x1000"
repl_port="4005"
config_file=""
session_mode="default"
session_file=""
if [[ -f "$default_config_file" ]]; then
config_file="$default_config_file"
fi
usage() {
cat <<EOF
Usage: $(basename "$0") [options]
Start Lime in a disposable Xephyr session from the current desktop.
-c, --config PATH Load this Lime configuration file.
--no-config Use Lime's starter bindings instead of a config file.
-d, --display NAME Nested display name (default: :99).
-s, --screen SIZE Xephyr size (default: 1600x1000).
-p, --repl-port N Loopback Slynk port (default: 4005).
--session-file PATH Use a specific saved desk (separate from configuration).
--no-session Disable session restore/autosave for a disposable test.
-h, --help Show this help.
Without an explicit config choice, the runner loads
$default_config_file
when it exists; otherwise it uses the starter bindings.
Session data defaults to Lime's XDG state directory, separated by DISPLAY.
EOF
}
while (($#)); do
case "$1" in
-c|--config)
[[ $# -ge 2 ]] || { printf '%s needs a path.\n' "$1" >&2; exit 2; }
config_file="$2"
shift 2
;;
--no-config)
config_file=""
shift
;;
--session-file)
[[ $# -ge 2 ]] || { printf '%s needs a path.\n' "$1" >&2; exit 2; }
session_mode="file"
session_file="$2"
[[ "$session_file" = /* ]] || session_file="$PWD/$session_file"
shift 2
;;
--no-session)
session_mode="disabled"
shift
;;
-d|--display)
[[ $# -ge 2 ]] || { printf '%s needs a display name.\n' "$1" >&2; exit 2; }
display_name="$2"
shift 2
;;
-s|--screen)
[[ $# -ge 2 ]] || { printf '%s needs a size.\n' "$1" >&2; exit 2; }
screen_size="$2"
shift 2
;;
-p|--repl-port)
[[ $# -ge 2 ]] || { printf '%s needs a port number.\n' "$1" >&2; exit 2; }
repl_port="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
printf 'Unknown option: %s\n\n' "$1" >&2
usage >&2
exit 2
;;
esac
done
if ! [[ "$display_name" =~ ^:[0-9]+$ ]]; then
printf '%s\n' "--display must look like :99, got $display_name" >&2
exit 2
fi
display_number="${display_name#:}"
socket_path="/tmp/.X11-unix/X${display_number}"
if [[ -S "$socket_path" ]]; then
printf 'Display %s is already in use (%s). Choose another with --display.\n' \
"$display_name" "$socket_path" >&2
exit 2
fi
if ! [[ "$repl_port" =~ ^[1-9][0-9]*$ ]] || ((repl_port > 65535)); then
printf 'Slynk port must be an integer from 1 through 65535, got %s\n' "$repl_port" >&2
exit 2
fi
if [[ -n "$config_file" ]]; then
if [[ ! -f "$config_file" ]]; then
printf 'Lime config does not exist: %s\n' "$config_file" >&2
exit 2
fi
# Resolve before changing into the repository so an explicit relative path
# still means the path relative to the shell that launched this runner.
config_file="$(cd -- "$(dirname -- "$config_file")" && pwd)/$(basename -- "$config_file")"
fi
command -v Xephyr >/dev/null || {
printf 'Xephyr is required but was not found in PATH.\n' >&2
exit 127
}
command -v sbcl >/dev/null || {
printf 'SBCL is required but was not found in PATH.\n' >&2
exit 127
}
cleanup() {
if kill -0 "$xephyr_pid" 2>/dev/null; then
kill -TERM "$xephyr_pid" 2>/dev/null || true
wait "$xephyr_pid" 2>/dev/null || true
fi
}
Xephyr "$display_name" -ac -screen "$screen_size" -br -nolisten tcp &
xephyr_pid=$!
trap cleanup EXIT INT TERM
for attempt in {1..50}; do
if [[ -S "$socket_path" ]]; then
break
fi
if ! kill -0 "$xephyr_pid" 2>/dev/null; then
wait "$xephyr_pid"
fi
sleep 0.1
done
if [[ ! -S "$socket_path" ]]; then
printf 'Xephyr did not make display %s ready.\n' "$display_name" >&2
exit 1
fi
# DISPLAY is exported only to this script and its children. Your normal dwm
# terminal/session keeps its original display and its existing applications.
export DISPLAY="$display_name"
export LIME_XEPHYR_INTERNAL_CONFIG_FILE="$config_file"
export LIME_XEPHYR_INTERNAL_REPL_PORT="$repl_port"
export LIME_XEPHYR_INTERNAL_SESSION_MODE="$session_mode"
export LIME_XEPHYR_INTERNAL_SESSION_FILE="$session_file"
printf 'Lime test session: Xephyr %s (%s), Slynk 127.0.0.1:%s\n' \
"$display_name" "$screen_size" "$repl_port"
if [[ -n "$config_file" ]]; then
printf 'Configuration: %s\n' "$config_file"
else
printf 'Configuration: starter bindings (no config file)\n'
fi
printf 'Close Lime or press Ctrl-C here to tear down the nested session.\n'
cd "$repo_dir"
sbcl --noinform --non-interactive \
--eval '(require :asdf)' \
--eval '(asdf:load-asd (merge-pathnames "lime.asd" (uiop:getcwd)))' \
--eval '(asdf:load-system "lime/dev")' \
--eval '(lime.dev:run-development-session
:display-name (uiop:getenv "DISPLAY")
:session-file (let ((mode (uiop:getenv "LIME_XEPHYR_INTERNAL_SESSION_MODE")))
(cond ((string= mode "disabled") nil)
((string= mode "file") (uiop:getenv "LIME_XEPHYR_INTERNAL_SESSION_FILE"))
(t :default)))
:config-file (let ((path (uiop:getenv "LIME_XEPHYR_INTERNAL_CONFIG_FILE")))
(and (plusp (length path)) path))
:repl-port (parse-integer (uiop:getenv "LIME_XEPHYR_INTERNAL_REPL_PORT")))'