Ctrl K
ComunidadGuías…

scripts/demo-app-gif.sh

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env bash
# SPDX-License-Identifier: GPL-3.0-or-later
# Render a GIF (and optionally an MP4) from the REAL desktop app window.
#
# The app runs on an isolated Xvfb display (not your desktop session), is
# driven with the dashboard's keyboard shortcuts, captured frame by frame with
# ImageMagick `import`, and assembled with ffmpeg.
#
# Requirements: Xvfb, openbox, xdotool, ImageMagick (import), ffmpeg, and a
# built app binary (src-tauri/target/release/linux-doctor, or APP=...).
# On immutable distros, install the first four in a toolbox and run with
# TOOLBOX=<name>; ffmpeg runs on the host.
#
# Usage:
#   scripts/demo-app-gif.sh [output.gif]
#   TOOLBOX=ldbuild scripts/demo-app-gif.sh
#
# Knobs (env):
#   SCREEN=1400x860   Xvfb screen size
#   WINDOW=           force the app window size (e.g. 1820x1120); empty lets the app decide
#   SCALE=940         output width in pixels
#   MP4=              also write an MP4 from the same frames (Reddit prefers video)
#   EXTRACT=          override the crop geometry, e.g. "1600:1000:120:20"
set -euo pipefail

ROOT=$(cd "$(dirname "$0")/.." && pwd)
OUT=${1:-"$ROOT/docs/screenshots/app.gif"}
APP=${APP:-"$ROOT/src-tauri/target/release/linux-doctor"}
FRAMES_DIR="$ROOT/.demo-app-frames"
FPS=8
SCREEN=${SCREEN:-1400x860}
WINDOW=${WINDOW:-}
SCALE=${SCALE:-940}
MP4=${MP4:-}
EXTRACT=${EXTRACT:-}
# Fallback crop (window region inside the default Xvfb root), used when the
# window geometry cannot be read. Measured from the capture margins.
FALLBACK_CROP="1322:807:78:53"

rm -rf "$FRAMES_DIR"; mkdir -p "$FRAMES_DIR"

# The desktop app needs a session bus: the single-instance plugin and the tray
# both talk to one, and without it the process starts and never maps a window
# (silent hang, no output). A container has none, and pointing the app at the
# host's bus would register a tray icon in the user's real session, so a private
# bus is started for the capture and torn down afterwards.
BUS_PATH="/run/user/$(id -u)/ld-gif-bus"
bus_start() {
  rm -f "$BUS_PATH"
  dbus-daemon --session --address="unix:path=$BUS_PATH" --fork 2>/dev/null || true
  [ -S "$BUS_PATH" ] && echo "session bus: $BUS_PATH"
}
bus_stop() {
  node -e '
    const fs = require("fs");
    const path = process.argv[1];
    for (const p of fs.readdirSync("/proc").filter((d) => /^\d+$/.test(d))) {
      if (Number(p) === process.pid) continue; // never match this very process
      let c = "";
      try { c = fs.readFileSync(`/proc/${p}/cmdline`, "utf8").replace(/\0/g, " "); } catch { continue; }
      if (c.includes(path)) { try { process.kill(Number(p), "SIGTERM"); } catch {} }
    }
    try { fs.unlinkSync(path); } catch {}
  ' "$BUS_PATH" 2>/dev/null || true
}
if [ -n "${TOOLBOX:-}" ]; then trap bus_stop EXIT INT TERM; bus_start; fi

capture() {
  export XDG_RUNTIME_DIR=$(mktemp -d)
  Xvfb :99 -ac -screen 0 "$SCREEN"x24 >/tmp/ld-xvfb.log 2>&1 & local xp=$!
  sleep 2
  export DISPLAY=:99
  openbox >/tmp/ld-openbox.log 2>&1 & local op=$!
  sleep 1
  export LIBGL_ALWAYS_SOFTWARE=1 WEBKIT_DISABLE_DMABUF_RENDERER=1 WEBKIT_DISABLE_COMPOSITING_MODE=1
  if [ -n "${HOST_APP:-}" ]; then
    # Run the app on the HOST so it inspects the host, not this container. A
    # capture taken inside the toolbox showed "Fedora Linux 42" and container
    # skips — a demo of a container, which is not what the README shows. It
    # still renders HERE: the Xvfb socket lands in /tmp/.X11-unix, which toolbox
    # shares with the host, so DISPLAY=:99 crosses the boundary. flatpak-spawn
    # --host is how the container runs anything on the host.
    # GDK_BACKEND=x11 + no WAYLAND_DISPLAY: on the host the app is Wayland-first,
    # and an inherited WAYLAND_DISPLAY makes it render on the real session
    # instead of this Xvfb (a window on the user's screen, and none here).
    flatpak-spawn --host env -u WAYLAND_DISPLAY \
      DISPLAY=:99 GDK_BACKEND=x11 LIBGL_ALWAYS_SOFTWARE=1 \
      WEBKIT_DISABLE_DMABUF_RENDERER=1 WEBKIT_DISABLE_COMPOSITING_MODE=1 \
      DBUS_SESSION_BUS_ADDRESS="$DBUS_SESSION_BUS_ADDRESS" \
      sh -c 'echo $$ > /tmp/ld-app.pid; exec "$0"' "$APP" >/tmp/ld-app.log 2>&1 & local ap=$!
  else
    "$APP" >/tmp/ld-app.log 2>&1 & local ap=$!
  fi
  local id="" i=0
  for i in $(seq 1 90); do
    id=$(xdotool search --name "Linux Doctor" 2>/dev/null | head -1 || true)
    [ -n "$id" ] && break
    sleep 1
  done
  if [ -z "$id" ]; then
    echo "app window never appeared (see /tmp/ld-app.log)" >&2
    if [ -n "${HOST_APP:-}" ]; then flatpak-spawn --host sh -c '[ -f /tmp/ld-app.pid ] && kill "$(cat /tmp/ld-app.pid)"' 2>/dev/null || true; fi
    kill $ap $op $xp 2>/dev/null || true
    return 1
  fi
  xdotool windowactivate --sync "$id" 2>/dev/null || true
  if [ -n "$WINDOW" ]; then
    xdotool windowsize "$id" "${WINDOW%x*}" "${WINDOW#*x}" 2>/dev/null || true
  fi
  # The host app takes longer to draw its first report than one running in this
  # container (package cold start, real /proc reads): 7s left "Loading…" on
  # screen for the first few frames.
  if [ -n "${HOST_APP:-}" ]; then sleep 25; else sleep 7; fi

  # Crop to the window's client area, so the frame is the app and nothing else.
  # `getwindowgeometry` reports the client window in root coordinates even when
  # the WM reparents it, so no titlebar sneaks into the crop.
  local crop="$FALLBACK_CROP"
  if geo=$(xdotool getwindowgeometry --shell "$id" 2>/dev/null); then
    eval "$geo" # X= Y= WIDTH= HEIGHT=
    if [ -n "${WIDTH:-}" ] && [ -n "${HEIGHT:-}" ]; then
      crop="${WIDTH}:${HEIGHT}:${X:-0}:${Y:-0}"
    fi
  fi
  [ -n "$EXTRACT" ] && crop="$EXTRACT"
  echo "$crop" > "$FRAMES_DIR/.crop"

  # One frame per step; the dashboard shortcuts drive the UI:
  #   1..5 = views, ArrowDown = focus a card, Enter = open it.
  #
  # Order matters for the story: the report lands on Overview first and HOLDS
  # for ~2s, so the score is what a reader sees before anything moves. Only
  # then does it walk the views, comes back to Overview, and opens a finding.
  i=0
  while [ $i -lt 72 ]; do
    import -window root "$FRAMES_DIR/$(printf %04d $i).png" 2>/dev/null || true
    case $i in
      16) xdotool key 2 ;;
      28) xdotool key 3 ;;
      38) xdotool key 5 ;;
      46) xdotool key 1 ;;
      54) xdotool key Down ;;
      60) xdotool key Return ;;
    esac
    i=$((i + 1))
  done
  # Stop the host app by the pid it wrote (pkill -f would also match the shell
  # command line that starts with APP=…, which hangs this very session).
  if [ -n "${HOST_APP:-}" ]; then flatpak-spawn --host sh -c '[ -f /tmp/ld-app.pid ] && kill "$(cat /tmp/ld-app.pid)"' 2>/dev/null || true; fi
  pkill -x Xvfb 2>/dev/null || true
  pkill -x openbox 2>/dev/null || true
  kill $ap $op $xp 2>/dev/null || true
}

if [ -n "${TOOLBOX:-}" ]; then
  toolbox run -c "$TOOLBOX" -- bash -lc "$(declare -f capture); ROOT='$ROOT' APP='$APP' FRAMES_DIR='$FRAMES_DIR' SCREEN='$SCREEN' WINDOW='$WINDOW' EXTRACT='$EXTRACT' HOST_APP='${HOST_APP:-}' DBUS_SESSION_BUS_ADDRESS='unix:path=$BUS_PATH' capture"
else
  capture
fi

CROP=$(cat "$FRAMES_DIR/.crop" 2>/dev/null || echo "$FALLBACK_CROP")

ffmpeg -y -loglevel error -framerate "$FPS" -i "$FRAMES_DIR/%04d.png" \
  -vf "crop=$CROP,fps=$FPS,scale=$SCALE:-1:flags=lanczos,split[s0][s1];[s0]palettegen=max_colors=128[p];[s1][p]paletteuse=dither=none" \
  -loop 0 "$OUT"
echo "Wrote $OUT ($(identify -format '%wx%h' "$OUT[0]") · $(du -h "$OUT" | cut -f1))"

if [ -n "$MP4" ]; then
  # Reddit prefers video: a GIF over 20 MB is rejected and anything animated is
  # converted to video anyway. H.264 with faststart plays inline.
  ffmpeg -y -loglevel error -framerate "$FPS" -i "$FRAMES_DIR/%04d.png" \
    -vf "crop=$CROP,fps=$FPS,scale=$SCALE:-2:flags=lanczos" \
    -c:v libx264 -crf 24 -preset slow -pix_fmt yuv420p -movflags +faststart -an "$MP4"
  echo "Wrote $MP4 ($(du -h "$MP4" | cut -f1))"
fi

rm -rf "$FRAMES_DIR"