asciinema_win 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/rich/cells.rb ADDED
@@ -0,0 +1,512 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rich
4
+ # Cell width calculation for Unicode characters.
5
+ # Handles East Asian Width and emoji character widths for proper terminal alignment.
6
+ module Cells
7
+ # Zero-width character categories (based on Unicode East Asian Width)
8
+ # These characters do not take up any visual space
9
+ ZERO_WIDTH_RANGES = [
10
+ 0x0000..0x001F, # C0 control codes
11
+ 0x007F..0x009F, # C1 control codes
12
+ 0x00AD..0x00AD, # Soft hyphen
13
+ 0x0300..0x036F, # Combining diacritical marks
14
+ 0x0483..0x0489, # Combining Cyrillic marks
15
+ 0x0591..0x05BD, # Hebrew combining marks
16
+ 0x05BF..0x05BF,
17
+ 0x05C1..0x05C2,
18
+ 0x05C4..0x05C5,
19
+ 0x05C7..0x05C7,
20
+ 0x0600..0x0605, # Arabic marks
21
+ 0x0610..0x061A,
22
+ 0x061C..0x061C,
23
+ 0x064B..0x065F,
24
+ 0x0670..0x0670,
25
+ 0x06D6..0x06DC,
26
+ 0x06DF..0x06E4,
27
+ 0x06E7..0x06E8,
28
+ 0x06EA..0x06ED,
29
+ 0x070F..0x070F,
30
+ 0x0711..0x0711,
31
+ 0x0730..0x074A,
32
+ 0x07A6..0x07B0,
33
+ 0x07EB..0x07F3,
34
+ 0x0816..0x0819,
35
+ 0x081B..0x0823,
36
+ 0x0825..0x0827,
37
+ 0x0829..0x082D,
38
+ 0x0859..0x085B,
39
+ 0x08D4..0x08E1,
40
+ 0x08E3..0x0902,
41
+ 0x093A..0x093A,
42
+ 0x093C..0x093C,
43
+ 0x0941..0x0948,
44
+ 0x094D..0x094D,
45
+ 0x0951..0x0957,
46
+ 0x0962..0x0963,
47
+ 0x0981..0x0981,
48
+ 0x09BC..0x09BC,
49
+ 0x09C1..0x09C4,
50
+ 0x09CD..0x09CD,
51
+ 0x09E2..0x09E3,
52
+ 0x0A01..0x0A02,
53
+ 0x0A3C..0x0A3C,
54
+ 0x0A41..0x0A42,
55
+ 0x0A47..0x0A48,
56
+ 0x0A4B..0x0A4D,
57
+ 0x0A51..0x0A51,
58
+ 0x0A70..0x0A71,
59
+ 0x0A75..0x0A75,
60
+ 0x0A81..0x0A82,
61
+ 0x0ABC..0x0ABC,
62
+ 0x0AC1..0x0AC5,
63
+ 0x0AC7..0x0AC8,
64
+ 0x0ACD..0x0ACD,
65
+ 0x0AE2..0x0AE3,
66
+ 0x0B01..0x0B01,
67
+ 0x0B3C..0x0B3C,
68
+ 0x0B3F..0x0B3F,
69
+ 0x0B41..0x0B44,
70
+ 0x0B4D..0x0B4D,
71
+ 0x0B56..0x0B56,
72
+ 0x0B62..0x0B63,
73
+ 0x0B82..0x0B82,
74
+ 0x0BC0..0x0BC0,
75
+ 0x0BCD..0x0BCD,
76
+ 0x0C00..0x0C00,
77
+ 0x0C3E..0x0C40,
78
+ 0x0C46..0x0C48,
79
+ 0x0C4A..0x0C4D,
80
+ 0x0C55..0x0C56,
81
+ 0x0C62..0x0C63,
82
+ 0x0C81..0x0C81,
83
+ 0x0CBC..0x0CBC,
84
+ 0x0CBF..0x0CBF,
85
+ 0x0CC6..0x0CC6,
86
+ 0x0CCC..0x0CCD,
87
+ 0x0CE2..0x0CE3,
88
+ 0x0D01..0x0D01,
89
+ 0x0D41..0x0D44,
90
+ 0x0D4D..0x0D4D,
91
+ 0x0D62..0x0D63,
92
+ 0x0DCA..0x0DCA,
93
+ 0x0DD2..0x0DD4,
94
+ 0x0DD6..0x0DD6,
95
+ 0x0E31..0x0E31,
96
+ 0x0E34..0x0E3A,
97
+ 0x0E47..0x0E4E,
98
+ 0x0EB1..0x0EB1,
99
+ 0x0EB4..0x0EB9,
100
+ 0x0EBB..0x0EBC,
101
+ 0x0EC8..0x0ECD,
102
+ 0x0F18..0x0F19,
103
+ 0x0F35..0x0F35,
104
+ 0x0F37..0x0F37,
105
+ 0x0F39..0x0F39,
106
+ 0x0F71..0x0F7E,
107
+ 0x0F80..0x0F84,
108
+ 0x0F86..0x0F87,
109
+ 0x0F8D..0x0F97,
110
+ 0x0F99..0x0FBC,
111
+ 0x0FC6..0x0FC6,
112
+ 0x102D..0x1030,
113
+ 0x1032..0x1037,
114
+ 0x1039..0x103A,
115
+ 0x103D..0x103E,
116
+ 0x1058..0x1059,
117
+ 0x105E..0x1060,
118
+ 0x1071..0x1074,
119
+ 0x1082..0x1082,
120
+ 0x1085..0x1086,
121
+ 0x108D..0x108D,
122
+ 0x109D..0x109D,
123
+ 0x1160..0x11FF, # Hangul Jungseong/Jongseong
124
+ 0x135D..0x135F,
125
+ 0x1712..0x1714,
126
+ 0x1732..0x1734,
127
+ 0x1752..0x1753,
128
+ 0x1772..0x1773,
129
+ 0x17B4..0x17B5,
130
+ 0x17B7..0x17BD,
131
+ 0x17C6..0x17C6,
132
+ 0x17C9..0x17D3,
133
+ 0x17DD..0x17DD,
134
+ 0x180B..0x180D,
135
+ 0x1885..0x1886,
136
+ 0x18A9..0x18A9,
137
+ 0x1920..0x1922,
138
+ 0x1927..0x1928,
139
+ 0x1932..0x1932,
140
+ 0x1939..0x193B,
141
+ 0x1A17..0x1A18,
142
+ 0x1A1B..0x1A1B,
143
+ 0x1A56..0x1A56,
144
+ 0x1A58..0x1A5E,
145
+ 0x1A60..0x1A60,
146
+ 0x1A62..0x1A62,
147
+ 0x1A65..0x1A6C,
148
+ 0x1A73..0x1A7C,
149
+ 0x1A7F..0x1A7F,
150
+ 0x1AB0..0x1ABE,
151
+ 0x1B00..0x1B03,
152
+ 0x1B34..0x1B34,
153
+ 0x1B36..0x1B3A,
154
+ 0x1B3C..0x1B3C,
155
+ 0x1B42..0x1B42,
156
+ 0x1B6B..0x1B73,
157
+ 0x1B80..0x1B81,
158
+ 0x1BA2..0x1BA5,
159
+ 0x1BA8..0x1BA9,
160
+ 0x1BAB..0x1BAD,
161
+ 0x1BE6..0x1BE6,
162
+ 0x1BE8..0x1BE9,
163
+ 0x1BED..0x1BED,
164
+ 0x1BEF..0x1BF1,
165
+ 0x1C2C..0x1C33,
166
+ 0x1C36..0x1C37,
167
+ 0x1CD0..0x1CD2,
168
+ 0x1CD4..0x1CE0,
169
+ 0x1CE2..0x1CE8,
170
+ 0x1CED..0x1CED,
171
+ 0x1CF4..0x1CF4,
172
+ 0x1CF8..0x1CF9,
173
+ 0x1DC0..0x1DF5,
174
+ 0x1DFC..0x1DFF,
175
+ 0x200B..0x200F, # Zero-width spaces and direction marks
176
+ 0x202A..0x202E,
177
+ 0x2060..0x2064,
178
+ 0x2066..0x206F,
179
+ 0x20D0..0x20F0, # Combining marks for symbols
180
+ 0x2CEF..0x2CF1,
181
+ 0x2D7F..0x2D7F,
182
+ 0x2DE0..0x2DFF,
183
+ 0x302A..0x302D,
184
+ 0x3099..0x309A,
185
+ 0xA66F..0xA672,
186
+ 0xA674..0xA67D,
187
+ 0xA69E..0xA69F,
188
+ 0xA6F0..0xA6F1,
189
+ 0xA802..0xA802,
190
+ 0xA806..0xA806,
191
+ 0xA80B..0xA80B,
192
+ 0xA825..0xA826,
193
+ 0xA8C4..0xA8C4,
194
+ 0xA8E0..0xA8F1,
195
+ 0xA926..0xA92D,
196
+ 0xA947..0xA951,
197
+ 0xA980..0xA982,
198
+ 0xA9B3..0xA9B3,
199
+ 0xA9B6..0xA9B9,
200
+ 0xA9BC..0xA9BC,
201
+ 0xA9E5..0xA9E5,
202
+ 0xAA29..0xAA2E,
203
+ 0xAA31..0xAA32,
204
+ 0xAA35..0xAA36,
205
+ 0xAA43..0xAA43,
206
+ 0xAA4C..0xAA4C,
207
+ 0xAA7C..0xAA7C,
208
+ 0xAAB0..0xAAB0,
209
+ 0xAAB2..0xAAB4,
210
+ 0xAAB7..0xAAB8,
211
+ 0xAABE..0xAABF,
212
+ 0xAAC1..0xAAC1,
213
+ 0xAAEC..0xAAED,
214
+ 0xAAF6..0xAAF6,
215
+ 0xABE5..0xABE5,
216
+ 0xABE8..0xABE8,
217
+ 0xABED..0xABED,
218
+ 0xFB1E..0xFB1E,
219
+ 0xFE00..0xFE0F, # Variation selectors
220
+ 0xFE20..0xFE2F,
221
+ 0xFEFF..0xFEFF, # BOM/ZWNBSP
222
+ 0xFFF9..0xFFFB,
223
+ 0x101FD..0x101FD,
224
+ 0x102E0..0x102E0,
225
+ 0x10376..0x1037A,
226
+ 0x10A01..0x10A03,
227
+ 0x10A05..0x10A06,
228
+ 0x10A0C..0x10A0F,
229
+ 0x10A38..0x10A3A,
230
+ 0x10A3F..0x10A3F,
231
+ 0x10AE5..0x10AE6,
232
+ 0x11001..0x11001,
233
+ 0x11038..0x11046,
234
+ 0x1107F..0x11081,
235
+ 0x110B3..0x110B6,
236
+ 0x110B9..0x110BA,
237
+ 0x11100..0x11102,
238
+ 0x11127..0x1112B,
239
+ 0x1112D..0x11134,
240
+ 0x11173..0x11173,
241
+ 0x11180..0x11181,
242
+ 0x111B6..0x111BE,
243
+ 0x111CA..0x111CC,
244
+ 0x1122F..0x11231,
245
+ 0x11234..0x11234,
246
+ 0x11236..0x11237,
247
+ 0x1123E..0x1123E,
248
+ 0x112DF..0x112DF,
249
+ 0x112E3..0x112EA,
250
+ 0x11300..0x11301,
251
+ 0x1133C..0x1133C,
252
+ 0x11340..0x11340,
253
+ 0x11366..0x1136C,
254
+ 0x11370..0x11374,
255
+ 0x11438..0x1143F,
256
+ 0x11442..0x11444,
257
+ 0x11446..0x11446,
258
+ 0x114B3..0x114B8,
259
+ 0x114BA..0x114BA,
260
+ 0x114BF..0x114C0,
261
+ 0x114C2..0x114C3,
262
+ 0x115B2..0x115B5,
263
+ 0x115BC..0x115BD,
264
+ 0x115BF..0x115C0,
265
+ 0x115DC..0x115DD,
266
+ 0x11633..0x1163A,
267
+ 0x1163D..0x1163D,
268
+ 0x1163F..0x11640,
269
+ 0x116AB..0x116AB,
270
+ 0x116AD..0x116AD,
271
+ 0x116B0..0x116B5,
272
+ 0x116B7..0x116B7,
273
+ 0x1171D..0x1171F,
274
+ 0x11722..0x11725,
275
+ 0x11727..0x1172B,
276
+ 0x11C30..0x11C36,
277
+ 0x11C38..0x11C3D,
278
+ 0x11C3F..0x11C3F,
279
+ 0x11C92..0x11CA7,
280
+ 0x11CAA..0x11CB0,
281
+ 0x11CB2..0x11CB3,
282
+ 0x11CB5..0x11CB6,
283
+ 0x16AF0..0x16AF4,
284
+ 0x16B30..0x16B36,
285
+ 0x16F8F..0x16F92,
286
+ 0x1BC9D..0x1BC9E,
287
+ 0x1D167..0x1D169,
288
+ 0x1D173..0x1D182,
289
+ 0x1D185..0x1D18B,
290
+ 0x1D1AA..0x1D1AD,
291
+ 0x1D242..0x1D244,
292
+ 0x1DA00..0x1DA36,
293
+ 0x1DA3B..0x1DA6C,
294
+ 0x1DA75..0x1DA75,
295
+ 0x1DA84..0x1DA84,
296
+ 0x1DA9B..0x1DA9F,
297
+ 0x1DAA1..0x1DAAF,
298
+ 0x1E000..0x1E006,
299
+ 0x1E008..0x1E018,
300
+ 0x1E01B..0x1E021,
301
+ 0x1E023..0x1E024,
302
+ 0x1E026..0x1E02A,
303
+ 0x1E8D0..0x1E8D6,
304
+ 0x1E944..0x1E94A,
305
+ 0xE0001..0xE0001,
306
+ 0xE0020..0xE007F,
307
+ 0xE0100..0xE01EF
308
+ ].freeze
309
+
310
+ # Wide (double-width) character ranges (CJK, etc.)
311
+ WIDE_RANGES = [
312
+ 0x1100..0x115F, # Hangul Jamo
313
+ 0x231A..0x231B, # Watch, Hourglass
314
+ 0x2329..0x232A, # Angle brackets
315
+ 0x23E9..0x23F3, # Media controls
316
+ 0x23F8..0x23FA,
317
+ 0x25FD..0x25FE, # Squares
318
+ 0x2614..0x2615, # Umbrella, Hot beverage
319
+ 0x2648..0x2653, # Zodiac
320
+ 0x267F..0x267F, # Wheelchair
321
+ 0x2693..0x2693, # Anchor
322
+ 0x26A1..0x26A1, # Lightning
323
+ 0x26AA..0x26AB, # Circles
324
+ 0x26BD..0x26BE, # Sports
325
+ 0x26C4..0x26C5, # Weather
326
+ 0x26CE..0x26CE, # Ophiuchus
327
+ 0x26D4..0x26D4, # No entry
328
+ 0x26EA..0x26EA, # Church
329
+ 0x26F2..0x26F3, # Fountain, Golf
330
+ 0x26F5..0x26F5, # Sailboat
331
+ 0x26FA..0x26FA, # Tent
332
+ 0x26FD..0x26FD, # Fuel pump
333
+ 0x2702..0x2702, # Scissors
334
+ 0x2705..0x2705, # Check mark
335
+ 0x2708..0x270D, # Various
336
+ 0x270F..0x270F, # Pencil
337
+ 0x2712..0x2712, # Black nib
338
+ 0x2714..0x2714, # Check mark
339
+ 0x2716..0x2716, # X mark
340
+ 0x271D..0x271D, # Cross
341
+ 0x2721..0x2721, # Star of David
342
+ 0x2728..0x2728, # Sparkles
343
+ 0x2733..0x2734, # Asterisks
344
+ 0x2744..0x2744, # Snowflake
345
+ 0x2747..0x2747, # Sparkle
346
+ 0x274C..0x274C, # Cross mark
347
+ 0x274E..0x274E,
348
+ 0x2753..0x2755, # Question marks
349
+ 0x2757..0x2757, # Exclamation
350
+ 0x2763..0x2764, # Heart
351
+ 0x2795..0x2797, # Math symbols
352
+ 0x27A1..0x27A1, # Arrow
353
+ 0x27B0..0x27B0, # Loop
354
+ 0x27BF..0x27BF, # Loop
355
+ 0x2934..0x2935, # Arrows
356
+ 0x2B05..0x2B07, # Arrows
357
+ 0x2B1B..0x2B1C, # Squares
358
+ 0x2B50..0x2B50, # Star
359
+ 0x2B55..0x2B55, # Circle
360
+ 0x2E80..0x2E99, # CJK radicals
361
+ 0x2E9B..0x2EF3,
362
+ 0x2F00..0x2FD5, # Kangxi radicals
363
+ 0x2FF0..0x2FFB, # Ideographic description
364
+ 0x3000..0x303E, # CJK punctuation
365
+ 0x3041..0x3096, # Hiragana
366
+ 0x3099..0x30FF, # Katakana
367
+ 0x3105..0x312D, # Bopomofo
368
+ 0x3131..0x318E, # Hangul compatibility
369
+ 0x3190..0x31BA, # Kanbun
370
+ 0x31C0..0x31E3, # CJK strokes
371
+ 0x31F0..0x321E, # Katakana extensions
372
+ 0x3220..0x3247, # Enclosed CJK
373
+ 0x3250..0x32FE,
374
+ 0x3300..0x4DBF, # CJK unified
375
+ 0x4E00..0x9FFF, # CJK unified ideographs
376
+ 0xA000..0xA48C, # Yi syllables
377
+ 0xA490..0xA4C6, # Yi radicals
378
+ 0xA960..0xA97C, # Hangul Jamo extended
379
+ 0xAC00..0xD7A3, # Hangul syllables
380
+ 0xF900..0xFAFF, # CJK compatibility ideographs
381
+ 0xFE10..0xFE19, # Vertical forms
382
+ 0xFE30..0xFE52, # CJK compatibility forms
383
+ 0xFE54..0xFE66,
384
+ 0xFE68..0xFE6B,
385
+ 0xFF01..0xFF60, # Fullwidth forms
386
+ 0xFFE0..0xFFE6,
387
+ 0x16FE0..0x16FE1, # Various
388
+ 0x17000..0x187EC, # Tangut
389
+ 0x18800..0x18AF2,
390
+ 0x1B000..0x1B11E, # Kana supplement
391
+ 0x1B170..0x1B2FB,
392
+ 0x1F004..0x1F004, # Mahjong
393
+ 0x1F0CF..0x1F0CF, # Playing card
394
+ 0x1F18E..0x1F18E, # Squared AB
395
+ 0x1F191..0x1F19A, # Squared
396
+ 0x1F200..0x1F202,
397
+ 0x1F210..0x1F23B,
398
+ 0x1F240..0x1F248,
399
+ 0x1F250..0x1F251,
400
+ 0x1F260..0x1F265,
401
+ 0x1F300..0x1F64F, # Emoji
402
+ 0x1F680..0x1F6C5,
403
+ 0x1F6CC..0x1F6CC,
404
+ 0x1F6D0..0x1F6D2,
405
+ 0x1F6EB..0x1F6EC,
406
+ 0x1F6F4..0x1F6F8,
407
+ 0x1F910..0x1F93E,
408
+ 0x1F940..0x1F94C,
409
+ 0x1F950..0x1F96B,
410
+ 0x1F980..0x1F997,
411
+ 0x1F9C0..0x1F9C0,
412
+ 0x1F9D0..0x1F9E6,
413
+ 0x20000..0x2FFFD, # CJK extension B
414
+ 0x30000..0x3FFFD # CJK extension G
415
+ ].freeze
416
+
417
+ class << self
418
+ # Get the display width of a single character
419
+ # @param char [String] Single character
420
+ # @return [Integer] Width (0, 1, or 2)
421
+ def char_width(char)
422
+ return 0 if char.nil? || char.empty?
423
+
424
+ codepoint = char.ord
425
+
426
+ # Tab, newline, and carriage return should have width 1 for measurement
427
+ # (even though they may have different visual behavior)
428
+ return 1 if codepoint == 0x09 || codepoint == 0x0A || codepoint == 0x0D
429
+
430
+ # Check zero-width first (most common for combining marks)
431
+ ZERO_WIDTH_RANGES.each do |range|
432
+ return 0 if range.cover?(codepoint)
433
+ end
434
+
435
+ # Check wide characters
436
+ WIDE_RANGES.each do |range|
437
+ return 2 if range.cover?(codepoint)
438
+ end
439
+
440
+ # Default to single width
441
+ 1
442
+ end
443
+
444
+ # Get the display width of a string
445
+ # @param text [String] Text to measure
446
+ # @return [Integer] Total display width in cells
447
+ def cell_len(text)
448
+ return 0 if text.nil? || text.empty?
449
+
450
+ # On Windows legacy console without ANSI, use byte-based width
451
+ if Gem.win_platform? && defined?(Rich::Win32Console) && !Rich::Win32Console.supports_ansi?
452
+ return text.bytesize
453
+ end
454
+
455
+ text.each_char.sum { |c| char_width(c) }
456
+ end
457
+
458
+ # Get the display width of a string, with cache
459
+ # @param text [String] Text to measure
460
+ # @return [Integer] Total display width
461
+ def cached_cell_len(text)
462
+ @cache_mutex ||= Mutex.new
463
+ @cache ||= {}
464
+
465
+ return 0 if text.nil? || text.empty?
466
+
467
+ # Use cache for repeated lookups
468
+ @cache_mutex.synchronize do
469
+ return @cache[text] if @cache.key?(text)
470
+
471
+ result = cell_len(text)
472
+
473
+ # Limit cache size
474
+ @cache.shift if @cache.size > 10000
475
+ @cache[text] = result
476
+
477
+ result
478
+ end
479
+ end
480
+
481
+ # Set the cell size for a specific character (override)
482
+ # @param char [String] Character to override
483
+ # @param size [Integer] Width to use
484
+ def set_cell_size(char, size)
485
+ @overrides ||= {}
486
+ @overrides[char] = size
487
+ end
488
+
489
+ # Clear the cell width cache
490
+ def clear_cache
491
+ @cache_mutex ||= Mutex.new
492
+ @cache_mutex.synchronize do
493
+ @cache&.clear
494
+ end
495
+ end
496
+
497
+ # Check if a character is a zero-width character
498
+ # @param char [String] Character to check
499
+ # @return [Boolean]
500
+ def zero_width?(char)
501
+ char_width(char) == 0
502
+ end
503
+
504
+ # Check if a character is a wide (double-width) character
505
+ # @param char [String] Character to check
506
+ # @return [Boolean]
507
+ def wide?(char)
508
+ char_width(char) == 2
509
+ end
510
+ end
511
+ end
512
+ end