tty-prompt 0.11.0 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -0
  3. data/README.md +66 -7
  4. data/examples/key_events.rb +11 -0
  5. data/examples/keypress.rb +3 -5
  6. data/examples/multiline.rb +9 -0
  7. data/examples/pause.rb +7 -0
  8. data/lib/tty/prompt.rb +82 -44
  9. data/lib/tty/prompt/confirm_question.rb +20 -36
  10. data/lib/tty/prompt/enum_list.rb +32 -23
  11. data/lib/tty/prompt/expander.rb +35 -31
  12. data/lib/tty/prompt/keypress.rb +91 -0
  13. data/lib/tty/prompt/list.rb +38 -23
  14. data/lib/tty/prompt/mask_question.rb +4 -7
  15. data/lib/tty/prompt/multi_list.rb +3 -1
  16. data/lib/tty/prompt/multiline.rb +71 -0
  17. data/lib/tty/prompt/question.rb +33 -35
  18. data/lib/tty/prompt/reader.rb +154 -38
  19. data/lib/tty/prompt/reader/codes.rb +4 -4
  20. data/lib/tty/prompt/reader/console.rb +1 -1
  21. data/lib/tty/prompt/reader/history.rb +145 -0
  22. data/lib/tty/prompt/reader/key_event.rb +4 -0
  23. data/lib/tty/prompt/reader/line.rb +162 -0
  24. data/lib/tty/prompt/reader/mode.rb +2 -2
  25. data/lib/tty/prompt/reader/win_console.rb +5 -1
  26. data/lib/tty/prompt/slider.rb +18 -12
  27. data/lib/tty/prompt/timeout.rb +48 -0
  28. data/lib/tty/prompt/version.rb +1 -1
  29. data/spec/unit/ask_spec.rb +15 -0
  30. data/spec/unit/converters/convert_bool_spec.rb +1 -0
  31. data/spec/unit/keypress_spec.rb +35 -6
  32. data/spec/unit/multi_select_spec.rb +18 -0
  33. data/spec/unit/multiline_spec.rb +67 -9
  34. data/spec/unit/question/default_spec.rb +1 -0
  35. data/spec/unit/question/echo_spec.rb +8 -0
  36. data/spec/unit/question/in_spec.rb +13 -0
  37. data/spec/unit/question/required_spec.rb +31 -2
  38. data/spec/unit/question/validate_spec.rb +39 -9
  39. data/spec/unit/reader/history_spec.rb +172 -0
  40. data/spec/unit/reader/key_event_spec.rb +12 -8
  41. data/spec/unit/reader/line_spec.rb +110 -0
  42. data/spec/unit/reader/publish_keypress_event_spec.rb +11 -0
  43. data/spec/unit/reader/read_line_spec.rb +32 -2
  44. data/spec/unit/reader/read_multiline_spec.rb +21 -7
  45. data/spec/unit/select_spec.rb +40 -1
  46. data/spec/unit/yes_no_spec.rb +48 -4
  47. metadata +14 -3
  48. data/lib/tty/prompt/history.rb +0 -16
@@ -0,0 +1,172 @@
1
+ # encoding: utf-8
2
+
3
+ RSpec.describe TTY::Prompt::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(3, 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(3)
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(3, 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
+ expect(history.previous?).to eq(false)
115
+ expect(history.next?).to eq(false)
116
+
117
+ history << "line #1"
118
+ history << "line #2"
119
+ expect(history.previous?).to eq(true)
120
+ expect(history.next?).to eq(false)
121
+
122
+ history.previous
123
+ expect(history.previous?).to eq(true)
124
+ expect(history.next?).to eq(true)
125
+
126
+ history.previous
127
+ expect(history.previous?).to eq(true)
128
+ expect(history.next?).to eq(true)
129
+ end
130
+
131
+ it "gets line based on index" do
132
+ history = described_class.new(3, cycle: true)
133
+ history << "line #1"
134
+ history << "line #2"
135
+ history << "line #3"
136
+
137
+ expect(history[-1]).to eq('line #3')
138
+ expect(history[1]).to eq('line #2')
139
+ expect {
140
+ history[11]
141
+ }.to raise_error(IndexError, 'invalid index')
142
+ end
143
+
144
+ it "retrieves current line" do
145
+ history = described_class.new(3, cycle: true)
146
+ expect(history.get).to eq(nil)
147
+
148
+ history << "line #1"
149
+ history << "line #2"
150
+ history << "line #3"
151
+
152
+ expect(history.get).to eq("line #3")
153
+ history.previous
154
+ history.previous
155
+ expect(history.get).to eq("line #1")
156
+ history.next
157
+ expect(history.get).to eq("line #2")
158
+ end
159
+
160
+ it "clears all lines" do
161
+ history = described_class.new(3)
162
+
163
+ history << "line #1"
164
+ history << "line #2"
165
+ history << "line #3"
166
+
167
+ expect(history.size).to eq(3)
168
+ history.clear
169
+ expect(history.size).to eq(0)
170
+ expect(history.index).to eq(0)
171
+ end
172
+ end
@@ -59,15 +59,19 @@ RSpec.describe TTY::Prompt::Reader::KeyEvent, '#from' do
59
59
  end
60
60
  end
61
61
 
62
- # arrow keys & page navigation
62
+ # arrow keys & text editing
63
63
  {
64
- up: ["\e[A"],
65
- down: ["\e[B"],
66
- right: ["\e[C"],
67
- left: ["\e[D"],
68
- clear: ["\e[E"],
69
- end: ["\e[F"],
70
- home: ["\e[H"]
64
+ up: ["\e[A"],
65
+ down: ["\e[B"],
66
+ right: ["\e[C"],
67
+ left: ["\e[D"],
68
+ clear: ["\e[E"],
69
+ home: ["\e[1~"],
70
+ insert: ["\e[2~"],
71
+ delete: ["\e[3~"],
72
+ end: ["\e[4~"],
73
+ page_up: ["\e[5~"],
74
+ page_down: ["\e[6~"]
71
75
  }.each do |name, codes|
72
76
  codes.each do |code|
73
77
  it "parses #{Shellwords.escape(code)} as #{name} key" do
@@ -0,0 +1,110 @@
1
+ # encoding: utf-8
2
+
3
+ RSpec.describe TTY::Prompt::Reader::Line do
4
+ it "inserts characters in line" do
5
+ line = described_class.new('aaaaa')
6
+ line[0] = 'test'
7
+ expect(line.text).to eq('testaaaaa')
8
+ line[4..6] = ''
9
+ expect(line.text).to eq('testaa')
10
+ end
11
+
12
+ it "moves cursor left and right" do
13
+ line = described_class.new('aaaaa')
14
+ 5.times { line.left }
15
+ expect(line.cursor).to eq(0)
16
+ expect(line.start?).to eq(true)
17
+ line.left(5)
18
+ expect(line.cursor).to eq(0)
19
+ line.right(20)
20
+ expect(line.cursor).to eq(5)
21
+ expect(line.end?).to eq(true)
22
+ end
23
+
24
+ it "inserts char at start of the line" do
25
+ line = described_class.new('aaaaa')
26
+ expect(line.cursor).to eq(5)
27
+ line[0] = 'b'
28
+ expect(line.cursor).to eq(1)
29
+ expect(line.text).to eq('baaaaa')
30
+ line.insert('b')
31
+ expect(line.text).to eq('bbaaaaa')
32
+ end
33
+
34
+ it "inserts char at end of the line" do
35
+ line = described_class.new('aaaaa')
36
+ expect(line.cursor).to eq(5)
37
+ line[4] = 'b'
38
+ expect(line.cursor).to eq(5)
39
+ expect(line.text).to eq('aaaaab')
40
+ end
41
+
42
+ it "inserts char inside the line" do
43
+ line = described_class.new('aaaaa')
44
+ expect(line.cursor).to eq(5)
45
+ line[2] = 'b'
46
+ expect(line.cursor).to eq(3)
47
+ expect(line.text).to eq('aabaaa')
48
+ end
49
+
50
+ it "inserts char outside of the line size" do
51
+ line = described_class.new('aaaaa')
52
+ expect(line.cursor).to eq(5)
53
+ line[10] = 'b'
54
+ expect(line.cursor).to eq(11)
55
+ expect(line.text).to eq('aaaaa b')
56
+ end
57
+
58
+ it "inserts chars on empty string" do
59
+ line = described_class.new('')
60
+ expect(line.cursor).to eq(0)
61
+ line.insert('a')
62
+ expect(line.cursor).to eq(1)
63
+ line.insert('b')
64
+ expect(line.cursor).to eq(2)
65
+ expect(line.to_s).to eq('ab')
66
+ line.insert('cc')
67
+ expect(line.cursor).to eq(4)
68
+ expect(line.to_s).to eq('abcc')
69
+ end
70
+
71
+ it "inserts characters with #insert call" do
72
+ line = described_class.new('aaaaa')
73
+ line.left(2)
74
+ expect(line.cursor).to eq(3)
75
+ line.insert(' test ')
76
+ expect(line.text).to eq('aaa test aa')
77
+ expect(line.cursor).to eq(9)
78
+ line.right
79
+ expect(line.cursor).to eq(10)
80
+ end
81
+
82
+ it "removes char before current cursor position" do
83
+ line = described_class.new('abcdef')
84
+ expect(line.cursor).to eq(6)
85
+ line.remove
86
+ line.remove
87
+ expect(line.text).to eq('abcd')
88
+ expect(line.cursor).to eq(4)
89
+ line.left
90
+ line.left
91
+ line.remove
92
+ expect(line.text).to eq('acd')
93
+ expect(line.cursor).to eq(1)
94
+ line.insert('x')
95
+ expect(line.text).to eq('axcd')
96
+ end
97
+
98
+ it "deletes char under current cursor position" do
99
+ line = described_class.new('abcdef')
100
+ line.left(3)
101
+ line.delete
102
+ expect(line.text).to eq('abcef')
103
+ line.right
104
+ line.delete
105
+ expect(line.text).to eq('abce')
106
+ line.left(4)
107
+ line.delete
108
+ expect(line.text).to eq('bce')
109
+ end
110
+ end
@@ -80,4 +80,15 @@ RSpec.describe TTY::Prompt::Reader, '#publish_keypress_event' do
80
80
  expect(keys).to eq(["keyenter", "keypress"])
81
81
  expect(answer).to eq("\n")
82
82
  end
83
+
84
+ it "subscribes to ctrl+X type of event event" do
85
+ input << ?\C-z
86
+ input.rewind
87
+ keys = []
88
+ reader.on(:keyctrl_z) { |event| keys << "ctrl_z" }
89
+
90
+ answer = reader.read_keypress
91
+ expect(keys).to eq(['ctrl_z'])
92
+ expect(answer).to eq(?\C-z)
93
+ end
83
94
  end
@@ -19,10 +19,40 @@ RSpec.describe TTY::Prompt::Reader, '#read_line' do
19
19
  input.rewind
20
20
  answer = reader.read_line
21
21
  expect(answer).to eq("password\n")
22
- expect(output.string).to eq("")
22
+ expect(output.string).to eq([
23
+ "\e[2K\e[1Gp",
24
+ "\e[2K\e[1Gpa",
25
+ "\e[2K\e[1Gpas",
26
+ "\e[2K\e[1Gpass",
27
+ "\e[2K\e[1Gpassw",
28
+ "\e[2K\e[1Gpasswo",
29
+ "\e[2K\e[1Gpasswor",
30
+ "\e[2K\e[1Gpassword",
31
+ "\e[2K\e[1Gpassword\n"
32
+ ].join)
23
33
  end
24
34
 
25
- it 'deletes characters when backspace pressed' do
35
+ it "doesn't echo characters back" do
36
+ input << "password\n"
37
+ input.rewind
38
+ answer = reader.read_line(echo: false)
39
+ expect(answer).to eq("password\n")
40
+ expect(output.string).to eq('')
41
+ end
42
+
43
+ it "displays a prompt before input" do
44
+ input << "aa\n"
45
+ input.rewind
46
+ answer = reader.read_line('>> ')
47
+ expect(answer).to eq("aa\n")
48
+ expect(output.string).to eq([
49
+ "\e[2K\e[1G>> a",
50
+ "\e[2K\e[1G>> aa",
51
+ "\e[2K\e[1G>> aa\n"
52
+ ].join)
53
+ end
54
+
55
+ xit 'deletes characters when backspace pressed' do
26
56
  input << "aa\ba\bcc\n"
27
57
  input.rewind
28
58
  answer = reader.read_line
@@ -8,28 +8,42 @@ RSpec.describe TTY::Prompt::Reader, '#read_multiline' do
8
8
  subject(:reader) { described_class.new(input, output, env: env) }
9
9
 
10
10
  it 'reads no lines' do
11
- input << ''
11
+ input << "\C-d"
12
12
  input.rewind
13
13
  answer = reader.read_multiline
14
14
  expect(answer).to eq([])
15
15
  end
16
16
 
17
- it "reads a line" do
18
- input << "Single line\n"
17
+ it "reads a line and terminates on Ctrl+d" do
18
+ input << "Single line\C-d"
19
19
  input.rewind
20
20
  answer = reader.read_multiline
21
- expect(answer).to eq(["Single line\n"])
21
+ expect(answer).to eq(["Single line"])
22
+ end
23
+
24
+ it "reads a line and terminates on Ctrl+z" do
25
+ input << "Single line\C-z"
26
+ input.rewind
27
+ answer = reader.read_multiline
28
+ expect(answer).to eq(["Single line"])
22
29
  end
23
30
 
24
31
  it 'reads few lines' do
25
- input << "First line\nSecond line\nThird line\n"
32
+ input << "First line\nSecond line\nThird line\n\C-d"
26
33
  input.rewind
27
34
  answer = reader.read_multiline
28
35
  expect(answer).to eq(["First line\n", "Second line\n", "Third line\n"])
29
36
  end
30
37
 
38
+ it "skips empty lines" do
39
+ input << "\n\nFirst line\n\n\n\n\nSecond line\C-d"
40
+ input.rewind
41
+ answer = reader.read_multiline
42
+ expect(answer).to eq(["First line\n", "Second line"])
43
+ end
44
+
31
45
  it 'reads and yiels every line' do
32
- input << "First line\nSecond line\nThird line"
46
+ input << "First line\nSecond line\nThird line\C-z"
33
47
  input.rewind
34
48
  lines = []
35
49
  reader.read_multiline { |line| lines << line }
@@ -37,7 +51,7 @@ RSpec.describe TTY::Prompt::Reader, '#read_multiline' do
37
51
  end
38
52
 
39
53
  it 'reads multibyte lines' do
40
- input << "국경의 긴 터널을 빠져나오자\n설국이었다."
54
+ input << "국경의 긴 터널을 빠져나오자\n설국이었다.\C-d"
41
55
  input.rewind
42
56
  lines = []
43
57
  reader.read_multiline { |line| lines << line }
@@ -51,7 +51,7 @@ RSpec.describe TTY::Prompt, '#select' do
51
51
  choices = {large: 1, medium: 2, small: 3}
52
52
  prompt.input << " "
53
53
  prompt.input.rewind
54
- expect(prompt.select('What size?', choices)).to eq(1)
54
+ expect(prompt.select('What size?', choices, default: 1)).to eq(1)
55
55
  expect(prompt.output.string).to eq([
56
56
  "\e[?25lWhat size? \e[90m(Use arrow keys, press Enter to select)\e[0m\n",
57
57
  "\e[32m#{symbols[:pointer]} large\e[0m\n",
@@ -216,6 +216,26 @@ RSpec.describe TTY::Prompt, '#select' do
216
216
  ].join)
217
217
  end
218
218
 
219
+ it "changes help text through DSL" do
220
+ choices = %w(Large Medium Small)
221
+ prompt.input << " "
222
+ prompt.input.rewind
223
+ value = prompt.select('What size?') do |menu|
224
+ menu.help "(Bash keyboard)"
225
+ menu.choices choices
226
+ end
227
+ expect(value).to eq('Large')
228
+ expect(prompt.output.string).to eq([
229
+ "\e[?25lWhat size? \e[90m(Bash keyboard)\e[0m\n",
230
+ "\e[32m#{symbols[:pointer]} Large\e[0m\n",
231
+ " Medium\n",
232
+ " Small",
233
+ "\e[2K\e[1G\e[1A" * 3,
234
+ "\e[2K\e[1G",
235
+ "What size? \e[32mLarge\e[0m\n\e[?25h"
236
+ ].join)
237
+ end
238
+
219
239
  it "sets prompt prefix" do
220
240
  prompt = TTY::TestPrompt.new(prefix: '[?] ')
221
241
  choices = %w(Large Medium Small)
@@ -251,6 +271,25 @@ RSpec.describe TTY::Prompt, '#select' do
251
271
  ].join)
252
272
  end
253
273
 
274
+ it "paginates choices as hash object" do
275
+ prompt = TTY::TestPrompt.new
276
+ choices = {A: 1, B: 2, C: 3, D: 4, E: 5, F: 6, G: 7, H: 8}
277
+ prompt.input << "\r"
278
+ prompt.input.rewind
279
+ value = prompt.select("What letter?", choices, per_page: 3, default: 4)
280
+ expect(value).to eq(4)
281
+ expect(prompt.output.string).to eq([
282
+ "\e[?25lWhat letter? \e[90m(Use arrow keys, press Enter to select)\e[0m\n",
283
+ "\e[32m#{symbols[:pointer]} D\e[0m\n",
284
+ " E\n",
285
+ " F\n",
286
+ "\e[90m(Move up or down to reveal more choices)\e[0m",
287
+ "\e[2K\e[1G\e[1A" * 4,
288
+ "\e[2K\e[1G",
289
+ "What letter? \e[32mD\e[0m\n\e[?25h",
290
+ ].join)
291
+ end
292
+
254
293
  it "paginates long selections through DSL" do
255
294
  prompt = TTY::TestPrompt.new
256
295
  choices = %w(A B C D E F G H)