tty2-prompt 0.23.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +14 -0
  3. data/LICENSE.txt +23 -0
  4. data/README.md +52 -0
  5. data/lib/tty2/prompt/answers_collector.rb +78 -0
  6. data/lib/tty2/prompt/block_paginator.rb +59 -0
  7. data/lib/tty2/prompt/choice.rb +147 -0
  8. data/lib/tty2/prompt/choices.rb +129 -0
  9. data/lib/tty2/prompt/confirm_question.rb +158 -0
  10. data/lib/tty2/prompt/const.rb +17 -0
  11. data/lib/tty2/prompt/converter_dsl.rb +21 -0
  12. data/lib/tty2/prompt/converter_registry.rb +69 -0
  13. data/lib/tty2/prompt/converters.rb +182 -0
  14. data/lib/tty2/prompt/distance.rb +49 -0
  15. data/lib/tty2/prompt/enum_list.rb +433 -0
  16. data/lib/tty2/prompt/errors.rb +31 -0
  17. data/lib/tty2/prompt/evaluator.rb +29 -0
  18. data/lib/tty2/prompt/expander.rb +321 -0
  19. data/lib/tty2/prompt/keypress.rb +98 -0
  20. data/lib/tty2/prompt/list.rb +589 -0
  21. data/lib/tty2/prompt/mask_question.rb +96 -0
  22. data/lib/tty2/prompt/multi_list.rb +224 -0
  23. data/lib/tty2/prompt/multiline.rb +72 -0
  24. data/lib/tty2/prompt/paginator.rb +111 -0
  25. data/lib/tty2/prompt/question/checks.rb +105 -0
  26. data/lib/tty2/prompt/question/modifier.rb +96 -0
  27. data/lib/tty2/prompt/question/validation.rb +72 -0
  28. data/lib/tty2/prompt/question.rb +391 -0
  29. data/lib/tty2/prompt/result.rb +42 -0
  30. data/lib/tty2/prompt/selected_choices.rb +77 -0
  31. data/lib/tty2/prompt/slider.rb +286 -0
  32. data/lib/tty2/prompt/statement.rb +55 -0
  33. data/lib/tty2/prompt/suggestion.rb +113 -0
  34. data/lib/tty2/prompt/symbols.rb +89 -0
  35. data/lib/tty2/prompt/test.rb +36 -0
  36. data/lib/tty2/prompt/timer.rb +75 -0
  37. data/lib/tty2/prompt/utils.rb +42 -0
  38. data/lib/tty2/prompt/version.rb +7 -0
  39. data/lib/tty2/prompt.rb +589 -0
  40. data/lib/tty2-prompt.rb +1 -0
  41. metadata +148 -0
@@ -0,0 +1,589 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+ require "pastel"
5
+ require "tty-cursor"
6
+ require "tty2-reader"
7
+ require "tty-screen"
8
+
9
+ require_relative "prompt/answers_collector"
10
+ require_relative "prompt/confirm_question"
11
+ require_relative "prompt/errors"
12
+ require_relative "prompt/expander"
13
+ require_relative "prompt/enum_list"
14
+ require_relative "prompt/keypress"
15
+ require_relative "prompt/list"
16
+ require_relative "prompt/multi_list"
17
+ require_relative "prompt/multiline"
18
+ require_relative "prompt/mask_question"
19
+ require_relative "prompt/question"
20
+ require_relative "prompt/slider"
21
+ require_relative "prompt/statement"
22
+ require_relative "prompt/suggestion"
23
+ require_relative "prompt/symbols"
24
+ require_relative "prompt/utils"
25
+ require_relative "prompt/version"
26
+
27
+ module TTY2
28
+ # A main entry for asking prompt questions.
29
+ class Prompt
30
+ extend Forwardable
31
+
32
+ # @api private
33
+ attr_reader :input
34
+
35
+ # @api private
36
+ attr_reader :output
37
+
38
+ attr_reader :reader
39
+
40
+ attr_reader :cursor
41
+
42
+ # Prompt prefix
43
+ #
44
+ # @example
45
+ # prompt = TTY2::Prompt.new(prefix: [?])
46
+ #
47
+ # @return [String]
48
+ #
49
+ # @api private
50
+ attr_reader :prefix
51
+
52
+ # Theme colors
53
+ #
54
+ # @api private
55
+ attr_reader :active_color, :help_color, :error_color, :enabled_color
56
+
57
+ # Quiet mode
58
+ #
59
+ # @api private
60
+ attr_reader :quiet
61
+
62
+ # The collection of display symbols
63
+ #
64
+ # @example
65
+ # prompt = TTY2::Prompt.new(symbols: {marker: ">"})
66
+ #
67
+ # @return [Hash]
68
+ #
69
+ # @api private
70
+ attr_reader :symbols
71
+
72
+ def_delegators :@pastel, :strip
73
+
74
+ def_delegators :@cursor, :clear_lines, :clear_line,
75
+ :show, :hide
76
+
77
+ def_delegators :@reader, :read_char, :read_keypress, :read_line,
78
+ :read_multiline, :on, :subscribe, :unsubscribe, :trigger,
79
+ :count_screen_lines
80
+
81
+ def_delegators :@output, :print, :puts, :flush
82
+
83
+ def self.messages
84
+ {
85
+ range?: "Value %{value} must be within the range %{in}",
86
+ valid?: "Your answer is invalid (must match %{valid})",
87
+ required?: "Value must be provided",
88
+ convert?: "Cannot convert `%{value}` to '%{type}' type"
89
+ }
90
+ end
91
+
92
+ # Initialize a Prompt
93
+ #
94
+ # @param [IO] :input
95
+ # the input stream
96
+ # @param [IO] :output
97
+ # the output stream
98
+ # @param [Hash] :env
99
+ # the environment variables
100
+ # @param [Hash] :symbols
101
+ # the symbols displayed in prompts such as :marker, :cross
102
+ # @param options [Boolean] :quiet
103
+ # enable quiet mode, don't re-echo the question
104
+ # @param [String] :prefix
105
+ # the prompt prefix, by default empty
106
+ # @param [Symbol] :interrupt
107
+ # handling of Ctrl+C key out of :signal, :exit, :noop
108
+ # @param [Boolean] :track_history
109
+ # disable line history tracking, true by default
110
+ # @param [Boolean] :enable_color
111
+ # enable color support, true by default
112
+ # @param [String,Proc] :active_color
113
+ # the color used for selected option
114
+ # @param [String,Proc] :help_color
115
+ # the color used for help text
116
+ # @param [String] :error_color
117
+ # the color used for displaying error messages
118
+ #
119
+ # @api public
120
+ def initialize(input: $stdin, output: $stdout, env: ENV, symbols: {},
121
+ prefix: "", interrupt: :error, track_history: true,
122
+ quiet: false, enable_color: nil, active_color: :green,
123
+ help_color: :bright_black, error_color: :red)
124
+ @input = input
125
+ @output = output
126
+ @env = env
127
+ @prefix = prefix
128
+ @enabled_color = enable_color
129
+ @active_color = active_color
130
+ @help_color = help_color
131
+ @error_color = error_color
132
+ @interrupt = interrupt
133
+ @track_history = track_history
134
+ @symbols = Symbols.symbols.merge(symbols)
135
+ @quiet = quiet
136
+
137
+ @cursor = TTY::Cursor
138
+ @pastel = enabled_color.nil? ? Pastel.new : Pastel.new(enabled: enabled_color)
139
+ @reader = TTY2::Reader.new(
140
+ input: input,
141
+ output: output,
142
+ interrupt: interrupt,
143
+ track_history: track_history,
144
+ env: env
145
+ )
146
+ end
147
+
148
+ # Decorate a string with colors
149
+ #
150
+ # @param [String] :string
151
+ # the string to color
152
+ # @param [Array<Proc|Symbol>] :colors
153
+ # collection of color symbols or callable object
154
+ #
155
+ # @api public
156
+ def decorate(string, *colors)
157
+ if Utils.blank?(string) || @enabled_color == false || colors.empty?
158
+ return string
159
+ end
160
+
161
+ coloring = colors.first
162
+ if coloring.respond_to?(:call)
163
+ coloring.call(string)
164
+ else
165
+ @pastel.decorate(string, *colors)
166
+ end
167
+ end
168
+
169
+ # Invoke a question type of prompt
170
+ #
171
+ # @example
172
+ # prompt = TTY2::Prompt.new
173
+ # prompt.invoke_question(Question, "Your name? ")
174
+ #
175
+ # @return [String]
176
+ #
177
+ # @api public
178
+ def invoke_question(object, message, **options, &block)
179
+ options[:messages] = self.class.messages
180
+ question = object.new(self, **options)
181
+ question.(message, &block)
182
+ end
183
+
184
+ # Ask a question.
185
+ #
186
+ # @example
187
+ # propmt = TTY2::Prompt.new
188
+ # prompt.ask("What is your name?")
189
+ #
190
+ # @param [String] message
191
+ # the question to be asked
192
+ #
193
+ # @yieldparam [TTY2::Prompt::Question] question
194
+ # further configure the question
195
+ #
196
+ # @yield [question]
197
+ #
198
+ # @return [TTY2::Prompt::Question]
199
+ #
200
+ # @api public
201
+ def ask(message = "", **options, &block)
202
+ invoke_question(Question, message, **options, &block)
203
+ end
204
+
205
+ # Ask a question with a keypress answer
206
+ #
207
+ # @see #ask
208
+ #
209
+ # @api public
210
+ def keypress(message = "", **options, &block)
211
+ invoke_question(Keypress, message, **options, &block)
212
+ end
213
+
214
+ # Ask a question with a multiline answer
215
+ #
216
+ # @example
217
+ # prompt.multiline("Description?")
218
+ #
219
+ # @return [Array[String]]
220
+ #
221
+ # @api public
222
+ def multiline(message = "", **options, &block)
223
+ invoke_question(Multiline, message, **options, &block)
224
+ end
225
+
226
+ # Invoke a list type of prompt
227
+ #
228
+ # @example
229
+ # prompt = TTY2::Prompt.new
230
+ # editors = %w(emacs nano vim)
231
+ # prompt.invoke_select(EnumList, "Select editor: ", editors)
232
+ #
233
+ # @return [String]
234
+ #
235
+ # @api public
236
+ def invoke_select(object, question, *args, &block)
237
+ options = Utils.extract_options!(args)
238
+ choices = if args.empty? && !block
239
+ possible = options.dup
240
+ options = {}
241
+ possible
242
+ elsif args.size == 1 && args[0].is_a?(Hash)
243
+ Utils.extract_options!(args)
244
+ else
245
+ args.flatten
246
+ end
247
+
248
+ list = object.new(self, **options)
249
+ list.(question, choices, &block)
250
+ end
251
+
252
+ # Ask masked question
253
+ #
254
+ # @example
255
+ # propmt = TTY2::Prompt.new
256
+ # prompt.mask("What is your secret?")
257
+ #
258
+ # @return [TTY2::Prompt::MaskQuestion]
259
+ #
260
+ # @api public
261
+ def mask(message = "", **options, &block)
262
+ invoke_question(MaskQuestion, message, **options, &block)
263
+ end
264
+
265
+ # Ask a question with a list of options
266
+ #
267
+ # @example
268
+ # prompt = TTY2::Prompt.new
269
+ # prompt.select("What size?", %w(large medium small))
270
+ #
271
+ # @example
272
+ # prompt = TTY2::Prompt.new
273
+ # prompt.select("What size?") do |menu|
274
+ # menu.choice :large
275
+ # menu.choices %w(:medium :small)
276
+ # end
277
+ #
278
+ # @param [String] question
279
+ # the question to ask
280
+ #
281
+ # @param [Array[Object]] choices
282
+ # the choices to select from
283
+ #
284
+ # @api public
285
+ def select(question, *args, &block)
286
+ invoke_select(List, question, *args, &block)
287
+ end
288
+
289
+ # Ask a question with multiple attributes activated
290
+ #
291
+ # @example
292
+ # prompt = TTY2::Prompt.new
293
+ # choices = %w(Scorpion Jax Kitana Baraka Jade)
294
+ # prompt.multi_select("Choose your destiny?", choices)
295
+ #
296
+ # @param [String] question
297
+ # the question to ask
298
+ #
299
+ # @param [Array[Object]] choices
300
+ # the choices to select from
301
+ #
302
+ # @return [String]
303
+ #
304
+ # @api public
305
+ def multi_select(question, *args, &block)
306
+ invoke_select(MultiList, question, *args, &block)
307
+ end
308
+
309
+ # Ask a question with indexed list
310
+ #
311
+ # @example
312
+ # prompt = TTY2::Prompt.new
313
+ # editors = %w(emacs nano vim)
314
+ # prompt.enum_select(EnumList, "Select editor: ", editors)
315
+ #
316
+ # @param [String] question
317
+ # the question to ask
318
+ #
319
+ # @param [Array[Object]] choices
320
+ # the choices to select from
321
+ #
322
+ # @return [String]
323
+ #
324
+ # @api public
325
+ def enum_select(question, *args, &block)
326
+ invoke_select(EnumList, question, *args, &block)
327
+ end
328
+
329
+ # A shortcut method to ask the user positive question and return
330
+ # true for "yes" reply, false for "no".
331
+ #
332
+ # @example
333
+ # prompt = TTY2::Prompt.new
334
+ # prompt.yes?("Are you human?")
335
+ # # => Are you human? (Y/n)
336
+ #
337
+ # @return [Boolean]
338
+ #
339
+ # @api public
340
+ def yes?(message, **options, &block)
341
+ opts = { default: true }.merge(options)
342
+ question = ConfirmQuestion.new(self, **opts)
343
+ question.call(message, &block)
344
+ end
345
+
346
+ # A shortcut method to ask the user negative question and return
347
+ # true for "no" reply.
348
+ #
349
+ # @example
350
+ # prompt = TTY2::Prompt.new
351
+ # prompt.no?("Are you alien?") # => true
352
+ # # => Are you human? (y/N)
353
+ #
354
+ # @return [Boolean]
355
+ #
356
+ # @api public
357
+ def no?(message, **options, &block)
358
+ opts = { default: false }.merge(options)
359
+ question = ConfirmQuestion.new(self, **opts)
360
+ !question.call(message, &block)
361
+ end
362
+
363
+ # Expand available options
364
+ #
365
+ # @example
366
+ # prompt = TTY2::Prompt.new
367
+ # choices = [{
368
+ # key: "Y",
369
+ # name: "Overwrite",
370
+ # value: :yes
371
+ # }, {
372
+ # key: "n",
373
+ # name: "Skip",
374
+ # value: :no
375
+ # }]
376
+ # prompt.expand("Overwirte Gemfile?", choices)
377
+ #
378
+ # @return [Object]
379
+ # the user specified value
380
+ #
381
+ # @api public
382
+ def expand(message, *args, &block)
383
+ invoke_select(Expander, message, *args, &block)
384
+ end
385
+
386
+ # Ask a question with a range slider
387
+ #
388
+ # @example
389
+ # prompt = TTY2::Prompt.new
390
+ # prompt.slider("What size?", min: 32, max: 54, step: 2)
391
+ # prompt.slider("What size?", [ 'xs', 's', 'm', 'l', 'xl' ])
392
+ #
393
+ # @param [String] question
394
+ # the question to ask
395
+ #
396
+ # @param [Array] choices
397
+ # the choices to display
398
+ #
399
+ # @return [String]
400
+ #
401
+ # @api public
402
+ def slider(question, choices = nil, **options, &block)
403
+ slider = Slider.new(self, **options)
404
+ slider.call(question, choices, &block)
405
+ end
406
+
407
+ # Print statement out. If the supplied message ends with a space or
408
+ # tab character, a new line will not be appended.
409
+ #
410
+ # @example
411
+ # say("Simple things.", color: :red)
412
+ #
413
+ # @param [String] message
414
+ #
415
+ # @return [String]
416
+ #
417
+ # @api public
418
+ def say(message = "", **options)
419
+ message = message.to_s
420
+ return if message.empty?
421
+
422
+ statement = Statement.new(self, **options)
423
+ statement.call(message)
424
+ end
425
+
426
+ # Print statement(s) out in red green.
427
+ #
428
+ # @example
429
+ # prompt.ok "Are you sure?"
430
+ # prompt.ok "All is fine!", "This is fine too."
431
+ #
432
+ # @param [Array] messages
433
+ #
434
+ # @return [Array] messages
435
+ #
436
+ # @api public
437
+ def ok(*args, **options)
438
+ opts = { color: :green }.merge(options)
439
+ args.each { |message| say(message, **opts) }
440
+ end
441
+
442
+ # Print statement(s) out in yellow color.
443
+ #
444
+ # @example
445
+ # prompt.warn "This action can have dire consequences"
446
+ # prompt.warn "Carefull young apprentice", "This is potentially dangerous"
447
+ #
448
+ # @param [Array] messages
449
+ #
450
+ # @return [Array] messages
451
+ #
452
+ # @api public
453
+ def warn(*args, **options)
454
+ opts = { color: :yellow }.merge(options)
455
+ args.each { |message| say(message, **opts) }
456
+ end
457
+
458
+ # Print statement(s) out in red color.
459
+ #
460
+ # @example
461
+ # prompt.error "Shutting down all systems!"
462
+ # prompt.error "Nothing is fine!", "All is broken!"
463
+ #
464
+ # @param [Array] messages
465
+ #
466
+ # @return [Array] messages
467
+ #
468
+ # @api public
469
+ def error(*args, **options)
470
+ opts = { color: :red }.merge(options)
471
+ args.each { |message| say(message, **opts) }
472
+ end
473
+
474
+ # Print debug information in terminal top right corner
475
+ #
476
+ # @example
477
+ # prompt.debug "info1", "info2"
478
+ #
479
+ # @param [Array] messages
480
+ #
481
+ # @retrun [nil]
482
+ #
483
+ # @api public
484
+ def debug(*messages)
485
+ longest = messages.max_by(&:length).size
486
+ width = TTY::Screen.width - longest
487
+ print cursor.save
488
+ messages.reverse_each do |msg|
489
+ print cursor.column(width) + cursor.up + cursor.clear_line_after
490
+ print msg
491
+ end
492
+ ensure
493
+ print cursor.restore
494
+ end
495
+
496
+ # Takes the string provided by the user and compare it with other possible
497
+ # matches to suggest an unambigous string
498
+ #
499
+ # @example
500
+ # prompt.suggest("sta", ["status", "stage", "commit", "branch"])
501
+ # # => "status, stage"
502
+ #
503
+ # @param [String] message
504
+ #
505
+ # @param [Array] possibilities
506
+ #
507
+ # @param [Hash] options
508
+ # @option options [String] :indent
509
+ # The number of spaces for indentation
510
+ # @option options [String] :single_text
511
+ # The text for a single suggestion
512
+ # @option options [String] :plural_text
513
+ # The text for multiple suggestions
514
+ #
515
+ # @return [String]
516
+ #
517
+ # @api public
518
+ def suggest(message, possibilities, **options)
519
+ suggestion = Suggestion.new(**options)
520
+ say(suggestion.suggest(message, possibilities))
521
+ end
522
+
523
+ # Gathers more than one aswer
524
+ #
525
+ # @example
526
+ # prompt.collect do
527
+ # key(:name).ask("Name?")
528
+ # end
529
+ #
530
+ # @return [Hash]
531
+ # the collection of answers
532
+ #
533
+ # @api public
534
+ def collect(**options, &block)
535
+ collector = AnswersCollector.new(self, **options)
536
+ collector.call(&block)
537
+ end
538
+
539
+ # Check if outputing to terminal
540
+ #
541
+ # @return [Boolean]
542
+ #
543
+ # @api public
544
+ def tty?
545
+ stdout.tty?
546
+ end
547
+
548
+ # Return standard in
549
+ #
550
+ # @api private
551
+ def stdin
552
+ $stdin
553
+ end
554
+
555
+ # Return standard out
556
+ #
557
+ # @api private
558
+ def stdout
559
+ $stdout
560
+ end
561
+
562
+ # Return standard error
563
+ #
564
+ # @api private
565
+ def stderr
566
+ $stderr
567
+ end
568
+
569
+ # Inspect this instance public attributes
570
+ #
571
+ # @return [String]
572
+ #
573
+ # @api public
574
+ def inspect
575
+ attributes = [
576
+ :prefix,
577
+ :quiet,
578
+ :enabled_color,
579
+ :active_color,
580
+ :error_color,
581
+ :help_color,
582
+ :input,
583
+ :output,
584
+ ]
585
+ name = self.class.name
586
+ "#<#{name}#{attributes.map { |attr| " #{attr}=#{send(attr).inspect}" }.join}>"
587
+ end
588
+ end # Prompt
589
+ end # TTY2
@@ -0,0 +1 @@
1
+ require_relative "tty2/prompt"