feat(ui): kitty graphics layer and terminal detection #70

Merged
deadstyle merged 8 commits from feature/66-kitty-graphics-layer into develop 2026-10-08 23:37:01 -05:00
Owner

Summary

Adds the terminal-graphics layer for inline images (#60): detection of kitty-graphics terminals and the escape sequences that draw an image as Unicode placeholder cells, so it scrolls and clips like text. Nothing user-visible changes yet.

Changes

  • New package internal/ui/termimg: Transmit, Place, Delete, Cells, the IDs allocator (IDs 16-255, least-recently-used reuse with a delete), and the Detector. Every cell carries row and column diacritics and the ID as a 38;5;N foreground; tmux output is wrapped in ansi.TmuxPassthrough.
  • Detection runs once per session through Deps.Detect (zero value means no detection, so existing tests never probe the terminal). Outside tmux it needs XTVERSION to name kitty or ghostty and the graphics query answered before DA1 within 1 s; inside tmux it needs allow-passthrough on/all and a kitty/ghostty client terminal type. Colour profile must be at least ANSI256. Cell size from CSI 16 t, default 8x16. tmux settings are never changed.
  • ui.App receives the detection result and holds Support.
  • x/ansi, ultraviolet and colorprofile become direct requirements (already in the module graph; no new module). ADR 0016's module path corrected; docs/architecture.md and CLAUDE.md name the new package.

Decisions

  • Opus design (see #60, ADR 0016): placeholders only; the allowlist is kitty and Ghostty (WezTerm answers the graphics query but lacks placeholders), per the PM's answer on #60.
  • Opus planner: Transmit sets Transmission: kitty.Direct (without it no image data is sent) and returns "" on an encode failure; if the tmux probes pass and the timeout fires before DA1, support is on only when the colour profile is at least ANSI256.

Test plan

  • make build, make test (race, ~18 s), go vet, gofumpt, go mod tidy -diff, and golangci-lint v2.14.0 (built locally; 0 issues). shellcheck is not installed locally; CI covers it.
  • A real tea.Program renders into a buffer at ANSI256 while the viewport scrolls and the test asserts placeholders, diacritics and 38;5;N survive the renderer; it passes 30 of 30 runs and fails if the diacritics or the 256-colour ID are dropped.
  • Every new test was mutation-checked. One mutant is equivalent (png.Encode already rejects zero-size images).
  • Not testable here: real terminals (kitty, Ghostty, tmux). The manual check happens with the integration ticket.

Follow-ups

  • Markdown and Details integration, filed after this merges (needs the PM answers recorded on #60).

Refs #66

🤖 Generated with Claude Code

https://claude.ai/code/session_01APHERaPcnmg6YuAkRuoDYq

## Summary Adds the terminal-graphics layer for inline images (#60): detection of kitty-graphics terminals and the escape sequences that draw an image as Unicode placeholder cells, so it scrolls and clips like text. Nothing user-visible changes yet. ## Changes - New package `internal/ui/termimg`: `Transmit`, `Place`, `Delete`, `Cells`, the `IDs` allocator (IDs 16-255, least-recently-used reuse with a delete), and the `Detector`. Every cell carries row and column diacritics and the ID as a `38;5;N` foreground; tmux output is wrapped in `ansi.TmuxPassthrough`. - Detection runs once per session through `Deps.Detect` (zero value means no detection, so existing tests never probe the terminal). Outside tmux it needs XTVERSION to name kitty or ghostty and the graphics query answered before DA1 within 1 s; inside tmux it needs `allow-passthrough` on/all and a kitty/ghostty client terminal type. Colour profile must be at least ANSI256. Cell size from `CSI 16 t`, default 8x16. tmux settings are never changed. - `ui.App` receives the detection result and holds `Support`. - `x/ansi`, `ultraviolet` and `colorprofile` become direct requirements (already in the module graph; no new module). ADR 0016's module path corrected; `docs/architecture.md` and `CLAUDE.md` name the new package. ## Decisions - Opus design (see #60, ADR 0016): placeholders only; the allowlist is kitty and Ghostty (WezTerm answers the graphics query but lacks placeholders), per the PM's answer on #60. - Opus planner: `Transmit` sets `Transmission: kitty.Direct` (without it no image data is sent) and returns `""` on an encode failure; if the tmux probes pass and the timeout fires before DA1, support is on only when the colour profile is at least ANSI256. ## Test plan - `make build`, `make test` (race, ~18 s), `go vet`, `gofumpt`, `go mod tidy -diff`, and golangci-lint v2.14.0 (built locally; 0 issues). shellcheck is not installed locally; CI covers it. - A real `tea.Program` renders into a buffer at ANSI256 while the viewport scrolls and the test asserts placeholders, diacritics and `38;5;N` survive the renderer; it passes 30 of 30 runs and fails if the diacritics or the 256-colour ID are dropped. - Every new test was mutation-checked. One mutant is equivalent (`png.Encode` already rejects zero-size images). - Not testable here: real terminals (kitty, Ghostty, tmux). The manual check happens with the integration ticket. ## Follow-ups - Markdown and Details integration, filed after this merges (needs the PM answers recorded on #60). Refs #66 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01APHERaPcnmg6YuAkRuoDYq
Adds the Unicode-placeholder sequences (Transmit, Place, Delete, Cells) with tmux wrapping, and an ID allocator over 16-255 that reuses the least recently acquired ID once all are held.

Refs #66

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APHERaPcnmg6YuAkRuoDYq
termimg.Detector asks the terminal (XTVERSION, a graphics query, cell size, DA1) and emits DetectedMsg once; inside tmux it checks allow-passthrough and the client terminal instead. ui.App holds the result as termimg.Support, and cmd injects the detector through Deps.Detect. Nothing draws yet.

Refs #66

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APHERaPcnmg6YuAkRuoDYq
ADR 0016 named golang.org/x/ansi; the module is github.com/charmbracelet/x/ansi. It also records the ultraviolet and colorprofile imports termimg makes, and architecture.md and CLAUDE.md now list internal/ui/termimg.

Refs #66

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APHERaPcnmg6YuAkRuoDYq
Red-first tests for the kitty graphics layer, written from the plan's acceptance criteria and seams: exact escape strings, chunking, ID allocation, detector replies inside and outside tmux, the renderer scrolling test, and App wiring.

Refs #66

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APHERaPcnmg6YuAkRuoDYq
Refs #66

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APHERaPcnmg6YuAkRuoDYq
The test sent all scrolls and Quit back to back, so the renderer
coalesced them and only the last frame was painted; image 16 never
appeared. Each scroll now waits for the previous frame to reach the
output.

Refs #66

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APHERaPcnmg6YuAkRuoDYq
Refs #66

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APHERaPcnmg6YuAkRuoDYq
test(termimg): thread the zero Detector through its replies
All checks were successful
ci / check (pull_request) Successful in 1m13s
f3a84fb14d
TestZeroDetectorIsInert passed the same zero value to every Update and dropped the returned detector, so no reply ever accumulated. A mutation that let an idle detector finish on DA1 survived. The test now carries the returned detector forward, and that mutation fails it.

Refs #66

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APHERaPcnmg6YuAkRuoDYq
deadstyle merged commit eb19ddc846 into develop 2026-10-08 23:37:01 -05:00
deadstyle deleted branch feature/66-kitty-graphics-layer 2026-10-08 23:37:01 -05:00
Sign in to join this conversation.
No description provided.