fux 0.2.0

Agent-native terminal workspace
Documentation
use super::{
    AgentFlags, AgentState, Cell, CopyState, Cursor, PaneId, PaneModes, PaneView, Popup, Tab,
    TabId, WorkspaceMetadata, WorkspaceState,
};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};

#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct CellRun {
    pub start: usize,
    pub cells: Vec<Cell>,
}

#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct PaneDelta {
    pub full: Option<PaneView>,
    pub cell_runs: Vec<CellRun>,
    pub cursor: Option<Cursor>,
    pub modes: Option<PaneModes>,
    pub title: Option<String>,
    pub agent_id: Option<Option<String>>,
    pub agent_state: Option<AgentState>,
    pub agent_flags: Option<AgentFlags>,
    pub agent_sequence: Option<u64>,
    pub agent_exited: Option<bool>,
    pub agent_message: Option<Option<String>>,
    pub viewport_offset: Option<u32>,
    pub copy: Option<CopyState>,
    pub wrapped_rows: Option<Vec<bool>>,
}

#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct WorkspaceDiff {
    pub removed_panes: Vec<PaneId>,
    pub panes: BTreeMap<PaneId, PaneDelta>,
    pub tabs: Option<Vec<Tab>>,
    pub active_tab: Option<Option<TabId>>,
    pub popups: Option<Vec<Popup>>,
    pub metadata: Option<WorkspaceMetadata>,
}

impl WorkspaceDiff {
    #[must_use]
    pub fn between(base: &WorkspaceState, target: &WorkspaceState) -> Self {
        let removed_panes = base
            .panes()
            .keys()
            .filter(|id| !target.panes().contains_key(id))
            .copied()
            .collect();
        let mut panes = BTreeMap::new();
        for (id, pane) in target.panes() {
            let delta = match base.pane(*id) {
                None => PaneDelta {
                    full: Some(pane.clone()),
                    ..PaneDelta::default()
                },
                Some(old)
                    if old.rows != pane.rows
                        || old.columns != pane.columns
                        || old.cells.len() != pane.cells.len() =>
                {
                    PaneDelta {
                        full: Some(pane.clone()),
                        ..PaneDelta::default()
                    }
                }
                Some(old) => pane_delta(old, pane),
            };
            if delta != PaneDelta::default() {
                panes.insert(*id, delta);
            }
        }
        Self {
            removed_panes,
            panes,
            tabs: (base.tabs() != target.tabs()).then(|| target.tabs().to_vec()),
            active_tab: (base.active_tab() != target.active_tab()).then(|| target.active_tab()),
            popups: (base.popups() != target.popups()).then(|| target.popups().to_vec()),
            metadata: (base.metadata() != target.metadata()).then(|| target.metadata().clone()),
        }
    }

    pub(crate) fn apply_to(&self, state: &mut WorkspaceState) {
        let mut panes = state.panes().clone();
        let removed: BTreeSet<_> = self.removed_panes.iter().copied().collect();
        for id in removed {
            panes.remove(&id);
        }
        for (id, delta) in &self.panes {
            if let Some(full) = &delta.full {
                if full.valid() {
                    panes.insert(*id, full.clone());
                }
                continue;
            }
            let Some(pane) = panes.get_mut(id) else {
                continue;
            };
            apply_pane_delta(pane, delta);
        }
        let tabs = self.tabs.as_deref().unwrap_or(state.tabs()).to_vec();
        let active = self.active_tab.unwrap_or(state.active_tab());
        let popups = self.popups.as_deref().unwrap_or(state.popups()).to_vec();
        let metadata = self.metadata.as_ref().unwrap_or(state.metadata()).clone();
        let candidate = WorkspaceState::from_parts(tabs, active, panes, popups, metadata);
        if candidate.validate().is_ok() && candidate.validate_complete_topology().is_ok() {
            *state = candidate;
        }
    }
}

fn pane_delta(old: &PaneView, new: &PaneView) -> PaneDelta {
    PaneDelta {
        full: None,
        cell_runs: changed_runs(&old.cells, &new.cells),
        cursor: (old.cursor != new.cursor).then_some(new.cursor),
        modes: (old.modes != new.modes).then_some(new.modes),
        title: (old.title != new.title).then(|| new.title.clone()),
        agent_id: (old.agent.id != new.agent.id).then(|| new.agent.id.clone()),
        agent_state: (old.agent.state != new.agent.state).then_some(new.agent.state),
        agent_flags: (old.agent.flags != new.agent.flags).then_some(new.agent.flags),
        agent_sequence: (old.agent.sequence != new.agent.sequence).then_some(new.agent.sequence),
        agent_exited: (old.agent.exited != new.agent.exited).then_some(new.agent.exited),
        agent_message: (old.agent.message != new.agent.message).then(|| new.agent.message.clone()),
        viewport_offset: (old.viewport_offset != new.viewport_offset)
            .then_some(new.viewport_offset),
        copy: (old.copy != new.copy).then_some(new.copy),
        wrapped_rows: (old.wrapped_rows != new.wrapped_rows).then(|| new.wrapped_rows.clone()),
    }
}

fn changed_runs(old: &[Cell], new: &[Cell]) -> Vec<CellRun> {
    let mut output = Vec::new();
    let mut index = 0;
    while index < new.len() {
        if old.get(index) == new.get(index) {
            index += 1;
            continue;
        }
        let start = index;
        let mut cells = Vec::new();
        while index < new.len() && old.get(index) != new.get(index) {
            if let Some(cell) = new.get(index) {
                cells.push(cell.clone());
            }
            index += 1;
        }
        output.push(CellRun { start, cells });
    }
    output
}

fn apply_pane_delta(pane: &mut PaneView, delta: &PaneDelta) {
    for run in &delta.cell_runs {
        let Some(end) = run.start.checked_add(run.cells.len()) else {
            continue;
        };
        let Some(target) = pane.cells.get_mut(run.start..end) else {
            continue;
        };
        target.clone_from_slice(&run.cells);
    }
    if let Some(value) = delta.cursor {
        pane.cursor = value;
    }
    if let Some(value) = delta.modes {
        pane.modes = value;
    }
    if let Some(value) = &delta.title {
        pane.title.clone_from(value);
    }
    if let Some(value) = &delta.agent_id {
        pane.agent.id.clone_from(value);
    }
    if let Some(value) = delta.agent_state {
        pane.agent.state = value;
    }
    if let Some(value) = delta.agent_flags {
        pane.agent.flags = value;
    }
    if let Some(value) = delta.agent_sequence {
        pane.agent.sequence = value;
    }
    if let Some(value) = delta.agent_exited {
        pane.agent.exited = value;
    }
    if let Some(value) = &delta.agent_message {
        pane.agent.message.clone_from(value);
    }
    if let Some(value) = delta.viewport_offset {
        pane.viewport_offset = value;
    }
    if let Some(value) = delta.copy {
        pane.copy = value;
    }
    if let Some(value) = &delta.wrapped_rows
        && value.len() == usize::from(pane.rows)
    {
        pane.wrapped_rows.clone_from(value);
    }
}