terminal_rb 0.15.0 → 0.16.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.
- checksums.yaml +4 -4
- data/README.md +2 -1
- data/examples/key-codes.rb +6 -3
- data/lib/terminal/ansi.rb +121 -133
- data/lib/terminal/input/key_event.rb +165 -149
- data/lib/terminal/input.rb +113 -45
- data/lib/terminal/shell.rb +2 -6
- data/lib/terminal/text.rb +8 -11
- data/lib/terminal/version.rb +1 -1
- data/lib/terminal.rb +4 -4
- data/terminal_rb.gemspec +8 -7
- metadata +6 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 05eda4c596ad7060b576cb01f6fe7dafd38e114274b44c14c4c90e1b3a2b322a
|
|
4
|
+
data.tar.gz: '0479860569041d6fb874d38de0d183a2cc778a6803d3eed302425cd795133e06'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ab038b9b4cc9ed41ba09673be9a2e2d25aa2edc2ffbf1fa0cad826ca7f55c8fef3aab734648f13f63750ea7799bce638479839e5f609e597eeb0bf4508b0f0e7
|
|
7
|
+
data.tar.gz: b501721dfd7d6d79ceb8821fa5dd9d877e764e235435f3e6441e9b02b424a5873aba1318365a4b064a9430c135882be39470361d0042b56726be3dd2505e9db5
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Terminal.rb 
|
|
2
2
|
|
|
3
|
-
Terminal.rb supports you with input and output on your terminal. Simple [BBCode](https://en.wikipedia.org/wiki/BBCode)-like markup for attributes and coloring, word-wise line breaks,
|
|
3
|
+
Terminal.rb supports you with input and output on your terminal. Simple [BBCode](https://en.wikipedia.org/wiki/BBCode)-like markup for attributes and coloring, word-wise line breaks, correct special key recognition and mouse event reporting enable you to implement your CLI app quickly and easily.
|
|
4
4
|
|
|
5
5
|
- Gem: [rubygems.org](https://rubygems.org/gems/terminal_rb)
|
|
6
6
|
- Source: [codeberg.org](https://codeberg.org/mblumtritt/Terminal.rb)
|
|
@@ -16,6 +16,7 @@ Terminal.rb supports you with input and output on your terminal. Simple [BBCode]
|
|
|
16
16
|
- calculation for correct display width of strings containing Unicdode chars inclusive emojis
|
|
17
17
|
- word-wise line break generator
|
|
18
18
|
- supports [CSIu protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol)
|
|
19
|
+
- mouse events support
|
|
19
20
|
|
|
20
21
|
## Examples
|
|
21
22
|
|
data/examples/key-codes.rb
CHANGED
|
@@ -13,10 +13,13 @@ TEXT
|
|
|
13
13
|
Terminal.hide_cursor
|
|
14
14
|
at_exit { Terminal.show_cursor } # required for some terminals :/
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
# if you like to have mouse position changes reported then use
|
|
17
|
+
# 'mouse_move: true' in next line
|
|
18
|
+
Terminal.on_key_event(mouse: true, focus: true, mouse_move: false) do |event|
|
|
18
19
|
str = "[blue]: [yellow]#{event.raw.inspect}"
|
|
19
20
|
str << " [bold bright_green]#{event.name}[/]" unless event.simple?
|
|
21
|
+
str << " [dim]#{event.position.inspect}[/]" if event.position
|
|
20
22
|
Terminal.puts(str)
|
|
21
|
-
|
|
23
|
+
event.name != 'Esc'
|
|
22
24
|
end
|
|
25
|
+
puts
|
data/lib/terminal/ansi.rb
CHANGED
|
@@ -34,8 +34,8 @@ module Terminal
|
|
|
34
34
|
# @!group ANSI control code generator functions
|
|
35
35
|
#
|
|
36
36
|
|
|
37
|
-
# Combine given
|
|
38
|
-
#
|
|
37
|
+
# Combine given {attributes}, {colors}, {named_colors} and color codes to
|
|
38
|
+
# an ANSI control code sequence.
|
|
39
39
|
#
|
|
40
40
|
# Colors can specified by their name for ANSI 3-bit and 4-bit colors.
|
|
41
41
|
# For 8-bit ANSI colors use 2-digit hexadecimal values `00`...`ff`.
|
|
@@ -96,7 +96,7 @@ module Terminal
|
|
|
96
96
|
return +'' if attributes.empty?
|
|
97
97
|
"\e[#{
|
|
98
98
|
attributes
|
|
99
|
-
.map do |arg|
|
|
99
|
+
.map! do |arg|
|
|
100
100
|
case arg
|
|
101
101
|
when String
|
|
102
102
|
@attr_map[arg] || _invalid(arg)
|
|
@@ -154,8 +154,7 @@ module Terminal
|
|
|
154
154
|
# @param str [#to_s] string to be modified
|
|
155
155
|
# @return [String] string without ANSI attributes
|
|
156
156
|
def undecorate(str)
|
|
157
|
-
str = str.to_s
|
|
158
|
-
str.index("\e") ? str.gsub(@re_test, '') : str.dup
|
|
157
|
+
(str = str.to_s).index("\e") ? str.gsub(@re_test, '') : str.dup
|
|
159
158
|
end
|
|
160
159
|
|
|
161
160
|
# Try to combine given ANSI attributes and colors.
|
|
@@ -263,8 +262,7 @@ module Terminal
|
|
|
263
262
|
# @param str [#to_s] string to be modified
|
|
264
263
|
# @return [String] string without BBCode and ANSI control codes.
|
|
265
264
|
def plain(str)
|
|
266
|
-
str = str.to_s
|
|
267
|
-
unless str.index('[')
|
|
265
|
+
unless (str = str.to_s).index('[')
|
|
268
266
|
return str.index("\e") ? str.gsub(@re_test, '') : str.dup
|
|
269
267
|
end
|
|
270
268
|
str =
|
|
@@ -289,16 +287,14 @@ module Terminal
|
|
|
289
287
|
pos = -1
|
|
290
288
|
@pi2_third ||= 2.0 * Math::PI / 3.0
|
|
291
289
|
@pi4_third ||= 4.0 * Math::PI / 3.0
|
|
292
|
-
|
|
293
|
-
.to_s
|
|
294
|
-
.chars
|
|
295
|
-
.map! do |char|
|
|
290
|
+
(
|
|
291
|
+
str.to_s.chars.map! do |char|
|
|
296
292
|
i = (seed + ((pos += 1) / spread)) * frequency
|
|
297
293
|
"\e[38;2;#{(Math.sin(i) * 255).to_i.abs};" \
|
|
298
294
|
"#{(Math.sin(i + @pi2_third) * 255).to_i.abs};" \
|
|
299
295
|
"#{(Math.sin(i + @pi4_third) * 255).to_i.abs}m#{char}"
|
|
300
|
-
end
|
|
301
|
-
|
|
296
|
+
end << RESET
|
|
297
|
+
).join
|
|
302
298
|
end
|
|
303
299
|
|
|
304
300
|
#
|
|
@@ -349,6 +345,21 @@ module Terminal
|
|
|
349
345
|
# @return (see cursor_up)
|
|
350
346
|
def cursor_column(column = 1) = "\e[#{column}G"
|
|
351
347
|
|
|
348
|
+
# Move cursor to given column in the current row relative to the current
|
|
349
|
+
# position.
|
|
350
|
+
# (Skip some columns.)
|
|
351
|
+
#
|
|
352
|
+
# @param (see cursor_column)
|
|
353
|
+
# @return (see cursor_up)
|
|
354
|
+
def cursor_column_rel(column = 1) = "\e[#{column}a"
|
|
355
|
+
|
|
356
|
+
# Move cursor to given row relative to the current position.
|
|
357
|
+
# (Skip some rows.)
|
|
358
|
+
#
|
|
359
|
+
# @param row [Integer] row index
|
|
360
|
+
# @return (see cursor_up)
|
|
361
|
+
def cursor_row_rel(row = 1) = "\e[#{row}e"
|
|
362
|
+
|
|
352
363
|
# Move to given row and column.
|
|
353
364
|
#
|
|
354
365
|
# @param row [Integer] row index
|
|
@@ -389,20 +400,7 @@ module Terminal
|
|
|
389
400
|
#
|
|
390
401
|
# @param part [:below, :above, :all, :scrollback] screen part to erase
|
|
391
402
|
# @return (see cursor_up)
|
|
392
|
-
def screen_erase(part = :all)
|
|
393
|
-
"\e[#{
|
|
394
|
-
case part
|
|
395
|
-
when :below
|
|
396
|
-
# nop
|
|
397
|
-
when :above
|
|
398
|
-
'1'
|
|
399
|
-
when :scrollback
|
|
400
|
-
'3'
|
|
401
|
-
else # all
|
|
402
|
-
'2'
|
|
403
|
-
end
|
|
404
|
-
}J"
|
|
405
|
-
end
|
|
403
|
+
def screen_erase(part = :all) = "\e[#{@screen_erase[part]}J"
|
|
406
404
|
|
|
407
405
|
# Safe current screen.
|
|
408
406
|
#
|
|
@@ -442,22 +440,17 @@ module Terminal
|
|
|
442
440
|
# @!group Other ANSI control functions
|
|
443
441
|
#
|
|
444
442
|
|
|
443
|
+
# Repeat last char.
|
|
444
|
+
#
|
|
445
|
+
# @param count [Integer] repeat count
|
|
446
|
+
# @return (see cursor_up)
|
|
447
|
+
def char_repeat(count = 1) = "\e[#{count}b"
|
|
448
|
+
|
|
445
449
|
# Erase part of line.
|
|
446
450
|
#
|
|
447
451
|
# @param part [:to_end, :to_start, :all] line part to erase
|
|
448
452
|
# @return (see cursor_up)
|
|
449
|
-
def line_erase(part = :all)
|
|
450
|
-
"\e[#{
|
|
451
|
-
case part
|
|
452
|
-
when :to_end
|
|
453
|
-
# nop
|
|
454
|
-
when :to_start
|
|
455
|
-
'1'
|
|
456
|
-
else # :all
|
|
457
|
-
'2'
|
|
458
|
-
end
|
|
459
|
-
}K"
|
|
460
|
-
end
|
|
453
|
+
def line_erase(part = :all) = "\e[#{@line_erase[part]}K"
|
|
461
454
|
|
|
462
455
|
# Set (tab) title.
|
|
463
456
|
# This is not widely supported; works for
|
|
@@ -570,27 +563,6 @@ module Terminal
|
|
|
570
563
|
caller(1)
|
|
571
564
|
)
|
|
572
565
|
end
|
|
573
|
-
|
|
574
|
-
def _color(str)
|
|
575
|
-
b, v = /\A(fg|bg|on|ul)?_?#?([[:xdigit:]]{1,6})\z/.match(str)&.captures
|
|
576
|
-
if v
|
|
577
|
-
return(
|
|
578
|
-
case v.size
|
|
579
|
-
when 1, 2
|
|
580
|
-
"#{@cbase[b]};5;#{v.hex}"
|
|
581
|
-
when 3
|
|
582
|
-
"#{@cbase[b]};2;#{(v[0] * 2).hex};#{
|
|
583
|
-
(v[1] * 2).hex
|
|
584
|
-
};#{(v[2] * 2).hex}"
|
|
585
|
-
when 6
|
|
586
|
-
"#{@cbase[b]};2;#{v[0, 2].hex};#{v[2, 2].hex};#{v[4, 2].hex}"
|
|
587
|
-
end
|
|
588
|
-
)
|
|
589
|
-
end
|
|
590
|
-
b, v = /\A(fg|bg|on|ul)?_?([a-z]{3,}[0-9]{0,3})\z/.match(str)&.captures
|
|
591
|
-
return unless v
|
|
592
|
-
name = NAMED_COLORS[v] and return "#{@cbase[b]};#{name}"
|
|
593
|
-
end
|
|
594
566
|
end
|
|
595
567
|
|
|
596
568
|
@cbase = { 'bg' => '48', 'on' => '48', 'ul' => '58' }
|
|
@@ -606,56 +578,6 @@ module Terminal
|
|
|
606
578
|
|
|
607
579
|
@re_bbcode = /(?:\[((?~[\[\]]))\])/
|
|
608
580
|
|
|
609
|
-
attr_map = {
|
|
610
|
-
'' => 'reset',
|
|
611
|
-
'1' => 'bold',
|
|
612
|
-
'2' => 'faint',
|
|
613
|
-
'3' => 'italic',
|
|
614
|
-
'4' => 'underline',
|
|
615
|
-
'5' => 'blink',
|
|
616
|
-
'6' => 'rapid_blink',
|
|
617
|
-
'7' => 'invert',
|
|
618
|
-
'8' => 'hide',
|
|
619
|
-
'9' => 'strike',
|
|
620
|
-
'10' => 'primary_font',
|
|
621
|
-
'11' => 'font1',
|
|
622
|
-
'12' => 'font2',
|
|
623
|
-
'13' => 'font3',
|
|
624
|
-
'14' => 'font4',
|
|
625
|
-
'15' => 'font5',
|
|
626
|
-
'16' => 'font6',
|
|
627
|
-
'17' => 'font7',
|
|
628
|
-
'18' => 'font8',
|
|
629
|
-
'19' => 'font9',
|
|
630
|
-
'20' => 'fraktur',
|
|
631
|
-
'21' => 'double_underline',
|
|
632
|
-
'22' => 'bold_off', # faint_off
|
|
633
|
-
'23' => 'italic_off', # fraktur_off
|
|
634
|
-
'24' => 'underline_off', # double_underline_off
|
|
635
|
-
'25' => 'blink_off', # rapid_blink_off
|
|
636
|
-
'26' => 'proportional',
|
|
637
|
-
'27' => 'invert_off',
|
|
638
|
-
'28' => 'hide_off',
|
|
639
|
-
'29' => 'strike_off',
|
|
640
|
-
# colors ...
|
|
641
|
-
'50' => 'proportional_off',
|
|
642
|
-
'51' => 'framed',
|
|
643
|
-
'52' => 'encircled',
|
|
644
|
-
'53' => 'overlined',
|
|
645
|
-
'54' => 'framed_off', # encircled_off
|
|
646
|
-
'55' => 'overlined_off',
|
|
647
|
-
# ...
|
|
648
|
-
'73' => 'superscript',
|
|
649
|
-
'74' => 'subscript',
|
|
650
|
-
'75' => 'superscript_off', # subscript_off
|
|
651
|
-
# special underline
|
|
652
|
-
'4:3' => 'curly_underline',
|
|
653
|
-
'4:4' => 'dotted_underline',
|
|
654
|
-
'4:5' => 'dashed_underline',
|
|
655
|
-
'4:0' => 'curly_underline_off' # dotted_underline_off, dashed_underline_off
|
|
656
|
-
}.invert
|
|
657
|
-
attr_alias = ->(t, s) { attr_map[t] = attr_map[s] }
|
|
658
|
-
|
|
659
581
|
clr_map = {
|
|
660
582
|
# foreground
|
|
661
583
|
'30' => 'black',
|
|
@@ -725,6 +647,56 @@ module Terminal
|
|
|
725
647
|
clr_alias['fg_default', 'default']
|
|
726
648
|
clr_alias['bg_default', 'on_default']
|
|
727
649
|
|
|
650
|
+
attr_map = {
|
|
651
|
+
'' => 'reset',
|
|
652
|
+
'1' => 'bold',
|
|
653
|
+
'2' => 'faint',
|
|
654
|
+
'3' => 'italic',
|
|
655
|
+
'4' => 'underline',
|
|
656
|
+
'5' => 'blink',
|
|
657
|
+
'6' => 'rapid_blink',
|
|
658
|
+
'7' => 'invert',
|
|
659
|
+
'8' => 'hide',
|
|
660
|
+
'9' => 'strike',
|
|
661
|
+
'10' => 'primary_font',
|
|
662
|
+
'11' => 'font1',
|
|
663
|
+
'12' => 'font2',
|
|
664
|
+
'13' => 'font3',
|
|
665
|
+
'14' => 'font4',
|
|
666
|
+
'15' => 'font5',
|
|
667
|
+
'16' => 'font6',
|
|
668
|
+
'17' => 'font7',
|
|
669
|
+
'18' => 'font8',
|
|
670
|
+
'19' => 'font9',
|
|
671
|
+
'20' => 'fraktur',
|
|
672
|
+
'21' => 'double_underline',
|
|
673
|
+
'22' => 'bold_off', # faint_off
|
|
674
|
+
'23' => 'italic_off', # fraktur_off
|
|
675
|
+
'24' => 'underline_off', # double_underline_off
|
|
676
|
+
'25' => 'blink_off', # rapid_blink_off
|
|
677
|
+
'26' => 'proportional',
|
|
678
|
+
'27' => 'invert_off',
|
|
679
|
+
'28' => 'hide_off',
|
|
680
|
+
'29' => 'strike_off',
|
|
681
|
+
# colors ...
|
|
682
|
+
'50' => 'proportional_off',
|
|
683
|
+
'51' => 'framed',
|
|
684
|
+
'52' => 'encircled',
|
|
685
|
+
'53' => 'overlined',
|
|
686
|
+
'54' => 'framed_off', # encircled_off
|
|
687
|
+
'55' => 'overlined_off',
|
|
688
|
+
# ...
|
|
689
|
+
'73' => 'superscript',
|
|
690
|
+
'74' => 'subscript',
|
|
691
|
+
'75' => 'superscript_off', # subscript_off
|
|
692
|
+
# special underline
|
|
693
|
+
'4:3' => 'curly_underline',
|
|
694
|
+
'4:4' => 'dotted_underline',
|
|
695
|
+
'4:5' => 'dashed_underline',
|
|
696
|
+
'4:0' => 'curly_underline_off' # dotted_underline_off, dashed_underline_off
|
|
697
|
+
}.invert
|
|
698
|
+
attr_alias = ->(t, s) { attr_map[t] = attr_map[s] }
|
|
699
|
+
|
|
728
700
|
attr_alias['faint_off', 'bold_off']
|
|
729
701
|
attr_alias['fraktur_off', 'italic_off']
|
|
730
702
|
attr_alias['double_underline_off', 'underline_off']
|
|
@@ -788,7 +760,23 @@ module Terminal
|
|
|
788
760
|
attr_map['/bg'] = clr_map['on_default']
|
|
789
761
|
attr_map['/ul'] = clr_map['ul_default']
|
|
790
762
|
|
|
791
|
-
@attr_map =
|
|
763
|
+
@attr_map =
|
|
764
|
+
Hash.new do |_, str|
|
|
765
|
+
b, v = /\A(fg|bg|on|ul)?_?#?([[:xdigit:]]{1,6})\z/.match(str)&.captures
|
|
766
|
+
unless v
|
|
767
|
+
b = /\A(fg|bg|on|ul)?_?([a-z]{3,}[0-9]{0,3})\z/.match(str) or next
|
|
768
|
+
name = NAMED_COLORS[b[2]] or next
|
|
769
|
+
next "#{@cbase[b[1]]};#{name}"
|
|
770
|
+
end
|
|
771
|
+
case v.size
|
|
772
|
+
when 1, 2
|
|
773
|
+
"#{@cbase[b]};5;#{v.hex}"
|
|
774
|
+
when 3
|
|
775
|
+
"#{@cbase[b]};2;#{(v[0] * 2).hex};#{(v[1] * 2).hex};#{(v[2] * 2).hex}"
|
|
776
|
+
when 6
|
|
777
|
+
"#{@cbase[b]};2;#{v[0, 2].hex};#{v[2, 2].hex};#{v[4, 2].hex}"
|
|
778
|
+
end
|
|
779
|
+
end
|
|
792
780
|
attr_map.merge!(clr_map).keys.sort!.each { @attr_map[_1] = attr_map[_1] }
|
|
793
781
|
@attr_map.freeze
|
|
794
782
|
|
|
@@ -796,21 +784,29 @@ module Terminal
|
|
|
796
784
|
@attrs_map.default_proc = @attr_map.default_proc
|
|
797
785
|
@attrs_map.compare_by_identity.freeze
|
|
798
786
|
|
|
787
|
+
@screen_erase = { below: nil, above: '1', scrollback: '3' }
|
|
788
|
+
@screen_erase.default = '2'
|
|
789
|
+
@screen_erase.compare_by_identity.freeze
|
|
790
|
+
|
|
791
|
+
@line_erase = { to_end: nil, to_start: '1' }
|
|
792
|
+
@line_erase.default = '2'
|
|
793
|
+
@line_erase.compare_by_identity.freeze
|
|
794
|
+
|
|
799
795
|
autoload :NAMED_COLORS, "#{__dir__}/ansi/named_colors.rb"
|
|
800
796
|
private_constant :NAMED_COLORS
|
|
801
797
|
|
|
802
798
|
# @private
|
|
803
|
-
RESET = self[:reset]
|
|
799
|
+
RESET = -self[:reset]
|
|
804
800
|
|
|
805
801
|
# @private
|
|
806
802
|
FULL_RESET = "\ec"
|
|
807
803
|
|
|
808
804
|
# @private
|
|
809
|
-
CURSOR_HOME = cursor_pos(nil, nil)
|
|
805
|
+
CURSOR_HOME = -cursor_pos(nil, nil)
|
|
810
806
|
# @private
|
|
811
|
-
CURSOR_FIRST_ROW = cursor_pos(1)
|
|
807
|
+
CURSOR_FIRST_ROW = -cursor_pos(1)
|
|
812
808
|
# @private
|
|
813
|
-
CURSOR_FIRST_COLUMN = cursor_column(1)
|
|
809
|
+
CURSOR_FIRST_COLUMN = -cursor_column(1)
|
|
814
810
|
|
|
815
811
|
# @private
|
|
816
812
|
CURSOR_SHOW = "\e[?25h"
|
|
@@ -828,13 +824,13 @@ module Terminal
|
|
|
828
824
|
CURSOR_POS_RESTORE = "\e8"
|
|
829
825
|
|
|
830
826
|
# @private
|
|
831
|
-
SCREEN_ERASE = screen_erase
|
|
827
|
+
SCREEN_ERASE = -screen_erase
|
|
832
828
|
# @private
|
|
833
|
-
SCREEN_ERASE_BELOW = screen_erase(:below)
|
|
829
|
+
SCREEN_ERASE_BELOW = -screen_erase(:below)
|
|
834
830
|
# @private
|
|
835
|
-
SCREEN_ERASE_ABOVE = screen_erase(:above)
|
|
831
|
+
SCREEN_ERASE_ABOVE = -screen_erase(:above)
|
|
836
832
|
# @private
|
|
837
|
-
SCREEN_ERASE_SCROLLBACK = screen_erase(:scrollback)
|
|
833
|
+
SCREEN_ERASE_SCROLLBACK = -screen_erase(:scrollback)
|
|
838
834
|
|
|
839
835
|
# @private
|
|
840
836
|
SCREEN_SAVE = "\e[?47h"
|
|
@@ -843,7 +839,7 @@ module Terminal
|
|
|
843
839
|
|
|
844
840
|
# @private
|
|
845
841
|
# @comment at least Kitty requires CURSOR_HOME too
|
|
846
|
-
SCREEN_ALTERNATE = "\e[?1049h#{CURSOR_HOME}"
|
|
842
|
+
SCREEN_ALTERNATE = -"\e[?1049h#{CURSOR_HOME}"
|
|
847
843
|
# @private
|
|
848
844
|
SCREEN_ALTERNATE_OFF = "\e[?1049l"
|
|
849
845
|
|
|
@@ -853,21 +849,19 @@ module Terminal
|
|
|
853
849
|
SCREEN_REVERSE_MODE_OFF = "\e[?5l"
|
|
854
850
|
|
|
855
851
|
# @private
|
|
856
|
-
LINE_ERASE = line_erase
|
|
852
|
+
LINE_ERASE = -line_erase
|
|
857
853
|
# @private
|
|
858
|
-
LINE_ERASE_TO_END = line_erase(:to_end)
|
|
854
|
+
LINE_ERASE_TO_END = -line_erase(:to_end)
|
|
859
855
|
# @private
|
|
860
|
-
LINE_ERASE_TO_START = line_erase(:to_start)
|
|
856
|
+
LINE_ERASE_TO_START = -line_erase(:to_start)
|
|
861
857
|
# @private
|
|
862
|
-
LINE_ERASE_PREV = "#{cursor_prev_line(nil)}#{LINE_ERASE}"
|
|
858
|
+
LINE_ERASE_PREV = -"#{cursor_prev_line(nil)}#{LINE_ERASE}"
|
|
863
859
|
|
|
864
860
|
# @comment seems not widely supported:
|
|
865
861
|
# doubled: def cursor_column(column = 1) = "\e[#{column}`"
|
|
866
862
|
# doubled: def cursor_row(row = 1) = "\e[#{row}d"
|
|
867
863
|
# doubled: def cursor_pos(row, col) = "\e[#{row};#{col}f"
|
|
868
864
|
#
|
|
869
|
-
# def cursor_column_rel(columns = 1) = "\e[#{columns}a"
|
|
870
|
-
# def cursor_row_rel(rows = 1) = "\e[#{rows}e"
|
|
871
865
|
# def cursor_tab(count = 1) = "\e[#{column}I"
|
|
872
866
|
# def cursor_reverse_tab(count = 1) = "\e[#{count}Z"
|
|
873
867
|
#
|
|
@@ -878,20 +872,14 @@ module Terminal
|
|
|
878
872
|
# def chars_delete(count = 1) = "\e[#{count}P"
|
|
879
873
|
# def chars_erase(count = 1) = "\e[#{count}X"
|
|
880
874
|
#
|
|
881
|
-
# def chars_repeat_last(count = 1) = "\e[#{count}b"
|
|
882
|
-
#
|
|
883
|
-
# def notify(title) = "\e]9;#{title}\a"
|
|
884
|
-
#
|
|
885
875
|
# def set_scroll_region(top = nil, bottom = nil) = "\e[#{top};#{bottom}r"
|
|
886
876
|
|
|
887
877
|
# @comment other:
|
|
888
878
|
# "\eE" same as "\r\n"
|
|
889
|
-
# "\eD" same as "\n" but preserves
|
|
879
|
+
# "\eD" same as "\n" but preserves column
|
|
890
880
|
# "\eM" reverse "\n"
|
|
891
881
|
# "\e[6n" report Cursor Position as "ESC \[ row ; col R"
|
|
892
882
|
# "\e[21t report window’s title as ESC ] l title ESC \"
|
|
893
|
-
# "\e[?1004h" report focus lost "\e[O" and focus get "\e[I"
|
|
894
|
-
# (disable by "\e[?1004l")
|
|
895
883
|
|
|
896
884
|
# @comment TODO:
|
|
897
885
|
# https://sw.kovidgoyal.net/kitty/desktop-notifications
|
|
@@ -2,9 +2,85 @@
|
|
|
2
2
|
|
|
3
3
|
module Terminal
|
|
4
4
|
#
|
|
5
|
-
# Key event reported from {read_key_event}.
|
|
5
|
+
# Key event reported from {read_key_event} and {on_key_event}.
|
|
6
6
|
#
|
|
7
7
|
class KeyEvent
|
|
8
|
+
# ANSI code sequence received from standard input.
|
|
9
|
+
# This can be a simple value like `"a"`or more complex input like
|
|
10
|
+
# `"\e[24;6~"` (for Shift+Ctrl+F12).
|
|
11
|
+
#
|
|
12
|
+
# @return [String] received ANSI code sequence
|
|
13
|
+
attr_reader :raw
|
|
14
|
+
|
|
15
|
+
# Pressed key without any modifiers.
|
|
16
|
+
# This can be a string for simple keys like `"a"` or a Symbol like `:F12`
|
|
17
|
+
# (see {.key_names}).
|
|
18
|
+
# @return [String, Symbol] key without modifiers
|
|
19
|
+
attr_reader :key
|
|
20
|
+
|
|
21
|
+
# Modifier key code. This represents the encoded key modifier like `Shift`
|
|
22
|
+
# or `Alt`.
|
|
23
|
+
#
|
|
24
|
+
# @return [Integer] modifier key code
|
|
25
|
+
attr_reader :modifier
|
|
26
|
+
|
|
27
|
+
# Mouse event position.
|
|
28
|
+
#
|
|
29
|
+
# @return [[Integer,Integer]] row and column
|
|
30
|
+
# @return nil when no coordinates are assigned
|
|
31
|
+
attr_reader :position
|
|
32
|
+
|
|
33
|
+
# Name of the key event.
|
|
34
|
+
# This can be a simple name like `"a"` or `"Shift+Ctrl+F12"` for combined
|
|
35
|
+
# keys.
|
|
36
|
+
#
|
|
37
|
+
# @return [String] key name
|
|
38
|
+
attr_reader :name
|
|
39
|
+
|
|
40
|
+
# @attribute [r] modifier?
|
|
41
|
+
# @return [true, false] whether a key modifier was pressed
|
|
42
|
+
def modifier? = @modifier != 0
|
|
43
|
+
|
|
44
|
+
# @attribute [r] simple?
|
|
45
|
+
# @return [true, false] whether a simple key was pressed
|
|
46
|
+
def simple? = @raw == @name
|
|
47
|
+
|
|
48
|
+
# All pressed keys.
|
|
49
|
+
# This is composed by all {modifier} and the {key}.
|
|
50
|
+
#
|
|
51
|
+
# @return [Array<Symbol, String>] all pressed keys
|
|
52
|
+
def to_a = @ary.dup
|
|
53
|
+
|
|
54
|
+
# @private
|
|
55
|
+
def to_ary = simple? ? [@raw] : [@raw, @name]
|
|
56
|
+
|
|
57
|
+
# @private
|
|
58
|
+
def to_s = @name.dup
|
|
59
|
+
|
|
60
|
+
# @private
|
|
61
|
+
def inspect = "<#{self.class.name} #{to_ary.map(&:inspect).join(' ')}>"
|
|
62
|
+
|
|
63
|
+
# @private
|
|
64
|
+
def freeze
|
|
65
|
+
@raw.freeze
|
|
66
|
+
@key.freeze
|
|
67
|
+
@position.freeze
|
|
68
|
+
@name.freeze
|
|
69
|
+
super
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def initialize(raw, key, modifier, position)
|
|
75
|
+
@raw = raw
|
|
76
|
+
@key = key
|
|
77
|
+
@modifier = modifier
|
|
78
|
+
@position = position
|
|
79
|
+
@ary = MODIFIERS.filter_map { |b, n| n if modifier.allbits?(b) }
|
|
80
|
+
@ary << key if key
|
|
81
|
+
@name = @ary.join('+') # .encode(Encoding::UTF_8)
|
|
82
|
+
end
|
|
83
|
+
|
|
8
84
|
class << self
|
|
9
85
|
# @attribute [w] caching
|
|
10
86
|
# @return [true, false] whether {KeyEvent}s should be cached
|
|
@@ -12,24 +88,33 @@ module Terminal
|
|
|
12
88
|
|
|
13
89
|
# @attribute [w] caching
|
|
14
90
|
def caching=(value)
|
|
15
|
-
|
|
16
|
-
@cache ||= {}
|
|
17
|
-
else
|
|
18
|
-
@cache = nil
|
|
19
|
-
end
|
|
91
|
+
value ? @cache ||= {} : @cache = nil
|
|
20
92
|
end
|
|
21
93
|
|
|
22
|
-
#
|
|
94
|
+
# @attribute [r] key_names
|
|
95
|
+
# @return [Array<Symbol>] list of all avilable key names (see {key})
|
|
96
|
+
def key_names
|
|
97
|
+
(
|
|
98
|
+
@names ||=
|
|
99
|
+
(@csiu.values + @csi.values + @ss3.values)
|
|
100
|
+
.uniq
|
|
101
|
+
.keep_if { Symbol === _1 }
|
|
102
|
+
.sort!
|
|
103
|
+
).dup
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Translate a ANSI input code sequence into a related KeyEvent.
|
|
23
107
|
#
|
|
24
108
|
# @param raw [String] keyboard input
|
|
25
109
|
# @return [KeyEvent] related key event
|
|
26
110
|
def [](raw)
|
|
111
|
+
cached = @cache[raw] and return cached if @cache
|
|
27
112
|
return new(raw, *@single_key[raw.ord]) if raw.size == 1
|
|
28
113
|
return unknown(raw) if raw[0] != "\e"
|
|
29
114
|
return esc1(raw, raw[1]) if raw.size == 2 # ESC ?
|
|
30
115
|
case raw[1]
|
|
31
|
-
when "\e"
|
|
32
|
-
return esc_esc(raw)
|
|
116
|
+
when "\e"
|
|
117
|
+
return esc_esc(raw) # ESC ESC ...
|
|
33
118
|
when 'O'
|
|
34
119
|
return new(raw, *@ss3[raw[2].ord]) if raw.size == 3 # ESC O ?
|
|
35
120
|
when '['
|
|
@@ -37,15 +122,17 @@ module Terminal
|
|
|
37
122
|
if raw.size == 6 && raw[2] == 'M' # ESC [ M b c r
|
|
38
123
|
return mouse_vt200(raw)
|
|
39
124
|
end
|
|
40
|
-
|
|
125
|
+
if raw.size > 4 && raw[2] == '1' && raw[3] == ';'
|
|
126
|
+
return csi(raw) # ESC [ 1 ; ...
|
|
127
|
+
end
|
|
41
128
|
case raw[-1]
|
|
42
|
-
when '~'
|
|
43
|
-
return legacy(raw)
|
|
44
|
-
when 'u'
|
|
45
|
-
return csi_u(raw)
|
|
46
|
-
when 'M'
|
|
47
|
-
|
|
48
|
-
|
|
129
|
+
when '~'
|
|
130
|
+
return legacy(raw) # ESC [ ... ~
|
|
131
|
+
when 'u'
|
|
132
|
+
return csi_u(raw) # ESC [ ... u
|
|
133
|
+
when 'M', 'm'
|
|
134
|
+
# ESC [ ... M
|
|
135
|
+
# ESC [ ... m
|
|
49
136
|
return mouse_sgr(raw) if raw[2] == '<'
|
|
50
137
|
end
|
|
51
138
|
end
|
|
@@ -53,40 +140,29 @@ module Terminal
|
|
|
53
140
|
end
|
|
54
141
|
|
|
55
142
|
# @private
|
|
56
|
-
def new(raw, key = raw, modifier = 0,
|
|
57
|
-
@cache ?
|
|
143
|
+
def new(raw, key = raw, modifier = 0, position = nil)
|
|
144
|
+
(@cache && position.nil? ? @cache[raw] ||= super : super).freeze
|
|
58
145
|
end
|
|
59
146
|
|
|
60
147
|
private
|
|
61
148
|
|
|
62
149
|
def unknown(raw) = new(raw, nil)
|
|
63
150
|
|
|
64
|
-
def
|
|
65
|
-
# ESC
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
idx = raw.index(';')
|
|
69
|
-
code = raw[2..(idx ? idx - 1 : -2)].to_i
|
|
70
|
-
new(
|
|
71
|
-
raw,
|
|
72
|
-
@csiu[code] || code.chr(Encoding::UTF_8),
|
|
73
|
-
idx ? [raw[(idx + 1)..-2].to_i - 1, 0].max : 0
|
|
74
|
-
)
|
|
151
|
+
def esc1(raw, char)
|
|
152
|
+
# ESC ?
|
|
153
|
+
key, modifier = @esc1[char.ord]
|
|
154
|
+
new(raw, key || char, modifier || 2)
|
|
75
155
|
end
|
|
76
156
|
|
|
77
|
-
def
|
|
78
|
-
# ESC
|
|
79
|
-
# ESC
|
|
80
|
-
return
|
|
81
|
-
|
|
82
|
-
new(
|
|
83
|
-
raw,
|
|
84
|
-
@csi[raw[2..(idx ? idx - 1 : -2)].to_i],
|
|
85
|
-
idx ? [raw[(idx + 1)..-2].to_i - 1, 0].max : 0
|
|
86
|
-
)
|
|
157
|
+
def esc_esc(raw)
|
|
158
|
+
# ESC ESC ?
|
|
159
|
+
# ESC ESC ...
|
|
160
|
+
return esc1(raw, raw[2]) if raw.size == 3
|
|
161
|
+
ret = self[raw[1..]]
|
|
162
|
+
new(raw, ret.key, ret.modifier | 2)
|
|
87
163
|
end
|
|
88
164
|
|
|
89
|
-
def
|
|
165
|
+
def csi(raw)
|
|
90
166
|
# ESC [ 1 ; [~ABCDEFHPQRS]
|
|
91
167
|
# ESC [ 1 ; <modifier> [~ABCDEFHPQRS]
|
|
92
168
|
return unknown(raw) if raw.size < 5
|
|
@@ -97,19 +173,33 @@ module Terminal
|
|
|
97
173
|
new(raw, key, modifier)
|
|
98
174
|
end
|
|
99
175
|
|
|
100
|
-
def
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
176
|
+
def legacy(raw)
|
|
177
|
+
# ESC [ <code> ~
|
|
178
|
+
# ESC [ <code> ; <modifier> ~
|
|
179
|
+
return unknown(raw) if raw.size < 4
|
|
180
|
+
idx = raw.index(';')
|
|
181
|
+
new(
|
|
182
|
+
raw,
|
|
183
|
+
@csi[raw[2..(idx ? idx - 1 : -2)].to_i],
|
|
184
|
+
idx ? [raw[(idx + 1)..-2].to_i - 1, 0].max : 0
|
|
185
|
+
)
|
|
104
186
|
end
|
|
105
187
|
|
|
106
|
-
def
|
|
107
|
-
|
|
108
|
-
|
|
188
|
+
def csi_u(raw)
|
|
189
|
+
# ESC [ <code> u
|
|
190
|
+
# ESC [ <code> ; <modifier> u
|
|
191
|
+
return unknown(raw) if raw.size < 4
|
|
192
|
+
idx = raw.index(';')
|
|
193
|
+
code = raw[2..(idx ? idx - 1 : -2)].to_i
|
|
194
|
+
new(
|
|
195
|
+
raw,
|
|
196
|
+
@csiu[code] || code.chr(Encoding::UTF_8),
|
|
197
|
+
idx ? [raw[(idx + 1)..-2].to_i - 1, 0].max : 0
|
|
198
|
+
)
|
|
109
199
|
end
|
|
110
200
|
|
|
111
201
|
def mouse_vt200(raw)
|
|
112
|
-
# ESC [ M
|
|
202
|
+
# ESC [ M <btn> <col> <row>
|
|
113
203
|
mouse_event(raw, *raw[3..].chars.map! { _1.ord - 32 })
|
|
114
204
|
end
|
|
115
205
|
|
|
@@ -118,117 +208,40 @@ module Terminal
|
|
|
118
208
|
# ESC [ < <code> ; <col> ; <row> m
|
|
119
209
|
return unknown(raw) if raw.size < 8
|
|
120
210
|
bcr = raw[3..-2].split(';', 3).map!(&:to_i)
|
|
121
|
-
bcr.size == 3 ? mouse_event(raw, *bcr) : unknown(raw)
|
|
211
|
+
bcr.size == 3 ? mouse_event(raw, *bcr, raw[-1] == 'm') : unknown(raw)
|
|
122
212
|
end
|
|
123
213
|
|
|
124
|
-
def mouse_urxvt(raw)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
bcr.size == 3 ? mouse_event(raw, *bcr) : unknown(raw)
|
|
129
|
-
end
|
|
214
|
+
# def mouse_urxvt(raw)
|
|
215
|
+
# # ESC [ <code> ; <col> ; <row> M
|
|
216
|
+
# unknown(raw) # currently not supported
|
|
217
|
+
# end
|
|
130
218
|
|
|
131
|
-
def mouse_event(raw, btn, col, row)
|
|
219
|
+
def mouse_event(raw, btn, col, row, up = false)
|
|
132
220
|
return unknown(raw) if btn < 0 || col < 1 || row < 1
|
|
133
|
-
key
|
|
221
|
+
key = (btn & 1).nonzero? ? 1 : 0
|
|
222
|
+
key += 2 if btn.allbits?(2)
|
|
134
223
|
modifier = btn.allbits?(4) ? 1 : 0
|
|
135
224
|
modifier += 2 if btn.allbits?(8)
|
|
136
225
|
modifier += 4 if btn.allbits?(16)
|
|
137
|
-
|
|
226
|
+
modifier += 256 if moved = btn.allbits?(32)
|
|
227
|
+
key += 4 if btn.allbits?(64)
|
|
228
|
+
key += 8 if btn.allbits?(128)
|
|
229
|
+
new(raw, mouse_name(key, up, moved), modifier, [row, col])
|
|
138
230
|
end
|
|
139
231
|
|
|
140
|
-
def
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
232
|
+
def mouse_name(key, up, moved)
|
|
233
|
+
return up ? :MBLeftUp : :MBLeft if key == 0
|
|
234
|
+
return up ? :MBMiddleUp : :MBMiddle if key == 1
|
|
235
|
+
return up ? :MBRightUp : :MBRight if key == 2
|
|
236
|
+
return moved ? :Mouse : :MBUp if key == 3
|
|
237
|
+
return :MWUp if key == 4
|
|
238
|
+
return :MWDown if key == 5
|
|
239
|
+
return :MWLeft if key == 6
|
|
240
|
+
return :MWRight if key == 7
|
|
241
|
+
:"MB_#{key - 8}#{'Up' if up}"
|
|
145
242
|
end
|
|
146
243
|
end
|
|
147
244
|
|
|
148
|
-
# Event string received from standard input.
|
|
149
|
-
# This can be a simple value like `"a"`or more complex input like
|
|
150
|
-
# `"\e[24;6~"` (for Shift+Ctrl+F12).
|
|
151
|
-
#
|
|
152
|
-
# @return [String] received event string
|
|
153
|
-
attr_reader :raw
|
|
154
|
-
|
|
155
|
-
# Pressed key without any modifiers.
|
|
156
|
-
# This can be a string for simple keys like `"a"` or a Symbol like `:F12`.
|
|
157
|
-
# @return [String, Symbol] key without modifiers
|
|
158
|
-
attr_reader :key
|
|
159
|
-
|
|
160
|
-
# Modifier key code. This represents the encoded key modifier like `Shift`
|
|
161
|
-
# or `Alt`.
|
|
162
|
-
#
|
|
163
|
-
# @return [Integer] modifier key code
|
|
164
|
-
attr_reader :modifier
|
|
165
|
-
|
|
166
|
-
# @comment for mouse events
|
|
167
|
-
# @private
|
|
168
|
-
attr_reader :extra
|
|
169
|
-
|
|
170
|
-
# Name of the key event.
|
|
171
|
-
# This can be a simple name like `"a"` or `"Shift+Ctrl+F12"` for combined
|
|
172
|
-
# keys.
|
|
173
|
-
#
|
|
174
|
-
# @return [String] key name
|
|
175
|
-
attr_reader :name
|
|
176
|
-
|
|
177
|
-
# @attribute [r] modifier?
|
|
178
|
-
# @return [true, false] whether a key modifier was pressed
|
|
179
|
-
def modifier? = @modifier != 0
|
|
180
|
-
|
|
181
|
-
# @attribute [r] simple?
|
|
182
|
-
# @return [true, false] whether a simple key was pressed
|
|
183
|
-
def simple? = @raw == @name
|
|
184
|
-
|
|
185
|
-
# All pressed keys.
|
|
186
|
-
# This is composed by all {modifier} and the {key}.
|
|
187
|
-
#
|
|
188
|
-
# @return [Array<Symbol, String>] all pressed keys
|
|
189
|
-
def to_a = @ary.dup
|
|
190
|
-
|
|
191
|
-
# @private
|
|
192
|
-
def to_ary = simple? ? [@raw] : [@raw, @name]
|
|
193
|
-
|
|
194
|
-
# @private
|
|
195
|
-
def to_s = @name.dup
|
|
196
|
-
|
|
197
|
-
# @private
|
|
198
|
-
def inspect = "<#{self.class.name} #{to_ary.map(&:inspect).join(' ')}>"
|
|
199
|
-
|
|
200
|
-
# @private
|
|
201
|
-
def freeze
|
|
202
|
-
@raw.freeze
|
|
203
|
-
@key.freeze
|
|
204
|
-
@extra.freeze
|
|
205
|
-
@name.freeze
|
|
206
|
-
super
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
private
|
|
210
|
-
|
|
211
|
-
def initialize(raw, key, modifier, extra)
|
|
212
|
-
@raw = raw
|
|
213
|
-
@key = key
|
|
214
|
-
@modifier = modifier
|
|
215
|
-
@extra = extra
|
|
216
|
-
@ary = MODIFIERS.filter_map { |b, n| n if modifier.allbits?(b) }
|
|
217
|
-
@ary << key if key
|
|
218
|
-
@name = @ary.join('+') # .encode(Encoding::UTF_8)
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
@mouse_kind = %i[
|
|
222
|
-
MButton1
|
|
223
|
-
MButton2
|
|
224
|
-
MButton3
|
|
225
|
-
MButtonUp
|
|
226
|
-
MButton4
|
|
227
|
-
MButton5
|
|
228
|
-
].freeze
|
|
229
|
-
|
|
230
|
-
@mouse_wheel_kind = %i[MWheelDown MWheelUp MWheelRight MWheelLeft].freeze
|
|
231
|
-
|
|
232
245
|
@csiu = {
|
|
233
246
|
0x02 => :Ins,
|
|
234
247
|
0x09 => :Tab,
|
|
@@ -357,6 +370,8 @@ module Terminal
|
|
|
357
370
|
0x44 => :Left, # D
|
|
358
371
|
0x46 => :End, # F
|
|
359
372
|
0x48 => :Home, # H
|
|
373
|
+
0x49 => :Focus, # I
|
|
374
|
+
0x4f => :UnFocus, # O
|
|
360
375
|
0x50 => :F1, # P
|
|
361
376
|
0x51 => :F2, # Q
|
|
362
377
|
0x52 => :F3, # R
|
|
@@ -421,7 +436,8 @@ module Terminal
|
|
|
421
436
|
16 => :Hyper,
|
|
422
437
|
32 => :Meta,
|
|
423
438
|
64 => :Caps,
|
|
424
|
-
128 => :Num
|
|
439
|
+
128 => :Num,
|
|
440
|
+
256 => :Move # mouse
|
|
425
441
|
}.freeze
|
|
426
442
|
private_constant :MODIFIERS
|
|
427
443
|
end
|
data/lib/terminal/input.rb
CHANGED
|
@@ -16,7 +16,7 @@ module Terminal
|
|
|
16
16
|
# @return [:dumb]
|
|
17
17
|
# for non-interactive input (pipes etc.)
|
|
18
18
|
# @return [:error]
|
|
19
|
-
# when input device is closed
|
|
19
|
+
# when input device is not avail (closed)
|
|
20
20
|
def input_mode
|
|
21
21
|
@input_mode ||= find_input_mode
|
|
22
22
|
end
|
|
@@ -34,6 +34,10 @@ module Terminal
|
|
|
34
34
|
# @return [[String, String]] key code and key name in `:both` mode
|
|
35
35
|
# @return [nil] in error case
|
|
36
36
|
def read_key(mode: :named)
|
|
37
|
+
warn(
|
|
38
|
+
'Terminal.read_key is deprecaded; use Terminal.read_key_event instead.',
|
|
39
|
+
uplevel: 1
|
|
40
|
+
)
|
|
37
41
|
event = read_key_event or return
|
|
38
42
|
return event.raw if mode == :raw
|
|
39
43
|
key, name = event
|
|
@@ -48,25 +52,109 @@ module Terminal
|
|
|
48
52
|
case input_mode
|
|
49
53
|
when :dumb
|
|
50
54
|
raw = read_dumb or return
|
|
51
|
-
opts =
|
|
55
|
+
opts = @dumb_keys[raw.ord] if raw.size == 1
|
|
52
56
|
KeyEvent.new(raw, *opts)
|
|
53
57
|
when :csi_u, :legacy
|
|
54
|
-
|
|
55
|
-
KeyEvent[raw]
|
|
58
|
+
raw = read_tty or return
|
|
59
|
+
KeyEvent[raw]
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Event loop for key and mouse events.
|
|
64
|
+
#
|
|
65
|
+
# @param mouse [true, false]
|
|
66
|
+
# whether mouse buttons should be reported
|
|
67
|
+
# @param mouse_move [true, false]
|
|
68
|
+
# whether mouse movement should be reported
|
|
69
|
+
# @param focus [true, false]
|
|
70
|
+
# whether focus/unfocus of terminal window should be reported
|
|
71
|
+
# @yieldparam event [KeyEvent]
|
|
72
|
+
# next event
|
|
73
|
+
# @yieldreturn [true, false]
|
|
74
|
+
# whether the loop should be continued
|
|
75
|
+
# @return [true]
|
|
76
|
+
# when the loop was started
|
|
77
|
+
# @return [false]
|
|
78
|
+
# when the current input device is not available
|
|
79
|
+
# @return [nil]
|
|
80
|
+
# when no block was given
|
|
81
|
+
def on_key_event(mouse: false, mouse_move: false, focus: false, &block)
|
|
82
|
+
return unless block
|
|
83
|
+
case input_mode
|
|
84
|
+
when :dumb
|
|
85
|
+
on_bumb_key_event(&block)
|
|
86
|
+
true
|
|
87
|
+
when :csi_u, :legacy
|
|
88
|
+
on_tty_key_event(mouse, mouse_move, focus, &block)
|
|
89
|
+
true
|
|
90
|
+
else
|
|
91
|
+
false
|
|
56
92
|
end
|
|
57
93
|
end
|
|
58
94
|
|
|
59
95
|
private
|
|
60
96
|
|
|
97
|
+
def on_tty_key_event(mouse, mouse_move, focus)
|
|
98
|
+
# highlight: '1001'
|
|
99
|
+
# drag: '1002'
|
|
100
|
+
# move: '1003'
|
|
101
|
+
# focus: '1004' - lost: "\e[O", get: "\e[I"
|
|
102
|
+
# ext: '1005'
|
|
103
|
+
# sgr: '1006'
|
|
104
|
+
# urxvt: '1015'
|
|
105
|
+
# pixel: '1016'
|
|
106
|
+
opts =
|
|
107
|
+
if mouse || mouse_move || focus
|
|
108
|
+
opts = mouse ? +"\e[?1006;1000" : +"\e[?1006"
|
|
109
|
+
opts << ';1003' if mouse_move
|
|
110
|
+
opts << ';1004' if focus
|
|
111
|
+
raw_write("#{opts}h") ? "#{opts}l" : nil
|
|
112
|
+
end
|
|
113
|
+
STDIN.noecho do |stdin|
|
|
114
|
+
while (raw = stdin.getch)
|
|
115
|
+
if raw != "\e"
|
|
116
|
+
yield(KeyEvent[raw]) ? next : break
|
|
117
|
+
end
|
|
118
|
+
lesci = 0
|
|
119
|
+
while (nc = stdin.read_nonblock(1, exception: false))
|
|
120
|
+
break unless String === nc
|
|
121
|
+
lesci = raw.size if nc == "\e"
|
|
122
|
+
raw << nc
|
|
123
|
+
end
|
|
124
|
+
if lesci < 2
|
|
125
|
+
yield(KeyEvent[raw]) ? next : break
|
|
126
|
+
end
|
|
127
|
+
break unless raw[1..].split("\e").all? { yield(KeyEvent["\e#{_1}"]) }
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
rescue Interrupt
|
|
131
|
+
# nop
|
|
132
|
+
rescue IOError, SystemCallError
|
|
133
|
+
@input_mode = :error
|
|
134
|
+
ensure
|
|
135
|
+
raw_write(opts) if opts
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def on_bumb_key_event
|
|
139
|
+
while (raw = read_dumb)
|
|
140
|
+
opts = @dumb_keys[raw.ord] if raw.size == 1
|
|
141
|
+
return unless yield(KeyEvent.new(raw, *opts))
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
61
145
|
def find_input_mode
|
|
62
|
-
# order is important!
|
|
63
146
|
im = ENV['INPUT_MODE']
|
|
64
|
-
return :dumb if im == 'dumb'
|
|
65
147
|
return :legacy if im == 'legacy'
|
|
66
|
-
return :dumb
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
148
|
+
return :dumb if im == 'dumb' || !STDIN.tty?
|
|
149
|
+
STDIN.noecho do |stdin|
|
|
150
|
+
if raw_write("\e[>1u\e[?u\e[c")
|
|
151
|
+
inp = +''
|
|
152
|
+
inp << stdin.getch until inp.rindex('c')
|
|
153
|
+
if inp.include?("\e[?1u")
|
|
154
|
+
at_exit { raw_write("\e[<u") }
|
|
155
|
+
return :csi_u
|
|
156
|
+
end
|
|
157
|
+
end
|
|
70
158
|
end
|
|
71
159
|
:legacy
|
|
72
160
|
rescue Interrupt
|
|
@@ -75,12 +163,6 @@ module Terminal
|
|
|
75
163
|
:error
|
|
76
164
|
end
|
|
77
165
|
|
|
78
|
-
def csi_u?
|
|
79
|
-
inp = +''
|
|
80
|
-
STDIN.raw { inp << _1.getch until inp.rindex('c') }
|
|
81
|
-
inp.include?("\e[?1u")
|
|
82
|
-
end
|
|
83
|
-
|
|
84
166
|
def read_dumb
|
|
85
167
|
STDIN.getc
|
|
86
168
|
rescue Interrupt
|
|
@@ -90,45 +172,31 @@ module Terminal
|
|
|
90
172
|
nil
|
|
91
173
|
end
|
|
92
174
|
|
|
93
|
-
def read_tty_with_mouse(each_move: false)
|
|
94
|
-
# highlight: '1001'
|
|
95
|
-
# drag: '1002'
|
|
96
|
-
# move: '1003'
|
|
97
|
-
# ext: '1005'
|
|
98
|
-
# sgr: '1006'
|
|
99
|
-
# urxvt: '1015'
|
|
100
|
-
# pixel: '1016'
|
|
101
|
-
opts = each_move ? '1000;1003;1006;1015' : '1000;1006;1015'
|
|
102
|
-
opts = raw_write("\e[?#{opts}h") ? "\e[?#{opts}l" : nil
|
|
103
|
-
read_tty
|
|
104
|
-
ensure
|
|
105
|
-
raw_write(opts) if opts
|
|
106
|
-
end
|
|
107
|
-
|
|
108
175
|
def read_tty
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
176
|
+
STDIN.noecho do |stdin|
|
|
177
|
+
if (key = stdin.getch) == "\e"
|
|
178
|
+
while (nc = stdin.read_nonblock(1, exception: false))
|
|
179
|
+
String === nc ? key << nc : break
|
|
180
|
+
end
|
|
112
181
|
end
|
|
182
|
+
key
|
|
113
183
|
end
|
|
114
|
-
key
|
|
115
184
|
rescue Interrupt
|
|
116
185
|
nil
|
|
117
186
|
rescue IOError, SystemCallError
|
|
118
187
|
@input_mode = :error
|
|
119
188
|
nil
|
|
120
189
|
end
|
|
121
|
-
|
|
122
|
-
DUMB_KEYS = {
|
|
123
|
-
0x05 => ['c', 4],
|
|
124
|
-
0x08 => :Back,
|
|
125
|
-
0x09 => :Tab,
|
|
126
|
-
0x0a => :Enter,
|
|
127
|
-
0x0d => :Return,
|
|
128
|
-
0x1b => :Esc
|
|
129
|
-
}.compare_by_identity.freeze
|
|
130
|
-
private_constant :DUMB_KEYS
|
|
131
190
|
end
|
|
132
191
|
|
|
192
|
+
@dumb_keys = {
|
|
193
|
+
0x05 => ['c', 4],
|
|
194
|
+
0x08 => :Back,
|
|
195
|
+
0x09 => :Tab,
|
|
196
|
+
0x0a => :Enter,
|
|
197
|
+
0x0d => :Return,
|
|
198
|
+
0x1b => :Esc
|
|
199
|
+
}.compare_by_identity.freeze
|
|
200
|
+
|
|
133
201
|
autoload :KeyEvent, "#{__dir__}/input/key_event.rb"
|
|
134
202
|
end
|
data/lib/terminal/shell.rb
CHANGED
|
@@ -116,12 +116,8 @@ module Terminal
|
|
|
116
116
|
end
|
|
117
117
|
|
|
118
118
|
def initialize(enum)
|
|
119
|
-
@enum =
|
|
120
|
-
|
|
121
|
-
enum.enum_for(:each)
|
|
122
|
-
else
|
|
123
|
-
Enumerator.new { |y| enum.each { y << _1 } }
|
|
124
|
-
end
|
|
119
|
+
return @enum = enum.enum_for(:each) if enum.respond_to?(:enum_for)
|
|
120
|
+
@enum = Enumerator.new { |y| enum.each { y << _1 } }
|
|
125
121
|
end
|
|
126
122
|
end
|
|
127
123
|
end
|
data/lib/terminal/text.rb
CHANGED
|
@@ -24,14 +24,14 @@ module Terminal
|
|
|
24
24
|
# some are ambiguous. The function uses {ambiguous_char_width} for each of
|
|
25
25
|
# these characters.
|
|
26
26
|
#
|
|
27
|
-
# @param
|
|
27
|
+
# @param str [#to_s] str to process
|
|
28
28
|
# @param bbcode [true|false] whether to interpret embedded BBCode
|
|
29
29
|
# @return [Integer] display width
|
|
30
30
|
def width(str, bbcode: true)
|
|
31
31
|
return 0 if (str = bbcode ? Ansi.unbbcode(str) : str.to_s).empty?
|
|
32
32
|
str = str.encode(@encoding) if str.encoding != @encoding
|
|
33
33
|
width = 0
|
|
34
|
-
str.scan(
|
|
34
|
+
str.scan(@scan_width) do |sp, gc|
|
|
35
35
|
next width += char_width(gc) if gc
|
|
36
36
|
width += 1 if sp
|
|
37
37
|
end
|
|
@@ -343,7 +343,7 @@ module Terminal
|
|
|
343
343
|
|
|
344
344
|
txt = txt.encode(@encoding) if txt.encoding != @encoding
|
|
345
345
|
|
|
346
|
-
txt.scan(
|
|
346
|
+
txt.scan(@scan_snippet) do |nl, csi, osc, space, gc|
|
|
347
347
|
if gc
|
|
348
348
|
next last.add(gc, char_width(gc)) if last.is_a?(word_class)
|
|
349
349
|
next ret << (last = word_class.new(gc, char_width(gc)))
|
|
@@ -441,13 +441,10 @@ module Terminal
|
|
|
441
441
|
end
|
|
442
442
|
end
|
|
443
443
|
|
|
444
|
-
private_constant :Osc, :CsiEnd, :Csi, :Word, :WordEx
|
|
445
|
-
|
|
446
444
|
@ambiguous_char_width = 1
|
|
447
|
-
@encoding = Encoding::UTF_8
|
|
448
|
-
@empty = String.new(encoding: @encoding).freeze
|
|
445
|
+
@empty = String.new(encoding: @encoding = Encoding::UTF_8).freeze
|
|
449
446
|
|
|
450
|
-
|
|
447
|
+
@scan_snippet =
|
|
451
448
|
/\G(?:
|
|
452
449
|
(\r?\n)
|
|
453
450
|
| (\e\[[\d;:?]*[ABCDEFGHJKSTfhilmnsu])
|
|
@@ -456,14 +453,13 @@ module Terminal
|
|
|
456
453
|
| (\X)
|
|
457
454
|
)/x
|
|
458
455
|
|
|
459
|
-
|
|
456
|
+
@scan_width =
|
|
460
457
|
/\G(?:
|
|
461
458
|
(?:\e\[[\d;:?]*[ABCDEFGHJKSTfhilmnsu])
|
|
462
459
|
| (?:\e\]\d+(?:;[^\a\e]+)*(?:\a|\e\\))
|
|
463
460
|
| (\s+)
|
|
464
461
|
| (\X)
|
|
465
462
|
)/x
|
|
466
|
-
private_constant :SCAN_EXPR, :WIDTH_SCANNER
|
|
467
463
|
|
|
468
464
|
@ctrlchar_width = {
|
|
469
465
|
0x00 => 0,
|
|
@@ -501,6 +497,7 @@ module Terminal
|
|
|
501
497
|
}.compare_by_identity.freeze
|
|
502
498
|
|
|
503
499
|
autoload :CharWidth, "#{__dir__}/text/char_width.rb"
|
|
504
|
-
|
|
500
|
+
|
|
501
|
+
private_constant :Osc, :Csi, :CsiEnd, :Word, :WordEx, :CharWidth
|
|
505
502
|
end
|
|
506
503
|
end
|
data/lib/terminal/version.rb
CHANGED
data/lib/terminal.rb
CHANGED
|
@@ -10,7 +10,7 @@ require_relative 'terminal/input'
|
|
|
10
10
|
# It automagically detects whether your terminal supports ANSI features, like
|
|
11
11
|
# coloring (see {colors}) or the
|
|
12
12
|
# [CSIu protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol) support
|
|
13
|
-
# (see {read_key_event} and {input_mode}).
|
|
13
|
+
# (see {read_key_event}, {on_key_event} and {input_mode}).
|
|
14
14
|
# It calculates the display width for Unicode chars (see {Text.width}) and help
|
|
15
15
|
# you to display text with word-wise line breaks (see {Text.each_line}).
|
|
16
16
|
#
|
|
@@ -19,8 +19,8 @@ module Terminal
|
|
|
19
19
|
# Return true if the current terminal supports ANSI control codes.
|
|
20
20
|
# When the terminal does not support it, {colors} will return 2 (two) and
|
|
21
21
|
# all output methods ({<<}, {print}, {puts}) will not forward ANSI control
|
|
22
|
-
# codes to the terminal, {read_key_event} will not
|
|
23
|
-
# codes.
|
|
22
|
+
# codes to the terminal, {read_key_event} and {on_key_event} will not
|
|
23
|
+
# support extended key codes and/or mouse events.
|
|
24
24
|
#
|
|
25
25
|
# @attribute [r] ansi?
|
|
26
26
|
# @return [Boolean] whether ANSI control codes are supported
|
|
@@ -290,7 +290,7 @@ module Terminal
|
|
|
290
290
|
|
|
291
291
|
# @private
|
|
292
292
|
def raw_write(str)
|
|
293
|
-
@out&.
|
|
293
|
+
@out&.syswrite(str)
|
|
294
294
|
rescue IOError
|
|
295
295
|
@out = nil
|
|
296
296
|
end
|
data/terminal_rb.gemspec
CHANGED
|
@@ -5,13 +5,15 @@ require_relative 'lib/terminal/version'
|
|
|
5
5
|
Gem::Specification.new do |spec|
|
|
6
6
|
spec.name = 'terminal_rb'
|
|
7
7
|
spec.version = Terminal::VERSION
|
|
8
|
-
spec.summary =
|
|
9
|
-
|
|
8
|
+
spec.summary = <<~SUMMARY.tr("\n", ' ')
|
|
9
|
+
Fast terminal access with ANSI, CSIu, mouse eventing, BBCode, word-wise
|
|
10
|
+
line break support and much more.
|
|
11
|
+
SUMMARY
|
|
10
12
|
spec.description = <<~DESCRIPTION.tr("\n", ' ')
|
|
11
13
|
Terminal.rb supports you with input and output on your terminal.
|
|
12
14
|
Simple BBCode-like markup for attributes and coloring, word-wise line
|
|
13
|
-
breaks,
|
|
14
|
-
CLI app quickly and easily.
|
|
15
|
+
breaks, correct special key recognition and mouse event reporting enable
|
|
16
|
+
you to implement your CLI app quickly and easily.
|
|
15
17
|
DESCRIPTION
|
|
16
18
|
|
|
17
19
|
spec.author = 'Mike Blumtritt'
|
|
@@ -25,9 +27,8 @@ Gem::Specification.new do |spec|
|
|
|
25
27
|
|
|
26
28
|
spec.required_ruby_version = '> 3.0'
|
|
27
29
|
|
|
28
|
-
spec.files = Dir['lib/**/*.rb']
|
|
29
|
-
spec.files
|
|
30
|
-
spec.files += %w[terminal_rb.gemspec .yardopts]
|
|
30
|
+
spec.files = Dir['lib/**/*.rb'] + Dir['examples/*.rb']
|
|
31
|
+
spec.files << 'terminal_rb.gemspec' << '.yardopts'
|
|
31
32
|
spec.executables = %w[bbcode]
|
|
32
33
|
spec.extra_rdoc_files = %w[README.md]
|
|
33
34
|
end
|
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: 0.
|
|
4
|
+
version: 0.16.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mike Blumtritt
|
|
@@ -10,8 +10,9 @@ cert_chain: []
|
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies: []
|
|
12
12
|
description: 'Terminal.rb supports you with input and output on your terminal. Simple
|
|
13
|
-
BBCode-like markup for attributes and coloring, word-wise line breaks,
|
|
14
|
-
|
|
13
|
+
BBCode-like markup for attributes and coloring, word-wise line breaks, correct special
|
|
14
|
+
key recognition and mouse event reporting enable you to implement your CLI app quickly
|
|
15
|
+
and easily. '
|
|
15
16
|
executables:
|
|
16
17
|
- bbcode
|
|
17
18
|
extensions: []
|
|
@@ -68,5 +69,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
68
69
|
requirements: []
|
|
69
70
|
rubygems_version: 3.7.2
|
|
70
71
|
specification_version: 4
|
|
71
|
-
summary: Fast terminal access with ANSI, CSIu, BBCode, word-wise line
|
|
72
|
+
summary: Fast terminal access with ANSI, CSIu, mouse eventing, BBCode, word-wise line
|
|
73
|
+
break support and much more.
|
|
72
74
|
test_files: []
|