diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c70996..676333d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,18 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`). ## Unreleased +### Added + +- **`dot-watch` helper** (`/usr/local/bin/dot-watch`) — auto-rerenders a + Graphviz `.dot` file to PNG on every save via mtime polling (no + `inotify` dependency). pi-studio renders Mermaid natively but has no + DOT renderer; since its markdown preview displays local PNG/JPG/GIF/WEBP + images, this closes the loop for Graphviz: edit `.dot` → `dot-watch` + regenerates `.png` → Studio *refresh-from-disk* shows the update. + `graphviz` was already in the base image, so no new package. Baked into + `Dockerfile.base` following the `studio-expose` pattern; documented in + the README Studio section. + ## v1.1.0 — 2026-06-10 ### Added — `:latest-studio` variant diff --git a/Dockerfile.base b/Dockerfile.base index 57a5e0f..191d03c 100644 --- a/Dockerfile.base +++ b/Dockerfile.base @@ -48,6 +48,8 @@ ENV DEBIAN_FRONTEND=noninteractive # preview/export pipelines and broadly useful for any # agent-driven document workflow. ~200 MB. # graphviz — `dot` rendering for many diagram tools. ~10 MB. +# See the bundled `dot-watch` helper for live .dot -> PNG +# re-render (handy with pi-studio's image preview). # imagemagick — image conversion / resizing for thumbnails, etc. ~50 MB. # yq — YAML-aware companion to jq. # socat — TCP relay. Powers `studio-expose`, which bridges @@ -436,10 +438,12 @@ COPY rootfs/home/developer/.inputrc /etc/skel-devbox/.inputrc # ── Entrypoint ──────────────────────────────────────────────────────── COPY rootfs/usr/local/lib/pi-devbox/ /usr/local/lib/pi-devbox/ COPY rootfs/usr/local/bin/studio-expose /usr/local/bin/studio-expose +COPY rootfs/usr/local/bin/dot-watch /usr/local/bin/dot-watch COPY entrypoint.sh /usr/local/bin/entrypoint.sh COPY entrypoint-user.sh /usr/local/bin/entrypoint-user.sh RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/entrypoint-user.sh \ /usr/local/bin/studio-expose \ + /usr/local/bin/dot-watch \ /usr/local/lib/pi-devbox/*.sh 2>/dev/null || true # Start as root — entrypoint adjusts UID/GID then drops to developer diff --git a/README.md b/README.md index 43dd6ec..e7b62e5 100644 --- a/README.md +++ b/README.md @@ -276,6 +276,26 @@ Assuming the compose file publishes `127.0.0.1:8765:8765` (see method B): > which is **not** in `-studio` (only the planned `-studio-tex`). HTML > export, KaTeX, Mermaid, and all REPL features work without it. +### Graphviz diagrams in Studio: `dot-watch` + +pi-studio renders **Mermaid** natively but has **no Graphviz/DOT renderer**. +Its markdown preview *does* render local image links (`.png`/`.jpg`/`.gif`/ +`.webp`), so the workflow for Graphviz is: write a `.dot` file, render it to +PNG with `dot`, and preview the PNG (directly, or embedded in a markdown +file). The bundled **`dot-watch`** helper automates the re-render so edits +show up on Studio's *refresh-from-disk*: + +```bash +dot-watch graph.dot # dot engine, 150 dpi -> graph.png +dot-watch graph.dot neato 200 # pick layout engine + dpi +``` + +It polls the file's mtime (no `inotify` dependency) and regenerates +`.png` on every save, printing timestamped status and indenting any +DOT syntax errors instead of crashing. Then in Studio: open the PNG (or a +`.md` that embeds it) and hit **refresh-from-disk** after each edit. +Note: SVG is **not** in Studio's local-image-link allowlist — use PNG. + ## docker-compose.yml — basic shape ```yaml diff --git a/rootfs/usr/local/bin/dot-watch b/rootfs/usr/local/bin/dot-watch new file mode 100755 index 0000000..0b1dd23 --- /dev/null +++ b/rootfs/usr/local/bin/dot-watch @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# dot-watch — auto-rerender a graphviz .dot file to PNG on every save. +# +# WHY THIS EXISTS +# pi-studio renders mermaid natively but has no graphviz/DOT renderer. +# Its markdown preview DOES render local image links (.png/.jpg/.gif/.webp), +# and the editor offers "refresh from disk". This helper closes the loop: +# edit a .dot file -> dot-watch regenerates .png -> hit refresh in +# Studio to see the update. Uses mtime polling (no inotify dependency, +# which isn't in the trixie-slim base). +# +# USAGE +# dot-watch [layout] [dpi] +# layout: dot|neato|fdp|circo|twopi (default: dot) +# dpi: output resolution (default: 150) +# env: DOT_WATCH_INTERVAL= poll interval (default: 1) +# +# EXAMPLES +# dot-watch /workspace/graph.dot +# dot-watch graph.dot neato 200 + +set -euo pipefail + +SRC="${1:?usage: dot-watch [layout] [dpi]}" +LAYOUT="${2:-dot}" +DPI="${3:-150}" + +[[ -f "$SRC" ]] || { echo "error: no such file: $SRC" >&2; exit 1; } +command -v "$LAYOUT" >/dev/null || { echo "error: layout engine '$LAYOUT' not found" >&2; exit 1; } + +OUT="${SRC%.dot}.png" +INTERVAL="${DOT_WATCH_INTERVAL:-1}" # seconds between polls +ERRLOG="$(mktemp -t dot-watch.XXXXXX.err)" +trap 'rm -f "$ERRLOG"' EXIT + +render() { + if "$LAYOUT" -Tpng -Gdpi="$DPI" "$SRC" -o "$OUT" 2> "$ERRLOG"; then + printf '[%s] rendered -> %s\n' "$(date +%H:%M:%S)" "$OUT" + else + printf '[%s] DOT error:\n' "$(date +%H:%M:%S)" + sed 's/^/ /' "$ERRLOG" + fi +} + +# portable mtime (GNU stat, fallback to BSD stat) +mtime() { stat -c %Y "$1" 2>/dev/null || stat -f %m "$1" 2>/dev/null; } + +echo "watching $SRC ($LAYOUT, ${DPI}dpi) -> $OUT [Ctrl-C to stop]" +render +last="$(mtime "$SRC")" +while true; do + sleep "$INTERVAL" + [[ -f "$SRC" ]] || continue + now="$(mtime "$SRC")" + if [[ "$now" != "$last" ]]; then + last="$now" + render + fi +done