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