terminal_rb 1.0.6 → 1.0.9
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.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/examples/bbcode.rb +13 -13
- data/examples/key-codes.rb +1 -1
- data/lib/terminal/ansi.rb +195 -127
- data/lib/terminal/input/dumb.rb +2 -2
- data/lib/terminal/input/key_event.rb +4 -4
- data/lib/terminal/input.rb +2 -3
- data/lib/terminal/output/ansi.rb +11 -14
- data/lib/terminal/output/dumb.rb +3 -9
- data/lib/terminal/output.rb +2 -3
- data/lib/terminal/rspec/helper.rb +5 -4
- data/lib/terminal/text/char_width.rb +2778 -2744
- data/lib/terminal/text/formatter.rb +233 -69
- data/lib/terminal/text.rb +78 -55
- data/lib/terminal/version.rb +1 -1
- data/lib/terminal.rb +38 -13
- metadata +3 -3
|
@@ -9,15 +9,15 @@ module Terminal
|
|
|
9
9
|
# @see Terminal::Text
|
|
10
10
|
#
|
|
11
11
|
# @example Basic word-wrapping
|
|
12
|
-
#
|
|
13
|
-
#
|
|
12
|
+
# formatter = Terminal::Text::Formatter.new('Hello World, this is a test')
|
|
13
|
+
# formatter.lines(width: 12)
|
|
14
14
|
# # => ["Hello World,", "this is a", "test"]
|
|
15
15
|
#
|
|
16
16
|
# @example Formatted output with alignment
|
|
17
17
|
# Terminal::Text::Formatter.format(
|
|
18
18
|
# 'Hello', align: :center, width: 20
|
|
19
19
|
# )
|
|
20
|
-
# # => ["
|
|
20
|
+
# # => [" Hello "]
|
|
21
21
|
class Formatter
|
|
22
22
|
# Parse text into lines, optionally with display widths.
|
|
23
23
|
#
|
|
@@ -55,7 +55,7 @@ module Terminal
|
|
|
55
55
|
#
|
|
56
56
|
# @param (see #initialize)
|
|
57
57
|
# @param (see #format)
|
|
58
|
-
# @
|
|
58
|
+
# @return (see #format)
|
|
59
59
|
# @raise (see #format)
|
|
60
60
|
def self.format(
|
|
61
61
|
*str,
|
|
@@ -80,6 +80,31 @@ module Terminal
|
|
|
80
80
|
)
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
+
# Format text with alignment.
|
|
84
|
+
#
|
|
85
|
+
# @see #fmt
|
|
86
|
+
#
|
|
87
|
+
# @example
|
|
88
|
+
# Terminal::Text::Formatter.fmt('Hi', align: :center, width: 10)
|
|
89
|
+
# # => [" Hi "]
|
|
90
|
+
#
|
|
91
|
+
# @param (see #initialize)
|
|
92
|
+
# @param (see #fmt)
|
|
93
|
+
# @return (see #fmt)
|
|
94
|
+
# @raise (see #fmt)
|
|
95
|
+
def self.fmt(
|
|
96
|
+
*str,
|
|
97
|
+
ansi: true,
|
|
98
|
+
bbcode: true,
|
|
99
|
+
spaces: true,
|
|
100
|
+
eol: true,
|
|
101
|
+
align: nil,
|
|
102
|
+
width: nil,
|
|
103
|
+
height: nil
|
|
104
|
+
)
|
|
105
|
+
new(*str, ansi:, bbcode:, spaces:, eol:).fmt(align:, width:, height:)
|
|
106
|
+
end
|
|
107
|
+
|
|
83
108
|
# Create a new formatter with given text and options.
|
|
84
109
|
#
|
|
85
110
|
# @param str [Array<#to_s>] text to process
|
|
@@ -108,8 +133,8 @@ module Terminal
|
|
|
108
133
|
# Word-wrap and return lines with their display widths.
|
|
109
134
|
#
|
|
110
135
|
# @example
|
|
111
|
-
#
|
|
112
|
-
#
|
|
136
|
+
# formatter = Terminal::Text::Formatter.new('Hello World')
|
|
137
|
+
# formatter.lines_with_size(width: 5)
|
|
113
138
|
# # => [["Hello", 5], ["World", 5]]
|
|
114
139
|
#
|
|
115
140
|
# @param width [#to_i, nil] maximum line width in columns;
|
|
@@ -127,8 +152,8 @@ module Terminal
|
|
|
127
152
|
# Word-wrap and return lines as strings.
|
|
128
153
|
#
|
|
129
154
|
# @example
|
|
130
|
-
#
|
|
131
|
-
#
|
|
155
|
+
# formatter = Terminal::Text::Formatter.new('Hello World')
|
|
156
|
+
# formatter.lines(width: 5)
|
|
132
157
|
# # => ["Hello", "World"]
|
|
133
158
|
#
|
|
134
159
|
# @param width [#to_i, nil] maximum line width in columns
|
|
@@ -138,16 +163,22 @@ module Terminal
|
|
|
138
163
|
|
|
139
164
|
# Format lines with alignment, padding, and decorations.
|
|
140
165
|
#
|
|
141
|
-
# @
|
|
142
|
-
#
|
|
143
|
-
#
|
|
144
|
-
#
|
|
166
|
+
# @see #fmt
|
|
167
|
+
#
|
|
168
|
+
# @example Prefix each line
|
|
169
|
+
# formatter = Terminal::Text::Formatter.new("Hello\nWorld")
|
|
170
|
+
# formatter.format(prefix: '> ')
|
|
171
|
+
# # => ["> Hello", "> World"]
|
|
172
|
+
# @example Framed box with padding
|
|
173
|
+
# formatter = Terminal::Text::Formatter.new('Hi')
|
|
174
|
+
# formatter.format(width: 6, padding: [1, 2], prefix: '|', suffix: '|')
|
|
175
|
+
# # => ["| |", "| Hi |", "| |"]
|
|
145
176
|
#
|
|
146
177
|
# @param align [Symbol, nil] text alignment:
|
|
147
178
|
# `:left`, `:right`, `:center`, or `nil` (no fill)
|
|
148
179
|
# @param width [#to_i, nil] output line width in columns
|
|
149
180
|
# @param height [#to_i, nil] number of output lines;
|
|
150
|
-
# negative values take lines from the end
|
|
181
|
+
# negative values take lines from the end; `0` returns `[""]`
|
|
151
182
|
# @param padding [#to_i, Array, nil] CSS-style padding
|
|
152
183
|
# - 1 value: all sides;
|
|
153
184
|
# - 2 values: [vertical, horizontal];
|
|
@@ -165,6 +196,10 @@ module Terminal
|
|
|
165
196
|
prefix: nil,
|
|
166
197
|
suffix: nil
|
|
167
198
|
)
|
|
199
|
+
if padding.nil? && prefix.nil? && suffix.nil?
|
|
200
|
+
return fmt(align:, width:, height:)
|
|
201
|
+
end
|
|
202
|
+
|
|
168
203
|
top, right, bottom, left = _padding(padding)
|
|
169
204
|
|
|
170
205
|
if height
|
|
@@ -200,11 +235,9 @@ module Terminal
|
|
|
200
235
|
left = left < 1 ? prefix.to_s : "#{prefix}#{space[left]}"
|
|
201
236
|
right = right < 1 ? suffix.to_s : "#{space[right]}#{suffix}"
|
|
202
237
|
|
|
203
|
-
# TODO: styling
|
|
204
|
-
|
|
205
238
|
case align
|
|
206
|
-
when
|
|
207
|
-
lws.each { |l,
|
|
239
|
+
when nil, false
|
|
240
|
+
lws.each { |l, _| ret << "#{left}#{l}#{right}" }
|
|
208
241
|
when :right
|
|
209
242
|
lws.each { |l, s| ret << "#{left}#{space[w - s]}#{l}#{right}" }
|
|
210
243
|
when :center
|
|
@@ -214,13 +247,58 @@ module Terminal
|
|
|
214
247
|
ret << "#{left}#{space[s - rs]}#{l}#{space[rs]}#{right}"
|
|
215
248
|
end
|
|
216
249
|
else
|
|
217
|
-
lws.each { |l,
|
|
250
|
+
lws.each { |l, s| ret << "#{left}#{l}#{space[w - s]}#{right}" }
|
|
218
251
|
end
|
|
219
252
|
|
|
220
253
|
return ret.fill(empty, ret.size, bottom) if bottom > 0
|
|
221
254
|
ret.empty? ? ret << '' : ret
|
|
222
255
|
end
|
|
223
256
|
|
|
257
|
+
# Format lines with alignment.
|
|
258
|
+
#
|
|
259
|
+
# Like {#format} without padding, prefix, and suffix.
|
|
260
|
+
#
|
|
261
|
+
# @see #format
|
|
262
|
+
#
|
|
263
|
+
# @example
|
|
264
|
+
# Terminal::Text::Formatter.new('Hi').fmt(align: :right, width: 6)
|
|
265
|
+
# # => [" Hi"]
|
|
266
|
+
#
|
|
267
|
+
# @param align (see #format)
|
|
268
|
+
# @param width (see #format)
|
|
269
|
+
# @param height (see #format)
|
|
270
|
+
# @return (see #format)
|
|
271
|
+
# @raise (see #format)
|
|
272
|
+
def fmt(width: nil, height: nil, align: nil)
|
|
273
|
+
if height
|
|
274
|
+
return [] if (height = height.to_i) == 0
|
|
275
|
+
height = -height if (tail = (height < 0))
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
lines =
|
|
279
|
+
if width
|
|
280
|
+
w = (width = width.to_i)
|
|
281
|
+
raise(ArgumentError, "invalid width - #{width.inspect}") if w < 1
|
|
282
|
+
_limited(w)
|
|
283
|
+
else
|
|
284
|
+
@unlimited || _unlimited
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
return [+''] if lines.empty?
|
|
288
|
+
|
|
289
|
+
lines = tail ? lines.last(height) : lines.take(height) if height
|
|
290
|
+
w ||= lines.max_by(&:last)[-1] # unless width
|
|
291
|
+
|
|
292
|
+
return lines.map(&:first) unless align
|
|
293
|
+
space = SPACE_CACHE.dup
|
|
294
|
+
return lines.map { |l, s| "#{space[w - s]}#{l}" } if align == :right
|
|
295
|
+
return lines.map { |l, s| "#{l}#{space[w - s]}" } if align != :center
|
|
296
|
+
lines.map do |l, s|
|
|
297
|
+
rs = ((s = w - s) / 2.0).round
|
|
298
|
+
rs < 1 ? l : "#{space[s - rs]}#{l}#{space[rs]}"
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
224
302
|
# Whether the formatter has any content.
|
|
225
303
|
#
|
|
226
304
|
# @return [true, false]
|
|
@@ -229,8 +307,8 @@ module Terminal
|
|
|
229
307
|
# Maximum display width of any line.
|
|
230
308
|
#
|
|
231
309
|
# @example
|
|
232
|
-
#
|
|
233
|
-
#
|
|
310
|
+
# formatter = Terminal::Text::Formatter.new("short\na longer line")
|
|
311
|
+
# formatter.max_line_width # => 13
|
|
234
312
|
#
|
|
235
313
|
# @return [Integer] widest line in columns, `0` when empty
|
|
236
314
|
def max_line_width
|
|
@@ -352,9 +430,7 @@ module Terminal
|
|
|
352
430
|
next if current.is_a?(Space) # ignore trailing space
|
|
353
431
|
|
|
354
432
|
# handle Word...
|
|
355
|
-
splitted = current.split(width)
|
|
356
|
-
last = splitted.pop
|
|
357
|
-
|
|
433
|
+
*splitted, last = current.split(width)
|
|
358
434
|
if csi.empty?
|
|
359
435
|
splitted.each { ret << [it.to_str, it.size] }
|
|
360
436
|
else
|
|
@@ -368,7 +444,10 @@ module Terminal
|
|
|
368
444
|
size = last.size
|
|
369
445
|
end
|
|
370
446
|
|
|
371
|
-
ret.
|
|
447
|
+
if ret.size > 1
|
|
448
|
+
str, width = ret[-1]
|
|
449
|
+
ret.pop if width == 0 && str.empty?
|
|
450
|
+
end
|
|
372
451
|
ret
|
|
373
452
|
end
|
|
374
453
|
|
|
@@ -392,18 +471,41 @@ module Terminal
|
|
|
392
471
|
|
|
393
472
|
line.encode!(ENCODING) if line.encoding != ENCODING
|
|
394
473
|
|
|
395
|
-
line.scan(SCAN_REGEX) do |
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
474
|
+
line.scan(SCAN_REGEX) do |token|
|
|
475
|
+
b = token.getbyte(0)
|
|
476
|
+
|
|
477
|
+
if b > 0x20 # word chunk: an ASCII run or a single grapheme
|
|
478
|
+
# a single-byte token is printable ASCII: one character, one
|
|
479
|
+
# column - no need to ask for its width
|
|
480
|
+
size = token.bytesize
|
|
481
|
+
if size == 1
|
|
482
|
+
(last = @lex[-1]).is_a?(Word) and next last.add(token, 1)
|
|
483
|
+
next @lex << Word.new(token, 1)
|
|
484
|
+
end
|
|
485
|
+
# an ASCII run is the only multi-byte token which cannot be a
|
|
486
|
+
# grapheme cluster - there its width is simply its byte count
|
|
487
|
+
size = nil unless b < 0x80 && token.ascii_only?
|
|
488
|
+
if (last = @lex[-1]).is_a?(Word)
|
|
489
|
+
next size ? last.add(token, size, true) : last.add(token)
|
|
490
|
+
end
|
|
491
|
+
next @lex << Word.new(token, size, true) if size
|
|
492
|
+
next @lex << Word.new(token)
|
|
399
493
|
end
|
|
400
494
|
|
|
401
|
-
if
|
|
402
|
-
|
|
403
|
-
|
|
495
|
+
if SPACE_BYTE[b] && !(b == 0x0d && token.bytesize == 2)
|
|
496
|
+
size = token.bytesize
|
|
497
|
+
(last = @lex[-1]).is_a?(Space) and next last.size += size
|
|
498
|
+
next @lex << Space.new(size)
|
|
404
499
|
end
|
|
405
500
|
|
|
406
|
-
next @lex << EOL if
|
|
501
|
+
next @lex << EOL if b == 0x0a || b == 0x0d
|
|
502
|
+
|
|
503
|
+
# escape sequences are dropped without `ansi`, any other control
|
|
504
|
+
# character is text
|
|
505
|
+
next if b == 0x1b && ((c = token.getbyte(1)) == 0x5b || c == 0x5d)
|
|
506
|
+
|
|
507
|
+
(last = @lex[-1]).is_a?(Word) and next last.add(token)
|
|
508
|
+
@lex << Word.new(token)
|
|
407
509
|
end
|
|
408
510
|
|
|
409
511
|
@lex[-1] == EOL ? @lex[-1] = EOP : @lex << EOP
|
|
@@ -421,23 +523,46 @@ module Terminal
|
|
|
421
523
|
line.encode!(ENCODING) if line.encoding != ENCODING
|
|
422
524
|
csis = 0
|
|
423
525
|
|
|
424
|
-
line.scan(SCAN_REGEX) do |
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
526
|
+
line.scan(SCAN_REGEX) do |token|
|
|
527
|
+
b = token.getbyte(0)
|
|
528
|
+
|
|
529
|
+
if b > 0x20 # word chunk: an ASCII run or a single grapheme
|
|
530
|
+
# a single-byte token is printable ASCII: one character, one
|
|
531
|
+
# column - no need to ask for its width
|
|
532
|
+
size = token.bytesize
|
|
533
|
+
if size == 1
|
|
534
|
+
(last = @lex[-1]).is_a?(Word) and next last.add(token, 1)
|
|
535
|
+
next @lex << Word.new(token, 1)
|
|
536
|
+
end
|
|
537
|
+
# an ASCII run is the only multi-byte token which cannot be a
|
|
538
|
+
# grapheme cluster - there its width is simply its byte count
|
|
539
|
+
size = nil unless b < 0x80 && token.ascii_only?
|
|
540
|
+
if (last = @lex[-1]).is_a?(Word)
|
|
541
|
+
next size ? last.add(token, size, true) : last.add(token)
|
|
542
|
+
end
|
|
543
|
+
next @lex << Word.new(token, size, true) if size
|
|
544
|
+
next @lex << Word.new(token)
|
|
428
545
|
end
|
|
429
546
|
|
|
430
|
-
if
|
|
431
|
-
|
|
432
|
-
|
|
547
|
+
if SPACE_BYTE[b] && !(b == 0x0d && token.bytesize == 2)
|
|
548
|
+
size = token.bytesize
|
|
549
|
+
(last = @lex[-1]).is_a?(Space) and next last.size += size
|
|
550
|
+
next @lex << Space.new(size)
|
|
433
551
|
end
|
|
434
552
|
|
|
435
|
-
next @lex << EOL if
|
|
553
|
+
next @lex << EOL if b == 0x0a || b == 0x0d
|
|
436
554
|
|
|
437
|
-
|
|
555
|
+
if (c = token.getbyte(1)) == 0x5d # Osc
|
|
556
|
+
next @lex << Osc.new(token)
|
|
557
|
+
end
|
|
558
|
+
if c != 0x5b # Csi
|
|
559
|
+
# a bare escape is just text
|
|
560
|
+
(last = @lex[-1]).is_a?(Word) and next last.add(token)
|
|
561
|
+
next @lex << Word.new(token)
|
|
562
|
+
end
|
|
438
563
|
|
|
439
|
-
# Handle
|
|
440
|
-
if
|
|
564
|
+
# Handle Csi...
|
|
565
|
+
if token == "\e[m" || token == "\e[0m"
|
|
441
566
|
next if csis == 0
|
|
442
567
|
|
|
443
568
|
if @lex[-1].is_a?(Csi)
|
|
@@ -449,21 +574,17 @@ module Terminal
|
|
|
449
574
|
next @lex << CsiEnd
|
|
450
575
|
end
|
|
451
576
|
|
|
452
|
-
(last = @lex[-1]).is_a?(Csi) and next last.add(
|
|
577
|
+
(last = @lex[-1]).is_a?(Csi) and next last.add(token)
|
|
453
578
|
|
|
454
579
|
csis += 1
|
|
455
|
-
@lex << Csi.new(
|
|
580
|
+
@lex << Csi.new(token)
|
|
456
581
|
end
|
|
457
582
|
|
|
458
583
|
@lex.pop if (last = @lex[-1]) == EOL
|
|
459
584
|
|
|
460
585
|
next @lex << EOP if csis == 0
|
|
461
586
|
|
|
462
|
-
|
|
463
|
-
@lex.pop
|
|
464
|
-
else
|
|
465
|
-
@lex << CsiEnd
|
|
466
|
-
end
|
|
587
|
+
last.is_a?(Csi) && csis == 1 ? @lex.pop : @lex << CsiEnd
|
|
467
588
|
@lex << EOP
|
|
468
589
|
end
|
|
469
590
|
|
|
@@ -517,33 +638,56 @@ module Terminal
|
|
|
517
638
|
class Space
|
|
518
639
|
attr_accessor :size
|
|
519
640
|
def to_str = (@size == 1 ? ' ' : ' ' * @size)
|
|
520
|
-
def initialize = (@size =
|
|
641
|
+
def initialize(size = 1) = (@size = size)
|
|
521
642
|
def inc = (@size += 1)
|
|
522
643
|
def inspect = @size == 1 ? '<Space>' : "<Space #{@size}>"
|
|
523
644
|
end
|
|
524
645
|
|
|
525
646
|
# @private
|
|
647
|
+
# A chunk is either a single grapheme cluster or a run of printable
|
|
648
|
+
# ASCII, for which the display width equals the character count.
|
|
526
649
|
class Word
|
|
527
|
-
def to_str = (@to_str ||= @
|
|
528
|
-
def size = (@size ||= @sizes.sum)
|
|
529
|
-
def inspect = "<Word #{@sizes.sum}:#{@
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
650
|
+
def to_str = (@to_str ||= @chunks.size == 1 ? @chunks[0] : @chunks.join)
|
|
651
|
+
def size = (@size ||= @sizes.size == 1 ? @sizes[0] : @sizes.sum)
|
|
652
|
+
def inspect = "<Word #{@sizes.sum}:#{@chunks.join.inspect}>"
|
|
653
|
+
|
|
654
|
+
# `run` marks a chunk holding more than one character; only the lexer
|
|
655
|
+
# creates those and it knows without having to inspect the chunk again
|
|
656
|
+
def initialize(chunk, size = Text.char_width(chunk), run = false)
|
|
657
|
+
@chunks = [chunk]
|
|
533
658
|
@sizes = [size]
|
|
659
|
+
@runs = run
|
|
534
660
|
end
|
|
535
661
|
|
|
536
|
-
def add(
|
|
537
|
-
@
|
|
662
|
+
def add(chunk, size = Text.char_width(chunk), run = false)
|
|
663
|
+
@chunks << chunk
|
|
538
664
|
@sizes << size
|
|
665
|
+
@runs ||= run
|
|
539
666
|
nil
|
|
540
667
|
end
|
|
541
668
|
|
|
542
669
|
def split(width)
|
|
543
|
-
|
|
670
|
+
if @runs
|
|
671
|
+
# ASCII runs have to become single characters again to be split -
|
|
672
|
+
# a multi-character grapheme cluster must stay untouched
|
|
673
|
+
chars = []
|
|
674
|
+
sizes = []
|
|
675
|
+
@chunks.each_with_index do |chunk, idx|
|
|
676
|
+
if chunk.bytesize > 1 && chunk.getbyte(0) < 0x80 &&
|
|
677
|
+
chunk.ascii_only?
|
|
678
|
+
next chunk.each_char { chars << it and sizes << 1 }
|
|
679
|
+
end
|
|
680
|
+
chars << chunk
|
|
681
|
+
sizes << @sizes[idx]
|
|
682
|
+
end
|
|
683
|
+
else
|
|
684
|
+
chars = @chunks
|
|
685
|
+
sizes = @sizes
|
|
686
|
+
end
|
|
687
|
+
return [self] if chars.size == 1
|
|
544
688
|
ret = last = size = nil
|
|
545
|
-
|
|
546
|
-
csize =
|
|
689
|
+
chars.each_with_index do |char, idx|
|
|
690
|
+
csize = sizes[idx]
|
|
547
691
|
next ret = [last = Word.new(char, size = csize)] unless ret
|
|
548
692
|
next last.add(char, csize) if (size += csize) <= width
|
|
549
693
|
ret << (last = Word.new(char, size = csize))
|
|
@@ -597,16 +741,36 @@ module Terminal
|
|
|
597
741
|
space_cache[2]
|
|
598
742
|
SPACE_CACHE = space_cache.freeze
|
|
599
743
|
|
|
744
|
+
# A run of printable ASCII is only taken when the following character is
|
|
745
|
+
# ASCII too: everything which can extend a grapheme cluster (combining
|
|
746
|
+
# marks, ZWJ, variation selectors, ...) is non-ASCII, so a run can never
|
|
747
|
+
# swallow the base of a cluster `\X` would have kept together.
|
|
600
748
|
SCAN_REGEX =
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
|
604
|
-
|
|
|
605
|
-
|
|
|
606
|
-
| (\
|
|
607
|
-
|
|
749
|
+
/
|
|
750
|
+
[\x21-\x7e]+(?=[\x00-\x7f]|\z)
|
|
751
|
+
| \x20+
|
|
752
|
+
| \r?\n
|
|
753
|
+
| \e\[[\x30-\x3f]*[\x20-\x2f]*[a-zA-Z]
|
|
754
|
+
| \e\]\d+(?:;[^\a\e]+)*(?:\a|\e\\)
|
|
755
|
+
| \s
|
|
756
|
+
| \X
|
|
757
|
+
/x
|
|
758
|
+
|
|
759
|
+
SPACE_BYTE =
|
|
760
|
+
Hash
|
|
761
|
+
.new(false)
|
|
762
|
+
.compare_by_identity
|
|
763
|
+
.merge!(
|
|
764
|
+
0x20 => true,
|
|
765
|
+
0x09 => true,
|
|
766
|
+
0x0b => true,
|
|
767
|
+
0x0c => true,
|
|
768
|
+
0x0d => true
|
|
769
|
+
)
|
|
770
|
+
.freeze
|
|
608
771
|
|
|
609
772
|
private_constant(
|
|
773
|
+
:SPACE_BYTE,
|
|
610
774
|
:Space,
|
|
611
775
|
:Word,
|
|
612
776
|
:Osc,
|
data/lib/terminal/text.rb
CHANGED
|
@@ -29,14 +29,20 @@ module Terminal
|
|
|
29
29
|
# Terminal::Text.ambiguous_char_width = 2 # for CJK terminals
|
|
30
30
|
#
|
|
31
31
|
# @return [Integer] default: `1`
|
|
32
|
-
|
|
32
|
+
attr_reader :ambiguous_char_width
|
|
33
|
+
|
|
34
|
+
# @attribute [w] ambiguous_char_width
|
|
35
|
+
def ambiguous_char_width=(value)
|
|
36
|
+
autoload?(:CharWidth) or CharWidth.clear
|
|
37
|
+
@ambiguous_char_width = value
|
|
38
|
+
end
|
|
33
39
|
|
|
34
40
|
# Calculate the display width of a string in terminal columns.
|
|
35
41
|
#
|
|
36
42
|
# @example
|
|
37
43
|
# Terminal::Text.width('Hello') # => 5
|
|
38
44
|
# Terminal::Text.width('[bold]Hi[/bold]') # => 2
|
|
39
|
-
# Terminal::Text.width('[bold]Hi[/bold]', bbcode: false) # =>
|
|
45
|
+
# Terminal::Text.width('[bold]Hi[/bold]', bbcode: false) # => 15
|
|
40
46
|
#
|
|
41
47
|
# @param str [#to_s] the string to measure
|
|
42
48
|
# @param bbcode (see Formatter#initialize)
|
|
@@ -100,11 +106,30 @@ module Terminal
|
|
|
100
106
|
#
|
|
101
107
|
# @see Formatter.format
|
|
102
108
|
#
|
|
109
|
+
# @example
|
|
110
|
+
# Terminal::Text.format(
|
|
111
|
+
# 'Hello World', width: 9, align: :left, prefix: '| ', suffix: ' |'
|
|
112
|
+
# )
|
|
113
|
+
# # => ["| Hello |", "| World |"]
|
|
114
|
+
#
|
|
103
115
|
# @param (see Formatter.format)
|
|
104
116
|
# @return (see Formatter.format)
|
|
105
117
|
# @raise (see Formatter.format)
|
|
106
118
|
def format(...) = Formatter.format(...)
|
|
107
119
|
|
|
120
|
+
# Format text with alignment.
|
|
121
|
+
#
|
|
122
|
+
# @see Formatter.fmt
|
|
123
|
+
#
|
|
124
|
+
# @example
|
|
125
|
+
# Terminal::Text.fmt('Hi', align: :center, width: 10)
|
|
126
|
+
# # => [" Hi "]
|
|
127
|
+
#
|
|
128
|
+
# @param (see Formatter.fmt)
|
|
129
|
+
# @return (see Formatter.fmt)
|
|
130
|
+
# @raise (see Formatter.fmt)
|
|
131
|
+
def fmt(...) = Formatter.fmt(...)
|
|
132
|
+
|
|
108
133
|
# @deprecated Use {.lines} and iterate over the result.
|
|
109
134
|
#
|
|
110
135
|
# @comment @param str [Array<#to_s>] text to process
|
|
@@ -157,7 +182,7 @@ module Terminal
|
|
|
157
182
|
)
|
|
158
183
|
warn(
|
|
159
184
|
'[DEPRECATED] `each_line_with_size` is deprecated ' \
|
|
160
|
-
'– use `.
|
|
185
|
+
'– use `.lines_with_size.each` instead',
|
|
161
186
|
category: :deprecated,
|
|
162
187
|
uplevel: 1
|
|
163
188
|
)
|
|
@@ -206,66 +231,64 @@ module Terminal
|
|
|
206
231
|
def char_width(char)
|
|
207
232
|
ord = char.ord
|
|
208
233
|
return @ctrlchar_width[ord] if ord < 0x20
|
|
209
|
-
if char.
|
|
210
|
-
|
|
211
|
-
end
|
|
234
|
+
return ord < 0xa1 ? 1 : CharWidth[ord] if char.bytesize == 1
|
|
235
|
+
return 2 if ord == 0x1f3f3 && @very_special_flags.include?(char)
|
|
212
236
|
sum = 0
|
|
213
|
-
|
|
214
|
-
char.
|
|
215
|
-
next
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
sum += CharWidth[ord] || @ambiguous_char_width
|
|
237
|
+
skip = false
|
|
238
|
+
char.each_codepoint do |cp|
|
|
239
|
+
next skip = false if skip
|
|
240
|
+
next skip = true if cp == 0x200d # zero width joiner
|
|
241
|
+
sum += cp < 0xa1 ? 1 : CharWidth[cp]
|
|
219
242
|
end
|
|
220
243
|
sum
|
|
221
244
|
end
|
|
222
245
|
end
|
|
223
246
|
|
|
224
|
-
@
|
|
247
|
+
@very_special_flags = [
|
|
248
|
+
"🏳\u{FE0F}\u{200D}🌈", # rainbow flag
|
|
249
|
+
"🏳\u{FE0F}\u{200D}⚧\u{FE0F}" # transgender flag
|
|
250
|
+
].freeze
|
|
225
251
|
|
|
226
|
-
@ctrlchar_width =
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
0x1f => 1
|
|
263
|
-
)
|
|
264
|
-
.freeze
|
|
252
|
+
@ctrlchar_width = [
|
|
253
|
+
0,
|
|
254
|
+
1,
|
|
255
|
+
1,
|
|
256
|
+
1,
|
|
257
|
+
1,
|
|
258
|
+
0,
|
|
259
|
+
1,
|
|
260
|
+
0,
|
|
261
|
+
0,
|
|
262
|
+
1,
|
|
263
|
+
0,
|
|
264
|
+
0,
|
|
265
|
+
0,
|
|
266
|
+
0,
|
|
267
|
+
0,
|
|
268
|
+
0,
|
|
269
|
+
1,
|
|
270
|
+
1,
|
|
271
|
+
1,
|
|
272
|
+
1,
|
|
273
|
+
1,
|
|
274
|
+
1,
|
|
275
|
+
1,
|
|
276
|
+
1,
|
|
277
|
+
1,
|
|
278
|
+
1,
|
|
279
|
+
1,
|
|
280
|
+
1,
|
|
281
|
+
1,
|
|
282
|
+
1,
|
|
283
|
+
1,
|
|
284
|
+
1
|
|
285
|
+
].freeze
|
|
286
|
+
|
|
287
|
+
@ambiguous_char_width = 1
|
|
265
288
|
|
|
266
|
-
|
|
267
|
-
autoload :CharWidth,
|
|
268
|
-
autoload :UNICODE_VERSION,
|
|
289
|
+
char_width_file = "#{__dir__}/text/char_width.rb"
|
|
290
|
+
autoload :CharWidth, char_width_file
|
|
291
|
+
autoload :UNICODE_VERSION, char_width_file
|
|
269
292
|
private_constant :CharWidth
|
|
270
293
|
end
|
|
271
294
|
end
|
data/lib/terminal/version.rb
CHANGED