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