#!/bin/sh
# Runs inside the compositor as its single client. Starting explorer is the
# whole job; when explorer exits, the compositor exits and the session ends.
set -eu

SG_LIB="${SG_LIB:-/usr/lib/stained-glass}"
# shellcheck source=lib/sg-common.sh
. "$SG_LIB/sg-common.sh"
sg_wine_env

GEOM="${1:-${SG_DESKTOP_W}x${SG_DESKTOP_H}}"

SG_DRIVER=$(sg_graphics_driver)

# Find the X display, waiting for it if necessary.
#
# cage starts XWayland asynchronously and will happily exec us before it is
# ready, in which case DISPLAY is simply absent from our environment -- and,
# since a process cannot gain environment variables after exec, it will never
# appear there however long we wait on it. So discover the display from the
# socket instead.
#
# This is a real race, not a theoretical one: it stayed hidden on fast machines
# and showed up on a loaded CI runner as a session published with an empty
# DISPLAY, which the gate then could not attach to.
if [ "${SG_DISPLAY_PATH:-x11}" = "x11" ] && [ -z "${DISPLAY:-}" ]; then
    sg_log "DISPLAY unset; waiting for an X server socket"
    waited=0
    while [ -z "${DISPLAY:-}" ] && [ "$waited" -lt "${SG_XWAYLAND_TIMEOUT:-60}" ]; do
        for sock in /tmp/.X11-unix/X*; do
            [ -S "$sock" ] || continue
            DISPLAY=":${sock##*/X}"
            export DISPLAY
            break
        done
        [ -n "${DISPLAY:-}" ] && break
        sleep 1
        waited=$((waited + 1))
    done
    if [ -n "${DISPLAY:-}" ]; then
        sg_log "found X server at $DISPLAY after ${waited}s"
    else
        sg_die "no X server socket appeared within ${SG_XWAYLAND_TIMEOUT:-60}s"
    fi
fi

# The session's displays for services D-Bus starts in the user's manager:
# the portal's screen sharing (xdg-desktop-portal-wlr) captures this
# compositor's output, so it must know where it is (OBS's screen capture).
if [ -n "${WAYLAND_DISPLAY:-}" ]; then
    systemctl --user import-environment WAYLAND_DISPLAY DISPLAY >/dev/null 2>&1 || true
    if command -v dbus-update-activation-environment >/dev/null 2>&1; then
        dbus-update-activation-environment WAYLAND_DISPLAY DISPLAY >/dev/null 2>&1 || true
    fi
fi

# Linux programs' windows go through Xwayland, where the compositor places
# them, gives them title bars, Minimize and taskbar buttons, and honours what
# client-drawn title bars ask (GTK 4's). Natively on Wayland, GTK 4 and Qt
# windows came up at the screen's corner with none of that (zenity). Set for
# everything started from here -- the Start menu's Linux apps, SG Store's
# Run among it -- and again for the user's systemd manager and D-Bus: the
# compositor (wlroots) has just set XDG_SESSION_TYPE=wayland for everything
# it starts, and Electron 39+ goes by that alone (sg-common.sh).
sg_linux_app_env

# The resolution and scale the user chose in Settings > Display, per monitor
# (sg-settingsctl keeps them): put back before the desktop is measured, so it
# comes up at that size -- a mode set in Settings was gone after a reboot.
if command -v sg-settingsctl >/dev/null 2>&1 && [ -n "${WAYLAND_DISPLAY:-}" ]; then
    if restored=$(sg-settingsctl display restore 2>/dev/null) && echo "$restored" | grep -q '^RESTORED'; then
        sg_log "display restored: $(echo "$restored" | grep '^RESTORED' | tr '\t\n' ' ')"
        sleep 1   # Xwayland takes the output's new size a moment later
    fi
fi

# The desktop is the monitor's size. A fixed default (SG_DESKTOP_W x _H) either
# leaves a larger screen unfilled or asks Wine for a mode the output cannot
# show -- and when that mode is refused, a fresh prefix keeps Wine's 1024x768
# default and lays the taskbar out for it. The X root window is the
# compositor's output, so measure it. SG_DESKTOP_AUTO=0 keeps the configured
# size for administrators who want a fixed one.
if [ "${SG_DESKTOP_AUTO:-1}" != 0 ] && [ "${SG_DISPLAY_PATH:-x11}" = "x11" ] && command -v xwininfo >/dev/null 2>&1; then
    root=""
    tries=0
    while [ -z "$root" ] && [ "$tries" -lt 20 ]; do
        root=$(xwininfo -root 2>/dev/null | awk '/^ *Width:/ {w=$2} /^ *Height:/ {h=$2} END {if (w && h) print w "x" h}')
        [ -n "$root" ] || { sleep 0.5; tries=$((tries + 1)); }
    done
    if [ -n "$root" ]; then
        [ "$root" != "$GEOM" ] && sg_log "desktop sized to the output: $root (configured $GEOM)"
        GEOM=$root
    else
        sg_log "could not measure the output; using the configured $GEOM"
    fi
fi

# Publish where the session lives, so a check running over ssh can attach to it
# without having to guess. Without this the gate cannot find the display.
mkdir -p "$SG_STATE"
cat > "$(sg_session_env)" <<ENV
DISPLAY=${DISPLAY:-}
WAYLAND_DISPLAY=${WAYLAND_DISPLAY:-}
XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR:-}
WINEPREFIX=$WINEPREFIX
SG_DISPLAY_PATH=${SG_DISPLAY_PATH:-x11}
SG_DESKTOP_GEOMETRY=$GEOM
ENV

# The shell is the interactive session and must be on WinSta0. Unset rather
# than assume: this process may have inherited session 0's station from a
# service manager that started the session.
unset SG_WINSTATION

sg_log "explorer starting: driver=$SG_DRIVER desktop=shell,$GEOM display=${DISPLAY:-none}"

# These writes decide which desktop every later process joins, so they must
# actually land -- and that turns out to be the whole game. On a shared system
# prefix the wineserver may not be reachable yet when we run: the first `wine`
# then fails, and the desktop still comes up because `explorer /desktop=shell`
# below starts the server itself. The result is a working shell whose
# HKCU\...\Explorer\Desktop was never written, so `wine notepad` reads no
# desktop, lands on an empty "Default", finds no graphics driver there, and
# dies with "no driver could be loaded" -- while the shell looks fine. It cost
# a day to see, because the desktop working hides that the writes did not.
#
# So wait for the server to answer, then write, then confirm. A desktop we
# cannot associate apps with is not a usable session, so this is fatal, not
# best-effort.
reg_set() {
    _key=$1 _val=$2 _data=$3 _i=0
    while [ "$_i" -lt "${SG_REG_TIMEOUT:-30}" ]; do
        if wine reg add "$_key" /v "$_val" /t REG_SZ /d "$_data" /f >/dev/null 2>&1 &&
           wine reg query "$_key" /v "$_val" >/dev/null 2>&1; then
            return 0
        fi
        sleep 1
        _i=$((_i + 1))
    done
    sg_die "could not set $_key\\$_val after ${SG_REG_TIMEOUT:-30}s: the wineserver never became writable"
}

# The graphics driver is machine-wide, in HKLM, set by sg-prefix-init as SYSTEM
# (multi-user debt D4); a user's session no longer writes its own copy.
# The virtual desktop as the default for every process in this prefix: without
# it an app opens its own toplevel outside the desktop, which under a
# fullscreen compositor covers the desktop and taskbar entirely.
reg_set 'HKCU\Software\Wine\Explorer' Desktop  shell
reg_set 'HKCU\Software\Wine\Explorer\Desktops' shell "$GEOM"

# The display scale: the recommended one for this screen unless the user
# picked one (sg-common.sh sg_auto_scale; David 2026-10-05, a Surface Pro 7
# at 2736x1824: "everything is tiny"). Set before the shell starts; Windows
# programs follow a later change too (wine-sg 0890). Linux programs are told
# by the XSETTINGS manager started here and the X resources, and follow a
# change while they run (sg_linux_scale_env; Settings runs
# sg-display-scale). At 1080p and below it is 100%, as before.
SG_SCALE=$(sg_auto_scale "${GEOM%x*}" "${GEOM#*x}")
export SG_SCALE
sg_linux_scale_env "$SG_SCALE"

# The kiosk app (Settings > Accounts > Kiosk): its account's taskbar hides
# itself at the bottom edge, set before the shell reads it -- and the
# person's own setting comes back once there is no kiosk app (sg-kiosk).
if [ -x "$SG_LIB/sg-kiosk" ] && [ "$("$SG_LIB/sg-kiosk" prepare 2>/dev/null)" = kiosk ]; then
    sg_log "kiosk app on for $(id -un): the taskbar hides itself"
fi

# The work area for Linux programs: the screen less the taskbar (sg-common.sh).
[ "${SG_DISPLAY_PATH:-x11}" = "x11" ] && sg_x11_workarea "${GEOM%x*}" "${GEOM#*x}"
# and root's Linux admin tools (pkexec) may show on it, and XAUTHORITY
# names a file as Linux launchers expect (sg-common.sh)
sg_x_admit_root
sg_x_authority

# Microsoft Office draws in software unless the user chose otherwise: its
# hardware drawing shares Direct3D textures the Vulkan layer cannot share
# under Wine, and every Office window stayed empty. New users have this from
# the Default User profile (sg-shell defaults/88-sg-office.reg); an account
# made before it gets it here, once -- a value already there is the user's.
_offgfx='HKCU\Software\Microsoft\Office\16.0\Common\Graphics'
wine reg query "$_offgfx" /v DisableHardwareAcceleration >/dev/null 2>&1 ||
    wine reg add "$_offgfx" /v DisableHardwareAcceleration /t REG_DWORD /d 1 /f >/dev/null 2>&1 ||
    sg_log "could not set Office's software drawing"

# A domain user's registry policy (Group Policy, recorded at sign-in by
# sg-domain-logon) goes into HKCU before the shell starts, as Windows applies
# it before the desktop. reg.exe, a console program: it opens no desktop.
_netdir="/run/stained-glass-net/drives/$(id -u)"
if [ -r "$_netdir/volatile.reg" ]; then
    wine reg import "Z:$_netdir/volatile.reg" >/dev/null 2>&1 || sg_log "the domain logon variables were not set"
fi
# The user's Group Policy (HKCU), with de-tattoo: a value removed from the
# user's GPOs is deleted, so a restriction lifted in the directory lifts here
# at the next login. State persists in the user's own home (HKCU is theirs).
_upstate="${HOME:-$SG_STATE}/.local/state/stained-glass/user-policy.applied"
if [ -r "$_netdir/user-policy.reg" ] || [ -f "$_upstate" ]; then
    sg_log "applying the user's Group Policy registry settings"
    sg_apply_user_policy "$_netdir/user-policy.reg" "$_upstate"
fi

# The first-run setup's choices (region, keyboard layouts, privacy), applied to
# this user once, before the shell: at their first sign-in.
if [ -r "${SG_OOBE_CONF:-/etc/stained-glass/oobe.conf}" ] && [ -x "$SG_LIB/sg-oobe-user" ]; then
    "$SG_LIB/sg-oobe-user" || sg_log "the first-run setup's choices were not applied"
fi

# The session's working directory is the user's profile, C:\users\<name>:
# what the shell and Start run without a directory of their own (cmd,
# PowerShell, the Run box) starts there, as on Windows. Left at the Unix home
# it showed as Z:\home\<name>.
_profile="$WINEPREFIX/drive_c/users/$(id -un)"
if [ -d "$_profile" ]; then cd "$_profile"; else sg_log "no profile directory at $_profile"; fi

# What starts alongside the shell waits until the shell owns the desktop.
# A GUI program started first makes Wine create the desktop itself (a bare
# `explorer /desktop`); the shell's own explorer then found it taken, exited
# 0 at once, and the supervisor below took that for a sign-out -- the desktop
# stayed up, but with nothing watching it.
#   - the network icon in the notification area (sg-netflyout), if sg-shell
#     and sg-netctl are installed.
#   - the battery icon (sg-battery), on a computer with a battery.
#   - the volume icon (sg-volume), if sg-settingsctl can reach the sound
#     server for it.
#   - the notification centre's icon (sg-notify): the toasts and balloons
#     wine-sg keeps, listed in its panel (its icon, Win+A).
#   - Linux programs' tray icons (sg-sni-bridge, with sg-shell's sg-sni64.exe):
#     the session's StatusNotifierWatcher; Flameshot's, KeePassXC's and
#     Nextcloud's icons appear beside ours.
#   - voice typing's hold-to-talk listener (sg-dictate /background), if
#     sg-shell and sg-dictate are installed; it exits at once unless the user
#     turned hold-to-talk on (Control Panel > Speech Recognition).
#   - the desktop's compositor (sg-deskcomp, sg_start_deskcomp): window
#     shadows, real alpha and the window effects, if sg-compositor has it.
#   - in a virtual machine with SPICE, its clipboard agent (spice-vdagent,
#     sg_start_vdagent): copy and paste with the host.
#   - the Start menu, if sg-shell is installed: a persistent listener that
#     explorer's Start button toggles (wine-sg patch 0012). Best-effort: a
#     session without it still has Wine's built-in Start menu. Its output
#     goes where the session's does -- the journal, as this user (debt D11).
#   - Settings' night light and idle timers (sg-settingsctl session-start).
#   - a domain user's logon scripts (the account's scriptPath from NETLOGON,
#     then the GPOs'), one after another, hidden; Windows does not wait for
#     them either.
_sgstart="${SG_SHELL_DIR:-/usr/libexec/stained-glass/shell}/sg-start64.exe"
_sgnet="${SG_SHELL_DIR:-/usr/libexec/stained-glass/shell}/sg-netflyout64.exe"
_sgdictate="${SG_SHELL_DIR:-/usr/libexec/stained-glass/shell}/sg-dictate64.exe"
_sgbattery="${SG_SHELL_DIR:-/usr/libexec/stained-glass/shell}/sg-battery64.exe"
_sgcalendar="${SG_SHELL_DIR:-/usr/libexec/stained-glass/shell}/sg-calendar64.exe"
_sgvolume="${SG_SHELL_DIR:-/usr/libexec/stained-glass/shell}/sg-volume64.exe"
_sgnotify="${SG_SHELL_DIR:-/usr/libexec/stained-glass/shell}/sg-notify64.exe"
_sgsni="${SG_SHELL_DIR:-/usr/libexec/stained-glass/shell}/sg-sni64.exe"
_sgshelldir="${SG_SHELL_DIR:-/usr/libexec/stained-glass/shell}"
# The shell's helpers, kept running while the desktop is there (sg_keep_running):
# started now, and again within seconds of one dying or the shell's restart --
# at most SG_HELPER_RESTARTS times each, so one that cannot run is let be.
sg_helper() {   # NAME COUNTVAR CMD...
    _hn=$1 _hv=$2
    shift 2
    _hc=0
    eval "_hc=\${$_hv:-0}"
    [ "$_hc" -le "${SG_HELPER_RESTARTS:-10}" ] || return 0
    if ! sg_keep_running "$_hn" "$@"; then
        eval "$_hv=$((_hc + 1))"
        [ "$_hc" -gt 0 ] && sg_log "$_hn was not running: started again"
    fi
    return 0
}
sg_shell_helpers() {
    (
        # a loop for the whole session: a check that fails (pkill finding
        # nothing, a test) must not end it, as set -e would
        set +e
        # the whole session (this script's), through the shell's restarts:
        # acting while the desktop is there
        _shell=""
        while kill -0 "$$" 2>/dev/null; do
            _desk=$(xwininfo -root -tree 2>/dev/null | awk '/"shell - Wine Desktop"/ { print $1; exit }')
            if [ -z "$_desk" ]; then
                sleep 1
                continue
            fi
            # not while the machine's wineserver is away (restarting): a
            # program started then gets a wineserver of the user's own, and
            # the machine's could not start again beside it
            if id "${SG_SYSTEM_USER:-sgsystem}" >/dev/null 2>&1 &&
               ! pgrep -u "${SG_SYSTEM_USER:-sgsystem}" -x wineserver >/dev/null 2>&1; then
                sleep 1
                continue
            fi
            # the shell is the desktop window's process (_NET_WM_PID): its
            # window id can be the same after a restart, the X server giving
            # the dead one's ids again
            _now=$(xprop -id "$_desk" _NET_WM_PID 2>/dev/null | sed -n 's/.* = //p')
            if [ -n "$_shell" ] && [ -n "$_now" ] && [ "$_now" != "$_shell" ]; then
                # the shell came back: what was running still talks to the
                # dead one (Start opened from its old taskbar, the icons sat
                # in the old tray, sg-deskcomp drew into the old desktop) --
                # all started again, for the new shell
                sg_log "the shell came back: its helpers started again for it"
                for _h in sg-deskcomp sg-start64.exe sg-netflyout64.exe sg-battery64.exe sg-volume64.exe sg-notify64.exe sg-dictate64.exe sg-sni-bridge sg-sni64.exe; do
                    pkill -u "$(id -u)" -x "$(printf '%.15s' "$_h")" 2>/dev/null
                done
                sleep 1
            fi
            [ -n "$_now" ] && _shell=$_now
            if [ "${SG_DESKCOMP:-1}" != 0 ] && [ -x "${SG_LIBEXEC:-/usr/libexec/stained-glass}/sg-deskcomp" ]; then
                sg_helper sg-deskcomp _n_dc "${SG_LIBEXEC:-/usr/libexec/stained-glass}/sg-deskcomp"
            fi
            [ -f "$_sgstart" ] && sg_helper sg-start64.exe _n_start wine "$_sgstart"
            # the network icon re-launches itself through sg-netctl's bridge
            if [ -f "$_sgnet" ] && command -v sg-netctl >/dev/null 2>&1; then
                sg_helper sg-netflyout64.exe _n_net wine "$_sgnet"
            fi
            if [ -f "$_sgbattery" ] && grep -qx Battery /sys/class/power_supply/*/type 2>/dev/null; then
                sg_helper sg-battery64.exe _n_bat wine "$_sgbattery"
            fi
            if [ -f "$_sgvolume" ] && command -v sg-settingsctl >/dev/null 2>&1; then
                sg_helper sg-volume64.exe _n_vol wine "$_sgvolume"
            fi
            [ -f "$_sgnotify" ] && sg_helper sg-notify64.exe _n_ntf wine "$_sgnotify"
            # the taskbar clock's calendar, hidden until the clock is clicked:
            # running already, it opens at once (David 2026-10-28)
            [ -f "$_sgcalendar" ] && sg_helper sg-calendar64.exe _n_cal wine "$_sgcalendar" --background
            # the session's polkit agent (sg-session-start starts it): gone,
            # a Linux program that asks for an administrator (GSmartControl,
            # GParted) was refused without a prompt until the next sign-in
            if [ -x "${SG_LIBEXEC:-/usr/libexec/stained-glass}/sg-polkit-agent" ]; then
                sg_helper sg-polkit-agent _n_pk "${SG_LIBEXEC:-/usr/libexec/stained-glass}/sg-polkit-agent"
            fi
            if [ -f "$_sgsni" ] && [ -x "$SG_LIB/sg-sni-bridge" ]; then
                SG_SHELL_DIR="$_sgshelldir" sg_helper sg-sni-bridge _n_sni "$SG_LIB/sg-sni-bridge"
            fi
            if [ -f "$_sgdictate" ] && command -v sg-dictate >/dev/null 2>&1 && sg_dictate_wanted; then
                sg_helper sg-dictate64.exe _n_dic wine "$_sgdictate" /background
            fi
            # what SG Defender quarantined from this person's files: shown once
            [ -f "$_sgshelldir/sg-defender-notice64.exe" ] && sh "$SG_LIB/sg-defender-notify"
            # an update that needs a restart (/run/reboot-required): once a boot
            [ -f "$_sgshelldir/sg-restart-notice64.exe" ] && sh "$SG_LIB/sg-restart-notify"
            sleep "${SG_HELPER_INTERVAL:-5}"
        done
    ) &
}
(
    _w=0
    while [ "$_w" -lt 120 ] && ! xwininfo -root -tree 2>/dev/null | grep -q '"shell - Wine Desktop"'; do
        sleep 0.5; _w=$((_w + 1))
    done
    sg_start_vdagent
    sg_shell_helpers
    # the kiosk app, opened full screen and again whenever it closes, for as
    # long as this session lasts (sg-kiosk; nothing unless this account has one)
    if [ -x "$SG_LIB/sg-kiosk" ]; then
        SG_KIOSK_PARENT=$$ "$SG_LIB/sg-kiosk" run </dev/null &
    fi
    # Settings' night light and screen/sleep timers (sg-settingsctl): the
    # user's choices, applied again for this session.
    if command -v sg-settingsctl >/dev/null 2>&1; then
        sg-settingsctl session-start >/dev/null 2>&1 || sg_log "sg-settingsctl session-start failed"
    fi
    if [ -r "$_netdir/logon-script" ]; then
        cd / && while IFS= read -r _script; do
            [ -n "$_script" ] || continue
            sg_log "running the logon script $_script"
            wine cmd.exe /c "$_script" </dev/null
        done < "$_netdir/logon-script"
    fi
) &

# The desktop follows the output after it starts, too (sg_desktop_follow).
if [ "${SG_DESKTOP_AUTO:-1}" != 0 ] && [ "${SG_DISPLAY_PATH:-x11}" = "x11" ] && command -v xwininfo >/dev/null 2>&1; then
    sg_desktop_follow "$GEOM" $$ &
fi

# The shell: a single virtual-desktop window named "shell" at the output size.
#
# Supervised, not exec'd (multi-user debt D7). This script is the compositor's
# only client, so while it lives the session lives. On Windows the shell is a
# process that can die and be restarted without ending the session
# (AutoRestartShell); here too:
#   - explorer exits 0 when the session is ending -- sign-out goes through
#     wineboot --end-session, which closes the desktop window, and the desktop
#     posts WM_QUIT and exits cleanly -- so a clean exit ends the session;
#   - a crash, a kill or taskkill exits non-zero, and the shell is restarted.
# A shell that keeps dying is not restarted forever: more than
# SG_SHELL_MAX_RESTARTS abnormal exits inside SG_SHELL_RESTART_WINDOW seconds
# ends the session, with a log line saying why.
sg_supervise_shell "$GEOM"
