1use sim_kernel::{Error, Expr, Symbol};
27use sim_value::{access, build};
28
29pub const SURFACE_NAMESPACE: &str = "surface";
31
32pub const CAPS_KIND: &str = "caps";
34
35pub const SURFACE_PRESETS: &[&str] = &[
41 "cli", "tui", "webui", "watch", "glasses", "phone", "desktop",
42];
43
44#[derive(Clone, Debug, PartialEq)]
51pub struct SurfaceCaps {
52 pub client_id: String,
54 pub preset: Symbol,
56 pub display: Expr,
58 pub input: Expr,
60 pub transport: Expr,
62 pub privacy: Expr,
64 pub rate: Expr,
66 pub codecs: Vec<Symbol>,
68}
69
70#[derive(Clone, Debug, PartialEq, Eq)]
74pub enum SurfaceError {
75 NotCaps,
77 MissingField(&'static str),
79 BadField(&'static str),
81 Field(String),
87}
88
89impl core::fmt::Display for SurfaceError {
90 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
91 match self {
92 SurfaceError::NotCaps => write!(f, "value is not a surface/caps map"),
93 SurfaceError::MissingField(name) => write!(f, "surface caps missing field: {name}"),
94 SurfaceError::BadField(name) => write!(f, "surface caps field has wrong shape: {name}"),
95 SurfaceError::Field(message) => write!(f, "surface caps field: {message}"),
96 }
97 }
98}
99
100impl std::error::Error for SurfaceError {}
101
102impl From<Error> for SurfaceError {
103 fn from(err: Error) -> Self {
108 SurfaceError::Field(err.to_string())
109 }
110}
111
112impl SurfaceCaps {
113 pub fn from_preset(preset_name: &str, client_id: impl Into<String>) -> Option<Self> {
117 let mut caps = preset(preset_name)?;
118 caps.client_id = client_id.into();
119 Some(caps)
120 }
121
122 pub fn to_expr(&self) -> Expr {
124 build::map(vec![
125 (
126 "kind",
127 Expr::Symbol(Symbol::qualified(SURFACE_NAMESPACE, CAPS_KIND)),
128 ),
129 ("client-id", build::text(self.client_id.clone())),
130 ("preset", Expr::Symbol(self.preset.clone())),
131 ("display", self.display.clone()),
132 ("input", self.input.clone()),
133 ("transport", self.transport.clone()),
134 ("privacy", self.privacy.clone()),
135 ("rate", self.rate.clone()),
136 (
137 "codecs",
138 build::list(self.codecs.iter().cloned().map(Expr::Symbol).collect()),
139 ),
140 ])
141 }
142
143 pub fn from_expr(expr: &Expr) -> Result<Self, SurfaceError> {
145 let Expr::Map(entries) = expr else {
146 return Err(SurfaceError::NotCaps);
147 };
148 match access::entry_field(entries, "kind") {
149 Some(Expr::Symbol(kind))
150 if kind.namespace.as_deref() == Some(SURFACE_NAMESPACE)
151 && &*kind.name == CAPS_KIND => {}
152 _ => return Err(SurfaceError::NotCaps),
153 }
154 let client_id = match access::entry_field(entries, "client-id") {
155 Some(Expr::String(text)) => text.clone(),
156 Some(_) => return Err(SurfaceError::BadField("client-id")),
157 None => return Err(SurfaceError::MissingField("client-id")),
158 };
159 let preset = match access::entry_field(entries, "preset") {
160 Some(Expr::Symbol(symbol)) => symbol.clone(),
161 Some(_) => return Err(SurfaceError::BadField("preset")),
162 None => return Err(SurfaceError::MissingField("preset")),
163 };
164 let display = map_field(entries, "display")?;
165 let input = map_field(entries, "input")?;
166 let transport = map_field(entries, "transport")?;
167 let privacy = map_field(entries, "privacy")?;
168 let rate = match access::entry_field(entries, "rate") {
169 Some(value @ Expr::Map(_)) => value.clone(),
170 Some(_) => return Err(SurfaceError::BadField("rate")),
171 None => rate_map(1, 1, 1000),
172 };
173 let codecs = match access::entry_field(entries, "codecs") {
174 Some(Expr::List(items)) => {
175 let mut out = Vec::with_capacity(items.len());
176 for item in items {
177 let Expr::Symbol(symbol) = item else {
178 return Err(SurfaceError::BadField("codecs"));
179 };
180 out.push(symbol.clone());
181 }
182 out
183 }
184 Some(_) => return Err(SurfaceError::BadField("codecs")),
185 None => return Err(SurfaceError::MissingField("codecs")),
186 };
187 Ok(SurfaceCaps {
188 client_id,
189 preset,
190 display,
191 input,
192 transport,
193 privacy,
194 rate,
195 codecs,
196 })
197 }
198
199 pub fn preset_name(&self) -> &str {
201 &self.preset.name
202 }
203
204 pub fn input_flag(&self, name: &str) -> bool {
206 matches!(access::field(&self.input, name), Some(Expr::Bool(true)))
207 }
208
209 pub fn display_density(&self) -> Option<Symbol> {
211 match access::field(&self.display, "density") {
212 Some(Expr::Symbol(symbol)) => Some(symbol.clone()),
213 _ => None,
214 }
215 }
216
217 pub fn accepts_codec(&self, codec: &str) -> bool {
219 self.codecs.iter().any(|symbol| &*symbol.name == codec)
220 }
221}
222
223pub fn preset(name: &str) -> Option<SurfaceCaps> {
228 let (display, input, transport, privacy, rate) = match name {
229 "cli" => (
230 display_map(&[("density", sym("dense")), ("color", sym("ansi"))]),
231 input_map(&["keyboard"]),
232 transport_map("tty", 1, false),
233 privacy_map("local", 60_000),
234 rate_map(1, 1, 1000),
235 ),
236 "tui" => (
237 display_map(&[("density", sym("dense")), ("color", sym("ansi256"))]),
238 input_map(&["keyboard", "pointer"]),
239 transport_map("tty", 1, false),
240 privacy_map("local", 60_000),
241 rate_map(1, 1, 1000),
242 ),
243 "webui" => (
244 display_map(&[("density", sym("regular")), ("color", sym("truecolor"))]),
245 input_map(&["keyboard", "pointer", "touch", "wheel", "file-drop"]),
246 transport_map("websocket", 40, false),
247 privacy_map("session", 600_000),
248 rate_map(5, 30, 500),
249 ),
250 "watch" => (
251 display_map(&[("density", sym("glance")), ("shape", sym("round"))]),
252 input_map(&["touch", "tap", "crown", "haptic-ack"]),
253 transport_map("relay", 250, true),
254 privacy_map("local", 60_000),
255 rate_map(1, 1, 1000),
256 ),
257 "glasses" => (
258 display_map(&[("density", sym("glance")), ("lines", build::uint(2))]),
259 input_map(&["voice", "tap"]),
260 transport_map("relay", 250, true),
261 privacy_map("local", 60_000),
262 rate_map(5, 30, 500),
263 ),
264 "phone" => (
265 display_map(&[("density", sym("compact")), ("color", sym("truecolor"))]),
266 input_map(&["touch", "voice", "camera"]),
267 transport_map("relay", 120, true),
268 privacy_map("session", 300_000),
269 rate_map(5, 30, 500),
270 ),
271 "desktop" => (
272 display_map(&[("density", sym("dense")), ("color", sym("truecolor"))]),
273 input_map(&["keyboard", "pointer", "wheel", "file-drop"]),
274 transport_map("local", 1, false),
275 privacy_map("session", 600_000),
276 rate_map(60, 120, 100),
277 ),
278 _ => return None,
279 };
280 Some(SurfaceCaps {
281 client_id: name.to_owned(),
282 preset: Symbol::qualified(SURFACE_NAMESPACE, name),
283 display,
284 input,
285 transport,
286 privacy,
287 rate,
288 codecs: vec![
289 Symbol::qualified(SURFACE_NAMESPACE, "lisp"),
290 Symbol::qualified(SURFACE_NAMESPACE, "json"),
291 ],
292 })
293}
294
295use sim_value::build::sym;
296
297fn display_map(extra: &[(&str, Expr)]) -> Expr {
298 let mut entries: Vec<(&str, Expr)> = vec![("media", build::list(Vec::new()))];
299 entries.extend(extra.iter().map(|(k, v)| (*k, v.clone())));
300 build::map(entries)
301}
302
303fn input_map(flags: &[&str]) -> Expr {
304 build::map(flags.iter().map(|flag| (*flag, Expr::Bool(true))).collect())
305}
306
307fn transport_map(kind: &str, round_trip_ms: u64, offline_queue: bool) -> Expr {
308 build::map(vec![
309 ("kind", build::sym(kind)),
310 ("round-trip-ms", build::uint(round_trip_ms)),
311 ("offline-queue", Expr::Bool(offline_queue)),
312 ("ordered", Expr::Bool(true)),
313 ])
314}
315
316fn privacy_map(class: &str, retain_ms: u64) -> Expr {
317 build::map(vec![
318 ("class", build::sym(class)),
319 ("retain-ms", build::uint(retain_ms)),
320 ("private-fields", build::list(Vec::new())),
321 ])
322}
323
324fn rate_map(content_hz: u64, adapt_hz: u64, max_stale_ms: u64) -> Expr {
325 build::map(vec![
326 ("content-hz", build::uint(content_hz)),
327 ("adapt-hz", build::uint(adapt_hz)),
328 ("max-stale-ms", build::uint(max_stale_ms)),
329 ])
330}
331
332fn map_field(entries: &[(Expr, Expr)], name: &'static str) -> Result<Expr, SurfaceError> {
333 match access::entry_required(entries, name, "surface caps").map_err(SurfaceError::from)? {
338 value @ Expr::Map(_) => Ok(value.clone()),
339 _ => Err(SurfaceError::BadField(name)),
340 }
341}
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346
347 #[test]
348 fn every_preset_round_trips() {
349 for name in SURFACE_PRESETS {
350 let caps = preset(name).expect("preset exists");
351 assert_eq!(caps.preset_name(), *name);
352 let back = SurfaceCaps::from_expr(&caps.to_expr()).expect("round-trips");
353 assert_eq!(caps, back, "{name} caps must round-trip losslessly");
354 }
355 }
356
357 #[test]
358 fn unknown_preset_is_none() {
359 assert!(preset("hologram").is_none());
360 }
361
362 #[test]
363 fn from_preset_overrides_client_id() {
364 let caps = SurfaceCaps::from_preset("cli", "tty.local.7").unwrap();
365 assert_eq!(caps.client_id, "tty.local.7");
366 assert_eq!(caps.preset_name(), "cli");
367 }
368
369 #[test]
370 fn capability_accessors_read_fields() {
371 let cli = preset("cli").unwrap();
372 assert!(cli.input_flag("keyboard"));
373 assert!(!cli.input_flag("touch"));
374 assert_eq!(cli.display_density().unwrap().name.as_ref(), "dense");
375 assert!(cli.accepts_codec("lisp"));
376 assert!(!cli.accepts_codec("algol"));
377
378 let watch = preset("watch").unwrap();
379 assert!(watch.input_flag("haptic-ack"));
380 assert_eq!(watch.display_density().unwrap().name.as_ref(), "glance");
381 }
382
383 #[test]
384 fn surface_map_field_wrong_shape_fails_closed() {
385 let mut entries = match preset("cli").unwrap().to_expr() {
388 Expr::Map(entries) => entries,
389 _ => unreachable!(),
390 };
391 for (key, value) in entries.iter_mut() {
392 if matches!(key, Expr::Symbol(symbol) if &*symbol.name == "display") {
393 *value = Expr::Bool(true);
394 }
395 }
396 assert_eq!(
397 SurfaceCaps::from_expr(&Expr::Map(entries)),
398 Err(SurfaceError::BadField("display"))
399 );
400 }
401
402 #[test]
403 fn surface_map_field_missing_flows_through_sim_value_reader() {
404 let mut entries = match preset("cli").unwrap().to_expr() {
407 Expr::Map(entries) => entries,
408 _ => unreachable!(),
409 };
410 entries.retain(|(key, _)| !matches!(key, Expr::Symbol(s) if &*s.name == "transport"));
411 match SurfaceCaps::from_expr(&Expr::Map(entries)) {
412 Err(SurfaceError::Field(message)) => assert!(message.contains("transport")),
413 other => panic!("expected a located field error, got {other:?}"),
414 }
415 }
416
417 #[test]
418 fn parse_fails_closed() {
419 assert_eq!(
420 SurfaceCaps::from_expr(&Expr::Nil),
421 Err(SurfaceError::NotCaps)
422 );
423 let mut entries = match preset("cli").unwrap().to_expr() {
425 Expr::Map(entries) => entries,
426 _ => unreachable!(),
427 };
428 entries.retain(|(key, _)| !matches!(key, Expr::Symbol(s) if &*s.name == "codecs"));
429 assert_eq!(
430 SurfaceCaps::from_expr(&Expr::Map(entries)),
431 Err(SurfaceError::MissingField("codecs"))
432 );
433 }
434
435 #[test]
436 fn missing_rate_map_defaults_to_safe_envelope() {
437 let mut entries = match preset("cli").unwrap().to_expr() {
438 Expr::Map(entries) => entries,
439 _ => unreachable!(),
440 };
441 entries.retain(|(key, _)| !matches!(key, Expr::Symbol(s) if &*s.name == "rate"));
442 let caps = SurfaceCaps::from_expr(&Expr::Map(entries)).expect("older caps parse");
443 assert_eq!(caps.rate, rate_map(1, 1, 1000));
444 }
445}