tty-reader 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,4 +1,3 @@
1
- # encoding: utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
3
  require 'io/console'
@@ -1,7 +1,7 @@
1
- # encoding: utf-8
1
+ # frozen_string_literal: true
2
2
 
3
3
  module TTY
4
4
  class Reader
5
- VERSION = '0.4.0'.freeze
5
+ VERSION = '0.5.0'
6
6
  end # Reader
7
7
  end # TTY
@@ -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
- Handle = RUBY_VERSION >= "2.0.0" ? Fiddle::Handle : DL::Handle
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
  #
@@ -1,4 +1,3 @@
1
- # encoding: utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
3
  require_relative 'keys'
@@ -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