feat(core): expose no_zwj as opt-in WidthMethod#751
Open
Flare576 wants to merge 1 commit intoanomalyco:mainfrom
Open
feat(core): expose no_zwj as opt-in WidthMethod#751Flare576 wants to merge 1 commit intoanomalyco:mainfrom
Flare576 wants to merge 1 commit intoanomalyco:mainfrom
Conversation
The Zig layer has fully implemented and tested no_zwj width calculation since its introduction, but the TypeScript API only exposed two of the three enum values (wcwidth and unicode), making no_zwj unreachable from JavaScript consumers. Wire up the missing value end-to-end: - Add "no_zwj" to the WidthMethod union type in types.ts - Map "no_zwj" -> 2 in the four widthMethodCode conversions in zig.ts - Map u8 value 2 -> .no_zwj in the four Zig FFI entry points in lib.zig - Map .no_zwj -> 2 in getTerminalCapabilities serialization in lib.zig - Update the widthMethod getter in renderer.ts to return "no_zwj" when terminal capabilities report value 2 (e.g. via OPENTUI_FORCE_NOZWJ) No defaults are changed. Existing users of "wcwidth" and "unicode" are completely unaffected. The new value allows applications like OpenCode to opt into no_zwj mode for terminals that do not render ZWJ emoji sequences as joined glyphs (tmux, older terminals, etc.), preventing layout corruption without affecting users on terminals that support ZWJ.
kommander
reviewed
Mar 3, 2026
| const pool = gp.initGlobalPool(globalArena); | ||
| const link_pool = link.initGlobalLinkPool(globalArena); | ||
| const wMethod: utf8.WidthMethod = if (widthMethod == 0) .wcwidth else .unicode; | ||
| const wMethod: utf8.WidthMethod = if (widthMethod == 0) .wcwidth else if (widthMethod == 2) .no_zwj else .unicode; |
Collaborator
There was a problem hiding this comment.
Seeing the repetition so clearly I think these should be using @enumFromInt and @enumToInt utils.
kommander
reviewed
Mar 3, 2026
| } | ||
|
|
||
| const widthMethodCode = widthMethod === "wcwidth" ? 0 : 1 | ||
| const widthMethodCode = widthMethod === "wcwidth" ? 0 : widthMethod === "no_zwj" ? 2 : 1 |
Collaborator
There was a problem hiding this comment.
Similar here, seeing the repetition, it would make sense to encapsulate that conversion.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
ZWJ emoji sequences (👩🚀 👨👩👧 🏳️🌈) cause layout corruption in terminals that don't render them as joined glyphs — most notably tmux, but also many older terminals. Text after a ZWJ sequence shifts across the screen, wraps incorrectly, and input box spacing breaks.
The
no_zwjwidth calculation mode already exists in the Zig layer, is fully implemented, and has comprehensive test coverage (utf8_no_zwj_test.zig). The environment variableOPENTUI_FORCE_NOZWJis already registered and wired throughterminal.zig. However,no_zwjis missing from the TypeScriptWidthMethodunion type and from the numeric mapping at the FFI boundary, making it completely unreachable from JavaScript consumers.Solution
Wire
no_zwjthrough the full TypeScript→FFI chain without changing any defaults:packages/core/src/types.ts: Add"no_zwj"to theWidthMethodunionpackages/core/src/zig.ts: Map"no_zwj"→2in the fourwidthMethodCodeconversions (previously only0/1existed)packages/core/src/zig/lib.zig: Mapu8value2→.no_zwjin the four FFI entry points; map.no_zwj→2ingetTerminalCapabilitiesserializationpackages/core/src/renderer.ts: Update thewidthMethodgetter to return"no_zwj"when capabilities report2(e.g. viaOPENTUI_FORCE_NOZWJ)Behavior
unicode(default, unchanged)no_zwj(opt-in)Only ZWJ-joined sequences are affected. Skin tone modifiers, regional indicator flags, combining accents, and CJK characters are all unaffected.
No Breaking Changes
"unicode"everywhere"wcwidth"behavior is unchangedOPENTUI_FORCE_NOZWJenv var now correctly round-trips throughgetTerminalCapabilities→widthMethodgetter"wcwidth"or"unicode"see identical behaviorRelated