pub mod components;
pub mod events;
pub mod messages;
pub mod resources;
pub mod support;
pub mod systems;
pub use messages::{Effect, Inbound, ManagerAction, ManagerOutcome, Requester, ViewerRequest};
pub use resources::{Clock, Deadlines, Ids, Limits, Registry, ServerIdentity, Waits};
use bevy_ecs::prelude::*;
use bevy_ecs::schedule::{ScheduleLabel, SingleThreadedExecutor};
#[derive(ScheduleLabel, Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Step;
#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Phase {
Ingest,
Output,
Requests,
Completions,
Waits,
Lifecycle,
Layout,
Snapshot,
Publish,
}
pub struct Session {
world: World,
schedule: Schedule,
}
impl Session {
pub fn new(config: &crate::config::Config) -> anyhow::Result<Self> {
let bindings = crate::commands::configured_bindings(config)?;
let mut world = World::new();
world.insert_resource(Limits::from_config(config));
world.insert_resource(Registry {
bindings,
default_command: config.default_command.argv.clone(),
});
world.init_resource::<Ids>();
world.init_resource::<resources::InputOperations>();
world.init_resource::<resources::FinalRecords>();
world.init_resource::<Clock>();
world.init_resource::<Deadlines>();
world.init_resource::<resources::ShuttingDown>();
world.init_resource::<ServerIdentity>();
world.init_resource::<Waits>();
world.init_resource::<resources::WorkspaceCounter>();
world.init_resource::<Messages<Inbound>>();
world.init_resource::<Messages<Effect>>();
let mut schedule = Schedule::new(Step);
schedule.set_executor(SingleThreadedExecutor::new());
schedule.configure_sets(
(
Phase::Ingest,
Phase::Output,
Phase::Requests,
Phase::Completions,
Phase::Waits,
Phase::Lifecycle,
Phase::Layout,
Phase::Snapshot,
Phase::Publish,
)
.chain(),
);
schedule.add_systems((
systems::requests::apply_attachments.in_set(Phase::Ingest),
(
systems::output::apply_pane_output,
systems::input::apply_completions,
)
.chain()
.in_set(Phase::Output),
(
systems::requests::apply_requests,
systems::requests::drain_viewer_queues,
)
.chain()
.in_set(Phase::Requests),
(
systems::creation::apply_spawn_completions,
systems::requests::drain_viewer_queues,
)
.chain()
.in_set(Phase::Completions),
systems::requests::resolve_waits.in_set(Phase::Waits),
systems::lifecycle::resolve_lifecycle.in_set(Phase::Lifecycle),
systems::layout::resolve_layout.in_set(Phase::Layout),
(
systems::snapshot::refresh_grids,
systems::snapshot::publish_frames,
)
.chain()
.in_set(Phase::Snapshot),
systems::snapshot::finish_step.in_set(Phase::Publish),
));
Ok(Self { world, schedule })
}
pub fn step(&mut self, now_ms: u64, inbound: impl IntoIterator<Item = Inbound>) -> Vec<Effect> {
{
let mut clock = self.world.resource_mut::<Clock>();
clock.now_ms = now_ms;
clock.step = clock.step.wrapping_add(1);
}
self.world.resource_mut::<Deadlines>().next_ms = None;
self.world
.resource_mut::<Messages<Inbound>>()
.write_batch(inbound);
self.schedule.run(&mut self.world);
self.world.clear_trackers();
self.world
.resource_mut::<Messages<Effect>>()
.drain()
.collect()
}
pub fn set_identity(&mut self, identity: ServerIdentity) {
self.world.insert_resource(identity);
}
pub fn next_deadline_ms(&self) -> Option<u64> {
self.world.resource::<Deadlines>().next_ms
}
pub fn retained_messages(&self) -> usize {
self.world.resource::<Messages<Inbound>>().len()
+ self.world.resource::<Messages<Effect>>().len()
}
pub fn world(&self) -> &World {
&self.world
}
pub fn workspace_names(&mut self) -> Vec<String> {
let mut names: Vec<String> = self
.world
.query::<&components::Workspace>()
.iter(&self.world)
.filter(|workspace| workspace.open && workspace.retiring.is_none())
.map(|workspace| workspace.name.clone())
.collect();
names.sort();
names
}
pub fn entity_counts(&mut self) -> EntityCounts {
EntityCounts {
workspaces: self
.world
.query::<&components::Workspace>()
.iter(&self.world)
.count(),
tabs: self
.world
.query::<&components::Tab>()
.iter(&self.world)
.count(),
panes: self
.world
.query::<&components::Pane>()
.iter(&self.world)
.count(),
viewers: self
.world
.query::<&components::Viewer>()
.iter(&self.world)
.count(),
}
}
pub fn check_invariants(&mut self) -> Result<(), String> {
let ids = self.world.resource::<Ids>().clone();
let mut layout_panes = std::collections::HashSet::new();
for (entity, tab) in self
.world
.query::<(Entity, &components::Tab)>()
.iter(&self.world)
{
tab.layout
.validate()
.map_err(|error| format!("tab {} layout: {error}", tab.id))?;
let workspace = self
.world
.get::<components::Workspace>(tab.workspace)
.ok_or_else(|| format!("tab {} has no workspace", tab.id))?;
if ids.tab(tab.id) != Some(entity) {
return Err(format!("tab {} is not registered", tab.id));
}
let member = self
.world
.get::<components::TabOf>(entity)
.is_some_and(|member| member.0 == tab.workspace);
for pane in tab.layout.leaves() {
if pane == Entity::PLACEHOLDER {
if member {
return Err(format!("tab {} exposes a placeholder pane", tab.id));
}
continue;
}
if !layout_panes.insert(pane) {
return Err(format!("pane {pane:?} appears in two layouts"));
}
let component = self
.world
.get::<components::Pane>(pane)
.ok_or_else(|| format!("tab {} references a missing pane", tab.id))?;
if component.tab != entity {
return Err(format!("pane {} is in a foreign layout", component.id));
}
if matches!(component.state, components::PaneState::Starting) {
return Err(format!("starting pane {} is visible", component.id));
}
}
if member && tab.layout.is_empty() && workspace.retiring.is_none() {
return Err(format!("tab {} is empty but still a member", tab.id));
}
}
for (entity, pane) in self
.world
.query::<(Entity, &components::Pane)>()
.iter(&self.world)
{
if ids.pane(pane.id) != Some(entity) {
return Err(format!("pane {} is not registered", pane.id));
}
if self.world.get::<components::Tab>(pane.tab).is_none()
&& !matches!(pane.state, components::PaneState::Terminating { .. })
{
return Err(format!("pane {} has no tab", pane.id));
}
}
for (entity, workspace) in self
.world
.query::<(Entity, &components::Workspace)>()
.iter(&self.world)
{
if ids.workspace(&workspace.name) != Some(entity) {
return Err(format!("workspace {} is not registered", workspace.name));
}
let members = support::member_tabs(&self.world, entity);
for tab in &members {
if self
.world
.get::<components::Tab>(*tab)
.is_none_or(|tab| tab.workspace != entity)
{
return Err(format!("workspace {} lists a foreign tab", workspace.name));
}
}
if let Some(tab) = workspace.selection.tab
&& !members.contains(&tab)
&& workspace.retiring.is_none()
&& workspace.open
{
return Err(format!(
"workspace {} selects a foreign tab",
workspace.name
));
}
}
for (entity, viewer) in self
.world
.query::<(Entity, &components::Viewer)>()
.iter(&self.world)
{
if ids.viewer(viewer.id) != Some(entity) {
return Err(format!("viewer {} is not registered", viewer.id));
}
let workspace = self
.world
.get::<components::Workspace>(viewer.workspace)
.ok_or_else(|| format!("viewer {} has no workspace", viewer.id))?;
if let Some(tab) = viewer.selection.tab
&& !support::is_member(&self.world, viewer.workspace, tab)
&& workspace.retiring.is_none()
{
return Err(format!("viewer {} shows a foreign tab", viewer.id));
}
if let Some(barrier) = viewer.barrier
&& self.world.get::<components::Creation>(barrier).is_none()
{
return Err(format!("viewer {} waits on a finished creation", viewer.id));
}
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct EntityCounts {
pub workspaces: usize,
pub tabs: usize,
pub panes: usize,
pub viewers: usize,
}