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>