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}
282
283#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
284#[serde(rename_all = "snake_case")]
285pub enum DaemonPhase {
286    #[default]
287    Ready,
288    ReplacementPending,
289    Stopping,
290}
291
292#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
293pub struct DaemonLifecycle {
294    pub protocol: u32,
295    pub epoch: u64,
296    pub binary_id: String,
297    #[serde(default)]
298    pub version: String,
299    pub started_unix_ms: u64,
300    pub phase: DaemonPhase,
301    pub live_runtimes: usize,
302    pub active_clients: usize,
303    #[serde(default)]
304    pub active_tuis: usize,
305    pub recovered_from_backup: bool,
306    #[serde(default)]
307    pub replacement_target: Option<String>,
308    #[serde(default)]
309    pub replacement_target_version: String,
310    #[serde(default)]
311    pub replacement_blockers: Vec<ReplacementBlocker>,
312}
313
314#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
315pub struct TuiClientPresence {
316    pub instance_id: u64,
317    pub version: String,
318    pub target_binary_id: String,
319}
320
321#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
322#[serde(rename_all = "snake_case")]
323pub enum ReplacementDisposition {
324    Deferred,
325    Stopping,
326}
327
328#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
329#[serde(rename_all = "snake_case")]
330pub enum ReplacementBlocker {
331    OtherTui,
332    WorkingAgent,
333    LegacyDaemon,
334    PendingTarget,
335}
336
337#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
338pub struct CellModifiers {
339    pub bold: bool,
340    pub italic: bool,
341    pub underline: bool,
342    pub inverse: bool,
343    pub dim: bool,
344    pub strike: bool,
345}
346
347#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
348#[serde(rename_all = "snake_case")]
349pub enum CellWidth {
350    #[default]
351    Narrow,
352    Wide,
353    SpacerHead,
354    SpacerTail,
355}
356
357#[derive(Debug, Clone, PartialEq, Eq, Default)]
358pub struct Cell {
359    pub symbol: String,
360    pub fg: Option<[u8; 3]>,
361    pub bg: Option<[u8; 3]>,
362    pub modifiers: CellModifiers,
363    pub width: CellWidth,
364}
365
366impl CellModifiers {
367    fn bits(self) -> u8 {
368        u8::from(self.bold)
369            | (u8::from(self.italic) << 1)
370            | (u8::from(self.underline) << 2)
371            | (u8::from(self.inverse) << 3)
372            | (u8::from(self.dim) << 4)
373            | (u8::from(self.strike) << 5)
374    }
375
376    fn from_bits(bits: u8) -> Result<Self, &'static str> {
377        if bits & !0x3f != 0 {
378            return Err("terminal cell modifier bits are invalid");
379        }
380        Ok(Self {
381            bold: bits & 1 != 0,
382            italic: bits & 2 != 0,
383            underline: bits & 4 != 0,
384            inverse: bits & 8 != 0,
385            dim: bits & 16 != 0,
386            strike: bits & 32 != 0,
387        })
388    }
389}
390
391impl CellWidth {
392    fn code(self) -> u8 {
393        match self {
394            Self::Narrow => 0,
395            Self::Wide => 1,
396            Self::SpacerHead => 2,
397            Self::SpacerTail => 3,
398        }
399    }
400
401    fn from_code(code: u8) -> Result<Self, &'static str> {
402        match code {
403            0 => Ok(Self::Narrow),
404            1 => Ok(Self::Wide),
405            2 => Ok(Self::SpacerHead),
406            3 => Ok(Self::SpacerTail),
407            _ => Err("terminal cell width code is invalid"),
408        }
409    }
410}
411
412impl Serialize for Cell {
413    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
414    where
415        S: Serializer,
416    {
417        let mut tuple = serializer.serialize_tuple(5)?;
418        tuple.serialize_element(&self.symbol)?;
419        tuple.serialize_element(&self.fg)?;
420        tuple.serialize_element(&self.bg)?;
421        tuple.serialize_element(&self.modifiers.bits())?;
422        tuple.serialize_element(&self.width.code())?;
423        tuple.end()
424    }
425}
426
427impl<'de> Deserialize<'de> for Cell {
428    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
429    where
430        D: Deserializer<'de>,
431    {
432        let (symbol, fg, bg, modifier_bits, width_code) =
433            <(String, Option<[u8; 3]>, Option<[u8; 3]>, u8, u8)>::deserialize(deserializer)?;
434        Ok(Self {
435            symbol,
436            fg,
437            bg,
438            modifiers: CellModifiers::from_bits(modifier_bits).map_err(serde::de::Error::custom)?,
439            width: CellWidth::from_code(width_code).map_err(serde::de::Error::custom)?,
440        })
441    }
442}
443
444#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
445pub struct Cursor {
446    pub x: u16,
447    pub y: u16,
448    pub visible: bool,
449    pub blinking: bool,
450    pub shape: u8,
451}
452
453// ^ [[Terminal Presentation and Latency]] Range changes must stay aligned with
454// Ghostty projection, legacy protocol defaults, daemon lease cleanup, and TUI rendering.
455#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
456pub struct TerminalSelectionRange {
457    pub row: u16,
458    pub start_col: u16,
459    pub end_col: u16,
460}
461
462#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
463pub struct TerminalFrame {
464    pub pane_id: PaneId,
465    pub terminal_id: TerminalId,
466    pub revision: u64,
467    pub cols: u16,
468    pub rows: u16,
469    pub cells: Vec<Cell>,
470    pub cursor: Cursor,
471    #[serde(default)]
472    pub selection: Vec<TerminalSelectionRange>,
473}
474
475#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
476pub struct TerminalRowPatch {
477    pub row: u16,
478    pub cells: Vec<Cell>,
479}
480
481#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
482#[serde(tag = "kind", content = "data", rename_all = "snake_case")]
483pub enum TerminalUpdate {
484    Full(TerminalFrame),
485    Patch {
486        pane_id: PaneId,
487        terminal_id: TerminalId,
488        base_revision: u64,
489        revision: u64,
490        cols: u16,
491        rows: u16,
492        changed_rows: Vec<TerminalRowPatch>,
493        cursor: Cursor,
494        #[serde(default)]
495        selection: Vec<TerminalSelectionRange>,
496    },
497}
498
499impl TerminalUpdate {
500    pub fn identity(&self) -> (PaneId, TerminalId) {
501        match self {
502            Self::Full(frame) => (frame.pane_id, frame.terminal_id),
503            Self::Patch {
504                pane_id,
505                terminal_id,
506                ..
507            } => (*pane_id, *terminal_id),
508        }
509    }
510
511    pub fn revision(&self) -> u64 {
512        match self {
513            Self::Full(frame) => frame.revision,
514            Self::Patch { revision, .. } => *revision,
515        }
516    }
517
518    pub fn apply_to(self, frame: &mut Option<TerminalFrame>) -> Result<(), &'static str> {
519        let (
520            pane_id,
521            terminal_id,
522            base_revision,
523            revision,
524            cols,
525            rows,
526            changed_rows,
527            cursor,
528            selection,
529        ) = match self {
530            Self::Full(full) => {
531                if full.rows == 0
532                    || full.cols == 0
533                    || full.cells.len() != usize::from(full.cols) * usize::from(full.rows)
534                    || !valid_terminal_selection(&full.selection, full.rows, full.cols)
535                {
536                    return Err("terminal full frame dimensions are invalid");
537                }
538                *frame = Some(full);
539                return Ok(());
540            }
541            Self::Patch {
542                pane_id,
543                terminal_id,
544                base_revision,
545                revision,
546                cols,
547                rows,
548                changed_rows,
549                cursor,
550                selection,
551            } => (
552                pane_id,
553                terminal_id,
554                base_revision,
555                revision,
556                cols,
557                rows,
558                changed_rows,
559                cursor,
560                selection,
561            ),
562        };
563        if rows == 0
564            || cols == 0
565            || changed_rows.len() > usize::from(rows)
566            || !valid_terminal_selection(&selection, rows, cols)
567        {
568            return Err("terminal patch dimensions are invalid");
569        }
570        let current = frame.as_mut().ok_or("terminal patch has no baseline")?;
571        if current.pane_id != pane_id
572            || current.terminal_id != terminal_id
573            || current.revision != base_revision
574            || current.cols != cols
575            || current.rows != rows
576            || current.cells.len() != usize::from(cols) * usize::from(rows)
577        {
578            return Err("terminal patch baseline does not match");
579        }
580        for patch in changed_rows {
581            if patch.row >= rows || patch.cells.len() != usize::from(cols) {
582                return Err("terminal patch row is invalid");
583            }
584            let start = usize::from(patch.row) * usize::from(cols);
585            current.cells[start..start + usize::from(cols)].clone_from_slice(&patch.cells);
586        }
587        current.revision = revision;
588        current.cursor = cursor;
589        current.selection = selection;
590        Ok(())
591    }
592}
593
594fn valid_terminal_selection(selection: &[TerminalSelectionRange], rows: u16, cols: u16) -> bool {
595    selection.len() <= usize::from(rows)
596        && selection.iter().all(|range| {
597            range.row < rows && range.start_col <= range.end_col && range.end_col < cols
598        })
599        && selection.windows(2).all(|pair| pair[0].row < pair[1].row)
600}
601
602#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
603#[serde(rename_all = "snake_case")]
604pub enum KeyCode {
605    Text,
606    Enter,
607    Backspace,
608    Tab,
609    Escape,
610    Insert,
611    Delete,
612    Home,
613    End,
614    PageUp,
615    PageDown,
616    Left,
617    Right,
618    Up,
619    Down,
620    Function(u8),
621}
622
623#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
624pub struct KeyEvent {
625    pub code: KeyCode,
626    pub text: String,
627    pub shift: bool,
628    pub control: bool,
629    pub alt: bool,
630    pub super_key: bool,
631    pub repeat: bool,
632}
633
634#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
635#[serde(rename_all = "snake_case")]
636pub enum MouseAction {
637    Press,
638    Release,
639    Motion,
640}
641
642#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
643#[serde(rename_all = "snake_case")]
644pub enum MouseButton {
645    Left,
646    Middle,
647    Right,
648    WheelUp,
649    WheelDown,
650    WheelLeft,
651    WheelRight,
652    None,
653}
654
655#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
656pub struct MouseEvent {
657    pub action: MouseAction,
658    pub button: MouseButton,
659    pub x: u16,
660    pub y: u16,
661    #[serde(default = "default_true")]
662    pub in_bounds: bool,
663    pub shift: bool,
664    pub control: bool,
665    pub alt: bool,
666    pub super_key: bool,
667}
668
669const fn default_true() -> bool {
670    true
671}
672
673#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
674pub struct PluginManifest {
675    pub api_version: u32,
676    pub id: String,
677    pub name: String,
678    pub command: Vec<String>,
679    pub events: Vec<String>,
680    pub enabled: bool,
681}
682
683#[cfg(test)]
684mod tests {
685    use super::*;
686
687    #[test]
688    fn old_project_json_without_activity_deserializes() {
689        let project: Project =
690            serde_json::from_str(r#"{"id":1,"path":"/repo","name":"repo","revision":2}"#).unwrap();
691        assert_eq!(project.last_agent_active_unix_ms, None);
692        assert_eq!(project.last_terminal_active_unix_ms, None);
693    }
694
695    #[test]
696    fn split_and_remove_preserve_pane_identity() {
697        let mut layout = PaneLayout::Leaf { pane_id: PaneId(1) };
698        assert!(layout.split(PaneId(1), PaneId(2), SplitAxis::Vertical));
699        let mut panes = Vec::new();
700        layout.panes(&mut panes);
701        assert_eq!(panes, vec![PaneId(1), PaneId(2)]);
702        assert!(layout.remove(PaneId(1)));
703        assert_eq!(layout, PaneLayout::Leaf { pane_id: PaneId(2) });
704    }
705
706    #[test]
707    fn terminal_cell_compact_wire_round_trips_width_and_style() {
708        let cell = Cell {
709            symbol: "界".into(),
710            fg: Some([1, 2, 3]),
711            bg: Some([4, 5, 6]),
712            modifiers: CellModifiers {
713                bold: true,
714                underline: true,
715                ..CellModifiers::default()
716            },
717            width: CellWidth::Wide,
718        };
719        let encoded = serde_json::to_string(&cell).unwrap();
720        assert_eq!(encoded, r#"["界",[1,2,3],[4,5,6],5,1]"#);
721        assert_eq!(serde_json::from_str::<Cell>(&encoded).unwrap(), cell);
722        assert!(serde_json::from_str::<Cell>(r#"["x",null,null,64,0]"#).is_err());
723        assert!(serde_json::from_str::<Cell>(r#"["x",null,null,0,4]"#).is_err());
724    }
725
726    #[test]
727    fn terminal_full_frame_rejects_invalid_cell_count() {
728        let mut baseline = None;
729        assert!(TerminalUpdate::Full(TerminalFrame {
730            pane_id: PaneId(1),
731            terminal_id: TerminalId(2),
732            revision: 1,
733            cols: 2,
734            rows: 2,
735            cells: vec![Cell::default(); 3],
736            cursor: Cursor {
737                x: 0,
738                y: 0,
739                visible: false,
740                blinking: false,
741                shape: 0,
742            },
743            selection: Vec::new(),
744        })
745        .apply_to(&mut baseline)
746        .is_err());
747        assert!(baseline.is_none());
748
749        assert!(TerminalUpdate::Full(TerminalFrame {
750            pane_id: PaneId(1),
751            terminal_id: TerminalId(2),
752            revision: 1,
753            cols: 2,
754            rows: 2,
755            cells: vec![Cell::default(); 4],
756            cursor: Cursor {
757                x: 0,
758                y: 0,
759                visible: false,
760                blinking: false,
761                shape: 0,
762            },
763            selection: vec![TerminalSelectionRange {
764                row: 0,
765                start_col: 0,
766                end_col: 2,
767            }],
768        })
769        .apply_to(&mut baseline)
770        .is_err());
771        assert!(baseline.is_none());
772    }
773
774    #[test]
775    fn terminal_patch_requires_and_updates_the_exact_baseline() {
776        let cursor = Cursor {
777            x: 0,
778            y: 0,
779            visible: true,
780            blinking: false,
781            shape: 0,
782        };
783        let mut frame = Some(TerminalFrame {
784            pane_id: PaneId(1),
785            terminal_id: TerminalId(2),
786            revision: 3,
787            cols: 2,
788            rows: 2,
789            cells: vec![Cell::default(); 4],
790            cursor,
791            selection: Vec::new(),
792        });
793        TerminalUpdate::Patch {
794            pane_id: PaneId(1),
795            terminal_id: TerminalId(2),
796            base_revision: 3,
797            revision: 4,
798            cols: 2,
799            rows: 2,
800            changed_rows: vec![TerminalRowPatch {
801                row: 1,
802                cells: vec![
803                    Cell {
804                        symbol: "x".into(),
805                        ..Cell::default()
806                    },
807                    Cell::default(),
808                ],
809            }],
810            cursor: Cursor {
811                x: 1,
812                y: 1,
813                ..cursor
814            },
815            selection: vec![TerminalSelectionRange {
816                row: 1,
817                start_col: 0,
818                end_col: 1,
819            }],
820        }
821        .apply_to(&mut frame)
822        .unwrap();
823        let frame = frame.unwrap();
824        assert_eq!(frame.revision, 4);
825        assert_eq!(frame.cells[2].symbol, "x");
826        assert_eq!((frame.cursor.x, frame.cursor.y), (1, 1));
827        assert_eq!(frame.selection.len(), 1);
828
829        let mut frame = Some(frame);
830        assert!(TerminalUpdate::Patch {
831            pane_id: PaneId(1),
832            terminal_id: TerminalId(2),
833            base_revision: 3,
834            revision: 5,
835            cols: 2,
836            rows: 2,
837            changed_rows: vec![],
838            cursor,
839            selection: Vec::new(),
840        }
841        .apply_to(&mut frame)
842        .is_err());
843    }
844}