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;
70
71pub const S2C_UPDATE: u8 = 0x00;
72pub const S2C_CREATED: u8 = 0x01;
73pub const S2C_CLOSED: u8 = 0x02;
74pub const S2C_LIST: u8 = 0x03;
75pub const S2C_TITLE: u8 = 0x04;
76pub const S2C_SEARCH_RESULTS: u8 = 0x05;
77pub const S2C_CREATED_N: u8 = 0x06;
78pub const S2C_HELLO: u8 = 0x07;
79pub const S2C_EXITED: u8 = 0x08;
85pub const EXIT_STATUS_UNKNOWN: i32 = i32::MIN;
86pub const S2C_READY: u8 = 0x09;
89pub const S2C_TEXT: u8 = 0x0A;
95
96pub const FEATURE_CREATE_NONCE: u32 = 1 << 0;
97pub const FEATURE_RESTART: u32 = 1 << 1;
98pub const FEATURE_RESIZE_BATCH: u32 = 1 << 2;
99
100#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
101pub enum Color {
102 #[default]
103 Default,
104 Indexed(u8),
105 Rgb(u8, u8, u8),
106}
107
108#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
109pub struct CellStyle {
110 pub fg: Color,
111 pub bg: Color,
112 pub bold: bool,
113 pub dim: bool,
114 pub italic: bool,
115 pub underline: bool,
116 pub inverse: bool,
117}
118
119#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
120pub struct Rect {
121 pub row: u16,
122 pub col: u16,
123 pub rows: u16,
124 pub cols: u16,
125}
126
127impl Rect {
128 pub const fn new(row: u16, col: u16, rows: u16, cols: u16) -> Self {
129 Self {
130 row,
131 col,
132 rows,
133 cols,
134 }
135 }
136}
137
138#[derive(Clone, Debug, Default, PartialEq, Eq)]
139pub struct FrameState {
140 rows: u16,
141 cols: u16,
142 cells: Vec<u8>,
143 cursor_row: u16,
144 cursor_col: u16,
145 mode: u16,
146 title: String,
147 overflow: BTreeMap<usize, String>,
150 line_flags: Vec<u8>,
152 scrollback_lines: u32,
154}
155
156impl FrameState {
157 pub fn new(rows: u16, cols: u16) -> Self {
158 let total = rows as usize * cols as usize;
159 Self {
160 rows,
161 cols,
162 cells: vec![0; total * CELL_SIZE],
163 cursor_row: 0,
164 cursor_col: 0,
165 mode: 0,
166 title: String::new(),
167 overflow: BTreeMap::new(),
168 line_flags: vec![0; rows as usize],
169 scrollback_lines: 0,
170 }
171 }
172
173 pub fn from_parts(
174 rows: u16,
175 cols: u16,
176 cursor_row: u16,
177 cursor_col: u16,
178 mode: u16,
179 title: impl Into<String>,
180 cells: Vec<u8>,
181 ) -> Self {
182 let mut state = Self::new(rows, cols);
183 if cells.len() == state.cells.len() {
184 state.cells = cells;
185 }
186 state.cursor_row = cursor_row;
187 state.cursor_col = cursor_col;
188 state.mode = mode;
189 state.title = title.into();
190 state
191 }
192
193 pub fn rows(&self) -> u16 {
194 self.rows
195 }
196
197 pub fn cols(&self) -> u16 {
198 self.cols
199 }
200
201 pub fn cursor_row(&self) -> u16 {
202 self.cursor_row
203 }
204
205 pub fn cursor_col(&self) -> u16 {
206 self.cursor_col
207 }
208
209 pub fn mode(&self) -> u16 {
210 self.mode
211 }
212
213 pub fn title(&self) -> &str {
214 &self.title
215 }
216
217 pub fn cells(&self) -> &[u8] {
218 &self.cells
219 }
220
221 pub fn cells_mut(&mut self) -> &mut [u8] {
222 &mut self.cells
223 }
224
225 pub fn overflow(&self) -> &BTreeMap<usize, String> {
226 &self.overflow
227 }
228
229 pub fn overflow_mut(&mut self) -> &mut BTreeMap<usize, String> {
230 &mut self.overflow
231 }
232
233 pub fn line_flags(&self) -> &[u8] {
234 &self.line_flags
235 }
236
237 pub fn line_flags_mut(&mut self) -> &mut Vec<u8> {
238 &mut self.line_flags
239 }
240
241 pub fn scrollback_lines(&self) -> u32 {
242 self.scrollback_lines
243 }
244
245 pub fn set_scrollback_lines(&mut self, lines: u32) {
246 self.scrollback_lines = lines;
247 }
248
249 pub fn is_wrapped(&self, row: u16) -> bool {
250 self.line_flags.get(row as usize).copied().unwrap_or(0) & ROW_FLAG_WRAPPED != 0
251 }
252
253 pub fn set_wrapped(&mut self, row: u16, wrapped: bool) {
254 if let Some(flags) = self.line_flags.get_mut(row as usize) {
255 if wrapped {
256 *flags |= ROW_FLAG_WRAPPED;
257 } else {
258 *flags &= !ROW_FLAG_WRAPPED;
259 }
260 }
261 }
262
263 pub fn cell_content(&self, row: u16, col: u16) -> &str {
265 if row >= self.rows || col >= self.cols {
266 return "";
267 }
268 let flat = row as usize * self.cols as usize + col as usize;
269 let idx = flat * CELL_SIZE;
270 let f1 = self.cells[idx + 1];
271 if f1 & 4 != 0 {
272 return ""; }
274 let content_len = ((f1 >> 3) & 7) as usize;
275 if content_len == CONTENT_OVERFLOW as usize {
276 if let Some(s) = self.overflow.get(&flat) {
277 return s.as_str();
278 }
279 return "";
280 }
281 if content_len == 0 {
282 return " ";
283 }
284 std::str::from_utf8(&self.cells[idx + 8..idx + 8 + content_len]).unwrap_or(" ")
285 }
286
287 pub fn resize(&mut self, rows: u16, cols: u16) {
288 if rows == self.rows && cols == self.cols {
289 return;
290 }
291 self.rows = rows;
292 self.cols = cols;
293 self.cells = vec![0; rows as usize * cols as usize * CELL_SIZE];
294 self.overflow.clear();
295 self.line_flags = vec![0; rows as usize];
296 self.cursor_row = self.cursor_row.min(rows.saturating_sub(1));
297 self.cursor_col = self.cursor_col.min(cols.saturating_sub(1));
298 }
299
300 pub fn set_cursor(&mut self, row: u16, col: u16) {
301 self.cursor_row = row.min(self.rows.saturating_sub(1));
302 self.cursor_col = col.min(self.cols.saturating_sub(1));
303 }
304
305 pub fn set_mode(&mut self, mode: u16) {
306 self.mode = mode;
307 }
308
309 pub fn set_title(&mut self, title: impl Into<String>) -> bool {
310 let title = title.into();
311 if self.title == title {
312 return false;
313 }
314 self.title = title;
315 true
316 }
317
318 pub fn clear(&mut self, style: CellStyle) {
319 for row in 0..self.rows {
320 for col in 0..self.cols {
321 self.set_blank_cell(row, col, style);
322 }
323 }
324 }
325
326 pub fn fill_rect(&mut self, rect: Rect, ch: char, style: CellStyle) {
327 let row_end = rect.row.saturating_add(rect.rows).min(self.rows);
328 let col_end = rect.col.saturating_add(rect.cols).min(self.cols);
329 for row in rect.row..row_end {
330 let mut col = rect.col;
331 while col < col_end {
332 let width = self.set_cell(row, col, ch, style);
333 if width == 0 {
334 break;
335 }
336 col = col.saturating_add(width);
337 }
338 }
339 }
340
341 pub fn write_text(&mut self, row: u16, col: u16, text: &str, style: CellStyle) -> u16 {
342 if row >= self.rows || col >= self.cols {
343 return col;
344 }
345 let mut cur_col = col;
346 for ch in text.chars() {
347 if cur_col >= self.cols {
348 break;
349 }
350 let width = self.set_cell(row, cur_col, ch, style);
351 if width == 0 {
352 continue;
353 }
354 cur_col = cur_col.saturating_add(width);
355 }
356 cur_col
357 }
358
359 pub fn write_wrapped_text(&mut self, rect: Rect, text: &str, style: CellStyle) -> usize {
360 if rect.rows == 0 || rect.cols == 0 {
361 return 0;
362 }
363 let lines = wrap_text_lines(text, rect.cols as usize);
364 let max_rows = rect.rows.min(self.rows.saturating_sub(rect.row));
365 for (idx, line) in lines.iter().take(max_rows as usize).enumerate() {
366 let row = rect.row + idx as u16;
367 self.write_text(row, rect.col, line, style);
368 }
369 lines.len()
370 }
371
372 pub fn write_scrolling_text<S: AsRef<str>>(
373 &mut self,
374 rect: Rect,
375 lines: &[S],
376 offset_from_bottom: usize,
377 style: CellStyle,
378 ) {
379 if rect.rows == 0 || rect.cols == 0 {
380 return;
381 }
382 let mut wrapped = Vec::new();
383 for line in lines {
384 let line = line.as_ref();
385 let out = wrap_text_lines(line, rect.cols as usize);
386 if out.is_empty() {
387 wrapped.push(String::new());
388 } else {
389 wrapped.extend(out);
390 }
391 }
392 let visible = rect.rows as usize;
393 let end = wrapped.len().saturating_sub(offset_from_bottom);
394 let start = end.saturating_sub(visible);
395 for row in 0..rect.rows {
396 self.fill_rect(
397 Rect::new(rect.row + row, rect.col, 1, rect.cols),
398 ' ',
399 style,
400 );
401 }
402 for (idx, line) in wrapped[start..end].iter().enumerate() {
403 self.write_text(rect.row + idx as u16, rect.col, line, style);
404 }
405 }
406
407 pub fn get_text(&self, start_row: u16, start_col: u16, end_row: u16, end_col: u16) -> String {
408 let mut result = String::new();
409 if self.rows == 0 || self.cols == 0 {
410 return result;
411 }
412 for row in start_row..=end_row.min(self.rows.saturating_sub(1)) {
413 let c0 = if row == start_row { start_col } else { 0 };
414 let c1 = if row == end_row {
415 end_col
416 } else {
417 self.cols - 1
418 };
419 let mut line = String::new();
420 let mut col = c0;
421 while col <= c1.min(self.cols - 1) {
422 line.push_str(self.cell_content(row, col));
423 col += 1;
424 }
425 result.push_str(line.trim_end());
426 if row < end_row.min(self.rows.saturating_sub(1)) {
427 result.push('\n');
428 }
429 }
430 result
431 }
432
433 pub fn get_all_text(&self) -> String {
434 if self.rows == 0 || self.cols == 0 {
435 return String::new();
436 }
437 self.get_text(0, 0, self.rows - 1, self.cols - 1)
438 }
439
440 fn cell_style(&self, row: u16, col: u16) -> CellStyle {
441 if row >= self.rows || col >= self.cols {
442 return CellStyle::default();
443 }
444 let idx = self.cell_offset(row, col);
445 let f0 = self.cells[idx];
446 let f1 = self.cells[idx + 1];
447 let fg_type = f0 & 3;
448 let bg_type = (f0 >> 2) & 3;
449 let fg = match fg_type {
450 1 => Color::Indexed(self.cells[idx + 2]),
451 2 => Color::Rgb(
452 self.cells[idx + 2],
453 self.cells[idx + 3],
454 self.cells[idx + 4],
455 ),
456 _ => Color::Default,
457 };
458 let bg = match bg_type {
459 1 => Color::Indexed(self.cells[idx + 5]),
460 2 => Color::Rgb(
461 self.cells[idx + 5],
462 self.cells[idx + 6],
463 self.cells[idx + 7],
464 ),
465 _ => Color::Default,
466 };
467 CellStyle {
468 fg,
469 bg,
470 bold: (f0 >> 4) & 1 != 0,
471 dim: (f0 >> 5) & 1 != 0,
472 italic: (f0 >> 6) & 1 != 0,
473 underline: (f0 >> 7) & 1 != 0,
474 inverse: f1 & 1 != 0,
475 }
476 }
477
478 pub fn get_ansi_text(&self) -> String {
479 if self.rows == 0 || self.cols == 0 {
480 return String::new();
481 }
482 let mut result = String::new();
483 let mut cur_style = CellStyle::default();
484 for row in 0..self.rows {
485 let mut line = String::new();
486 let mut col = 0u16;
487 while col < self.cols {
488 let style = self.cell_style(row, col);
489 if style != cur_style {
490 push_sgr(&mut line, &style);
491 cur_style = style;
492 }
493 line.push_str(self.cell_content(row, col));
494 col += 1;
495 }
496 let trimmed = line.trim_end();
497 result.push_str(trimmed);
498 if cur_style != CellStyle::default() {
499 result.push_str("\x1b[0m");
500 cur_style = CellStyle::default();
501 }
502 if row < self.rows - 1 {
503 result.push('\n');
504 }
505 }
506 result
507 }
508
509 pub fn get_cell(&self, row: u16, col: u16) -> Vec<u8> {
510 if row >= self.rows || col >= self.cols {
511 return Vec::new();
512 }
513 let idx = self.cell_offset(row, col);
514 self.cells[idx..idx + CELL_SIZE].to_vec()
515 }
516
517 fn cell_offset(&self, row: u16, col: u16) -> usize {
518 (row as usize * self.cols as usize + col as usize) * CELL_SIZE
519 }
520
521 fn set_cell(&mut self, row: u16, col: u16, ch: char, style: CellStyle) -> u16 {
522 if row >= self.rows || col >= self.cols {
523 return 0;
524 }
525 let raw_width = UnicodeWidthChar::width(ch).unwrap_or(0);
526 if raw_width == 0 {
527 return 0;
528 }
529 let width = if raw_width > 1 && col + 1 < self.cols {
530 2
531 } else {
532 1
533 };
534 let idx = self.cell_offset(row, col);
535 encode_cell(
536 &mut self.cells[idx..idx + CELL_SIZE],
537 Some(ch),
538 style,
539 width == 2,
540 false,
541 );
542 if width == 2 {
543 let cont_idx = self.cell_offset(row, col + 1);
544 encode_cell(
545 &mut self.cells[cont_idx..cont_idx + CELL_SIZE],
546 None,
547 style,
548 false,
549 true,
550 );
551 }
552 width
553 }
554
555 fn set_blank_cell(&mut self, row: u16, col: u16, style: CellStyle) {
556 if row >= self.rows || col >= self.cols {
557 return;
558 }
559 let idx = self.cell_offset(row, col);
560 encode_cell(
561 &mut self.cells[idx..idx + CELL_SIZE],
562 None,
563 style,
564 false,
565 false,
566 );
567 }
568}
569
570#[derive(Clone, Debug)]
571pub struct TerminalState {
572 frame: FrameState,
573}
574
575impl TerminalState {
576 pub fn new(rows: u16, cols: u16) -> Self {
577 let frame = FrameState::new(rows, cols);
578 Self { frame }
579 }
580
581 pub fn frame(&self) -> &FrameState {
582 &self.frame
583 }
584
585 pub fn frame_mut(&mut self) -> &mut FrameState {
586 &mut self.frame
587 }
588
589 pub fn title(&self) -> &str {
590 self.frame.title()
591 }
592
593 pub fn rows(&self) -> u16 {
594 self.frame.rows()
595 }
596
597 pub fn cols(&self) -> u16 {
598 self.frame.cols()
599 }
600
601 pub fn is_wrapped(&self, row: u16) -> bool {
602 self.frame.is_wrapped(row)
603 }
604
605 pub fn cursor_row(&self) -> u16 {
606 self.frame.cursor_row()
607 }
608
609 pub fn cursor_col(&self) -> u16 {
610 self.frame.cursor_col()
611 }
612
613 pub fn mode(&self) -> u16 {
614 self.frame.mode()
615 }
616
617 pub fn cells(&self) -> &[u8] {
618 self.frame.cells()
619 }
620
621 pub fn set_title(&mut self, title: &str) -> bool {
622 self.frame.set_title(title.to_owned())
623 }
624
625 pub fn get_text(&self, start_row: u16, start_col: u16, end_row: u16, end_col: u16) -> String {
626 self.frame.get_text(start_row, start_col, end_row, end_col)
627 }
628
629 pub fn get_all_text(&self) -> String {
630 self.frame.get_all_text()
631 }
632
633 pub fn get_ansi_text(&self) -> String {
634 self.frame.get_ansi_text()
635 }
636
637 pub fn get_cell(&self, row: u16, col: u16) -> Vec<u8> {
638 self.frame.get_cell(row, col)
639 }
640
641 pub fn feed_compressed(&mut self, data: &[u8]) -> bool {
642 let payload = match decompress_size_prepended(data) {
643 Ok(d) => d,
644 Err(_) => return false,
645 };
646 self.apply_payload(&payload)
647 }
648
649 pub fn feed_compressed_batch(&mut self, batch: &[u8]) -> bool {
650 let mut changed = false;
651 let mut off = 0usize;
652 while off + 4 <= batch.len() {
653 let len =
654 u32::from_le_bytes([batch[off], batch[off + 1], batch[off + 2], batch[off + 3]])
655 as usize;
656 off += 4;
657 if off + len > batch.len() {
658 break;
659 }
660 if let Ok(payload) = decompress_size_prepended(&batch[off..off + len]) {
661 changed |= self.apply_payload(&payload);
662 }
663 off += len;
664 }
665 changed
666 }
667
668 fn apply_payload(&mut self, payload: &[u8]) -> bool {
669 if payload.len() < 12 {
670 return false;
671 }
672
673 let new_rows = u16::from_le_bytes([payload[0], payload[1]]);
674 let new_cols = u16::from_le_bytes([payload[2], payload[3]]);
675 let new_cursor_row = u16::from_le_bytes([payload[4], payload[5]]);
676 let new_cursor_col = u16::from_le_bytes([payload[6], payload[7]]);
677 let new_mode = u16::from_le_bytes([payload[8], payload[9]]);
678 let title_field = u16::from_le_bytes([payload[10], payload[11]]);
679 let title_present = title_field & TITLE_PRESENT != 0;
680 let ops_present = title_field & OPS_PRESENT != 0;
681 let strings_present = title_field & STRINGS_PRESENT != 0;
682 let line_flags_present = title_field & LINE_FLAGS_PRESENT != 0;
683 let title_len = (title_field & TITLE_LEN_MASK) as usize;
684
685 let title_start = 12usize;
686 let title_end = title_start.saturating_add(title_len);
687 if payload.len() < title_end {
688 return false;
689 }
690 let title_changed = if title_present {
691 let title = String::from_utf8_lossy(&payload[title_start..title_end]).into_owned();
692 self.frame.set_title(title)
693 } else {
694 false
695 };
696
697 let resized = new_rows != self.frame.rows || new_cols != self.frame.cols;
698 if resized {
699 self.frame.resize(new_rows, new_cols);
700 }
701
702 let old_cursor_row = self.frame.cursor_row;
703 let old_cursor_col = self.frame.cursor_col;
704 let old_mode = self.frame.mode;
705
706 let (content_changed, ops_end) = if ops_present {
707 let ops_start = title_end;
708 if payload.len() < ops_start + 2 {
709 return false;
710 }
711 let (changed, consumed) = self
712 .apply_ops_payload(&payload[ops_start..])
713 .unwrap_or((false, 0));
714 (changed, ops_start + consumed)
715 } else {
716 let (changed, consumed) = self
717 .apply_legacy_patch_payload(&payload[title_end..])
718 .unwrap_or((false, 0));
719 (changed, title_end + consumed)
720 };
721
722 let mut after_strings = ops_end;
723 if strings_present {
724 after_strings = self.apply_overflow_strings(&payload[ops_end..]);
725 after_strings += ops_end;
726 }
727
728 let (line_flags_changed, after_line_flags) = if line_flags_present {
729 let lf_start = after_strings;
730 let lf_end = lf_start + new_rows as usize;
731 if payload.len() >= lf_end {
732 let new_flags = &payload[lf_start..lf_end];
733 let changed = self.frame.line_flags != new_flags;
734 self.frame.line_flags.clear();
735 self.frame.line_flags.extend_from_slice(new_flags);
736 (changed, lf_end)
737 } else {
738 (false, after_strings)
739 }
740 } else {
741 (false, after_strings)
742 };
743
744 if payload.len() >= after_line_flags + 4 {
746 self.frame.scrollback_lines = u32::from_le_bytes([
747 payload[after_line_flags],
748 payload[after_line_flags + 1],
749 payload[after_line_flags + 2],
750 payload[after_line_flags + 3],
751 ]);
752 }
753
754 self.frame.cursor_row = new_cursor_row.min(self.frame.rows.saturating_sub(1));
755 self.frame.cursor_col = new_cursor_col.min(self.frame.cols.saturating_sub(1));
756 self.frame.mode = new_mode;
757 resized
758 || title_changed
759 || content_changed
760 || line_flags_changed
761 || new_cursor_row != old_cursor_row
762 || new_cursor_col != old_cursor_col
763 || new_mode != old_mode
764 }
765
766 fn apply_legacy_patch_payload(&mut self, payload: &[u8]) -> Option<(bool, usize)> {
767 let total_cells = self.frame.rows as usize * self.frame.cols as usize;
768 let bitmask_len = total_cells.div_ceil(8);
769 if payload.len() < bitmask_len {
770 return None;
771 }
772 let bitmask = &payload[..bitmask_len];
773 let dirty_count = (0..total_cells)
774 .filter(|&i| bitmask[i / 8] & (1 << (i % 8)) != 0)
775 .count();
776 let data = &payload[bitmask_len..];
777 if data.len() < dirty_count * CELL_SIZE {
778 return None;
779 }
780 self.apply_patch_cells(bitmask, &data[..dirty_count * CELL_SIZE], dirty_count);
781 Some((dirty_count > 0, bitmask_len + dirty_count * CELL_SIZE))
782 }
783
784 fn apply_ops_payload(&mut self, payload: &[u8]) -> Option<(bool, usize)> {
785 if payload.len() < 2 {
786 return None;
787 }
788 let op_count = u16::from_le_bytes([payload[0], payload[1]]) as usize;
789 let total_cells = self.frame.rows as usize * self.frame.cols as usize;
790 let bitmask_len = total_cells.div_ceil(8);
791 let mut off = 2usize;
792 let mut changed = false;
793
794 for _ in 0..op_count {
795 if off >= payload.len() {
796 return None;
797 }
798 let op = payload[off];
799 off += 1;
800 match op {
801 OP_COPY_RECT => {
802 if payload.len() < off + 12 {
803 return None;
804 }
805 let src_row = u16::from_le_bytes([payload[off], payload[off + 1]]);
806 let src_col = u16::from_le_bytes([payload[off + 2], payload[off + 3]]);
807 let dst_row = u16::from_le_bytes([payload[off + 4], payload[off + 5]]);
808 let dst_col = u16::from_le_bytes([payload[off + 6], payload[off + 7]]);
809 let rows = u16::from_le_bytes([payload[off + 8], payload[off + 9]]);
810 let cols = u16::from_le_bytes([payload[off + 10], payload[off + 11]]);
811 off += 12;
812 changed |= self.apply_copy_rect(src_row, src_col, dst_row, dst_col, rows, cols);
813 }
814 OP_FILL_RECT => {
815 if payload.len() < off + 8 + CELL_SIZE {
816 return None;
817 }
818 let row = u16::from_le_bytes([payload[off], payload[off + 1]]);
819 let col = u16::from_le_bytes([payload[off + 2], payload[off + 3]]);
820 let rows = u16::from_le_bytes([payload[off + 4], payload[off + 5]]);
821 let cols = u16::from_le_bytes([payload[off + 6], payload[off + 7]]);
822 off += 8;
823 let mut cell = [0u8; CELL_SIZE];
824 cell.copy_from_slice(&payload[off..off + CELL_SIZE]);
825 off += CELL_SIZE;
826 changed |= self.apply_fill_rect(row, col, rows, cols, &cell);
827 }
828 OP_PATCH_CELLS => {
829 if payload.len() < off + bitmask_len {
830 return None;
831 }
832 let bitmask = &payload[off..off + bitmask_len];
833 off += bitmask_len;
834 let dirty_count = (0..total_cells)
835 .filter(|&i| bitmask[i / 8] & (1 << (i % 8)) != 0)
836 .count();
837 if payload.len() < off + dirty_count * CELL_SIZE {
838 return None;
839 }
840 self.apply_patch_cells(
841 bitmask,
842 &payload[off..off + dirty_count * CELL_SIZE],
843 dirty_count,
844 );
845 off += dirty_count * CELL_SIZE;
846 changed |= dirty_count > 0;
847 }
848 _ => return None,
849 }
850 }
851
852 Some((changed, off))
853 }
854
855 fn apply_patch_cells(&mut self, bitmask: &[u8], data: &[u8], dirty_count: usize) {
856 let total_cells = self.frame.rows as usize * self.frame.cols as usize;
857 let mut dirty_idx = 0usize;
858 for i in 0..total_cells {
859 if bitmask[i / 8] & (1 << (i % 8)) == 0 {
860 continue;
861 }
862 let cell_idx = i * CELL_SIZE;
863 for byte_pos in 0..CELL_SIZE {
864 self.frame.cells[cell_idx + byte_pos] = data[byte_pos * dirty_count + dirty_idx];
865 }
866 let new_content_len = (self.frame.cells[cell_idx + 1] >> 3) & 7;
869 if new_content_len != CONTENT_OVERFLOW {
870 self.frame.overflow.remove(&i);
871 }
872 dirty_idx += 1;
873 }
874 }
875
876 fn apply_copy_rect(
877 &mut self,
878 src_row: u16,
879 src_col: u16,
880 dst_row: u16,
881 dst_col: u16,
882 rows: u16,
883 cols: u16,
884 ) -> bool {
885 let rows = rows
886 .min(self.frame.rows.saturating_sub(src_row))
887 .min(self.frame.rows.saturating_sub(dst_row));
888 let cols = cols
889 .min(self.frame.cols.saturating_sub(src_col))
890 .min(self.frame.cols.saturating_sub(dst_col));
891 if rows == 0 || cols == 0 {
892 return false;
893 }
894
895 let frame_cols = self.frame.cols as usize;
896
897 let mut overflow_temp: Vec<(usize, String)> = Vec::new();
899 for r in 0..rows as usize {
900 for c in 0..cols as usize {
901 let src_flat = (src_row as usize + r) * frame_cols + src_col as usize + c;
902 if let Some(s) = self.frame.overflow.get(&src_flat) {
903 let dst_flat = (dst_row as usize + r) * frame_cols + dst_col as usize + c;
904 overflow_temp.push((dst_flat, s.clone()));
905 }
906 }
907 }
908
909 let mut temp = vec![0u8; rows as usize * cols as usize * CELL_SIZE];
910 for r in 0..rows as usize {
911 let src_off = self.frame.cell_offset(src_row + r as u16, src_col);
912 let src_end = src_off + cols as usize * CELL_SIZE;
913 let dst_off = r * cols as usize * CELL_SIZE;
914 temp[dst_off..dst_off + cols as usize * CELL_SIZE]
915 .copy_from_slice(&self.frame.cells[src_off..src_end]);
916 }
917 for r in 0..rows as usize {
918 let dst_off = self.frame.cell_offset(dst_row + r as u16, dst_col);
919 let dst_end = dst_off + cols as usize * CELL_SIZE;
920 let src_off = r * cols as usize * CELL_SIZE;
921 self.frame.cells[dst_off..dst_end]
922 .copy_from_slice(&temp[src_off..src_off + cols as usize * CELL_SIZE]);
923 }
924
925 for r in 0..rows as usize {
926 for c in 0..cols as usize {
927 let dst_flat = (dst_row as usize + r) * frame_cols + dst_col as usize + c;
928 self.frame.overflow.remove(&dst_flat);
929 }
930 }
931 for (idx, s) in overflow_temp {
932 self.frame.overflow.insert(idx, s);
933 }
934
935 true
936 }
937
938 fn apply_fill_rect(
939 &mut self,
940 row: u16,
941 col: u16,
942 rows: u16,
943 cols: u16,
944 cell: &[u8; CELL_SIZE],
945 ) -> bool {
946 let row_end = row.saturating_add(rows).min(self.frame.rows);
947 let col_end = col.saturating_add(cols).min(self.frame.cols);
948 let frame_cols = self.frame.cols as usize;
950 for r in row..row_end {
951 for c in col..col_end {
952 self.frame
953 .overflow
954 .remove(&(r as usize * frame_cols + c as usize));
955 }
956 }
957 if row >= row_end || col >= col_end {
958 return false;
959 }
960 for r in row..row_end {
961 for c in col..col_end {
962 let off = self.frame.cell_offset(r, c);
963 self.frame.cells[off..off + CELL_SIZE].copy_from_slice(cell);
964 }
965 }
966 true
967 }
968
969 fn apply_overflow_strings(&mut self, data: &[u8]) -> usize {
970 if data.len() < 2 {
971 return 0;
972 }
973 let count = u16::from_le_bytes([data[0], data[1]]) as usize;
974 let mut off = 2usize;
975 for _ in 0..count {
976 if off + 6 > data.len() {
977 break;
978 }
979 let cell_idx =
980 u32::from_le_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]])
981 as usize;
982 let len = u16::from_le_bytes([data[off + 4], data[off + 5]]) as usize;
983 off += 6;
984 if off + len > data.len() {
985 break;
986 }
987 if let Ok(s) = std::str::from_utf8(&data[off..off + len]) {
988 self.frame.overflow.insert(cell_idx, s.to_owned());
989 }
990 off += len;
991 }
992 off
993 }
994}
995
996#[derive(Clone, Debug)]
997pub enum Node {
998 Fill {
999 rect: Rect,
1000 ch: char,
1001 style: CellStyle,
1002 },
1003 Text {
1004 row: u16,
1005 col: u16,
1006 text: String,
1007 style: CellStyle,
1008 },
1009 WrappedText {
1010 rect: Rect,
1011 text: String,
1012 style: CellStyle,
1013 },
1014 ScrollingText {
1015 rect: Rect,
1016 lines: Vec<String>,
1017 offset_from_bottom: usize,
1018 style: CellStyle,
1019 },
1020}
1021
1022#[derive(Clone, Debug, Default)]
1023pub struct Dom {
1024 background: CellStyle,
1025 title: Option<String>,
1026 nodes: Vec<Node>,
1027}
1028
1029impl Dom {
1030 pub fn new() -> Self {
1031 Self::default()
1032 }
1033
1034 pub fn clear(&mut self) {
1035 self.title = None;
1036 self.nodes.clear();
1037 }
1038
1039 pub fn set_background(&mut self, style: CellStyle) {
1040 self.background = style;
1041 }
1042
1043 pub fn set_title(&mut self, title: impl Into<String>) {
1044 self.title = Some(title.into());
1045 }
1046
1047 pub fn fill(&mut self, rect: Rect, ch: char, style: CellStyle) {
1048 self.nodes.push(Node::Fill { rect, ch, style });
1049 }
1050
1051 pub fn text(&mut self, row: u16, col: u16, text: impl Into<String>, style: CellStyle) {
1052 self.nodes.push(Node::Text {
1053 row,
1054 col,
1055 text: text.into(),
1056 style,
1057 });
1058 }
1059
1060 pub fn wrapped_text(&mut self, rect: Rect, text: impl Into<String>, style: CellStyle) {
1061 self.nodes.push(Node::WrappedText {
1062 rect,
1063 text: text.into(),
1064 style,
1065 });
1066 }
1067
1068 pub fn scrolling_text<S, I>(
1069 &mut self,
1070 rect: Rect,
1071 lines: I,
1072 offset_from_bottom: usize,
1073 style: CellStyle,
1074 ) where
1075 S: Into<String>,
1076 I: IntoIterator<Item = S>,
1077 {
1078 self.nodes.push(Node::ScrollingText {
1079 rect,
1080 lines: lines.into_iter().map(Into::into).collect(),
1081 offset_from_bottom,
1082 style,
1083 });
1084 }
1085
1086 pub fn render_to(&self, frame: &mut FrameState) {
1087 frame.clear(self.background);
1088 frame.set_title(self.title.clone().unwrap_or_default());
1089 for node in &self.nodes {
1090 match node {
1091 Node::Fill { rect, ch, style } => frame.fill_rect(*rect, *ch, *style),
1092 Node::Text {
1093 row,
1094 col,
1095 text,
1096 style,
1097 } => {
1098 frame.write_text(*row, *col, text, *style);
1099 }
1100 Node::WrappedText { rect, text, style } => {
1101 frame.write_wrapped_text(*rect, text, *style);
1102 }
1103 Node::ScrollingText {
1104 rect,
1105 lines,
1106 offset_from_bottom,
1107 style,
1108 } => {
1109 frame.write_scrolling_text(*rect, lines, *offset_from_bottom, *style);
1110 }
1111 }
1112 }
1113 }
1114}
1115
1116#[derive(Clone, Debug)]
1117pub struct CallbackRenderer {
1118 dom: Dom,
1119 frame: FrameState,
1120}
1121
1122impl CallbackRenderer {
1123 pub fn new(rows: u16, cols: u16) -> Self {
1124 Self {
1125 dom: Dom::new(),
1126 frame: FrameState::new(rows, cols),
1127 }
1128 }
1129
1130 pub fn resize(&mut self, rows: u16, cols: u16) {
1131 self.frame.resize(rows, cols);
1132 }
1133
1134 pub fn frame(&self) -> &FrameState {
1135 &self.frame
1136 }
1137
1138 pub fn render<F>(&mut self, render: F) -> &FrameState
1139 where
1140 F: FnOnce(&mut Dom),
1141 {
1142 self.dom.clear();
1143 render(&mut self.dom);
1144 self.dom.render_to(&mut self.frame);
1145 &self.frame
1146 }
1147}
1148
1149pub enum ServerMsg<'a> {
1150 Hello {
1151 version: u16,
1152 features: u32,
1153 },
1154 Update {
1155 pty_id: u16,
1156 payload: &'a [u8],
1157 },
1158 Created {
1159 pty_id: u16,
1160 tag: &'a str,
1161 },
1162 CreatedN {
1163 nonce: u16,
1164 pty_id: u16,
1165 tag: &'a str,
1166 },
1167 Closed {
1168 pty_id: u16,
1169 },
1170 Exited {
1171 pty_id: u16,
1172 exit_status: i32,
1173 },
1174 List {
1175 entries: Vec<PtyListEntry<'a>>,
1176 },
1177 Title {
1178 pty_id: u16,
1179 title: &'a [u8],
1180 },
1181 SearchResults {
1182 request_id: u16,
1183 results: Vec<SearchResultEntry<'a>>,
1184 },
1185 Ready,
1186 Text {
1187 nonce: u16,
1188 pty_id: u16,
1189 total_lines: u32,
1190 offset: u32,
1191 text: &'a str,
1192 },
1193}
1194
1195#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1196pub struct PtyListEntry<'a> {
1197 pub pty_id: u16,
1198 pub tag: &'a str,
1199}
1200
1201#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1202pub struct SearchResultEntry<'a> {
1203 pub pty_id: u16,
1204 pub score: u32,
1205 pub primary_source: u8,
1206 pub matched_sources: u8,
1207 pub scroll_offset: Option<u32>,
1208 pub context: &'a [u8],
1209}
1210
1211pub fn parse_server_msg(data: &[u8]) -> Option<ServerMsg<'_>> {
1212 if data.is_empty() {
1213 return None;
1214 }
1215 match data[0] {
1216 S2C_HELLO => {
1217 if data.len() < 7 {
1218 return None;
1219 }
1220 let version = u16::from_le_bytes([data[1], data[2]]);
1221 let features = u32::from_le_bytes([data[3], data[4], data[5], data[6]]);
1222 Some(ServerMsg::Hello { version, features })
1223 }
1224 S2C_UPDATE => {
1225 if data.len() < 3 {
1226 return None;
1227 }
1228 Some(ServerMsg::Update {
1229 pty_id: u16::from_le_bytes([data[1], data[2]]),
1230 payload: &data[3..],
1231 })
1232 }
1233 S2C_CREATED => {
1234 if data.len() < 3 {
1235 return None;
1236 }
1237 let tag = std::str::from_utf8(data.get(3..).unwrap_or_default()).unwrap_or_default();
1238 Some(ServerMsg::Created {
1239 pty_id: u16::from_le_bytes([data[1], data[2]]),
1240 tag,
1241 })
1242 }
1243 S2C_CREATED_N => {
1244 if data.len() < 5 {
1245 return None;
1246 }
1247 let nonce = u16::from_le_bytes([data[1], data[2]]);
1248 let pty_id = u16::from_le_bytes([data[3], data[4]]);
1249 let tag = std::str::from_utf8(data.get(5..).unwrap_or_default()).unwrap_or_default();
1250 Some(ServerMsg::CreatedN { nonce, pty_id, tag })
1251 }
1252 S2C_CLOSED => {
1253 if data.len() < 3 {
1254 return None;
1255 }
1256 Some(ServerMsg::Closed {
1257 pty_id: u16::from_le_bytes([data[1], data[2]]),
1258 })
1259 }
1260 S2C_EXITED => {
1261 if data.len() < 7 {
1262 return None;
1263 }
1264 Some(ServerMsg::Exited {
1265 pty_id: u16::from_le_bytes([data[1], data[2]]),
1266 exit_status: i32::from_le_bytes([data[3], data[4], data[5], data[6]]),
1267 })
1268 }
1269 S2C_LIST => {
1270 if data.len() < 3 {
1271 return None;
1272 }
1273 let count = u16::from_le_bytes([data[1], data[2]]) as usize;
1274 let mut entries = Vec::with_capacity(count);
1275 let mut offset = 3;
1276 for _ in 0..count {
1277 if offset + 4 > data.len() {
1278 break;
1279 }
1280 let pty_id = u16::from_le_bytes([data[offset], data[offset + 1]]);
1281 let tag_len = u16::from_le_bytes([data[offset + 2], data[offset + 3]]) as usize;
1282 offset += 4;
1283 if offset + tag_len > data.len() {
1284 break;
1285 }
1286 let tag = std::str::from_utf8(&data[offset..offset + tag_len]).unwrap_or_default();
1287 offset += tag_len;
1288 entries.push(PtyListEntry { pty_id, tag });
1289 }
1290 Some(ServerMsg::List { entries })
1291 }
1292 S2C_TITLE => {
1293 if data.len() < 3 {
1294 return None;
1295 }
1296 Some(ServerMsg::Title {
1297 pty_id: u16::from_le_bytes([data[1], data[2]]),
1298 title: &data[3..],
1299 })
1300 }
1301 S2C_SEARCH_RESULTS => {
1302 if data.len() < 5 {
1303 return None;
1304 }
1305 let request_id = u16::from_le_bytes([data[1], data[2]]);
1306 let count = u16::from_le_bytes([data[3], data[4]]) as usize;
1307 let mut results = Vec::with_capacity(count);
1308 let mut offset = 5usize;
1309 for _ in 0..count {
1310 if offset + 14 > data.len() {
1311 return None;
1312 }
1313 let pty_id = u16::from_le_bytes([data[offset], data[offset + 1]]);
1314 let score = u32::from_le_bytes([
1315 data[offset + 2],
1316 data[offset + 3],
1317 data[offset + 4],
1318 data[offset + 5],
1319 ]);
1320 let primary_source = data[offset + 6];
1321 let matched_sources = data[offset + 7];
1322 let scroll_offset = u32::from_le_bytes([
1323 data[offset + 8],
1324 data[offset + 9],
1325 data[offset + 10],
1326 data[offset + 11],
1327 ]);
1328 let context_len =
1329 u16::from_le_bytes([data[offset + 12], data[offset + 13]]) as usize;
1330 offset += 14;
1331 if offset + context_len > data.len() {
1332 return None;
1333 }
1334 results.push(SearchResultEntry {
1335 pty_id,
1336 score,
1337 primary_source,
1338 matched_sources,
1339 scroll_offset: if scroll_offset == u32::MAX {
1340 None
1341 } else {
1342 Some(scroll_offset)
1343 },
1344 context: &data[offset..offset + context_len],
1345 });
1346 offset += context_len;
1347 }
1348 Some(ServerMsg::SearchResults {
1349 request_id,
1350 results,
1351 })
1352 }
1353 S2C_READY => Some(ServerMsg::Ready),
1354 S2C_TEXT => {
1355 if data.len() < 13 {
1356 return None;
1357 }
1358 let nonce = u16::from_le_bytes([data[1], data[2]]);
1359 let pty_id = u16::from_le_bytes([data[3], data[4]]);
1360 let total_lines = u32::from_le_bytes([data[5], data[6], data[7], data[8]]);
1361 let offset = u32::from_le_bytes([data[9], data[10], data[11], data[12]]);
1362 let text = std::str::from_utf8(data.get(13..).unwrap_or_default()).unwrap_or_default();
1363 Some(ServerMsg::Text {
1364 nonce,
1365 pty_id,
1366 total_lines,
1367 offset,
1368 text,
1369 })
1370 }
1371 _ => None,
1372 }
1373}
1374
1375pub fn msg_hello(version: u16, features: u32) -> Vec<u8> {
1376 let mut msg = Vec::with_capacity(7);
1377 msg.push(S2C_HELLO);
1378 msg.extend_from_slice(&version.to_le_bytes());
1379 msg.extend_from_slice(&features.to_le_bytes());
1380 msg
1381}
1382
1383pub fn msg_create(rows: u16, cols: u16) -> Vec<u8> {
1384 msg_create_tagged(rows, cols, "")
1385}
1386
1387pub fn msg_create_tagged(rows: u16, cols: u16, tag: &str) -> Vec<u8> {
1388 let tag_bytes = tag.as_bytes();
1389 let tag_len = tag_bytes.len().min(u16::MAX as usize);
1390 let mut msg = Vec::with_capacity(7 + tag_len);
1391 msg.push(C2S_CREATE);
1392 msg.extend_from_slice(&rows.to_le_bytes());
1393 msg.extend_from_slice(&cols.to_le_bytes());
1394 msg.extend_from_slice(&(tag_len as u16).to_le_bytes());
1395 msg.extend_from_slice(&tag_bytes[..tag_len]);
1396 msg
1397}
1398
1399pub fn msg_create_at(rows: u16, cols: u16, tag: &str, src_pty_id: u16) -> Vec<u8> {
1401 let tag_bytes = tag.as_bytes();
1402 let tag_len = tag_bytes.len().min(u16::MAX as usize);
1403 let mut msg = Vec::with_capacity(9 + tag_len);
1404 msg.push(C2S_CREATE_AT);
1405 msg.extend_from_slice(&rows.to_le_bytes());
1406 msg.extend_from_slice(&cols.to_le_bytes());
1407 msg.extend_from_slice(&(tag_len as u16).to_le_bytes());
1408 msg.extend_from_slice(&tag_bytes[..tag_len]);
1409 msg.extend_from_slice(&src_pty_id.to_le_bytes());
1410 msg
1411}
1412
1413pub fn msg_create_n(nonce: u16, rows: u16, cols: u16, tag: &str) -> Vec<u8> {
1414 let tag_bytes = tag.as_bytes();
1415 let tag_len = tag_bytes.len().min(u16::MAX as usize);
1416 let mut msg = Vec::with_capacity(9 + tag_len);
1417 msg.push(C2S_CREATE_N);
1418 msg.extend_from_slice(&nonce.to_le_bytes());
1419 msg.extend_from_slice(&rows.to_le_bytes());
1420 msg.extend_from_slice(&cols.to_le_bytes());
1421 msg.extend_from_slice(&(tag_len as u16).to_le_bytes());
1422 msg.extend_from_slice(&tag_bytes[..tag_len]);
1423 msg
1424}
1425
1426pub fn msg_create_n_command(nonce: u16, rows: u16, cols: u16, tag: &str, command: &str) -> Vec<u8> {
1427 let mut msg = msg_create_n(nonce, rows, cols, tag);
1428 msg.extend_from_slice(command.as_bytes());
1429 msg
1430}
1431
1432pub fn msg_create_command(rows: u16, cols: u16, command: &str) -> Vec<u8> {
1433 msg_create_tagged_command(rows, cols, "", command)
1434}
1435
1436pub fn msg_create_tagged_command(rows: u16, cols: u16, tag: &str, command: &str) -> Vec<u8> {
1437 let mut msg = msg_create_tagged(rows, cols, tag);
1438 msg.extend_from_slice(command.as_bytes());
1439 msg
1440}
1441
1442pub fn msg_input(pty_id: u16, data: &[u8]) -> Vec<u8> {
1443 let mut msg = Vec::with_capacity(3 + data.len());
1444 msg.push(C2S_INPUT);
1445 msg.extend_from_slice(&pty_id.to_le_bytes());
1446 msg.extend_from_slice(data);
1447 msg
1448}
1449
1450pub fn msg_resize(pty_id: u16, rows: u16, cols: u16) -> Vec<u8> {
1451 let mut msg = Vec::with_capacity(7);
1452 msg.push(C2S_RESIZE);
1453 msg.extend_from_slice(&pty_id.to_le_bytes());
1454 msg.extend_from_slice(&rows.to_le_bytes());
1455 msg.extend_from_slice(&cols.to_le_bytes());
1456 msg
1457}
1458
1459pub fn msg_resize_batch(entries: &[(u16, u16, u16)]) -> Vec<u8> {
1460 let mut msg = Vec::with_capacity(1 + entries.len() * 6);
1461 msg.push(C2S_RESIZE);
1462 for &(pty_id, rows, cols) in entries {
1463 msg.extend_from_slice(&pty_id.to_le_bytes());
1464 msg.extend_from_slice(&rows.to_le_bytes());
1465 msg.extend_from_slice(&cols.to_le_bytes());
1466 }
1467 msg
1468}
1469
1470pub fn msg_focus(pty_id: u16) -> Vec<u8> {
1471 let mut msg = Vec::with_capacity(3);
1472 msg.push(C2S_FOCUS);
1473 msg.extend_from_slice(&pty_id.to_le_bytes());
1474 msg
1475}
1476
1477pub fn msg_close(pty_id: u16) -> Vec<u8> {
1478 let mut msg = Vec::with_capacity(3);
1479 msg.push(C2S_CLOSE);
1480 msg.extend_from_slice(&pty_id.to_le_bytes());
1481 msg
1482}
1483
1484pub fn msg_restart(pty_id: u16) -> Vec<u8> {
1485 let mut msg = Vec::with_capacity(3);
1486 msg.push(C2S_RESTART);
1487 msg.extend_from_slice(&pty_id.to_le_bytes());
1488 msg
1489}
1490
1491pub fn msg_subscribe(pty_id: u16) -> Vec<u8> {
1492 let mut msg = Vec::with_capacity(3);
1493 msg.push(C2S_SUBSCRIBE);
1494 msg.extend_from_slice(&pty_id.to_le_bytes());
1495 msg
1496}
1497
1498pub fn msg_unsubscribe(pty_id: u16) -> Vec<u8> {
1499 let mut msg = Vec::with_capacity(3);
1500 msg.push(C2S_UNSUBSCRIBE);
1501 msg.extend_from_slice(&pty_id.to_le_bytes());
1502 msg
1503}
1504
1505pub fn msg_search(request_id: u16, query: &str) -> Vec<u8> {
1506 let query = query.as_bytes();
1507 let mut msg = Vec::with_capacity(3 + query.len());
1508 msg.push(C2S_SEARCH);
1509 msg.extend_from_slice(&request_id.to_le_bytes());
1510 msg.extend_from_slice(query);
1511 msg
1512}
1513
1514pub fn msg_ack() -> Vec<u8> {
1515 vec![C2S_ACK]
1516}
1517
1518pub fn msg_scroll(pty_id: u16, offset: u32) -> Vec<u8> {
1519 let mut msg = Vec::with_capacity(7);
1520 msg.push(C2S_SCROLL);
1521 msg.extend_from_slice(&pty_id.to_le_bytes());
1522 msg.extend_from_slice(&offset.to_le_bytes());
1523 msg
1524}
1525
1526pub fn msg_display_rate(fps: u16) -> Vec<u8> {
1527 let mut msg = Vec::with_capacity(3);
1528 msg.push(C2S_DISPLAY_RATE);
1529 msg.extend_from_slice(&fps.to_le_bytes());
1530 msg
1531}
1532
1533pub fn msg_client_metrics(backlog: u16, ack_ahead: u16, apply_ms_x10: u16) -> Vec<u8> {
1534 let mut msg = Vec::with_capacity(7);
1535 msg.push(C2S_CLIENT_METRICS);
1536 msg.extend_from_slice(&backlog.to_le_bytes());
1537 msg.extend_from_slice(&ack_ahead.to_le_bytes());
1538 msg.extend_from_slice(&apply_ms_x10.to_le_bytes());
1539 msg
1540}
1541
1542pub fn msg_read(nonce: u16, pty_id: u16, offset: u32, limit: u32, flags: u8) -> Vec<u8> {
1543 let mut msg = Vec::with_capacity(14);
1544 msg.push(C2S_READ);
1545 msg.extend_from_slice(&nonce.to_le_bytes());
1546 msg.extend_from_slice(&pty_id.to_le_bytes());
1547 msg.extend_from_slice(&offset.to_le_bytes());
1548 msg.extend_from_slice(&limit.to_le_bytes());
1549 msg.push(flags);
1550 msg
1551}
1552
1553pub fn msg_exited(pty_id: u16, exit_status: i32) -> Vec<u8> {
1554 let mut msg = Vec::with_capacity(7);
1555 msg.push(S2C_EXITED);
1556 msg.extend_from_slice(&pty_id.to_le_bytes());
1557 msg.extend_from_slice(&exit_status.to_le_bytes());
1558 msg
1559}
1560
1561fn push_sgr(out: &mut String, style: &CellStyle) {
1562 use std::fmt::Write;
1563 out.push_str("\x1b[0");
1564 if style.bold {
1565 out.push_str(";1");
1566 }
1567 if style.dim {
1568 out.push_str(";2");
1569 }
1570 if style.italic {
1571 out.push_str(";3");
1572 }
1573 if style.underline {
1574 out.push_str(";4");
1575 }
1576 if style.inverse {
1577 out.push_str(";7");
1578 }
1579 match style.fg {
1580 Color::Indexed(n) => {
1581 let _ = write!(out, ";38;5;{n}");
1582 }
1583 Color::Rgb(r, g, b) => {
1584 let _ = write!(out, ";38;2;{r};{g};{b}");
1585 }
1586 Color::Default => {}
1587 }
1588 match style.bg {
1589 Color::Indexed(n) => {
1590 let _ = write!(out, ";48;5;{n}");
1591 }
1592 Color::Rgb(r, g, b) => {
1593 let _ = write!(out, ";48;2;{r};{g};{b}");
1594 }
1595 Color::Default => {}
1596 }
1597 out.push('m');
1598}
1599
1600const MODE_ALT_SCREEN: u16 = 1 << 11;
1601
1602fn mode_is_cooked(mode: u16) -> bool {
1603 mode & MODE_ECHO != 0 && mode & MODE_ICANON != 0 && mode & MODE_ALT_SCREEN == 0
1604}
1605
1606pub fn build_update_msg(
1607 pty_id: u16,
1608 current: &FrameState,
1609 previous: &FrameState,
1610) -> Option<Vec<u8>> {
1611 let title_changed = current.title != previous.title;
1612 let same_size = previous.rows == current.rows
1613 && previous.cols == current.cols
1614 && previous.cells.len() == current.cells.len();
1615
1616 let mut ops = Vec::new();
1618 let mut op_count = 0u16;
1619
1620 let scroll_eligible = (mode_is_cooked(current.mode) && mode_is_cooked(previous.mode))
1624 || current.mode == 0
1625 || previous.mode == 0;
1626 if ENABLE_SCROLL_OPS && same_size && previous.cells != current.cells && scroll_eligible {
1627 if let Some(delta_rows) = detect_vertical_scroll(current, previous) {
1628 let mut basis = previous.clone();
1629 encode_copy_rect_op(&mut ops, current, delta_rows);
1630 apply_vertical_scroll_copy(&mut basis, delta_rows);
1631 op_count += 1;
1632 append_full_width_fill_ops(current, &mut basis, &mut ops, &mut op_count);
1633 if let Some(patch_op) = build_patch_op(current, &basis) {
1634 ops.extend_from_slice(&patch_op);
1635 op_count += 1;
1636 }
1637 }
1638 }
1639
1640 if op_count == 0 {
1642 let basis = if same_size {
1643 previous
1644 } else {
1645 &FrameState::new(current.rows, current.cols)
1646 };
1647 if let Some(patch_op) = build_patch_op(current, basis) {
1648 ops = patch_op;
1649 op_count = 1;
1650 }
1651 }
1652
1653 if op_count == 0 {
1654 if !title_changed
1656 && current.cursor_row == previous.cursor_row
1657 && current.cursor_col == previous.cursor_col
1658 && current.mode == previous.mode
1659 {
1660 return None;
1661 }
1662 }
1663
1664 let has_overflow = !current.overflow.is_empty();
1669 let overflow_section = if has_overflow {
1670 serialize_overflow_strings(current)
1671 } else {
1672 Vec::new()
1673 };
1674
1675 let line_flags_changed =
1676 current.line_flags != previous.line_flags || current.rows != previous.rows;
1677 let has_line_flags = line_flags_changed && !current.line_flags.iter().all(|&f| f == 0);
1678
1679 let title_bytes = if title_changed {
1680 current.title.as_bytes()
1681 } else {
1682 &[]
1683 };
1684 let title_len = title_bytes.len().min(TITLE_LEN_MASK as usize);
1685 let title_field = OPS_PRESENT
1686 | if has_overflow { STRINGS_PRESENT } else { 0 }
1687 | if has_line_flags {
1688 LINE_FLAGS_PRESENT
1689 } else {
1690 0
1691 }
1692 | if title_changed {
1693 TITLE_PRESENT | title_len as u16
1694 } else {
1695 0
1696 };
1697
1698 let mut payload = Vec::with_capacity(
1699 12 + title_len
1700 + 2
1701 + ops.len()
1702 + overflow_section.len()
1703 + if has_line_flags {
1704 current.rows as usize
1705 } else {
1706 0
1707 }
1708 + 4,
1709 );
1710 payload.extend_from_slice(¤t.rows.to_le_bytes());
1711 payload.extend_from_slice(¤t.cols.to_le_bytes());
1712 payload.extend_from_slice(¤t.cursor_row.to_le_bytes());
1713 payload.extend_from_slice(¤t.cursor_col.to_le_bytes());
1714 payload.extend_from_slice(¤t.mode.to_le_bytes());
1715 payload.extend_from_slice(&title_field.to_le_bytes());
1716 if title_changed {
1717 payload.extend_from_slice(&title_bytes[..title_len]);
1718 }
1719 payload.extend_from_slice(&op_count.to_le_bytes());
1720 payload.extend_from_slice(&ops);
1721 payload.extend_from_slice(&overflow_section);
1722 if has_line_flags {
1723 payload.extend_from_slice(¤t.line_flags);
1724 }
1725 payload.extend_from_slice(¤t.scrollback_lines.to_le_bytes());
1727
1728 let compressed = compress_prepend_size(&payload);
1729 let mut msg = Vec::with_capacity(3 + compressed.len());
1730 msg.push(S2C_UPDATE);
1731 msg.extend_from_slice(&pty_id.to_le_bytes());
1732 msg.extend_from_slice(&compressed);
1733 Some(msg)
1734}
1735
1736fn serialize_overflow_strings(frame: &FrameState) -> Vec<u8> {
1738 let count = frame.overflow.len().min(u16::MAX as usize);
1739 let mut out = Vec::new();
1740 out.extend_from_slice(&(count as u16).to_le_bytes());
1741 for (&cell_idx, s) in frame.overflow.iter().take(count) {
1742 let bytes = s.as_bytes();
1743 let len = bytes.len().min(u16::MAX as usize);
1744 out.extend_from_slice(&(cell_idx as u32).to_le_bytes());
1745 out.extend_from_slice(&(len as u16).to_le_bytes());
1746 out.extend_from_slice(&bytes[..len]);
1747 }
1748 out
1749}
1750
1751fn build_patch_op(current: &FrameState, previous: &FrameState) -> Option<Vec<u8>> {
1752 let total_cells = current.rows as usize * current.cols as usize;
1753 let bitmask_len = total_cells.div_ceil(8);
1754 let mut bitmask = vec![0u8; bitmask_len];
1755 let mut dirty_count = 0usize;
1756 for i in 0..total_cells {
1757 let off = i * CELL_SIZE;
1758 if current.cells[off..off + CELL_SIZE] != previous.cells[off..off + CELL_SIZE] {
1759 bitmask[i / 8] |= 1 << (i % 8);
1760 dirty_count += 1;
1761 }
1762 }
1763 if dirty_count == 0 {
1764 return None;
1765 }
1766
1767 let mut op = Vec::with_capacity(1 + bitmask_len + dirty_count * CELL_SIZE);
1768 op.push(OP_PATCH_CELLS);
1769 op.extend_from_slice(&bitmask);
1770 for byte_pos in 0..CELL_SIZE {
1771 for i in 0..total_cells {
1772 if bitmask[i / 8] & (1 << (i % 8)) != 0 {
1773 op.push(current.cells[i * CELL_SIZE + byte_pos]);
1774 }
1775 }
1776 }
1777 Some(op)
1778}
1779
1780fn detect_vertical_scroll(current: &FrameState, previous: &FrameState) -> Option<i16> {
1781 let rows = current.rows as usize;
1782 let cols = current.cols as usize;
1783 if rows < 4 || cols == 0 {
1784 return None;
1785 }
1786 let row_bytes = cols * CELL_SIZE;
1787 let max_delta = rows.saturating_sub(1).min(8);
1788 let mut best: Option<(usize, i16)> = None;
1789
1790 for delta in 1..=max_delta {
1791 let overlap = rows - delta;
1792 if overlap < 3 {
1793 continue;
1794 }
1795 for signed_delta in [-(delta as i16), delta as i16] {
1796 let mut matched = 0usize;
1797 for row in 0..rows {
1798 let src_row = row as i32 - signed_delta as i32;
1799 if src_row < 0 || src_row >= rows as i32 {
1800 continue;
1801 }
1802 let cur_off = row * row_bytes;
1803 let prev_off = src_row as usize * row_bytes;
1804 if current.cells[cur_off..cur_off + row_bytes]
1805 == previous.cells[prev_off..prev_off + row_bytes]
1806 {
1807 matched += 1;
1808 }
1809 }
1810 if matched * 5 < overlap * 4 {
1811 continue;
1812 }
1813 let replace = match best {
1814 None => true,
1815 Some((best_matched, best_delta)) => {
1816 matched > best_matched
1817 || (matched == best_matched
1818 && signed_delta.unsigned_abs() < best_delta.unsigned_abs())
1819 }
1820 };
1821 if replace {
1822 best = Some((matched, signed_delta));
1823 }
1824 }
1825 }
1826
1827 best.map(|(_, delta)| delta)
1828}
1829
1830fn encode_copy_rect_op(out: &mut Vec<u8>, current: &FrameState, delta_rows: i16) {
1831 let rows = current.rows;
1832 let cols = current.cols;
1833 let delta = delta_rows.unsigned_abs();
1834 let (src_row, dst_row, copy_rows) = if delta_rows > 0 {
1835 (0, delta, rows.saturating_sub(delta))
1836 } else {
1837 (delta, 0, rows.saturating_sub(delta))
1838 };
1839 out.push(OP_COPY_RECT);
1840 out.extend_from_slice(&src_row.to_le_bytes());
1841 out.extend_from_slice(&0u16.to_le_bytes());
1842 out.extend_from_slice(&dst_row.to_le_bytes());
1843 out.extend_from_slice(&0u16.to_le_bytes());
1844 out.extend_from_slice(©_rows.to_le_bytes());
1845 out.extend_from_slice(&cols.to_le_bytes());
1846}
1847
1848fn apply_vertical_scroll_copy(frame: &mut FrameState, delta_rows: i16) {
1849 let delta = delta_rows.unsigned_abs();
1850 if delta == 0 || delta >= frame.rows {
1851 return;
1852 }
1853 let (src_row, dst_row, rows) = if delta_rows > 0 {
1854 (0, delta, frame.rows - delta)
1855 } else {
1856 (delta, 0, frame.rows - delta)
1857 };
1858 apply_copy_rect_frame(frame, src_row, 0, dst_row, 0, rows, frame.cols);
1859}
1860
1861fn apply_copy_rect_frame(
1862 frame: &mut FrameState,
1863 src_row: u16,
1864 src_col: u16,
1865 dst_row: u16,
1866 dst_col: u16,
1867 rows: u16,
1868 cols: u16,
1869) {
1870 let rows = rows
1871 .min(frame.rows.saturating_sub(src_row))
1872 .min(frame.rows.saturating_sub(dst_row));
1873 let cols = cols
1874 .min(frame.cols.saturating_sub(src_col))
1875 .min(frame.cols.saturating_sub(dst_col));
1876 if rows == 0 || cols == 0 {
1877 return;
1878 }
1879 let mut temp = vec![0u8; rows as usize * cols as usize * CELL_SIZE];
1880 for r in 0..rows as usize {
1881 let src_off = frame.cell_offset(src_row + r as u16, src_col);
1882 let src_end = src_off + cols as usize * CELL_SIZE;
1883 let dst_off = r * cols as usize * CELL_SIZE;
1884 temp[dst_off..dst_off + cols as usize * CELL_SIZE]
1885 .copy_from_slice(&frame.cells[src_off..src_end]);
1886 }
1887 for r in 0..rows as usize {
1888 let dst_off = frame.cell_offset(dst_row + r as u16, dst_col);
1889 let dst_end = dst_off + cols as usize * CELL_SIZE;
1890 let src_off = r * cols as usize * CELL_SIZE;
1891 frame.cells[dst_off..dst_end]
1892 .copy_from_slice(&temp[src_off..src_off + cols as usize * CELL_SIZE]);
1893 }
1894}
1895
1896fn append_full_width_fill_ops(
1897 current: &FrameState,
1898 basis: &mut FrameState,
1899 out: &mut Vec<u8>,
1900 op_count: &mut u16,
1901) {
1902 let rows = current.rows as usize;
1903 let cols = current.cols as usize;
1904 if rows == 0 || cols == 0 {
1905 return;
1906 }
1907
1908 let row_bytes = cols * CELL_SIZE;
1909 let mut row = 0usize;
1910 while row < rows {
1911 let row_off = row * row_bytes;
1912 if current.cells[row_off..row_off + row_bytes] == basis.cells[row_off..row_off + row_bytes]
1913 {
1914 row += 1;
1915 continue;
1916 }
1917 let Some(cell) = uniform_row_cell(current, row) else {
1918 row += 1;
1919 continue;
1920 };
1921 let mut end = row + 1;
1922 while end < rows {
1923 if uniform_row_cell(current, end).as_ref() != Some(&cell) {
1924 break;
1925 }
1926 end += 1;
1927 }
1928
1929 if *op_count == u16::MAX {
1930 break;
1931 }
1932 out.push(OP_FILL_RECT);
1933 out.extend_from_slice(&(row as u16).to_le_bytes());
1934 out.extend_from_slice(&0u16.to_le_bytes());
1935 out.extend_from_slice(&((end - row) as u16).to_le_bytes());
1936 out.extend_from_slice(¤t.cols.to_le_bytes());
1937 out.extend_from_slice(&cell);
1938 *op_count = op_count.saturating_add(1);
1939
1940 for r in row..end {
1941 let row_off = basis.cell_offset(r as u16, 0);
1942 for c in 0..cols {
1943 let off = row_off + c * CELL_SIZE;
1944 basis.cells[off..off + CELL_SIZE].copy_from_slice(&cell);
1945 }
1946 }
1947
1948 row = end;
1949 }
1950}
1951
1952fn uniform_row_cell(frame: &FrameState, row: usize) -> Option<[u8; CELL_SIZE]> {
1953 let cols = frame.cols as usize;
1954 if row >= frame.rows as usize || cols == 0 {
1955 return None;
1956 }
1957 let start = row * cols * CELL_SIZE;
1958 let mut first = [0u8; CELL_SIZE];
1959 first.copy_from_slice(&frame.cells[start..start + CELL_SIZE]);
1960 if first[1] & 0b110 != 0 {
1961 return None;
1962 }
1963 for col in 1..cols {
1964 let off = start + col * CELL_SIZE;
1965 if frame.cells[off..off + CELL_SIZE] != first {
1966 return None;
1967 }
1968 }
1969 Some(first)
1970}
1971
1972fn encode_cell(dst: &mut [u8], ch: Option<char>, style: CellStyle, wide: bool, wide_cont: bool) {
1973 dst.fill(0);
1974
1975 let mut f0 = 0u8;
1976 encode_color(style.fg, &mut f0, &mut dst[2..5], false);
1977 encode_color(style.bg, &mut f0, &mut dst[5..8], true);
1978 if style.bold {
1979 f0 |= 1 << 4;
1980 }
1981 if style.dim {
1982 f0 |= 1 << 5;
1983 }
1984 if style.italic {
1985 f0 |= 1 << 6;
1986 }
1987 if style.underline {
1988 f0 |= 1 << 7;
1989 }
1990 dst[0] = f0;
1991
1992 let mut f1 = 0u8;
1993 if style.inverse {
1994 f1 |= 1;
1995 }
1996 if wide {
1997 f1 |= 1 << 1;
1998 }
1999 if wide_cont {
2000 f1 |= 1 << 2;
2001 }
2002 if let Some(ch) = ch {
2003 let mut buf = [0u8; 4];
2004 let encoded = ch.encode_utf8(&mut buf).as_bytes();
2005 let len = encoded.len().min(4);
2006 dst[8..8 + len].copy_from_slice(&encoded[..len]);
2007 f1 |= (len as u8) << 3;
2008 }
2009 dst[1] = f1;
2010}
2011
2012fn encode_color(color: Color, flags: &mut u8, dst: &mut [u8], is_bg: bool) {
2013 let shift = if is_bg { 2 } else { 0 };
2014 match color {
2015 Color::Default => {}
2016 Color::Indexed(idx) => {
2017 *flags |= 1 << shift;
2018 dst[0] = idx;
2019 }
2020 Color::Rgb(r, g, b) => {
2021 *flags |= 2 << shift;
2022 dst[0] = r;
2023 dst[1] = g;
2024 dst[2] = b;
2025 }
2026 }
2027}
2028
2029fn wrap_text_lines(text: &str, width: usize) -> Vec<String> {
2030 if width == 0 {
2031 return Vec::new();
2032 }
2033 let mut out = Vec::new();
2034 for paragraph in text.split('\n') {
2035 if paragraph.is_empty() {
2036 out.push(String::new());
2037 continue;
2038 }
2039 let mut line = String::new();
2040 let mut line_width = 0usize;
2041 for word in paragraph.split_whitespace() {
2042 push_wrapped_word(word, width, &mut out, &mut line, &mut line_width);
2043 }
2044 if !line.is_empty() {
2045 out.push(line);
2046 }
2047 }
2048 if out.is_empty() {
2049 out.push(String::new());
2050 }
2051 out
2052}
2053
2054fn push_wrapped_word(
2055 word: &str,
2056 width: usize,
2057 out: &mut Vec<String>,
2058 line: &mut String,
2059 line_width: &mut usize,
2060) {
2061 let word_width = UnicodeWidthStr::width(word);
2062 if line.is_empty() {
2063 if word_width <= width {
2064 line.push_str(word);
2065 *line_width = word_width;
2066 return;
2067 }
2068 } else if *line_width + 1 + word_width <= width {
2069 line.push(' ');
2070 line.push_str(word);
2071 *line_width += 1 + word_width;
2072 return;
2073 } else {
2074 out.push(std::mem::take(line));
2075 *line_width = 0;
2076 if word_width <= width {
2077 line.push_str(word);
2078 *line_width = word_width;
2079 return;
2080 }
2081 }
2082
2083 for ch in word.chars() {
2084 let ch_width = UnicodeWidthChar::width(ch).unwrap_or(1).max(1);
2085 if *line_width + ch_width > width && !line.is_empty() {
2086 out.push(std::mem::take(line));
2087 *line_width = 0;
2088 }
2089 line.push(ch);
2090 *line_width += ch_width;
2091 }
2092}
2093
2094#[cfg(test)]
2095mod tests {
2096 use super::*;
2097
2098 #[test]
2099 fn update_round_trip_preserves_title_and_cells() {
2100 let style = CellStyle::default();
2101 let mut prev = FrameState::new(2, 8);
2102 prev.set_title("one");
2103 prev.write_text(0, 0, "hello", style);
2104
2105 let mut next = prev.clone();
2106 next.set_title("two");
2107 next.write_text(1, 0, "world", style);
2108
2109 let baseline = build_update_msg(7, &prev, &FrameState::default()).unwrap();
2110 let delta = build_update_msg(7, &next, &prev).unwrap();
2111
2112 let mut term = TerminalState::new(2, 8);
2113 let ServerMsg::Update { payload, .. } = parse_server_msg(&baseline).unwrap() else {
2114 panic!("expected update");
2115 };
2116 assert!(term.feed_compressed(payload));
2117 assert_eq!(term.title(), "one");
2118
2119 let ServerMsg::Update { payload, .. } = parse_server_msg(&delta).unwrap() else {
2120 panic!("expected update");
2121 };
2122 assert!(term.feed_compressed(payload));
2123 assert_eq!(term.title(), "two");
2124 assert_eq!(term.get_all_text(), "hello\nworld");
2125 }
2126
2127 #[test]
2128 fn title_can_be_cleared_via_update() {
2129 let style = CellStyle::default();
2130 let mut prev = FrameState::new(1, 4);
2131 prev.set_title("busy");
2132 prev.write_text(0, 0, "ping", style);
2133
2134 let mut next = prev.clone();
2135 next.set_title("");
2136
2137 let baseline = build_update_msg(1, &prev, &FrameState::default()).unwrap();
2138 let delta = build_update_msg(1, &next, &prev).unwrap();
2139
2140 let mut term = TerminalState::new(1, 4);
2141 let ServerMsg::Update { payload, .. } = parse_server_msg(&baseline).unwrap() else {
2142 panic!("expected update");
2143 };
2144 term.feed_compressed(payload);
2145 let ServerMsg::Update { payload, .. } = parse_server_msg(&delta).unwrap() else {
2146 panic!("expected update");
2147 };
2148 term.feed_compressed(payload);
2149 assert_eq!(term.title(), "");
2150 }
2151
2152 #[test]
2153 fn scroll_heavy_update_can_use_ops_payload() {
2154 let style = CellStyle::default();
2155 let mut prev = FrameState::new(5, 6);
2156 prev.write_text(0, 0, "one", style);
2157 prev.write_text(1, 0, "two", style);
2158 prev.write_text(2, 0, "three", style);
2159 prev.write_text(3, 0, "four", style);
2160 prev.write_text(4, 0, "five", style);
2161
2162 let mut next = FrameState::new(5, 6);
2163 next.write_text(0, 0, "two", style);
2164 next.write_text(1, 0, "three", style);
2165 next.write_text(2, 0, "four", style);
2166 next.write_text(3, 0, "five", style);
2167
2168 let delta = build_update_msg(9, &next, &prev).unwrap();
2169 let ServerMsg::Update { payload, .. } = parse_server_msg(&delta).unwrap() else {
2170 panic!("expected update");
2171 };
2172 let decoded = decompress_size_prepended(payload).unwrap();
2173 let title_field = u16::from_le_bytes([decoded[10], decoded[11]]);
2174 assert_ne!(title_field & OPS_PRESENT, 0);
2175
2176 let mut term = TerminalState::new(5, 6);
2177 let baseline = build_update_msg(9, &prev, &FrameState::default()).unwrap();
2178 let ServerMsg::Update { payload, .. } = parse_server_msg(&baseline).unwrap() else {
2179 panic!("expected update");
2180 };
2181 assert!(term.feed_compressed(payload));
2182 let ServerMsg::Update { payload, .. } = parse_server_msg(&delta).unwrap() else {
2183 panic!("expected update");
2184 };
2185 assert!(term.feed_compressed(payload));
2186 assert_eq!(term.get_all_text(), "two\nthree\nfour\nfive\n");
2187 }
2188
2189 #[test]
2190 fn cooked_scroll_heavy_update_uses_copy_rect_op() {
2191 let style = CellStyle::default();
2192 let mut prev = FrameState::new(5, 6);
2193 prev.set_mode(MODE_ECHO | MODE_ICANON);
2194 prev.write_text(0, 0, "one", style);
2195 prev.write_text(1, 0, "two", style);
2196 prev.write_text(2, 0, "three", style);
2197 prev.write_text(3, 0, "four", style);
2198 prev.write_text(4, 0, "five", style);
2199
2200 let mut next = FrameState::new(5, 6);
2201 next.set_mode(MODE_ECHO | MODE_ICANON);
2202 next.write_text(0, 0, "two", style);
2203 next.write_text(1, 0, "three", style);
2204 next.write_text(2, 0, "four", style);
2205 next.write_text(3, 0, "five", style);
2206
2207 let delta = build_update_msg(9, &next, &prev).unwrap();
2208 let ServerMsg::Update { payload, .. } = parse_server_msg(&delta).unwrap() else {
2209 panic!("expected update");
2210 };
2211 let decoded = decompress_size_prepended(payload).unwrap();
2212 let op_count = u16::from_le_bytes([decoded[12], decoded[13]]);
2213 assert!(op_count >= 1);
2214 assert_eq!(decoded[14], OP_COPY_RECT);
2215 }
2216
2217 #[test]
2218 fn mode_zero_scroll_uses_copy_rect() {
2219 let style = CellStyle::default();
2220 let mut prev = FrameState::new(5, 6);
2221 prev.write_text(0, 0, "one", style);
2222 prev.write_text(1, 0, "two", style);
2223 prev.write_text(2, 0, "three", style);
2224 prev.write_text(3, 0, "four", style);
2225 prev.write_text(4, 0, "five", style);
2226
2227 let mut next = FrameState::new(5, 6);
2228 next.write_text(0, 0, "two", style);
2229 next.write_text(1, 0, "three", style);
2230 next.write_text(2, 0, "four", style);
2231 next.write_text(3, 0, "five", style);
2232
2233 let delta = build_update_msg(9, &next, &prev).unwrap();
2234 let ServerMsg::Update { payload, .. } = parse_server_msg(&delta).unwrap() else {
2235 panic!("expected update");
2236 };
2237 let decoded = decompress_size_prepended(payload).unwrap();
2238 let op_count = u16::from_le_bytes([decoded[12], decoded[13]]);
2239 assert!(op_count >= 1);
2240 assert_eq!(decoded[14], OP_COPY_RECT);
2242
2243 let baseline = build_update_msg(9, &prev, &FrameState::new(5, 6)).unwrap();
2245 let mut state = TerminalState::new(5, 6);
2246 let ServerMsg::Update { payload: bp, .. } = parse_server_msg(&baseline).unwrap() else {
2247 panic!("expected update");
2248 };
2249 state.feed_compressed(bp);
2250 state.feed_compressed(payload);
2251 assert_eq!(state.frame().cells(), next.cells());
2252 }
2253
2254 #[test]
2255 fn callback_renderer_wraps_text() {
2256 let mut renderer = CallbackRenderer::new(2, 8);
2257 renderer.render(|dom| {
2258 dom.wrapped_text(
2259 Rect::new(0, 0, 2, 8),
2260 "alpha beta gamma",
2261 CellStyle::default(),
2262 );
2263 });
2264 assert_eq!(renderer.frame().get_all_text(), "alpha\nbeta");
2265 }
2266
2267 #[test]
2268 fn scrolling_text_shows_tail() {
2269 let mut frame = FrameState::new(3, 8);
2270 frame.write_scrolling_text(
2271 Rect::new(0, 0, 3, 8),
2272 &["one", "two", "three", "four"],
2273 0,
2274 CellStyle::default(),
2275 );
2276 assert_eq!(frame.get_all_text(), "two\nthree\nfour");
2277 }
2278
2279 #[test]
2280 fn search_results_round_trip_with_context() {
2281 let msg = [
2282 vec![S2C_SEARCH_RESULTS],
2283 7u16.to_le_bytes().to_vec(),
2284 1u16.to_le_bytes().to_vec(),
2285 42u16.to_le_bytes().to_vec(),
2286 1234u32.to_le_bytes().to_vec(),
2287 vec![1, 0b111],
2288 9u32.to_le_bytes().to_vec(),
2289 5u16.to_le_bytes().to_vec(),
2290 b"hello".to_vec(),
2291 ]
2292 .concat();
2293
2294 let ServerMsg::SearchResults {
2295 request_id,
2296 results,
2297 } = parse_server_msg(&msg).unwrap()
2298 else {
2299 panic!("expected search results");
2300 };
2301 assert_eq!(request_id, 7);
2302 assert_eq!(results.len(), 1);
2303 assert_eq!(results[0].pty_id, 42);
2304 assert_eq!(results[0].score, 1234);
2305 assert_eq!(results[0].primary_source, 1);
2306 assert_eq!(results[0].matched_sources, 0b111);
2307 assert_eq!(results[0].scroll_offset, Some(9));
2308 assert_eq!(results[0].context, b"hello");
2309 }
2310
2311 #[test]
2314 fn msg_create_no_tag_has_zero_tag_len() {
2315 let msg = msg_create(24, 80);
2316 assert_eq!(msg.len(), 7);
2317 assert_eq!(msg[0], C2S_CREATE);
2318 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 24);
2319 assert_eq!(u16::from_le_bytes([msg[3], msg[4]]), 80);
2320 assert_eq!(u16::from_le_bytes([msg[5], msg[6]]), 0);
2321 }
2322
2323 #[test]
2324 fn msg_create_tagged_encodes_tag() {
2325 let msg = msg_create_tagged(24, 80, "my-pty");
2326 assert_eq!(msg[0], C2S_CREATE);
2327 let tag_len = u16::from_le_bytes([msg[5], msg[6]]) as usize;
2328 assert_eq!(tag_len, 6);
2329 assert_eq!(&msg[7..7 + tag_len], b"my-pty");
2330 assert_eq!(msg.len(), 7 + tag_len);
2331 }
2332
2333 #[test]
2334 fn msg_create_tagged_command_encodes_both() {
2335 let msg = msg_create_tagged_command(30, 120, "editor", "vim");
2336 let tag_len = u16::from_le_bytes([msg[5], msg[6]]) as usize;
2337 assert_eq!(tag_len, 6);
2338 assert_eq!(&msg[7..13], b"editor");
2339 assert_eq!(&msg[13..], b"vim");
2340 }
2341
2342 #[test]
2343 fn msg_create_command_has_empty_tag() {
2344 let msg = msg_create_command(24, 80, "ls");
2345 let tag_len = u16::from_le_bytes([msg[5], msg[6]]) as usize;
2346 assert_eq!(tag_len, 0);
2347 assert_eq!(&msg[7..], b"ls");
2348 }
2349
2350 #[test]
2351 fn msg_create_tagged_empty_tag() {
2352 let msg = msg_create_tagged(24, 80, "");
2353 assert_eq!(msg.len(), 7);
2354 assert_eq!(u16::from_le_bytes([msg[5], msg[6]]), 0);
2355 }
2356
2357 #[test]
2358 fn msg_create_tagged_unicode_tag() {
2359 let msg = msg_create_tagged(24, 80, "日本語");
2360 let tag_len = u16::from_le_bytes([msg[5], msg[6]]) as usize;
2361 assert_eq!(tag_len, "日本語".len());
2362 assert_eq!(std::str::from_utf8(&msg[7..7 + tag_len]).unwrap(), "日本語");
2363 }
2364
2365 #[test]
2366 fn parse_created_with_tag() {
2367 let mut wire = vec![S2C_CREATED, 0x05, 0x00];
2368 wire.extend_from_slice(b"hello");
2369 let msg = parse_server_msg(&wire).unwrap();
2370 match msg {
2371 ServerMsg::Created { pty_id, tag } => {
2372 assert_eq!(pty_id, 5);
2373 assert_eq!(tag, "hello");
2374 }
2375 _ => panic!("expected Created"),
2376 }
2377 }
2378
2379 #[test]
2380 fn parse_created_without_tag() {
2381 let wire = vec![S2C_CREATED, 0x03, 0x00];
2382 let msg = parse_server_msg(&wire).unwrap();
2383 match msg {
2384 ServerMsg::Created { pty_id, tag } => {
2385 assert_eq!(pty_id, 3);
2386 assert_eq!(tag, "");
2387 }
2388 _ => panic!("expected Created"),
2389 }
2390 }
2391
2392 #[test]
2393 fn parse_created_n_with_tag() {
2394 let mut wire = vec![S2C_CREATED_N, 0x2a, 0x00, 0x05, 0x00];
2395 wire.extend_from_slice(b"hello");
2396 let msg = parse_server_msg(&wire).unwrap();
2397 match msg {
2398 ServerMsg::CreatedN { nonce, pty_id, tag } => {
2399 assert_eq!(nonce, 42);
2400 assert_eq!(pty_id, 5);
2401 assert_eq!(tag, "hello");
2402 }
2403 _ => panic!("expected CreatedN"),
2404 }
2405 }
2406
2407 #[test]
2408 fn msg_create_n_format() {
2409 let msg = msg_create_n(42, 24, 80, "test");
2410 assert_eq!(msg[0], C2S_CREATE_N);
2411 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 42);
2412 assert_eq!(u16::from_le_bytes([msg[3], msg[4]]), 24);
2413 assert_eq!(u16::from_le_bytes([msg[5], msg[6]]), 80);
2414 assert_eq!(u16::from_le_bytes([msg[7], msg[8]]), 4);
2415 assert_eq!(&msg[9..], b"test");
2416 }
2417
2418 #[test]
2419 fn msg_create_n_command_format() {
2420 let msg = msg_create_n_command(7, 30, 120, "bg", "make build");
2421 assert_eq!(msg[0], C2S_CREATE_N);
2422 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 7);
2423 assert_eq!(u16::from_le_bytes([msg[3], msg[4]]), 30);
2424 assert_eq!(u16::from_le_bytes([msg[5], msg[6]]), 120);
2425 let tag_len = u16::from_le_bytes([msg[7], msg[8]]) as usize;
2426 assert_eq!(tag_len, 2);
2427 assert_eq!(&msg[9..9 + tag_len], b"bg");
2428 assert_eq!(&msg[9 + tag_len..], b"make build");
2429 }
2430
2431 #[test]
2432 fn parse_list_with_tags() {
2433 let mut wire = vec![S2C_LIST, 0x02, 0x00];
2435 wire.extend_from_slice(&1u16.to_le_bytes());
2437 wire.extend_from_slice(&2u16.to_le_bytes());
2438 wire.extend_from_slice(b"ab");
2439 wire.extend_from_slice(&2u16.to_le_bytes());
2441 wire.extend_from_slice(&0u16.to_le_bytes());
2442
2443 let msg = parse_server_msg(&wire).unwrap();
2444 match msg {
2445 ServerMsg::List { entries } => {
2446 assert_eq!(entries.len(), 2);
2447 assert_eq!(entries[0].pty_id, 1);
2448 assert_eq!(entries[0].tag, "ab");
2449 assert_eq!(entries[1].pty_id, 2);
2450 assert_eq!(entries[1].tag, "");
2451 }
2452 _ => panic!("expected List"),
2453 }
2454 }
2455
2456 #[test]
2457 fn parse_list_empty() {
2458 let wire = vec![S2C_LIST, 0x00, 0x00];
2459 let msg = parse_server_msg(&wire).unwrap();
2460 match msg {
2461 ServerMsg::List { entries } => assert_eq!(entries.len(), 0),
2462 _ => panic!("expected List"),
2463 }
2464 }
2465
2466 #[test]
2467 fn parse_list_truncated_gracefully() {
2468 let mut wire = vec![S2C_LIST, 0x02, 0x00];
2470 wire.extend_from_slice(&1u16.to_le_bytes());
2471 wire.extend_from_slice(&0u16.to_le_bytes());
2472 let msg = parse_server_msg(&wire).unwrap();
2474 match msg {
2475 ServerMsg::List { entries } => assert_eq!(entries.len(), 1),
2476 _ => panic!("expected List"),
2477 }
2478 }
2479
2480 #[test]
2481 fn parse_list_with_long_tags() {
2482 let long_tag = "a".repeat(300);
2483 let mut wire = vec![S2C_LIST, 0x01, 0x00];
2484 wire.extend_from_slice(&42u16.to_le_bytes());
2485 wire.extend_from_slice(&(long_tag.len() as u16).to_le_bytes());
2486 wire.extend_from_slice(long_tag.as_bytes());
2487
2488 let msg = parse_server_msg(&wire).unwrap();
2489 match msg {
2490 ServerMsg::List { entries } => {
2491 assert_eq!(entries.len(), 1);
2492 assert_eq!(entries[0].pty_id, 42);
2493 assert_eq!(entries[0].tag, long_tag);
2494 }
2495 _ => panic!("expected List"),
2496 }
2497 }
2498
2499 #[test]
2500 fn create_and_created_tag_round_trip() {
2501 let create_msg = msg_create_tagged(24, 80, "my-session");
2503 let tag_len = u16::from_le_bytes([create_msg[5], create_msg[6]]) as usize;
2504 let tag = std::str::from_utf8(&create_msg[7..7 + tag_len]).unwrap();
2505
2506 let mut created_wire = vec![S2C_CREATED, 0x07, 0x00]; created_wire.extend_from_slice(tag.as_bytes());
2509
2510 let msg = parse_server_msg(&created_wire).unwrap();
2511 match msg {
2512 ServerMsg::Created {
2513 pty_id,
2514 tag: parsed_tag,
2515 } => {
2516 assert_eq!(pty_id, 7);
2517 assert_eq!(parsed_tag, "my-session");
2518 }
2519 _ => panic!("expected Created"),
2520 }
2521 }
2522
2523 #[test]
2526 fn frame_state_accessors() {
2527 let mut f = FrameState::new(4, 10);
2528 assert_eq!(f.rows(), 4);
2529 assert_eq!(f.cols(), 10);
2530 assert_eq!(f.cursor_row(), 0);
2531 assert_eq!(f.cursor_col(), 0);
2532 assert_eq!(f.mode(), 0);
2533 assert_eq!(f.title(), "");
2534 assert_eq!(f.cells().len(), 4 * 10 * CELL_SIZE);
2535 assert_eq!(f.cells_mut().len(), 4 * 10 * CELL_SIZE);
2536 assert!(f.overflow().is_empty());
2537 assert!(f.overflow_mut().is_empty());
2538 }
2539
2540 #[test]
2541 fn frame_state_from_parts() {
2542 let cells = vec![0u8; 2 * 4 * CELL_SIZE];
2543 let f = FrameState::from_parts(2, 4, 1, 3, 0x0F, "hello", cells.clone());
2544 assert_eq!(f.rows(), 2);
2545 assert_eq!(f.cols(), 4);
2546 assert_eq!(f.cursor_row(), 1);
2547 assert_eq!(f.cursor_col(), 3);
2548 assert_eq!(f.mode(), 0x0F);
2549 assert_eq!(f.title(), "hello");
2550 assert_eq!(f.cells(), &cells[..]);
2551 }
2552
2553 #[test]
2554 fn frame_state_from_parts_wrong_size() {
2555 let cells = vec![0u8; 10]; let f = FrameState::from_parts(2, 4, 0, 0, 0, "", cells);
2558 assert_eq!(f.cells().len(), 2 * 4 * CELL_SIZE);
2559 }
2560
2561 #[test]
2562 fn frame_state_resize() {
2563 let mut f = FrameState::new(4, 10);
2564 f.set_cursor(3, 9);
2565 f.resize(2, 5);
2566 assert_eq!(f.rows(), 2);
2567 assert_eq!(f.cols(), 5);
2568 assert_eq!(f.cursor_row(), 1); assert_eq!(f.cursor_col(), 4); assert_eq!(f.cells().len(), 2 * 5 * CELL_SIZE);
2571 }
2572
2573 #[test]
2574 fn frame_state_resize_noop() {
2575 let mut f = FrameState::new(4, 10);
2576 let ptr_before = f.cells().as_ptr();
2577 f.resize(4, 10); let ptr_after = f.cells().as_ptr();
2579 assert_eq!(ptr_before, ptr_after); }
2581
2582 #[test]
2583 fn frame_state_set_cursor_clamps() {
2584 let mut f = FrameState::new(4, 10);
2585 f.set_cursor(100, 200);
2586 assert_eq!(f.cursor_row(), 3);
2587 assert_eq!(f.cursor_col(), 9);
2588 }
2589
2590 #[test]
2591 fn frame_state_set_title() {
2592 let mut f = FrameState::new(2, 2);
2593 assert!(f.set_title("new title"));
2594 assert_eq!(f.title(), "new title");
2595 assert!(!f.set_title("new title")); assert!(f.set_title("other"));
2597 }
2598
2599 #[test]
2600 fn frame_state_get_text_and_write_text() {
2601 let mut f = FrameState::new(2, 10);
2602 f.write_text(0, 0, "Hello", CellStyle::default());
2603 f.write_text(1, 0, "World", CellStyle::default());
2604 let text = f.get_text(0, 0, 1, 9);
2605 assert!(text.contains("Hello"));
2606 assert!(text.contains("World"));
2607 let all = f.get_all_text();
2608 assert!(all.contains("Hello"));
2609 }
2610
2611 #[test]
2612 fn frame_state_get_text_empty() {
2613 let f = FrameState::new(0, 0);
2614 assert_eq!(f.get_text(0, 0, 0, 0), "");
2615 assert_eq!(f.get_all_text(), "");
2616 }
2617
2618 #[test]
2619 fn frame_state_get_cell() {
2620 let f = FrameState::new(2, 4);
2621 let cell = f.get_cell(0, 0);
2622 assert_eq!(cell.len(), CELL_SIZE);
2623 assert!(f.get_cell(100, 100).is_empty());
2625 }
2626
2627 #[test]
2628 fn frame_state_cell_content_blank() {
2629 let f = FrameState::new(2, 4);
2630 assert_eq!(f.cell_content(0, 0), " "); assert_eq!(f.cell_content(100, 0), ""); }
2633
2634 #[test]
2635 fn frame_state_cell_content_with_text() {
2636 let mut f = FrameState::new(2, 10);
2637 f.write_text(0, 0, "A", CellStyle::default());
2638 assert_eq!(f.cell_content(0, 0), "A");
2639 }
2640
2641 #[test]
2642 fn frame_state_fill_rect() {
2643 let mut f = FrameState::new(4, 10);
2644 f.fill_rect(Rect::new(0, 0, 2, 5), 'X', CellStyle::default());
2645 assert_eq!(f.cell_content(0, 0), "X");
2646 assert_eq!(f.cell_content(1, 4), "X");
2647 assert_eq!(f.cell_content(2, 0), " "); }
2649
2650 #[test]
2651 fn frame_state_wrapped_text() {
2652 let mut f = FrameState::new(4, 10);
2653 let lines =
2654 f.write_wrapped_text(Rect::new(0, 0, 4, 5), "hello world", CellStyle::default());
2655 assert!(lines >= 2); }
2657
2658 #[test]
2659 fn frame_state_wrapped_text_empty_rect() {
2660 let mut f = FrameState::new(4, 10);
2661 assert_eq!(
2662 f.write_wrapped_text(Rect::new(0, 0, 0, 0), "hi", CellStyle::default()),
2663 0
2664 );
2665 }
2666
2667 #[test]
2668 fn frame_state_scrolling_text() {
2669 let mut f = FrameState::new(4, 10);
2670 f.write_scrolling_text(
2671 Rect::new(0, 0, 3, 10),
2672 &["line1", "line2", "line3", "line4"],
2673 0,
2674 CellStyle::default(),
2675 );
2676 assert_eq!(f.cell_content(0, 0), "l"); }
2679
2680 #[test]
2681 fn frame_state_scrolling_text_empty_rect() {
2682 let mut f = FrameState::new(4, 10);
2683 f.write_scrolling_text(Rect::new(0, 0, 0, 0), &["hi"], 0, CellStyle::default());
2684 }
2686
2687 #[test]
2688 fn frame_state_clear() {
2689 let mut f = FrameState::new(2, 4);
2690 f.write_text(0, 0, "AB", CellStyle::default());
2691 f.clear(CellStyle::default());
2692 assert_eq!(f.cell_content(0, 0), " ");
2693 }
2694
2695 #[test]
2698 fn terminal_state_accessors() {
2699 let t = TerminalState::new(24, 80);
2700 assert_eq!(t.rows(), 24);
2701 assert_eq!(t.cols(), 80);
2702 assert_eq!(t.cursor_row(), 0);
2703 assert_eq!(t.cursor_col(), 0);
2704 assert_eq!(t.mode(), 0);
2705 assert_eq!(t.title(), "");
2706 assert_eq!(t.cells().len(), 24 * 80 * CELL_SIZE);
2707 assert_eq!(t.frame().rows(), 24);
2708 }
2709
2710 #[test]
2711 fn terminal_state_mutators() {
2712 let mut t = TerminalState::new(4, 10);
2713 t.frame_mut().set_title("test");
2714 assert_eq!(t.title(), "test");
2715 }
2716
2717 #[test]
2718 fn terminal_state_set_title() {
2719 let mut t = TerminalState::new(4, 10);
2720 assert!(t.frame_mut().set_title("hello"));
2721 assert_eq!(t.title(), "hello");
2722 assert!(!t.frame_mut().set_title("hello")); }
2724
2725 #[test]
2726 fn terminal_state_get_text() {
2727 let t = TerminalState::new(2, 10);
2728 let text = t.get_text(0, 0, 0, 9);
2729 assert!(text.is_empty() || text.chars().all(|c| c == ' ' || c == '\n'));
2730 assert!(t.get_cell(0, 0).len() == CELL_SIZE);
2731 assert!(t.get_cell(100, 100).is_empty());
2732 }
2733
2734 #[test]
2735 fn terminal_state_resize() {
2736 let mut t = TerminalState::new(4, 10);
2737 t.frame_mut().resize(2, 5);
2738 assert_eq!(t.rows(), 2);
2741 assert_eq!(t.cols(), 5);
2742 }
2743
2744 #[test]
2745 fn terminal_state_feed_compressed_invalid() {
2746 let mut t = TerminalState::new(4, 10);
2747 assert!(!t.feed_compressed(b"garbage"));
2748 assert!(!t.feed_compressed(&[]));
2749 }
2750
2751 #[test]
2752 fn terminal_state_feed_compressed_batch_empty() {
2753 let mut t = TerminalState::new(4, 10);
2754 assert!(!t.feed_compressed_batch(&[]));
2755 }
2756
2757 #[test]
2758 fn terminal_state_feed_compressed_batch_truncated() {
2759 let mut t = TerminalState::new(4, 10);
2760 let batch = &[100, 0, 0, 0];
2762 assert!(!t.feed_compressed_batch(batch));
2763 }
2764
2765 #[test]
2768 fn msg_input_format() {
2769 let msg = msg_input(5, b"hello");
2770 assert_eq!(msg[0], C2S_INPUT);
2771 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 5);
2772 assert_eq!(&msg[3..], b"hello");
2773 }
2774
2775 #[test]
2776 fn msg_resize_format() {
2777 let msg = msg_resize(3, 24, 80);
2778 assert_eq!(msg[0], C2S_RESIZE);
2779 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 3);
2780 assert_eq!(u16::from_le_bytes([msg[3], msg[4]]), 24);
2781 assert_eq!(u16::from_le_bytes([msg[5], msg[6]]), 80);
2782 }
2783
2784 #[test]
2785 fn msg_resize_batch_format() {
2786 let msg = msg_resize_batch(&[(3, 24, 80), (5, 40, 120)]);
2787 assert_eq!(msg[0], C2S_RESIZE);
2788 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 3);
2789 assert_eq!(u16::from_le_bytes([msg[3], msg[4]]), 24);
2790 assert_eq!(u16::from_le_bytes([msg[5], msg[6]]), 80);
2791 assert_eq!(u16::from_le_bytes([msg[7], msg[8]]), 5);
2792 assert_eq!(u16::from_le_bytes([msg[9], msg[10]]), 40);
2793 assert_eq!(u16::from_le_bytes([msg[11], msg[12]]), 120);
2794 }
2795
2796 #[test]
2797 fn msg_focus_format() {
2798 let msg = msg_focus(7);
2799 assert_eq!(msg[0], C2S_FOCUS);
2800 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 7);
2801 assert_eq!(msg.len(), 3);
2802 }
2803
2804 #[test]
2805 fn msg_close_format() {
2806 let msg = msg_close(9);
2807 assert_eq!(msg[0], C2S_CLOSE);
2808 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 9);
2809 }
2810
2811 #[test]
2812 fn msg_subscribe_unsubscribe_format() {
2813 let sub = msg_subscribe(1);
2814 assert_eq!(sub[0], C2S_SUBSCRIBE);
2815 assert_eq!(u16::from_le_bytes([sub[1], sub[2]]), 1);
2816
2817 let unsub = msg_unsubscribe(2);
2818 assert_eq!(unsub[0], C2S_UNSUBSCRIBE);
2819 assert_eq!(u16::from_le_bytes([unsub[1], unsub[2]]), 2);
2820 }
2821
2822 #[test]
2823 fn msg_search_format() {
2824 let msg = msg_search(42, "test query");
2825 assert_eq!(msg[0], C2S_SEARCH);
2826 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 42);
2827 assert_eq!(&msg[3..], b"test query");
2828 }
2829
2830 #[test]
2831 fn msg_ack_format() {
2832 let msg = msg_ack();
2833 assert_eq!(msg, vec![C2S_ACK]);
2834 }
2835
2836 #[test]
2837 fn msg_scroll_format() {
2838 let msg = msg_scroll(5, 1000);
2839 assert_eq!(msg[0], C2S_SCROLL);
2840 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 5);
2841 assert_eq!(u32::from_le_bytes([msg[3], msg[4], msg[5], msg[6]]), 1000);
2842 }
2843
2844 #[test]
2845 fn msg_display_rate_format() {
2846 let msg = msg_display_rate(120);
2847 assert_eq!(msg[0], C2S_DISPLAY_RATE);
2848 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 120);
2849 }
2850
2851 #[test]
2852 fn msg_client_metrics_format() {
2853 let msg = msg_client_metrics(3, 5, 100);
2854 assert_eq!(msg[0], C2S_CLIENT_METRICS);
2855 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 3);
2856 assert_eq!(u16::from_le_bytes([msg[3], msg[4]]), 5);
2857 assert_eq!(u16::from_le_bytes([msg[5], msg[6]]), 100);
2858 }
2859
2860 #[test]
2863 fn callback_renderer_resize() {
2864 let mut r = CallbackRenderer::new(2, 8);
2865 assert_eq!(r.frame().rows(), 2);
2866 r.resize(4, 16);
2867 assert_eq!(r.frame().rows(), 4);
2868 assert_eq!(r.frame().cols(), 16);
2869 }
2870
2871 #[test]
2872 fn callback_renderer_fill() {
2873 let mut r = CallbackRenderer::new(4, 10);
2874 r.render(|dom| {
2875 dom.fill(Rect::new(0, 0, 2, 5), '#', CellStyle::default());
2876 });
2877 assert_eq!(r.frame().cell_content(0, 0), "#");
2878 assert_eq!(r.frame().cell_content(1, 4), "#");
2879 }
2880
2881 #[test]
2882 fn callback_renderer_text() {
2883 let mut r = CallbackRenderer::new(4, 20);
2884 r.render(|dom| {
2885 dom.text(0, 0, "Hello", CellStyle::default());
2886 });
2887 assert_eq!(r.frame().cell_content(0, 0), "H");
2888 assert_eq!(r.frame().cell_content(0, 4), "o");
2889 }
2890
2891 #[test]
2892 fn callback_renderer_set_title() {
2893 let mut r = CallbackRenderer::new(2, 8);
2894 r.render(|dom| {
2895 dom.set_title("my title");
2896 });
2897 assert_eq!(r.frame().title(), "my title");
2898 }
2899
2900 #[test]
2901 fn callback_renderer_set_background() {
2902 let mut r = CallbackRenderer::new(2, 4);
2903 let style = CellStyle {
2904 bg: Color::Rgb(255, 0, 0),
2905 ..CellStyle::default()
2906 };
2907 r.render(|dom| {
2908 dom.set_background(style);
2909 });
2910 assert_eq!(r.frame().cells().len(), 2 * 4 * CELL_SIZE);
2912 }
2913
2914 #[test]
2915 fn callback_renderer_scrolling_text() {
2916 let mut r = CallbackRenderer::new(4, 20);
2917 r.render(|dom| {
2918 dom.scrolling_text(
2919 Rect::new(0, 0, 3, 20),
2920 ["a", "b", "c", "d", "e"].map(String::from),
2921 0,
2922 CellStyle::default(),
2923 );
2924 });
2925 assert_eq!(r.frame().cell_content(0, 0), "c");
2927 }
2928
2929 #[test]
2932 fn parse_empty_returns_none() {
2933 assert!(parse_server_msg(&[]).is_none());
2934 }
2935
2936 #[test]
2937 fn parse_unknown_type_returns_none() {
2938 assert!(parse_server_msg(&[0xFF, 0x00, 0x00]).is_none());
2939 }
2940
2941 #[test]
2942 fn parse_update_too_short() {
2943 assert!(parse_server_msg(&[S2C_UPDATE, 0x00]).is_none());
2944 }
2945
2946 #[test]
2947 fn parse_closed() {
2948 let msg = parse_server_msg(&[S2C_CLOSED, 0x05, 0x00]).unwrap();
2949 match msg {
2950 ServerMsg::Closed { pty_id } => assert_eq!(pty_id, 5),
2951 _ => panic!("expected Closed"),
2952 }
2953 }
2954
2955 #[test]
2956 fn parse_title() {
2957 let mut wire = vec![S2C_TITLE, 0x01, 0x00];
2958 wire.extend_from_slice(b"mytitle");
2959 let msg = parse_server_msg(&wire).unwrap();
2960 match msg {
2961 ServerMsg::Title { pty_id, title } => {
2962 assert_eq!(pty_id, 1);
2963 assert_eq!(title, b"mytitle");
2964 }
2965 _ => panic!("expected Title"),
2966 }
2967 }
2968
2969 #[test]
2972 fn build_update_msg_round_trip_with_resize() {
2973 let style = CellStyle::default();
2974 let mut prev = FrameState::new(2, 4);
2975 prev.write_text(0, 0, "AB", style);
2976
2977 let mut next = FrameState::new(3, 5); next.write_text(0, 0, "XY", style);
2979 next.set_title("resized");
2980
2981 let msg = build_update_msg(1, &next, &prev).unwrap();
2982 assert!(!msg.is_empty());
2983
2984 let mut t = TerminalState::new(2, 4);
2986 assert!(t.feed_compressed(&msg[3..])); assert_eq!(t.rows(), 3);
2988 assert_eq!(t.cols(), 5);
2989 assert_eq!(t.title(), "resized");
2990 }
2991
2992 #[test]
2993 fn build_update_msg_cursor_change() {
2994 let mut prev = FrameState::new(4, 10);
2995 prev.set_cursor(0, 0);
2996
2997 let mut next = prev.clone();
2998 next.set_cursor(2, 5);
2999
3000 let msg = build_update_msg(0, &next, &prev).unwrap();
3001
3002 let mut t = TerminalState::new(4, 10);
3003 assert!(t.feed_compressed(&msg[3..]));
3004 assert_eq!(t.cursor_row(), 2);
3005 assert_eq!(t.cursor_col(), 5);
3006 }
3007
3008 #[test]
3009 fn build_update_msg_mode_change() {
3010 let prev = FrameState::new(2, 4);
3011 let mut next = prev.clone();
3012 next.set_mode(0x0F);
3013
3014 let msg = build_update_msg(0, &next, &prev).unwrap();
3015 let mut t = TerminalState::new(2, 4);
3016 assert!(t.feed_compressed(&msg[3..]));
3017 assert_eq!(t.mode(), 0x0F);
3018 }
3019
3020 #[test]
3021 fn feed_compressed_batch_multiple_frames() {
3022 let style = CellStyle::default();
3023 let prev = FrameState::new(2, 4);
3024
3025 let mut mid = prev.clone();
3026 mid.write_text(0, 0, "AB", style);
3027 let msg1 = build_update_msg(0, &mid, &prev).unwrap();
3028
3029 let mut next = mid.clone();
3030 next.write_text(1, 0, "CD", style);
3031 let msg2 = build_update_msg(0, &next, &mid).unwrap();
3032
3033 let payload1 = &msg1[3..];
3035 let payload2 = &msg2[3..];
3036 let mut batch = Vec::new();
3037 batch.extend_from_slice(&(payload1.len() as u32).to_le_bytes());
3038 batch.extend_from_slice(payload1);
3039 batch.extend_from_slice(&(payload2.len() as u32).to_le_bytes());
3040 batch.extend_from_slice(payload2);
3041
3042 let mut t = TerminalState::new(2, 4);
3043 assert!(t.feed_compressed_batch(&batch));
3044 let text = t.get_all_text();
3045 assert!(text.contains("AB"));
3046 assert!(text.contains("CD"));
3047 }
3048}