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