charmbracelet / charmbracelet/x
vt: keep grapheme clusters intact across Write calls
- Dominant language
- Go
- Stars
- 314
- Forks
- 94
- Avg merge
- 3d 22h
- Merged PRs (30d)
- 2
Description
## Problem
The final screen changes depending on how the same text is divided between calls to `Emulator.Write`.
That is unsafe for terminal output because readers do not receive meaningful text boundaries. A pipe, socket, PTY, or tmux control client may split one visible character across two reads.
## Minimal reproduction
Using `github.com/charmbracelet/x/vt` at `3755ebad01b1`:
```go
whole := vt.NewEmulator(20, 2)
_, _ = whole.Write([]byte("❤️ vs ❤"))
split := vt.NewEmulator(20, 2)
_, _ = split.Write([]byte("❤"))
_, _ = split.Write([]byte("\ufe0f vs ❤"))
```
The two emulators received identical bytes in the same order. Only the `Write` boundaries differ.
## Expected
Both emulators should produce the same cells, widths, and cursor position. The first `❤️` should occupy one grapheme cell.
## Actual
The split form commits `❤` before the variation selector arrives. The selector is then handled separately, the following cells shift, and the cursor ends in a different column.
A related case happens without a split:
```go
_, _ = emu.Write([]byte("e\u0301")) // NFD form of é
```
The combining accent does not remain attached to the ASCII `e`.
## Why this matters
This can affect ordinary terminal traffic even when the caller writes complete strings, because transport layers are free to split those bytes differently. Emoji, combining accents, variation selectors, and ZWJ emoji are the likely inputs.
A useful regression test would feed the same byte sequence whole and at every possible split point, then require identical cells, widths, and cursor position. The caller cannot provide a reliable “end of character” signal because a `Write` boundary is not that signal.
Contributor guide
Research direction
Start at the vt Emulator.Write entry point and trace how grapheme clusters, combining accents, and variation selectors are assigned to cells across calls. Reproduce the whole-versus-split examples, then add coverage that feeds the same bytes at every split point and verifies identical cells, widths, and cursor position.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- cli
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 68/100