use sim_kernel::{Expr, Symbol};
use sim_value::build;
pub const SURFACE_NAMESPACE: &str = "surface";
pub const CAPS_KIND: &str = "caps";
pub const SURFACE_PRESETS: &[&str] = &[
"cli", "tui", "webui", "watch", "glasses", "phone", "desktop",
];
#[derive(Clone, Debug, PartialEq)]
pub struct SurfaceCaps {
pub client_id: String,
pub preset: Symbol,
pub display: Expr,
pub input: Expr,
pub transport: Expr,
pub privacy: Expr,
pub codecs: Vec<Symbol>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SurfaceError {
NotCaps,
MissingField(&'static str),
BadField(&'static str),
}
impl core::fmt::Display for SurfaceError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
SurfaceError::NotCaps => write!(f, "value is not a surface/caps map"),
SurfaceError::MissingField(name) => write!(f, "surface caps missing field: {name}"),
SurfaceError::BadField(name) => write!(f, "surface caps field has wrong shape: {name}"),
}
}
}
impl std::error::Error for SurfaceError {}
impl SurfaceCaps {
pub fn from_preset(preset_name: &str, client_id: impl Into<String>) -> Option<Self> {
let mut caps = preset(preset_name)?;
caps.client_id = client_id.into();
Some(caps)
}
pub fn to_expr(&self) -> Expr {
build::map(vec![
(
"kind",
Expr::Symbol(Symbol::qualified(SURFACE_NAMESPACE, CAPS_KIND)),
),
("client-id", build::text(self.client_id.clone())),
("preset", Expr::Symbol(self.preset.clone())),
("display", self.display.clone()),
("input", self.input.clone()),
("transport", self.transport.clone()),
("privacy", self.privacy.clone()),
(
"codecs",
build::list(self.codecs.iter().cloned().map(Expr::Symbol).collect()),
),
])
}
pub fn from_expr(expr: &Expr) -> Result<Self, SurfaceError> {
let Expr::Map(entries) = expr else {
return Err(SurfaceError::NotCaps);
};
match field(entries, "kind") {
Some(Expr::Symbol(kind))
if kind.namespace.as_deref() == Some(SURFACE_NAMESPACE)
&& &*kind.name == CAPS_KIND => {}
_ => return Err(SurfaceError::NotCaps),
}
let client_id = match field(entries, "client-id") {
Some(Expr::String(text)) => text.clone(),
Some(_) => return Err(SurfaceError::BadField("client-id")),
None => return Err(SurfaceError::MissingField("client-id")),
};
let preset = match field(entries, "preset") {
Some(Expr::Symbol(symbol)) => symbol.clone(),
Some(_) => return Err(SurfaceError::BadField("preset")),
None => return Err(SurfaceError::MissingField("preset")),
};
let display = map_field(entries, "display")?;
let input = map_field(entries, "input")?;
let transport = map_field(entries, "transport")?;
let privacy = map_field(entries, "privacy")?;
let codecs = match field(entries, "codecs") {
Some(Expr::List(items)) => {
let mut out = Vec::with_capacity(items.len());
for item in items {
let Expr::Symbol(symbol) = item else {
return Err(SurfaceError::BadField("codecs"));
};
out.push(symbol.clone());
}
out
}
Some(_) => return Err(SurfaceError::BadField("codecs")),
None => return Err(SurfaceError::MissingField("codecs")),
};
Ok(SurfaceCaps {
client_id,
preset,
display,
input,
transport,
privacy,
codecs,
})
}
pub fn preset_name(&self) -> &str {
&self.preset.name
}
pub fn input_flag(&self, name: &str) -> bool {
matches!(map_get(&self.input, name), Some(Expr::Bool(true)))
}
pub fn display_density(&self) -> Option<Symbol> {
match map_get(&self.display, "density") {
Some(Expr::Symbol(symbol)) => Some(symbol.clone()),
_ => None,
}
}
pub fn accepts_codec(&self, codec: &str) -> bool {
self.codecs.iter().any(|symbol| &*symbol.name == codec)
}
}
pub fn preset(name: &str) -> Option<SurfaceCaps> {
let (display, input, transport, privacy) = match name {
"cli" => (
display_map(&[("density", sym("dense")), ("color", sym("ansi"))]),
input_map(&["keyboard"]),
transport_map("tty", 1, false),
privacy_map("local", 60_000),
),
"tui" => (
display_map(&[("density", sym("dense")), ("color", sym("ansi256"))]),
input_map(&["keyboard", "pointer"]),
transport_map("tty", 1, false),
privacy_map("local", 60_000),
),
"webui" => (
display_map(&[("density", sym("regular")), ("color", sym("truecolor"))]),
input_map(&["keyboard", "pointer", "touch", "wheel", "file-drop"]),
transport_map("websocket", 40, false),
privacy_map("session", 600_000),
),
"watch" => (
display_map(&[("density", sym("glance")), ("shape", sym("round"))]),
input_map(&["touch", "tap", "crown", "haptic-ack"]),
transport_map("relay", 250, true),
privacy_map("local", 60_000),
),
"glasses" => (
display_map(&[("density", sym("glance")), ("lines", build::uint(2))]),
input_map(&["voice", "tap"]),
transport_map("relay", 250, true),
privacy_map("local", 60_000),
),
"phone" => (
display_map(&[("density", sym("compact")), ("color", sym("truecolor"))]),
input_map(&["touch", "voice", "camera"]),
transport_map("relay", 120, true),
privacy_map("session", 300_000),
),
"desktop" => (
display_map(&[("density", sym("dense")), ("color", sym("truecolor"))]),
input_map(&["keyboard", "pointer", "wheel", "file-drop"]),
transport_map("local", 1, false),
privacy_map("session", 600_000),
),
_ => return None,
};
Some(SurfaceCaps {
client_id: name.to_owned(),
preset: Symbol::qualified(SURFACE_NAMESPACE, name),
display,
input,
transport,
privacy,
codecs: vec![
Symbol::qualified(SURFACE_NAMESPACE, "lisp"),
Symbol::qualified(SURFACE_NAMESPACE, "json"),
],
})
}
use sim_value::build::sym;
fn display_map(extra: &[(&str, Expr)]) -> Expr {
let mut entries: Vec<(&str, Expr)> = vec![("media", build::list(Vec::new()))];
entries.extend(extra.iter().map(|(k, v)| (*k, v.clone())));
build::map(entries)
}
fn input_map(flags: &[&str]) -> Expr {
build::map(flags.iter().map(|flag| (*flag, Expr::Bool(true))).collect())
}
fn transport_map(kind: &str, round_trip_ms: u64, offline_queue: bool) -> Expr {
build::map(vec![
("kind", build::sym(kind)),
("round-trip-ms", build::uint(round_trip_ms)),
("offline-queue", Expr::Bool(offline_queue)),
("ordered", Expr::Bool(true)),
])
}
fn privacy_map(class: &str, retain_ms: u64) -> Expr {
build::map(vec![
("class", build::sym(class)),
("retain-ms", build::uint(retain_ms)),
("private-fields", build::list(Vec::new())),
])
}
fn field<'a>(entries: &'a [(Expr, Expr)], name: &str) -> Option<&'a Expr> {
entries.iter().find_map(|(key, value)| {
matches!(key, Expr::Symbol(symbol) if &*symbol.name == name && symbol.namespace.is_none())
.then_some(value)
})
}
fn map_field(entries: &[(Expr, Expr)], name: &'static str) -> Result<Expr, SurfaceError> {
match field(entries, name) {
Some(value @ Expr::Map(_)) => Ok(value.clone()),
Some(_) => Err(SurfaceError::BadField(name)),
None => Err(SurfaceError::MissingField(name)),
}
}
fn map_get<'a>(map: &'a Expr, name: &str) -> Option<&'a Expr> {
match map {
Expr::Map(entries) => field(entries, name),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_preset_round_trips() {
for name in SURFACE_PRESETS {
let caps = preset(name).expect("preset exists");
assert_eq!(caps.preset_name(), *name);
let back = SurfaceCaps::from_expr(&caps.to_expr()).expect("round-trips");
assert_eq!(caps, back, "{name} caps must round-trip losslessly");
}
}
#[test]
fn unknown_preset_is_none() {
assert!(preset("hologram").is_none());
}
#[test]
fn from_preset_overrides_client_id() {
let caps = SurfaceCaps::from_preset("cli", "tty.local.7").unwrap();
assert_eq!(caps.client_id, "tty.local.7");
assert_eq!(caps.preset_name(), "cli");
}
#[test]
fn capability_accessors_read_fields() {
let cli = preset("cli").unwrap();
assert!(cli.input_flag("keyboard"));
assert!(!cli.input_flag("touch"));
assert_eq!(cli.display_density().unwrap().name.as_ref(), "dense");
assert!(cli.accepts_codec("lisp"));
assert!(!cli.accepts_codec("algol"));
let watch = preset("watch").unwrap();
assert!(watch.input_flag("haptic-ack"));
assert_eq!(watch.display_density().unwrap().name.as_ref(), "glance");
}
#[test]
fn parse_fails_closed() {
assert_eq!(
SurfaceCaps::from_expr(&Expr::Nil),
Err(SurfaceError::NotCaps)
);
let mut entries = match preset("cli").unwrap().to_expr() {
Expr::Map(entries) => entries,
_ => unreachable!(),
};
entries.retain(|(key, _)| !matches!(key, Expr::Symbol(s) if &*s.name == "codecs"));
assert_eq!(
SurfaceCaps::from_expr(&Expr::Map(entries)),
Err(SurfaceError::MissingField("codecs"))
);
}
}