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