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