From 14341d958e17a779b91a397ff4f1ea9baaa582aa Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Wed, 20 May 2026 09:11:33 -0400 Subject: [PATCH 1/4] session 1 commit 1/4: workspace + identity types moved to pmacs-protocol MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Workspace skeleton: root Cargo.toml becomes a workspace with members [".", "pmacs-protocol"]; [workspace.dependencies] pins serde, postcard, thiserror so both crates use byte-identical versions (the wire format depends on it). pmacs main package keeps its existing shape (no file moves); it just gains pmacs-protocol as a path dependency. Identity types moved: BufferId (from buffer.rs), FrontendId + ByteRange (from protocol.rs), Position type alias (from rope.rs). All four are self-contained — no custom-type dependencies — so the first stage of the move can land atomically without dragging cell/message types along. src/buffer.rs / src/protocol.rs / src/rope.rs each gain a 'pub use pmacs_protocol::...' re-export for the moved names, so existing internal imports (crate::buffer::BufferId, crate::rope::Position, etc.) continue to resolve unchanged. New consumers (pmacs-gpu, debug tools) will depend on pmacs-protocol directly. One visibility change: BufferId::from_raw was pub(crate); promoted to pub with a doc note that it's not stable API for external consumers. The (crate) restriction was advisory only — external deserialization already worked via the derived Deserialize, so making it pub doesn't widen the actual surface, just makes it honest. Lib gate: 1314 passed, no regressions. Co-Authored-By: Claude Opus 4.7 (1M context) --- Cargo.lock | 10 +++++ Cargo.toml | 29 ++++++++++++-- pmacs-protocol/Cargo.toml | 46 ++++++++++++++++++++++ pmacs-protocol/src/ids.rs | 80 +++++++++++++++++++++++++++++++++++++++ pmacs-protocol/src/lib.rs | 43 +++++++++++++++++++++ src/buffer.rs | 41 +++----------------- src/protocol.rs | 35 +++++------------ src/rope.rs | 10 ++--- 8 files changed, 226 insertions(+), 68 deletions(-) create mode 100644 pmacs-protocol/Cargo.toml create mode 100644 pmacs-protocol/src/ids.rs create mode 100644 pmacs-protocol/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 9e307e3..7f4c9da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1249,6 +1249,7 @@ dependencies = [ "loro", "mlua", "nix 0.29.0", + "pmacs-protocol", "portable-pty", "postcard", "proptest", @@ -1272,6 +1273,15 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "pmacs-protocol" +version = "1.0.0" +dependencies = [ + "postcard", + "serde", + "thiserror 2.0.18", +] + [[package]] name = "portable-pty" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index 02294ef..5c4e2b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,19 @@ +[workspace] +# Session 1 of the pmacs-gpu arc: `pmacs-protocol` is the wire-types +# crate the post-v1.0 frontend (`pmacs-gpu`) will consume directly. The +# root `pmacs` package stays a workspace member (no file moves); the +# new crate lives under `pmacs-protocol/`. See +# `docs/pmacs-gpu-design.md`. +members = [".", "pmacs-protocol"] + +[workspace.dependencies] +# Shared between `pmacs` and `pmacs-protocol`. Pinned here so both +# crates use byte-identical postcard / serde versions — the wire +# format depends on it. +serde = { version = "1", features = ["derive"] } +postcard = { version = "1", features = ["use-std"] } +thiserror = "2" + [package] name = "pmacs" version = "1.0.0" @@ -60,7 +76,7 @@ crdt = ["dep:loro"] [dependencies] crossterm = "0.28" -thiserror = "2" +thiserror = { workspace = true } unicode-width = "0.2" # Work-stealing deque, MPMC channels, and parking primitives for the # M3 worker pool (spec §6.3). The umbrella crate re-exports @@ -70,8 +86,15 @@ crossbeam = "0.8" # trait, `rmp-serde` is the MessagePack codec the spec calls out by # name; in-process and out-of-process workers must look identical to # Lua, which means even the in-process bus encodes through MessagePack. -serde = { version = "1", features = ["derive"] } +serde = { workspace = true } rmp-serde = "1" +# Wire-types crate (session 1 of pmacs-gpu arc). Owns the +# `InstanceMessage` / `FrontendEvent` / capability / `SemanticFrame` +# family, plus the cell/buffer/rope wire types they reference. The +# `pmacs` crate re-exports through `crate::protocol`, `crate::cell`, +# `crate::buffer`, and `crate::rope` so existing internal imports keep +# working unchanged. +pmacs-protocol = { version = "1.0.0", path = "pmacs-protocol" } # Wire format for the M5 frontend ↔ instance protocol (T M5.5b). # Length-prefix framing wraps postcard-encoded payloads. Chosen for # compactness on the cell-stream traffic (60 Hz cell deltas dominate @@ -81,7 +104,7 @@ rmp-serde = "1" # `src/protocol.rs::Hello`. The worker-protocol encoding (§5.5 spec) # remains MessagePack via `rmp-serde`; different subsystems with # different requirements (compactness vs schema evolution). -postcard = { version = "1", features = ["use-std"] } +postcard = { workspace = true } # Signal handling for the M5.5 daemon (T M5.5e). Provides a safe # wrapper for installing handlers that set an AtomicBool flag, which # our accept loop polls between iterations. Used for SIGTERM/SIGINT diff --git a/pmacs-protocol/Cargo.toml b/pmacs-protocol/Cargo.toml new file mode 100644 index 0000000..bcad954 --- /dev/null +++ b/pmacs-protocol/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "pmacs-protocol" +version = "1.0.0" +edition = "2024" +rust-version = "1.95" +description = "Wire types for the pmacs daemon ↔ frontend protocol (the SemanticFrame family, capabilities, attach handshake)" +license = "MIT OR Apache-2.0" +authors = ["Pmacs contributors"] +readme = "../README.md" +repository = "https://git.levineuwirth.org/neuwirth/pmacs" +homepage = "https://levineuwirth.org/essays/pmacs" +keywords = ["editor", "emacs", "protocol", "ipc"] +categories = ["text-editors", "data-structures"] + +[lints.rust] +unsafe_code = "forbid" +missing_docs = "warn" + +[lints.clippy] +all = { level = "warn", priority = -1 } +pedantic = { level = "warn", priority = -1 } +cargo = { level = "warn", priority = -1 } +# Allows: same set as the root `pmacs` crate, mirrored so wire types +# moved here don't trip lints the originals didn't. +module_name_repetitions = "allow" +must_use_candidate = "allow" +missing_errors_doc = "allow" +missing_panics_doc = "allow" +cast_possible_truncation = "allow" +cast_sign_loss = "allow" +cast_precision_loss = "allow" +similar_names = "allow" +multiple_crate_versions = "allow" + +[features] +# Mirrors the `crdt` feature on the parent `pmacs` crate. When enabled, +# the `InstanceMessage::CrdtOp` / `FrontendEvent::CrdtOp` variants exist +# and the wire-level `CrdtOp` struct is in scope. When disabled, those +# variants are compiled out so v0.1-era binaries built without `crdt` +# match the pre-v1.0 wire surface byte-for-byte. +crdt = [] + +[dependencies] +serde = { workspace = true } +postcard = { workspace = true } +thiserror = { workspace = true } diff --git a/pmacs-protocol/src/ids.rs b/pmacs-protocol/src/ids.rs new file mode 100644 index 0000000..f4b2d0c --- /dev/null +++ b/pmacs-protocol/src/ids.rs @@ -0,0 +1,80 @@ +//! Identity and range types — moved from `pmacs::buffer`, +//! `pmacs::protocol`, and `pmacs::rope` in session 1 of the +//! `pmacs-gpu` arc. The originals re-export these names so internal +//! `pmacs` imports (`crate::buffer::BufferId`, `crate::rope::Position`, +//! etc.) keep working unchanged. + +use std::sync::atomic::{AtomicU64, Ordering}; + +/// Opaque, per-process identifier for a buffer. +/// +/// The internal representation is private (R22): callers cannot reach +/// for `.0`; construction goes through [`BufferId::next`]. +/// +/// T M10.5: `Serialize` / `Deserialize` derived so `BufferId` can be +/// the routing key on `InstanceMessage::CrdtOp` / `FrontendEvent::CrdtOp`. +/// The serialized form is the bare `u64` (transparent newtype). +#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, serde::Serialize, serde::Deserialize)] +pub struct BufferId(u64); + +impl BufferId { + /// Allocate a fresh [`BufferId`] from the process-wide counter. + /// + /// Threading: any thread. + #[must_use] + pub fn next() -> Self { + static COUNTER: AtomicU64 = AtomicU64::new(1); + Self(COUNTER.fetch_add(1, Ordering::Relaxed)) + } + + /// Inspect the raw value. Useful for logging and FFI. + #[must_use] + pub const fn raw(self) -> u64 { + self.0 + } + + /// Rebuild an ID from a raw value for crate-internal references that + /// persist an already-issued buffer identity in generated text. + /// + /// Was `pub(crate)` before the session-1 crate split; promoted to + /// `pub` to remain reachable from `pmacs` after the move. Not + /// stable API for external consumers — external callers should + /// either round-trip via `serde` or accept that the constructor + /// may change. + #[must_use] + pub const fn from_raw(raw: u64) -> Self { + Self(raw) + } +} + +/// Opaque identifier for a frontend attached to an instance. +/// +/// Every input event carries a `FrontendId`. v0.1 uses one ID per +/// instance ([`FrontendId::LOCAL`]); v0.3 generalizes to multi-frontend +/// (multi-window, multi-user) without a protocol break. +#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, serde::Serialize, serde::Deserialize)] +pub struct FrontendId(pub u64); + +impl FrontendId { + /// The single frontend used in v0.1's local-attach mode. + /// + /// Future multi-frontend deployments allocate IDs from a counter + /// starting after this value; the constant is reserved. + pub const LOCAL: FrontendId = FrontendId(1); +} + +/// Byte offset into a rope. Buffer-wide; cursor / selection / span +/// anchors all use this type. Type alias rather than newtype so +/// arithmetic on offsets (slice ranges, byte deltas) doesn't need +/// conversions. +pub type Position = u64; + +/// Half-open byte range `[start, end)` into a buffer's rope, matching +/// the rope's own range convention. +#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct ByteRange { + /// Inclusive start byte offset. + pub start: u64, + /// Exclusive end byte offset. + pub end: u64, +} diff --git a/pmacs-protocol/src/lib.rs b/pmacs-protocol/src/lib.rs new file mode 100644 index 0000000..68eb20b --- /dev/null +++ b/pmacs-protocol/src/lib.rs @@ -0,0 +1,43 @@ +//! Wire types for the pmacs daemon ↔ frontend protocol. +//! +//! Session 1 of the pmacs-gpu arc — see `docs/pmacs-gpu-design.md` in +//! the workspace root. This crate owns every type that appears on the +//! `InstanceMessage` / `FrontendEvent` wire so a future `pmacs-gpu` +//! frontend can depend on it directly without pulling in the `pmacs` +//! main crate (and its Lua / tree-sitter / process-supervisor surface). +//! +//! What lives here: +//! - The `SemanticFrame` family: `StyleSpans`, `Decorations`, +//! `InlineAdornments`, `BlockAdornments`, `FoldState`, +//! `ResourceOffer`, `FileStyleSummary`. +//! - The grid-rendering family: `CellDelta`, plus `Cell`, `Glyph`, +//! `Style`, `Color`, `UnderlineStyle`, `CellCoord`, `CellSize`, +//! `DiffSpan`, `Attachment`. +//! - Identity types: `BufferId`, `FrontendId`, `Position`, `ByteRange`. +//! - The full message envelopes: `InstanceMessage`, `FrontendEvent`, +//! `GoodbyeReason`, capability structs, `PresenceUpdate`, etc. +//! - The optional `CrdtOp` wire variant (feature-gated on `crdt`). +//! +//! What does NOT live here: +//! - `crate::cell::CellGrid` and `crate::cell::diff()` (rendering +//! helpers, not wire types — stay in the `pmacs` crate). +//! - `Buffer` / `BufferRegistry` / `Rope` / `Edit` / `Range` +//! (instance-side editor machinery). +//! - `AttachTarget` and the attach-CLI binding error types +//! (`pmacs`-binary-only logic; `pmacs-gpu` builds its own attach +//! client). +//! - Lua / tree-sitter / process-supervisor everything. +//! +//! The `pmacs` crate re-exports back through its existing module paths +//! (`crate::cell::Style`, `crate::buffer::BufferId`, etc.) so internal +//! pmacs code doesn't churn its imports. New consumers +//! (`pmacs-gpu`, debug tools, future ports) depend on this crate +//! directly. + +pub mod ids; + +pub use ids::{BufferId, ByteRange, FrontendId, Position}; + +// Cell wire types, the SemanticFrame family, top-level message +// envelopes, and the feature-gated `CrdtOp` follow in subsequent +// commits within this PR. diff --git a/src/buffer.rs b/src/buffer.rs index 5dfd44c..a145981 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -27,7 +27,6 @@ //! observe `&Buffer` while the buffer's own `&mut self` is held. use std::path::{Path, PathBuf}; -use std::sync::atomic::{AtomicU64, Ordering}; use crate::file_io::FileMeta; use crate::rope::{Edit, Position, Range, Rope, RopeError}; @@ -37,40 +36,12 @@ use crate::view::{InterceptContext, View}; // Identifiers // --------------------------------------------------------------------------- -/// Opaque, per-process identifier for a buffer. -/// -/// The internal representation is private (R22): callers cannot reach for -/// `.0`; construction goes through [`BufferId::next`]. -/// -/// T M10.5: `Serialize` / `Deserialize` derived so `BufferId` can be the -/// routing key on `InstanceMessage::CrdtOp` / `FrontendEvent::CrdtOp`. -/// The serialized form is the bare `u64` (transparent newtype). -#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, serde::Serialize, serde::Deserialize)] -pub struct BufferId(u64); - -impl BufferId { - /// Allocate a fresh [`BufferId`] from the process-wide counter. - /// - /// Threading: any thread. - #[must_use] - pub fn next() -> Self { - static COUNTER: AtomicU64 = AtomicU64::new(1); - Self(COUNTER.fetch_add(1, Ordering::Relaxed)) - } - - /// Inspect the raw value. Useful for logging and FFI. - #[must_use] - pub const fn raw(self) -> u64 { - self.0 - } - - /// Rebuild an ID from a raw value for crate-internal references that - /// persist an already-issued buffer identity in generated text. - #[must_use] - pub(crate) const fn from_raw(raw: u64) -> Self { - Self(raw) - } -} +/// Re-export of `pmacs_protocol::BufferId` (moved there in session 1 +/// of the `pmacs-gpu` arc — see `docs/pmacs-gpu-design.md`). Existing +/// `crate::buffer::BufferId` import paths continue to resolve through +/// this re-export; new consumers (`pmacs-gpu`, debug tools) should +/// depend on `pmacs-protocol` directly. +pub use pmacs_protocol::BufferId; /// Opaque, per-buffer identifier for an attached view. /// diff --git a/src/protocol.rs b/src/protocol.rs index 122260e..7710364 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -44,24 +44,16 @@ use crate::cell::{Cell, CellCoord, CellSize, DiffSpan}; use std::path::PathBuf; // --------------------------------------------------------------------------- -// Frontend identity +// Frontend identity, byte ranges — re-exports from `pmacs-protocol` // --------------------------------------------------------------------------- -/// Opaque identifier for a frontend attached to an instance. -/// -/// Every input event carries a `FrontendId`. v0.1 uses one ID per -/// instance ([`FrontendId::LOCAL`]); v0.3 generalizes to multi-frontend -/// (multi-window, multi-user) without a protocol break. -#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, serde::Serialize, serde::Deserialize)] -pub struct FrontendId(pub u64); - -impl FrontendId { - /// The single frontend used in v0.1's local-attach mode. - /// - /// Future multi-frontend deployments allocate IDs from a counter - /// starting after this value; the constant is reserved. - pub const LOCAL: FrontendId = FrontendId(1); -} +// Session 1 of the `pmacs-gpu` arc moved the wire-types subset of +// this module into the `pmacs-protocol` crate. See +// `docs/pmacs-gpu-design.md`. Existing `crate::protocol::FrontendId` +// / `crate::protocol::ByteRange` imports resolve through these +// re-exports; new consumers (`pmacs-gpu`, debug tools) should depend +// on `pmacs-protocol` directly. +pub use pmacs_protocol::{ByteRange, FrontendId}; // --------------------------------------------------------------------------- // Key encoding @@ -793,15 +785,8 @@ pub struct SelectionSnapshot { // variants is mooted exactly as it is for `CursorByte`. // --------------------------------------------------------------------------- -/// Half-open byte range `[start, end)` into a buffer's rope, matching -/// the rope's own range convention. -#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] -pub struct ByteRange { - /// Inclusive start byte offset. - pub start: u64, - /// Exclusive end byte offset. - pub end: u64, -} +// `ByteRange` moved to `pmacs-protocol` (see the re-export near the +// top of this file). /// One run of buffer bytes carrying a resolved visual style. The /// instance is the single syntax/face authority; the frontend lays diff --git a/src/rope.rs b/src/rope.rs index d25aa16..86af642 100644 --- a/src/rope.rs +++ b/src/rope.rs @@ -43,11 +43,11 @@ const MAX_CHILDREN: usize = 8; // Public types // --------------------------------------------------------------------------- -/// Byte offset into a [`Rope`]. -/// -/// Not a codepoint index, not a grapheme index. Grapheme awareness is a -/// view-layer concern. -pub type Position = u64; +// `Position` is re-exported from `pmacs-protocol` (session 1 of the +// `pmacs-gpu` arc — see `docs/pmacs-gpu-design.md`). The type alias +// is a `u64` byte offset into a [`Rope`]: not a codepoint index, not +// a grapheme index. Grapheme awareness is a view-layer concern. +pub use pmacs_protocol::Position; /// A persistent rope of bytes. /// From 2c04102aadd6f5eeb8f057374f158574894b91ec Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Wed, 20 May 2026 09:20:24 -0400 Subject: [PATCH 2/4] session 1 commit 2/4: cell wire types moved to pmacs-protocol MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moves Cell, Glyph, Style, Color, UnderlineStyle, CellCoord, CellSize, DiffSpan, Attachment to pmacs-protocol::cell. CellGrid (borrowed-slice render surface) and fn diff() (rendering helper) stay in src/cell.rs since they're instance-side rendering machinery, not wire shapes. src/cell.rs gains 'pub use pmacs_protocol::{Cell, Glyph, Style, ...};' at the top so every existing internal import (crate::cell::Cell, etc.) keeps resolving. The cell-module tests live alongside CellGrid + diff and reference the re-exported types via 'use super::*' — same as before; no test changes needed. Lib gate: still green (no regressions, 1314 passing). Co-Authored-By: Claude Opus 4.7 (1M context) --- pmacs-protocol/src/cell.rs | 177 +++++++++++++++++++++++++++++++++++ pmacs-protocol/src/lib.rs | 9 +- src/cell.rs | 185 +++---------------------------------- 3 files changed, 197 insertions(+), 174 deletions(-) create mode 100644 pmacs-protocol/src/cell.rs diff --git a/pmacs-protocol/src/cell.rs b/pmacs-protocol/src/cell.rs new file mode 100644 index 0000000..a79c233 --- /dev/null +++ b/pmacs-protocol/src/cell.rs @@ -0,0 +1,177 @@ +//! Cell wire types — moved from `pmacs::cell` in session 1 of the +//! `pmacs-gpu` arc. The original `pmacs::cell` module keeps +//! `CellGrid` (borrowed-slice render surface) and `fn diff()` +//! (rendering helper) since those are instance-side rendering +//! machinery, not wire shapes; the data types below all travel on +//! the `InstanceMessage::CellDelta` wire and on the +//! `SemanticFrame` family's `StyleSpan` / `Decoration` shapes. + +// --------------------------------------------------------------------------- +// Coordinates +// --------------------------------------------------------------------------- + +/// Coordinate in the cell grid (row, col), measured in cells. +#[derive(Copy, Clone, Eq, PartialEq, Debug, Default, serde::Serialize, serde::Deserialize)] +pub struct CellCoord { + /// 0-based row. + pub row: u32, + /// 0-based column. + pub col: u32, +} + +impl CellCoord { + /// Construct a cell coordinate. + #[must_use] + pub const fn new(row: u32, col: u32) -> Self { + Self { row, col } + } +} + +/// Dimensions of a cell grid, measured in cells. +#[derive(Copy, Clone, Eq, PartialEq, Debug, Default, serde::Serialize, serde::Deserialize)] +pub struct CellSize { + /// Number of rows. + pub rows: u32, + /// Number of columns. + pub cols: u32, +} + +impl CellSize { + /// Construct a cell size. + #[must_use] + pub const fn new(rows: u32, cols: u32) -> Self { + Self { rows, cols } + } + + /// Number of cells in the grid (`rows * cols`). + #[must_use] + pub const fn area(self) -> u32 { + self.rows * self.cols + } +} + +// --------------------------------------------------------------------------- +// Cell content +// --------------------------------------------------------------------------- + +/// A glyph in a cell. +/// +/// `Char` is the common case (single Unicode codepoint, single column). +/// `Cluster` carries a UTF-8 grapheme cluster spanning multiple codepoints +/// (e.g. emoji with modifiers, combining characters). `Continuation` is the +/// trailing column of a wide character: it has no glyph of its own; the +/// preceding cell's glyph occupies both columns. +#[derive(Clone, Eq, PartialEq, Debug, serde::Serialize, serde::Deserialize)] +pub enum Glyph { + /// A single Unicode codepoint occupying one column. + Char(char), + /// A grapheme cluster (one or more codepoints, encoded as UTF-8). + Cluster(Box<[u8]>), + /// The trailing column of a wide character. The preceding cell's glyph + /// renders into both columns; this cell's `glyph` and `style` are + /// ignored by frontends. + Continuation, +} + +impl Default for Glyph { + fn default() -> Self { + Self::Char(' ') + } +} + +/// A 24-bit RGB color, plus a `Default` sentinel meaning "use terminal +/// foreground/background". +#[derive(Copy, Clone, Eq, PartialEq, Debug, Default, serde::Serialize, serde::Deserialize)] +pub enum Color { + /// Use the terminal's default foreground or background. + #[default] + Default, + /// Truecolor RGB. + Rgb(u8, u8, u8), + /// 8-bit indexed terminal color (0..=255). + Indexed(u8), +} + +/// Underline style. +#[derive(Copy, Clone, Eq, PartialEq, Debug, Default, serde::Serialize, serde::Deserialize)] +pub enum UnderlineStyle { + /// No underline. + #[default] + None, + /// Single straight underline. + Single, + /// Double underline. + Double, + /// Curly (wavy) underline, typical for diagnostics. + Curly, + /// Dotted underline. + Dotted, + /// Dashed underline. + Dashed, +} + +/// Visual style applied to a cell. +#[derive(Copy, Clone, Eq, PartialEq, Debug, Default, serde::Serialize, serde::Deserialize)] +pub struct Style { + /// Foreground color. + pub fg: Color, + /// Background color. + pub bg: Color, + /// Bold. + pub bold: bool, + /// Italic. + pub italic: bool, + /// Underline. + pub underline: UnderlineStyle, + /// Reverse video. + pub reverse: bool, +} + +/// A non-text attachment carried in a cell (TUI ignores this). +/// +/// The TUI backend never inspects `Attachment`; a GUI backend interprets it +/// to render images, embedded widgets, and the like. +#[derive(Clone, Eq, PartialEq, Debug, serde::Serialize, serde::Deserialize)] +pub enum Attachment { + /// One cell of an image. The image is identified by `image_id` and the + /// cell's location within the image is `(sub_x, sub_y)`. + ImageCell { + /// Identifier into the frontend's image registry. + image_id: u32, + /// Sub-cell X offset. + sub_x: u16, + /// Sub-cell Y offset. + sub_y: u16, + }, +} + +/// One cell in the grid. +#[derive(Clone, Eq, PartialEq, Debug, Default, serde::Serialize, serde::Deserialize)] +pub struct Cell { + /// What is drawn in the cell. + pub glyph: Glyph, + /// How it is drawn. + pub style: Style, + /// Frontend-specific attachment (ignored by the TUI). + pub attachment: Option, +} + +// --------------------------------------------------------------------------- +// Diff span (wire shape for `InstanceMessage::CellDelta`) +// --------------------------------------------------------------------------- + +/// A run of changed cells starting at one position. +/// +/// Frontend translation: emit one cursor-move escape and then write the +/// cells in order. Wide characters appear as a leading `Char(_)` followed +/// by a [`Glyph::Continuation`] in the same span; the frontend consumes +/// both cells but only emits the leading glyph (the terminal handles the +/// width). +#[derive(Clone, Eq, PartialEq, Debug, serde::Serialize, serde::Deserialize)] +pub struct DiffSpan { + /// First cell of the span. + pub start: CellCoord, + /// New contents of the cells in the span, in row-major order. The + /// span occupies a contiguous run on `start.row`. + pub cells: Vec, +} diff --git a/pmacs-protocol/src/lib.rs b/pmacs-protocol/src/lib.rs index 68eb20b..521526a 100644 --- a/pmacs-protocol/src/lib.rs +++ b/pmacs-protocol/src/lib.rs @@ -34,10 +34,13 @@ //! (`pmacs-gpu`, debug tools, future ports) depend on this crate //! directly. +pub mod cell; pub mod ids; +pub use cell::{ + Attachment, Cell, CellCoord, CellSize, Color, DiffSpan, Glyph, Style, UnderlineStyle, +}; pub use ids::{BufferId, ByteRange, FrontendId, Position}; -// Cell wire types, the SemanticFrame family, top-level message -// envelopes, and the feature-gated `CrdtOp` follow in subsequent -// commits within this PR. +// The `SemanticFrame` family, top-level message envelopes, and the +// feature-gated `CrdtOp` follow in subsequent commits within this PR. diff --git a/src/cell.rs b/src/cell.rs index fc05d9e..1ea1c5e 100644 --- a/src/cell.rs +++ b/src/cell.rs @@ -7,162 +7,21 @@ //! [`Style`], and an optional [`Attachment`]. The TUI ignores `Attachment`; //! a future GUI backend interprets it. //! -//! The full layout and helpers (composition, diffing) land in T M1.6. T M1.4 -//! pulls in the public types so the [`crate::view::View`] trait can reference -//! them. +//! ## Module split (session 1 of the `pmacs-gpu` arc) +//! +//! The data types — `Cell`, `Glyph`, `Style`, `Color`, `UnderlineStyle`, +//! `CellCoord`, `CellSize`, `Attachment`, `DiffSpan` — moved to +//! `pmacs-protocol::cell` and are re-exported here so existing +//! `crate::cell::Cell` import paths keep resolving. [`CellGrid`] and +//! [`diff`] stay in this module — they are instance-side rendering +//! machinery, not wire shapes. See `docs/pmacs-gpu-design.md`. + +pub use pmacs_protocol::{ + Attachment, Cell, CellCoord, CellSize, Color, DiffSpan, Glyph, Style, UnderlineStyle, +}; // --------------------------------------------------------------------------- -// Coordinates -// --------------------------------------------------------------------------- - -/// Coordinate in the cell grid (row, col), measured in cells. -#[derive(Copy, Clone, Eq, PartialEq, Debug, Default, serde::Serialize, serde::Deserialize)] -pub struct CellCoord { - /// 0-based row. - pub row: u32, - /// 0-based column. - pub col: u32, -} - -impl CellCoord { - /// Construct a cell coordinate. - #[must_use] - pub const fn new(row: u32, col: u32) -> Self { - Self { row, col } - } -} - -/// Dimensions of a cell grid, measured in cells. -#[derive(Copy, Clone, Eq, PartialEq, Debug, Default, serde::Serialize, serde::Deserialize)] -pub struct CellSize { - /// Number of rows. - pub rows: u32, - /// Number of columns. - pub cols: u32, -} - -impl CellSize { - /// Construct a cell size. - #[must_use] - pub const fn new(rows: u32, cols: u32) -> Self { - Self { rows, cols } - } - - /// Number of cells in the grid (`rows * cols`). - #[must_use] - pub const fn area(self) -> u32 { - self.rows * self.cols - } -} - -// --------------------------------------------------------------------------- -// Cell content -// --------------------------------------------------------------------------- - -/// A glyph in a cell. -/// -/// `Char` is the common case (single Unicode codepoint, single column). -/// `Cluster` carries a UTF-8 grapheme cluster spanning multiple codepoints -/// (e.g. emoji with modifiers, combining characters). `Continuation` is the -/// trailing column of a wide character: it has no glyph of its own; the -/// preceding cell's glyph occupies both columns. -#[derive(Clone, Eq, PartialEq, Debug, serde::Serialize, serde::Deserialize)] -pub enum Glyph { - /// A single Unicode codepoint occupying one column. - Char(char), - /// A grapheme cluster (one or more codepoints, encoded as UTF-8). - Cluster(Box<[u8]>), - /// The trailing column of a wide character. The preceding cell's glyph - /// renders into both columns; this cell's `glyph` and `style` are - /// ignored by frontends. - Continuation, -} - -impl Default for Glyph { - fn default() -> Self { - Self::Char(' ') - } -} - -/// A 24-bit RGB color, plus a `Default` sentinel meaning "use terminal -/// foreground/background". -#[derive(Copy, Clone, Eq, PartialEq, Debug, Default, serde::Serialize, serde::Deserialize)] -pub enum Color { - /// Use the terminal's default foreground or background. - #[default] - Default, - /// Truecolor RGB. - Rgb(u8, u8, u8), - /// 8-bit indexed terminal color (0..=255). - Indexed(u8), -} - -/// Underline style. -#[derive(Copy, Clone, Eq, PartialEq, Debug, Default, serde::Serialize, serde::Deserialize)] -pub enum UnderlineStyle { - /// No underline. - #[default] - None, - /// Single straight underline. - Single, - /// Double underline. - Double, - /// Curly (wavy) underline, typical for diagnostics. - Curly, - /// Dotted underline. - Dotted, - /// Dashed underline. - Dashed, -} - -/// Visual style applied to a cell. -#[derive(Copy, Clone, Eq, PartialEq, Debug, Default, serde::Serialize, serde::Deserialize)] -pub struct Style { - /// Foreground color. - pub fg: Color, - /// Background color. - pub bg: Color, - /// Bold. - pub bold: bool, - /// Italic. - pub italic: bool, - /// Underline. - pub underline: UnderlineStyle, - /// Reverse video. - pub reverse: bool, -} - -/// A non-text attachment carried in a cell (TUI ignores this). -/// -/// The TUI backend never inspects `Attachment`; a GUI backend interprets it -/// to render images, embedded widgets, and the like. -#[derive(Clone, Eq, PartialEq, Debug, serde::Serialize, serde::Deserialize)] -pub enum Attachment { - /// One cell of an image. The image is identified by `image_id` and the - /// cell's location within the image is `(sub_x, sub_y)`. - ImageCell { - /// Identifier into the frontend's image registry. - image_id: u32, - /// Sub-cell X offset. - sub_x: u16, - /// Sub-cell Y offset. - sub_y: u16, - }, -} - -/// One cell in the grid. -#[derive(Clone, Eq, PartialEq, Debug, Default, serde::Serialize, serde::Deserialize)] -pub struct Cell { - /// What is drawn in the cell. - pub glyph: Glyph, - /// How it is drawn. - pub style: Style, - /// Frontend-specific attachment (ignored by the TUI). - pub attachment: Option, -} - -// --------------------------------------------------------------------------- -// Grid +// Grid (instance-side render surface; borrowed slice; does not move) // --------------------------------------------------------------------------- /// A mutable view onto a row-major cell buffer. @@ -213,25 +72,9 @@ impl CellGrid<'_> { } // --------------------------------------------------------------------------- -// Diff +// Diff (instance-side renderer helper; does not move) // --------------------------------------------------------------------------- -/// A run of changed cells starting at one position. -/// -/// Frontend translation: emit one cursor-move escape and then write the -/// cells in order. Wide characters appear as a leading `Char(_)` followed -/// by a [`Glyph::Continuation`] in the same span; the frontend consumes -/// both cells but only emits the leading glyph (the terminal handles the -/// width). -#[derive(Clone, Eq, PartialEq, Debug, serde::Serialize, serde::Deserialize)] -pub struct DiffSpan { - /// First cell of the span. - pub start: CellCoord, - /// New contents of the cells in the span, in row-major order. The - /// span occupies a contiguous run on `start.row`. - pub cells: Vec, -} - /// Compute the diff between two cell buffers of identical layout. /// /// `prev` and `next` are row-major slices, each of length at least From 5ffc47aa33c22fc8c868ae99d1ac735c338a434c Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Wed, 20 May 2026 09:32:09 -0400 Subject: [PATCH 3/4] session 1 commit 3/4: CrdtOp moved to pmacs-protocol CrdtOp { peer_id: u64, bytes: Vec } moves from src/rope.rs to pmacs-protocol::crdt. The type is unconditional (not #[cfg]-gated), matching the original's 'always present to avoid feature-flag proliferation through every Edit consumer' decision: the parent pmacs crate's 'crdt' feature gates loro and op application, not wire shape. Removed the unused 'crdt' feature stub I'd added to pmacs-protocol/Cargo.toml at session start; nothing in pmacs-protocol needs it. src/rope.rs adds 'pub use pmacs_protocol::CrdtOp;' so existing crate::rope::CrdtOp imports keep resolving. Lib gate: still 1314 passing. Co-Authored-By: Claude Opus 4.7 (1M context) --- pmacs-protocol/Cargo.toml | 12 +++++------ pmacs-protocol/src/crdt.rs | 44 ++++++++++++++++++++++++++++++++++++++ pmacs-protocol/src/lib.rs | 6 ++++-- src/rope.rs | 33 ++++------------------------ 4 files changed, 57 insertions(+), 38 deletions(-) create mode 100644 pmacs-protocol/src/crdt.rs diff --git a/pmacs-protocol/Cargo.toml b/pmacs-protocol/Cargo.toml index bcad954..27adff8 100644 --- a/pmacs-protocol/Cargo.toml +++ b/pmacs-protocol/Cargo.toml @@ -32,13 +32,11 @@ cast_precision_loss = "allow" similar_names = "allow" multiple_crate_versions = "allow" -[features] -# Mirrors the `crdt` feature on the parent `pmacs` crate. When enabled, -# the `InstanceMessage::CrdtOp` / `FrontendEvent::CrdtOp` variants exist -# and the wire-level `CrdtOp` struct is in scope. When disabled, those -# variants are compiled out so v0.1-era binaries built without `crdt` -# match the pre-v1.0 wire surface byte-for-byte. -crdt = [] +# No features — the wire type set is unconditional. `CrdtOp` is +# always compiled (matching the original `pmacs::rope::CrdtOp`'s +# "not `#[cfg]`-gated to avoid feature-flag proliferation through +# every Edit consumer" decision); the parent `pmacs` crate's `crdt` +# feature gates loro and op-application, not wire shape. [dependencies] serde = { workspace = true } diff --git a/pmacs-protocol/src/crdt.rs b/pmacs-protocol/src/crdt.rs new file mode 100644 index 0000000..6f186a7 --- /dev/null +++ b/pmacs-protocol/src/crdt.rs @@ -0,0 +1,44 @@ +//! `CrdtOp` — moved from `pmacs::rope` in session 1 of the `pmacs-gpu` +//! arc. Carried on the `InstanceMessage::CrdtOp` / +//! `FrontendEvent::CrdtOp` wire variants when an attached session +//! negotiated `crdt_replica: true`. +//! +//! The type is unconditional (not `#[cfg]`-gated) — the comment on +//! the original `pmacs::rope::CrdtOp` explained why: "Always present +//! (not `#[cfg]`-gated) to avoid feature-flag proliferation through +//! every Edit consumer." Keeping the same shape here. The `crdt` +//! feature on the parent `pmacs` crate gates loro and the actual +//! application of CRDT ops; the wire-type definition stays compiled +//! unconditionally so consumers (`pmacs-gpu`, debug tools) don't have +//! to mirror the feature flag to handle a wire-level variant they +//! may never see. + +/// T M10.2 Day 3: CRDT-op metadata carried by `Edit` in CRDT mode. +/// +/// Two fields: +/// +/// * `peer_id` — the producing-frontend identity. M10.4's per-frontend +/// undo reads this as the "is this op mine?" filter; saves the +/// consumer from parsing the op bytes to extract identity. +/// * `bytes` — wire-format serialization of the CRDT ops produced by +/// the originating edit, as returned by loro's +/// `ExportMode::updates_owned(pre_version)`. M10.5+ sends these +/// over the wire; receiving frontends import them via loro's +/// `import` to apply on their local CRDT. +/// +/// Constructed by `Buffer::apply_edit` (and `undo` / `redo`) in CRDT +/// mode; rope's edit constructors set `Edit::crdt_op` to `None` and +/// the Buffer wraps after the rope returns. +/// +/// T M10.5: serde derives added so this type can be the payload of +/// `InstanceMessage::CrdtOp` and `FrontendEvent::CrdtOp` on the wire. +/// `bytes` is opaque to the protocol layer — it's loro's incremental- +/// update format; the receiving end's `CrdtState::import_updates` +/// decodes it. +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct CrdtOp { + /// Producing frontend's identity (loro `PeerID`). + pub peer_id: u64, + /// Wire-format op bytes (loro `ExportMode::updates_owned` output). + pub bytes: Vec, +} diff --git a/pmacs-protocol/src/lib.rs b/pmacs-protocol/src/lib.rs index 521526a..45dfd1b 100644 --- a/pmacs-protocol/src/lib.rs +++ b/pmacs-protocol/src/lib.rs @@ -35,12 +35,14 @@ //! directly. pub mod cell; +pub mod crdt; pub mod ids; pub use cell::{ Attachment, Cell, CellCoord, CellSize, Color, DiffSpan, Glyph, Style, UnderlineStyle, }; +pub use crdt::CrdtOp; pub use ids::{BufferId, ByteRange, FrontendId, Position}; -// The `SemanticFrame` family, top-level message envelopes, and the -// feature-gated `CrdtOp` follow in subsequent commits within this PR. +// The `SemanticFrame` family and the top-level message envelopes +// follow in the final commit within this PR. diff --git a/src/rope.rs b/src/rope.rs index 86af642..3081158 100644 --- a/src/rope.rs +++ b/src/rope.rs @@ -330,35 +330,10 @@ pub struct Edit { pub crdt_op: Option>, } -/// T M10.2 Day 3: CRDT-op metadata carried by [`Edit`] in CRDT mode. -/// -/// Two fields: -/// -/// * `peer_id` — the producing-frontend identity. M10.4's per-frontend -/// undo reads this as the "is this op mine?" filter; saves the -/// consumer from parsing the op bytes to extract identity. -/// * `bytes` — wire-format serialization of the CRDT ops produced by -/// the originating edit, as returned by loro's -/// `ExportMode::updates_owned(pre_version)`. M10.5+ sends these -/// over the wire; receiving frontends import them via loro's -/// `import` to apply on their local CRDT. -/// -/// Constructed by `Buffer::apply_edit` (and `undo` / `redo`) in CRDT -/// mode; rope's edit constructors set `Edit::crdt_op` to `None` and -/// the Buffer wraps after the rope returns. -/// -/// T M10.5: serde derives added so this type can be the payload of -/// `InstanceMessage::CrdtOp` and `FrontendEvent::CrdtOp` on the wire. -/// `bytes` is opaque to the protocol layer — it's loro's incremental- -/// update format; the receiving end's `CrdtState::import_updates` -/// decodes it. -#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] -pub struct CrdtOp { - /// Producing frontend's identity (loro `PeerID`). - pub peer_id: u64, - /// Wire-format op bytes (loro `ExportMode::updates_owned` output). - pub bytes: Vec, -} +// `CrdtOp` moved to `pmacs-protocol::crdt` (session 1 of the +// `pmacs-gpu` arc — see `docs/pmacs-gpu-design.md`). Re-exported here +// so existing `crate::rope::CrdtOp` import paths continue to resolve. +pub use pmacs_protocol::CrdtOp; /// A half-open byte range `[start, end)` into a rope. /// From a820e913895ca6137139901deb5783c6e143ba57 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Wed, 20 May 2026 09:55:27 -0400 Subject: [PATCH 4/4] session 1 commit 4/4: message envelopes moved to pmacs-protocol MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The big move that completes session 1. Wire types moved from src/protocol.rs to pmacs-protocol/src/message.rs: - Input event family: Key, Modifiers, KeyEvent, MouseButton, MouseKind, MouseEvent, FrontendEvent (and its variants — Resize, KeyEvent, MouseEvent, Resume, Pause, Detach, ResizeAck, CrdtOp, Viewport). - Instance-side message family: CursorState, InstanceSignal, GoodbyeReason, InstanceMessage (Hello/Cursor/CellDelta/CursorByte/ CrdtOp/BufferSnapshot/Goodbye/PresenceUpdate + the SemanticFrame variants). - SelectionSnapshot. - SemanticFrame family components: StyleSpan, StyleSegment, DecorationKind, Decoration, DecorationSegment, AdornmentPlacement, AdornmentContent, InlineAdornment, BlockAdornment, ResourceBody. - Handshake: PROTOCOL_VERSION, SUPPORTED_PROTOCOL_VERSIONS, is_supported_protocol_version, InstanceIdentity, InstanceCapabilities, FrontendCapabilities, NegotiatedCapabilities, negotiate_capabilities, Hello, AttachRequest. What stays in src/protocol.rs: - AttachTarget / AttachError / AttachTargetParseError / AttachTargetValidationError / AttachTargetError / AttachmentHandle (CLI / binding internals, not wire). - crossterm_translate submodule (the crossterm ↔ pmacs-protocol-types translation layer; sits at the binding boundary, not on the wire). - Existing tests (wire-format roundtrip + AttachTarget + crossterm translation), unchanged — they reach the moved types through the 'pub use pmacs_protocol::*' re-export. Mechanical rewrites inside the moved chunk: crate::buffer::BufferId → crate::BufferId, crate::rope::Position → crate::Position, crate::rope::CrdtOp → crate::CrdtOp (the message module is inside pmacs-protocol; identity types live at the crate root). Feature re-added on pmacs-protocol: 'crdt' (was removed in commit 3 as I'd thought CrdtOp was the only feature-gated thing — but InstanceCapabilities::default and FrontendCapabilities::default both call cfg!(feature = 'crdt') for their multi_frontend / crdt_replica / semantic_render defaults). Re-added with a doc comment explaining why. The parent pmacs crate's 'crdt' feature now activates 'pmacs-protocol/crdt' so the cfg!() check evaluates consistently in both crates. Full gate green: fmt, clippy --all-targets -D warnings, lib 1314, m4_acceptance 83, m8_1/m8_9/m8_10 10/26/19, m9_1 18, m5_8 5, m11_5_semantic_acceptance --features crdt 2. Co-Authored-By: Claude Opus 4.7 (1M context) --- Cargo.toml | 2 +- pmacs-protocol/Cargo.toml | 14 +- pmacs-protocol/src/lib.rs | 12 +- pmacs-protocol/src/message.rs | 1350 ++++++++++++++++++++++++++++++++ src/protocol.rs | 1355 +-------------------------------- 5 files changed, 1383 insertions(+), 1350 deletions(-) create mode 100644 pmacs-protocol/src/message.rs diff --git a/Cargo.toml b/Cargo.toml index 5c4e2b5..1c46ca0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,7 +72,7 @@ lua54 = ["mlua/lua54", "mlua/vendored"] # field on the Buffer struct layout, no branch on apply_edit). v1.0 # builds enable `crdt`; the rope-projection redirect from M10.1 means # the feature flip is invisible to v0.1 frontends and to workers. -crdt = ["dep:loro"] +crdt = ["dep:loro", "pmacs-protocol/crdt"] [dependencies] crossterm = "0.28" diff --git a/pmacs-protocol/Cargo.toml b/pmacs-protocol/Cargo.toml index 27adff8..a1ced85 100644 --- a/pmacs-protocol/Cargo.toml +++ b/pmacs-protocol/Cargo.toml @@ -32,11 +32,15 @@ cast_precision_loss = "allow" similar_names = "allow" multiple_crate_versions = "allow" -# No features — the wire type set is unconditional. `CrdtOp` is -# always compiled (matching the original `pmacs::rope::CrdtOp`'s -# "not `#[cfg]`-gated to avoid feature-flag proliferation through -# every Edit consumer" decision); the parent `pmacs` crate's `crdt` -# feature gates loro and op-application, not wire shape. +[features] +# Mirrors the parent `pmacs` crate's `crdt` feature. The wire type +# set is unconditional (`CrdtOp` is always compiled — see +# `crdt.rs`); the feature exists so `cfg!(feature = "crdt")` checks +# inside capability-default helpers (`InstanceCapabilities::default`, +# `FrontendCapabilities::default`) evaluate to the same value here +# as in the parent crate. The parent activates it via +# `pmacs-protocol/crdt` from its own `crdt` feature. +crdt = [] [dependencies] serde = { workspace = true } diff --git a/pmacs-protocol/src/lib.rs b/pmacs-protocol/src/lib.rs index 45dfd1b..be5da5c 100644 --- a/pmacs-protocol/src/lib.rs +++ b/pmacs-protocol/src/lib.rs @@ -37,12 +37,18 @@ pub mod cell; pub mod crdt; pub mod ids; +pub mod message; pub use cell::{ Attachment, Cell, CellCoord, CellSize, Color, DiffSpan, Glyph, Style, UnderlineStyle, }; pub use crdt::CrdtOp; pub use ids::{BufferId, ByteRange, FrontendId, Position}; - -// The `SemanticFrame` family and the top-level message envelopes -// follow in the final commit within this PR. +pub use message::{ + AdornmentContent, AdornmentPlacement, AttachRequest, BlockAdornment, CursorState, Decoration, + DecorationKind, DecorationSegment, FrontendCapabilities, FrontendEvent, GoodbyeReason, Hello, + InlineAdornment, InstanceCapabilities, InstanceIdentity, InstanceMessage, InstanceSignal, Key, + KeyEvent, Modifiers, MouseButton, MouseEvent, MouseKind, NegotiatedCapabilities, + PROTOCOL_VERSION, ResourceBody, SUPPORTED_PROTOCOL_VERSIONS, SelectionSnapshot, StyleSegment, + StyleSpan, is_supported_protocol_version, negotiate_capabilities, +}; diff --git a/pmacs-protocol/src/message.rs b/pmacs-protocol/src/message.rs new file mode 100644 index 0000000..ea9d91c --- /dev/null +++ b/pmacs-protocol/src/message.rs @@ -0,0 +1,1350 @@ +//! Wire message envelopes — moved from `pmacs::protocol` in session 1 +//! of the `pmacs-gpu` arc. Contains the input event types +//! (`Key`/`Modifiers`/`KeyEvent`, mouse types, `FrontendEvent`), the +//! instance-side message types (`InstanceMessage`, `CursorState`, +//! `InstanceSignal`, `GoodbyeReason`, `SelectionSnapshot`), the +//! `SemanticFrame` family (`StyleSpan`/`StyleSegment`, `Decoration`, +//! `Adornment*`, `ResourceBody`), and the attach handshake +//! (`Hello`/`AttachRequest`/`InstanceCapabilities`/`FrontendCapabilities`/ +//! `NegotiatedCapabilities`/`InstanceIdentity` + `PROTOCOL_VERSION` / +//! `SUPPORTED_PROTOCOL_VERSIONS`). +//! +//! The original `pmacs::protocol` module keeps internal CLI / binding +//! types (`AttachTarget`, `AttachError`, `AttachmentHandle`, the +//! `crossterm_translate` submodule) plus the existing wire-format +//! roundtrip tests; it re-exports everything below so existing +//! `crate::protocol::*` imports stay stable. + +use crate::cell::{Cell, CellCoord, CellSize, DiffSpan}; +use crate::ids::{ByteRange, FrontendId}; + +// --------------------------------------------------------------------------- +// Key encoding +// --------------------------------------------------------------------------- + +/// Key code, normalized away from any specific terminal protocol. +/// +/// `Char` covers printable input. The named variants cover the keys +/// terminals report distinctly (arrows, function keys, etc.). `Unknown` +/// is the escape hatch: a key the protocol layer cannot encode in +/// any of the named variants is preserved as a u32 sentinel so it +/// can round-trip through serialization without becoming an error. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)] +pub enum Key { + /// A printable character. The character is the user-visible + /// codepoint after layout / IME processing. + Char(char), + /// A function key. `n` is 1-based: `F(1)` is F1. + F(u8), + /// Backspace / `^H`. + Backspace, + /// Enter / Return / `^M`. + Enter, + /// Left arrow. + Left, + /// Right arrow. + Right, + /// Up arrow. + Up, + /// Down arrow. + Down, + /// Home key. + Home, + /// End key. + End, + /// Page Up. + PageUp, + /// Page Down. + PageDown, + /// Tab. + Tab, + /// Shift-Tab. + BackTab, + /// Forward delete. + Delete, + /// Insert. + Insert, + /// Escape. + Escape, + /// Caps Lock. + CapsLock, + /// Scroll Lock. + ScrollLock, + /// Num Lock. + NumLock, + /// Print Screen. + PrintScreen, + /// Pause / Break. + Pause, + /// Menu / context-menu key. + Menu, + /// Numeric-keypad center key. + KeypadBegin, + /// The "null" keycode (terminal-protocol artifact). + Null, + /// A key the protocol layer does not recognize. The `u32` + /// preserves whatever sentinel value the upstream layer attached + /// (e.g. a media-key code from kitty's keyboard protocol). Round-trips + /// through serialization but is not actionable by commands. + Unknown(u32), +} + +/// Modifier-key set. Bit-flag encoding for compact wire shape. +/// +/// `META` corresponds to the "logo" / "super" key on most keyboards. +/// `HYPER` is reserved for the rare keyboards that distinguish it +/// from `META` (kitty's keyboard protocol surfaces both). +#[derive( + Copy, Clone, Eq, PartialEq, Hash, Debug, Default, serde::Serialize, serde::Deserialize, +)] +pub struct Modifiers(u8); + +impl Modifiers { + /// Empty set: no modifiers held. + pub const NONE: Modifiers = Modifiers(0); + /// Shift. + pub const SHIFT: Modifiers = Modifiers(1 << 0); + /// Control. + pub const CTRL: Modifiers = Modifiers(1 << 1); + /// Alt / Option. + pub const ALT: Modifiers = Modifiers(1 << 2); + /// Meta / Super / Logo / Command. + pub const META: Modifiers = Modifiers(1 << 3); + /// Hyper. Distinguished from `META` only on keyboards that + /// surface both (kitty's keyboard protocol). + pub const HYPER: Modifiers = Modifiers(1 << 4); + + /// Construct from a raw bit set. Bits outside the defined range + /// are silently masked off so a future-extended wire cannot smuggle + /// undefined bits past current decoders. + #[must_use] + pub const fn from_bits_truncate(bits: u8) -> Self { + Self(bits & 0b0001_1111) + } + + /// Raw bit set. + #[must_use] + pub const fn bits(self) -> u8 { + self.0 + } + + /// Whether `self` includes every bit set in `other`. + #[must_use] + pub const fn contains(self, other: Modifiers) -> bool { + (self.0 & other.0) == other.0 + } + + /// Whether no modifiers are held. + #[must_use] + pub const fn is_empty(self) -> bool { + self.0 == 0 + } +} + +impl std::ops::BitOr for Modifiers { + type Output = Modifiers; + fn bitor(self, rhs: Modifiers) -> Modifiers { + Modifiers(self.0 | rhs.0) + } +} + +impl std::ops::BitOrAssign for Modifiers { + fn bitor_assign(&mut self, rhs: Modifiers) { + self.0 |= rhs.0; + } +} + +/// A keyboard event. +#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct KeyEvent { + /// Frontend that produced the event. + pub frontend_id: FrontendId, + /// The key code. + pub key: Key, + /// Modifier set held when the key was pressed. + pub mods: Modifiers, + /// Monotonic timestamp at which the frontend captured the event. + /// Zero means "no timestamp available" (e.g. test-synthesized + /// events). + pub timestamp_ns: u64, +} + +// --------------------------------------------------------------------------- +// Mouse encoding +// --------------------------------------------------------------------------- + +/// Mouse button. +#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +pub enum MouseButton { + /// Left button. + Left, + /// Right button. + Right, + /// Middle button. + Middle, +} + +/// Kind of mouse interaction. +#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +pub enum MouseKind { + /// Button pressed. + Down(MouseButton), + /// Button released. + Up(MouseButton), + /// Drag with the named button held. + Drag(MouseButton), + /// Pointer moved with no button held. + Move, + /// Wheel scrolled up. + ScrollUp, + /// Wheel scrolled down. + ScrollDown, + /// Wheel scrolled left. + ScrollLeft, + /// Wheel scrolled right. + ScrollRight, +} + +/// A mouse event. +#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct MouseEvent { + /// Frontend that produced the event. + pub frontend_id: FrontendId, + /// Kind of mouse interaction. + pub kind: MouseKind, + /// Cell-grid coordinate of the pointer at the moment of the event. + pub coord: CellCoord, + /// Modifiers held during the event. + pub mods: Modifiers, +} + +// --------------------------------------------------------------------------- +// Frontend → Instance events +// --------------------------------------------------------------------------- + +/// Input event from frontend to instance. +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +pub enum FrontendEvent { + /// A key event. + Key(KeyEvent), + /// A mouse event. + Mouse(MouseEvent), + /// Frontend's terminal resized. + Resize { + /// Frontend that resized. + frontend_id: FrontendId, + /// New size, in cells. + size: CellSize, + }, + /// Bracketed-paste payload from the frontend. + Paste { + /// Frontend that produced the paste. + frontend_id: FrontendId, + /// Raw bytes pasted (the instance decodes as UTF-8 if relevant). + data: Vec, + }, + /// Frontend gained input focus. + FocusGained(FrontendId), + /// Frontend lost input focus. + FocusLost(FrontendId), + /// Frontend is going away. Instance treats this as immediate + /// detach; no acknowledgement required. + Detach(FrontendId), + /// T M10.5: CRDT operation produced by this frontend's local + /// edit, sent to the instance for broadcast to the other + /// attached frontends. The actual flow that produces these + /// (frontend maintaining a local CRDT state, applying edits + /// optimistically, sending the resulting op) is wired in M10.8 + /// + M10.10; M10.5 declares the wire shape so the protocol + /// version bump (1 → 2) covers it. + /// + /// Only sent by v1.0 frontends (`protocol_version = 2`); v0.1 + /// frontends never emit this variant. Sessions negotiated at + /// protocol version 1 must NOT receive this on the + /// instance-side dispatcher (the daemon filters per-session; + /// the editor-core treats it as an unknown frontend event if + /// it ever arrives from a v1 session, which it shouldn't). + CrdtOp { + /// Which attached frontend produced this op. The instance + /// uses this to avoid echoing the op back to its sender. + frontend_id: FrontendId, + /// Which buffer this op affects. The instance routes the + /// op to that buffer's CRDT state. + buffer_id: crate::BufferId, + /// The CRDT operation payload — `peer_id` + opaque wire bytes + /// loro's `import_updates` decodes. + op: crate::CrdtOp, + }, + /// T M11.1: the buffer byte range a semantic frontend currently + /// has on screen, in buffer coordinates. Replaces the + /// instance-derived grid viewport for `semantic_render` sessions: + /// the instance scopes its `StyleSpans` / `Decorations` / … to + /// this range rather than shipping a whole file's styling. + /// + /// **No pixels.** This carries a byte range, never viewport pixel + /// size, DPI, font metrics, or glyph advances — the contract + /// boundary invariant from the semantic-frontend design note. The + /// frontend owns all visual-motion semantics and resolves + /// pixel→offset locally; there is deliberately no hit-test + /// request variant and no `SemanticResize`, both of which would + /// leak pixels across the boundary. + /// + /// `generation` ties the declared range to a CRDT version so the + /// instance can ignore a viewport that races a not-yet-applied + /// edit (symmetric with `StyleSpans::generation`). + /// + /// Only emitted by sessions that negotiated `semantic_render`; + /// a non-semantic session never sends it. M11.1 declares the + /// wire shape; the instance-side consumer is wired with the + /// projection seam in M11.2. + Viewport { + /// Which frontend's viewport this is. + frontend_id: FrontendId, + /// Which buffer the visible range indexes into. + buffer_id: crate::BufferId, + /// Half-open byte range currently on screen. + visible: ByteRange, + /// CRDT generation the frontend computed `visible` against. + generation: u64, + }, +} + +impl FrontendEvent { + /// The frontend that produced this event. + #[must_use] + pub fn frontend_id(&self) -> FrontendId { + match self { + Self::Key(e) => e.frontend_id, + Self::Mouse(e) => e.frontend_id, + Self::Resize { frontend_id, .. } + | Self::Paste { frontend_id, .. } + | Self::FocusGained(frontend_id) + | Self::FocusLost(frontend_id) + | Self::Detach(frontend_id) + | Self::CrdtOp { frontend_id, .. } + | Self::Viewport { frontend_id, .. } => *frontend_id, + } + } +} + +// --------------------------------------------------------------------------- +// Instance → Frontend messages +// --------------------------------------------------------------------------- + +/// Cursor position and visibility. +#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct CursorState { + /// Cell where the cursor should be drawn. + pub coord: CellCoord, + /// Whether the cursor is visible at all. + pub visible: bool, +} + +/// Instance-level signal that is not a render message. +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +pub enum InstanceSignal { + /// Terminal bell. + Bell, + /// Window-title change request. + Title(String), + /// Clipboard set request (OSC 52). + Clipboard(Vec), +} + +/// Reason an instance terminates an attachment. +/// +/// Only the four variants the v0.1 daemon actually emits or rejects on. +/// `Evicted` (multi-frontend takeover) and similar will land alongside +/// the v0.3 multi-frontend work; until then `AlreadyAttached` covers +/// the single-slot equivalent. +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +pub enum GoodbyeReason { + /// Instance is shutting down (SIGTERM / SIGINT or clean exit). + ShuttingDown, + /// Frontend's `protocol_version` does not match the instance's. + /// The handshake fails before any further messages. + VersionMismatch { + /// The instance's `PROTOCOL_VERSION`. + server: u32, + /// The version the frontend announced in its `AttachRequest`. + client: u32, + }, + /// Another frontend is currently attached. v0.1 rejects concurrent + /// attaches; v0.3 will replace this with eviction or multiplexing. + AlreadyAttached, + /// Frontend sent a malformed message or otherwise violated the + /// protocol. The connection is closed without further dialogue. + ProtocolError, + /// T M10.7: frontend declared one or more negotiated capability + /// bits that the instance cannot honor. The handshake fails after + /// the version check but before any further messages. + /// + /// `missing` lists the capability *field names* (e.g., + /// `"multi_frontend"`, `"crdt_replica"`) the frontend requested + /// (`true`) that the instance reports as `false`. These strings + /// are stable wire-format identifiers: they are exactly the + /// `FrontendCapabilities` / `InstanceCapabilities` field names, + /// not human-readable descriptions. The frontend translates them + /// for display via [`AttachError`]'s formatting. Renaming a + /// capability bit requires changing both the field name AND the + /// missing-string emission in `negotiate_capabilities` in + /// lockstep — see the M10.7 audit's wire-format-stability + /// section. + CapabilityMismatch { + /// The capability bit names the frontend asked for that the + /// instance does not support. Each entry is a verbatim + /// `FrontendCapabilities` field name. + missing: Vec, + }, +} + +/// Rendering and signals from instance to frontend. +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +pub enum InstanceMessage { + /// Cell deltas. `full_grid = true` is the initial sync sent on + /// fresh attach (or after a resize where the previous grid is no + /// longer applicable); `full_grid = false` is a differential + /// frame. + CellDelta { + /// One run of changed cells per `DiffSpan`. + spans: Vec, + /// Whether `spans` represents a full-grid resync (true on + /// fresh attach or post-resize) versus an incremental frame. + full_grid: bool, + }, + /// Cursor position and visibility update. + Cursor(Option), + /// Modeline cells. Reserved for v0.3 GUI use; v0.1 ships modeline + /// inside [`InstanceMessage::CellDelta`]. The variant exists in + /// the protocol from day one so adding the discrete channel later + /// is not a breaking change. + ModeLine(Vec), + /// Side-channel signal (bell, title, clipboard). + Signal(InstanceSignal), + /// Instance is terminating the attachment. + Goodbye(GoodbyeReason), + /// T M10.5: CRDT operation broadcast from the instance to all + /// attached frontends. The originating frontend produced this op + /// (via `FrontendEvent::CrdtOp` or via a local editor-core edit + /// that synthesizes one); the instance fans it out so every + /// attached frontend can apply the op to its local CRDT state. + /// + /// Only sent to v1.0 frontends — sessions negotiated at + /// `protocol_version = 1` never receive this variant, per + /// `§sec:m10-backward-compat`. The daemon filters at the + /// outgoing-message path; this variant simply existing in the + /// enum is not a wire-compat issue for v1 sessions because the + /// daemon never emits it to them. + /// + /// M10.5 declares the wire shape. M10.8 wires the editor-core → + /// daemon → frontend flow that actually emits these. + CrdtOp { + /// Which buffer this op affects. v1.0 frontends maintain + /// a per-buffer local CRDT state; this routes to the right + /// one. + buffer_id: crate::BufferId, + /// The CRDT operation payload — `peer_id` + opaque wire bytes + /// loro's `import_updates` decodes. + op: crate::CrdtOp, + }, + /// T M10.6: cursor + selection state of one attached frontend, + /// broadcast to the other v1.0 frontends so they can render + /// peer-presence overlays. Coalesced at the daemon: rapid cursor + /// movement produces one `PresenceUpdate` per tick per source + /// frontend, carrying the *final* state, not intermediate values. + /// + /// Sender exclusion: the source frontend never receives its own + /// `PresenceUpdate`. v0.1 sessions (negotiated `protocol_version = + /// 1`) are filtered out at the daemon's outgoing-message path. + /// + /// M10.6 declares the wire shape AND wires the daemon-side + /// sweep with per-session filter. In single-frontend deployments + /// the recipient list is structurally empty (sender exclusion + /// with no other v2 sessions); M10.8 enables the multi-frontend + /// case where this message actually crosses the wire. The + /// frontend's renderer for peer-cursor overlays is also M10.8. + PresenceUpdate { + /// Which attached frontend this presence belongs to. v1.0 + /// frontends use this to label the peer-cursor overlay + /// ("user 4 is editing here"). + frontend_id: FrontendId, + /// Which buffer the source frontend's cursor is in. + buffer_id: crate::BufferId, + /// Byte offset of the source frontend's cursor within + /// `buffer_id`. Frontends convert to line/column at render + /// time via the rope's coord-mapping; the wire carries the + /// canonical byte offset to avoid encoding-vs-rendering + /// drift across frontends. + cursor: crate::Position, + /// Active selection range, if any. + selection: Option, + }, + /// T M10.10: bootstrap a frontend's local CRDT replica with the + /// instance's current authoritative state. Sent once per active + /// buffer at `SessionEstablished` time (and on subsequent + /// buffer-creation events) to frontends that negotiated + /// `crdt_replica: true`. Frontends that didn't negotiate the + /// capability never receive this variant — the daemon's + /// outgoing-message filter gates the send on + /// `NegotiatedCapabilities::crdt_replica`. + /// + /// `crdt_snapshot` carries loro's run-encoded snapshot + /// (`CrdtState::export_snapshot()`) — the CRDT-internal state + /// including peer IDs, version vectors, and op-history structure. + /// Raw byte contents are insufficient because a fresh CRDT replica + /// initialized from bytes alone diverges on the first concurrent + /// edit. + /// + /// Cursor position is intentionally absent: cursor is per-frontend + /// window state (M10.8 `FrontendView`), not per-buffer CRDT + /// state. The same buffer can appear in multiple windows on one + /// frontend with different cursors; coupling cursor to + /// `BufferSnapshot` would break this model. + BufferSnapshot { + /// Which buffer's CRDT state this snapshot represents. + buffer_id: crate::BufferId, + /// `loro::LoroDoc::export(ExportMode::Snapshot)` output. Applied + /// to a fresh `CrdtState::new(peer_id_from_frontend(my_id))` + /// via `import_snapshot(bytes)` on the receiving frontend. + crdt_snapshot: Vec, + }, + /// T M10.10: the active buffer for a replica frontend, with the + /// cursor position within it. + /// + /// # Semantics (Day 3 step 3b composition-check broadened + /// contract) + /// + /// `CursorByte` represents "the active buffer for this frontend + /// is `buffer_id`; the cursor in that buffer is at `byte_pos`." + /// Not just "the cursor moved." This contract matters: a narrow + /// "cursor moved" emission would miss active-buffer-changed- + /// without-cursor-motion events (Lua-driven buffer switch + /// landing at the same byte position), and the frontend's + /// active-buffer tracking would go stale. + /// + /// Daemon emits `CursorByte` on every per-tick render frame for + /// replica frontends, derived fresh from `active_window_for(fid)`. + /// Cursor move, active-buffer change, and active-window change + /// all produce a new emission carrying the current `(buffer_id, + /// byte_pos)`. The per-tick rate (16ms at 60Hz) is the same as + /// `Cursor`'s grid-coord variant. + /// + /// # Why a separate variant from `Cursor` + /// + /// `Cursor` carries grid coordinates (row/col cells) — the + /// frontend uses them to paint the cursor. The optimistic-apply + /// path needs byte position (CRDT insert/delete is byte-indexed), + /// which the grid coordinate can't recover without duplicating + /// the daemon's view-layout logic (tab expansion, line wrap, + /// double-width chars, viewport offset). `CursorByte` is the + /// authoritative byte position for the active buffer. + /// + /// # Atomicity with `Cursor` + /// + /// The daemon emits `Cursor` and `CursorByte` together for + /// replica frontends — both derived from the same render-frame + /// iteration so they describe the cursor in the same instant in + /// two reference frames. Non-replica frontends receive only + /// `Cursor` (existing behavior). The replica frontend that sees + /// `Cursor` without a paired `CursorByte` would interpret stale + /// byte position; the daemon guarantees both emit together by + /// derivation, not by message-protocol atomicity. + /// + /// # Wire-format compatibility + /// + /// New variant in v2; receivers without M10.10 hard-error on + /// decode (postcard does not gracefully degrade unknown variants, + /// per M10.10-FRAMING.md Refinement 3). Capability-gated: daemon + /// sends only to frontends that negotiated `crdt_replica: true`. + /// `PROTOCOL_VERSION` stays at 2. + CursorByte { + /// The buffer the cursor is in. A replica frontend tracks + /// per-buffer cursors; this routes the update to the right + /// entry. + buffer_id: crate::BufferId, + /// Byte offset of the cursor within `buffer_id`. Source of + /// truth for the optimistic-apply path's insert / delete + /// position arguments. Wire type matches + /// `PresenceUpdate::cursor` (`u64`) for consistency; frontend + /// converts to `usize` for the loro API. + byte_pos: crate::Position, + }, + /// T M11.1 — syntax + face styling over the semantic frontend's + /// current viewport range. `generation` ties the spans to a CRDT + /// version so the frontend can discard styling that predates an + /// edit it has already applied optimistically. Ships **no text** — + /// the frontend holds the rope via the `crdt_replica` machinery + /// and interprets these spans over it. + /// + /// # Diff shape (T M11.4) + /// + /// Mirrors `CellDelta`'s `full_grid` + changed-runs structure, + /// lifted from positional cells to byte-anchored ranges. `full = + /// true` is a resync: the frontend discards all prior styling for + /// `buffer_id` and the `segments` are authoritative for the whole + /// declared viewport (first frame after a `Viewport`, a viewport + /// jump, or a generation discontinuity). `full = false` is + /// incremental: each [`StyleSegment`] replaces styling **only** + /// within its `range`; bytes covered by no segment keep their + /// previously-applied style. A frame whose styling is unchanged + /// ships no `StyleSpans` at all. + /// + /// Because byte offsets cascade on edits (an insert shifts every + /// later span), an incremental frame after an edit dirties + /// `[edit, viewport_end)` — still bounded, and no-edit frames + /// (cursor move, scroll within the declared viewport, selection) + /// cost nothing. Each segment carries *all* current spans + /// intersecting its range (clipped), not only changed ones, so an + /// unchanged span overlapping a dirty range is faithfully + /// reconstructed. + /// + /// Gated on negotiated `semantic_render`; never sent to a grid + /// session (the daemon's per-session outgoing filter — wired with + /// the producer in M11.2 — never emits it there, so postcard's + /// hard-error on unknown variants is mooted exactly as it is for + /// `CursorByte`). + StyleSpans { + /// Buffer these spans interpret. + buffer_id: crate::BufferId, + /// CRDT generation the spans were computed against. + generation: u64, + /// `true` → discard all prior styling for `buffer_id` first; + /// `segments` are authoritative for the declared viewport. + full: bool, + /// Dirty byte regions and the styling now covering them. + segments: Vec, + }, + /// T M11.1 — diagnostics, search hits, current-line, and any + /// other "this region means something" overlay, as offset ranges + /// plus a kind. Peer selection is **not** here — it stays on the + /// existing `PresenceUpdate` path. Gated on `semantic_render`. + /// + /// T M11.4 — same `full` + segment diff shape as `StyleSpans` + /// (see its docs), and gains `generation` for parity: a frontend + /// wants the CRDT version decorations were computed against for + /// the same optimistic-edit race reason styling does. + Decorations { + /// Buffer these decorations apply to. + buffer_id: crate::BufferId, + /// CRDT generation the decorations were computed against. + generation: u64, + /// `true` → discard all prior decorations for `buffer_id` + /// first; `segments` are authoritative for the viewport. + full: bool, + /// Dirty byte regions and the decorations now covering them. + segments: Vec, + }, + /// T M11.1 — inlay hints, blame, lens, virtual text. Anchored at + /// a single offset with a placement; occupies no document bytes — + /// the frontend interleaves it at layout time. Gated on + /// `semantic_render`. + InlineAdornments { + /// Buffer these adornments annotate. + buffer_id: crate::BufferId, + /// The adornment items for the declared viewport. + items: Vec, + }, + /// Coarse whole-file styling summary for a minimap / scrollbar + /// overview, resolving design-note Open Q#2. One [`Style`] per + /// source line (the *dominant* style for that line by byte count + /// across the producer's current spans); the frontend maps minimap + /// rows to one or more of these. Unlike [`Self::StyleSpans`], this + /// is **not** viewport-scoped — the minimap shows the whole file. + /// + /// Recomputed when the buffer's CRDT `generation` advances; an + /// unchanged buffer ships no further summary. A coarser + /// representation (fixed bands) or finer (run-length style runs) + /// is recorded in `docs/semantic-frontend-protocol.md` as future + /// refinements; per-line dominant style is the v1 choice because + /// minimap rows naturally correspond to code lines. + /// + /// Gated on negotiated `semantic_render`; the daemon emits it only + /// for sessions that have a [`crate::semantic_render::SemanticRenderState`] + /// (structural gating, same as every other semantic family). + FileStyleSummary { + /// Buffer this summary describes. + buffer_id: crate::BufferId, + /// CRDT generation the summary was computed against. The + /// frontend can discard a summary that predates an edit it + /// has already applied optimistically. + generation: u64, + /// One [`crate::cell::Style`] per source line, in line order + /// from line 0. Empty when the buffer is empty. + lines: Vec, + }, + /// T M11.1 — diff zones, folded-region placeholders, anything + /// occupying its own vertical band. Anchored to the offset of the + /// line it precedes or replaces; the frontend allocates the + /// vertical space. Gated on `semantic_render`. + BlockAdornments { + /// Buffer these adornments annotate. + buffer_id: crate::BufferId, + /// The block items for the declared viewport. + items: Vec, + }, + /// T M11.1 — the instance's authoritative fold set as document + /// facts. Folding is an instance command-semantics concern (Lua + /// can fold); the visual collapse is a frontend layout concern — + /// the frontend renders the placeholder and adjusts its own + /// layout. Gated on `semantic_render`. + FoldState { + /// Buffer whose fold set this is. + buffer_id: crate::BufferId, + /// Folded byte ranges. + folds: Vec, + }, + /// T M11.1 — out-of-band content an adornment refers to (images, + /// blame avatars). Sent once, referenced by `handle`, so it is + /// not re-shipped per frame. Gated on `semantic_render`. + ResourceOffer { + /// Stable handle adornments reference via + /// [`AdornmentContent::Resource`]. + handle: u64, + /// MIME type of `body`. + mime: String, + /// Inline bytes or a URI the frontend resolves itself. + body: ResourceBody, + }, +} + +/// Flat selection state for the wire. +/// +/// Mirrors [`crate::window::Selection`] but as a self-contained pair +/// of byte offsets — `anchor` is where the selection began, +/// `active` is the current selection cursor. Either may be the +/// numerically larger value; callers wanting `(lo, hi)` order +/// compute it locally. +/// +/// Kept flat (no nested types) so [`PartialEq`] equality is exactly +/// wire-representation equality: two `SelectionSnapshot`s compare +/// equal iff they serialize to identical bytes. The presence-diff +/// sweep relies on this — see [`crate::presence::SessionRegistry`]. +#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct SelectionSnapshot { + /// Where the selection began. + pub anchor: crate::Position, + /// The active end (typically the cursor at the moment of the + /// snapshot). + pub active: crate::Position, +} + +// --------------------------------------------------------------------------- +// T M11.1 — Semantic-frontend projection types +// +// The payloads of the `InstanceMessage::StyleSpans` … `ResourceOffer` +// family and `FrontendEvent::Viewport`. Everything is anchored in +// **byte offsets** (consistent with `CursorByte`): line/col is a +// frontend rendering concern, CRDT position is replica-internal. The +// instance never learns a pixel — see the contract boundary in +// `docs/semantic-frontend-protocol.md`. +// +// The variant/kind sets here are provisional and co-evolve within the +// M11 arc behind the `semantic_render` capability + protocol v3, +// exactly as the CRDT op shape evolved M10.5→M10.10 behind +// `crdt_replica`. They are not a wire-compat hazard for non-semantic +// sessions: the daemon's per-session outgoing filter (wired with the +// producer in M11.2) never emits the family to a session that didn't +// negotiate `semantic_render`, so postcard's hard-error on unknown +// variants is mooted exactly as it is for `CursorByte`. +// --------------------------------------------------------------------------- + +// `ByteRange` moved to `pmacs-protocol` (see the re-export near the +// top of this file). + +/// One run of buffer bytes carrying a resolved visual style. The +/// instance is the single syntax/face authority; the frontend lays +/// the style out locally over rope text it already holds. Reuses +/// [`crate::cell::Style`] so the grid and semantic projections share +/// one style vocabulary. +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct StyleSpan { + /// Byte range this style covers. + pub range: ByteRange, + /// The resolved style (syntax highlight ∘ faces ∘ theme). + pub style: crate::cell::Style, +} + +/// T M11.4 — one dirty byte region of an `InstanceMessage::StyleSpans` +/// frame and the styling now covering it. The semantic analog of a +/// `CellDelta` changed-run: the frontend clears styling within +/// `range` and applies `spans` (already clipped to `range`). `spans` +/// is every current span intersecting `range`, not only changed ones, +/// so an unchanged span overlapping the dirty region is preserved. +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct StyleSegment { + /// The byte region the frontend should clear and repaint. + pub range: ByteRange, + /// Spans intersecting `range`, each clipped to it. + pub spans: Vec, +} + +/// What a [`Decoration`] region *means*. Provisional variant set (see +/// the module-section note above). Peer selection is deliberately +/// absent — it stays on the `PresenceUpdate` path. +#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +pub enum DecorationKind { + /// LSP diagnostic, error severity. + DiagnosticError, + /// LSP diagnostic, warning severity. + DiagnosticWarning, + /// LSP diagnostic, information severity. + DiagnosticInfo, + /// LSP diagnostic, hint severity. + DiagnosticHint, + /// The local selection region. + Selection, + /// A non-active search match. + SearchMatch, + /// The currently-focused search match. + SearchMatchActive, + /// The line containing the cursor. + CurrentLine, +} + +/// A byte range tagged with what it means. The frontend decides how +/// to paint each [`DecorationKind`] (squiggle, highlight, gutter +/// mark) — the instance only states the fact. +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct Decoration { + /// Byte range the decoration covers. + pub range: ByteRange, + /// What the region signifies. + pub kind: DecorationKind, +} + +/// T M11.4 — `StyleSegment`'s analog for the `Decorations` family: +/// one dirty byte region and the decorations now covering it (every +/// current decoration intersecting `range`, clipped to it). +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct DecorationSegment { + /// The byte region the frontend should clear and repaint. + pub range: ByteRange, + /// Decorations intersecting `range`, each clipped to it. + pub decorations: Vec, +} + +/// Where an [`InlineAdornment`] sits relative to its anchor offset. +#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +pub enum AdornmentPlacement { + /// On its own, before the line containing `at`. + BeforeLine, + /// At the end of the line containing `at`. + EndOfLine, + /// Inline, exactly at the byte offset `at`. + AtOffset, +} + +/// Adornment payload: either inline styled text, or a handle into a +/// previously-sent [`InstanceMessage::ResourceOffer`] so out-of-band +/// content (images, blame avatars) is shipped once, not per frame. +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +pub enum AdornmentContent { + /// Styled virtual text. + Text { + /// The virtual text to display. + text: String, + /// Its style. + style: crate::cell::Style, + }, + /// A handle into a `ResourceOffer`. + Resource { + /// The offered resource's handle. + handle: u64, + }, +} + +/// Virtual text occupying no document bytes (inlay hints, blame, +/// lens). Anchored at a single offset; the frontend interleaves it +/// at layout time. +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct InlineAdornment { + /// Buffer byte offset this adornment anchors to. + pub at: u64, + /// Placement relative to `at`. + pub placement: AdornmentPlacement, + /// What to render. + pub content: AdornmentContent, +} + +/// Content occupying its own vertical band (diff zones, folded-region +/// placeholders). Anchored to the offset of the line it precedes or +/// replaces. `replaces` is `Some` when the band stands in for a +/// collapsed region (the frontend renders the placeholder instead of +/// that range), `None` for an additive band. The frontend allocates +/// the vertical space — the instance never dictates pixel height. +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct BlockAdornment { + /// Buffer byte offset of the line this band precedes/replaces. + pub at: u64, + /// The byte range this band stands in for, if it replaces one. + pub replaces: Option, + /// What to render in the band. + pub content: AdornmentContent, +} + +/// The body of an [`InstanceMessage::ResourceOffer`] — inline bytes +/// for small payloads, or a URI the frontend resolves itself for +/// large or remote resources. +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +pub enum ResourceBody { + /// The resource bytes, carried inline. + Inline(Vec), + /// A URI the frontend fetches/resolves on its own. + Uri(String), +} +// --------------------------------------------------------------------------- +// Handshake — version, identity, capabilities +// --------------------------------------------------------------------------- + +/// Wire-protocol version. Bumped on any breaking change to the +/// `Hello` / `AttachRequest` / event-message shapes. +/// +/// The handshake compares against [`SUPPORTED_PROTOCOL_VERSIONS`]; +/// mismatches close the connection with +/// [`GoodbyeReason::VersionMismatch`]. v1.0 servers and clients accept +/// either the v0.1 wire (version 1) or the v1.0 wire (version 2) per +/// `§sec:m10-backward-compat` — both directions of the version +/// asymmetry need symmetric relaxation so v0.1-era binaries connect +/// to v1.0-era binaries (and vice versa) once both have shipped. +/// +/// T M10.5: bumped from 1 to 2. The v0.1 wire (version 1) remains +/// accepted by v1.0 binaries; CRDT-only message variants +/// (`InstanceMessage::CrdtOp`, `FrontendEvent::CrdtOp`) are filtered +/// per-session for v1 negotiated sessions. +/// +/// T M11.1: bumped from 2 to 3. The v1.0 wire (version 2) remains +/// accepted; the semantic-frontend variant family +/// (`InstanceMessage::StyleSpans` … `ResourceOffer`, +/// `FrontendEvent::Viewport`) is filtered per-session for sessions +/// that did not negotiate `semantic_render`. Mechanically identical +/// to the M10.5 bump: the slice-membership handshake check (not +/// strict equality) means v0.1/v1.0 binaries keep connecting +/// unchanged, and the new variants simply existing in the enums is +/// not a wire-compat issue for non-semantic sessions because the +/// daemon never emits them to those sessions. +pub const PROTOCOL_VERSION: u32 = 3; + +/// T M10.5: the set of protocol versions a v1.0 binary accepts on +/// the wire. v0.1 binaries only accepted `[1]`; v1.0 binaries accept +/// `[1, 2]` so the version asymmetry the §sec:m10-backward-compat +/// spec section describes is handled symmetrically on both sides. +/// +/// T M11.1: extended to `[1, 2, 3]`. v1.1 binaries accept the v0.1 +/// (1), v1.0 (2), and semantic-frontend (3) wires. The check remains +/// slice membership — "is the peer's `protocol_version` present in +/// this slice?" — not strict equality on `PROTOCOL_VERSION`. The +/// session's negotiated version (the peer's) is recorded for +/// downstream filtering: v1 sessions don't receive +/// `InstanceMessage::CrdtOp` / `PresenceUpdate` messages even from +/// a v3 daemon, and only sessions that negotiated `semantic_render` +/// receive the `SemanticFrame` variant family. +pub const SUPPORTED_PROTOCOL_VERSIONS: &[u32] = &[1, 2, 3]; + +/// T M10.5: predicate for the handshake check. Returns `true` if +/// `peer_version` is in [`SUPPORTED_PROTOCOL_VERSIONS`]. +#[must_use] +pub fn is_supported_protocol_version(peer_version: u32) -> bool { + SUPPORTED_PROTOCOL_VERSIONS.contains(&peer_version) +} + +/// Identifies an instance for client-side display. +/// +/// Sent inside [`Hello`] from instance to frontend. Use of `uptime_secs` +/// instead of an absolute start time is deliberate: instance and +/// frontend may run on machines whose clocks disagree, so the frontend +/// computes "instance has been running N seconds" using only the +/// instance's view of time. +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct InstanceIdentity { + /// Pmacs version string (`env!("CARGO_PKG_VERSION")`). + pub pmacs_version: String, + /// Short git hash if the build embedded one. `None` for releases or + /// source-tarball builds where no git checkout was available. + pub build_hash: Option, + /// The name the instance was launched under (`--socket NAME`). + /// `None` for the default daemon (no `--socket` argument). + pub instance_name: Option, + /// Seconds since the instance started, from the instance's clock. + /// Frontend displays "running 47m" by interpreting this against + /// its own notion of "now," avoiding cross-machine clock skew. + pub uptime_secs: u64, + /// Working directory the instance is running in. Encoded as a + /// UTF-8 string; non-UTF-8 paths are rejected at the boundary. + pub working_directory: String, +} + +impl InstanceIdentity { + /// Build an identity for the running pmacs process. + /// + /// `instance_name` is the user-facing name (typically the + /// `--socket NAME` value for the daemon path; `None` for the + /// in-process Local mode and the unnamed default daemon). + /// `started` is the wall-clock anchor used to compute + /// [`Self::uptime_secs`]; the elapsed seconds are evaluated at the + /// call site, so calling twice on different days surfaces different + /// uptimes from the same anchor. + /// + /// The version comes from `CARGO_PKG_VERSION` and the build hash + /// from the optional `PMACS_GIT_HASH` environment variable populated + /// by the build script. + #[must_use] + pub fn for_running_process(instance_name: Option, started: std::time::Instant) -> Self { + Self { + pmacs_version: env!("CARGO_PKG_VERSION").into(), + build_hash: option_env!("PMACS_GIT_HASH").map(String::from), + instance_name, + uptime_secs: started.elapsed().as_secs(), + working_directory: std::env::current_dir() + .ok() + .map(|p| p.to_string_lossy().into_owned()) + .unwrap_or_default(), + } + } +} + +/// Capabilities the instance advertises to attaching frontends. +/// +/// Empty for v0.1; the type exists so that adding capabilities in v0.2+ +/// is not a breaking-change. Symmetric with [`FrontendCapabilities`]. +/// +/// T M10.5: added `multi_frontend` and `crdt_replica` bits with +/// `#[serde(default)]` so v1 wire bytes still deserialize. The +/// negotiation logic (which side advertises what, and what the +/// instance does with mismatches) is M10.7 scope; M10.5 just makes +/// the bit positions stable in the wire format. +/// +/// T M10.5/8: bit defaults evolve with the substrate. +/// +/// - M10.5 declared the bits with `#[serde(default)]` so v1 wire +/// bytes deserialize forward-compatibly. M10.5–M10.7 set both bits +/// to `false` so a frontend declaring `multi_frontend: true` got +/// `Goodbye(CapabilityMismatch)` — the multi-frontend path +/// wasn't actually wired yet. +/// - **T M10.8 Day 4 flip**: the instance's `multi_frontend` and +/// `crdt_replica` defaults flip to `true`. This is the "M10.8 enables +/// multi-frontend" moment — the underlying dispatcher (Day 3) and +/// broadcast routing (Day 4) support both capabilities, so the +/// instance advertises them. +/// +/// The frontend-side defaults remain `false` (a frontend that omits +/// the field is conservatively treated as not supporting the +/// capability; matches v0.1 wire-format semantics). +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct InstanceCapabilities { + /// T M10.5: instance can host multi-frontend sessions on the + /// same buffer (per `§sec:m10-collab`). T M10.8 Day 4: default + /// flipped to `true` — the dispatcher supports multiple + /// attached frontends. + #[serde(default = "default_true")] + pub multi_frontend: bool, + /// T M10.5: instance can broadcast `InstanceMessage::CrdtOp` + /// messages. T M10.8 Day 4: default flipped to `true` — the + /// broadcast routing for CRDT ops wires up in this milestone. + #[serde(default = "default_true")] + pub crdt_replica: bool, + /// T M11.1: instance can produce the semantic-frontend variant + /// family (`InstanceMessage::StyleSpans` … `ResourceOffer`) and + /// consume `FrontendEvent::Viewport`. + /// + /// T M11.1 declared the bit position + negotiation mechanics with + /// the default `false` (no producer yet). T M11.2 landed the + /// instance-side projection seam (`SemanticRenderState`) and + /// flipped the default to `cfg!(feature = "crdt")`: the instance + /// now advertises `semantic_render` on CRDT builds. It tracks the + /// `crdt` feature rather than being unconditional because the + /// negotiation dependency rule makes a semantic session + /// necessarily a text replica — a non-CRDT build can host + /// neither. See [`Default`] impl below. + #[serde(default)] + pub semantic_render: bool, +} + +// Clippy in non-CRDT builds notes that `cfg!(feature = "crdt")` +// evaluates to `false`, making this impl derivable. In CRDT builds +// the values are `true`, so the impl is genuinely manual. Allow. +#[allow(clippy::derivable_impls)] +impl Default for InstanceCapabilities { + fn default() -> Self { + // T M10.10 — the `crdt_replica` default tracks the `crdt` + // Cargo feature. A daemon built without the `crdt` feature + // can't honor a `crdt_replica: true` negotiation (the + // CRDT-handling code paths are conditionally compiled out + // — `send_buffer_snapshots`, `apply_remote_crdt_op`, the + // dispatcher's CursorByte emit). Advertising `true` + // unconditionally would be wire-protocol false advertising. + // + // `multi_frontend` is conceptually independent of CRDT but + // in M10.10's architecture every multi-frontend participant + // is also a CRDT replica; gating both on the same feature + // keeps the daemon's advertised capabilities consistent + // with what it can actually do. + // + // T M11.1 declared `semantic_render` defaulting to `false` + // unconditionally — no projection-seam producer existed, so + // advertising it would have been wire-protocol false + // advertising (the M10.5→M10.7 "bits false until the path is + // wired" discipline). + // + // T M11.2 — **the flip**: the instance-side projection seam + // (`SemanticRenderState`, the producer) has landed and the + // dispatcher selects it per session, so the instance now + // advertises `semantic_render`. It tracks `cfg!(feature = + // "crdt")` like `crdt_replica` because the negotiation + // dependency rule makes a semantic session necessarily a + // text replica; a non-CRDT build can host neither. This is + // the "M11.2 enables semantic" moment, exactly analogous to + // the M10.8 Day-4 multi_frontend/crdt_replica flip. + Self { + multi_frontend: cfg!(feature = "crdt"), + crdt_replica: cfg!(feature = "crdt"), + semantic_render: cfg!(feature = "crdt"), + } + } +} + +#[allow(clippy::missing_const_for_fn)] +fn default_true() -> bool { + true +} + +/// Capabilities the frontend advertises to the instance. +/// +/// All bools default to `false` so a frontend that omits a field via an +/// older `AttachRequest` is conservatively treated as not supporting +/// the capability. New capabilities added in v0.2+ get +/// `#[serde(default)]` so old wire bytes still deserialize. +// A capability set is exactly the case `struct_excessive_bools` warns +// against — but each flag is independent and the alternative (an enum +// or bitset) loses the per-field `#[serde(default)]` semantics that +// make schema evolution work. +#[allow(clippy::struct_excessive_bools)] +#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct FrontendCapabilities { + /// Frontend understands DEC 2026 `BeginSynchronizedUpdate` / + /// `EndSynchronizedUpdate` markers. Instance strips them when false. + #[serde(default)] + pub synchronized_output: bool, + /// Frontend can render Unicode beyond the Basic Multilingual Plane. + /// Instance can substitute a fallback glyph when false. + #[serde(default)] + pub unicode_smp: bool, + /// Frontend supports 24-bit color (truecolor SGR sequences). + /// Instance maps to the 256-color palette when false. + #[serde(default)] + pub true_color: bool, + /// Frontend captures and forwards mouse events. + #[serde(default)] + pub mouse: bool, + /// Frontend supports bracketed paste — distinguishes pasted bytes + /// from typed bytes. Instance treats all input as keystrokes when false. + #[serde(default)] + pub bracketed_paste: bool, + /// Optional human-readable terminal identifier for logs and + /// debugging only. The instance does not branch on this value; + /// branching is done on the explicit capability bits above. + #[serde(default)] + pub terminal_kind: Option, + /// T M10.5: frontend can participate in multi-frontend sessions + /// (per `§sec:m10-collab`). false for v0.1 frontends — they + /// attach as single-frontend and never receive `CrdtOp` / + /// `PresenceUpdate` broadcasts. v1.0 frontends opt in via M10.7's + /// negotiation handshake. M10.5 declares the bit position; M10.7 + /// wires the negotiation. + /// + /// Default is `false` — v1 frontends are treated as not + /// supporting this feature, which matches reality (v1 frontends + /// have no local CRDT state). A `true` default would have v1 + /// frontends claimed to support features they don't. + #[serde(default)] + pub multi_frontend: bool, + /// T M10.5: frontend can apply incoming `CrdtOp` messages to a + /// local CRDT state. false for v0.1; v1.0 opts in. M10.7 wires + /// negotiation; M10.5 declares the bit position. + #[serde(default)] + pub crdt_replica: bool, + /// T M11.1: frontend is a semantic (layout-local) renderer — it + /// consumes the `InstanceMessage::StyleSpans` … `ResourceOffer` + /// family and emits `FrontendEvent::Viewport`. false for v0.1 and + /// v1.0 grid/TUI frontends; a future GPU/GUI frontend opts in. + /// + /// A semantic frontend is *required* to also be a text replica: + /// the semantic frame ships no text, so the frontend must hold + /// the rope locally via the `crdt_replica` machinery. This + /// dependency is enforced in [`negotiate_capabilities`], not just + /// documented — declaring `semantic_render: true` without + /// `crdt_replica: true` is a capability mismatch, never a silent + /// degrade. + #[serde(default)] + pub semantic_render: bool, +} + +/// T M10.7 — the negotiated capability bits for one attached session. +/// +/// Computed by [`negotiate_capabilities`] from the frontend's +/// [`FrontendCapabilities`] and the instance's [`InstanceCapabilities`]. +/// Each negotiated bit is the AND of the two declared bits. Fields +/// added here in future milestones append at the end with sensible +/// defaults so existing call sites stay valid. +/// +/// This is a daemon-internal struct (not on the wire); the +/// negotiation result is communicated to the frontend via the +/// success of the handshake (no capability-mismatch `Goodbye`) and +/// the instance's behavior thereafter. +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub struct NegotiatedCapabilities { + /// Session is eligible for multi-frontend operation. True iff + /// both the frontend and the instance declared `multi_frontend = + /// true`. v0.1 frontends always end up here as `false` (the v0.1 + /// wire format does not carry the field; `#[serde(default)]` + /// makes the deserialized value `false`). + pub multi_frontend: bool, + /// Session can produce/consume `InstanceMessage::CrdtOp` / + /// `FrontendEvent::CrdtOp`. True iff both sides declared + /// `crdt_replica = true`. The daemon's outgoing-message filter for + /// `CrdtOp` consults this in M10.8. + pub crdt_replica: bool, + /// T M11.1 — session uses the semantic projection: it + /// produces/consumes the `InstanceMessage::StyleSpans` … + /// `ResourceOffer` family and `FrontendEvent::Viewport`. True iff + /// both sides declared `semantic_render = true` *and* the session + /// also negotiated `crdt_replica = true` (a semantic session is a + /// text replica; see [`negotiate_capabilities`]). The daemon's + /// per-session outgoing filter gates the entire semantic family + /// on this bit — wired with the producer in M11.2. + pub semantic_render: bool, +} + +/// T M10.7 — pure-function capability negotiation. +/// +/// For each negotiated bit (`multi_frontend`, `crdt_replica`, +/// `semantic_render`): +/// +/// | Frontend wants | Instance has | Result | +/// |----------------|--------------|--------| +/// | `false` | `false` | bit `false`, no error | +/// | `false` | `true` | bit `false`, no error | +/// | `true` | `true` | bit `true`, no error | +/// | `true` | `false` | bit appears in `missing` | +/// +/// If any bit ends up in `missing`, the negotiation fails as a whole +/// (returns `Err`). Otherwise the negotiated bits are returned as +/// [`NegotiatedCapabilities`]. The `Err` form gathers ALL missing +/// bits into one `CapabilityMismatch` — one round-trip carries the +/// complete picture rather than serial rejections. Missing bits are +/// ordered `multi_frontend`, `crdt_replica`, `semantic_render` for +/// deterministic wire output. +/// +/// # T M11.1 — the `semantic_render ⇒ crdt_replica` dependency +/// +/// A semantic-render session ships no text on the semantic frame; +/// the frontend holds the rope locally via the `crdt_replica` +/// machinery (`BufferSnapshot` to bootstrap, `CrdtOp` to stay live). +/// So `semantic_render` is only coherent on a session that also +/// negotiated `crdt_replica`. When the AND-rule would yield +/// `semantic_render = true` but the session did not also negotiate +/// `crdt_replica = true`, this function rejects with +/// `"semantic_render"` in `missing` rather than silently degrading +/// the session to a text-only replica. The rejected identifier is +/// `"semantic_render"` (the capability whose precondition is unmet), +/// not `"crdt_replica"`. +/// +/// # Wire-format stability +/// +/// The strings emitted into `missing` are exactly the +/// `FrontendCapabilities` field names (`"multi_frontend"`, +/// `"crdt_replica"`). These are stable wire-format identifiers, not +/// human-readable descriptions. User-facing translation is the +/// frontend's responsibility (see [`AttachError`]'s `Display` impl). +/// Renaming a capability bit requires updating both the field name +/// and the missing-string emission here in lockstep. +pub fn negotiate_capabilities( + frontend: &FrontendCapabilities, + instance: &InstanceCapabilities, +) -> Result { + let mut missing = Vec::new(); + let multi_frontend = match (frontend.multi_frontend, instance.multi_frontend) { + (true, false) => { + missing.push("multi_frontend".to_string()); + false + } + (a, b) => a && b, + }; + let crdt_replica = match (frontend.crdt_replica, instance.crdt_replica) { + (true, false) => { + missing.push("crdt_replica".to_string()); + false + } + (a, b) => a && b, + }; + let semantic_render = match (frontend.semantic_render, instance.semantic_render) { + (true, false) => { + missing.push("semantic_render".to_string()); + false + } + (a, b) => a && b, + }; + // T M11.1 — dependency rule. A semantic session is a text replica + // (the semantic frame carries no text). If both sides declared + // `semantic_render` but the session did not also negotiate + // `crdt_replica`, reject rather than silently degrade. Guard + // against a duplicate push: the only path where `semantic_render` + // is already in `missing` is the `(true, false)` arm above, which + // also sets the local `semantic_render` to false, so the + // condition below cannot re-fire for that case — but the explicit + // membership check keeps this robust against future reordering. + if semantic_render && !crdt_replica && !missing.iter().any(|m| m == "semantic_render") { + missing.push("semantic_render".to_string()); + } + let semantic_render = semantic_render && crdt_replica; + if missing.is_empty() { + Ok(NegotiatedCapabilities { + multi_frontend, + crdt_replica, + semantic_render, + }) + } else { + Err(GoodbyeReason::CapabilityMismatch { missing }) + } +} + +/// First message sent by the instance to a freshly-attached frontend. +/// +/// Sent immediately after the connection is accepted, before reading +/// the frontend's [`AttachRequest`]. The frontend uses +/// `instance_identity` for status display and `protocol_version` / +/// `instance_capabilities` for compatibility decisions. +/// +/// The instance also stamps the `assigned_frontend_id` which the +/// frontend will use as the `FrontendId` on every event it sends. +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct Hello { + /// The instance's `PROTOCOL_VERSION`. + pub protocol_version: u32, + /// `FrontendId` assigned to this attachment by the instance. The + /// frontend stamps this onto subsequent events. v0.1 daemons start + /// allocation at `FrontendId(2)` (1 reserved for the in-process TUI). + pub assigned_frontend_id: FrontendId, + /// Instance self-identification (version, name, uptime, cwd). + pub instance_identity: InstanceIdentity, + /// Instance capabilities. Empty for v0.1. + pub instance_capabilities: InstanceCapabilities, +} + +/// First message sent by a frontend after receiving [`Hello`]. +/// +/// Carries the frontend's view of the protocol version, the +/// capabilities it can support, and its initial terminal size. On +/// version mismatch the instance closes with +/// [`GoodbyeReason::VersionMismatch`] and no further messages flow. +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct AttachRequest { + /// The frontend's `PROTOCOL_VERSION`. + pub protocol_version: u32, + /// Frontend capabilities. Defaults to all-false if omitted. + #[serde(default)] + pub frontend_capabilities: FrontendCapabilities, + /// The frontend's terminal size at attach time. Authoritative + /// until the frontend sends a [`FrontendEvent::Resize`]. The + /// instance uses this for the initial full-grid render. + pub initial_size: CellSize, +} diff --git a/src/protocol.rs b/src/protocol.rs index 7710364..8caa2e2 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -40,895 +40,25 @@ //! that touches `crossterm`. SSH transports do not use this submodule; //! they decode the wire directly into [`KeyEvent`] / [`MouseEvent`]. -use crate::cell::{Cell, CellCoord, CellSize, DiffSpan}; +// Cell wire types reach this module through the `pub use +// pmacs_protocol::*` block below; the `crossterm_translate` +// submodule's `use super::{CellCoord, ...}` resolves through that +// glob. use std::path::PathBuf; // --------------------------------------------------------------------------- -// Frontend identity, byte ranges — re-exports from `pmacs-protocol` +// Wire types — re-exports from `pmacs-protocol` // --------------------------------------------------------------------------- -// Session 1 of the `pmacs-gpu` arc moved the wire-types subset of -// this module into the `pmacs-protocol` crate. See -// `docs/pmacs-gpu-design.md`. Existing `crate::protocol::FrontendId` -// / `crate::protocol::ByteRange` imports resolve through these -// re-exports; new consumers (`pmacs-gpu`, debug tools) should depend -// on `pmacs-protocol` directly. -pub use pmacs_protocol::{ByteRange, FrontendId}; - -// --------------------------------------------------------------------------- -// Key encoding -// --------------------------------------------------------------------------- - -/// Key code, normalized away from any specific terminal protocol. -/// -/// `Char` covers printable input. The named variants cover the keys -/// terminals report distinctly (arrows, function keys, etc.). `Unknown` -/// is the escape hatch: a key the protocol layer cannot encode in -/// any of the named variants is preserved as a u32 sentinel so it -/// can round-trip through serialization without becoming an error. -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)] -pub enum Key { - /// A printable character. The character is the user-visible - /// codepoint after layout / IME processing. - Char(char), - /// A function key. `n` is 1-based: `F(1)` is F1. - F(u8), - /// Backspace / `^H`. - Backspace, - /// Enter / Return / `^M`. - Enter, - /// Left arrow. - Left, - /// Right arrow. - Right, - /// Up arrow. - Up, - /// Down arrow. - Down, - /// Home key. - Home, - /// End key. - End, - /// Page Up. - PageUp, - /// Page Down. - PageDown, - /// Tab. - Tab, - /// Shift-Tab. - BackTab, - /// Forward delete. - Delete, - /// Insert. - Insert, - /// Escape. - Escape, - /// Caps Lock. - CapsLock, - /// Scroll Lock. - ScrollLock, - /// Num Lock. - NumLock, - /// Print Screen. - PrintScreen, - /// Pause / Break. - Pause, - /// Menu / context-menu key. - Menu, - /// Numeric-keypad center key. - KeypadBegin, - /// The "null" keycode (terminal-protocol artifact). - Null, - /// A key the protocol layer does not recognize. The `u32` - /// preserves whatever sentinel value the upstream layer attached - /// (e.g. a media-key code from kitty's keyboard protocol). Round-trips - /// through serialization but is not actionable by commands. - Unknown(u32), -} - -/// Modifier-key set. Bit-flag encoding for compact wire shape. -/// -/// `META` corresponds to the "logo" / "super" key on most keyboards. -/// `HYPER` is reserved for the rare keyboards that distinguish it -/// from `META` (kitty's keyboard protocol surfaces both). -#[derive( - Copy, Clone, Eq, PartialEq, Hash, Debug, Default, serde::Serialize, serde::Deserialize, -)] -pub struct Modifiers(u8); - -impl Modifiers { - /// Empty set: no modifiers held. - pub const NONE: Modifiers = Modifiers(0); - /// Shift. - pub const SHIFT: Modifiers = Modifiers(1 << 0); - /// Control. - pub const CTRL: Modifiers = Modifiers(1 << 1); - /// Alt / Option. - pub const ALT: Modifiers = Modifiers(1 << 2); - /// Meta / Super / Logo / Command. - pub const META: Modifiers = Modifiers(1 << 3); - /// Hyper. Distinguished from `META` only on keyboards that - /// surface both (kitty's keyboard protocol). - pub const HYPER: Modifiers = Modifiers(1 << 4); - - /// Construct from a raw bit set. Bits outside the defined range - /// are silently masked off so a future-extended wire cannot smuggle - /// undefined bits past current decoders. - #[must_use] - pub const fn from_bits_truncate(bits: u8) -> Self { - Self(bits & 0b0001_1111) - } - - /// Raw bit set. - #[must_use] - pub const fn bits(self) -> u8 { - self.0 - } - - /// Whether `self` includes every bit set in `other`. - #[must_use] - pub const fn contains(self, other: Modifiers) -> bool { - (self.0 & other.0) == other.0 - } - - /// Whether no modifiers are held. - #[must_use] - pub const fn is_empty(self) -> bool { - self.0 == 0 - } -} - -impl std::ops::BitOr for Modifiers { - type Output = Modifiers; - fn bitor(self, rhs: Modifiers) -> Modifiers { - Modifiers(self.0 | rhs.0) - } -} - -impl std::ops::BitOrAssign for Modifiers { - fn bitor_assign(&mut self, rhs: Modifiers) { - self.0 |= rhs.0; - } -} - -/// A keyboard event. -#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] -pub struct KeyEvent { - /// Frontend that produced the event. - pub frontend_id: FrontendId, - /// The key code. - pub key: Key, - /// Modifier set held when the key was pressed. - pub mods: Modifiers, - /// Monotonic timestamp at which the frontend captured the event. - /// Zero means "no timestamp available" (e.g. test-synthesized - /// events). - pub timestamp_ns: u64, -} - -// --------------------------------------------------------------------------- -// Mouse encoding -// --------------------------------------------------------------------------- - -/// Mouse button. -#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] -pub enum MouseButton { - /// Left button. - Left, - /// Right button. - Right, - /// Middle button. - Middle, -} - -/// Kind of mouse interaction. -#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] -pub enum MouseKind { - /// Button pressed. - Down(MouseButton), - /// Button released. - Up(MouseButton), - /// Drag with the named button held. - Drag(MouseButton), - /// Pointer moved with no button held. - Move, - /// Wheel scrolled up. - ScrollUp, - /// Wheel scrolled down. - ScrollDown, - /// Wheel scrolled left. - ScrollLeft, - /// Wheel scrolled right. - ScrollRight, -} - -/// A mouse event. -#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] -pub struct MouseEvent { - /// Frontend that produced the event. - pub frontend_id: FrontendId, - /// Kind of mouse interaction. - pub kind: MouseKind, - /// Cell-grid coordinate of the pointer at the moment of the event. - pub coord: CellCoord, - /// Modifiers held during the event. - pub mods: Modifiers, -} - -// --------------------------------------------------------------------------- -// Frontend → Instance events -// --------------------------------------------------------------------------- - -/// Input event from frontend to instance. -#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] -pub enum FrontendEvent { - /// A key event. - Key(KeyEvent), - /// A mouse event. - Mouse(MouseEvent), - /// Frontend's terminal resized. - Resize { - /// Frontend that resized. - frontend_id: FrontendId, - /// New size, in cells. - size: CellSize, - }, - /// Bracketed-paste payload from the frontend. - Paste { - /// Frontend that produced the paste. - frontend_id: FrontendId, - /// Raw bytes pasted (the instance decodes as UTF-8 if relevant). - data: Vec, - }, - /// Frontend gained input focus. - FocusGained(FrontendId), - /// Frontend lost input focus. - FocusLost(FrontendId), - /// Frontend is going away. Instance treats this as immediate - /// detach; no acknowledgement required. - Detach(FrontendId), - /// T M10.5: CRDT operation produced by this frontend's local - /// edit, sent to the instance for broadcast to the other - /// attached frontends. The actual flow that produces these - /// (frontend maintaining a local CRDT state, applying edits - /// optimistically, sending the resulting op) is wired in M10.8 - /// + M10.10; M10.5 declares the wire shape so the protocol - /// version bump (1 → 2) covers it. - /// - /// Only sent by v1.0 frontends (`protocol_version = 2`); v0.1 - /// frontends never emit this variant. Sessions negotiated at - /// protocol version 1 must NOT receive this on the - /// instance-side dispatcher (the daemon filters per-session; - /// the editor-core treats it as an unknown frontend event if - /// it ever arrives from a v1 session, which it shouldn't). - CrdtOp { - /// Which attached frontend produced this op. The instance - /// uses this to avoid echoing the op back to its sender. - frontend_id: FrontendId, - /// Which buffer this op affects. The instance routes the - /// op to that buffer's CRDT state. - buffer_id: crate::buffer::BufferId, - /// The CRDT operation payload — `peer_id` + opaque wire bytes - /// loro's `import_updates` decodes. - op: crate::rope::CrdtOp, - }, - /// T M11.1: the buffer byte range a semantic frontend currently - /// has on screen, in buffer coordinates. Replaces the - /// instance-derived grid viewport for `semantic_render` sessions: - /// the instance scopes its `StyleSpans` / `Decorations` / … to - /// this range rather than shipping a whole file's styling. - /// - /// **No pixels.** This carries a byte range, never viewport pixel - /// size, DPI, font metrics, or glyph advances — the contract - /// boundary invariant from the semantic-frontend design note. The - /// frontend owns all visual-motion semantics and resolves - /// pixel→offset locally; there is deliberately no hit-test - /// request variant and no `SemanticResize`, both of which would - /// leak pixels across the boundary. - /// - /// `generation` ties the declared range to a CRDT version so the - /// instance can ignore a viewport that races a not-yet-applied - /// edit (symmetric with `StyleSpans::generation`). - /// - /// Only emitted by sessions that negotiated `semantic_render`; - /// a non-semantic session never sends it. M11.1 declares the - /// wire shape; the instance-side consumer is wired with the - /// projection seam in M11.2. - Viewport { - /// Which frontend's viewport this is. - frontend_id: FrontendId, - /// Which buffer the visible range indexes into. - buffer_id: crate::buffer::BufferId, - /// Half-open byte range currently on screen. - visible: ByteRange, - /// CRDT generation the frontend computed `visible` against. - generation: u64, - }, -} - -impl FrontendEvent { - /// The frontend that produced this event. - #[must_use] - pub fn frontend_id(&self) -> FrontendId { - match self { - Self::Key(e) => e.frontend_id, - Self::Mouse(e) => e.frontend_id, - Self::Resize { frontend_id, .. } - | Self::Paste { frontend_id, .. } - | Self::FocusGained(frontend_id) - | Self::FocusLost(frontend_id) - | Self::Detach(frontend_id) - | Self::CrdtOp { frontend_id, .. } - | Self::Viewport { frontend_id, .. } => *frontend_id, - } - } -} - -// --------------------------------------------------------------------------- -// Instance → Frontend messages -// --------------------------------------------------------------------------- - -/// Cursor position and visibility. -#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] -pub struct CursorState { - /// Cell where the cursor should be drawn. - pub coord: CellCoord, - /// Whether the cursor is visible at all. - pub visible: bool, -} - -/// Instance-level signal that is not a render message. -#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] -pub enum InstanceSignal { - /// Terminal bell. - Bell, - /// Window-title change request. - Title(String), - /// Clipboard set request (OSC 52). - Clipboard(Vec), -} - -/// Reason an instance terminates an attachment. -/// -/// Only the four variants the v0.1 daemon actually emits or rejects on. -/// `Evicted` (multi-frontend takeover) and similar will land alongside -/// the v0.3 multi-frontend work; until then `AlreadyAttached` covers -/// the single-slot equivalent. -#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] -pub enum GoodbyeReason { - /// Instance is shutting down (SIGTERM / SIGINT or clean exit). - ShuttingDown, - /// Frontend's `protocol_version` does not match the instance's. - /// The handshake fails before any further messages. - VersionMismatch { - /// The instance's `PROTOCOL_VERSION`. - server: u32, - /// The version the frontend announced in its `AttachRequest`. - client: u32, - }, - /// Another frontend is currently attached. v0.1 rejects concurrent - /// attaches; v0.3 will replace this with eviction or multiplexing. - AlreadyAttached, - /// Frontend sent a malformed message or otherwise violated the - /// protocol. The connection is closed without further dialogue. - ProtocolError, - /// T M10.7: frontend declared one or more negotiated capability - /// bits that the instance cannot honor. The handshake fails after - /// the version check but before any further messages. - /// - /// `missing` lists the capability *field names* (e.g., - /// `"multi_frontend"`, `"crdt_replica"`) the frontend requested - /// (`true`) that the instance reports as `false`. These strings - /// are stable wire-format identifiers: they are exactly the - /// `FrontendCapabilities` / `InstanceCapabilities` field names, - /// not human-readable descriptions. The frontend translates them - /// for display via [`AttachError`]'s formatting. Renaming a - /// capability bit requires changing both the field name AND the - /// missing-string emission in `negotiate_capabilities` in - /// lockstep — see the M10.7 audit's wire-format-stability - /// section. - CapabilityMismatch { - /// The capability bit names the frontend asked for that the - /// instance does not support. Each entry is a verbatim - /// `FrontendCapabilities` field name. - missing: Vec, - }, -} - -/// Rendering and signals from instance to frontend. -#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] -pub enum InstanceMessage { - /// Cell deltas. `full_grid = true` is the initial sync sent on - /// fresh attach (or after a resize where the previous grid is no - /// longer applicable); `full_grid = false` is a differential - /// frame. - CellDelta { - /// One run of changed cells per `DiffSpan`. - spans: Vec, - /// Whether `spans` represents a full-grid resync (true on - /// fresh attach or post-resize) versus an incremental frame. - full_grid: bool, - }, - /// Cursor position and visibility update. - Cursor(Option), - /// Modeline cells. Reserved for v0.3 GUI use; v0.1 ships modeline - /// inside [`InstanceMessage::CellDelta`]. The variant exists in - /// the protocol from day one so adding the discrete channel later - /// is not a breaking change. - ModeLine(Vec), - /// Side-channel signal (bell, title, clipboard). - Signal(InstanceSignal), - /// Instance is terminating the attachment. - Goodbye(GoodbyeReason), - /// T M10.5: CRDT operation broadcast from the instance to all - /// attached frontends. The originating frontend produced this op - /// (via `FrontendEvent::CrdtOp` or via a local editor-core edit - /// that synthesizes one); the instance fans it out so every - /// attached frontend can apply the op to its local CRDT state. - /// - /// Only sent to v1.0 frontends — sessions negotiated at - /// `protocol_version = 1` never receive this variant, per - /// `§sec:m10-backward-compat`. The daemon filters at the - /// outgoing-message path; this variant simply existing in the - /// enum is not a wire-compat issue for v1 sessions because the - /// daemon never emits it to them. - /// - /// M10.5 declares the wire shape. M10.8 wires the editor-core → - /// daemon → frontend flow that actually emits these. - CrdtOp { - /// Which buffer this op affects. v1.0 frontends maintain - /// a per-buffer local CRDT state; this routes to the right - /// one. - buffer_id: crate::buffer::BufferId, - /// The CRDT operation payload — `peer_id` + opaque wire bytes - /// loro's `import_updates` decodes. - op: crate::rope::CrdtOp, - }, - /// T M10.6: cursor + selection state of one attached frontend, - /// broadcast to the other v1.0 frontends so they can render - /// peer-presence overlays. Coalesced at the daemon: rapid cursor - /// movement produces one `PresenceUpdate` per tick per source - /// frontend, carrying the *final* state, not intermediate values. - /// - /// Sender exclusion: the source frontend never receives its own - /// `PresenceUpdate`. v0.1 sessions (negotiated `protocol_version = - /// 1`) are filtered out at the daemon's outgoing-message path. - /// - /// M10.6 declares the wire shape AND wires the daemon-side - /// sweep with per-session filter. In single-frontend deployments - /// the recipient list is structurally empty (sender exclusion - /// with no other v2 sessions); M10.8 enables the multi-frontend - /// case where this message actually crosses the wire. The - /// frontend's renderer for peer-cursor overlays is also M10.8. - PresenceUpdate { - /// Which attached frontend this presence belongs to. v1.0 - /// frontends use this to label the peer-cursor overlay - /// ("user 4 is editing here"). - frontend_id: FrontendId, - /// Which buffer the source frontend's cursor is in. - buffer_id: crate::buffer::BufferId, - /// Byte offset of the source frontend's cursor within - /// `buffer_id`. Frontends convert to line/column at render - /// time via the rope's coord-mapping; the wire carries the - /// canonical byte offset to avoid encoding-vs-rendering - /// drift across frontends. - cursor: crate::rope::Position, - /// Active selection range, if any. - selection: Option, - }, - /// T M10.10: bootstrap a frontend's local CRDT replica with the - /// instance's current authoritative state. Sent once per active - /// buffer at `SessionEstablished` time (and on subsequent - /// buffer-creation events) to frontends that negotiated - /// `crdt_replica: true`. Frontends that didn't negotiate the - /// capability never receive this variant — the daemon's - /// outgoing-message filter gates the send on - /// `NegotiatedCapabilities::crdt_replica`. - /// - /// `crdt_snapshot` carries loro's run-encoded snapshot - /// (`CrdtState::export_snapshot()`) — the CRDT-internal state - /// including peer IDs, version vectors, and op-history structure. - /// Raw byte contents are insufficient because a fresh CRDT replica - /// initialized from bytes alone diverges on the first concurrent - /// edit. - /// - /// Cursor position is intentionally absent: cursor is per-frontend - /// window state (M10.8 `FrontendView`), not per-buffer CRDT - /// state. The same buffer can appear in multiple windows on one - /// frontend with different cursors; coupling cursor to - /// `BufferSnapshot` would break this model. - BufferSnapshot { - /// Which buffer's CRDT state this snapshot represents. - buffer_id: crate::buffer::BufferId, - /// `loro::LoroDoc::export(ExportMode::Snapshot)` output. Applied - /// to a fresh `CrdtState::new(peer_id_from_frontend(my_id))` - /// via `import_snapshot(bytes)` on the receiving frontend. - crdt_snapshot: Vec, - }, - /// T M10.10: the active buffer for a replica frontend, with the - /// cursor position within it. - /// - /// # Semantics (Day 3 step 3b composition-check broadened - /// contract) - /// - /// `CursorByte` represents "the active buffer for this frontend - /// is `buffer_id`; the cursor in that buffer is at `byte_pos`." - /// Not just "the cursor moved." This contract matters: a narrow - /// "cursor moved" emission would miss active-buffer-changed- - /// without-cursor-motion events (Lua-driven buffer switch - /// landing at the same byte position), and the frontend's - /// active-buffer tracking would go stale. - /// - /// Daemon emits `CursorByte` on every per-tick render frame for - /// replica frontends, derived fresh from `active_window_for(fid)`. - /// Cursor move, active-buffer change, and active-window change - /// all produce a new emission carrying the current `(buffer_id, - /// byte_pos)`. The per-tick rate (16ms at 60Hz) is the same as - /// `Cursor`'s grid-coord variant. - /// - /// # Why a separate variant from `Cursor` - /// - /// `Cursor` carries grid coordinates (row/col cells) — the - /// frontend uses them to paint the cursor. The optimistic-apply - /// path needs byte position (CRDT insert/delete is byte-indexed), - /// which the grid coordinate can't recover without duplicating - /// the daemon's view-layout logic (tab expansion, line wrap, - /// double-width chars, viewport offset). `CursorByte` is the - /// authoritative byte position for the active buffer. - /// - /// # Atomicity with `Cursor` - /// - /// The daemon emits `Cursor` and `CursorByte` together for - /// replica frontends — both derived from the same render-frame - /// iteration so they describe the cursor in the same instant in - /// two reference frames. Non-replica frontends receive only - /// `Cursor` (existing behavior). The replica frontend that sees - /// `Cursor` without a paired `CursorByte` would interpret stale - /// byte position; the daemon guarantees both emit together by - /// derivation, not by message-protocol atomicity. - /// - /// # Wire-format compatibility - /// - /// New variant in v2; receivers without M10.10 hard-error on - /// decode (postcard does not gracefully degrade unknown variants, - /// per M10.10-FRAMING.md Refinement 3). Capability-gated: daemon - /// sends only to frontends that negotiated `crdt_replica: true`. - /// `PROTOCOL_VERSION` stays at 2. - CursorByte { - /// The buffer the cursor is in. A replica frontend tracks - /// per-buffer cursors; this routes the update to the right - /// entry. - buffer_id: crate::buffer::BufferId, - /// Byte offset of the cursor within `buffer_id`. Source of - /// truth for the optimistic-apply path's insert / delete - /// position arguments. Wire type matches - /// `PresenceUpdate::cursor` (`u64`) for consistency; frontend - /// converts to `usize` for the loro API. - byte_pos: crate::rope::Position, - }, - /// T M11.1 — syntax + face styling over the semantic frontend's - /// current viewport range. `generation` ties the spans to a CRDT - /// version so the frontend can discard styling that predates an - /// edit it has already applied optimistically. Ships **no text** — - /// the frontend holds the rope via the `crdt_replica` machinery - /// and interprets these spans over it. - /// - /// # Diff shape (T M11.4) - /// - /// Mirrors `CellDelta`'s `full_grid` + changed-runs structure, - /// lifted from positional cells to byte-anchored ranges. `full = - /// true` is a resync: the frontend discards all prior styling for - /// `buffer_id` and the `segments` are authoritative for the whole - /// declared viewport (first frame after a `Viewport`, a viewport - /// jump, or a generation discontinuity). `full = false` is - /// incremental: each [`StyleSegment`] replaces styling **only** - /// within its `range`; bytes covered by no segment keep their - /// previously-applied style. A frame whose styling is unchanged - /// ships no `StyleSpans` at all. - /// - /// Because byte offsets cascade on edits (an insert shifts every - /// later span), an incremental frame after an edit dirties - /// `[edit, viewport_end)` — still bounded, and no-edit frames - /// (cursor move, scroll within the declared viewport, selection) - /// cost nothing. Each segment carries *all* current spans - /// intersecting its range (clipped), not only changed ones, so an - /// unchanged span overlapping a dirty range is faithfully - /// reconstructed. - /// - /// Gated on negotiated `semantic_render`; never sent to a grid - /// session (the daemon's per-session outgoing filter — wired with - /// the producer in M11.2 — never emits it there, so postcard's - /// hard-error on unknown variants is mooted exactly as it is for - /// `CursorByte`). - StyleSpans { - /// Buffer these spans interpret. - buffer_id: crate::buffer::BufferId, - /// CRDT generation the spans were computed against. - generation: u64, - /// `true` → discard all prior styling for `buffer_id` first; - /// `segments` are authoritative for the declared viewport. - full: bool, - /// Dirty byte regions and the styling now covering them. - segments: Vec, - }, - /// T M11.1 — diagnostics, search hits, current-line, and any - /// other "this region means something" overlay, as offset ranges - /// plus a kind. Peer selection is **not** here — it stays on the - /// existing `PresenceUpdate` path. Gated on `semantic_render`. - /// - /// T M11.4 — same `full` + segment diff shape as `StyleSpans` - /// (see its docs), and gains `generation` for parity: a frontend - /// wants the CRDT version decorations were computed against for - /// the same optimistic-edit race reason styling does. - Decorations { - /// Buffer these decorations apply to. - buffer_id: crate::buffer::BufferId, - /// CRDT generation the decorations were computed against. - generation: u64, - /// `true` → discard all prior decorations for `buffer_id` - /// first; `segments` are authoritative for the viewport. - full: bool, - /// Dirty byte regions and the decorations now covering them. - segments: Vec, - }, - /// T M11.1 — inlay hints, blame, lens, virtual text. Anchored at - /// a single offset with a placement; occupies no document bytes — - /// the frontend interleaves it at layout time. Gated on - /// `semantic_render`. - InlineAdornments { - /// Buffer these adornments annotate. - buffer_id: crate::buffer::BufferId, - /// The adornment items for the declared viewport. - items: Vec, - }, - /// Coarse whole-file styling summary for a minimap / scrollbar - /// overview, resolving design-note Open Q#2. One [`Style`] per - /// source line (the *dominant* style for that line by byte count - /// across the producer's current spans); the frontend maps minimap - /// rows to one or more of these. Unlike [`Self::StyleSpans`], this - /// is **not** viewport-scoped — the minimap shows the whole file. - /// - /// Recomputed when the buffer's CRDT `generation` advances; an - /// unchanged buffer ships no further summary. A coarser - /// representation (fixed bands) or finer (run-length style runs) - /// is recorded in `docs/semantic-frontend-protocol.md` as future - /// refinements; per-line dominant style is the v1 choice because - /// minimap rows naturally correspond to code lines. - /// - /// Gated on negotiated `semantic_render`; the daemon emits it only - /// for sessions that have a [`crate::semantic_render::SemanticRenderState`] - /// (structural gating, same as every other semantic family). - FileStyleSummary { - /// Buffer this summary describes. - buffer_id: crate::buffer::BufferId, - /// CRDT generation the summary was computed against. The - /// frontend can discard a summary that predates an edit it - /// has already applied optimistically. - generation: u64, - /// One [`crate::cell::Style`] per source line, in line order - /// from line 0. Empty when the buffer is empty. - lines: Vec, - }, - /// T M11.1 — diff zones, folded-region placeholders, anything - /// occupying its own vertical band. Anchored to the offset of the - /// line it precedes or replaces; the frontend allocates the - /// vertical space. Gated on `semantic_render`. - BlockAdornments { - /// Buffer these adornments annotate. - buffer_id: crate::buffer::BufferId, - /// The block items for the declared viewport. - items: Vec, - }, - /// T M11.1 — the instance's authoritative fold set as document - /// facts. Folding is an instance command-semantics concern (Lua - /// can fold); the visual collapse is a frontend layout concern — - /// the frontend renders the placeholder and adjusts its own - /// layout. Gated on `semantic_render`. - FoldState { - /// Buffer whose fold set this is. - buffer_id: crate::buffer::BufferId, - /// Folded byte ranges. - folds: Vec, - }, - /// T M11.1 — out-of-band content an adornment refers to (images, - /// blame avatars). Sent once, referenced by `handle`, so it is - /// not re-shipped per frame. Gated on `semantic_render`. - ResourceOffer { - /// Stable handle adornments reference via - /// [`AdornmentContent::Resource`]. - handle: u64, - /// MIME type of `body`. - mime: String, - /// Inline bytes or a URI the frontend resolves itself. - body: ResourceBody, - }, -} - -/// Flat selection state for the wire. -/// -/// Mirrors [`crate::window::Selection`] but as a self-contained pair -/// of byte offsets — `anchor` is where the selection began, -/// `active` is the current selection cursor. Either may be the -/// numerically larger value; callers wanting `(lo, hi)` order -/// compute it locally. -/// -/// Kept flat (no nested types) so [`PartialEq`] equality is exactly -/// wire-representation equality: two `SelectionSnapshot`s compare -/// equal iff they serialize to identical bytes. The presence-diff -/// sweep relies on this — see [`crate::presence::SessionRegistry`]. -#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] -pub struct SelectionSnapshot { - /// Where the selection began. - pub anchor: crate::rope::Position, - /// The active end (typically the cursor at the moment of the - /// snapshot). - pub active: crate::rope::Position, -} - -// --------------------------------------------------------------------------- -// T M11.1 — Semantic-frontend projection types -// -// The payloads of the `InstanceMessage::StyleSpans` … `ResourceOffer` -// family and `FrontendEvent::Viewport`. Everything is anchored in -// **byte offsets** (consistent with `CursorByte`): line/col is a -// frontend rendering concern, CRDT position is replica-internal. The -// instance never learns a pixel — see the contract boundary in -// `docs/semantic-frontend-protocol.md`. -// -// The variant/kind sets here are provisional and co-evolve within the -// M11 arc behind the `semantic_render` capability + protocol v3, -// exactly as the CRDT op shape evolved M10.5→M10.10 behind -// `crdt_replica`. They are not a wire-compat hazard for non-semantic -// sessions: the daemon's per-session outgoing filter (wired with the -// producer in M11.2) never emits the family to a session that didn't -// negotiate `semantic_render`, so postcard's hard-error on unknown -// variants is mooted exactly as it is for `CursorByte`. -// --------------------------------------------------------------------------- - -// `ByteRange` moved to `pmacs-protocol` (see the re-export near the -// top of this file). - -/// One run of buffer bytes carrying a resolved visual style. The -/// instance is the single syntax/face authority; the frontend lays -/// the style out locally over rope text it already holds. Reuses -/// [`crate::cell::Style`] so the grid and semantic projections share -/// one style vocabulary. -#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] -pub struct StyleSpan { - /// Byte range this style covers. - pub range: ByteRange, - /// The resolved style (syntax highlight ∘ faces ∘ theme). - pub style: crate::cell::Style, -} - -/// T M11.4 — one dirty byte region of an `InstanceMessage::StyleSpans` -/// frame and the styling now covering it. The semantic analog of a -/// `CellDelta` changed-run: the frontend clears styling within -/// `range` and applies `spans` (already clipped to `range`). `spans` -/// is every current span intersecting `range`, not only changed ones, -/// so an unchanged span overlapping the dirty region is preserved. -#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] -pub struct StyleSegment { - /// The byte region the frontend should clear and repaint. - pub range: ByteRange, - /// Spans intersecting `range`, each clipped to it. - pub spans: Vec, -} - -/// What a [`Decoration`] region *means*. Provisional variant set (see -/// the module-section note above). Peer selection is deliberately -/// absent — it stays on the `PresenceUpdate` path. -#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] -pub enum DecorationKind { - /// LSP diagnostic, error severity. - DiagnosticError, - /// LSP diagnostic, warning severity. - DiagnosticWarning, - /// LSP diagnostic, information severity. - DiagnosticInfo, - /// LSP diagnostic, hint severity. - DiagnosticHint, - /// The local selection region. - Selection, - /// A non-active search match. - SearchMatch, - /// The currently-focused search match. - SearchMatchActive, - /// The line containing the cursor. - CurrentLine, -} - -/// A byte range tagged with what it means. The frontend decides how -/// to paint each [`DecorationKind`] (squiggle, highlight, gutter -/// mark) — the instance only states the fact. -#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] -pub struct Decoration { - /// Byte range the decoration covers. - pub range: ByteRange, - /// What the region signifies. - pub kind: DecorationKind, -} - -/// T M11.4 — `StyleSegment`'s analog for the `Decorations` family: -/// one dirty byte region and the decorations now covering it (every -/// current decoration intersecting `range`, clipped to it). -#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] -pub struct DecorationSegment { - /// The byte region the frontend should clear and repaint. - pub range: ByteRange, - /// Decorations intersecting `range`, each clipped to it. - pub decorations: Vec, -} - -/// Where an [`InlineAdornment`] sits relative to its anchor offset. -#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] -pub enum AdornmentPlacement { - /// On its own, before the line containing `at`. - BeforeLine, - /// At the end of the line containing `at`. - EndOfLine, - /// Inline, exactly at the byte offset `at`. - AtOffset, -} - -/// Adornment payload: either inline styled text, or a handle into a -/// previously-sent [`InstanceMessage::ResourceOffer`] so out-of-band -/// content (images, blame avatars) is shipped once, not per frame. -#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] -pub enum AdornmentContent { - /// Styled virtual text. - Text { - /// The virtual text to display. - text: String, - /// Its style. - style: crate::cell::Style, - }, - /// A handle into a `ResourceOffer`. - Resource { - /// The offered resource's handle. - handle: u64, - }, -} - -/// Virtual text occupying no document bytes (inlay hints, blame, -/// lens). Anchored at a single offset; the frontend interleaves it -/// at layout time. -#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] -pub struct InlineAdornment { - /// Buffer byte offset this adornment anchors to. - pub at: u64, - /// Placement relative to `at`. - pub placement: AdornmentPlacement, - /// What to render. - pub content: AdornmentContent, -} - -/// Content occupying its own vertical band (diff zones, folded-region -/// placeholders). Anchored to the offset of the line it precedes or -/// replaces. `replaces` is `Some` when the band stands in for a -/// collapsed region (the frontend renders the placeholder instead of -/// that range), `None` for an additive band. The frontend allocates -/// the vertical space — the instance never dictates pixel height. -#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] -pub struct BlockAdornment { - /// Buffer byte offset of the line this band precedes/replaces. - pub at: u64, - /// The byte range this band stands in for, if it replaces one. - pub replaces: Option, - /// What to render in the band. - pub content: AdornmentContent, -} - -/// The body of an [`InstanceMessage::ResourceOffer`] — inline bytes -/// for small payloads, or a URI the frontend resolves itself for -/// large or remote resources. -#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] -pub enum ResourceBody { - /// The resource bytes, carried inline. - Inline(Vec), - /// A URI the frontend fetches/resolves on its own. - Uri(String), -} +// Session 1 of the `pmacs-gpu` arc moved every wire type out of this +// module into the `pmacs-protocol` crate (`docs/pmacs-gpu-design.md`). +// What stays below are the CLI / binding internals (`AttachTarget` and +// friends, `AttachmentHandle`, the `crossterm_translate` submodule) +// plus the existing wire-format roundtrip tests. The blanket re-export +// keeps every `crate::protocol::*` import path working unchanged; new +// consumers (`pmacs-gpu`, debug tools) should depend on `pmacs-protocol` +// directly. +pub use pmacs_protocol::*; // --------------------------------------------------------------------------- // Attachment @@ -1424,463 +554,6 @@ impl AttachmentHandle { } } -// --------------------------------------------------------------------------- -// Handshake — version, identity, capabilities -// --------------------------------------------------------------------------- - -/// Wire-protocol version. Bumped on any breaking change to the -/// `Hello` / `AttachRequest` / event-message shapes. -/// -/// The handshake compares against [`SUPPORTED_PROTOCOL_VERSIONS`]; -/// mismatches close the connection with -/// [`GoodbyeReason::VersionMismatch`]. v1.0 servers and clients accept -/// either the v0.1 wire (version 1) or the v1.0 wire (version 2) per -/// `§sec:m10-backward-compat` — both directions of the version -/// asymmetry need symmetric relaxation so v0.1-era binaries connect -/// to v1.0-era binaries (and vice versa) once both have shipped. -/// -/// T M10.5: bumped from 1 to 2. The v0.1 wire (version 1) remains -/// accepted by v1.0 binaries; CRDT-only message variants -/// (`InstanceMessage::CrdtOp`, `FrontendEvent::CrdtOp`) are filtered -/// per-session for v1 negotiated sessions. -/// -/// T M11.1: bumped from 2 to 3. The v1.0 wire (version 2) remains -/// accepted; the semantic-frontend variant family -/// (`InstanceMessage::StyleSpans` … `ResourceOffer`, -/// `FrontendEvent::Viewport`) is filtered per-session for sessions -/// that did not negotiate `semantic_render`. Mechanically identical -/// to the M10.5 bump: the slice-membership handshake check (not -/// strict equality) means v0.1/v1.0 binaries keep connecting -/// unchanged, and the new variants simply existing in the enums is -/// not a wire-compat issue for non-semantic sessions because the -/// daemon never emits them to those sessions. -pub const PROTOCOL_VERSION: u32 = 3; - -/// T M10.5: the set of protocol versions a v1.0 binary accepts on -/// the wire. v0.1 binaries only accepted `[1]`; v1.0 binaries accept -/// `[1, 2]` so the version asymmetry the §sec:m10-backward-compat -/// spec section describes is handled symmetrically on both sides. -/// -/// T M11.1: extended to `[1, 2, 3]`. v1.1 binaries accept the v0.1 -/// (1), v1.0 (2), and semantic-frontend (3) wires. The check remains -/// slice membership — "is the peer's `protocol_version` present in -/// this slice?" — not strict equality on `PROTOCOL_VERSION`. The -/// session's negotiated version (the peer's) is recorded for -/// downstream filtering: v1 sessions don't receive -/// `InstanceMessage::CrdtOp` / `PresenceUpdate` messages even from -/// a v3 daemon, and only sessions that negotiated `semantic_render` -/// receive the `SemanticFrame` variant family. -pub const SUPPORTED_PROTOCOL_VERSIONS: &[u32] = &[1, 2, 3]; - -/// T M10.5: predicate for the handshake check. Returns `true` if -/// `peer_version` is in [`SUPPORTED_PROTOCOL_VERSIONS`]. -#[must_use] -pub fn is_supported_protocol_version(peer_version: u32) -> bool { - SUPPORTED_PROTOCOL_VERSIONS.contains(&peer_version) -} - -/// Identifies an instance for client-side display. -/// -/// Sent inside [`Hello`] from instance to frontend. Use of `uptime_secs` -/// instead of an absolute start time is deliberate: instance and -/// frontend may run on machines whose clocks disagree, so the frontend -/// computes "instance has been running N seconds" using only the -/// instance's view of time. -#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] -pub struct InstanceIdentity { - /// Pmacs version string (`env!("CARGO_PKG_VERSION")`). - pub pmacs_version: String, - /// Short git hash if the build embedded one. `None` for releases or - /// source-tarball builds where no git checkout was available. - pub build_hash: Option, - /// The name the instance was launched under (`--socket NAME`). - /// `None` for the default daemon (no `--socket` argument). - pub instance_name: Option, - /// Seconds since the instance started, from the instance's clock. - /// Frontend displays "running 47m" by interpreting this against - /// its own notion of "now," avoiding cross-machine clock skew. - pub uptime_secs: u64, - /// Working directory the instance is running in. Encoded as a - /// UTF-8 string; non-UTF-8 paths are rejected at the boundary. - pub working_directory: String, -} - -impl InstanceIdentity { - /// Build an identity for the running pmacs process. - /// - /// `instance_name` is the user-facing name (typically the - /// `--socket NAME` value for the daemon path; `None` for the - /// in-process Local mode and the unnamed default daemon). - /// `started` is the wall-clock anchor used to compute - /// [`Self::uptime_secs`]; the elapsed seconds are evaluated at the - /// call site, so calling twice on different days surfaces different - /// uptimes from the same anchor. - /// - /// The version comes from `CARGO_PKG_VERSION` and the build hash - /// from the optional `PMACS_GIT_HASH` environment variable populated - /// by the build script. - #[must_use] - pub fn for_running_process(instance_name: Option, started: std::time::Instant) -> Self { - Self { - pmacs_version: env!("CARGO_PKG_VERSION").into(), - build_hash: option_env!("PMACS_GIT_HASH").map(String::from), - instance_name, - uptime_secs: started.elapsed().as_secs(), - working_directory: std::env::current_dir() - .ok() - .map(|p| p.to_string_lossy().into_owned()) - .unwrap_or_default(), - } - } -} - -/// Capabilities the instance advertises to attaching frontends. -/// -/// Empty for v0.1; the type exists so that adding capabilities in v0.2+ -/// is not a breaking-change. Symmetric with [`FrontendCapabilities`]. -/// -/// T M10.5: added `multi_frontend` and `crdt_replica` bits with -/// `#[serde(default)]` so v1 wire bytes still deserialize. The -/// negotiation logic (which side advertises what, and what the -/// instance does with mismatches) is M10.7 scope; M10.5 just makes -/// the bit positions stable in the wire format. -/// -/// T M10.5/8: bit defaults evolve with the substrate. -/// -/// - M10.5 declared the bits with `#[serde(default)]` so v1 wire -/// bytes deserialize forward-compatibly. M10.5–M10.7 set both bits -/// to `false` so a frontend declaring `multi_frontend: true` got -/// `Goodbye(CapabilityMismatch)` — the multi-frontend path -/// wasn't actually wired yet. -/// - **T M10.8 Day 4 flip**: the instance's `multi_frontend` and -/// `crdt_replica` defaults flip to `true`. This is the "M10.8 enables -/// multi-frontend" moment — the underlying dispatcher (Day 3) and -/// broadcast routing (Day 4) support both capabilities, so the -/// instance advertises them. -/// -/// The frontend-side defaults remain `false` (a frontend that omits -/// the field is conservatively treated as not supporting the -/// capability; matches v0.1 wire-format semantics). -#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] -pub struct InstanceCapabilities { - /// T M10.5: instance can host multi-frontend sessions on the - /// same buffer (per `§sec:m10-collab`). T M10.8 Day 4: default - /// flipped to `true` — the dispatcher supports multiple - /// attached frontends. - #[serde(default = "default_true")] - pub multi_frontend: bool, - /// T M10.5: instance can broadcast `InstanceMessage::CrdtOp` - /// messages. T M10.8 Day 4: default flipped to `true` — the - /// broadcast routing for CRDT ops wires up in this milestone. - #[serde(default = "default_true")] - pub crdt_replica: bool, - /// T M11.1: instance can produce the semantic-frontend variant - /// family (`InstanceMessage::StyleSpans` … `ResourceOffer`) and - /// consume `FrontendEvent::Viewport`. - /// - /// T M11.1 declared the bit position + negotiation mechanics with - /// the default `false` (no producer yet). T M11.2 landed the - /// instance-side projection seam (`SemanticRenderState`) and - /// flipped the default to `cfg!(feature = "crdt")`: the instance - /// now advertises `semantic_render` on CRDT builds. It tracks the - /// `crdt` feature rather than being unconditional because the - /// negotiation dependency rule makes a semantic session - /// necessarily a text replica — a non-CRDT build can host - /// neither. See [`Default`] impl below. - #[serde(default)] - pub semantic_render: bool, -} - -// Clippy in non-CRDT builds notes that `cfg!(feature = "crdt")` -// evaluates to `false`, making this impl derivable. In CRDT builds -// the values are `true`, so the impl is genuinely manual. Allow. -#[allow(clippy::derivable_impls)] -impl Default for InstanceCapabilities { - fn default() -> Self { - // T M10.10 — the `crdt_replica` default tracks the `crdt` - // Cargo feature. A daemon built without the `crdt` feature - // can't honor a `crdt_replica: true` negotiation (the - // CRDT-handling code paths are conditionally compiled out - // — `send_buffer_snapshots`, `apply_remote_crdt_op`, the - // dispatcher's CursorByte emit). Advertising `true` - // unconditionally would be wire-protocol false advertising. - // - // `multi_frontend` is conceptually independent of CRDT but - // in M10.10's architecture every multi-frontend participant - // is also a CRDT replica; gating both on the same feature - // keeps the daemon's advertised capabilities consistent - // with what it can actually do. - // - // T M11.1 declared `semantic_render` defaulting to `false` - // unconditionally — no projection-seam producer existed, so - // advertising it would have been wire-protocol false - // advertising (the M10.5→M10.7 "bits false until the path is - // wired" discipline). - // - // T M11.2 — **the flip**: the instance-side projection seam - // (`SemanticRenderState`, the producer) has landed and the - // dispatcher selects it per session, so the instance now - // advertises `semantic_render`. It tracks `cfg!(feature = - // "crdt")` like `crdt_replica` because the negotiation - // dependency rule makes a semantic session necessarily a - // text replica; a non-CRDT build can host neither. This is - // the "M11.2 enables semantic" moment, exactly analogous to - // the M10.8 Day-4 multi_frontend/crdt_replica flip. - Self { - multi_frontend: cfg!(feature = "crdt"), - crdt_replica: cfg!(feature = "crdt"), - semantic_render: cfg!(feature = "crdt"), - } - } -} - -#[allow(clippy::missing_const_for_fn)] -fn default_true() -> bool { - true -} - -/// Capabilities the frontend advertises to the instance. -/// -/// All bools default to `false` so a frontend that omits a field via an -/// older `AttachRequest` is conservatively treated as not supporting -/// the capability. New capabilities added in v0.2+ get -/// `#[serde(default)]` so old wire bytes still deserialize. -// A capability set is exactly the case `struct_excessive_bools` warns -// against — but each flag is independent and the alternative (an enum -// or bitset) loses the per-field `#[serde(default)]` semantics that -// make schema evolution work. -#[allow(clippy::struct_excessive_bools)] -#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize)] -pub struct FrontendCapabilities { - /// Frontend understands DEC 2026 `BeginSynchronizedUpdate` / - /// `EndSynchronizedUpdate` markers. Instance strips them when false. - #[serde(default)] - pub synchronized_output: bool, - /// Frontend can render Unicode beyond the Basic Multilingual Plane. - /// Instance can substitute a fallback glyph when false. - #[serde(default)] - pub unicode_smp: bool, - /// Frontend supports 24-bit color (truecolor SGR sequences). - /// Instance maps to the 256-color palette when false. - #[serde(default)] - pub true_color: bool, - /// Frontend captures and forwards mouse events. - #[serde(default)] - pub mouse: bool, - /// Frontend supports bracketed paste — distinguishes pasted bytes - /// from typed bytes. Instance treats all input as keystrokes when false. - #[serde(default)] - pub bracketed_paste: bool, - /// Optional human-readable terminal identifier for logs and - /// debugging only. The instance does not branch on this value; - /// branching is done on the explicit capability bits above. - #[serde(default)] - pub terminal_kind: Option, - /// T M10.5: frontend can participate in multi-frontend sessions - /// (per `§sec:m10-collab`). false for v0.1 frontends — they - /// attach as single-frontend and never receive `CrdtOp` / - /// `PresenceUpdate` broadcasts. v1.0 frontends opt in via M10.7's - /// negotiation handshake. M10.5 declares the bit position; M10.7 - /// wires the negotiation. - /// - /// Default is `false` — v1 frontends are treated as not - /// supporting this feature, which matches reality (v1 frontends - /// have no local CRDT state). A `true` default would have v1 - /// frontends claimed to support features they don't. - #[serde(default)] - pub multi_frontend: bool, - /// T M10.5: frontend can apply incoming `CrdtOp` messages to a - /// local CRDT state. false for v0.1; v1.0 opts in. M10.7 wires - /// negotiation; M10.5 declares the bit position. - #[serde(default)] - pub crdt_replica: bool, - /// T M11.1: frontend is a semantic (layout-local) renderer — it - /// consumes the `InstanceMessage::StyleSpans` … `ResourceOffer` - /// family and emits `FrontendEvent::Viewport`. false for v0.1 and - /// v1.0 grid/TUI frontends; a future GPU/GUI frontend opts in. - /// - /// A semantic frontend is *required* to also be a text replica: - /// the semantic frame ships no text, so the frontend must hold - /// the rope locally via the `crdt_replica` machinery. This - /// dependency is enforced in [`negotiate_capabilities`], not just - /// documented — declaring `semantic_render: true` without - /// `crdt_replica: true` is a capability mismatch, never a silent - /// degrade. - #[serde(default)] - pub semantic_render: bool, -} - -/// T M10.7 — the negotiated capability bits for one attached session. -/// -/// Computed by [`negotiate_capabilities`] from the frontend's -/// [`FrontendCapabilities`] and the instance's [`InstanceCapabilities`]. -/// Each negotiated bit is the AND of the two declared bits. Fields -/// added here in future milestones append at the end with sensible -/// defaults so existing call sites stay valid. -/// -/// This is a daemon-internal struct (not on the wire); the -/// negotiation result is communicated to the frontend via the -/// success of the handshake (no capability-mismatch `Goodbye`) and -/// the instance's behavior thereafter. -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] -pub struct NegotiatedCapabilities { - /// Session is eligible for multi-frontend operation. True iff - /// both the frontend and the instance declared `multi_frontend = - /// true`. v0.1 frontends always end up here as `false` (the v0.1 - /// wire format does not carry the field; `#[serde(default)]` - /// makes the deserialized value `false`). - pub multi_frontend: bool, - /// Session can produce/consume `InstanceMessage::CrdtOp` / - /// `FrontendEvent::CrdtOp`. True iff both sides declared - /// `crdt_replica = true`. The daemon's outgoing-message filter for - /// `CrdtOp` consults this in M10.8. - pub crdt_replica: bool, - /// T M11.1 — session uses the semantic projection: it - /// produces/consumes the `InstanceMessage::StyleSpans` … - /// `ResourceOffer` family and `FrontendEvent::Viewport`. True iff - /// both sides declared `semantic_render = true` *and* the session - /// also negotiated `crdt_replica = true` (a semantic session is a - /// text replica; see [`negotiate_capabilities`]). The daemon's - /// per-session outgoing filter gates the entire semantic family - /// on this bit — wired with the producer in M11.2. - pub semantic_render: bool, -} - -/// T M10.7 — pure-function capability negotiation. -/// -/// For each negotiated bit (`multi_frontend`, `crdt_replica`, -/// `semantic_render`): -/// -/// | Frontend wants | Instance has | Result | -/// |----------------|--------------|--------| -/// | `false` | `false` | bit `false`, no error | -/// | `false` | `true` | bit `false`, no error | -/// | `true` | `true` | bit `true`, no error | -/// | `true` | `false` | bit appears in `missing` | -/// -/// If any bit ends up in `missing`, the negotiation fails as a whole -/// (returns `Err`). Otherwise the negotiated bits are returned as -/// [`NegotiatedCapabilities`]. The `Err` form gathers ALL missing -/// bits into one `CapabilityMismatch` — one round-trip carries the -/// complete picture rather than serial rejections. Missing bits are -/// ordered `multi_frontend`, `crdt_replica`, `semantic_render` for -/// deterministic wire output. -/// -/// # T M11.1 — the `semantic_render ⇒ crdt_replica` dependency -/// -/// A semantic-render session ships no text on the semantic frame; -/// the frontend holds the rope locally via the `crdt_replica` -/// machinery (`BufferSnapshot` to bootstrap, `CrdtOp` to stay live). -/// So `semantic_render` is only coherent on a session that also -/// negotiated `crdt_replica`. When the AND-rule would yield -/// `semantic_render = true` but the session did not also negotiate -/// `crdt_replica = true`, this function rejects with -/// `"semantic_render"` in `missing` rather than silently degrading -/// the session to a text-only replica. The rejected identifier is -/// `"semantic_render"` (the capability whose precondition is unmet), -/// not `"crdt_replica"`. -/// -/// # Wire-format stability -/// -/// The strings emitted into `missing` are exactly the -/// `FrontendCapabilities` field names (`"multi_frontend"`, -/// `"crdt_replica"`). These are stable wire-format identifiers, not -/// human-readable descriptions. User-facing translation is the -/// frontend's responsibility (see [`AttachError`]'s `Display` impl). -/// Renaming a capability bit requires updating both the field name -/// and the missing-string emission here in lockstep. -pub fn negotiate_capabilities( - frontend: &FrontendCapabilities, - instance: &InstanceCapabilities, -) -> Result { - let mut missing = Vec::new(); - let multi_frontend = match (frontend.multi_frontend, instance.multi_frontend) { - (true, false) => { - missing.push("multi_frontend".to_string()); - false - } - (a, b) => a && b, - }; - let crdt_replica = match (frontend.crdt_replica, instance.crdt_replica) { - (true, false) => { - missing.push("crdt_replica".to_string()); - false - } - (a, b) => a && b, - }; - let semantic_render = match (frontend.semantic_render, instance.semantic_render) { - (true, false) => { - missing.push("semantic_render".to_string()); - false - } - (a, b) => a && b, - }; - // T M11.1 — dependency rule. A semantic session is a text replica - // (the semantic frame carries no text). If both sides declared - // `semantic_render` but the session did not also negotiate - // `crdt_replica`, reject rather than silently degrade. Guard - // against a duplicate push: the only path where `semantic_render` - // is already in `missing` is the `(true, false)` arm above, which - // also sets the local `semantic_render` to false, so the - // condition below cannot re-fire for that case — but the explicit - // membership check keeps this robust against future reordering. - if semantic_render && !crdt_replica && !missing.iter().any(|m| m == "semantic_render") { - missing.push("semantic_render".to_string()); - } - let semantic_render = semantic_render && crdt_replica; - if missing.is_empty() { - Ok(NegotiatedCapabilities { - multi_frontend, - crdt_replica, - semantic_render, - }) - } else { - Err(GoodbyeReason::CapabilityMismatch { missing }) - } -} - -/// First message sent by the instance to a freshly-attached frontend. -/// -/// Sent immediately after the connection is accepted, before reading -/// the frontend's [`AttachRequest`]. The frontend uses -/// `instance_identity` for status display and `protocol_version` / -/// `instance_capabilities` for compatibility decisions. -/// -/// The instance also stamps the `assigned_frontend_id` which the -/// frontend will use as the `FrontendId` on every event it sends. -#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] -pub struct Hello { - /// The instance's `PROTOCOL_VERSION`. - pub protocol_version: u32, - /// `FrontendId` assigned to this attachment by the instance. The - /// frontend stamps this onto subsequent events. v0.1 daemons start - /// allocation at `FrontendId(2)` (1 reserved for the in-process TUI). - pub assigned_frontend_id: FrontendId, - /// Instance self-identification (version, name, uptime, cwd). - pub instance_identity: InstanceIdentity, - /// Instance capabilities. Empty for v0.1. - pub instance_capabilities: InstanceCapabilities, -} - -/// First message sent by a frontend after receiving [`Hello`]. -/// -/// Carries the frontend's view of the protocol version, the -/// capabilities it can support, and its initial terminal size. On -/// version mismatch the instance closes with -/// [`GoodbyeReason::VersionMismatch`] and no further messages flow. -#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] -pub struct AttachRequest { - /// The frontend's `PROTOCOL_VERSION`. - pub protocol_version: u32, - /// Frontend capabilities. Defaults to all-false if omitted. - #[serde(default)] - pub frontend_capabilities: FrontendCapabilities, - /// The frontend's terminal size at attach time. Authoritative - /// until the frontend sends a [`FrontendEvent::Resize`]. The - /// instance uses this for the initial full-grid render. - pub initial_size: CellSize, -} - // --------------------------------------------------------------------------- // Crossterm translation (the only crossterm seam in this module) // ---------------------------------------------------------------------------