use sim_kernel::{Cx, Error, Expr, Result, Symbol};
use sim_lib_view::{
LensRegistry, Mode, SurfaceCaps, UNIVERSAL_SURFACE_CODEC_ID, surface, universal_scene,
};
use crate::transport::{SessionStatus, Transport};
const MAX_PANES: usize = 64;
const MAX_PANE_NAME: usize = 128;
const MAX_RESOURCE_NAME: usize = 512;
fn validate_pane_name(pane: &Symbol) -> Result<()> {
let name = pane.as_qualified_str();
if name.is_empty() || name.len() > MAX_PANE_NAME {
return Err(Error::HostError(format!(
"pane name must be 1..={MAX_PANE_NAME} bytes, got {}",
name.len()
)));
}
if !name.bytes().all(|byte| byte.is_ascii_graphic()) {
return Err(Error::HostError(
"pane name must be printable ASCII without spaces".to_owned(),
));
}
Ok(())
}
fn validate_resource_name(resource: &Symbol) -> Result<()> {
let name = resource.as_qualified_str();
if name.is_empty() || name.len() > MAX_RESOURCE_NAME {
return Err(Error::HostError(format!(
"resource name must be 1..={MAX_RESOURCE_NAME} bytes, got {}",
name.len()
)));
}
Ok(())
}
struct Subscription {
pane: Symbol,
resource: Symbol,
codec: Symbol,
caps: SurfaceCaps,
rendered_value: Expr,
last_scene: Expr,
}
#[derive(Clone, Debug)]
pub struct SceneUpdate {
pub pane: Symbol,
pub scene: Expr,
pub diff: Expr,
}
pub struct Session<T: Transport> {
transport: T,
subscriptions: Vec<Subscription>,
mode: Mode,
}
impl<T: Transport> Session<T> {
pub fn new(transport: T) -> Self {
Self {
transport,
subscriptions: Vec::new(),
mode: Mode::Builder,
}
}
pub fn status(&self) -> SessionStatus {
self.transport.status()
}
pub fn mode(&self) -> Mode {
self.mode
}
pub fn set_mode(&mut self, intent: &Expr) -> Result<()> {
match sim_value::access::field(intent, "kind") {
Some(Expr::Symbol(kind)) if &*kind.name == "set-mode" => {}
_ => {
return Err(Error::HostError(
"set_mode expects an intent/set-mode".to_owned(),
));
}
}
let mode = match sim_value::access::field(intent, "mode") {
Some(Expr::Symbol(symbol)) => Mode::from_name(&symbol.name),
_ => None,
};
self.mode = mode.ok_or_else(|| {
Error::HostError(
"intent/set-mode 'mode' must be household, builder, or systems".to_owned(),
)
})?;
Ok(())
}
pub fn render_universal(&self, value: &Expr) -> Expr {
universal_scene(value, self.mode)
}
pub fn transport_mut(&mut self) -> &mut T {
&mut self.transport
}
pub fn open_codec(
&mut self,
cx: &mut Cx,
registry: &LensRegistry,
pane: Symbol,
resource: Symbol,
codec: Symbol,
caps: SurfaceCaps,
) -> Result<Expr> {
validate_pane_name(&pane)?;
validate_resource_name(&resource)?;
let replacing = self.subscriptions.iter().any(|sub| sub.pane == pane);
if !replacing && self.subscriptions.len() >= MAX_PANES {
return Err(Error::HostError(format!(
"session is at its pane limit ({MAX_PANES}); close a pane before opening another"
)));
}
let surface_codec = registry
.surface_codec(&codec)
.ok_or_else(|| Error::UnknownSymbol {
symbol: codec.clone(),
})?;
let value = self.transport.read(cx, &resource)?;
let scene = surface_codec.encode(cx, &value, &caps)?;
self.subscriptions.retain(|sub| sub.pane != pane);
self.subscriptions.push(Subscription {
pane,
resource,
codec,
caps,
rendered_value: value,
last_scene: scene.clone(),
});
Ok(scene)
}
pub fn open(
&mut self,
cx: &mut Cx,
registry: &LensRegistry,
pane: Symbol,
resource: Symbol,
_view_lens: Symbol,
_editor_lens: Symbol,
) -> Result<Expr> {
self.open_codec(
cx,
registry,
pane,
resource,
Symbol::new(UNIVERSAL_SURFACE_CODEC_ID),
surface::preset("desktop").expect("desktop is a known surface preset"),
)
}
pub fn submit_intent(
&mut self,
cx: &mut Cx,
registry: &LensRegistry,
pane: &Symbol,
intent: &Expr,
) -> Result<()> {
self.submit_intent_with_policy(cx, registry, pane, intent, false)
}
pub fn submit_intent_at_rendered_revision(
&mut self,
cx: &mut Cx,
registry: &LensRegistry,
pane: &Symbol,
intent: &Expr,
) -> Result<()> {
self.submit_intent_with_policy(cx, registry, pane, intent, true)
}
fn submit_intent_with_policy(
&mut self,
cx: &mut Cx,
registry: &LensRegistry,
pane: &Symbol,
intent: &Expr,
require_rendered_revision: bool,
) -> Result<()> {
let (resource, codec, rendered_value) = {
let sub = self
.subscriptions
.iter()
.find(|sub| &sub.pane == pane)
.ok_or_else(|| Error::HostError(format!("pane '{pane}' is not open")))?;
(
sub.resource.clone(),
sub.codec.clone(),
sub.rendered_value.clone(),
)
};
let value = if require_rendered_revision {
rendered_value.clone()
} else {
self.transport.read(cx, &resource)?
};
let surface_codec = registry
.surface_codec(&codec)
.ok_or(Error::UnknownSymbol { symbol: codec })?;
let draft = surface_codec.decode(cx, &value, intent)?;
let operation = surface_codec.commit(cx, &draft)?;
self.transport.commit_operation(
cx,
&resource,
&operation,
require_rendered_revision.then_some(&rendered_value),
)?;
Ok(())
}
pub fn pump(&mut self, cx: &mut Cx, registry: &LensRegistry) -> Result<Vec<SceneUpdate>> {
let events = self.transport.drain_events(cx)?;
let mut updates = Vec::new();
let Self {
transport,
subscriptions,
..
} = self;
for event in events {
for sub in subscriptions
.iter_mut()
.filter(|sub| sub.resource == event.resource)
{
let value = transport.read(cx, &sub.resource)?;
let surface_codec =
registry
.surface_codec(&sub.codec)
.ok_or_else(|| Error::UnknownSymbol {
symbol: sub.codec.clone(),
})?;
let scene = surface_codec.encode(cx, &value, &sub.caps)?;
let diff = sim_lib_scene::diff(&sub.last_scene, &scene);
sub.rendered_value = value;
sub.last_scene = scene.clone();
updates.push(SceneUpdate {
pane: sub.pane.clone(),
scene,
diff,
});
}
}
Ok(updates)
}
}
#[cfg(test)]
mod tests {
use sim_kernel::{Cx, Expr, Symbol};
use sim_lib_view::{
LensRegistry, UNIVERSAL_EDITOR_ID, UNIVERSAL_VIEW_ID, register_universal_default,
};
use super::{MAX_PANES, Session};
use crate::fixture::FixtureTransport;
use sim_value::build::keyword as sym;
use sim_kernel::testing::eager_cx as cx;
fn registry() -> LensRegistry {
let mut registry = LensRegistry::new();
register_universal_default(&mut registry, false);
registry
}
fn open(
session: &mut Session<FixtureTransport>,
cx: &mut Cx,
registry: &LensRegistry,
pane: &str,
) -> sim_kernel::Result<Expr> {
session.open(
cx,
registry,
sym(pane),
sym("doc"),
Symbol::new(UNIVERSAL_VIEW_ID),
Symbol::new(UNIVERSAL_EDITOR_ID),
)
}
#[test]
fn open_bounds_the_number_of_panes() {
let mut cx = cx();
let registry = registry();
let mut session = Session::new(FixtureTransport::new().with(sym("doc"), Expr::Nil));
for index in 0..MAX_PANES {
open(&mut session, &mut cx, ®istry, &format!("pane-{index}")).unwrap();
}
assert!(
open(&mut session, &mut cx, ®istry, "pane-overflow").is_err(),
"opening past the pane cap must be refused"
);
open(&mut session, &mut cx, ®istry, "pane-0").unwrap();
}
#[test]
fn open_rejects_untrusted_pane_names() {
let mut cx = cx();
let registry = registry();
let mut session = Session::new(FixtureTransport::new().with(sym("doc"), Expr::Nil));
assert!(
open(&mut session, &mut cx, ®istry, "").is_err(),
"empty pane"
);
let huge = "p".repeat(super::MAX_PANE_NAME + 1);
assert!(
open(&mut session, &mut cx, ®istry, &huge).is_err(),
"over-long pane name"
);
assert!(
open(&mut session, &mut cx, ®istry, "has space").is_err(),
"pane name with a space"
);
}
}