LAW4b — Viz Self-Screenshot (ViewportCommand::Screenshot)
Design
LAW4b is the live-app half of the screenshot-proof law (LAW4):
"Screenshot PROOF, egui-native only —
egui_kittestwgpu snapshot (tests) + liveViewportCommand::Screenshot(app screenshots itself) → PNG inrobot_test_results. Never KDE/spectacle/compositor grabs."
The test-side half (LAW4) already shipped: egui_kittest wgpu snapshot harness (tests/viz_snapshots.rs). This feature ships the live-app half.
Architecture
Three access paths all converge on the same state machine:
CLI: nornir viz --screenshot <path>
└─ writes VizCommand::Screenshot to $NORNIR_VIZ_CMD
└─ nornir-ui picks it up on frame 1
Keyboard: Ctrl+Shift+S (inside the running viz)
└─ calls app.request_screenshot(Some(path))
Control channel: viz.click MCP / robot driver
└─ writes VizCommand::Screenshot to $NORNIR_VIZ_CMD
All three → request_screenshot(out_path) → screenshot_pending = true
Per-frame loop (in draw_ui):
poll_control_channel()— consumes anyVizCommand::Screenshotand callsrequest_screenshot(out_path).- Ctrl+Shift+S keyboard check — calls
request_screenshot(…)directly. - If
screenshot_pending == true→ firectx.send_viewport_cmd(ViewportCommand::Screenshot(UserData::default())). - Drain
ctx.input(|i| i.raw.events)forEvent::Screenshot { image, .. }. - On receipt: encode
ColorImage→ PNG bytes (via theimagecrate, already in thevizfeature graph), write toout_pathon disk (whenSome), and append to the warehouserobot_test_resultsblob store (LAW5 persist-to-warehouse). - Set
screenshot_pending = false,screenshot_last_path = actual_path.
State JSON shape
{
"screenshot": {
"pending": false,
"last_path": "/tmp/nornir_viz_screenshot_20260614_120000.png"
}
}
A robot-driver polls screenshot.pending == false to know the PNG is ready.
Data types
VizCommand::Screenshot
{ "screenshot": { "out_path": "/tmp/out.png" } }
out_path is optional; omit for warehouse-only mode.
ScreenshotRequest
pub struct ScreenshotRequest {
pub out_path: Option<String>,
}
Warehouse persistence
Screenshots are appended to the robot_test_results blob store via put_blobs_dedup (content-addressed, dedup'd by sha256). The snapshot_id is screenshot-<YYYYMMDD-HHMMSS>, the filename is the basename of out_path (or screenshot.png for warehouse-only mode).
Remote/empty-local mode: the warehouse append is skipped (the blob store is local); only the disk write happens.
Guide
Take a screenshot from the CLI
nornir viz --screenshot /tmp/my_screenshot.png
# Starts the viz, takes a screenshot on frame 1, writes PNG, continues running.
Take a screenshot with the keyboard shortcut
Press Ctrl+Shift+S while the viz window is focused. The PNG is written to /tmp/nornir_viz_screenshot_<YYYYMMDD_HHMMSS>.png and appended to the warehouse.
Drive a screenshot from a robot script
Write a VizCommand::Screenshot to $NORNIR_VIZ_CMD:
# Default path: /tmp/nornir_viz_cmd.json
echo '{"screenshot":{"out_path":"/tmp/robot_shot.png"}}' > /tmp/nornir_viz_cmd.json
# The viz picks it up within 250 ms (one poll cycle), takes the screenshot,
# and writes the PNG.
Then poll $NORNIR_VIZ_STATE until screenshot.pending == false:
until python3 -c "
import json, sys
s = json.load(open('/tmp/nornir_viz_state.json'))
sys.exit(0 if not s['screenshot']['pending'] else 1)
"; do sleep 0.25; done
echo "Screenshot ready at $(python3 -c \"import json; print(json.load(open('/tmp/nornir_viz_state.json'))['screenshot']['last_path'])\")"
Test the control+state_json path headlessly
cargo test --features viz --test viz_law4b_screenshot -- --nocapture
The four tests drive apply_command and request_screenshot against a headless UrdrThreadsApp (no GPU, no window) and assert state_json.
Assertions
state_json["screenshot"]["pending"]flips totruethe frame a screenshot is requested and back tofalsethe frameEvent::Screenshotis consumed.state_json["screenshot"]["last_path"]holds the last successfully written path, ornullbefore the first screenshot.