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>