cli-ui 1.5.0 → 2.0.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/README.md +17 -17
- data/lib/cli/ui/ansi.rb +157 -129
- data/lib/cli/ui/color.rb +39 -20
- data/lib/cli/ui/formatter.rb +45 -21
- data/lib/cli/ui/frame/frame_stack.rb +32 -13
- data/lib/cli/ui/frame/frame_style/box.rb +15 -4
- data/lib/cli/ui/frame/frame_style/bracket.rb +18 -7
- data/lib/cli/ui/frame/frame_style.rb +84 -87
- data/lib/cli/ui/frame.rb +55 -24
- data/lib/cli/ui/glyph.rb +44 -31
- data/lib/cli/ui/os.rb +44 -48
- data/lib/cli/ui/printer.rb +65 -47
- data/lib/cli/ui/progress.rb +49 -32
- data/lib/cli/ui/prompt/interactive_options.rb +91 -44
- data/lib/cli/ui/prompt/options_handler.rb +8 -0
- data/lib/cli/ui/prompt.rb +84 -31
- data/lib/cli/ui/sorbet_runtime_stub.rb +157 -0
- data/lib/cli/ui/spinner/async.rb +15 -4
- data/lib/cli/ui/spinner/spin_group.rb +67 -11
- data/lib/cli/ui/spinner.rb +48 -28
- data/lib/cli/ui/stdout_router.rb +71 -34
- data/lib/cli/ui/terminal.rb +37 -25
- data/lib/cli/ui/truncater.rb +7 -2
- data/lib/cli/ui/version.rb +3 -1
- data/lib/cli/ui/widgets/base.rb +23 -4
- data/lib/cli/ui/widgets/status.rb +19 -1
- data/lib/cli/ui/widgets.rb +42 -23
- data/lib/cli/ui/wrap.rb +8 -1
- data/lib/cli/ui.rb +325 -188
- metadata +10 -21
- data/.dependabot/config.yml +0 -8
- data/.github/CODEOWNERS +0 -1
- data/.github/probots.yml +0 -2
- data/.gitignore +0 -14
- data/.rubocop.yml +0 -41
- data/.travis.yml +0 -7
- data/Gemfile +0 -17
- data/Gemfile.lock +0 -60
- data/Rakefile +0 -20
- data/bin/console +0 -14
- data/cli-ui.gemspec +0 -27
- data/dev.yml +0 -14
@@ -1,34 +1,48 @@
|
|
1
1
|
# coding: utf-8
|
2
|
+
|
3
|
+
# typed: true
|
4
|
+
|
2
5
|
require 'io/console'
|
3
6
|
|
4
7
|
module CLI
|
5
8
|
module UI
|
6
9
|
module Prompt
|
7
10
|
class InteractiveOptions
|
11
|
+
extend T::Sig
|
12
|
+
|
8
13
|
DONE = 'Done'
|
9
14
|
CHECKBOX_ICON = { false => '☐', true => '☑' }
|
10
15
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
16
|
+
class << self
|
17
|
+
extend T::Sig
|
18
|
+
|
19
|
+
# Prompts the user with options
|
20
|
+
# Uses an interactive session to allow the user to pick an answer
|
21
|
+
# Can use arrows, y/n, numbers (1/2), and vim bindings to control
|
22
|
+
# For more than 9 options, hitting 'e', ':', or 'G' will enter select
|
23
|
+
# mode allowing the user to type in longer numbers
|
24
|
+
# Pressing 'f' or '/' will allow the user to filter the results
|
25
|
+
#
|
26
|
+
# https://user-images.githubusercontent.com/3074765/33797984-0ebb5e64-dcdf-11e7-9e7e-7204f279cece.gif
|
27
|
+
#
|
28
|
+
# ==== Example Usage:
|
29
|
+
#
|
30
|
+
# Ask an interactive question
|
31
|
+
# CLI::UI::Prompt::InteractiveOptions.call(%w(rails go python))
|
32
|
+
#
|
33
|
+
sig do
|
34
|
+
params(options: T::Array[String], multiple: T::Boolean, default: T.nilable(T.any(String, T::Array[String])))
|
35
|
+
.returns(T.any(String, T::Array[String]))
|
36
|
+
end
|
37
|
+
def call(options, multiple: false, default: nil)
|
38
|
+
list = new(options, multiple: multiple, default: default)
|
39
|
+
selected = list.call
|
40
|
+
case selected
|
41
|
+
when Array
|
42
|
+
selected.map { |s| T.must(options[s - 1]) }
|
43
|
+
else
|
44
|
+
T.must(options[selected - 1])
|
45
|
+
end
|
32
46
|
end
|
33
47
|
end
|
34
48
|
|
@@ -39,6 +53,10 @@ module CLI
|
|
39
53
|
#
|
40
54
|
# CLI::UI::Prompt::InteractiveOptions.new(%w(rails go python))
|
41
55
|
#
|
56
|
+
sig do
|
57
|
+
params(options: T::Array[String], multiple: T::Boolean, default: T.nilable(T.any(String, T::Array[String])))
|
58
|
+
.void
|
59
|
+
end
|
42
60
|
def initialize(options, multiple: false, default: nil)
|
43
61
|
@options = options
|
44
62
|
@active = 1
|
@@ -60,12 +78,13 @@ module CLI
|
|
60
78
|
end
|
61
79
|
end
|
62
80
|
@redraw = true
|
63
|
-
@presented_options = []
|
81
|
+
@presented_options = T.let([], T::Array[[String, T.nilable(Integer)]])
|
64
82
|
end
|
65
83
|
|
66
84
|
# Calls the +InteractiveOptions+ and asks the question
|
67
85
|
# Usually used from +self.call+
|
68
86
|
#
|
87
|
+
sig { returns(T.any(Integer, T::Array[Integer])) }
|
69
88
|
def call
|
70
89
|
calculate_option_line_lengths
|
71
90
|
CLI::UI.raw { print(ANSI.hide_cursor) }
|
@@ -85,6 +104,7 @@ module CLI
|
|
85
104
|
|
86
105
|
private
|
87
106
|
|
107
|
+
sig { void }
|
88
108
|
def calculate_option_line_lengths
|
89
109
|
@terminal_width_at_calculation_time = CLI::UI::Terminal.width
|
90
110
|
# options will be an array of questions but each option can be multi-line
|
@@ -103,13 +123,13 @@ module CLI
|
|
103
123
|
width ||= text
|
104
124
|
.split("\n")
|
105
125
|
.reject(&:empty?)
|
106
|
-
.
|
107
|
-
.reduce(&:+)
|
126
|
+
.sum { |l| (CLI::UI.fmt(l, enable_color: false).length / max_width).ceil }
|
108
127
|
|
109
128
|
width
|
110
129
|
end
|
111
130
|
end
|
112
131
|
|
132
|
+
sig { params(number_of_lines: Integer).void }
|
113
133
|
def reset_position(number_of_lines = num_lines)
|
114
134
|
# This will put us back at the beginning of the options
|
115
135
|
# When we redraw the options, they will be overwritten
|
@@ -118,6 +138,7 @@ module CLI
|
|
118
138
|
end
|
119
139
|
end
|
120
140
|
|
141
|
+
sig { params(number_of_lines: Integer).void }
|
121
142
|
def clear_output(number_of_lines = num_lines)
|
122
143
|
CLI::UI.raw do
|
123
144
|
# Write over all lines with whitespace
|
@@ -133,22 +154,26 @@ module CLI
|
|
133
154
|
|
134
155
|
# Don't use this in place of +@displaying_metadata+, this updates too
|
135
156
|
# quickly to be useful when drawing to the screen.
|
157
|
+
sig { returns(T::Boolean) }
|
136
158
|
def display_metadata?
|
137
159
|
filtering? || selecting? || has_filter?
|
138
160
|
end
|
139
161
|
|
162
|
+
sig { returns(Integer) }
|
140
163
|
def num_lines
|
141
164
|
calculate_option_line_lengths if terminal_width_changed?
|
142
165
|
|
143
166
|
option_length = presented_options.reduce(0) do |total_length, (_, option_number)|
|
144
167
|
# Handle continuation markers and "Done" option when multiple is true
|
145
168
|
next total_length + 1 if option_number.nil? || option_number.zero?
|
169
|
+
|
146
170
|
total_length + @option_lengths[option_number - 1]
|
147
171
|
end
|
148
172
|
|
149
173
|
option_length + (@displaying_metadata ? 1 : 0)
|
150
174
|
end
|
151
175
|
|
176
|
+
sig { returns(T::Boolean) }
|
152
177
|
def terminal_width_changed?
|
153
178
|
@terminal_width_at_calculation_time != CLI::UI::Terminal.width
|
154
179
|
end
|
@@ -158,6 +183,7 @@ module CLI
|
|
158
183
|
CTRL_C = "\u0003"
|
159
184
|
CTRL_D = "\u0004"
|
160
185
|
|
186
|
+
sig { void }
|
161
187
|
def up
|
162
188
|
active_index = @filtered_options.index { |_, num| num == @active } || 0
|
163
189
|
|
@@ -168,6 +194,7 @@ module CLI
|
|
168
194
|
@redraw = true
|
169
195
|
end
|
170
196
|
|
197
|
+
sig { void }
|
171
198
|
def down
|
172
199
|
active_index = @filtered_options.index { |_, num| num == @active } || 0
|
173
200
|
|
@@ -180,6 +207,7 @@ module CLI
|
|
180
207
|
|
181
208
|
# n is 1-indexed selection
|
182
209
|
# n == 0 if "Done" was selected in @multiple mode
|
210
|
+
sig { params(n: Integer).void }
|
183
211
|
def select_n(n)
|
184
212
|
if @multiple
|
185
213
|
if n == 0
|
@@ -200,24 +228,29 @@ module CLI
|
|
200
228
|
@redraw = true
|
201
229
|
end
|
202
230
|
|
231
|
+
sig { params(char: String).void }
|
203
232
|
def select_bool(char)
|
204
|
-
return unless (@options -
|
205
|
-
|
206
|
-
|
207
|
-
@
|
233
|
+
return unless (@options - ['yes', 'no']).empty?
|
234
|
+
|
235
|
+
index = T.must(@options.index { |o| o.start_with?(char) })
|
236
|
+
@active = index + 1
|
237
|
+
@answer = index + 1
|
208
238
|
@redraw = true
|
209
239
|
end
|
210
240
|
|
241
|
+
sig { params(char: String).void }
|
211
242
|
def build_selection(char)
|
212
243
|
@active = (@active.to_s + char).to_i
|
213
244
|
@redraw = true
|
214
245
|
end
|
215
246
|
|
247
|
+
sig { void }
|
216
248
|
def chop_selection
|
217
249
|
@active = @active.to_s.chop.to_i
|
218
250
|
@redraw = true
|
219
251
|
end
|
220
252
|
|
253
|
+
sig { params(char: String).void }
|
221
254
|
def update_search(char)
|
222
255
|
@redraw = true
|
223
256
|
|
@@ -235,25 +268,28 @@ module CLI
|
|
235
268
|
end
|
236
269
|
end
|
237
270
|
|
271
|
+
sig { void }
|
238
272
|
def select_current
|
239
273
|
# Prevent selection of invisible options
|
240
274
|
return unless presented_options.any? { |_, num| num == @active }
|
275
|
+
|
241
276
|
select_n(@active)
|
242
277
|
end
|
243
278
|
|
279
|
+
sig { void }
|
244
280
|
def process_input_until_redraw_required
|
245
281
|
@redraw = false
|
246
282
|
wait_for_user_input until @redraw
|
247
283
|
end
|
248
284
|
|
249
285
|
# rubocop:disable Style/WhenThen,Layout/SpaceBeforeSemicolon,Style/Semicolon
|
286
|
+
sig { void }
|
250
287
|
def wait_for_user_input
|
251
288
|
char = read_char
|
252
289
|
@last_char = char
|
253
290
|
|
254
291
|
case char
|
255
|
-
when
|
256
|
-
when CTRL_C ; raise Interrupt
|
292
|
+
when CTRL_C, nil ; raise Interrupt
|
257
293
|
end
|
258
294
|
|
259
295
|
max_digit = [@options.size, 9].min.to_s
|
@@ -302,51 +338,59 @@ module CLI
|
|
302
338
|
end
|
303
339
|
end
|
304
340
|
end
|
305
|
-
# rubocop:enable Style/WhenThen,Layout/SpaceBeforeSemicolon
|
341
|
+
# rubocop:enable Style/WhenThen,Layout/SpaceBeforeSemicolon,Style/Semicolon
|
306
342
|
|
343
|
+
sig { returns(T::Boolean) }
|
307
344
|
def selecting?
|
308
345
|
@state == :line_select
|
309
346
|
end
|
310
347
|
|
348
|
+
sig { returns(T::Boolean) }
|
311
349
|
def filtering?
|
312
350
|
@state == :filter
|
313
351
|
end
|
314
352
|
|
353
|
+
sig { returns(T::Boolean) }
|
315
354
|
def has_filter?
|
316
355
|
!@filter.empty?
|
317
356
|
end
|
318
357
|
|
358
|
+
sig { void }
|
319
359
|
def start_filter
|
320
360
|
@state = :filter
|
321
361
|
@redraw = true
|
322
362
|
end
|
323
363
|
|
364
|
+
sig { void }
|
324
365
|
def start_line_select
|
325
366
|
@state = :line_select
|
326
367
|
@active = 0
|
327
368
|
@redraw = true
|
328
369
|
end
|
329
370
|
|
371
|
+
sig { void }
|
330
372
|
def stop_line_select
|
331
373
|
@state = :root
|
332
374
|
@active = 1 if @active.zero?
|
333
375
|
@redraw = true
|
334
376
|
end
|
335
377
|
|
378
|
+
sig { returns(T.nilable(String)) }
|
336
379
|
def read_char
|
337
380
|
if $stdin.tty? && !ENV['TEST']
|
338
381
|
$stdin.getch # raw mode for tty
|
339
382
|
else
|
340
|
-
$stdin.getc
|
383
|
+
$stdin.getc # returns nil at end of input
|
341
384
|
end
|
342
|
-
rescue IOError
|
385
|
+
rescue Errno::EIO, Errno::EPIPE, IOError
|
343
386
|
"\e"
|
344
387
|
end
|
345
388
|
|
389
|
+
sig { params(recalculate: T::Boolean).returns(T::Array[[String, T.nilable(Integer)]]) }
|
346
390
|
def presented_options(recalculate: false)
|
347
391
|
return @presented_options unless recalculate
|
348
392
|
|
349
|
-
@presented_options = @options.zip(1..
|
393
|
+
@presented_options = @options.zip(1..)
|
350
394
|
if has_filter?
|
351
395
|
@presented_options.select! { |option, _| option.downcase.include?(@filter.downcase) }
|
352
396
|
end
|
@@ -385,36 +429,44 @@ module CLI
|
|
385
429
|
@presented_options
|
386
430
|
end
|
387
431
|
|
432
|
+
sig { void }
|
388
433
|
def ensure_visible_is_active
|
389
434
|
unless presented_options.any? { |_, num| num == @active }
|
390
435
|
@active = presented_options.first&.last.to_i
|
391
436
|
end
|
392
437
|
end
|
393
438
|
|
439
|
+
sig { returns(Integer) }
|
394
440
|
def distance_from_selection_to_end
|
395
441
|
@presented_options.count - index_of_active_option
|
396
442
|
end
|
397
443
|
|
444
|
+
sig { returns(Integer) }
|
398
445
|
def distance_from_start_to_selection
|
399
446
|
index_of_active_option
|
400
447
|
end
|
401
448
|
|
449
|
+
sig { returns(Integer) }
|
402
450
|
def index_of_active_option
|
403
451
|
@presented_options.index { |_, num| num == @active }.to_i
|
404
452
|
end
|
405
453
|
|
454
|
+
sig { void }
|
406
455
|
def ensure_last_item_is_continuation_marker
|
407
|
-
@presented_options.push(['...', nil]) if @presented_options.last
|
456
|
+
@presented_options.push(['...', nil]) if @presented_options.last&.last
|
408
457
|
end
|
409
458
|
|
459
|
+
sig { void }
|
410
460
|
def ensure_first_item_is_continuation_marker
|
411
|
-
@presented_options.unshift(['...', nil]) if @presented_options.first
|
461
|
+
@presented_options.unshift(['...', nil]) if @presented_options.first&.last
|
412
462
|
end
|
413
463
|
|
464
|
+
sig { returns(Integer) }
|
414
465
|
def max_lines
|
415
466
|
CLI::UI::Terminal.height - (@displaying_metadata ? 3 : 2) # Keeps a one line question visible
|
416
467
|
end
|
417
468
|
|
469
|
+
sig { void }
|
418
470
|
def render_options
|
419
471
|
previously_displayed_lines = num_lines
|
420
472
|
|
@@ -436,11 +488,7 @@ module CLI
|
|
436
488
|
"Filter: #{filter_text}"
|
437
489
|
end
|
438
490
|
|
439
|
-
if metadata_text
|
440
|
-
CLI::UI.with_frame_color(:blue) do
|
441
|
-
puts CLI::UI.fmt(" {{green:#{metadata_text}}}#{ANSI.clear_to_end_of_line}")
|
442
|
-
end
|
443
|
-
end
|
491
|
+
puts CLI::UI.fmt(" {{green:#{metadata_text}}}#{ANSI.clear_to_end_of_line}") if metadata_text
|
444
492
|
|
445
493
|
options.each do |choice, num|
|
446
494
|
is_chosen = @multiple && num && @chosen[num - 1] && num != 0
|
@@ -463,12 +511,11 @@ module CLI
|
|
463
511
|
message = message.split("\n").map { |l| "{{#{color}:> #{l.strip}}}" }.join("\n")
|
464
512
|
end
|
465
513
|
|
466
|
-
CLI::UI.
|
467
|
-
puts CLI::UI.fmt(message)
|
468
|
-
end
|
514
|
+
puts CLI::UI.fmt(message)
|
469
515
|
end
|
470
516
|
end
|
471
517
|
|
518
|
+
sig { params(format: String, choice: String).returns(String) }
|
472
519
|
def format_choice(format, choice)
|
473
520
|
eol = CLI::UI::ANSI.clear_to_end_of_line
|
474
521
|
lines = choice.split("\n")
|
@@ -1,20 +1,28 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
1
3
|
module CLI
|
2
4
|
module UI
|
3
5
|
module Prompt
|
4
6
|
# A class that handles the various options of an InteractivePrompt and their callbacks
|
5
7
|
class OptionsHandler
|
8
|
+
extend T::Sig
|
9
|
+
|
10
|
+
sig { void }
|
6
11
|
def initialize
|
7
12
|
@options = {}
|
8
13
|
end
|
9
14
|
|
15
|
+
sig { returns(T::Array[String]) }
|
10
16
|
def options
|
11
17
|
@options.keys
|
12
18
|
end
|
13
19
|
|
20
|
+
sig { params(option: String, handler: T.proc.params(option: String).returns(String)).void }
|
14
21
|
def option(option, &handler)
|
15
22
|
@options[option] = handler
|
16
23
|
end
|
17
24
|
|
25
|
+
sig { params(options: T.any(T::Array[String], String)).returns(String) }
|
18
26
|
def call(options)
|
19
27
|
case options
|
20
28
|
when Array
|
data/lib/cli/ui/prompt.rb
CHANGED
@@ -1,4 +1,7 @@
|
|
1
1
|
# coding: utf-8
|
2
|
+
|
3
|
+
# typed: true
|
4
|
+
|
2
5
|
require 'cli/ui'
|
3
6
|
require 'readline'
|
4
7
|
|
@@ -22,9 +25,10 @@ module CLI
|
|
22
25
|
module Prompt
|
23
26
|
autoload :InteractiveOptions, 'cli/ui/prompt/interactive_options'
|
24
27
|
autoload :OptionsHandler, 'cli/ui/prompt/options_handler'
|
25
|
-
private_constant :InteractiveOptions, :OptionsHandler
|
26
28
|
|
27
29
|
class << self
|
30
|
+
extend T::Sig
|
31
|
+
|
28
32
|
# Ask a user a question with either free form answer or a set of answers (multiple choice)
|
29
33
|
# Can use arrows, y/n, numbers (1/2), and vim bindings to control multiple choice selection
|
30
34
|
# Do not use this method for yes/no questions. Use +confirm+
|
@@ -92,26 +96,52 @@ module CLI
|
|
92
96
|
# handler.option('python') { |selection| selection }
|
93
97
|
# end
|
94
98
|
#
|
99
|
+
sig do
|
100
|
+
params(
|
101
|
+
question: String,
|
102
|
+
options: T.nilable(T::Array[String]),
|
103
|
+
default: T.nilable(T.any(String, T::Array[String])),
|
104
|
+
is_file: T::Boolean,
|
105
|
+
allow_empty: T::Boolean,
|
106
|
+
multiple: T::Boolean,
|
107
|
+
filter_ui: T::Boolean,
|
108
|
+
select_ui: T::Boolean,
|
109
|
+
options_proc: T.nilable(T.proc.params(handler: OptionsHandler).void),
|
110
|
+
).returns(T.any(String, T::Array[String]))
|
111
|
+
end
|
95
112
|
def ask(
|
96
113
|
question,
|
97
114
|
options: nil,
|
98
115
|
default: nil,
|
99
|
-
is_file:
|
116
|
+
is_file: false,
|
100
117
|
allow_empty: true,
|
101
118
|
multiple: false,
|
102
119
|
filter_ui: true,
|
103
120
|
select_ui: true,
|
104
121
|
&options_proc
|
105
122
|
)
|
106
|
-
|
107
|
-
|
123
|
+
has_options = !!(options || block_given?)
|
124
|
+
if has_options && default && !multiple
|
125
|
+
raise(ArgumentError, 'conflicting arguments: default may not be provided with options when not multiple')
|
126
|
+
end
|
127
|
+
|
128
|
+
if has_options && is_file
|
129
|
+
raise(ArgumentError, 'conflicting arguments: is_file is only useful when options are not provided')
|
108
130
|
end
|
109
131
|
|
110
|
-
if options && multiple && default && !(default - options).empty?
|
132
|
+
if options && multiple && default && !(Array(default) - options).empty?
|
111
133
|
raise(ArgumentError, 'conflicting arguments: default should only include elements present in options')
|
112
134
|
end
|
113
135
|
|
114
|
-
if
|
136
|
+
if multiple && !has_options
|
137
|
+
raise(ArgumentError, 'conflicting arguments: options must be provided when multiple is true')
|
138
|
+
end
|
139
|
+
|
140
|
+
if !multiple && default.is_a?(Array)
|
141
|
+
raise(ArgumentError, 'conflicting arguments: multiple defaults may only be provided when multiple is true')
|
142
|
+
end
|
143
|
+
|
144
|
+
if has_options
|
115
145
|
ask_interactive(
|
116
146
|
question,
|
117
147
|
options,
|
@@ -122,7 +152,7 @@ module CLI
|
|
122
152
|
&options_proc
|
123
153
|
)
|
124
154
|
else
|
125
|
-
ask_free_form(question, default, is_file, allow_empty)
|
155
|
+
ask_free_form(question, T.cast(default, T.nilable(String)), is_file, allow_empty)
|
126
156
|
end
|
127
157
|
end
|
128
158
|
|
@@ -133,24 +163,23 @@ module CLI
|
|
133
163
|
#
|
134
164
|
# The password, without a trailing newline.
|
135
165
|
# If the user simply presses "Enter" without typing any password, this will return an empty string.
|
166
|
+
sig { params(question: String).returns(String) }
|
136
167
|
def ask_password(question)
|
137
168
|
require 'io/console'
|
138
169
|
|
139
|
-
CLI::UI.
|
140
|
-
STDOUT.print(CLI::UI.fmt('{{?}} ' + question)) # Do not use puts_question to avoid the new line.
|
170
|
+
STDOUT.print(CLI::UI.fmt('{{?}} ' + question)) # Do not use puts_question to avoid the new line.
|
141
171
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
172
|
+
# noecho interacts poorly with Readline under system Ruby, so do a manual `gets` here.
|
173
|
+
# No fancy Readline integration (like echoing back) is required for a password prompt anyway.
|
174
|
+
password = STDIN.noecho do
|
175
|
+
# Chomp will remove the one new line character added by `gets`, without touching potential extra spaces:
|
176
|
+
# " 123 \n".chomp => " 123 "
|
177
|
+
STDIN.gets.to_s.chomp
|
178
|
+
end
|
149
179
|
|
150
|
-
|
180
|
+
STDOUT.puts # Complete the line
|
151
181
|
|
152
|
-
|
153
|
-
end
|
182
|
+
password
|
154
183
|
end
|
155
184
|
|
156
185
|
# Asks the user a yes/no question.
|
@@ -163,12 +192,17 @@ module CLI
|
|
163
192
|
#
|
164
193
|
# CLI::UI::Prompt.confirm('Do a dangerous thing?', default: false)
|
165
194
|
#
|
195
|
+
sig { params(question: String, default: T::Boolean).returns(T::Boolean) }
|
166
196
|
def confirm(question, default: true)
|
167
|
-
ask_interactive(question, default ?
|
197
|
+
ask_interactive(question, default ? ['yes', 'no'] : ['no', 'yes'], filter_ui: false) == 'yes'
|
168
198
|
end
|
169
199
|
|
170
200
|
private
|
171
201
|
|
202
|
+
sig do
|
203
|
+
params(question: String, default: T.nilable(String), is_file: T::Boolean, allow_empty: T::Boolean)
|
204
|
+
.returns(String)
|
205
|
+
end
|
172
206
|
def ask_free_form(question, default, is_file, allow_empty)
|
173
207
|
if default && !allow_empty
|
174
208
|
raise(ArgumentError, 'conflicting arguments: default enabled but allow_empty is false')
|
@@ -195,6 +229,16 @@ module CLI
|
|
195
229
|
end
|
196
230
|
end
|
197
231
|
|
232
|
+
sig do
|
233
|
+
params(
|
234
|
+
question: String,
|
235
|
+
options: T.nilable(T::Array[String]),
|
236
|
+
multiple: T::Boolean,
|
237
|
+
default: T.nilable(T.any(String, T::Array[String])),
|
238
|
+
filter_ui: T::Boolean,
|
239
|
+
select_ui: T::Boolean,
|
240
|
+
).returns(T.any(String, T::Array[String]))
|
241
|
+
end
|
198
242
|
def ask_interactive(question, options = nil, multiple: false, default: nil, filter_ui: true, select_ui: true)
|
199
243
|
raise(ArgumentError, 'conflicting arguments: options and block given') if options && block_given?
|
200
244
|
|
@@ -205,7 +249,8 @@ module CLI
|
|
205
249
|
end
|
206
250
|
|
207
251
|
raise(ArgumentError, 'insufficient options') if options.nil? || options.empty?
|
208
|
-
|
252
|
+
|
253
|
+
navigate_text = if CLI::UI::OS.current.suggest_arrow_keys?
|
209
254
|
'Choose with ↑ ↓ ⏎'
|
210
255
|
else
|
211
256
|
"Navigate up with 'k' and down with 'j', press Enter to select"
|
@@ -223,9 +268,9 @@ module CLI
|
|
223
268
|
print(ANSI.previous_line + "\n")
|
224
269
|
|
225
270
|
# reset the question to include the answer
|
226
|
-
resp_text = resp
|
227
|
-
|
228
|
-
|
271
|
+
resp_text = case resp
|
272
|
+
when Array
|
273
|
+
case resp.size
|
229
274
|
when 0
|
230
275
|
'<nothing>'
|
231
276
|
when 1..2
|
@@ -233,18 +278,26 @@ module CLI
|
|
233
278
|
else
|
234
279
|
"#{resp.size} items"
|
235
280
|
end
|
281
|
+
else
|
282
|
+
resp
|
236
283
|
end
|
237
284
|
puts_question("#{question} (You chose: {{italic:#{resp_text}}})")
|
238
285
|
|
239
|
-
return handler.call(resp) if block_given?
|
286
|
+
return T.must(handler).call(resp) if block_given?
|
287
|
+
|
240
288
|
resp
|
241
289
|
end
|
242
290
|
|
243
291
|
# Useful for stubbing in tests
|
292
|
+
sig do
|
293
|
+
params(options: T::Array[String], multiple: T::Boolean, default: T.nilable(T.any(T::Array[String], String)))
|
294
|
+
.returns(T.any(T::Array[String], String))
|
295
|
+
end
|
244
296
|
def interactive_prompt(options, multiple: false, default: nil)
|
245
297
|
InteractiveOptions.call(options, multiple: multiple, default: default)
|
246
298
|
end
|
247
299
|
|
300
|
+
sig { params(default: String).void }
|
248
301
|
def write_default_over_empty_input(default)
|
249
302
|
CLI::UI.raw do
|
250
303
|
STDERR.puts(
|
@@ -252,17 +305,17 @@ module CLI
|
|
252
305
|
"\r" +
|
253
306
|
CLI::UI::ANSI.cursor_forward(4) + # TODO: width
|
254
307
|
default +
|
255
|
-
CLI::UI::Color::RESET.code
|
308
|
+
CLI::UI::Color::RESET.code,
|
256
309
|
)
|
257
310
|
end
|
258
311
|
end
|
259
312
|
|
313
|
+
sig { params(str: String).void }
|
260
314
|
def puts_question(str)
|
261
|
-
CLI::UI.
|
262
|
-
STDOUT.puts(CLI::UI.fmt('{{?}} ' + str))
|
263
|
-
end
|
315
|
+
STDOUT.puts(CLI::UI.fmt('{{?}} ' + str))
|
264
316
|
end
|
265
317
|
|
318
|
+
sig { params(is_file: T::Boolean).returns(String) }
|
266
319
|
def readline(is_file: false)
|
267
320
|
if is_file
|
268
321
|
Readline.completion_proc = Readline::FILENAME_COMPLETION_PROC
|
@@ -276,11 +329,11 @@ module CLI
|
|
276
329
|
# work. We could work around this by having CLI::UI use a pipe and a
|
277
330
|
# thread to manage output, but the current strategy feels like a
|
278
331
|
# better tradeoff.
|
279
|
-
prefix = CLI::UI
|
332
|
+
prefix = CLI::UI::Frame.prefix
|
280
333
|
# If a prompt is interrupted on Windows it locks the colour of the terminal from that point on, so we should
|
281
334
|
# not change the colour here.
|
282
335
|
prompt = prefix + CLI::UI.fmt('{{blue:> }}')
|
283
|
-
prompt += CLI::UI::Color::YELLOW.code if CLI::UI::OS.current.
|
336
|
+
prompt += CLI::UI::Color::YELLOW.code if CLI::UI::OS.current.use_color_prompt?
|
284
337
|
|
285
338
|
begin
|
286
339
|
line = Readline.readline(prompt, true)
|