terminal_rb 1.0.6 → 1.0.8
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/key-codes.rb +1 -1
- data/lib/terminal/ansi.rb +40 -30
- data/lib/terminal/output/ansi.rb +2 -0
- data/lib/terminal/text/char_width.rb +2754 -2743
- data/lib/terminal/text/formatter.rb +131 -45
- data/lib/terminal/text.rb +59 -53
- data/lib/terminal/version.rb +1 -1
- metadata +3 -3
|
@@ -392,18 +392,41 @@ module Terminal
|
|
|
392
392
|
|
|
393
393
|
line.encode!(ENCODING) if line.encoding != ENCODING
|
|
394
394
|
|
|
395
|
-
line.scan(SCAN_REGEX) do |
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
395
|
+
line.scan(SCAN_REGEX) do |token|
|
|
396
|
+
b = token.getbyte(0)
|
|
397
|
+
|
|
398
|
+
if b > 0x20 # word chunk: an ASCII run or a single grapheme
|
|
399
|
+
# a single-byte token is printable ASCII: one character, one
|
|
400
|
+
# column - no need to ask for its width
|
|
401
|
+
size = token.bytesize
|
|
402
|
+
if size == 1
|
|
403
|
+
(last = @lex[-1]).is_a?(Word) and next last.add(token, 1)
|
|
404
|
+
next @lex << Word.new(token, 1)
|
|
405
|
+
end
|
|
406
|
+
# an ASCII run is the only multi-byte token which cannot be a
|
|
407
|
+
# grapheme cluster - there its width is simply its byte count
|
|
408
|
+
size = nil unless b < 0x80 && token.ascii_only?
|
|
409
|
+
if (last = @lex[-1]).is_a?(Word)
|
|
410
|
+
next size ? last.add(token, size, true) : last.add(token)
|
|
411
|
+
end
|
|
412
|
+
next @lex << Word.new(token, size, true) if size
|
|
413
|
+
next @lex << Word.new(token)
|
|
399
414
|
end
|
|
400
415
|
|
|
401
|
-
if
|
|
402
|
-
|
|
403
|
-
|
|
416
|
+
if SPACE_BYTE[b] && !(b == 0x0d && token.bytesize == 2)
|
|
417
|
+
size = token.bytesize
|
|
418
|
+
(last = @lex[-1]).is_a?(Space) and next last.size += size
|
|
419
|
+
next @lex << Space.new(size)
|
|
404
420
|
end
|
|
405
421
|
|
|
406
|
-
next @lex << EOL if
|
|
422
|
+
next @lex << EOL if b == 0x0a || b == 0x0d
|
|
423
|
+
|
|
424
|
+
# escape sequences are dropped without `ansi`, any other control
|
|
425
|
+
# character is text
|
|
426
|
+
next if b == 0x1b && ((c = token.getbyte(1)) == 0x5b || c == 0x5d)
|
|
427
|
+
|
|
428
|
+
(last = @lex[-1]).is_a?(Word) and next last.add(token)
|
|
429
|
+
@lex << Word.new(token)
|
|
407
430
|
end
|
|
408
431
|
|
|
409
432
|
@lex[-1] == EOL ? @lex[-1] = EOP : @lex << EOP
|
|
@@ -421,23 +444,46 @@ module Terminal
|
|
|
421
444
|
line.encode!(ENCODING) if line.encoding != ENCODING
|
|
422
445
|
csis = 0
|
|
423
446
|
|
|
424
|
-
line.scan(SCAN_REGEX) do |
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
447
|
+
line.scan(SCAN_REGEX) do |token|
|
|
448
|
+
b = token.getbyte(0)
|
|
449
|
+
|
|
450
|
+
if b > 0x20 # word chunk: an ASCII run or a single grapheme
|
|
451
|
+
# a single-byte token is printable ASCII: one character, one
|
|
452
|
+
# column - no need to ask for its width
|
|
453
|
+
size = token.bytesize
|
|
454
|
+
if size == 1
|
|
455
|
+
(last = @lex[-1]).is_a?(Word) and next last.add(token, 1)
|
|
456
|
+
next @lex << Word.new(token, 1)
|
|
457
|
+
end
|
|
458
|
+
# an ASCII run is the only multi-byte token which cannot be a
|
|
459
|
+
# grapheme cluster - there its width is simply its byte count
|
|
460
|
+
size = nil unless b < 0x80 && token.ascii_only?
|
|
461
|
+
if (last = @lex[-1]).is_a?(Word)
|
|
462
|
+
next size ? last.add(token, size, true) : last.add(token)
|
|
463
|
+
end
|
|
464
|
+
next @lex << Word.new(token, size, true) if size
|
|
465
|
+
next @lex << Word.new(token)
|
|
428
466
|
end
|
|
429
467
|
|
|
430
|
-
if
|
|
431
|
-
|
|
432
|
-
|
|
468
|
+
if SPACE_BYTE[b] && !(b == 0x0d && token.bytesize == 2)
|
|
469
|
+
size = token.bytesize
|
|
470
|
+
(last = @lex[-1]).is_a?(Space) and next last.size += size
|
|
471
|
+
next @lex << Space.new(size)
|
|
433
472
|
end
|
|
434
473
|
|
|
435
|
-
next @lex << EOL if
|
|
474
|
+
next @lex << EOL if b == 0x0a || b == 0x0d
|
|
436
475
|
|
|
437
|
-
|
|
476
|
+
if (c = token.getbyte(1)) == 0x5d # Osc
|
|
477
|
+
next @lex << Osc.new(token)
|
|
478
|
+
end
|
|
479
|
+
if c != 0x5b # Csi
|
|
480
|
+
# a bare escape is just text
|
|
481
|
+
(last = @lex[-1]).is_a?(Word) and next last.add(token)
|
|
482
|
+
next @lex << Word.new(token)
|
|
483
|
+
end
|
|
438
484
|
|
|
439
|
-
# Handle
|
|
440
|
-
if
|
|
485
|
+
# Handle Csi...
|
|
486
|
+
if token == "\e[m" || token == "\e[0m"
|
|
441
487
|
next if csis == 0
|
|
442
488
|
|
|
443
489
|
if @lex[-1].is_a?(Csi)
|
|
@@ -449,21 +495,17 @@ module Terminal
|
|
|
449
495
|
next @lex << CsiEnd
|
|
450
496
|
end
|
|
451
497
|
|
|
452
|
-
(last = @lex[-1]).is_a?(Csi) and next last.add(
|
|
498
|
+
(last = @lex[-1]).is_a?(Csi) and next last.add(token)
|
|
453
499
|
|
|
454
500
|
csis += 1
|
|
455
|
-
@lex << Csi.new(
|
|
501
|
+
@lex << Csi.new(token)
|
|
456
502
|
end
|
|
457
503
|
|
|
458
504
|
@lex.pop if (last = @lex[-1]) == EOL
|
|
459
505
|
|
|
460
506
|
next @lex << EOP if csis == 0
|
|
461
507
|
|
|
462
|
-
|
|
463
|
-
@lex.pop
|
|
464
|
-
else
|
|
465
|
-
@lex << CsiEnd
|
|
466
|
-
end
|
|
508
|
+
last.is_a?(Csi) && csis == 1 ? @lex.pop : @lex << CsiEnd
|
|
467
509
|
@lex << EOP
|
|
468
510
|
end
|
|
469
511
|
|
|
@@ -517,33 +559,57 @@ module Terminal
|
|
|
517
559
|
class Space
|
|
518
560
|
attr_accessor :size
|
|
519
561
|
def to_str = (@size == 1 ? ' ' : ' ' * @size)
|
|
520
|
-
def initialize = (@size =
|
|
562
|
+
def initialize(size = 1) = (@size = size)
|
|
521
563
|
def inc = (@size += 1)
|
|
522
564
|
def inspect = @size == 1 ? '<Space>' : "<Space #{@size}>"
|
|
523
565
|
end
|
|
524
566
|
|
|
525
567
|
# @private
|
|
568
|
+
# A chunk is either a single grapheme cluster or a run of printable
|
|
569
|
+
# ASCII, for which the display width equals the character count.
|
|
526
570
|
class Word
|
|
527
|
-
def to_str = (@to_str ||= @
|
|
528
|
-
def size = (@size ||= @sizes.sum)
|
|
529
|
-
def inspect = "<Word #{@sizes.sum}:#{@
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
571
|
+
def to_str = (@to_str ||= @chunks.size == 1 ? @chunks[0] : @chunks.join)
|
|
572
|
+
def size = (@size ||= @sizes.size == 1 ? @sizes[0] : @sizes.sum)
|
|
573
|
+
def inspect = "<Word #{@sizes.sum}:#{@chunks.join.inspect}>"
|
|
574
|
+
|
|
575
|
+
# `run` marks a chunk holding more than one character; only the lexer
|
|
576
|
+
# creates those and it knows without having to inspect the chunk again
|
|
577
|
+
def initialize(chunk, size = Text.char_width(chunk), run = false)
|
|
578
|
+
@chunks = [chunk]
|
|
533
579
|
@sizes = [size]
|
|
580
|
+
@runs = run
|
|
534
581
|
end
|
|
535
582
|
|
|
536
|
-
def add(
|
|
537
|
-
@
|
|
583
|
+
def add(chunk, size = Text.char_width(chunk), run = false)
|
|
584
|
+
@chunks << chunk
|
|
538
585
|
@sizes << size
|
|
586
|
+
@runs ||= run
|
|
539
587
|
nil
|
|
540
588
|
end
|
|
541
589
|
|
|
542
590
|
def split(width)
|
|
543
|
-
|
|
591
|
+
if @runs
|
|
592
|
+
# ASCII runs have to become single characters again to be split -
|
|
593
|
+
# a multi-character grapheme cluster must stay untouched
|
|
594
|
+
chars = []
|
|
595
|
+
sizes = []
|
|
596
|
+
@chunks.each_with_index do |chunk, idx|
|
|
597
|
+
if chunk.bytesize > 1 && chunk.getbyte(0) < 0x80 &&
|
|
598
|
+
chunk.ascii_only?
|
|
599
|
+
chunk.each_char { chars << it and sizes << 1 }
|
|
600
|
+
else
|
|
601
|
+
chars << chunk
|
|
602
|
+
sizes << @sizes[idx]
|
|
603
|
+
end
|
|
604
|
+
end
|
|
605
|
+
else
|
|
606
|
+
chars = @chunks
|
|
607
|
+
sizes = @sizes
|
|
608
|
+
end
|
|
609
|
+
return [self] if chars.size == 1
|
|
544
610
|
ret = last = size = nil
|
|
545
|
-
|
|
546
|
-
csize =
|
|
611
|
+
chars.each_with_index do |char, idx|
|
|
612
|
+
csize = sizes[idx]
|
|
547
613
|
next ret = [last = Word.new(char, size = csize)] unless ret
|
|
548
614
|
next last.add(char, csize) if (size += csize) <= width
|
|
549
615
|
ret << (last = Word.new(char, size = csize))
|
|
@@ -597,16 +663,36 @@ module Terminal
|
|
|
597
663
|
space_cache[2]
|
|
598
664
|
SPACE_CACHE = space_cache.freeze
|
|
599
665
|
|
|
666
|
+
# A run of printable ASCII is only taken when the following character is
|
|
667
|
+
# ASCII too: everything which can extend a grapheme cluster (combining
|
|
668
|
+
# marks, ZWJ, variation selectors, ...) is non-ASCII, so a run can never
|
|
669
|
+
# swallow the base of a cluster `\X` would have kept together.
|
|
600
670
|
SCAN_REGEX =
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
|
604
|
-
|
|
|
605
|
-
|
|
|
606
|
-
| (\
|
|
607
|
-
|
|
671
|
+
/
|
|
672
|
+
[\x21-\x7e]+(?=[\x00-\x7f]|\z)
|
|
673
|
+
| \x20+
|
|
674
|
+
| \r?\n
|
|
675
|
+
| \e\[[\x30-\x3f]*[\x20-\x2f]*[a-zA-Z]
|
|
676
|
+
| \e\]\d+(?:;[^\a\e]+)*(?:\a|\e\\)
|
|
677
|
+
| \s
|
|
678
|
+
| \X
|
|
679
|
+
/x
|
|
680
|
+
|
|
681
|
+
SPACE_BYTE =
|
|
682
|
+
Hash
|
|
683
|
+
.new(false)
|
|
684
|
+
.compare_by_identity
|
|
685
|
+
.merge!(
|
|
686
|
+
0x20 => true,
|
|
687
|
+
0x09 => true,
|
|
688
|
+
0x0b => true,
|
|
689
|
+
0x0c => true,
|
|
690
|
+
0x0d => true
|
|
691
|
+
)
|
|
692
|
+
.freeze
|
|
608
693
|
|
|
609
694
|
private_constant(
|
|
695
|
+
:SPACE_BYTE,
|
|
610
696
|
:Space,
|
|
611
697
|
:Word,
|
|
612
698
|
:Osc,
|
data/lib/terminal/text.rb
CHANGED
|
@@ -29,7 +29,13 @@ 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
|
#
|
|
@@ -206,66 +212,66 @@ module Terminal
|
|
|
206
212
|
def char_width(char)
|
|
207
213
|
ord = char.ord
|
|
208
214
|
return @ctrlchar_width[ord] if ord < 0x20
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
215
|
+
# CharWidth knows this rule too - a comparison just beats a memo hit
|
|
216
|
+
return ord < 0xa1 ? 1 : CharWidth[ord] if char.bytesize == 1
|
|
217
|
+
return 2 if ord == 0x1f3f3 && @very_special_flags.include?(char)
|
|
212
218
|
sum = 0
|
|
213
|
-
|
|
214
|
-
char.
|
|
215
|
-
next
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
sum += CharWidth[ord] || @ambiguous_char_width
|
|
219
|
+
skip = false
|
|
220
|
+
char.each_codepoint do |cp|
|
|
221
|
+
next skip = false if skip
|
|
222
|
+
next skip = true if cp == 0x200d # zero width joiner
|
|
223
|
+
sum += CharWidth[cp]
|
|
219
224
|
end
|
|
220
225
|
sum
|
|
221
226
|
end
|
|
222
227
|
end
|
|
223
228
|
|
|
224
|
-
@
|
|
229
|
+
@very_special_flags = [
|
|
230
|
+
"🏳\u{FE0F}\u{200D}🌈", # rainbow flag
|
|
231
|
+
"🏳\u{FE0F}\u{200D}⚧\u{FE0F}" # transgender flag
|
|
232
|
+
].freeze
|
|
225
233
|
|
|
226
|
-
|
|
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
|
-
|
|
263
|
-
)
|
|
264
|
-
.freeze
|
|
234
|
+
# indexed by ordinal, only ever asked for control characters
|
|
235
|
+
@ctrlchar_width = [
|
|
236
|
+
0,
|
|
237
|
+
1,
|
|
238
|
+
1,
|
|
239
|
+
1,
|
|
240
|
+
1,
|
|
241
|
+
0,
|
|
242
|
+
1,
|
|
243
|
+
0,
|
|
244
|
+
0,
|
|
245
|
+
1,
|
|
246
|
+
0,
|
|
247
|
+
0,
|
|
248
|
+
0,
|
|
249
|
+
0,
|
|
250
|
+
0,
|
|
251
|
+
0,
|
|
252
|
+
1,
|
|
253
|
+
1,
|
|
254
|
+
1,
|
|
255
|
+
1,
|
|
256
|
+
1,
|
|
257
|
+
1,
|
|
258
|
+
1,
|
|
259
|
+
1,
|
|
260
|
+
1,
|
|
261
|
+
1,
|
|
262
|
+
1,
|
|
263
|
+
1,
|
|
264
|
+
1,
|
|
265
|
+
1,
|
|
266
|
+
1,
|
|
267
|
+
1
|
|
268
|
+
].freeze
|
|
269
|
+
|
|
270
|
+
@ambiguous_char_width = 1
|
|
265
271
|
|
|
266
|
-
|
|
267
|
-
autoload :CharWidth,
|
|
268
|
-
autoload :UNICODE_VERSION,
|
|
272
|
+
char_width_file = "#{__dir__}/text/char_width.rb"
|
|
273
|
+
autoload :CharWidth, char_width_file
|
|
274
|
+
autoload :UNICODE_VERSION, char_width_file
|
|
269
275
|
private_constant :CharWidth
|
|
270
276
|
end
|
|
271
277
|
end
|
data/lib/terminal/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: terminal_rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mike Blumtritt
|
|
@@ -60,7 +60,7 @@ metadata:
|
|
|
60
60
|
yard.run: yard
|
|
61
61
|
source_code_uri: https://codeberg.org/mblumtritt/Terminal.rb
|
|
62
62
|
bug_tracker_uri: https://codeberg.org/mblumtritt/Terminal.rb/issues
|
|
63
|
-
documentation_uri: https://rubydoc.info/gems/terminal_rb/1.0.
|
|
63
|
+
documentation_uri: https://rubydoc.info/gems/terminal_rb/1.0.8
|
|
64
64
|
rdoc_options: []
|
|
65
65
|
require_paths:
|
|
66
66
|
- lib
|
|
@@ -75,7 +75,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
75
75
|
- !ruby/object:Gem::Version
|
|
76
76
|
version: '0'
|
|
77
77
|
requirements: []
|
|
78
|
-
rubygems_version: 4.0.
|
|
78
|
+
rubygems_version: 4.0.18
|
|
79
79
|
specification_version: 4
|
|
80
80
|
summary: Fast terminal access with ANSI, CSIu, mouse events, BBCode, word-wise line
|
|
81
81
|
break support and much more.
|