Module: Charming::UI::Width

Defined in:
lib/charming/presentation/ui/width.rb

Overview

Width is a namespace for measuring and normalising the visual width of strings that may contain ANSI escape sequences. It delegates to ‘Unicode::DisplayWidth` while automatically stripping formatting codes so layout primitives can calculate exact character positions.

Constant Summary collapse

ANSI_PATTERN =

Matches OSC sequences (e.g. OSC 8 hyperlinks, terminated by BEL or ST), CSI sequences (SGR colors/attributes, cursor movement), and single-character Fe escapes. The OSC branch must come first and the Fe class must exclude “[” and “]”, or “e]” would match as a bare Fe escape and leave the OSC payload counted as visible text.

/\e(?:\][^\a]*?(?:\a|\e\\)|\[[0-9;?]*[@-~]|[@-Z\\^_])/
EMOJI_PRESENTATION =

A grapheme cluster containing either codepoint renders as a single emoji (double-width) cell in a terminal: U+200D ZWJ joins a multi-glyph emoji sequence, and U+FE0F (VS16) requests emoji presentation. The unicode-display_width tables disagree with terminals here — e.g. “⚔️” measures 1 and “🧙‍♂️” measures 3 — so we pin such clusters to 2.

/[\u200D\uFE0F]/
GRAPHEME =

Onig’s X matches one extended grapheme cluster, keeping multi-codepoint emoji together so each is measured (and later sliced) as one unit.

/\X/

Class Method Summary collapse

Class Method Details

.cluster_width(cluster) ⇒ Object



38
39
40
41
42
# File 'lib/charming/presentation/ui/width.rb', line 38

def cluster_width(cluster)
  return 2 if cluster.match?(EMOJI_PRESENTATION)

  Unicode::DisplayWidth.of(cluster)
end

.measure(value) ⇒ Object



31
32
33
34
35
36
# File 'lib/charming/presentation/ui/width.rb', line 31

def measure(value)
  stripped = strip_ansi(value.to_s)
  return Unicode::DisplayWidth.of(stripped) unless stripped.match?(EMOJI_PRESENTATION)

  stripped.scan(GRAPHEME).sum { |cluster| cluster_width(cluster) }
end

.strip_ansi(value) ⇒ Object



44
45
46
# File 'lib/charming/presentation/ui/width.rb', line 44

def strip_ansi(value)
  value.to_s.gsub(ANSI_PATTERN, "")
end