#!/usr/bin/env bash set -u readonly REPO="/home/Rbanh/Documents/Bevy" readonly TARGET_DIR="$REPO/target" readonly EDITOR_BINARY="$TARGET_DIR/debug/editor" readonly LAUNCHER_BINARY="$TARGET_DIR/debug/project_launcher" readonly STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/blacksite-editor" readonly LOG_FILE="$STATE_DIR/launch.log" readonly STATUS_FILE="$STATE_DIR/splash.status" readonly SPLASH="$REPO/scripts/editor/splash.py" readonly BUILD_PROGRESS="$REPO/scripts/editor/build_progress.py" readonly BUILD_STORAGE="$REPO/scripts/codex/build_storage.py" build_only=false project_browser=false gpu_validation=0 sdr_fallback=false splash_pid="" keep_splash=false case "${1:-}" in --build-only) build_only=true ;; --launcher) project_browser=true ;; --gpu-validation) gpu_validation=1 ;; --sdr) sdr_fallback=true ;; "") ;; *) printf 'Unknown option: %s\n' "$1" >&2 exit 2 ;; esac if $project_browser; then binary="$LAUNCHER_BINARY" process_name="project_launcher" service_description="Blacksite Project Browser" else binary="$EDITOR_BINARY" process_name="editor" service_description="Blacksite Editor" fi if ! $build_only && [[ "${BLACKSITE_EDITOR_SERVICE:-0}" != 1 ]]; then service_environment=(--setenv=BLACKSITE_EDITOR_SERVICE=1 --setenv=PATH) for variable in \ WAYLAND_DISPLAY DISPLAY XAUTHORITY XDG_RUNTIME_DIR \ HYPRLAND_INSTANCE_SIGNATURE DBUS_SESSION_BUS_ADDRESS \ XDG_CURRENT_DESKTOP XDG_SESSION_DESKTOP XDG_SESSION_TYPE \ XDG_DATA_HOME XDG_CONFIG_HOME XDG_STATE_HOME GDK_BACKEND; do if [[ -v "$variable" ]]; then service_environment+=(--setenv="$variable") fi done launcher_arguments=() [[ $# -gt 0 ]] && launcher_arguments+=("$1") exec systemd-run --user --collect --quiet --service-type=exec \ --description="$service_description" \ "${service_environment[@]}" \ "$0" "${launcher_arguments[@]}" fi write_status() { local phase="$1" local detail="$2" local progress="$3" local state="${4:-running}" local crate_count="${5:-}" printf '%s\n%s\n%s\n%s\n%s\n' \ "$phase" "$detail" "$progress" "$state" "$crate_count" >"$STATUS_FILE.tmp" mv "$STATUS_FILE.tmp" "$STATUS_FILE" } close_splash() { if [[ -n "$splash_pid" ]] && ! $keep_splash; then write_status "Editor ready" "Opening workspace" 100 close fi } trap close_splash EXIT find_existing() { local pid executable while read -r pid; do [[ -n "$pid" ]] || continue executable="$(readlink -f "/proc/$pid/exe" 2>/dev/null || true)" if [[ "$executable" == "$binary" ]]; then printf '%s\n' "$pid" return 0 fi done < <(pgrep -x "$process_name" 2>/dev/null || true) return 1 } focus_process() { local pid="$1" if command -v hyprctl >/dev/null 2>&1; then hyprctl dispatch focuswindow "pid:$pid" >/dev/null 2>&1 || true fi } running_binary_is_current() { local pid="$1" local running_identity current_identity running_identity="$(stat -Lc '%d:%i' "/proc/$pid/exe" 2>/dev/null || true)" current_identity="$(stat -Lc '%d:%i' "$binary" 2>/dev/null || true)" [[ -n "$running_identity" && "$running_identity" == "$current_identity" ]] } build_current_editor() { write_status "Checking build storage" "Validating the managed development lane" 6 if ! python3 "$BUILD_STORAGE" enforce --phase pre >>"$LOG_FILE" 2>&1; then write_status "Build storage limit reached" \ "Open the launch log for the checked storage report" 6 error return 1 fi python3 "$BUILD_PROGRESS" "$STATUS_FILE" "$LOG_FILE" local build_result=$? if ! python3 "$BUILD_STORAGE" enforce --phase post >>"$LOG_FILE" 2>&1; then printf '%s\n' \ "warning: build-storage postflight failed; see the report above" \ >>"$LOG_FILE" fi return "$build_result" } mkdir -p "$STATE_DIR" exec 9>"$STATE_DIR/build.lock" if ! flock -n 9; then exit 0 fi : >"$LOG_FILE" write_status "Preparing workspace" "Reading project configuration" 5 if ! $build_only; then GSK_RENDERER=gl python3 "$SPLASH" "$STATUS_FILE" "$LOG_FILE" & splash_pid=$! fi if ! cd "$REPO"; then write_status "Project unavailable" "$REPO could not be opened" 100 error keep_splash=true exit 1 fi # Capture the process before Cargo may atomically replace the on-disk executable. A launcher click # always checks/builds source updates first; it never silently focuses a stale process and makes the # user believe the running editor already contains the rebuilt UI. existing_pid="" if ! $build_only; then existing_pid="$(find_existing || true)" fi if ! build_current_editor; then if $build_only; then tail -n 120 "$LOG_FILE" >&2 else keep_splash=true fi exit 1 fi if [[ -n "$existing_pid" ]] && kill -0 "$existing_pid" 2>/dev/null; then focus_process "$existing_pid" if running_binary_is_current "$existing_pid"; then write_status "Editor already open" "Focused the current development build" 100 close else write_status "Update ready" "Close and reopen the editor to activate it" 100 close if command -v notify-send >/dev/null 2>&1; then notify-send --app-name="Blacksite Editor" \ "Blacksite update ready" \ "Close the running editor normally, then reopen it to activate the rebuilt UI." \ >/dev/null 2>&1 || true fi fi # The status above already asked the splash process to close. Prevent the EXIT trap from # replacing the meaningful "already open" or "update ready" result with a startup message. splash_pid="" exit 0 fi if $build_only; then printf 'Blacksite editor and project browser are current.\n' exit 0 fi if [[ ! -x "$binary" ]]; then write_status "Editor build unavailable" \ "The managed build completed without the requested binary" 100 error keep_splash=true exit 1 fi export RUST_BACKTRACE=1 export WGPU_VALIDATION="$gpu_validation" if $sdr_fallback; then export BEVY_FPS_HDR=0 fi export LD_LIBRARY_PATH="$TARGET_DIR/debug/deps${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" write_status "Starting editor" "Opening the current development build" 82 watch_editor_window() { local editor_pid="$1" trap - EXIT if command -v hyprctl >/dev/null 2>&1 && command -v jq >/dev/null 2>&1; then for ((attempt = 0; attempt < 300; attempt++)); do if ! kill -0 "$editor_pid" 2>/dev/null; then write_status "Editor stopped during startup" \ "Open the launch log for runtime details" 100 error return fi if hyprctl clients -j 2>/dev/null | jq -e --argjson pid "$editor_pid" \ 'any(.[]; .pid == $pid and .mapped == true)' >/dev/null; then write_status "Editor ready" "Opening workspace" 100 sleep 0.4 write_status "Editor ready" "Opening workspace" 100 close return fi if ((attempt == 10)); then write_status "Initializing renderer" "Connecting to the graphics device" 88 elif ((attempt == 35)); then write_status "Loading editor scene" "Preparing the workspace" 94 fi sleep 0.1 done else write_status "Loading editor scene" "Preparing the workspace" 94 sleep 5 if kill -0 "$editor_pid" 2>/dev/null; then write_status "Editor ready" "Opening workspace" 100 sleep 0.4 write_status "Editor ready" "Opening workspace" 100 close return fi fi write_status "Editor is taking longer than expected" \ "The process is still running; check the launch log" 100 error } # The lock protects the managed build and startup handoff, not the editor runtime. Leaving this # descriptor inherited across `exec` prevents later build-only updates for as long as the editor is # open. flock -u 9 exec 9>&- watch_editor_window "$$" & exec "$binary" >>"$LOG_FILE" 2>&1 write_status "Editor could not be started" \ "Open the launch log for runtime details" 100 error keep_splash=true exit 1