1use std::collections::BTreeMap;
2
3use lz4_flex::{compress_prepend_size, decompress_size_prepended};
4use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
5
6pub mod fs;
9
10pub mod git;
13
14pub mod lsp;
17
18pub mod kv;
21
22pub mod net;
25
26pub const MAX_DECOMPRESSED: usize = 64 * 1024 * 1024;
31
32pub const CELL_SIZE: usize = 12;
33const TITLE_PRESENT: u16 = 1 << 15;
34const OPS_PRESENT: u16 = 1 << 14;
35const STRINGS_PRESENT: u16 = 1 << 13;
36const LINE_FLAGS_PRESENT: u16 = 1 << 12;
37const TITLE_LEN_MASK: u16 = LINE_FLAGS_PRESENT - 1;
38
39pub const ROW_FLAG_WRAPPED: u8 = 1 << 0;
41
42const CONTENT_OVERFLOW: u8 = 7;
47
48const ENABLE_SCROLL_OPS: bool = true;
49const MODE_ECHO: u16 = 1 << 9;
50const MODE_ICANON: u16 = 1 << 10;
51
52const OP_COPY_RECT: u8 = 0x01;
53const OP_FILL_RECT: u8 = 0x02;
54const OP_PATCH_CELLS: u8 = 0x03;
55
56pub const C2S_INPUT: u8 = 0x00;
57pub const C2S_RESIZE: u8 = 0x01;
62pub const C2S_SCROLL: u8 = 0x02;
63pub const C2S_ACK: u8 = 0x03;
64pub const C2S_DISPLAY_RATE: u8 = 0x04;
65pub const C2S_CLIENT_METRICS: u8 = 0x05;
66pub const C2S_PING: u8 = 0x08;
70pub const C2S_MOUSE: u8 = 0x06;
75pub const C2S_RESTART: u8 = 0x07;
78pub const C2S_CREATE: u8 = 0x10;
79pub const C2S_FOCUS: u8 = 0x11;
80pub const C2S_CLOSE: u8 = 0x12;
81pub const C2S_SUBSCRIBE: u8 = 0x13;
82pub const C2S_UNSUBSCRIBE: u8 = 0x14;
83pub const C2S_SEARCH: u8 = 0x15;
84pub const C2S_CREATE_AT: u8 = 0x16;
85pub const C2S_CREATE_N: u8 = 0x17;
86pub const C2S_CREATE2: u8 = 0x18;
90pub const CREATE2_HAS_SRC_PTY: u8 = 1 << 0;
91pub const CREATE2_HAS_COMMAND: u8 = 1 << 1;
92pub const CREATE2_HAS_CWD: u8 = 1 << 2;
93pub const C2S_READ: u8 = 0x19;
99pub const READ_ANSI: u8 = 1 << 0;
100pub const READ_TAIL: u8 = 1 << 1;
101pub const C2S_COPY_RANGE: u8 = 0x1B;
108pub const C2S_KILL: u8 = 0x1A;
111pub const C2S_TERM_CWD: u8 = 0x1C;
113
114pub const C2S_SURFACE_INPUT: u8 = 0x20;
117pub const C2S_SURFACE_POINTER: u8 = 0x21;
121pub const C2S_SURFACE_POINTER_AXIS: u8 = 0x22;
125pub const C2S_SURFACE_RESIZE: u8 = 0x23;
128pub const C2S_SURFACE_FOCUS: u8 = 0x24;
130pub const C2S_CLIPBOARD_SET: u8 = 0x25;
133pub const C2S_SURFACE_LIST: u8 = 0x26;
135pub const C2S_SURFACE_CAPTURE: u8 = 0x27;
140pub const CAPTURE_FORMAT_PNG: u8 = 0;
141pub const CAPTURE_FORMAT_AVIF: u8 = 1;
142pub const C2S_SURFACE_SUBSCRIBE: u8 = 0x28;
173
174pub const SURFACE_BANDWIDTH_DEFAULT: u8 = 0;
177pub const SURFACE_BANDWIDTH_LOW: u8 = 1;
178pub const SURFACE_BANDWIDTH_MEDIUM: u8 = 2;
179pub const SURFACE_BANDWIDTH_HIGH: u8 = 3;
180pub const SURFACE_BANDWIDTH_ULTRA: u8 = 4;
181
182pub const SURFACE_SPEED_DEFAULT: u8 = 0;
185pub const SURFACE_SPEED_SLOW: u8 = 1;
186pub const SURFACE_SPEED_MEDIUM: u8 = 2;
187pub const SURFACE_SPEED_FAST: u8 = 3;
188pub const SURFACE_SPEED_REALTIME: u8 = 4;
189pub const C2S_SURFACE_UNSUBSCRIBE: u8 = 0x29;
191pub const C2S_SURFACE_ACK: u8 = 0x2A;
193pub const C2S_SURFACE_CLOSE: u8 = 0x2B;
196pub const C2S_CLIPBOARD_LIST: u8 = 0x2C;
199pub const C2S_CLIENT_FEATURES: u8 = 0x2D;
207pub const C2S_SURFACE_TEXT: u8 = 0x2F;
213pub const C2S_CLIPBOARD_GET: u8 = 0x2E;
217pub const C2S_QUIT: u8 = 0x0F;
220
221pub const S2C_UPDATE: u8 = 0x00;
222pub const S2C_CREATED: u8 = 0x01;
223pub const S2C_CLOSED: u8 = 0x02;
224pub const S2C_LIST: u8 = 0x03;
225pub const S2C_TITLE: u8 = 0x04;
226pub const S2C_SEARCH_RESULTS: u8 = 0x05;
227pub const S2C_CREATED_N: u8 = 0x06;
228pub const S2C_HELLO: u8 = 0x07;
229pub const S2C_EXITED: u8 = 0x08;
235pub const EXIT_STATUS_UNKNOWN: i32 = i32::MIN;
236pub const S2C_READY: u8 = 0x09;
239pub const S2C_PING: u8 = 0x0B;
243pub const S2C_QUIT: u8 = 0x0C;
246pub const S2C_USED_ROWS: u8 = 0x0D;
251pub const S2C_TERM_CWD: u8 = 0x0E;
256pub const S2C_TERM_CWD_EVENT: u8 = 0x0F;
265pub const TERM_CWD_MAX: usize = 4096;
271pub const S2C_TEXT: u8 = 0x0A;
277
278pub fn msg_s2c_used_rows(pty_id: u16, used_rows: u16) -> Vec<u8> {
279 let mut msg = Vec::with_capacity(5);
280 msg.push(S2C_USED_ROWS);
281 msg.extend_from_slice(&pty_id.to_le_bytes());
282 msg.extend_from_slice(&used_rows.to_le_bytes());
283 msg
284}
285
286pub const S2C_SURFACE_CREATED: u8 = 0x20;
290pub const S2C_SURFACE_DESTROYED: u8 = 0x21;
292pub const S2C_SURFACE_FRAME: u8 = 0x22;
297pub const S2C_SURFACE_TITLE: u8 = 0x23;
299pub const S2C_SURFACE_RESIZED: u8 = 0x24;
301pub const S2C_SURFACE_APP_ID: u8 = 0x28;
303pub const S2C_CLIPBOARD_CONTENT: u8 = 0x25;
306pub const S2C_SURFACE_LIST: u8 = 0x26;
309pub const S2C_SURFACE_CAPTURE: u8 = 0x27;
313
314pub const S2C_SURFACE_CURSOR: u8 = 0x29;
317
318pub const S2C_SURFACE_ENCODER: u8 = 0x2A;
322
323pub const S2C_CLIPBOARD_LIST: u8 = 0x2C;
326
327pub const C2S_AUDIO_SUBSCRIBE: u8 = 0x30;
334pub const C2S_AUDIO_UNSUBSCRIBE: u8 = 0x31;
336pub const S2C_AUDIO_FRAME: u8 = 0x30;
341
342pub const AUDIO_FRAME_CODEC_MASK: u8 = 0b110;
343pub const AUDIO_FRAME_CODEC_OPUS: u8 = 0 << 1;
344
345pub const S2C_FRAGMENT: u8 = 0x2B;
365pub const FRAGMENT_FLAG_LAST: u8 = 1 << 0;
366
367pub const SURFACE_FRAME_FLAG_KEYFRAME: u8 = 1 << 0;
368pub const SURFACE_FRAME_CODEC_MASK: u8 = 0b110;
369pub const SURFACE_FRAME_CODEC_H264: u8 = 0 << 1;
370pub const SURFACE_FRAME_CODEC_AV1: u8 = 1 << 1;
371pub const SURFACE_FRAME_CODEC_PNG: u8 = 2 << 1;
372
373pub const CODEC_SUPPORT_H264: u8 = 1 << 0;
376pub const CODEC_SUPPORT_AV1: u8 = 1 << 1;
377pub const CODEC_SUPPORT_H264_444: u8 = 1 << 2;
378pub const CODEC_SUPPORT_AV1_444: u8 = 1 << 3;
379
380pub const FEATURE_CREATE_NONCE: u32 = 1 << 0;
381pub const FEATURE_RESTART: u32 = 1 << 1;
382pub const FEATURE_RESIZE_BATCH: u32 = 1 << 2;
383pub const FEATURE_COPY_RANGE: u32 = 1 << 3;
384pub const FEATURE_COMPOSITOR: u32 = 1 << 4;
385pub const FEATURE_AUDIO: u32 = 1 << 5;
386
387#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
388pub enum Color {
389 #[default]
390 Default,
391 Indexed(u8),
392 Rgb(u8, u8, u8),
393}
394
395#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
396pub struct CellStyle {
397 pub fg: Color,
398 pub bg: Color,
399 pub bold: bool,
400 pub dim: bool,
401 pub italic: bool,
402 pub underline: bool,
403 pub inverse: bool,
404}
405
406#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
407pub struct Rect {
408 pub row: u16,
409 pub col: u16,
410 pub rows: u16,
411 pub cols: u16,
412}
413
414impl Rect {
415 pub const fn new(row: u16, col: u16, rows: u16, cols: u16) -> Self {
416 Self {
417 row,
418 col,
419 rows,
420 cols,
421 }
422 }
423}
424
425#[derive(Clone, Debug, Default, PartialEq, Eq)]
426pub struct FrameState {
427 rows: u16,
428 cols: u16,
429 cells: Vec<u8>,
430 cursor_row: u16,
431 cursor_col: u16,
432 mode: u16,
433 title: String,
434 overflow: BTreeMap<usize, String>,
437 line_flags: Vec<u8>,
439 scrollback_lines: u32,
441}
442
443impl FrameState {
444 pub fn new(rows: u16, cols: u16) -> Self {
445 let total = rows as usize * cols as usize;
446 Self {
447 rows,
448 cols,
449 cells: vec![0; total * CELL_SIZE],
450 cursor_row: 0,
451 cursor_col: 0,
452 mode: 0,
453 title: String::new(),
454 overflow: BTreeMap::new(),
455 line_flags: vec![0; rows as usize],
456 scrollback_lines: 0,
457 }
458 }
459
460 pub fn from_parts(
461 rows: u16,
462 cols: u16,
463 cursor_row: u16,
464 cursor_col: u16,
465 mode: u16,
466 title: impl Into<String>,
467 cells: Vec<u8>,
468 ) -> Self {
469 let mut state = Self::new(rows, cols);
470 if cells.len() == state.cells.len() {
471 state.cells = cells;
472 }
473 state.cursor_row = cursor_row;
474 state.cursor_col = cursor_col;
475 state.mode = mode;
476 state.title = title.into();
477 state
478 }
479
480 pub fn rows(&self) -> u16 {
481 self.rows
482 }
483
484 pub fn cols(&self) -> u16 {
485 self.cols
486 }
487
488 pub fn cursor_row(&self) -> u16 {
489 self.cursor_row
490 }
491
492 pub fn cursor_col(&self) -> u16 {
493 self.cursor_col
494 }
495
496 pub fn mode(&self) -> u16 {
497 self.mode
498 }
499
500 pub fn title(&self) -> &str {
501 &self.title
502 }
503
504 pub fn cells(&self) -> &[u8] {
505 &self.cells
506 }
507
508 pub fn cells_mut(&mut self) -> &mut [u8] {
509 &mut self.cells
510 }
511
512 pub fn overflow(&self) -> &BTreeMap<usize, String> {
513 &self.overflow
514 }
515
516 pub fn overflow_mut(&mut self) -> &mut BTreeMap<usize, String> {
517 &mut self.overflow
518 }
519
520 pub fn line_flags(&self) -> &[u8] {
521 &self.line_flags
522 }
523
524 pub fn line_flags_mut(&mut self) -> &mut Vec<u8> {
525 &mut self.line_flags
526 }
527
528 pub fn scrollback_lines(&self) -> u32 {
529 self.scrollback_lines
530 }
531
532 pub fn set_scrollback_lines(&mut self, lines: u32) {
533 self.scrollback_lines = lines;
534 }
535
536 pub fn is_wrapped(&self, row: u16) -> bool {
537 self.line_flags.get(row as usize).copied().unwrap_or(0) & ROW_FLAG_WRAPPED != 0
538 }
539
540 pub fn set_wrapped(&mut self, row: u16, wrapped: bool) {
541 if let Some(flags) = self.line_flags.get_mut(row as usize) {
542 if wrapped {
543 *flags |= ROW_FLAG_WRAPPED;
544 } else {
545 *flags &= !ROW_FLAG_WRAPPED;
546 }
547 }
548 }
549
550 pub fn cell_content(&self, row: u16, col: u16) -> &str {
552 if row >= self.rows || col >= self.cols {
553 return "";
554 }
555 let flat = row as usize * self.cols as usize + col as usize;
556 let idx = flat * CELL_SIZE;
557 let f1 = self.cells[idx + 1];
558 if f1 & 4 != 0 {
559 return ""; }
561 let content_len = ((f1 >> 3) & 7) as usize;
562 if content_len == CONTENT_OVERFLOW as usize {
563 if let Some(s) = self.overflow.get(&flat) {
564 return s.as_str();
565 }
566 return "";
567 }
568 if content_len == 0 {
569 return " ";
570 }
571 std::str::from_utf8(&self.cells[idx + 8..idx + 8 + content_len]).unwrap_or(" ")
572 }
573
574 pub fn resize(&mut self, rows: u16, cols: u16) {
575 if rows == self.rows && cols == self.cols {
576 return;
577 }
578 self.rows = rows;
579 self.cols = cols;
580 self.cells = vec![0; rows as usize * cols as usize * CELL_SIZE];
581 self.overflow.clear();
582 self.line_flags = vec![0; rows as usize];
583 self.cursor_row = self.cursor_row.min(rows.saturating_sub(1));
584 self.cursor_col = self.cursor_col.min(cols.saturating_sub(1));
585 }
586
587 pub fn set_cursor(&mut self, row: u16, col: u16) {
588 self.cursor_row = row.min(self.rows.saturating_sub(1));
589 self.cursor_col = col.min(self.cols.saturating_sub(1));
590 }
591
592 pub fn set_mode(&mut self, mode: u16) {
593 self.mode = mode;
594 }
595
596 pub fn set_title(&mut self, title: impl Into<String>) -> bool {
597 let title = title.into();
598 if self.title == title {
599 return false;
600 }
601 self.title = title;
602 true
603 }
604
605 pub fn clear(&mut self, style: CellStyle) {
606 for row in 0..self.rows {
607 for col in 0..self.cols {
608 self.set_blank_cell(row, col, style);
609 }
610 }
611 }
612
613 pub fn fill_rect(&mut self, rect: Rect, ch: char, style: CellStyle) {
614 let row_end = rect.row.saturating_add(rect.rows).min(self.rows);
615 let col_end = rect.col.saturating_add(rect.cols).min(self.cols);
616 for row in rect.row..row_end {
617 let mut col = rect.col;
618 while col < col_end {
619 let width = self.set_cell(row, col, ch, style);
620 if width == 0 {
621 break;
622 }
623 col = col.saturating_add(width);
624 }
625 }
626 }
627
628 pub fn write_text(&mut self, row: u16, col: u16, text: &str, style: CellStyle) -> u16 {
629 if row >= self.rows || col >= self.cols {
630 return col;
631 }
632 let mut cur_col = col;
633 for ch in text.chars() {
634 if cur_col >= self.cols {
635 break;
636 }
637 let width = self.set_cell(row, cur_col, ch, style);
638 if width == 0 {
639 continue;
640 }
641 cur_col = cur_col.saturating_add(width);
642 }
643 cur_col
644 }
645
646 pub fn write_wrapped_text(&mut self, rect: Rect, text: &str, style: CellStyle) -> usize {
647 if rect.rows == 0 || rect.cols == 0 {
648 return 0;
649 }
650 let lines = wrap_text_lines(text, rect.cols as usize);
651 let max_rows = rect.rows.min(self.rows.saturating_sub(rect.row));
652 for (idx, line) in lines.iter().take(max_rows as usize).enumerate() {
653 let row = rect.row + idx as u16;
654 self.write_text(row, rect.col, line, style);
655 }
656 lines.len()
657 }
658
659 pub fn write_scrolling_text<S: AsRef<str>>(
660 &mut self,
661 rect: Rect,
662 lines: &[S],
663 offset_from_bottom: usize,
664 style: CellStyle,
665 ) {
666 if rect.rows == 0 || rect.cols == 0 {
667 return;
668 }
669 let mut wrapped = Vec::with_capacity(lines.len());
670 for line in lines {
671 let line = line.as_ref();
672 let out = wrap_text_lines(line, rect.cols as usize);
673 if out.is_empty() {
674 wrapped.push(String::new());
675 } else {
676 wrapped.extend(out);
677 }
678 }
679 let visible = rect.rows as usize;
680 let end = wrapped.len().saturating_sub(offset_from_bottom);
681 let start = end.saturating_sub(visible);
682 for row in 0..rect.rows {
683 self.fill_rect(
684 Rect::new(rect.row + row, rect.col, 1, rect.cols),
685 ' ',
686 style,
687 );
688 }
689 for (idx, line) in wrapped[start..end].iter().enumerate() {
690 self.write_text(rect.row + idx as u16, rect.col, line, style);
691 }
692 }
693
694 pub fn get_text(&self, start_row: u16, start_col: u16, end_row: u16, end_col: u16) -> String {
695 let mut result = String::new();
696 if self.rows == 0 || self.cols == 0 {
697 return result;
698 }
699 for row in start_row..=end_row.min(self.rows.saturating_sub(1)) {
700 let c0 = if row == start_row { start_col } else { 0 };
701 let c1 = if row == end_row {
702 end_col
703 } else {
704 self.cols - 1
705 };
706 let mut line = String::new();
707 let mut col = c0;
708 while col <= c1.min(self.cols - 1) {
709 line.push_str(self.cell_content(row, col));
710 col += 1;
711 }
712 let wrapped = self.is_wrapped(row);
713 if wrapped {
715 result.push_str(&line);
716 } else {
717 result.push_str(line.trim_end());
718 }
719 if row < end_row.min(self.rows.saturating_sub(1)) && !wrapped {
720 result.push('\n');
721 }
722 }
723 result
724 }
725
726 pub fn get_all_text(&self) -> String {
727 if self.rows == 0 || self.cols == 0 {
728 return String::new();
729 }
730 self.get_text(0, 0, self.rows - 1, self.cols - 1)
731 }
732
733 fn cell_style(&self, row: u16, col: u16) -> CellStyle {
734 if row >= self.rows || col >= self.cols {
735 return CellStyle::default();
736 }
737 let idx = self.cell_offset(row, col);
738 let f0 = self.cells[idx];
739 let f1 = self.cells[idx + 1];
740 let fg_type = f0 & 3;
741 let bg_type = (f0 >> 2) & 3;
742 let fg = match fg_type {
743 1 => Color::Indexed(self.cells[idx + 2]),
744 2 => Color::Rgb(
745 self.cells[idx + 2],
746 self.cells[idx + 3],
747 self.cells[idx + 4],
748 ),
749 _ => Color::Default,
750 };
751 let bg = match bg_type {
752 1 => Color::Indexed(self.cells[idx + 5]),
753 2 => Color::Rgb(
754 self.cells[idx + 5],
755 self.cells[idx + 6],
756 self.cells[idx + 7],
757 ),
758 _ => Color::Default,
759 };
760 CellStyle {
761 fg,
762 bg,
763 bold: (f0 >> 4) & 1 != 0,
764 dim: (f0 >> 5) & 1 != 0,
765 italic: (f0 >> 6) & 1 != 0,
766 underline: (f0 >> 7) & 1 != 0,
767 inverse: f1 & 1 != 0,
768 }
769 }
770
771 pub fn get_ansi_text(&self) -> String {
772 if self.rows == 0 || self.cols == 0 {
773 return String::new();
774 }
775 let mut result = String::new();
776 let mut cur_style = CellStyle::default();
777 for row in 0..self.rows {
778 let mut line = String::new();
779 let mut col = 0u16;
780 while col < self.cols {
781 let style = self.cell_style(row, col);
782 if style != cur_style {
783 push_sgr(&mut line, &style);
784 cur_style = style;
785 }
786 line.push_str(self.cell_content(row, col));
787 col += 1;
788 }
789 let trimmed = line.trim_end();
790 result.push_str(trimmed);
791 if cur_style != CellStyle::default() {
792 result.push_str("\x1b[0m");
793 cur_style = CellStyle::default();
794 }
795 if row < self.rows - 1 {
796 result.push('\n');
797 }
798 }
799 result
800 }
801
802 pub fn get_cell(&self, row: u16, col: u16) -> Vec<u8> {
803 if row >= self.rows || col >= self.cols {
804 return Vec::new();
805 }
806 let idx = self.cell_offset(row, col);
807 self.cells[idx..idx + CELL_SIZE].to_vec()
808 }
809
810 fn cell_offset(&self, row: u16, col: u16) -> usize {
811 (row as usize * self.cols as usize + col as usize) * CELL_SIZE
812 }
813
814 fn set_cell(&mut self, row: u16, col: u16, ch: char, style: CellStyle) -> u16 {
815 if row >= self.rows || col >= self.cols {
816 return 0;
817 }
818 let raw_width = UnicodeWidthChar::width(ch).unwrap_or(0);
819 if raw_width == 0 {
820 return 0;
821 }
822 let width = if raw_width > 1 && col + 1 < self.cols {
823 2
824 } else {
825 1
826 };
827 let idx = self.cell_offset(row, col);
828 encode_cell(
829 &mut self.cells[idx..idx + CELL_SIZE],
830 Some(ch),
831 style,
832 width == 2,
833 false,
834 );
835 if width == 2 {
836 let cont_idx = self.cell_offset(row, col + 1);
837 encode_cell(
838 &mut self.cells[cont_idx..cont_idx + CELL_SIZE],
839 None,
840 style,
841 false,
842 true,
843 );
844 }
845 width
846 }
847
848 fn set_blank_cell(&mut self, row: u16, col: u16, style: CellStyle) {
849 if row >= self.rows || col >= self.cols {
850 return;
851 }
852 let idx = self.cell_offset(row, col);
853 encode_cell(
854 &mut self.cells[idx..idx + CELL_SIZE],
855 None,
856 style,
857 false,
858 false,
859 );
860 }
861}
862
863#[derive(Clone, Debug)]
864pub struct TerminalState {
865 frame: FrameState,
866}
867
868impl TerminalState {
869 pub fn new(rows: u16, cols: u16) -> Self {
870 let frame = FrameState::new(rows, cols);
871 Self { frame }
872 }
873
874 pub fn frame(&self) -> &FrameState {
875 &self.frame
876 }
877
878 pub fn frame_mut(&mut self) -> &mut FrameState {
879 &mut self.frame
880 }
881
882 pub fn title(&self) -> &str {
883 self.frame.title()
884 }
885
886 pub fn rows(&self) -> u16 {
887 self.frame.rows()
888 }
889
890 pub fn cols(&self) -> u16 {
891 self.frame.cols()
892 }
893
894 pub fn is_wrapped(&self, row: u16) -> bool {
895 self.frame.is_wrapped(row)
896 }
897
898 pub fn cursor_row(&self) -> u16 {
899 self.frame.cursor_row()
900 }
901
902 pub fn cursor_col(&self) -> u16 {
903 self.frame.cursor_col()
904 }
905
906 pub fn mode(&self) -> u16 {
907 self.frame.mode()
908 }
909
910 pub fn cells(&self) -> &[u8] {
911 self.frame.cells()
912 }
913
914 pub fn set_title(&mut self, title: &str) -> bool {
915 self.frame.set_title(title.to_owned())
916 }
917
918 pub fn get_text(&self, start_row: u16, start_col: u16, end_row: u16, end_col: u16) -> String {
919 self.frame.get_text(start_row, start_col, end_row, end_col)
920 }
921
922 pub fn get_all_text(&self) -> String {
923 self.frame.get_all_text()
924 }
925
926 pub fn get_ansi_text(&self) -> String {
927 self.frame.get_ansi_text()
928 }
929
930 pub fn get_cell(&self, row: u16, col: u16) -> Vec<u8> {
931 self.frame.get_cell(row, col)
932 }
933
934 const MAX_DECOMPRESSED_SIZE: usize = 50 * 1024 * 1024;
937
938 fn safe_decompress(data: &[u8]) -> Result<Vec<u8>, ()> {
941 if data.len() < 4 {
942 return Err(());
943 }
944 let claimed = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
945 if claimed > Self::MAX_DECOMPRESSED_SIZE {
946 return Err(());
947 }
948 decompress_size_prepended(data).map_err(|_| ())
949 }
950
951 pub fn feed_compressed(&mut self, data: &[u8]) -> bool {
952 let payload = match Self::safe_decompress(data) {
953 Ok(d) => d,
954 Err(_) => return false,
955 };
956 self.apply_payload(&payload)
957 }
958
959 pub fn feed_compressed_batch(&mut self, batch: &[u8]) -> bool {
960 let mut changed = false;
961 let mut off = 0usize;
962 while off + 4 <= batch.len() {
963 let len =
964 u32::from_le_bytes([batch[off], batch[off + 1], batch[off + 2], batch[off + 3]])
965 as usize;
966 off += 4;
967 if len == 0 {
968 break;
969 }
970 if off + len > batch.len() {
971 break;
972 }
973 if let Ok(payload) = Self::safe_decompress(&batch[off..off + len]) {
974 changed |= self.apply_payload(&payload);
975 }
976 off += len;
977 }
978 changed
979 }
980
981 const MAX_CELL_COUNT: usize = 500_000;
986
987 fn apply_payload(&mut self, payload: &[u8]) -> bool {
988 if payload.len() < 12 {
989 return false;
990 }
991
992 let new_rows = u16::from_le_bytes([payload[0], payload[1]]);
993 let new_cols = u16::from_le_bytes([payload[2], payload[3]]);
994
995 if (new_rows as usize) * (new_cols as usize) > Self::MAX_CELL_COUNT {
997 return false;
998 }
999 let new_cursor_row = u16::from_le_bytes([payload[4], payload[5]]);
1000 let new_cursor_col = u16::from_le_bytes([payload[6], payload[7]]);
1001 let new_mode = u16::from_le_bytes([payload[8], payload[9]]);
1002 let title_field = u16::from_le_bytes([payload[10], payload[11]]);
1003 let title_present = title_field & TITLE_PRESENT != 0;
1004 let ops_present = title_field & OPS_PRESENT != 0;
1005 let strings_present = title_field & STRINGS_PRESENT != 0;
1006 let line_flags_present = title_field & LINE_FLAGS_PRESENT != 0;
1007 let title_len = (title_field & TITLE_LEN_MASK) as usize;
1008
1009 let title_start = 12usize;
1010 let title_end = title_start.saturating_add(title_len);
1011 if payload.len() < title_end {
1012 return false;
1013 }
1014 let title_changed = if title_present {
1015 let title = String::from_utf8_lossy(&payload[title_start..title_end]).into_owned();
1016 self.frame.set_title(title)
1017 } else {
1018 false
1019 };
1020
1021 let resized = new_rows != self.frame.rows || new_cols != self.frame.cols;
1022 if resized {
1023 self.frame.resize(new_rows, new_cols);
1024 }
1025
1026 let old_cursor_row = self.frame.cursor_row;
1027 let old_cursor_col = self.frame.cursor_col;
1028 let old_mode = self.frame.mode;
1029
1030 let (content_changed, ops_end) = if ops_present {
1031 let ops_start = title_end;
1032 if payload.len() < ops_start + 2 {
1033 return false;
1034 }
1035 let (changed, consumed) = self
1036 .apply_ops_payload(&payload[ops_start..])
1037 .unwrap_or((false, 0));
1038 (changed, ops_start + consumed)
1039 } else {
1040 let (changed, consumed) = self
1041 .apply_legacy_patch_payload(&payload[title_end..])
1042 .unwrap_or((false, 0));
1043 (changed, title_end + consumed)
1044 };
1045
1046 let mut after_strings = ops_end;
1047 if strings_present {
1048 after_strings = self.apply_overflow_strings(&payload[ops_end..]);
1049 after_strings += ops_end;
1050 }
1051
1052 let (line_flags_changed, after_line_flags) = if line_flags_present {
1053 let lf_start = after_strings;
1054 let lf_end = lf_start + new_rows as usize;
1055 if payload.len() >= lf_end {
1056 let new_flags = &payload[lf_start..lf_end];
1057 let changed = self.frame.line_flags != new_flags;
1058 self.frame.line_flags.clear();
1059 self.frame.line_flags.extend_from_slice(new_flags);
1060 (changed, lf_end)
1061 } else {
1062 (false, after_strings)
1063 }
1064 } else {
1065 (false, after_strings)
1066 };
1067
1068 if payload.len() >= after_line_flags + 4 {
1070 self.frame.scrollback_lines = u32::from_le_bytes([
1071 payload[after_line_flags],
1072 payload[after_line_flags + 1],
1073 payload[after_line_flags + 2],
1074 payload[after_line_flags + 3],
1075 ]);
1076 }
1077
1078 self.frame.cursor_row = new_cursor_row.min(self.frame.rows.saturating_sub(1));
1079 self.frame.cursor_col = new_cursor_col.min(self.frame.cols.saturating_sub(1));
1080 self.frame.mode = new_mode;
1081 resized
1082 || title_changed
1083 || content_changed
1084 || line_flags_changed
1085 || new_cursor_row != old_cursor_row
1086 || new_cursor_col != old_cursor_col
1087 || new_mode != old_mode
1088 }
1089
1090 fn apply_legacy_patch_payload(&mut self, payload: &[u8]) -> Option<(bool, usize)> {
1091 let total_cells = self.frame.rows as usize * self.frame.cols as usize;
1092 let bitmask_len = total_cells.div_ceil(8);
1093 if payload.len() < bitmask_len {
1094 return None;
1095 }
1096 let bitmask = &payload[..bitmask_len];
1097 let dirty_count = (0..total_cells)
1098 .filter(|&i| bitmask[i / 8] & (1 << (i % 8)) != 0)
1099 .count();
1100 let data = &payload[bitmask_len..];
1101 if data.len() < dirty_count * CELL_SIZE {
1102 return None;
1103 }
1104 self.apply_patch_cells(bitmask, &data[..dirty_count * CELL_SIZE], dirty_count);
1105 Some((dirty_count > 0, bitmask_len + dirty_count * CELL_SIZE))
1106 }
1107
1108 fn apply_ops_payload(&mut self, payload: &[u8]) -> Option<(bool, usize)> {
1109 if payload.len() < 2 {
1110 return None;
1111 }
1112 let op_count = u16::from_le_bytes([payload[0], payload[1]]) as usize;
1113 let total_cells = self.frame.rows as usize * self.frame.cols as usize;
1114 let bitmask_len = total_cells.div_ceil(8);
1115 let mut off = 2usize;
1116 let mut changed = false;
1117
1118 for _ in 0..op_count {
1119 if off >= payload.len() {
1120 return None;
1121 }
1122 let op = payload[off];
1123 off += 1;
1124 match op {
1125 OP_COPY_RECT => {
1126 if payload.len() < off + 12 {
1127 return None;
1128 }
1129 let src_row = u16::from_le_bytes([payload[off], payload[off + 1]]);
1130 let src_col = u16::from_le_bytes([payload[off + 2], payload[off + 3]]);
1131 let dst_row = u16::from_le_bytes([payload[off + 4], payload[off + 5]]);
1132 let dst_col = u16::from_le_bytes([payload[off + 6], payload[off + 7]]);
1133 let rows = u16::from_le_bytes([payload[off + 8], payload[off + 9]]);
1134 let cols = u16::from_le_bytes([payload[off + 10], payload[off + 11]]);
1135 off += 12;
1136 changed |= self.apply_copy_rect(src_row, src_col, dst_row, dst_col, rows, cols);
1137 }
1138 OP_FILL_RECT => {
1139 if payload.len() < off + 8 + CELL_SIZE {
1140 return None;
1141 }
1142 let row = u16::from_le_bytes([payload[off], payload[off + 1]]);
1143 let col = u16::from_le_bytes([payload[off + 2], payload[off + 3]]);
1144 let rows = u16::from_le_bytes([payload[off + 4], payload[off + 5]]);
1145 let cols = u16::from_le_bytes([payload[off + 6], payload[off + 7]]);
1146 off += 8;
1147 let mut cell = [0u8; CELL_SIZE];
1148 cell.copy_from_slice(&payload[off..off + CELL_SIZE]);
1149 off += CELL_SIZE;
1150 changed |= self.apply_fill_rect(row, col, rows, cols, &cell);
1151 }
1152 OP_PATCH_CELLS => {
1153 if payload.len() < off + bitmask_len {
1154 return None;
1155 }
1156 let bitmask = &payload[off..off + bitmask_len];
1157 off += bitmask_len;
1158 let dirty_count = (0..total_cells)
1159 .filter(|&i| bitmask[i / 8] & (1 << (i % 8)) != 0)
1160 .count();
1161 if payload.len() < off + dirty_count * CELL_SIZE {
1162 return None;
1163 }
1164 self.apply_patch_cells(
1165 bitmask,
1166 &payload[off..off + dirty_count * CELL_SIZE],
1167 dirty_count,
1168 );
1169 off += dirty_count * CELL_SIZE;
1170 changed |= dirty_count > 0;
1171 }
1172 _ => return None,
1173 }
1174 }
1175
1176 Some((changed, off))
1177 }
1178
1179 fn apply_patch_cells(&mut self, bitmask: &[u8], data: &[u8], dirty_count: usize) {
1180 let total_cells = self.frame.rows as usize * self.frame.cols as usize;
1181 let mut dirty_idx = 0usize;
1182 for i in 0..total_cells {
1183 if bitmask[i / 8] & (1 << (i % 8)) == 0 {
1184 continue;
1185 }
1186 let cell_idx = i * CELL_SIZE;
1187 for byte_pos in 0..CELL_SIZE {
1188 self.frame.cells[cell_idx + byte_pos] = data[byte_pos * dirty_count + dirty_idx];
1189 }
1190 let new_content_len = (self.frame.cells[cell_idx + 1] >> 3) & 7;
1193 if new_content_len != CONTENT_OVERFLOW {
1194 self.frame.overflow.remove(&i);
1195 }
1196 dirty_idx += 1;
1197 }
1198 }
1199
1200 fn apply_copy_rect(
1201 &mut self,
1202 src_row: u16,
1203 src_col: u16,
1204 dst_row: u16,
1205 dst_col: u16,
1206 rows: u16,
1207 cols: u16,
1208 ) -> bool {
1209 let rows = rows
1210 .min(self.frame.rows.saturating_sub(src_row))
1211 .min(self.frame.rows.saturating_sub(dst_row));
1212 let cols = cols
1213 .min(self.frame.cols.saturating_sub(src_col))
1214 .min(self.frame.cols.saturating_sub(dst_col));
1215 if rows == 0 || cols == 0 {
1216 return false;
1217 }
1218
1219 let frame_cols = self.frame.cols as usize;
1220
1221 let mut overflow_temp: Vec<(usize, String)> = Vec::new();
1223 for r in 0..rows as usize {
1224 for c in 0..cols as usize {
1225 let src_flat = (src_row as usize + r) * frame_cols + src_col as usize + c;
1226 if let Some(s) = self.frame.overflow.get(&src_flat) {
1227 let dst_flat = (dst_row as usize + r) * frame_cols + dst_col as usize + c;
1228 overflow_temp.push((dst_flat, s.clone()));
1229 }
1230 }
1231 }
1232
1233 let mut temp = vec![0u8; rows as usize * cols as usize * CELL_SIZE];
1234 for r in 0..rows as usize {
1235 let src_off = self.frame.cell_offset(src_row + r as u16, src_col);
1236 let src_end = src_off + cols as usize * CELL_SIZE;
1237 let dst_off = r * cols as usize * CELL_SIZE;
1238 temp[dst_off..dst_off + cols as usize * CELL_SIZE]
1239 .copy_from_slice(&self.frame.cells[src_off..src_end]);
1240 }
1241 for r in 0..rows as usize {
1242 let dst_off = self.frame.cell_offset(dst_row + r as u16, dst_col);
1243 let dst_end = dst_off + cols as usize * CELL_SIZE;
1244 let src_off = r * cols as usize * CELL_SIZE;
1245 self.frame.cells[dst_off..dst_end]
1246 .copy_from_slice(&temp[src_off..src_off + cols as usize * CELL_SIZE]);
1247 }
1248
1249 for r in 0..rows as usize {
1250 for c in 0..cols as usize {
1251 let dst_flat = (dst_row as usize + r) * frame_cols + dst_col as usize + c;
1252 self.frame.overflow.remove(&dst_flat);
1253 }
1254 }
1255 for (idx, s) in overflow_temp {
1256 self.frame.overflow.insert(idx, s);
1257 }
1258
1259 true
1260 }
1261
1262 fn apply_fill_rect(
1263 &mut self,
1264 row: u16,
1265 col: u16,
1266 rows: u16,
1267 cols: u16,
1268 cell: &[u8; CELL_SIZE],
1269 ) -> bool {
1270 let row_end = row.saturating_add(rows).min(self.frame.rows);
1271 let col_end = col.saturating_add(cols).min(self.frame.cols);
1272 let frame_cols = self.frame.cols as usize;
1274 for r in row..row_end {
1275 for c in col..col_end {
1276 self.frame
1277 .overflow
1278 .remove(&(r as usize * frame_cols + c as usize));
1279 }
1280 }
1281 if row >= row_end || col >= col_end {
1282 return false;
1283 }
1284 for r in row..row_end {
1285 for c in col..col_end {
1286 let off = self.frame.cell_offset(r, c);
1287 self.frame.cells[off..off + CELL_SIZE].copy_from_slice(cell);
1288 }
1289 }
1290 true
1291 }
1292
1293 fn apply_overflow_strings(&mut self, data: &[u8]) -> usize {
1294 if data.len() < 2 {
1295 return 0;
1296 }
1297 let count = u16::from_le_bytes([data[0], data[1]]) as usize;
1298 let mut off = 2usize;
1299 for _ in 0..count {
1300 if off + 6 > data.len() {
1301 break;
1302 }
1303 let cell_idx =
1304 u32::from_le_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]])
1305 as usize;
1306 let len = u16::from_le_bytes([data[off + 4], data[off + 5]]) as usize;
1307 off += 6;
1308 if off + len > data.len() {
1309 break;
1310 }
1311 if let Ok(s) = std::str::from_utf8(&data[off..off + len]) {
1312 let max_idx = self.frame.rows as usize * self.frame.cols as usize;
1315 if cell_idx < max_idx {
1316 self.frame.overflow.insert(cell_idx, s.to_owned());
1317 }
1318 }
1319 off += len;
1320 }
1321 off
1322 }
1323}
1324
1325#[derive(Clone, Debug)]
1326pub enum Node {
1327 Fill {
1328 rect: Rect,
1329 ch: char,
1330 style: CellStyle,
1331 },
1332 Text {
1333 row: u16,
1334 col: u16,
1335 text: String,
1336 style: CellStyle,
1337 },
1338 WrappedText {
1339 rect: Rect,
1340 text: String,
1341 style: CellStyle,
1342 },
1343 ScrollingText {
1344 rect: Rect,
1345 lines: Vec<String>,
1346 offset_from_bottom: usize,
1347 style: CellStyle,
1348 },
1349}
1350
1351#[derive(Clone, Debug, Default)]
1352pub struct Dom {
1353 background: CellStyle,
1354 title: Option<String>,
1355 nodes: Vec<Node>,
1356}
1357
1358impl Dom {
1359 pub fn new() -> Self {
1360 Self::default()
1361 }
1362
1363 pub fn clear(&mut self) {
1364 self.title = None;
1365 self.nodes.clear();
1366 }
1367
1368 pub fn set_background(&mut self, style: CellStyle) {
1369 self.background = style;
1370 }
1371
1372 pub fn set_title(&mut self, title: impl Into<String>) {
1373 self.title = Some(title.into());
1374 }
1375
1376 pub fn fill(&mut self, rect: Rect, ch: char, style: CellStyle) {
1377 self.nodes.push(Node::Fill { rect, ch, style });
1378 }
1379
1380 pub fn text(&mut self, row: u16, col: u16, text: impl Into<String>, style: CellStyle) {
1381 self.nodes.push(Node::Text {
1382 row,
1383 col,
1384 text: text.into(),
1385 style,
1386 });
1387 }
1388
1389 pub fn wrapped_text(&mut self, rect: Rect, text: impl Into<String>, style: CellStyle) {
1390 self.nodes.push(Node::WrappedText {
1391 rect,
1392 text: text.into(),
1393 style,
1394 });
1395 }
1396
1397 pub fn scrolling_text<S, I>(
1398 &mut self,
1399 rect: Rect,
1400 lines: I,
1401 offset_from_bottom: usize,
1402 style: CellStyle,
1403 ) where
1404 S: Into<String>,
1405 I: IntoIterator<Item = S>,
1406 {
1407 self.nodes.push(Node::ScrollingText {
1408 rect,
1409 lines: lines.into_iter().map(Into::into).collect(),
1410 offset_from_bottom,
1411 style,
1412 });
1413 }
1414
1415 pub fn render_to(&self, frame: &mut FrameState) {
1416 frame.clear(self.background);
1417 frame.set_title(self.title.clone().unwrap_or_default());
1418 for node in &self.nodes {
1419 match node {
1420 Node::Fill { rect, ch, style } => frame.fill_rect(*rect, *ch, *style),
1421 Node::Text {
1422 row,
1423 col,
1424 text,
1425 style,
1426 } => {
1427 frame.write_text(*row, *col, text, *style);
1428 }
1429 Node::WrappedText { rect, text, style } => {
1430 frame.write_wrapped_text(*rect, text, *style);
1431 }
1432 Node::ScrollingText {
1433 rect,
1434 lines,
1435 offset_from_bottom,
1436 style,
1437 } => {
1438 frame.write_scrolling_text(*rect, lines, *offset_from_bottom, *style);
1439 }
1440 }
1441 }
1442 }
1443}
1444
1445#[derive(Clone, Debug)]
1446pub struct CallbackRenderer {
1447 dom: Dom,
1448 frame: FrameState,
1449}
1450
1451impl CallbackRenderer {
1452 pub fn new(rows: u16, cols: u16) -> Self {
1453 Self {
1454 dom: Dom::new(),
1455 frame: FrameState::new(rows, cols),
1456 }
1457 }
1458
1459 pub fn resize(&mut self, rows: u16, cols: u16) {
1460 self.frame.resize(rows, cols);
1461 }
1462
1463 pub fn frame(&self) -> &FrameState {
1464 &self.frame
1465 }
1466
1467 pub fn render<F>(&mut self, render: F) -> &FrameState
1468 where
1469 F: FnOnce(&mut Dom),
1470 {
1471 self.dom.clear();
1472 render(&mut self.dom);
1473 self.dom.render_to(&mut self.frame);
1474 &self.frame
1475 }
1476}
1477
1478pub enum ServerMsg<'a> {
1479 Hello {
1480 version: u16,
1481 features: u32,
1482 boot_generation: Option<u64>,
1483 server_version: Option<&'a str>,
1486 },
1487 Update {
1488 pty_id: u16,
1489 payload: &'a [u8],
1490 },
1491 Created {
1492 pty_id: u16,
1493 tag: &'a str,
1494 },
1495 CreatedN {
1496 nonce: u16,
1497 pty_id: u16,
1498 tag: &'a str,
1499 },
1500 Closed {
1501 pty_id: u16,
1502 },
1503 Exited {
1504 pty_id: u16,
1505 exit_status: i32,
1506 },
1507 List {
1508 entries: Vec<PtyListEntry<'a>>,
1509 },
1510 Title {
1511 pty_id: u16,
1512 title: &'a [u8],
1513 },
1514 SearchResults {
1515 request_id: u16,
1516 results: Vec<SearchResultEntry<'a>>,
1517 },
1518 Ready,
1519 Text {
1520 nonce: u16,
1521 pty_id: u16,
1522 total_lines: u32,
1523 offset: u32,
1524 text: &'a str,
1525 },
1526 SurfaceCreated {
1527 surface_id: u16,
1528 parent_id: u16,
1529 width: u16,
1530 height: u16,
1531 title: &'a str,
1532 app_id: &'a str,
1533 },
1534 SurfaceDestroyed {
1535 surface_id: u16,
1536 },
1537 SurfaceFrame {
1538 surface_id: u16,
1539 timestamp: u32,
1540 flags: u8,
1541 width: u16,
1542 height: u16,
1543 data: &'a [u8],
1544 },
1545 SurfaceTitle {
1546 surface_id: u16,
1547 title: &'a str,
1548 },
1549 SurfaceAppId {
1550 surface_id: u16,
1551 app_id: &'a str,
1552 },
1553 SurfaceResized {
1554 surface_id: u16,
1555 width: u16,
1556 height: u16,
1557 },
1558 ClipboardContent {
1559 mime_type: &'a str,
1560 data: &'a [u8],
1561 },
1562 SurfaceList {
1563 entries: Vec<SurfaceListEntry>,
1564 },
1565 SurfaceCapture {
1566 surface_id: u16,
1567 width: u32,
1568 height: u32,
1569 image_data: &'a [u8],
1570 },
1571 ClipboardList {
1572 mime_types: Vec<String>,
1573 },
1574 Quit,
1575}
1576
1577#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1578pub struct PtyListEntry<'a> {
1579 pub pty_id: u16,
1580 pub tag: &'a str,
1581 pub command: &'a str,
1582}
1583
1584#[derive(Clone, Debug, PartialEq, Eq)]
1585pub struct SurfaceListEntry {
1586 pub surface_id: u16,
1587 pub parent_id: u16,
1588 pub width: u16,
1589 pub height: u16,
1590 pub title: String,
1591 pub app_id: String,
1592}
1593
1594#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1595pub struct SearchResultEntry<'a> {
1596 pub pty_id: u16,
1597 pub score: u32,
1598 pub primary_source: u8,
1599 pub matched_sources: u8,
1600 pub scroll_offset: Option<u32>,
1601 pub context: &'a [u8],
1602}
1603
1604pub fn parse_server_msg(data: &[u8]) -> Option<ServerMsg<'_>> {
1605 if data.is_empty() {
1606 return None;
1607 }
1608 match data[0] {
1609 S2C_HELLO => {
1610 if data.len() < 7 {
1611 return None;
1612 }
1613 let version = u16::from_le_bytes([data[1], data[2]]);
1614 let features = u32::from_le_bytes([data[3], data[4], data[5], data[6]]);
1615 let boot_generation = (data.len() >= 15)
1618 .then(|| u64::from_le_bytes(data[7..15].try_into().expect("checked HELLO length")));
1619 let server_version = (data.len() >= 17)
1622 .then(|| {
1623 let len = u16::from_le_bytes([data[15], data[16]]) as usize;
1624 data.get(17..17 + len)
1625 .and_then(|v| std::str::from_utf8(v).ok())
1626 .filter(|v| !v.is_empty())
1627 })
1628 .flatten();
1629 Some(ServerMsg::Hello {
1630 version,
1631 features,
1632 boot_generation,
1633 server_version,
1634 })
1635 }
1636 S2C_UPDATE => {
1637 if data.len() < 3 {
1638 return None;
1639 }
1640 Some(ServerMsg::Update {
1641 pty_id: u16::from_le_bytes([data[1], data[2]]),
1642 payload: &data[3..],
1643 })
1644 }
1645 S2C_CREATED => {
1646 if data.len() < 3 {
1647 return None;
1648 }
1649 let tag = std::str::from_utf8(data.get(3..).unwrap_or_default()).unwrap_or_default();
1650 Some(ServerMsg::Created {
1651 pty_id: u16::from_le_bytes([data[1], data[2]]),
1652 tag,
1653 })
1654 }
1655 S2C_CREATED_N => {
1656 if data.len() < 5 {
1657 return None;
1658 }
1659 let nonce = u16::from_le_bytes([data[1], data[2]]);
1660 let pty_id = u16::from_le_bytes([data[3], data[4]]);
1661 let tag = std::str::from_utf8(data.get(5..).unwrap_or_default()).unwrap_or_default();
1662 Some(ServerMsg::CreatedN { nonce, pty_id, tag })
1663 }
1664 S2C_CLOSED => {
1665 if data.len() < 3 {
1666 return None;
1667 }
1668 Some(ServerMsg::Closed {
1669 pty_id: u16::from_le_bytes([data[1], data[2]]),
1670 })
1671 }
1672 S2C_EXITED => {
1673 if data.len() < 7 {
1674 return None;
1675 }
1676 Some(ServerMsg::Exited {
1677 pty_id: u16::from_le_bytes([data[1], data[2]]),
1678 exit_status: i32::from_le_bytes([data[3], data[4], data[5], data[6]]),
1679 })
1680 }
1681 S2C_LIST => {
1682 if data.len() < 3 {
1683 return None;
1684 }
1685 let count = u16::from_le_bytes([data[1], data[2]]) as usize;
1686 let mut entries = Vec::with_capacity(count);
1687 let mut offset = 3;
1688 for _ in 0..count {
1689 if offset + 4 > data.len() {
1690 break;
1691 }
1692 let pty_id = u16::from_le_bytes([data[offset], data[offset + 1]]);
1693 let tag_len = u16::from_le_bytes([data[offset + 2], data[offset + 3]]) as usize;
1694 offset += 4;
1695 if offset + tag_len > data.len() {
1696 break;
1697 }
1698 let tag = std::str::from_utf8(&data[offset..offset + tag_len]).unwrap_or_default();
1699 offset += tag_len;
1700 let command = if offset + 2 <= data.len() {
1701 let cmd_len = u16::from_le_bytes([data[offset], data[offset + 1]]) as usize;
1702 offset += 2;
1703 if offset + cmd_len <= data.len() {
1704 let cmd = std::str::from_utf8(&data[offset..offset + cmd_len])
1705 .unwrap_or_default();
1706 offset += cmd_len;
1707 cmd
1708 } else {
1709 offset = data.len();
1712 ""
1713 }
1714 } else {
1715 ""
1716 };
1717 entries.push(PtyListEntry {
1718 pty_id,
1719 tag,
1720 command,
1721 });
1722 }
1723 Some(ServerMsg::List { entries })
1724 }
1725 S2C_TITLE => {
1726 if data.len() < 3 {
1727 return None;
1728 }
1729 Some(ServerMsg::Title {
1730 pty_id: u16::from_le_bytes([data[1], data[2]]),
1731 title: &data[3..],
1732 })
1733 }
1734 S2C_SEARCH_RESULTS => {
1735 if data.len() < 5 {
1736 return None;
1737 }
1738 let request_id = u16::from_le_bytes([data[1], data[2]]);
1739 let count = u16::from_le_bytes([data[3], data[4]]) as usize;
1740 let mut results = Vec::with_capacity(count);
1741 let mut offset = 5usize;
1742 for _ in 0..count {
1743 if offset + 14 > data.len() {
1744 return None;
1745 }
1746 let pty_id = u16::from_le_bytes([data[offset], data[offset + 1]]);
1747 let score = u32::from_le_bytes([
1748 data[offset + 2],
1749 data[offset + 3],
1750 data[offset + 4],
1751 data[offset + 5],
1752 ]);
1753 let primary_source = data[offset + 6];
1754 let matched_sources = data[offset + 7];
1755 let scroll_offset = u32::from_le_bytes([
1756 data[offset + 8],
1757 data[offset + 9],
1758 data[offset + 10],
1759 data[offset + 11],
1760 ]);
1761 let context_len =
1762 u16::from_le_bytes([data[offset + 12], data[offset + 13]]) as usize;
1763 offset += 14;
1764 if offset + context_len > data.len() {
1765 return None;
1766 }
1767 results.push(SearchResultEntry {
1768 pty_id,
1769 score,
1770 primary_source,
1771 matched_sources,
1772 scroll_offset: if scroll_offset == u32::MAX {
1773 None
1774 } else {
1775 Some(scroll_offset)
1776 },
1777 context: &data[offset..offset + context_len],
1778 });
1779 offset += context_len;
1780 }
1781 Some(ServerMsg::SearchResults {
1782 request_id,
1783 results,
1784 })
1785 }
1786 S2C_READY => Some(ServerMsg::Ready),
1787 S2C_TEXT => {
1788 if data.len() < 13 {
1789 return None;
1790 }
1791 let nonce = u16::from_le_bytes([data[1], data[2]]);
1792 let pty_id = u16::from_le_bytes([data[3], data[4]]);
1793 let total_lines = u32::from_le_bytes([data[5], data[6], data[7], data[8]]);
1794 let offset = u32::from_le_bytes([data[9], data[10], data[11], data[12]]);
1795 let text = std::str::from_utf8(data.get(13..).unwrap_or_default()).unwrap_or_default();
1796 Some(ServerMsg::Text {
1797 nonce,
1798 pty_id,
1799 total_lines,
1800 offset,
1801 text,
1802 })
1803 }
1804 S2C_SURFACE_CREATED => {
1805 if data.len() < 13 {
1806 return None;
1807 }
1808 let surface_id = u16::from_le_bytes([data[1], data[2]]);
1809 let parent_id = u16::from_le_bytes([data[3], data[4]]);
1810 let width = u16::from_le_bytes([data[5], data[6]]);
1811 let height = u16::from_le_bytes([data[7], data[8]]);
1812 let title_len = u16::from_le_bytes([data[9], data[10]]) as usize;
1813 let mut off = 11;
1814 if off + title_len + 2 > data.len() {
1815 return None;
1816 }
1817 let title = std::str::from_utf8(&data[off..off + title_len]).unwrap_or_default();
1818 off += title_len;
1819 let app_id_len = u16::from_le_bytes([data[off], data[off + 1]]) as usize;
1820 off += 2;
1821 if off + app_id_len > data.len() {
1822 return None;
1823 }
1824 let app_id = std::str::from_utf8(&data[off..off + app_id_len]).unwrap_or_default();
1825 Some(ServerMsg::SurfaceCreated {
1826 surface_id,
1827 parent_id,
1828 width,
1829 height,
1830 title,
1831 app_id,
1832 })
1833 }
1834 S2C_SURFACE_DESTROYED => {
1835 if data.len() < 3 {
1836 return None;
1837 }
1838 Some(ServerMsg::SurfaceDestroyed {
1839 surface_id: u16::from_le_bytes([data[1], data[2]]),
1840 })
1841 }
1842 S2C_SURFACE_FRAME => {
1843 if data.len() < 12 {
1844 return None;
1845 }
1846 Some(ServerMsg::SurfaceFrame {
1847 surface_id: u16::from_le_bytes([data[1], data[2]]),
1848 timestamp: u32::from_le_bytes([data[3], data[4], data[5], data[6]]),
1849 flags: data[7],
1850 width: u16::from_le_bytes([data[8], data[9]]),
1851 height: u16::from_le_bytes([data[10], data[11]]),
1852 data: data.get(12..).unwrap_or_default(),
1853 })
1854 }
1855 S2C_SURFACE_TITLE => {
1856 if data.len() < 3 {
1857 return None;
1858 }
1859 let title = std::str::from_utf8(data.get(3..).unwrap_or_default()).unwrap_or_default();
1860 Some(ServerMsg::SurfaceTitle {
1861 surface_id: u16::from_le_bytes([data[1], data[2]]),
1862 title,
1863 })
1864 }
1865 S2C_SURFACE_APP_ID => {
1866 if data.len() < 3 {
1867 return None;
1868 }
1869 let app_id = std::str::from_utf8(data.get(3..).unwrap_or_default()).unwrap_or_default();
1870 Some(ServerMsg::SurfaceAppId {
1871 surface_id: u16::from_le_bytes([data[1], data[2]]),
1872 app_id,
1873 })
1874 }
1875 S2C_SURFACE_RESIZED => {
1876 if data.len() < 7 {
1877 return None;
1878 }
1879 Some(ServerMsg::SurfaceResized {
1880 surface_id: u16::from_le_bytes([data[1], data[2]]),
1881 width: u16::from_le_bytes([data[3], data[4]]),
1882 height: u16::from_le_bytes([data[5], data[6]]),
1883 })
1884 }
1885 S2C_CLIPBOARD_CONTENT => {
1886 if data.len() < 7 {
1887 return None;
1888 }
1889 let mime_len = u16::from_le_bytes([data[1], data[2]]) as usize;
1890 let mut off = 3;
1891 if off + mime_len + 4 > data.len() {
1892 return None;
1893 }
1894 let mime_type = std::str::from_utf8(&data[off..off + mime_len]).unwrap_or_default();
1895 off += mime_len;
1896 let data_len =
1897 u32::from_le_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]])
1898 as usize;
1899 off += 4;
1900 if off + data_len > data.len() {
1901 return None;
1902 }
1903 Some(ServerMsg::ClipboardContent {
1904 mime_type,
1905 data: &data[off..off + data_len],
1906 })
1907 }
1908 S2C_SURFACE_LIST => {
1909 if data.len() < 3 {
1910 return None;
1911 }
1912 let count = u16::from_le_bytes([data[1], data[2]]) as usize;
1913 let mut entries = Vec::with_capacity(count);
1914 let mut offset = 3;
1915 for _ in 0..count {
1916 if offset + 8 > data.len() {
1917 break;
1918 }
1919 let surface_id = u16::from_le_bytes([data[offset], data[offset + 1]]);
1920 let parent_id = u16::from_le_bytes([data[offset + 2], data[offset + 3]]);
1921 let width = u16::from_le_bytes([data[offset + 4], data[offset + 5]]);
1922 let height = u16::from_le_bytes([data[offset + 6], data[offset + 7]]);
1923 offset += 8;
1924 if offset + 2 > data.len() {
1925 break;
1926 }
1927 let title_len = u16::from_le_bytes([data[offset], data[offset + 1]]) as usize;
1928 offset += 2;
1929 if offset + title_len > data.len() {
1930 break;
1931 }
1932 let title =
1933 std::str::from_utf8(&data[offset..offset + title_len]).unwrap_or_default();
1934 offset += title_len;
1935 if offset + 2 > data.len() {
1936 break;
1937 }
1938 let app_id_len = u16::from_le_bytes([data[offset], data[offset + 1]]) as usize;
1939 offset += 2;
1940 if offset + app_id_len > data.len() {
1941 break;
1942 }
1943 let app_id =
1944 std::str::from_utf8(&data[offset..offset + app_id_len]).unwrap_or_default();
1945 offset += app_id_len;
1946 entries.push(SurfaceListEntry {
1947 surface_id,
1948 parent_id,
1949 width,
1950 height,
1951 title: title.to_string(),
1952 app_id: app_id.to_string(),
1953 });
1954 }
1955 Some(ServerMsg::SurfaceList { entries })
1956 }
1957 S2C_SURFACE_CAPTURE => {
1958 if data.len() < 11 {
1959 return None;
1960 }
1961 let surface_id = u16::from_le_bytes([data[1], data[2]]);
1962 let width = u32::from_le_bytes([data[3], data[4], data[5], data[6]]);
1963 let height = u32::from_le_bytes([data[7], data[8], data[9], data[10]]);
1964 let image_data = data.get(11..).unwrap_or_default();
1965 Some(ServerMsg::SurfaceCapture {
1966 surface_id,
1967 width,
1968 height,
1969 image_data,
1970 })
1971 }
1972 S2C_CLIPBOARD_LIST => {
1973 if data.len() < 3 {
1974 return None;
1975 }
1976 let count = u16::from_le_bytes([data[1], data[2]]) as usize;
1977 let mut mime_types = Vec::with_capacity(count);
1978 let mut offset = 3;
1979 for _ in 0..count {
1980 if offset + 2 > data.len() {
1981 break;
1982 }
1983 let mime_len = u16::from_le_bytes([data[offset], data[offset + 1]]) as usize;
1984 offset += 2;
1985 if offset + mime_len > data.len() {
1986 break;
1987 }
1988 let mime =
1989 std::str::from_utf8(&data[offset..offset + mime_len]).unwrap_or_default();
1990 mime_types.push(mime.to_string());
1991 offset += mime_len;
1992 }
1993 Some(ServerMsg::ClipboardList { mime_types })
1994 }
1995 S2C_QUIT => Some(ServerMsg::Quit),
1996 _ => None,
1997 }
1998}
1999
2000pub fn msg_hello(
2001 version: u16,
2002 features: u32,
2003 boot_generation: u64,
2004 server_version: &str,
2005) -> Vec<u8> {
2006 let ver_bytes = server_version.as_bytes();
2007 let ver_len = ver_bytes.len().min(u16::MAX as usize);
2008 let mut msg = Vec::with_capacity(17 + ver_len);
2009 msg.push(S2C_HELLO);
2010 msg.extend_from_slice(&version.to_le_bytes());
2011 msg.extend_from_slice(&features.to_le_bytes());
2012 msg.extend_from_slice(&boot_generation.to_le_bytes());
2013 msg.extend_from_slice(&(ver_len as u16).to_le_bytes());
2014 msg.extend_from_slice(&ver_bytes[..ver_len]);
2015 msg
2016}
2017
2018pub fn msg_create(rows: u16, cols: u16) -> Vec<u8> {
2019 msg_create_tagged(rows, cols, "")
2020}
2021
2022pub fn msg_create_tagged(rows: u16, cols: u16, tag: &str) -> Vec<u8> {
2023 let tag_bytes = tag.as_bytes();
2024 let tag_len = tag_bytes.len().min(u16::MAX as usize);
2025 let mut msg = Vec::with_capacity(7 + tag_len);
2026 msg.push(C2S_CREATE);
2027 msg.extend_from_slice(&rows.to_le_bytes());
2028 msg.extend_from_slice(&cols.to_le_bytes());
2029 msg.extend_from_slice(&(tag_len as u16).to_le_bytes());
2030 msg.extend_from_slice(&tag_bytes[..tag_len]);
2031 msg
2032}
2033
2034pub fn msg_create_at(rows: u16, cols: u16, tag: &str, src_pty_id: u16) -> Vec<u8> {
2036 let tag_bytes = tag.as_bytes();
2037 let tag_len = tag_bytes.len().min(u16::MAX as usize);
2038 let mut msg = Vec::with_capacity(9 + tag_len);
2039 msg.push(C2S_CREATE_AT);
2040 msg.extend_from_slice(&rows.to_le_bytes());
2041 msg.extend_from_slice(&cols.to_le_bytes());
2042 msg.extend_from_slice(&(tag_len as u16).to_le_bytes());
2043 msg.extend_from_slice(&tag_bytes[..tag_len]);
2044 msg.extend_from_slice(&src_pty_id.to_le_bytes());
2045 msg
2046}
2047
2048pub fn msg_create_n(nonce: u16, rows: u16, cols: u16, tag: &str) -> Vec<u8> {
2049 let tag_bytes = tag.as_bytes();
2050 let tag_len = tag_bytes.len().min(u16::MAX as usize);
2051 let mut msg = Vec::with_capacity(9 + tag_len);
2052 msg.push(C2S_CREATE_N);
2053 msg.extend_from_slice(&nonce.to_le_bytes());
2054 msg.extend_from_slice(&rows.to_le_bytes());
2055 msg.extend_from_slice(&cols.to_le_bytes());
2056 msg.extend_from_slice(&(tag_len as u16).to_le_bytes());
2057 msg.extend_from_slice(&tag_bytes[..tag_len]);
2058 msg
2059}
2060
2061pub fn msg_create_n_command(nonce: u16, rows: u16, cols: u16, tag: &str, command: &str) -> Vec<u8> {
2062 let mut msg = msg_create_n(nonce, rows, cols, tag);
2063 msg.extend_from_slice(command.as_bytes());
2064 msg
2065}
2066
2067pub fn msg_create2(
2068 nonce: u16,
2069 rows: u16,
2070 cols: u16,
2071 tag: &str,
2072 command: &str,
2073 features: u8,
2074) -> Vec<u8> {
2075 msg_create2_with_cwd(nonce, rows, cols, tag, command, features, None)
2076}
2077
2078pub fn msg_create2_with_cwd(
2079 nonce: u16,
2080 rows: u16,
2081 cols: u16,
2082 tag: &str,
2083 command: &str,
2084 features: u8,
2085 cwd: Option<&str>,
2086) -> Vec<u8> {
2087 let tag_bytes = tag.as_bytes();
2088 let cmd_bytes = command.as_bytes();
2089 let cwd_bytes = cwd.unwrap_or_default().as_bytes();
2090 let has_cmd = !command.is_empty();
2091 let cwd_len = cwd_bytes.len().min(u16::MAX as usize);
2092 let has_cwd = cwd_len > 0;
2093 let feat = features
2094 | if has_cmd { CREATE2_HAS_COMMAND } else { 0 }
2095 | if has_cwd { CREATE2_HAS_CWD } else { 0 };
2096 let tag_len = tag_bytes.len().min(u16::MAX as usize);
2097 let mut msg =
2098 Vec::with_capacity(10 + tag_len + if has_cwd { 2 + cwd_len } else { 0 } + cmd_bytes.len());
2099 msg.push(C2S_CREATE2);
2100 msg.extend_from_slice(&nonce.to_le_bytes());
2101 msg.extend_from_slice(&rows.to_le_bytes());
2102 msg.extend_from_slice(&cols.to_le_bytes());
2103 msg.push(feat);
2104 msg.extend_from_slice(&(tag_len as u16).to_le_bytes());
2105 msg.extend_from_slice(&tag_bytes[..tag_len]);
2106 if has_cwd {
2107 msg.extend_from_slice(&(cwd_len as u16).to_le_bytes());
2108 msg.extend_from_slice(&cwd_bytes[..cwd_len]);
2109 }
2110 if has_cmd {
2111 msg.extend_from_slice(cmd_bytes);
2112 }
2113 msg
2114}
2115
2116pub fn msg_create_command(rows: u16, cols: u16, command: &str) -> Vec<u8> {
2117 msg_create_tagged_command(rows, cols, "", command)
2118}
2119
2120pub fn msg_create_tagged_command(rows: u16, cols: u16, tag: &str, command: &str) -> Vec<u8> {
2121 let mut msg = msg_create_tagged(rows, cols, tag);
2122 msg.extend_from_slice(command.as_bytes());
2123 msg
2124}
2125
2126pub fn msg_input(pty_id: u16, data: &[u8]) -> Vec<u8> {
2127 let mut msg = Vec::with_capacity(3 + data.len());
2128 msg.push(C2S_INPUT);
2129 msg.extend_from_slice(&pty_id.to_le_bytes());
2130 msg.extend_from_slice(data);
2131 msg
2132}
2133
2134pub fn msg_mouse(pty_id: u16, type_: u8, button: u8, col: u16, row: u16) -> Vec<u8> {
2135 let mut msg = Vec::with_capacity(9);
2136 msg.push(C2S_MOUSE);
2137 msg.extend_from_slice(&pty_id.to_le_bytes());
2138 msg.push(type_);
2139 msg.push(button);
2140 msg.extend_from_slice(&col.to_le_bytes());
2141 msg.extend_from_slice(&row.to_le_bytes());
2142 msg
2143}
2144
2145pub fn msg_resize(pty_id: u16, rows: u16, cols: u16) -> Vec<u8> {
2146 let mut msg = Vec::with_capacity(7);
2147 msg.push(C2S_RESIZE);
2148 msg.extend_from_slice(&pty_id.to_le_bytes());
2149 msg.extend_from_slice(&rows.to_le_bytes());
2150 msg.extend_from_slice(&cols.to_le_bytes());
2151 msg
2152}
2153
2154pub fn msg_resize_batch(entries: &[(u16, u16, u16)]) -> Vec<u8> {
2155 let mut msg = Vec::with_capacity(1 + entries.len() * 6);
2156 msg.push(C2S_RESIZE);
2157 for &(pty_id, rows, cols) in entries {
2158 msg.extend_from_slice(&pty_id.to_le_bytes());
2159 msg.extend_from_slice(&rows.to_le_bytes());
2160 msg.extend_from_slice(&cols.to_le_bytes());
2161 }
2162 msg
2163}
2164
2165pub fn msg_focus(pty_id: u16) -> Vec<u8> {
2166 let mut msg = Vec::with_capacity(3);
2167 msg.push(C2S_FOCUS);
2168 msg.extend_from_slice(&pty_id.to_le_bytes());
2169 msg
2170}
2171
2172pub fn msg_close(pty_id: u16) -> Vec<u8> {
2173 let mut msg = Vec::with_capacity(3);
2174 msg.push(C2S_CLOSE);
2175 msg.extend_from_slice(&pty_id.to_le_bytes());
2176 msg
2177}
2178
2179pub fn msg_kill(pty_id: u16, signal: i32) -> Vec<u8> {
2180 let mut msg = Vec::with_capacity(7);
2181 msg.push(C2S_KILL);
2182 msg.extend_from_slice(&pty_id.to_le_bytes());
2183 msg.extend_from_slice(&signal.to_le_bytes());
2184 msg
2185}
2186
2187pub fn msg_restart(pty_id: u16) -> Vec<u8> {
2188 let mut msg = Vec::with_capacity(3);
2189 msg.push(C2S_RESTART);
2190 msg.extend_from_slice(&pty_id.to_le_bytes());
2191 msg
2192}
2193
2194pub fn msg_subscribe(pty_id: u16) -> Vec<u8> {
2195 let mut msg = Vec::with_capacity(3);
2196 msg.push(C2S_SUBSCRIBE);
2197 msg.extend_from_slice(&pty_id.to_le_bytes());
2198 msg
2199}
2200
2201pub fn msg_unsubscribe(pty_id: u16) -> Vec<u8> {
2202 let mut msg = Vec::with_capacity(3);
2203 msg.push(C2S_UNSUBSCRIBE);
2204 msg.extend_from_slice(&pty_id.to_le_bytes());
2205 msg
2206}
2207
2208pub fn msg_search(request_id: u16, query: &str) -> Vec<u8> {
2209 let query = query.as_bytes();
2210 let mut msg = Vec::with_capacity(3 + query.len());
2211 msg.push(C2S_SEARCH);
2212 msg.extend_from_slice(&request_id.to_le_bytes());
2213 msg.extend_from_slice(query);
2214 msg
2215}
2216
2217pub fn msg_ack() -> Vec<u8> {
2218 vec![C2S_ACK]
2219}
2220
2221pub fn msg_scroll(pty_id: u16, offset: u32) -> Vec<u8> {
2222 let mut msg = Vec::with_capacity(7);
2223 msg.push(C2S_SCROLL);
2224 msg.extend_from_slice(&pty_id.to_le_bytes());
2225 msg.extend_from_slice(&offset.to_le_bytes());
2226 msg
2227}
2228
2229pub fn msg_display_rate(fps: u16) -> Vec<u8> {
2230 let mut msg = Vec::with_capacity(3);
2231 msg.push(C2S_DISPLAY_RATE);
2232 msg.extend_from_slice(&fps.to_le_bytes());
2233 msg
2234}
2235
2236pub fn msg_client_metrics(backlog: u16, ack_ahead: u16, apply_ms_x10: u16) -> Vec<u8> {
2237 let mut msg = Vec::with_capacity(7);
2238 msg.push(C2S_CLIENT_METRICS);
2239 msg.extend_from_slice(&backlog.to_le_bytes());
2240 msg.extend_from_slice(&ack_ahead.to_le_bytes());
2241 msg.extend_from_slice(&apply_ms_x10.to_le_bytes());
2242 msg
2243}
2244
2245pub fn msg_read(nonce: u16, pty_id: u16, offset: u32, limit: u32, flags: u8) -> Vec<u8> {
2246 let mut msg = Vec::with_capacity(14);
2247 msg.push(C2S_READ);
2248 msg.extend_from_slice(&nonce.to_le_bytes());
2249 msg.extend_from_slice(&pty_id.to_le_bytes());
2250 msg.extend_from_slice(&offset.to_le_bytes());
2251 msg.extend_from_slice(&limit.to_le_bytes());
2252 msg.push(flags);
2253 msg
2254}
2255
2256pub fn msg_term_cwd(nonce: u16, pty_id: u16) -> Vec<u8> {
2258 let mut m = Vec::with_capacity(5);
2259 m.push(C2S_TERM_CWD);
2260 m.extend_from_slice(&nonce.to_le_bytes());
2261 m.extend_from_slice(&pty_id.to_le_bytes());
2262 m
2263}
2264
2265pub fn parse_term_cwd(data: &[u8]) -> Option<(u16, u16)> {
2267 if data.first().copied() != Some(C2S_TERM_CWD) || data.len() < 5 {
2268 return None;
2269 }
2270 Some((
2271 u16::from_le_bytes([data[1], data[2]]),
2272 u16::from_le_bytes([data[3], data[4]]),
2273 ))
2274}
2275
2276pub fn msg_term_cwd_reply(nonce: u16, cwd: &str) -> Vec<u8> {
2278 let cb = cwd.as_bytes();
2279 let mut m = Vec::with_capacity(5 + cb.len());
2280 m.push(S2C_TERM_CWD);
2281 m.extend_from_slice(&nonce.to_le_bytes());
2282 m.extend_from_slice(&(cb.len() as u16).to_le_bytes());
2283 m.extend_from_slice(cb);
2284 m
2285}
2286
2287pub fn parse_term_cwd_reply(data: &[u8]) -> Option<(u16, String)> {
2289 if data.first().copied() != Some(S2C_TERM_CWD) || data.len() < 5 {
2290 return None;
2291 }
2292 let nonce = u16::from_le_bytes([data[1], data[2]]);
2293 let len = u16::from_le_bytes([data[3], data[4]]) as usize;
2294 if data.len() < 5 + len {
2295 return None;
2296 }
2297 Some((
2298 nonce,
2299 String::from_utf8_lossy(&data[5..5 + len]).into_owned(),
2300 ))
2301}
2302
2303pub fn msg_term_cwd_event(pty_id: u16, cwd: &str) -> Vec<u8> {
2305 let cb = cwd.as_bytes();
2306 let mut m = Vec::with_capacity(3 + cb.len());
2307 m.push(S2C_TERM_CWD_EVENT);
2308 m.extend_from_slice(&pty_id.to_le_bytes());
2309 m.extend_from_slice(cb);
2310 m
2311}
2312
2313pub fn parse_term_cwd_event(data: &[u8]) -> Option<(u16, String)> {
2315 if data.first().copied() != Some(S2C_TERM_CWD_EVENT) || data.len() < 3 {
2316 return None;
2317 }
2318 Some((
2319 u16::from_le_bytes([data[1], data[2]]),
2320 String::from_utf8_lossy(&data[3..]).into_owned(),
2321 ))
2322}
2323
2324pub fn msg_copy_range(
2325 nonce: u16,
2326 pty_id: u16,
2327 start_tail: u32,
2328 start_col: u16,
2329 end_tail: u32,
2330 end_col: u16,
2331 flags: u8,
2332) -> Vec<u8> {
2333 let mut msg = Vec::with_capacity(18);
2334 msg.push(C2S_COPY_RANGE);
2335 msg.extend_from_slice(&nonce.to_le_bytes());
2336 msg.extend_from_slice(&pty_id.to_le_bytes());
2337 msg.extend_from_slice(&start_tail.to_le_bytes());
2338 msg.extend_from_slice(&start_col.to_le_bytes());
2339 msg.extend_from_slice(&end_tail.to_le_bytes());
2340 msg.extend_from_slice(&end_col.to_le_bytes());
2341 msg.push(flags);
2342 msg
2343}
2344
2345pub fn msg_exited(pty_id: u16, exit_status: i32) -> Vec<u8> {
2346 let mut msg = Vec::with_capacity(7);
2347 msg.push(S2C_EXITED);
2348 msg.extend_from_slice(&pty_id.to_le_bytes());
2349 msg.extend_from_slice(&exit_status.to_le_bytes());
2350 msg
2351}
2352
2353pub fn msg_quit() -> Vec<u8> {
2355 vec![C2S_QUIT]
2356}
2357
2358pub fn msg_s2c_quit() -> Vec<u8> {
2360 vec![S2C_QUIT]
2361}
2362
2363pub fn msg_surface_created(
2364 surface_id: u16,
2365 parent_id: u16,
2366 width: u16,
2367 height: u16,
2368 title: &str,
2369 app_id: &str,
2370) -> Vec<u8> {
2371 let title_bytes = title.as_bytes();
2372 let app_id_bytes = app_id.as_bytes();
2373 let mut msg = Vec::with_capacity(13 + title_bytes.len() + app_id_bytes.len());
2374 msg.push(S2C_SURFACE_CREATED);
2375 msg.extend_from_slice(&surface_id.to_le_bytes());
2376 msg.extend_from_slice(&parent_id.to_le_bytes());
2377 msg.extend_from_slice(&width.to_le_bytes());
2378 msg.extend_from_slice(&height.to_le_bytes());
2379 msg.extend_from_slice(&(title_bytes.len() as u16).to_le_bytes());
2380 msg.extend_from_slice(title_bytes);
2381 msg.extend_from_slice(&(app_id_bytes.len() as u16).to_le_bytes());
2382 msg.extend_from_slice(app_id_bytes);
2383 msg
2384}
2385
2386pub fn msg_surface_destroyed(surface_id: u16) -> Vec<u8> {
2387 let mut msg = Vec::with_capacity(3);
2388 msg.push(S2C_SURFACE_DESTROYED);
2389 msg.extend_from_slice(&surface_id.to_le_bytes());
2390 msg
2391}
2392
2393pub fn msg_surface_frame(
2394 surface_id: u16,
2395 timestamp: u32,
2396 flags: u8,
2397 width: u16,
2398 height: u16,
2399 data: &[u8],
2400) -> Vec<u8> {
2401 let mut msg = Vec::with_capacity(12 + data.len());
2402 msg.push(S2C_SURFACE_FRAME);
2403 msg.extend_from_slice(&surface_id.to_le_bytes());
2404 msg.extend_from_slice(×tamp.to_le_bytes());
2405 msg.push(flags);
2406 msg.extend_from_slice(&width.to_le_bytes());
2407 msg.extend_from_slice(&height.to_le_bytes());
2408 msg.extend_from_slice(data);
2409 msg
2410}
2411
2412pub fn msg_surface_title(surface_id: u16, title: &str) -> Vec<u8> {
2413 let title_bytes = title.as_bytes();
2414 let mut msg = Vec::with_capacity(3 + title_bytes.len());
2415 msg.push(S2C_SURFACE_TITLE);
2416 msg.extend_from_slice(&surface_id.to_le_bytes());
2417 msg.extend_from_slice(title_bytes);
2418 msg
2419}
2420
2421pub fn msg_surface_app_id(surface_id: u16, app_id: &str) -> Vec<u8> {
2422 let app_id_bytes = app_id.as_bytes();
2423 let mut msg = Vec::with_capacity(3 + app_id_bytes.len());
2424 msg.push(S2C_SURFACE_APP_ID);
2425 msg.extend_from_slice(&surface_id.to_le_bytes());
2426 msg.extend_from_slice(app_id_bytes);
2427 msg
2428}
2429
2430pub fn msg_surface_encoder(surface_id: u16, encoder_name: &str, codec_string: &str) -> Vec<u8> {
2435 let name_bytes = encoder_name.as_bytes();
2436 let codec_bytes = codec_string.as_bytes();
2437 let mut msg = Vec::with_capacity(3 + name_bytes.len() + 1 + codec_bytes.len());
2438 msg.push(S2C_SURFACE_ENCODER);
2439 msg.extend_from_slice(&surface_id.to_le_bytes());
2440 msg.extend_from_slice(name_bytes);
2441 msg.push(0); msg.extend_from_slice(codec_bytes);
2443 msg
2444}
2445
2446pub fn msg_surface_resized(surface_id: u16, width: u16, height: u16) -> Vec<u8> {
2447 let mut msg = Vec::with_capacity(7);
2448 msg.push(S2C_SURFACE_RESIZED);
2449 msg.extend_from_slice(&surface_id.to_le_bytes());
2450 msg.extend_from_slice(&width.to_le_bytes());
2451 msg.extend_from_slice(&height.to_le_bytes());
2452 msg
2453}
2454
2455pub fn msg_s2c_clipboard_content(mime_type: &str, data: &[u8]) -> Vec<u8> {
2456 let mime_bytes = mime_type.as_bytes();
2457 let mut msg = Vec::with_capacity(7 + mime_bytes.len() + data.len());
2458 msg.push(S2C_CLIPBOARD_CONTENT);
2459 msg.extend_from_slice(&(mime_bytes.len() as u16).to_le_bytes());
2460 msg.extend_from_slice(mime_bytes);
2461 msg.extend_from_slice(&(data.len() as u32).to_le_bytes());
2462 msg.extend_from_slice(data);
2463 msg
2464}
2465
2466pub fn msg_surface_input(surface_id: u16, data: &[u8]) -> Vec<u8> {
2467 let mut msg = Vec::with_capacity(3 + data.len());
2468 msg.push(C2S_SURFACE_INPUT);
2469 msg.extend_from_slice(&surface_id.to_le_bytes());
2470 msg.extend_from_slice(data);
2471 msg
2472}
2473
2474pub fn msg_surface_pointer(surface_id: u16, event_type: u8, button: u8, x: u16, y: u16) -> Vec<u8> {
2475 let mut msg = Vec::with_capacity(8);
2476 msg.push(C2S_SURFACE_POINTER);
2477 msg.extend_from_slice(&surface_id.to_le_bytes());
2478 msg.push(event_type);
2479 msg.push(button);
2480 msg.extend_from_slice(&x.to_le_bytes());
2481 msg.extend_from_slice(&y.to_le_bytes());
2482 msg
2483}
2484
2485pub fn msg_surface_pointer_axis(surface_id: u16, axis: u8, value_x100: i32) -> Vec<u8> {
2486 let mut msg = Vec::with_capacity(8);
2487 msg.push(C2S_SURFACE_POINTER_AXIS);
2488 msg.extend_from_slice(&surface_id.to_le_bytes());
2489 msg.push(axis);
2490 msg.extend_from_slice(&value_x100.to_le_bytes());
2491 msg
2492}
2493
2494pub fn msg_surface_resize(surface_id: u16, width: u16, height: u16, scale_120: u16) -> Vec<u8> {
2498 let mut msg = Vec::with_capacity(9);
2499 msg.push(C2S_SURFACE_RESIZE);
2500 msg.extend_from_slice(&surface_id.to_le_bytes());
2501 msg.extend_from_slice(&width.to_le_bytes());
2502 msg.extend_from_slice(&height.to_le_bytes());
2503 msg.extend_from_slice(&scale_120.to_le_bytes());
2504 msg
2505}
2506
2507pub fn msg_surface_focus(surface_id: u16) -> Vec<u8> {
2508 let mut msg = Vec::with_capacity(3);
2509 msg.push(C2S_SURFACE_FOCUS);
2510 msg.extend_from_slice(&surface_id.to_le_bytes());
2511 msg
2512}
2513
2514pub fn msg_surface_text(surface_id: u16, text: &str) -> Vec<u8> {
2522 let tb = text.as_bytes();
2523 let mut msg = Vec::with_capacity(3 + tb.len());
2524 msg.push(C2S_SURFACE_TEXT);
2525 msg.extend_from_slice(&surface_id.to_le_bytes());
2526 msg.extend_from_slice(tb);
2527 msg
2528}
2529
2530pub fn msg_surface_subscribe(surface_id: u16) -> Vec<u8> {
2531 let mut msg = Vec::with_capacity(3);
2532 msg.push(C2S_SURFACE_SUBSCRIBE);
2533 msg.extend_from_slice(&surface_id.to_le_bytes());
2534 msg
2535}
2536
2537pub fn msg_surface_subscribe_ext(
2544 surface_id: u16,
2545 codec_support: u8,
2546 bandwidth: u8,
2547 speed: u8,
2548) -> Vec<u8> {
2549 let mut msg = Vec::with_capacity(6);
2550 msg.push(C2S_SURFACE_SUBSCRIBE);
2551 msg.extend_from_slice(&surface_id.to_le_bytes());
2552 msg.push(codec_support);
2553 msg.push(bandwidth);
2554 msg.push(speed);
2555 msg
2556}
2557
2558pub fn msg_surface_subscribe_scaled(
2567 surface_id: u16,
2568 codec_support: u8,
2569 bandwidth: u8,
2570 speed: u8,
2571 width: u16,
2572 height: u16,
2573) -> Vec<u8> {
2574 let mut msg = Vec::with_capacity(10);
2575 msg.push(C2S_SURFACE_SUBSCRIBE);
2576 msg.extend_from_slice(&surface_id.to_le_bytes());
2577 msg.push(codec_support);
2578 msg.push(bandwidth);
2579 msg.push(speed);
2580 msg.extend_from_slice(&width.to_le_bytes());
2581 msg.extend_from_slice(&height.to_le_bytes());
2582 msg
2583}
2584
2585pub fn msg_surface_unsubscribe(surface_id: u16) -> Vec<u8> {
2586 let mut msg = Vec::with_capacity(3);
2587 msg.push(C2S_SURFACE_UNSUBSCRIBE);
2588 msg.extend_from_slice(&surface_id.to_le_bytes());
2589 msg
2590}
2591
2592pub fn msg_surface_close(surface_id: u16) -> Vec<u8> {
2593 let mut msg = Vec::with_capacity(3);
2594 msg.push(C2S_SURFACE_CLOSE);
2595 msg.extend_from_slice(&surface_id.to_le_bytes());
2596 msg
2597}
2598
2599pub fn msg_c2s_clipboard_list() -> Vec<u8> {
2601 vec![C2S_CLIPBOARD_LIST]
2602}
2603
2604pub fn msg_c2s_clipboard_get(mime_type: &str) -> Vec<u8> {
2606 let mime_bytes = mime_type.as_bytes();
2607 let mut msg = Vec::with_capacity(3 + mime_bytes.len());
2608 msg.push(C2S_CLIPBOARD_GET);
2609 msg.extend_from_slice(&(mime_bytes.len() as u16).to_le_bytes());
2610 msg.extend_from_slice(mime_bytes);
2611 msg
2612}
2613
2614pub fn msg_s2c_clipboard_list(mime_types: &[String]) -> Vec<u8> {
2616 let count = mime_types.len().min(u16::MAX as usize);
2617 let mut msg = Vec::with_capacity(3 + count * 20);
2618 msg.push(S2C_CLIPBOARD_LIST);
2619 msg.extend_from_slice(&(count as u16).to_le_bytes());
2620 for mime in mime_types.iter().take(count) {
2621 let bytes = mime.as_bytes();
2622 msg.extend_from_slice(&(bytes.len() as u16).to_le_bytes());
2623 msg.extend_from_slice(bytes);
2624 }
2625 msg
2626}
2627
2628pub fn msg_c2s_clipboard_set(mime_type: &str, data: &[u8]) -> Vec<u8> {
2629 let mime_bytes = mime_type.as_bytes();
2630 let mut msg = Vec::with_capacity(7 + mime_bytes.len() + data.len());
2631 msg.push(C2S_CLIPBOARD_SET);
2632 msg.extend_from_slice(&(mime_bytes.len() as u16).to_le_bytes());
2633 msg.extend_from_slice(mime_bytes);
2634 msg.extend_from_slice(&(data.len() as u32).to_le_bytes());
2635 msg.extend_from_slice(data);
2636 msg
2637}
2638
2639fn push_sgr(out: &mut String, style: &CellStyle) {
2640 use std::fmt::Write;
2641 out.push_str("\x1b[0");
2642 if style.bold {
2643 out.push_str(";1");
2644 }
2645 if style.dim {
2646 out.push_str(";2");
2647 }
2648 if style.italic {
2649 out.push_str(";3");
2650 }
2651 if style.underline {
2652 out.push_str(";4");
2653 }
2654 if style.inverse {
2655 out.push_str(";7");
2656 }
2657 match style.fg {
2658 Color::Indexed(n) => {
2659 let _ = write!(out, ";38;5;{n}");
2660 }
2661 Color::Rgb(r, g, b) => {
2662 let _ = write!(out, ";38;2;{r};{g};{b}");
2663 }
2664 Color::Default => {}
2665 }
2666 match style.bg {
2667 Color::Indexed(n) => {
2668 let _ = write!(out, ";48;5;{n}");
2669 }
2670 Color::Rgb(r, g, b) => {
2671 let _ = write!(out, ";48;2;{r};{g};{b}");
2672 }
2673 Color::Default => {}
2674 }
2675 out.push('m');
2676}
2677
2678const MODE_ALT_SCREEN: u16 = 1 << 11;
2679
2680fn mode_is_cooked(mode: u16) -> bool {
2681 mode & MODE_ECHO != 0 && mode & MODE_ICANON != 0 && mode & MODE_ALT_SCREEN == 0
2682}
2683
2684pub fn build_update_msg(
2685 pty_id: u16,
2686 current: &FrameState,
2687 previous: &FrameState,
2688) -> Option<Vec<u8>> {
2689 let same_size = previous.rows == current.rows
2690 && previous.cols == current.cols
2691 && previous.cells.len() == current.cells.len();
2692 let keyframe = !same_size;
2699 let title_changed = keyframe || current.title != previous.title;
2700
2701 let mut ops = Vec::new();
2703 let mut op_count = 0u16;
2704
2705 let scroll_eligible = (mode_is_cooked(current.mode) && mode_is_cooked(previous.mode))
2709 || current.mode == 0
2710 || previous.mode == 0;
2711 if ENABLE_SCROLL_OPS
2712 && same_size
2713 && previous.cells != current.cells
2714 && scroll_eligible
2715 && let Some(delta_rows) = detect_vertical_scroll(current, previous)
2716 {
2717 let mut basis = previous.clone();
2718 encode_copy_rect_op(&mut ops, current, delta_rows);
2719 apply_vertical_scroll_copy(&mut basis, delta_rows);
2720 op_count += 1;
2721 append_full_width_fill_ops(current, &mut basis, &mut ops, &mut op_count);
2722 if let Some(patch_op) = build_patch_op(current, &basis) {
2723 ops.extend_from_slice(&patch_op);
2724 op_count += 1;
2725 }
2726 }
2727
2728 if op_count == 0 {
2734 let blank;
2735 let basis = if same_size {
2736 previous
2737 } else {
2738 ops.push(OP_FILL_RECT);
2739 ops.extend_from_slice(&0u16.to_le_bytes());
2740 ops.extend_from_slice(&0u16.to_le_bytes());
2741 ops.extend_from_slice(¤t.rows.to_le_bytes());
2742 ops.extend_from_slice(¤t.cols.to_le_bytes());
2743 ops.extend_from_slice(&[0u8; CELL_SIZE]);
2744 op_count = 1;
2745 blank = FrameState::new(current.rows, current.cols);
2746 &blank
2747 };
2748 if let Some(patch_op) = build_patch_op(current, basis) {
2749 ops.extend_from_slice(&patch_op);
2750 op_count += 1;
2751 }
2752 }
2753
2754 if op_count == 0 {
2755 if !title_changed
2757 && current.cursor_row == previous.cursor_row
2758 && current.cursor_col == previous.cursor_col
2759 && current.mode == previous.mode
2760 {
2761 return None;
2762 }
2763 }
2764
2765 let has_overflow = !current.overflow.is_empty();
2770 let overflow_section = if has_overflow {
2771 serialize_overflow_strings(current)
2772 } else {
2773 Vec::new()
2774 };
2775
2776 let line_flags_changed =
2777 current.line_flags != previous.line_flags || current.rows != previous.rows;
2778 let has_line_flags =
2779 keyframe || (line_flags_changed && !current.line_flags.iter().all(|&f| f == 0));
2780
2781 let title_bytes = if title_changed {
2782 current.title.as_bytes()
2783 } else {
2784 &[]
2785 };
2786 let title_len = title_bytes.len().min(TITLE_LEN_MASK as usize);
2787 let title_field = OPS_PRESENT
2788 | if has_overflow { STRINGS_PRESENT } else { 0 }
2789 | if has_line_flags {
2790 LINE_FLAGS_PRESENT
2791 } else {
2792 0
2793 }
2794 | if title_changed {
2795 TITLE_PRESENT | title_len as u16
2796 } else {
2797 0
2798 };
2799
2800 let mut payload = Vec::with_capacity(
2801 12 + title_len
2802 + 2
2803 + ops.len()
2804 + overflow_section.len()
2805 + if has_line_flags {
2806 current.rows as usize
2807 } else {
2808 0
2809 }
2810 + 4,
2811 );
2812 payload.extend_from_slice(¤t.rows.to_le_bytes());
2813 payload.extend_from_slice(¤t.cols.to_le_bytes());
2814 payload.extend_from_slice(¤t.cursor_row.to_le_bytes());
2815 payload.extend_from_slice(¤t.cursor_col.to_le_bytes());
2816 payload.extend_from_slice(¤t.mode.to_le_bytes());
2817 payload.extend_from_slice(&title_field.to_le_bytes());
2818 if title_changed {
2819 payload.extend_from_slice(&title_bytes[..title_len]);
2820 }
2821 payload.extend_from_slice(&op_count.to_le_bytes());
2822 payload.extend_from_slice(&ops);
2823 payload.extend_from_slice(&overflow_section);
2824 if has_line_flags {
2825 payload.extend_from_slice(¤t.line_flags);
2826 }
2827 payload.extend_from_slice(¤t.scrollback_lines.to_le_bytes());
2829
2830 let compressed = compress_prepend_size(&payload);
2831 let mut msg = Vec::with_capacity(3 + compressed.len());
2832 msg.push(S2C_UPDATE);
2833 msg.extend_from_slice(&pty_id.to_le_bytes());
2834 msg.extend_from_slice(&compressed);
2835 Some(msg)
2836}
2837
2838fn serialize_overflow_strings(frame: &FrameState) -> Vec<u8> {
2840 let count = frame.overflow.len().min(u16::MAX as usize);
2841 let mut out = Vec::with_capacity(2 + count * 8);
2842 out.extend_from_slice(&(count as u16).to_le_bytes());
2843 for (&cell_idx, s) in frame.overflow.iter().take(count) {
2844 let bytes = s.as_bytes();
2845 let len = bytes.len().min(u16::MAX as usize);
2846 out.extend_from_slice(&(cell_idx as u32).to_le_bytes());
2847 out.extend_from_slice(&(len as u16).to_le_bytes());
2848 out.extend_from_slice(&bytes[..len]);
2849 }
2850 out
2851}
2852
2853fn build_patch_op(current: &FrameState, previous: &FrameState) -> Option<Vec<u8>> {
2854 let total_cells = current.rows as usize * current.cols as usize;
2855 let total_bytes = total_cells * CELL_SIZE;
2856 if current.cells.len() >= total_bytes
2860 && previous.cells.len() >= total_bytes
2861 && current.cells[..total_bytes] == previous.cells[..total_bytes]
2862 {
2863 return None;
2864 }
2865 let bitmask_len = total_cells.div_ceil(8);
2866 let mut bitmask = vec![0u8; bitmask_len];
2867 let mut dirty_count = 0usize;
2868 for i in 0..total_cells {
2869 let off = i * CELL_SIZE;
2870 if current.cells[off..off + CELL_SIZE] != previous.cells[off..off + CELL_SIZE] {
2871 bitmask[i / 8] |= 1 << (i % 8);
2872 dirty_count += 1;
2873 }
2874 }
2875 if dirty_count == 0 {
2876 return None;
2877 }
2878
2879 let mut op = Vec::with_capacity(1 + bitmask_len + dirty_count * CELL_SIZE);
2880 op.push(OP_PATCH_CELLS);
2881 op.extend_from_slice(&bitmask);
2882 for byte_pos in 0..CELL_SIZE {
2883 for i in 0..total_cells {
2884 if bitmask[i / 8] & (1 << (i % 8)) != 0 {
2885 op.push(current.cells[i * CELL_SIZE + byte_pos]);
2886 }
2887 }
2888 }
2889 Some(op)
2890}
2891
2892fn detect_vertical_scroll(current: &FrameState, previous: &FrameState) -> Option<i16> {
2893 let rows = current.rows as usize;
2894 let cols = current.cols as usize;
2895 if rows < 4 || cols == 0 {
2896 return None;
2897 }
2898 let row_bytes = cols * CELL_SIZE;
2899 let max_delta = rows.saturating_sub(1).min(8);
2900 let mut best: Option<(usize, i16)> = None;
2901
2902 for delta in 1..=max_delta {
2903 let overlap = rows - delta;
2904 if overlap < 3 {
2905 continue;
2906 }
2907 for signed_delta in [-(delta as i16), delta as i16] {
2908 let mut matched = 0usize;
2909 for row in 0..rows {
2910 let src_row = row as i32 - signed_delta as i32;
2911 if src_row < 0 || src_row >= rows as i32 {
2912 continue;
2913 }
2914 let cur_off = row * row_bytes;
2915 let prev_off = src_row as usize * row_bytes;
2916 if current.cells[cur_off..cur_off + row_bytes]
2917 == previous.cells[prev_off..prev_off + row_bytes]
2918 {
2919 matched += 1;
2920 }
2921 }
2922 if matched * 5 < overlap * 4 {
2923 continue;
2924 }
2925 let replace = match best {
2926 None => true,
2927 Some((best_matched, best_delta)) => {
2928 matched > best_matched
2929 || (matched == best_matched
2930 && signed_delta.unsigned_abs() < best_delta.unsigned_abs())
2931 }
2932 };
2933 if replace {
2934 best = Some((matched, signed_delta));
2935 }
2936 }
2937 }
2938
2939 best.map(|(_, delta)| delta)
2940}
2941
2942fn encode_copy_rect_op(out: &mut Vec<u8>, current: &FrameState, delta_rows: i16) {
2943 let rows = current.rows;
2944 let cols = current.cols;
2945 let delta = delta_rows.unsigned_abs();
2946 let (src_row, dst_row, copy_rows) = if delta_rows > 0 {
2947 (0, delta, rows.saturating_sub(delta))
2948 } else {
2949 (delta, 0, rows.saturating_sub(delta))
2950 };
2951 out.push(OP_COPY_RECT);
2952 out.extend_from_slice(&src_row.to_le_bytes());
2953 out.extend_from_slice(&0u16.to_le_bytes());
2954 out.extend_from_slice(&dst_row.to_le_bytes());
2955 out.extend_from_slice(&0u16.to_le_bytes());
2956 out.extend_from_slice(©_rows.to_le_bytes());
2957 out.extend_from_slice(&cols.to_le_bytes());
2958}
2959
2960fn apply_vertical_scroll_copy(frame: &mut FrameState, delta_rows: i16) {
2961 let delta = delta_rows.unsigned_abs();
2962 if delta == 0 || delta >= frame.rows {
2963 return;
2964 }
2965 let (src_row, dst_row, rows) = if delta_rows > 0 {
2966 (0, delta, frame.rows - delta)
2967 } else {
2968 (delta, 0, frame.rows - delta)
2969 };
2970 apply_copy_rect_frame(frame, src_row, 0, dst_row, 0, rows, frame.cols);
2971}
2972
2973fn apply_copy_rect_frame(
2974 frame: &mut FrameState,
2975 src_row: u16,
2976 src_col: u16,
2977 dst_row: u16,
2978 dst_col: u16,
2979 rows: u16,
2980 cols: u16,
2981) {
2982 let rows = rows
2983 .min(frame.rows.saturating_sub(src_row))
2984 .min(frame.rows.saturating_sub(dst_row));
2985 let cols = cols
2986 .min(frame.cols.saturating_sub(src_col))
2987 .min(frame.cols.saturating_sub(dst_col));
2988 if rows == 0 || cols == 0 {
2989 return;
2990 }
2991 let mut temp = vec![0u8; rows as usize * cols as usize * CELL_SIZE];
2992 for r in 0..rows as usize {
2993 let src_off = frame.cell_offset(src_row + r as u16, src_col);
2994 let src_end = src_off + cols as usize * CELL_SIZE;
2995 let dst_off = r * cols as usize * CELL_SIZE;
2996 temp[dst_off..dst_off + cols as usize * CELL_SIZE]
2997 .copy_from_slice(&frame.cells[src_off..src_end]);
2998 }
2999 for r in 0..rows as usize {
3000 let dst_off = frame.cell_offset(dst_row + r as u16, dst_col);
3001 let dst_end = dst_off + cols as usize * CELL_SIZE;
3002 let src_off = r * cols as usize * CELL_SIZE;
3003 frame.cells[dst_off..dst_end]
3004 .copy_from_slice(&temp[src_off..src_off + cols as usize * CELL_SIZE]);
3005 }
3006}
3007
3008fn append_full_width_fill_ops(
3009 current: &FrameState,
3010 basis: &mut FrameState,
3011 out: &mut Vec<u8>,
3012 op_count: &mut u16,
3013) {
3014 let rows = current.rows as usize;
3015 let cols = current.cols as usize;
3016 if rows == 0 || cols == 0 {
3017 return;
3018 }
3019
3020 let row_bytes = cols * CELL_SIZE;
3021 let mut row = 0usize;
3022 while row < rows {
3023 let row_off = row * row_bytes;
3024 if current.cells[row_off..row_off + row_bytes] == basis.cells[row_off..row_off + row_bytes]
3025 {
3026 row += 1;
3027 continue;
3028 }
3029 let Some(cell) = uniform_row_cell(current, row) else {
3030 row += 1;
3031 continue;
3032 };
3033 let mut end = row + 1;
3034 while end < rows {
3035 if uniform_row_cell(current, end).as_ref() != Some(&cell) {
3036 break;
3037 }
3038 end += 1;
3039 }
3040
3041 if *op_count == u16::MAX {
3042 break;
3043 }
3044 out.push(OP_FILL_RECT);
3045 out.extend_from_slice(&(row as u16).to_le_bytes());
3046 out.extend_from_slice(&0u16.to_le_bytes());
3047 out.extend_from_slice(&((end - row) as u16).to_le_bytes());
3048 out.extend_from_slice(¤t.cols.to_le_bytes());
3049 out.extend_from_slice(&cell);
3050 *op_count = op_count.saturating_add(1);
3051
3052 for r in row..end {
3053 let row_off = basis.cell_offset(r as u16, 0);
3054 for c in 0..cols {
3055 let off = row_off + c * CELL_SIZE;
3056 basis.cells[off..off + CELL_SIZE].copy_from_slice(&cell);
3057 }
3058 }
3059
3060 row = end;
3061 }
3062}
3063
3064fn uniform_row_cell(frame: &FrameState, row: usize) -> Option<[u8; CELL_SIZE]> {
3065 let cols = frame.cols as usize;
3066 if row >= frame.rows as usize || cols == 0 {
3067 return None;
3068 }
3069 let start = row * cols * CELL_SIZE;
3070 let mut first = [0u8; CELL_SIZE];
3071 first.copy_from_slice(&frame.cells[start..start + CELL_SIZE]);
3072 if first[1] & 0b110 != 0 {
3073 return None;
3074 }
3075 for col in 1..cols {
3076 let off = start + col * CELL_SIZE;
3077 if frame.cells[off..off + CELL_SIZE] != first {
3078 return None;
3079 }
3080 }
3081 Some(first)
3082}
3083
3084fn encode_cell(dst: &mut [u8], ch: Option<char>, style: CellStyle, wide: bool, wide_cont: bool) {
3085 dst.fill(0);
3086
3087 let mut f0 = 0u8;
3088 encode_color(style.fg, &mut f0, &mut dst[2..5], false);
3089 encode_color(style.bg, &mut f0, &mut dst[5..8], true);
3090 if style.bold {
3091 f0 |= 1 << 4;
3092 }
3093 if style.dim {
3094 f0 |= 1 << 5;
3095 }
3096 if style.italic {
3097 f0 |= 1 << 6;
3098 }
3099 if style.underline {
3100 f0 |= 1 << 7;
3101 }
3102 dst[0] = f0;
3103
3104 let mut f1 = 0u8;
3105 if style.inverse {
3106 f1 |= 1;
3107 }
3108 if wide {
3109 f1 |= 1 << 1;
3110 }
3111 if wide_cont {
3112 f1 |= 1 << 2;
3113 }
3114 if let Some(ch) = ch {
3115 let mut buf = [0u8; 4];
3116 let encoded = ch.encode_utf8(&mut buf).as_bytes();
3117 let len = encoded.len().min(4);
3118 dst[8..8 + len].copy_from_slice(&encoded[..len]);
3119 f1 |= (len as u8) << 3;
3120 }
3121 dst[1] = f1;
3122}
3123
3124fn encode_color(color: Color, flags: &mut u8, dst: &mut [u8], is_bg: bool) {
3125 let shift = if is_bg { 2 } else { 0 };
3126 match color {
3127 Color::Default => {}
3128 Color::Indexed(idx) => {
3129 *flags |= 1 << shift;
3130 dst[0] = idx;
3131 }
3132 Color::Rgb(r, g, b) => {
3133 *flags |= 2 << shift;
3134 dst[0] = r;
3135 dst[1] = g;
3136 dst[2] = b;
3137 }
3138 }
3139}
3140
3141fn wrap_text_lines(text: &str, width: usize) -> Vec<String> {
3142 if width == 0 {
3143 return Vec::new();
3144 }
3145 let mut out = Vec::new();
3146 for paragraph in text.split('\n') {
3147 if paragraph.is_empty() {
3148 out.push(String::new());
3149 continue;
3150 }
3151 let mut line = String::new();
3152 let mut line_width = 0usize;
3153 for word in paragraph.split_whitespace() {
3154 push_wrapped_word(word, width, &mut out, &mut line, &mut line_width);
3155 }
3156 if !line.is_empty() {
3157 out.push(line);
3158 }
3159 }
3160 if out.is_empty() {
3161 out.push(String::new());
3162 }
3163 out
3164}
3165
3166fn push_wrapped_word(
3167 word: &str,
3168 width: usize,
3169 out: &mut Vec<String>,
3170 line: &mut String,
3171 line_width: &mut usize,
3172) {
3173 let word_width = UnicodeWidthStr::width(word);
3174 if line.is_empty() {
3175 if word_width <= width {
3176 line.push_str(word);
3177 *line_width = word_width;
3178 return;
3179 }
3180 } else if *line_width + 1 + word_width <= width {
3181 line.push(' ');
3182 line.push_str(word);
3183 *line_width += 1 + word_width;
3184 return;
3185 } else {
3186 out.push(std::mem::take(line));
3187 *line_width = 0;
3188 if word_width <= width {
3189 line.push_str(word);
3190 *line_width = word_width;
3191 return;
3192 }
3193 }
3194
3195 for ch in word.chars() {
3196 let ch_width = UnicodeWidthChar::width(ch).unwrap_or(1).max(1);
3197 if *line_width + ch_width > width && !line.is_empty() {
3198 out.push(std::mem::take(line));
3199 *line_width = 0;
3200 }
3201 line.push(ch);
3202 *line_width += ch_width;
3203 }
3204}
3205
3206#[cfg(test)]
3207mod tests {
3208 use super::*;
3209
3210 #[test]
3213 fn surface_input_puts_the_surface_id_first() {
3214 let mut payload = 30u32.to_le_bytes().to_vec(); payload.push(1); let msg = msg_surface_input(7, &payload);
3217
3218 assert_eq!(msg.len(), 8);
3219 assert_eq!(msg[0], C2S_SURFACE_INPUT);
3220 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 7);
3221 assert_eq!(u32::from_le_bytes([msg[3], msg[4], msg[5], msg[6]]), 30);
3222 assert_eq!(msg[7], 1);
3223 }
3224
3225 #[test]
3226 fn hello_roundtrip_with_boot_generation() {
3227 let msg = msg_hello(1, 0x1234_5678, 0xfedc_ba98_7654_3210, "0.40.1");
3228 assert_eq!(msg.len(), 17 + 6);
3229 assert_eq!(&msg[7..15], &0xfedc_ba98_7654_3210_u64.to_le_bytes());
3230 assert!(matches!(
3231 parse_server_msg(&msg),
3232 Some(ServerMsg::Hello {
3233 version: 1,
3234 features: 0x1234_5678,
3235 boot_generation: Some(0xfedc_ba98_7654_3210),
3236 server_version: Some("0.40.1"),
3237 })
3238 ));
3239
3240 assert!(matches!(
3242 parse_server_msg(&msg[..15]),
3243 Some(ServerMsg::Hello {
3244 boot_generation: Some(0xfedc_ba98_7654_3210),
3245 server_version: None,
3246 ..
3247 })
3248 ));
3249 assert!(matches!(
3250 parse_server_msg(&msg[..7]),
3251 Some(ServerMsg::Hello {
3252 boot_generation: None,
3253 server_version: None,
3254 ..
3255 })
3256 ));
3257
3258 assert!(matches!(
3260 parse_server_msg(&msg[..20]),
3261 Some(ServerMsg::Hello {
3262 server_version: None,
3263 ..
3264 })
3265 ));
3266 }
3267
3268 #[test]
3269 fn term_cwd_roundtrip() {
3270 let req = msg_term_cwd(12, 42);
3271 assert_eq!(parse_term_cwd(&req), Some((12, 42)));
3272 let reply = msg_term_cwd_reply(12, "/home/user/src/linux");
3273 assert_eq!(
3274 parse_term_cwd_reply(&reply),
3275 Some((12, "/home/user/src/linux".to_string()))
3276 );
3277 assert_eq!(
3279 parse_term_cwd_reply(&msg_term_cwd_reply(1, "")),
3280 Some((1, String::new()))
3281 );
3282 }
3283
3284 #[test]
3285 fn term_cwd_event_roundtrip() {
3286 let msg = msg_term_cwd_event(0x1234, "/home/user/src/linux");
3287 assert_eq!(msg[0], S2C_TERM_CWD_EVENT);
3289 assert_eq!(&msg[1..3], &[0x34, 0x12]);
3290 assert_eq!(&msg[3..], b"/home/user/src/linux");
3291 assert_eq!(
3292 parse_term_cwd_event(&msg),
3293 Some((0x1234, "/home/user/src/linux".to_string()))
3294 );
3295 assert_eq!(parse_term_cwd_event(&[S2C_TERM_CWD_EVENT, 0]), None);
3297 assert_eq!(parse_term_cwd_event(&msg_term_cwd(1, 2)), None);
3298 }
3299
3300 #[test]
3301 fn update_round_trip_preserves_title_and_cells() {
3302 let style = CellStyle::default();
3303 let mut prev = FrameState::new(2, 8);
3304 prev.set_title("one");
3305 prev.write_text(0, 0, "hello", style);
3306
3307 let mut next = prev.clone();
3308 next.set_title("two");
3309 next.write_text(1, 0, "world", style);
3310
3311 let baseline = build_update_msg(7, &prev, &FrameState::default()).unwrap();
3312 let delta = build_update_msg(7, &next, &prev).unwrap();
3313
3314 let mut term = TerminalState::new(2, 8);
3315 let ServerMsg::Update { payload, .. } = parse_server_msg(&baseline).unwrap() else {
3316 panic!("expected update");
3317 };
3318 assert!(term.feed_compressed(payload));
3319 assert_eq!(term.title(), "one");
3320
3321 let ServerMsg::Update { payload, .. } = parse_server_msg(&delta).unwrap() else {
3322 panic!("expected update");
3323 };
3324 assert!(term.feed_compressed(payload));
3325 assert_eq!(term.title(), "two");
3326 assert_eq!(term.get_all_text(), "hello\nworld");
3327 }
3328
3329 #[test]
3330 fn title_can_be_cleared_via_update() {
3331 let style = CellStyle::default();
3332 let mut prev = FrameState::new(1, 4);
3333 prev.set_title("busy");
3334 prev.write_text(0, 0, "ping", style);
3335
3336 let mut next = prev.clone();
3337 next.set_title("");
3338
3339 let baseline = build_update_msg(1, &prev, &FrameState::default()).unwrap();
3340 let delta = build_update_msg(1, &next, &prev).unwrap();
3341
3342 let mut term = TerminalState::new(1, 4);
3343 let ServerMsg::Update { payload, .. } = parse_server_msg(&baseline).unwrap() else {
3344 panic!("expected update");
3345 };
3346 term.feed_compressed(payload);
3347 let ServerMsg::Update { payload, .. } = parse_server_msg(&delta).unwrap() else {
3348 panic!("expected update");
3349 };
3350 term.feed_compressed(payload);
3351 assert_eq!(term.title(), "");
3352 }
3353
3354 #[test]
3355 fn scroll_heavy_update_can_use_ops_payload() {
3356 let style = CellStyle::default();
3357 let mut prev = FrameState::new(5, 6);
3358 prev.write_text(0, 0, "one", style);
3359 prev.write_text(1, 0, "two", style);
3360 prev.write_text(2, 0, "three", style);
3361 prev.write_text(3, 0, "four", style);
3362 prev.write_text(4, 0, "five", style);
3363
3364 let mut next = FrameState::new(5, 6);
3365 next.write_text(0, 0, "two", style);
3366 next.write_text(1, 0, "three", style);
3367 next.write_text(2, 0, "four", style);
3368 next.write_text(3, 0, "five", style);
3369
3370 let delta = build_update_msg(9, &next, &prev).unwrap();
3371 let ServerMsg::Update { payload, .. } = parse_server_msg(&delta).unwrap() else {
3372 panic!("expected update");
3373 };
3374 let decoded = decompress_size_prepended(payload).unwrap();
3375 let title_field = u16::from_le_bytes([decoded[10], decoded[11]]);
3376 assert_ne!(title_field & OPS_PRESENT, 0);
3377
3378 let mut term = TerminalState::new(5, 6);
3379 let baseline = build_update_msg(9, &prev, &FrameState::default()).unwrap();
3380 let ServerMsg::Update { payload, .. } = parse_server_msg(&baseline).unwrap() else {
3381 panic!("expected update");
3382 };
3383 assert!(term.feed_compressed(payload));
3384 let ServerMsg::Update { payload, .. } = parse_server_msg(&delta).unwrap() else {
3385 panic!("expected update");
3386 };
3387 assert!(term.feed_compressed(payload));
3388 assert_eq!(term.get_all_text(), "two\nthree\nfour\nfive\n");
3389 }
3390
3391 #[test]
3392 fn cooked_scroll_heavy_update_uses_copy_rect_op() {
3393 let style = CellStyle::default();
3394 let mut prev = FrameState::new(5, 6);
3395 prev.set_mode(MODE_ECHO | MODE_ICANON);
3396 prev.write_text(0, 0, "one", style);
3397 prev.write_text(1, 0, "two", style);
3398 prev.write_text(2, 0, "three", style);
3399 prev.write_text(3, 0, "four", style);
3400 prev.write_text(4, 0, "five", style);
3401
3402 let mut next = FrameState::new(5, 6);
3403 next.set_mode(MODE_ECHO | MODE_ICANON);
3404 next.write_text(0, 0, "two", style);
3405 next.write_text(1, 0, "three", style);
3406 next.write_text(2, 0, "four", style);
3407 next.write_text(3, 0, "five", style);
3408
3409 let delta = build_update_msg(9, &next, &prev).unwrap();
3410 let ServerMsg::Update { payload, .. } = parse_server_msg(&delta).unwrap() else {
3411 panic!("expected update");
3412 };
3413 let decoded = decompress_size_prepended(payload).unwrap();
3414 let op_count = u16::from_le_bytes([decoded[12], decoded[13]]);
3415 assert!(op_count >= 1);
3416 assert_eq!(decoded[14], OP_COPY_RECT);
3417 }
3418
3419 #[test]
3420 fn mode_zero_scroll_uses_copy_rect() {
3421 let style = CellStyle::default();
3422 let mut prev = FrameState::new(5, 6);
3423 prev.write_text(0, 0, "one", style);
3424 prev.write_text(1, 0, "two", style);
3425 prev.write_text(2, 0, "three", style);
3426 prev.write_text(3, 0, "four", style);
3427 prev.write_text(4, 0, "five", style);
3428
3429 let mut next = FrameState::new(5, 6);
3430 next.write_text(0, 0, "two", style);
3431 next.write_text(1, 0, "three", style);
3432 next.write_text(2, 0, "four", style);
3433 next.write_text(3, 0, "five", style);
3434
3435 let delta = build_update_msg(9, &next, &prev).unwrap();
3436 let ServerMsg::Update { payload, .. } = parse_server_msg(&delta).unwrap() else {
3437 panic!("expected update");
3438 };
3439 let decoded = decompress_size_prepended(payload).unwrap();
3440 let op_count = u16::from_le_bytes([decoded[12], decoded[13]]);
3441 assert!(op_count >= 1);
3442 assert_eq!(decoded[14], OP_COPY_RECT);
3444
3445 let baseline = build_update_msg(9, &prev, &FrameState::new(5, 6)).unwrap();
3447 let mut state = TerminalState::new(5, 6);
3448 let ServerMsg::Update { payload: bp, .. } = parse_server_msg(&baseline).unwrap() else {
3449 panic!("expected update");
3450 };
3451 state.feed_compressed(bp);
3452 state.feed_compressed(payload);
3453 assert_eq!(state.frame().cells(), next.cells());
3454 }
3455
3456 #[test]
3457 fn callback_renderer_wraps_text() {
3458 let mut renderer = CallbackRenderer::new(2, 8);
3459 renderer.render(|dom| {
3460 dom.wrapped_text(
3461 Rect::new(0, 0, 2, 8),
3462 "alpha beta gamma",
3463 CellStyle::default(),
3464 );
3465 });
3466 assert_eq!(renderer.frame().get_all_text(), "alpha\nbeta");
3467 }
3468
3469 #[test]
3470 fn scrolling_text_shows_tail() {
3471 let mut frame = FrameState::new(3, 8);
3472 frame.write_scrolling_text(
3473 Rect::new(0, 0, 3, 8),
3474 &["one", "two", "three", "four"],
3475 0,
3476 CellStyle::default(),
3477 );
3478 assert_eq!(frame.get_all_text(), "two\nthree\nfour");
3479 }
3480
3481 #[test]
3482 fn search_results_round_trip_with_context() {
3483 let msg = [
3484 vec![S2C_SEARCH_RESULTS],
3485 7u16.to_le_bytes().to_vec(),
3486 1u16.to_le_bytes().to_vec(),
3487 42u16.to_le_bytes().to_vec(),
3488 1234u32.to_le_bytes().to_vec(),
3489 vec![1, 0b111],
3490 9u32.to_le_bytes().to_vec(),
3491 5u16.to_le_bytes().to_vec(),
3492 b"hello".to_vec(),
3493 ]
3494 .concat();
3495
3496 let ServerMsg::SearchResults {
3497 request_id,
3498 results,
3499 } = parse_server_msg(&msg).unwrap()
3500 else {
3501 panic!("expected search results");
3502 };
3503 assert_eq!(request_id, 7);
3504 assert_eq!(results.len(), 1);
3505 assert_eq!(results[0].pty_id, 42);
3506 assert_eq!(results[0].score, 1234);
3507 assert_eq!(results[0].primary_source, 1);
3508 assert_eq!(results[0].matched_sources, 0b111);
3509 assert_eq!(results[0].scroll_offset, Some(9));
3510 assert_eq!(results[0].context, b"hello");
3511 }
3512
3513 #[test]
3516 fn msg_create_no_tag_has_zero_tag_len() {
3517 let msg = msg_create(24, 80);
3518 assert_eq!(msg.len(), 7);
3519 assert_eq!(msg[0], C2S_CREATE);
3520 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 24);
3521 assert_eq!(u16::from_le_bytes([msg[3], msg[4]]), 80);
3522 assert_eq!(u16::from_le_bytes([msg[5], msg[6]]), 0);
3523 }
3524
3525 #[test]
3526 fn msg_create_tagged_encodes_tag() {
3527 let msg = msg_create_tagged(24, 80, "my-pty");
3528 assert_eq!(msg[0], C2S_CREATE);
3529 let tag_len = u16::from_le_bytes([msg[5], msg[6]]) as usize;
3530 assert_eq!(tag_len, 6);
3531 assert_eq!(&msg[7..7 + tag_len], b"my-pty");
3532 assert_eq!(msg.len(), 7 + tag_len);
3533 }
3534
3535 #[test]
3536 fn msg_create_tagged_command_encodes_both() {
3537 let msg = msg_create_tagged_command(30, 120, "editor", "vim");
3538 let tag_len = u16::from_le_bytes([msg[5], msg[6]]) as usize;
3539 assert_eq!(tag_len, 6);
3540 assert_eq!(&msg[7..13], b"editor");
3541 assert_eq!(&msg[13..], b"vim");
3542 }
3543
3544 #[test]
3545 fn msg_create_command_has_empty_tag() {
3546 let msg = msg_create_command(24, 80, "ls");
3547 let tag_len = u16::from_le_bytes([msg[5], msg[6]]) as usize;
3548 assert_eq!(tag_len, 0);
3549 assert_eq!(&msg[7..], b"ls");
3550 }
3551
3552 #[test]
3553 fn msg_create_tagged_empty_tag() {
3554 let msg = msg_create_tagged(24, 80, "");
3555 assert_eq!(msg.len(), 7);
3556 assert_eq!(u16::from_le_bytes([msg[5], msg[6]]), 0);
3557 }
3558
3559 #[test]
3560 fn msg_create_tagged_unicode_tag() {
3561 let msg = msg_create_tagged(24, 80, "日本語");
3562 let tag_len = u16::from_le_bytes([msg[5], msg[6]]) as usize;
3563 assert_eq!(tag_len, "日本語".len());
3564 assert_eq!(std::str::from_utf8(&msg[7..7 + tag_len]).unwrap(), "日本語");
3565 }
3566
3567 #[test]
3568 fn parse_created_with_tag() {
3569 let mut wire = vec![S2C_CREATED, 0x05, 0x00];
3570 wire.extend_from_slice(b"hello");
3571 let msg = parse_server_msg(&wire).unwrap();
3572 match msg {
3573 ServerMsg::Created { pty_id, tag } => {
3574 assert_eq!(pty_id, 5);
3575 assert_eq!(tag, "hello");
3576 }
3577 _ => panic!("expected Created"),
3578 }
3579 }
3580
3581 #[test]
3582 fn parse_created_without_tag() {
3583 let wire = vec![S2C_CREATED, 0x03, 0x00];
3584 let msg = parse_server_msg(&wire).unwrap();
3585 match msg {
3586 ServerMsg::Created { pty_id, tag } => {
3587 assert_eq!(pty_id, 3);
3588 assert_eq!(tag, "");
3589 }
3590 _ => panic!("expected Created"),
3591 }
3592 }
3593
3594 #[test]
3595 fn parse_created_n_with_tag() {
3596 let mut wire = vec![S2C_CREATED_N, 0x2a, 0x00, 0x05, 0x00];
3597 wire.extend_from_slice(b"hello");
3598 let msg = parse_server_msg(&wire).unwrap();
3599 match msg {
3600 ServerMsg::CreatedN { nonce, pty_id, tag } => {
3601 assert_eq!(nonce, 42);
3602 assert_eq!(pty_id, 5);
3603 assert_eq!(tag, "hello");
3604 }
3605 _ => panic!("expected CreatedN"),
3606 }
3607 }
3608
3609 #[test]
3610 fn msg_create_n_format() {
3611 let msg = msg_create_n(42, 24, 80, "test");
3612 assert_eq!(msg[0], C2S_CREATE_N);
3613 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 42);
3614 assert_eq!(u16::from_le_bytes([msg[3], msg[4]]), 24);
3615 assert_eq!(u16::from_le_bytes([msg[5], msg[6]]), 80);
3616 assert_eq!(u16::from_le_bytes([msg[7], msg[8]]), 4);
3617 assert_eq!(&msg[9..], b"test");
3618 }
3619
3620 #[test]
3621 fn msg_create_n_command_format() {
3622 let msg = msg_create_n_command(7, 30, 120, "bg", "make build");
3623 assert_eq!(msg[0], C2S_CREATE_N);
3624 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 7);
3625 assert_eq!(u16::from_le_bytes([msg[3], msg[4]]), 30);
3626 assert_eq!(u16::from_le_bytes([msg[5], msg[6]]), 120);
3627 let tag_len = u16::from_le_bytes([msg[7], msg[8]]) as usize;
3628 assert_eq!(tag_len, 2);
3629 assert_eq!(&msg[9..9 + tag_len], b"bg");
3630 assert_eq!(&msg[9 + tag_len..], b"make build");
3631 }
3632
3633 #[test]
3634 fn parse_list_with_tags() {
3635 let mut wire = vec![S2C_LIST, 0x02, 0x00];
3637 wire.extend_from_slice(&1u16.to_le_bytes());
3639 wire.extend_from_slice(&2u16.to_le_bytes());
3640 wire.extend_from_slice(b"ab");
3641 wire.extend_from_slice(&0u16.to_le_bytes());
3642 wire.extend_from_slice(&2u16.to_le_bytes());
3644 wire.extend_from_slice(&0u16.to_le_bytes());
3645 wire.extend_from_slice(&0u16.to_le_bytes());
3646
3647 let msg = parse_server_msg(&wire).unwrap();
3648 match msg {
3649 ServerMsg::List { entries } => {
3650 assert_eq!(entries.len(), 2);
3651 assert_eq!(entries[0].pty_id, 1);
3652 assert_eq!(entries[0].tag, "ab");
3653 assert_eq!(entries[1].pty_id, 2);
3654 assert_eq!(entries[1].tag, "");
3655 }
3656 _ => panic!("expected List"),
3657 }
3658 }
3659
3660 #[test]
3661 fn parse_list_empty() {
3662 let wire = vec![S2C_LIST, 0x00, 0x00];
3663 let msg = parse_server_msg(&wire).unwrap();
3664 match msg {
3665 ServerMsg::List { entries } => assert_eq!(entries.len(), 0),
3666 _ => panic!("expected List"),
3667 }
3668 }
3669
3670 #[test]
3671 fn parse_list_truncated_gracefully() {
3672 let mut wire = vec![S2C_LIST, 0x02, 0x00];
3674 wire.extend_from_slice(&1u16.to_le_bytes());
3675 wire.extend_from_slice(&0u16.to_le_bytes());
3676 let msg = parse_server_msg(&wire).unwrap();
3678 match msg {
3679 ServerMsg::List { entries } => assert_eq!(entries.len(), 1),
3680 _ => panic!("expected List"),
3681 }
3682 }
3683
3684 #[test]
3685 fn parse_list_with_long_tags() {
3686 let long_tag = "a".repeat(300);
3687 let mut wire = vec![S2C_LIST, 0x01, 0x00];
3688 wire.extend_from_slice(&42u16.to_le_bytes());
3689 wire.extend_from_slice(&(long_tag.len() as u16).to_le_bytes());
3690 wire.extend_from_slice(long_tag.as_bytes());
3691
3692 let msg = parse_server_msg(&wire).unwrap();
3693 match msg {
3694 ServerMsg::List { entries } => {
3695 assert_eq!(entries.len(), 1);
3696 assert_eq!(entries[0].pty_id, 42);
3697 assert_eq!(entries[0].tag, long_tag);
3698 }
3699 _ => panic!("expected List"),
3700 }
3701 }
3702
3703 #[test]
3704 fn create_and_created_tag_round_trip() {
3705 let create_msg = msg_create_tagged(24, 80, "my-session");
3707 let tag_len = u16::from_le_bytes([create_msg[5], create_msg[6]]) as usize;
3708 let tag = std::str::from_utf8(&create_msg[7..7 + tag_len]).unwrap();
3709
3710 let mut created_wire = vec![S2C_CREATED, 0x07, 0x00]; created_wire.extend_from_slice(tag.as_bytes());
3713
3714 let msg = parse_server_msg(&created_wire).unwrap();
3715 match msg {
3716 ServerMsg::Created {
3717 pty_id,
3718 tag: parsed_tag,
3719 } => {
3720 assert_eq!(pty_id, 7);
3721 assert_eq!(parsed_tag, "my-session");
3722 }
3723 _ => panic!("expected Created"),
3724 }
3725 }
3726
3727 #[test]
3730 fn frame_state_accessors() {
3731 let mut f = FrameState::new(4, 10);
3732 assert_eq!(f.rows(), 4);
3733 assert_eq!(f.cols(), 10);
3734 assert_eq!(f.cursor_row(), 0);
3735 assert_eq!(f.cursor_col(), 0);
3736 assert_eq!(f.mode(), 0);
3737 assert_eq!(f.title(), "");
3738 assert_eq!(f.cells().len(), 4 * 10 * CELL_SIZE);
3739 assert_eq!(f.cells_mut().len(), 4 * 10 * CELL_SIZE);
3740 assert!(f.overflow().is_empty());
3741 assert!(f.overflow_mut().is_empty());
3742 }
3743
3744 #[test]
3745 fn frame_state_from_parts() {
3746 let cells = vec![0u8; 2 * 4 * CELL_SIZE];
3747 let f = FrameState::from_parts(2, 4, 1, 3, 0x0F, "hello", cells.clone());
3748 assert_eq!(f.rows(), 2);
3749 assert_eq!(f.cols(), 4);
3750 assert_eq!(f.cursor_row(), 1);
3751 assert_eq!(f.cursor_col(), 3);
3752 assert_eq!(f.mode(), 0x0F);
3753 assert_eq!(f.title(), "hello");
3754 assert_eq!(f.cells(), &cells[..]);
3755 }
3756
3757 #[test]
3758 fn frame_state_from_parts_wrong_size() {
3759 let cells = vec![0u8; 10]; let f = FrameState::from_parts(2, 4, 0, 0, 0, "", cells);
3762 assert_eq!(f.cells().len(), 2 * 4 * CELL_SIZE);
3763 }
3764
3765 #[test]
3766 fn frame_state_resize() {
3767 let mut f = FrameState::new(4, 10);
3768 f.set_cursor(3, 9);
3769 f.resize(2, 5);
3770 assert_eq!(f.rows(), 2);
3771 assert_eq!(f.cols(), 5);
3772 assert_eq!(f.cursor_row(), 1); assert_eq!(f.cursor_col(), 4); assert_eq!(f.cells().len(), 2 * 5 * CELL_SIZE);
3775 }
3776
3777 #[test]
3778 fn frame_state_resize_noop() {
3779 let mut f = FrameState::new(4, 10);
3780 let ptr_before = f.cells().as_ptr();
3781 f.resize(4, 10); let ptr_after = f.cells().as_ptr();
3783 assert_eq!(ptr_before, ptr_after); }
3785
3786 #[test]
3787 fn frame_state_set_cursor_clamps() {
3788 let mut f = FrameState::new(4, 10);
3789 f.set_cursor(100, 200);
3790 assert_eq!(f.cursor_row(), 3);
3791 assert_eq!(f.cursor_col(), 9);
3792 }
3793
3794 #[test]
3795 fn frame_state_set_title() {
3796 let mut f = FrameState::new(2, 2);
3797 assert!(f.set_title("new title"));
3798 assert_eq!(f.title(), "new title");
3799 assert!(!f.set_title("new title")); assert!(f.set_title("other"));
3801 }
3802
3803 #[test]
3804 fn frame_state_get_text_and_write_text() {
3805 let mut f = FrameState::new(2, 10);
3806 f.write_text(0, 0, "Hello", CellStyle::default());
3807 f.write_text(1, 0, "World", CellStyle::default());
3808 let text = f.get_text(0, 0, 1, 9);
3809 assert!(text.contains("Hello"));
3810 assert!(text.contains("World"));
3811 let all = f.get_all_text();
3812 assert!(all.contains("Hello"));
3813 }
3814
3815 #[test]
3816 fn frame_state_get_text_empty() {
3817 let f = FrameState::new(0, 0);
3818 assert_eq!(f.get_text(0, 0, 0, 0), "");
3819 assert_eq!(f.get_all_text(), "");
3820 }
3821
3822 #[test]
3823 fn frame_state_get_cell() {
3824 let f = FrameState::new(2, 4);
3825 let cell = f.get_cell(0, 0);
3826 assert_eq!(cell.len(), CELL_SIZE);
3827 assert!(f.get_cell(100, 100).is_empty());
3829 }
3830
3831 #[test]
3832 fn frame_state_cell_content_blank() {
3833 let f = FrameState::new(2, 4);
3834 assert_eq!(f.cell_content(0, 0), " "); assert_eq!(f.cell_content(100, 0), ""); }
3837
3838 #[test]
3839 fn frame_state_cell_content_with_text() {
3840 let mut f = FrameState::new(2, 10);
3841 f.write_text(0, 0, "A", CellStyle::default());
3842 assert_eq!(f.cell_content(0, 0), "A");
3843 }
3844
3845 #[test]
3846 fn frame_state_fill_rect() {
3847 let mut f = FrameState::new(4, 10);
3848 f.fill_rect(Rect::new(0, 0, 2, 5), 'X', CellStyle::default());
3849 assert_eq!(f.cell_content(0, 0), "X");
3850 assert_eq!(f.cell_content(1, 4), "X");
3851 assert_eq!(f.cell_content(2, 0), " "); }
3853
3854 #[test]
3855 fn frame_state_wrapped_text() {
3856 let mut f = FrameState::new(4, 10);
3857 let lines =
3858 f.write_wrapped_text(Rect::new(0, 0, 4, 5), "hello world", CellStyle::default());
3859 assert!(lines >= 2); }
3861
3862 #[test]
3863 fn frame_state_wrapped_text_empty_rect() {
3864 let mut f = FrameState::new(4, 10);
3865 assert_eq!(
3866 f.write_wrapped_text(Rect::new(0, 0, 0, 0), "hi", CellStyle::default()),
3867 0
3868 );
3869 }
3870
3871 #[test]
3872 fn frame_state_scrolling_text() {
3873 let mut f = FrameState::new(4, 10);
3874 f.write_scrolling_text(
3875 Rect::new(0, 0, 3, 10),
3876 &["line1", "line2", "line3", "line4"],
3877 0,
3878 CellStyle::default(),
3879 );
3880 assert_eq!(f.cell_content(0, 0), "l"); }
3883
3884 #[test]
3885 fn frame_state_scrolling_text_empty_rect() {
3886 let mut f = FrameState::new(4, 10);
3887 f.write_scrolling_text(Rect::new(0, 0, 0, 0), &["hi"], 0, CellStyle::default());
3888 }
3890
3891 #[test]
3892 fn frame_state_clear() {
3893 let mut f = FrameState::new(2, 4);
3894 f.write_text(0, 0, "AB", CellStyle::default());
3895 f.clear(CellStyle::default());
3896 assert_eq!(f.cell_content(0, 0), " ");
3897 }
3898
3899 #[test]
3902 fn terminal_state_accessors() {
3903 let t = TerminalState::new(24, 80);
3904 assert_eq!(t.rows(), 24);
3905 assert_eq!(t.cols(), 80);
3906 assert_eq!(t.cursor_row(), 0);
3907 assert_eq!(t.cursor_col(), 0);
3908 assert_eq!(t.mode(), 0);
3909 assert_eq!(t.title(), "");
3910 assert_eq!(t.cells().len(), 24 * 80 * CELL_SIZE);
3911 assert_eq!(t.frame().rows(), 24);
3912 }
3913
3914 #[test]
3915 fn terminal_state_mutators() {
3916 let mut t = TerminalState::new(4, 10);
3917 t.frame_mut().set_title("test");
3918 assert_eq!(t.title(), "test");
3919 }
3920
3921 #[test]
3922 fn terminal_state_set_title() {
3923 let mut t = TerminalState::new(4, 10);
3924 assert!(t.frame_mut().set_title("hello"));
3925 assert_eq!(t.title(), "hello");
3926 assert!(!t.frame_mut().set_title("hello")); }
3928
3929 #[test]
3930 fn terminal_state_get_text() {
3931 let t = TerminalState::new(2, 10);
3932 let text = t.get_text(0, 0, 0, 9);
3933 assert!(text.is_empty() || text.chars().all(|c| c == ' ' || c == '\n'));
3934 assert!(t.get_cell(0, 0).len() == CELL_SIZE);
3935 assert!(t.get_cell(100, 100).is_empty());
3936 }
3937
3938 #[test]
3939 fn terminal_state_resize() {
3940 let mut t = TerminalState::new(4, 10);
3941 t.frame_mut().resize(2, 5);
3942 assert_eq!(t.rows(), 2);
3945 assert_eq!(t.cols(), 5);
3946 }
3947
3948 #[test]
3949 fn terminal_state_feed_compressed_invalid() {
3950 let mut t = TerminalState::new(4, 10);
3951 assert!(!t.feed_compressed(b"garbage"));
3952 assert!(!t.feed_compressed(&[]));
3953 }
3954
3955 #[test]
3956 fn terminal_state_feed_compressed_batch_empty() {
3957 let mut t = TerminalState::new(4, 10);
3958 assert!(!t.feed_compressed_batch(&[]));
3959 }
3960
3961 #[test]
3962 fn terminal_state_feed_compressed_batch_truncated() {
3963 let mut t = TerminalState::new(4, 10);
3964 let batch = &[100, 0, 0, 0];
3966 assert!(!t.feed_compressed_batch(batch));
3967 }
3968
3969 #[test]
3972 fn msg_input_format() {
3973 let msg = msg_input(5, b"hello");
3974 assert_eq!(msg[0], C2S_INPUT);
3975 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 5);
3976 assert_eq!(&msg[3..], b"hello");
3977 }
3978
3979 #[test]
3980 fn msg_resize_format() {
3981 let msg = msg_resize(3, 24, 80);
3982 assert_eq!(msg[0], C2S_RESIZE);
3983 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 3);
3984 assert_eq!(u16::from_le_bytes([msg[3], msg[4]]), 24);
3985 assert_eq!(u16::from_le_bytes([msg[5], msg[6]]), 80);
3986 }
3987
3988 #[test]
3989 fn msg_resize_batch_format() {
3990 let msg = msg_resize_batch(&[(3, 24, 80), (5, 40, 120)]);
3991 assert_eq!(msg[0], C2S_RESIZE);
3992 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 3);
3993 assert_eq!(u16::from_le_bytes([msg[3], msg[4]]), 24);
3994 assert_eq!(u16::from_le_bytes([msg[5], msg[6]]), 80);
3995 assert_eq!(u16::from_le_bytes([msg[7], msg[8]]), 5);
3996 assert_eq!(u16::from_le_bytes([msg[9], msg[10]]), 40);
3997 assert_eq!(u16::from_le_bytes([msg[11], msg[12]]), 120);
3998 }
3999
4000 #[test]
4001 fn msg_focus_format() {
4002 let msg = msg_focus(7);
4003 assert_eq!(msg[0], C2S_FOCUS);
4004 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 7);
4005 assert_eq!(msg.len(), 3);
4006 }
4007
4008 #[test]
4009 fn msg_close_format() {
4010 let msg = msg_close(9);
4011 assert_eq!(msg[0], C2S_CLOSE);
4012 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 9);
4013 }
4014
4015 #[test]
4016 fn msg_subscribe_unsubscribe_format() {
4017 let sub = msg_subscribe(1);
4018 assert_eq!(sub[0], C2S_SUBSCRIBE);
4019 assert_eq!(u16::from_le_bytes([sub[1], sub[2]]), 1);
4020
4021 let unsub = msg_unsubscribe(2);
4022 assert_eq!(unsub[0], C2S_UNSUBSCRIBE);
4023 assert_eq!(u16::from_le_bytes([unsub[1], unsub[2]]), 2);
4024 }
4025
4026 #[test]
4027 fn msg_search_format() {
4028 let msg = msg_search(42, "test query");
4029 assert_eq!(msg[0], C2S_SEARCH);
4030 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 42);
4031 assert_eq!(&msg[3..], b"test query");
4032 }
4033
4034 #[test]
4035 fn msg_ack_format() {
4036 let msg = msg_ack();
4037 assert_eq!(msg, vec![C2S_ACK]);
4038 }
4039
4040 #[test]
4041 fn msg_scroll_format() {
4042 let msg = msg_scroll(5, 1000);
4043 assert_eq!(msg[0], C2S_SCROLL);
4044 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 5);
4045 assert_eq!(u32::from_le_bytes([msg[3], msg[4], msg[5], msg[6]]), 1000);
4046 }
4047
4048 #[test]
4049 fn msg_display_rate_format() {
4050 let msg = msg_display_rate(120);
4051 assert_eq!(msg[0], C2S_DISPLAY_RATE);
4052 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 120);
4053 }
4054
4055 #[test]
4056 fn msg_client_metrics_format() {
4057 let msg = msg_client_metrics(3, 5, 100);
4058 assert_eq!(msg[0], C2S_CLIENT_METRICS);
4059 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 3);
4060 assert_eq!(u16::from_le_bytes([msg[3], msg[4]]), 5);
4061 assert_eq!(u16::from_le_bytes([msg[5], msg[6]]), 100);
4062 }
4063
4064 #[test]
4067 fn callback_renderer_resize() {
4068 let mut r = CallbackRenderer::new(2, 8);
4069 assert_eq!(r.frame().rows(), 2);
4070 r.resize(4, 16);
4071 assert_eq!(r.frame().rows(), 4);
4072 assert_eq!(r.frame().cols(), 16);
4073 }
4074
4075 #[test]
4076 fn callback_renderer_fill() {
4077 let mut r = CallbackRenderer::new(4, 10);
4078 r.render(|dom| {
4079 dom.fill(Rect::new(0, 0, 2, 5), '#', CellStyle::default());
4080 });
4081 assert_eq!(r.frame().cell_content(0, 0), "#");
4082 assert_eq!(r.frame().cell_content(1, 4), "#");
4083 }
4084
4085 #[test]
4086 fn callback_renderer_text() {
4087 let mut r = CallbackRenderer::new(4, 20);
4088 r.render(|dom| {
4089 dom.text(0, 0, "Hello", CellStyle::default());
4090 });
4091 assert_eq!(r.frame().cell_content(0, 0), "H");
4092 assert_eq!(r.frame().cell_content(0, 4), "o");
4093 }
4094
4095 #[test]
4096 fn callback_renderer_set_title() {
4097 let mut r = CallbackRenderer::new(2, 8);
4098 r.render(|dom| {
4099 dom.set_title("my title");
4100 });
4101 assert_eq!(r.frame().title(), "my title");
4102 }
4103
4104 #[test]
4105 fn callback_renderer_set_background() {
4106 let mut r = CallbackRenderer::new(2, 4);
4107 let style = CellStyle {
4108 bg: Color::Rgb(255, 0, 0),
4109 ..CellStyle::default()
4110 };
4111 r.render(|dom| {
4112 dom.set_background(style);
4113 });
4114 assert_eq!(r.frame().cells().len(), 2 * 4 * CELL_SIZE);
4116 }
4117
4118 #[test]
4119 fn callback_renderer_scrolling_text() {
4120 let mut r = CallbackRenderer::new(4, 20);
4121 r.render(|dom| {
4122 dom.scrolling_text(
4123 Rect::new(0, 0, 3, 20),
4124 ["a", "b", "c", "d", "e"].map(String::from),
4125 0,
4126 CellStyle::default(),
4127 );
4128 });
4129 assert_eq!(r.frame().cell_content(0, 0), "c");
4131 }
4132
4133 #[test]
4136 fn parse_empty_returns_none() {
4137 assert!(parse_server_msg(&[]).is_none());
4138 }
4139
4140 #[test]
4141 fn parse_unknown_type_returns_none() {
4142 assert!(parse_server_msg(&[0xFF, 0x00, 0x00]).is_none());
4143 }
4144
4145 #[test]
4146 fn parse_update_too_short() {
4147 assert!(parse_server_msg(&[S2C_UPDATE, 0x00]).is_none());
4148 }
4149
4150 #[test]
4151 fn parse_closed() {
4152 let msg = parse_server_msg(&[S2C_CLOSED, 0x05, 0x00]).unwrap();
4153 match msg {
4154 ServerMsg::Closed { pty_id } => assert_eq!(pty_id, 5),
4155 _ => panic!("expected Closed"),
4156 }
4157 }
4158
4159 #[test]
4160 fn parse_title() {
4161 let mut wire = vec![S2C_TITLE, 0x01, 0x00];
4162 wire.extend_from_slice(b"mytitle");
4163 let msg = parse_server_msg(&wire).unwrap();
4164 match msg {
4165 ServerMsg::Title { pty_id, title } => {
4166 assert_eq!(pty_id, 1);
4167 assert_eq!(title, b"mytitle");
4168 }
4169 _ => panic!("expected Title"),
4170 }
4171 }
4172
4173 #[test]
4176 fn build_update_msg_round_trip_with_resize() {
4177 let style = CellStyle::default();
4178 let mut prev = FrameState::new(2, 4);
4179 prev.write_text(0, 0, "AB", style);
4180
4181 let mut next = FrameState::new(3, 5); next.write_text(0, 0, "XY", style);
4183 next.set_title("resized");
4184
4185 let msg = build_update_msg(1, &next, &prev).unwrap();
4186 assert!(!msg.is_empty());
4187
4188 let mut t = TerminalState::new(2, 4);
4190 assert!(t.feed_compressed(&msg[3..])); assert_eq!(t.rows(), 3);
4192 assert_eq!(t.cols(), 5);
4193 assert_eq!(t.title(), "resized");
4194 }
4195
4196 #[test]
4201 fn keyframe_clears_stale_client_grid() {
4202 let style = CellStyle::default();
4203
4204 let mut t = TerminalState::new(2, 8);
4207 t.frame_mut().write_text(0, 0, "GARBAGE!", style);
4208 t.frame_mut().write_text(1, 0, "LEFTOVER", style);
4209 t.frame_mut().set_title("stale");
4210 t.frame_mut().line_flags[0] = ROW_FLAG_WRAPPED;
4211
4212 let mut cur = FrameState::new(2, 8);
4214 cur.write_text(0, 0, "ok", style);
4215
4216 let msg = build_update_msg(1, &cur, &FrameState::default()).unwrap();
4217 assert!(t.feed_compressed(&msg[3..]));
4218 assert_eq!(t.frame().cells(), cur.cells());
4219 assert_eq!(t.title(), "");
4220 assert!(!t.is_wrapped(0));
4221 }
4222
4223 #[test]
4226 fn keyframe_emitted_for_blank_frame() {
4227 let style = CellStyle::default();
4228 let mut t = TerminalState::new(2, 8);
4229 t.frame_mut().write_text(0, 0, "GARBAGE!", style);
4230
4231 let cur = FrameState::new(2, 8);
4232 let msg = build_update_msg(1, &cur, &FrameState::default())
4233 .expect("blank keyframe must still be sent");
4234 assert!(t.feed_compressed(&msg[3..]));
4235 assert_eq!(t.frame().cells(), cur.cells());
4236 }
4237
4238 #[test]
4242 fn keyframe_leads_with_whole_grid_fill() {
4243 let style = CellStyle::default();
4244 let mut cur = FrameState::new(3, 5);
4245 cur.write_text(0, 0, "hi", style);
4246
4247 let msg = build_update_msg(1, &cur, &FrameState::default()).unwrap();
4248 let ServerMsg::Update { payload, .. } = parse_server_msg(&msg).unwrap() else {
4249 panic!("expected update");
4250 };
4251 let decoded = decompress_size_prepended(payload).unwrap();
4252 let op_count = u16::from_le_bytes([decoded[12], decoded[13]]);
4253 assert_eq!(op_count, 2, "FILL_RECT + PATCH_CELLS");
4254 assert_eq!(decoded[14], OP_FILL_RECT);
4255 let row = u16::from_le_bytes([decoded[15], decoded[16]]);
4256 let col = u16::from_le_bytes([decoded[17], decoded[18]]);
4257 let rows = u16::from_le_bytes([decoded[19], decoded[20]]);
4258 let cols = u16::from_le_bytes([decoded[21], decoded[22]]);
4259 assert_eq!((row, col, rows, cols), (0, 0, 3, 5));
4260 assert_eq!(&decoded[23..23 + CELL_SIZE], &[0u8; CELL_SIZE]);
4261 assert_eq!(decoded[23 + CELL_SIZE], OP_PATCH_CELLS);
4262 }
4263
4264 #[test]
4265 fn build_update_msg_cursor_change() {
4266 let mut prev = FrameState::new(4, 10);
4267 prev.set_cursor(0, 0);
4268
4269 let mut next = prev.clone();
4270 next.set_cursor(2, 5);
4271
4272 let msg = build_update_msg(0, &next, &prev).unwrap();
4273
4274 let mut t = TerminalState::new(4, 10);
4275 assert!(t.feed_compressed(&msg[3..]));
4276 assert_eq!(t.cursor_row(), 2);
4277 assert_eq!(t.cursor_col(), 5);
4278 }
4279
4280 #[test]
4281 fn build_update_msg_mode_change() {
4282 let prev = FrameState::new(2, 4);
4283 let mut next = prev.clone();
4284 next.set_mode(0x0F);
4285
4286 let msg = build_update_msg(0, &next, &prev).unwrap();
4287 let mut t = TerminalState::new(2, 4);
4288 assert!(t.feed_compressed(&msg[3..]));
4289 assert_eq!(t.mode(), 0x0F);
4290 }
4291
4292 #[test]
4293 fn feed_compressed_batch_multiple_frames() {
4294 let style = CellStyle::default();
4295 let prev = FrameState::new(2, 4);
4296
4297 let mut mid = prev.clone();
4298 mid.write_text(0, 0, "AB", style);
4299 let msg1 = build_update_msg(0, &mid, &prev).unwrap();
4300
4301 let mut next = mid.clone();
4302 next.write_text(1, 0, "CD", style);
4303 let msg2 = build_update_msg(0, &next, &mid).unwrap();
4304
4305 let payload1 = &msg1[3..];
4307 let payload2 = &msg2[3..];
4308 let mut batch = Vec::new();
4309 batch.extend_from_slice(&(payload1.len() as u32).to_le_bytes());
4310 batch.extend_from_slice(payload1);
4311 batch.extend_from_slice(&(payload2.len() as u32).to_le_bytes());
4312 batch.extend_from_slice(payload2);
4313
4314 let mut t = TerminalState::new(2, 4);
4315 assert!(t.feed_compressed_batch(&batch));
4316 let text = t.get_all_text();
4317 assert!(text.contains("AB"));
4318 assert!(text.contains("CD"));
4319 }
4320}