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