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