Skip to content

fix(terminal): render country flag emoji as one glyph - #10732

Closed
amirerfantim wants to merge 2 commits into
stablyai:mainfrom
amirerfantim:fix/terminal-country-flag-emoji
Closed

amirerfantim wants to merge 2 commits into
stablyai:mainfrom
amirerfantim:fix/terminal-country-flag-emoji

Conversation

@amirerfantim

@amirerfantim amirerfantim commented Jul 26, 2026 •

Copy link
Copy Markdown

Fixes #10731.

User-visible change

Country flag emoji (🇺🇸 🇩🇪 🇯🇵) rendered as two lettered tiles in the terminal instead of the flag. They now render as a single flag glyph.

Visual change: yes, terminal glyph rendering only — no UI/layout/component change. Before: printf '🇺🇸 🇩🇪 🇯🇵' paints boxed letter pairs (🇺 🇸). After: it paints the flags. No screenshot attached because the difference is font shaping in the terminal grid; the cell-level assertions in terminal-unicode-provider.test.ts capture it precisely (the row string is identical either way — only the cell clustering differs, which is exactly why this went unnoticed).

Root cause

A country flag is two adjacent Regional Indicator Symbols (U+1F1E6–U+1F1FF) with no ZWJ — e.g. 🇺🇸 = U+1F1FA U+1F1F8. OrcaUnicodeProvider.charProperties only joins across ZWJ, so each indicator stayed in its own cell. Chromium then shaped each one in isolation and drew a lone letter tile rather than shaping the pair into a flag.

Note the indicators are not in xterm's Unicode 11 wide table (wcwidth = 1 each), so total column budgeting was already 2 — the bug is purely missing cluster joining, not width.

Fix

Pair regional indicators into one two-cell cluster, mirroring the existing ZWJ branch:

  • charKind resets to 0 on the completed pair, so a third indicator opens a new flag instead of chaining into a three-glyph cluster (RI RI RI → flag + lone indicator, per UAX Break up large files into well-organized modules #29).
  • Joined width is 2, matching the pair's total wcwidth (each indicator is East_Asian_Width=Neutral → 1 + 1). This keeps cursor budgeting in step with what CLIs compute, so positioned writes don't drift.

Considered and rejected: swapping to @xterm/addon-unicode-graphemes (full UAX #29 segmentation). It would compute widths by spec rather than by the CLI convention this provider deliberately encodes for ZWJ sequences, regressing that tuned behavior and the terminal rendering goldens. The targeted change fits the existing architecture.

Tests

  • src/shared/terminal-unicode-provider.test.ts (new) — asserts cell contents + widths via createRendererParityTerminal: pair folding, consecutive flags, trailing unpaired indicator, column budgeting, and a ZWJ/CJK/plain-emoji regression guard.
  • src/main/daemon/headless-emulator-unicode-width.test.ts — flag cases in the existing style, covering the daemon mirror used by mobile/remote.

Verified the three cluster-level tests fail without the provider change and pass with it. The width-budget assertions pass either way by design — they lock the two-cell contract against a future "fix" that returns precedingWidth.

pnpm lint (oxlint on changed files) and pnpm typecheck (tsconfig.node.json) are clean. Ran the src/main/daemon, src/renderer/src/lib/pane-manager, and src/renderer/src/components/terminal-pane suites: 4123 passed. The only failures are node-pty-fd-leak.test.ts / shell-ready.test.ts with posix_spawnp failed — reproduced identically on a clean tree with these changes stashed (unbuilt native node-pty in my sandbox), unrelated to this change.

Code review summary

  • Cross-platform: pure Unicode arithmetic in src/shared, no platform branches, no paths, no keybindings. Identical on macOS/Linux/Windows.
  • SSH / remote / local: the provider is shared by the renderer pane, the daemon headless emulator, and the web bundle, so local, SSH, remote-server and mobile viewers all pick it up from one place; no host assumptions added. Daemon path covered by tests.
  • Agents / integrations / git providers: none touched — this is below the agent layer. Agent TUIs benefit because flag budgeting is unchanged (still 2 columns) while clustering is now correct.
  • Performance: two integer range comparisons on a path that already ran per codepoint, and only for codepoints that reach the fallback. No allocation, no new indirection in the write hot path.
  • UI quality: no component, token, or layout change; STYLEGUIDE.md not implicated.
  • Security: no I/O, no parsing of untrusted structure beyond codepoint values already flowing through the emulator; no new surface.

Notes for reviewers

Skin-tone modifiers, variation selectors and keycap sequences are deliberately out of scope here — they're separate cluster classes and would each need their own width decision. Happy to follow up if you'd like them covered.

Maintainer validation (2026-07-26)

  • Confirmed in an isolated Electron dev instance with the real WebGL terminal: the US, Germany, and Japan flags each render as one combined two-column glyph. An adjacent ZWJ emoji, plain wide emoji, and CJK glyph remained correct.
  • The follow-up commit splits the first flag across two terminal writes, covering arbitrary PTY chunk boundaries while retaining the cell-level clustering assertion.
  • pnpm typecheck passed. Targeted oxlint passed. The provider, daemon width, renderer/daemon parity, and hidden/reveal fuzz suites passed: 17 tests passed and 2 were skipped by their existing gates.
  • Performance audit: no allocation, polling, IPC, or renderer lifecycle work was added. Synthetic headless runs processed 2 million mixed characters in about 30-37 ms of CPU versus 27-31 ms for the prior provider; the measured absolute delta was about 3-6 ms per 2 million characters. The live renderer finished with zero pending or in-flight characters.
  • Full pnpm lint reached pre-existing localization-coverage findings in untouched settings-search files; all changed files passed oxlint, formatting hooks, and typecheck.

Country flags are two adjacent regional indicators with no ZWJ, so the
ZWJ-only join rule in OrcaUnicodeProvider left each indicator in its own
cell. Chromium then shaped them separately and painted two lettered tiles
instead of the flag.

Pair regional indicators into a single two-cell cluster, resetting charKind
on the pair so a third indicator opens the next flag rather than chaining.
Two cells matches the pair's total wcwidth (each indicator is
East_Asian_Width Neutral), so cursor budgeting stays in step with CLIs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 26, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds regional-indicator detection and pairing to terminal Unicode width handling. New tests cover flag clustering, consecutive flag pairs, unpaired indicators, cell budgeting, cursor positioning, ZWJ emoji sequences, and other wide glyphs.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes match #10731 by pairing adjacent Regional Indicators into flag glyphs and starting a new pair on a third indicator.
Out of Scope Changes check ✅ Passed The PR only changes Unicode flag handling and related tests, with no unrelated code or feature additions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title clearly summarizes the main change: fixing country flag emoji rendering in the terminal.
Description check ✅ Passed The description covers summary, visual impact, tests, AI review, security, and notes, though it doesn't use the exact template headings.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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.

Country flag emoji (Regional Indicator pairs) don't render as combined flag glyph in terminal

2 participants