Files
Tirsvad bf2c82ae98
TirSystem/git-workflows: Release / release (pull_request) Successful in 17s
TirSystem/git-workflows: Validate / validate (pull_request) Successful in 36s
TirSystem/git-workflows: OpenGrep / opengrep (pull_request) Successful in 5m45s
TirSystem/git-workflows: Sync GitHub mirror metadata / sync-metadata (push) Successful in 14s
TirSystem/git-workflows: Publish wiki / publish-wiki (push) Successful in 1m5s
TirSystem/git-workflows: Validate / validate (push) Successful in 39s
TirSystem/git-workflows: Publish accepted milestones / publish (push) Successful in 48s
TirSystem/git-workflows: OpenGrep / opengrep (push) Successful in 6m4s
Keep the nosemgrep marker on the call line of the screenshot tool
2026-11-06 13:07:24 +08:00

265 lines
8.3 KiB
Python

"""Create the lecture screenshots in docs/images/ without touching the desktop.
Every screenshot is taken by a headless Edge or Chrome (no window, no mouse, no
keyboard). Terminal screenshots render the real output of real commands.
Usage (from the repository root, with the project's .venv active):
python docs/images/capture.py 1 2 3 4 5 6
"""
# Developer tool only (not shipped in the package): every command it runs is a fixed list
# built from constants, sys.executable and local paths; it reads no external input.
import html
import os
import re
import shutil
import subprocess
import sys
import tempfile
import time
import urllib.request
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
OUT = ROOT / "docs" / "images"
SOURCES = OUT / "sources"
BROWSERS = [
r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe",
r"C:\Program Files\Google\Chrome\Application\chrome.exe",
"microsoft-edge",
"google-chrome",
"chromium",
]
PORT = 5057
APP = "flask_day_54.presentation.hello"
TERMINAL_CSS = """
body{margin:0;background:#1e1e1e;font-family:Consolas,'DejaVu Sans Mono',monospace;color:#d4d4d4}
.bar{background:#333;color:#ccc;padding:8px 14px;font-size:14px}
pre{margin:0;padding:16px 18px;font-size:15px;line-height:1.45;white-space:pre-wrap}
.p{color:#6a9955}.c{color:#9cdcfe}
"""
BROWSER_CSS = """
body{margin:0;background:#f3f3f3;font-family:Segoe UI,Arial,sans-serif}
.tab{background:#dde3ea;padding:8px 14px;font-size:13px;color:#333}
.url{background:#fff;margin:8px;padding:6px 12px;border:1px solid #bbb;border-radius:14px;font-size:14px}
.page{background:#fff;height:150px;padding:16px;font-size:16px;font-family:Times New Roman,serif}
"""
def find_browser() -> str:
for candidate in BROWSERS:
found = shutil.which(candidate) or (
candidate if os.path.exists(candidate) else None
)
if found:
return found
raise SystemExit("No Edge or Chrome found for headless screenshots")
def shot(page: Path, png: Path, width: int, height: int) -> None:
"""Take a headless screenshot of a local page or a URL."""
target = page.as_uri() if isinstance(page, Path) else page
profile = tempfile.mkdtemp(prefix="shot-")
subprocess.run( # nosemgrep
[
find_browser(),
"--headless=new",
"--disable-gpu",
"--hide-scrollbars",
f"--user-data-dir={profile}",
f"--window-size={width},{height}",
f"--screenshot={png}",
"--virtual-time-budget=3000",
target,
],
check=True,
capture_output=True,
timeout=60,
)
shutil.rmtree(profile, ignore_errors=True)
print("wrote", png.relative_to(ROOT))
def run(cmd: list[str], cwd: Path, timeout: int = 30) -> str:
done = subprocess.run( # nosemgrep
cmd, cwd=cwd, capture_output=True, text=True, timeout=timeout
)
return (done.stdout + done.stderr).rstrip("\n")
def terminal_page(
title: str, entries: list[tuple[str, str]], png: Path, height: int
) -> None:
"""Render (command, output) pairs as a terminal and take the screenshot."""
lines = []
for command, output in entries:
lines.append(
f'<span class="p">$</span> <span class="c">{html.escape(command)}</span>'
)
if output:
lines.append(html.escape(output))
page = (
f"<!doctype html><meta charset='utf-8'><style>{TERMINAL_CSS}</style>"
f"<div class='bar'>{html.escape(title)}</div><pre>"
+ "\n".join(lines)
+ "</pre>"
)
SOURCES.mkdir(exist_ok=True)
src = SOURCES / (png.stem + ".html")
src.write_text(page, encoding="utf-8")
shot(src, png, 900, height)
def stop(proc: subprocess.Popen) -> None:
"""Stop a server and its reloader child."""
if os.name == "nt":
subprocess.run( # nosemgrep
["taskkill", "/F", "/T", "/PID", str(proc.pid)], capture_output=True
)
else:
proc.kill()
proc.wait(timeout=10)
def python() -> str:
return sys.executable
def step1() -> None:
shot(SOURCES / "lecture-01.html", OUT / "lecture-01.png", 900, 400)
def start_server() -> subprocess.Popen:
proc = subprocess.Popen( # nosemgrep
[python(), "-m", "flask", "--app", APP, "run", "--port", str(PORT)],
cwd=ROOT,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
)
for _ in range(40):
try:
urllib.request.urlopen(f"http://127.0.0.1:{PORT}/", timeout=1).read()
return proc
except OSError:
time.sleep(0.25) # nosemgrep
proc.kill()
raise SystemExit("server did not start")
def step2() -> None:
proc = start_server()
try:
body = (
urllib.request.urlopen(f"http://127.0.0.1:{PORT}/", timeout=3)
.read()
.decode()
)
page = (
f"<!doctype html><meta charset='utf-8'><style>{BROWSER_CSS}</style>"
f"<div class='tab'>127.0.0.1:5057</div>"
f"<div class='url'>http://127.0.0.1:{PORT}/</div>"
f"<div class='page'>{body}</div>"
)
src = SOURCES / "lecture-02.html"
src.write_text(page, encoding="utf-8")
shot(src, OUT / "lecture-02.png", 700, 230)
finally:
stop(proc)
def step3() -> None:
work = Path(tempfile.mkdtemp(prefix="cli-demo-"))
bash = shutil.which("bash") or "bash"
entries = []
script = [
"pwd",
"ls",
"mkdir my_project",
"cd my_project && touch hello.py && ls",
"rm -rf my_project && ls",
]
for command in script:
output = run([bash, "-c", command], work)
entries.append((command, re.sub(r"/tmp/cli-demo-\w+", "~/demo", output)))
shutil.rmtree(work, ignore_errors=True)
terminal_page(
"Terminal - lecture 3: the command line", entries, OUT / "lecture-03.png", 330
)
def step4() -> None:
entries = []
cmd = "python -m flask_day_54.domain.name_demo"
entries.append((cmd, run([python(), "-m", "flask_day_54.domain.name_demo"], ROOT)))
imp = 'python -c "import flask_day_54.domain.name_demo as m; print(m.describe_name(m.__name__))"'
entries.append(
(
imp,
run(
[
python(),
"-c",
"import flask_day_54.domain.name_demo as m; print(m.describe_name(m.__name__))",
],
ROOT,
),
)
)
server = subprocess.Popen( # nosemgrep
[python(), "-m", "flask_day_54.presentation.hello"],
cwd=ROOT,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
)
time.sleep(3) # nosemgrep
stop(server)
raw = (server.stdout.read() if server.stdout else "").strip()
# Leave out the .env tip and the random debugger PIN; they say nothing about the lecture.
kept = [
line
for line in raw.splitlines()
if "Tip:" not in line and "Debugger PIN" not in line
]
out = "\n".join(kept)
entries.append(("python -m flask_day_54.presentation.hello", out + "\n^C"))
terminal_page(
"Terminal - lecture 4: __name__ and __main__",
entries,
OUT / "lecture-04.png",
560,
)
def step5() -> None:
module = "flask_day_54.domain.first_class_functions"
entries = [(f"python -m {module}", run([python(), "-m", module], ROOT))]
terminal_page(
"Terminal - lecture 5: functions as first-class objects",
entries,
OUT / "lecture-05.png",
420,
)
def step6() -> None:
module = "flask_day_54.domain.decorators"
entries = [(f"python -m {module}", run([python(), "-m", module], ROOT))]
terminal_page(
"Terminal - lecture 6: decorators and the @ syntax",
entries,
OUT / "lecture-06.png",
380,
)
STEPS = {1: step1, 2: step2, 3: step3, 4: step4, 5: step5, 6: step6}
if __name__ == "__main__":
os.environ["PYTHONPATH"] = str(ROOT / "src")
for number in [int(a) for a in sys.argv[1:]] or STEPS:
STEPS[number]()