# Compatibility Analysis: displaywidth, go-runewidth, and uniseg

> Generated by Cursor IDE using Claude Sonnet 4.5, and edited by @clipperhouse

This document summarizes the compatibility findings between three Go libraries for Unicode string width calculation:

- [clipperhouse/displaywidth](https://github.com/clipperhouse/displaywidth) (this package)
- [mattn/go-runewidth](https://github.com/mattn/go-runewidth)
- [rivo/uniseg](https://github.com/rivo/uniseg)

## Basic Unicode Categories

Most Unicode categories show good compatibility.

| Category | displaywidth | go-runewidth | uniseg |
|----------|--------------|--------------|---------|
| ASCII | ✅ Compatible | ✅ Compatible | ✅ Compatible |
| Latin Extended | ✅ Compatible | ✅ Compatible | ✅ Compatible |
| CJK (Chinese/Japanese/Korean) | ✅ Compatible | ✅ Compatible | ✅ Compatible |
| Arabic | ✅ Compatible | ✅ Compatible | ✅ Compatible |
| Combining Marks | ✅ Compatible | ✅ Compatible | ✅ Compatible |
| Zero-Width Characters | ✅ Compatible | ✅ Compatible | ✅ Compatible |

## Emojis

Regular emojis (😀, 🚀, 🎉, etc.) behave identically:

| Library | Regular Emoji Width |
|---------|---------------------|
| **displaywidth** | Always 2 |
| **go-runewidth** | Always 2 |
| **uniseg** | Always 2 |

### Regional Indicator Pairs (Flags)

Regional indicator pairs (flags like 🇺🇸) are composed of two Regional Indicator symbols.

| Library | Behavior |
|--------|----------|
| **displaywidth** | Width 2 per flag |
| **go-runewidth** | Width 1 per flag |
| **uniseg** | Width 2 per flag |

**Example:** `🇺🇸🇯🇵🇬🇧` (3 flags)
- displaywidth: 6 columns (2+2+2)
- go-runewidth: 3 columns (1+1+1)
- uniseg: 6 columns (2+2+2)

I (@clipperhouse) believe that 2 is the correct width, they are emojis. Ghostty and iTerm display
regional flags as width 2, as does VS Code. Mac Terminal (Tahoe macOS 26) displays them as 1. Sigh.

To repro in your terminal of choice:

```
echo "🇺🇸🇯🇵🇬🇧abc\n123456"
```

I have considered detecting the terminal (like a user agent) and using width 1 for
Mac Terminal as a special case. I kinda hate that, because if Apple corrects it, then
the behavior changes. OTOH, I assume Mac Terminal is the most popular terminal and
so it might be better for end-users.

## Variation Selectors

VS15 and VS16 from [Unicode TR51](https://unicode.org/reports/tr51/#Emoji_Variation_Sequences)

| Library | VS16 (U+FE0F) | VS15 (U+FE0E) |
|---------|---------------|---------------|
| **displaywidth** | Forces emoji presentation (width 2) | No effect, preserves base width |
| **go-runewidth** | Treated as separate character (width 1) | Treated as separate character (width 1) |
| **uniseg** | Treated as part of emoji (width 2) | Forces width 1 |

**Example:** `☺️⌛︎❤️` (3 emoji with variation selectors)
- displaywidth: 6 columns
- go-runewidth: 4 columns
- uniseg: 5 columns

I would appear to me (@clipperhouse) that the handling of VS15 is not widely
agreed upon. Some libraries and standards (such as wcwidth) interpret it as
"always narrow to width 1". Others (such as this library) interpret it as
"no effect on width, use the base character width".

Here is [a conversation on GitHub](https://github.com/contour-terminal/contour/discussions/1178#discussioncomment-6778716) and an [explanation from Grok](https://grok.com/share/bGVnYWN5LWNvcHk%3D_274f540c-c9a6-47c7-9d4f-47697ed20032).

## Keycap Sequences

Keycap sequences like 1️⃣ are formed by: base character + variation selector (U+FE0F) + combining enclosing keycap (U+20E3).

| Library | Behavior | Width per Keycap |
|---------|----------|------------------|
| **displaywidth** | Treats as emoji | 2 columns |
| **go-runewidth** | Treats base character | 1 column |
| **uniseg** | Treats as base character | 1 column |

**Example:** `1️⃣#️⃣` (2 keycap sequences)
- displaywidth: 4 columns (2 per keycap)
- go-runewidth: 2 columns (1 per keycap)
- uniseg: 2 columns (1 per keycap)

## East Asian Ambiguous Width

[East Asian Ambiguous characters](https://www.unicode.org/reports/tr11/#Ambiguous) (★, °, ±, etc.) can be rendered as either narrow (1 column) or wide (2 columns) depending on configuration.

| Library | Default | With EastAsianWidth=true |
|---------|---------|--------------------------|
| **displaywidth** | Width 1 | Width 2 |
| **go-runewidth** | Width 1 | Width 2 |
| **uniseg** | Width 1 | Width 2 |

**Example:** `★°±` (3 ambiguous characters)
- displaywidth default: 3 columns
- displaywidth with EastAsianWidth=true: 6 columns
- go-runewidth default: 3 columns
- go-runewidth with EastAsianWidth=true: 6 columns
- uniseg default: 3 columns
- uniseg with EastAsianAmbiguousWidth=2: 5 columns (usually)

## Detailed Test Results

> Run `go test -v` in the `comparison/` directory to see comprehensive behavior comparisons between libraries.

### Test Case: "Hello 世界! 😀🇺🇸"

**Breakdown:**
- "Hello " = 6 columns (ASCII)
- "世界" = 4 columns (CJK, 2 each)
- "! " = 2 columns (ASCII)
- "😀" = 2 columns (emoji)
- "🇺🇸" = 1-2 columns (flag, depends on library)

**Results:**
- displaywidth: 16 columns (flag = 2 columns)
- go-runewidth: 15 columns (flag = 1 column)
- uniseg: 16 columns (flag = 2 columns)

### Test Case: "🚀🚀🚀" (3 rocket emoji)

**Results:**
- displaywidth: 6 columns (2 per emoji)
- go-runewidth: 6 columns (2 per emoji)
- uniseg: 6 columns (2 per emoji)

## Further Reading

- [State of Terminal Emulation 2025](https://www.jeffquast.com/post/state-of-terminal-emulation-2025/)
