use crate::automation::command::{app_command, parse_args, schema_of, CommandSpec, Ctx, NoArgs, Notice, Outcome};
use serde::{Deserialize, Serialize};
use serde_json::{json, Map, Value};
use std::collections::BTreeMap;
#[derive(Serialize, schemars::JsonSchema)]
pub struct StateEntry {
pub name: String,
pub doc: String,
pub typed: bool,
pub bytes: usize,
}
#[derive(Serialize, schemars::JsonSchema)]
pub struct StateList {
pub entries: Vec<StateEntry>,
}
#[derive(Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct StateGetArgs {
pub names: Vec<String>,
}
#[derive(Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct StateSchemaArgs {
pub names: Vec<String>,
}
#[derive(Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct HitRectsArgs {
#[serde(default)]
pub prefix: Option<String>,
}
#[derive(Serialize, schemars::JsonSchema)]
pub struct HitRects {
pub rects: BTreeMap<String, [f32; 4]>,
pub ppp: f32,
pub frame: u64,
}
#[derive(Serialize, schemars::JsonSchema)]
pub struct Notices {
pub notices: Vec<Notice>,
}
fn state_list(ctx: &mut Ctx<'_>, args: Value) -> Result<Outcome, String> {
let _: NoArgs = parse_args(args)?;
let mut entries: BTreeMap<String, StateEntry> = BTreeMap::new();
for (name, doc) in crate::automation::state_blobs::STATE_BLOBS {
let bytes = ctx.app.state_blob_json(name, ctx.egui).map_or(0, |json| json.len());
entries.insert(name.to_string(), StateEntry { name: name.to_string(), doc: doc.to_string(), typed: false, bytes });
}
for (name, p) in crate::automation::registry::lock().iter() {
entries.entry(name.to_string()).or_insert_with(|| StateEntry { name: name.to_string(), doc: p.doc.to_string(), typed: p.schema.is_some(), bytes: p.json.len() });
}
serde_json::to_value(StateList { entries: entries.into_values().collect() }).map(Outcome::Done).map_err(|e| e.to_string())
}
fn blob_text(ctx: &mut Ctx<'_>, name: &str) -> Option<(String, &'static str, Option<Value>)> {
if let Some(json) = ctx.app.state_blob_json(name, ctx.egui) {
return Some((json, crate::automation::state_blobs::doc(name), None));
}
let reg = crate::automation::registry::lock();
reg.get(name).map(|p| (p.json.clone(), p.doc, p.schema.clone()))
}
fn state_get_result_schema() -> Value {
json!({"type":"object","additionalProperties":true,
"description":"Requested state names mapped to publisher JSON or raw text; consult state_schema for typed publishers."})
}
fn toasts_raise_result_schema() -> Value {
json!({"type":"object","properties":{"raised":{"type":"integer","minimum":0}},
"required":["raised"],"additionalProperties":false})
}
fn toasts_dismiss_result_schema() -> Value {
json!({"type":"object","properties":{"dismissed":{"type":"integer","minimum":0}},
"required":["dismissed"],"additionalProperties":false})
}
fn state_get(ctx: &mut Ctx<'_>, args: Value) -> Result<Outcome, String> {
let a: StateGetArgs = parse_args(args)?;
let mut out = Map::new();
for name in a.names {
match blob_text(ctx, &name) {
Some((json, _, _)) => {
let v = serde_json::from_str::<Value>(&json).unwrap_or(Value::String(json));
out.insert(name, v);
}
None => return Err(format!("no state blob `{name}` (see state_list)")),
}
}
Ok(Outcome::Done(Value::Object(out)))
}
fn state_schema_result_schema() -> Value {
json!({"type":"object","additionalProperties":{
"type":"object",
"properties":{"doc":{"type":"string"},"schema":{},"example":{}},
"required":["doc","schema","example"],"additionalProperties":false
}})
}
fn state_schema(ctx: &mut Ctx<'_>, args: Value) -> Result<Outcome, String> {
let a: StateSchemaArgs = parse_args(args)?;
let mut out = Map::new();
for name in a.names {
let (json, doc, schema) = blob_text(ctx, &name).ok_or_else(|| format!("no state blob `{name}` (see state_list)"))?;
out.insert(
name,
json!({
"doc": doc,
"schema": schema,
"example": serde_json::from_str::<Value>(&json).unwrap_or(Value::Null),
}),
);
}
Ok(Outcome::Done(Value::Object(out)))
}
fn hit_rects(ctx: &mut Ctx<'_>, args: Value) -> Result<Outcome, String> {
let a: HitRectsArgs = parse_args(args)?;
let rects = crate::automation::registry::lock().hit_rects(a.prefix.as_deref());
serde_json::to_value(HitRects { rects, ppp: ctx.egui.pixels_per_point(), frame: ctx.egui.cumulative_frame_nr() })
.map(Outcome::Done)
.map_err(|e| e.to_string())
}
fn hit_key_docs_result_schema() -> Value {
json!({"type":"object","properties":{"keys":{"type":"array","items":{
"type":"object","properties":{
"panel":{"type":"string"},"prefix":{"type":"string"},
"meaning":{"type":"string"},"command":{"type":["string","null"]}
},"required":["panel","prefix","meaning","command"],"additionalProperties":false
}}},"required":["keys"],"additionalProperties":false})
}
fn hit_key_docs(_ctx: &mut Ctx<'_>, args: Value) -> Result<Outcome, String> {
let _: NoArgs = parse_args(args)?;
Ok(Outcome::Done(json!({ "keys": crate::automation::hit_keys::hit_key_docs_json() })))
}
fn notices(ctx: &mut Ctx<'_>, args: Value) -> Result<Outcome, String> {
let _: NoArgs = parse_args(args)?;
let notices = ctx.app.automation.take_notices();
serde_json::to_value(Notices { notices }).map(Outcome::Done).map_err(|e| e.to_string())
}
fn toasts_dismiss(ctx: &mut Ctx<'_>, args: Value) -> Result<Outcome, String> {
let _: NoArgs = parse_args(args)?;
let n = ctx.app.toasts.dismiss_all();
Ok(Outcome::Done(json!({ "dismissed": n })))
}
#[derive(Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ToastsRaiseArgs {
pub texts: Vec<String>,
#[serde(default)]
pub severity: ToastSeverity,
}
#[derive(Deserialize, schemars::JsonSchema, Default, Clone, Copy)]
#[serde(rename_all = "lowercase")]
pub enum ToastSeverity {
#[default]
Error,
Warning,
Info,
}
fn toasts_raise(ctx: &mut Ctx<'_>, args: Value) -> Result<Outcome, String> {
let a: ToastsRaiseArgs = parse_args(args)?;
let n = a.texts.len();
let severity = match a.severity {
ToastSeverity::Error => brep_render::engine_state::NoticeSeverity::Error,
ToastSeverity::Warning => brep_render::engine_state::NoticeSeverity::Warning,
ToastSeverity::Info => brep_render::engine_state::NoticeSeverity::Info,
};
for text in a.texts {
ctx.app.docs.engine_mut().push_notice_as(severity, text);
}
Ok(Outcome::Done(json!({ "raised": n })))
}
pub static COMMANDS: &[CommandSpec] = &[
app_command!("state", "state_list", "Every state blob: the plain-state blobs (built from the app now) and the layout blobs the last drawn pass published, with each doc line and whether a schema is known.", NoArgs, state_list, Read, READ, schema_of::<StateList>),
app_command!("state", "state_get", "Read named state blobs, parsed as JSON: a plain-state blob is built from the app now, a layout blob (widget rects, the viewport rect, focus, the dock) comes from the current layout.", StateGetArgs, state_get, Read, READ, state_get_result_schema),
app_command!("state", "state_schema", "The JSON Schema of named state blobs (where a blob is published from a typed value) and each one's current value as an example. Takes `names`, like `state_get`.", StateSchemaArgs, state_schema, Read, READ, state_schema_result_schema),
app_command!("widgets", "hit_rects", "Every clickable widget the app published this frame as `panel/key` → [x, y, w, h] in egui points. `panel/panel:clip` is a pane's visible region; a rect outside it must be scrolled into view first.", HitRectsArgs, hit_rects, Read, READ, schema_of::<HitRects>),
app_command!("widgets", "hit_key_docs", "The documented hit-key prefixes per panel: what each `panel/key` means.", NoArgs, hit_key_docs, Read, READ, hit_key_docs_result_schema),
app_command!("state", "notices", "Drain the notices (panics, logged errors, runner refusals, toasts) that arrived since the last drain.", NoArgs, notices, Read, READ, schema_of::<Notices>),
app_command!("state", "toasts_raise", "Queue `texts` as engine notices, so each shows as a toast (and drains from `notices`) from the next frame, as a refusal's would — for checking the toast overlay itself: its cap, its coalescing of repeats, where its cards sit, and (`severity`: error, the default, warning or info) how each kind is drawn.", ToastsRaiseArgs, toasts_raise, Mutate, MUTATE_NOWAIT, toasts_raise_result_schema),
app_command!("state", "toasts_dismiss", "Dismiss every toast so a capture is deterministic; their text stays in `notices`.", NoArgs, toasts_dismiss, Mutate, MUTATE_NOWAIT, toasts_dismiss_result_schema),
];