Module: RubyTUI::Unicode
- Defined in:
- lib/rubytui/unicode.rb
Overview
Unicode text width utilities for terminal rendering.
Handles display width calculation for wide characters (CJK, emoji), combining marks (Devanagari, diacritics), variation selectors (FE0F), and grapheme cluster segmentation.
Constant Summary collapse
- EMOJI_PRESENTATION =
Unicode Emoji_Presentation codepoints below U+3000. These render as 2-wide by default (without FE0F) in modern terminals. Source: Unicode 16.0 emoji-data.txt
Set[ 0x231A, 0x231B, # watch, hourglass 0x23E9, 0x23EA, 0x23EB, 0x23EC, 0x23F0, 0x23F3, # fast-forward etc. 0x25FD, 0x25FE, # medium small squares 0x2614, 0x2615, # umbrella, hot beverage 0x2648, 0x2649, 0x264A, 0x264B, 0x264C, 0x264D, # zodiac signs 0x264E, 0x264F, 0x2650, 0x2651, 0x2652, 0x2653, 0x267F, # wheelchair 0x2693, # anchor 0x26A1, # high voltage 0x26AA, 0x26AB, # circles 0x26BD, 0x26BE, # soccer, baseball 0x26C4, 0x26C5, # snowman, sun behind cloud 0x26CE, # ophiuchus 0x26D4, # no entry 0x26EA, # church 0x26F2, 0x26F3, # fountain, golf 0x26F5, # sailboat 0x26FA, # tent 0x26FD, # fuel pump 0x2705, # check mark 0x270A, 0x270B, # fists 0x2728, # sparkles 0x274C, 0x274E, # cross marks 0x2753, 0x2754, 0x2755, # question/exclamation 0x2757, # exclamation 0x2795, 0x2796, 0x2797, # plus/minus/division 0x27B0, 0x27BF, # curly loops 0x2B1B, 0x2B1C, # large squares 0x2B50, # star 0x2B55, # circle ].freeze
Class Method Summary collapse
-
.char_width(ch) ⇒ Integer
Estimate the display width of a single character.
-
.cluster_width(cluster) ⇒ Integer
Calculate display width of a single grapheme cluster.
-
.combining_mark?(cp) ⇒ Boolean
True if codepoint is a combining mark (zero-width).
-
.string_width(string) ⇒ Integer
Calculate display width of a string in terminal columns.
-
.truncate_to_width(string, max_width) ⇒ String
Truncate a string to fit within a maximum display width.
-
.wide_char?(cp) ⇒ Boolean
True if codepoint is a wide character (2-column).
Class Method Details
.char_width(ch) ⇒ Integer
Estimate the display width of a single character.
118 119 120 121 122 123 124 125 |
# File 'lib/rubytui/unicode.rb', line 118 def char_width(ch) cp = ch.ord return 1 if cp >= 0x20 && cp < 0x7F return 0 if cp < 0x20 || cp == 0x7F return 0 if combining_mark?(cp) return 2 if wide_char?(cp) 1 end |
.cluster_width(cluster) ⇒ Integer
Calculate display width of a single grapheme cluster.
73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
# File 'lib/rubytui/unicode.rb', line 73 def cluster_width(cluster) return char_width(cluster) if cluster.bytesize == 1 codepoints = cluster.codepoints has_fe0f = codepoints.include?(0xFE0F) codepoints.each do |cp| next if combining_mark?(cp) return 2 if wide_char?(cp) return 2 if has_fe0f end 1 end |
.combining_mark?(cp) ⇒ Boolean
Returns true if codepoint is a combining mark (zero-width).
129 130 131 132 133 134 135 136 137 138 139 140 |
# File 'lib/rubytui/unicode.rb', line 129 def combining_mark?(cp) (cp >= 0x0300 && cp <= 0x036F) || # Combining Diacritical Marks (cp >= 0x0900 && cp <= 0x097F && # Devanagari dependent vowels, virama, anusvara ((cp >= 0x0901 && cp <= 0x0903) || (cp >= 0x093A && cp <= 0x094F) || (cp >= 0x0951 && cp <= 0x0957) || cp == 0x0962 || cp == 0x0963)) || (cp >= 0x1AB0 && cp <= 0x1AFF) || # Combining Diacritical Marks Extended (cp >= 0x1DC0 && cp <= 0x1DFF) || # Combining Diacritical Marks Supplement (cp >= 0x20D0 && cp <= 0x20FF) || # Combining Marks for Symbols (cp >= 0xFE00 && cp <= 0xFE0F) || # Variation Selectors (cp >= 0xFE20 && cp <= 0xFE2F) || # Combining Half Marks (cp >= 0xE0100 && cp <= 0xE01EF) # Variation Selectors Supplement end |
.string_width(string) ⇒ Integer
Calculate display width of a string in terminal columns.
Operates on grapheme clusters so that combining sequences (Devanagari matras/virama, Latin diacritics, emoji ZWJ sequences) are measured as a single unit. Wide characters (CJK, emoji) count as 2 columns.
65 66 67 |
# File 'lib/rubytui/unicode.rb', line 65 def string_width(string) string.grapheme_clusters.sum { |gc| cluster_width(gc) } end |
.truncate_to_width(string, max_width) ⇒ String
Truncate a string to fit within a maximum display width.
Unlike simple character slicing, this respects grapheme clusters and wide characters that take 2 columns.
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
# File 'lib/rubytui/unicode.rb', line 96 def truncate_to_width(string, max_width) return "" if max_width <= 0 result = String.new width = 0 string.grapheme_clusters.each do |gc| w = cluster_width(gc) break if width + w > max_width result << gc width += w end result end |
.wide_char?(cp) ⇒ Boolean
Returns true if codepoint is a wide character (2-column).
144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 |
# File 'lib/rubytui/unicode.rb', line 144 def wide_char?(cp) (cp >= 0x1100 && cp <= 0x115F) || # Hangul Jamo EMOJI_PRESENTATION.include?(cp) || # Emoji with default 2-wide presentation (cp >= 0x2E80 && cp <= 0x303E) || # CJK Radicals, Kangxi, CJK Symbols (cp >= 0x3040 && cp <= 0x33BF) || # Hiragana, Katakana, CJK Compat (cp >= 0x3400 && cp <= 0x4DBF) || # CJK Unified Extension A (cp >= 0x4E00 && cp <= 0x9FFF) || # CJK Unified Ideographs (cp >= 0xA960 && cp <= 0xA97F) || # Hangul Jamo Extended-A (cp >= 0xAC00 && cp <= 0xD7AF) || # Hangul Syllables (cp >= 0xF900 && cp <= 0xFAFF) || # CJK Compatibility Ideographs (cp >= 0xFE10 && cp <= 0xFE19) || # Vertical Forms (cp >= 0xFE30 && cp <= 0xFE6F) || # CJK Compatibility Forms (cp >= 0xFF01 && cp <= 0xFF60) || # Fullwidth Forms (cp >= 0xFFE0 && cp <= 0xFFE6) || # Fullwidth Sign (cp >= 0x1F000 && cp <= 0x1FAFF) || # Emoji & Symbols (cp >= 0x20000 && cp <= 0x2FA1F) # CJK Unified Extensions B-F end |