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.
@@ -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