Merge pull request #39 from levineuwirth/worktree-protocol-crate-extraction

Session 1: pmacs-protocol crate extraction
This commit is contained in:
Levi Neuwirth 2026-05-20 14:00:41 +00:00 committed by GitHub
commit dab2a48bb0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 1833 additions and 1600 deletions

10
Cargo.lock generated
View File

@ -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"

View File

@ -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"
@ -56,11 +72,11 @@ 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"
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

48
pmacs-protocol/Cargo.toml Normal file
View File

@ -0,0 +1,48 @@
[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 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 }
postcard = { workspace = true }
thiserror = { workspace = true }

177
pmacs-protocol/src/cell.rs Normal file
View File

@ -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<Attachment>,
}
// ---------------------------------------------------------------------------
// 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<Cell>,
}

View File

@ -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<u8>,
}

80
pmacs-protocol/src/ids.rs Normal file
View File

@ -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,
}

54
pmacs-protocol/src/lib.rs Normal file
View File

@ -0,0 +1,54 @@
//! 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 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};
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,
};

File diff suppressed because it is too large Load Diff

View File

@ -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.
///

View File

@ -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<Attachment>,
}
// ---------------------------------------------------------------------------
// 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<Cell>,
}
/// Compute the diff between two cell buffers of identical layout.
///
/// `prev` and `next` are row-major slices, each of length at least

File diff suppressed because it is too large Load Diff

View File

@ -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.
///
@ -330,35 +330,10 @@ pub struct Edit {
pub crdt_op: Option<Box<CrdtOp>>,
}
/// 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<u8>,
}
// `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.
///