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_mouse(pty_id: u16, type_: u8, button: u8, col: u16, row: u16) -> Vec<u8> {
1997 let mut msg = Vec::with_capacity(9);
1998 msg.push(C2S_MOUSE);
1999 msg.extend_from_slice(&pty_id.to_le_bytes());
2000 msg.push(type_);
2001 msg.push(button);
2002 msg.extend_from_slice(&col.to_le_bytes());
2003 msg.extend_from_slice(&row.to_le_bytes());
2004 msg
2005}
2006
2007pub fn msg_resize(pty_id: u16, rows: u16, cols: u16) -> Vec<u8> {
2008 let mut msg = Vec::with_capacity(7);
2009 msg.push(C2S_RESIZE);
2010 msg.extend_from_slice(&pty_id.to_le_bytes());
2011 msg.extend_from_slice(&rows.to_le_bytes());
2012 msg.extend_from_slice(&cols.to_le_bytes());
2013 msg
2014}
2015
2016pub fn msg_resize_batch(entries: &[(u16, u16, u16)]) -> Vec<u8> {
2017 let mut msg = Vec::with_capacity(1 + entries.len() * 6);
2018 msg.push(C2S_RESIZE);
2019 for &(pty_id, rows, cols) in entries {
2020 msg.extend_from_slice(&pty_id.to_le_bytes());
2021 msg.extend_from_slice(&rows.to_le_bytes());
2022 msg.extend_from_slice(&cols.to_le_bytes());
2023 }
2024 msg
2025}
2026
2027pub fn msg_focus(pty_id: u16) -> Vec<u8> {
2028 let mut msg = Vec::with_capacity(3);
2029 msg.push(C2S_FOCUS);
2030 msg.extend_from_slice(&pty_id.to_le_bytes());
2031 msg
2032}
2033
2034pub fn msg_close(pty_id: u16) -> Vec<u8> {
2035 let mut msg = Vec::with_capacity(3);
2036 msg.push(C2S_CLOSE);
2037 msg.extend_from_slice(&pty_id.to_le_bytes());
2038 msg
2039}
2040
2041pub fn msg_kill(pty_id: u16, signal: i32) -> Vec<u8> {
2042 let mut msg = Vec::with_capacity(7);
2043 msg.push(C2S_KILL);
2044 msg.extend_from_slice(&pty_id.to_le_bytes());
2045 msg.extend_from_slice(&signal.to_le_bytes());
2046 msg
2047}
2048
2049pub fn msg_restart(pty_id: u16) -> Vec<u8> {
2050 let mut msg = Vec::with_capacity(3);
2051 msg.push(C2S_RESTART);
2052 msg.extend_from_slice(&pty_id.to_le_bytes());
2053 msg
2054}
2055
2056pub fn msg_subscribe(pty_id: u16) -> Vec<u8> {
2057 let mut msg = Vec::with_capacity(3);
2058 msg.push(C2S_SUBSCRIBE);
2059 msg.extend_from_slice(&pty_id.to_le_bytes());
2060 msg
2061}
2062
2063pub fn msg_unsubscribe(pty_id: u16) -> Vec<u8> {
2064 let mut msg = Vec::with_capacity(3);
2065 msg.push(C2S_UNSUBSCRIBE);
2066 msg.extend_from_slice(&pty_id.to_le_bytes());
2067 msg
2068}
2069
2070pub fn msg_search(request_id: u16, query: &str) -> Vec<u8> {
2071 let query = query.as_bytes();
2072 let mut msg = Vec::with_capacity(3 + query.len());
2073 msg.push(C2S_SEARCH);
2074 msg.extend_from_slice(&request_id.to_le_bytes());
2075 msg.extend_from_slice(query);
2076 msg
2077}
2078
2079pub fn msg_ack() -> Vec<u8> {
2080 vec![C2S_ACK]
2081}
2082
2083pub fn msg_scroll(pty_id: u16, offset: u32) -> Vec<u8> {
2084 let mut msg = Vec::with_capacity(7);
2085 msg.push(C2S_SCROLL);
2086 msg.extend_from_slice(&pty_id.to_le_bytes());
2087 msg.extend_from_slice(&offset.to_le_bytes());
2088 msg
2089}
2090
2091pub fn msg_display_rate(fps: u16) -> Vec<u8> {
2092 let mut msg = Vec::with_capacity(3);
2093 msg.push(C2S_DISPLAY_RATE);
2094 msg.extend_from_slice(&fps.to_le_bytes());
2095 msg
2096}
2097
2098pub fn msg_client_metrics(backlog: u16, ack_ahead: u16, apply_ms_x10: u16) -> Vec<u8> {
2099 let mut msg = Vec::with_capacity(7);
2100 msg.push(C2S_CLIENT_METRICS);
2101 msg.extend_from_slice(&backlog.to_le_bytes());
2102 msg.extend_from_slice(&ack_ahead.to_le_bytes());
2103 msg.extend_from_slice(&apply_ms_x10.to_le_bytes());
2104 msg
2105}
2106
2107pub fn msg_read(nonce: u16, pty_id: u16, offset: u32, limit: u32, flags: u8) -> Vec<u8> {
2108 let mut msg = Vec::with_capacity(14);
2109 msg.push(C2S_READ);
2110 msg.extend_from_slice(&nonce.to_le_bytes());
2111 msg.extend_from_slice(&pty_id.to_le_bytes());
2112 msg.extend_from_slice(&offset.to_le_bytes());
2113 msg.extend_from_slice(&limit.to_le_bytes());
2114 msg.push(flags);
2115 msg
2116}
2117
2118pub fn msg_copy_range(
2119 nonce: u16,
2120 pty_id: u16,
2121 start_tail: u32,
2122 start_col: u16,
2123 end_tail: u32,
2124 end_col: u16,
2125 flags: u8,
2126) -> Vec<u8> {
2127 let mut msg = Vec::with_capacity(18);
2128 msg.push(C2S_COPY_RANGE);
2129 msg.extend_from_slice(&nonce.to_le_bytes());
2130 msg.extend_from_slice(&pty_id.to_le_bytes());
2131 msg.extend_from_slice(&start_tail.to_le_bytes());
2132 msg.extend_from_slice(&start_col.to_le_bytes());
2133 msg.extend_from_slice(&end_tail.to_le_bytes());
2134 msg.extend_from_slice(&end_col.to_le_bytes());
2135 msg.push(flags);
2136 msg
2137}
2138
2139pub fn msg_exited(pty_id: u16, exit_status: i32) -> Vec<u8> {
2140 let mut msg = Vec::with_capacity(7);
2141 msg.push(S2C_EXITED);
2142 msg.extend_from_slice(&pty_id.to_le_bytes());
2143 msg.extend_from_slice(&exit_status.to_le_bytes());
2144 msg
2145}
2146
2147pub fn msg_quit() -> Vec<u8> {
2149 vec![C2S_QUIT]
2150}
2151
2152pub fn msg_s2c_quit() -> Vec<u8> {
2154 vec![S2C_QUIT]
2155}
2156
2157pub fn msg_surface_created(
2158 surface_id: u16,
2159 parent_id: u16,
2160 width: u16,
2161 height: u16,
2162 title: &str,
2163 app_id: &str,
2164) -> Vec<u8> {
2165 let title_bytes = title.as_bytes();
2166 let app_id_bytes = app_id.as_bytes();
2167 let mut msg = Vec::with_capacity(13 + title_bytes.len() + app_id_bytes.len());
2168 msg.push(S2C_SURFACE_CREATED);
2169 msg.extend_from_slice(&surface_id.to_le_bytes());
2170 msg.extend_from_slice(&parent_id.to_le_bytes());
2171 msg.extend_from_slice(&width.to_le_bytes());
2172 msg.extend_from_slice(&height.to_le_bytes());
2173 msg.extend_from_slice(&(title_bytes.len() as u16).to_le_bytes());
2174 msg.extend_from_slice(title_bytes);
2175 msg.extend_from_slice(&(app_id_bytes.len() as u16).to_le_bytes());
2176 msg.extend_from_slice(app_id_bytes);
2177 msg
2178}
2179
2180pub fn msg_surface_destroyed(surface_id: u16) -> Vec<u8> {
2181 let mut msg = Vec::with_capacity(3);
2182 msg.push(S2C_SURFACE_DESTROYED);
2183 msg.extend_from_slice(&surface_id.to_le_bytes());
2184 msg
2185}
2186
2187pub fn msg_surface_frame(
2188 surface_id: u16,
2189 timestamp: u32,
2190 flags: u8,
2191 width: u16,
2192 height: u16,
2193 data: &[u8],
2194) -> Vec<u8> {
2195 let mut msg = Vec::with_capacity(12 + data.len());
2196 msg.push(S2C_SURFACE_FRAME);
2197 msg.extend_from_slice(&surface_id.to_le_bytes());
2198 msg.extend_from_slice(×tamp.to_le_bytes());
2199 msg.push(flags);
2200 msg.extend_from_slice(&width.to_le_bytes());
2201 msg.extend_from_slice(&height.to_le_bytes());
2202 msg.extend_from_slice(data);
2203 msg
2204}
2205
2206pub fn msg_surface_title(surface_id: u16, title: &str) -> Vec<u8> {
2207 let title_bytes = title.as_bytes();
2208 let mut msg = Vec::with_capacity(3 + title_bytes.len());
2209 msg.push(S2C_SURFACE_TITLE);
2210 msg.extend_from_slice(&surface_id.to_le_bytes());
2211 msg.extend_from_slice(title_bytes);
2212 msg
2213}
2214
2215pub fn msg_surface_app_id(surface_id: u16, app_id: &str) -> Vec<u8> {
2216 let app_id_bytes = app_id.as_bytes();
2217 let mut msg = Vec::with_capacity(3 + app_id_bytes.len());
2218 msg.push(S2C_SURFACE_APP_ID);
2219 msg.extend_from_slice(&surface_id.to_le_bytes());
2220 msg.extend_from_slice(app_id_bytes);
2221 msg
2222}
2223
2224pub fn msg_surface_encoder(surface_id: u16, encoder_name: &str, codec_string: &str) -> Vec<u8> {
2229 let name_bytes = encoder_name.as_bytes();
2230 let codec_bytes = codec_string.as_bytes();
2231 let mut msg = Vec::with_capacity(3 + name_bytes.len() + 1 + codec_bytes.len());
2232 msg.push(S2C_SURFACE_ENCODER);
2233 msg.extend_from_slice(&surface_id.to_le_bytes());
2234 msg.extend_from_slice(name_bytes);
2235 msg.push(0); msg.extend_from_slice(codec_bytes);
2237 msg
2238}
2239
2240pub fn msg_surface_resized(surface_id: u16, width: u16, height: u16) -> Vec<u8> {
2241 let mut msg = Vec::with_capacity(7);
2242 msg.push(S2C_SURFACE_RESIZED);
2243 msg.extend_from_slice(&surface_id.to_le_bytes());
2244 msg.extend_from_slice(&width.to_le_bytes());
2245 msg.extend_from_slice(&height.to_le_bytes());
2246 msg
2247}
2248
2249pub fn msg_s2c_clipboard_content(mime_type: &str, data: &[u8]) -> Vec<u8> {
2250 let mime_bytes = mime_type.as_bytes();
2251 let mut msg = Vec::with_capacity(7 + mime_bytes.len() + data.len());
2252 msg.push(S2C_CLIPBOARD_CONTENT);
2253 msg.extend_from_slice(&(mime_bytes.len() as u16).to_le_bytes());
2254 msg.extend_from_slice(mime_bytes);
2255 msg.extend_from_slice(&(data.len() as u32).to_le_bytes());
2256 msg.extend_from_slice(data);
2257 msg
2258}
2259
2260pub fn msg_surface_input(surface_id: u16, data: &[u8]) -> Vec<u8> {
2261 let mut msg = Vec::with_capacity(3 + data.len());
2262 msg.push(C2S_SURFACE_INPUT);
2263 msg.extend_from_slice(&surface_id.to_le_bytes());
2264 msg.extend_from_slice(data);
2265 msg
2266}
2267
2268pub fn msg_surface_pointer(surface_id: u16, event_type: u8, button: u8, x: u16, y: u16) -> Vec<u8> {
2269 let mut msg = Vec::with_capacity(8);
2270 msg.push(C2S_SURFACE_POINTER);
2271 msg.extend_from_slice(&surface_id.to_le_bytes());
2272 msg.push(event_type);
2273 msg.push(button);
2274 msg.extend_from_slice(&x.to_le_bytes());
2275 msg.extend_from_slice(&y.to_le_bytes());
2276 msg
2277}
2278
2279pub fn msg_surface_pointer_axis(surface_id: u16, axis: u8, value_x100: i32) -> Vec<u8> {
2280 let mut msg = Vec::with_capacity(8);
2281 msg.push(C2S_SURFACE_POINTER_AXIS);
2282 msg.extend_from_slice(&surface_id.to_le_bytes());
2283 msg.push(axis);
2284 msg.extend_from_slice(&value_x100.to_le_bytes());
2285 msg
2286}
2287
2288pub fn msg_surface_resize(surface_id: u16, width: u16, height: u16, scale_120: u16) -> Vec<u8> {
2292 let mut msg = Vec::with_capacity(9);
2293 msg.push(C2S_SURFACE_RESIZE);
2294 msg.extend_from_slice(&surface_id.to_le_bytes());
2295 msg.extend_from_slice(&width.to_le_bytes());
2296 msg.extend_from_slice(&height.to_le_bytes());
2297 msg.extend_from_slice(&scale_120.to_le_bytes());
2298 msg
2299}
2300
2301pub fn msg_surface_focus(surface_id: u16) -> Vec<u8> {
2302 let mut msg = Vec::with_capacity(3);
2303 msg.push(C2S_SURFACE_FOCUS);
2304 msg.extend_from_slice(&surface_id.to_le_bytes());
2305 msg
2306}
2307
2308pub fn msg_surface_subscribe(surface_id: u16) -> Vec<u8> {
2309 let mut msg = Vec::with_capacity(3);
2310 msg.push(C2S_SURFACE_SUBSCRIBE);
2311 msg.extend_from_slice(&surface_id.to_le_bytes());
2312 msg
2313}
2314
2315pub fn msg_surface_subscribe_ext(surface_id: u16, codec_support: u8, quality: u8) -> Vec<u8> {
2320 let mut msg = Vec::with_capacity(5);
2321 msg.push(C2S_SURFACE_SUBSCRIBE);
2322 msg.extend_from_slice(&surface_id.to_le_bytes());
2323 msg.push(codec_support);
2324 msg.push(quality);
2325 msg
2326}
2327
2328pub fn msg_surface_subscribe_scaled(
2337 surface_id: u16,
2338 codec_support: u8,
2339 quality: u8,
2340 width: u16,
2341 height: u16,
2342) -> Vec<u8> {
2343 let mut msg = Vec::with_capacity(9);
2344 msg.push(C2S_SURFACE_SUBSCRIBE);
2345 msg.extend_from_slice(&surface_id.to_le_bytes());
2346 msg.push(codec_support);
2347 msg.push(quality);
2348 msg.extend_from_slice(&width.to_le_bytes());
2349 msg.extend_from_slice(&height.to_le_bytes());
2350 msg
2351}
2352
2353pub fn msg_surface_unsubscribe(surface_id: u16) -> Vec<u8> {
2354 let mut msg = Vec::with_capacity(3);
2355 msg.push(C2S_SURFACE_UNSUBSCRIBE);
2356 msg.extend_from_slice(&surface_id.to_le_bytes());
2357 msg
2358}
2359
2360pub fn msg_surface_close(surface_id: u16) -> Vec<u8> {
2361 let mut msg = Vec::with_capacity(3);
2362 msg.push(C2S_SURFACE_CLOSE);
2363 msg.extend_from_slice(&surface_id.to_le_bytes());
2364 msg
2365}
2366
2367pub fn msg_c2s_clipboard_list() -> Vec<u8> {
2369 vec![C2S_CLIPBOARD_LIST]
2370}
2371
2372pub fn msg_c2s_clipboard_get(mime_type: &str) -> Vec<u8> {
2374 let mime_bytes = mime_type.as_bytes();
2375 let mut msg = Vec::with_capacity(3 + mime_bytes.len());
2376 msg.push(C2S_CLIPBOARD_GET);
2377 msg.extend_from_slice(&(mime_bytes.len() as u16).to_le_bytes());
2378 msg.extend_from_slice(mime_bytes);
2379 msg
2380}
2381
2382pub fn msg_s2c_clipboard_list(mime_types: &[String]) -> Vec<u8> {
2384 let count = mime_types.len().min(u16::MAX as usize);
2385 let mut msg = Vec::with_capacity(3 + count * 20);
2386 msg.push(S2C_CLIPBOARD_LIST);
2387 msg.extend_from_slice(&(count as u16).to_le_bytes());
2388 for mime in mime_types.iter().take(count) {
2389 let bytes = mime.as_bytes();
2390 msg.extend_from_slice(&(bytes.len() as u16).to_le_bytes());
2391 msg.extend_from_slice(bytes);
2392 }
2393 msg
2394}
2395
2396pub fn msg_c2s_clipboard_set(mime_type: &str, data: &[u8]) -> Vec<u8> {
2397 let mime_bytes = mime_type.as_bytes();
2398 let mut msg = Vec::with_capacity(7 + mime_bytes.len() + data.len());
2399 msg.push(C2S_CLIPBOARD_SET);
2400 msg.extend_from_slice(&(mime_bytes.len() as u16).to_le_bytes());
2401 msg.extend_from_slice(mime_bytes);
2402 msg.extend_from_slice(&(data.len() as u32).to_le_bytes());
2403 msg.extend_from_slice(data);
2404 msg
2405}
2406
2407fn push_sgr(out: &mut String, style: &CellStyle) {
2408 use std::fmt::Write;
2409 out.push_str("\x1b[0");
2410 if style.bold {
2411 out.push_str(";1");
2412 }
2413 if style.dim {
2414 out.push_str(";2");
2415 }
2416 if style.italic {
2417 out.push_str(";3");
2418 }
2419 if style.underline {
2420 out.push_str(";4");
2421 }
2422 if style.inverse {
2423 out.push_str(";7");
2424 }
2425 match style.fg {
2426 Color::Indexed(n) => {
2427 let _ = write!(out, ";38;5;{n}");
2428 }
2429 Color::Rgb(r, g, b) => {
2430 let _ = write!(out, ";38;2;{r};{g};{b}");
2431 }
2432 Color::Default => {}
2433 }
2434 match style.bg {
2435 Color::Indexed(n) => {
2436 let _ = write!(out, ";48;5;{n}");
2437 }
2438 Color::Rgb(r, g, b) => {
2439 let _ = write!(out, ";48;2;{r};{g};{b}");
2440 }
2441 Color::Default => {}
2442 }
2443 out.push('m');
2444}
2445
2446const MODE_ALT_SCREEN: u16 = 1 << 11;
2447
2448fn mode_is_cooked(mode: u16) -> bool {
2449 mode & MODE_ECHO != 0 && mode & MODE_ICANON != 0 && mode & MODE_ALT_SCREEN == 0
2450}
2451
2452pub fn build_update_msg(
2453 pty_id: u16,
2454 current: &FrameState,
2455 previous: &FrameState,
2456) -> Option<Vec<u8>> {
2457 let title_changed = current.title != previous.title;
2458 let same_size = previous.rows == current.rows
2459 && previous.cols == current.cols
2460 && previous.cells.len() == current.cells.len();
2461
2462 let mut ops = Vec::new();
2464 let mut op_count = 0u16;
2465
2466 let scroll_eligible = (mode_is_cooked(current.mode) && mode_is_cooked(previous.mode))
2470 || current.mode == 0
2471 || previous.mode == 0;
2472 if ENABLE_SCROLL_OPS
2473 && same_size
2474 && previous.cells != current.cells
2475 && scroll_eligible
2476 && let Some(delta_rows) = detect_vertical_scroll(current, previous)
2477 {
2478 let mut basis = previous.clone();
2479 encode_copy_rect_op(&mut ops, current, delta_rows);
2480 apply_vertical_scroll_copy(&mut basis, delta_rows);
2481 op_count += 1;
2482 append_full_width_fill_ops(current, &mut basis, &mut ops, &mut op_count);
2483 if let Some(patch_op) = build_patch_op(current, &basis) {
2484 ops.extend_from_slice(&patch_op);
2485 op_count += 1;
2486 }
2487 }
2488
2489 if op_count == 0 {
2491 let basis = if same_size {
2492 previous
2493 } else {
2494 &FrameState::new(current.rows, current.cols)
2495 };
2496 if let Some(patch_op) = build_patch_op(current, basis) {
2497 ops = patch_op;
2498 op_count = 1;
2499 }
2500 }
2501
2502 if op_count == 0 {
2503 if !title_changed
2505 && current.cursor_row == previous.cursor_row
2506 && current.cursor_col == previous.cursor_col
2507 && current.mode == previous.mode
2508 {
2509 return None;
2510 }
2511 }
2512
2513 let has_overflow = !current.overflow.is_empty();
2518 let overflow_section = if has_overflow {
2519 serialize_overflow_strings(current)
2520 } else {
2521 Vec::new()
2522 };
2523
2524 let line_flags_changed =
2525 current.line_flags != previous.line_flags || current.rows != previous.rows;
2526 let has_line_flags = line_flags_changed && !current.line_flags.iter().all(|&f| f == 0);
2527
2528 let title_bytes = if title_changed {
2529 current.title.as_bytes()
2530 } else {
2531 &[]
2532 };
2533 let title_len = title_bytes.len().min(TITLE_LEN_MASK as usize);
2534 let title_field = OPS_PRESENT
2535 | if has_overflow { STRINGS_PRESENT } else { 0 }
2536 | if has_line_flags {
2537 LINE_FLAGS_PRESENT
2538 } else {
2539 0
2540 }
2541 | if title_changed {
2542 TITLE_PRESENT | title_len as u16
2543 } else {
2544 0
2545 };
2546
2547 let mut payload = Vec::with_capacity(
2548 12 + title_len
2549 + 2
2550 + ops.len()
2551 + overflow_section.len()
2552 + if has_line_flags {
2553 current.rows as usize
2554 } else {
2555 0
2556 }
2557 + 4,
2558 );
2559 payload.extend_from_slice(¤t.rows.to_le_bytes());
2560 payload.extend_from_slice(¤t.cols.to_le_bytes());
2561 payload.extend_from_slice(¤t.cursor_row.to_le_bytes());
2562 payload.extend_from_slice(¤t.cursor_col.to_le_bytes());
2563 payload.extend_from_slice(¤t.mode.to_le_bytes());
2564 payload.extend_from_slice(&title_field.to_le_bytes());
2565 if title_changed {
2566 payload.extend_from_slice(&title_bytes[..title_len]);
2567 }
2568 payload.extend_from_slice(&op_count.to_le_bytes());
2569 payload.extend_from_slice(&ops);
2570 payload.extend_from_slice(&overflow_section);
2571 if has_line_flags {
2572 payload.extend_from_slice(¤t.line_flags);
2573 }
2574 payload.extend_from_slice(¤t.scrollback_lines.to_le_bytes());
2576
2577 let compressed = compress_prepend_size(&payload);
2578 let mut msg = Vec::with_capacity(3 + compressed.len());
2579 msg.push(S2C_UPDATE);
2580 msg.extend_from_slice(&pty_id.to_le_bytes());
2581 msg.extend_from_slice(&compressed);
2582 Some(msg)
2583}
2584
2585fn serialize_overflow_strings(frame: &FrameState) -> Vec<u8> {
2587 let count = frame.overflow.len().min(u16::MAX as usize);
2588 let mut out = Vec::with_capacity(2 + count * 8);
2589 out.extend_from_slice(&(count as u16).to_le_bytes());
2590 for (&cell_idx, s) in frame.overflow.iter().take(count) {
2591 let bytes = s.as_bytes();
2592 let len = bytes.len().min(u16::MAX as usize);
2593 out.extend_from_slice(&(cell_idx as u32).to_le_bytes());
2594 out.extend_from_slice(&(len as u16).to_le_bytes());
2595 out.extend_from_slice(&bytes[..len]);
2596 }
2597 out
2598}
2599
2600fn build_patch_op(current: &FrameState, previous: &FrameState) -> Option<Vec<u8>> {
2601 let total_cells = current.rows as usize * current.cols as usize;
2602 let total_bytes = total_cells * CELL_SIZE;
2603 if current.cells.len() >= total_bytes
2607 && previous.cells.len() >= total_bytes
2608 && current.cells[..total_bytes] == previous.cells[..total_bytes]
2609 {
2610 return None;
2611 }
2612 let bitmask_len = total_cells.div_ceil(8);
2613 let mut bitmask = vec![0u8; bitmask_len];
2614 let mut dirty_count = 0usize;
2615 for i in 0..total_cells {
2616 let off = i * CELL_SIZE;
2617 if current.cells[off..off + CELL_SIZE] != previous.cells[off..off + CELL_SIZE] {
2618 bitmask[i / 8] |= 1 << (i % 8);
2619 dirty_count += 1;
2620 }
2621 }
2622 if dirty_count == 0 {
2623 return None;
2624 }
2625
2626 let mut op = Vec::with_capacity(1 + bitmask_len + dirty_count * CELL_SIZE);
2627 op.push(OP_PATCH_CELLS);
2628 op.extend_from_slice(&bitmask);
2629 for byte_pos in 0..CELL_SIZE {
2630 for i in 0..total_cells {
2631 if bitmask[i / 8] & (1 << (i % 8)) != 0 {
2632 op.push(current.cells[i * CELL_SIZE + byte_pos]);
2633 }
2634 }
2635 }
2636 Some(op)
2637}
2638
2639fn detect_vertical_scroll(current: &FrameState, previous: &FrameState) -> Option<i16> {
2640 let rows = current.rows as usize;
2641 let cols = current.cols as usize;
2642 if rows < 4 || cols == 0 {
2643 return None;
2644 }
2645 let row_bytes = cols * CELL_SIZE;
2646 let max_delta = rows.saturating_sub(1).min(8);
2647 let mut best: Option<(usize, i16)> = None;
2648
2649 for delta in 1..=max_delta {
2650 let overlap = rows - delta;
2651 if overlap < 3 {
2652 continue;
2653 }
2654 for signed_delta in [-(delta as i16), delta as i16] {
2655 let mut matched = 0usize;
2656 for row in 0..rows {
2657 let src_row = row as i32 - signed_delta as i32;
2658 if src_row < 0 || src_row >= rows as i32 {
2659 continue;
2660 }
2661 let cur_off = row * row_bytes;
2662 let prev_off = src_row as usize * row_bytes;
2663 if current.cells[cur_off..cur_off + row_bytes]
2664 == previous.cells[prev_off..prev_off + row_bytes]
2665 {
2666 matched += 1;
2667 }
2668 }
2669 if matched * 5 < overlap * 4 {
2670 continue;
2671 }
2672 let replace = match best {
2673 None => true,
2674 Some((best_matched, best_delta)) => {
2675 matched > best_matched
2676 || (matched == best_matched
2677 && signed_delta.unsigned_abs() < best_delta.unsigned_abs())
2678 }
2679 };
2680 if replace {
2681 best = Some((matched, signed_delta));
2682 }
2683 }
2684 }
2685
2686 best.map(|(_, delta)| delta)
2687}
2688
2689fn encode_copy_rect_op(out: &mut Vec<u8>, current: &FrameState, delta_rows: i16) {
2690 let rows = current.rows;
2691 let cols = current.cols;
2692 let delta = delta_rows.unsigned_abs();
2693 let (src_row, dst_row, copy_rows) = if delta_rows > 0 {
2694 (0, delta, rows.saturating_sub(delta))
2695 } else {
2696 (delta, 0, rows.saturating_sub(delta))
2697 };
2698 out.push(OP_COPY_RECT);
2699 out.extend_from_slice(&src_row.to_le_bytes());
2700 out.extend_from_slice(&0u16.to_le_bytes());
2701 out.extend_from_slice(&dst_row.to_le_bytes());
2702 out.extend_from_slice(&0u16.to_le_bytes());
2703 out.extend_from_slice(©_rows.to_le_bytes());
2704 out.extend_from_slice(&cols.to_le_bytes());
2705}
2706
2707fn apply_vertical_scroll_copy(frame: &mut FrameState, delta_rows: i16) {
2708 let delta = delta_rows.unsigned_abs();
2709 if delta == 0 || delta >= frame.rows {
2710 return;
2711 }
2712 let (src_row, dst_row, rows) = if delta_rows > 0 {
2713 (0, delta, frame.rows - delta)
2714 } else {
2715 (delta, 0, frame.rows - delta)
2716 };
2717 apply_copy_rect_frame(frame, src_row, 0, dst_row, 0, rows, frame.cols);
2718}
2719
2720fn apply_copy_rect_frame(
2721 frame: &mut FrameState,
2722 src_row: u16,
2723 src_col: u16,
2724 dst_row: u16,
2725 dst_col: u16,
2726 rows: u16,
2727 cols: u16,
2728) {
2729 let rows = rows
2730 .min(frame.rows.saturating_sub(src_row))
2731 .min(frame.rows.saturating_sub(dst_row));
2732 let cols = cols
2733 .min(frame.cols.saturating_sub(src_col))
2734 .min(frame.cols.saturating_sub(dst_col));
2735 if rows == 0 || cols == 0 {
2736 return;
2737 }
2738 let mut temp = vec![0u8; rows as usize * cols as usize * CELL_SIZE];
2739 for r in 0..rows as usize {
2740 let src_off = frame.cell_offset(src_row + r as u16, src_col);
2741 let src_end = src_off + cols as usize * CELL_SIZE;
2742 let dst_off = r * cols as usize * CELL_SIZE;
2743 temp[dst_off..dst_off + cols as usize * CELL_SIZE]
2744 .copy_from_slice(&frame.cells[src_off..src_end]);
2745 }
2746 for r in 0..rows as usize {
2747 let dst_off = frame.cell_offset(dst_row + r as u16, dst_col);
2748 let dst_end = dst_off + cols as usize * CELL_SIZE;
2749 let src_off = r * cols as usize * CELL_SIZE;
2750 frame.cells[dst_off..dst_end]
2751 .copy_from_slice(&temp[src_off..src_off + cols as usize * CELL_SIZE]);
2752 }
2753}
2754
2755fn append_full_width_fill_ops(
2756 current: &FrameState,
2757 basis: &mut FrameState,
2758 out: &mut Vec<u8>,
2759 op_count: &mut u16,
2760) {
2761 let rows = current.rows as usize;
2762 let cols = current.cols as usize;
2763 if rows == 0 || cols == 0 {
2764 return;
2765 }
2766
2767 let row_bytes = cols * CELL_SIZE;
2768 let mut row = 0usize;
2769 while row < rows {
2770 let row_off = row * row_bytes;
2771 if current.cells[row_off..row_off + row_bytes] == basis.cells[row_off..row_off + row_bytes]
2772 {
2773 row += 1;
2774 continue;
2775 }
2776 let Some(cell) = uniform_row_cell(current, row) else {
2777 row += 1;
2778 continue;
2779 };
2780 let mut end = row + 1;
2781 while end < rows {
2782 if uniform_row_cell(current, end).as_ref() != Some(&cell) {
2783 break;
2784 }
2785 end += 1;
2786 }
2787
2788 if *op_count == u16::MAX {
2789 break;
2790 }
2791 out.push(OP_FILL_RECT);
2792 out.extend_from_slice(&(row as u16).to_le_bytes());
2793 out.extend_from_slice(&0u16.to_le_bytes());
2794 out.extend_from_slice(&((end - row) as u16).to_le_bytes());
2795 out.extend_from_slice(¤t.cols.to_le_bytes());
2796 out.extend_from_slice(&cell);
2797 *op_count = op_count.saturating_add(1);
2798
2799 for r in row..end {
2800 let row_off = basis.cell_offset(r as u16, 0);
2801 for c in 0..cols {
2802 let off = row_off + c * CELL_SIZE;
2803 basis.cells[off..off + CELL_SIZE].copy_from_slice(&cell);
2804 }
2805 }
2806
2807 row = end;
2808 }
2809}
2810
2811fn uniform_row_cell(frame: &FrameState, row: usize) -> Option<[u8; CELL_SIZE]> {
2812 let cols = frame.cols as usize;
2813 if row >= frame.rows as usize || cols == 0 {
2814 return None;
2815 }
2816 let start = row * cols * CELL_SIZE;
2817 let mut first = [0u8; CELL_SIZE];
2818 first.copy_from_slice(&frame.cells[start..start + CELL_SIZE]);
2819 if first[1] & 0b110 != 0 {
2820 return None;
2821 }
2822 for col in 1..cols {
2823 let off = start + col * CELL_SIZE;
2824 if frame.cells[off..off + CELL_SIZE] != first {
2825 return None;
2826 }
2827 }
2828 Some(first)
2829}
2830
2831fn encode_cell(dst: &mut [u8], ch: Option<char>, style: CellStyle, wide: bool, wide_cont: bool) {
2832 dst.fill(0);
2833
2834 let mut f0 = 0u8;
2835 encode_color(style.fg, &mut f0, &mut dst[2..5], false);
2836 encode_color(style.bg, &mut f0, &mut dst[5..8], true);
2837 if style.bold {
2838 f0 |= 1 << 4;
2839 }
2840 if style.dim {
2841 f0 |= 1 << 5;
2842 }
2843 if style.italic {
2844 f0 |= 1 << 6;
2845 }
2846 if style.underline {
2847 f0 |= 1 << 7;
2848 }
2849 dst[0] = f0;
2850
2851 let mut f1 = 0u8;
2852 if style.inverse {
2853 f1 |= 1;
2854 }
2855 if wide {
2856 f1 |= 1 << 1;
2857 }
2858 if wide_cont {
2859 f1 |= 1 << 2;
2860 }
2861 if let Some(ch) = ch {
2862 let mut buf = [0u8; 4];
2863 let encoded = ch.encode_utf8(&mut buf).as_bytes();
2864 let len = encoded.len().min(4);
2865 dst[8..8 + len].copy_from_slice(&encoded[..len]);
2866 f1 |= (len as u8) << 3;
2867 }
2868 dst[1] = f1;
2869}
2870
2871fn encode_color(color: Color, flags: &mut u8, dst: &mut [u8], is_bg: bool) {
2872 let shift = if is_bg { 2 } else { 0 };
2873 match color {
2874 Color::Default => {}
2875 Color::Indexed(idx) => {
2876 *flags |= 1 << shift;
2877 dst[0] = idx;
2878 }
2879 Color::Rgb(r, g, b) => {
2880 *flags |= 2 << shift;
2881 dst[0] = r;
2882 dst[1] = g;
2883 dst[2] = b;
2884 }
2885 }
2886}
2887
2888fn wrap_text_lines(text: &str, width: usize) -> Vec<String> {
2889 if width == 0 {
2890 return Vec::new();
2891 }
2892 let mut out = Vec::new();
2893 for paragraph in text.split('\n') {
2894 if paragraph.is_empty() {
2895 out.push(String::new());
2896 continue;
2897 }
2898 let mut line = String::new();
2899 let mut line_width = 0usize;
2900 for word in paragraph.split_whitespace() {
2901 push_wrapped_word(word, width, &mut out, &mut line, &mut line_width);
2902 }
2903 if !line.is_empty() {
2904 out.push(line);
2905 }
2906 }
2907 if out.is_empty() {
2908 out.push(String::new());
2909 }
2910 out
2911}
2912
2913fn push_wrapped_word(
2914 word: &str,
2915 width: usize,
2916 out: &mut Vec<String>,
2917 line: &mut String,
2918 line_width: &mut usize,
2919) {
2920 let word_width = UnicodeWidthStr::width(word);
2921 if line.is_empty() {
2922 if word_width <= width {
2923 line.push_str(word);
2924 *line_width = word_width;
2925 return;
2926 }
2927 } else if *line_width + 1 + word_width <= width {
2928 line.push(' ');
2929 line.push_str(word);
2930 *line_width += 1 + word_width;
2931 return;
2932 } else {
2933 out.push(std::mem::take(line));
2934 *line_width = 0;
2935 if word_width <= width {
2936 line.push_str(word);
2937 *line_width = word_width;
2938 return;
2939 }
2940 }
2941
2942 for ch in word.chars() {
2943 let ch_width = UnicodeWidthChar::width(ch).unwrap_or(1).max(1);
2944 if *line_width + ch_width > width && !line.is_empty() {
2945 out.push(std::mem::take(line));
2946 *line_width = 0;
2947 }
2948 line.push(ch);
2949 *line_width += ch_width;
2950 }
2951}
2952
2953#[cfg(test)]
2954mod tests {
2955 use super::*;
2956
2957 #[test]
2958 fn update_round_trip_preserves_title_and_cells() {
2959 let style = CellStyle::default();
2960 let mut prev = FrameState::new(2, 8);
2961 prev.set_title("one");
2962 prev.write_text(0, 0, "hello", style);
2963
2964 let mut next = prev.clone();
2965 next.set_title("two");
2966 next.write_text(1, 0, "world", style);
2967
2968 let baseline = build_update_msg(7, &prev, &FrameState::default()).unwrap();
2969 let delta = build_update_msg(7, &next, &prev).unwrap();
2970
2971 let mut term = TerminalState::new(2, 8);
2972 let ServerMsg::Update { payload, .. } = parse_server_msg(&baseline).unwrap() else {
2973 panic!("expected update");
2974 };
2975 assert!(term.feed_compressed(payload));
2976 assert_eq!(term.title(), "one");
2977
2978 let ServerMsg::Update { payload, .. } = parse_server_msg(&delta).unwrap() else {
2979 panic!("expected update");
2980 };
2981 assert!(term.feed_compressed(payload));
2982 assert_eq!(term.title(), "two");
2983 assert_eq!(term.get_all_text(), "hello\nworld");
2984 }
2985
2986 #[test]
2987 fn title_can_be_cleared_via_update() {
2988 let style = CellStyle::default();
2989 let mut prev = FrameState::new(1, 4);
2990 prev.set_title("busy");
2991 prev.write_text(0, 0, "ping", style);
2992
2993 let mut next = prev.clone();
2994 next.set_title("");
2995
2996 let baseline = build_update_msg(1, &prev, &FrameState::default()).unwrap();
2997 let delta = build_update_msg(1, &next, &prev).unwrap();
2998
2999 let mut term = TerminalState::new(1, 4);
3000 let ServerMsg::Update { payload, .. } = parse_server_msg(&baseline).unwrap() else {
3001 panic!("expected update");
3002 };
3003 term.feed_compressed(payload);
3004 let ServerMsg::Update { payload, .. } = parse_server_msg(&delta).unwrap() else {
3005 panic!("expected update");
3006 };
3007 term.feed_compressed(payload);
3008 assert_eq!(term.title(), "");
3009 }
3010
3011 #[test]
3012 fn scroll_heavy_update_can_use_ops_payload() {
3013 let style = CellStyle::default();
3014 let mut prev = FrameState::new(5, 6);
3015 prev.write_text(0, 0, "one", style);
3016 prev.write_text(1, 0, "two", style);
3017 prev.write_text(2, 0, "three", style);
3018 prev.write_text(3, 0, "four", style);
3019 prev.write_text(4, 0, "five", style);
3020
3021 let mut next = FrameState::new(5, 6);
3022 next.write_text(0, 0, "two", style);
3023 next.write_text(1, 0, "three", style);
3024 next.write_text(2, 0, "four", style);
3025 next.write_text(3, 0, "five", style);
3026
3027 let delta = build_update_msg(9, &next, &prev).unwrap();
3028 let ServerMsg::Update { payload, .. } = parse_server_msg(&delta).unwrap() else {
3029 panic!("expected update");
3030 };
3031 let decoded = decompress_size_prepended(payload).unwrap();
3032 let title_field = u16::from_le_bytes([decoded[10], decoded[11]]);
3033 assert_ne!(title_field & OPS_PRESENT, 0);
3034
3035 let mut term = TerminalState::new(5, 6);
3036 let baseline = build_update_msg(9, &prev, &FrameState::default()).unwrap();
3037 let ServerMsg::Update { payload, .. } = parse_server_msg(&baseline).unwrap() else {
3038 panic!("expected update");
3039 };
3040 assert!(term.feed_compressed(payload));
3041 let ServerMsg::Update { payload, .. } = parse_server_msg(&delta).unwrap() else {
3042 panic!("expected update");
3043 };
3044 assert!(term.feed_compressed(payload));
3045 assert_eq!(term.get_all_text(), "two\nthree\nfour\nfive\n");
3046 }
3047
3048 #[test]
3049 fn cooked_scroll_heavy_update_uses_copy_rect_op() {
3050 let style = CellStyle::default();
3051 let mut prev = FrameState::new(5, 6);
3052 prev.set_mode(MODE_ECHO | MODE_ICANON);
3053 prev.write_text(0, 0, "one", style);
3054 prev.write_text(1, 0, "two", style);
3055 prev.write_text(2, 0, "three", style);
3056 prev.write_text(3, 0, "four", style);
3057 prev.write_text(4, 0, "five", style);
3058
3059 let mut next = FrameState::new(5, 6);
3060 next.set_mode(MODE_ECHO | MODE_ICANON);
3061 next.write_text(0, 0, "two", style);
3062 next.write_text(1, 0, "three", style);
3063 next.write_text(2, 0, "four", style);
3064 next.write_text(3, 0, "five", style);
3065
3066 let delta = build_update_msg(9, &next, &prev).unwrap();
3067 let ServerMsg::Update { payload, .. } = parse_server_msg(&delta).unwrap() else {
3068 panic!("expected update");
3069 };
3070 let decoded = decompress_size_prepended(payload).unwrap();
3071 let op_count = u16::from_le_bytes([decoded[12], decoded[13]]);
3072 assert!(op_count >= 1);
3073 assert_eq!(decoded[14], OP_COPY_RECT);
3074 }
3075
3076 #[test]
3077 fn mode_zero_scroll_uses_copy_rect() {
3078 let style = CellStyle::default();
3079 let mut prev = FrameState::new(5, 6);
3080 prev.write_text(0, 0, "one", style);
3081 prev.write_text(1, 0, "two", style);
3082 prev.write_text(2, 0, "three", style);
3083 prev.write_text(3, 0, "four", style);
3084 prev.write_text(4, 0, "five", style);
3085
3086 let mut next = FrameState::new(5, 6);
3087 next.write_text(0, 0, "two", style);
3088 next.write_text(1, 0, "three", style);
3089 next.write_text(2, 0, "four", style);
3090 next.write_text(3, 0, "five", style);
3091
3092 let delta = build_update_msg(9, &next, &prev).unwrap();
3093 let ServerMsg::Update { payload, .. } = parse_server_msg(&delta).unwrap() else {
3094 panic!("expected update");
3095 };
3096 let decoded = decompress_size_prepended(payload).unwrap();
3097 let op_count = u16::from_le_bytes([decoded[12], decoded[13]]);
3098 assert!(op_count >= 1);
3099 assert_eq!(decoded[14], OP_COPY_RECT);
3101
3102 let baseline = build_update_msg(9, &prev, &FrameState::new(5, 6)).unwrap();
3104 let mut state = TerminalState::new(5, 6);
3105 let ServerMsg::Update { payload: bp, .. } = parse_server_msg(&baseline).unwrap() else {
3106 panic!("expected update");
3107 };
3108 state.feed_compressed(bp);
3109 state.feed_compressed(payload);
3110 assert_eq!(state.frame().cells(), next.cells());
3111 }
3112
3113 #[test]
3114 fn callback_renderer_wraps_text() {
3115 let mut renderer = CallbackRenderer::new(2, 8);
3116 renderer.render(|dom| {
3117 dom.wrapped_text(
3118 Rect::new(0, 0, 2, 8),
3119 "alpha beta gamma",
3120 CellStyle::default(),
3121 );
3122 });
3123 assert_eq!(renderer.frame().get_all_text(), "alpha\nbeta");
3124 }
3125
3126 #[test]
3127 fn scrolling_text_shows_tail() {
3128 let mut frame = FrameState::new(3, 8);
3129 frame.write_scrolling_text(
3130 Rect::new(0, 0, 3, 8),
3131 &["one", "two", "three", "four"],
3132 0,
3133 CellStyle::default(),
3134 );
3135 assert_eq!(frame.get_all_text(), "two\nthree\nfour");
3136 }
3137
3138 #[test]
3139 fn search_results_round_trip_with_context() {
3140 let msg = [
3141 vec![S2C_SEARCH_RESULTS],
3142 7u16.to_le_bytes().to_vec(),
3143 1u16.to_le_bytes().to_vec(),
3144 42u16.to_le_bytes().to_vec(),
3145 1234u32.to_le_bytes().to_vec(),
3146 vec![1, 0b111],
3147 9u32.to_le_bytes().to_vec(),
3148 5u16.to_le_bytes().to_vec(),
3149 b"hello".to_vec(),
3150 ]
3151 .concat();
3152
3153 let ServerMsg::SearchResults {
3154 request_id,
3155 results,
3156 } = parse_server_msg(&msg).unwrap()
3157 else {
3158 panic!("expected search results");
3159 };
3160 assert_eq!(request_id, 7);
3161 assert_eq!(results.len(), 1);
3162 assert_eq!(results[0].pty_id, 42);
3163 assert_eq!(results[0].score, 1234);
3164 assert_eq!(results[0].primary_source, 1);
3165 assert_eq!(results[0].matched_sources, 0b111);
3166 assert_eq!(results[0].scroll_offset, Some(9));
3167 assert_eq!(results[0].context, b"hello");
3168 }
3169
3170 #[test]
3173 fn msg_create_no_tag_has_zero_tag_len() {
3174 let msg = msg_create(24, 80);
3175 assert_eq!(msg.len(), 7);
3176 assert_eq!(msg[0], C2S_CREATE);
3177 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 24);
3178 assert_eq!(u16::from_le_bytes([msg[3], msg[4]]), 80);
3179 assert_eq!(u16::from_le_bytes([msg[5], msg[6]]), 0);
3180 }
3181
3182 #[test]
3183 fn msg_create_tagged_encodes_tag() {
3184 let msg = msg_create_tagged(24, 80, "my-pty");
3185 assert_eq!(msg[0], C2S_CREATE);
3186 let tag_len = u16::from_le_bytes([msg[5], msg[6]]) as usize;
3187 assert_eq!(tag_len, 6);
3188 assert_eq!(&msg[7..7 + tag_len], b"my-pty");
3189 assert_eq!(msg.len(), 7 + tag_len);
3190 }
3191
3192 #[test]
3193 fn msg_create_tagged_command_encodes_both() {
3194 let msg = msg_create_tagged_command(30, 120, "editor", "vim");
3195 let tag_len = u16::from_le_bytes([msg[5], msg[6]]) as usize;
3196 assert_eq!(tag_len, 6);
3197 assert_eq!(&msg[7..13], b"editor");
3198 assert_eq!(&msg[13..], b"vim");
3199 }
3200
3201 #[test]
3202 fn msg_create_command_has_empty_tag() {
3203 let msg = msg_create_command(24, 80, "ls");
3204 let tag_len = u16::from_le_bytes([msg[5], msg[6]]) as usize;
3205 assert_eq!(tag_len, 0);
3206 assert_eq!(&msg[7..], b"ls");
3207 }
3208
3209 #[test]
3210 fn msg_create_tagged_empty_tag() {
3211 let msg = msg_create_tagged(24, 80, "");
3212 assert_eq!(msg.len(), 7);
3213 assert_eq!(u16::from_le_bytes([msg[5], msg[6]]), 0);
3214 }
3215
3216 #[test]
3217 fn msg_create_tagged_unicode_tag() {
3218 let msg = msg_create_tagged(24, 80, "日本語");
3219 let tag_len = u16::from_le_bytes([msg[5], msg[6]]) as usize;
3220 assert_eq!(tag_len, "日本語".len());
3221 assert_eq!(std::str::from_utf8(&msg[7..7 + tag_len]).unwrap(), "日本語");
3222 }
3223
3224 #[test]
3225 fn parse_created_with_tag() {
3226 let mut wire = vec![S2C_CREATED, 0x05, 0x00];
3227 wire.extend_from_slice(b"hello");
3228 let msg = parse_server_msg(&wire).unwrap();
3229 match msg {
3230 ServerMsg::Created { pty_id, tag } => {
3231 assert_eq!(pty_id, 5);
3232 assert_eq!(tag, "hello");
3233 }
3234 _ => panic!("expected Created"),
3235 }
3236 }
3237
3238 #[test]
3239 fn parse_created_without_tag() {
3240 let wire = vec![S2C_CREATED, 0x03, 0x00];
3241 let msg = parse_server_msg(&wire).unwrap();
3242 match msg {
3243 ServerMsg::Created { pty_id, tag } => {
3244 assert_eq!(pty_id, 3);
3245 assert_eq!(tag, "");
3246 }
3247 _ => panic!("expected Created"),
3248 }
3249 }
3250
3251 #[test]
3252 fn parse_created_n_with_tag() {
3253 let mut wire = vec![S2C_CREATED_N, 0x2a, 0x00, 0x05, 0x00];
3254 wire.extend_from_slice(b"hello");
3255 let msg = parse_server_msg(&wire).unwrap();
3256 match msg {
3257 ServerMsg::CreatedN { nonce, pty_id, tag } => {
3258 assert_eq!(nonce, 42);
3259 assert_eq!(pty_id, 5);
3260 assert_eq!(tag, "hello");
3261 }
3262 _ => panic!("expected CreatedN"),
3263 }
3264 }
3265
3266 #[test]
3267 fn msg_create_n_format() {
3268 let msg = msg_create_n(42, 24, 80, "test");
3269 assert_eq!(msg[0], C2S_CREATE_N);
3270 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 42);
3271 assert_eq!(u16::from_le_bytes([msg[3], msg[4]]), 24);
3272 assert_eq!(u16::from_le_bytes([msg[5], msg[6]]), 80);
3273 assert_eq!(u16::from_le_bytes([msg[7], msg[8]]), 4);
3274 assert_eq!(&msg[9..], b"test");
3275 }
3276
3277 #[test]
3278 fn msg_create_n_command_format() {
3279 let msg = msg_create_n_command(7, 30, 120, "bg", "make build");
3280 assert_eq!(msg[0], C2S_CREATE_N);
3281 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 7);
3282 assert_eq!(u16::from_le_bytes([msg[3], msg[4]]), 30);
3283 assert_eq!(u16::from_le_bytes([msg[5], msg[6]]), 120);
3284 let tag_len = u16::from_le_bytes([msg[7], msg[8]]) as usize;
3285 assert_eq!(tag_len, 2);
3286 assert_eq!(&msg[9..9 + tag_len], b"bg");
3287 assert_eq!(&msg[9 + tag_len..], b"make build");
3288 }
3289
3290 #[test]
3291 fn parse_list_with_tags() {
3292 let mut wire = vec![S2C_LIST, 0x02, 0x00];
3294 wire.extend_from_slice(&1u16.to_le_bytes());
3296 wire.extend_from_slice(&2u16.to_le_bytes());
3297 wire.extend_from_slice(b"ab");
3298 wire.extend_from_slice(&0u16.to_le_bytes());
3299 wire.extend_from_slice(&2u16.to_le_bytes());
3301 wire.extend_from_slice(&0u16.to_le_bytes());
3302 wire.extend_from_slice(&0u16.to_le_bytes());
3303
3304 let msg = parse_server_msg(&wire).unwrap();
3305 match msg {
3306 ServerMsg::List { entries } => {
3307 assert_eq!(entries.len(), 2);
3308 assert_eq!(entries[0].pty_id, 1);
3309 assert_eq!(entries[0].tag, "ab");
3310 assert_eq!(entries[1].pty_id, 2);
3311 assert_eq!(entries[1].tag, "");
3312 }
3313 _ => panic!("expected List"),
3314 }
3315 }
3316
3317 #[test]
3318 fn parse_list_empty() {
3319 let wire = vec![S2C_LIST, 0x00, 0x00];
3320 let msg = parse_server_msg(&wire).unwrap();
3321 match msg {
3322 ServerMsg::List { entries } => assert_eq!(entries.len(), 0),
3323 _ => panic!("expected List"),
3324 }
3325 }
3326
3327 #[test]
3328 fn parse_list_truncated_gracefully() {
3329 let mut wire = vec![S2C_LIST, 0x02, 0x00];
3331 wire.extend_from_slice(&1u16.to_le_bytes());
3332 wire.extend_from_slice(&0u16.to_le_bytes());
3333 let msg = parse_server_msg(&wire).unwrap();
3335 match msg {
3336 ServerMsg::List { entries } => assert_eq!(entries.len(), 1),
3337 _ => panic!("expected List"),
3338 }
3339 }
3340
3341 #[test]
3342 fn parse_list_with_long_tags() {
3343 let long_tag = "a".repeat(300);
3344 let mut wire = vec![S2C_LIST, 0x01, 0x00];
3345 wire.extend_from_slice(&42u16.to_le_bytes());
3346 wire.extend_from_slice(&(long_tag.len() as u16).to_le_bytes());
3347 wire.extend_from_slice(long_tag.as_bytes());
3348
3349 let msg = parse_server_msg(&wire).unwrap();
3350 match msg {
3351 ServerMsg::List { entries } => {
3352 assert_eq!(entries.len(), 1);
3353 assert_eq!(entries[0].pty_id, 42);
3354 assert_eq!(entries[0].tag, long_tag);
3355 }
3356 _ => panic!("expected List"),
3357 }
3358 }
3359
3360 #[test]
3361 fn create_and_created_tag_round_trip() {
3362 let create_msg = msg_create_tagged(24, 80, "my-session");
3364 let tag_len = u16::from_le_bytes([create_msg[5], create_msg[6]]) as usize;
3365 let tag = std::str::from_utf8(&create_msg[7..7 + tag_len]).unwrap();
3366
3367 let mut created_wire = vec![S2C_CREATED, 0x07, 0x00]; created_wire.extend_from_slice(tag.as_bytes());
3370
3371 let msg = parse_server_msg(&created_wire).unwrap();
3372 match msg {
3373 ServerMsg::Created {
3374 pty_id,
3375 tag: parsed_tag,
3376 } => {
3377 assert_eq!(pty_id, 7);
3378 assert_eq!(parsed_tag, "my-session");
3379 }
3380 _ => panic!("expected Created"),
3381 }
3382 }
3383
3384 #[test]
3387 fn frame_state_accessors() {
3388 let mut f = FrameState::new(4, 10);
3389 assert_eq!(f.rows(), 4);
3390 assert_eq!(f.cols(), 10);
3391 assert_eq!(f.cursor_row(), 0);
3392 assert_eq!(f.cursor_col(), 0);
3393 assert_eq!(f.mode(), 0);
3394 assert_eq!(f.title(), "");
3395 assert_eq!(f.cells().len(), 4 * 10 * CELL_SIZE);
3396 assert_eq!(f.cells_mut().len(), 4 * 10 * CELL_SIZE);
3397 assert!(f.overflow().is_empty());
3398 assert!(f.overflow_mut().is_empty());
3399 }
3400
3401 #[test]
3402 fn frame_state_from_parts() {
3403 let cells = vec![0u8; 2 * 4 * CELL_SIZE];
3404 let f = FrameState::from_parts(2, 4, 1, 3, 0x0F, "hello", cells.clone());
3405 assert_eq!(f.rows(), 2);
3406 assert_eq!(f.cols(), 4);
3407 assert_eq!(f.cursor_row(), 1);
3408 assert_eq!(f.cursor_col(), 3);
3409 assert_eq!(f.mode(), 0x0F);
3410 assert_eq!(f.title(), "hello");
3411 assert_eq!(f.cells(), &cells[..]);
3412 }
3413
3414 #[test]
3415 fn frame_state_from_parts_wrong_size() {
3416 let cells = vec![0u8; 10]; let f = FrameState::from_parts(2, 4, 0, 0, 0, "", cells);
3419 assert_eq!(f.cells().len(), 2 * 4 * CELL_SIZE);
3420 }
3421
3422 #[test]
3423 fn frame_state_resize() {
3424 let mut f = FrameState::new(4, 10);
3425 f.set_cursor(3, 9);
3426 f.resize(2, 5);
3427 assert_eq!(f.rows(), 2);
3428 assert_eq!(f.cols(), 5);
3429 assert_eq!(f.cursor_row(), 1); assert_eq!(f.cursor_col(), 4); assert_eq!(f.cells().len(), 2 * 5 * CELL_SIZE);
3432 }
3433
3434 #[test]
3435 fn frame_state_resize_noop() {
3436 let mut f = FrameState::new(4, 10);
3437 let ptr_before = f.cells().as_ptr();
3438 f.resize(4, 10); let ptr_after = f.cells().as_ptr();
3440 assert_eq!(ptr_before, ptr_after); }
3442
3443 #[test]
3444 fn frame_state_set_cursor_clamps() {
3445 let mut f = FrameState::new(4, 10);
3446 f.set_cursor(100, 200);
3447 assert_eq!(f.cursor_row(), 3);
3448 assert_eq!(f.cursor_col(), 9);
3449 }
3450
3451 #[test]
3452 fn frame_state_set_title() {
3453 let mut f = FrameState::new(2, 2);
3454 assert!(f.set_title("new title"));
3455 assert_eq!(f.title(), "new title");
3456 assert!(!f.set_title("new title")); assert!(f.set_title("other"));
3458 }
3459
3460 #[test]
3461 fn frame_state_get_text_and_write_text() {
3462 let mut f = FrameState::new(2, 10);
3463 f.write_text(0, 0, "Hello", CellStyle::default());
3464 f.write_text(1, 0, "World", CellStyle::default());
3465 let text = f.get_text(0, 0, 1, 9);
3466 assert!(text.contains("Hello"));
3467 assert!(text.contains("World"));
3468 let all = f.get_all_text();
3469 assert!(all.contains("Hello"));
3470 }
3471
3472 #[test]
3473 fn frame_state_get_text_empty() {
3474 let f = FrameState::new(0, 0);
3475 assert_eq!(f.get_text(0, 0, 0, 0), "");
3476 assert_eq!(f.get_all_text(), "");
3477 }
3478
3479 #[test]
3480 fn frame_state_get_cell() {
3481 let f = FrameState::new(2, 4);
3482 let cell = f.get_cell(0, 0);
3483 assert_eq!(cell.len(), CELL_SIZE);
3484 assert!(f.get_cell(100, 100).is_empty());
3486 }
3487
3488 #[test]
3489 fn frame_state_cell_content_blank() {
3490 let f = FrameState::new(2, 4);
3491 assert_eq!(f.cell_content(0, 0), " "); assert_eq!(f.cell_content(100, 0), ""); }
3494
3495 #[test]
3496 fn frame_state_cell_content_with_text() {
3497 let mut f = FrameState::new(2, 10);
3498 f.write_text(0, 0, "A", CellStyle::default());
3499 assert_eq!(f.cell_content(0, 0), "A");
3500 }
3501
3502 #[test]
3503 fn frame_state_fill_rect() {
3504 let mut f = FrameState::new(4, 10);
3505 f.fill_rect(Rect::new(0, 0, 2, 5), 'X', CellStyle::default());
3506 assert_eq!(f.cell_content(0, 0), "X");
3507 assert_eq!(f.cell_content(1, 4), "X");
3508 assert_eq!(f.cell_content(2, 0), " "); }
3510
3511 #[test]
3512 fn frame_state_wrapped_text() {
3513 let mut f = FrameState::new(4, 10);
3514 let lines =
3515 f.write_wrapped_text(Rect::new(0, 0, 4, 5), "hello world", CellStyle::default());
3516 assert!(lines >= 2); }
3518
3519 #[test]
3520 fn frame_state_wrapped_text_empty_rect() {
3521 let mut f = FrameState::new(4, 10);
3522 assert_eq!(
3523 f.write_wrapped_text(Rect::new(0, 0, 0, 0), "hi", CellStyle::default()),
3524 0
3525 );
3526 }
3527
3528 #[test]
3529 fn frame_state_scrolling_text() {
3530 let mut f = FrameState::new(4, 10);
3531 f.write_scrolling_text(
3532 Rect::new(0, 0, 3, 10),
3533 &["line1", "line2", "line3", "line4"],
3534 0,
3535 CellStyle::default(),
3536 );
3537 assert_eq!(f.cell_content(0, 0), "l"); }
3540
3541 #[test]
3542 fn frame_state_scrolling_text_empty_rect() {
3543 let mut f = FrameState::new(4, 10);
3544 f.write_scrolling_text(Rect::new(0, 0, 0, 0), &["hi"], 0, CellStyle::default());
3545 }
3547
3548 #[test]
3549 fn frame_state_clear() {
3550 let mut f = FrameState::new(2, 4);
3551 f.write_text(0, 0, "AB", CellStyle::default());
3552 f.clear(CellStyle::default());
3553 assert_eq!(f.cell_content(0, 0), " ");
3554 }
3555
3556 #[test]
3559 fn terminal_state_accessors() {
3560 let t = TerminalState::new(24, 80);
3561 assert_eq!(t.rows(), 24);
3562 assert_eq!(t.cols(), 80);
3563 assert_eq!(t.cursor_row(), 0);
3564 assert_eq!(t.cursor_col(), 0);
3565 assert_eq!(t.mode(), 0);
3566 assert_eq!(t.title(), "");
3567 assert_eq!(t.cells().len(), 24 * 80 * CELL_SIZE);
3568 assert_eq!(t.frame().rows(), 24);
3569 }
3570
3571 #[test]
3572 fn terminal_state_mutators() {
3573 let mut t = TerminalState::new(4, 10);
3574 t.frame_mut().set_title("test");
3575 assert_eq!(t.title(), "test");
3576 }
3577
3578 #[test]
3579 fn terminal_state_set_title() {
3580 let mut t = TerminalState::new(4, 10);
3581 assert!(t.frame_mut().set_title("hello"));
3582 assert_eq!(t.title(), "hello");
3583 assert!(!t.frame_mut().set_title("hello")); }
3585
3586 #[test]
3587 fn terminal_state_get_text() {
3588 let t = TerminalState::new(2, 10);
3589 let text = t.get_text(0, 0, 0, 9);
3590 assert!(text.is_empty() || text.chars().all(|c| c == ' ' || c == '\n'));
3591 assert!(t.get_cell(0, 0).len() == CELL_SIZE);
3592 assert!(t.get_cell(100, 100).is_empty());
3593 }
3594
3595 #[test]
3596 fn terminal_state_resize() {
3597 let mut t = TerminalState::new(4, 10);
3598 t.frame_mut().resize(2, 5);
3599 assert_eq!(t.rows(), 2);
3602 assert_eq!(t.cols(), 5);
3603 }
3604
3605 #[test]
3606 fn terminal_state_feed_compressed_invalid() {
3607 let mut t = TerminalState::new(4, 10);
3608 assert!(!t.feed_compressed(b"garbage"));
3609 assert!(!t.feed_compressed(&[]));
3610 }
3611
3612 #[test]
3613 fn terminal_state_feed_compressed_batch_empty() {
3614 let mut t = TerminalState::new(4, 10);
3615 assert!(!t.feed_compressed_batch(&[]));
3616 }
3617
3618 #[test]
3619 fn terminal_state_feed_compressed_batch_truncated() {
3620 let mut t = TerminalState::new(4, 10);
3621 let batch = &[100, 0, 0, 0];
3623 assert!(!t.feed_compressed_batch(batch));
3624 }
3625
3626 #[test]
3629 fn msg_input_format() {
3630 let msg = msg_input(5, b"hello");
3631 assert_eq!(msg[0], C2S_INPUT);
3632 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 5);
3633 assert_eq!(&msg[3..], b"hello");
3634 }
3635
3636 #[test]
3637 fn msg_resize_format() {
3638 let msg = msg_resize(3, 24, 80);
3639 assert_eq!(msg[0], C2S_RESIZE);
3640 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 3);
3641 assert_eq!(u16::from_le_bytes([msg[3], msg[4]]), 24);
3642 assert_eq!(u16::from_le_bytes([msg[5], msg[6]]), 80);
3643 }
3644
3645 #[test]
3646 fn msg_resize_batch_format() {
3647 let msg = msg_resize_batch(&[(3, 24, 80), (5, 40, 120)]);
3648 assert_eq!(msg[0], C2S_RESIZE);
3649 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 3);
3650 assert_eq!(u16::from_le_bytes([msg[3], msg[4]]), 24);
3651 assert_eq!(u16::from_le_bytes([msg[5], msg[6]]), 80);
3652 assert_eq!(u16::from_le_bytes([msg[7], msg[8]]), 5);
3653 assert_eq!(u16::from_le_bytes([msg[9], msg[10]]), 40);
3654 assert_eq!(u16::from_le_bytes([msg[11], msg[12]]), 120);
3655 }
3656
3657 #[test]
3658 fn msg_focus_format() {
3659 let msg = msg_focus(7);
3660 assert_eq!(msg[0], C2S_FOCUS);
3661 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 7);
3662 assert_eq!(msg.len(), 3);
3663 }
3664
3665 #[test]
3666 fn msg_close_format() {
3667 let msg = msg_close(9);
3668 assert_eq!(msg[0], C2S_CLOSE);
3669 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 9);
3670 }
3671
3672 #[test]
3673 fn msg_subscribe_unsubscribe_format() {
3674 let sub = msg_subscribe(1);
3675 assert_eq!(sub[0], C2S_SUBSCRIBE);
3676 assert_eq!(u16::from_le_bytes([sub[1], sub[2]]), 1);
3677
3678 let unsub = msg_unsubscribe(2);
3679 assert_eq!(unsub[0], C2S_UNSUBSCRIBE);
3680 assert_eq!(u16::from_le_bytes([unsub[1], unsub[2]]), 2);
3681 }
3682
3683 #[test]
3684 fn msg_search_format() {
3685 let msg = msg_search(42, "test query");
3686 assert_eq!(msg[0], C2S_SEARCH);
3687 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 42);
3688 assert_eq!(&msg[3..], b"test query");
3689 }
3690
3691 #[test]
3692 fn msg_ack_format() {
3693 let msg = msg_ack();
3694 assert_eq!(msg, vec![C2S_ACK]);
3695 }
3696
3697 #[test]
3698 fn msg_scroll_format() {
3699 let msg = msg_scroll(5, 1000);
3700 assert_eq!(msg[0], C2S_SCROLL);
3701 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 5);
3702 assert_eq!(u32::from_le_bytes([msg[3], msg[4], msg[5], msg[6]]), 1000);
3703 }
3704
3705 #[test]
3706 fn msg_display_rate_format() {
3707 let msg = msg_display_rate(120);
3708 assert_eq!(msg[0], C2S_DISPLAY_RATE);
3709 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 120);
3710 }
3711
3712 #[test]
3713 fn msg_client_metrics_format() {
3714 let msg = msg_client_metrics(3, 5, 100);
3715 assert_eq!(msg[0], C2S_CLIENT_METRICS);
3716 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 3);
3717 assert_eq!(u16::from_le_bytes([msg[3], msg[4]]), 5);
3718 assert_eq!(u16::from_le_bytes([msg[5], msg[6]]), 100);
3719 }
3720
3721 #[test]
3724 fn callback_renderer_resize() {
3725 let mut r = CallbackRenderer::new(2, 8);
3726 assert_eq!(r.frame().rows(), 2);
3727 r.resize(4, 16);
3728 assert_eq!(r.frame().rows(), 4);
3729 assert_eq!(r.frame().cols(), 16);
3730 }
3731
3732 #[test]
3733 fn callback_renderer_fill() {
3734 let mut r = CallbackRenderer::new(4, 10);
3735 r.render(|dom| {
3736 dom.fill(Rect::new(0, 0, 2, 5), '#', CellStyle::default());
3737 });
3738 assert_eq!(r.frame().cell_content(0, 0), "#");
3739 assert_eq!(r.frame().cell_content(1, 4), "#");
3740 }
3741
3742 #[test]
3743 fn callback_renderer_text() {
3744 let mut r = CallbackRenderer::new(4, 20);
3745 r.render(|dom| {
3746 dom.text(0, 0, "Hello", CellStyle::default());
3747 });
3748 assert_eq!(r.frame().cell_content(0, 0), "H");
3749 assert_eq!(r.frame().cell_content(0, 4), "o");
3750 }
3751
3752 #[test]
3753 fn callback_renderer_set_title() {
3754 let mut r = CallbackRenderer::new(2, 8);
3755 r.render(|dom| {
3756 dom.set_title("my title");
3757 });
3758 assert_eq!(r.frame().title(), "my title");
3759 }
3760
3761 #[test]
3762 fn callback_renderer_set_background() {
3763 let mut r = CallbackRenderer::new(2, 4);
3764 let style = CellStyle {
3765 bg: Color::Rgb(255, 0, 0),
3766 ..CellStyle::default()
3767 };
3768 r.render(|dom| {
3769 dom.set_background(style);
3770 });
3771 assert_eq!(r.frame().cells().len(), 2 * 4 * CELL_SIZE);
3773 }
3774
3775 #[test]
3776 fn callback_renderer_scrolling_text() {
3777 let mut r = CallbackRenderer::new(4, 20);
3778 r.render(|dom| {
3779 dom.scrolling_text(
3780 Rect::new(0, 0, 3, 20),
3781 ["a", "b", "c", "d", "e"].map(String::from),
3782 0,
3783 CellStyle::default(),
3784 );
3785 });
3786 assert_eq!(r.frame().cell_content(0, 0), "c");
3788 }
3789
3790 #[test]
3793 fn parse_empty_returns_none() {
3794 assert!(parse_server_msg(&[]).is_none());
3795 }
3796
3797 #[test]
3798 fn parse_unknown_type_returns_none() {
3799 assert!(parse_server_msg(&[0xFF, 0x00, 0x00]).is_none());
3800 }
3801
3802 #[test]
3803 fn parse_update_too_short() {
3804 assert!(parse_server_msg(&[S2C_UPDATE, 0x00]).is_none());
3805 }
3806
3807 #[test]
3808 fn parse_closed() {
3809 let msg = parse_server_msg(&[S2C_CLOSED, 0x05, 0x00]).unwrap();
3810 match msg {
3811 ServerMsg::Closed { pty_id } => assert_eq!(pty_id, 5),
3812 _ => panic!("expected Closed"),
3813 }
3814 }
3815
3816 #[test]
3817 fn parse_title() {
3818 let mut wire = vec![S2C_TITLE, 0x01, 0x00];
3819 wire.extend_from_slice(b"mytitle");
3820 let msg = parse_server_msg(&wire).unwrap();
3821 match msg {
3822 ServerMsg::Title { pty_id, title } => {
3823 assert_eq!(pty_id, 1);
3824 assert_eq!(title, b"mytitle");
3825 }
3826 _ => panic!("expected Title"),
3827 }
3828 }
3829
3830 #[test]
3833 fn build_update_msg_round_trip_with_resize() {
3834 let style = CellStyle::default();
3835 let mut prev = FrameState::new(2, 4);
3836 prev.write_text(0, 0, "AB", style);
3837
3838 let mut next = FrameState::new(3, 5); next.write_text(0, 0, "XY", style);
3840 next.set_title("resized");
3841
3842 let msg = build_update_msg(1, &next, &prev).unwrap();
3843 assert!(!msg.is_empty());
3844
3845 let mut t = TerminalState::new(2, 4);
3847 assert!(t.feed_compressed(&msg[3..])); assert_eq!(t.rows(), 3);
3849 assert_eq!(t.cols(), 5);
3850 assert_eq!(t.title(), "resized");
3851 }
3852
3853 #[test]
3854 fn build_update_msg_cursor_change() {
3855 let mut prev = FrameState::new(4, 10);
3856 prev.set_cursor(0, 0);
3857
3858 let mut next = prev.clone();
3859 next.set_cursor(2, 5);
3860
3861 let msg = build_update_msg(0, &next, &prev).unwrap();
3862
3863 let mut t = TerminalState::new(4, 10);
3864 assert!(t.feed_compressed(&msg[3..]));
3865 assert_eq!(t.cursor_row(), 2);
3866 assert_eq!(t.cursor_col(), 5);
3867 }
3868
3869 #[test]
3870 fn build_update_msg_mode_change() {
3871 let prev = FrameState::new(2, 4);
3872 let mut next = prev.clone();
3873 next.set_mode(0x0F);
3874
3875 let msg = build_update_msg(0, &next, &prev).unwrap();
3876 let mut t = TerminalState::new(2, 4);
3877 assert!(t.feed_compressed(&msg[3..]));
3878 assert_eq!(t.mode(), 0x0F);
3879 }
3880
3881 #[test]
3882 fn feed_compressed_batch_multiple_frames() {
3883 let style = CellStyle::default();
3884 let prev = FrameState::new(2, 4);
3885
3886 let mut mid = prev.clone();
3887 mid.write_text(0, 0, "AB", style);
3888 let msg1 = build_update_msg(0, &mid, &prev).unwrap();
3889
3890 let mut next = mid.clone();
3891 next.write_text(1, 0, "CD", style);
3892 let msg2 = build_update_msg(0, &next, &mid).unwrap();
3893
3894 let payload1 = &msg1[3..];
3896 let payload2 = &msg2[3..];
3897 let mut batch = Vec::new();
3898 batch.extend_from_slice(&(payload1.len() as u32).to_le_bytes());
3899 batch.extend_from_slice(payload1);
3900 batch.extend_from_slice(&(payload2.len() as u32).to_le_bytes());
3901 batch.extend_from_slice(payload2);
3902
3903 let mut t = TerminalState::new(2, 4);
3904 assert!(t.feed_compressed_batch(&batch));
3905 let text = t.get_all_text();
3906 assert!(text.contains("AB"));
3907 assert!(text.contains("CD"));
3908 }
3909}