docs: illustrated LaTeX user guide (docs/guide) with generated screenshots

This commit is contained in:
Andy Dixon
2026-10-11 19:41:31 +01:00
parent ce9c5c6b44
commit 8766846a3b
55 changed files with 1947 additions and 0 deletions
+27
View File
@@ -0,0 +1,27 @@
#!/bin/bash
# ForceCommand for the throwaway screenshot sshd: never a login shell (no ~/.profile, no byobu).
DEMO_HOME=$1 SFTP_SERVER=$2
export LC_ALL=C.UTF-8
case "$SSH_ORIGINAL_COMMAND" in
*sftp*) exec "$SFTP_SERVER" -d "$DEMO_HOME" ;;
?*) cd "$DEMO_HOME" && HOME="$DEMO_HOME" exec bash --norc --noprofile -c "$SSH_ORIGINAL_COMMAND" ;;
esac
cd "$DEMO_HOME" || exit 1
export HOME="$DEMO_HOME" PS1='\[\e[1;32m\]andy@atlas\[\e[0m\]:\[\e[1;34m\]\w\[\e[0m\]\$ '
row() { # boxed line, padded on visible width
local plain; plain=$(printf '%b' "$1" | sed 's/\x1b\[[0-9;]*m//g')
printf '\e[1;36m│\e[0m %b%*s\e[1;36m│\e[0m\n' "$1" $((44 - ${#plain})) ''
}
printf '\e[1;36m╭──────────────────────────────────────────────╮\e[0m\n'
row 'Welcome to \e[1matlas\e[0m · Debian 13 · kernel 6.12'
row 'Load \e[32m0.42\e[0m Memory \e[33m61%\e[0m Disk \e[32m38%\e[0m'
row 'Last deploy: \e[1m2 hours ago\e[0m by andy'
printf '\e[1;36m╰──────────────────────────────────────────────╯\e[0m\n\n'
printf '\e[1;32m$\e[0m ls -l --color\n'; ls -l --color=always
printf '\n\e[1;32m$\e[0m ./deploy.sh --dry-run\n'
for s in "Resolving build" "Uploading artefacts" "Rolling out to 4 nodes" "Health checks"; do
printf ' \e[32m✔\e[0m %-26s \e[2m%s\e[0m\n' "$s" "$((RANDOM % 900 + 100))ms"
done
printf ' \e[33m⚠\e[0m 1 warning: \e[1mnginx.conf\e[0m has a deprecated directive\n'
printf ' \e[1;32mDone.\e[0m Nothing was actually deployed (dry run).\n\n'
exec bash --norc --noprofile -i
+214
View File
@@ -0,0 +1,214 @@
#!/usr/bin/env python3
"""Tiny fake servers for the user-guide screenshots: RAW sensor feed (42023), IRC (42667),
GitHub API (42080) and a VNC desktop (42900). Stdlib only. Runs until killed."""
import json, math, random, socket, socketserver, struct, threading, time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
class Threaded(socketserver.ThreadingMixIn, socketserver.TCPServer):
allow_reuse_address = True
daemon_threads = True
# --- RAW: a chatty sensor --------------------------------------------------------------
class Raw(socketserver.BaseRequestHandler):
def handle(self):
s = self.request
s.sendall(b"SENSOR-7 ready. Commands: STATUS, RESET\n")
t = 20.5
s.settimeout(0.25)
for i in range(400):
t += random.uniform(-0.3, 0.3)
s.sendall(f"temp={t:.2f}C hum={48 + 4 * math.sin(i / 3):.1f}% seq={i}\n".encode())
try:
if (cmd := s.recv(256)):
s.sendall(b"OK " + cmd.strip() + b"\n")
except socket.timeout:
pass
# --- IRC: a small, friendly network ----------------------------------------------------
CHAT = [
("ferris", "anyone tried the new Nexus build yet?"),
("crabby", "yep, the split panes are lovely"),
("ferris", "the serial console support saved my afternoon tbh"),
("lin", "does it do jump hosts?"),
("crabby", "lin: multiple hops, plus SOCKS5/HTTP proxies"),
("ferris", "and it's Rust all the way down now :)"),
]
class Irc(socketserver.StreamRequestHandler):
def send(self, line):
self.wfile.write((line + "\r\n").encode())
def handle(self):
nick = "andy"
for raw in self.rfile:
line = raw.decode(errors="replace").strip()
cmd, _, rest = line.partition(" ")
if cmd == "NICK":
nick = rest.strip()
elif cmd == "USER":
self.send(f":irc.demo 001 {nick} :Welcome to the Demo IRC Network, {nick}")
self.send(f":irc.demo 375 {nick} :- irc.demo Message of the Day -")
self.send(f":irc.demo 372 {nick} :- Be excellent to each other.")
self.send(f":irc.demo 376 {nick} :End of /MOTD command.")
elif cmd == "JOIN":
for room in rest.split()[0].split(","):
self.send(f":{nick}!u@demo JOIN {room}")
self.send(f":irc.demo 332 {nick} {room} :Nexus Terminal chat · be kind · no pasting walls of logs")
self.send(f":irc.demo 353 {nick} = {room} :@ferris +crabby lin {nick} octo rustacean")
self.send(f":irc.demo 366 {nick} {room} :End of /NAMES list.")
if room == "#nexus":
for who, msg in CHAT:
self.send(f":{who}!u@demo PRIVMSG #nexus :{msg}")
else:
self.send(f":octo!u@demo PRIVMSG {room} :borrow checker 1, me 0")
elif cmd == "LIST":
for room, n, topic in [("#nexus", 42, "Nexus Terminal"), ("#rust", 3110, "Rust"), ("#linux", 905, "Linux"), ("#homelab", 77, "Racks & routers")]:
self.send(f":irc.demo 322 {nick} {room} {n} :{topic}")
self.send(f":irc.demo 323 {nick} :End of /LIST")
elif cmd == "PING":
self.send(f":irc.demo PONG irc.demo {rest}")
elif cmd == "QUIT":
return
# --- GitHub API --------------------------------------------------------------------------
REPOS = [
("andydixon", "nexus-terminal", "SSH, Telnet, Serial, IRC and VNC in one tidy native app", "Rust", 128, False, False, False, 1),
("andydixon", "leviathan", "The egui design system Nexus borrows its looks from", "Rust", 64, False, False, False, 3),
("andydixon", "dotfiles", "Shell, editor and terminal config", "Shell", 12, False, False, False, 6),
("andydixon", "homelab-ansible", "Playbooks for the rack under the stairs", "Jinja", 7, True, False, False, 9),
("andydixon", "alchementary-link", "The Wails/Go predecessor to Nexus Terminal", "Go", 31, False, False, True, 40),
("alchementary", "website", "Marketing site", "TypeScript", 3, True, False, False, 2),
("alchementary", "infra", "Terraform for everything", "HCL", 2, True, False, False, 5),
("alchementary", "russh", "Fork with a jump-host fix", "Rust", 0, False, True, False, 15),
]
WHO = ["Andy Dixon", "ferris", "crabby", "dependabot[bot]"]
def iso(days):
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(time.time() - days * 86400 - 3600 * 5))
class Github(BaseHTTPRequestHandler):
def log_message(self, *a):
pass
def reply(self, obj):
body = json.dumps(obj).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_GET(self):
if self.path == "/user":
return self.reply({"login": "andydixon"})
if self.path.startswith("/user/repos"):
page = 1 if "page=1" in self.path else 2
if page != 1:
return self.reply([])
return self.reply([
{"full_name": f"{o}/{n}", "name": n, "description": d, "html_url": f"https://github.com/{o}/{n}",
"default_branch": "main", "private": p, "fork": f, "archived": a, "language": l,
"stargazers_count": s, "pushed_at": iso(age), "updated_at": iso(age), "owner": {"login": o}}
for o, n, d, l, s, p, f, a, age in REPOS])
if "/commits/" in self.path:
i = sum(map(ord, self.path)) % len(WHO)
age = next((r[8] for r in REPOS if f"/{r[1]}/" in self.path), 1)
return self.reply({"commit": {"committer": {"name": WHO[i], "date": iso(age)}, "author": {"name": WHO[i], "date": iso(age)}}})
self.send_error(404)
# --- VNC: a static "desktop" ---------------------------------------------------------------
W, H = 1280, 800
def desktop():
px = bytearray(W * H * 4)
wins = [(90, 90, 620, 420, (40, 44, 52)), (560, 260, 1180, 700, (250, 250, 252)), (140, 560, 460, 740, (32, 140, 120))]
for y in range(H):
for x in range(W):
# Diagonal violet → teal wallpaper.
k = (x + y) / (W + H)
r, g, b = int(88 * (1 - k) + 20 * k), int(40 * (1 - k) + 150 * k), int(160 * (1 - k) + 170 * k)
for x0, y0, x1, y1, c in wins:
if x0 <= x < x1 and y0 <= y < y1:
r, g, b = (200, 70, 90) if y < y0 + 28 else c
if y >= y0 + 28 and c == (40, 44, 52) and (y - y0) % 22 < 9 and x0 + 20 < x < x0 + 20 + ((y * 37) % 400):
r, g, b = (120, 220, 140) if (y // 22) % 3 else (230, 200, 120)
if y >= H - 40:
r, g, b = 24, 24, 30
i = (y * W + x) * 4
px[i:i + 4] = bytes((r, g, b, 0))
return bytes(px)
PIXELS = None
class Vnc(socketserver.BaseRequestHandler):
def recv(self, n):
b = b""
while len(b) < n:
c = self.request.recv(n - len(b))
if not c:
raise EOFError
b += c
return b
def handle(self):
s = self.request
s.sendall(b"RFB 003.008\n")
self.recv(12)
s.sendall(bytes([1, 1])) # one security type: None
self.recv(1)
s.sendall(struct.pack(">I", 0))
self.recv(1) # ClientInit
name = b"studio:0 (demo)"
s.sendall(struct.pack(">HH", W, H) + bytes([32, 24, 0, 1, 0, 255, 0, 255, 0, 255, 0, 8, 16, 0, 0, 0]) + struct.pack(">I", len(name)) + name)
sent = False
try:
while True:
t = self.recv(1)[0]
if t == 0:
self.recv(19)
elif t == 2:
n = struct.unpack(">xH", self.recv(3))[0]
self.recv(4 * n)
elif t == 3:
self.recv(9)
if not sent:
s.sendall(struct.pack(">BxH", 0, 1) + struct.pack(">HHHHi", 0, 0, W, H, 0) + PIXELS)
sent = True
else:
time.sleep(0.5)
s.sendall(struct.pack(">BxH", 0, 0))
elif t == 4:
self.recv(7)
elif t == 5:
self.recv(5)
elif t == 6:
n = struct.unpack(">3xI", self.recv(7))[0]
self.recv(n)
else:
return
except (EOFError, ConnectionError):
pass
if __name__ == "__main__":
PIXELS = desktop()
for port, handler in [(42023, Raw), (42667, Irc), (42900, Vnc)]:
threading.Thread(target=Threaded(("127.0.0.1", port), handler).serve_forever, daemon=True).start()
print("fakes ready", flush=True)
ThreadingHTTPServer(("127.0.0.1", 42080), Github).serve_forever()
+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/env bash
# Regenerates docs/guide/img/*.png: a throwaway sshd (ForceCommand, never a login shell),
# fake RAW/IRC/GitHub/VNC servers, a demo data directory, then one headless app run per shot.
# Needs: xvfb-run, sshd, ssh-keygen, python3, ImageMagick. Usage: docs/guide/tools/screenshots.sh [shot...]
set -euo pipefail
cd "$(dirname "$0")/.."
GUIDE=$PWD
B=${NEXUS_GUIDE_TMP:-/tmp/nexus-guide}
IMG=$GUIDE/img
APP=$GUIDE/../../rust/target/debug/nexus-terminal
SCALE=${SCALE:-2}
mkdir -p "$IMG"
rm -rf "$B" && mkdir -p "$B/home/.ssh" "$B/sshd"
(cd ../../rust && cargo build -q -p nexus && cargo build -q -p nexus-core --example demo_data)
# Demo home: what the SFTP browser, the shell and the SSH key manager see.
H=$B/home
mkdir -p "$H"/{projects/nexus,backups,logs,www/static}
printf 'server {\n listen 443 ssl;\n}\n' > "$H/nginx.conf"
echo '#!/bin/sh' > "$H/deploy.sh"; chmod +x "$H/deploy.sh"
head -c 180000 /dev/urandom > "$H/backups/db-2026-10-10.sql.gz"
head -c 52000 /dev/urandom > "$H/logs/access.log"
echo "# Notes" > "$H/README.md"
ssh-keygen -q -t ed25519 -N '' -C 'andy@laptop' -f "$H/.ssh/id_ed25519"
ssh-keygen -q -t rsa -b 2048 -N '' -C 'andy@work' -f "$H/.ssh/work_rsa"
ssh-keygen -q -t ecdsa -N '' -C 'deploy-bot' -f "$H/.ssh/deploy_ecdsa"
# sshd on 42022 (ForceCommand keeps it away from ~/.profile).
S=$B/sshd
ssh-keygen -q -t ed25519 -N '' -f "$S/host"
cp "$H/.ssh/id_ed25519.pub" "$S/authorized_keys"
SFTP=$(ls /usr/lib/openssh/sftp-server /usr/libexec/openssh/sftp-server /usr/libexec/sftp-server 2>/dev/null | head -1 || true)
cat > "$S/config" <<CFG
Port 42022
ListenAddress 127.0.0.1
HostKey $S/host
AuthorizedKeysFile $S/authorized_keys
PidFile $S/pid
StrictModes no
UsePAM no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
PrintMotd no
PrintLastLog no
Subsystem sftp $SFTP
ForceCommand $GUIDE/tools/demo-shell.sh $H $SFTP
CFG
"$(command -v sshd)" -f "$S/config" -E "$S/log"
python3 -I "$GUIDE/tools/fakes.py" > "$B/fakes.log" 2>&1 &
FAKES=$!
trap 'kill $FAKES 2>/dev/null; kill "$(cat "$S/pid" 2>/dev/null)" 2>/dev/null || true' EXIT
until grep -q ready "$B/fakes.log"; do sleep 0.2; done
"$(dirname "$APP")/examples/demo_data" "$B/data" "$H/.ssh/id_ed25519" > /dev/null
cp -r "$B/data" "$B/data.clean"
shot() { # name, then app args
local name=$1; shift
if [ ${#ONLY[@]} -gt 0 ] && [[ ! " ${ONLY[*]} " =~ " $name " ]]; then return 0; fi
rm -rf "$B/data" && cp -r "$B/data.clean" "$B/data"
HOME=$H WINIT_X11_SCALE_FACTOR=$SCALE timeout 60 xvfb-run -a -s "-screen 0 3200x2000x24" \
"$APP" --data-dir "$B/data" --size 1360x860 --screenshot "$IMG/$name.png" "$@" 2>> "$B/app.log" || echo " ! $name failed"
# 256-colour PNGs are a quarter of the size and look identical for UI shots.
[ -f "$IMG/$name.png" ] && magick "$IMG/$name.png" -colors 256 "PNG8:$IMG/$name.png"
echo " $name"
}
ONLY=("$@")
shot welcome
shot welcome-midnight --theme midnight
shot welcome-light --theme light
shot ssh --connect "Sensor feed" --connect "Libera Chat" --connect atlas --shot-delay 5500
shot ssh-light --theme light --connect atlas --shot-delay 5500
shot hostkey --connect hello-ssh --shot-delay 5000
shot split --connect atlas --do "Split Pane Right" --shot-delay 5500
shot sftp --connect atlas --do "SFTP Browser" --shot-delay 6000
shot broadcast --connect build-runner --connect atlas --do "Toggle Broadcast Mode" --shot-delay 5500
shot raw --connect "Sensor feed" --shot-delay 5500
shot irc --connect "Libera Chat" --shot-delay 5500
shot github --connect GitHub --answer ghp_demo --shot-delay 6000
shot vnc --connect "Studio desktop" --answer demo --shot-delay 6000
shot local --run-local 'printf "\e[1;35mhello from the local shell\e[0m\n"; ls -la --color=always' --shot-delay 5000
shot palette --open palette
shot quick --open quick
shot editor-ssh --open editor:ssh
shot editor-new --open editor:new
shot editor-serial --open editor:serial
shot editor-telnet --open editor:telnet
shot editor-raw --open editor:raw
shot editor-vnc --open editor:vnc
shot editor-irc --open editor:irc
shot editor-github --open editor:github
for t in general terminal security knownhosts help about; do shot "settings-$t" --open "settings:$t"; done
shot keys --open keys
shot snippets --open snippets
shot triggers --open triggers
shot recordings --open recordings
shot export --open export