terminal_rb 0.9.1 → 0.9.4
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 +4 -4
- data/examples/info.rb +1 -0
- data/examples/key-codes.rb +2 -2
- data/lib/terminal/ansi.rb +88 -14
- data/lib/terminal/input/csiu_mode.rb +117 -0
- data/lib/terminal/input/legacy_mode.rb +122 -0
- data/lib/terminal/input.rb +55 -159
- data/lib/terminal/rspec/helper.rb +1 -0
- data/lib/terminal/text.rb +254 -239
- data/lib/terminal/version.rb +1 -1
- data/lib/terminal.rb +8 -3
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c9409becbf807905a0659436c9e0f5c3644208006e23c038dbf43a621410c7c7
|
4
|
+
data.tar.gz: 7a583438aa3b66faa19a9bbd035ad26d2c74f889b923e2f410d6468291006495
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3aa083acc102175661145c924b62d3cee717a9453bb73abc6415800d70193ef957abe998732592d1171244a2393767141254891db15a73b96ff2d76bca5c38d8
|
7
|
+
data.tar.gz: 3375ed736835332ed58078ca090922f483a81b3c7a4e6d10b87c9abb64fc6260a67649c5e4a06147e0fa89bea44d623f6613f803e24f02c171830647864a88f2
|
data/README.md
CHANGED
@@ -4,7 +4,7 @@ Terminal access with support for ANSI control codes and [BBCode-like](https://en
|
|
4
4
|
|
5
5
|
- Gem: [rubygems.org](https://rubygems.org/gems/terminal_rb)
|
6
6
|
- Source: [codeberg.org](https://codeberg.org/mblumtritt/Terminal.rb)
|
7
|
-
- Help: [rubydoc.info](https://rubydoc.info/gems/terminal_rb/
|
7
|
+
- Help: [rubydoc.info](https://rubydoc.info/gems/terminal_rb/Terminal)
|
8
8
|
|
9
9
|
## Features
|
10
10
|
|
@@ -14,8 +14,8 @@ Terminal access with support for ANSI control codes and [BBCode-like](https://en
|
|
14
14
|
- [NO_COLOR convention](https://no-color.org) support
|
15
15
|
- markup of text attributes and colors using a [BBCode](https://en.wikipedia.org/wiki/BBCode)-like syntax
|
16
16
|
- calculation for correct display width of strings containing Unicdode chars inclusive emojis
|
17
|
-
- word-
|
18
|
-
-
|
17
|
+
- generation of word-by-word wrapped text
|
18
|
+
- supports [CSIu protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol)
|
19
19
|
|
20
20
|
## Examples
|
21
21
|
|
@@ -43,7 +43,7 @@ Terminal::Text.width('ライブラリは中国語、日本語、韓国語のテ
|
|
43
43
|
# => 64
|
44
44
|
```
|
45
45
|
|
46
|
-
### Split text word-
|
46
|
+
### Split text word-by-word and limit line width to 30 chars
|
47
47
|
|
48
48
|
```ruby
|
49
49
|
Terminal::Text.each_line(<<~TEXT, limit: 30).to_a
|
data/examples/info.rb
CHANGED
@@ -11,5 +11,6 @@ Terminal.puts <<~TEXT
|
|
11
11
|
Supported Colors: [b]#{Terminal.colors}#{Terminal::Ansi.rainbow(' (truecolor)') if Terminal.true_color?}[/]
|
12
12
|
Terminal Size: [b]#{Terminal.size.join(' x ')}[/]
|
13
13
|
Cursor Position: [b]#{Terminal.pos&.join(', ')}[/]
|
14
|
+
Input Mode: [b]#{Terminal.input_mode}[/]
|
14
15
|
|
15
16
|
TEXT
|
data/examples/key-codes.rb
CHANGED
@@ -6,7 +6,7 @@ Terminal.puts <<~TEXT
|
|
6
6
|
|
7
7
|
✅ [b bright_green]Terminal.rb[/b] — Key Codes:[/]
|
8
8
|
Press any key to display it's control code and name.
|
9
|
-
[bright_black](
|
9
|
+
[bright_black]([b]#{Terminal.input_mode}[/b] mode - exit with ESC)[/fg]
|
10
10
|
|
11
11
|
TEXT
|
12
12
|
|
@@ -18,5 +18,5 @@ while true
|
|
18
18
|
Terminal.puts(
|
19
19
|
" [blue]: [yellow]#{raw.inspect} [bold bright_green]#{name}[/]"
|
20
20
|
)
|
21
|
-
break puts if name == '
|
21
|
+
break puts if name == 'Esc'
|
22
22
|
end
|
data/lib/terminal/ansi.rb
CHANGED
@@ -482,6 +482,65 @@ module Terminal
|
|
482
482
|
# @return (see cursor_up)
|
483
483
|
def link(url, text) = "\e]8;;#{url}\a#{text}\e]8;;\a"
|
484
484
|
|
485
|
+
# Create scaled text.
|
486
|
+
# It uses the
|
487
|
+
# [text sizing protocol](https://sw.kovidgoyal.net/kitty/text-sizing-protocol).
|
488
|
+
# This is not widely supported.
|
489
|
+
#
|
490
|
+
# @example Double-height Greeting
|
491
|
+
# Terminal::Ansi.scale('Hello Ruby!', scale: 2)
|
492
|
+
#
|
493
|
+
# @example Half-height Greeting
|
494
|
+
# Terminal::Ansi.scale('Hello Ruby!', fracn: 1, fracd: 2, vertical: :centered)
|
495
|
+
#
|
496
|
+
# @param [#to_s] text text to scale
|
497
|
+
# @param [Integer, nil] scale
|
498
|
+
# overall scale size, range 1..7
|
499
|
+
# @param [Integer, nil] width
|
500
|
+
# with in cells, range 0..7
|
501
|
+
# @param [Integer, nil] fracn
|
502
|
+
# numerator for the fractional scale, range: 0..15
|
503
|
+
# @param [Integer, nil] fracd
|
504
|
+
# denominator for the fractional scale, range: 0..15, > fracn
|
505
|
+
# @param [:top, :bottom, :centered, nil] vertical
|
506
|
+
# vertical alignment to use for fractionally scaled text
|
507
|
+
# @param [:left, :right, :centered, nil] horizontal
|
508
|
+
# horizontal alignment to use for fractionally scaled text
|
509
|
+
# @return (see cursor_up)
|
510
|
+
def scale(
|
511
|
+
text,
|
512
|
+
scale: nil,
|
513
|
+
width: nil,
|
514
|
+
fracn: nil,
|
515
|
+
fracd: nil,
|
516
|
+
vertical: nil,
|
517
|
+
horizontal: nil
|
518
|
+
)
|
519
|
+
opts = scale ? ["s=#{scale.clamp(1, 7)}"] : []
|
520
|
+
opts << "w=#{width.clamp(0, 7)}" if width
|
521
|
+
if fracn
|
522
|
+
opts << "n=#{fracn = fracn.clamp(0, 15)}"
|
523
|
+
opts << "d=#{fracd.clamp(fracn + 1, 15)}" if fracd
|
524
|
+
case vertical
|
525
|
+
when 0, :top
|
526
|
+
opts << 'v=0'
|
527
|
+
when 1, :bottom
|
528
|
+
opts << 'v=1'
|
529
|
+
when 2, :centered, :center
|
530
|
+
opts << 'v=2'
|
531
|
+
end
|
532
|
+
case horizontal
|
533
|
+
when 0, :left
|
534
|
+
opts << 'h=0'
|
535
|
+
when 1, :right
|
536
|
+
opts << 'h=1'
|
537
|
+
when 2, :centered, :center
|
538
|
+
opts << 'h=2'
|
539
|
+
end
|
540
|
+
end
|
541
|
+
"\e]66;#{opts.join(':')};#{text}\a"
|
542
|
+
end
|
543
|
+
|
485
544
|
#
|
486
545
|
# @!endgroup
|
487
546
|
#
|
@@ -559,13 +618,13 @@ module Terminal
|
|
559
618
|
# @!visibility private
|
560
619
|
CURSOR_HIDE = "\e[?25l"
|
561
620
|
|
562
|
-
# CURSOR_POS_SAVE_SCO = "\e[s"
|
563
|
-
# CURSOR_POS_SAVE_DEC = "\e7"
|
621
|
+
# @comment CURSOR_POS_SAVE_SCO = "\e[s"
|
622
|
+
# @comment CURSOR_POS_SAVE_DEC = "\e7"
|
564
623
|
# @!visibility private
|
565
624
|
CURSOR_POS_SAVE = "\e7"
|
566
625
|
|
567
|
-
# CURSOR_POS_RESTORE_SCO = "\e[u"
|
568
|
-
# CURSOR_POS_RESTORE_DEC = "\e8"
|
626
|
+
# @comment CURSOR_POS_RESTORE_SCO = "\e[u"
|
627
|
+
# @comment CURSOR_POS_RESTORE_DEC = "\e8"
|
569
628
|
# @!visibility private
|
570
629
|
CURSOR_POS_RESTORE = "\e8"
|
571
630
|
|
@@ -582,11 +641,18 @@ module Terminal
|
|
582
641
|
SCREEN_SAVE = "\e[?47h"
|
583
642
|
# @!visibility private
|
584
643
|
SCREEN_RESTORE = "\e[?47l"
|
644
|
+
|
585
645
|
# @!visibility private
|
586
|
-
|
646
|
+
# @comment at least Kitty requires CURSOR_HOME too
|
647
|
+
SCREEN_ALTERNATE = "\e[?1049h#{CURSOR_HOME}".freeze
|
587
648
|
# @!visibility private
|
588
649
|
SCREEN_ALTERNATE_OFF = "\e[?1049l"
|
589
650
|
|
651
|
+
# @!visibility private
|
652
|
+
SCREEN_REVERSE_MODE_ON = "\e[?5h"
|
653
|
+
# @!visibility private
|
654
|
+
SCREEN_REVERSE_MODE_OFF = "\e[?5l"
|
655
|
+
|
590
656
|
# @!visibility private
|
591
657
|
LINE_ERASE = line_erase.freeze
|
592
658
|
# @!visibility private
|
@@ -597,14 +663,22 @@ module Terminal
|
|
597
663
|
LINE_ERASE_PREV = "#{cursor_prev_line(nil)}#{LINE_ERASE}".freeze
|
598
664
|
|
599
665
|
# @comment seems not widely supported:
|
600
|
-
#
|
601
|
-
#
|
602
|
-
#
|
603
|
-
#
|
604
|
-
#
|
605
|
-
#
|
606
|
-
#
|
607
|
-
#
|
608
|
-
#
|
666
|
+
# doubled!? def cursor_column(column = 1) = "\e[#{column}`"
|
667
|
+
# doubled!? def cursor_row(row = 1) = "\e[#{row}d"
|
668
|
+
# def cursor_column_rel(columns = 1) = "\e[#{columns}a"
|
669
|
+
# def cursor_row_rel(rows = 1) = "\e[#{rows}e"
|
670
|
+
# def cursor_tab(count = 1) = "\e[#{column}I"
|
671
|
+
# def cursor_reverse_tab(count = 1) = "\e[#{count}Z"
|
672
|
+
# def chars_delete(count = 1) = "\e[#{count}P"
|
673
|
+
# def chars_erase(count = 1) = "\e[#{count}X"
|
674
|
+
# def notify(title) = "\e]9;#{title}\a"
|
675
|
+
|
676
|
+
# @comment TODO:
|
677
|
+
# https://sw.kovidgoyal.net/kitty/desktop-notifications
|
678
|
+
# https://sw.kovidgoyal.net/kitty/pointer-shapes
|
679
|
+
# https://sw.kovidgoyal.net/kitty/unscroll
|
680
|
+
# https://sw.kovidgoyal.net/kitty/color-stack
|
681
|
+
# https://sw.kovidgoyal.net/kitty/deccara
|
682
|
+
# https://sw.kovidgoyal.net/kitty/clipboard
|
609
683
|
end
|
610
684
|
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Terminal
|
4
|
+
module CSIUMode
|
5
|
+
class << self
|
6
|
+
def type = :csi_u
|
7
|
+
|
8
|
+
def key_name(key)
|
9
|
+
return unless key
|
10
|
+
return ORD[key.ord] if key.size == 1
|
11
|
+
return if key[0] != "\e"
|
12
|
+
return C0_LEGACY[key[1].ord] if key.size == 2 # ESC ?
|
13
|
+
return csi(key) if key[1] == '['
|
14
|
+
SS3[key[2].ord] if key[1] == 'O' && key.size == 3 # ESC O ?
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def csi(key)
|
20
|
+
type = key[-1]
|
21
|
+
return unicode(key) if type == 'u' # ESC [ ? u
|
22
|
+
return legacy(key) if type == '~' # ESC [ ? ; <modifier> ~
|
23
|
+
if key.size == 3 # ESC [ ?
|
24
|
+
ord = type.ord
|
25
|
+
return ord == 0x5a ? 'Shift+Tab' : SS3[ord]
|
26
|
+
end
|
27
|
+
return if key.size < 5
|
28
|
+
return unless key.start_with?("\e[1;")
|
29
|
+
# ESC [ 1 ; <modifier> ?
|
30
|
+
name = SS3[type.ord] or return
|
31
|
+
mod = key[4..-2]
|
32
|
+
mod.empty? ? name : modifier(name, mod.to_i - 1)
|
33
|
+
end
|
34
|
+
|
35
|
+
def unicode(key)
|
36
|
+
with_modifier(key) { ORD[_1] || _1.chr(Encoding::UTF_8) }
|
37
|
+
end
|
38
|
+
|
39
|
+
def legacy(key)
|
40
|
+
with_modifier(key) { _1 == 13 ? 'F3' : ORD[_1] }
|
41
|
+
end
|
42
|
+
|
43
|
+
def with_modifier(key)
|
44
|
+
code, mod = key[2..-2].split(';', 2)
|
45
|
+
code = yield(code.to_i) or return
|
46
|
+
mod ? modifier(code, mod.to_i - 1) : code
|
47
|
+
end
|
48
|
+
|
49
|
+
def modifier(name, mod)
|
50
|
+
(MODS.filter_map { |b, n| n if mod.allbits?(b) } << name).join('+')
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
ORD = {
|
55
|
+
0x02 => 'Ins',
|
56
|
+
0x03 => 'Del',
|
57
|
+
0x05 => 'PageUp',
|
58
|
+
0x06 => 'PageDown',
|
59
|
+
0x07 => 'Home',
|
60
|
+
0x08 => 'End',
|
61
|
+
0x09 => 'Tab',
|
62
|
+
0x0b => 'F1',
|
63
|
+
0x0c => 'F2',
|
64
|
+
0x0d => 'Enter',
|
65
|
+
0x0e => 'F4',
|
66
|
+
0x0f => 'F5',
|
67
|
+
0x11 => 'F6',
|
68
|
+
0x12 => 'F7',
|
69
|
+
0x13 => 'F8',
|
70
|
+
0x14 => 'F9',
|
71
|
+
0x15 => 'F10',
|
72
|
+
0x17 => 'F11',
|
73
|
+
0x18 => 'F12',
|
74
|
+
0x1b => 'Esc',
|
75
|
+
0x7f => 'Back'
|
76
|
+
}.compare_by_identity.freeze
|
77
|
+
|
78
|
+
# ESC ?
|
79
|
+
C0_LEGACY = {
|
80
|
+
0x00 => 'Ctrl+Space',
|
81
|
+
0x08 => 'Ctrl+Back',
|
82
|
+
0x09 => 'Alt+Tab',
|
83
|
+
0x0d => 'Alt+Enter',
|
84
|
+
0x1b => 'Alt+Esc',
|
85
|
+
0x20 => 'Alt+Space',
|
86
|
+
0x64 => 'Alt+Del',
|
87
|
+
0x7f => 'Alt+Back'
|
88
|
+
}.compare_by_identity.freeze
|
89
|
+
|
90
|
+
# ESC [ ?
|
91
|
+
# ESC [ O ?
|
92
|
+
SS3 = {
|
93
|
+
0x41 => 'Up', # A
|
94
|
+
0x42 => 'Down', # B
|
95
|
+
0x43 => 'Right', # C
|
96
|
+
0x44 => 'Left', # D
|
97
|
+
0x46 => 'End', # F
|
98
|
+
0x48 => 'Home', # H
|
99
|
+
0x50 => 'F1', # P
|
100
|
+
0x51 => 'F2', # Q
|
101
|
+
0x52 => 'F3', # R
|
102
|
+
0x53 => 'F4' # S
|
103
|
+
}.compare_by_identity.freeze
|
104
|
+
|
105
|
+
MODS = {
|
106
|
+
1 => 'Shift',
|
107
|
+
2 => 'Alt',
|
108
|
+
4 => 'Ctrl',
|
109
|
+
8 => 'Super',
|
110
|
+
16 => 'Hyper',
|
111
|
+
32 => 'Meta',
|
112
|
+
64 => 'Caps',
|
113
|
+
128 => 'Num'
|
114
|
+
}.freeze
|
115
|
+
end
|
116
|
+
private_constant :CSIUMode
|
117
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Terminal
|
4
|
+
module LegacyMode
|
5
|
+
class << self
|
6
|
+
def type = :legacy
|
7
|
+
|
8
|
+
def key_name(key)
|
9
|
+
return unless key
|
10
|
+
return ORD[key.ord] if key.size == 1
|
11
|
+
return if key[0] != "\e"
|
12
|
+
return csi(key) if key[1] == '['
|
13
|
+
SS3[key[2].ord] if key[1] == 'O' && key.size == 3 # ESC O ?
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def csi(key)
|
19
|
+
return legacy(key) if key[-1] == '~' # ESC [ ? ; <modifier> ~
|
20
|
+
if key.size == 3 # ESC [ ?
|
21
|
+
ord = key[2].ord
|
22
|
+
return ord == 0x5a ? 'Shift+Tab' : SS3[ord]
|
23
|
+
end
|
24
|
+
# ESC [ 1 ; <modifier> ?
|
25
|
+
return if key.size < 5
|
26
|
+
return unless key.start_with?("\e[1;")
|
27
|
+
# ESC [ ? {ABCDEFHPQRS}
|
28
|
+
name = SS3[key[-1].ord] or return
|
29
|
+
mod = key[4..-2]
|
30
|
+
mod.empty? ? name : modifier(name, mod.to_i - 1)
|
31
|
+
end
|
32
|
+
|
33
|
+
def legacy(key)
|
34
|
+
with_modifier(key) { LEGACY[_1] }
|
35
|
+
end
|
36
|
+
|
37
|
+
def with_modifier(key)
|
38
|
+
code, mod = key[2..-2].split(';', 2)
|
39
|
+
code = yield(code.to_i) or return
|
40
|
+
mod ? modifier(code, mod.to_i - 1) : code
|
41
|
+
end
|
42
|
+
|
43
|
+
def modifier(name, mod)
|
44
|
+
(MODS.filter_map { |b, n| n if mod.allbits?(b) } << name).join('+')
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
ORD = {
|
49
|
+
0x00 => 'Ctrl+2',
|
50
|
+
0x01 => 'Ctrl+a',
|
51
|
+
0x02 => 'Ctrl+b',
|
52
|
+
0x03 => 'Ctrl+c',
|
53
|
+
0x04 => 'Del',
|
54
|
+
0x05 => 'Ctrl+e',
|
55
|
+
0x06 => 'Ctrl+f',
|
56
|
+
0x07 => 'Ctrl+g',
|
57
|
+
0x08 => 'Ctrl+h',
|
58
|
+
0x09 => 'Tab',
|
59
|
+
0x0a => 'Ctrl+j',
|
60
|
+
0x0b => 'Ctrl+k',
|
61
|
+
0x0c => 'Ctrl+l',
|
62
|
+
0x0d => 'Enter',
|
63
|
+
0x0e => 'Ctrl+n',
|
64
|
+
0x0f => 'Ctrl+o',
|
65
|
+
0x10 => 'Ctrl+p',
|
66
|
+
0x11 => 'Ctrl+q',
|
67
|
+
0x12 => 'Ctrl+r',
|
68
|
+
0x13 => 'Ctrl+s',
|
69
|
+
0x14 => 'Ctrl+t',
|
70
|
+
0x15 => 'Ctrl+u',
|
71
|
+
0x16 => 'Ctrl+v',
|
72
|
+
0x17 => 'Ctrl+w',
|
73
|
+
0x18 => 'Ctrl+x',
|
74
|
+
0x19 => 'Ctrl+y',
|
75
|
+
0x1a => 'Ctrl+z',
|
76
|
+
0x1d => 'Ctrl+ü', # FIND AM
|
77
|
+
0x1e => 'Ctrl+^', # FIND AM
|
78
|
+
0x1f => 'Ctrl+-',
|
79
|
+
0x1b => 'Esc',
|
80
|
+
0x7f => 'Back'
|
81
|
+
}.compare_by_identity.freeze
|
82
|
+
|
83
|
+
# ESC [ ?
|
84
|
+
# ESC [ O ?
|
85
|
+
SS3 = {
|
86
|
+
0x41 => 'Up', # A
|
87
|
+
0x42 => 'Down', # B
|
88
|
+
0x43 => 'Right', # C
|
89
|
+
0x44 => 'Left', # D
|
90
|
+
0x46 => 'End', # F
|
91
|
+
0x48 => 'Home', # H
|
92
|
+
0x50 => 'F1', # P
|
93
|
+
0x51 => 'F2', # Q
|
94
|
+
0x52 => 'F3', # R
|
95
|
+
0x53 => 'F4' # S
|
96
|
+
}.compare_by_identity.freeze
|
97
|
+
|
98
|
+
LEGACY = {
|
99
|
+
0x03 => 'Del',
|
100
|
+
0x0f => 'F5',
|
101
|
+
0x11 => 'F6',
|
102
|
+
0x12 => 'F7',
|
103
|
+
0x13 => 'F8',
|
104
|
+
0x14 => 'F9',
|
105
|
+
0x15 => 'F10',
|
106
|
+
0x17 => 'F11',
|
107
|
+
0x18 => 'F12'
|
108
|
+
}.compare_by_identity.freeze
|
109
|
+
|
110
|
+
MODS = {
|
111
|
+
1 => 'Shift',
|
112
|
+
2 => 'Alt',
|
113
|
+
4 => 'Ctrl',
|
114
|
+
8 => 'Super',
|
115
|
+
16 => 'Hyper',
|
116
|
+
32 => 'Meta',
|
117
|
+
64 => 'Caps',
|
118
|
+
128 => 'Num'
|
119
|
+
}.freeze
|
120
|
+
end
|
121
|
+
private_constant :LegacyMode
|
122
|
+
end
|
data/lib/terminal/input.rb
CHANGED
@@ -4,9 +4,22 @@ require 'io/console'
|
|
4
4
|
|
5
5
|
module Terminal
|
6
6
|
class << self
|
7
|
+
# Supported input mode.
|
8
|
+
#
|
9
|
+
# @attribute [r] input_mode
|
10
|
+
# @return [:csi_u] when CSIu protocol support
|
11
|
+
# @return [:legacy] for standard terminal
|
12
|
+
# @return [:dumb] for non-interactive input (pipes etc.)
|
13
|
+
# @return [:error] when input device is closed
|
14
|
+
def input_mode
|
15
|
+
return @inp_mode.type if (@inp ||= find_inp) == READ_TTY_INP
|
16
|
+
return :dumb if @inp == READ_DUMB_INP
|
17
|
+
:error
|
18
|
+
end
|
19
|
+
|
7
20
|
# Read next keyboard input.
|
8
21
|
#
|
9
|
-
# The input will be returned as named key codes like "Ctrl+
|
22
|
+
# The input will be returned as named key codes like "Ctrl+c" by default.
|
10
23
|
# This can be changed by `mode`.
|
11
24
|
#
|
12
25
|
# @param [:named, :raw, :both] mode modifies the result
|
@@ -14,175 +27,58 @@ module Terminal
|
|
14
27
|
# @return [String] key name in `:named` mode
|
15
28
|
# @return [[String, String]] key code and key name in `:both` mode
|
16
29
|
def read_key(mode: :named)
|
17
|
-
key =
|
30
|
+
key = (@inp ||= find_inp).call or return
|
18
31
|
return key if mode == :raw
|
19
|
-
|
32
|
+
return key, @inp_mode.key_name(key) if mode == :both
|
33
|
+
@inp_mode.key_name(key) || key
|
20
34
|
end
|
21
35
|
|
22
36
|
private
|
23
37
|
|
24
|
-
def
|
25
|
-
return unless
|
26
|
-
return
|
27
|
-
|
28
|
-
|
29
|
-
|
38
|
+
def find_inp
|
39
|
+
return READ_DUMB_INP unless STDIN.tty?
|
40
|
+
return READ_TTY_INP if !ansi? || _write("\e[>1u\e[?u\e[c").nil?
|
41
|
+
while true
|
42
|
+
result = READ_TTY_INP.call
|
43
|
+
if result.include?("\e[?1u")
|
44
|
+
@inp_mode = CSIUMode
|
45
|
+
at_exit { _write("\e[<u") } if ENV['ENV'] != 'test'
|
46
|
+
end
|
47
|
+
return READ_TTY_INP if result[-1] == 'c'
|
30
48
|
end
|
31
|
-
key
|
32
|
-
rescue Interrupt
|
33
|
-
key
|
34
49
|
rescue IOError, SystemCallError
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
def _key_name(key)
|
39
|
-
return unless key
|
40
|
-
return KEY_MAP[key]&.dup if key.size != 1
|
41
|
-
KEY_MAP[key]&.dup if (ord = key.ord) == 127 || (ord > 0 && ord < 28)
|
50
|
+
-> {}
|
51
|
+
ensure
|
52
|
+
@inp_mode ||= LegacyMode
|
42
53
|
end
|
43
54
|
end
|
44
55
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
end
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
def self.add_keys(keys)
|
59
|
-
keys.each_pair { |name, code| @map["\e[#{code}"] = name }
|
60
|
-
add_modifiers(keys)
|
61
|
-
end
|
62
|
-
|
63
|
-
def self.add_fkeys(keys)
|
64
|
-
keys.each_pair { |name, code| @map["\e[#{code}~"] = name }
|
65
|
-
@mods.each_pair do |mod, prefix|
|
66
|
-
keys.each_pair do |name, code|
|
67
|
-
@map["\e[#{code};#{mod}~"] = "#{prefix}+#{name}"
|
68
|
-
end
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
|
-
def self.to_hash
|
73
|
-
num = 0
|
74
|
-
@map = ('A'..'Z').to_h { [(num += 1).chr, "Ctrl+#{_1}"] }
|
75
|
-
|
76
|
-
add_keys(
|
77
|
-
'F1' => 'P',
|
78
|
-
'F2' => 'Q',
|
79
|
-
'F3' => 'R',
|
80
|
-
'F4' => 'S',
|
81
|
-
'Up' => 'A',
|
82
|
-
'Down' => 'B',
|
83
|
-
'Right' => 'C',
|
84
|
-
'Left' => 'D',
|
85
|
-
'End' => 'F',
|
86
|
-
'Home' => 'H'
|
87
|
-
)
|
88
|
-
|
89
|
-
add_fkeys(
|
90
|
-
'Del' => '3',
|
91
|
-
'PgUp' => '5',
|
92
|
-
'PgDown' => '6',
|
93
|
-
# -
|
94
|
-
'F1' => 'F1',
|
95
|
-
'F2' => 'F2',
|
96
|
-
'F3' => 'F3',
|
97
|
-
'F4' => 'F4',
|
98
|
-
# -
|
99
|
-
'F5' => '15',
|
100
|
-
'F6' => '17',
|
101
|
-
'F7' => '18',
|
102
|
-
'F8' => '19',
|
103
|
-
'F9' => '20',
|
104
|
-
'F10' => '21',
|
105
|
-
'F11' => '23',
|
106
|
-
'F12' => '24',
|
107
|
-
'F13' => '25',
|
108
|
-
'F14' => '26',
|
109
|
-
'F15' => '28',
|
110
|
-
'F16' => '29',
|
111
|
-
'F17' => '31',
|
112
|
-
'F18' => '32',
|
113
|
-
'F19' => '33',
|
114
|
-
'F20' => '34'
|
115
|
-
)
|
116
|
-
|
117
|
-
add_fkeys('F3' => '13') # Kitty
|
118
|
-
|
119
|
-
# Kitty:
|
120
|
-
@mods.each_pair do |mod, pref|
|
121
|
-
('a'..'z').each do |char|
|
122
|
-
@map["\e[#{char.ord};#{mod}u"] = "#{pref}+#{char.upcase}"
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
{
|
127
|
-
'Esc' => "\e",
|
128
|
-
'Enter' => "\r",
|
129
|
-
'Tab' => "\t",
|
130
|
-
'Back' => "\u007F",
|
131
|
-
'Shift+Tab' => "\e[Z"
|
132
|
-
}.each_pair do |name, code|
|
133
|
-
@map[code] = name
|
134
|
-
@map["\e#{code}"] = "Alt+#{name}" # Kitty
|
135
|
-
end
|
136
|
-
|
137
|
-
# overrides and additional keys
|
138
|
-
@map.merge!(
|
139
|
-
"\b" => 'Ctrl+Back',
|
140
|
-
"\e\b" => 'Ctrl+Alt+Back', # Kitty
|
141
|
-
"\4" => 'Del',
|
142
|
-
"\e[5" => 'PgUp',
|
143
|
-
"\e[6" => 'PgDown',
|
144
|
-
# SS3 control (VT 100 etc)
|
145
|
-
"\eOA" => 'Up',
|
146
|
-
"\eOB" => 'Down',
|
147
|
-
"\eOC" => 'Right',
|
148
|
-
"\eOD" => 'Left',
|
149
|
-
"\eOP" => 'F1',
|
150
|
-
"\eOQ" => 'F2',
|
151
|
-
"\eOR" => 'F3',
|
152
|
-
"\eOS" => 'F4',
|
153
|
-
"\eO2P" => 'Shift+F1',
|
154
|
-
"\eO2Q" => 'Shift+F2',
|
155
|
-
"\eO2R" => 'Shift+F3',
|
156
|
-
"\eO2S" => 'Shift+F4',
|
157
|
-
"\eOt" => 'F5',
|
158
|
-
"\eOu" => 'F6',
|
159
|
-
"\eOv" => 'F7',
|
160
|
-
"\eOw" => 'F8',
|
161
|
-
"\eOl" => 'F9',
|
162
|
-
"\eOx" => 'F10'
|
163
|
-
)
|
164
|
-
end
|
56
|
+
READ_DUMB_INP =
|
57
|
+
lambda do
|
58
|
+
STDIN.getc
|
59
|
+
rescue Interrupt
|
60
|
+
nil
|
61
|
+
rescue IOError, SystemCallError
|
62
|
+
@inp = -> {}
|
63
|
+
nil
|
64
|
+
end
|
165
65
|
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
6 => 'Ctrl-Shift',
|
172
|
-
7 => 'Ctrl-Alt',
|
173
|
-
8 => 'Ctrl-Alt-Shift',
|
174
|
-
9 => 'Meta',
|
175
|
-
10 => 'Meta-Shift',
|
176
|
-
11 => 'Meta-Alt',
|
177
|
-
12 => 'Meta-Alt-Shift',
|
178
|
-
13 => 'Ctrl-Meta',
|
179
|
-
14 => 'Ctrl-Meta-Shift',
|
180
|
-
15 => 'Ctrl-Meta-Alt',
|
181
|
-
16 => 'Ctrl-Meta-Alt-Shift'
|
182
|
-
}.compare_by_identity
|
66
|
+
READ_TTY_INP =
|
67
|
+
lambda do
|
68
|
+
key = STDIN.getch
|
69
|
+
while (nc = STDIN.read_nonblock(1, exception: false))
|
70
|
+
String === nc ? key += nc : break
|
183
71
|
end
|
184
|
-
|
185
|
-
|
72
|
+
key
|
73
|
+
rescue Interrupt
|
74
|
+
key
|
75
|
+
rescue IOError, SystemCallError
|
76
|
+
@inp = READ_DUMB_INP
|
77
|
+
nil
|
78
|
+
end
|
186
79
|
|
187
|
-
|
80
|
+
dir = __dir__
|
81
|
+
autoload :LegacyMode, "#{dir}/input/legacy_mode.rb"
|
82
|
+
autoload :CSIUMode, "#{dir}/input/csiu_mode.rb"
|
83
|
+
private_constant :READ_DUMB_INP, :READ_TTY_INP, :LegacyMode, :CSIUMode
|
188
84
|
end
|
@@ -7,6 +7,7 @@ RSpec.shared_context 'with Terminal' do |mod, ansi: true, winsize: [25, 80]|
|
|
7
7
|
let(:terminal) do
|
8
8
|
load('terminal/ansi/attributes.rb', wrapper_module)
|
9
9
|
load('terminal/ansi.rb', wrapper_module)
|
10
|
+
load('terminal/input/legacy_mode.rb', wrapper_module)
|
10
11
|
load('terminal/input.rb', wrapper_module)
|
11
12
|
load('terminal/detect.rb', wrapper_module)
|
12
13
|
load('terminal.rb', wrapper_module)
|