Skip to main content

wsx_core/runtime/
domain.rs

1use serde::{ser::SerializeTuple, Deserialize, Deserializer, Serialize, Serializer};
2use std::path::PathBuf;
3
4macro_rules! id_type {
5    ($name:ident) => {
6        #[derive(
7            Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize,
8        )]
9        #[serde(transparent)]
10        pub struct $name(pub u64);
11        impl std::fmt::Display for $name {
12            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
13                self.0.fmt(f)
14            }
15        }
16        impl std::str::FromStr for $name {
17            type Err = std::num::ParseIntError;
18            fn from_str(value: &str) -> Result<Self, Self::Err> {
19                value.parse().map(Self)
20            }
21        }
22    };
23}
24
25id_type!(ProjectId);
26id_type!(WorktreeId);
27id_type!(SessionId);
28id_type!(PaneId);
29id_type!(TerminalId);
30id_type!(AgentInstanceId);
31
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
33pub struct ProjectSpec {
34    pub path: PathBuf,
35    pub name: String,
36    pub worktrees: Vec<WorktreeSpec>,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40pub struct WorktreeSpec {
41    pub path: PathBuf,
42    pub branch: String,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46pub struct Project {
47    pub id: ProjectId,
48    pub path: PathBuf,
49    pub name: String,
50    pub revision: u64,
51    #[serde(default)]
52    pub last_agent_active_unix_ms: Option<u64>,
53    #[serde(default)]
54    pub last_terminal_active_unix_ms: Option<u64>,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58pub struct Worktree {
59    pub id: WorktreeId,
60    pub project_id: ProjectId,
61    pub path: PathBuf,
62    pub branch: String,
63    pub revision: u64,
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
67#[serde(rename_all = "snake_case")]
68pub enum SplitAxis {
69    Horizontal,
70    Vertical,
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
74#[serde(tag = "kind", rename_all = "snake_case")]
75pub enum PaneLayout {
76    Leaf {
77        pane_id: PaneId,
78    },
79    Split {
80        axis: SplitAxis,
81        ratio_millis: u16,
82        first: Box<PaneLayout>,
83        second: Box<PaneLayout>,
84    },
85}
86
87impl PaneLayout {
88    pub fn panes(&self, output: &mut Vec<PaneId>) {
89        match self {
90            Self::Leaf { pane_id } => output.push(*pane_id),
91            Self::Split { first, second, .. } => {
92                first.panes(output);
93                second.panes(output);
94            }
95        }
96    }
97
98    pub fn split(&mut self, target: PaneId, pane_id: PaneId, axis: SplitAxis) -> bool {
99        match self {
100            Self::Leaf { pane_id: current } if *current == target => {
101                *self = Self::Split {
102                    axis,
103                    ratio_millis: 500,
104                    first: Box::new(Self::Leaf { pane_id: target }),
105                    second: Box::new(Self::Leaf { pane_id }),
106                };
107                true
108            }
109            Self::Split { first, second, .. } => {
110                first.split(target, pane_id, axis) || second.split(target, pane_id, axis)
111            }
112            Self::Leaf { .. } => false,
113        }
114    }
115
116    pub fn remove(&mut self, target: PaneId) -> bool {
117        match self {
118            Self::Leaf { .. } => false,
119            Self::Split { first, second, .. } => {
120                if matches!(first.as_ref(), Self::Leaf { pane_id } if *pane_id == target) {
121                    *self = (**second).clone();
122                    true
123                } else if matches!(second.as_ref(), Self::Leaf { pane_id } if *pane_id == target) {
124                    *self = (**first).clone();
125                    true
126                } else {
127                    first.remove(target) || second.remove(target)
128                }
129            }
130        }
131    }
132}
133
134#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
135#[serde(rename_all = "snake_case")]
136pub enum SessionPlacement {
137    Before,
138    After,
139}
140
141#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
142pub struct Session {
143    pub id: SessionId,
144    pub worktree_id: WorktreeId,
145    pub label: String,
146    pub primary_pane: PaneId,
147    pub focused_pane: PaneId,
148    pub panes: Vec<PaneId>,
149    pub layout: PaneLayout,
150    pub revision: u64,
151}
152
153#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
154#[serde(rename_all = "snake_case")]
155pub enum AgentState {
156    #[default]
157    Unknown,
158    Idle,
159    Working,
160    Blocked,
161    Done,
162    Error,
163}
164
165const MAX_AGENT_SESSION_ID_BYTES: usize = 512;
166const MAX_AGENT_SESSION_PATH_BYTES: usize = 4096;
167
168#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
169#[serde(rename_all = "snake_case")]
170pub enum AgentSessionRefKind {
171    Id,
172    Path,
173}
174
175#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
176pub struct AgentSessionRef {
177    pub kind: AgentSessionRefKind,
178    pub value: String,
179}
180
181impl AgentSessionRef {
182    pub fn id(value: impl Into<String>) -> Option<Self> {
183        let value = value.into();
184        (valid_agent_session_value(&value, MAX_AGENT_SESSION_ID_BYTES) && !value.starts_with('-'))
185            .then_some(Self {
186                kind: AgentSessionRefKind::Id,
187                value,
188            })
189    }
190
191    pub fn path(value: impl Into<String>) -> Option<Self> {
192        let value = value.into();
193        (valid_agent_session_value(&value, MAX_AGENT_SESSION_PATH_BYTES)
194            && PathBuf::from(&value).is_absolute())
195        .then_some(Self {
196            kind: AgentSessionRefKind::Path,
197            value,
198        })
199    }
200}
201
202fn valid_agent_session_value(value: &str, max_bytes: usize) -> bool {
203    !value.is_empty() && value.len() <= max_bytes && !value.chars().any(char::is_control)
204}
205
206#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
207#[serde(default)]
208pub struct AgentCapabilities {
209    pub prompt: bool,
210    pub resume: bool,
211    pub lifecycle: bool,
212    pub escape_interrupts: bool,
213}
214
215#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
216pub struct AgentInfo {
217    pub id: AgentInstanceId,
218    pub provider: String,
219    pub state: AgentState,
220    #[serde(default)]
221    pub conversation_id: Option<String>,
222    #[serde(default)]
223    pub session_ref: Option<AgentSessionRef>,
224    pub capabilities: AgentCapabilities,
225    pub source: String,
226}
227
228#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
229pub struct Pane {
230    pub id: PaneId,
231    pub terminal_id: TerminalId,
232    pub session_id: SessionId,
233    pub label: String,
234    pub agent: Option<AgentInfo>,
235    pub exited: bool,
236    pub revision: u64,
237}
238
239#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
240pub struct PanePorts {
241    pub pane_id: PaneId,
242    pub tcp: Vec<u16>,
243}
244
245#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
246pub struct PaneActivity {
247    pub pane_id: PaneId,
248    pub foreground_job: bool,
249}
250
251#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
252pub struct Snapshot {
253    pub protocol: u32,
254    pub epoch: u64,
255    pub revision: u64,
256    pub projects: Vec<Project>,
257    pub worktrees: Vec<Worktree>,
258    pub sessions: Vec<Session>,
259    pub panes: Vec<Pane>,
260    #[serde(default, skip_serializing_if = "Vec::is_empty")]
261    pub listening_ports: Vec<PanePorts>,
262    #[serde(default, skip_serializing_if = "Vec::is_empty")]
263    pub pane_activity: Vec<PaneActivity>,
264    #[serde(default)]
265    pub capabilities: Capabilities,
266}
267
268#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
269#[serde(default)]
270pub struct Capabilities {
271    pub pane_splits: bool,
272    pub plugins: bool,
273    pub agent_reports: bool,
274    pub agent_session_restore: bool,
275    pub resume_shell_fallback: bool,
276    pub listening_ports: bool,
277    pub foreground_jobs: bool,
278    pub process_restore: bool,
279    pub lifecycle_coordination: bool,
280    pub version_coordination: bool,
281    pub daemon_revision_coordination: bool,
282}
283
284#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
285#[serde(rename_all = "snake_case")]
286pub enum DaemonPhase {
287    #[default]
288    Ready,
289    ReplacementPending,
290    Stopping,
291}
292
293#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
294pub struct DaemonLifecycle {
295    pub protocol: u32,
296    pub epoch: u64,
297    pub binary_id: String,
298    #[serde(default)]
299    pub version: String,
300    #[serde(default)]
301    pub daemon_revision: u32,
302    pub started_unix_ms: u64,
303    pub phase: DaemonPhase,
304    pub live_runtimes: usize,
305    pub active_clients: usize,
306    #[serde(default)]
307    pub active_tuis: usize,
308    pub recovered_from_backup: bool,
309    #[serde(default)]
310    pub replacement_target: Option<String>,
311    #[serde(default)]
312    pub replacement_target_version: String,
313    #[serde(default)]
314    pub replacement_blockers: Vec<ReplacementBlocker>,
315}
316
317#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
318pub struct TuiClientPresence {
319    pub instance_id: u64,
320    pub version: String,
321    pub target_binary_id: String,
322    #[serde(default)]
323    pub target_daemon_revision: u32,
324}
325
326#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
327#[serde(rename_all = "snake_case")]
328pub enum ReplacementDisposition {
329    Deferred,
330    Stopping,
331}
332
333#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
334#[serde(rename_all = "snake_case")]
335pub enum ReplacementBlocker {
336    OtherTui,
337    WorkingAgent,
338    ListenerScanPending,
339    ForegroundJob,
340    ListeningPort,
341    LegacyDaemon,
342    PendingTarget,
343}
344
345#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
346pub struct CellModifiers {
347    pub bold: bool,
348    pub italic: bool,
349    pub underline: bool,
350    pub inverse: bool,
351    pub dim: bool,
352    pub strike: bool,
353}
354
355#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
356#[serde(rename_all = "snake_case")]
357pub enum CellWidth {
358    #[default]
359    Narrow,
360    Wide,
361    SpacerHead,
362    SpacerTail,
363}
364
365#[derive(Debug, Clone, PartialEq, Eq, Default)]
366pub struct Cell {
367    pub symbol: String,
368    pub fg: Option<[u8; 3]>,
369    pub bg: Option<[u8; 3]>,
370    pub modifiers: CellModifiers,
371    pub width: CellWidth,
372}
373
374impl CellModifiers {
375    fn bits(self) -> u8 {
376        u8::from(self.bold)
377            | (u8::from(self.italic) << 1)
378            | (u8::from(self.underline) << 2)
379            | (u8::from(self.inverse) << 3)
380            | (u8::from(self.dim) << 4)
381            | (u8::from(self.strike) << 5)
382    }
383
384    fn from_bits(bits: u8) -> Result<Self, &'static str> {
385        if bits & !0x3f != 0 {
386            return Err("terminal cell modifier bits are invalid");
387        }
388        Ok(Self {
389            bold: bits & 1 != 0,
390            italic: bits & 2 != 0,
391            underline: bits & 4 != 0,
392            inverse: bits & 8 != 0,
393            dim: bits & 16 != 0,
394            strike: bits & 32 != 0,
395        })
396    }
397}
398
399impl CellWidth {
400    fn code(self) -> u8 {
401        match self {
402            Self::Narrow => 0,
403            Self::Wide => 1,
404            Self::SpacerHead => 2,
405            Self::SpacerTail => 3,
406        }
407    }
408
409    fn from_code(code: u8) -> Result<Self, &'static str> {
410        match code {
411            0 => Ok(Self::Narrow),
412            1 => Ok(Self::Wide),
413            2 => Ok(Self::SpacerHead),
414            3 => Ok(Self::SpacerTail),
415            _ => Err("terminal cell width code is invalid"),
416        }
417    }
418}
419
420impl Serialize for Cell {
421    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
422    where
423        S: Serializer,
424    {
425        let mut tuple = serializer.serialize_tuple(5)?;
426        tuple.serialize_element(&self.symbol)?;
427        tuple.serialize_element(&self.fg)?;
428        tuple.serialize_element(&self.bg)?;
429        tuple.serialize_element(&self.modifiers.bits())?;
430        tuple.serialize_element(&self.width.code())?;
431        tuple.end()
432    }
433}
434
435impl<'de> Deserialize<'de> for Cell {
436    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
437    where
438        D: Deserializer<'de>,
439    {
440        let (symbol, fg, bg, modifier_bits, width_code) =
441            <(String, Option<[u8; 3]>, Option<[u8; 3]>, u8, u8)>::deserialize(deserializer)?;
442        Ok(Self {
443            symbol,
444            fg,
445            bg,
446            modifiers: CellModifiers::from_bits(modifier_bits).map_err(serde::de::Error::custom)?,
447            width: CellWidth::from_code(width_code).map_err(serde::de::Error::custom)?,
448        })
449    }
450}
451
452#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
453pub struct Cursor {
454    pub x: u16,
455    pub y: u16,
456    pub visible: bool,
457    pub blinking: bool,
458    pub shape: u8,
459}
460
461// ^ [[Terminal Presentation and Latency]] Range changes must stay aligned with
462// Ghostty projection, legacy protocol defaults, daemon lease cleanup, and TUI rendering.
463#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
464pub struct TerminalSelectionRange {
465    pub row: u16,
466    pub start_col: u16,
467    pub end_col: u16,
468}
469
470#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
471pub struct TerminalFrame {
472    pub pane_id: PaneId,
473    pub terminal_id: TerminalId,
474    pub revision: u64,
475    pub cols: u16,
476    pub rows: u16,
477    pub cells: Vec<Cell>,
478    pub cursor: Cursor,
479    #[serde(default)]
480    pub selection: Vec<TerminalSelectionRange>,
481}
482
483#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
484pub struct TerminalRowPatch {
485    pub row: u16,
486    pub cells: Vec<Cell>,
487}
488
489#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
490#[serde(tag = "kind", content = "data", rename_all = "snake_case")]
491pub enum TerminalUpdate {
492    Full(TerminalFrame),
493    Patch {
494        pane_id: PaneId,
495        terminal_id: TerminalId,
496        base_revision: u64,
497        revision: u64,
498        cols: u16,
499        rows: u16,
500        changed_rows: Vec<TerminalRowPatch>,
501        cursor: Cursor,
502        #[serde(default)]
503        selection: Vec<TerminalSelectionRange>,
504    },
505}
506
507impl TerminalUpdate {
508    pub fn identity(&self) -> (PaneId, TerminalId) {
509        match self {
510            Self::Full(frame) => (frame.pane_id, frame.terminal_id),
511            Self::Patch {
512                pane_id,
513                terminal_id,
514                ..
515            } => (*pane_id, *terminal_id),
516        }
517    }
518
519    pub fn revision(&self) -> u64 {
520        match self {
521            Self::Full(frame) => frame.revision,
522            Self::Patch { revision, .. } => *revision,
523        }
524    }
525
526    pub fn apply_to(self, frame: &mut Option<TerminalFrame>) -> Result<(), &'static str> {
527        let (
528            pane_id,
529            terminal_id,
530            base_revision,
531            revision,
532            cols,
533            rows,
534            changed_rows,
535            cursor,
536            selection,
537        ) = match self {
538            Self::Full(full) => {
539                if full.rows == 0
540                    || full.cols == 0
541                    || full.cells.len() != usize::from(full.cols) * usize::from(full.rows)
542                    || !valid_terminal_selection(&full.selection, full.rows, full.cols)
543                {
544                    return Err("terminal full frame dimensions are invalid");
545                }
546                *frame = Some(full);
547                return Ok(());
548            }
549            Self::Patch {
550                pane_id,
551                terminal_id,
552                base_revision,
553                revision,
554                cols,
555                rows,
556                changed_rows,
557                cursor,
558                selection,
559            } => (
560                pane_id,
561                terminal_id,
562                base_revision,
563                revision,
564                cols,
565                rows,
566                changed_rows,
567                cursor,
568                selection,
569            ),
570        };
571        if rows == 0
572            || cols == 0
573            || changed_rows.len() > usize::from(rows)
574            || !valid_terminal_selection(&selection, rows, cols)
575        {
576            return Err("terminal patch dimensions are invalid");
577        }
578        let current = frame.as_mut().ok_or("terminal patch has no baseline")?;
579        if current.pane_id != pane_id
580            || current.terminal_id != terminal_id
581            || current.revision != base_revision
582            || current.cols != cols
583            || current.rows != rows
584            || current.cells.len() != usize::from(cols) * usize::from(rows)
585        {
586            return Err("terminal patch baseline does not match");
587        }
588        for patch in changed_rows {
589            if patch.row >= rows || patch.cells.len() != usize::from(cols) {
590                return Err("terminal patch row is invalid");
591            }
592            let start = usize::from(patch.row) * usize::from(cols);
593            current.cells[start..start + usize::from(cols)].clone_from_slice(&patch.cells);
594        }
595        current.revision = revision;
596        current.cursor = cursor;
597        current.selection = selection;
598        Ok(())
599    }
600}
601
602fn valid_terminal_selection(selection: &[TerminalSelectionRange], rows: u16, cols: u16) -> bool {
603    selection.len() <= usize::from(rows)
604        && selection.iter().all(|range| {
605            range.row < rows && range.start_col <= range.end_col && range.end_col < cols
606        })
607        && selection.windows(2).all(|pair| pair[0].row < pair[1].row)
608}
609
610#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
611#[serde(rename_all = "snake_case")]
612pub enum KeyCode {
613    Text,
614    Enter,
615    Backspace,
616    Tab,
617    Escape,
618    Insert,
619    Delete,
620    Home,
621    End,
622    PageUp,
623    PageDown,
624    Left,
625    Right,
626    Up,
627    Down,
628    Function(u8),
629}
630
631#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
632pub struct KeyEvent {
633    pub code: KeyCode,
634    pub text: String,
635    pub shift: bool,
636    pub control: bool,
637    pub alt: bool,
638    pub super_key: bool,
639    pub repeat: bool,
640}
641
642#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
643#[serde(rename_all = "snake_case")]
644pub enum MouseAction {
645    Press,
646    Release,
647    Motion,
648}
649
650#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
651#[serde(rename_all = "snake_case")]
652pub enum MouseButton {
653    Left,
654    Middle,
655    Right,
656    WheelUp,
657    WheelDown,
658    WheelLeft,
659    WheelRight,
660    None,
661}
662
663#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
664pub struct MouseEvent {
665    pub action: MouseAction,
666    pub button: MouseButton,
667    pub x: u16,
668    pub y: u16,
669    #[serde(default = "default_true")]
670    pub in_bounds: bool,
671    pub shift: bool,
672    pub control: bool,
673    pub alt: bool,
674    pub super_key: bool,
675}
676
677const fn default_true() -> bool {
678    true
679}
680
681#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
682pub struct PluginManifest {
683    pub api_version: u32,
684    pub id: String,
685    pub name: String,
686    pub command: Vec<String>,
687    pub events: Vec<String>,
688    pub enabled: bool,
689}
690
691#[cfg(test)]
692mod tests {
693    use super::*;
694
695    #[test]
696    fn old_project_json_without_activity_deserializes() {
697        let project: Project =
698            serde_json::from_str(r#"{"id":1,"path":"/repo","name":"repo","revision":2}"#).unwrap();
699        assert_eq!(project.last_agent_active_unix_ms, None);
700        assert_eq!(project.last_terminal_active_unix_ms, None);
701    }
702
703    #[test]
704    fn split_and_remove_preserve_pane_identity() {
705        let mut layout = PaneLayout::Leaf { pane_id: PaneId(1) };
706        assert!(layout.split(PaneId(1), PaneId(2), SplitAxis::Vertical));
707        let mut panes = Vec::new();
708        layout.panes(&mut panes);
709        assert_eq!(panes, vec![PaneId(1), PaneId(2)]);
710        assert!(layout.remove(PaneId(1)));
711        assert_eq!(layout, PaneLayout::Leaf { pane_id: PaneId(2) });
712    }
713
714    #[test]
715    fn terminal_cell_compact_wire_round_trips_width_and_style() {
716        let cell = Cell {
717            symbol: "界".into(),
718            fg: Some([1, 2, 3]),
719            bg: Some([4, 5, 6]),
720            modifiers: CellModifiers {
721                bold: true,
722                underline: true,
723                ..CellModifiers::default()
724            },
725            width: CellWidth::Wide,
726        };
727        let encoded = serde_json::to_string(&cell).unwrap();
728        assert_eq!(encoded, r#"["界",[1,2,3],[4,5,6],5,1]"#);
729        assert_eq!(serde_json::from_str::<Cell>(&encoded).unwrap(), cell);
730        assert!(serde_json::from_str::<Cell>(r#"["x",null,null,64,0]"#).is_err());
731        assert!(serde_json::from_str::<Cell>(r#"["x",null,null,0,4]"#).is_err());
732    }
733
734    #[test]
735    fn terminal_full_frame_rejects_invalid_cell_count() {
736        let mut baseline = None;
737        assert!(TerminalUpdate::Full(TerminalFrame {
738            pane_id: PaneId(1),
739            terminal_id: TerminalId(2),
740            revision: 1,
741            cols: 2,
742            rows: 2,
743            cells: vec![Cell::default(); 3],
744            cursor: Cursor {
745                x: 0,
746                y: 0,
747                visible: false,
748                blinking: false,
749                shape: 0,
750            },
751            selection: Vec::new(),
752        })
753        .apply_to(&mut baseline)
754        .is_err());
755        assert!(baseline.is_none());
756
757        assert!(TerminalUpdate::Full(TerminalFrame {
758            pane_id: PaneId(1),
759            terminal_id: TerminalId(2),
760            revision: 1,
761            cols: 2,
762            rows: 2,
763            cells: vec![Cell::default(); 4],
764            cursor: Cursor {
765                x: 0,
766                y: 0,
767                visible: false,
768                blinking: false,
769                shape: 0,
770            },
771            selection: vec![TerminalSelectionRange {
772                row: 0,
773                start_col: 0,
774                end_col: 2,
775            }],
776        })
777        .apply_to(&mut baseline)
778        .is_err());
779        assert!(baseline.is_none());
780    }
781
782    #[test]
783    fn terminal_patch_requires_and_updates_the_exact_baseline() {
784        let cursor = Cursor {
785            x: 0,
786            y: 0,
787            visible: true,
788            blinking: false,
789            shape: 0,
790        };
791        let mut frame = Some(TerminalFrame {
792            pane_id: PaneId(1),
793            terminal_id: TerminalId(2),
794            revision: 3,
795            cols: 2,
796            rows: 2,
797            cells: vec![Cell::default(); 4],
798            cursor,
799            selection: Vec::new(),
800        });
801        TerminalUpdate::Patch {
802            pane_id: PaneId(1),
803            terminal_id: TerminalId(2),
804            base_revision: 3,
805            revision: 4,
806            cols: 2,
807            rows: 2,
808            changed_rows: vec![TerminalRowPatch {
809                row: 1,
810                cells: vec![
811                    Cell {
812                        symbol: "x".into(),
813                        ..Cell::default()
814                    },
815                    Cell::default(),
816                ],
817            }],
818            cursor: Cursor {
819                x: 1,
820                y: 1,
821                ..cursor
822            },
823            selection: vec![TerminalSelectionRange {
824                row: 1,
825                start_col: 0,
826                end_col: 1,
827            }],
828        }
829        .apply_to(&mut frame)
830        .unwrap();
831        let frame = frame.unwrap();
832        assert_eq!(frame.revision, 4);
833        assert_eq!(frame.cells[2].symbol, "x");
834        assert_eq!((frame.cursor.x, frame.cursor.y), (1, 1));
835        assert_eq!(frame.selection.len(), 1);
836
837        let mut frame = Some(frame);
838        assert!(TerminalUpdate::Patch {
839            pane_id: PaneId(1),
840            terminal_id: TerminalId(2),
841            base_revision: 3,
842            revision: 5,
843            cols: 2,
844            rows: 2,
845            changed_rows: vec![],
846            cursor,
847            selection: Vec::new(),
848        }
849        .apply_to(&mut frame)
850        .is_err());
851    }
852}