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