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