tty-reader 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.travis.yml +25 -0
- data/CHANGELOG.md +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +21 -0
- data/LICENSE.txt +21 -0
- data/README.md +187 -0
- data/Rakefile +10 -0
- data/appveyor.yml +25 -0
- data/bin/console +6 -0
- data/bin/setup +8 -0
- data/lib/tty-reader.rb +3 -0
- data/lib/tty/reader.rb +348 -0
- data/lib/tty/reader/codes.rb +120 -0
- data/lib/tty/reader/console.rb +56 -0
- data/lib/tty/reader/history.rb +144 -0
- data/lib/tty/reader/key_event.rb +90 -0
- data/lib/tty/reader/line.rb +161 -0
- data/lib/tty/reader/mode.rb +43 -0
- data/lib/tty/reader/version.rb +7 -0
- data/lib/tty/reader/win_api.rb +54 -0
- data/lib/tty/reader/win_console.rb +90 -0
- data/tasks/console.rake +11 -0
- data/tasks/coverage.rake +11 -0
- data/tasks/spec.rake +29 -0
- data/tty-reader.gemspec +28 -0
- metadata +137 -0
@@ -0,0 +1,120 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module TTY
|
5
|
+
class Reader
|
6
|
+
module Codes
|
7
|
+
def ctrl_keys
|
8
|
+
{
|
9
|
+
ctrl_a: ?\C-a,
|
10
|
+
ctrl_b: ?\C-b,
|
11
|
+
ctrl_c: ?\C-c,
|
12
|
+
ctrl_d: ?\C-d,
|
13
|
+
ctrl_e: ?\C-e,
|
14
|
+
ctrl_f: ?\C-f,
|
15
|
+
ctrl_g: ?\C-g,
|
16
|
+
ctrl_h: ?\C-h,
|
17
|
+
ctrl_i: ?\C-i,
|
18
|
+
ctrl_j: ?\C-j,
|
19
|
+
ctrl_k: ?\C-k,
|
20
|
+
ctrl_l: ?\C-l,
|
21
|
+
ctrl_m: ?\C-m,
|
22
|
+
ctrl_n: ?\C-n,
|
23
|
+
ctrl_o: ?\C-o,
|
24
|
+
ctrl_p: ?\C-p,
|
25
|
+
ctrl_q: ?\C-q,
|
26
|
+
ctrl_r: ?\C-r,
|
27
|
+
ctrl_s: ?\C-s,
|
28
|
+
ctrl_t: ?\C-t,
|
29
|
+
ctrl_u: ?\C-u,
|
30
|
+
ctrl_v: ?\C-v,
|
31
|
+
ctrl_w: ?\C-w,
|
32
|
+
ctrl_x: ?\C-x,
|
33
|
+
ctrl_y: ?\C-y,
|
34
|
+
ctrl_z: ?\C-z
|
35
|
+
}
|
36
|
+
end
|
37
|
+
module_function :ctrl_keys
|
38
|
+
|
39
|
+
def keys
|
40
|
+
{
|
41
|
+
tab: "\t",
|
42
|
+
enter: "\n",
|
43
|
+
return: "\r",
|
44
|
+
escape: "\e",
|
45
|
+
space: " ",
|
46
|
+
backspace: ?\C-?,
|
47
|
+
home: "\e[1~",
|
48
|
+
insert: "\e[2~",
|
49
|
+
delete: "\e[3~",
|
50
|
+
end: "\e[4~",
|
51
|
+
page_up: "\e[5~",
|
52
|
+
page_down: "\e[6~",
|
53
|
+
|
54
|
+
up: "\e[A",
|
55
|
+
down: "\e[B",
|
56
|
+
right: "\e[C",
|
57
|
+
left: "\e[D",
|
58
|
+
clear: "\e[E",
|
59
|
+
|
60
|
+
f1_xterm: "\eOP",
|
61
|
+
f2_xterm: "\eOQ",
|
62
|
+
f3_xterm: "\eOR",
|
63
|
+
f4_xterm: "\eOS",
|
64
|
+
|
65
|
+
f1: "\e[11~",
|
66
|
+
f2: "\e[12~",
|
67
|
+
f3: "\e[13~",
|
68
|
+
f4: "\e[14~",
|
69
|
+
f5: "\e[15~",
|
70
|
+
f6: "\e[17~",
|
71
|
+
f7: "\e[18~",
|
72
|
+
f8: "\e[19~",
|
73
|
+
f9: "\e[20~",
|
74
|
+
f10: "\e[21~",
|
75
|
+
f11: "\e[23~",
|
76
|
+
f12: "\e[24~"
|
77
|
+
}.merge(ctrl_keys)
|
78
|
+
end
|
79
|
+
module_function :keys
|
80
|
+
|
81
|
+
def win_keys
|
82
|
+
{
|
83
|
+
tab: "\t",
|
84
|
+
enter: "\r",
|
85
|
+
return: "\r",
|
86
|
+
escape: "\e",
|
87
|
+
space: " ",
|
88
|
+
backspace: "\b",
|
89
|
+
home: [224, 71].pack('U*'),
|
90
|
+
end: [224, 79].pack('U*'),
|
91
|
+
insert: [224, 82].pack('U*'),
|
92
|
+
delete: [224, 83].pack('U*'),
|
93
|
+
page_up: [224, 73].pack('U*'),
|
94
|
+
page_down: [224, 81].pack('U*'),
|
95
|
+
|
96
|
+
up: [224, 72].pack('U*'),
|
97
|
+
down: [224, 80].pack('U*'),
|
98
|
+
right: [224, 77].pack('U*'),
|
99
|
+
left: [224, 75].pack('U*'),
|
100
|
+
clear: [224, 83].pack('U*'),
|
101
|
+
|
102
|
+
f1: "\x00;",
|
103
|
+
f2: "\x00<",
|
104
|
+
f3: "\x00",
|
105
|
+
f4: "\x00=",
|
106
|
+
f5: "\x00?",
|
107
|
+
f6: "\x00@",
|
108
|
+
f7: "\x00A",
|
109
|
+
f8: "\x00B",
|
110
|
+
f9: "\x00C",
|
111
|
+
f10: "\x00D",
|
112
|
+
f11: "\x00\x85",
|
113
|
+
f12: "\x00\x86"
|
114
|
+
}.merge(ctrl_keys)
|
115
|
+
end
|
116
|
+
module_function :win_keys
|
117
|
+
|
118
|
+
end # Codes
|
119
|
+
end # Reader
|
120
|
+
end # TTY
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative 'codes'
|
5
|
+
require_relative 'mode'
|
6
|
+
|
7
|
+
module TTY
|
8
|
+
class Reader
|
9
|
+
class Console
|
10
|
+
ESC = "\e".freeze
|
11
|
+
CSI = "\e[".freeze
|
12
|
+
|
13
|
+
# Key codes
|
14
|
+
#
|
15
|
+
# @return [Hash[Symbol]]
|
16
|
+
#
|
17
|
+
# @api public
|
18
|
+
attr_reader :keys
|
19
|
+
|
20
|
+
# Escape codes
|
21
|
+
#
|
22
|
+
# @return [Array[Integer]]
|
23
|
+
#
|
24
|
+
# @api public
|
25
|
+
attr_reader :escape_codes
|
26
|
+
|
27
|
+
def initialize(input)
|
28
|
+
@input = input
|
29
|
+
@mode = Mode.new(input)
|
30
|
+
@keys = Codes.keys
|
31
|
+
@escape_codes = [[ESC.ord], CSI.bytes.to_a]
|
32
|
+
end
|
33
|
+
|
34
|
+
# Get a character from console with echo
|
35
|
+
#
|
36
|
+
# @param [Hash[Symbol]] options
|
37
|
+
# @option options [Symbol] :echo
|
38
|
+
# the echo toggle
|
39
|
+
#
|
40
|
+
# @return [String]
|
41
|
+
#
|
42
|
+
# @api private
|
43
|
+
def get_char(options)
|
44
|
+
mode.raw(options[:raw]) do
|
45
|
+
mode.echo(options[:echo]) { input.getc }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
protected
|
50
|
+
|
51
|
+
attr_reader :mode
|
52
|
+
|
53
|
+
attr_reader :input
|
54
|
+
end # Console
|
55
|
+
end # Reader
|
56
|
+
end # TTY
|
@@ -0,0 +1,144 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'forwardable'
|
5
|
+
|
6
|
+
module TTY
|
7
|
+
class Reader
|
8
|
+
# A class responsible for storing a history of all lines entered by
|
9
|
+
# user when interacting with shell prompt.
|
10
|
+
#
|
11
|
+
# @api private
|
12
|
+
class History
|
13
|
+
include Enumerable
|
14
|
+
extend Forwardable
|
15
|
+
|
16
|
+
# Default maximum size
|
17
|
+
DEFAULT_SIZE = 32 << 4
|
18
|
+
|
19
|
+
def_delegators :@history, :size, :length, :to_s, :inspect
|
20
|
+
|
21
|
+
# Set and retrieve the maximum size of the buffer
|
22
|
+
attr_accessor :max_size
|
23
|
+
|
24
|
+
attr_reader :index
|
25
|
+
|
26
|
+
attr_accessor :cycle
|
27
|
+
|
28
|
+
attr_accessor :duplicates
|
29
|
+
|
30
|
+
attr_accessor :exclude
|
31
|
+
|
32
|
+
# Create a History buffer
|
33
|
+
#
|
34
|
+
# param [Integer] max_size
|
35
|
+
# the maximum size for history buffer
|
36
|
+
#
|
37
|
+
# param [Hash[Symbol]] options
|
38
|
+
# @option options [Boolean] :duplicates
|
39
|
+
# whether or not to store duplicates, true by default
|
40
|
+
#
|
41
|
+
# @api public
|
42
|
+
def initialize(max_size = DEFAULT_SIZE, options = {})
|
43
|
+
@max_size = max_size
|
44
|
+
@index = 0
|
45
|
+
@history = []
|
46
|
+
@duplicates = options.fetch(:duplicates) { true }
|
47
|
+
@exclude = options.fetch(:exclude) { proc {} }
|
48
|
+
@cycle = options.fetch(:cycle) { false }
|
49
|
+
yield self if block_given?
|
50
|
+
end
|
51
|
+
|
52
|
+
# Iterates over history lines
|
53
|
+
#
|
54
|
+
# @api public
|
55
|
+
def each
|
56
|
+
if block_given?
|
57
|
+
@history.each { |line| yield line }
|
58
|
+
else
|
59
|
+
@history.to_enum
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Add the last typed line to history buffer
|
64
|
+
#
|
65
|
+
# @param [String] line
|
66
|
+
#
|
67
|
+
# @api public
|
68
|
+
def push(line)
|
69
|
+
@history.delete(line) unless @duplicates
|
70
|
+
return if line.to_s.empty? || @exclude[line]
|
71
|
+
|
72
|
+
@history.shift if size >= max_size
|
73
|
+
@history << line
|
74
|
+
@index = @history.size - 1
|
75
|
+
|
76
|
+
self
|
77
|
+
end
|
78
|
+
alias << push
|
79
|
+
|
80
|
+
# Move the pointer to the next line in the history
|
81
|
+
#
|
82
|
+
# @api public
|
83
|
+
def next
|
84
|
+
return if size.zero?
|
85
|
+
if @index == size - 1
|
86
|
+
@index = 0 if @cycle
|
87
|
+
else
|
88
|
+
@index += 1
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def next?
|
93
|
+
size > 0 && !(@index == size - 1 && !@cycle)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Move the pointer to the previous line in the history
|
97
|
+
def previous
|
98
|
+
return if size.zero?
|
99
|
+
if @index.zero?
|
100
|
+
@index = size - 1 if @cycle
|
101
|
+
else
|
102
|
+
@index -= 1
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def previous?
|
107
|
+
size > 0 && !(@index < 0 && !@cycle)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Return line at the specified index
|
111
|
+
#
|
112
|
+
# @raise [IndexError] index out of range
|
113
|
+
#
|
114
|
+
# @api public
|
115
|
+
def [](index)
|
116
|
+
if index < 0
|
117
|
+
index += @history.size if index < 0
|
118
|
+
end
|
119
|
+
line = @history[index]
|
120
|
+
if line.nil?
|
121
|
+
raise IndexError, 'invalid index'
|
122
|
+
end
|
123
|
+
line.dup
|
124
|
+
end
|
125
|
+
|
126
|
+
# Get current line
|
127
|
+
#
|
128
|
+
# @api public
|
129
|
+
def get
|
130
|
+
return if size.zero?
|
131
|
+
|
132
|
+
self[@index]
|
133
|
+
end
|
134
|
+
|
135
|
+
# Empty all history lines
|
136
|
+
#
|
137
|
+
# @api public
|
138
|
+
def clear
|
139
|
+
@history.clear
|
140
|
+
@index = 0
|
141
|
+
end
|
142
|
+
end # History
|
143
|
+
end # Reader
|
144
|
+
end # TTY
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module TTY
|
5
|
+
class Reader
|
6
|
+
# Responsible for meta-data information about key pressed
|
7
|
+
#
|
8
|
+
# @api private
|
9
|
+
class Key < Struct.new(:name, :ctrl, :meta, :shift)
|
10
|
+
def initialize(*)
|
11
|
+
super(nil, false, false, false)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Represents key event emitted during keyboard press
|
16
|
+
#
|
17
|
+
# @api public
|
18
|
+
class KeyEvent < Struct.new(:value, :key)
|
19
|
+
# Create key event from read input codes
|
20
|
+
#
|
21
|
+
# @param [Hash[Symbol]] keys
|
22
|
+
# the keys and codes mapping
|
23
|
+
# @param [Array[Integer]] codes
|
24
|
+
#
|
25
|
+
# @return [KeyEvent]
|
26
|
+
#
|
27
|
+
# @api public
|
28
|
+
def self.from(keys, char)
|
29
|
+
key = Key.new
|
30
|
+
ctrls = keys.keys.grep(/ctrl/)
|
31
|
+
|
32
|
+
case char
|
33
|
+
when keys[:return] then key.name = :return
|
34
|
+
when keys[:enter] then key.name = :enter
|
35
|
+
when keys[:tab] then key.name = :tab
|
36
|
+
when keys[:backspace] then key.name = :backspace
|
37
|
+
when keys[:delete] then key.name = :delete
|
38
|
+
when keys[:space] then key.name = :space
|
39
|
+
when keys[:escape] then key.name = :escape
|
40
|
+
when proc { |c| c =~ /^[a-z]{1}$/ }
|
41
|
+
key.name = :alpha
|
42
|
+
when proc { |c| c =~ /^[A-Z]{1}$/ }
|
43
|
+
key.name = :alpha
|
44
|
+
key.shift = true
|
45
|
+
when proc { |c| c =~ /^\d+$/ }
|
46
|
+
key.name = :num
|
47
|
+
# arrows
|
48
|
+
when keys[:up] then key.name = :up
|
49
|
+
when keys[:down] then key.name = :down
|
50
|
+
when keys[:left] then key.name = :left
|
51
|
+
when keys[:right] then key.name = :right
|
52
|
+
# editing
|
53
|
+
when keys[:clear] then key.name = :clear
|
54
|
+
when keys[:end] then key.name = :end
|
55
|
+
when keys[:home] then key.name = :home
|
56
|
+
when keys[:insert] then key.name = :insert
|
57
|
+
when keys[:page_up] then key.name = :page_up
|
58
|
+
when keys[:page_down] then key.name = :page_down
|
59
|
+
when proc { |cs| ctrls.any? { |name| keys[name] == cs } }
|
60
|
+
key.name = keys.key(char)
|
61
|
+
key.ctrl = true
|
62
|
+
# f1 - f12
|
63
|
+
when keys[:f1], keys[:f1_xterm] then key.name = :f1
|
64
|
+
when keys[:f2], keys[:f2_xterm] then key.name = :f2
|
65
|
+
when keys[:f3], keys[:f3_xterm] then key.name = :f3
|
66
|
+
when keys[:f4], keys[:f4_xterm] then key.name = :f4
|
67
|
+
when keys[:f5] then key.name = :f5
|
68
|
+
when keys[:f6] then key.name = :f6
|
69
|
+
when keys[:f7] then key.name = :f7
|
70
|
+
when keys[:f8] then key.name = :f8
|
71
|
+
when keys[:f9] then key.name = :f9
|
72
|
+
when keys[:f10] then key.name = :f10
|
73
|
+
when keys[:f11] then key.name = :f11
|
74
|
+
when keys[:f12] then key.name = :f12
|
75
|
+
end
|
76
|
+
|
77
|
+
new(char, key)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Check if key event can be triggered
|
81
|
+
#
|
82
|
+
# @return [Boolean]
|
83
|
+
#
|
84
|
+
# @api public
|
85
|
+
def trigger?
|
86
|
+
!key.nil? && !key.name.nil?
|
87
|
+
end
|
88
|
+
end # KeyEvent
|
89
|
+
end # Reader
|
90
|
+
end # TTY
|
@@ -0,0 +1,161 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'forwardable'
|
5
|
+
|
6
|
+
module TTY
|
7
|
+
class Reader
|
8
|
+
class Line
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
def_delegators :@text, :size, :length, :to_s, :inspect,
|
12
|
+
:slice!, :empty?
|
13
|
+
|
14
|
+
attr_accessor :text
|
15
|
+
|
16
|
+
attr_accessor :cursor
|
17
|
+
|
18
|
+
def initialize(text = "")
|
19
|
+
@text = text
|
20
|
+
@cursor = [0, @text.length].max
|
21
|
+
yield self if block_given?
|
22
|
+
end
|
23
|
+
|
24
|
+
# Check if cursor reached beginning of the line
|
25
|
+
#
|
26
|
+
# @return [Boolean]
|
27
|
+
#
|
28
|
+
# @api public
|
29
|
+
def start?
|
30
|
+
@cursor == 0
|
31
|
+
end
|
32
|
+
|
33
|
+
# Check if cursor reached end of the line
|
34
|
+
#
|
35
|
+
# @return [Boolean]
|
36
|
+
#
|
37
|
+
# @api public
|
38
|
+
def end?
|
39
|
+
@cursor == @text.length
|
40
|
+
end
|
41
|
+
|
42
|
+
# Move line position to the left by n chars
|
43
|
+
#
|
44
|
+
# @api public
|
45
|
+
def left(n = 1)
|
46
|
+
@cursor = [0, @cursor - n].max
|
47
|
+
end
|
48
|
+
|
49
|
+
# Move line position to the right by n chars
|
50
|
+
#
|
51
|
+
# @api public
|
52
|
+
def right(n = 1)
|
53
|
+
@cursor = [@text.length, @cursor + n].min
|
54
|
+
end
|
55
|
+
|
56
|
+
# Move cursor to beginning position
|
57
|
+
#
|
58
|
+
# @api public
|
59
|
+
def move_to_start
|
60
|
+
@cursor = 0
|
61
|
+
end
|
62
|
+
|
63
|
+
# Move cursor to end position
|
64
|
+
#
|
65
|
+
# @api public
|
66
|
+
def move_to_end
|
67
|
+
@cursor = @text.length # put cursor outside of text
|
68
|
+
end
|
69
|
+
|
70
|
+
# Insert characters inside a line. When the lines exceeds
|
71
|
+
# maximum length, an extra space is added to accomodate index.
|
72
|
+
#
|
73
|
+
# @param [Integer] i
|
74
|
+
# the index to insert at
|
75
|
+
#
|
76
|
+
# @example
|
77
|
+
# text = 'aaa'
|
78
|
+
# line[5]= 'b'
|
79
|
+
# => 'aaa b'
|
80
|
+
#
|
81
|
+
# @api public
|
82
|
+
def []=(i, chars)
|
83
|
+
if i.is_a?(Range)
|
84
|
+
@text[i] = chars
|
85
|
+
@cursor += chars.length
|
86
|
+
return
|
87
|
+
end
|
88
|
+
|
89
|
+
if i <= 0
|
90
|
+
before_text = ''
|
91
|
+
after_text = @text.dup
|
92
|
+
elsif i == @text.length - 1
|
93
|
+
before_text = @text.dup
|
94
|
+
after_text = ''
|
95
|
+
elsif i > @text.length - 1
|
96
|
+
before_text = @text.dup
|
97
|
+
after_text = ?\s * (i - @text.length)
|
98
|
+
@cursor += after_text.length
|
99
|
+
else
|
100
|
+
before_text = @text[0..i-1].dup
|
101
|
+
after_text = @text[i..-1].dup
|
102
|
+
end
|
103
|
+
|
104
|
+
if i > @text.length - 1
|
105
|
+
@text = before_text + after_text + chars
|
106
|
+
else
|
107
|
+
@text = before_text + chars + after_text
|
108
|
+
end
|
109
|
+
|
110
|
+
@cursor = i + chars.length
|
111
|
+
end
|
112
|
+
|
113
|
+
# Read character
|
114
|
+
#
|
115
|
+
# @api public
|
116
|
+
def [](i)
|
117
|
+
@text[i]
|
118
|
+
end
|
119
|
+
|
120
|
+
# Replace current line with new text
|
121
|
+
#
|
122
|
+
# @param [String] text
|
123
|
+
#
|
124
|
+
# @api public
|
125
|
+
def replace(text)
|
126
|
+
@text = text
|
127
|
+
@cursor = @text.length # put cursor outside of text
|
128
|
+
end
|
129
|
+
|
130
|
+
# Insert char(s) at cursor position
|
131
|
+
#
|
132
|
+
# @api public
|
133
|
+
def insert(chars)
|
134
|
+
self[@cursor] = chars
|
135
|
+
end
|
136
|
+
|
137
|
+
# Add char and move cursor
|
138
|
+
#
|
139
|
+
# @api public
|
140
|
+
def <<(char)
|
141
|
+
@text << char
|
142
|
+
@cursor += 1
|
143
|
+
end
|
144
|
+
|
145
|
+
# Remove char from the line at current position
|
146
|
+
#
|
147
|
+
# @api public
|
148
|
+
def delete
|
149
|
+
@text.slice!(@cursor, 1)
|
150
|
+
end
|
151
|
+
|
152
|
+
# Remove char from the line in front of the cursor
|
153
|
+
#
|
154
|
+
# @api public
|
155
|
+
def remove
|
156
|
+
left
|
157
|
+
@text.slice!(@cursor, 1)
|
158
|
+
end
|
159
|
+
end # Line
|
160
|
+
end # Reader
|
161
|
+
end # TTY
|