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::new();
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 =
1302 u16::from_le_bytes([data[offset], data[offset + 1]]) as usize;
1303 offset += 2;
1304 let cmd = if offset + cmd_len <= data.len() {
1305 std::str::from_utf8(&data[offset..offset + cmd_len]).unwrap_or_default()
1306 } else {
1307 ""
1308 };
1309 offset += cmd_len;
1310 cmd
1311 } else {
1312 ""
1313 };
1314 entries.push(PtyListEntry { pty_id, tag, command });
1315 }
1316 Some(ServerMsg::List { entries })
1317 }
1318 S2C_TITLE => {
1319 if data.len() < 3 {
1320 return None;
1321 }
1322 Some(ServerMsg::Title {
1323 pty_id: u16::from_le_bytes([data[1], data[2]]),
1324 title: &data[3..],
1325 })
1326 }
1327 S2C_SEARCH_RESULTS => {
1328 if data.len() < 5 {
1329 return None;
1330 }
1331 let request_id = u16::from_le_bytes([data[1], data[2]]);
1332 let count = u16::from_le_bytes([data[3], data[4]]) as usize;
1333 let mut results = Vec::with_capacity(count);
1334 let mut offset = 5usize;
1335 for _ in 0..count {
1336 if offset + 14 > data.len() {
1337 return None;
1338 }
1339 let pty_id = u16::from_le_bytes([data[offset], data[offset + 1]]);
1340 let score = u32::from_le_bytes([
1341 data[offset + 2],
1342 data[offset + 3],
1343 data[offset + 4],
1344 data[offset + 5],
1345 ]);
1346 let primary_source = data[offset + 6];
1347 let matched_sources = data[offset + 7];
1348 let scroll_offset = u32::from_le_bytes([
1349 data[offset + 8],
1350 data[offset + 9],
1351 data[offset + 10],
1352 data[offset + 11],
1353 ]);
1354 let context_len =
1355 u16::from_le_bytes([data[offset + 12], data[offset + 13]]) as usize;
1356 offset += 14;
1357 if offset + context_len > data.len() {
1358 return None;
1359 }
1360 results.push(SearchResultEntry {
1361 pty_id,
1362 score,
1363 primary_source,
1364 matched_sources,
1365 scroll_offset: if scroll_offset == u32::MAX {
1366 None
1367 } else {
1368 Some(scroll_offset)
1369 },
1370 context: &data[offset..offset + context_len],
1371 });
1372 offset += context_len;
1373 }
1374 Some(ServerMsg::SearchResults {
1375 request_id,
1376 results,
1377 })
1378 }
1379 S2C_READY => Some(ServerMsg::Ready),
1380 S2C_TEXT => {
1381 if data.len() < 13 {
1382 return None;
1383 }
1384 let nonce = u16::from_le_bytes([data[1], data[2]]);
1385 let pty_id = u16::from_le_bytes([data[3], data[4]]);
1386 let total_lines = u32::from_le_bytes([data[5], data[6], data[7], data[8]]);
1387 let offset = u32::from_le_bytes([data[9], data[10], data[11], data[12]]);
1388 let text = std::str::from_utf8(data.get(13..).unwrap_or_default()).unwrap_or_default();
1389 Some(ServerMsg::Text {
1390 nonce,
1391 pty_id,
1392 total_lines,
1393 offset,
1394 text,
1395 })
1396 }
1397 _ => None,
1398 }
1399}
1400
1401pub fn msg_hello(version: u16, features: u32) -> Vec<u8> {
1402 let mut msg = Vec::with_capacity(7);
1403 msg.push(S2C_HELLO);
1404 msg.extend_from_slice(&version.to_le_bytes());
1405 msg.extend_from_slice(&features.to_le_bytes());
1406 msg
1407}
1408
1409pub fn msg_create(rows: u16, cols: u16) -> Vec<u8> {
1410 msg_create_tagged(rows, cols, "")
1411}
1412
1413pub fn msg_create_tagged(rows: u16, cols: u16, tag: &str) -> Vec<u8> {
1414 let tag_bytes = tag.as_bytes();
1415 let tag_len = tag_bytes.len().min(u16::MAX as usize);
1416 let mut msg = Vec::with_capacity(7 + tag_len);
1417 msg.push(C2S_CREATE);
1418 msg.extend_from_slice(&rows.to_le_bytes());
1419 msg.extend_from_slice(&cols.to_le_bytes());
1420 msg.extend_from_slice(&(tag_len as u16).to_le_bytes());
1421 msg.extend_from_slice(&tag_bytes[..tag_len]);
1422 msg
1423}
1424
1425pub fn msg_create_at(rows: u16, cols: u16, tag: &str, src_pty_id: u16) -> Vec<u8> {
1427 let tag_bytes = tag.as_bytes();
1428 let tag_len = tag_bytes.len().min(u16::MAX as usize);
1429 let mut msg = Vec::with_capacity(9 + tag_len);
1430 msg.push(C2S_CREATE_AT);
1431 msg.extend_from_slice(&rows.to_le_bytes());
1432 msg.extend_from_slice(&cols.to_le_bytes());
1433 msg.extend_from_slice(&(tag_len as u16).to_le_bytes());
1434 msg.extend_from_slice(&tag_bytes[..tag_len]);
1435 msg.extend_from_slice(&src_pty_id.to_le_bytes());
1436 msg
1437}
1438
1439pub fn msg_create_n(nonce: u16, rows: u16, cols: u16, tag: &str) -> Vec<u8> {
1440 let tag_bytes = tag.as_bytes();
1441 let tag_len = tag_bytes.len().min(u16::MAX as usize);
1442 let mut msg = Vec::with_capacity(9 + tag_len);
1443 msg.push(C2S_CREATE_N);
1444 msg.extend_from_slice(&nonce.to_le_bytes());
1445 msg.extend_from_slice(&rows.to_le_bytes());
1446 msg.extend_from_slice(&cols.to_le_bytes());
1447 msg.extend_from_slice(&(tag_len as u16).to_le_bytes());
1448 msg.extend_from_slice(&tag_bytes[..tag_len]);
1449 msg
1450}
1451
1452pub fn msg_create_n_command(nonce: u16, rows: u16, cols: u16, tag: &str, command: &str) -> Vec<u8> {
1453 let mut msg = msg_create_n(nonce, rows, cols, tag);
1454 msg.extend_from_slice(command.as_bytes());
1455 msg
1456}
1457
1458pub fn msg_create_command(rows: u16, cols: u16, command: &str) -> Vec<u8> {
1459 msg_create_tagged_command(rows, cols, "", command)
1460}
1461
1462pub fn msg_create_tagged_command(rows: u16, cols: u16, tag: &str, command: &str) -> Vec<u8> {
1463 let mut msg = msg_create_tagged(rows, cols, tag);
1464 msg.extend_from_slice(command.as_bytes());
1465 msg
1466}
1467
1468pub fn msg_input(pty_id: u16, data: &[u8]) -> Vec<u8> {
1469 let mut msg = Vec::with_capacity(3 + data.len());
1470 msg.push(C2S_INPUT);
1471 msg.extend_from_slice(&pty_id.to_le_bytes());
1472 msg.extend_from_slice(data);
1473 msg
1474}
1475
1476pub fn msg_resize(pty_id: u16, rows: u16, cols: u16) -> Vec<u8> {
1477 let mut msg = Vec::with_capacity(7);
1478 msg.push(C2S_RESIZE);
1479 msg.extend_from_slice(&pty_id.to_le_bytes());
1480 msg.extend_from_slice(&rows.to_le_bytes());
1481 msg.extend_from_slice(&cols.to_le_bytes());
1482 msg
1483}
1484
1485pub fn msg_resize_batch(entries: &[(u16, u16, u16)]) -> Vec<u8> {
1486 let mut msg = Vec::with_capacity(1 + entries.len() * 6);
1487 msg.push(C2S_RESIZE);
1488 for &(pty_id, rows, cols) in entries {
1489 msg.extend_from_slice(&pty_id.to_le_bytes());
1490 msg.extend_from_slice(&rows.to_le_bytes());
1491 msg.extend_from_slice(&cols.to_le_bytes());
1492 }
1493 msg
1494}
1495
1496pub fn msg_focus(pty_id: u16) -> Vec<u8> {
1497 let mut msg = Vec::with_capacity(3);
1498 msg.push(C2S_FOCUS);
1499 msg.extend_from_slice(&pty_id.to_le_bytes());
1500 msg
1501}
1502
1503pub fn msg_close(pty_id: u16) -> Vec<u8> {
1504 let mut msg = Vec::with_capacity(3);
1505 msg.push(C2S_CLOSE);
1506 msg.extend_from_slice(&pty_id.to_le_bytes());
1507 msg
1508}
1509
1510pub fn msg_kill(pty_id: u16, signal: i32) -> Vec<u8> {
1511 let mut msg = Vec::with_capacity(7);
1512 msg.push(C2S_KILL);
1513 msg.extend_from_slice(&pty_id.to_le_bytes());
1514 msg.extend_from_slice(&signal.to_le_bytes());
1515 msg
1516}
1517
1518pub fn msg_restart(pty_id: u16) -> Vec<u8> {
1519 let mut msg = Vec::with_capacity(3);
1520 msg.push(C2S_RESTART);
1521 msg.extend_from_slice(&pty_id.to_le_bytes());
1522 msg
1523}
1524
1525pub fn msg_subscribe(pty_id: u16) -> Vec<u8> {
1526 let mut msg = Vec::with_capacity(3);
1527 msg.push(C2S_SUBSCRIBE);
1528 msg.extend_from_slice(&pty_id.to_le_bytes());
1529 msg
1530}
1531
1532pub fn msg_unsubscribe(pty_id: u16) -> Vec<u8> {
1533 let mut msg = Vec::with_capacity(3);
1534 msg.push(C2S_UNSUBSCRIBE);
1535 msg.extend_from_slice(&pty_id.to_le_bytes());
1536 msg
1537}
1538
1539pub fn msg_search(request_id: u16, query: &str) -> Vec<u8> {
1540 let query = query.as_bytes();
1541 let mut msg = Vec::with_capacity(3 + query.len());
1542 msg.push(C2S_SEARCH);
1543 msg.extend_from_slice(&request_id.to_le_bytes());
1544 msg.extend_from_slice(query);
1545 msg
1546}
1547
1548pub fn msg_ack() -> Vec<u8> {
1549 vec![C2S_ACK]
1550}
1551
1552pub fn msg_scroll(pty_id: u16, offset: u32) -> Vec<u8> {
1553 let mut msg = Vec::with_capacity(7);
1554 msg.push(C2S_SCROLL);
1555 msg.extend_from_slice(&pty_id.to_le_bytes());
1556 msg.extend_from_slice(&offset.to_le_bytes());
1557 msg
1558}
1559
1560pub fn msg_display_rate(fps: u16) -> Vec<u8> {
1561 let mut msg = Vec::with_capacity(3);
1562 msg.push(C2S_DISPLAY_RATE);
1563 msg.extend_from_slice(&fps.to_le_bytes());
1564 msg
1565}
1566
1567pub fn msg_client_metrics(backlog: u16, ack_ahead: u16, apply_ms_x10: u16) -> Vec<u8> {
1568 let mut msg = Vec::with_capacity(7);
1569 msg.push(C2S_CLIENT_METRICS);
1570 msg.extend_from_slice(&backlog.to_le_bytes());
1571 msg.extend_from_slice(&ack_ahead.to_le_bytes());
1572 msg.extend_from_slice(&apply_ms_x10.to_le_bytes());
1573 msg
1574}
1575
1576pub fn msg_read(nonce: u16, pty_id: u16, offset: u32, limit: u32, flags: u8) -> Vec<u8> {
1577 let mut msg = Vec::with_capacity(14);
1578 msg.push(C2S_READ);
1579 msg.extend_from_slice(&nonce.to_le_bytes());
1580 msg.extend_from_slice(&pty_id.to_le_bytes());
1581 msg.extend_from_slice(&offset.to_le_bytes());
1582 msg.extend_from_slice(&limit.to_le_bytes());
1583 msg.push(flags);
1584 msg
1585}
1586
1587pub fn msg_copy_range(
1588 nonce: u16,
1589 pty_id: u16,
1590 start_tail: u32,
1591 start_col: u16,
1592 end_tail: u32,
1593 end_col: u16,
1594 flags: u8,
1595) -> Vec<u8> {
1596 let mut msg = Vec::with_capacity(18);
1597 msg.push(C2S_COPY_RANGE);
1598 msg.extend_from_slice(&nonce.to_le_bytes());
1599 msg.extend_from_slice(&pty_id.to_le_bytes());
1600 msg.extend_from_slice(&start_tail.to_le_bytes());
1601 msg.extend_from_slice(&start_col.to_le_bytes());
1602 msg.extend_from_slice(&end_tail.to_le_bytes());
1603 msg.extend_from_slice(&end_col.to_le_bytes());
1604 msg.push(flags);
1605 msg
1606}
1607
1608pub fn msg_exited(pty_id: u16, exit_status: i32) -> Vec<u8> {
1609 let mut msg = Vec::with_capacity(7);
1610 msg.push(S2C_EXITED);
1611 msg.extend_from_slice(&pty_id.to_le_bytes());
1612 msg.extend_from_slice(&exit_status.to_le_bytes());
1613 msg
1614}
1615
1616fn push_sgr(out: &mut String, style: &CellStyle) {
1617 use std::fmt::Write;
1618 out.push_str("\x1b[0");
1619 if style.bold {
1620 out.push_str(";1");
1621 }
1622 if style.dim {
1623 out.push_str(";2");
1624 }
1625 if style.italic {
1626 out.push_str(";3");
1627 }
1628 if style.underline {
1629 out.push_str(";4");
1630 }
1631 if style.inverse {
1632 out.push_str(";7");
1633 }
1634 match style.fg {
1635 Color::Indexed(n) => {
1636 let _ = write!(out, ";38;5;{n}");
1637 }
1638 Color::Rgb(r, g, b) => {
1639 let _ = write!(out, ";38;2;{r};{g};{b}");
1640 }
1641 Color::Default => {}
1642 }
1643 match style.bg {
1644 Color::Indexed(n) => {
1645 let _ = write!(out, ";48;5;{n}");
1646 }
1647 Color::Rgb(r, g, b) => {
1648 let _ = write!(out, ";48;2;{r};{g};{b}");
1649 }
1650 Color::Default => {}
1651 }
1652 out.push('m');
1653}
1654
1655const MODE_ALT_SCREEN: u16 = 1 << 11;
1656
1657fn mode_is_cooked(mode: u16) -> bool {
1658 mode & MODE_ECHO != 0 && mode & MODE_ICANON != 0 && mode & MODE_ALT_SCREEN == 0
1659}
1660
1661pub fn build_update_msg(
1662 pty_id: u16,
1663 current: &FrameState,
1664 previous: &FrameState,
1665) -> Option<Vec<u8>> {
1666 let title_changed = current.title != previous.title;
1667 let same_size = previous.rows == current.rows
1668 && previous.cols == current.cols
1669 && previous.cells.len() == current.cells.len();
1670
1671 let mut ops = Vec::new();
1673 let mut op_count = 0u16;
1674
1675 let scroll_eligible = (mode_is_cooked(current.mode) && mode_is_cooked(previous.mode))
1679 || current.mode == 0
1680 || previous.mode == 0;
1681 if ENABLE_SCROLL_OPS && same_size && previous.cells != current.cells && scroll_eligible {
1682 if let Some(delta_rows) = detect_vertical_scroll(current, previous) {
1683 let mut basis = previous.clone();
1684 encode_copy_rect_op(&mut ops, current, delta_rows);
1685 apply_vertical_scroll_copy(&mut basis, delta_rows);
1686 op_count += 1;
1687 append_full_width_fill_ops(current, &mut basis, &mut ops, &mut op_count);
1688 if let Some(patch_op) = build_patch_op(current, &basis) {
1689 ops.extend_from_slice(&patch_op);
1690 op_count += 1;
1691 }
1692 }
1693 }
1694
1695 if op_count == 0 {
1697 let basis = if same_size {
1698 previous
1699 } else {
1700 &FrameState::new(current.rows, current.cols)
1701 };
1702 if let Some(patch_op) = build_patch_op(current, basis) {
1703 ops = patch_op;
1704 op_count = 1;
1705 }
1706 }
1707
1708 if op_count == 0 {
1709 if !title_changed
1711 && current.cursor_row == previous.cursor_row
1712 && current.cursor_col == previous.cursor_col
1713 && current.mode == previous.mode
1714 {
1715 return None;
1716 }
1717 }
1718
1719 let has_overflow = !current.overflow.is_empty();
1724 let overflow_section = if has_overflow {
1725 serialize_overflow_strings(current)
1726 } else {
1727 Vec::new()
1728 };
1729
1730 let line_flags_changed =
1731 current.line_flags != previous.line_flags || current.rows != previous.rows;
1732 let has_line_flags = line_flags_changed && !current.line_flags.iter().all(|&f| f == 0);
1733
1734 let title_bytes = if title_changed {
1735 current.title.as_bytes()
1736 } else {
1737 &[]
1738 };
1739 let title_len = title_bytes.len().min(TITLE_LEN_MASK as usize);
1740 let title_field = OPS_PRESENT
1741 | if has_overflow { STRINGS_PRESENT } else { 0 }
1742 | if has_line_flags {
1743 LINE_FLAGS_PRESENT
1744 } else {
1745 0
1746 }
1747 | if title_changed {
1748 TITLE_PRESENT | title_len as u16
1749 } else {
1750 0
1751 };
1752
1753 let mut payload = Vec::with_capacity(
1754 12 + title_len
1755 + 2
1756 + ops.len()
1757 + overflow_section.len()
1758 + if has_line_flags {
1759 current.rows as usize
1760 } else {
1761 0
1762 }
1763 + 4,
1764 );
1765 payload.extend_from_slice(¤t.rows.to_le_bytes());
1766 payload.extend_from_slice(¤t.cols.to_le_bytes());
1767 payload.extend_from_slice(¤t.cursor_row.to_le_bytes());
1768 payload.extend_from_slice(¤t.cursor_col.to_le_bytes());
1769 payload.extend_from_slice(¤t.mode.to_le_bytes());
1770 payload.extend_from_slice(&title_field.to_le_bytes());
1771 if title_changed {
1772 payload.extend_from_slice(&title_bytes[..title_len]);
1773 }
1774 payload.extend_from_slice(&op_count.to_le_bytes());
1775 payload.extend_from_slice(&ops);
1776 payload.extend_from_slice(&overflow_section);
1777 if has_line_flags {
1778 payload.extend_from_slice(¤t.line_flags);
1779 }
1780 payload.extend_from_slice(¤t.scrollback_lines.to_le_bytes());
1782
1783 let compressed = compress_prepend_size(&payload);
1784 let mut msg = Vec::with_capacity(3 + compressed.len());
1785 msg.push(S2C_UPDATE);
1786 msg.extend_from_slice(&pty_id.to_le_bytes());
1787 msg.extend_from_slice(&compressed);
1788 Some(msg)
1789}
1790
1791fn serialize_overflow_strings(frame: &FrameState) -> Vec<u8> {
1793 let count = frame.overflow.len().min(u16::MAX as usize);
1794 let mut out = Vec::new();
1795 out.extend_from_slice(&(count as u16).to_le_bytes());
1796 for (&cell_idx, s) in frame.overflow.iter().take(count) {
1797 let bytes = s.as_bytes();
1798 let len = bytes.len().min(u16::MAX as usize);
1799 out.extend_from_slice(&(cell_idx as u32).to_le_bytes());
1800 out.extend_from_slice(&(len as u16).to_le_bytes());
1801 out.extend_from_slice(&bytes[..len]);
1802 }
1803 out
1804}
1805
1806fn build_patch_op(current: &FrameState, previous: &FrameState) -> Option<Vec<u8>> {
1807 let total_cells = current.rows as usize * current.cols as usize;
1808 let bitmask_len = total_cells.div_ceil(8);
1809 let mut bitmask = vec![0u8; bitmask_len];
1810 let mut dirty_count = 0usize;
1811 for i in 0..total_cells {
1812 let off = i * CELL_SIZE;
1813 if current.cells[off..off + CELL_SIZE] != previous.cells[off..off + CELL_SIZE] {
1814 bitmask[i / 8] |= 1 << (i % 8);
1815 dirty_count += 1;
1816 }
1817 }
1818 if dirty_count == 0 {
1819 return None;
1820 }
1821
1822 let mut op = Vec::with_capacity(1 + bitmask_len + dirty_count * CELL_SIZE);
1823 op.push(OP_PATCH_CELLS);
1824 op.extend_from_slice(&bitmask);
1825 for byte_pos in 0..CELL_SIZE {
1826 for i in 0..total_cells {
1827 if bitmask[i / 8] & (1 << (i % 8)) != 0 {
1828 op.push(current.cells[i * CELL_SIZE + byte_pos]);
1829 }
1830 }
1831 }
1832 Some(op)
1833}
1834
1835fn detect_vertical_scroll(current: &FrameState, previous: &FrameState) -> Option<i16> {
1836 let rows = current.rows as usize;
1837 let cols = current.cols as usize;
1838 if rows < 4 || cols == 0 {
1839 return None;
1840 }
1841 let row_bytes = cols * CELL_SIZE;
1842 let max_delta = rows.saturating_sub(1).min(8);
1843 let mut best: Option<(usize, i16)> = None;
1844
1845 for delta in 1..=max_delta {
1846 let overlap = rows - delta;
1847 if overlap < 3 {
1848 continue;
1849 }
1850 for signed_delta in [-(delta as i16), delta as i16] {
1851 let mut matched = 0usize;
1852 for row in 0..rows {
1853 let src_row = row as i32 - signed_delta as i32;
1854 if src_row < 0 || src_row >= rows as i32 {
1855 continue;
1856 }
1857 let cur_off = row * row_bytes;
1858 let prev_off = src_row as usize * row_bytes;
1859 if current.cells[cur_off..cur_off + row_bytes]
1860 == previous.cells[prev_off..prev_off + row_bytes]
1861 {
1862 matched += 1;
1863 }
1864 }
1865 if matched * 5 < overlap * 4 {
1866 continue;
1867 }
1868 let replace = match best {
1869 None => true,
1870 Some((best_matched, best_delta)) => {
1871 matched > best_matched
1872 || (matched == best_matched
1873 && signed_delta.unsigned_abs() < best_delta.unsigned_abs())
1874 }
1875 };
1876 if replace {
1877 best = Some((matched, signed_delta));
1878 }
1879 }
1880 }
1881
1882 best.map(|(_, delta)| delta)
1883}
1884
1885fn encode_copy_rect_op(out: &mut Vec<u8>, current: &FrameState, delta_rows: i16) {
1886 let rows = current.rows;
1887 let cols = current.cols;
1888 let delta = delta_rows.unsigned_abs();
1889 let (src_row, dst_row, copy_rows) = if delta_rows > 0 {
1890 (0, delta, rows.saturating_sub(delta))
1891 } else {
1892 (delta, 0, rows.saturating_sub(delta))
1893 };
1894 out.push(OP_COPY_RECT);
1895 out.extend_from_slice(&src_row.to_le_bytes());
1896 out.extend_from_slice(&0u16.to_le_bytes());
1897 out.extend_from_slice(&dst_row.to_le_bytes());
1898 out.extend_from_slice(&0u16.to_le_bytes());
1899 out.extend_from_slice(©_rows.to_le_bytes());
1900 out.extend_from_slice(&cols.to_le_bytes());
1901}
1902
1903fn apply_vertical_scroll_copy(frame: &mut FrameState, delta_rows: i16) {
1904 let delta = delta_rows.unsigned_abs();
1905 if delta == 0 || delta >= frame.rows {
1906 return;
1907 }
1908 let (src_row, dst_row, rows) = if delta_rows > 0 {
1909 (0, delta, frame.rows - delta)
1910 } else {
1911 (delta, 0, frame.rows - delta)
1912 };
1913 apply_copy_rect_frame(frame, src_row, 0, dst_row, 0, rows, frame.cols);
1914}
1915
1916fn apply_copy_rect_frame(
1917 frame: &mut FrameState,
1918 src_row: u16,
1919 src_col: u16,
1920 dst_row: u16,
1921 dst_col: u16,
1922 rows: u16,
1923 cols: u16,
1924) {
1925 let rows = rows
1926 .min(frame.rows.saturating_sub(src_row))
1927 .min(frame.rows.saturating_sub(dst_row));
1928 let cols = cols
1929 .min(frame.cols.saturating_sub(src_col))
1930 .min(frame.cols.saturating_sub(dst_col));
1931 if rows == 0 || cols == 0 {
1932 return;
1933 }
1934 let mut temp = vec![0u8; rows as usize * cols as usize * CELL_SIZE];
1935 for r in 0..rows as usize {
1936 let src_off = frame.cell_offset(src_row + r as u16, src_col);
1937 let src_end = src_off + cols as usize * CELL_SIZE;
1938 let dst_off = r * cols as usize * CELL_SIZE;
1939 temp[dst_off..dst_off + cols as usize * CELL_SIZE]
1940 .copy_from_slice(&frame.cells[src_off..src_end]);
1941 }
1942 for r in 0..rows as usize {
1943 let dst_off = frame.cell_offset(dst_row + r as u16, dst_col);
1944 let dst_end = dst_off + cols as usize * CELL_SIZE;
1945 let src_off = r * cols as usize * CELL_SIZE;
1946 frame.cells[dst_off..dst_end]
1947 .copy_from_slice(&temp[src_off..src_off + cols as usize * CELL_SIZE]);
1948 }
1949}
1950
1951fn append_full_width_fill_ops(
1952 current: &FrameState,
1953 basis: &mut FrameState,
1954 out: &mut Vec<u8>,
1955 op_count: &mut u16,
1956) {
1957 let rows = current.rows as usize;
1958 let cols = current.cols as usize;
1959 if rows == 0 || cols == 0 {
1960 return;
1961 }
1962
1963 let row_bytes = cols * CELL_SIZE;
1964 let mut row = 0usize;
1965 while row < rows {
1966 let row_off = row * row_bytes;
1967 if current.cells[row_off..row_off + row_bytes] == basis.cells[row_off..row_off + row_bytes]
1968 {
1969 row += 1;
1970 continue;
1971 }
1972 let Some(cell) = uniform_row_cell(current, row) else {
1973 row += 1;
1974 continue;
1975 };
1976 let mut end = row + 1;
1977 while end < rows {
1978 if uniform_row_cell(current, end).as_ref() != Some(&cell) {
1979 break;
1980 }
1981 end += 1;
1982 }
1983
1984 if *op_count == u16::MAX {
1985 break;
1986 }
1987 out.push(OP_FILL_RECT);
1988 out.extend_from_slice(&(row as u16).to_le_bytes());
1989 out.extend_from_slice(&0u16.to_le_bytes());
1990 out.extend_from_slice(&((end - row) as u16).to_le_bytes());
1991 out.extend_from_slice(¤t.cols.to_le_bytes());
1992 out.extend_from_slice(&cell);
1993 *op_count = op_count.saturating_add(1);
1994
1995 for r in row..end {
1996 let row_off = basis.cell_offset(r as u16, 0);
1997 for c in 0..cols {
1998 let off = row_off + c * CELL_SIZE;
1999 basis.cells[off..off + CELL_SIZE].copy_from_slice(&cell);
2000 }
2001 }
2002
2003 row = end;
2004 }
2005}
2006
2007fn uniform_row_cell(frame: &FrameState, row: usize) -> Option<[u8; CELL_SIZE]> {
2008 let cols = frame.cols as usize;
2009 if row >= frame.rows as usize || cols == 0 {
2010 return None;
2011 }
2012 let start = row * cols * CELL_SIZE;
2013 let mut first = [0u8; CELL_SIZE];
2014 first.copy_from_slice(&frame.cells[start..start + CELL_SIZE]);
2015 if first[1] & 0b110 != 0 {
2016 return None;
2017 }
2018 for col in 1..cols {
2019 let off = start + col * CELL_SIZE;
2020 if frame.cells[off..off + CELL_SIZE] != first {
2021 return None;
2022 }
2023 }
2024 Some(first)
2025}
2026
2027fn encode_cell(dst: &mut [u8], ch: Option<char>, style: CellStyle, wide: bool, wide_cont: bool) {
2028 dst.fill(0);
2029
2030 let mut f0 = 0u8;
2031 encode_color(style.fg, &mut f0, &mut dst[2..5], false);
2032 encode_color(style.bg, &mut f0, &mut dst[5..8], true);
2033 if style.bold {
2034 f0 |= 1 << 4;
2035 }
2036 if style.dim {
2037 f0 |= 1 << 5;
2038 }
2039 if style.italic {
2040 f0 |= 1 << 6;
2041 }
2042 if style.underline {
2043 f0 |= 1 << 7;
2044 }
2045 dst[0] = f0;
2046
2047 let mut f1 = 0u8;
2048 if style.inverse {
2049 f1 |= 1;
2050 }
2051 if wide {
2052 f1 |= 1 << 1;
2053 }
2054 if wide_cont {
2055 f1 |= 1 << 2;
2056 }
2057 if let Some(ch) = ch {
2058 let mut buf = [0u8; 4];
2059 let encoded = ch.encode_utf8(&mut buf).as_bytes();
2060 let len = encoded.len().min(4);
2061 dst[8..8 + len].copy_from_slice(&encoded[..len]);
2062 f1 |= (len as u8) << 3;
2063 }
2064 dst[1] = f1;
2065}
2066
2067fn encode_color(color: Color, flags: &mut u8, dst: &mut [u8], is_bg: bool) {
2068 let shift = if is_bg { 2 } else { 0 };
2069 match color {
2070 Color::Default => {}
2071 Color::Indexed(idx) => {
2072 *flags |= 1 << shift;
2073 dst[0] = idx;
2074 }
2075 Color::Rgb(r, g, b) => {
2076 *flags |= 2 << shift;
2077 dst[0] = r;
2078 dst[1] = g;
2079 dst[2] = b;
2080 }
2081 }
2082}
2083
2084fn wrap_text_lines(text: &str, width: usize) -> Vec<String> {
2085 if width == 0 {
2086 return Vec::new();
2087 }
2088 let mut out = Vec::new();
2089 for paragraph in text.split('\n') {
2090 if paragraph.is_empty() {
2091 out.push(String::new());
2092 continue;
2093 }
2094 let mut line = String::new();
2095 let mut line_width = 0usize;
2096 for word in paragraph.split_whitespace() {
2097 push_wrapped_word(word, width, &mut out, &mut line, &mut line_width);
2098 }
2099 if !line.is_empty() {
2100 out.push(line);
2101 }
2102 }
2103 if out.is_empty() {
2104 out.push(String::new());
2105 }
2106 out
2107}
2108
2109fn push_wrapped_word(
2110 word: &str,
2111 width: usize,
2112 out: &mut Vec<String>,
2113 line: &mut String,
2114 line_width: &mut usize,
2115) {
2116 let word_width = UnicodeWidthStr::width(word);
2117 if line.is_empty() {
2118 if word_width <= width {
2119 line.push_str(word);
2120 *line_width = word_width;
2121 return;
2122 }
2123 } else if *line_width + 1 + word_width <= width {
2124 line.push(' ');
2125 line.push_str(word);
2126 *line_width += 1 + word_width;
2127 return;
2128 } else {
2129 out.push(std::mem::take(line));
2130 *line_width = 0;
2131 if word_width <= width {
2132 line.push_str(word);
2133 *line_width = word_width;
2134 return;
2135 }
2136 }
2137
2138 for ch in word.chars() {
2139 let ch_width = UnicodeWidthChar::width(ch).unwrap_or(1).max(1);
2140 if *line_width + ch_width > width && !line.is_empty() {
2141 out.push(std::mem::take(line));
2142 *line_width = 0;
2143 }
2144 line.push(ch);
2145 *line_width += ch_width;
2146 }
2147}
2148
2149#[cfg(test)]
2150mod tests {
2151 use super::*;
2152
2153 #[test]
2154 fn update_round_trip_preserves_title_and_cells() {
2155 let style = CellStyle::default();
2156 let mut prev = FrameState::new(2, 8);
2157 prev.set_title("one");
2158 prev.write_text(0, 0, "hello", style);
2159
2160 let mut next = prev.clone();
2161 next.set_title("two");
2162 next.write_text(1, 0, "world", style);
2163
2164 let baseline = build_update_msg(7, &prev, &FrameState::default()).unwrap();
2165 let delta = build_update_msg(7, &next, &prev).unwrap();
2166
2167 let mut term = TerminalState::new(2, 8);
2168 let ServerMsg::Update { payload, .. } = parse_server_msg(&baseline).unwrap() else {
2169 panic!("expected update");
2170 };
2171 assert!(term.feed_compressed(payload));
2172 assert_eq!(term.title(), "one");
2173
2174 let ServerMsg::Update { payload, .. } = parse_server_msg(&delta).unwrap() else {
2175 panic!("expected update");
2176 };
2177 assert!(term.feed_compressed(payload));
2178 assert_eq!(term.title(), "two");
2179 assert_eq!(term.get_all_text(), "hello\nworld");
2180 }
2181
2182 #[test]
2183 fn title_can_be_cleared_via_update() {
2184 let style = CellStyle::default();
2185 let mut prev = FrameState::new(1, 4);
2186 prev.set_title("busy");
2187 prev.write_text(0, 0, "ping", style);
2188
2189 let mut next = prev.clone();
2190 next.set_title("");
2191
2192 let baseline = build_update_msg(1, &prev, &FrameState::default()).unwrap();
2193 let delta = build_update_msg(1, &next, &prev).unwrap();
2194
2195 let mut term = TerminalState::new(1, 4);
2196 let ServerMsg::Update { payload, .. } = parse_server_msg(&baseline).unwrap() else {
2197 panic!("expected update");
2198 };
2199 term.feed_compressed(payload);
2200 let ServerMsg::Update { payload, .. } = parse_server_msg(&delta).unwrap() else {
2201 panic!("expected update");
2202 };
2203 term.feed_compressed(payload);
2204 assert_eq!(term.title(), "");
2205 }
2206
2207 #[test]
2208 fn scroll_heavy_update_can_use_ops_payload() {
2209 let style = CellStyle::default();
2210 let mut prev = FrameState::new(5, 6);
2211 prev.write_text(0, 0, "one", style);
2212 prev.write_text(1, 0, "two", style);
2213 prev.write_text(2, 0, "three", style);
2214 prev.write_text(3, 0, "four", style);
2215 prev.write_text(4, 0, "five", style);
2216
2217 let mut next = FrameState::new(5, 6);
2218 next.write_text(0, 0, "two", style);
2219 next.write_text(1, 0, "three", style);
2220 next.write_text(2, 0, "four", style);
2221 next.write_text(3, 0, "five", style);
2222
2223 let delta = build_update_msg(9, &next, &prev).unwrap();
2224 let ServerMsg::Update { payload, .. } = parse_server_msg(&delta).unwrap() else {
2225 panic!("expected update");
2226 };
2227 let decoded = decompress_size_prepended(payload).unwrap();
2228 let title_field = u16::from_le_bytes([decoded[10], decoded[11]]);
2229 assert_ne!(title_field & OPS_PRESENT, 0);
2230
2231 let mut term = TerminalState::new(5, 6);
2232 let baseline = build_update_msg(9, &prev, &FrameState::default()).unwrap();
2233 let ServerMsg::Update { payload, .. } = parse_server_msg(&baseline).unwrap() else {
2234 panic!("expected update");
2235 };
2236 assert!(term.feed_compressed(payload));
2237 let ServerMsg::Update { payload, .. } = parse_server_msg(&delta).unwrap() else {
2238 panic!("expected update");
2239 };
2240 assert!(term.feed_compressed(payload));
2241 assert_eq!(term.get_all_text(), "two\nthree\nfour\nfive\n");
2242 }
2243
2244 #[test]
2245 fn cooked_scroll_heavy_update_uses_copy_rect_op() {
2246 let style = CellStyle::default();
2247 let mut prev = FrameState::new(5, 6);
2248 prev.set_mode(MODE_ECHO | MODE_ICANON);
2249 prev.write_text(0, 0, "one", style);
2250 prev.write_text(1, 0, "two", style);
2251 prev.write_text(2, 0, "three", style);
2252 prev.write_text(3, 0, "four", style);
2253 prev.write_text(4, 0, "five", style);
2254
2255 let mut next = FrameState::new(5, 6);
2256 next.set_mode(MODE_ECHO | MODE_ICANON);
2257 next.write_text(0, 0, "two", style);
2258 next.write_text(1, 0, "three", style);
2259 next.write_text(2, 0, "four", style);
2260 next.write_text(3, 0, "five", style);
2261
2262 let delta = build_update_msg(9, &next, &prev).unwrap();
2263 let ServerMsg::Update { payload, .. } = parse_server_msg(&delta).unwrap() else {
2264 panic!("expected update");
2265 };
2266 let decoded = decompress_size_prepended(payload).unwrap();
2267 let op_count = u16::from_le_bytes([decoded[12], decoded[13]]);
2268 assert!(op_count >= 1);
2269 assert_eq!(decoded[14], OP_COPY_RECT);
2270 }
2271
2272 #[test]
2273 fn mode_zero_scroll_uses_copy_rect() {
2274 let style = CellStyle::default();
2275 let mut prev = FrameState::new(5, 6);
2276 prev.write_text(0, 0, "one", style);
2277 prev.write_text(1, 0, "two", style);
2278 prev.write_text(2, 0, "three", style);
2279 prev.write_text(3, 0, "four", style);
2280 prev.write_text(4, 0, "five", style);
2281
2282 let mut next = FrameState::new(5, 6);
2283 next.write_text(0, 0, "two", style);
2284 next.write_text(1, 0, "three", style);
2285 next.write_text(2, 0, "four", style);
2286 next.write_text(3, 0, "five", style);
2287
2288 let delta = build_update_msg(9, &next, &prev).unwrap();
2289 let ServerMsg::Update { payload, .. } = parse_server_msg(&delta).unwrap() else {
2290 panic!("expected update");
2291 };
2292 let decoded = decompress_size_prepended(payload).unwrap();
2293 let op_count = u16::from_le_bytes([decoded[12], decoded[13]]);
2294 assert!(op_count >= 1);
2295 assert_eq!(decoded[14], OP_COPY_RECT);
2297
2298 let baseline = build_update_msg(9, &prev, &FrameState::new(5, 6)).unwrap();
2300 let mut state = TerminalState::new(5, 6);
2301 let ServerMsg::Update { payload: bp, .. } = parse_server_msg(&baseline).unwrap() else {
2302 panic!("expected update");
2303 };
2304 state.feed_compressed(bp);
2305 state.feed_compressed(payload);
2306 assert_eq!(state.frame().cells(), next.cells());
2307 }
2308
2309 #[test]
2310 fn callback_renderer_wraps_text() {
2311 let mut renderer = CallbackRenderer::new(2, 8);
2312 renderer.render(|dom| {
2313 dom.wrapped_text(
2314 Rect::new(0, 0, 2, 8),
2315 "alpha beta gamma",
2316 CellStyle::default(),
2317 );
2318 });
2319 assert_eq!(renderer.frame().get_all_text(), "alpha\nbeta");
2320 }
2321
2322 #[test]
2323 fn scrolling_text_shows_tail() {
2324 let mut frame = FrameState::new(3, 8);
2325 frame.write_scrolling_text(
2326 Rect::new(0, 0, 3, 8),
2327 &["one", "two", "three", "four"],
2328 0,
2329 CellStyle::default(),
2330 );
2331 assert_eq!(frame.get_all_text(), "two\nthree\nfour");
2332 }
2333
2334 #[test]
2335 fn search_results_round_trip_with_context() {
2336 let msg = [
2337 vec![S2C_SEARCH_RESULTS],
2338 7u16.to_le_bytes().to_vec(),
2339 1u16.to_le_bytes().to_vec(),
2340 42u16.to_le_bytes().to_vec(),
2341 1234u32.to_le_bytes().to_vec(),
2342 vec![1, 0b111],
2343 9u32.to_le_bytes().to_vec(),
2344 5u16.to_le_bytes().to_vec(),
2345 b"hello".to_vec(),
2346 ]
2347 .concat();
2348
2349 let ServerMsg::SearchResults {
2350 request_id,
2351 results,
2352 } = parse_server_msg(&msg).unwrap()
2353 else {
2354 panic!("expected search results");
2355 };
2356 assert_eq!(request_id, 7);
2357 assert_eq!(results.len(), 1);
2358 assert_eq!(results[0].pty_id, 42);
2359 assert_eq!(results[0].score, 1234);
2360 assert_eq!(results[0].primary_source, 1);
2361 assert_eq!(results[0].matched_sources, 0b111);
2362 assert_eq!(results[0].scroll_offset, Some(9));
2363 assert_eq!(results[0].context, b"hello");
2364 }
2365
2366 #[test]
2369 fn msg_create_no_tag_has_zero_tag_len() {
2370 let msg = msg_create(24, 80);
2371 assert_eq!(msg.len(), 7);
2372 assert_eq!(msg[0], C2S_CREATE);
2373 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 24);
2374 assert_eq!(u16::from_le_bytes([msg[3], msg[4]]), 80);
2375 assert_eq!(u16::from_le_bytes([msg[5], msg[6]]), 0);
2376 }
2377
2378 #[test]
2379 fn msg_create_tagged_encodes_tag() {
2380 let msg = msg_create_tagged(24, 80, "my-pty");
2381 assert_eq!(msg[0], C2S_CREATE);
2382 let tag_len = u16::from_le_bytes([msg[5], msg[6]]) as usize;
2383 assert_eq!(tag_len, 6);
2384 assert_eq!(&msg[7..7 + tag_len], b"my-pty");
2385 assert_eq!(msg.len(), 7 + tag_len);
2386 }
2387
2388 #[test]
2389 fn msg_create_tagged_command_encodes_both() {
2390 let msg = msg_create_tagged_command(30, 120, "editor", "vim");
2391 let tag_len = u16::from_le_bytes([msg[5], msg[6]]) as usize;
2392 assert_eq!(tag_len, 6);
2393 assert_eq!(&msg[7..13], b"editor");
2394 assert_eq!(&msg[13..], b"vim");
2395 }
2396
2397 #[test]
2398 fn msg_create_command_has_empty_tag() {
2399 let msg = msg_create_command(24, 80, "ls");
2400 let tag_len = u16::from_le_bytes([msg[5], msg[6]]) as usize;
2401 assert_eq!(tag_len, 0);
2402 assert_eq!(&msg[7..], b"ls");
2403 }
2404
2405 #[test]
2406 fn msg_create_tagged_empty_tag() {
2407 let msg = msg_create_tagged(24, 80, "");
2408 assert_eq!(msg.len(), 7);
2409 assert_eq!(u16::from_le_bytes([msg[5], msg[6]]), 0);
2410 }
2411
2412 #[test]
2413 fn msg_create_tagged_unicode_tag() {
2414 let msg = msg_create_tagged(24, 80, "日本語");
2415 let tag_len = u16::from_le_bytes([msg[5], msg[6]]) as usize;
2416 assert_eq!(tag_len, "日本語".len());
2417 assert_eq!(std::str::from_utf8(&msg[7..7 + tag_len]).unwrap(), "日本語");
2418 }
2419
2420 #[test]
2421 fn parse_created_with_tag() {
2422 let mut wire = vec![S2C_CREATED, 0x05, 0x00];
2423 wire.extend_from_slice(b"hello");
2424 let msg = parse_server_msg(&wire).unwrap();
2425 match msg {
2426 ServerMsg::Created { pty_id, tag } => {
2427 assert_eq!(pty_id, 5);
2428 assert_eq!(tag, "hello");
2429 }
2430 _ => panic!("expected Created"),
2431 }
2432 }
2433
2434 #[test]
2435 fn parse_created_without_tag() {
2436 let wire = vec![S2C_CREATED, 0x03, 0x00];
2437 let msg = parse_server_msg(&wire).unwrap();
2438 match msg {
2439 ServerMsg::Created { pty_id, tag } => {
2440 assert_eq!(pty_id, 3);
2441 assert_eq!(tag, "");
2442 }
2443 _ => panic!("expected Created"),
2444 }
2445 }
2446
2447 #[test]
2448 fn parse_created_n_with_tag() {
2449 let mut wire = vec![S2C_CREATED_N, 0x2a, 0x00, 0x05, 0x00];
2450 wire.extend_from_slice(b"hello");
2451 let msg = parse_server_msg(&wire).unwrap();
2452 match msg {
2453 ServerMsg::CreatedN { nonce, pty_id, tag } => {
2454 assert_eq!(nonce, 42);
2455 assert_eq!(pty_id, 5);
2456 assert_eq!(tag, "hello");
2457 }
2458 _ => panic!("expected CreatedN"),
2459 }
2460 }
2461
2462 #[test]
2463 fn msg_create_n_format() {
2464 let msg = msg_create_n(42, 24, 80, "test");
2465 assert_eq!(msg[0], C2S_CREATE_N);
2466 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 42);
2467 assert_eq!(u16::from_le_bytes([msg[3], msg[4]]), 24);
2468 assert_eq!(u16::from_le_bytes([msg[5], msg[6]]), 80);
2469 assert_eq!(u16::from_le_bytes([msg[7], msg[8]]), 4);
2470 assert_eq!(&msg[9..], b"test");
2471 }
2472
2473 #[test]
2474 fn msg_create_n_command_format() {
2475 let msg = msg_create_n_command(7, 30, 120, "bg", "make build");
2476 assert_eq!(msg[0], C2S_CREATE_N);
2477 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 7);
2478 assert_eq!(u16::from_le_bytes([msg[3], msg[4]]), 30);
2479 assert_eq!(u16::from_le_bytes([msg[5], msg[6]]), 120);
2480 let tag_len = u16::from_le_bytes([msg[7], msg[8]]) as usize;
2481 assert_eq!(tag_len, 2);
2482 assert_eq!(&msg[9..9 + tag_len], b"bg");
2483 assert_eq!(&msg[9 + tag_len..], b"make build");
2484 }
2485
2486 #[test]
2487 fn parse_list_with_tags() {
2488 let mut wire = vec![S2C_LIST, 0x02, 0x00];
2490 wire.extend_from_slice(&1u16.to_le_bytes());
2492 wire.extend_from_slice(&2u16.to_le_bytes());
2493 wire.extend_from_slice(b"ab");
2494 wire.extend_from_slice(&0u16.to_le_bytes());
2495 wire.extend_from_slice(&2u16.to_le_bytes());
2497 wire.extend_from_slice(&0u16.to_le_bytes());
2498 wire.extend_from_slice(&0u16.to_le_bytes());
2499
2500 let msg = parse_server_msg(&wire).unwrap();
2501 match msg {
2502 ServerMsg::List { entries } => {
2503 assert_eq!(entries.len(), 2);
2504 assert_eq!(entries[0].pty_id, 1);
2505 assert_eq!(entries[0].tag, "ab");
2506 assert_eq!(entries[1].pty_id, 2);
2507 assert_eq!(entries[1].tag, "");
2508 }
2509 _ => panic!("expected List"),
2510 }
2511 }
2512
2513 #[test]
2514 fn parse_list_empty() {
2515 let wire = vec![S2C_LIST, 0x00, 0x00];
2516 let msg = parse_server_msg(&wire).unwrap();
2517 match msg {
2518 ServerMsg::List { entries } => assert_eq!(entries.len(), 0),
2519 _ => panic!("expected List"),
2520 }
2521 }
2522
2523 #[test]
2524 fn parse_list_truncated_gracefully() {
2525 let mut wire = vec![S2C_LIST, 0x02, 0x00];
2527 wire.extend_from_slice(&1u16.to_le_bytes());
2528 wire.extend_from_slice(&0u16.to_le_bytes());
2529 let msg = parse_server_msg(&wire).unwrap();
2531 match msg {
2532 ServerMsg::List { entries } => assert_eq!(entries.len(), 1),
2533 _ => panic!("expected List"),
2534 }
2535 }
2536
2537 #[test]
2538 fn parse_list_with_long_tags() {
2539 let long_tag = "a".repeat(300);
2540 let mut wire = vec![S2C_LIST, 0x01, 0x00];
2541 wire.extend_from_slice(&42u16.to_le_bytes());
2542 wire.extend_from_slice(&(long_tag.len() as u16).to_le_bytes());
2543 wire.extend_from_slice(long_tag.as_bytes());
2544
2545 let msg = parse_server_msg(&wire).unwrap();
2546 match msg {
2547 ServerMsg::List { entries } => {
2548 assert_eq!(entries.len(), 1);
2549 assert_eq!(entries[0].pty_id, 42);
2550 assert_eq!(entries[0].tag, long_tag);
2551 }
2552 _ => panic!("expected List"),
2553 }
2554 }
2555
2556 #[test]
2557 fn create_and_created_tag_round_trip() {
2558 let create_msg = msg_create_tagged(24, 80, "my-session");
2560 let tag_len = u16::from_le_bytes([create_msg[5], create_msg[6]]) as usize;
2561 let tag = std::str::from_utf8(&create_msg[7..7 + tag_len]).unwrap();
2562
2563 let mut created_wire = vec![S2C_CREATED, 0x07, 0x00]; created_wire.extend_from_slice(tag.as_bytes());
2566
2567 let msg = parse_server_msg(&created_wire).unwrap();
2568 match msg {
2569 ServerMsg::Created {
2570 pty_id,
2571 tag: parsed_tag,
2572 } => {
2573 assert_eq!(pty_id, 7);
2574 assert_eq!(parsed_tag, "my-session");
2575 }
2576 _ => panic!("expected Created"),
2577 }
2578 }
2579
2580 #[test]
2583 fn frame_state_accessors() {
2584 let mut f = FrameState::new(4, 10);
2585 assert_eq!(f.rows(), 4);
2586 assert_eq!(f.cols(), 10);
2587 assert_eq!(f.cursor_row(), 0);
2588 assert_eq!(f.cursor_col(), 0);
2589 assert_eq!(f.mode(), 0);
2590 assert_eq!(f.title(), "");
2591 assert_eq!(f.cells().len(), 4 * 10 * CELL_SIZE);
2592 assert_eq!(f.cells_mut().len(), 4 * 10 * CELL_SIZE);
2593 assert!(f.overflow().is_empty());
2594 assert!(f.overflow_mut().is_empty());
2595 }
2596
2597 #[test]
2598 fn frame_state_from_parts() {
2599 let cells = vec![0u8; 2 * 4 * CELL_SIZE];
2600 let f = FrameState::from_parts(2, 4, 1, 3, 0x0F, "hello", cells.clone());
2601 assert_eq!(f.rows(), 2);
2602 assert_eq!(f.cols(), 4);
2603 assert_eq!(f.cursor_row(), 1);
2604 assert_eq!(f.cursor_col(), 3);
2605 assert_eq!(f.mode(), 0x0F);
2606 assert_eq!(f.title(), "hello");
2607 assert_eq!(f.cells(), &cells[..]);
2608 }
2609
2610 #[test]
2611 fn frame_state_from_parts_wrong_size() {
2612 let cells = vec![0u8; 10]; let f = FrameState::from_parts(2, 4, 0, 0, 0, "", cells);
2615 assert_eq!(f.cells().len(), 2 * 4 * CELL_SIZE);
2616 }
2617
2618 #[test]
2619 fn frame_state_resize() {
2620 let mut f = FrameState::new(4, 10);
2621 f.set_cursor(3, 9);
2622 f.resize(2, 5);
2623 assert_eq!(f.rows(), 2);
2624 assert_eq!(f.cols(), 5);
2625 assert_eq!(f.cursor_row(), 1); assert_eq!(f.cursor_col(), 4); assert_eq!(f.cells().len(), 2 * 5 * CELL_SIZE);
2628 }
2629
2630 #[test]
2631 fn frame_state_resize_noop() {
2632 let mut f = FrameState::new(4, 10);
2633 let ptr_before = f.cells().as_ptr();
2634 f.resize(4, 10); let ptr_after = f.cells().as_ptr();
2636 assert_eq!(ptr_before, ptr_after); }
2638
2639 #[test]
2640 fn frame_state_set_cursor_clamps() {
2641 let mut f = FrameState::new(4, 10);
2642 f.set_cursor(100, 200);
2643 assert_eq!(f.cursor_row(), 3);
2644 assert_eq!(f.cursor_col(), 9);
2645 }
2646
2647 #[test]
2648 fn frame_state_set_title() {
2649 let mut f = FrameState::new(2, 2);
2650 assert!(f.set_title("new title"));
2651 assert_eq!(f.title(), "new title");
2652 assert!(!f.set_title("new title")); assert!(f.set_title("other"));
2654 }
2655
2656 #[test]
2657 fn frame_state_get_text_and_write_text() {
2658 let mut f = FrameState::new(2, 10);
2659 f.write_text(0, 0, "Hello", CellStyle::default());
2660 f.write_text(1, 0, "World", CellStyle::default());
2661 let text = f.get_text(0, 0, 1, 9);
2662 assert!(text.contains("Hello"));
2663 assert!(text.contains("World"));
2664 let all = f.get_all_text();
2665 assert!(all.contains("Hello"));
2666 }
2667
2668 #[test]
2669 fn frame_state_get_text_empty() {
2670 let f = FrameState::new(0, 0);
2671 assert_eq!(f.get_text(0, 0, 0, 0), "");
2672 assert_eq!(f.get_all_text(), "");
2673 }
2674
2675 #[test]
2676 fn frame_state_get_cell() {
2677 let f = FrameState::new(2, 4);
2678 let cell = f.get_cell(0, 0);
2679 assert_eq!(cell.len(), CELL_SIZE);
2680 assert!(f.get_cell(100, 100).is_empty());
2682 }
2683
2684 #[test]
2685 fn frame_state_cell_content_blank() {
2686 let f = FrameState::new(2, 4);
2687 assert_eq!(f.cell_content(0, 0), " "); assert_eq!(f.cell_content(100, 0), ""); }
2690
2691 #[test]
2692 fn frame_state_cell_content_with_text() {
2693 let mut f = FrameState::new(2, 10);
2694 f.write_text(0, 0, "A", CellStyle::default());
2695 assert_eq!(f.cell_content(0, 0), "A");
2696 }
2697
2698 #[test]
2699 fn frame_state_fill_rect() {
2700 let mut f = FrameState::new(4, 10);
2701 f.fill_rect(Rect::new(0, 0, 2, 5), 'X', CellStyle::default());
2702 assert_eq!(f.cell_content(0, 0), "X");
2703 assert_eq!(f.cell_content(1, 4), "X");
2704 assert_eq!(f.cell_content(2, 0), " "); }
2706
2707 #[test]
2708 fn frame_state_wrapped_text() {
2709 let mut f = FrameState::new(4, 10);
2710 let lines =
2711 f.write_wrapped_text(Rect::new(0, 0, 4, 5), "hello world", CellStyle::default());
2712 assert!(lines >= 2); }
2714
2715 #[test]
2716 fn frame_state_wrapped_text_empty_rect() {
2717 let mut f = FrameState::new(4, 10);
2718 assert_eq!(
2719 f.write_wrapped_text(Rect::new(0, 0, 0, 0), "hi", CellStyle::default()),
2720 0
2721 );
2722 }
2723
2724 #[test]
2725 fn frame_state_scrolling_text() {
2726 let mut f = FrameState::new(4, 10);
2727 f.write_scrolling_text(
2728 Rect::new(0, 0, 3, 10),
2729 &["line1", "line2", "line3", "line4"],
2730 0,
2731 CellStyle::default(),
2732 );
2733 assert_eq!(f.cell_content(0, 0), "l"); }
2736
2737 #[test]
2738 fn frame_state_scrolling_text_empty_rect() {
2739 let mut f = FrameState::new(4, 10);
2740 f.write_scrolling_text(Rect::new(0, 0, 0, 0), &["hi"], 0, CellStyle::default());
2741 }
2743
2744 #[test]
2745 fn frame_state_clear() {
2746 let mut f = FrameState::new(2, 4);
2747 f.write_text(0, 0, "AB", CellStyle::default());
2748 f.clear(CellStyle::default());
2749 assert_eq!(f.cell_content(0, 0), " ");
2750 }
2751
2752 #[test]
2755 fn terminal_state_accessors() {
2756 let t = TerminalState::new(24, 80);
2757 assert_eq!(t.rows(), 24);
2758 assert_eq!(t.cols(), 80);
2759 assert_eq!(t.cursor_row(), 0);
2760 assert_eq!(t.cursor_col(), 0);
2761 assert_eq!(t.mode(), 0);
2762 assert_eq!(t.title(), "");
2763 assert_eq!(t.cells().len(), 24 * 80 * CELL_SIZE);
2764 assert_eq!(t.frame().rows(), 24);
2765 }
2766
2767 #[test]
2768 fn terminal_state_mutators() {
2769 let mut t = TerminalState::new(4, 10);
2770 t.frame_mut().set_title("test");
2771 assert_eq!(t.title(), "test");
2772 }
2773
2774 #[test]
2775 fn terminal_state_set_title() {
2776 let mut t = TerminalState::new(4, 10);
2777 assert!(t.frame_mut().set_title("hello"));
2778 assert_eq!(t.title(), "hello");
2779 assert!(!t.frame_mut().set_title("hello")); }
2781
2782 #[test]
2783 fn terminal_state_get_text() {
2784 let t = TerminalState::new(2, 10);
2785 let text = t.get_text(0, 0, 0, 9);
2786 assert!(text.is_empty() || text.chars().all(|c| c == ' ' || c == '\n'));
2787 assert!(t.get_cell(0, 0).len() == CELL_SIZE);
2788 assert!(t.get_cell(100, 100).is_empty());
2789 }
2790
2791 #[test]
2792 fn terminal_state_resize() {
2793 let mut t = TerminalState::new(4, 10);
2794 t.frame_mut().resize(2, 5);
2795 assert_eq!(t.rows(), 2);
2798 assert_eq!(t.cols(), 5);
2799 }
2800
2801 #[test]
2802 fn terminal_state_feed_compressed_invalid() {
2803 let mut t = TerminalState::new(4, 10);
2804 assert!(!t.feed_compressed(b"garbage"));
2805 assert!(!t.feed_compressed(&[]));
2806 }
2807
2808 #[test]
2809 fn terminal_state_feed_compressed_batch_empty() {
2810 let mut t = TerminalState::new(4, 10);
2811 assert!(!t.feed_compressed_batch(&[]));
2812 }
2813
2814 #[test]
2815 fn terminal_state_feed_compressed_batch_truncated() {
2816 let mut t = TerminalState::new(4, 10);
2817 let batch = &[100, 0, 0, 0];
2819 assert!(!t.feed_compressed_batch(batch));
2820 }
2821
2822 #[test]
2825 fn msg_input_format() {
2826 let msg = msg_input(5, b"hello");
2827 assert_eq!(msg[0], C2S_INPUT);
2828 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 5);
2829 assert_eq!(&msg[3..], b"hello");
2830 }
2831
2832 #[test]
2833 fn msg_resize_format() {
2834 let msg = msg_resize(3, 24, 80);
2835 assert_eq!(msg[0], C2S_RESIZE);
2836 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 3);
2837 assert_eq!(u16::from_le_bytes([msg[3], msg[4]]), 24);
2838 assert_eq!(u16::from_le_bytes([msg[5], msg[6]]), 80);
2839 }
2840
2841 #[test]
2842 fn msg_resize_batch_format() {
2843 let msg = msg_resize_batch(&[(3, 24, 80), (5, 40, 120)]);
2844 assert_eq!(msg[0], C2S_RESIZE);
2845 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 3);
2846 assert_eq!(u16::from_le_bytes([msg[3], msg[4]]), 24);
2847 assert_eq!(u16::from_le_bytes([msg[5], msg[6]]), 80);
2848 assert_eq!(u16::from_le_bytes([msg[7], msg[8]]), 5);
2849 assert_eq!(u16::from_le_bytes([msg[9], msg[10]]), 40);
2850 assert_eq!(u16::from_le_bytes([msg[11], msg[12]]), 120);
2851 }
2852
2853 #[test]
2854 fn msg_focus_format() {
2855 let msg = msg_focus(7);
2856 assert_eq!(msg[0], C2S_FOCUS);
2857 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 7);
2858 assert_eq!(msg.len(), 3);
2859 }
2860
2861 #[test]
2862 fn msg_close_format() {
2863 let msg = msg_close(9);
2864 assert_eq!(msg[0], C2S_CLOSE);
2865 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 9);
2866 }
2867
2868 #[test]
2869 fn msg_subscribe_unsubscribe_format() {
2870 let sub = msg_subscribe(1);
2871 assert_eq!(sub[0], C2S_SUBSCRIBE);
2872 assert_eq!(u16::from_le_bytes([sub[1], sub[2]]), 1);
2873
2874 let unsub = msg_unsubscribe(2);
2875 assert_eq!(unsub[0], C2S_UNSUBSCRIBE);
2876 assert_eq!(u16::from_le_bytes([unsub[1], unsub[2]]), 2);
2877 }
2878
2879 #[test]
2880 fn msg_search_format() {
2881 let msg = msg_search(42, "test query");
2882 assert_eq!(msg[0], C2S_SEARCH);
2883 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 42);
2884 assert_eq!(&msg[3..], b"test query");
2885 }
2886
2887 #[test]
2888 fn msg_ack_format() {
2889 let msg = msg_ack();
2890 assert_eq!(msg, vec![C2S_ACK]);
2891 }
2892
2893 #[test]
2894 fn msg_scroll_format() {
2895 let msg = msg_scroll(5, 1000);
2896 assert_eq!(msg[0], C2S_SCROLL);
2897 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 5);
2898 assert_eq!(u32::from_le_bytes([msg[3], msg[4], msg[5], msg[6]]), 1000);
2899 }
2900
2901 #[test]
2902 fn msg_display_rate_format() {
2903 let msg = msg_display_rate(120);
2904 assert_eq!(msg[0], C2S_DISPLAY_RATE);
2905 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 120);
2906 }
2907
2908 #[test]
2909 fn msg_client_metrics_format() {
2910 let msg = msg_client_metrics(3, 5, 100);
2911 assert_eq!(msg[0], C2S_CLIENT_METRICS);
2912 assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 3);
2913 assert_eq!(u16::from_le_bytes([msg[3], msg[4]]), 5);
2914 assert_eq!(u16::from_le_bytes([msg[5], msg[6]]), 100);
2915 }
2916
2917 #[test]
2920 fn callback_renderer_resize() {
2921 let mut r = CallbackRenderer::new(2, 8);
2922 assert_eq!(r.frame().rows(), 2);
2923 r.resize(4, 16);
2924 assert_eq!(r.frame().rows(), 4);
2925 assert_eq!(r.frame().cols(), 16);
2926 }
2927
2928 #[test]
2929 fn callback_renderer_fill() {
2930 let mut r = CallbackRenderer::new(4, 10);
2931 r.render(|dom| {
2932 dom.fill(Rect::new(0, 0, 2, 5), '#', CellStyle::default());
2933 });
2934 assert_eq!(r.frame().cell_content(0, 0), "#");
2935 assert_eq!(r.frame().cell_content(1, 4), "#");
2936 }
2937
2938 #[test]
2939 fn callback_renderer_text() {
2940 let mut r = CallbackRenderer::new(4, 20);
2941 r.render(|dom| {
2942 dom.text(0, 0, "Hello", CellStyle::default());
2943 });
2944 assert_eq!(r.frame().cell_content(0, 0), "H");
2945 assert_eq!(r.frame().cell_content(0, 4), "o");
2946 }
2947
2948 #[test]
2949 fn callback_renderer_set_title() {
2950 let mut r = CallbackRenderer::new(2, 8);
2951 r.render(|dom| {
2952 dom.set_title("my title");
2953 });
2954 assert_eq!(r.frame().title(), "my title");
2955 }
2956
2957 #[test]
2958 fn callback_renderer_set_background() {
2959 let mut r = CallbackRenderer::new(2, 4);
2960 let style = CellStyle {
2961 bg: Color::Rgb(255, 0, 0),
2962 ..CellStyle::default()
2963 };
2964 r.render(|dom| {
2965 dom.set_background(style);
2966 });
2967 assert_eq!(r.frame().cells().len(), 2 * 4 * CELL_SIZE);
2969 }
2970
2971 #[test]
2972 fn callback_renderer_scrolling_text() {
2973 let mut r = CallbackRenderer::new(4, 20);
2974 r.render(|dom| {
2975 dom.scrolling_text(
2976 Rect::new(0, 0, 3, 20),
2977 ["a", "b", "c", "d", "e"].map(String::from),
2978 0,
2979 CellStyle::default(),
2980 );
2981 });
2982 assert_eq!(r.frame().cell_content(0, 0), "c");
2984 }
2985
2986 #[test]
2989 fn parse_empty_returns_none() {
2990 assert!(parse_server_msg(&[]).is_none());
2991 }
2992
2993 #[test]
2994 fn parse_unknown_type_returns_none() {
2995 assert!(parse_server_msg(&[0xFF, 0x00, 0x00]).is_none());
2996 }
2997
2998 #[test]
2999 fn parse_update_too_short() {
3000 assert!(parse_server_msg(&[S2C_UPDATE, 0x00]).is_none());
3001 }
3002
3003 #[test]
3004 fn parse_closed() {
3005 let msg = parse_server_msg(&[S2C_CLOSED, 0x05, 0x00]).unwrap();
3006 match msg {
3007 ServerMsg::Closed { pty_id } => assert_eq!(pty_id, 5),
3008 _ => panic!("expected Closed"),
3009 }
3010 }
3011
3012 #[test]
3013 fn parse_title() {
3014 let mut wire = vec![S2C_TITLE, 0x01, 0x00];
3015 wire.extend_from_slice(b"mytitle");
3016 let msg = parse_server_msg(&wire).unwrap();
3017 match msg {
3018 ServerMsg::Title { pty_id, title } => {
3019 assert_eq!(pty_id, 1);
3020 assert_eq!(title, b"mytitle");
3021 }
3022 _ => panic!("expected Title"),
3023 }
3024 }
3025
3026 #[test]
3029 fn build_update_msg_round_trip_with_resize() {
3030 let style = CellStyle::default();
3031 let mut prev = FrameState::new(2, 4);
3032 prev.write_text(0, 0, "AB", style);
3033
3034 let mut next = FrameState::new(3, 5); next.write_text(0, 0, "XY", style);
3036 next.set_title("resized");
3037
3038 let msg = build_update_msg(1, &next, &prev).unwrap();
3039 assert!(!msg.is_empty());
3040
3041 let mut t = TerminalState::new(2, 4);
3043 assert!(t.feed_compressed(&msg[3..])); assert_eq!(t.rows(), 3);
3045 assert_eq!(t.cols(), 5);
3046 assert_eq!(t.title(), "resized");
3047 }
3048
3049 #[test]
3050 fn build_update_msg_cursor_change() {
3051 let mut prev = FrameState::new(4, 10);
3052 prev.set_cursor(0, 0);
3053
3054 let mut next = prev.clone();
3055 next.set_cursor(2, 5);
3056
3057 let msg = build_update_msg(0, &next, &prev).unwrap();
3058
3059 let mut t = TerminalState::new(4, 10);
3060 assert!(t.feed_compressed(&msg[3..]));
3061 assert_eq!(t.cursor_row(), 2);
3062 assert_eq!(t.cursor_col(), 5);
3063 }
3064
3065 #[test]
3066 fn build_update_msg_mode_change() {
3067 let prev = FrameState::new(2, 4);
3068 let mut next = prev.clone();
3069 next.set_mode(0x0F);
3070
3071 let msg = build_update_msg(0, &next, &prev).unwrap();
3072 let mut t = TerminalState::new(2, 4);
3073 assert!(t.feed_compressed(&msg[3..]));
3074 assert_eq!(t.mode(), 0x0F);
3075 }
3076
3077 #[test]
3078 fn feed_compressed_batch_multiple_frames() {
3079 let style = CellStyle::default();
3080 let prev = FrameState::new(2, 4);
3081
3082 let mut mid = prev.clone();
3083 mid.write_text(0, 0, "AB", style);
3084 let msg1 = build_update_msg(0, &mid, &prev).unwrap();
3085
3086 let mut next = mid.clone();
3087 next.write_text(1, 0, "CD", style);
3088 let msg2 = build_update_msg(0, &next, &mid).unwrap();
3089
3090 let payload1 = &msg1[3..];
3092 let payload2 = &msg2[3..];
3093 let mut batch = Vec::new();
3094 batch.extend_from_slice(&(payload1.len() as u32).to_le_bytes());
3095 batch.extend_from_slice(payload1);
3096 batch.extend_from_slice(&(payload2.len() as u32).to_le_bytes());
3097 batch.extend_from_slice(payload2);
3098
3099 let mut t = TerminalState::new(2, 4);
3100 assert!(t.feed_compressed_batch(&batch));
3101 let text = t.get_all_text();
3102 assert!(text.contains("AB"));
3103 assert!(text.contains("CD"));
3104 }
3105}