Skip to content

GNUstep windowmanager

Joe Maloney edited this page Sep 17, 2026 · 3 revisions

This is a rework of gershwin-windowmanager based on XCBKIT to get theme info dynamically

telegram-cloud-photo-size-1-4997275246280772816-y telegram-cloud-photo-size-1-4997275246280772815-y

To test the POC:

git clone https://github.com/pkgdemon/gnustep-developer/
./Library/Scripts/checkout.sh
./Library/Scripts/bootstrap.sh
make install

Optionally use DirectoryServices

sudo dscli init

Login as admin after reboot

Custom .xinitrc script in /Local/Users/admin/.xinitrc

admin@incubator:~$ cat .xinitrc
. /System/Library/Makefiles/GNUstep.sh
exec gnustep-session

Custom session script in /Local/Library/Tools/gnustep-session

#!/bin/sh
#
# gnustep-session - start and supervise the GNUstep desktop session
#
#   gnustep-session            start the session (run from ~/.xinitrc)
#   gnustep-session --logout   gracefully end the running session
#
# Startup order: WindowManager -> DockWM -> GWorkspace. Each step polls
# until the previous component is up (or a timeout passes) before moving on.
# WindowManager and DockWM are restarted if they crash. The session ends
# when GWorkspace quits or on --logout/SIGTERM/SIGHUP/SIGINT, shutting down
# GWorkspace, then DockWM, then WindowManager.

. /System/Library/Makefiles/GNUstep.sh

WM_APP=/System/Library/CoreServices/Applications/WindowManager.app
DOCK_APP=DockWM
SHELL_APP=GWorkspace

READY_TIMEOUT=15      # seconds to wait for each component to come up
STOP_TIMEOUT=5        # seconds to wait for a component to exit after SIGTERM
MAX_RESTARTS=5        # crash restarts allowed per component per RESTART_WINDOW
RESTART_WINDOW=60

LOG_DIR="$HOME/Library/Logs"
RUN_DIR="${XDG_RUNTIME_DIR:-$HOME/.cache}"
PID_FILE="$RUN_DIR/gnustep-session$(echo "${DISPLAY:-:0}" | tr ':/' '__').pid"

log() {
  echo "$(date '+%Y-%m-%d %H:%M:%S') gnustep-session: $*" >> "$LOG_DIR/session.log"
}

alive() {
  [ -n "$1" ] && kill -0 "$1" 2>/dev/null
}

# launch <name> <app>: start app in background, logging to <name>.log; sets LAUNCHED_PID
launch() {
  openapp "$2" >> "$LOG_DIR/$1.log" 2>&1 &
  LAUNCHED_PID=$!
  log "started $1 (pid $LAUNCHED_PID)"
}

wm_ready() {
  xprop -root _NET_SUPPORTING_WM_CHECK 2>/dev/null | grep -q "window id"
}

pid_has_window() {
  xdotool search --onlyvisible --pid "$1" >/dev/null 2>&1
}

# wait_until <pid> <check-command...>: poll until check succeeds, pid dies, or timeout
wait_until() {
  _pid=$1; shift
  _i=0
  while [ $_i -lt $((READY_TIMEOUT * 4)) ]; do
    alive "$_pid" || return 1
    "$@" && return 0
    sleep 0.25
    _i=$((_i + 1))
  done
  return 2
}

# stop <name> <pid>: SIGTERM, poll for exit, then SIGKILL
stop() {
  alive "$2" || return 0
  log "stopping $1 (pid $2)"
  kill -TERM "$2" 2>/dev/null
  _i=0
  while alive "$2" && [ $_i -lt $((STOP_TIMEOUT * 4)) ]; do
    sleep 0.25
    _i=$((_i + 1))
  done
  if alive "$2"; then
    log "$1 did not exit, killing"
    kill -KILL "$2" 2>/dev/null
  fi
  wait "$2" 2>/dev/null
}

if [ "$1" = "--logout" ]; then
  if [ -f "$PID_FILE" ] && alive "$(cat "$PID_FILE")"; then
    kill -TERM "$(cat "$PID_FILE")"
    exit 0
  fi
  echo "gnustep-session: no running session for display ${DISPLAY:-:0}" >&2
  exit 1
fi

mkdir -p "$LOG_DIR" "$RUN_DIR"
if [ -f "$PID_FILE" ] && alive "$(cat "$PID_FILE")"; then
  echo "gnustep-session: session already running (pid $(cat "$PID_FILE"))" >&2
  exit 1
fi
echo $$ > "$PID_FILE"
log "session starting on display $DISPLAY"

WM_PID=; DOCK_PID=; SHELL_PID=
SHUTTING_DOWN=0

shutdown() {
  [ $SHUTTING_DOWN -eq 1 ] && return
  SHUTTING_DOWN=1
  log "session ending ($1)"
  stop "$SHELL_APP" "$SHELL_PID"
  stop DockWM "$DOCK_PID"
  stop WindowManager "$WM_PID"
  rm -f "$PID_FILE"
  log "session ended"
  exit 0
}
trap 'shutdown SIGTERM' TERM
trap 'shutdown SIGHUP' HUP
trap 'shutdown SIGINT' INT

start_wm() {
  launch WindowManager "$WM_APP"; WM_PID=$LAUNCHED_PID
  wait_until "$WM_PID" wm_ready
  case $? in
    0) log "WindowManager ready" ;;
    1) log "WindowManager exited during startup" ;;
    *) log "WindowManager not ready after ${READY_TIMEOUT}s, continuing" ;;
  esac
}

start_dock() {
  launch DockWM "$DOCK_APP"; DOCK_PID=$LAUNCHED_PID
  wait_until "$DOCK_PID" pid_has_window "$DOCK_PID"
  case $? in
    0) log "DockWM ready" ;;
    1) log "DockWM exited during startup" ;;
    *) log "DockWM window not seen after ${READY_TIMEOUT}s, continuing" ;;
  esac
}

start_wm
start_dock
launch "$SHELL_APP" "$SHELL_APP"; SHELL_PID=$LAUNCHED_PID

WM_RESTARTS=0; WM_WINDOW_START=$(date +%s)
DOCK_RESTARTS=0; DOCK_WINDOW_START=$(date +%s)

# may_restart <count-var> <window-start-var>: rate-limit crash restarts
may_restart() {
  _now=$(date +%s)
  eval "_start=\$$2; _count=\$$1"
  if [ $((_now - _start)) -gt $RESTART_WINDOW ]; then
    _start=$_now; _count=0
  fi
  _count=$((_count + 1))
  eval "$1=$_count; $2=$_start"
  [ $_count -le $MAX_RESTARTS ]
}

# Supervise. Sleep in the background so signals interrupt the wait promptly.
while :; do
  if ! alive "$SHELL_PID"; then
    wait "$SHELL_PID" 2>/dev/null
    shutdown "$SHELL_APP exited"
  fi
  if [ -n "$WM_PID" ] && ! alive "$WM_PID"; then
    wait "$WM_PID" 2>/dev/null
    if may_restart WM_RESTARTS WM_WINDOW_START; then
      log "WindowManager exited unexpectedly, restarting"
      start_wm
    else
      log "WindowManager crashed $MAX_RESTARTS times in ${RESTART_WINDOW}s, not restarting"
      WM_PID=
    fi
  fi
  if [ -n "$DOCK_PID" ] && ! alive "$DOCK_PID"; then
    wait "$DOCK_PID" 2>/dev/null
    if may_restart DOCK_RESTARTS DOCK_WINDOW_START; then
      log "DockWM exited unexpectedly, restarting"
      start_dock
    else
      log "DockWM crashed $MAX_RESTARTS times in ${RESTART_WINDOW}s, not restarting"
      DOCK_PID=
    fi
  fi
  sleep 1 &
  wait $!
done

Clone this wiki locally