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