tty-reader 0.4.0 → 0.5.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/CHANGELOG.md +54 -0
- data/README.md +52 -11
- data/benchmarks/speed_read_char.rb +34 -0
- data/benchmarks/speed_read_line.rb +34 -0
- data/examples/shell.rb +2 -2
- data/lib/tty-reader.rb +0 -2
- data/lib/tty/reader.rb +12 -7
- data/lib/tty/reader/console.rb +9 -2
- data/lib/tty/reader/history.rb +0 -1
- data/lib/tty/reader/key_event.rb +3 -4
- data/lib/tty/reader/keys.rb +0 -1
- data/lib/tty/reader/line.rb +12 -5
- data/lib/tty/reader/mode.rb +0 -1
- data/lib/tty/reader/version.rb +2 -2
- data/lib/tty/reader/win_api.rb +1 -4
- data/lib/tty/reader/win_console.rb +0 -1
- data/spec/spec_helper.rb +43 -0
- data/spec/unit/history_spec.rb +177 -0
- data/spec/unit/key_event_spec.rb +102 -0
- data/spec/unit/line_spec.rb +159 -0
- data/spec/unit/publish_keypress_event_spec.rb +109 -0
- data/spec/unit/read_keypress_spec.rb +96 -0
- data/spec/unit/read_line_spec.rb +69 -0
- data/spec/unit/read_multiline_spec.rb +76 -0
- data/spec/unit/subscribe_spec.rb +74 -0
- data/tty-reader.gemspec +4 -3
- metadata +17 -4
data/lib/tty/reader/mode.rb
CHANGED
data/lib/tty/reader/version.rb
CHANGED
data/lib/tty/reader/win_api.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
# encoding: utf-8
|
2
1
|
# frozen_string_literal: true
|
3
2
|
|
4
3
|
require 'fiddle'
|
@@ -8,9 +7,7 @@ module TTY
|
|
8
7
|
module WinAPI
|
9
8
|
include Fiddle
|
10
9
|
|
11
|
-
|
12
|
-
|
13
|
-
CRT_HANDLE = Handle.new("msvcrt") rescue Handle.new("crtdll")
|
10
|
+
CRT_HANDLE = Fiddle::Handle.new("msvcrt") rescue Fiddle::Handle.new("crtdll")
|
14
11
|
|
15
12
|
# Get a character from the console without echo.
|
16
13
|
#
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
if ENV['COVERAGE'] || ENV['TRAVIS']
|
2
|
+
require 'simplecov'
|
3
|
+
require 'coveralls'
|
4
|
+
|
5
|
+
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
|
6
|
+
SimpleCov::Formatter::HTMLFormatter,
|
7
|
+
Coveralls::SimpleCov::Formatter
|
8
|
+
]
|
9
|
+
|
10
|
+
SimpleCov.start do
|
11
|
+
command_name 'spec'
|
12
|
+
add_filter 'spec'
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
require "bundler/setup"
|
17
|
+
require "tty-reader"
|
18
|
+
|
19
|
+
RSpec.configure do |config|
|
20
|
+
# Enable flags like --only-failures and --next-failure
|
21
|
+
config.example_status_persistence_file_path = ".rspec_status"
|
22
|
+
|
23
|
+
config.mock_with :rspec do |mocks|
|
24
|
+
mocks.verify_partial_doubles = true
|
25
|
+
end
|
26
|
+
|
27
|
+
# Disable RSpec exposing methods globally on `Module` and `main`
|
28
|
+
config.disable_monkey_patching!
|
29
|
+
|
30
|
+
# This setting enables warnings. It's recommended, but in some cases may
|
31
|
+
# be too noisy due to issues in dependencies.
|
32
|
+
config.warnings = true
|
33
|
+
|
34
|
+
if config.files_to_run.one?
|
35
|
+
config.default_formatter = 'doc'
|
36
|
+
end
|
37
|
+
|
38
|
+
config.profile_examples = 2
|
39
|
+
|
40
|
+
config.order = :random
|
41
|
+
|
42
|
+
Kernel.srand config.seed
|
43
|
+
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe TTY::Reader::History do
|
4
|
+
it "has no lines" do
|
5
|
+
history = described_class.new
|
6
|
+
expect(history.size).to eq(0)
|
7
|
+
end
|
8
|
+
|
9
|
+
it "doesn't navigate through empty buffer" do
|
10
|
+
history = described_class.new
|
11
|
+
expect(history.next?).to eq(false)
|
12
|
+
expect(history.previous?).to eq(false)
|
13
|
+
end
|
14
|
+
|
15
|
+
it "allows to cycle through non-empty buffer" do
|
16
|
+
history = described_class.new(3, {cycle: true})
|
17
|
+
history << "line"
|
18
|
+
expect(history.next?).to eq(true)
|
19
|
+
expect(history.previous?).to eq(true)
|
20
|
+
end
|
21
|
+
|
22
|
+
it "defaults maximum size" do
|
23
|
+
history = described_class.new
|
24
|
+
expect(history.max_size).to eq(512)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "presents string representation" do
|
28
|
+
history = described_class.new
|
29
|
+
expect(history.to_s).to eq("[]")
|
30
|
+
end
|
31
|
+
|
32
|
+
it "adds items to history without overflowing" do
|
33
|
+
history = described_class.new(3)
|
34
|
+
history << "line #1"
|
35
|
+
history << "line #2"
|
36
|
+
history << "line #3"
|
37
|
+
history << "line #4"
|
38
|
+
|
39
|
+
expect(history.to_a).to eq(["line #2", "line #3", "line #4"])
|
40
|
+
expect(history.index).to eq(2)
|
41
|
+
end
|
42
|
+
|
43
|
+
it "excludes items" do
|
44
|
+
exclude = proc { |line| /line #[23]/.match(line) }
|
45
|
+
history = described_class.new(exclude: exclude)
|
46
|
+
history << "line #1"
|
47
|
+
history << "line #2"
|
48
|
+
history << "line #3"
|
49
|
+
|
50
|
+
expect(history.to_a).to eq(["line #1"])
|
51
|
+
expect(history.index).to eq(0)
|
52
|
+
end
|
53
|
+
|
54
|
+
it "allows duplicates" do
|
55
|
+
history = described_class.new
|
56
|
+
history << "line #1"
|
57
|
+
history << "line #1"
|
58
|
+
history << "line #1"
|
59
|
+
|
60
|
+
expect(history.to_a).to eq(["line #1", "line #1", "line #1"])
|
61
|
+
end
|
62
|
+
|
63
|
+
it "prevents duplicates" do
|
64
|
+
history = described_class.new(duplicates: false)
|
65
|
+
history << "line #1"
|
66
|
+
history << "line #1"
|
67
|
+
history << "line #1"
|
68
|
+
|
69
|
+
expect(history.to_a).to eq(["line #1"])
|
70
|
+
end
|
71
|
+
|
72
|
+
it "navigates through history buffer without cycling" do
|
73
|
+
history = described_class.new(3)
|
74
|
+
history << "line #1"
|
75
|
+
history << "line #2"
|
76
|
+
history << "line #3"
|
77
|
+
|
78
|
+
expect(history.index).to eq(2)
|
79
|
+
history.previous
|
80
|
+
history.previous
|
81
|
+
expect(history.index).to eq(0)
|
82
|
+
history.previous
|
83
|
+
expect(history.index).to eq(0)
|
84
|
+
history.next
|
85
|
+
history.next
|
86
|
+
expect(history.index).to eq(2)
|
87
|
+
history.next
|
88
|
+
expect(history.next?).to eq(false)
|
89
|
+
expect(history.index).to eq(2)
|
90
|
+
end
|
91
|
+
|
92
|
+
it "navigates through history buffer with cycling" do
|
93
|
+
history = described_class.new(3, cycle: true)
|
94
|
+
history << "line #1"
|
95
|
+
history << "line #2"
|
96
|
+
history << "line #3"
|
97
|
+
|
98
|
+
expect(history.index).to eq(2)
|
99
|
+
history.previous
|
100
|
+
history.previous
|
101
|
+
expect(history.index).to eq(0)
|
102
|
+
history.previous
|
103
|
+
expect(history.index).to eq(2)
|
104
|
+
expect(history.next?).to eq(true)
|
105
|
+
history.next
|
106
|
+
history.next
|
107
|
+
expect(history.index).to eq(1)
|
108
|
+
history.next
|
109
|
+
expect(history.index).to eq(2)
|
110
|
+
end
|
111
|
+
|
112
|
+
it "checks if navigation is possible" do
|
113
|
+
history = described_class.new(3)
|
114
|
+
|
115
|
+
expect(history.index).to eq(nil)
|
116
|
+
expect(history.previous?).to eq(false)
|
117
|
+
expect(history.next?).to eq(false)
|
118
|
+
|
119
|
+
history << "line #1"
|
120
|
+
history << "line #2"
|
121
|
+
expect(history.index).to eq(1)
|
122
|
+
expect(history.previous?).to eq(true)
|
123
|
+
expect(history.next?).to eq(false)
|
124
|
+
|
125
|
+
history.previous
|
126
|
+
expect(history.index).to eq(0)
|
127
|
+
expect(history.previous?).to eq(true)
|
128
|
+
expect(history.next?).to eq(true)
|
129
|
+
|
130
|
+
history.previous
|
131
|
+
expect(history.index).to eq(0)
|
132
|
+
expect(history.previous?).to eq(true)
|
133
|
+
expect(history.next?).to eq(true)
|
134
|
+
end
|
135
|
+
|
136
|
+
it "gets line based on index" do
|
137
|
+
history = described_class.new(3, cycle: true)
|
138
|
+
history << "line #1"
|
139
|
+
history << "line #2"
|
140
|
+
history << "line #3"
|
141
|
+
|
142
|
+
expect(history[-1]).to eq('line #3')
|
143
|
+
expect(history[1]).to eq('line #2')
|
144
|
+
expect {
|
145
|
+
history[11]
|
146
|
+
}.to raise_error(IndexError, 'invalid index')
|
147
|
+
end
|
148
|
+
|
149
|
+
it "retrieves current line" do
|
150
|
+
history = described_class.new(3, cycle: true)
|
151
|
+
expect(history.get).to eq(nil)
|
152
|
+
|
153
|
+
history << "line #1"
|
154
|
+
history << "line #2"
|
155
|
+
history << "line #3"
|
156
|
+
|
157
|
+
expect(history.get).to eq("line #3")
|
158
|
+
history.previous
|
159
|
+
history.previous
|
160
|
+
expect(history.get).to eq("line #1")
|
161
|
+
history.next
|
162
|
+
expect(history.get).to eq("line #2")
|
163
|
+
end
|
164
|
+
|
165
|
+
it "clears all lines" do
|
166
|
+
history = described_class.new(3)
|
167
|
+
|
168
|
+
history << "line #1"
|
169
|
+
history << "line #2"
|
170
|
+
history << "line #3"
|
171
|
+
|
172
|
+
expect(history.size).to eq(3)
|
173
|
+
history.clear
|
174
|
+
expect(history.size).to eq(0)
|
175
|
+
expect(history.index).to eq(0)
|
176
|
+
end
|
177
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'shellwords'
|
4
|
+
|
5
|
+
RSpec.describe TTY::Reader::KeyEvent, '#from' do
|
6
|
+
let(:keys) { TTY::Reader::Keys.keys }
|
7
|
+
|
8
|
+
it "parses backspace" do
|
9
|
+
event = described_class.from(keys, "\x7f")
|
10
|
+
expect(event.key.name).to eq(:backspace)
|
11
|
+
expect(event.value).to eq("\x7f")
|
12
|
+
end
|
13
|
+
|
14
|
+
it "parses lowercase char" do
|
15
|
+
event = described_class.from(keys, 'a')
|
16
|
+
expect(event.key.name).to eq(:alpha)
|
17
|
+
expect(event.value).to eq('a')
|
18
|
+
end
|
19
|
+
|
20
|
+
it "parses uppercase char" do
|
21
|
+
event = described_class.from(keys, 'A')
|
22
|
+
expect(event.key.name).to eq(:alpha)
|
23
|
+
expect(event.value).to eq('A')
|
24
|
+
end
|
25
|
+
|
26
|
+
it "parses number char" do
|
27
|
+
event = described_class.from(keys, '666')
|
28
|
+
expect(event.key.name).to eq(:num)
|
29
|
+
expect(event.value).to eq('666')
|
30
|
+
end
|
31
|
+
|
32
|
+
it "parses ctrl-a to ctrl-z inputs" do
|
33
|
+
(1..26).zip('a'..'z').each do |code, char|
|
34
|
+
event = described_class.from(TTY::Reader::Keys.ctrl_keys, code.chr)
|
35
|
+
expect(event.key.name).to eq(:"ctrl_#{char}")
|
36
|
+
expect(event.value).to eq(code.chr)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
it "parses uknown key" do
|
41
|
+
no_keys = {}
|
42
|
+
event = described_class.from(no_keys, '*')
|
43
|
+
expect(event.key.name).to eq(:ignore)
|
44
|
+
expect(event.value).to eq('*')
|
45
|
+
end
|
46
|
+
|
47
|
+
it "exposes line value" do
|
48
|
+
event = described_class.from(keys, 'c', 'ab')
|
49
|
+
expect(event.line).to eq('ab')
|
50
|
+
end
|
51
|
+
|
52
|
+
# F1-F12 keys
|
53
|
+
{
|
54
|
+
f1: ["\eOP","\e[[A","\e[11~"],
|
55
|
+
f2: ["\eOQ","\e[[B","\e[12~"],
|
56
|
+
f3: ["\eOR","\e[[C","\e[13~"],
|
57
|
+
f4: ["\eOS","\e[[D","\e[14~"],
|
58
|
+
f5: [ "\e[[E","\e[15~"],
|
59
|
+
f6: [ "\e[17~"],
|
60
|
+
f7: [ "\e[18~"],
|
61
|
+
f8: [ "\e[19~"],
|
62
|
+
f9: [ "\e[20~"],
|
63
|
+
f10: [ "\e[21~"],
|
64
|
+
f11: [ "\e[23~"],
|
65
|
+
f12: [ "\e[24~"]
|
66
|
+
}.each do |name, codes|
|
67
|
+
codes.each do |code|
|
68
|
+
it "parses #{Shellwords.escape(code)} as #{name} key" do
|
69
|
+
event = described_class.from(keys, code)
|
70
|
+
expect(event.key.name).to eq(name)
|
71
|
+
expect(event.key.meta).to eq(false)
|
72
|
+
expect(event.key.ctrl).to eq(false)
|
73
|
+
expect(event.key.shift).to eq(false)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# arrow keys & text editing
|
79
|
+
{
|
80
|
+
up: ["\e[A"],
|
81
|
+
down: ["\e[B"],
|
82
|
+
right: ["\e[C"],
|
83
|
+
left: ["\e[D"],
|
84
|
+
clear: ["\e[E"],
|
85
|
+
home: ["\e[1~", "\e[7~", "\e[H"],
|
86
|
+
end: ["\e[4~", "\eOF", "\e[F"],
|
87
|
+
insert: ["\e[2~"],
|
88
|
+
delete: ["\e[3~"],
|
89
|
+
page_up: ["\e[5~"],
|
90
|
+
page_down: ["\e[6~"]
|
91
|
+
}.each do |name, codes|
|
92
|
+
codes.each do |code|
|
93
|
+
it "parses #{Shellwords.escape(code)} as #{name} key" do
|
94
|
+
event = described_class.from(keys, code)
|
95
|
+
expect(event.key.name).to eq(name)
|
96
|
+
expect(event.key.meta).to eq(false)
|
97
|
+
expect(event.key.ctrl).to eq(false)
|
98
|
+
expect(event.key.shift).to eq(false)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe TTY::Reader::Line do
|
4
|
+
it "provides access to the prompt" do
|
5
|
+
line = described_class.new('aaa', prompt: '>> ')
|
6
|
+
expect(line.prompt).to eq('>> ')
|
7
|
+
expect(line.text).to eq('aaa')
|
8
|
+
expect(line.size).to eq(6)
|
9
|
+
expect(line.to_s).to eq(">> aaa")
|
10
|
+
end
|
11
|
+
|
12
|
+
it "inserts characters inside a line" do
|
13
|
+
line = described_class.new('aaaaa')
|
14
|
+
|
15
|
+
line[0] = 'test'
|
16
|
+
expect(line.text).to eq('testaaaaa')
|
17
|
+
|
18
|
+
line[4..6] = ''
|
19
|
+
expect(line.text).to eq('testaa')
|
20
|
+
end
|
21
|
+
|
22
|
+
it "moves cursor left and right" do
|
23
|
+
line = described_class.new('aaaaa')
|
24
|
+
|
25
|
+
5.times { line.left }
|
26
|
+
expect(line.cursor).to eq(0)
|
27
|
+
expect(line.start?).to eq(true)
|
28
|
+
|
29
|
+
line.left(5)
|
30
|
+
expect(line.cursor).to eq(0)
|
31
|
+
|
32
|
+
line.right(20)
|
33
|
+
expect(line.cursor).to eq(5)
|
34
|
+
expect(line.end?).to eq(true)
|
35
|
+
end
|
36
|
+
|
37
|
+
it "inserts char at start of the line" do
|
38
|
+
line = described_class.new('aaaaa')
|
39
|
+
expect(line.cursor).to eq(5)
|
40
|
+
|
41
|
+
line[0] = 'b'
|
42
|
+
expect(line.cursor).to eq(1)
|
43
|
+
expect(line.text).to eq('baaaaa')
|
44
|
+
|
45
|
+
line.insert('b')
|
46
|
+
expect(line.text).to eq('bbaaaaa')
|
47
|
+
end
|
48
|
+
|
49
|
+
it "inserts char at end of the line" do
|
50
|
+
line = described_class.new('aaaaa')
|
51
|
+
expect(line.cursor).to eq(5)
|
52
|
+
|
53
|
+
line[4] = 'b'
|
54
|
+
expect(line.cursor).to eq(5)
|
55
|
+
expect(line.text).to eq('aaaaba')
|
56
|
+
end
|
57
|
+
|
58
|
+
it "inserts char inside the line" do
|
59
|
+
line = described_class.new('aaaaa')
|
60
|
+
expect(line.cursor).to eq(5)
|
61
|
+
|
62
|
+
line[2] = 'b'
|
63
|
+
expect(line.cursor).to eq(3)
|
64
|
+
expect(line.text).to eq('aabaaa')
|
65
|
+
end
|
66
|
+
|
67
|
+
it "inserts char outside of the line size" do
|
68
|
+
line = described_class.new('aaaaa')
|
69
|
+
expect(line.cursor).to eq(5)
|
70
|
+
|
71
|
+
line[10] = 'b'
|
72
|
+
expect(line.cursor).to eq(11)
|
73
|
+
expect(line.text).to eq('aaaaa b')
|
74
|
+
end
|
75
|
+
|
76
|
+
it "inserts chars in empty string" do
|
77
|
+
line = described_class.new('')
|
78
|
+
expect(line.cursor).to eq(0)
|
79
|
+
|
80
|
+
line.insert('a')
|
81
|
+
expect(line.cursor).to eq(1)
|
82
|
+
|
83
|
+
line.insert('b')
|
84
|
+
expect(line.cursor).to eq(2)
|
85
|
+
expect(line.to_s).to eq('ab')
|
86
|
+
|
87
|
+
line.insert('cc')
|
88
|
+
expect(line.cursor).to eq(4)
|
89
|
+
expect(line.to_s).to eq('abcc')
|
90
|
+
end
|
91
|
+
|
92
|
+
it "inserts characters with #insert call" do
|
93
|
+
line = described_class.new('aaaaa')
|
94
|
+
expect(line.cursor).to eq(5)
|
95
|
+
|
96
|
+
line.left(2)
|
97
|
+
expect(line.cursor).to eq(3)
|
98
|
+
|
99
|
+
line.insert(' test ')
|
100
|
+
expect(line.text).to eq('aaa test aa')
|
101
|
+
expect(line.cursor).to eq(9)
|
102
|
+
|
103
|
+
line.right
|
104
|
+
expect(line.cursor).to eq(10)
|
105
|
+
end
|
106
|
+
|
107
|
+
it "removes char before current cursor position" do
|
108
|
+
line = described_class.new('abcdef')
|
109
|
+
expect(line.cursor).to eq(6)
|
110
|
+
|
111
|
+
line.remove
|
112
|
+
line.remove
|
113
|
+
expect(line.text).to eq('abcd')
|
114
|
+
expect(line.cursor).to eq(4)
|
115
|
+
|
116
|
+
line.left
|
117
|
+
line.left
|
118
|
+
line.remove
|
119
|
+
expect(line.text).to eq('acd')
|
120
|
+
expect(line.cursor).to eq(1)
|
121
|
+
|
122
|
+
line.insert('x')
|
123
|
+
expect(line.text).to eq('axcd')
|
124
|
+
end
|
125
|
+
|
126
|
+
it "deletes char under current cursor position" do
|
127
|
+
line = described_class.new('abcdef')
|
128
|
+
|
129
|
+
line.left(3)
|
130
|
+
line.delete
|
131
|
+
expect(line.text).to eq('abcef')
|
132
|
+
|
133
|
+
line.right
|
134
|
+
line.delete
|
135
|
+
expect(line.text).to eq('abce')
|
136
|
+
|
137
|
+
line.left(4)
|
138
|
+
line.delete
|
139
|
+
expect(line.text).to eq('bce')
|
140
|
+
end
|
141
|
+
|
142
|
+
it "replaces current line with new preserving cursor" do
|
143
|
+
line = described_class.new('x' * 6)
|
144
|
+
expect(line.text).to eq('xxxxxx')
|
145
|
+
expect(line.cursor).to eq(6)
|
146
|
+
expect(line.mode).to eq(:edit)
|
147
|
+
expect(line.editing?).to eq(true)
|
148
|
+
|
149
|
+
line.replace('y' * 8)
|
150
|
+
expect(line.text).to eq('y' * 8)
|
151
|
+
expect(line.cursor).to eq(8)
|
152
|
+
expect(line.replacing?).to eq(true)
|
153
|
+
|
154
|
+
line.insert('z')
|
155
|
+
expect(line.text).to eq('y' * 8 + 'z')
|
156
|
+
expect(line.cursor).to eq(9)
|
157
|
+
expect(line.editing?).to eq(true)
|
158
|
+
end
|
159
|
+
end
|