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