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