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