justerm_core/serialize.rs
1//! Issue #6 — binary, reference-based wire format for a damage frame.
2//!
3//! `encode` a [`Frame`] to bytes, `decode` them back; the round-trip is the
4//! contract. Reference-based (colour refs, Unicode scalars — never resolved RGB
5//! or atlas ids) so the engine stays theme- and font-agnostic; the consumer's
6//! adapter resolves references before handing cells to the renderer. Format spec
7//! and rationale: `docs/architecture.md` §Serialization + ADR-0005.
8
9use crate::cell::{Cell, CellFlags};
10use crate::color::Color;
11use crate::cursor::CursorShape;
12use crate::damage::ScrollOp;
13use crate::input::MouseEvents;
14use crate::selection::SelectionSpan;
15use core::num::NonZeroU32;
16use std::collections::BTreeMap;
17
18/// Wire magic ("juSTerm") + format version. A new feature bumps `VERSION`.
19const MAGIC: [u8; 2] = *b"JT";
20const VERSION: u8 = 11; // v11 adds a fourth overlay group: every live marker's absolute buffer line for the overview ruler (#120 S3); v10 adds a marker kind discriminant + optional i32 exit to the overlay marker group (#159); v9 adds the alt-screen flag in the header (#149); v8 adds the mouse wanted-events mask in the header (#129/ADR-0016); v7 overlay marker group (#118/ADR-0015); v6 overlay selection + search-match spans (#108/ADR-0014); v5 scroll position (#112/ADR-0013); v4 cursor shape+blink (#81); v3 cursor row/col/visibility (#38)
21
22/// The wire-format version (the gating `VERSION` byte), exposed so a binding can
23/// assert at load that its decoder matches the backend encoder (#34/ADR-0008).
24pub const WIRE_VERSION: u8 = VERSION;
25
26/// Whether a frame redraws everything or just its spans.
27#[derive(Clone, Copy, PartialEq, Eq, Debug)]
28pub enum FrameKind {
29 /// Every row is present (resize / alt-screen clear).
30 Full,
31 /// Only the listed spans changed since the consumer's ack.
32 Partial,
33}
34
35/// A damaged column run on one line, with its cells.
36///
37/// `combining` and `links` map a span-relative column to its frame-local index
38/// (1-based) — `combining` into [`Frame::side_table`], `links` into
39/// [`Frame::link_table`]. These are the per-cell `extra`/`link` references lifted
40/// out of the cell now that combining clusters (#45) and hyperlinks (#46) live in
41/// per-row maps. A column is present iff its cell carries the matching bit; on the
42/// wire they are the cell record's `extra`/`link` fields, so the bytes are
43/// unchanged.
44#[derive(Clone, PartialEq, Eq, Debug)]
45pub struct Span {
46 pub line: u16,
47 pub left: u16,
48 pub right: u16,
49 pub cells: Vec<Cell>,
50 pub combining: BTreeMap<usize, NonZeroU32>,
51 pub links: BTreeMap<usize, NonZeroU32>,
52}
53
54/// A stable handle to a buffer line, handed out by `Engine::add_marker` (#118).
55/// Monotonic per engine. The consumer attaches a decoration to the id; the frame
56/// reports where the marker currently sits, and `TermEvent::MarkerDisposed`
57/// signals when its line has left the buffer.
58#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
59pub struct MarkerId(pub u32);
60
61/// What a marker means (#158). A plain `add_marker` decoration carries no
62/// semantics ([`MarkerKind::Plain`]); OSC 133 shell-integration marks carry the
63/// command-boundary role (prompt/command/output start, or command finished with
64/// its optional exit code). The engine only *parses and anchors* these — the
65/// success/failure colour, earcon and prompt-to-prompt navigation are consumer
66/// policy (ADR-0017), driven off the kind + exit the wire (#159) carries.
67#[derive(Clone, Copy, PartialEq, Eq, Debug)]
68pub enum MarkerKind {
69 /// A `add_marker` decoration anchor (#118) — no OSC-133 semantics.
70 Plain,
71 /// OSC `133;A` — the shell prompt begins here.
72 PromptStart,
73 /// OSC `133;B` — the typed command begins here (the prompt ended).
74 CommandStart,
75 /// OSC `133;C` — the command was submitted; its output begins here.
76 OutputStart,
77 /// OSC `133;D[;exit]` — the command finished, with its exit code if reported
78 /// (absent, empty or non-numeric → `None`).
79 CommandFinished(Option<i32>),
80}
81
82/// A marker projected onto the viewport (#118): its id, the row it sits on, and
83/// its kind (#159). Only markers visible in the current viewport are reported; an
84/// off-screen marker is omitted but still alive (death comes via `MarkerDisposed`,
85/// not absence — so the consumer can tell "scrolled away" from "gone"). The kind
86/// carries the OSC 133 command-boundary role + exit code so the consumer can drive
87/// prompt-to-prompt navigation and success/fail signals (#160).
88#[derive(Clone, Copy, PartialEq, Eq, Debug)]
89pub struct MarkerPosition {
90 pub id: MarkerId,
91 pub row: usize,
92 pub kind: MarkerKind,
93}
94
95/// A marker's absolute buffer line (#120 S3, v11). Unlike [`MarkerPosition`],
96/// this is reported for EVERY live marker — on-screen or not — so a frame-mode
97/// consumer can place overview-ruler marks buffer-relatively (dividing by
98/// `scrollback + rows`), the whole point of a ruler being to show off-viewport
99/// anchors. The consumer joins `id` with its decoration registry; the ruler mark's
100/// colour is the consumer's (theme-agnostic), so no kind/exit rides here.
101#[derive(Clone, Copy, PartialEq, Eq, Debug)]
102pub struct MarkerLine {
103 pub id: MarkerId,
104 /// Absolute buffer line, in the same `[0, scrollback_len + rows)` frame the
105 /// header's `scrollback_len`/`display_offset` use.
106 pub line: u32,
107}
108
109/// Interaction overlays projected onto the viewport (#108): highlight spans the
110/// engine carries on the frame so a frame-mode consumer can paint them without
111/// an in-process model query. Positions only — highlight colour is the
112/// consumer's (theme-agnostic). Coordinates are viewport rows/cols, re-projected
113/// by `frame()` against the scroll offset so the engine stays the single
114/// anchoring authority.
115#[derive(Clone, PartialEq, Eq, Debug, Default)]
116pub struct Overlay {
117 /// The live selection projected onto visible rows (`selection_range`).
118 pub selection: Vec<SelectionSpan>,
119 /// The active search highlights projected onto visible rows. Search matches
120 /// are consumer-owned (next/prev navigation holds the `Vec<Match>`), so the
121 /// consumer hands the active set back via `set_search_highlights` and the
122 /// engine projects it here — mirroring how the engine-owned selection rides.
123 pub matches: Vec<SelectionSpan>,
124 /// Engine-owned markers visible in this viewport (#118): persistent line
125 /// anchors for decorations. Unlike the selection (cleared on a screen swap)
126 /// and search highlights (invalidated on output), markers re-anchor through
127 /// buffer mutation and survive an alt-screen excursion; only their viewport
128 /// position rides here.
129 pub markers: Vec<MarkerPosition>,
130 /// Every live marker's absolute buffer line (#120 S3, v11), on-screen or not —
131 /// the overview ruler needs off-viewport anchors, which `markers` (viewport-
132 /// only) can't supply. A superset of `markers` by id; different frame of
133 /// reference (absolute line, not viewport row).
134 pub marker_lines: Vec<MarkerLine>,
135}
136
137/// One serialized damage cycle: the decoded logical form that `encode`/`decode`
138/// round-trip. `side_table` holds this frame's grapheme clusters (referenced by
139/// each cell's frame-local `extra`); `link_table` holds its OSC 8 hyperlink URIs
140/// (referenced by each cell's frame-local `link`).
141#[derive(Clone, PartialEq, Eq, Debug)]
142pub struct Frame {
143 pub cols: u16,
144 pub rows: u16,
145 pub kind: FrameKind,
146 /// Cursor row/col in screen coordinates (0-based), and whether the engine
147 /// shows it (DECTCEM). Rides in the header because the cursor moves with
148 /// almost every frame (#38). *Drawing* the cursor — cell-invert / overlay —
149 /// stays the consumer's renderer adapter; the engine only reports state.
150 pub cursor_row: u16,
151 pub cursor_col: u16,
152 pub cursor_visible: bool,
153 /// The caret shape (DECSCUSR #89) and whether it blinks (att610 ?12, #81).
154 /// Reported for the renderer; drawing/animation stays the consumer's.
155 pub cursor_shape: CursorShape,
156 pub cursor_blink: bool,
157 /// Viewport scroll position (#112 / ADR-0013), for the consumer's scrollbar.
158 /// `display_offset` = lines scrolled up from the bottom (0 = following the
159 /// live screen); `scrollback_len` = history lines (total = `+ rows`). Ride in
160 /// the header like the cursor — per-frame viewport state, not cell content.
161 pub display_offset: u32,
162 pub scrollback_len: u32,
163 /// The mouse tracking mode as a *wanted-events* mask (#129): which mouse
164 /// event categories the app asked to receive, so the consumer routes an event
165 /// to the app (bit set) or keeps it local. `empty()` = no reporting. Rides the
166 /// header like the cursor — per-frame mode state the consumer reads, not cell
167 /// content. Positions/encoding never cross; the backend encodes via
168 /// `encode_mouse`.
169 pub mouse_events: MouseEvents,
170 /// Whether the alternate screen (`?1049`/`?47`) is active (#149). Buffer-global
171 /// state a frame-mode consumer can't derive from viewport damage — the
172 /// accessibility announce policy (#119) gates on it (suppress TUI repaints).
173 /// Rides the header like the cursor scalars (ADR-0014).
174 pub alt_screen: bool,
175 pub scroll: Option<ScrollOp>,
176 pub spans: Vec<Span>,
177 pub side_table: Vec<Vec<char>>,
178 pub link_table: Vec<String>,
179 /// Interaction overlays (selection/search highlights) for this viewport (#108).
180 pub overlay: Overlay,
181}
182
183/// Why a byte buffer could not be decoded into a [`Frame`].
184#[derive(Clone, Copy, PartialEq, Eq, Debug)]
185pub enum DecodeError {
186 /// Ran out of bytes mid-field.
187 Truncated,
188 /// First two bytes are not the wire magic.
189 BadMagic,
190 /// Unsupported format version.
191 BadVersion(u8),
192 /// A tag/kind byte held a value outside its defined set.
193 BadTag,
194 /// A span's `left` was past its `right` (would underflow the cell count).
195 BadSpan,
196}
197
198/// Serialize a frame to the binary wire format.
199pub fn encode(frame: &Frame) -> Vec<u8> {
200 let mut out = Vec::new();
201 out.extend_from_slice(&MAGIC);
202 out.push(VERSION);
203 out.push(frame.scroll.is_some() as u8);
204 out.push(match frame.kind {
205 FrameKind::Full => 0,
206 FrameKind::Partial => 1,
207 });
208 out.extend_from_slice(&frame.cols.to_le_bytes());
209 out.extend_from_slice(&frame.rows.to_le_bytes());
210 out.extend_from_slice(&frame.cursor_row.to_le_bytes());
211 out.extend_from_slice(&frame.cursor_col.to_le_bytes());
212 out.push(frame.cursor_visible as u8);
213 out.push(match frame.cursor_shape {
214 CursorShape::Block => 0,
215 CursorShape::Underline => 1,
216 CursorShape::Bar => 2,
217 });
218 out.push(frame.cursor_blink as u8);
219 out.extend_from_slice(&frame.display_offset.to_le_bytes());
220 out.extend_from_slice(&frame.scrollback_len.to_le_bytes());
221 // Mouse wanted-events mask (#129): one byte in the header, like the cursor
222 // scalars. Off = 0.
223 out.push(frame.mouse_events.bits());
224 // Alt-screen flag (#149): one byte in the header, like the cursor scalars.
225 out.push(frame.alt_screen as u8);
226 if let Some(s) = frame.scroll {
227 out.extend_from_slice(&(s.top as u16).to_le_bytes());
228 out.extend_from_slice(&(s.bottom as u16).to_le_bytes());
229 out.extend_from_slice(&(s.count as i16).to_le_bytes());
230 }
231 out.extend_from_slice(&(frame.spans.len() as u16).to_le_bytes());
232 for span in &frame.spans {
233 out.extend_from_slice(&span.line.to_le_bytes());
234 out.extend_from_slice(&span.left.to_le_bytes());
235 out.extend_from_slice(&span.right.to_le_bytes());
236 for (col, cell) in span.cells.iter().enumerate() {
237 // The grapheme and hyperlink indices now ride on the span (per
238 // column), not the cell.
239 let extra = span.combining.get(&col).map_or(0, |n| n.get() as u16);
240 let link = span.links.get(&col).map_or(0, |n| n.get() as u16);
241 out.extend_from_slice(&encode_cell_record(cell, extra, link));
242 }
243 }
244 out.extend_from_slice(&(frame.side_table.len() as u16).to_le_bytes());
245 for cluster in &frame.side_table {
246 out.extend_from_slice(&(cluster.len() as u16).to_le_bytes());
247 for &ch in cluster {
248 out.extend_from_slice(&(ch as u32).to_le_bytes());
249 }
250 }
251 // Hyperlink side-table: each URI as a length-prefixed UTF-8 byte run (#26).
252 out.extend_from_slice(&(frame.link_table.len() as u16).to_le_bytes());
253 for uri in &frame.link_table {
254 out.extend_from_slice(&(uri.len() as u16).to_le_bytes());
255 out.extend_from_slice(uri.as_bytes());
256 }
257 // Overlay section (#108): each group is a u16 count then that many
258 // `(row, left, right)` u16 viewport triples. Selection first, then search
259 // matches. Append-only, version-gated — a future group (markers, #118) adds
260 // a third count here at the next version bump.
261 encode_overlay_spans(&mut out, &frame.overlay.selection);
262 encode_overlay_spans(&mut out, &frame.overlay.matches);
263 // Third overlay group (#118): markers as `(id u32, row u16)` pairs — a
264 // different record shape from the span groups (a marker is a line anchor,
265 // not a column run). v10 (#159) appends a kind discriminant (u8, like
266 // `cursor_shape`), and — only for `CommandFinished` — a presence byte + i32
267 // exit code (the presence pattern mirrors the header's `scroll` option).
268 out.extend_from_slice(&(frame.overlay.markers.len() as u16).to_le_bytes());
269 for m in &frame.overlay.markers {
270 out.extend_from_slice(&m.id.0.to_le_bytes());
271 out.extend_from_slice(&(m.row as u16).to_le_bytes());
272 out.push(match m.kind {
273 MarkerKind::Plain => 0,
274 MarkerKind::PromptStart => 1,
275 MarkerKind::CommandStart => 2,
276 MarkerKind::OutputStart => 3,
277 MarkerKind::CommandFinished(_) => 4,
278 });
279 if let MarkerKind::CommandFinished(exit) = m.kind {
280 out.push(exit.is_some() as u8);
281 out.extend_from_slice(&exit.unwrap_or(0).to_le_bytes());
282 }
283 }
284 // Fourth overlay group (#120 S3, v11): every live marker's absolute buffer
285 // line as `(id u32, line u32)` pairs — a superset of the viewport marker group
286 // above, for placing overview-ruler marks off-viewport. Count-prefixed like the
287 // others.
288 out.extend_from_slice(&(frame.overlay.marker_lines.len() as u16).to_le_bytes());
289 for m in &frame.overlay.marker_lines {
290 out.extend_from_slice(&m.id.0.to_le_bytes());
291 out.extend_from_slice(&m.line.to_le_bytes());
292 }
293 out
294}
295
296/// Encode one overlay group: a u16 span count, then each span as three u16s
297/// (`row`, `left`, `right`) in viewport coordinates.
298fn encode_overlay_spans(out: &mut Vec<u8>, spans: &[SelectionSpan]) {
299 out.extend_from_slice(&(spans.len() as u16).to_le_bytes());
300 for s in spans {
301 out.extend_from_slice(&(s.row as u16).to_le_bytes());
302 out.extend_from_slice(&(s.left as u16).to_le_bytes());
303 out.extend_from_slice(&(s.right as u16).to_le_bytes());
304 }
305}
306
307/// Length in bytes of one fixed-width wire cell record (see
308/// [`encode_cell_record`]).
309pub const CELL_RECORD_LEN: usize = 18;
310
311/// Encode one [`Cell`] to its fixed 18-byte little-endian record:
312/// `c` u32 (Unicode scalar) · `fg` u32 · `bg` u32 · `flags` u16 · `extra` u16
313/// (frame-local grapheme index, 0 = none) · `link` u16 (frame-local hyperlink
314/// index, 0 = none). Width derives from `flags`.
315///
316/// `extra` and `link` are passed in rather than read from the cell: combining
317/// clusters (#45) and hyperlinks (#46) now live in per-row maps, so both indices
318/// ride on the [`Span`], not the cell. The wire bytes are unchanged.
319///
320/// This is the single definition of the cell record layout — [`encode`] writes
321/// it per span cell, and an alternate consumer (the WASM decoder, #34/ADR-0008)
322/// reuses it to lay decoded cells out flat without re-implementing the layout,
323/// so the two cannot drift.
324pub fn encode_cell_record(cell: &Cell, extra: u16, link: u16) -> [u8; CELL_RECORD_LEN] {
325 let mut r = [0u8; CELL_RECORD_LEN];
326 r[0..4].copy_from_slice(&(cell.c() as u32).to_le_bytes());
327 r[4..8].copy_from_slice(&encode_color(cell.fg()).to_le_bytes());
328 r[8..12].copy_from_slice(&encode_color(cell.bg()).to_le_bytes());
329 r[12..14].copy_from_slice(&cell.flags().bits().to_le_bytes());
330 r[14..16].copy_from_slice(&extra.to_le_bytes());
331 r[16..18].copy_from_slice(&link.to_le_bytes());
332 r
333}
334
335/// A colour reference as a tagged u32: high byte = tag
336/// (0 = Default, 1 = Indexed, 2 = Rgb), low 24 bits = payload. The tag is
337/// mandatory so `Default`, `Indexed(0)`, and `Rgb(0,0,0)` stay distinct.
338///
339/// Public so an alternate consumer (the WASM decoder's structure-of-arrays
340/// `fg`/`bg` columns, #35) reuses this single definition of the colour-ref
341/// encoding instead of re-implementing the tag packing — no drift.
342pub fn encode_color(c: Color) -> u32 {
343 match c {
344 Color::Default => 0,
345 Color::Indexed(i) => (1 << 24) | i as u32,
346 Color::Rgb(r, g, b) => (2 << 24) | (r as u32) << 16 | (g as u32) << 8 | b as u32,
347 }
348}
349
350/// Deserialize the binary wire format back into a [`Frame`].
351pub fn decode(bytes: &[u8]) -> Result<Frame, DecodeError> {
352 let mut r = Reader::new(bytes);
353 if r.take(2)? != MAGIC {
354 return Err(DecodeError::BadMagic);
355 }
356 let version = r.u8()?;
357 if version != VERSION {
358 return Err(DecodeError::BadVersion(version));
359 }
360 let has_scroll = r.u8()? != 0;
361 let kind = match r.u8()? {
362 0 => FrameKind::Full,
363 1 => FrameKind::Partial,
364 _ => return Err(DecodeError::BadTag),
365 };
366 let cols = r.u16()?;
367 let rows = r.u16()?;
368 let cursor_row = r.u16()?;
369 let cursor_col = r.u16()?;
370 let cursor_visible = r.u8()? != 0;
371 let cursor_shape = match r.u8()? {
372 0 => CursorShape::Block,
373 1 => CursorShape::Underline,
374 2 => CursorShape::Bar,
375 _ => return Err(DecodeError::BadTag),
376 };
377 let cursor_blink = r.u8()? != 0;
378 let display_offset = r.u32()?;
379 let scrollback_len = r.u32()?;
380 let mouse_events = MouseEvents::from_bits_retain(r.u8()?);
381 let alt_screen = r.u8()? != 0;
382 let scroll = if has_scroll {
383 let top = r.u16()? as usize;
384 let bottom = r.u16()? as usize;
385 let count = (r.u16()? as i16) as isize;
386 Some(ScrollOp { top, bottom, count })
387 } else {
388 None
389 };
390 let span_count = r.u16()?;
391 let mut spans = Vec::with_capacity(span_count as usize);
392 for _ in 0..span_count {
393 let line = r.u16()?;
394 let left = r.u16()?;
395 let right = r.u16()?;
396 if right < left {
397 return Err(DecodeError::BadSpan);
398 }
399 // Widen before the arithmetic: `right - left + 1` in `u16` overflows
400 // when `right == u16::MAX` (e.g. left=0, right=65535), panicking under
401 // overflow checks. `right >= left` is enforced just above, so the
402 // subtraction in `usize` cannot underflow.
403 let n = right as usize - left as usize + 1;
404 let mut cells = Vec::with_capacity(n);
405 let mut combining = BTreeMap::new();
406 let mut links = BTreeMap::new();
407 for col in 0..n {
408 let (cell, extra, link) = decode_cell(&mut r)?;
409 if let Some(idx) = NonZeroU32::new(extra as u32) {
410 combining.insert(col, idx);
411 }
412 if let Some(idx) = NonZeroU32::new(link as u32) {
413 links.insert(col, idx);
414 }
415 cells.push(cell);
416 }
417 spans.push(Span {
418 line,
419 left,
420 right,
421 cells,
422 combining,
423 links,
424 });
425 }
426 let side_table_count = r.u16()?;
427 let mut side_table = Vec::with_capacity(side_table_count as usize);
428 for _ in 0..side_table_count {
429 let len = r.u16()?;
430 let mut cluster = Vec::with_capacity(len as usize);
431 for _ in 0..len {
432 cluster.push(char::from_u32(r.u32()?).ok_or(DecodeError::BadTag)?);
433 }
434 side_table.push(cluster);
435 }
436 let link_count = r.u16()?;
437 let mut link_table = Vec::with_capacity(link_count as usize);
438 for _ in 0..link_count {
439 let len = r.u16()? as usize;
440 let bytes = r.take(len)?;
441 link_table.push(String::from_utf8_lossy(bytes).into_owned());
442 }
443 // Overlay section (#108): selection group then match group, each a count +
444 // `(row, left, right)` triples (inverse of `encode_overlay_spans`).
445 let selection = decode_overlay_spans(&mut r)?;
446 let matches = decode_overlay_spans(&mut r)?;
447 // Third group (#118): marker `(id u32, row u16)` records, each followed by a
448 // kind discriminant (v10, #159) and — for `CommandFinished` — a presence byte
449 // + i32 exit (inverse of the marker encode loop).
450 let marker_count = r.u16()?;
451 let mut markers = Vec::with_capacity(marker_count as usize);
452 for _ in 0..marker_count {
453 let id = MarkerId(r.u32()?);
454 let row = r.u16()? as usize;
455 let kind = match r.u8()? {
456 0 => MarkerKind::Plain,
457 1 => MarkerKind::PromptStart,
458 2 => MarkerKind::CommandStart,
459 3 => MarkerKind::OutputStart,
460 4 => {
461 // Always read presence + i32 (the encoder writes both); a 0
462 // presence means the exit bytes are padding to discard.
463 let present = r.u8()? != 0;
464 let exit = r.u32()? as i32;
465 MarkerKind::CommandFinished(present.then_some(exit))
466 }
467 _ => return Err(DecodeError::BadTag),
468 };
469 markers.push(MarkerPosition { id, row, kind });
470 }
471 // Fourth group (#120 S3, v11): every live marker's `(id u32, line u32)` — the
472 // absolute-line superset for the overview ruler (inverse of the encode loop).
473 let marker_line_count = r.u16()?;
474 let mut marker_lines = Vec::with_capacity(marker_line_count as usize);
475 for _ in 0..marker_line_count {
476 let id = MarkerId(r.u32()?);
477 let line = r.u32()?;
478 marker_lines.push(MarkerLine { id, line });
479 }
480 let overlay = Overlay {
481 selection,
482 matches,
483 markers,
484 marker_lines,
485 };
486 Ok(Frame {
487 cols,
488 rows,
489 kind,
490 cursor_row,
491 cursor_col,
492 cursor_visible,
493 cursor_shape,
494 cursor_blink,
495 display_offset,
496 scrollback_len,
497 mouse_events,
498 alt_screen,
499 scroll,
500 spans,
501 side_table,
502 link_table,
503 overlay,
504 })
505}
506
507/// Decode one overlay group: a u16 span count, then that many `(row, left,
508/// right)` u16 triples back into viewport [`SelectionSpan`]s (inverse of
509/// [`encode_overlay_spans`]).
510fn decode_overlay_spans(r: &mut Reader) -> Result<Vec<SelectionSpan>, DecodeError> {
511 let count = r.u16()?;
512 let mut spans = Vec::with_capacity(count as usize);
513 for _ in 0..count {
514 let row = r.u16()? as usize;
515 let left = r.u16()? as usize;
516 let right = r.u16()? as usize;
517 spans.push(SelectionSpan { row, left, right });
518 }
519 Ok(spans)
520}
521
522/// Decode one 18-byte cell record (inverse of [`encode_cell_record`]), returning
523/// the cell and its raw `extra` grapheme index and `link` index (0 = none). A
524/// non-zero index sets the corresponding presence bit; the caller records the
525/// indices on the span.
526fn decode_cell(r: &mut Reader) -> Result<(Cell, u16, u16), DecodeError> {
527 let c = char::from_u32(r.u32()?).ok_or(DecodeError::BadTag)?;
528 let fg = decode_color(r.u32()?)?;
529 let bg = decode_color(r.u32()?)?;
530 let flags = CellFlags::from_bits_retain(r.u16()?);
531 let extra = r.u16()?;
532 let link = r.u16()?;
533 let mut cell = Cell::from_parts(c, fg, bg, flags);
534 cell.set_combined(extra != 0);
535 cell.set_linked(link != 0);
536 Ok((cell, extra, link))
537}
538
539/// Decode a tagged-u32 colour reference (inverse of [`encode_color`]).
540fn decode_color(v: u32) -> Result<Color, DecodeError> {
541 let payload = v & 0x00FF_FFFF;
542 match v >> 24 {
543 0 => Ok(Color::Default),
544 1 => Ok(Color::Indexed(payload as u8)),
545 2 => Ok(Color::Rgb(
546 (payload >> 16) as u8,
547 (payload >> 8) as u8,
548 payload as u8,
549 )),
550 _ => Err(DecodeError::BadTag),
551 }
552}
553
554/// A little-endian cursor over the wire bytes.
555struct Reader<'a> {
556 bytes: &'a [u8],
557 pos: usize,
558}
559
560impl<'a> Reader<'a> {
561 fn new(bytes: &'a [u8]) -> Self {
562 Reader { bytes, pos: 0 }
563 }
564
565 fn take(&mut self, n: usize) -> Result<&'a [u8], DecodeError> {
566 let end = self.pos.checked_add(n).ok_or(DecodeError::Truncated)?;
567 let slice = self
568 .bytes
569 .get(self.pos..end)
570 .ok_or(DecodeError::Truncated)?;
571 self.pos = end;
572 Ok(slice)
573 }
574
575 fn u8(&mut self) -> Result<u8, DecodeError> {
576 Ok(self.take(1)?[0])
577 }
578
579 fn u16(&mut self) -> Result<u16, DecodeError> {
580 let b = self.take(2)?;
581 Ok(u16::from_le_bytes([b[0], b[1]]))
582 }
583
584 fn u32(&mut self) -> Result<u32, DecodeError> {
585 let b = self.take(4)?;
586 Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
587 }
588}