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