Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ extension Character {
/// There is no standard for this, but it seems like most terminals treat
/// emojis and ideographs as double width.
public var displayWidth: Int {
Copy link
Copy Markdown
Contributor

@pepicrft pepicrft Dec 26, 2025

Choose a reason for hiding this comment

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

I'd suggest to adjust the implementation to use wcwidth, which is a POSIX C function that returns the number of columns a character occupies in a terminal. Terminal, shells, and CLI tools like ls & vim use it:

#if canImport(Darwin)
import Darwin
#elseif canImport(Glibc)
import Glibc
#endif

extension Character {
    public var displayWidth: Int {
        unicodeScalars.reduce(0) { total, scalar in
            let w = wcwidth(wchar_t(scalar.value))
            // wcwidth returns -1 for non-printable, treat as 0
            return total + max(0, Int(w))
        }
    }
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Here's the problem I ran into with wcwidth

  1. Emoji sequences - Characters like 👨‍👩‍👦 are actually multiple Unicode scalars joined by Zero-Width Joiners (ZWJ):
    👨 + ZWJ + 👩 + ZWJ + 👦 = 👨‍👩‍👦
  2. wcwidth sees each piece separately and sums them (~8), but terminals render it as one 2-column character.
  3. Variation selectors - ✓ (U+2713) vs ✓️ (U+2713 + U+FE0F). The second has a variation selector that tells the terminal "render this as emoji" (width 2), but wcwidth ignores it.
  4. I put a flag emoji - 🇺🇸 is two "regional indicator" characters. wcwidth returns -1 (unknown) for each, but terminals show a single 2-column flag.

What do you think we should do?

if unicodeScalars.contains(where: \.properties.isEmojiPresentation) {
let hasEmojiPresentation = unicodeScalars.contains(where: \.properties.isEmojiPresentation)
let hasEmojiVariationSelector = unicodeScalars.contains { $0.value == 0xFE0F }

if hasEmojiPresentation || hasEmojiVariationSelector {
return 2
} else if unicodeScalars.contains(where: \.properties.isIdeographic) {
return 2
Expand Down
1 change: 1 addition & 0 deletions cli/Tests/NooraTests/Utilities/DisplayWidthTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ struct DisplayWidthTests {
@Test func measures_common_widths() {
#expect("abc".displayWidth == 3)
#expect("✓".displayWidth == 1)
#expect("✓️".displayWidth == 2)
#expect("😀".displayWidth == 2)
#expect("🇺🇸".displayWidth == 2)
#expect("界".displayWidth == 2)
Expand Down
Loading