Skip to content

Flag emojis and other multi-codepoint grapheme clusters render as blanks in pane outputΒ #243

Description

@juliogc

Current behavior

Pane output that contains flag emojis (e.g. πŸ‡§πŸ‡·, πŸ‡¦πŸ‡·) or other multi-codepoint grapheme clusters (e.g. ZWJ family sequences like πŸ‘¨β€πŸ‘©β€πŸ‘§) renders as a blank space inside herdr panes, even when the host terminal renders them correctly outside herdr.

In src/pane/terminal.rs::ghostty_buffer_symbol_into, the symbol scratch is filled with the grapheme cluster, then validated against the cell's expected width:

// src/pane/terminal.rs:1035-1046
let expected_width = match wide {
    crate::ghostty::CellWide::Wide => 2,
    crate::ghostty::CellWide::Narrow | crate::ghostty::CellWide::SpacerHead => 1,
    crate::ghostty::CellWide::SpacerTail => 0,
};
let actual_width = symbol_scratch.width();
if actual_width != expected_width
    && !(wide == crate::ghostty::CellWide::Narrow && actual_width == 2)
{
    symbol_scratch.clear();
    symbol_scratch.push_str(ghostty_blank_symbol_for_width(wide));
}

For a Regional Indicator pair like πŸ‡§πŸ‡· (U+1F1E7 U+1F1F7):

  • Ghostty classifies the cell as CellWide::Wide β†’ expected_width = 2.
  • unicode_width::UnicodeWidthStr::width() sums per-codepoint widths β†’ returns 4 (each RI codepoint is width 2 in isolation).
  • The existing exception only covers Narrow + actual_width == 2, so this case falls through and the cluster is wiped to " ".

Same path strips other multi-codepoint Wide clusters (ZWJ family/profession sequences, etc.).

Expected behavior

When Ghostty classifies the cell as Wide and the symbol is a multi-codepoint grapheme cluster whose summed codepoint widths exceed 2, herdr should trust the terminal's cell classification and emit the cluster as-is β€” symmetric with the existing Narrow && actual_width == 2 exception that already trusts the host for clusters like πŸ’³.

Concretely, an additional escape hatch in the same conditional:

if actual_width != expected_width
    && !(wide == crate::ghostty::CellWide::Narrow && actual_width == 2)
    && !(wide == crate::ghostty::CellWide::Wide
         && actual_width > 2
         && symbol_scratch.chars().count() > 1)
{
    symbol_scratch.clear();
    symbol_scratch.push_str(ghostty_blank_symbol_for_width(wide));
}

The chars().count() > 1 guard restricts the new exception to genuine multi-codepoint clusters and avoids accepting an accidentally-wide single char. No new dependency required.

Test additions in the existing ghostty_normalize_buffer_symbol_prefers_grapheme_width_when_metadata_disagrees test (src/pane/terminal.rs:1612):

const FLAG_GRAPHEME: &str = "πŸ‡§πŸ‡·";
const FAMILY_GRAPHEME: &str = "πŸ‘¨β€πŸ‘©β€πŸ‘§";

assert_eq!(
    ghostty_normalize_buffer_symbol(FLAG_GRAPHEME, crate::ghostty::CellWide::Wide),
    FLAG_GRAPHEME
);
assert_eq!(
    ghostty_normalize_buffer_symbol(FAMILY_GRAPHEME, crate::ghostty::CellWide::Wide),
    FAMILY_GRAPHEME
);

Reproduction

  1. Open herdr.
  2. In any pane, run:
    printf 'πŸ‡§πŸ‡· πŸ‡¦πŸ‡· πŸ‘¨β€πŸ‘©β€πŸ‘§\n'
  3. Each cluster appears as a blank gap; the same command in the host terminal renders the flags/family glyphs correctly.

Impact

Any agent output, statusline, or shell prompt that uses flag or ZWJ emojis is silently stripped inside herdr panes. I hit this with a Claude Code statusline that uses country flags for a World Cup 2026 match section β€” the flags vanish when running under herdr.

Environment

  • herdr 0.6.0
  • macOS (darwin 25.4.0)
  • Ghostty terminal
  • zsh

Contribution intent

  • I intend to open a PR for this issue.
  • I understand this does not mean the issue is approved.

Metadata

Metadata

Assignees

No one assigned

    Labels

    intends-to-prOpened with intent to implement and submit a PR after approval

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions