Accepted
Unicode text that looks like a single character on screen can be made up of multiple codepoints. The classic wcwidth approach (assigning a width to each codepoint and summing) breaks for these multi-codepoint sequences because it doesn't understand that the terminal renders them as one grapheme cluster.
Examples:
| Sequence | Codepoints | wcwidth sum | Actual terminal width |
|---|---|---|---|
| ๐จโ๐ฉโ๐ง (family) | ๐จ + ZWJ + ๐ฉ + ZWJ + ๐ง (5 codepoints) | 2+0+2+0+2 = 6 | 2 |
| ๐ฉ๐ช (flag) | ๐ฉ + ๐ช (2 regional indicators) | 2+2 = 4 | 2 |
| ๐๐ฝ (wave + skin tone) | ๐ + ๐ฝ (2 codepoints) | 2+2 = 4 | 2 |
When TUI code uses the wrong width, everything that depends on character measurement breaks:
pad-right adds too few spaces (it thinks the string is wider than it is)truncate cuts too early, clipping visible text to fit a phantom widthDisplay.update() miscalculates cursor positions, causing render artifacts (ghost characters, misplaced redraws)JLine 3's columnLength() uses per-codepoint wcwidth, so it produces the wrong values for all the cases above.
The fundamental issue is that the terminal, not the application, decides how to cluster codepoints into graphemes. Different terminals use different Unicode versions and clustering rules. An application that hardcodes a width table will always be wrong on some terminals.
Mode 2027 solves this by letting the terminal tell the application "I support grapheme clustering"โmeaning the terminal itself treats each grapheme cluster as a single unit for cursor movement. Once the application knows the terminal clusters correctly, it can count each cluster as width 2 (for emoji) or 1 (for text), rather than summing per-codepoint widths.
Mode 2027 (CSI ?2027h) is a terminal protocol that enables grapheme clustering for cursor movement. The protocol works as follows:
CSI ?2027$p (DECRQM โ request mode) to ask the terminal if it supports grapheme clusteringCSI ?2027;1$y (mode set) or CSI ?2027;2$y (mode reset but recognized). A non-supporting terminal either doesn't reply or returns an unrecognized-mode responseCSI ?2027h to activate grapheme clustering๐จโ๐ฉโ๐ง occupies 2 cells and the cursor advances by 2, not by 6CSI ?2027l to restore default behaviorSupported by Ghostty, Contour, Foot, WezTerm, kitty, and others. Terminals that don't recognize it silently ignore the sequence (standard VT behavior for unknown private modes), so enabling it is always safe.
JLine 4.0.0 added built-in Mode 2027 support, handling the entire lifecycle:
TerminalBuilder.build() automatically sends the DECRQM query and parses the response to detect Mode 2027 supportcolumnLength() and columnSubSequence() use grapheme clustering internallyDisplay.update() uses the terminal's grapheme mode for its internal cursor position tracking, fixing render artifacts with emojiAbstractTerminal.doClose() automatically sends CSI ?2027l to disable the mode, so the terminal is left in a clean stateInitially (JLine 4.0.0โ4.0.8), the no-arg columnLength() still used per-codepoint wcwidth and grapheme-aware width required passing a Terminal instance via columnLength(terminal). This was fixed in JLine 4.0.9โ4.0.10 through several issues:
Since JLine 4.0.10, the no-arg columnLength() correctly handles grapheme clusters, so no Terminal parameter needs to be threaded through application code.
Upgrade from JLine 3.30.6 to JLine 4.0.10 and use its built-in Mode 2027 support. No custom width logic or terminal threading is needed โ JLine handles probing, enabling, width calculation, and cleanup transparently.
The charm.ansi.width namespace provides thin wrappers (column-length, column-sub-sequence) around JLine's AttributedString methods for type hints and int coercion, keeping call sites clean.
Terminal reference through the call stackDisplay also benefits from grapheme mode, fixing screen diffing artifactsCan you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| โ | Move to previous article |
| โ | Move to next article |
| Ctrl+/ | Jump to the search field |