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