tty-reader 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|