//! 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), } /// The nine built-in auto-pair characters (docs/auto-pairing-framing.md /// Q#AP1). Both frontends' optimistic classifiers exclude these so they /// always round-trip through daemon dispatch: the typed opener and the /// pairing hook's closer then land as adjacent daemon-peer undo units, /// dispatch-path CUA type-over applies, and skip-over-close never /// paints a transient duplicate. Shared here — not duplicated per /// frontend — because a frontend that drifts from this set silently /// re-degrades pair undo to the cross-peer mixed-history case. pub const BUILTIN_PAIR_CHARS: [char; 9] = ['(', ')', '[', ']', '{', '}', '"', '\'', '`']; /// True when `c` is one of [`BUILTIN_PAIR_CHARS`]. #[must_use] pub fn is_builtin_pair_char(c: char) -> bool { BUILTIN_PAIR_CHARS.contains(&c) } /// 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, }, /// Q#M1 (protocol v5): a pointer gesture a *semantic* frontend /// hit-tested locally to a **source byte offset**. The pixel→byte /// resolution happens entirely frontend-side (the frontend owns /// layout: fonts, inline adornments, scroll) — consistent with /// `Viewport`'s no-pixels contract; the instance replays its /// existing mouse gesture semantics in byte space, so selection /// behavior stays single-sourced with the grid path. /// /// Cell-grid frontends keep using [`FrontendEvent::Mouse`]; the /// two variants are per-session-kind, never mixed. Only sent when /// the instance's `Hello.protocol_version >= 5` (an older /// instance cannot decode the variant). Pointer { /// Which frontend produced the gesture (untrusted; the /// instance routes by the authenticated session, matching /// the `CrdtOp` / `Viewport` source-trust rule). frontend_id: FrontendId, /// Buffer the frontend was displaying when it hit-tested. buffer_id: crate::BufferId, /// Source byte offset of the hit (frontend-local hit test; /// adornment runs already snapped to their anchors). byte: u64, /// Which gesture step this is. kind: PointerKind, /// Modifiers held during the gesture. Carried for future /// Shift-click extension; the v5 instance ignores them. mods: Modifiers, }, /// Navigate an open context menu (Q#CM1, protocol v11). A semantic /// frontend hit-tests the popup it drew locally and reports the row /// the pointer is over, so the daemon never needs the frontend's /// pixels — symmetric with how the GPU owns its viewport. Sent only /// while the daemon's menu is open. MenuPointer { /// Which frontend produced the gesture. frontend_id: FrontendId, /// Row index the pointer is over, or `None` when it left the /// popup (a hover off the menu, or a click outside). index: Option, /// `true` on a click/release: invoke `index`'s item, or dismiss /// the menu when `index` is `None`. `false` is a hover that only /// moves the highlight. invoke: bool, }, } /// Gesture step for [`FrontendEvent::Pointer`]. Double-click /// detection is frontend-side (`DoubleDown` instead of a second /// `Down`): only the frontend knows pixel proximity and its own /// double-click interval. #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, PartialEq, Eq)] pub enum PointerKind { /// Primary button pressed at `byte`. Down, /// Pointer moved to `byte` with the primary button held. Drag, /// Primary button released at `byte`. Up, /// Second press at the same hit within the frontend's /// double-click window — selects the word at `byte`. DoubleDown, /// Third press at the same hit within the frontend's /// multi-click window — selects the whole line at `byte`, /// trailing newline included (Q#M4, protocol v7). The frontend /// sends this only to a `>= 7` instance; against an older one /// the third click restarts the chain as a plain `Down`. TripleDown, /// Secondary (right) button pressed at `byte` — opens the context /// menu there (Q#CM1, protocol v11). The frontend sends this only to /// a `>= 11` instance. Context, } 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, .. } | Self::Pointer { frontend_id, .. } | Self::MenuPointer { 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, }, /// Q#S1 (status band, protocol v8) — instance-authoritative /// status facts a semantic frontend cannot derive locally: /// buffer name, modified flag, whole-file diagnostic counts. /// Cursor position and scroll stay frontend-derived (the /// optimistic caret must not lag a round trip). Emitted by the /// semantic producer when any fact changes; kept off wires /// negotiated `< 8` (additive variant — an older peer would /// hard-error decoding it). StatusFacts { /// Buffer these facts describe. buffer_id: crate::BufferId, /// Buffer display name. name: String, /// Unsaved-changes flag. modified: bool, /// Whole-file `Error`-severity diagnostic count. diag_errors: u32, /// Whole-file `Warning`-severity diagnostic count. diag_warnings: u32, /// The core's transient status message (`pmacs.editor. /// set_status` — LSP command summaries like "12 references", /// error reports, ...), or `None` when clear. Added in v15: /// the attached TUI gets the message for free through the /// rendered cell grid's bottom row, but a semantic frontend /// only sees what's on this wire — without it every modeline /// summary was TUI-only. Encoding change to this variant; its /// daemon gate moved `>= 8` → `>= 15` (the v10 `SearchPrompt` /// / v14 `LineNumbers` precedent). message: Option, }, /// 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, }, /// Daemon-side input-dispatcher idleness signal. Tells a /// `crdt_replica` frontend whether the next key event would be /// **intercepted** by the daemon (minibuffer prompt active or /// dispatcher holds a pending multi-key prefix) versus would /// self-insert into the active buffer. /// /// Frontends running the optimistic-apply layer (`src/optimistic.rs`) /// gate the local apply on `idle == true`: when the daemon is not /// idle, plain-char keystrokes must round-trip as /// [`FrontendEvent::Key`] so the daemon's minibuffer / prefix /// dispatcher receives them. Without this signal, characters typed /// into a minibuffer prompt would be applied as `CrdtOp`s to the /// previously-active document instead — the surfacing of which /// motivated this wire addition. /// /// Emission contract (daemon side): /// /// - Once after `AttachRequest` is accepted, before any other /// non-handshake message, so a fresh frontend starts from a /// known idle state regardless of the daemon's current input /// condition. /// - At every transition: minibuffer begin / dismiss; dispatcher /// `pending` empty↔non-empty. /// - Coalesced where consecutive transitions land on the same /// value (no spurious back-to-back identical signals). /// /// Gated on negotiated `crdt_replica` — frontends without the /// optimistic-apply layer have no use for this signal and the /// daemon's per-session filter drops it for them. DispatchIdle { /// `true` → the next key event would self-insert into the /// active buffer (no minibuffer, no pending prefix); optimistic /// apply is correct. /// `false` → the daemon would intercept the next key; the /// frontend must round-trip via [`FrontendEvent::Key`]. idle: bool, }, /// Q#SR5 (incremental search, protocol v9) — the live isearch /// prompt for a semantic frontend that cannot host a minibuffer. /// Carries the query as typed and the match readout so the /// frontend can render an `I-search: (n/m)` band; the /// matches themselves arrive as [`DecorationKind::SearchMatch`] / /// [`DecorationKind::SearchMatchActive`] decorations. A `query` of /// `None` means no search is running — the frontend hides the band. /// /// Emitted by the semantic producer when the search state changes /// (cached-compare suppressed, like [`Self::StatusFacts`]). The /// `regex` / `invalid` fields (Q#RX6) changed this variant's /// encoding, so the daemon's per-session filter now keeps it off /// wires negotiated `< 10` (was `< 9` for the original four-field /// shape — see `PROTOCOL_VERSION`). SearchPrompt { /// Buffer the search is anchored in (the active buffer). buffer_id: crate::BufferId, /// The query as typed so far, or `None` when no search runs. /// `Some("")` is a freshly-started search with an empty query. query: Option, /// 0-based index of the active match, or `None` when the query /// has no matches (a failing search). active: Option, /// Total number of matches for the current query. total: u32, /// `true` when the search is in regex mode (Q#RX3) — the /// frontend prefixes the prompt with `Regex `. regex: bool, /// `true` when a regex pattern failed to compile — the frontend /// shows `[invalid]` instead of a match count. Always `false` /// in literal mode. invalid: bool, }, /// Open-menu contents for a semantic frontend (Q#CM1, protocol v11). /// The daemon resolves the visible items (predicates / context tags) /// and ships the rendered rows + highlight; the frontend draws the /// popup at the pixel it remembered from the right-click and reports /// navigation via [`FrontendEvent::MenuPointer`]. An empty `rows` /// closes the menu. Cached-compare suppressed like `SearchPrompt`. MenuPrompt { /// Buffer the menu is anchored in (the active buffer). buffer_id: crate::BufferId, /// Rows top-to-bottom (items + separators); empty = closed. rows: Vec, /// Index into `rows` of the highlighted item, or `None` when /// closed. active: Option, }, /// Minibuffer prompt state for a semantic frontend (Q#MB1, protocol /// v12). The minibuffer is a single *global* core instance, so this /// is bufferless; the producer still emits it from the active-buffer /// viewport. `prompt: None` clears the GUI. Cached-compare /// suppressed like `SearchPrompt`; daemon-gated `>= 12`. MinibufferPrompt { /// The prompt string (e.g. `"M-x "`), or `None` when no /// minibuffer is open. prompt: Option, /// The text typed so far. input: String, /// Codepoints before the cursor within `input` (the caret /// position). cursor: u32, /// A windowed slice of the completion candidates (best-first, /// already filtered/sorted by the core), `<= MB_VISIBLE`. candidates: Vec, /// Highlighted row *within* `candidates`, or `None`. selected: Option, /// Total candidate count (the window is a slice of this). total: u32, }, /// UX gutter — the per-window line-number gutter *mode* for the /// frontend's active window. A semantic frontend renders line numbers /// *locally* (it owns the text + its own cursor line, so it can draw /// relative/hybrid without a round trip), but the mode toggle lives /// daemon-side, so the daemon ships which mode. Bumped 13 → 14: the /// v13 shape carried a bare `enabled: bool` (off/absolute only); v14 /// carries the full [`LineNumberMode`] so relative/hybrid can ride the /// same message. Daemon-gated `>= 14` — a v13 peer negotiates v13 and /// receives no `LineNumbers` (its gutter stays off) rather than /// mis-decoding the wider shape. LineNumbers { /// Buffer the active window shows (routing/consistency; the mode /// is a window property, not a buffer one). buffer_id: crate::BufferId, /// The line-number gutter mode for that window. mode: LineNumberMode, }, /// In-buffer completion popup state for a semantic frontend /// (Arc 1a Q#C5, protocol v15). Unlike the band-anchored /// [`Self::MinibufferPrompt`], the popup is anchored *at a byte* /// (the typed prefix's start) — the frontend maps byte → glyph /// rect locally, exactly as it does for the caret, so the /// instance never learns a pixel. Rows are display-only: accept /// is a daemon round-trip (`dispatch_completion_key`), so insert /// text never ships. `anchor: None` clears the popup. /// Cached-compare suppressed like `SearchPrompt`; daemon-gated /// `>= 15`. CompletionPopup { /// Buffer the popup targets. buffer_id: crate::BufferId, /// Byte offset of the prefix start, or `None` when closed. anchor: Option, /// Bytes of typed prefix at `anchor` (a frontend may embolden /// the matched prefix within each label). prefix_len: u32, /// A windowed slice of the candidates (best-first, already /// scored/filtered by the core), `<= POPUP_VISIBLE`. rows: Vec, /// Highlighted row *within* `rows`, or `None`. selected: Option, /// Total candidate count (the window is a slice of this). total: u32, }, /// Themes arc (Q#TH7, protocol v16). The daemon-resolved UI faces. /// The theme is one global instance, so this is bufferless (the /// [`Self::MinibufferPrompt`] shape). Complete replacement each /// send: a face absent from `faces` is unset, and the frontend /// uses its own default for that surface. Every attachment /// receives exactly one authoritative table — the empty table /// included — with its first emission after viewport declaration; /// cached-compare suppressed thereafter. Daemon-gated `>= 16`. /// /// Appended as the final v16 variant deliberately: postcard /// discriminants are ordinal, so inserting earlier would shift /// every later variant's tag and corrupt v15 peers on ungated /// channels. The `CompletionPopup` byte pin in `src/protocol.rs` /// guards this placement; the `ThemeFacts` byte pin there guards /// the v17 `FontFacts` placement after it in turn. ThemeFacts { /// Every stage-1 face that resolves to a style (the Q#TH4 /// dotted-prefix walk, resolved daemon-side — frontends do /// exact-name lookup, no walk), full names, sorted by name /// for deterministic comparison. faces: Vec, }, /// Themes arc stage 2 (Q#F4, protocol v17). The daemon-relayed /// GPU font preference. One global instance ⇒ bufferless (the /// [`Self::MinibufferPrompt`] shape). Complete replacement each /// send; `None` means the frontend's built-in default for that /// axis. The daemon relays a PREFERENCE — it never learns /// metrics, advances, or what resolves; the frontend owns /// resolution and every pixel consequence (the no-pixels /// invariant). Every attachment receives exactly one /// authoritative preference — the all-default `(None, None)` /// included — with its first emission after viewport /// declaration; cached-compare suppressed thereafter. /// Daemon-gated `>= 17`. /// /// Appended as the FINAL variant deliberately: postcard /// discriminants are ordinal, so inserting earlier would shift /// every later variant's tag and corrupt v16 peers on ungated /// channels. The `ThemeFacts` byte pin in `src/protocol.rs` /// guards this placement. FontFacts { /// Font family name to resolve frontend-locally, or `None` /// for the frontend's default family query. family: Option, /// Font size in HUNDREDTHS of a logical pixel (1600 = /// today's 16.0) — an integer because this enum derives /// `Eq`, which `f32` cannot satisfy, and because cosmic-text /// metrics are logical pixels, not typographic points. /// Valid range 600..=7200; frontends validate and fail /// closed (deserialized protocol input is untrusted). size_centi_px: Option, }, } /// One resolved UI face for [`InstanceMessage::ThemeFacts`]: a full /// face name (e.g. `"ui.modeline"`) and the style the daemon resolved /// for it. The face's component *mask* (which components a frontend /// may read) is a stage-1 contract documented per face in the themes /// framing; out-of-mask components are never read by either frontend. #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] pub struct ThemeFace { /// Full face name (`ui` or `ui.`-prefixed). pub name: String, /// The daemon-resolved style for this face. pub style: crate::cell::Style, } /// Line-number gutter mode for a window (UX gutter arc). Shared across the /// wire, the daemon, and both frontends so the *number rule* — what value /// each line shows — is identical everywhere (Q#UX7). `pmacs` re-exports /// this as `crate::window::LineNumberMode`. #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum LineNumberMode { /// No gutter; zero layout change (the default, Emacs tradition). #[default] Off, /// Absolute 1-based line numbers. Absolute, /// Distance from the cursor line (the cursor line shows `0`). Relative, /// Like `Relative`, but the cursor line shows its absolute 1-based /// number instead of `0` (Vim `number` + `relativenumber`). Hybrid, } impl LineNumberMode { /// Whether this mode reserves a gutter at all (everything but `Off`). #[must_use] pub fn is_on(self) -> bool { !matches!(self, Self::Off) } /// The displayed number for a 0-based buffer `line` given the cursor's /// 0-based buffer line, or `None` in `Off`. `Relative`/`Hybrid` depend /// on `cursor_line`; `Absolute` ignores it. #[must_use] pub fn number_for(self, line: usize, cursor_line: usize) -> Option { match self { Self::Off => None, Self::Absolute => Some(line + 1), Self::Relative => Some(line.abs_diff(cursor_line)), Self::Hybrid => Some(if line == cursor_line { line + 1 } else { line.abs_diff(cursor_line) }), } } } /// One row of an open menu on the wire ([`InstanceMessage::MenuPrompt`]). #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] pub struct MenuPromptRow { /// Display label (empty and ignored when `separator`). pub label: String, /// `true` for a non-selectable group divider. pub separator: bool, } /// One row of the in-buffer completion popup on the wire /// ([`InstanceMessage::CompletionPopup`]). Display fields only --- /// accept resolves daemon-side against the core session, so the /// insert text stays off the wire. #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] pub struct CompletionPopupRow { /// Display label. pub label: String, /// LSP `CompletionItemKind` numeric code (1..=25; frontends map /// unknown codes to a plain-text glyph, per the LSP contract). pub kind: u8, /// Optional one-line detail rendered after the label. pub detail: Option, } /// 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. /// /// T M11.6: bumped from 3 to 4. v1, v2, v3 wires remain accepted; /// [`InstanceMessage::DispatchIdle`] is filtered per-session for /// sessions whose negotiated wire is `<= 3`. Same gating shape as the /// `CrdtOp` / semantic-frontend bumps: the daemon's per-tick emission /// checks the session's negotiated version and skips the variant for /// older peers. An old peer would hard-error on decode of an unknown /// postcard variant; gating prevents that. /// /// Q#M1 (mouse framing): bumped from 4 to 5 for /// [`FrontendEvent::Pointer`]. The gate runs in the *frontend* this /// time (the new variant travels frontend→instance): a semantic /// frontend sends `Pointer` only when the instance's /// `Hello.protocol_version >= 5`, because an older instance would /// hard-error decoding the unknown variant. /// /// T M4.6 (diagnostic surface): bumped from 5 to 6 for /// `Style::underline_color`. Unlike every previous bump, this one /// changes the *encoding of an existing struct* — `Style` rides /// inside `Cell` / `CellDelta` / `Snapshot` / `StyleSpans`, messages /// every session receives — so per-session send gating cannot /// preserve compatibility (postcard is not self-describing; a v5 /// decoder mis-reads any v6 `Style`). v6 binaries therefore accept /// only v6 peers: a version-mismatched pair fails the handshake with /// [`GoodbyeReason::VersionMismatch`] instead of garbling cell /// traffic mid-session. /// /// Q#M4 (mouse deferred set): bumped from 6 to 7 for /// [`PointerKind::TripleDown`]. Back to the cheap additive shape: /// a new variant on a frontend→instance enum, gated in the frontend /// (sent only when the instance's `Hello.protocol_version >= 7`), /// so the compat ladder restarts on the v6 encoding floor — /// `SUPPORTED_PROTOCOL_VERSIONS` grows to `[6, 7]`. /// /// Q#S1 (status band): bumped from 7 to 8 for /// [`InstanceMessage::StatusFacts`]. Additive again, gated in the /// *daemon* this time (the variant travels instance→frontend): the /// per-session filter keeps it off wires negotiated `< 8`, the same /// shape as the `DispatchIdle` (v4) gate. /// /// Q#SR5 (incremental search): bumped from 8 to 9 for /// [`InstanceMessage::SearchPrompt`]. Additive and daemon-gated per /// session, identical shape to the `StatusFacts` (v8) bump. /// /// Q#RX6 (regex search): bumped from 9 to 10 — `SearchPrompt` gained /// `regex` / `invalid` fields, changing that variant's postcard /// encoding. Still daemon-gated per session (now at `< 10`); a v9 peer /// negotiates v9 and simply receives no `SearchPrompt` (the decorations /// still highlight), rather than mis-decoding the wider shape. /// /// UX gutter: bumped 12 → 13 for [`InstanceMessage::LineNumbers`] — a new /// additive variant carrying the per-window line-number gutter mode. /// Daemon-gated `< 13`; a v12 peer negotiates v12 and receives no /// `LineNumbers` (its gutter simply stays off), like every prior additive /// bump. /// /// UX gutter modes: bumped 13 → 14 — `LineNumbers` swapped its `enabled: /// bool` for a [`LineNumberMode`] enum so relative/hybrid ride the same /// message. Encoding change to that variant; daemon-gated `< 14` (a v13 /// peer negotiates v13 and receives no `LineNumbers` rather than /// mis-decoding the wider shape), same shape as the v10 `SearchPrompt` bump. /// /// Completion popup (Arc 1a Q#C5): bumped 14 → 15 for /// [`InstanceMessage::CompletionPopup`] — a new additive variant /// carrying the byte-anchored in-buffer completion dropdown. /// Daemon-gated `< 15`; a v14 peer negotiates v14 and simply receives /// no `CompletionPopup` (completion still works via the daemon's TUI /// rendering and the key round-trip), like every prior additive bump. /// v15 also widened `StatusFacts` with the transient status `message` /// (encoding change to that variant; its gate moved `>= 8` → `>= 15`, /// so a v14 peer's status band goes dark rather than mis-decoding — /// the v10 `SearchPrompt` / v14 `LineNumbers` shape). /// /// Theme faces (Q#TH7): bumped 15 → 16 for /// [`InstanceMessage::ThemeFacts`] — a new additive variant carrying /// the daemon-resolved UI face table. Daemon-gated `< 16`; a v15 peer /// negotiates v15 and simply receives no `ThemeFacts` (its chrome /// stays on the frontend defaults), like every prior additive bump. /// The variant is appended after `CompletionPopup` — the final v15 /// variant — because postcard discriminants are ordinal and an /// earlier insertion would shift existing tags under v15 peers. /// /// GPU font preference (Q#F4): bumped 16 → 17 for /// [`InstanceMessage::FontFacts`] — a new additive variant relaying /// the global font preference to GPU-capable peers. Daemon-gated /// `< 17`; a v16 peer negotiates v16 and simply keeps its built-in /// font. Appended after `ThemeFacts` — the final v16 variant — /// same ordinal-discriminant reasoning as every additive bump. pub const PROTOCOL_VERSION: u32 = 17; /// 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. /// /// T M11.6: extended to `[1, 2, 3, 4]`. v4 sessions receive /// `InstanceMessage::DispatchIdle`; older sessions are filtered out /// of that emission. /// /// Mouse framing Q#M1: extended to `[1, 2, 3, 4, 5]`. v5 peers may /// send `FrontendEvent::Pointer`; the frontend-side gate (see /// [`PROTOCOL_VERSION`]) keeps the variant off wires negotiated `< 5`. /// /// T M4.6: narrowed to `[6]`. `Style::underline_color` changed the /// postcard encoding of every cell-carrying message, so v6 binaries /// cannot exchange cell traffic with any earlier wire. The v1–v5 /// compat ladder (additive variants, per-session filtering) assumed /// shared-struct encodings never changed; this bump is the first /// that breaks that assumption, and slice membership is how the /// handshake communicates it. /// /// Q#M4: extended to `[6, 7]`. `PointerKind::TripleDown` is additive /// and frontend-gated (like `Pointer` itself at v5), so the ladder /// resumes: v6 and v7 binaries interoperate, with the new variant /// kept off wires whose instance negotiated `< 7`. /// /// Q#S1: extended to `[6, 7, 8]`. `InstanceMessage::StatusFacts` is /// additive and daemon-gated per session. /// /// Q#SR5: extended to `[6, 7, 8, 9]`. `InstanceMessage::SearchPrompt` /// is additive and daemon-gated per session. /// /// Q#RX6: extended to `[6, 7, 8, 9, 10]`. `SearchPrompt` gained /// `regex` / `invalid` (encoding change to that variant); v9 and v10 /// interoperate because the variant is daemon-gated per session, so a /// v9 peer is simply never sent the wider shape. /// /// Q#CM1: extended to `[6, 7, 8, 9, 10, 11]`. The context menu adds /// `PointerKind::Context` (frontend-gated like `Pointer`/`TripleDown`), /// `FrontendEvent::MenuPointer`, and `InstanceMessage::MenuPrompt` /// (daemon-gated per session) — all additive, so the ladder resumes. /// /// Q#MB1: extended to `[6, 7, 8, 9, 10, 11, 12]`. /// `InstanceMessage::MinibufferPrompt` is additive and daemon-gated per /// session, so the ladder resumes again. /// /// Q#C5: extended to `[6, ..., 15]`. `InstanceMessage::CompletionPopup` /// is additive and daemon-gated per session. /// /// Q#TH7: extended to `[6, ..., 16]`. `InstanceMessage::ThemeFacts` /// is additive and daemon-gated per session, so the ladder resumes. /// /// Q#F4: extended to `[6, ..., 17]`. `InstanceMessage::FontFacts` /// is additive and daemon-gated per session, so the ladder resumes. pub const SUPPORTED_PROTOCOL_VERSIONS: &[u32] = &[6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]; /// 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, }