Skip to main content

blit_remote/
lib.rs

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
13/// Per-row flag: this row's content continues on the next row (line wrap).
14pub const ROW_FLAG_WRAPPED: u8 = 1 << 0;
15
16/// Sentinel value for content_len indicating the cell's text lives in the
17/// overflow string table.  Bytes 8-11 then hold an FNV-1a hash of the full
18/// UTF-8 string (for diff correctness), and the actual string is stored in
19/// `FrameState::overflow` keyed by cell index.
20const 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;
31/// Desired viewport size(s): [0x01][pty_id:2][rows:2][cols:2]...
32/// Clients may batch multiple PTY resize entries in one message. The server
33/// mediates these per-client desired sizes into each PTY's effective size.
34/// A `rows, cols` pair of `0, 0` clears this client's desired size for that PTY.
35pub 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;
40/// Mouse event: [0x06][pty_id:2][type:1][button:1][col:2][row:2]
41/// type: 0=down, 1=up, 2=move
42/// button: 0=left, 1=mid, 2=right, 3=release, 64=wheel_up, 65=wheel_down
43/// The server generates the correct escape sequence based on mouse_mode and mouse_encoding.
44pub const C2S_MOUSE: u8 = 0x06;
45/// Restart an exited PTY: [0x07][pty_id:2]
46/// Server spawns a new shell in the same PTY slot, preserving the pty_id.
47pub 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;
56/// Generic create: [0x18][nonce:2][rows:2][cols:2][features:1][tag_len:2][tag:N][...optional fields]
57/// Features: bit 0 = has src_pty_id (2 bytes after tag), bit 1 = has command (remaining bytes after src_pty_id if present)
58/// Server responds with S2C_CREATED_N using the same nonce.
59pub const C2S_CREATE2: u8 = 0x18;
60pub const CREATE2_HAS_SRC_PTY: u8 = 1 << 0;
61pub const CREATE2_HAS_COMMAND: u8 = 1 << 1;
62/// Read text from a PTY's scrollback + viewport: [0x19][nonce:2][pty_id:2][offset:4][limit:4][flags:1]
63/// offset: number of lines to skip from the top (oldest = 0), or from the end if READ_TAIL is set
64/// limit: max lines to return (0 = all)
65/// flags: bit 0 = include ANSI styling, bit 1 = offset counts from the end
66/// Server responds with S2C_TEXT using the same nonce.
67pub const C2S_READ: u8 = 0x19;
68pub const READ_ANSI: u8 = 1 << 0;
69pub const READ_TAIL: u8 = 1 << 1;
70/// Copy text from a range of absolute row/col positions in scrollback + viewport:
71/// [0x1B][nonce:2][pty_id:2][start_tail:4][start_col:2][end_tail:4][end_col:2][flags:1]
72/// start_tail/end_tail: physical row distance from the bottom (0 = last row).
73/// start is the earlier position (closer to top), so start_tail >= end_tail.
74/// flags: reserved (0 for now).
75/// Server responds with S2C_TEXT using the same nonce.
76pub const C2S_COPY_RANGE: u8 = 0x1B;
77/// Send a signal to a PTY's session leader: [0x1A][pty_id:2][signal:4]
78/// signal is a raw libc signal number (e.g. SIGTERM=15, SIGKILL=9).
79pub const C2S_KILL: u8 = 0x1A;
80
81/// Keyboard input for a Wayland surface: [0x20][session_id:2][surface_id:2][data:N]
82/// data contains evdev keycodes encoded as [keycode:4][pressed:1] sequences.
83pub const C2S_SURFACE_INPUT: u8 = 0x20;
84/// Pointer motion/button for a Wayland surface: [0x21][session_id:2][surface_id:2][type:1][button:1][x:2][y:2]
85/// type: 0=down, 1=up, 2=move
86/// x,y: pixel coordinates relative to the surface origin
87pub const C2S_SURFACE_POINTER: u8 = 0x21;
88/// Pointer axis/scroll for a Wayland surface: [0x22][session_id:2][surface_id:2][axis:1][value_x100:4_signed]
89/// axis: 0=vertical, 1=horizontal
90/// value_x100: scroll amount * 100 (signed, positive = down/right)
91pub const C2S_SURFACE_POINTER_AXIS: u8 = 0x22;
92/// Resize a Wayland surface: [0x23][session_id:2][surface_id:2][width:2][height:2]
93pub const C2S_SURFACE_RESIZE: u8 = 0x23;
94/// Set keyboard/pointer focus to a Wayland surface: [0x24][session_id:2][surface_id:2]
95pub const C2S_SURFACE_FOCUS: u8 = 0x24;
96/// Send clipboard content to a Wayland surface:
97/// [0x25][session_id:2][surface_id:2][mime_len:2][mime:N][data_len:4][data:N]
98pub const C2S_CLIPBOARD: u8 = 0x25;
99/// Request a list of all compositor surfaces: [0x26][session_id:2]
100pub const C2S_SURFACE_LIST: u8 = 0x26;
101/// Request a screenshot of a surface:
102/// [0x27][session_id:2][surface_id:2]              — legacy (defaults to PNG lossless)
103/// [0x27][session_id:2][surface_id:2][format:1][quality:1] — extended
104/// format: 0 = PNG, 1 = AVIF.  quality: 0 = lossless, 1–100 = lossy (AVIF only).
105pub const C2S_SURFACE_CAPTURE: u8 = 0x27;
106pub const CAPTURE_FORMAT_PNG: u8 = 0;
107pub const CAPTURE_FORMAT_AVIF: u8 = 1;
108/// Subscribe to surface frame updates: [0x28][session_id:2][surface_id:2]
109pub const C2S_SURFACE_SUBSCRIBE: u8 = 0x28;
110/// Unsubscribe from surface frame updates: [0x29][session_id:2][surface_id:2]
111pub const C2S_SURFACE_UNSUBSCRIBE: u8 = 0x29;
112/// Acknowledge receipt of a surface video frame: [0x2A][surface_id:2]
113pub const C2S_SURFACE_ACK: u8 = 0x2A;
114/// Request a keyframe for a surface: [0x2C][surface_id:2]
115pub const C2S_SURFACE_REQUEST_KEYFRAME: u8 = 0x2C;
116/// Request close of a Wayland surface (sends xdg_toplevel close event):
117/// [0x2B][session_id:2][surface_id:2]
118pub const C2S_SURFACE_CLOSE: u8 = 0x2B;
119/// Client feature/capability advertisement: [0x2D][payload:N]
120/// Currently defined payload bytes:
121///   [0] codec_support — bitmask of CODEC_SUPPORT_* flags the client can
122///       decode.  0 = accept anything (legacy).
123/// Sent once after connection when capability probing completes.  The
124/// message is extensible: the server ignores trailing bytes it doesn't
125/// understand, and missing bytes default to 0.
126pub const C2S_CLIENT_FEATURES: u8 = 0x2D;
127
128pub const S2C_UPDATE: u8 = 0x00;
129pub const S2C_CREATED: u8 = 0x01;
130pub const S2C_CLOSED: u8 = 0x02;
131pub const S2C_LIST: u8 = 0x03;
132pub const S2C_TITLE: u8 = 0x04;
133pub const S2C_SEARCH_RESULTS: u8 = 0x05;
134pub const S2C_CREATED_N: u8 = 0x06;
135pub const S2C_HELLO: u8 = 0x07;
136/// The PTY's subprocess has exited but the terminal state is retained.
137/// Clients can still read/scroll the last frame. Send C2S_CLOSE to dismiss.
138/// Wire: [0x08][pty_id:2][exit_status:4]
139/// exit_status: WEXITSTATUS if normal exit, negative signal number if signalled,
140///              EXIT_STATUS_UNKNOWN if not yet collected.
141pub const S2C_EXITED: u8 = 0x08;
142pub const EXIT_STATUS_UNKNOWN: i32 = i32::MIN;
143/// Sent after the initial burst (HELLO, LIST, TITLE*, EXITED*) is complete.
144/// Clients can use this to know when the initial state has been fully transmitted.
145pub const S2C_READY: u8 = 0x09;
146/// Text response: [0x0A][nonce:2][pty_id:2][total_lines:4][offset:4][text:N]
147/// nonce: echoed from C2S_READ request
148/// total_lines: total available lines (scrollback + viewport rows)
149/// offset: the offset that was requested
150/// text: UTF-8 text, lines separated by \n
151pub const S2C_TEXT: u8 = 0x0A;
152
153/// A new Wayland toplevel surface was created:
154/// [0x20][session_id:2][surface_id:2][parent_id:2][width:2][height:2][title_len:2][title:N][app_id_len:2][app_id:N]
155/// parent_id: 0 = no parent (top-level), non-zero = dialog/child of that surface
156pub const S2C_SURFACE_CREATED: u8 = 0x20;
157/// A Wayland surface was destroyed: [0x21][session_id:2][surface_id:2]
158pub const S2C_SURFACE_DESTROYED: u8 = 0x21;
159/// An encoded video frame for a Wayland surface:
160/// [0x22][session_id:2][surface_id:2][timestamp:4][flags:1][width:2][height:2][data:N]
161/// flags: bit 0 = keyframe, bits 1-2 = codec (0 = H.264, 1 = AV1).
162/// timestamp: milliseconds since compositor session start.
163pub const S2C_SURFACE_FRAME: u8 = 0x22;
164/// A Wayland surface's title changed: [0x23][session_id:2][surface_id:2][title:N]
165pub const S2C_SURFACE_TITLE: u8 = 0x23;
166/// A Wayland surface was resized by the app: [0x24][session_id:2][surface_id:2][width:2][height:2]
167pub const S2C_SURFACE_RESIZED: u8 = 0x24;
168/// A Wayland surface's app_id changed: [0x28][session_id:2][surface_id:2][app_id:N]
169pub const S2C_SURFACE_APP_ID: u8 = 0x28;
170/// Clipboard content from a Wayland surface:
171/// [0x25][session_id:2][surface_id:2][mime_len:2][mime:N][data_len:4][data:N]
172pub const S2C_CLIPBOARD: u8 = 0x25;
173/// List of all compositor surfaces:
174/// [0x26][count:2] repeated{ [surface_id:2][parent_id:2][width:2][height:2][title_len:2][title:N][app_id_len:2][app_id:N] }
175pub const S2C_SURFACE_LIST: u8 = 0x26;
176/// Screenshot of a surface: [0x27][surface_id:2][width:4][height:4][image_data:N]
177/// image_data is PNG or AVIF depending on the request format.
178/// If the surface was not found or has no buffer, width=0 and height=0 with empty data.
179pub const S2C_SURFACE_CAPTURE: u8 = 0x27;
180
181pub const SURFACE_FRAME_FLAG_KEYFRAME: u8 = 1 << 0;
182pub const SURFACE_FRAME_CODEC_MASK: u8 = 0b110;
183pub const SURFACE_FRAME_CODEC_H264: u8 = 0 << 1;
184pub const SURFACE_FRAME_CODEC_AV1: u8 = 1 << 1;
185pub const SURFACE_FRAME_CODEC_PNG: u8 = 2 << 1;
186pub const SURFACE_FRAME_CODEC_H265: u8 = 3 << 1;
187
188/// Bitmask for client-supported codecs in C2S_SURFACE_RESIZE.
189/// 0 means "accept anything" for backward compatibility.
190pub const CODEC_SUPPORT_H264: u8 = 1 << 0;
191pub const CODEC_SUPPORT_AV1: u8 = 1 << 1;
192pub const CODEC_SUPPORT_H265: u8 = 1 << 2;
193
194pub const FEATURE_CREATE_NONCE: u32 = 1 << 0;
195pub const FEATURE_RESTART: u32 = 1 << 1;
196pub const FEATURE_RESIZE_BATCH: u32 = 1 << 2;
197pub const FEATURE_COPY_RANGE: u32 = 1 << 3;
198pub const FEATURE_COMPOSITOR: u32 = 1 << 4;
199
200#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
201pub enum Color {
202    #[default]
203    Default,
204    Indexed(u8),
205    Rgb(u8, u8, u8),
206}
207
208#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
209pub struct CellStyle {
210    pub fg: Color,
211    pub bg: Color,
212    pub bold: bool,
213    pub dim: bool,
214    pub italic: bool,
215    pub underline: bool,
216    pub inverse: bool,
217}
218
219#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
220pub struct Rect {
221    pub row: u16,
222    pub col: u16,
223    pub rows: u16,
224    pub cols: u16,
225}
226
227impl Rect {
228    pub const fn new(row: u16, col: u16, rows: u16, cols: u16) -> Self {
229        Self {
230            row,
231            col,
232            rows,
233            cols,
234        }
235    }
236}
237
238#[derive(Clone, Debug, Default, PartialEq, Eq)]
239pub struct FrameState {
240    rows: u16,
241    cols: u16,
242    cells: Vec<u8>,
243    cursor_row: u16,
244    cursor_col: u16,
245    mode: u16,
246    title: String,
247    /// Overflow strings for cells whose content exceeds 4 bytes.
248    /// Keyed by flat cell index (row * cols + col).
249    overflow: BTreeMap<usize, String>,
250    /// Per-row flags. `ROW_FLAG_WRAPPED` means the row continues on the next.
251    line_flags: Vec<u8>,
252    /// Total scrollback lines available for this PTY.
253    scrollback_lines: u32,
254}
255
256impl FrameState {
257    pub fn new(rows: u16, cols: u16) -> Self {
258        let total = rows as usize * cols as usize;
259        Self {
260            rows,
261            cols,
262            cells: vec![0; total * CELL_SIZE],
263            cursor_row: 0,
264            cursor_col: 0,
265            mode: 0,
266            title: String::new(),
267            overflow: BTreeMap::new(),
268            line_flags: vec![0; rows as usize],
269            scrollback_lines: 0,
270        }
271    }
272
273    pub fn from_parts(
274        rows: u16,
275        cols: u16,
276        cursor_row: u16,
277        cursor_col: u16,
278        mode: u16,
279        title: impl Into<String>,
280        cells: Vec<u8>,
281    ) -> Self {
282        let mut state = Self::new(rows, cols);
283        if cells.len() == state.cells.len() {
284            state.cells = cells;
285        }
286        state.cursor_row = cursor_row;
287        state.cursor_col = cursor_col;
288        state.mode = mode;
289        state.title = title.into();
290        state
291    }
292
293    pub fn rows(&self) -> u16 {
294        self.rows
295    }
296
297    pub fn cols(&self) -> u16 {
298        self.cols
299    }
300
301    pub fn cursor_row(&self) -> u16 {
302        self.cursor_row
303    }
304
305    pub fn cursor_col(&self) -> u16 {
306        self.cursor_col
307    }
308
309    pub fn mode(&self) -> u16 {
310        self.mode
311    }
312
313    pub fn title(&self) -> &str {
314        &self.title
315    }
316
317    pub fn cells(&self) -> &[u8] {
318        &self.cells
319    }
320
321    pub fn cells_mut(&mut self) -> &mut [u8] {
322        &mut self.cells
323    }
324
325    pub fn overflow(&self) -> &BTreeMap<usize, String> {
326        &self.overflow
327    }
328
329    pub fn overflow_mut(&mut self) -> &mut BTreeMap<usize, String> {
330        &mut self.overflow
331    }
332
333    pub fn line_flags(&self) -> &[u8] {
334        &self.line_flags
335    }
336
337    pub fn line_flags_mut(&mut self) -> &mut Vec<u8> {
338        &mut self.line_flags
339    }
340
341    pub fn scrollback_lines(&self) -> u32 {
342        self.scrollback_lines
343    }
344
345    pub fn set_scrollback_lines(&mut self, lines: u32) {
346        self.scrollback_lines = lines;
347    }
348
349    pub fn is_wrapped(&self, row: u16) -> bool {
350        self.line_flags.get(row as usize).copied().unwrap_or(0) & ROW_FLAG_WRAPPED != 0
351    }
352
353    pub fn set_wrapped(&mut self, row: u16, wrapped: bool) {
354        if let Some(flags) = self.line_flags.get_mut(row as usize) {
355            if wrapped {
356                *flags |= ROW_FLAG_WRAPPED;
357            } else {
358                *flags &= !ROW_FLAG_WRAPPED;
359            }
360        }
361    }
362
363    /// Returns the text content of a cell, resolving overflow if needed.
364    pub fn cell_content(&self, row: u16, col: u16) -> &str {
365        if row >= self.rows || col >= self.cols {
366            return "";
367        }
368        let flat = row as usize * self.cols as usize + col as usize;
369        let idx = flat * CELL_SIZE;
370        let f1 = self.cells[idx + 1];
371        if f1 & 4 != 0 {
372            return ""; // wide continuation
373        }
374        let content_len = ((f1 >> 3) & 7) as usize;
375        if content_len == CONTENT_OVERFLOW as usize {
376            if let Some(s) = self.overflow.get(&flat) {
377                return s.as_str();
378            }
379            return "";
380        }
381        if content_len == 0 {
382            return " ";
383        }
384        std::str::from_utf8(&self.cells[idx + 8..idx + 8 + content_len]).unwrap_or(" ")
385    }
386
387    pub fn resize(&mut self, rows: u16, cols: u16) {
388        if rows == self.rows && cols == self.cols {
389            return;
390        }
391        self.rows = rows;
392        self.cols = cols;
393        self.cells = vec![0; rows as usize * cols as usize * CELL_SIZE];
394        self.overflow.clear();
395        self.line_flags = vec![0; rows as usize];
396        self.cursor_row = self.cursor_row.min(rows.saturating_sub(1));
397        self.cursor_col = self.cursor_col.min(cols.saturating_sub(1));
398    }
399
400    pub fn set_cursor(&mut self, row: u16, col: u16) {
401        self.cursor_row = row.min(self.rows.saturating_sub(1));
402        self.cursor_col = col.min(self.cols.saturating_sub(1));
403    }
404
405    pub fn set_mode(&mut self, mode: u16) {
406        self.mode = mode;
407    }
408
409    pub fn set_title(&mut self, title: impl Into<String>) -> bool {
410        let title = title.into();
411        if self.title == title {
412            return false;
413        }
414        self.title = title;
415        true
416    }
417
418    pub fn clear(&mut self, style: CellStyle) {
419        for row in 0..self.rows {
420            for col in 0..self.cols {
421                self.set_blank_cell(row, col, style);
422            }
423        }
424    }
425
426    pub fn fill_rect(&mut self, rect: Rect, ch: char, style: CellStyle) {
427        let row_end = rect.row.saturating_add(rect.rows).min(self.rows);
428        let col_end = rect.col.saturating_add(rect.cols).min(self.cols);
429        for row in rect.row..row_end {
430            let mut col = rect.col;
431            while col < col_end {
432                let width = self.set_cell(row, col, ch, style);
433                if width == 0 {
434                    break;
435                }
436                col = col.saturating_add(width);
437            }
438        }
439    }
440
441    pub fn write_text(&mut self, row: u16, col: u16, text: &str, style: CellStyle) -> u16 {
442        if row >= self.rows || col >= self.cols {
443            return col;
444        }
445        let mut cur_col = col;
446        for ch in text.chars() {
447            if cur_col >= self.cols {
448                break;
449            }
450            let width = self.set_cell(row, cur_col, ch, style);
451            if width == 0 {
452                continue;
453            }
454            cur_col = cur_col.saturating_add(width);
455        }
456        cur_col
457    }
458
459    pub fn write_wrapped_text(&mut self, rect: Rect, text: &str, style: CellStyle) -> usize {
460        if rect.rows == 0 || rect.cols == 0 {
461            return 0;
462        }
463        let lines = wrap_text_lines(text, rect.cols as usize);
464        let max_rows = rect.rows.min(self.rows.saturating_sub(rect.row));
465        for (idx, line) in lines.iter().take(max_rows as usize).enumerate() {
466            let row = rect.row + idx as u16;
467            self.write_text(row, rect.col, line, style);
468        }
469        lines.len()
470    }
471
472    pub fn write_scrolling_text<S: AsRef<str>>(
473        &mut self,
474        rect: Rect,
475        lines: &[S],
476        offset_from_bottom: usize,
477        style: CellStyle,
478    ) {
479        if rect.rows == 0 || rect.cols == 0 {
480            return;
481        }
482        let mut wrapped = Vec::with_capacity(lines.len());
483        for line in lines {
484            let line = line.as_ref();
485            let out = wrap_text_lines(line, rect.cols as usize);
486            if out.is_empty() {
487                wrapped.push(String::new());
488            } else {
489                wrapped.extend(out);
490            }
491        }
492        let visible = rect.rows as usize;
493        let end = wrapped.len().saturating_sub(offset_from_bottom);
494        let start = end.saturating_sub(visible);
495        for row in 0..rect.rows {
496            self.fill_rect(
497                Rect::new(rect.row + row, rect.col, 1, rect.cols),
498                ' ',
499                style,
500            );
501        }
502        for (idx, line) in wrapped[start..end].iter().enumerate() {
503            self.write_text(rect.row + idx as u16, rect.col, line, style);
504        }
505    }
506
507    pub fn get_text(&self, start_row: u16, start_col: u16, end_row: u16, end_col: u16) -> String {
508        let mut result = String::new();
509        if self.rows == 0 || self.cols == 0 {
510            return result;
511        }
512        for row in start_row..=end_row.min(self.rows.saturating_sub(1)) {
513            let c0 = if row == start_row { start_col } else { 0 };
514            let c1 = if row == end_row {
515                end_col
516            } else {
517                self.cols - 1
518            };
519            let mut line = String::new();
520            let mut col = c0;
521            while col <= c1.min(self.cols - 1) {
522                line.push_str(self.cell_content(row, col));
523                col += 1;
524            }
525            result.push_str(line.trim_end());
526            if row < end_row.min(self.rows.saturating_sub(1)) && !self.is_wrapped(row) {
527                result.push('\n');
528            }
529        }
530        result
531    }
532
533    pub fn get_all_text(&self) -> String {
534        if self.rows == 0 || self.cols == 0 {
535            return String::new();
536        }
537        self.get_text(0, 0, self.rows - 1, self.cols - 1)
538    }
539
540    fn cell_style(&self, row: u16, col: u16) -> CellStyle {
541        if row >= self.rows || col >= self.cols {
542            return CellStyle::default();
543        }
544        let idx = self.cell_offset(row, col);
545        let f0 = self.cells[idx];
546        let f1 = self.cells[idx + 1];
547        let fg_type = f0 & 3;
548        let bg_type = (f0 >> 2) & 3;
549        let fg = match fg_type {
550            1 => Color::Indexed(self.cells[idx + 2]),
551            2 => Color::Rgb(
552                self.cells[idx + 2],
553                self.cells[idx + 3],
554                self.cells[idx + 4],
555            ),
556            _ => Color::Default,
557        };
558        let bg = match bg_type {
559            1 => Color::Indexed(self.cells[idx + 5]),
560            2 => Color::Rgb(
561                self.cells[idx + 5],
562                self.cells[idx + 6],
563                self.cells[idx + 7],
564            ),
565            _ => Color::Default,
566        };
567        CellStyle {
568            fg,
569            bg,
570            bold: (f0 >> 4) & 1 != 0,
571            dim: (f0 >> 5) & 1 != 0,
572            italic: (f0 >> 6) & 1 != 0,
573            underline: (f0 >> 7) & 1 != 0,
574            inverse: f1 & 1 != 0,
575        }
576    }
577
578    pub fn get_ansi_text(&self) -> String {
579        if self.rows == 0 || self.cols == 0 {
580            return String::new();
581        }
582        let mut result = String::new();
583        let mut cur_style = CellStyle::default();
584        for row in 0..self.rows {
585            let mut line = String::new();
586            let mut col = 0u16;
587            while col < self.cols {
588                let style = self.cell_style(row, col);
589                if style != cur_style {
590                    push_sgr(&mut line, &style);
591                    cur_style = style;
592                }
593                line.push_str(self.cell_content(row, col));
594                col += 1;
595            }
596            let trimmed = line.trim_end();
597            result.push_str(trimmed);
598            if cur_style != CellStyle::default() {
599                result.push_str("\x1b[0m");
600                cur_style = CellStyle::default();
601            }
602            if row < self.rows - 1 {
603                result.push('\n');
604            }
605        }
606        result
607    }
608
609    pub fn get_cell(&self, row: u16, col: u16) -> Vec<u8> {
610        if row >= self.rows || col >= self.cols {
611            return Vec::new();
612        }
613        let idx = self.cell_offset(row, col);
614        self.cells[idx..idx + CELL_SIZE].to_vec()
615    }
616
617    fn cell_offset(&self, row: u16, col: u16) -> usize {
618        (row as usize * self.cols as usize + col as usize) * CELL_SIZE
619    }
620
621    fn set_cell(&mut self, row: u16, col: u16, ch: char, style: CellStyle) -> u16 {
622        if row >= self.rows || col >= self.cols {
623            return 0;
624        }
625        let raw_width = UnicodeWidthChar::width(ch).unwrap_or(0);
626        if raw_width == 0 {
627            return 0;
628        }
629        let width = if raw_width > 1 && col + 1 < self.cols {
630            2
631        } else {
632            1
633        };
634        let idx = self.cell_offset(row, col);
635        encode_cell(
636            &mut self.cells[idx..idx + CELL_SIZE],
637            Some(ch),
638            style,
639            width == 2,
640            false,
641        );
642        if width == 2 {
643            let cont_idx = self.cell_offset(row, col + 1);
644            encode_cell(
645                &mut self.cells[cont_idx..cont_idx + CELL_SIZE],
646                None,
647                style,
648                false,
649                true,
650            );
651        }
652        width
653    }
654
655    fn set_blank_cell(&mut self, row: u16, col: u16, style: CellStyle) {
656        if row >= self.rows || col >= self.cols {
657            return;
658        }
659        let idx = self.cell_offset(row, col);
660        encode_cell(
661            &mut self.cells[idx..idx + CELL_SIZE],
662            None,
663            style,
664            false,
665            false,
666        );
667    }
668}
669
670#[derive(Clone, Debug)]
671pub struct TerminalState {
672    frame: FrameState,
673}
674
675impl TerminalState {
676    pub fn new(rows: u16, cols: u16) -> Self {
677        let frame = FrameState::new(rows, cols);
678        Self { frame }
679    }
680
681    pub fn frame(&self) -> &FrameState {
682        &self.frame
683    }
684
685    pub fn frame_mut(&mut self) -> &mut FrameState {
686        &mut self.frame
687    }
688
689    pub fn title(&self) -> &str {
690        self.frame.title()
691    }
692
693    pub fn rows(&self) -> u16 {
694        self.frame.rows()
695    }
696
697    pub fn cols(&self) -> u16 {
698        self.frame.cols()
699    }
700
701    pub fn is_wrapped(&self, row: u16) -> bool {
702        self.frame.is_wrapped(row)
703    }
704
705    pub fn cursor_row(&self) -> u16 {
706        self.frame.cursor_row()
707    }
708
709    pub fn cursor_col(&self) -> u16 {
710        self.frame.cursor_col()
711    }
712
713    pub fn mode(&self) -> u16 {
714        self.frame.mode()
715    }
716
717    pub fn cells(&self) -> &[u8] {
718        self.frame.cells()
719    }
720
721    pub fn set_title(&mut self, title: &str) -> bool {
722        self.frame.set_title(title.to_owned())
723    }
724
725    pub fn get_text(&self, start_row: u16, start_col: u16, end_row: u16, end_col: u16) -> String {
726        self.frame.get_text(start_row, start_col, end_row, end_col)
727    }
728
729    pub fn get_all_text(&self) -> String {
730        self.frame.get_all_text()
731    }
732
733    pub fn get_ansi_text(&self) -> String {
734        self.frame.get_ansi_text()
735    }
736
737    pub fn get_cell(&self, row: u16, col: u16) -> Vec<u8> {
738        self.frame.get_cell(row, col)
739    }
740
741    /// Maximum decompressed frame size (50 MiB). Prevents LZ4 decompression
742    /// bombs where a tiny compressed payload claims a multi-GiB output size.
743    const MAX_DECOMPRESSED_SIZE: usize = 50 * 1024 * 1024;
744
745    /// Read the LZ4 prepended uncompressed size without allocating, and reject
746    /// payloads that claim to decompress beyond `MAX_DECOMPRESSED_SIZE`.
747    fn safe_decompress(data: &[u8]) -> Result<Vec<u8>, ()> {
748        if data.len() < 4 {
749            return Err(());
750        }
751        let claimed = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
752        if claimed > Self::MAX_DECOMPRESSED_SIZE {
753            return Err(());
754        }
755        decompress_size_prepended(data).map_err(|_| ())
756    }
757
758    pub fn feed_compressed(&mut self, data: &[u8]) -> bool {
759        let payload = match Self::safe_decompress(data) {
760            Ok(d) => d,
761            Err(_) => return false,
762        };
763        self.apply_payload(&payload)
764    }
765
766    pub fn feed_compressed_batch(&mut self, batch: &[u8]) -> bool {
767        let mut changed = false;
768        let mut off = 0usize;
769        while off + 4 <= batch.len() {
770            let len =
771                u32::from_le_bytes([batch[off], batch[off + 1], batch[off + 2], batch[off + 3]])
772                    as usize;
773            off += 4;
774            if len == 0 {
775                break;
776            }
777            if off + len > batch.len() {
778                break;
779            }
780            if let Ok(payload) = Self::safe_decompress(&batch[off..off + len]) {
781                changed |= self.apply_payload(&payload);
782            }
783            off += len;
784        }
785        changed
786    }
787
788    /// Maximum total cell count allowed in a single frame (rows * cols).
789    /// 500 rows x 1000 cols = 500,000 cells x 12 bytes = 6 MB — generous for
790    /// any real terminal while preventing 48 GiB allocations from malicious
791    /// frames claiming rows=65535, cols=65535.
792    const MAX_CELL_COUNT: usize = 500_000;
793
794    fn apply_payload(&mut self, payload: &[u8]) -> bool {
795        if payload.len() < 12 {
796            return false;
797        }
798
799        let new_rows = u16::from_le_bytes([payload[0], payload[1]]);
800        let new_cols = u16::from_le_bytes([payload[2], payload[3]]);
801
802        // Reject absurd dimensions that would cause multi-GiB allocations.
803        if (new_rows as usize) * (new_cols as usize) > Self::MAX_CELL_COUNT {
804            return false;
805        }
806        let new_cursor_row = u16::from_le_bytes([payload[4], payload[5]]);
807        let new_cursor_col = u16::from_le_bytes([payload[6], payload[7]]);
808        let new_mode = u16::from_le_bytes([payload[8], payload[9]]);
809        let title_field = u16::from_le_bytes([payload[10], payload[11]]);
810        let title_present = title_field & TITLE_PRESENT != 0;
811        let ops_present = title_field & OPS_PRESENT != 0;
812        let strings_present = title_field & STRINGS_PRESENT != 0;
813        let line_flags_present = title_field & LINE_FLAGS_PRESENT != 0;
814        let title_len = (title_field & TITLE_LEN_MASK) as usize;
815
816        let title_start = 12usize;
817        let title_end = title_start.saturating_add(title_len);
818        if payload.len() < title_end {
819            return false;
820        }
821        let title_changed = if title_present {
822            let title = String::from_utf8_lossy(&payload[title_start..title_end]).into_owned();
823            self.frame.set_title(title)
824        } else {
825            false
826        };
827
828        let resized = new_rows != self.frame.rows || new_cols != self.frame.cols;
829        if resized {
830            self.frame.resize(new_rows, new_cols);
831        }
832
833        let old_cursor_row = self.frame.cursor_row;
834        let old_cursor_col = self.frame.cursor_col;
835        let old_mode = self.frame.mode;
836
837        let (content_changed, ops_end) = if ops_present {
838            let ops_start = title_end;
839            if payload.len() < ops_start + 2 {
840                return false;
841            }
842            let (changed, consumed) = self
843                .apply_ops_payload(&payload[ops_start..])
844                .unwrap_or((false, 0));
845            (changed, ops_start + consumed)
846        } else {
847            let (changed, consumed) = self
848                .apply_legacy_patch_payload(&payload[title_end..])
849                .unwrap_or((false, 0));
850            (changed, title_end + consumed)
851        };
852
853        let mut after_strings = ops_end;
854        if strings_present {
855            after_strings = self.apply_overflow_strings(&payload[ops_end..]);
856            after_strings += ops_end;
857        }
858
859        let (line_flags_changed, after_line_flags) = if line_flags_present {
860            let lf_start = after_strings;
861            let lf_end = lf_start + new_rows as usize;
862            if payload.len() >= lf_end {
863                let new_flags = &payload[lf_start..lf_end];
864                let changed = self.frame.line_flags != new_flags;
865                self.frame.line_flags.clear();
866                self.frame.line_flags.extend_from_slice(new_flags);
867                (changed, lf_end)
868            } else {
869                (false, after_strings)
870            }
871        } else {
872            (false, after_strings)
873        };
874
875        // Trailing scrollback count (backward-compatible extension).
876        if payload.len() >= after_line_flags + 4 {
877            self.frame.scrollback_lines = u32::from_le_bytes([
878                payload[after_line_flags],
879                payload[after_line_flags + 1],
880                payload[after_line_flags + 2],
881                payload[after_line_flags + 3],
882            ]);
883        }
884
885        self.frame.cursor_row = new_cursor_row.min(self.frame.rows.saturating_sub(1));
886        self.frame.cursor_col = new_cursor_col.min(self.frame.cols.saturating_sub(1));
887        self.frame.mode = new_mode;
888        resized
889            || title_changed
890            || content_changed
891            || line_flags_changed
892            || new_cursor_row != old_cursor_row
893            || new_cursor_col != old_cursor_col
894            || new_mode != old_mode
895    }
896
897    fn apply_legacy_patch_payload(&mut self, payload: &[u8]) -> Option<(bool, usize)> {
898        let total_cells = self.frame.rows as usize * self.frame.cols as usize;
899        let bitmask_len = total_cells.div_ceil(8);
900        if payload.len() < bitmask_len {
901            return None;
902        }
903        let bitmask = &payload[..bitmask_len];
904        let dirty_count = (0..total_cells)
905            .filter(|&i| bitmask[i / 8] & (1 << (i % 8)) != 0)
906            .count();
907        let data = &payload[bitmask_len..];
908        if data.len() < dirty_count * CELL_SIZE {
909            return None;
910        }
911        self.apply_patch_cells(bitmask, &data[..dirty_count * CELL_SIZE], dirty_count);
912        Some((dirty_count > 0, bitmask_len + dirty_count * CELL_SIZE))
913    }
914
915    fn apply_ops_payload(&mut self, payload: &[u8]) -> Option<(bool, usize)> {
916        if payload.len() < 2 {
917            return None;
918        }
919        let op_count = u16::from_le_bytes([payload[0], payload[1]]) as usize;
920        let total_cells = self.frame.rows as usize * self.frame.cols as usize;
921        let bitmask_len = total_cells.div_ceil(8);
922        let mut off = 2usize;
923        let mut changed = false;
924
925        for _ in 0..op_count {
926            if off >= payload.len() {
927                return None;
928            }
929            let op = payload[off];
930            off += 1;
931            match op {
932                OP_COPY_RECT => {
933                    if payload.len() < off + 12 {
934                        return None;
935                    }
936                    let src_row = u16::from_le_bytes([payload[off], payload[off + 1]]);
937                    let src_col = u16::from_le_bytes([payload[off + 2], payload[off + 3]]);
938                    let dst_row = u16::from_le_bytes([payload[off + 4], payload[off + 5]]);
939                    let dst_col = u16::from_le_bytes([payload[off + 6], payload[off + 7]]);
940                    let rows = u16::from_le_bytes([payload[off + 8], payload[off + 9]]);
941                    let cols = u16::from_le_bytes([payload[off + 10], payload[off + 11]]);
942                    off += 12;
943                    changed |= self.apply_copy_rect(src_row, src_col, dst_row, dst_col, rows, cols);
944                }
945                OP_FILL_RECT => {
946                    if payload.len() < off + 8 + CELL_SIZE {
947                        return None;
948                    }
949                    let row = u16::from_le_bytes([payload[off], payload[off + 1]]);
950                    let col = u16::from_le_bytes([payload[off + 2], payload[off + 3]]);
951                    let rows = u16::from_le_bytes([payload[off + 4], payload[off + 5]]);
952                    let cols = u16::from_le_bytes([payload[off + 6], payload[off + 7]]);
953                    off += 8;
954                    let mut cell = [0u8; CELL_SIZE];
955                    cell.copy_from_slice(&payload[off..off + CELL_SIZE]);
956                    off += CELL_SIZE;
957                    changed |= self.apply_fill_rect(row, col, rows, cols, &cell);
958                }
959                OP_PATCH_CELLS => {
960                    if payload.len() < off + bitmask_len {
961                        return None;
962                    }
963                    let bitmask = &payload[off..off + bitmask_len];
964                    off += bitmask_len;
965                    let dirty_count = (0..total_cells)
966                        .filter(|&i| bitmask[i / 8] & (1 << (i % 8)) != 0)
967                        .count();
968                    if payload.len() < off + dirty_count * CELL_SIZE {
969                        return None;
970                    }
971                    self.apply_patch_cells(
972                        bitmask,
973                        &payload[off..off + dirty_count * CELL_SIZE],
974                        dirty_count,
975                    );
976                    off += dirty_count * CELL_SIZE;
977                    changed |= dirty_count > 0;
978                }
979                _ => return None,
980            }
981        }
982
983        Some((changed, off))
984    }
985
986    fn apply_patch_cells(&mut self, bitmask: &[u8], data: &[u8], dirty_count: usize) {
987        let total_cells = self.frame.rows as usize * self.frame.cols as usize;
988        let mut dirty_idx = 0usize;
989        for i in 0..total_cells {
990            if bitmask[i / 8] & (1 << (i % 8)) == 0 {
991                continue;
992            }
993            let cell_idx = i * CELL_SIZE;
994            for byte_pos in 0..CELL_SIZE {
995                self.frame.cells[cell_idx + byte_pos] = data[byte_pos * dirty_count + dirty_idx];
996            }
997            // Remove stale overflow entry when a cell is updated — it may
998            // have transitioned from overflow (content_len=7) to inline.
999            let new_content_len = (self.frame.cells[cell_idx + 1] >> 3) & 7;
1000            if new_content_len != CONTENT_OVERFLOW {
1001                self.frame.overflow.remove(&i);
1002            }
1003            dirty_idx += 1;
1004        }
1005    }
1006
1007    fn apply_copy_rect(
1008        &mut self,
1009        src_row: u16,
1010        src_col: u16,
1011        dst_row: u16,
1012        dst_col: u16,
1013        rows: u16,
1014        cols: u16,
1015    ) -> bool {
1016        let rows = rows
1017            .min(self.frame.rows.saturating_sub(src_row))
1018            .min(self.frame.rows.saturating_sub(dst_row));
1019        let cols = cols
1020            .min(self.frame.cols.saturating_sub(src_col))
1021            .min(self.frame.cols.saturating_sub(dst_col));
1022        if rows == 0 || cols == 0 {
1023            return false;
1024        }
1025
1026        let frame_cols = self.frame.cols as usize;
1027
1028        // Copy overflow strings for the source region.
1029        let mut overflow_temp: Vec<(usize, String)> = Vec::new();
1030        for r in 0..rows as usize {
1031            for c in 0..cols as usize {
1032                let src_flat = (src_row as usize + r) * frame_cols + src_col as usize + c;
1033                if let Some(s) = self.frame.overflow.get(&src_flat) {
1034                    let dst_flat = (dst_row as usize + r) * frame_cols + dst_col as usize + c;
1035                    overflow_temp.push((dst_flat, s.clone()));
1036                }
1037            }
1038        }
1039
1040        let mut temp = vec![0u8; rows as usize * cols as usize * CELL_SIZE];
1041        for r in 0..rows as usize {
1042            let src_off = self.frame.cell_offset(src_row + r as u16, src_col);
1043            let src_end = src_off + cols as usize * CELL_SIZE;
1044            let dst_off = r * cols as usize * CELL_SIZE;
1045            temp[dst_off..dst_off + cols as usize * CELL_SIZE]
1046                .copy_from_slice(&self.frame.cells[src_off..src_end]);
1047        }
1048        for r in 0..rows as usize {
1049            let dst_off = self.frame.cell_offset(dst_row + r as u16, dst_col);
1050            let dst_end = dst_off + cols as usize * CELL_SIZE;
1051            let src_off = r * cols as usize * CELL_SIZE;
1052            self.frame.cells[dst_off..dst_end]
1053                .copy_from_slice(&temp[src_off..src_off + cols as usize * CELL_SIZE]);
1054        }
1055
1056        for r in 0..rows as usize {
1057            for c in 0..cols as usize {
1058                let dst_flat = (dst_row as usize + r) * frame_cols + dst_col as usize + c;
1059                self.frame.overflow.remove(&dst_flat);
1060            }
1061        }
1062        for (idx, s) in overflow_temp {
1063            self.frame.overflow.insert(idx, s);
1064        }
1065
1066        true
1067    }
1068
1069    fn apply_fill_rect(
1070        &mut self,
1071        row: u16,
1072        col: u16,
1073        rows: u16,
1074        cols: u16,
1075        cell: &[u8; CELL_SIZE],
1076    ) -> bool {
1077        let row_end = row.saturating_add(rows).min(self.frame.rows);
1078        let col_end = col.saturating_add(cols).min(self.frame.cols);
1079        // Fill cells never have overflow content — clear stale entries.
1080        let frame_cols = self.frame.cols as usize;
1081        for r in row..row_end {
1082            for c in col..col_end {
1083                self.frame
1084                    .overflow
1085                    .remove(&(r as usize * frame_cols + c as usize));
1086            }
1087        }
1088        if row >= row_end || col >= col_end {
1089            return false;
1090        }
1091        for r in row..row_end {
1092            for c in col..col_end {
1093                let off = self.frame.cell_offset(r, c);
1094                self.frame.cells[off..off + CELL_SIZE].copy_from_slice(cell);
1095            }
1096        }
1097        true
1098    }
1099
1100    fn apply_overflow_strings(&mut self, data: &[u8]) -> usize {
1101        if data.len() < 2 {
1102            return 0;
1103        }
1104        let count = u16::from_le_bytes([data[0], data[1]]) as usize;
1105        let mut off = 2usize;
1106        for _ in 0..count {
1107            if off + 6 > data.len() {
1108                break;
1109            }
1110            let cell_idx =
1111                u32::from_le_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]])
1112                    as usize;
1113            let len = u16::from_le_bytes([data[off + 4], data[off + 5]]) as usize;
1114            off += 6;
1115            if off + len > data.len() {
1116                break;
1117            }
1118            if let Ok(s) = std::str::from_utf8(&data[off..off + len]) {
1119                // Only accept indices within the current grid to prevent
1120                // unbounded BTreeMap growth from malicious wire data.
1121                let max_idx = self.frame.rows as usize * self.frame.cols as usize;
1122                if cell_idx < max_idx {
1123                    self.frame.overflow.insert(cell_idx, s.to_owned());
1124                }
1125            }
1126            off += len;
1127        }
1128        off
1129    }
1130}
1131
1132#[derive(Clone, Debug)]
1133pub enum Node {
1134    Fill {
1135        rect: Rect,
1136        ch: char,
1137        style: CellStyle,
1138    },
1139    Text {
1140        row: u16,
1141        col: u16,
1142        text: String,
1143        style: CellStyle,
1144    },
1145    WrappedText {
1146        rect: Rect,
1147        text: String,
1148        style: CellStyle,
1149    },
1150    ScrollingText {
1151        rect: Rect,
1152        lines: Vec<String>,
1153        offset_from_bottom: usize,
1154        style: CellStyle,
1155    },
1156}
1157
1158#[derive(Clone, Debug, Default)]
1159pub struct Dom {
1160    background: CellStyle,
1161    title: Option<String>,
1162    nodes: Vec<Node>,
1163}
1164
1165impl Dom {
1166    pub fn new() -> Self {
1167        Self::default()
1168    }
1169
1170    pub fn clear(&mut self) {
1171        self.title = None;
1172        self.nodes.clear();
1173    }
1174
1175    pub fn set_background(&mut self, style: CellStyle) {
1176        self.background = style;
1177    }
1178
1179    pub fn set_title(&mut self, title: impl Into<String>) {
1180        self.title = Some(title.into());
1181    }
1182
1183    pub fn fill(&mut self, rect: Rect, ch: char, style: CellStyle) {
1184        self.nodes.push(Node::Fill { rect, ch, style });
1185    }
1186
1187    pub fn text(&mut self, row: u16, col: u16, text: impl Into<String>, style: CellStyle) {
1188        self.nodes.push(Node::Text {
1189            row,
1190            col,
1191            text: text.into(),
1192            style,
1193        });
1194    }
1195
1196    pub fn wrapped_text(&mut self, rect: Rect, text: impl Into<String>, style: CellStyle) {
1197        self.nodes.push(Node::WrappedText {
1198            rect,
1199            text: text.into(),
1200            style,
1201        });
1202    }
1203
1204    pub fn scrolling_text<S, I>(
1205        &mut self,
1206        rect: Rect,
1207        lines: I,
1208        offset_from_bottom: usize,
1209        style: CellStyle,
1210    ) where
1211        S: Into<String>,
1212        I: IntoIterator<Item = S>,
1213    {
1214        self.nodes.push(Node::ScrollingText {
1215            rect,
1216            lines: lines.into_iter().map(Into::into).collect(),
1217            offset_from_bottom,
1218            style,
1219        });
1220    }
1221
1222    pub fn render_to(&self, frame: &mut FrameState) {
1223        frame.clear(self.background);
1224        frame.set_title(self.title.clone().unwrap_or_default());
1225        for node in &self.nodes {
1226            match node {
1227                Node::Fill { rect, ch, style } => frame.fill_rect(*rect, *ch, *style),
1228                Node::Text {
1229                    row,
1230                    col,
1231                    text,
1232                    style,
1233                } => {
1234                    frame.write_text(*row, *col, text, *style);
1235                }
1236                Node::WrappedText { rect, text, style } => {
1237                    frame.write_wrapped_text(*rect, text, *style);
1238                }
1239                Node::ScrollingText {
1240                    rect,
1241                    lines,
1242                    offset_from_bottom,
1243                    style,
1244                } => {
1245                    frame.write_scrolling_text(*rect, lines, *offset_from_bottom, *style);
1246                }
1247            }
1248        }
1249    }
1250}
1251
1252#[derive(Clone, Debug)]
1253pub struct CallbackRenderer {
1254    dom: Dom,
1255    frame: FrameState,
1256}
1257
1258impl CallbackRenderer {
1259    pub fn new(rows: u16, cols: u16) -> Self {
1260        Self {
1261            dom: Dom::new(),
1262            frame: FrameState::new(rows, cols),
1263        }
1264    }
1265
1266    pub fn resize(&mut self, rows: u16, cols: u16) {
1267        self.frame.resize(rows, cols);
1268    }
1269
1270    pub fn frame(&self) -> &FrameState {
1271        &self.frame
1272    }
1273
1274    pub fn render<F>(&mut self, render: F) -> &FrameState
1275    where
1276        F: FnOnce(&mut Dom),
1277    {
1278        self.dom.clear();
1279        render(&mut self.dom);
1280        self.dom.render_to(&mut self.frame);
1281        &self.frame
1282    }
1283}
1284
1285pub enum ServerMsg<'a> {
1286    Hello {
1287        version: u16,
1288        features: u32,
1289    },
1290    Update {
1291        pty_id: u16,
1292        payload: &'a [u8],
1293    },
1294    Created {
1295        pty_id: u16,
1296        tag: &'a str,
1297    },
1298    CreatedN {
1299        nonce: u16,
1300        pty_id: u16,
1301        tag: &'a str,
1302    },
1303    Closed {
1304        pty_id: u16,
1305    },
1306    Exited {
1307        pty_id: u16,
1308        exit_status: i32,
1309    },
1310    List {
1311        entries: Vec<PtyListEntry<'a>>,
1312    },
1313    Title {
1314        pty_id: u16,
1315        title: &'a [u8],
1316    },
1317    SearchResults {
1318        request_id: u16,
1319        results: Vec<SearchResultEntry<'a>>,
1320    },
1321    Ready,
1322    Text {
1323        nonce: u16,
1324        pty_id: u16,
1325        total_lines: u32,
1326        offset: u32,
1327        text: &'a str,
1328    },
1329    SurfaceCreated {
1330        session_id: u16,
1331        surface_id: u16,
1332        parent_id: u16,
1333        width: u16,
1334        height: u16,
1335        title: &'a str,
1336        app_id: &'a str,
1337    },
1338    SurfaceDestroyed {
1339        session_id: u16,
1340        surface_id: u16,
1341    },
1342    SurfaceFrame {
1343        session_id: u16,
1344        surface_id: u16,
1345        timestamp: u32,
1346        flags: u8,
1347        width: u16,
1348        height: u16,
1349        data: &'a [u8],
1350    },
1351    SurfaceTitle {
1352        session_id: u16,
1353        surface_id: u16,
1354        title: &'a str,
1355    },
1356    SurfaceAppId {
1357        session_id: u16,
1358        surface_id: u16,
1359        app_id: &'a str,
1360    },
1361    SurfaceResized {
1362        session_id: u16,
1363        surface_id: u16,
1364        width: u16,
1365        height: u16,
1366    },
1367    Clipboard {
1368        session_id: u16,
1369        surface_id: u16,
1370        mime_type: &'a str,
1371        data: &'a [u8],
1372    },
1373    SurfaceList {
1374        entries: Vec<SurfaceListEntry>,
1375    },
1376    SurfaceCapture {
1377        surface_id: u16,
1378        width: u32,
1379        height: u32,
1380        image_data: &'a [u8],
1381    },
1382}
1383
1384#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1385pub struct PtyListEntry<'a> {
1386    pub pty_id: u16,
1387    pub tag: &'a str,
1388    pub command: &'a str,
1389}
1390
1391#[derive(Clone, Debug, PartialEq, Eq)]
1392pub struct SurfaceListEntry {
1393    pub surface_id: u16,
1394    pub parent_id: u16,
1395    pub width: u16,
1396    pub height: u16,
1397    pub title: String,
1398    pub app_id: String,
1399}
1400
1401#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1402pub struct SearchResultEntry<'a> {
1403    pub pty_id: u16,
1404    pub score: u32,
1405    pub primary_source: u8,
1406    pub matched_sources: u8,
1407    pub scroll_offset: Option<u32>,
1408    pub context: &'a [u8],
1409}
1410
1411pub fn parse_server_msg(data: &[u8]) -> Option<ServerMsg<'_>> {
1412    if data.is_empty() {
1413        return None;
1414    }
1415    match data[0] {
1416        S2C_HELLO => {
1417            if data.len() < 7 {
1418                return None;
1419            }
1420            let version = u16::from_le_bytes([data[1], data[2]]);
1421            let features = u32::from_le_bytes([data[3], data[4], data[5], data[6]]);
1422            Some(ServerMsg::Hello { version, features })
1423        }
1424        S2C_UPDATE => {
1425            if data.len() < 3 {
1426                return None;
1427            }
1428            Some(ServerMsg::Update {
1429                pty_id: u16::from_le_bytes([data[1], data[2]]),
1430                payload: &data[3..],
1431            })
1432        }
1433        S2C_CREATED => {
1434            if data.len() < 3 {
1435                return None;
1436            }
1437            let tag = std::str::from_utf8(data.get(3..).unwrap_or_default()).unwrap_or_default();
1438            Some(ServerMsg::Created {
1439                pty_id: u16::from_le_bytes([data[1], data[2]]),
1440                tag,
1441            })
1442        }
1443        S2C_CREATED_N => {
1444            if data.len() < 5 {
1445                return None;
1446            }
1447            let nonce = u16::from_le_bytes([data[1], data[2]]);
1448            let pty_id = u16::from_le_bytes([data[3], data[4]]);
1449            let tag = std::str::from_utf8(data.get(5..).unwrap_or_default()).unwrap_or_default();
1450            Some(ServerMsg::CreatedN { nonce, pty_id, tag })
1451        }
1452        S2C_CLOSED => {
1453            if data.len() < 3 {
1454                return None;
1455            }
1456            Some(ServerMsg::Closed {
1457                pty_id: u16::from_le_bytes([data[1], data[2]]),
1458            })
1459        }
1460        S2C_EXITED => {
1461            if data.len() < 7 {
1462                return None;
1463            }
1464            Some(ServerMsg::Exited {
1465                pty_id: u16::from_le_bytes([data[1], data[2]]),
1466                exit_status: i32::from_le_bytes([data[3], data[4], data[5], data[6]]),
1467            })
1468        }
1469        S2C_LIST => {
1470            if data.len() < 3 {
1471                return None;
1472            }
1473            let count = u16::from_le_bytes([data[1], data[2]]) as usize;
1474            let mut entries = Vec::with_capacity(count);
1475            let mut offset = 3;
1476            for _ in 0..count {
1477                if offset + 4 > data.len() {
1478                    break;
1479                }
1480                let pty_id = u16::from_le_bytes([data[offset], data[offset + 1]]);
1481                let tag_len = u16::from_le_bytes([data[offset + 2], data[offset + 3]]) as usize;
1482                offset += 4;
1483                if offset + tag_len > data.len() {
1484                    break;
1485                }
1486                let tag = std::str::from_utf8(&data[offset..offset + tag_len]).unwrap_or_default();
1487                offset += tag_len;
1488                let command = if offset + 2 <= data.len() {
1489                    let cmd_len = u16::from_le_bytes([data[offset], data[offset + 1]]) as usize;
1490                    offset += 2;
1491                    if offset + cmd_len <= data.len() {
1492                        let cmd = std::str::from_utf8(&data[offset..offset + cmd_len])
1493                            .unwrap_or_default();
1494                        offset += cmd_len;
1495                        cmd
1496                    } else {
1497                        // Truncated command — don't advance offset past
1498                        // available data; stop parsing this entry.
1499                        offset = data.len();
1500                        ""
1501                    }
1502                } else {
1503                    ""
1504                };
1505                entries.push(PtyListEntry {
1506                    pty_id,
1507                    tag,
1508                    command,
1509                });
1510            }
1511            Some(ServerMsg::List { entries })
1512        }
1513        S2C_TITLE => {
1514            if data.len() < 3 {
1515                return None;
1516            }
1517            Some(ServerMsg::Title {
1518                pty_id: u16::from_le_bytes([data[1], data[2]]),
1519                title: &data[3..],
1520            })
1521        }
1522        S2C_SEARCH_RESULTS => {
1523            if data.len() < 5 {
1524                return None;
1525            }
1526            let request_id = u16::from_le_bytes([data[1], data[2]]);
1527            let count = u16::from_le_bytes([data[3], data[4]]) as usize;
1528            let mut results = Vec::with_capacity(count);
1529            let mut offset = 5usize;
1530            for _ in 0..count {
1531                if offset + 14 > data.len() {
1532                    return None;
1533                }
1534                let pty_id = u16::from_le_bytes([data[offset], data[offset + 1]]);
1535                let score = u32::from_le_bytes([
1536                    data[offset + 2],
1537                    data[offset + 3],
1538                    data[offset + 4],
1539                    data[offset + 5],
1540                ]);
1541                let primary_source = data[offset + 6];
1542                let matched_sources = data[offset + 7];
1543                let scroll_offset = u32::from_le_bytes([
1544                    data[offset + 8],
1545                    data[offset + 9],
1546                    data[offset + 10],
1547                    data[offset + 11],
1548                ]);
1549                let context_len =
1550                    u16::from_le_bytes([data[offset + 12], data[offset + 13]]) as usize;
1551                offset += 14;
1552                if offset + context_len > data.len() {
1553                    return None;
1554                }
1555                results.push(SearchResultEntry {
1556                    pty_id,
1557                    score,
1558                    primary_source,
1559                    matched_sources,
1560                    scroll_offset: if scroll_offset == u32::MAX {
1561                        None
1562                    } else {
1563                        Some(scroll_offset)
1564                    },
1565                    context: &data[offset..offset + context_len],
1566                });
1567                offset += context_len;
1568            }
1569            Some(ServerMsg::SearchResults {
1570                request_id,
1571                results,
1572            })
1573        }
1574        S2C_READY => Some(ServerMsg::Ready),
1575        S2C_TEXT => {
1576            if data.len() < 13 {
1577                return None;
1578            }
1579            let nonce = u16::from_le_bytes([data[1], data[2]]);
1580            let pty_id = u16::from_le_bytes([data[3], data[4]]);
1581            let total_lines = u32::from_le_bytes([data[5], data[6], data[7], data[8]]);
1582            let offset = u32::from_le_bytes([data[9], data[10], data[11], data[12]]);
1583            let text = std::str::from_utf8(data.get(13..).unwrap_or_default()).unwrap_or_default();
1584            Some(ServerMsg::Text {
1585                nonce,
1586                pty_id,
1587                total_lines,
1588                offset,
1589                text,
1590            })
1591        }
1592        S2C_SURFACE_CREATED => {
1593            if data.len() < 15 {
1594                return None;
1595            }
1596            let session_id = u16::from_le_bytes([data[1], data[2]]);
1597            let surface_id = u16::from_le_bytes([data[3], data[4]]);
1598            let parent_id = u16::from_le_bytes([data[5], data[6]]);
1599            let width = u16::from_le_bytes([data[7], data[8]]);
1600            let height = u16::from_le_bytes([data[9], data[10]]);
1601            let title_len = u16::from_le_bytes([data[11], data[12]]) as usize;
1602            let mut off = 13;
1603            if off + title_len + 2 > data.len() {
1604                return None;
1605            }
1606            let title = std::str::from_utf8(&data[off..off + title_len]).unwrap_or_default();
1607            off += title_len;
1608            let app_id_len = u16::from_le_bytes([data[off], data[off + 1]]) as usize;
1609            off += 2;
1610            if off + app_id_len > data.len() {
1611                return None;
1612            }
1613            let app_id = std::str::from_utf8(&data[off..off + app_id_len]).unwrap_or_default();
1614            Some(ServerMsg::SurfaceCreated {
1615                session_id,
1616                surface_id,
1617                parent_id,
1618                width,
1619                height,
1620                title,
1621                app_id,
1622            })
1623        }
1624        S2C_SURFACE_DESTROYED => {
1625            if data.len() < 5 {
1626                return None;
1627            }
1628            Some(ServerMsg::SurfaceDestroyed {
1629                session_id: u16::from_le_bytes([data[1], data[2]]),
1630                surface_id: u16::from_le_bytes([data[3], data[4]]),
1631            })
1632        }
1633        S2C_SURFACE_FRAME => {
1634            if data.len() < 14 {
1635                return None;
1636            }
1637            Some(ServerMsg::SurfaceFrame {
1638                session_id: u16::from_le_bytes([data[1], data[2]]),
1639                surface_id: u16::from_le_bytes([data[3], data[4]]),
1640                timestamp: u32::from_le_bytes([data[5], data[6], data[7], data[8]]),
1641                flags: data[9],
1642                width: u16::from_le_bytes([data[10], data[11]]),
1643                height: u16::from_le_bytes([data[12], data[13]]),
1644                data: data.get(14..).unwrap_or_default(),
1645            })
1646        }
1647        S2C_SURFACE_TITLE => {
1648            if data.len() < 5 {
1649                return None;
1650            }
1651            let title = std::str::from_utf8(data.get(5..).unwrap_or_default()).unwrap_or_default();
1652            Some(ServerMsg::SurfaceTitle {
1653                session_id: u16::from_le_bytes([data[1], data[2]]),
1654                surface_id: u16::from_le_bytes([data[3], data[4]]),
1655                title,
1656            })
1657        }
1658        S2C_SURFACE_APP_ID => {
1659            if data.len() < 5 {
1660                return None;
1661            }
1662            let app_id = std::str::from_utf8(data.get(5..).unwrap_or_default()).unwrap_or_default();
1663            Some(ServerMsg::SurfaceAppId {
1664                session_id: u16::from_le_bytes([data[1], data[2]]),
1665                surface_id: u16::from_le_bytes([data[3], data[4]]),
1666                app_id,
1667            })
1668        }
1669        S2C_SURFACE_RESIZED => {
1670            if data.len() < 9 {
1671                return None;
1672            }
1673            Some(ServerMsg::SurfaceResized {
1674                session_id: u16::from_le_bytes([data[1], data[2]]),
1675                surface_id: u16::from_le_bytes([data[3], data[4]]),
1676                width: u16::from_le_bytes([data[5], data[6]]),
1677                height: u16::from_le_bytes([data[7], data[8]]),
1678            })
1679        }
1680        S2C_CLIPBOARD => {
1681            if data.len() < 11 {
1682                return None;
1683            }
1684            let session_id = u16::from_le_bytes([data[1], data[2]]);
1685            let surface_id = u16::from_le_bytes([data[3], data[4]]);
1686            let mime_len = u16::from_le_bytes([data[5], data[6]]) as usize;
1687            let mut off = 7;
1688            if off + mime_len + 4 > data.len() {
1689                return None;
1690            }
1691            let mime_type = std::str::from_utf8(&data[off..off + mime_len]).unwrap_or_default();
1692            off += mime_len;
1693            let data_len =
1694                u32::from_le_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]])
1695                    as usize;
1696            off += 4;
1697            if off + data_len > data.len() {
1698                return None;
1699            }
1700            Some(ServerMsg::Clipboard {
1701                session_id,
1702                surface_id,
1703                mime_type,
1704                data: &data[off..off + data_len],
1705            })
1706        }
1707        S2C_SURFACE_LIST => {
1708            if data.len() < 3 {
1709                return None;
1710            }
1711            let count = u16::from_le_bytes([data[1], data[2]]) as usize;
1712            let mut entries = Vec::with_capacity(count);
1713            let mut offset = 3;
1714            for _ in 0..count {
1715                if offset + 8 > data.len() {
1716                    break;
1717                }
1718                let surface_id = u16::from_le_bytes([data[offset], data[offset + 1]]);
1719                let parent_id = u16::from_le_bytes([data[offset + 2], data[offset + 3]]);
1720                let width = u16::from_le_bytes([data[offset + 4], data[offset + 5]]);
1721                let height = u16::from_le_bytes([data[offset + 6], data[offset + 7]]);
1722                offset += 8;
1723                if offset + 2 > data.len() {
1724                    break;
1725                }
1726                let title_len = u16::from_le_bytes([data[offset], data[offset + 1]]) as usize;
1727                offset += 2;
1728                if offset + title_len > data.len() {
1729                    break;
1730                }
1731                let title =
1732                    std::str::from_utf8(&data[offset..offset + title_len]).unwrap_or_default();
1733                offset += title_len;
1734                if offset + 2 > data.len() {
1735                    break;
1736                }
1737                let app_id_len = u16::from_le_bytes([data[offset], data[offset + 1]]) as usize;
1738                offset += 2;
1739                if offset + app_id_len > data.len() {
1740                    break;
1741                }
1742                let app_id =
1743                    std::str::from_utf8(&data[offset..offset + app_id_len]).unwrap_or_default();
1744                offset += app_id_len;
1745                entries.push(SurfaceListEntry {
1746                    surface_id,
1747                    parent_id,
1748                    width,
1749                    height,
1750                    title: title.to_string(),
1751                    app_id: app_id.to_string(),
1752                });
1753            }
1754            Some(ServerMsg::SurfaceList { entries })
1755        }
1756        S2C_SURFACE_CAPTURE => {
1757            if data.len() < 11 {
1758                return None;
1759            }
1760            let surface_id = u16::from_le_bytes([data[1], data[2]]);
1761            let width = u32::from_le_bytes([data[3], data[4], data[5], data[6]]);
1762            let height = u32::from_le_bytes([data[7], data[8], data[9], data[10]]);
1763            let image_data = data.get(11..).unwrap_or_default();
1764            Some(ServerMsg::SurfaceCapture {
1765                surface_id,
1766                width,
1767                height,
1768                image_data,
1769            })
1770        }
1771        _ => None,
1772    }
1773}
1774
1775pub fn msg_hello(version: u16, features: u32) -> Vec<u8> {
1776    let mut msg = Vec::with_capacity(7);
1777    msg.push(S2C_HELLO);
1778    msg.extend_from_slice(&version.to_le_bytes());
1779    msg.extend_from_slice(&features.to_le_bytes());
1780    msg
1781}
1782
1783pub fn msg_create(rows: u16, cols: u16) -> Vec<u8> {
1784    msg_create_tagged(rows, cols, "")
1785}
1786
1787pub fn msg_create_tagged(rows: u16, cols: u16, tag: &str) -> Vec<u8> {
1788    let tag_bytes = tag.as_bytes();
1789    let tag_len = tag_bytes.len().min(u16::MAX as usize);
1790    let mut msg = Vec::with_capacity(7 + tag_len);
1791    msg.push(C2S_CREATE);
1792    msg.extend_from_slice(&rows.to_le_bytes());
1793    msg.extend_from_slice(&cols.to_le_bytes());
1794    msg.extend_from_slice(&(tag_len as u16).to_le_bytes());
1795    msg.extend_from_slice(&tag_bytes[..tag_len]);
1796    msg
1797}
1798
1799/// Spawn a new PTY in the same working directory as `src_pty_id`.
1800pub fn msg_create_at(rows: u16, cols: u16, tag: &str, src_pty_id: u16) -> Vec<u8> {
1801    let tag_bytes = tag.as_bytes();
1802    let tag_len = tag_bytes.len().min(u16::MAX as usize);
1803    let mut msg = Vec::with_capacity(9 + tag_len);
1804    msg.push(C2S_CREATE_AT);
1805    msg.extend_from_slice(&rows.to_le_bytes());
1806    msg.extend_from_slice(&cols.to_le_bytes());
1807    msg.extend_from_slice(&(tag_len as u16).to_le_bytes());
1808    msg.extend_from_slice(&tag_bytes[..tag_len]);
1809    msg.extend_from_slice(&src_pty_id.to_le_bytes());
1810    msg
1811}
1812
1813pub fn msg_create_n(nonce: u16, rows: u16, cols: u16, tag: &str) -> Vec<u8> {
1814    let tag_bytes = tag.as_bytes();
1815    let tag_len = tag_bytes.len().min(u16::MAX as usize);
1816    let mut msg = Vec::with_capacity(9 + tag_len);
1817    msg.push(C2S_CREATE_N);
1818    msg.extend_from_slice(&nonce.to_le_bytes());
1819    msg.extend_from_slice(&rows.to_le_bytes());
1820    msg.extend_from_slice(&cols.to_le_bytes());
1821    msg.extend_from_slice(&(tag_len as u16).to_le_bytes());
1822    msg.extend_from_slice(&tag_bytes[..tag_len]);
1823    msg
1824}
1825
1826pub fn msg_create_n_command(nonce: u16, rows: u16, cols: u16, tag: &str, command: &str) -> Vec<u8> {
1827    let mut msg = msg_create_n(nonce, rows, cols, tag);
1828    msg.extend_from_slice(command.as_bytes());
1829    msg
1830}
1831
1832pub fn msg_create2(
1833    nonce: u16,
1834    rows: u16,
1835    cols: u16,
1836    tag: &str,
1837    command: &str,
1838    features: u8,
1839) -> Vec<u8> {
1840    let tag_bytes = tag.as_bytes();
1841    let cmd_bytes = command.as_bytes();
1842    let has_cmd = !command.is_empty();
1843    let feat = features | if has_cmd { CREATE2_HAS_COMMAND } else { 0 };
1844    let mut msg = Vec::with_capacity(10 + tag_bytes.len() + cmd_bytes.len());
1845    msg.push(C2S_CREATE2);
1846    msg.extend_from_slice(&nonce.to_le_bytes());
1847    msg.extend_from_slice(&rows.to_le_bytes());
1848    msg.extend_from_slice(&cols.to_le_bytes());
1849    msg.push(feat);
1850    msg.extend_from_slice(&(tag_bytes.len() as u16).to_le_bytes());
1851    msg.extend_from_slice(tag_bytes);
1852    if has_cmd {
1853        msg.extend_from_slice(cmd_bytes);
1854    }
1855    msg
1856}
1857
1858pub fn msg_create_command(rows: u16, cols: u16, command: &str) -> Vec<u8> {
1859    msg_create_tagged_command(rows, cols, "", command)
1860}
1861
1862pub fn msg_create_tagged_command(rows: u16, cols: u16, tag: &str, command: &str) -> Vec<u8> {
1863    let mut msg = msg_create_tagged(rows, cols, tag);
1864    msg.extend_from_slice(command.as_bytes());
1865    msg
1866}
1867
1868pub fn msg_input(pty_id: u16, data: &[u8]) -> Vec<u8> {
1869    let mut msg = Vec::with_capacity(3 + data.len());
1870    msg.push(C2S_INPUT);
1871    msg.extend_from_slice(&pty_id.to_le_bytes());
1872    msg.extend_from_slice(data);
1873    msg
1874}
1875
1876pub fn msg_resize(pty_id: u16, rows: u16, cols: u16) -> Vec<u8> {
1877    let mut msg = Vec::with_capacity(7);
1878    msg.push(C2S_RESIZE);
1879    msg.extend_from_slice(&pty_id.to_le_bytes());
1880    msg.extend_from_slice(&rows.to_le_bytes());
1881    msg.extend_from_slice(&cols.to_le_bytes());
1882    msg
1883}
1884
1885pub fn msg_resize_batch(entries: &[(u16, u16, u16)]) -> Vec<u8> {
1886    let mut msg = Vec::with_capacity(1 + entries.len() * 6);
1887    msg.push(C2S_RESIZE);
1888    for &(pty_id, rows, cols) in entries {
1889        msg.extend_from_slice(&pty_id.to_le_bytes());
1890        msg.extend_from_slice(&rows.to_le_bytes());
1891        msg.extend_from_slice(&cols.to_le_bytes());
1892    }
1893    msg
1894}
1895
1896pub fn msg_focus(pty_id: u16) -> Vec<u8> {
1897    let mut msg = Vec::with_capacity(3);
1898    msg.push(C2S_FOCUS);
1899    msg.extend_from_slice(&pty_id.to_le_bytes());
1900    msg
1901}
1902
1903pub fn msg_close(pty_id: u16) -> Vec<u8> {
1904    let mut msg = Vec::with_capacity(3);
1905    msg.push(C2S_CLOSE);
1906    msg.extend_from_slice(&pty_id.to_le_bytes());
1907    msg
1908}
1909
1910pub fn msg_kill(pty_id: u16, signal: i32) -> Vec<u8> {
1911    let mut msg = Vec::with_capacity(7);
1912    msg.push(C2S_KILL);
1913    msg.extend_from_slice(&pty_id.to_le_bytes());
1914    msg.extend_from_slice(&signal.to_le_bytes());
1915    msg
1916}
1917
1918pub fn msg_restart(pty_id: u16) -> Vec<u8> {
1919    let mut msg = Vec::with_capacity(3);
1920    msg.push(C2S_RESTART);
1921    msg.extend_from_slice(&pty_id.to_le_bytes());
1922    msg
1923}
1924
1925pub fn msg_subscribe(pty_id: u16) -> Vec<u8> {
1926    let mut msg = Vec::with_capacity(3);
1927    msg.push(C2S_SUBSCRIBE);
1928    msg.extend_from_slice(&pty_id.to_le_bytes());
1929    msg
1930}
1931
1932pub fn msg_unsubscribe(pty_id: u16) -> Vec<u8> {
1933    let mut msg = Vec::with_capacity(3);
1934    msg.push(C2S_UNSUBSCRIBE);
1935    msg.extend_from_slice(&pty_id.to_le_bytes());
1936    msg
1937}
1938
1939pub fn msg_search(request_id: u16, query: &str) -> Vec<u8> {
1940    let query = query.as_bytes();
1941    let mut msg = Vec::with_capacity(3 + query.len());
1942    msg.push(C2S_SEARCH);
1943    msg.extend_from_slice(&request_id.to_le_bytes());
1944    msg.extend_from_slice(query);
1945    msg
1946}
1947
1948pub fn msg_ack() -> Vec<u8> {
1949    vec![C2S_ACK]
1950}
1951
1952pub fn msg_scroll(pty_id: u16, offset: u32) -> Vec<u8> {
1953    let mut msg = Vec::with_capacity(7);
1954    msg.push(C2S_SCROLL);
1955    msg.extend_from_slice(&pty_id.to_le_bytes());
1956    msg.extend_from_slice(&offset.to_le_bytes());
1957    msg
1958}
1959
1960pub fn msg_display_rate(fps: u16) -> Vec<u8> {
1961    let mut msg = Vec::with_capacity(3);
1962    msg.push(C2S_DISPLAY_RATE);
1963    msg.extend_from_slice(&fps.to_le_bytes());
1964    msg
1965}
1966
1967pub fn msg_client_metrics(backlog: u16, ack_ahead: u16, apply_ms_x10: u16) -> Vec<u8> {
1968    let mut msg = Vec::with_capacity(7);
1969    msg.push(C2S_CLIENT_METRICS);
1970    msg.extend_from_slice(&backlog.to_le_bytes());
1971    msg.extend_from_slice(&ack_ahead.to_le_bytes());
1972    msg.extend_from_slice(&apply_ms_x10.to_le_bytes());
1973    msg
1974}
1975
1976pub fn msg_read(nonce: u16, pty_id: u16, offset: u32, limit: u32, flags: u8) -> Vec<u8> {
1977    let mut msg = Vec::with_capacity(14);
1978    msg.push(C2S_READ);
1979    msg.extend_from_slice(&nonce.to_le_bytes());
1980    msg.extend_from_slice(&pty_id.to_le_bytes());
1981    msg.extend_from_slice(&offset.to_le_bytes());
1982    msg.extend_from_slice(&limit.to_le_bytes());
1983    msg.push(flags);
1984    msg
1985}
1986
1987pub fn msg_copy_range(
1988    nonce: u16,
1989    pty_id: u16,
1990    start_tail: u32,
1991    start_col: u16,
1992    end_tail: u32,
1993    end_col: u16,
1994    flags: u8,
1995) -> Vec<u8> {
1996    let mut msg = Vec::with_capacity(18);
1997    msg.push(C2S_COPY_RANGE);
1998    msg.extend_from_slice(&nonce.to_le_bytes());
1999    msg.extend_from_slice(&pty_id.to_le_bytes());
2000    msg.extend_from_slice(&start_tail.to_le_bytes());
2001    msg.extend_from_slice(&start_col.to_le_bytes());
2002    msg.extend_from_slice(&end_tail.to_le_bytes());
2003    msg.extend_from_slice(&end_col.to_le_bytes());
2004    msg.push(flags);
2005    msg
2006}
2007
2008pub fn msg_exited(pty_id: u16, exit_status: i32) -> Vec<u8> {
2009    let mut msg = Vec::with_capacity(7);
2010    msg.push(S2C_EXITED);
2011    msg.extend_from_slice(&pty_id.to_le_bytes());
2012    msg.extend_from_slice(&exit_status.to_le_bytes());
2013    msg
2014}
2015
2016pub fn msg_surface_created(
2017    session_id: u16,
2018    surface_id: u16,
2019    parent_id: u16,
2020    width: u16,
2021    height: u16,
2022    title: &str,
2023    app_id: &str,
2024) -> Vec<u8> {
2025    let title_bytes = title.as_bytes();
2026    let app_id_bytes = app_id.as_bytes();
2027    let mut msg = Vec::with_capacity(15 + title_bytes.len() + app_id_bytes.len());
2028    msg.push(S2C_SURFACE_CREATED);
2029    msg.extend_from_slice(&session_id.to_le_bytes());
2030    msg.extend_from_slice(&surface_id.to_le_bytes());
2031    msg.extend_from_slice(&parent_id.to_le_bytes());
2032    msg.extend_from_slice(&width.to_le_bytes());
2033    msg.extend_from_slice(&height.to_le_bytes());
2034    msg.extend_from_slice(&(title_bytes.len() as u16).to_le_bytes());
2035    msg.extend_from_slice(title_bytes);
2036    msg.extend_from_slice(&(app_id_bytes.len() as u16).to_le_bytes());
2037    msg.extend_from_slice(app_id_bytes);
2038    msg
2039}
2040
2041pub fn msg_surface_destroyed(session_id: u16, surface_id: u16) -> Vec<u8> {
2042    let mut msg = Vec::with_capacity(5);
2043    msg.push(S2C_SURFACE_DESTROYED);
2044    msg.extend_from_slice(&session_id.to_le_bytes());
2045    msg.extend_from_slice(&surface_id.to_le_bytes());
2046    msg
2047}
2048
2049pub fn msg_surface_frame(
2050    session_id: u16,
2051    surface_id: u16,
2052    timestamp: u32,
2053    flags: u8,
2054    width: u16,
2055    height: u16,
2056    data: &[u8],
2057) -> Vec<u8> {
2058    let mut msg = Vec::with_capacity(14 + data.len());
2059    msg.push(S2C_SURFACE_FRAME);
2060    msg.extend_from_slice(&session_id.to_le_bytes());
2061    msg.extend_from_slice(&surface_id.to_le_bytes());
2062    msg.extend_from_slice(&timestamp.to_le_bytes());
2063    msg.push(flags);
2064    msg.extend_from_slice(&width.to_le_bytes());
2065    msg.extend_from_slice(&height.to_le_bytes());
2066    msg.extend_from_slice(data);
2067    msg
2068}
2069
2070pub fn msg_surface_title(session_id: u16, surface_id: u16, title: &str) -> Vec<u8> {
2071    let title_bytes = title.as_bytes();
2072    let mut msg = Vec::with_capacity(5 + title_bytes.len());
2073    msg.push(S2C_SURFACE_TITLE);
2074    msg.extend_from_slice(&session_id.to_le_bytes());
2075    msg.extend_from_slice(&surface_id.to_le_bytes());
2076    msg.extend_from_slice(title_bytes);
2077    msg
2078}
2079
2080pub fn msg_surface_app_id(session_id: u16, surface_id: u16, app_id: &str) -> Vec<u8> {
2081    let app_id_bytes = app_id.as_bytes();
2082    let mut msg = Vec::with_capacity(5 + app_id_bytes.len());
2083    msg.push(S2C_SURFACE_APP_ID);
2084    msg.extend_from_slice(&session_id.to_le_bytes());
2085    msg.extend_from_slice(&surface_id.to_le_bytes());
2086    msg.extend_from_slice(app_id_bytes);
2087    msg
2088}
2089
2090pub fn msg_surface_resized(session_id: u16, surface_id: u16, width: u16, height: u16) -> Vec<u8> {
2091    let mut msg = Vec::with_capacity(9);
2092    msg.push(S2C_SURFACE_RESIZED);
2093    msg.extend_from_slice(&session_id.to_le_bytes());
2094    msg.extend_from_slice(&surface_id.to_le_bytes());
2095    msg.extend_from_slice(&width.to_le_bytes());
2096    msg.extend_from_slice(&height.to_le_bytes());
2097    msg
2098}
2099
2100pub fn msg_s2c_clipboard(
2101    session_id: u16,
2102    surface_id: u16,
2103    mime_type: &str,
2104    data: &[u8],
2105) -> Vec<u8> {
2106    let mime_bytes = mime_type.as_bytes();
2107    let mut msg = Vec::with_capacity(11 + mime_bytes.len() + data.len());
2108    msg.push(S2C_CLIPBOARD);
2109    msg.extend_from_slice(&session_id.to_le_bytes());
2110    msg.extend_from_slice(&surface_id.to_le_bytes());
2111    msg.extend_from_slice(&(mime_bytes.len() as u16).to_le_bytes());
2112    msg.extend_from_slice(mime_bytes);
2113    msg.extend_from_slice(&(data.len() as u32).to_le_bytes());
2114    msg.extend_from_slice(data);
2115    msg
2116}
2117
2118pub fn msg_surface_input(session_id: u16, surface_id: u16, data: &[u8]) -> Vec<u8> {
2119    let mut msg = Vec::with_capacity(5 + data.len());
2120    msg.push(C2S_SURFACE_INPUT);
2121    msg.extend_from_slice(&session_id.to_le_bytes());
2122    msg.extend_from_slice(&surface_id.to_le_bytes());
2123    msg.extend_from_slice(data);
2124    msg
2125}
2126
2127pub fn msg_surface_pointer(
2128    session_id: u16,
2129    surface_id: u16,
2130    event_type: u8,
2131    button: u8,
2132    x: u16,
2133    y: u16,
2134) -> Vec<u8> {
2135    let mut msg = Vec::with_capacity(10);
2136    msg.push(C2S_SURFACE_POINTER);
2137    msg.extend_from_slice(&session_id.to_le_bytes());
2138    msg.extend_from_slice(&surface_id.to_le_bytes());
2139    msg.push(event_type);
2140    msg.push(button);
2141    msg.extend_from_slice(&x.to_le_bytes());
2142    msg.extend_from_slice(&y.to_le_bytes());
2143    msg
2144}
2145
2146pub fn msg_surface_pointer_axis(
2147    session_id: u16,
2148    surface_id: u16,
2149    axis: u8,
2150    value_x100: i32,
2151) -> Vec<u8> {
2152    let mut msg = Vec::with_capacity(10);
2153    msg.push(C2S_SURFACE_POINTER_AXIS);
2154    msg.extend_from_slice(&session_id.to_le_bytes());
2155    msg.extend_from_slice(&surface_id.to_le_bytes());
2156    msg.push(axis);
2157    msg.extend_from_slice(&value_x100.to_le_bytes());
2158    msg
2159}
2160
2161/// `scale_120` is the device-pixel-ratio in 1/120th units, matching
2162/// Wayland's `fractional_scale_v1` convention: 120 = 1×, 180 = 1.5×,
2163/// 240 = 2×.  A value of 0 means "unspecified" (server defaults to 1×).
2164///
2165/// `codec_support` is a bitmask of codecs the client can decode
2166/// (`CODEC_SUPPORT_*`).  0 means "accept anything".
2167pub fn msg_surface_resize(
2168    session_id: u16,
2169    surface_id: u16,
2170    width: u16,
2171    height: u16,
2172    scale_120: u16,
2173    codec_support: u8,
2174) -> Vec<u8> {
2175    let mut msg = Vec::with_capacity(12);
2176    msg.push(C2S_SURFACE_RESIZE);
2177    msg.extend_from_slice(&session_id.to_le_bytes());
2178    msg.extend_from_slice(&surface_id.to_le_bytes());
2179    msg.extend_from_slice(&width.to_le_bytes());
2180    msg.extend_from_slice(&height.to_le_bytes());
2181    msg.extend_from_slice(&scale_120.to_le_bytes());
2182    msg.push(codec_support);
2183    msg
2184}
2185
2186pub fn msg_surface_focus(session_id: u16, surface_id: u16) -> Vec<u8> {
2187    let mut msg = Vec::with_capacity(5);
2188    msg.push(C2S_SURFACE_FOCUS);
2189    msg.extend_from_slice(&session_id.to_le_bytes());
2190    msg.extend_from_slice(&surface_id.to_le_bytes());
2191    msg
2192}
2193
2194pub fn msg_surface_subscribe(session_id: u16, surface_id: u16) -> Vec<u8> {
2195    let mut msg = Vec::with_capacity(5);
2196    msg.push(C2S_SURFACE_SUBSCRIBE);
2197    msg.extend_from_slice(&session_id.to_le_bytes());
2198    msg.extend_from_slice(&surface_id.to_le_bytes());
2199    msg
2200}
2201
2202pub fn msg_surface_unsubscribe(session_id: u16, surface_id: u16) -> Vec<u8> {
2203    let mut msg = Vec::with_capacity(5);
2204    msg.push(C2S_SURFACE_UNSUBSCRIBE);
2205    msg.extend_from_slice(&session_id.to_le_bytes());
2206    msg.extend_from_slice(&surface_id.to_le_bytes());
2207    msg
2208}
2209
2210pub fn msg_c2s_clipboard(
2211    session_id: u16,
2212    surface_id: u16,
2213    mime_type: &str,
2214    data: &[u8],
2215) -> Vec<u8> {
2216    let mime_bytes = mime_type.as_bytes();
2217    let mut msg = Vec::with_capacity(11 + mime_bytes.len() + data.len());
2218    msg.push(C2S_CLIPBOARD);
2219    msg.extend_from_slice(&session_id.to_le_bytes());
2220    msg.extend_from_slice(&surface_id.to_le_bytes());
2221    msg.extend_from_slice(&(mime_bytes.len() as u16).to_le_bytes());
2222    msg.extend_from_slice(mime_bytes);
2223    msg.extend_from_slice(&(data.len() as u32).to_le_bytes());
2224    msg.extend_from_slice(data);
2225    msg
2226}
2227
2228fn push_sgr(out: &mut String, style: &CellStyle) {
2229    use std::fmt::Write;
2230    out.push_str("\x1b[0");
2231    if style.bold {
2232        out.push_str(";1");
2233    }
2234    if style.dim {
2235        out.push_str(";2");
2236    }
2237    if style.italic {
2238        out.push_str(";3");
2239    }
2240    if style.underline {
2241        out.push_str(";4");
2242    }
2243    if style.inverse {
2244        out.push_str(";7");
2245    }
2246    match style.fg {
2247        Color::Indexed(n) => {
2248            let _ = write!(out, ";38;5;{n}");
2249        }
2250        Color::Rgb(r, g, b) => {
2251            let _ = write!(out, ";38;2;{r};{g};{b}");
2252        }
2253        Color::Default => {}
2254    }
2255    match style.bg {
2256        Color::Indexed(n) => {
2257            let _ = write!(out, ";48;5;{n}");
2258        }
2259        Color::Rgb(r, g, b) => {
2260            let _ = write!(out, ";48;2;{r};{g};{b}");
2261        }
2262        Color::Default => {}
2263    }
2264    out.push('m');
2265}
2266
2267const MODE_ALT_SCREEN: u16 = 1 << 11;
2268
2269fn mode_is_cooked(mode: u16) -> bool {
2270    mode & MODE_ECHO != 0 && mode & MODE_ICANON != 0 && mode & MODE_ALT_SCREEN == 0
2271}
2272
2273pub fn build_update_msg(
2274    pty_id: u16,
2275    current: &FrameState,
2276    previous: &FrameState,
2277) -> Option<Vec<u8>> {
2278    let title_changed = current.title != previous.title;
2279    let same_size = previous.rows == current.rows
2280        && previous.cols == current.cols
2281        && previous.cells.len() == current.cells.len();
2282
2283    // Try scroll-aware ops when dimensions match and content differs.
2284    let mut ops = Vec::new();
2285    let mut op_count = 0u16;
2286
2287    // Scroll-aware ops apply when content is "cooked" (shell output) or when
2288    // either frame has mode 0 (scrollback frames use mode=0, and their content
2289    // is always static text that benefits from COPY_RECT).
2290    let scroll_eligible = (mode_is_cooked(current.mode) && mode_is_cooked(previous.mode))
2291        || current.mode == 0
2292        || previous.mode == 0;
2293    if ENABLE_SCROLL_OPS
2294        && same_size
2295        && previous.cells != current.cells
2296        && scroll_eligible
2297        && let Some(delta_rows) = detect_vertical_scroll(current, previous)
2298    {
2299        let mut basis = previous.clone();
2300        encode_copy_rect_op(&mut ops, current, delta_rows);
2301        apply_vertical_scroll_copy(&mut basis, delta_rows);
2302        op_count += 1;
2303        append_full_width_fill_ops(current, &mut basis, &mut ops, &mut op_count);
2304        if let Some(patch_op) = build_patch_op(current, &basis) {
2305            ops.extend_from_slice(&patch_op);
2306            op_count += 1;
2307        }
2308    }
2309
2310    // Fallback: bare PATCH_CELLS against previous (or a blank frame on resize).
2311    if op_count == 0 {
2312        let basis = if same_size {
2313            previous
2314        } else {
2315            &FrameState::new(current.rows, current.cols)
2316        };
2317        if let Some(patch_op) = build_patch_op(current, basis) {
2318            ops = patch_op;
2319            op_count = 1;
2320        }
2321    }
2322
2323    if op_count == 0 {
2324        // No cell changes — still emit a frame if cursor/mode/title changed.
2325        if !title_changed
2326            && current.cursor_row == previous.cursor_row
2327            && current.cursor_col == previous.cursor_col
2328            && current.mode == previous.mode
2329        {
2330            return None;
2331        }
2332    }
2333
2334    // Collect overflow strings that need to be transmitted.
2335    // We send all overflow entries from the current frame that correspond
2336    // to cells that changed (are in the dirty set).  For a resize (not
2337    // same_size), all cells are "dirty", so we send all overflow entries.
2338    let has_overflow = !current.overflow.is_empty();
2339    let overflow_section = if has_overflow {
2340        serialize_overflow_strings(current)
2341    } else {
2342        Vec::new()
2343    };
2344
2345    let line_flags_changed =
2346        current.line_flags != previous.line_flags || current.rows != previous.rows;
2347    let has_line_flags = line_flags_changed && !current.line_flags.iter().all(|&f| f == 0);
2348
2349    let title_bytes = if title_changed {
2350        current.title.as_bytes()
2351    } else {
2352        &[]
2353    };
2354    let title_len = title_bytes.len().min(TITLE_LEN_MASK as usize);
2355    let title_field = OPS_PRESENT
2356        | if has_overflow { STRINGS_PRESENT } else { 0 }
2357        | if has_line_flags {
2358            LINE_FLAGS_PRESENT
2359        } else {
2360            0
2361        }
2362        | if title_changed {
2363            TITLE_PRESENT | title_len as u16
2364        } else {
2365            0
2366        };
2367
2368    let mut payload = Vec::with_capacity(
2369        12 + title_len
2370            + 2
2371            + ops.len()
2372            + overflow_section.len()
2373            + if has_line_flags {
2374                current.rows as usize
2375            } else {
2376                0
2377            }
2378            + 4,
2379    );
2380    payload.extend_from_slice(&current.rows.to_le_bytes());
2381    payload.extend_from_slice(&current.cols.to_le_bytes());
2382    payload.extend_from_slice(&current.cursor_row.to_le_bytes());
2383    payload.extend_from_slice(&current.cursor_col.to_le_bytes());
2384    payload.extend_from_slice(&current.mode.to_le_bytes());
2385    payload.extend_from_slice(&title_field.to_le_bytes());
2386    if title_changed {
2387        payload.extend_from_slice(&title_bytes[..title_len]);
2388    }
2389    payload.extend_from_slice(&op_count.to_le_bytes());
2390    payload.extend_from_slice(&ops);
2391    payload.extend_from_slice(&overflow_section);
2392    if has_line_flags {
2393        payload.extend_from_slice(&current.line_flags);
2394    }
2395    // Trailing scrollback count — old clients ignore extra bytes.
2396    payload.extend_from_slice(&current.scrollback_lines.to_le_bytes());
2397
2398    let compressed = compress_prepend_size(&payload);
2399    let mut msg = Vec::with_capacity(3 + compressed.len());
2400    msg.push(S2C_UPDATE);
2401    msg.extend_from_slice(&pty_id.to_le_bytes());
2402    msg.extend_from_slice(&compressed);
2403    Some(msg)
2404}
2405
2406/// Serialize overflow strings: [u16 count] [for each: u32 cell_index, u16 len, utf8 bytes]
2407fn serialize_overflow_strings(frame: &FrameState) -> Vec<u8> {
2408    let count = frame.overflow.len().min(u16::MAX as usize);
2409    let mut out = Vec::with_capacity(2 + count * 8);
2410    out.extend_from_slice(&(count as u16).to_le_bytes());
2411    for (&cell_idx, s) in frame.overflow.iter().take(count) {
2412        let bytes = s.as_bytes();
2413        let len = bytes.len().min(u16::MAX as usize);
2414        out.extend_from_slice(&(cell_idx as u32).to_le_bytes());
2415        out.extend_from_slice(&(len as u16).to_le_bytes());
2416        out.extend_from_slice(&bytes[..len]);
2417    }
2418    out
2419}
2420
2421fn build_patch_op(current: &FrameState, previous: &FrameState) -> Option<Vec<u8>> {
2422    let total_cells = current.rows as usize * current.cols as usize;
2423    let bitmask_len = total_cells.div_ceil(8);
2424    let mut bitmask = vec![0u8; bitmask_len];
2425    let mut dirty_count = 0usize;
2426    for i in 0..total_cells {
2427        let off = i * CELL_SIZE;
2428        if current.cells[off..off + CELL_SIZE] != previous.cells[off..off + CELL_SIZE] {
2429            bitmask[i / 8] |= 1 << (i % 8);
2430            dirty_count += 1;
2431        }
2432    }
2433    if dirty_count == 0 {
2434        return None;
2435    }
2436
2437    let mut op = Vec::with_capacity(1 + bitmask_len + dirty_count * CELL_SIZE);
2438    op.push(OP_PATCH_CELLS);
2439    op.extend_from_slice(&bitmask);
2440    for byte_pos in 0..CELL_SIZE {
2441        for i in 0..total_cells {
2442            if bitmask[i / 8] & (1 << (i % 8)) != 0 {
2443                op.push(current.cells[i * CELL_SIZE + byte_pos]);
2444            }
2445        }
2446    }
2447    Some(op)
2448}
2449
2450fn detect_vertical_scroll(current: &FrameState, previous: &FrameState) -> Option<i16> {
2451    let rows = current.rows as usize;
2452    let cols = current.cols as usize;
2453    if rows < 4 || cols == 0 {
2454        return None;
2455    }
2456    let row_bytes = cols * CELL_SIZE;
2457    let max_delta = rows.saturating_sub(1).min(8);
2458    let mut best: Option<(usize, i16)> = None;
2459
2460    for delta in 1..=max_delta {
2461        let overlap = rows - delta;
2462        if overlap < 3 {
2463            continue;
2464        }
2465        for signed_delta in [-(delta as i16), delta as i16] {
2466            let mut matched = 0usize;
2467            for row in 0..rows {
2468                let src_row = row as i32 - signed_delta as i32;
2469                if src_row < 0 || src_row >= rows as i32 {
2470                    continue;
2471                }
2472                let cur_off = row * row_bytes;
2473                let prev_off = src_row as usize * row_bytes;
2474                if current.cells[cur_off..cur_off + row_bytes]
2475                    == previous.cells[prev_off..prev_off + row_bytes]
2476                {
2477                    matched += 1;
2478                }
2479            }
2480            if matched * 5 < overlap * 4 {
2481                continue;
2482            }
2483            let replace = match best {
2484                None => true,
2485                Some((best_matched, best_delta)) => {
2486                    matched > best_matched
2487                        || (matched == best_matched
2488                            && signed_delta.unsigned_abs() < best_delta.unsigned_abs())
2489                }
2490            };
2491            if replace {
2492                best = Some((matched, signed_delta));
2493            }
2494        }
2495    }
2496
2497    best.map(|(_, delta)| delta)
2498}
2499
2500fn encode_copy_rect_op(out: &mut Vec<u8>, current: &FrameState, delta_rows: i16) {
2501    let rows = current.rows;
2502    let cols = current.cols;
2503    let delta = delta_rows.unsigned_abs();
2504    let (src_row, dst_row, copy_rows) = if delta_rows > 0 {
2505        (0, delta, rows.saturating_sub(delta))
2506    } else {
2507        (delta, 0, rows.saturating_sub(delta))
2508    };
2509    out.push(OP_COPY_RECT);
2510    out.extend_from_slice(&src_row.to_le_bytes());
2511    out.extend_from_slice(&0u16.to_le_bytes());
2512    out.extend_from_slice(&dst_row.to_le_bytes());
2513    out.extend_from_slice(&0u16.to_le_bytes());
2514    out.extend_from_slice(&copy_rows.to_le_bytes());
2515    out.extend_from_slice(&cols.to_le_bytes());
2516}
2517
2518fn apply_vertical_scroll_copy(frame: &mut FrameState, delta_rows: i16) {
2519    let delta = delta_rows.unsigned_abs();
2520    if delta == 0 || delta >= frame.rows {
2521        return;
2522    }
2523    let (src_row, dst_row, rows) = if delta_rows > 0 {
2524        (0, delta, frame.rows - delta)
2525    } else {
2526        (delta, 0, frame.rows - delta)
2527    };
2528    apply_copy_rect_frame(frame, src_row, 0, dst_row, 0, rows, frame.cols);
2529}
2530
2531fn apply_copy_rect_frame(
2532    frame: &mut FrameState,
2533    src_row: u16,
2534    src_col: u16,
2535    dst_row: u16,
2536    dst_col: u16,
2537    rows: u16,
2538    cols: u16,
2539) {
2540    let rows = rows
2541        .min(frame.rows.saturating_sub(src_row))
2542        .min(frame.rows.saturating_sub(dst_row));
2543    let cols = cols
2544        .min(frame.cols.saturating_sub(src_col))
2545        .min(frame.cols.saturating_sub(dst_col));
2546    if rows == 0 || cols == 0 {
2547        return;
2548    }
2549    let mut temp = vec![0u8; rows as usize * cols as usize * CELL_SIZE];
2550    for r in 0..rows as usize {
2551        let src_off = frame.cell_offset(src_row + r as u16, src_col);
2552        let src_end = src_off + cols as usize * CELL_SIZE;
2553        let dst_off = r * cols as usize * CELL_SIZE;
2554        temp[dst_off..dst_off + cols as usize * CELL_SIZE]
2555            .copy_from_slice(&frame.cells[src_off..src_end]);
2556    }
2557    for r in 0..rows as usize {
2558        let dst_off = frame.cell_offset(dst_row + r as u16, dst_col);
2559        let dst_end = dst_off + cols as usize * CELL_SIZE;
2560        let src_off = r * cols as usize * CELL_SIZE;
2561        frame.cells[dst_off..dst_end]
2562            .copy_from_slice(&temp[src_off..src_off + cols as usize * CELL_SIZE]);
2563    }
2564}
2565
2566fn append_full_width_fill_ops(
2567    current: &FrameState,
2568    basis: &mut FrameState,
2569    out: &mut Vec<u8>,
2570    op_count: &mut u16,
2571) {
2572    let rows = current.rows as usize;
2573    let cols = current.cols as usize;
2574    if rows == 0 || cols == 0 {
2575        return;
2576    }
2577
2578    let row_bytes = cols * CELL_SIZE;
2579    let mut row = 0usize;
2580    while row < rows {
2581        let row_off = row * row_bytes;
2582        if current.cells[row_off..row_off + row_bytes] == basis.cells[row_off..row_off + row_bytes]
2583        {
2584            row += 1;
2585            continue;
2586        }
2587        let Some(cell) = uniform_row_cell(current, row) else {
2588            row += 1;
2589            continue;
2590        };
2591        let mut end = row + 1;
2592        while end < rows {
2593            if uniform_row_cell(current, end).as_ref() != Some(&cell) {
2594                break;
2595            }
2596            end += 1;
2597        }
2598
2599        if *op_count == u16::MAX {
2600            break;
2601        }
2602        out.push(OP_FILL_RECT);
2603        out.extend_from_slice(&(row as u16).to_le_bytes());
2604        out.extend_from_slice(&0u16.to_le_bytes());
2605        out.extend_from_slice(&((end - row) as u16).to_le_bytes());
2606        out.extend_from_slice(&current.cols.to_le_bytes());
2607        out.extend_from_slice(&cell);
2608        *op_count = op_count.saturating_add(1);
2609
2610        for r in row..end {
2611            let row_off = basis.cell_offset(r as u16, 0);
2612            for c in 0..cols {
2613                let off = row_off + c * CELL_SIZE;
2614                basis.cells[off..off + CELL_SIZE].copy_from_slice(&cell);
2615            }
2616        }
2617
2618        row = end;
2619    }
2620}
2621
2622fn uniform_row_cell(frame: &FrameState, row: usize) -> Option<[u8; CELL_SIZE]> {
2623    let cols = frame.cols as usize;
2624    if row >= frame.rows as usize || cols == 0 {
2625        return None;
2626    }
2627    let start = row * cols * CELL_SIZE;
2628    let mut first = [0u8; CELL_SIZE];
2629    first.copy_from_slice(&frame.cells[start..start + CELL_SIZE]);
2630    if first[1] & 0b110 != 0 {
2631        return None;
2632    }
2633    for col in 1..cols {
2634        let off = start + col * CELL_SIZE;
2635        if frame.cells[off..off + CELL_SIZE] != first {
2636            return None;
2637        }
2638    }
2639    Some(first)
2640}
2641
2642fn encode_cell(dst: &mut [u8], ch: Option<char>, style: CellStyle, wide: bool, wide_cont: bool) {
2643    dst.fill(0);
2644
2645    let mut f0 = 0u8;
2646    encode_color(style.fg, &mut f0, &mut dst[2..5], false);
2647    encode_color(style.bg, &mut f0, &mut dst[5..8], true);
2648    if style.bold {
2649        f0 |= 1 << 4;
2650    }
2651    if style.dim {
2652        f0 |= 1 << 5;
2653    }
2654    if style.italic {
2655        f0 |= 1 << 6;
2656    }
2657    if style.underline {
2658        f0 |= 1 << 7;
2659    }
2660    dst[0] = f0;
2661
2662    let mut f1 = 0u8;
2663    if style.inverse {
2664        f1 |= 1;
2665    }
2666    if wide {
2667        f1 |= 1 << 1;
2668    }
2669    if wide_cont {
2670        f1 |= 1 << 2;
2671    }
2672    if let Some(ch) = ch {
2673        let mut buf = [0u8; 4];
2674        let encoded = ch.encode_utf8(&mut buf).as_bytes();
2675        let len = encoded.len().min(4);
2676        dst[8..8 + len].copy_from_slice(&encoded[..len]);
2677        f1 |= (len as u8) << 3;
2678    }
2679    dst[1] = f1;
2680}
2681
2682fn encode_color(color: Color, flags: &mut u8, dst: &mut [u8], is_bg: bool) {
2683    let shift = if is_bg { 2 } else { 0 };
2684    match color {
2685        Color::Default => {}
2686        Color::Indexed(idx) => {
2687            *flags |= 1 << shift;
2688            dst[0] = idx;
2689        }
2690        Color::Rgb(r, g, b) => {
2691            *flags |= 2 << shift;
2692            dst[0] = r;
2693            dst[1] = g;
2694            dst[2] = b;
2695        }
2696    }
2697}
2698
2699fn wrap_text_lines(text: &str, width: usize) -> Vec<String> {
2700    if width == 0 {
2701        return Vec::new();
2702    }
2703    let mut out = Vec::new();
2704    for paragraph in text.split('\n') {
2705        if paragraph.is_empty() {
2706            out.push(String::new());
2707            continue;
2708        }
2709        let mut line = String::new();
2710        let mut line_width = 0usize;
2711        for word in paragraph.split_whitespace() {
2712            push_wrapped_word(word, width, &mut out, &mut line, &mut line_width);
2713        }
2714        if !line.is_empty() {
2715            out.push(line);
2716        }
2717    }
2718    if out.is_empty() {
2719        out.push(String::new());
2720    }
2721    out
2722}
2723
2724fn push_wrapped_word(
2725    word: &str,
2726    width: usize,
2727    out: &mut Vec<String>,
2728    line: &mut String,
2729    line_width: &mut usize,
2730) {
2731    let word_width = UnicodeWidthStr::width(word);
2732    if line.is_empty() {
2733        if word_width <= width {
2734            line.push_str(word);
2735            *line_width = word_width;
2736            return;
2737        }
2738    } else if *line_width + 1 + word_width <= width {
2739        line.push(' ');
2740        line.push_str(word);
2741        *line_width += 1 + word_width;
2742        return;
2743    } else {
2744        out.push(std::mem::take(line));
2745        *line_width = 0;
2746        if word_width <= width {
2747            line.push_str(word);
2748            *line_width = word_width;
2749            return;
2750        }
2751    }
2752
2753    for ch in word.chars() {
2754        let ch_width = UnicodeWidthChar::width(ch).unwrap_or(1).max(1);
2755        if *line_width + ch_width > width && !line.is_empty() {
2756            out.push(std::mem::take(line));
2757            *line_width = 0;
2758        }
2759        line.push(ch);
2760        *line_width += ch_width;
2761    }
2762}
2763
2764#[cfg(test)]
2765mod tests {
2766    use super::*;
2767
2768    #[test]
2769    fn update_round_trip_preserves_title_and_cells() {
2770        let style = CellStyle::default();
2771        let mut prev = FrameState::new(2, 8);
2772        prev.set_title("one");
2773        prev.write_text(0, 0, "hello", style);
2774
2775        let mut next = prev.clone();
2776        next.set_title("two");
2777        next.write_text(1, 0, "world", style);
2778
2779        let baseline = build_update_msg(7, &prev, &FrameState::default()).unwrap();
2780        let delta = build_update_msg(7, &next, &prev).unwrap();
2781
2782        let mut term = TerminalState::new(2, 8);
2783        let ServerMsg::Update { payload, .. } = parse_server_msg(&baseline).unwrap() else {
2784            panic!("expected update");
2785        };
2786        assert!(term.feed_compressed(payload));
2787        assert_eq!(term.title(), "one");
2788
2789        let ServerMsg::Update { payload, .. } = parse_server_msg(&delta).unwrap() else {
2790            panic!("expected update");
2791        };
2792        assert!(term.feed_compressed(payload));
2793        assert_eq!(term.title(), "two");
2794        assert_eq!(term.get_all_text(), "hello\nworld");
2795    }
2796
2797    #[test]
2798    fn title_can_be_cleared_via_update() {
2799        let style = CellStyle::default();
2800        let mut prev = FrameState::new(1, 4);
2801        prev.set_title("busy");
2802        prev.write_text(0, 0, "ping", style);
2803
2804        let mut next = prev.clone();
2805        next.set_title("");
2806
2807        let baseline = build_update_msg(1, &prev, &FrameState::default()).unwrap();
2808        let delta = build_update_msg(1, &next, &prev).unwrap();
2809
2810        let mut term = TerminalState::new(1, 4);
2811        let ServerMsg::Update { payload, .. } = parse_server_msg(&baseline).unwrap() else {
2812            panic!("expected update");
2813        };
2814        term.feed_compressed(payload);
2815        let ServerMsg::Update { payload, .. } = parse_server_msg(&delta).unwrap() else {
2816            panic!("expected update");
2817        };
2818        term.feed_compressed(payload);
2819        assert_eq!(term.title(), "");
2820    }
2821
2822    #[test]
2823    fn scroll_heavy_update_can_use_ops_payload() {
2824        let style = CellStyle::default();
2825        let mut prev = FrameState::new(5, 6);
2826        prev.write_text(0, 0, "one", style);
2827        prev.write_text(1, 0, "two", style);
2828        prev.write_text(2, 0, "three", style);
2829        prev.write_text(3, 0, "four", style);
2830        prev.write_text(4, 0, "five", style);
2831
2832        let mut next = FrameState::new(5, 6);
2833        next.write_text(0, 0, "two", style);
2834        next.write_text(1, 0, "three", style);
2835        next.write_text(2, 0, "four", style);
2836        next.write_text(3, 0, "five", style);
2837
2838        let delta = build_update_msg(9, &next, &prev).unwrap();
2839        let ServerMsg::Update { payload, .. } = parse_server_msg(&delta).unwrap() else {
2840            panic!("expected update");
2841        };
2842        let decoded = decompress_size_prepended(payload).unwrap();
2843        let title_field = u16::from_le_bytes([decoded[10], decoded[11]]);
2844        assert_ne!(title_field & OPS_PRESENT, 0);
2845
2846        let mut term = TerminalState::new(5, 6);
2847        let baseline = build_update_msg(9, &prev, &FrameState::default()).unwrap();
2848        let ServerMsg::Update { payload, .. } = parse_server_msg(&baseline).unwrap() else {
2849            panic!("expected update");
2850        };
2851        assert!(term.feed_compressed(payload));
2852        let ServerMsg::Update { payload, .. } = parse_server_msg(&delta).unwrap() else {
2853            panic!("expected update");
2854        };
2855        assert!(term.feed_compressed(payload));
2856        assert_eq!(term.get_all_text(), "two\nthree\nfour\nfive\n");
2857    }
2858
2859    #[test]
2860    fn cooked_scroll_heavy_update_uses_copy_rect_op() {
2861        let style = CellStyle::default();
2862        let mut prev = FrameState::new(5, 6);
2863        prev.set_mode(MODE_ECHO | MODE_ICANON);
2864        prev.write_text(0, 0, "one", style);
2865        prev.write_text(1, 0, "two", style);
2866        prev.write_text(2, 0, "three", style);
2867        prev.write_text(3, 0, "four", style);
2868        prev.write_text(4, 0, "five", style);
2869
2870        let mut next = FrameState::new(5, 6);
2871        next.set_mode(MODE_ECHO | MODE_ICANON);
2872        next.write_text(0, 0, "two", style);
2873        next.write_text(1, 0, "three", style);
2874        next.write_text(2, 0, "four", style);
2875        next.write_text(3, 0, "five", style);
2876
2877        let delta = build_update_msg(9, &next, &prev).unwrap();
2878        let ServerMsg::Update { payload, .. } = parse_server_msg(&delta).unwrap() else {
2879            panic!("expected update");
2880        };
2881        let decoded = decompress_size_prepended(payload).unwrap();
2882        let op_count = u16::from_le_bytes([decoded[12], decoded[13]]);
2883        assert!(op_count >= 1);
2884        assert_eq!(decoded[14], OP_COPY_RECT);
2885    }
2886
2887    #[test]
2888    fn mode_zero_scroll_uses_copy_rect() {
2889        let style = CellStyle::default();
2890        let mut prev = FrameState::new(5, 6);
2891        prev.write_text(0, 0, "one", style);
2892        prev.write_text(1, 0, "two", style);
2893        prev.write_text(2, 0, "three", style);
2894        prev.write_text(3, 0, "four", style);
2895        prev.write_text(4, 0, "five", style);
2896
2897        let mut next = FrameState::new(5, 6);
2898        next.write_text(0, 0, "two", style);
2899        next.write_text(1, 0, "three", style);
2900        next.write_text(2, 0, "four", style);
2901        next.write_text(3, 0, "five", style);
2902
2903        let delta = build_update_msg(9, &next, &prev).unwrap();
2904        let ServerMsg::Update { payload, .. } = parse_server_msg(&delta).unwrap() else {
2905            panic!("expected update");
2906        };
2907        let decoded = decompress_size_prepended(payload).unwrap();
2908        let op_count = u16::from_le_bytes([decoded[12], decoded[13]]);
2909        assert!(op_count >= 1);
2910        // mode=0 frames (scrollback) now use COPY_RECT for efficient scrolling
2911        assert_eq!(decoded[14], OP_COPY_RECT);
2912
2913        // Verify round-trip correctness
2914        let baseline = build_update_msg(9, &prev, &FrameState::new(5, 6)).unwrap();
2915        let mut state = TerminalState::new(5, 6);
2916        let ServerMsg::Update { payload: bp, .. } = parse_server_msg(&baseline).unwrap() else {
2917            panic!("expected update");
2918        };
2919        state.feed_compressed(bp);
2920        state.feed_compressed(payload);
2921        assert_eq!(state.frame().cells(), next.cells());
2922    }
2923
2924    #[test]
2925    fn callback_renderer_wraps_text() {
2926        let mut renderer = CallbackRenderer::new(2, 8);
2927        renderer.render(|dom| {
2928            dom.wrapped_text(
2929                Rect::new(0, 0, 2, 8),
2930                "alpha beta gamma",
2931                CellStyle::default(),
2932            );
2933        });
2934        assert_eq!(renderer.frame().get_all_text(), "alpha\nbeta");
2935    }
2936
2937    #[test]
2938    fn scrolling_text_shows_tail() {
2939        let mut frame = FrameState::new(3, 8);
2940        frame.write_scrolling_text(
2941            Rect::new(0, 0, 3, 8),
2942            &["one", "two", "three", "four"],
2943            0,
2944            CellStyle::default(),
2945        );
2946        assert_eq!(frame.get_all_text(), "two\nthree\nfour");
2947    }
2948
2949    #[test]
2950    fn search_results_round_trip_with_context() {
2951        let msg = [
2952            vec![S2C_SEARCH_RESULTS],
2953            7u16.to_le_bytes().to_vec(),
2954            1u16.to_le_bytes().to_vec(),
2955            42u16.to_le_bytes().to_vec(),
2956            1234u32.to_le_bytes().to_vec(),
2957            vec![1, 0b111],
2958            9u32.to_le_bytes().to_vec(),
2959            5u16.to_le_bytes().to_vec(),
2960            b"hello".to_vec(),
2961        ]
2962        .concat();
2963
2964        let ServerMsg::SearchResults {
2965            request_id,
2966            results,
2967        } = parse_server_msg(&msg).unwrap()
2968        else {
2969            panic!("expected search results");
2970        };
2971        assert_eq!(request_id, 7);
2972        assert_eq!(results.len(), 1);
2973        assert_eq!(results[0].pty_id, 42);
2974        assert_eq!(results[0].score, 1234);
2975        assert_eq!(results[0].primary_source, 1);
2976        assert_eq!(results[0].matched_sources, 0b111);
2977        assert_eq!(results[0].scroll_offset, Some(9));
2978        assert_eq!(results[0].context, b"hello");
2979    }
2980
2981    // --- Tag tests ---
2982
2983    #[test]
2984    fn msg_create_no_tag_has_zero_tag_len() {
2985        let msg = msg_create(24, 80);
2986        assert_eq!(msg.len(), 7);
2987        assert_eq!(msg[0], C2S_CREATE);
2988        assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 24);
2989        assert_eq!(u16::from_le_bytes([msg[3], msg[4]]), 80);
2990        assert_eq!(u16::from_le_bytes([msg[5], msg[6]]), 0);
2991    }
2992
2993    #[test]
2994    fn msg_create_tagged_encodes_tag() {
2995        let msg = msg_create_tagged(24, 80, "my-pty");
2996        assert_eq!(msg[0], C2S_CREATE);
2997        let tag_len = u16::from_le_bytes([msg[5], msg[6]]) as usize;
2998        assert_eq!(tag_len, 6);
2999        assert_eq!(&msg[7..7 + tag_len], b"my-pty");
3000        assert_eq!(msg.len(), 7 + tag_len);
3001    }
3002
3003    #[test]
3004    fn msg_create_tagged_command_encodes_both() {
3005        let msg = msg_create_tagged_command(30, 120, "editor", "vim");
3006        let tag_len = u16::from_le_bytes([msg[5], msg[6]]) as usize;
3007        assert_eq!(tag_len, 6);
3008        assert_eq!(&msg[7..13], b"editor");
3009        assert_eq!(&msg[13..], b"vim");
3010    }
3011
3012    #[test]
3013    fn msg_create_command_has_empty_tag() {
3014        let msg = msg_create_command(24, 80, "ls");
3015        let tag_len = u16::from_le_bytes([msg[5], msg[6]]) as usize;
3016        assert_eq!(tag_len, 0);
3017        assert_eq!(&msg[7..], b"ls");
3018    }
3019
3020    #[test]
3021    fn msg_create_tagged_empty_tag() {
3022        let msg = msg_create_tagged(24, 80, "");
3023        assert_eq!(msg.len(), 7);
3024        assert_eq!(u16::from_le_bytes([msg[5], msg[6]]), 0);
3025    }
3026
3027    #[test]
3028    fn msg_create_tagged_unicode_tag() {
3029        let msg = msg_create_tagged(24, 80, "日本語");
3030        let tag_len = u16::from_le_bytes([msg[5], msg[6]]) as usize;
3031        assert_eq!(tag_len, "日本語".len());
3032        assert_eq!(std::str::from_utf8(&msg[7..7 + tag_len]).unwrap(), "日本語");
3033    }
3034
3035    #[test]
3036    fn parse_created_with_tag() {
3037        let mut wire = vec![S2C_CREATED, 0x05, 0x00];
3038        wire.extend_from_slice(b"hello");
3039        let msg = parse_server_msg(&wire).unwrap();
3040        match msg {
3041            ServerMsg::Created { pty_id, tag } => {
3042                assert_eq!(pty_id, 5);
3043                assert_eq!(tag, "hello");
3044            }
3045            _ => panic!("expected Created"),
3046        }
3047    }
3048
3049    #[test]
3050    fn parse_created_without_tag() {
3051        let wire = vec![S2C_CREATED, 0x03, 0x00];
3052        let msg = parse_server_msg(&wire).unwrap();
3053        match msg {
3054            ServerMsg::Created { pty_id, tag } => {
3055                assert_eq!(pty_id, 3);
3056                assert_eq!(tag, "");
3057            }
3058            _ => panic!("expected Created"),
3059        }
3060    }
3061
3062    #[test]
3063    fn parse_created_n_with_tag() {
3064        let mut wire = vec![S2C_CREATED_N, 0x2a, 0x00, 0x05, 0x00];
3065        wire.extend_from_slice(b"hello");
3066        let msg = parse_server_msg(&wire).unwrap();
3067        match msg {
3068            ServerMsg::CreatedN { nonce, pty_id, tag } => {
3069                assert_eq!(nonce, 42);
3070                assert_eq!(pty_id, 5);
3071                assert_eq!(tag, "hello");
3072            }
3073            _ => panic!("expected CreatedN"),
3074        }
3075    }
3076
3077    #[test]
3078    fn msg_create_n_format() {
3079        let msg = msg_create_n(42, 24, 80, "test");
3080        assert_eq!(msg[0], C2S_CREATE_N);
3081        assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 42);
3082        assert_eq!(u16::from_le_bytes([msg[3], msg[4]]), 24);
3083        assert_eq!(u16::from_le_bytes([msg[5], msg[6]]), 80);
3084        assert_eq!(u16::from_le_bytes([msg[7], msg[8]]), 4);
3085        assert_eq!(&msg[9..], b"test");
3086    }
3087
3088    #[test]
3089    fn msg_create_n_command_format() {
3090        let msg = msg_create_n_command(7, 30, 120, "bg", "make build");
3091        assert_eq!(msg[0], C2S_CREATE_N);
3092        assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 7);
3093        assert_eq!(u16::from_le_bytes([msg[3], msg[4]]), 30);
3094        assert_eq!(u16::from_le_bytes([msg[5], msg[6]]), 120);
3095        let tag_len = u16::from_le_bytes([msg[7], msg[8]]) as usize;
3096        assert_eq!(tag_len, 2);
3097        assert_eq!(&msg[9..9 + tag_len], b"bg");
3098        assert_eq!(&msg[9 + tag_len..], b"make build");
3099    }
3100
3101    #[test]
3102    fn parse_list_with_tags() {
3103        // 2 entries: id=1 tag="ab", id=2 tag=""
3104        let mut wire = vec![S2C_LIST, 0x02, 0x00];
3105        // entry 1: id=1, tag_len=2, tag="ab", cmd_len=0
3106        wire.extend_from_slice(&1u16.to_le_bytes());
3107        wire.extend_from_slice(&2u16.to_le_bytes());
3108        wire.extend_from_slice(b"ab");
3109        wire.extend_from_slice(&0u16.to_le_bytes());
3110        // entry 2: id=2, tag_len=0, cmd_len=0
3111        wire.extend_from_slice(&2u16.to_le_bytes());
3112        wire.extend_from_slice(&0u16.to_le_bytes());
3113        wire.extend_from_slice(&0u16.to_le_bytes());
3114
3115        let msg = parse_server_msg(&wire).unwrap();
3116        match msg {
3117            ServerMsg::List { entries } => {
3118                assert_eq!(entries.len(), 2);
3119                assert_eq!(entries[0].pty_id, 1);
3120                assert_eq!(entries[0].tag, "ab");
3121                assert_eq!(entries[1].pty_id, 2);
3122                assert_eq!(entries[1].tag, "");
3123            }
3124            _ => panic!("expected List"),
3125        }
3126    }
3127
3128    #[test]
3129    fn parse_list_empty() {
3130        let wire = vec![S2C_LIST, 0x00, 0x00];
3131        let msg = parse_server_msg(&wire).unwrap();
3132        match msg {
3133            ServerMsg::List { entries } => assert_eq!(entries.len(), 0),
3134            _ => panic!("expected List"),
3135        }
3136    }
3137
3138    #[test]
3139    fn parse_list_truncated_gracefully() {
3140        // count=2 but only 1 complete entry
3141        let mut wire = vec![S2C_LIST, 0x02, 0x00];
3142        wire.extend_from_slice(&1u16.to_le_bytes());
3143        wire.extend_from_slice(&0u16.to_le_bytes());
3144        // missing second entry
3145        let msg = parse_server_msg(&wire).unwrap();
3146        match msg {
3147            ServerMsg::List { entries } => assert_eq!(entries.len(), 1),
3148            _ => panic!("expected List"),
3149        }
3150    }
3151
3152    #[test]
3153    fn parse_list_with_long_tags() {
3154        let long_tag = "a".repeat(300);
3155        let mut wire = vec![S2C_LIST, 0x01, 0x00];
3156        wire.extend_from_slice(&42u16.to_le_bytes());
3157        wire.extend_from_slice(&(long_tag.len() as u16).to_le_bytes());
3158        wire.extend_from_slice(long_tag.as_bytes());
3159
3160        let msg = parse_server_msg(&wire).unwrap();
3161        match msg {
3162            ServerMsg::List { entries } => {
3163                assert_eq!(entries.len(), 1);
3164                assert_eq!(entries[0].pty_id, 42);
3165                assert_eq!(entries[0].tag, long_tag);
3166            }
3167            _ => panic!("expected List"),
3168        }
3169    }
3170
3171    #[test]
3172    fn create_and_created_tag_round_trip() {
3173        // Simulate: client sends create with tag, server echoes tag in created
3174        let create_msg = msg_create_tagged(24, 80, "my-session");
3175        let tag_len = u16::from_le_bytes([create_msg[5], create_msg[6]]) as usize;
3176        let tag = std::str::from_utf8(&create_msg[7..7 + tag_len]).unwrap();
3177
3178        // Server builds S2C_CREATED with the tag
3179        let mut created_wire = vec![S2C_CREATED, 0x07, 0x00]; // pty_id = 7
3180        created_wire.extend_from_slice(tag.as_bytes());
3181
3182        let msg = parse_server_msg(&created_wire).unwrap();
3183        match msg {
3184            ServerMsg::Created {
3185                pty_id,
3186                tag: parsed_tag,
3187            } => {
3188                assert_eq!(pty_id, 7);
3189                assert_eq!(parsed_tag, "my-session");
3190            }
3191            _ => panic!("expected Created"),
3192        }
3193    }
3194
3195    // --- FrameState tests ---
3196
3197    #[test]
3198    fn frame_state_accessors() {
3199        let mut f = FrameState::new(4, 10);
3200        assert_eq!(f.rows(), 4);
3201        assert_eq!(f.cols(), 10);
3202        assert_eq!(f.cursor_row(), 0);
3203        assert_eq!(f.cursor_col(), 0);
3204        assert_eq!(f.mode(), 0);
3205        assert_eq!(f.title(), "");
3206        assert_eq!(f.cells().len(), 4 * 10 * CELL_SIZE);
3207        assert_eq!(f.cells_mut().len(), 4 * 10 * CELL_SIZE);
3208        assert!(f.overflow().is_empty());
3209        assert!(f.overflow_mut().is_empty());
3210    }
3211
3212    #[test]
3213    fn frame_state_from_parts() {
3214        let cells = vec![0u8; 2 * 4 * CELL_SIZE];
3215        let f = FrameState::from_parts(2, 4, 1, 3, 0x0F, "hello", cells.clone());
3216        assert_eq!(f.rows(), 2);
3217        assert_eq!(f.cols(), 4);
3218        assert_eq!(f.cursor_row(), 1);
3219        assert_eq!(f.cursor_col(), 3);
3220        assert_eq!(f.mode(), 0x0F);
3221        assert_eq!(f.title(), "hello");
3222        assert_eq!(f.cells(), &cells[..]);
3223    }
3224
3225    #[test]
3226    fn frame_state_from_parts_wrong_size() {
3227        // cells with wrong size should be ignored (stays zeroed)
3228        let cells = vec![0u8; 10]; // wrong size
3229        let f = FrameState::from_parts(2, 4, 0, 0, 0, "", cells);
3230        assert_eq!(f.cells().len(), 2 * 4 * CELL_SIZE);
3231    }
3232
3233    #[test]
3234    fn frame_state_resize() {
3235        let mut f = FrameState::new(4, 10);
3236        f.set_cursor(3, 9);
3237        f.resize(2, 5);
3238        assert_eq!(f.rows(), 2);
3239        assert_eq!(f.cols(), 5);
3240        assert_eq!(f.cursor_row(), 1); // clamped
3241        assert_eq!(f.cursor_col(), 4); // clamped
3242        assert_eq!(f.cells().len(), 2 * 5 * CELL_SIZE);
3243    }
3244
3245    #[test]
3246    fn frame_state_resize_noop() {
3247        let mut f = FrameState::new(4, 10);
3248        let ptr_before = f.cells().as_ptr();
3249        f.resize(4, 10); // same size
3250        let ptr_after = f.cells().as_ptr();
3251        assert_eq!(ptr_before, ptr_after); // no realloc
3252    }
3253
3254    #[test]
3255    fn frame_state_set_cursor_clamps() {
3256        let mut f = FrameState::new(4, 10);
3257        f.set_cursor(100, 200);
3258        assert_eq!(f.cursor_row(), 3);
3259        assert_eq!(f.cursor_col(), 9);
3260    }
3261
3262    #[test]
3263    fn frame_state_set_title() {
3264        let mut f = FrameState::new(2, 2);
3265        assert!(f.set_title("new title"));
3266        assert_eq!(f.title(), "new title");
3267        assert!(!f.set_title("new title")); // same title returns false
3268        assert!(f.set_title("other"));
3269    }
3270
3271    #[test]
3272    fn frame_state_get_text_and_write_text() {
3273        let mut f = FrameState::new(2, 10);
3274        f.write_text(0, 0, "Hello", CellStyle::default());
3275        f.write_text(1, 0, "World", CellStyle::default());
3276        let text = f.get_text(0, 0, 1, 9);
3277        assert!(text.contains("Hello"));
3278        assert!(text.contains("World"));
3279        let all = f.get_all_text();
3280        assert!(all.contains("Hello"));
3281    }
3282
3283    #[test]
3284    fn frame_state_get_text_empty() {
3285        let f = FrameState::new(0, 0);
3286        assert_eq!(f.get_text(0, 0, 0, 0), "");
3287        assert_eq!(f.get_all_text(), "");
3288    }
3289
3290    #[test]
3291    fn frame_state_get_cell() {
3292        let f = FrameState::new(2, 4);
3293        let cell = f.get_cell(0, 0);
3294        assert_eq!(cell.len(), CELL_SIZE);
3295        // Out of bounds
3296        assert!(f.get_cell(100, 100).is_empty());
3297    }
3298
3299    #[test]
3300    fn frame_state_cell_content_blank() {
3301        let f = FrameState::new(2, 4);
3302        assert_eq!(f.cell_content(0, 0), " "); // blank cell
3303        assert_eq!(f.cell_content(100, 0), ""); // out of bounds
3304    }
3305
3306    #[test]
3307    fn frame_state_cell_content_with_text() {
3308        let mut f = FrameState::new(2, 10);
3309        f.write_text(0, 0, "A", CellStyle::default());
3310        assert_eq!(f.cell_content(0, 0), "A");
3311    }
3312
3313    #[test]
3314    fn frame_state_fill_rect() {
3315        let mut f = FrameState::new(4, 10);
3316        f.fill_rect(Rect::new(0, 0, 2, 5), 'X', CellStyle::default());
3317        assert_eq!(f.cell_content(0, 0), "X");
3318        assert_eq!(f.cell_content(1, 4), "X");
3319        assert_eq!(f.cell_content(2, 0), " "); // outside rect
3320    }
3321
3322    #[test]
3323    fn frame_state_wrapped_text() {
3324        let mut f = FrameState::new(4, 10);
3325        let lines =
3326            f.write_wrapped_text(Rect::new(0, 0, 4, 5), "hello world", CellStyle::default());
3327        assert!(lines >= 2); // "hello world" wraps at width 5
3328    }
3329
3330    #[test]
3331    fn frame_state_wrapped_text_empty_rect() {
3332        let mut f = FrameState::new(4, 10);
3333        assert_eq!(
3334            f.write_wrapped_text(Rect::new(0, 0, 0, 0), "hi", CellStyle::default()),
3335            0
3336        );
3337    }
3338
3339    #[test]
3340    fn frame_state_scrolling_text() {
3341        let mut f = FrameState::new(4, 10);
3342        f.write_scrolling_text(
3343            Rect::new(0, 0, 3, 10),
3344            &["line1", "line2", "line3", "line4"],
3345            0,
3346            CellStyle::default(),
3347        );
3348        // Last 3 lines visible with offset_from_bottom=0
3349        assert_eq!(f.cell_content(0, 0), "l"); // "line2"
3350    }
3351
3352    #[test]
3353    fn frame_state_scrolling_text_empty_rect() {
3354        let mut f = FrameState::new(4, 10);
3355        f.write_scrolling_text(Rect::new(0, 0, 0, 0), &["hi"], 0, CellStyle::default());
3356        // Should not panic
3357    }
3358
3359    #[test]
3360    fn frame_state_clear() {
3361        let mut f = FrameState::new(2, 4);
3362        f.write_text(0, 0, "AB", CellStyle::default());
3363        f.clear(CellStyle::default());
3364        assert_eq!(f.cell_content(0, 0), " ");
3365    }
3366
3367    // --- TerminalState tests ---
3368
3369    #[test]
3370    fn terminal_state_accessors() {
3371        let t = TerminalState::new(24, 80);
3372        assert_eq!(t.rows(), 24);
3373        assert_eq!(t.cols(), 80);
3374        assert_eq!(t.cursor_row(), 0);
3375        assert_eq!(t.cursor_col(), 0);
3376        assert_eq!(t.mode(), 0);
3377        assert_eq!(t.title(), "");
3378        assert_eq!(t.cells().len(), 24 * 80 * CELL_SIZE);
3379        assert_eq!(t.frame().rows(), 24);
3380    }
3381
3382    #[test]
3383    fn terminal_state_mutators() {
3384        let mut t = TerminalState::new(4, 10);
3385        t.frame_mut().set_title("test");
3386        assert_eq!(t.title(), "test");
3387    }
3388
3389    #[test]
3390    fn terminal_state_set_title() {
3391        let mut t = TerminalState::new(4, 10);
3392        assert!(t.frame_mut().set_title("hello"));
3393        assert_eq!(t.title(), "hello");
3394        assert!(!t.frame_mut().set_title("hello")); // same
3395    }
3396
3397    #[test]
3398    fn terminal_state_get_text() {
3399        let t = TerminalState::new(2, 10);
3400        let text = t.get_text(0, 0, 0, 9);
3401        assert!(text.is_empty() || text.chars().all(|c| c == ' ' || c == '\n'));
3402        assert!(t.get_cell(0, 0).len() == CELL_SIZE);
3403        assert!(t.get_cell(100, 100).is_empty());
3404    }
3405
3406    #[test]
3407    fn terminal_state_resize() {
3408        let mut t = TerminalState::new(4, 10);
3409        t.frame_mut().resize(2, 5);
3410        // Note: TerminalState.dirty isn't updated by frame_mut().resize()
3411        // directly — that happens through feed_compressed. So just check frame.
3412        assert_eq!(t.rows(), 2);
3413        assert_eq!(t.cols(), 5);
3414    }
3415
3416    #[test]
3417    fn terminal_state_feed_compressed_invalid() {
3418        let mut t = TerminalState::new(4, 10);
3419        assert!(!t.feed_compressed(b"garbage"));
3420        assert!(!t.feed_compressed(&[]));
3421    }
3422
3423    #[test]
3424    fn terminal_state_feed_compressed_batch_empty() {
3425        let mut t = TerminalState::new(4, 10);
3426        assert!(!t.feed_compressed_batch(&[]));
3427    }
3428
3429    #[test]
3430    fn terminal_state_feed_compressed_batch_truncated() {
3431        let mut t = TerminalState::new(4, 10);
3432        // Length header says 100 bytes but only 4 bytes present
3433        let batch = &[100, 0, 0, 0];
3434        assert!(!t.feed_compressed_batch(batch));
3435    }
3436
3437    // --- Client message builder tests ---
3438
3439    #[test]
3440    fn msg_input_format() {
3441        let msg = msg_input(5, b"hello");
3442        assert_eq!(msg[0], C2S_INPUT);
3443        assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 5);
3444        assert_eq!(&msg[3..], b"hello");
3445    }
3446
3447    #[test]
3448    fn msg_resize_format() {
3449        let msg = msg_resize(3, 24, 80);
3450        assert_eq!(msg[0], C2S_RESIZE);
3451        assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 3);
3452        assert_eq!(u16::from_le_bytes([msg[3], msg[4]]), 24);
3453        assert_eq!(u16::from_le_bytes([msg[5], msg[6]]), 80);
3454    }
3455
3456    #[test]
3457    fn msg_resize_batch_format() {
3458        let msg = msg_resize_batch(&[(3, 24, 80), (5, 40, 120)]);
3459        assert_eq!(msg[0], C2S_RESIZE);
3460        assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 3);
3461        assert_eq!(u16::from_le_bytes([msg[3], msg[4]]), 24);
3462        assert_eq!(u16::from_le_bytes([msg[5], msg[6]]), 80);
3463        assert_eq!(u16::from_le_bytes([msg[7], msg[8]]), 5);
3464        assert_eq!(u16::from_le_bytes([msg[9], msg[10]]), 40);
3465        assert_eq!(u16::from_le_bytes([msg[11], msg[12]]), 120);
3466    }
3467
3468    #[test]
3469    fn msg_focus_format() {
3470        let msg = msg_focus(7);
3471        assert_eq!(msg[0], C2S_FOCUS);
3472        assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 7);
3473        assert_eq!(msg.len(), 3);
3474    }
3475
3476    #[test]
3477    fn msg_close_format() {
3478        let msg = msg_close(9);
3479        assert_eq!(msg[0], C2S_CLOSE);
3480        assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 9);
3481    }
3482
3483    #[test]
3484    fn msg_subscribe_unsubscribe_format() {
3485        let sub = msg_subscribe(1);
3486        assert_eq!(sub[0], C2S_SUBSCRIBE);
3487        assert_eq!(u16::from_le_bytes([sub[1], sub[2]]), 1);
3488
3489        let unsub = msg_unsubscribe(2);
3490        assert_eq!(unsub[0], C2S_UNSUBSCRIBE);
3491        assert_eq!(u16::from_le_bytes([unsub[1], unsub[2]]), 2);
3492    }
3493
3494    #[test]
3495    fn msg_search_format() {
3496        let msg = msg_search(42, "test query");
3497        assert_eq!(msg[0], C2S_SEARCH);
3498        assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 42);
3499        assert_eq!(&msg[3..], b"test query");
3500    }
3501
3502    #[test]
3503    fn msg_ack_format() {
3504        let msg = msg_ack();
3505        assert_eq!(msg, vec![C2S_ACK]);
3506    }
3507
3508    #[test]
3509    fn msg_scroll_format() {
3510        let msg = msg_scroll(5, 1000);
3511        assert_eq!(msg[0], C2S_SCROLL);
3512        assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 5);
3513        assert_eq!(u32::from_le_bytes([msg[3], msg[4], msg[5], msg[6]]), 1000);
3514    }
3515
3516    #[test]
3517    fn msg_display_rate_format() {
3518        let msg = msg_display_rate(120);
3519        assert_eq!(msg[0], C2S_DISPLAY_RATE);
3520        assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 120);
3521    }
3522
3523    #[test]
3524    fn msg_client_metrics_format() {
3525        let msg = msg_client_metrics(3, 5, 100);
3526        assert_eq!(msg[0], C2S_CLIENT_METRICS);
3527        assert_eq!(u16::from_le_bytes([msg[1], msg[2]]), 3);
3528        assert_eq!(u16::from_le_bytes([msg[3], msg[4]]), 5);
3529        assert_eq!(u16::from_le_bytes([msg[5], msg[6]]), 100);
3530    }
3531
3532    // --- CallbackRenderer tests ---
3533
3534    #[test]
3535    fn callback_renderer_resize() {
3536        let mut r = CallbackRenderer::new(2, 8);
3537        assert_eq!(r.frame().rows(), 2);
3538        r.resize(4, 16);
3539        assert_eq!(r.frame().rows(), 4);
3540        assert_eq!(r.frame().cols(), 16);
3541    }
3542
3543    #[test]
3544    fn callback_renderer_fill() {
3545        let mut r = CallbackRenderer::new(4, 10);
3546        r.render(|dom| {
3547            dom.fill(Rect::new(0, 0, 2, 5), '#', CellStyle::default());
3548        });
3549        assert_eq!(r.frame().cell_content(0, 0), "#");
3550        assert_eq!(r.frame().cell_content(1, 4), "#");
3551    }
3552
3553    #[test]
3554    fn callback_renderer_text() {
3555        let mut r = CallbackRenderer::new(4, 20);
3556        r.render(|dom| {
3557            dom.text(0, 0, "Hello", CellStyle::default());
3558        });
3559        assert_eq!(r.frame().cell_content(0, 0), "H");
3560        assert_eq!(r.frame().cell_content(0, 4), "o");
3561    }
3562
3563    #[test]
3564    fn callback_renderer_set_title() {
3565        let mut r = CallbackRenderer::new(2, 8);
3566        r.render(|dom| {
3567            dom.set_title("my title");
3568        });
3569        assert_eq!(r.frame().title(), "my title");
3570    }
3571
3572    #[test]
3573    fn callback_renderer_set_background() {
3574        let mut r = CallbackRenderer::new(2, 4);
3575        let style = CellStyle {
3576            bg: Color::Rgb(255, 0, 0),
3577            ..CellStyle::default()
3578        };
3579        r.render(|dom| {
3580            dom.set_background(style);
3581        });
3582        // Background fill should have been applied to all cells
3583        assert_eq!(r.frame().cells().len(), 2 * 4 * CELL_SIZE);
3584    }
3585
3586    #[test]
3587    fn callback_renderer_scrolling_text() {
3588        let mut r = CallbackRenderer::new(4, 20);
3589        r.render(|dom| {
3590            dom.scrolling_text(
3591                Rect::new(0, 0, 3, 20),
3592                ["a", "b", "c", "d", "e"].map(String::from),
3593                0,
3594                CellStyle::default(),
3595            );
3596        });
3597        // Should show the last 3 lines
3598        assert_eq!(r.frame().cell_content(0, 0), "c");
3599    }
3600
3601    // --- parse_server_msg edge cases ---
3602
3603    #[test]
3604    fn parse_empty_returns_none() {
3605        assert!(parse_server_msg(&[]).is_none());
3606    }
3607
3608    #[test]
3609    fn parse_unknown_type_returns_none() {
3610        assert!(parse_server_msg(&[0xFF, 0x00, 0x00]).is_none());
3611    }
3612
3613    #[test]
3614    fn parse_update_too_short() {
3615        assert!(parse_server_msg(&[S2C_UPDATE, 0x00]).is_none());
3616    }
3617
3618    #[test]
3619    fn parse_closed() {
3620        let msg = parse_server_msg(&[S2C_CLOSED, 0x05, 0x00]).unwrap();
3621        match msg {
3622            ServerMsg::Closed { pty_id } => assert_eq!(pty_id, 5),
3623            _ => panic!("expected Closed"),
3624        }
3625    }
3626
3627    #[test]
3628    fn parse_title() {
3629        let mut wire = vec![S2C_TITLE, 0x01, 0x00];
3630        wire.extend_from_slice(b"mytitle");
3631        let msg = parse_server_msg(&wire).unwrap();
3632        match msg {
3633            ServerMsg::Title { pty_id, title } => {
3634                assert_eq!(pty_id, 1);
3635                assert_eq!(title, b"mytitle");
3636            }
3637            _ => panic!("expected Title"),
3638        }
3639    }
3640
3641    // --- build_update_msg round-trip ---
3642
3643    #[test]
3644    fn build_update_msg_round_trip_with_resize() {
3645        let style = CellStyle::default();
3646        let mut prev = FrameState::new(2, 4);
3647        prev.write_text(0, 0, "AB", style);
3648
3649        let mut next = FrameState::new(3, 5); // different size
3650        next.write_text(0, 0, "XY", style);
3651        next.set_title("resized");
3652
3653        let msg = build_update_msg(1, &next, &prev).unwrap();
3654        assert!(!msg.is_empty());
3655
3656        // Apply to a terminal
3657        let mut t = TerminalState::new(2, 4);
3658        assert!(t.feed_compressed(&msg[3..])); // skip pty_id header
3659        assert_eq!(t.rows(), 3);
3660        assert_eq!(t.cols(), 5);
3661        assert_eq!(t.title(), "resized");
3662    }
3663
3664    #[test]
3665    fn build_update_msg_cursor_change() {
3666        let mut prev = FrameState::new(4, 10);
3667        prev.set_cursor(0, 0);
3668
3669        let mut next = prev.clone();
3670        next.set_cursor(2, 5);
3671
3672        let msg = build_update_msg(0, &next, &prev).unwrap();
3673
3674        let mut t = TerminalState::new(4, 10);
3675        assert!(t.feed_compressed(&msg[3..]));
3676        assert_eq!(t.cursor_row(), 2);
3677        assert_eq!(t.cursor_col(), 5);
3678    }
3679
3680    #[test]
3681    fn build_update_msg_mode_change() {
3682        let prev = FrameState::new(2, 4);
3683        let mut next = prev.clone();
3684        next.set_mode(0x0F);
3685
3686        let msg = build_update_msg(0, &next, &prev).unwrap();
3687        let mut t = TerminalState::new(2, 4);
3688        assert!(t.feed_compressed(&msg[3..]));
3689        assert_eq!(t.mode(), 0x0F);
3690    }
3691
3692    #[test]
3693    fn feed_compressed_batch_multiple_frames() {
3694        let style = CellStyle::default();
3695        let prev = FrameState::new(2, 4);
3696
3697        let mut mid = prev.clone();
3698        mid.write_text(0, 0, "AB", style);
3699        let msg1 = build_update_msg(0, &mid, &prev).unwrap();
3700
3701        let mut next = mid.clone();
3702        next.write_text(1, 0, "CD", style);
3703        let msg2 = build_update_msg(0, &next, &mid).unwrap();
3704
3705        // Build batch: [len1:4][compressed1][len2:4][compressed2]
3706        let payload1 = &msg1[3..];
3707        let payload2 = &msg2[3..];
3708        let mut batch = Vec::new();
3709        batch.extend_from_slice(&(payload1.len() as u32).to_le_bytes());
3710        batch.extend_from_slice(payload1);
3711        batch.extend_from_slice(&(payload2.len() as u32).to_le_bytes());
3712        batch.extend_from_slice(payload2);
3713
3714        let mut t = TerminalState::new(2, 4);
3715        assert!(t.feed_compressed_batch(&batch));
3716        let text = t.get_all_text();
3717        assert!(text.contains("AB"));
3718        assert!(text.contains("CD"));
3719    }
3720}