Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_austenite/dev

6.1 KiB, 45 runs, executable

created by r1870400018:36504, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1#!/usr/bin/env bash
2# ┌──────────────────────────────────────────────┐
3# │ Live-preview a Typst document in the browser │
4# └──────────────────────────────────────────────┘
5#
6# Austenite's answer to `typst watch`. Watches a Typst source, recompiles it with
7# the compiler on every save, rasterises the pages, and serves an auto-refreshing
8# preview over HTTP -- so the loop is edit, save, and see the set page a moment
9# later, from this machine or any other on the network.
10#
11# ./dev [SOURCE.typ] default: samples/manuscript.typ
12#
13# Environment: PORT (default 8137), DPI (default 130), CARGO_TARGET_DIR.
14# A compile error is shown in the browser, in full, rather than a stale page.
15
16set -euo pipefail
17
18CRATE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
19SRC="${1:-$CRATE/samples/manuscript.typ}"
20PORT="${PORT:-8137}"
21DPI="${DPI:-130}"
22
23if [[ "$SRC" != *.typ ]]; then
24 echo "dev: the source should be a .typ file, not '$SRC'." >&2
25 exit 1
26fi
27if [[ ! -f "$SRC" ]]; then
28 echo "dev: no such file: $SRC" >&2
29 exit 1
30fi
31
32# The preview lives under the cargo target cache, off the source tree: not
33# synced, not tmpfs, and cleared fresh each run.
34SLOT="${RC_SLOT:-solo}"
35TARGET="${CARGO_TARGET_DIR:-$HOME/.cache/cargo-targets/$SLOT/austenite}"
36PREVIEW="$TARGET/dev-preview"
37AUSTENITE="$TARGET/debug/austenite"
38LOCALSERVE="$TARGET/debug/localserve"
39
40rm -rf "$PREVIEW"
41mkdir -p "$PREVIEW"
42
43cleanup() { kill $(jobs -p) 2>/dev/null || true; }
44trap cleanup EXIT INT TERM
45
46# Build the compiler once. A warm build is a second or two; a cold one longer.
47echo "dev: building austenite ..."
48( cd "$CRATE" && cargo build -q -p oxedyne_fe2o3_austenite --bin austenite )
49if [[ ! -x "$AUSTENITE" ]]; then
50 echo "dev: austenite binary not found at $AUSTENITE after build." >&2
51 exit 1
52fi
53
54# The static preview server: fe2o3's own std-only loopback/LAN server, built lean
55# (no async runtime, no TLS) as a drop-in for `python3 -m http.server`.
56echo "dev: building localserve ..."
57( cd "$CRATE" && cargo build -q -p oxedyne_fe2o3_net --no-default-features --bin localserve )
58if [[ ! -x "$LOCALSERVE" ]]; then
59 echo "dev: localserve binary not found at $LOCALSERVE after build." >&2
60 exit 1
61fi
62
63# The preview page: it polls a version stamp and, when it changes, redraws the
64# pages (cache-busted) or the compile error. Written once; the loop only updates
65# the data files beside it.
66cat > "$PREVIEW/index.html" <<'HTML'
67<!doctype html>
68<meta charset="utf-8">
69<title>Austenite live preview</title>
70<style>
71 :root { color-scheme: dark; }
72 body { margin: 0; background: #4a4d52; font: 14px system-ui, sans-serif; }
73 #bar { position: fixed; inset: 0 0 auto 0; z-index: 9;
74 background: #1e1f22; color: #cfd2d6; padding: 7px 14px;
75 box-shadow: 0 1px 6px rgba(0,0,0,.4); }
76 #bar b { color: #f0f2f4; }
77 #pages { padding: 46px 0 28px; display: flex; flex-direction: column;
78 align-items: center; gap: 18px; }
79 img { max-width: min(92vw, 900px); background: #fff;
80 box-shadow: 0 3px 16px rgba(0,0,0,.5); }
81 .err { white-space: pre-wrap; color: #ff9b9b; background: #2b1717;
82 border: 1px solid #5a2a2a; border-radius: 8px;
83 padding: 14px 16px; margin: 60px 16px; max-width: 900px;
84 font: 13px ui-monospace, monospace; }
85</style>
86<div id="bar">Austenite &mdash; <b id="stamp">starting &hellip;</b></div>
87<div id="pages"></div>
88<script>
89let cur = "";
90async function tick() {
91 try {
92 const v = (await (await fetch("version.txt", { cache: "no-store" })).text()).trim();
93 if (v && v !== cur) { cur = v; await render(v); }
94 } catch (e) { /* server between writes; try again */ }
95 setTimeout(tick, 600);
96}
97async function render(v) {
98 const m = await (await fetch("pages.json?" + v, { cache: "no-store" })).json();
99 document.getElementById("stamp").textContent =
100 m.error ? (m.stamp + " — compile error") : (m.stamp + " — " + m.pages + " page(s)");
101 const box = document.getElementById("pages");
102 box.innerHTML = "";
103 if (m.error) {
104 const d = document.createElement("div");
105 d.className = "err";
106 d.textContent = m.error;
107 box.appendChild(d);
108 return;
109 }
110 for (let i = 1; i <= m.pages; i++) {
111 const im = new Image();
112 im.src = "page-" + String(i).padStart(3, "0") + ".png?" + v;
113 box.appendChild(im);
114 }
115}
116tick();
117</script>
118HTML
119
120json_escape() { python3 -c 'import json,sys; sys.stdout.write(json.dumps(sys.stdin.read()))'; }
121
122compile() {
123 local ts err rc n f i
124 ts="$(date '+%H:%M:%S')"
125 rm -f "$PREVIEW"/page-*.png "$PREVIEW"/raw-*.png
126 set +e
127 err="$("$AUSTENITE" "$SRC" "$PREVIEW/out" 2>&1 >/dev/null)"; rc=$?
128 set -e
129 if [[ $rc -eq 0 && -f "$PREVIEW/out/document.pdf" ]]; then
130 pdftoppm -r "$DPI" -png "$PREVIEW/out/document.pdf" "$PREVIEW/raw" >/dev/null 2>&1 || true
131 i=0
132 for f in $(ls -v "$PREVIEW"/raw-*.png 2>/dev/null); do
133 i=$((i + 1))
134 mv "$f" "$PREVIEW/page-$(printf '%03d' "$i").png"
135 done
136 printf '{"stamp":"%s","pages":%d}\n' "$ts" "$i" > "$PREVIEW/pages.json"
137 echo "dev: [$ts] compiled $i page(s)"
138 else
139 printf '{"stamp":"%s","pages":0,"error":%s}\n' "$ts" "$(printf '%s' "$err" | json_escape)" \
140 > "$PREVIEW/pages.json"
141 echo "dev: [$ts] compile error:"
142 printf '%s\n' "$err"
143 fi
144 date +%s%N > "$PREVIEW/version.txt"
145}
146
147# Serve the preview directory. `--lan` binds 0.0.0.0 so a phone on the same network
148# can reach the preview, matching what the python server did before.
149( exec "$LOCALSERVE" "$PREVIEW" --port "$PORT" --lan >/dev/null 2>&1 ) &
150
151compile # first render, so the page is there the moment the browser opens
152
153echo
154echo "dev: watching $(basename "$SRC")"
155echo "dev: preview at http://localhost:$PORT/ (Ctrl-C to stop)"
156echo
157
158# Poll the source's modification time; recompile whenever it changes. `stat` is
159# cheaper than any file-watch daemon and needs nothing installed.
160last="$(stat -c %Y "$SRC" 2>/dev/null || echo 0)"
161while true; do
162 sleep 0.5
163 now="$(stat -c %Y "$SRC" 2>/dev/null || echo 0)"
164 if [[ "$now" != "$last" ]]; then
165 last="$now"
166 compile
167 fi
168done