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