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#[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}