ITADN

Regional Indicator columnWidth returns 2 but wcwidth() returns 1, breaking flag emoji in tmux

#515Opengi11es 创建于 2026-04-01
G
gi11escommented
## Bug ### Summary `UnicodeUtil.columnWidth(rune:)` returns **2** for individual Regional Indicator (RI) symbols (U+1F1E6–U+1F1FF), but the Unicode East Asian Width property for RI symbols is "N" (Neutral/Narrow), and `wcwidth()` returns **1** on all major platforms (macOS, Linux, iOS). This width mismatch causes cursor position divergence when SwiftTerm is used with terminal multiplexers (tmux) or any program that uses `wcwidth()` for cursor positioning. The result is broken flag emoji rendering: RI symbols from different flags get paired incorrectly, producing garbled output that only a full screen redraw can fix. ### Root cause In `Utilities.swift`, `columnWidth(rune:)`: ```swift if isRegionalIndicator(rune) { return 2 // ← Should be 1 to match wcwidth() } ``` This was added in commit 9d231170. The intent was correct (flag emoji _are_ 2 cells wide), but the width should be 1 for _individual_ RI symbols. Two RIs combine into a flag via the combining logic in `handlePrint`, and the _combined_ flag should then be widened to 2. ### How it breaks When tmux (or any multiplexer) runs inside SwiftTerm: 1. tmux treats each RI as width 1 (matching `wcwidth()`) 2. SwiftTerm treats each RI as width 2 3. For a flag like 🇫🇷 (F+R), tmux sends F at column N and R at column N+1 4. SwiftTerm places F at column N with width 2 (padding at N+1), cursor advances to N+2 5. R arrives at N+1 (tmux's cursor position) but SwiftTerm's cursor is at N+2 6. Cursor repositioning and subsequent characters land at wrong columns 7. RI symbols from different flags get combined with wrong partners (e.g., F+F instead of F+R) ### Reproduction 1. Run tmux inside a SwiftTerm-based terminal 2. Have a program output lines with flag emojis while other content updates: ```bash for i in $(seq 1 20); do printf "Board %d 🇫🇷: OK\n" $i; sleep 0.2; done ``` 3. As tmux performs partial screen repaints, flags break into individual boxed RI letters 4. Any full screen redraw (tmux copy-mode scroll, mouse drag) fixes all flags instantly Also reproduces during shell tab-completion of strings containing flag emojis. ### Verification ```python import ctypes libc = ctypes.CDLL('libc.dylib') # or 'libc.so.6' on Linux libc.wcwidth.argtypes = [ctypes.c_wchar] libc.wcwidth.restype = ctypes.c_int print(f"wcwidth(U+1F1EB) = {libc.wcwidth(chr(0x1F1EB))}") # → 1 ``` ```python import unicodedata print(unicodedata.east_asian_width(chr(0x1F1EB))) # → "N" (Narrow) ``` ### Fix Three changes needed: 1. **Width correction** (`Utilities.swift`): Change `columnWidth` for RI from 2 → 1 2. **Combine width-up** (`Terminal.swift`): When two width-1 RIs combine into a flag, widen the result to 2 and create a padding cell 3. **Adjacency guard** (`Terminal.swift`): Require `buffer.x == last.x + 1` in the RI combining check to prevent wrong pairing when a cell is overwritten during partial repaints I'll submit a PR with the fix and test coverage. ### Environment - SwiftTerm main branch (commit 9d231170 introduced the width-2 RI) - macOS 15.4, tmux 3.6a - Affects all platforms (macOS, iOS, Linux) — `wcwidth()` returns 1 everywhere
2 条评论