use crate::ids::{PaneId, TabId, ViewerId};
use crate::layout::{LayoutTree, Rect};
use crate::proto::attach::ServerMessage;
use crate::terminal::ServerTerminal;
use bevy_ecs::lifecycle::HookContext;
use bevy_ecs::prelude::*;
use bevy_ecs::world::DeferredWorld;
use std::collections::{BTreeMap, VecDeque};
use std::path::PathBuf;
use super::messages::{Requester, ViewerRequest};
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Selection {
pub tab: Option<Entity>,
pub focus: BTreeMap<Entity, Entity>,
pub history: FocusHistory,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct FocusHistory {
pub current: Option<Entity>,
pub previous: Option<Entity>,
}
impl FocusHistory {
pub fn observe(&mut self, focused: Option<Entity>) {
if focused != self.current {
if self.current.is_some() {
self.previous = self.current;
}
self.current = focused;
}
}
}
impl Selection {
#[must_use]
pub fn for_viewer(&self) -> Self {
let mut selection = self.clone();
selection.history = FocusHistory {
current: self.history.current.or(self.focused()),
previous: None,
};
selection
}
#[must_use]
pub fn focused_in(&self, tab: Entity, component: &Tab) -> Option<Entity> {
component
.zoomed
.filter(|pane| component.layout.contains(*pane))
.or_else(|| {
self.focus
.get(&tab)
.copied()
.filter(|pane| component.layout.contains(*pane))
})
.or_else(|| component.layout.leaves().first().copied())
}
#[must_use]
pub fn focused(&self) -> Option<Entity> {
self.focus.get(&self.tab?).copied()
}
pub fn set_focus(&mut self, tab: Entity, pane: Entity) {
self.focus.insert(tab, pane);
}
pub fn forget_tab(&mut self, tab: Entity) {
self.focus.remove(&tab);
if self.tab == Some(tab) {
self.tab = None;
}
}
pub fn select(&mut self, tab: Entity, pane: Option<Entity>) {
self.tab = Some(tab);
if let Some(pane) = pane {
self.set_focus(tab, pane);
}
}
pub fn retarget(&mut self, tab: Entity, next: Option<Entity>) {
match next {
Some(next) => self.set_focus(tab, next),
None => {
self.focus.remove(&tab);
}
}
}
}
#[derive(Component, Debug)]
#[component(on_remove = release_workspace_name)]
pub struct Workspace {
pub name: String,
pub label: Option<String>,
pub selection: Selection,
pub last_attached: u64,
pub tab_counter: u32,
}
#[derive(Component, Debug)]
pub struct Open;
#[derive(Component, Clone, Copy, Debug, PartialEq, Eq)]
pub struct Retiring {
pub since_ms: u64,
pub exit_code: Option<u32>,
}
pub type Accepting = (With<Open>, Without<Retiring>);
#[derive(Component, Debug)]
#[relationship(relationship_target = Tabs)]
pub struct TabOf(pub Entity);
#[derive(Component, Debug, Default)]
#[relationship_target(relationship = TabOf)]
pub struct Tabs(Vec<Entity>);
impl Tabs {
pub fn place_before(&mut self, tab: Entity, before: Option<Entity>) -> bool {
let Some(index) = self.0.iter().position(|entry| *entry == tab) else {
return false;
};
if before == Some(tab) {
return true;
}
if before.is_some_and(|before| !self.0.contains(&before)) {
return false;
}
self.0.remove(index);
let position = before
.and_then(|before| self.0.iter().position(|entry| *entry == before))
.unwrap_or(self.0.len());
self.0.insert(position, tab);
true
}
}
impl std::ops::Deref for Tabs {
type Target = [Entity];
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Component, Debug)]
#[component(on_remove = release_tab_id)]
pub struct Tab {
pub id: TabId,
pub workspace: Entity,
pub label: String,
pub layout: LayoutTree<Entity>,
pub geometry: Vec<(Entity, Rect)>,
pub area: Rect,
pub layout_changed: bool,
pub layout_generation: u64,
pub zoomed: Option<Entity>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PaneState {
Starting,
Live {
pid: u32,
},
Eof {
pid: u32,
},
Terminating {
pid: u32,
since_ms: u64,
},
Exited {
code: u32,
},
}
impl PaneState {
#[must_use]
pub fn pid(self) -> Option<u32> {
match self {
Self::Live { pid } | Self::Eof { pid } | Self::Terminating { pid, .. } => Some(pid),
Self::Starting | Self::Exited { .. } => None,
}
}
#[must_use]
pub fn exit_code(self) -> Option<u32> {
match self {
Self::Exited { code } => Some(code),
_ => None,
}
}
#[must_use]
pub fn accepts_input(self) -> bool {
matches!(self, Self::Live { .. })
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum WorkspacePin {
None,
Creation,
Explicit,
}
impl WorkspacePin {
pub fn is_fixed(self) -> bool {
self != Self::None
}
}
#[derive(Component)]
#[component(on_remove = release_pane_id)]
pub struct Pane {
pub routing_workspace: Entity,
pub workspace_pin: WorkspacePin,
pub id: PaneId,
pub workspace_name: String,
pub workspace_stream: u64,
pub tab: Entity,
pub argv: Vec<String>,
pub cwd: PathBuf,
pub state: PaneState,
pub terminal: ServerTerminal,
pub rect: Rect,
pub dirty: bool,
pub event_pending: bool,
pub last_event_seq: u64,
pub published_title: String,
pub right_click: crate::view::RightClickPolicy,
pub label: Option<String>,
pub input_sequence: u64,
pub last_output_event_ms: Option<u64>,
pub final_retain_ms: u64,
}
impl Pane {
pub fn refresh(&mut self) -> bool {
if !self.dirty {
return false;
}
self.dirty = false;
self.terminal
.refresh_grid(&self.published_title, self.state.exit_code())
}
#[must_use]
pub fn is_required_process(&self, want: &crate::proto::attach::InitialTarget) -> bool {
self.workspace_stream == want.stream
&& self.state.pid() == Some(want.pid)
&& self.state.accepts_input()
}
#[must_use]
pub fn terminal_size(rect: Rect) -> (u16, u16) {
crate::terminal::clamp_dims(rect.height, rect.width)
}
}
#[derive(Component, Debug)]
pub struct Creation {
pub requesters: Vec<(Requester, u64)>,
pub kind: CreationKind,
}
#[derive(Debug)]
pub enum CreationKind {
Split {
tab: Entity,
target: Entity,
axis: crate::layout::Axis,
ratio: std::num::NonZeroU16,
focus: bool,
},
NewTab { tab: Entity },
Workspace { tab: Entity },
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Sent {
pub right_click: crate::view::RightClickPolicy,
pub label: Option<String>,
pub rows: u16,
pub columns: u16,
pub seq: u64,
}
#[derive(Component)]
#[component(on_remove = release_viewer_id)]
pub struct Viewer {
pub required_process: Option<crate::proto::attach::InitialTarget>,
pub id: ViewerId,
pub workspace: Entity,
pub rows: u16,
pub cols: u16,
pub selection: Selection,
pub queue: VecDeque<ViewerRequest>,
pub barrier: Option<Entity>,
pub generation: u64,
pub layout: Vec<(Entity, Rect)>,
pub sent: BTreeMap<PaneId, Sent>,
pub sent_tabs: Vec<crate::view::TabEntry>,
pub sent_workspace_label: Option<String>,
pub dirty: bool,
pub pending: bool,
pub publish_now: bool,
pub input_ms: u64,
pub last_frame_ms: u64,
pub notice: Option<String>,
pub after_frame: Vec<ServerMessage>,
pub detaching: bool,
pub exit_sent: bool,
}
impl Viewer {
#[must_use]
pub fn focused(&self) -> Option<Entity> {
self.selection.focused()
}
#[must_use]
pub fn attached_to(&self, workspace: Entity) -> bool {
self.workspace == workspace && !self.detaching
}
}
fn release_pane_id(mut world: DeferredWorld, context: HookContext) {
let id = world.get_mut::<Pane>(context.entity).map(|pane| pane.id);
if let (Some(id), Some(mut ids)) = (id, world.get_resource_mut::<super::resources::Ids>()) {
ids.panes.remove(&id);
}
}
fn release_tab_id(mut world: DeferredWorld, context: HookContext) {
let id = world.get_mut::<Tab>(context.entity).map(|tab| tab.id);
if let (Some(id), Some(mut ids)) = (id, world.get_resource_mut::<super::resources::Ids>()) {
ids.tabs.remove(&id);
}
}
fn release_viewer_id(mut world: DeferredWorld, context: HookContext) {
let id = world
.get_mut::<Viewer>(context.entity)
.map(|viewer| viewer.id);
if let (Some(id), Some(mut ids)) = (id, world.get_resource_mut::<super::resources::Ids>()) {
ids.viewers.remove(&id);
}
}
fn release_workspace_name(mut world: DeferredWorld, context: HookContext) {
let name = world
.get_mut::<Workspace>(context.entity)
.map(|workspace| workspace.name.clone());
if let (Some(name), Some(mut ids)) = (name, world.get_resource_mut::<super::resources::Ids>()) {
ids.workspaces.remove(&name);
}
}