As a programmer, I spend most of my time in a terminal application like Kitty. I use Neovim as my code editor. I use CLI based AI agents. But the biggest pain, even in 2026, is that there is no terminal that can render complex scripts like Indic languages or Arabic. This is a significant limitation for me, as most of my work involves language processing.
In this article, I will give a brief overview of why this issue remains unsolved—covering the character-cell grid model, width measurement, and the distinction between text shaping and rendering—along with ongoing efforts and a small tool I built recently that illustrates a solution path.
Why complex script rendering is hard in terminals
The architecture of modern terminal emulators like GNOME Terminal, Kitty, and Ghostty is a software-based reproduction of physical hardware constraints from the 1970s. The transition from electromechanical teletypewriters to video display terminals established the character-cell grid as the universal interface for command-line computing. In this model, the screen is conceptualized as a matrix of rows and columns, where each intersection—a cell—houses exactly one graphic character.
Text User Interfaces (TUIs) rely on this assumption to calculate layouts and position the cursor. When a developer uses a tool like Vim or htop, the application calculates the exact column and row for every character, assuming that moving the cursor one “step” always corresponds to one cell on the display.
The “one character to one cell” mapping represents a fundamental architectural failure when applied to complex writing systems. These scripts, which include Arabic and the Indic family (Malayalam, Tamil, Devanagari), are characterized by contextual shaping, character reordering, and non-linear glyph fusion—concepts that are inherently incompatible with a static grid. Arabic has additional characteristics of right to left writing.The Arabic Calligraphic Problem
Monospace fonts
This problem is compounded by how monospace fonts work. Monospace fonts are designed for working with the above constraints. But the concept of “monospace” cannot be defined for Indic scripts or Arabic. Every character in a monospace font takes exactly the same amount of horizontal space. Latin letters can fit into that design constraint, but not other scripts.
As an illustration, see the following Malayalam ligatures and observe the width it takes:
A sequence of several Unicode codepoints might need to collapse into a single visual unit, or a single codepoint might need to expand to cover an irregular number of cells to remain legible. When terminals attempt to force these clusters into a rigid grid, the result is scrambled or overlapping charactersState of Terminal Emulators in 2025: The Errant Champions
Width Prediction
Given these constraints, how does a terminal decide how many cells a character should occupy? For decades, the standard method for a terminal application to determine the width of a character has been the wcwidth() function. This function takes a single Unicode codepoint and returns its width in cells (0, 1, or 2). However, this function only takes one argument—a single codepoint—and returns a fixed value representing its width, which is clearly insufficient for complex scripts where multiple codepoints contribute to each grapheme cluster.
source code for the C program that prepared this table
| Character | U+Code | wcwidth |
|---|---|---|
| Malayalam: അ (a) | U+000D05 | 1 |
| Malayalam: ആ (aa) | U+000D06 | 1 |
| Malayalam: ക (ka) | U+000D15 | 1 |
| Malayalam: ഴ (zha) | U+000D34 | 1 |
| Malayalam: ൾ (chillu ll) | U+000D7E | 1 |
| Malayalam virama ് (U+0D4D) | U+000D4D | 0 |
| Chinese: 中 (zhong, middle) | U+004E2D | 2 |
| Japanese: 日 (hi, sun/day) | U+0065E5 | 2 |
| Korean: 한 (han) | U+00D55C | 2 |
| CJK: 語 (language) | U+008A9E | 2 |
| CJK: 火 (fire) | U+00706B | 2 |
| Fullwidth: A (U+FF21) | U+00FF21 | 2 |
| Fullwidth: ! (U+FF01) | U+00FF01 | 2 |
| Symbol: ★ BLACK STAR (U+2605) | U+002605 | 1 |
| Symbol: → RIGHT ARROW (U+2192) | U+002192 | 1 |
| ZWNJ (U+200C) | U+00200C | 0 |
| ZWJ (U+200D) | U+00200D | 0 |
| Combining grave accent (U+0300) | U+000300 | 0 |
From the above table, you can observe that wcwidth is mostly concerned with East Asian scripts. For a text like my name സന്തോഷ്, wcwidth calculates 7 cells. However, there are only 3 ligatures in it. Kitty terminal does not use this strategy. It calculates that it needs 3 cells and each of these ligatures is ‘fitted’ into those 3 cells, resulting in the following issue:
| Actual text | Kitty | Gnome terminal |
|---|---|---|
| സന്തോഷ് | ![]() | ![]() |

Screenshot from https://www.unicode.org/L2/L2023/23107-terminal-suppt.pdf
When the terminal emulator and the TUI application disagree on the width of a character, the contract of the grid is broken. The application might think the cursor is at column 10, while the terminal, having fused several characters, renders it at column 8. This leads to misaligned input, overwritten text, and visual corruption.

Screenshot of Malayalam text in kitty
The Shaping vs. Rendering Distinction
To understand why terminals struggle with complex scripts, it helps to separate two stages that graphical applications handle transparently but terminals do not. Text rendering in a modern graphical environment is a two-stage process: shaping and rendering. Shaping is the process of converting a sequence of Unicode codepoints into a sequence of glyph IDs and their exact pixel positions. Rendering is the process of drawing those glyphs onto the screen.
Most terminals today skip shaping entirely and render codepoint by codepoint. A major reason is that advanced terminal applications such as shells and text editors need to know exactly where the cursor is at all times and remain in sync with the terminal grid state. Proper shaping would require the terminal to commit to a cell layout before the application can query it—a coordination problem that no standard currently solves cleanly. This is the gap that Kitty’s text sizing protocol attempts to bridge.
Kitty’s Text Sizing Protocol
Kitty, developed by Kovid Goyal, takes a different approach. While remaining strictly character-cell based for maximum performance, Kitty employs a custom text-splitting algorithm that meticulously interprets Unicode standards to determine how grapheme clusters should be divided into cells. Kitty has also pioneered a new “text sizing protocol” that allows programs running in the terminal to explicitly control how many cells a character occupies. This bypasses the limitations of wcwidth() and provides a mechanism for TUI applications to handle complex scripts without causing visual corruption.
Protocol (OSC 66)
Format: ESC ] 66 ; <key>=<value>[:<key>=<value>...] ; <text> BEL
ESC=\x1b,BEL=\x07(the terminator — marks end of escape sequence)- Metadata is colon-separated
key=valuepairs - Text payload is UTF-8, max 4096 bytes per escape code
- The
BELbyte terminates the escape sequence; it is NOT part of the text
Example in shell:
printf "\e]66;w=2;സ\a" # 2 cells for സ, terminated by BEL (\a)
printf "\e]66;w=4;ന്തോ\a" # 4 cells for ന്തോ
printf "\e]66;w=2;ഷ്\a" # 2 cells for ഷ്
Key metadata parameters:
s(1-7): Overall scale factor. Text occupiess*wcells wide,scells tall.w(0-7): Width in scaled cells. 0 = terminal auto-calculates from Unicode.n/d(0-15): Fractional scale numerator/denominator.v/h(0-2): Vertical/horizontal alignment for fractional scaling.
Constraints:
wmaxes out at 7.- Text payload max 4096 bytes per escape code.
- Text must be escape-code-safe UTF-8.
It is up to the clients to pass the correct w values (or other values — s/d/n/v/h, but all contribute to a calculated w). How do we calculate that? I wrote a program for that.
osc66
To test this in practice, I wanted to see whether OSC 66 could make Malayalam text legible in my own terminal workflow. The protocol requires the client to supply correct w values, but no existing tool did that for shaped Indic text. So I wrote one.
osc66 is a CLI utility, written in Rust, that accepts text as input and outputs text with OSC 66 escape characters.
Source code: https://github.com/santhoshtr/osc66
Algorithm
The processing pipeline flows from stdin through a line reader, then into HarfBuzz shaping, cluster grouping, width calculation, and finally OSC 66 emission to stdout.
Font loading begins with fontconfig locating the font file by family name, after which HarfBuzz loads a Face and Font from it. The tool then establishes a reference advance by shaping the ASCII character '0' to obtain its x_advance. This serves as the baseline—one cell equals that many font units. Each input line is then shaped as a single HarfBuzz buffer to preserve cross-glyph context such as ligatures and contextual forms.
Once shaping is complete, glyphs are grouped by their cluster field, which is a byte offset into the input. The x_advance values within each group are summed to produce the total advance for that cluster. The cell count is then calculated using the formula
\begin{aligned} \text{cells} = \left\lceil \frac{\text{cluster\_advance}}{\text{ref\_advance}} \right\rceil \end{aligned}
,clamped to 0–7 to fit the protocol’s 3-bit w field.
Finally, each cluster is emitted as an OSC 66 escape sequence in the form ESC]66;w=<cells>;<text>BEL. Zero-width clusters such as virama and ZWJ are skipped entirely.
Following is a screenshot of Malayalam rendering in Kitty when text is passed through osc66.

Screenshot of Malayalam text with OSC66 applied
As you can see the output rendering is much better and readable now. However there are visible spaces between glyphs in many places. This happens because we are mapping the glyph widths to one or more cells. Since there are no fractional cells and cell width is fixed, there is always some extra space left out. I tried various strategies for improving the rounding formula $ \lceil \frac{\text{cluster\_advance}}{\text{ref\_advance}} \rceil $ by replacing the ceiling function with floor and round, but none of them give a perfect solution. If we prioritise omitting glyph cut-off, we cannot use floor rounding. If we use ceiling, there will be space.
If a Malayalam ligature’s actual pixel width is 1.3x the width of a standard English character, forcing it into the terminal grid requires allocating exactly 2 cells.
Because the terminal cannot render a 0.7 “fractional cell” of background, that 0.7 space is left visually empty. Complex scripts are a continuous flow, and the grid forces discrete math on them.
Fractional cells are not possible. Maybe a ’narrow cell’ concept can be useful so that we can do more divisions and achieve more optimal rounding. Another expensive strategy is in the font, but that approach is fragile — designing the font so that it restricts glyph width to a multiple of a fixed cell width and avoids fractional width multipliers. Meaning, every glyph is of width 1x, 2x, 3x, etc., where x is the size of the smallest ligature. But this will have a negative impact on the aesthetics of the script for sure.

Screenshot of Hindi text with OSC66 applied
Adoption and Limitations
OSC 66 works well in Kitty, but the tool’s usefulness is constrained by how widely the protocol is supported. Currently it is supported by Kitty and Foot. Discussions are ongoing in other projects—Ghostty has an open pull request and a tracking issue for implementation, while neovim tracks the feature request in issue 32539.
Caveats
Note that if tmux is used within kitty, it overrides the rendering and you won't see the output text.Note that even after osc 66 application, kitty needs to be configured to render the output with the font you used.
symbol_map U+0D00-U+0DFF Manjari
symbol_map U+0900-U+09FF Noto Sans Devanagari
Standardization and the Future
OSC 66 demonstrates that the problem is solvable, but it is ultimately a workaround—a client-side shim for terminals that lack native complex script support. The real fix requires agreement at the protocol level, across terminal emulators, shell applications, and TUI frameworks simultaneously. That work is underway.
The TCSS Working Group
The most promising long-term solution to the complex script crisis is the formation of the Terminal Complex Script Support Working Group (TCSS WG) under the Unicode Technical Committee in 2023. This group, which includes representatives from Microsoft, Apple, and major terminal projects, is working to define a new standard for how terminals should handle shaped, bidirectional, and wide text on a fixed-width grid. I could not find any recent activity from this working group. The last activity I could find is from December 2023. Frederick Brennan submitted an opposition to this spec and was subsequently added to the committee.Sadly, Frederick Brennan passed away recently. I fondly remember some interactions we had about typography and type design software. RIP
The TCSS WG proposes moving away from codepoint-based rendering to a “Terminal Cluster” model. In this model:
- String-Based Measurement: The terminal analyzes a full string of text to determine width, rather than asking for individual codepoint widths.
- Explicit Metrics: Each cluster in the screen buffer is assigned a metric representing how many cells it occupies.
- Logical Ordering: The internal representation of text remains in logical (typing) order, ensuring that copy-paste and search continue to work, while the display layer handles BiDi reordering and shaping.
Mode 2027
Mode 2027 is a proposal for grapheme support in terminals. This proposal is from the author of the Contour terminal. The idea is that a program running in a terminal can notify the terminal that it wishes to operate with full support for grapheme clustering, and this feature can be turned on and off. However, I don’t see any recent activity about this proposal. I read about this proposal in an article by Ghostty’s developer Mitchell HashimotoGhostty 1.3.0, released recently, claims it improved complex script rendering. In my testing I did not find any improvement for Malayalam or Indic scripts.
The situation today is fragmented: one terminal with a working protocol, a few others with open discussions, and two competing standardisation efforts at different stages. Progress is real but slow. For those of us who write in Malayalam or other Indic scripts and live in the terminal, even partial support—like what osc66 provides in Kitty—is a meaningful improvement over nothing. At least now I can pipe the text to osc66 to read.
I hope the momentum builds. Thanks for reading.
Further Reading
- The TTY demystified
- What happens when you press a key in your terminal?
- A history of the tty
- Understanding ASCII (and terminals)
- Comprehensive keyboard handling in terminals
- Fix Keyboard Input on Terminals - Please
- Grapheme Clusters and Terminal Emulators
- State of the Terminal
- Control Sequences - ghostty documentation

