facett-demo/examples/robot_drive.rs
//! Drive facett-demo's robot control channel exactly as the GUI binary does
//! (write a DemoCommand to $FACETT_DEMO_CMD → `poll_control_channel` consumes it →
//! `dump_state` writes $FACETT_DEMO_STATE), printing the real state transitions.
//!
//! This is the EPIC-R robot-UI pattern — the same file-backed cmd/state seam
//! nornir's viz uses (`$NORNIR_VIZ_CMD` / `$NORNIR_VIZ_STATE`) — applied to the
//! facett showcase. It proves CLI/GUI parity: a driver "clicks" by writing a
//! command and "sees what's on screen" by reading the dumped `state_json`, with
//! no pointer and no window.
//!
//! Run: cargo run -p facett-demo --example robot_drive
use facett_demo::control::{self, write_command};
use facett_demo::{DemoCommand, DemoState};
use serde_json::Value;
fn read_state() -> Value {
let raw = std::fs::read_to_string(control::state_path()).unwrap_or_default();
serde_json::from_str(&raw).unwrap_or(Value::Null)
}
fn snap(label: &str, demo: &DemoState) {
// dump_state writes the file the same way the running binary does each frame.
demo.dump_state();
let s = read_state();
let tab = s["deck"]["active"].as_str().or_else(|| s["deck"]["active_title"].as_str());
let pal = s["deck_palette"]["name"].as_str();
let look = s["look"]["preset"].as_str();
let mode = s["look"]["mode"].as_str();
let last = s["last_command"].as_str();
let node = s["deck"]["facets"]["syschart"]["selected"].as_str();
println!(
" {label:<24} | tab={:<12} palette={:<16} look={}/{} last={:?} node={:?}",
tab.unwrap_or("?"),
pal.unwrap_or("—"),
look.unwrap_or("?"),
mode.unwrap_or("?"),
last,
node,
);
}
fn drive(demo: &mut DemoState, cmd: DemoCommand) {
write_command(&cmd).expect("write command file"); // → $FACETT_DEMO_CMD
let applied = demo.poll_control_channel(); // reads + consumes the file, applies it
assert!(applied, "the demo consumed a pending command");
}
fn main() {
// Use a private channel so this demo never collides with anything else.
let pid = std::process::id();
unsafe {
std::env::set_var("FACETT_DEMO_CMD", format!("/tmp/robot_drive_cmd_{pid}.json"));
std::env::set_var("FACETT_DEMO_STATE", format!("/tmp/robot_drive_state_{pid}.json"));
}
println!("channel: CMD={} STATE={}", control::cmd_path(), control::state_path());
let mut demo = DemoState::new();
println!("\navailable tabs: {:?}", demo.deck.titles());
println!("(driving the SAME DemoState the GUI binary wraps, via the file channel)\n");
snap("initial", &demo);
// Switch through three real tab titles (whatever the deck actually has).
let titles: Vec<String> = demo.deck.titles().iter().map(|s| s.to_string()).collect();
for t in titles.iter().take(3) {
drive(&mut demo, DemoCommand { tab: Some(t.clone()), ..Default::default() });
snap(&format!("tab → {t}"), &demo);
}
drive(&mut demo, DemoCommand { palette: Some("cyberpunk-neon".into()), ..Default::default() });
snap("palette → cyberpunk-neon", &demo);
drive(&mut demo, DemoCommand { look: Some("macos-dark".into()), ..Default::default() });
snap("look → macos-dark", &demo);
drive(&mut demo, DemoCommand { command: Some("fx.glow".into()), ..Default::default() });
snap("command → fx.glow", &demo);
// Select a syschart node by id (label-addressable) — auto-focuses its tab.
drive(&mut demo, DemoCommand { select_node: Some("Ingest".into()), ..Default::default() });
snap("select_node → Ingest", &demo);
// One combined write: switch tab + palette + fire a command in a single command.
if let Some(last) = titles.last() {
drive(
&mut demo,
DemoCommand {
tab: Some(last.clone()),
palette: Some("nordic-aurora".into()),
command: Some("raven.send".into()),
..Default::default()
},
);
snap("combined (tab+pal+cmd)", &demo);
}
let bytes = std::fs::metadata(control::state_path()).map(|m| m.len()).unwrap_or(0);
// Cleanup the private channel files.
let _ = std::fs::remove_file(control::cmd_path());
let _ = std::fs::remove_file(control::state_path());
println!("\nfinal state_json was {bytes} bytes — every command round-tripped through the files.");
// ── T1.3 SHOWCASE PROOF ─────────────────────────────────────────────────
// The driving above proves CLI/GUI parity as data. This second leg proves the
// NEW rendering (glass chrome + bloom-glow + smooth-scroll + tween motion) is
// actually ON — by rendering a real tab to a screenshot AND asserting the
// effects as DATA in state_json (never by eyeballing pixels).
prove_showcase_rendering();
}
/// Render the motion-heavy `jobs` tab through egui_kittest, screenshot it, and
/// assert — as DATA — that the T1.3 showcase rendering is wired ON:
/// * the deck's glass/card `chrome` wrap is enabled,
/// * the active look preset is full-effects with a **non-default glass surface**,
/// * the preset's motion durations are non-zero, and
/// * the rendered `jobs` facet reports live smooth-scroll + a selection cross-fade.
fn prove_showcase_rendering() {
use egui_kittest::Harness;
println!("\n── T1.3 showcase proof: render the `jobs` tab, screenshot it, assert effects as DATA ──");
let mut demo = DemoState::new();
demo.show_about = false;
// Drive to the motion-heavy `jobs` tab and select its first row so the
// row-highlight cross-fade is exercised this render (deterministic, no pointer).
assert!(demo.deck.set_active_by_title("jobs"), "the demo has a `jobs` tab");
let first_id = demo.state_json()["deck"]["facets"]["jobs"]["jobs"]
.get(0)
.and_then(|r| r["id"].as_str())
.map(str::to_string);
if let Some(id) = first_id.clone() && let Some(jv) = demo.deck.facet_mut::<facett_jobview::JobList>("jobs") {
jv.select(Some(id));
}
// Render the WHOLE demo through `draw_app` (the same path the GUI binary runs):
// it applies the active look preset (publishing EffectsPolicy::Full so the glass
// chrome paints) and renders the active facet through the deck (chrome wrap +
// bloom + SmoothScroll + the row fades). A few steps let the motion settle.
let mut harness = Harness::builder()
.with_size(egui::vec2(1100.0, 760.0))
.build_ui_state(|ui, demo: &mut DemoState| demo.draw_app(ui), demo);
harness.run_steps(6);
// The visible artifact: a real PNG screenshot of the rendered tab (wgpu). Kept
// best-effort so the DATA assertions below still run on a GPU-less box.
match harness.render() {
Ok(image) => {
let path = std::env::temp_dir().join("robot_drive_jobs.png");
if image::DynamicImage::ImageRgba8(image).save(&path).is_ok() {
let sz = std::fs::metadata(&path).map(|m| m.len()).unwrap_or(0);
assert!(sz > 5_000, "the screenshot PNG is non-trivial (got {sz} bytes)");
println!(" screenshot → {} ({sz} bytes)", path.display());
}
}
Err(e) => println!(" (no GPU for a PNG screenshot here: {e}; asserting effects as DATA only)"),
}
// ── assert the effects as DATA (the real gate) ───────────────────────────
let s = harness.state().state_json();
// 1) The active tab really is `jobs`.
assert_eq!(s["deck"]["active"].as_str(), Some("jobs"), "rendered the jobs tab");
// 2) The glass/card chrome wrap around the active facet is ON.
assert_eq!(s["deck"]["fx"]["chrome"].as_bool(), Some(true), "deck glass chrome is wired ON");
// 3) The active look preset is full-effects (non-`None`) with a NON-DEFAULT
// glass surface — the proof the new glass has something to render.
let look = &s["look"];
assert_eq!(look["effects"].as_str(), Some("Full"), "look preset is full-effects (non-None)");
assert_eq!(look["allows_transparency"].as_bool(), Some(true), "transparency (glass) allowed");
let surface = &look["surface"];
assert!(
!surface.is_string() || surface.as_str() != Some("Opaque"),
"the demo theme declares a NON-default glass surface, got {surface}"
);
assert!(surface.get("Frosted").is_some() || surface.get("Tint").is_some(), "surface is glass (Frosted/Tint): {surface}");
// 4) The preset's motion is real (non-zero durations → tween/scroll motion runs).
let dur = look["motion"]["duration"].as_f64().unwrap_or(0.0);
assert!(dur > 0.0, "non-zero standard motion duration, got {dur}");
// 5) The rendered `jobs` facet reports LIVE motion state: smooth scroll wired,
// and the selected row's cross-fade is being tracked (motion exercised).
let motion = &s["deck"]["facets"]["jobs"]["motion"];
assert_eq!(motion["smooth"].as_bool(), Some(true), "jobs row list uses SmoothScroll");
assert!(motion["scroll_target"].is_number(), "smooth-scroll offset state is present");
if first_id.is_some() {
let sel = motion["sel_fades"].as_u64().unwrap_or(0);
assert!(sel >= 1, "the selected row's cross-fade is tracked (motion exercised), got {sel}");
}
println!(
" PROVEN as data: chrome=on effects={} surface={} motion.duration={dur} jobs.sel_fades={}",
look["effects"], surface, motion["sel_fades"]
);
println!(" → glass + bloom-glow + smooth-scroll + tween motion are ON, asserted as DATA.");
}