1use sim_kernel::{Error, Expr, Result};
8use sim_value::{access, build};
9
10use crate::model::{node, validate_scene};
11
12pub const GLANCE_KIND: &str = "glance";
14
15#[derive(Clone, Debug, PartialEq, Eq)]
17pub struct GlanceMetric {
18 pub label: String,
20 pub value: String,
22}
23
24impl GlanceMetric {
25 pub fn new(label: impl Into<String>, value: impl Into<String>) -> Self {
27 Self {
28 label: label.into(),
29 value: value.into(),
30 }
31 }
32
33 fn to_expr(&self) -> Expr {
34 build::map(vec![
35 ("label", Expr::String(self.label.clone())),
36 ("value", Expr::String(self.value.clone())),
37 ])
38 }
39
40 fn from_expr(expr: &Expr) -> Result<Self> {
41 Ok(Self {
42 label: access::required_str(expr, "label", "scene/glance metric")?.to_owned(),
43 value: access::required_str(expr, "value", "scene/glance metric")?.to_owned(),
44 })
45 }
46}
47
48#[derive(Clone, Debug, PartialEq)]
50pub struct GlanceAction {
51 pub label: String,
53 pub target: Expr,
55}
56
57impl GlanceAction {
58 pub fn new(label: impl Into<String>, target: Expr) -> Self {
60 Self {
61 label: label.into(),
62 target,
63 }
64 }
65
66 fn to_expr(&self) -> Expr {
67 build::map(vec![
68 ("label", Expr::String(self.label.clone())),
69 ("target", self.target.clone()),
70 ])
71 }
72
73 fn from_expr(expr: &Expr) -> Result<Self> {
74 Ok(Self {
75 label: access::required_str(expr, "label", "scene/glance action")?.to_owned(),
76 target: access::required(expr, "target", "scene/glance action")?.clone(),
77 })
78 }
79}
80
81#[derive(Clone, Debug, PartialEq)]
83pub struct GlanceCard {
84 pub title: String,
86 pub metric: Option<GlanceMetric>,
88 pub action: Option<GlanceAction>,
90 pub urgency: String,
92 pub cells: u16,
94 pub bypass_budget: bool,
96}
97
98impl GlanceCard {
99 pub fn new(
101 title: impl Into<String>,
102 metric: Option<GlanceMetric>,
103 action: Option<GlanceAction>,
104 urgency: impl Into<String>,
105 cells: u16,
106 ) -> Self {
107 Self {
108 title: title.into(),
109 metric,
110 action,
111 urgency: urgency.into(),
112 cells,
113 bypass_budget: false,
114 }
115 }
116
117 pub fn with_budget_bypass(mut self, bypass: bool) -> Self {
119 self.bypass_budget = bypass;
120 self
121 }
122
123 pub fn to_scene(&self) -> Expr {
125 let mut entries = vec![
126 ("title", Expr::String(self.title.clone())),
127 ("urgency", build::sym(&self.urgency)),
128 ("cells", build::uint(u64::from(self.cells))),
129 ("bypass-budget", Expr::Bool(self.bypass_budget)),
130 ];
131 if let Some(metric) = &self.metric {
132 entries.push(("metric", metric.to_expr()));
133 }
134 if let Some(action) = &self.action {
135 entries.push(("action", action.to_expr()));
136 }
137 node(GLANCE_KIND, entries)
138 }
139
140 pub fn from_scene(expr: &Expr) -> Result<Self> {
142 validate_scene(expr)
143 .map_err(|err| Error::HostError(format!("invalid scene/glance: {err}")))?;
144 match crate::model::node_kind(expr) {
145 Some(kind)
146 if kind.namespace.as_deref() == Some(crate::kinds::SCENE_NAMESPACE)
147 && kind.name.as_ref() == GLANCE_KIND => {}
148 _ => {
149 return Err(Error::HostError("expected a scene/glance card".to_owned()));
150 }
151 }
152 let cells = field_u16(expr, "cells").unwrap_or(1);
153 Ok(Self {
154 title: access::required_str(expr, "title", "scene/glance")?.to_owned(),
155 metric: access::field(expr, "metric")
156 .map(GlanceMetric::from_expr)
157 .transpose()?,
158 action: access::field(expr, "action")
159 .map(GlanceAction::from_expr)
160 .transpose()?,
161 urgency: access::field_sym(expr, "urgency")
162 .map(|symbol| symbol.name.to_string())
163 .unwrap_or_else(|| "info".to_owned()),
164 cells,
165 bypass_budget: access::field_bool(expr, "bypass-budget").unwrap_or(false),
166 })
167 }
168}
169
170fn field_u16(expr: &Expr, name: &str) -> Option<u16> {
171 let Expr::Number(number) = access::field(expr, name)? else {
172 return None;
173 };
174 number.canonical.parse().ok()
175}
176
177pub fn glance_card(
179 title: impl Into<String>,
180 metric: Option<GlanceMetric>,
181 action: Option<GlanceAction>,
182 urgency: impl Into<String>,
183 cells: u16,
184) -> Expr {
185 GlanceCard::new(title, metric, action, urgency, cells).to_scene()
186}