Python 152 lines
#!/usr/bin/env python3
# boot_demo_typing.py — replays the FlashOS boot + fsh session for the VHS
# recorder that renders assets/boot_demo.gif.
#
# Three cadences, so the demo reads like a real console session:
# * TYPED lines — what a user types at a prompt: the username at `login:`,
# the masked password, and each `$ ` command (`help`, `ls`). The prompt
# prints instantly, then the typed text appears key-by-key.
# * COMMAND OUTPUT — a `$ ` command's result (the `help` text, the `ls`
# listing): dumped as one BLOCK, the whole thing at once, the way a real
# console paints command output — not line by line.
# * PRINTED lines — boot `[ OK ]` status and the login/Password handshake:
# emitted whole, one line at a time, as they scroll past at boot.
#
# Provenance: the boot block mirrors the current kernel's Mini-UART output
# (console_ui boot tags, `color` on — only the inner `OK` word is green, the
# brackets stay the default fg; no trailing periods on the `[ OK ]` lines).
# The post-login fsh session is RECONSTRUCTED from the programs' own byte
# output (login_elf's `login:` / masked `Password:`, fsh's homescreen banner
# + `help`, and `/bin/ls` of the initramfs root): the login gate blocks
# capturing the authenticated session under QEMU (the guest console receives
# no host stdin). Every byte emitted here is what those programs print — only
# the pacing and the green OK tint are added.
#
# The replayed content lives in scripts/boot_demo_session.txt (committed),
# not in boot.log: boot.log is gitignored and gets overwritten/truncated by
# each `picapture` run, which silently drops the reconstructed tail.
import sys, time, re, os
SESSION = os.path.join(os.path.dirname(__file__), "boot_demo_session.txt")
ZON = os.path.join(os.path.dirname(__file__), "..", "build.zig.zon")
BOOT_LINE = 0.16 # pause after a printed boot/handshake line
AFTER_PROMPT = 0.30 # pause after a prompt prints, before typing starts
KEY = 0.06 # seconds per typed character
AFTER_CMD = 0.45 # pause after a typed command line, before its output
BLOCK_PAUSE = 0.55 # pause after a command's output block is dumped
HOLD = 5.0 # hold the final live-prompt frame at the end
# Boot status tags carry color in the real console (console_ui palette,
# `color` on): only the inner `OK` word is tinted green, the brackets keep
# the default fg. session.txt holds the plain `[ OK ]` text; the green is
# added here at emit time so the file stays readable and the escape is
# spelled once, mirroring console_ui.writeTag.
GREEN = "\x1b[32m"
RESET = "\x1b[0m"
def colorize(line):
return line.replace("[ OK ]", "[ " + GREEN + "OK" + RESET + " ]")
# A line a user types: a prompt prefix + the typed text. `login: ` (the
# username), `Password: ` (the kernel masks each keystroke with '*', so the
# masked `*****` types out char-by-char), and each `$ ` command. A bare `# `
# line is cat/help output, not a prompt, so it is not matched.
TYPED = re.compile(r'^(login: |Password: |\$ )(\S.*)$')
def zon_version():
# Single-source the banner version from build.zig.zon (the one truth, the
# same field fsh derives its homescreen version from via build_options) so
# the demo's `FlashOS [v…]` line never drifts from the shipped release.
with open(ZON, "r", encoding="utf-8") as f:
m = re.search(r'\.version\s*=\s*"([^"]+)"', f.read())
return m.group(1) if m else "?"
def out(s):
sys.stdout.write(s)
sys.stdout.flush()
def is_comment(line):
return line.lstrip().startswith("#")
def type_command(prompt, text):
out(prompt) # prompt — instant
time.sleep(AFTER_PROMPT)
for ch in text: # typed text — key by key
out(ch)
time.sleep(KEY)
out("\r\n")
time.sleep(AFTER_CMD)
def main():
# Clear the screen (+ scrollback) and home the cursor before the first
# boot byte, so the GIF opens on the boot output at the top-left — not on
# the launch command the VHS tape typed (hidden) to start this replay.
# It also gives the final `reboot` a clean loop seam: when the GIF wraps,
# the screen is wiped just as a real machine reset would clear it.
out("\x1b[2J\x1b[3J\x1b[H")
with open(SESSION, "rb") as f:
lines = [ln.rstrip("\r") for ln in
f.read().decode("utf-8", "replace").split("\n")]
if lines and lines[-1] == "":
lines.pop()
version = zon_version()
lines = [ln.replace("{{VERSION}}", version) for ln in lines]
# The trailing line is the live prompt waiting for input — the `$ ` shell
# prompt after the last command, or a re-spawned `login:`. Print it
# without a newline and hold, so the GIF ends on the live prompt (cursor
# sitting on it) rather than scrolling past it.
final = None
if lines and lines[-1].rstrip() in ("login:", "$"):
final = lines.pop()
i, n = 0, len(lines)
while i < n:
line = lines[i]
m = TYPED.match(line)
if m:
type_command(m.group(1), m.group(2))
i += 1
# A `$ ` command's output is everything up to the next typed
# line. It paints in runs: consecutive comment (`#`) lines form
# one block and the file's actual command lines (an rc's `cd /`)
# form their own — so `cat`ing a commented file shows its
# comment header at once, then each command line as its own
# entry, instead of one glued blob; a plain listing (`ls`, with
# no comments) stays a single block. The `login:` handshake
# (m.group(1) != "$ ") is left to the line-by-line path below.
if m.group(1) == "$ ":
block = []
while i < n and not TYPED.match(lines[i]):
block.append(lines[i])
i += 1
k = 0
while k < len(block):
is_c = is_comment(block[k])
j = k
while j < len(block) and is_comment(block[j]) == is_c:
j += 1
out("\r\n".join(block[k:j]) + "\r\n")
time.sleep(BLOCK_PAUSE)
k = j
else:
out(colorize(line) + "\r\n") # boot / handshake — whole line, OK tinted
time.sleep(BOOT_LINE)
i += 1
if final is not None:
out(final + " ") # waiting prompt, no newline
time.sleep(HOLD)
if __name__ == "__main__":
main()