Rendering complex scripts in terminal and OSC 66 | Santhosh ThottingalSanthosh Thottingal
BlogTagsArchivesSearchHome » BlogsRendering complex scripts in terminal and OSC 66March 22, 2026 · 11 min · Santhosh ThottingalTable of ContentsWhy complex script rendering is hard in terminalsMonospace fontsWidth PredictionThe Shaping vs. Rendering DistinctionKitty’s Text Sizing ProtocolProtocol (OSC 66)osc66AlgorithmAdoption and LimitationsStandardization and the FutureThe TCSS Working GroupMode 2027Further ReadingAs 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 ChampionsWidth 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 tableCharacterU+CodewcwidthMalayalam: അ (a)U+000D051Malayalam: ആ (aa)U+000D061Malayalam: ക (ka)U+000D151Malayalam: ഴ (zha)U+000D341Malayalam: ൾ (chillu ll)U+000D7E1Malayalam virama ് (U+0D4D)U+000D4D0Chinese: 中 (zhong, middle)U+004E2D2Japanese: 日 (hi, sun/day)U+0065E52Korean: 한 (han)U+00D55C2CJK: 語 (language)U+008A9E2CJK: 火 (fire)U+00706B2Fullwidth: A (U+FF21)U+00FF212Fullwidth: ! (U+FF01)U+00FF012Symbol: ★ BLACK STAR (U+2605)U+0026051Symbol: → RIGHT ARROW (U+2192)U+0021921ZWNJ (U+200C)U+00200C0ZWJ (U+200D)U+00200D0Combining grave accent (U+0300)U+0003000From 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 textKittyGnome terminalസന്തോഷ്Screenshot from https://www.unicode.org/L2/L2023/23107-terminal-suppt.pdfWhen 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 kittyThe 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> BELESC = \x1b, BEL = \x07 (the terminator — marks end of escape sequence)Metadata is colon-separated key=value pairsText payload is UTF-8, max 4096 bytes per escape codeThe BEL byte terminates the escape sequence; it is NOT part of the textExample 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 occupies s*w cells wide, s cells 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:w maxes 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/osc66Algorithm#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 appliedAs 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 appliedAdoption 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.CaveatsNote 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. RIPThe 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 demystifiedWhat happens when you press a key in your terminal?A history of the ttyUnderstanding ASCII (and terminals)Comprehensive keyboard handling in terminalsFix Keyboard Input on Terminals - PleaseGrapheme Clusters and Terminal EmulatorsState of the TerminalControl Sequences - ghostty documentationNLPRenderingTerminalRustMalayalam© 2026 Santhosh Thottingal. All rights reserved. · Powered by Hugo & PaperMod |
The persistent challenge of rendering complex scripts within terminal environments, particularly languages like Arabic and the Indic scripts (Malayalam, Tamil, Devanagari), represents a significant limitation for developers such as Santhosh Thottingal who frequently work with language processing. This article elucidates the complexities, highlighting the fundamental issues with the character-cell grid model that underpins traditional terminal emulation. The core problem stems from the incompatibility of these scripts' contextual shaping, reordering, and glyph fusion—concepts fundamentally at odds with the static, fixed-width grid approach. Monospace fonts exacerbate this due to their inherent inability to accommodate the variable widths required by these scripts, creating a significant visual distortion when terminals attempt to force these clusters into a rigid grid.
The article then introduces Kitty, developed by Kovid Goyal, and its innovative “text sizing protocol” (OSC 66). This protocol, employing a custom algorithm, mitigates the limitations of the standard wcwidth() function by meticulously interpreting Unicode standards to determine grapheme cluster widths. OSC 66 operates via escape sequences transmitted between applications, allowing Kitty to inform the terminal of the actual cell counts required for rendering complex scripts. The protocol’s format involves key-value pairs, defining scale, width, and fractional scaling parameters, operating within a constrained framework. The utility, osc66, built in Rust, provides a command-line tool to automatically generate these OSC 66 escape sequences, illustrating a workable solution path. It addresses the problem by shaping the Unicode string based on HarfBuzz, a software library designed for shaping text.
The technical approach demonstrates a critical distinction between “shaping” and “rendering” within graphical environments. The shaping process converts a sequence of Unicode codepoints into glyph IDs and pixel positions, while rendering draws those glyphs onto the screen. Terminals typically bypass shaping, rendering codepoints individually, a design choice driven by the need to maintain precise cursor positioning and synchronization with the terminal grid. This creates a communication barrier between applications and the terminal, leading to misaligned input and visual corruption. Kitty’s protocol aims to bridge this gap by providing explicit control over cell occupancy, circumventing the limitations of wcwidth().
Santhosh Thottingal’s work highlights the ongoing efforts to standardize terminal support for complex scripts. The Terminal Complex Script Support Working Group (TCSS WG) within the Unicode Technical Committee is developing a new standard—the “Terminal Cluster” model—moving away from codepoint-based rendering to a system where terminl programs analyze entire strings to determine width. Additionally, Mode 2027, proposed by the author of the Contour terminal, is a grapheme-based support mechanism further illustrating the continued investigation into this area. Despite these developments, the situation remains fragmented, with Kitty currently offering the most reliable complex script support, while other projects have open discussions and ongoing explorations. For users of scripts like Malayalam and Indic languages, even the partial support provided by OSC 66 represents a tangible advancement compared to the unaddressed problem. Ongoing standardization efforts, coupled with continued innovation from projects like Kitty, suggest a future where terminal-based language processing is more readily accessible and visually accurate. |