diff options
| -rw-r--r-- | docs/osc66-research.md | 236 |
1 files changed, 236 insertions, 0 deletions
diff --git a/docs/osc66-research.md b/docs/osc66-research.md new file mode 100644 index 0000000..619f397 --- /dev/null +++ b/docs/osc66-research.md @@ -0,0 +1,236 @@ +# OSC 66 Text Sizing — Research Notes + +## User Observations (v0.4.0-dev, 2026-03-29) + +### What works +- Heading rendering at scale: ✅ (2x, 1.5x, 1.2x all working) +- Protocol detection (CPR-based): ✅ (`KittyGfx[K+T]` modeline) +- Folding (TAB/S-TAB): ✅ works +- Color extraction from org-level faces: ✅ + +### Known issues +- **Scroll artifacts**: small visual artifacts when scrolling +- **Heading horizontal drift**: headings shift left/right during + scroll — likely a column calculation issue in `posn-at-point` + or the cursor positioning escape (`\e[row;colH`) + +--- + +## Existing Implementations (Deep Dive) + +### 1. mdfried — Terminal Markdown Viewer (Rust + ratatui) +- **URL**: https://github.com/benjajaja/mdfried +- **Status**: Most mature real-world OSC 66 consumer (v0.12.0+) +- **Language**: Rust + +#### Architecture +Two rendering paths for headings: +1. **With OSC 66**: Direct escape sequences via `BigText` widget +2. **Without OSC 66**: Falls back to rendering heading text into + images via `cosmic-text` font shaping, displayed via Kitty/Sixel + +#### The Erase-Character Dance (KEY FINDING) +Before emitting OSC 66, mdfried **erases the target area** using ECH: +``` +\x1b[{width}X\x1B[?7l -- erase current line + disable DECAWM +\x1b[1B -- move down 1 row +\x1b[{width}X\x1B[?7l -- erase next line +\x1b[1A -- move back up +``` +This prevents ghost artifacts. Uses ECH (`\x1b[NX`) which is more +efficient than writing spaces. **We should consider adopting this +for `kitty-gfx--erase-heading`.** + +#### Fractional Scaling for Size Tiers (KEY FINDING) +All tiers use `s=2` as base, then `n:d` for fine-grained sizing: +``` +Tier 1: s=2, n=7, d=7 (full 2x) +Tier 2: s=2, n=5, d=6 +Tier 3: s=2, n=3, d=4 +Tier 4: s=2, n=2, d=3 +Tier 5: s=2, n=3, d=5 +Tier 6: s=2, n=1, d=3 +``` +Gives 6 distinct sizes that are smoother than integer `s=` alone. +All headings occupy exactly 2 terminal rows regardless of tier. + +#### Unicode Chunking (BUG FIX v0.18.1) +Wide characters (emoji, CJK) in headings broke rendering. +Fix: emit each wide character in its own OSC 66 escape sequence. +Width for partial chunks: `(chunk_width * n).div_ceil(d)` — round up. +Comment in code says uncertain if Kitty bug or expected behavior. + +#### Ratatui Buffer Integration +Puts entire escape sequence in a single cell, marks all other +cells as "skip". **Equivalent to our overlay `display` property +approach** — first cell gets the payload, rest is reserved space. + +#### Bug History +- v0.12.0: Initial text sizing +- v0.15.0: "All tiers above #1 had letter spacing too wide" + (wrong `w` calculation for partial chunks) +- v0.18.1: Emoji/wide grapheme rendering bugs (unicode_chunks fix) + +### 2. neovim/neovim#32539 — Variable Font Sizes +- **URL**: https://github.com/neovim/neovim/issues/32539 +- **Status**: Open, labeled `gsoc`. No implementation. + +#### justinmk (Neovim core): +Variable line-height is feasible, could allow markdown headings +at different heights. 54 hearts on this comment — massive demand. + +#### fredizzimo's Architecture Analysis (KEY FINDING) +Identified fundamental challenges for any terminal editor: +1. **j/k navigation**: scaled text breaks column mapping +2. **Line height**: UI must tell editor the rendered height +3. **Variable-width fonts**: UI must handle wrapping +4. **Grid protocol**: can't send more cols/rows than grid allows + +His proposed approach (matches ours exactly): +- Variable text is non-navigable (like virtual text / our overlays) +- Plugin switches to normal rendering when cursor is on those lines +- UI tells box height/width before drawing (avoid flicker) +- Text attached to buffer with extmarks, space reserved with vtext + +#### algmyr's Use Case +LSP inlay hints at smaller font — makes them visually distinct +AND saves horizontal space. 20 thumbs up. + +#### CWood-sdf's Rendering Approaches +1. Uniform row height: entire line height = tallest char (simpler) +2. Push-aside: large chars push aside text below (drop-cap style) + +### 3. render-markdown.nvim#560 — Figlet Headings Request +- **URL**: https://github.com/MeanderingProgrammer/render-markdown.nvim/issues/560 +- **Status**: Closed, deferred to OSC 66 + +MeanderingProgrammer (owner): waiting for kitty text sizing to +mature, get adoption, and get integrated in neovim first. + +**Key insight**: Neovim ecosystem is blocked on core UI changes. +Emacs has advantage — `send-string-to-terminal` bypasses framework. + +### 4. opentui commit c246044 — TypeScript+Zig TUI Framework +- **URL**: https://github.com/melMass/opentui/commit/c246044 +- **Language**: TypeScript + Zig +- **Status**: AI-generated implementation (Nov 2025) + +#### Separator Convention (confirmed) +- Within parameter group: colon `:` (e.g., `s=2:n=1:d=2`) +- Between params and text: semicolon `;` +- Format: `\x1b]66;PARAMS;TEXT\x1b\\` + +#### Terminator Choice +Uses `ESC \` (ST) not BEL — "seems safer". Our code uses BEL (`\a`). +Both are valid per spec. BEL is shorter (1 byte vs 2). + +#### Performance Note +~10% terminal throughput overhead from multicell bookkeeping. +"Use text sizing judiciously." + +### 5. foot!1927 — Width-Only OSC 66 Implementation +- **URL**: https://codeberg.org/dnkl/foot/pulls/1927 +- **Language**: C (terminal emulator internals) +- **Status**: Merged Feb 2025 + +#### Scope +Width parameter (`w=`) only. No font scaling, no multi-line characters. +This is the `'width` tier our detection reports. + +#### w=0 Semantics +Process each codepoint normally, as if printed directly to terminal. + +#### w!=0 Semantics +Entire text string becomes a single combining character stored in +first cell, followed by SPACER cells. Treats "foobar" with `w=6` +as a single 6-column wide "character". + +#### Variation Selector Bug (krobelus) +Splitting `⚠\ufe0f` across two OSC 66 sequences breaks rendering. +Fix: always emit codepoint + variation selector in same sequence. + +#### Cursor on Multi-Width Characters +Foot doesn't render cursor in the middle of a forced-width character. +"Mostly doesn't make sense to place cursor on filler cell." + +#### Minimum Terminal Width +Changed from 2 to 7 cells. Reflow logic breaks when a character +(via `w=`) is wider than the terminal itself. + +#### Glyph Bleeding +When glyph is wider than allocated cells, pixels bleed into adjacent +cells. Visual-only, doesn't affect grid state. + +--- + +## Actionable Findings for kitty-graphics.el + +### HIGH PRIORITY + +1. **Erase via ECH instead of spaces** (from mdfried) + Our `kitty-gfx--erase-heading` writes spaces. ECH (`\x1b[NX`) + is more efficient and what mdfried uses. Also consider disabling + DECAWM during erase to prevent wrapping artifacts. + +2. **Fractional scaling with s=2 base** (from mdfried) + Instead of our current s=1/2/3 integer scales, use s=2 for ALL + heading tiers with n:d ratios for fine-grained sizing. This means + all headings are exactly 2 rows tall, simplifying space reservation. + +3. **Unicode chunking for wide chars** (from mdfried v0.18.1) + If heading text contains emoji or CJK, emit each wide character + in its own OSC 66 sequence. We don't do this yet. + +### MEDIUM PRIORITY + +4. **Horizontal drift investigation** + The heading left/right shift during scroll could be related to: + - posn-at-point returning inconsistent col values + - body-left offset not being stable across redraws + - org-indent-mode or line-number-mode changing column offsets + fredizzimo's analysis confirms this is a fundamental challenge. + +5. **Pre-erase before OSC 66 emission** (from mdfried) + mdfried erases BEFORE emitting, not just when moving. We could + adopt this pattern in `kitty-gfx--place-heading`: erase area first, + then emit OSC 66. Prevents artifacts from partial overwrites. + +### LOW PRIORITY + +6. **ST vs BEL terminator** + Our code uses BEL (`\a`). Both valid. Consider ST (`\x1b\\`) for + consistency with other implementations, but BEL is 1 byte shorter. + +7. **w= parameter for heading text** + We currently use `w=0` (auto). This is fine for ASCII headings. + If we want exact column control (which could fix the drift), we + could calculate and emit explicit `w=` per character/chunk. + +--- + +## Terminal Support Matrix + +| Terminal | Width (w=) | Scale (s=) | Fractional (n:d) | Notes | +|----------|-----------|-----------|-----------------|-------| +| Kitty ≥0.40.0 | ✅ | ✅ | ✅ | Full support | +| Foot | ✅ | ❌ | ❌ | Width only (merged Feb 2025) | +| Ghostty | 🔄 | ❌ | ❌ | Parsing added, rendering WIP | +| tmux | ❌ | ❌ | ❌ | Breaks OSC 66 entirely | +| WezTerm | ❌ | ❌ | ❌ | No support | +| Neovide | ❌ | ❌ | ❌ | Feature requested | + +## Competitive Landscape + +| Project | Type | OSC 66 | Scroll Strategy | +|---------|------|--------|----------------| +| **kitty-graphics.el** | Editor overlay | ✅ | Two-phase re-emission | +| mdfried | Full-screen TUI | ✅ | Full redraw per frame | +| presenterm | Full-screen TUI | ✅ | Full redraw per frame | +| opentui | TUI framework | ✅ | Double-buffered | +| libvaxis | TUI framework | w= only | Double-buffered | +| render-markdown.nvim | Neovim plugin | ❌ | Blocked on neovim core | +| Neovim core | Editor | ❌ | GSoC project, no impl | + +**We are the only editor-overlay implementation.** All others +are full-screen TUIs that own the viewport, or are waiting +for framework support. |
