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