tty-prompt 0.11.0 → 0.12.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.
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)