The wiring that makes the menu and OS clipboard work end-to-end. The
protocol bump touches every exhaustive match on the wire enums, so the
daemon / frontend / GPU consumers all land together.
Protocol v11 (additive; SUPPORTED = [6..11]):
- `PointerKind::Context` (right-click), `FrontendEvent::MenuPointer`
(GPU->daemon navigation, index-only), `InstanceMessage::MenuPrompt` +
`MenuPromptRow` (daemon->GPU rows + highlight, daemon-gated >= 11).
Dispatch + producer:
- `EditorState`: menu interception in `dispatch_key`/`dispatch_mouse`,
`MenuKey`, `dispatch_menu_key`/`_mouse`, `open_context_menu` (TUI) /
`open_menu_at_byte` + `dispatch_menu_pointer` (GPU), `build_menu_rows`
(calls the Lua resolver), `dispatch_idle` now false while a menu is
open. `dispatch_pointer` gains the `Context` arm.
- daemon: routes `Context` -> open, `MenuPointer` -> navigate; gates
`MenuPrompt` >= 11; drains the clipboard publish as
`InstanceSignal::Clipboard`; honors the previously-dropped
`FrontendEvent::Paste` (so paste works for the first time).
- `semantic_render`: `MenuPrompt` producer with cached-compare.
Frontends:
- TUI (`frontend.rs`): OSC 52 clipboard write; ignores `MenuPrompt`
(the cell overlay renders the menu).
- GPU (`pmacs-gpu`): `arboard` dep; clipboard write/read + Ctrl-V inbound
paste; right-click -> `Context`; `MenuLocal` + `MenuPrompt` handler;
the popup (a second `TextRenderer` over bg quads) at the click pixel;
hover/click -> `MenuPointer`; key intercept while open.
Also folds a pre-existing clippy `unnested_or_patterns` nit in a search
test (`Color::Indexed(11 | 3)`) that newer CI clippy surfaced.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
The GPU resolves pixels to source bytes entirely locally (Q#M2) and
ships byte-position gestures (Q#M1, protocol v5):
- Hit chain: pixel − TEXT_LEFT/TOP → cosmic_text::Buffer::hit
(shaped line + byte-within-line) → projected byte via the
projected text's line table → source byte via the run map.
- The run map is built by reshape from the SAME RichChunks that feed
glyphon (each chunk now tags its origin: verbatim source run vs
injected adornment), so the map and the shaped buffer cannot
disagree; hits inside inlay-hint text snap to the hint's source
anchor.
- Gestures: left Down (with frontend-side double-click detection →
DoubleDown — only the frontend knows pixel proximity), Drag
coalesced on hit-byte change (predicted finding #4), Up. Sends are
gated on the daemon's Hello.protocol_version >= 5; an outgoing
pointer supersedes any unconfirmed optimistic-cursor floor (the
daemon's CursorByte answer is the click, not the typing
prediction).
- Wheel scroll: new local handler (no wire — the GPU owns the
viewport): LineDelta×3 / PixelDelta÷line-height, clamped scroll_top
adjust → reshape → scoped viewport re-declaration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Typing no longer round-trips. Plain chars, Enter, and Tab (whose
default bindings reduce to a plain insert_char) apply to the local
Loro replica immediately and ship as FrontendEvent::CrdtOp:
- optimistic_crdt_insert: gated on DispatchIdle + a fresh CursorByte
+ no own-window selection (CUA type-over must round-trip into the
region-aware commands, which a raw op bypasses). Predicted-cursor
floor ignores stale in-flight CursorBytes; round-trip keys typed
behind unconfirmed inserts defer until the floor confirms.
Optimistic Enter scroll-follows immediately and re-declares the
scoped viewport.
- attach: a writer thread owns the socket write half, so the winit
thread never blocks on daemon backpressure; send_crdt_op added.
- Incremental text maintenance: Loro text deltas patch current_text
and per-line byte/char offset tables in place (no whole-rope
materialization per keystroke); cached spans/decorations/adornments
translate through each edit.
- Unconfirmed-edit journal: incoming StyleSpans/Decorations frames
carry the daemon's CRDT version scalar as generation; the GPU
computes the same per-peer counter sum locally, prunes confirmed
entries, and translates the frame's ranges through the rest — a
frame computed before an in-flight keystroke no longer repaints the
viewport's colors a few bytes left for one frame.
- Extend-at-end heuristic: a span ending exactly at a pure insert
point extends over the typed text, so new chars inherit the
preceding token's color instead of blinking default until the next
parse settles.
- Per-edit viewport re-declaration narrowed to origin moves;
PMACS_GPU_DEBUG_APPLY times per-message apply cost.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First Phase B session: pmacs-gpu can move its own cursor. Consumer-only
(the daemon already dispatches FrontendEvent::Key through the same
keymap/command stack the TUI uses; verified in the Phase B framing).
- `AttachClient::send_key` emits `FrontendEvent::Key`.
- `translate_key` maps winit logical key + modifier state → protocol
(Key, Modifiers). Covers the full editing set; `is_motion_key` gates
B1 to cursor-motion keys only (arrows, Home/End, PageUp/PageDown) so
no buffer mutation happens yet — editing keys open in B2 by dropping
the gate. Modifiers tracked via winit `ModifiersChanged`.
- `window_event` rework: Escape stays a local quit; other pressed keys
translate and (motion-gated) `send_key`.
- Consume `InstanceMessage::CursorByte` → `own_cursor` (Q#B3: the
daemon is authoritative; the caret follows whatever it reports, incl.
command-driven motion this frontend never interprets).
- Caret: a thin quad bar drawn *over* the text at the cursor glyph,
byte→glyph mapping rebased per line via `line_byte_offsets[line_i]`
(bet B4 / the QB3 lesson applied up front).
- Un-suppress own-window `Selection`/`CurrentLine` washes from
`current_decorations` alongside peer presence (Q#B4): the QB1
suppression lifts now that the own cursor is live. The bg-wash
builder split into `collect_own_decoration_rects` +
`collect_peer_rects`.
- own_cursor cleared on BufferSnapshot (prior-buffer offsets).
Tests: `translate_key_maps_motion_named_keys_and_chars`,
`translate_key_carries_modifiers`. pmacs-gpu unit 18 (+2).
Gates green: fmt; clippy --all-targets --workspace -D warnings (default
+ crdt); pmacs-gpu unit 18. Daemon/lib untouched.
NOT YET VISUALLY VALIDATED — per the Phase B framing's process
correction, this must be confirmed in a running pmacs-gpu (arrow keys
move the caret + own current-line wash; TUI unaffected) before merge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First Phase A session. pmacs-gpu now sends FrontendEvent::Viewport
back to the daemon after BufferSnapshot lands, receives the resulting
InstanceMessage::StyleSpans frames, and renders the rope with
per-span colors via cosmic-text's set_rich_text.
What's wired:
- AttachClient gains a write-side Arc<Mutex<UnixStream>> and the
assigned FrontendId from Hello; new send_viewport method emits
FrontendEvent::Viewport. The mutex is over-cautious for our
single-threaded event loop but future-proofs against multi-window
emission.
- State adds current_buffer_id and current_spans (sorted by
range.start). BufferSnapshot bootstraps both, then the App's
user_event handler emits a follow-up Viewport via
AttachClient::send_viewport.
- StyleSpans handling distinguishes full vs incremental:
* full=true: replace_style_spans drops prior, takes segments as
authoritative for the declared viewport.
* full=false: merge_style_spans applies the M11.4 dirty-segment
rule — spans fully inside a dirty range drop; spans straddling
a dirty edge get clipped to outside the range (with the
straddles-both-edges case splitting into two); new spans append;
re-sort by start.
- reshape() walks current_text + current_spans, emits (substr,
Attrs) chunks at every span boundary (with .min(text_len) clamps
for safety against stale spans past EOF), calls set_rich_text.
- cell_color_to_glyphon converts cell::Color to glyphon::Color via
the standard xterm-style 256-color palette (16 ANSI + 6x6x6 cube +
24-step grayscale). Default → None so the renderer's default
Attrs color stays.
Adversarial verification scope (Phase A probes):
- #1 non-ASCII source: exercised through the UTF-16 col/byte
conversion already in pmacs's producer side; pmacs-gpu just renders
what the wire delivers. Non-ASCII files should show correct
styling at the right byte positions.
- #3 viewport-boundary tokens: the merge_style_spans path is exactly
bet #1 from the framing pass ('StyleSpans/Decorations dirty-segment
edges at viewport boundaries — headless-test-blind-spot probe').
Edits near a span edge exercise the clip-and-merge logic.
- #6 CRLF line endings: implicit — pmacs's rope uses byte offsets so
styling spans naturally include or exclude the \r as the producer
decided. pmacs-gpu doesn't special-case line endings.
Known limitation (session 4 acceptable artifact, documented in
set_text): CrdtOp + StyleSpans arrive separately. CrdtOp updates text;
StyleSpans for the new generation comes one tick later. Between the
two, current_spans points at pre-edit byte positions while the text
is post-edit — visually stale for one frame. The .min(text_len) clamp
in reshape() keeps it safe; the artifact is brief.
Headless test gap: there's no Rust-level test of merge_style_spans
or the rich-text segmentation. Phase A's framing intentionally
chose manual validation over headless tests for these paths (the
adversarial probes are visual). A Phase A audit doc lands at session
close with the predicted-vs-actual scoring; per-method unit tests
for the merge logic could land then if findings argue for them.
Gates: fmt; clippy --all-targets -D warnings clean across the whole
workspace; lib 1303 + pmacs-protocol 11 = 1314; m4_acceptance 83;
m11_5_semantic_acceptance --features crdt 2.
Manual validation pending — same daemon+TUI+pmacs-gpu setup as
session 3, now showing colored text in the GPU window.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Surfaced during manual validation: the pmacs-gpu window sits on
'(connecting...)' forever when attaching to a daemon built without
--features crdt. Handshake succeeds (negotiation reports semantic_render
+ crdt_replica as agreed by both sides), but the daemon's
crdt_replica default is cfg!(feature='crdt')=false in that build, so
send_buffer_snapshots() never fires and pmacs-gpu has nothing to
render.
Classified small under rule (iii). The structural answer (should the
daemon return a clearer signal when crdt_replica was negotiated but
isn't actually compiled in?) is genuine but deferred; for session 3
the failure mode is now documented inline at the build-AttachRequest
site so the next user to hit it recognizes the symptom.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
pmacs-gpu now has two run modes:
- no args: hello-world (session-2 behavior preserved)
- --attach <socket>: connect to a pmacs daemon, negotiate
semantic_render + crdt_replica, import BufferSnapshot into a local
loro replica, render the rope text. Live CrdtOp updates apply as
they arrive.
Architecture:
- pmacs-gpu/src/attach.rs (new): UnixStream connect + Hello /
AttachRequest handshake on the main thread; spawns a reader thread
that pumps decoded InstanceMessage frames through the winit
EventLoopProxy as AppEvent::Attach(AttachEvent::Message). Clean EOF
or transport errors surface as AttachEvent::Disconnected. Reader
thread holds the read half of the stream; AttachClient retains the
write half (unused yet — session 4 wires FrontendEvents back).
- pmacs-gpu/src/main.rs: ApplicationHandler<AppEvent> with a
user_event handler that dispatches Message variants. BufferSnapshot
builds a fresh LoroDoc, imports the snapshot bytes, extracts text
via doc.get_text('body').to_string(), and re-shapes the glyphon
buffer. CrdtOp passes the op bytes through doc.import (loro
accepts both shapes), re-extracts text, re-shapes. Other
InstanceMessage variants are intentionally ignored at session 3.
- Font size dropped from 48pt to 16pt now that we may render full
files (the hello-world 48pt was fine for one line, awful for code).
- Initial text is '(connecting...)' in attach mode, 'hello, pmacs' in
hello-world; attach failure falls back to '(attach failed; see
stderr)' so the window still opens.
One small finding logged in attach.rs's connect() doc: AttachRequest's
initial_size field is a CellSize (rows × cols), nominally
TUI-shaped. Sent as a placeholder (24×80) — a structural answer
('what does initial size mean for a pixel frontend?') belongs in its
own protocol thread, not session 3. Classified under rule (iii) as
deferred.
Container id for the loro text container ('body') hardcoded to match
pmacs::crdt::CrdtState — second finding worth pre-recording: the
container name is a wire-adjacent convention that isn't carried on
the wire itself. Both ends have to agree out-of-band. Not blocking
for session 3 but a structural smell for the producer arc. Logged
as deferred (rule iii structural; the answer is probably 'thread the
container id through BufferSnapshot' but it's not session-3 scope).
Gates: cargo fmt, cargo clippy --all-targets -D warnings (whole
workspace) clean; lib 1303 + pmacs-protocol 11 = 1314 unchanged;
m4_acceptance 83; m11_5_semantic_acceptance --features crdt 2.
Manual validation pending — agent environment is headless. User
walks through: start a pmacs daemon, run pmacs-gpu --attach <socket>,
confirm the window renders the daemon's file contents.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>