#!/usr/bin/env bash
# Record the Omairc TUI demo on Omarchy workspace 1 and encode a 1600-wide GIF.
#
# The terminal-client sibling of scripts/record-demo. It mirrors that script —
# same workspace, same fullscreen screenrecord, same evdev chords, same encode,
# same walkthrough — but runs the Go client (omairc-tui). omairc-tui is a
# terminal program with no Wayland toplevel of its own, so the terminal owns the
# window that gets focused and filmed.
#
# The terminal is launched the Omarchy way: through $TERMINAL
# (xdg-terminal-exec, Omarchy's own export), which resolves the user's default
# terminal and its theme. No window-mode flag is passed — Hyprland tiles the
# window, so it keeps Omarchy's gaps and borders instead of being forced
# fullscreen over them.
#
# Usage (from repo root):
#   .cursor/skills/record-omairc-demo/scripts/record-demo-tui
#   .cursor/skills/record-omairc-demo/scripts/record-demo-tui --encode-only /path/to.mp4
#
# Edit drive_walkthrough() to change keys or timings.
# Do not change Hyprland monitor mode/scale or the terminal's font from this script.

set -euo pipefail

SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
SKILL_DIR="$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)"
ROOT="$(CDPATH= cd -- "$SKILL_DIR/../../.." && pwd)"
INJECT=(python3 "$SCRIPT_DIR/inject-evdev.py")

# The terminal owns the Wayland toplevel. Omarchy exports TERMINAL=xdg-terminal-exec,
# which resolves the configured default terminal (foot here) with its Omarchy
# theme. It maps the window under the pid we spawn and forwards the shell's OSC 2
# title, which is what wait_demo_window matches.
BINARY="${OMAIRC_DEMO_BINARY:-$ROOT/tui/bin/omairc-tui}"
TERMINAL="${OMAIRC_DEMO_TERMINAL:-${TERMINAL:-xdg-terminal-exec}}"
# An agent shell (and some CI) exports NO_COLOR / FORCE_COLOR / CLICOLOR to
# suppress color in captured output. Those must never reach the TUI: lipgloss
# honors NO_COLOR and renders the whole client monochrome, while the terminal's
# own themed background still looks correct, so the failure is easy to miss.
# Strip them so the recording matches a terminal the user launched themselves.
COLOR_CLEAN=(env -u NO_COLOR -u FORCE_COLOR -u CLICOLOR -u CLICOLOR_FORCE)
OUTDIR="${OMAIRC_DEMO_OUTDIR:-${TMPDIR:-/tmp}/omairc-demo-tui-rec}"
OUTPUT="${OMAIRC_DEMO_GIF:-$ROOT/omairc-tui.gif}"
RECORD_WORKSPACE="${OMAIRC_DEMO_WORKSPACE:-1}"
RESTORE_WORKSPACE="${OMAIRC_DEMO_RESTORE_WORKSPACE:-2}"
USER_CONFIG="${XDG_CONFIG_HOME:-$HOME/.config}/omairc/omairc.conf"

# Final GIF is 1600-wide (16:9 → 900 tall). Capture stays native.
GIF_WIDTH="${GIF_WIDTH:-1600}"
GIF_HEIGHT="${GIF_HEIGHT:-900}"
GIF_FPS="${GIF_FPS:-15}"
# Flat UI compresses better without dither; rectangle only rewrites changed regions.
GIF_DITHER="${GIF_DITHER:-none}"
GIF_DIFF_MODE="${GIF_DIFF_MODE:-rectangle}"
TYPE_DELAY="${TYPE_DELAY:-0.08}"

SCENARIO="${OMAIRC_DEMO_SCENARIO:-}"
DRY_RUN=0

DEMO_PID=""
RECORDING_STARTED=0
CURSOR_HIDDEN=0
CURSOR_INVISIBLE_ORIG=false

die() {
  echo "record-demo-tui: $*" >&2
  exit 1
}

hl() {
  hyprctl dispatch "$1" >/dev/null
}

require_cmds() {
  local missing=""
  local cmd
  for cmd in "$@"; do
    command -v "$cmd" >/dev/null 2>&1 || missing="$missing $cmd"
  done
  [[ -z "$missing" ]] || die "missing tools:$missing"
}

require_tools() {
  require_cmds ffmpeg hyprctl omarchy python3 "$TERMINAL"
  id -nG | grep -qw input || die "user is not in group input (needed to write /dev/input)"
}

ensure_binary() {
  if [[ ! -x "$BINARY" ]]; then
    echo "record-demo-tui: building $BINARY" >&2
    "$ROOT/tui/bin/build"
  fi
  [[ -x "$BINARY" ]] || die "no demo binary at $BINARY"
  if [[ "$BINARY" == *"/usr/bin/omairc-tui" ]]; then
    die "refusing to drive the installed client"
  fi
}

cursor_invisible_lua() {
  hyprctl eval "hl.config({ cursor = { invisible = $1 } })" >/dev/null
}

hide_cursor() {
  CURSOR_INVISIBLE_ORIG=false
  if hyprctl getoption cursor:invisible -j | grep -q '"bool": *true'; then
    CURSOR_INVISIBLE_ORIG=true
  fi
  cursor_invisible_lua true
  CURSOR_HIDDEN=1
}

show_cursor() {
  [[ "$CURSOR_HIDDEN" -eq 1 ]] || return 0
  cursor_invisible_lua "$CURSOR_INVISIBLE_ORIG" || cursor_invisible_lua false
  CURSOR_HIDDEN=0
}

# Runs from the trap and from the success path, so every step is best-effort:
# a failure here must not skip the pointer unhide or the demo kill.
restore() {
  set +e
  if [[ "$RECORDING_STARTED" -eq 1 ]]; then
    OMARCHY_SCREENRECORD_DIR="$OUTDIR" omarchy screenrecord --stop-recording \
      >"$OUTDIR/stop.txt" 2>&1
    RECORDING_STARTED=0
  fi
  show_cursor
  if [[ -n "${DEMO_PID:-}" ]] && kill -0 "$DEMO_PID" 2>/dev/null; then
    kill "$DEMO_PID" 2>/dev/null
    wait "$DEMO_PID" 2>/dev/null
    DEMO_PID=""
  fi
  hl "hl.dsp.focus({ workspace = \"$RESTORE_WORKSPACE\" })"
  hl 'hl.dsp.focus({ window = "class:cursor" })'
  set -e
}

# Print the title of the window owned by $1, once it maps AND its title contains
# $2. Matching the pid we spawned (not the title) is what keeps us off the
# user's live client. The title wait is what makes a terminal work: foot maps
# immediately under its default title, and the shell's OSC 2 title only arrives
# once omairc-tui has rendered.
wait_demo_window() {
  python3 - "$1" "$2" <<'PY'
import json, subprocess, sys, time

pid = int(sys.argv[1])
needle = sys.argv[2]
deadline = time.time() + 12
while time.time() < deadline:
    clients = json.loads(subprocess.check_output(["hyprctl", "clients", "-j"]))
    for client in clients:
        title = client.get("title") or ""
        if client.get("pid") == pid and needle in title:
            print(title)
            sys.exit(0)
    time.sleep(0.1)
sys.exit(1)
PY
}

focus_demo() {
  hyprctl dispatch "hl.dsp.focus({ window = \"pid:$DEMO_PID\" })" >/dev/null
}

press() {
  focus_demo
  "${INJECT[@]}" chord "$1" >/dev/null
}

type_slow() {
  focus_demo
  "${INJECT[@]}" type --delay "$TYPE_DELAY" "$1" >/dev/null
}

# Edit this function to change the on-screen demo. Every pause is explicit here:
# press and type_slow return as soon as the keys are injected.
drive_walkthrough() {
  sleep 1.2
  press ctrl+slash          # Ctrl+/ shortcuts sheet
  sleep 1.9
  press escape
  sleep 0.55
  press alt+a               # Alt+A next unread lands on #ricing (mention)
  sleep 1.5
  press ctrl+k              # back to #omarchy
  sleep 0.65
  type_slow "omarchy"
  sleep 0.2
  press return
  sleep 1.2
  press ctrl+l              # focus the composer
  sleep 0.3
  type_slow "mi"            # Tab completes the nick, then send
  press tab
  sleep 0.45
  type_slow "optional it is."
  press return
  sleep 1.2
  press ctrl+l
  sleep 0.3
  type_slow "/list"         # /list channel browser overlay
  sleep 0.3
  press return
  sleep 1.6
  type_slow "ran"           # fuzzy filter narrows the table
  sleep 1.0
  press escape
  sleep 0.6
  press ctrl+k              # open the seeded anna direct message
  sleep 0.6
  type_slow "anna"
  sleep 0.2
  press return
  sleep 1.6
  press ctrl+k              # second network: OFTC #lab
  sleep 0.6
  type_slow "lab"
  sleep 0.2
  press return
  sleep 1.5
  press ctrl+shift+o        # Ctrl+Shift+O link sheet (seeded https link)
  sleep 2.0
  press escape
  sleep 0.6
  press ctrl+shift+a        # Ctrl+Shift+A session inbox (seeded invite)
  sleep 2.0
  press escape
  sleep 0.6
  press ctrl+shift+s        # Ctrl+Shift+S collapses the server rail
  sleep 1.4
  press ctrl+shift+s        # and restores it
  sleep 1.4
}

# A --scenario file redefines drive_walkthrough, so one-off clips never have to
# edit (and dirty) the committed default above. It runs in this shell, so it can
# call press, type_slow, and sleep.
load_scenario() {
  [[ -n "$SCENARIO" ]] || return 0
  [[ -f "$SCENARIO" ]] || die "no scenario file at $SCENARIO"
  local before after
  before="$(declare -f drive_walkthrough)"
  # shellcheck source=/dev/null
  source "$SCENARIO"
  after="$(declare -f drive_walkthrough)"
  [[ "$before" != "$after" ]] || die "$SCENARIO did not redefine drive_walkthrough"
  echo "record-demo-tui: scenario $SCENARIO"
}

newest_recording() {
  # Tolerate an empty dir: the caller reports the missing mp4, not pipefail.
  ls -1t "$OUTDIR"/screenrecording-*.mp4 2>/dev/null | head -1 || true
}

encode_gif() {
  local rec="$1"
  local out="$2"
  [[ -f "$rec" ]] || die "no recording at $rec"
  mkdir -p "$(dirname -- "$out")"
  ffmpeg -y -i "$rec" \
    -vf "fps=${GIF_FPS},scale=${GIF_WIDTH}:${GIF_HEIGHT}:flags=lanczos:force_original_aspect_ratio=decrease,pad=${GIF_WIDTH}:${GIF_HEIGHT}:(ow-iw)/2:(oh-ih)/2:color=black,split[s0][s1];[s0]palettegen=stats_mode=full[p];[s1][p]paletteuse=dither=${GIF_DITHER}:diff_mode=${GIF_DIFF_MODE}" \
    -loop 0 "$out"
  echo "record-demo-tui: wrote $out"
  if command -v identify >/dev/null 2>&1; then
    identify "$out" | awk 'NR==1 { print } END { print }'
  fi
  ls -lh "$out"
}

leak_check() {
  [[ -f "$USER_CONFIG" ]] || return 0
  # QSettings keys are "<networkId>\<key>", and IrcDemoServer's seeded network
  # ids are literally "omarchy" and "oftc". The real networks are uuid-keyed.
  if grep -Eq '^(omarchy|oftc)\\' "$USER_CONFIG"; then
    die "demo profiles leaked into $USER_CONFIG"
  fi
  echo "record-demo-tui: config clean"
}

print_monitor() {
  hyprctl monitors | awk '
    /^Monitor / { name = $2 }
    /^[[:space:]]+[0-9]+x[0-9]+@/ { mode = $1 }
    /^[[:space:]]+scale:/ {
      print "record-demo-tui: monitor " name " " mode " scale=" $2 " (unchanged)"
      exit
    }'
}

record_walkthrough() {
  mkdir -p "$OUTDIR"
  print_monitor

  hl "hl.dsp.focus({ workspace = \"$RECORD_WORKSPACE\" })"
  sleep 0.3

  # The terminal owns the toplevel; let Hyprland place and tile it (no window-mode
  # flag, so Omarchy's gaps and borders stay). Unset XDG_CONFIG_HOME so
  # --demo-server uses a temp config, not ~/.config, and strip the shell's
  # color-suppression vars so the TUI renders in color.
  "$TERMINAL" -- "${COLOR_CLEAN[@]}" -u XDG_CONFIG_HOME "$BINARY" --demo-server \
    >"$OUTDIR/omairc-demo.log" 2>&1 &
  DEMO_PID=$!

  local title
  title="$(wait_demo_window "$DEMO_PID" irc.example)" ||
    die "demo window did not appear (see $OUTDIR/omairc-demo.log)"
  [[ "$title" == *irc.example* ]] ||
    die "pid $DEMO_PID is not seeded by --demo-server: $title"
  echo "record-demo-tui: pid=$DEMO_PID title=$title"

  hl "hl.dsp.focus({ workspace = \"$RECORD_WORKSPACE\" })"
  focus_demo

  if [[ "$DRY_RUN" -eq 1 ]]; then
    # Drive the keys so a new scenario can be checked without paying for a
    # capture and encode. Leave the pointer alone: nothing is being filmed.
    echo "record-demo-tui: dry run, no capture and no gif"
    sleep 0.8
    drive_walkthrough
    restore
    trap - EXIT
    leak_check
    echo "record-demo-tui: dry run finished"
    return 0
  fi

  hl 'hl.dsp.cursor.move_to_corner({ corner = "topleft" })' || true
  hide_cursor
  sleep 0.8

  OMARCHY_SCREENRECORD_DIR="$OUTDIR" omarchy screenrecord --fullscreen
  RECORDING_STARTED=1
  sleep 2.4

  drive_walkthrough

  local stop_out="$OUTDIR/stop-path.txt"
  OMARCHY_SCREENRECORD_DIR="$OUTDIR" omarchy screenrecord --stop-recording \
    | tee "$stop_out"
  RECORDING_STARTED=0
  sleep 0.3

  local rec
  rec="$(grep -Eo '/[^[:space:]]+\.mp4' "$stop_out" | tail -1 || true)"
  if [[ -z "$rec" || ! -f "$rec" ]]; then
    rec="$(newest_recording)"
  fi
  [[ -n "$rec" && -f "$rec" ]] || die "screenrecord produced no mp4 in $OUTDIR"

  restore
  trap - EXIT

  leak_check
  echo "record-demo-tui: capture $rec"
  encode_gif "$rec" "$OUTPUT"
}

usage() {
  cat <<EOF
record-demo-tui - workspace-1 Omairc TUI demo GIF (1600x900)

Runs omairc-tui --demo-server in the default Omarchy terminal ($TERMINAL, i.e.
xdg-terminal-exec) on workspace 1. Hyprland tiles the window. The walkthrough
and the encode are identical to record-demo; only the client and the process
that owns the window differ.

Usage:
  record-demo-tui                       record the default walkthrough
  record-demo-tui --scenario <file>     record a custom walkthrough
  record-demo-tui --dry-run             drive the keys, no capture, no gif
  record-demo-tui --encode-only <mp4>   re-encode an existing capture
  record-demo-tui -h

A scenario file redefines drive_walkthrough and is sourced into this script,
so it can call press, type_slow, and sleep:

  drive_walkthrough() {
    sleep 1.2
    press ctrl+k
    sleep 0.5
    type_slow "#desktop"
    press return
    sleep 1.5
  }

Env:
  OMAIRC_DEMO_GIF        output path (default: $ROOT/omairc-tui.gif)
  OMAIRC_DEMO_OUTDIR     capture dir (default: $OUTDIR)
  OMAIRC_DEMO_BINARY     demo binary (default: $ROOT/tui/bin/omairc-tui)
  OMAIRC_DEMO_TERMINAL   launcher (default: $TERMINAL, i.e. xdg-terminal-exec)
  OMAIRC_DEMO_SCENARIO   scenario file (same as --scenario)
  GIF_WIDTH/GIF_HEIGHT   default 1600x900
  GIF_FPS                default 15
  GIF_DITHER             default none (try floyd_steinberg for photo-ish frames)
  GIF_DIFF_MODE          default rectangle
  TYPE_DELAY             seconds per typed character (default 0.08)
  OMAIRC_EVDEV_DEVICE    override keyboard event node
EOF
}

main() {
  local encode_only=""
  while [[ $# -gt 0 ]]; do
    case "$1" in
      -h | --help)
        usage
        exit 0
        ;;
      --encode-only)
        [[ -n "${2:-}" ]] || die "--encode-only needs an mp4 path"
        encode_only="$2"
        shift 2
        ;;
      --scenario)
        [[ -n "${2:-}" ]] || die "--scenario needs a file"
        SCENARIO="$2"
        shift 2
        ;;
      --dry-run)
        DRY_RUN=1
        shift
        ;;
      *)
        die "unknown argument: $1 (try --help)"
        ;;
    esac
  done

  if [[ -n "$encode_only" ]]; then
    require_cmds ffmpeg
    encode_gif "$encode_only" "$OUTPUT"
    exit 0
  fi

  require_tools
  ensure_binary
  load_scenario
  mkdir -p "$OUTDIR"
  trap restore EXIT
  record_walkthrough
}

main "$@"
