use super::components::{
Creation, Open, Pane, PaneState, Retiring, Selection, Tab, TabOf, Tabs, Viewer, Workspace,
};
use super::messages::{Effect, Requester};
use super::resources::{Ids, Registry};
use crate::ids::{PaneId, TabId, ViewerId};
use crate::layout::Rect;
use crate::proto::attach::ServerMessage;
use crate::proto::control::{self, ErrorCode, Reply, RequestId};
use bevy_ecs::prelude::*;
use bevy_ecs::system::SystemParam;
pub fn effect(world: &mut World, effect: Effect) {
world.resource_mut::<Messages<Effect>>().write(effect);
}
#[derive(SystemParam)]
pub struct Step<'w> {
pub ids: Res<'w, Ids>,
}
#[derive(SystemParam)]
pub struct ViewerExit<'w, 's> {
commands: Commands<'w, 's>,
}
impl ViewerExit<'_, '_> {
pub fn despawn(&mut self, viewer: Entity, id: ViewerId, effects: &mut Effects) {
self.commands.entity(viewer).despawn();
effects.emit(Effect::CloseViewer { viewer: id });
}
}
#[derive(SystemParam)]
pub struct Effects<'w, 's> {
logs: Query<'w, 's, &'static mut super::events::EventLog>,
writer: MessageWriter<'w, Effect>,
}
impl Effects<'_, '_> {
pub fn workspace_stream(&self, workspace: Entity) -> Option<u64> {
self.logs.get(workspace).ok().map(|log| log.cursor().stream)
}
pub fn emit(&mut self, effect: Effect) {
self.writer.write(effect);
}
pub fn event(&mut self, workspace: Entity, name: &str, event: control::Event) {
let Ok(mut log) = self.logs.get_mut(workspace) else {
return;
};
let Some((entry, size)) = log.push_sized(event) else {
return;
};
self.writer.write(Effect::Event {
cursor: entry.cursor,
workspace: name.to_owned(),
event: entry.event,
size,
});
}
}
pub fn event(world: &mut World, workspace: Entity, event: control::Event) {
let Some(name) = world
.get::<Workspace>(workspace)
.map(|workspace| workspace.name.clone())
else {
return;
};
let Some((entry, size)) = world
.get_mut::<super::events::EventLog>(workspace)
.and_then(|mut log| log.push_sized(event))
else {
return;
};
effect(
world,
Effect::Event {
cursor: entry.cursor,
workspace: name,
event: entry.event,
size,
},
);
}
pub fn reply(world: &mut World, requester: Requester, reply: Reply) {
match requester {
Requester::Viewer(id) => {
if let Some(entity) = viewer_entity(world, id)
&& let Some(mut viewer) = world.get_mut::<Viewer>(entity)
{
if let Reply::Failed { error, .. } = &reply {
viewer.notice = Some(sanitize_notice(&error.message));
}
viewer.after_frame.push(ServerMessage::Reply { reply });
viewer.dirty = true;
}
}
Requester::Control(token) => effect(world, Effect::ControlReply { token, reply }),
Requester::Manager(token) => {
let outcome = match reply {
Reply::Completed {
result: control::CommandResult::Workspace { name },
..
} => super::messages::ManagerOutcome::Attach {
stream: workspace_entity(world, &name)
.and_then(|entity| world.get::<super::events::EventLog>(entity))
.map_or(0, |log| log.cursor().stream),
name,
created: true,
},
Reply::Failed { error, .. } => {
super::messages::ManagerOutcome::failed(error.message)
}
other => super::messages::ManagerOutcome::failed(format!(
"unexpected manager result {other:?}"
)),
};
effect(world, Effect::Manager { token, outcome });
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Failure {
pub code: ErrorCode,
pub message: String,
}
impl Failure {
pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
}
}
pub fn not_found(message: impl Into<String>) -> Self {
Self::new(ErrorCode::NotFound, message)
}
pub fn conflict(message: impl Into<String>) -> Self {
Self::new(ErrorCode::Conflict, message)
}
pub fn limit(message: impl Into<String>) -> Self {
Self::new(ErrorCode::Limit, message)
}
pub fn invalid(message: impl Into<String>) -> Self {
Self::new(ErrorCode::InvalidRequest, message)
}
pub fn reply(self, id: RequestId) -> Reply {
Reply::failed(id, self.code, self.message)
}
}
impl std::fmt::Display for Failure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.message)
}
}
impl From<Failure> for String {
fn from(failure: Failure) -> Self {
failure.message
}
}
impl From<&control::ControlError> for Failure {
fn from(error: &control::ControlError) -> Self {
Self::new(error.code, error.message.clone())
}
}
pub fn despawn_viewer(world: &mut World, viewer: Entity) {
let Some(id) = world.get::<Viewer>(viewer).map(|viewer| viewer.id) else {
return;
};
world.despawn(viewer);
effect(world, Effect::CloseViewer { viewer: id });
}
pub fn reply_all(
world: &mut World,
requesters: impl IntoIterator<Item = (Requester, RequestId)>,
make: impl Fn(RequestId) -> Reply,
) {
for (requester, id) in requesters {
reply(world, requester, make(id));
}
}
pub fn sanitize_notice(message: &str) -> String {
crate::view::printable(message, crate::view::MAX_MESSAGE_BYTES / 4)
}
pub fn viewer_entity(world: &World, id: ViewerId) -> Option<Entity> {
world.resource::<Ids>().viewer(id)
}
pub fn pane_entity(world: &World, id: PaneId) -> Option<Entity> {
world.resource::<Ids>().pane(id)
}
pub fn tab_entity(world: &World, id: TabId) -> Option<Entity> {
world.resource::<Ids>().tab(id)
}
pub fn workspace_entity(world: &World, name: &str) -> Option<Entity> {
world.resource::<Ids>().workspace(name)
}
pub fn pane_id(world: &World, pane: Entity) -> Option<PaneId> {
world.get::<Pane>(pane).map(|pane| pane.id)
}
pub fn tab_id(world: &World, tab: Entity) -> Option<TabId> {
world.get::<Tab>(tab).map(|tab| tab.id)
}
pub fn pane_tab(world: &World, pane: Entity) -> Option<Entity> {
world.get::<Pane>(pane).map(|pane| pane.tab)
}
pub fn tab_workspace(world: &World, tab: Entity) -> Option<Entity> {
world.get::<Tab>(tab).map(|tab| tab.workspace)
}
pub fn member_tabs(world: &World, workspace: Entity) -> Vec<Entity> {
world
.get::<Tabs>(workspace)
.map(|tabs| tabs.to_vec())
.unwrap_or_default()
}
pub fn is_member(world: &World, workspace: Entity, tab: Entity) -> bool {
world
.get::<TabOf>(tab)
.is_some_and(|member| member.0 == workspace)
}
pub fn pane_workspace(world: &World, pane: Entity) -> Option<Entity> {
tab_workspace(world, pane_tab(world, pane)?)
}
pub fn pane_in_layout(world: &World, pane: Entity) -> bool {
pane_tab(world, pane)
.and_then(|tab| world.get::<Tab>(tab))
.is_some_and(|tab| tab.layout.contains(pane))
}
pub fn panes_in_workspace(world: &mut World, workspace: Entity) -> Vec<Entity> {
world
.query::<(Entity, &Pane)>()
.iter(world)
.filter(|(_, pane)| pane.routing_workspace == workspace)
.map(|(entity, _)| entity)
.collect()
}
pub fn viewers_where(world: &mut World, keep: impl Fn(&Viewer) -> bool) -> Vec<Entity> {
world
.query::<(Entity, &Viewer)>()
.iter(world)
.filter(|(_, viewer)| keep(viewer))
.map(|(entity, _)| entity)
.collect()
}
pub fn each_viewer(
world: &mut World,
keep: impl Fn(&Viewer) -> bool,
mut apply: impl FnMut(&mut Viewer),
) {
for mut viewer in world.query::<&mut Viewer>().iter_mut(world) {
if keep(&viewer) {
apply(&mut viewer);
}
}
}
pub fn attached_viewers(world: &mut World, workspace: Entity) -> usize {
world
.query::<&Viewer>()
.iter(world)
.filter(|viewer| viewer.attached_to(workspace))
.count()
}
pub fn refresh_focus_history(world: &mut World, workspace: Entity) {
let focused = world.get::<Workspace>(workspace).and_then(|component| {
component
.selection
.tab
.and_then(|tab| focus_in_tab(world, &component.selection, tab))
});
if let Some(mut component) = world.get_mut::<Workspace>(workspace) {
component.selection.history.observe(focused);
}
let updates: Vec<_> = world
.query::<(Entity, &Viewer)>()
.iter(world)
.filter(|(_, viewer)| viewer.workspace == workspace && !viewer.detaching)
.map(|(entity, viewer)| {
(
entity,
viewer
.selection
.tab
.and_then(|tab| focus_in_tab(world, &viewer.selection, tab)),
)
})
.collect();
for (entity, focused) in updates {
if let Some(mut viewer) = world.get_mut::<Viewer>(entity) {
viewer.selection.history.observe(focused);
}
}
}
pub fn mark_workspace_dirty(world: &mut World, workspace: Entity) {
refresh_focus_history(world, workspace);
event(world, workspace, control::Event::WorkspaceChanged { id: 0 });
each_viewer(
world,
|viewer| viewer.workspace == workspace && !viewer.detaching,
|viewer| viewer.dirty = true,
);
}
pub fn mark_tab_dirty(world: &mut World, tab: Entity) {
if let Some(workspace) = tab_workspace(world, tab) {
refresh_focus_history(world, workspace);
event(world, workspace, control::Event::WorkspaceChanged { id: 0 });
}
each_viewer(
world,
|viewer| viewer.selection.tab == Some(tab) && !viewer.detaching,
|viewer| viewer.dirty = true,
);
}
pub fn clear_barriers(world: &mut World, pane: Entity) {
each_viewer(
world,
|viewer| viewer.barrier == Some(pane),
|viewer| viewer.barrier = None,
);
}
pub fn retarget_focus(world: &mut World, tab: Entity, old: Entity, next: Option<Entity>) {
each_viewer(
world,
|viewer| viewer.selection.focus.get(&tab) == Some(&old),
|viewer| {
viewer.selection.retarget(tab, next);
viewer.dirty = true;
},
);
if let Some(workspace) = tab_workspace(world, tab)
&& let Some(mut workspace) = world.get_mut::<Workspace>(workspace)
&& workspace.selection.focus.get(&tab) == Some(&old)
{
workspace.selection.retarget(tab, next);
}
}
pub fn retire(world: &mut World, workspace: Entity, now_ms: u64, exit_code: Option<u32>) -> bool {
match world.get_entity_mut(workspace) {
Ok(mut entity) if !entity.contains::<Retiring>() => {
entity.insert(Retiring {
since_ms: now_ms,
exit_code,
});
true
}
_ => false,
}
}
pub fn is_accepting(world: &World, workspace: Entity) -> bool {
world
.get_entity(workspace)
.is_ok_and(|entity| entity.contains::<Open>() && !entity.contains::<Retiring>())
}
pub fn is_not_retiring(world: &World, workspace: Entity) -> bool {
world
.get_entity(workspace)
.is_ok_and(|entity| !entity.contains::<Retiring>())
}
pub fn is_pending(world: &World, workspace: Entity) -> bool {
world
.get_entity(workspace)
.is_ok_and(|entity| !entity.contains::<Open>() && !entity.contains::<Retiring>())
}
pub fn pane_closed(world: &mut World, workspace: Entity, pane: PaneId, code: Option<u32>) {
let exit_status = code.map(|code| i32::try_from(code).unwrap_or(i32::MAX));
event(
world,
workspace,
control::Event::PaneClosed {
id: 0,
pane,
exit_status,
},
);
}
pub fn despawn_tab(world: &mut World, tab: Entity) {
world.despawn(tab);
}
pub fn despawn_workspace(world: &mut World, workspace: Entity) {
world
.resource_mut::<super::resources::WorkspaceOrder>()
.0
.retain(|entry| *entry != workspace);
world.despawn(workspace);
}
pub fn focus_in_tab(world: &World, selection: &Selection, tab: Entity) -> Option<Entity> {
selection.focused_in(tab, world.get::<Tab>(tab)?)
}
pub fn remove_from_layout(world: &mut World, pane: Entity) -> Option<Option<Entity>> {
let tab = pane_tab(world, pane)?;
let next = {
let mut tab_component = world.get_mut::<Tab>(tab)?;
if !tab_component.layout.contains(pane) {
return None;
}
let next = tab_component.layout.close(pane).ok()?;
if tab_component.zoomed == Some(pane) {
tab_component.zoomed = None;
}
tab_component.layout_changed = true;
tab_component.layout_generation = tab_component.layout_generation.saturating_add(1);
next
};
retarget_focus(world, tab, pane, next);
mark_tab_dirty(world, tab);
Some(next)
}
pub fn despawn_pane(world: &mut World, pane: Entity) {
super::systems::final_records::remember(world, pane);
let Some(id) = pane_id(world, pane) else {
return;
};
clear_barriers(world, pane);
world.despawn(pane);
effect(world, Effect::ReleasePane { pane: id });
}
pub fn terminate_pane(world: &mut World, pane: Entity, now_ms: u64, grace_ms: u64) {
let Some(mut component) = world.get_mut::<Pane>(pane) else {
return;
};
let id = component.id;
match component.state {
PaneState::Live { pid } | PaneState::Eof { pid } => {
component.state = PaneState::Terminating {
pid,
since_ms: now_ms,
};
effect(world, Effect::Terminate { pane: id, grace_ms });
}
PaneState::Starting => {
}
PaneState::Terminating { .. } | PaneState::Exited { .. } => {}
}
}
pub fn close_tab(world: &mut World, tab: Entity, now_ms: u64, grace_ms: u64) {
let Some((workspace, id)) = world
.get::<Tab>(tab)
.map(|component| (component.workspace, component.id))
else {
return;
};
if world.get::<Workspace>(workspace).is_none() {
return;
}
let (index, neighbour) = {
let members = member_tabs(world, workspace);
let index = members.iter().position(|entry| *entry == tab);
let neighbour = index.and_then(|index| {
members
.get(index.wrapping_sub(1))
.or_else(|| members.get(index + 1))
.copied()
});
(index, neighbour)
};
let panes: Vec<Entity> = world
.get::<Tab>(tab)
.map(|component| component.layout.leaves())
.unwrap_or_default();
for pane in &panes {
if let Some(mut component) = world.get_mut::<Tab>(tab) {
let _ = component.layout.close(*pane);
}
terminate_pane(world, *pane, now_ms, grace_ms);
}
if index.is_some() {
world.entity_mut(tab).remove::<TabOf>();
}
let first = member_tabs(world, workspace).first().copied();
if let Some(mut component) = world.get_mut::<Workspace>(workspace) {
component.selection.forget_tab(tab);
if component.selection.tab.is_none() {
component.selection.tab = neighbour.or(first);
}
}
each_viewer(
world,
|viewer| viewer.workspace == workspace,
|viewer| {
let was_showing = viewer.selection.tab == Some(tab);
viewer.selection.forget_tab(tab);
if was_showing {
viewer.selection.tab = neighbour;
}
viewer.dirty = true;
},
);
for pane in panes {
if world
.get::<Pane>(pane)
.is_some_and(|component| matches!(component.state, PaneState::Exited { .. }))
{
despawn_pane(world, pane);
}
}
let pending: Vec<Entity> = world
.query_filtered::<(Entity, &Pane), With<Creation>>()
.iter(world)
.filter(|(_, pane)| pane.tab == tab)
.map(|(entity, _)| entity)
.collect();
fail_creations(world, &pending, "tab closed before the pane started", true);
despawn_tab(world, tab);
if index.is_some() {
event(
world,
workspace,
control::Event::TabClosed { id: 0, tab: id },
);
}
mark_workspace_dirty(world, workspace);
}
pub fn fail_creations(world: &mut World, panes: &[Entity], reason: &str, despawn: bool) {
for &entity in panes {
let Some(creation) = world.entity_mut(entity).take::<Creation>() else {
continue;
};
clear_barriers(world, entity);
reply_all(world, creation.requesters, |id| {
Failure::conflict(reason).reply(id)
});
if despawn {
despawn_pane(world, entity);
}
}
}
pub fn write_pane(world: &mut World, pane: Entity, bytes: &[u8]) -> bool {
let Some(mut component) = world.get_mut::<Pane>(pane) else {
return false;
};
if !component.state.accepts_input() {
return false;
}
if !bytes.is_empty() {
let Some(sequence) = component.input_sequence.checked_add(1) else {
return false;
};
component.input_sequence = sequence;
}
let id = component.id;
if !bytes.is_empty()
&& let Some(workspace) = pane_workspace(world, pane)
{
event(world, workspace, control::Event::WorkspaceChanged { id: 0 });
}
for chunk in bytes.chunks(crate::proto::attach::MAX_INPUT_CHUNK) {
effect(
world,
Effect::WriteInput {
pane: id,
bytes: chunk.to_vec(),
},
);
}
true
}
pub fn default_command(world: &World) -> Vec<String> {
world.resource::<Registry>().default_command.clone()
}
pub fn tab_area(rows: u16, cols: u16) -> Rect {
Rect {
x: 0,
y: 0,
width: cols,
height: rows.saturating_sub(1),
}
}