Skip to content

feat(core): expose no_zwj as opt-in WidthMethod#751

Open
Flare576 wants to merge 1 commit intoanomalyco:mainfrom
Flare576:feat/no-zwj-width-method
Open

feat(core): expose no_zwj as opt-in WidthMethod#751
Flare576 wants to merge 1 commit intoanomalyco:mainfrom
Flare576:feat/no-zwj-width-method

Conversation

@Flare576
Copy link

@Flare576 Flare576 commented Feb 27, 2026

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_zwj width calculation mode already exists in the Zig layer, is fully implemented, and has comprehensive test coverage (utf8_no_zwj_test.zig). The environment variable OPENTUI_FORCE_NOZWJ is already registered and wired through terminal.zig. However, no_zwj is missing from the TypeScript WidthMethod union type and from the numeric mapping at the FFI boundary, making it completely unreachable from JavaScript consumers.

Solution

Wire no_zwj through the full TypeScript→FFI chain without changing any defaults:

  • packages/core/src/types.ts: Add "no_zwj" to the WidthMethod union
  • packages/core/src/zig.ts: Map "no_zwj"2 in the four widthMethodCode conversions (previously only 0/1 existed)
  • packages/core/src/zig/lib.zig: Map u8 value 2.no_zwj in the four FFI entry points; map .no_zwj2 in getTerminalCapabilities serialization
  • packages/core/src/renderer.ts: Update the widthMethod getter to return "no_zwj" when capabilities report 2 (e.g. via OPENTUI_FORCE_NOZWJ)

Behavior

Mode ZWJ emoji (👩‍🚀) Skin tones (👋🏿) Flags (🇺🇸) Accents (é)
unicode (default, unchanged) 1 glyph, 2 cols 1 glyph, 2 cols 1 glyph, 2 cols 1 glyph, 1 col
no_zwj (opt-in) 2 glyphs, 4 cols 1 glyph, 2 cols 1 glyph, 2 cols 1 glyph, 1 col

Only ZWJ-joined sequences are affected. Skin tone modifiers, regional indicator flags, combining accents, and CJK characters are all unaffected.

No Breaking Changes

  • Default is still "unicode" everywhere
  • "wcwidth" behavior is unchanged
  • OPENTUI_FORCE_NOZWJ env var now correctly round-trips through getTerminalCapabilitieswidthMethod getter
  • Existing consumers passing "wcwidth" or "unicode" see identical behavior

Related

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.
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;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seeing the repetition so clearly I think these should be using @enumFromInt and @enumToInt utils.

}

const widthMethodCode = widthMethod === "wcwidth" ? 0 : 1
const widthMethodCode = widthMethod === "wcwidth" ? 0 : widthMethod === "no_zwj" ? 2 : 1
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar here, seeing the repetition, it would make sense to encapsulate that conversion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants