Repository navigation
Conversation
…without a display On headless machines (CI, WSL2, remote boxes), tools/octo shot cannot grab host pixels. This tool renders the live /snap widget tree (labels, buttons, text inputs at real positions with real text) to a PNG for review and iteration — honestly labeled as a redraw, not host pixels. Battle-tested through a full hackathon project: ~30 regression rounds and all submission screenshots were produced with an earlier app-specific version. Usage: python3 tools/snap-to-png.py --port 8141 --out shot.png
BH3GEI
left a comment
There was a problem hiding this comment.
Thanks for putting this together — the headless gap is real (/g answers "grab timeout" when there is no window server to render), and building the picture from the live widget tree instead of host pixels is the right shape for it.
I verified the tool end to end: built card-host from App Hub main against the locked runtime (6cf2d90a), ran a fresh octo new app with --hidden --detach, and compared the tool's PNG against a real octo shot of the same frame — title, input, button and list positions and text all match; typing into the input and clicking Add both re-render correctly; the card-studio /snap fixtures (clipped-phone, digest-glance, clipped-glance) render correctly. The Splash filter is doing real work (the card container carries the whole script source as its t), and so is the caption filter (with the caption visible, the "Card host [remote]" label has a live rect at [158,5,119,22]). python -m unittest discover -s tools (48 tests) and tools/check-links.py still pass on the branch.
Three concerns before this could be approved:
-
Password inputs render their cleartext value —
tools/snap-to-png.py:101-103. In the/snappayload,valis the widget's raw text (widget_tree.rssnapshot:value = Some(widget.text()), which forTextInputis the unobfuscated content), whiletisdisplay_text()(makepadtext_input.rs:921: password inputs come back as•).shown = val if val else textprefers the raw value, so a password field draws its cleartext into a PNG whose purpose is sharing. Note thattalone already carries everything this branch needs — non-empty input renders the text itself, empty renders the placeholder — so thevalpreference has no upside and this one downside. Reproduced with a payload shaped exactly as the serializer emits it (passwordTextInputwitht: "••••••••",val: "hunter2-super-secret"): the output PNG shows the cleartext. -
The ancestor app's text heuristics are still in the "generalized" tool —
tools/snap-to-png.py:105.text.startswith(("AI", "已", "把", "("))andwid in ("hint",)demote matching labels to the small grey style. These look like qianxian-guardian's status-line prefixes ("hint"is not a platform id — its only other appearance in the repo is an unrelated example app's widget). On any other app they hit ordinary content: I renderedAI regulation news,已完成的任务清单,把数据同步打开,(注释)说明文字— all four come out demoted, whileNormal body textrenders normally. That contradicts the PR's "no app-specific theming" claim; could this branch come out (or move behind a flag)? -
The redraw is labelled everywhere except in the PNG itself. The honest "not host pixels" framing lives in the docstring, the console line and the README row — all outside the artifact. The PNG defaults to 412x892 (the card-host window size), styled credibly, and
--noteis optional and arbitrary.docs/PUBLISHING.mdis explicit that "the gate does not judge whether the screenshots are real captures … Reviewers do" — and at 1x a redraw is indistinguishable from anocto shotcapture. Since screenshots are review evidence in this repo's flow, could the tool mark the artifact itself (e.g. a small permanent corner caption likewidget-tree redraw)? That would let the honesty claim travel with the file.
Smaller notes, none blocking on their own:
README.zh-CN.mdhas the mirrored tools table (lines 365-367) but no row for the new tool — the EN and zh tables have been kept in sync so far.FONT_PATHSlists Linux paths only, so on macOS/Windows CJK falls back to the bitmap font and renders as tofu (confirmed on macOS)./System/Library/Fonts/PingFang.ttcandC:\Windows\Fonts\msyh.ttcwould cover the other platforms the repo runs CI on.- Failure modes print raw tracebacks (nothing listening on the port, non-JSON reply, missing
"s"key) and--outinto a missing directory raises; theoctofamily prints a one-line actionable hint andmkdirs the output parent. - The output line counted 34 widgets for the
clipped-phonefixture while 13 actually get drawn (the count is taken before the rect/text draw filters). - Text truncation (
[:24]/[:40]/[:60]) has no ellipsis, and a label clipped to a narrow rect still draws its full text (the 12-px-wide label inclipped-glancepaints its whole headline), which sits a little oddly next to the "correctly absent" framing.
The core path — payload contract, live refresh, ordering, the Splash/caption filters — checks out against the serializer source and real captures; with the three concerns above addressed this would be a welcome addition.
…ling)
Three review concerns:
1. Password inputs no longer leak cleartext: the raw snapshot value
(val) is never painted — text inputs draw the snapshot's display
text (t), which is the • mask for passwords. val now serves only
as an is-it-empty flag for ink color.
2. App-specific text heuristics removed: no more startswith prefixes
or widget-id demotion carried over from the ancestor project;
styling is uniform (title rule + body), per the neutral-palette claim.
3. The artifact labels itself: a permanent bottom band reads
"widget-tree redraw · not host pixels", drawn last so content
cannot overpaint it — the honesty claim now travels with the PNG.
Smaller notes:
- README.zh-CN.md tools table gained the mirrored row (kept in sync).
- FONT_PATHS covers macOS (PingFang/STHeiti) and Windows (msyh/simhei)
so CJK no longer falls back to tofu there.
- Failure modes print one-line actionable hints (unreachable port /
unexpected payload) and --out parents are mkdir'd.
- The output line now counts drawn widgets ("N drawn of M").
- Truncation is width-based with an ellipsis, and clipped narrow
labels no longer paint their full text.
Verified: password-shaped payload renders the mask (not the secret),
the four formerly-demoted prefixes render as normal body text,
python -m unittest discover -s tools (48) and tools/check-links.py
pass, and a live card-host render redraws correctly.
|
Thanks for the thorough review — all three concerns are addressed in fcb4e2a:
|
What
Add
tools/snap-to-png.py: render a running script app's live/snapwidget tree to a PNG — for review and iteration on machines without a graphical session (headless CI, WSL2, remote boxes), wheretools/octo shotcannot read back host pixels.Why
octo shotneeds the render backend to produce frames; on headless machines it times out ("grab timeout"), so a working app cannot show any picture of itself. The widget tree (/snap) is fully available headless — this tool draws it honestly: labels, buttons and text inputs at their real positions with their real text.Honest limitation (stated in the header, the output line and the README row): this is a redraw of the live widget tree, not host pixels — the palette is neutral, and anything a virtualized list has scrolled out of the viewport is (correctly) absent, which makes the tool double as a "what is on screen right now" probe.
Usage
python3 tools/snap-to-png.py --port 8141 --out shot.png [--width 412] [--height 892] [--note "caption"]Requires Pillow. CJK-capable fonts are picked from common system paths when present.
Battle-tested
Developed and used through a full GOSIM Agentic App Hackathon project (kkkkikun/qianxian-guardian): an app-specific ancestor of this tool produced all submission screenshots and ~30 regression rounds' worth of "what changed on screen" evidence, including on WSL2 where host-pixel grabs are unavailable. This PR generalizes it: neutral palette (no app-specific theming), size flags, and a caption option.
Checklist
python3 tools/snap-to-png.py --port <port> --out x.pngagainst a running card-host (verified live)