dwarves-gui/src/robot_watch.rs
//! **`--robot-watch`** — replay the recorded per-tab robot scenarios in a REAL,
//! VISIBLE eframe window, so a human WATCHES the robot navigate the dwarves shell
//! (World-graph → Warehouse → Forge → … → Profile) instead of the headless
//! off-screen `egui_kittest` drive.
//!
//! This is the CHOOSABLE sibling of the headless `--robot-remote <socket>` mount:
//! both drive the SAME [`ShellApp`](crate::robot_shell::ShellApp) through the SAME
//! [`shell_draw`](crate::robot_shell::shell_draw) closure and the SAME recorded
//! [`crate::scenarios::dwarves_tab_scenarios`] — the ONLY difference is
//! [`RunMode`]: `Watch` opens the paced, visible window;
//! [`RunMode::Headless`](nornir_robotui::RunMode::Headless) fills the CI matrix.
//! The mechanism lives entirely in the shared `nornir-robotui` crate
//! ([`run_scenario_mode`]); this module only WIRES dwarves into it.
//!
//! ## How to launch
//! ```text
//! dwarves-ui --robot-watch [scenario.toml] # visible window replays the tab tour
//! dwarves-ui --robot-mode watch # same, no scenario file
//! NORNIR_ROBOT_MODE=watch dwarves-ui # env selector
//! ROBOTUI_WATCH=1 dwarves-ui # env selector (robotui's own toggle)
//! ```
//! Pacing/hold knobs come from `nornir_robotui::WatchConfig::from_env`:
//! `ROBOTUI_WATCH_DELAY_MS` (per step), `ROBOTUI_WATCH_LINGER_MS`,
//! `ROBOTUI_WATCH_HOLD=1` (keep the window open until closed by hand).
use std::path::Path;
use nornir_robotui::{run_scenario_mode, RunMode, RunOutcome, Scenario, WatchConfig};
use crate::robot_mode::all_tabs_scenario;
use crate::robot_shell::{build_shell_app, shell_draw};
/// Open the visible watch window and replay `scenario_path` (a `scenario.toml`), or
/// the flattened tab tour when `None`, against the REAL dwarves shell. Blocks on the
/// native event loop until the window closes (winit requires this run on the main
/// thread — `main` hands control here and returns after).
pub fn run_robot_watch(scenario_path: Option<&Path>) -> Result<(), String> {
let scenario = match scenario_path {
Some(p) => Scenario::load_toml(p).map_err(|e| e.to_string())?,
// The opt-in installer-RAID KVM reel is RETIRED (2026-08-06): it drove the ⚒
// pane's "▶ Build & Boot" button and that pane is gone. The engine it exercised
// ([`crate::boot_run::EddaRaidInstallerLauncher`]) is untouched and the scene
// reaches it through the Console pane, but no recorded WATCH reel drives it yet.
None => all_tabs_scenario(),
};
let mut cfg = WatchConfig::from_env();
cfg.title = "dwarves-ui — robot watch".to_string();
// Lay the WHOLE tab strip out on-screen: facett-app's tab strip scrolls, and a tab
// scrolled past the clip edge is not hit-testable, so a narrow window would leave the
// last tabs unreachable — the SAME width the headless mount uses to keep every tab in
// the tree (see `robot_remote::robot_session`).
cfg.size = egui::vec2(3000.0, 1300.0);
eprintln!(
"dwarves-gui: robot-watch — replaying {} step(s) in a visible window",
scenario.steps.len()
);
match run_scenario_mode(
build_shell_app(),
shell_draw,
&scenario,
"dwarves-gui",
RunMode::Watch,
cfg,
)
.map_err(|e| e.to_string())?
{
RunOutcome::Watched => Ok(()),
// The chosen mode is Watch, so this arm is unreachable in practice; treat any
// headless fall-through as a clean close rather than a panic.
RunOutcome::Headless(_) => Ok(()),
}
}
// `watch_requested()` USED TO LIVE HERE and it is why `--robot-mode headless` opened a
// window: it parsed `--robot-mode`, compared the value against `"watch"` alone, and
// returned "no watch requested" for everything else — which `main` reads as "open the
// GUI". The parse now belongs to [`crate::robot_mode::requested`], which knows every
// mode and REFUSES an unknown one. This module keeps only the window it owns.