Regional Indicator columnWidth returns 2 but wcwidth() returns 1, breaking flag emoji in tmux
## 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 条评论