tty-prompt 0.2.0 → 0.3.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 (108) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +9 -6
  3. data/CHANGELOG.md +40 -3
  4. data/Gemfile +0 -1
  5. data/README.md +246 -65
  6. data/examples/ask.rb +7 -0
  7. data/examples/echo.rb +7 -0
  8. data/examples/in.rb +7 -0
  9. data/examples/mask.rb +9 -0
  10. data/examples/multi_select.rb +8 -0
  11. data/examples/select.rb +8 -0
  12. data/examples/validation.rb +9 -0
  13. data/examples/yes_no.rb +7 -0
  14. data/lib/tty-prompt.rb +6 -4
  15. data/lib/tty/prompt.rb +100 -25
  16. data/lib/tty/prompt/choice.rb +1 -1
  17. data/lib/tty/prompt/converter_dsl.rb +19 -0
  18. data/lib/tty/prompt/converter_registry.rb +56 -0
  19. data/lib/tty/prompt/converters.rb +77 -0
  20. data/lib/tty/prompt/evaluator.rb +29 -0
  21. data/lib/tty/prompt/list.rb +38 -36
  22. data/lib/tty/prompt/mask_question.rb +85 -0
  23. data/lib/tty/prompt/multi_list.rb +21 -32
  24. data/lib/tty/prompt/question.rb +184 -162
  25. data/lib/tty/prompt/question/checks.rb +85 -0
  26. data/lib/tty/prompt/question/modifier.rb +4 -5
  27. data/lib/tty/prompt/question/validation.rb +29 -35
  28. data/lib/tty/prompt/reader.rb +98 -52
  29. data/lib/tty/prompt/reader/codes.rb +63 -0
  30. data/lib/tty/prompt/reader/key_event.rb +67 -0
  31. data/lib/tty/prompt/reader/mode.rb +66 -0
  32. data/lib/tty/prompt/reader/mode/echo.rb +43 -0
  33. data/lib/tty/prompt/reader/mode/raw.rb +43 -0
  34. data/lib/tty/prompt/result.rb +42 -0
  35. data/lib/tty/prompt/statement.rb +9 -14
  36. data/lib/tty/prompt/suggestion.rb +4 -2
  37. data/lib/tty/prompt/symbols.rb +13 -0
  38. data/lib/tty/prompt/test.rb +3 -2
  39. data/lib/tty/prompt/utils.rb +1 -1
  40. data/lib/tty/prompt/version.rb +1 -1
  41. data/spec/unit/ask_spec.rb +31 -48
  42. data/spec/unit/choice/eql_spec.rb +0 -2
  43. data/spec/unit/choice/from_spec.rb +0 -2
  44. data/spec/unit/choices/add_spec.rb +0 -2
  45. data/spec/unit/choices/each_spec.rb +0 -2
  46. data/spec/unit/choices/new_spec.rb +0 -2
  47. data/spec/unit/choices/pluck_spec.rb +0 -2
  48. data/spec/unit/converters/convert_bool_spec.rb +58 -0
  49. data/spec/unit/{response/read_char_spec.rb → converters/convert_char_spec.rb} +2 -4
  50. data/spec/unit/converters/convert_custom_spec.rb +14 -0
  51. data/spec/unit/converters/convert_date_spec.rb +25 -0
  52. data/spec/unit/converters/convert_file_spec.rb +14 -0
  53. data/spec/unit/{response/read_number_spec.rb → converters/convert_number_spec.rb} +5 -7
  54. data/spec/unit/converters/convert_path_spec.rb +15 -0
  55. data/spec/unit/{response/read_range_spec.rb → converters/convert_range_spec.rb} +3 -5
  56. data/spec/unit/converters/convert_regex_spec.rb +12 -0
  57. data/spec/unit/converters/convert_string_spec.rb +21 -0
  58. data/spec/unit/distance/distance_spec.rb +0 -2
  59. data/spec/unit/error_spec.rb +0 -6
  60. data/spec/unit/evaluator_spec.rb +67 -0
  61. data/spec/unit/keypress_spec.rb +19 -0
  62. data/spec/unit/mask_spec.rb +95 -0
  63. data/spec/unit/multi_select_spec.rb +36 -24
  64. data/spec/unit/multiline_spec.rb +19 -0
  65. data/spec/unit/new_spec.rb +18 -0
  66. data/spec/unit/ok_spec.rb +10 -0
  67. data/spec/unit/question/default_spec.rb +17 -4
  68. data/spec/unit/question/echo_spec.rb +31 -0
  69. data/spec/unit/question/in_spec.rb +48 -16
  70. data/spec/unit/question/initialize_spec.rb +2 -9
  71. data/spec/unit/question/modifier/apply_to_spec.rb +9 -16
  72. data/spec/unit/question/modifier/letter_case_spec.rb +0 -2
  73. data/spec/unit/question/modifier/whitespace_spec.rb +12 -20
  74. data/spec/unit/question/modify_spec.rb +3 -7
  75. data/spec/unit/question/required_spec.rb +20 -14
  76. data/spec/unit/question/validate_spec.rb +20 -19
  77. data/spec/unit/question/validation/call_spec.rb +15 -6
  78. data/spec/unit/question/validation/coerce_spec.rb +17 -11
  79. data/spec/unit/reader/publish_keypress_event_spec.rb +81 -0
  80. data/spec/unit/reader/read_keypress_spec.rb +22 -0
  81. data/spec/unit/reader/read_line_spec.rb +31 -0
  82. data/spec/unit/reader/read_multiline_spec.rb +37 -0
  83. data/spec/unit/result_spec.rb +40 -0
  84. data/spec/unit/say_spec.rb +18 -23
  85. data/spec/unit/select_spec.rb +37 -32
  86. data/spec/unit/statement/initialize_spec.rb +4 -4
  87. data/spec/unit/suggest_spec.rb +0 -2
  88. data/spec/unit/warn_spec.rb +0 -5
  89. data/spec/unit/yes_no_spec.rb +70 -0
  90. data/tty-prompt.gemspec +7 -4
  91. metadata +123 -40
  92. data/lib/tty/prompt/codes.rb +0 -32
  93. data/lib/tty/prompt/cursor.rb +0 -131
  94. data/lib/tty/prompt/error.rb +0 -26
  95. data/lib/tty/prompt/mode.rb +0 -64
  96. data/lib/tty/prompt/mode/echo.rb +0 -41
  97. data/lib/tty/prompt/mode/raw.rb +0 -41
  98. data/lib/tty/prompt/response.rb +0 -247
  99. data/lib/tty/prompt/response_delegation.rb +0 -42
  100. data/spec/unit/cursor/new_spec.rb +0 -74
  101. data/spec/unit/question/character_spec.rb +0 -13
  102. data/spec/unit/reader/getc_spec.rb +0 -42
  103. data/spec/unit/response/read_bool_spec.rb +0 -58
  104. data/spec/unit/response/read_date_spec.rb +0 -16
  105. data/spec/unit/response/read_email_spec.rb +0 -45
  106. data/spec/unit/response/read_multiple_spec.rb +0 -21
  107. data/spec/unit/response/read_spec.rb +0 -69
  108. data/spec/unit/response/read_string_spec.rb +0 -14
data/examples/ask.rb ADDED
@@ -0,0 +1,7 @@
1
+ # encoding: utf-8
2
+
3
+ require 'tty-prompt'
4
+
5
+ prompt = TTY::Prompt.new
6
+
7
+ prompt.ask('What is your name?', default: ENV['USER'])
data/examples/echo.rb ADDED
@@ -0,0 +1,7 @@
1
+ # encoding: utf-8
2
+
3
+ require 'tty-prompt'
4
+
5
+ prompt = TTY::Prompt.new
6
+
7
+ puts prompt.ask('Password?', echo: false)
data/examples/in.rb ADDED
@@ -0,0 +1,7 @@
1
+ # encoding: utf-8
2
+
3
+ require 'tty-prompt'
4
+
5
+ prompt = TTY::Prompt.new
6
+
7
+ prompt.ask('How do you like it on scale 1 - 10?', in: '1-10')
data/examples/mask.rb ADDED
@@ -0,0 +1,9 @@
1
+ # encoding: utf-8
2
+
3
+ require 'tty-prompt'
4
+
5
+ prompt = TTY::Prompt.new
6
+
7
+ prompt.mask('What is your secret?') do |q|
8
+ q.validate /[a-z]{5,8}/
9
+ end
@@ -0,0 +1,8 @@
1
+ # encoding: utf-8
2
+
3
+ require 'tty-prompt'
4
+
5
+ prompt = TTY::Prompt.new
6
+
7
+ drinks = %w(vodka beer wine whisky bourbon)
8
+ prompt.multi_select('Choose your favourite drink?', drinks)
@@ -0,0 +1,8 @@
1
+ # encoding: utf-8
2
+
3
+ require 'tty-prompt'
4
+
5
+ prompt = TTY::Prompt.new
6
+
7
+ warriors = %w(Scorpion Kano Jax)
8
+ prompt.select('Choose your destiny?', warriors)
@@ -0,0 +1,9 @@
1
+ # encoding: utf-8
2
+
3
+ require 'tty-prompt'
4
+
5
+ prompt = TTY::Prompt.new
6
+
7
+ prompt.ask('What is your username?') do |q|
8
+ q.validate(/^[^\.]+\.[^\.]+/)
9
+ end
@@ -0,0 +1,7 @@
1
+ # encoding: utf-8
2
+
3
+ require 'tty-prompt'
4
+
5
+ prompt = TTY::Prompt.new
6
+
7
+ prompt.yes?('Do you like Ruby?')
data/lib/tty-prompt.rb CHANGED
@@ -2,25 +2,27 @@
2
2
 
3
3
  require 'necromancer'
4
4
  require 'pastel'
5
+ require 'tty-cursor'
5
6
  require 'tty-platform'
6
7
 
7
8
  require 'tty/prompt'
8
9
  require 'tty/prompt/choice'
9
10
  require 'tty/prompt/choices'
10
- require 'tty/prompt/codes'
11
- require 'tty/prompt/cursor'
11
+ require 'tty/prompt/evaluator'
12
12
  require 'tty/prompt/list'
13
13
  require 'tty/prompt/multi_list'
14
- require 'tty/prompt/mode'
15
14
  require 'tty/prompt/question'
15
+ require 'tty/prompt/mask_question'
16
16
  require 'tty/prompt/reader'
17
- require 'tty/prompt/response'
18
17
  require 'tty/prompt/statement'
19
18
  require 'tty/prompt/suggestion'
19
+ require 'tty/prompt/symbols'
20
20
  require 'tty/prompt/test'
21
21
  require 'tty/prompt/utils'
22
22
  require 'tty/prompt/version'
23
23
 
24
+ # A collection of small libraries for building CLI apps,
25
+ # each following unix philosophy of focused task
24
26
  module TTY
25
27
  PromptConfigurationError = Class.new(StandardError)
26
28
  end
data/lib/tty/prompt.rb CHANGED
@@ -1,7 +1,12 @@
1
1
  # encoding: utf-8
2
2
 
3
+ require 'forwardable'
4
+
3
5
  module TTY
6
+ # A main entry for asking prompt questions.
4
7
  class Prompt
8
+ extend Forwardable
9
+
5
10
  # Raised when the passed in validation argument is of wrong type
6
11
  class ValidationCoercion < TypeError; end
7
12
 
@@ -20,35 +25,54 @@ module TTY
20
25
  # @api private
21
26
  attr_reader :output
22
27
 
28
+ attr_reader :reader
29
+
30
+ attr_reader :cursor
31
+
23
32
  # Prompt prefix
24
33
  #
25
34
  # @api private
26
35
  attr_reader :prefix
27
36
 
37
+ def_delegators :@pastel, :decorate
38
+
39
+ def_delegators :@cursor, :clear_lines, :clear_line,
40
+ :show, :hide
41
+
42
+ def_delegators :@reader, :read_line, :read_keypress,
43
+ :read_multiline, :on, :subscribe
44
+
45
+ def_delegators :@output, :print, :puts, :flush
46
+
28
47
  # Initialize a Prompt
29
48
  #
30
49
  # @api public
31
- def initialize(input = stdin, output = stdout, options = {})
32
- @input = input
33
- @output = output
50
+ def initialize(*args)
51
+ options = Utils.extract_options!(args)
52
+ @input = options.fetch(:input) { $stdin }
53
+ @output = options.fetch(:output) { $stdout }
34
54
  @prefix = options.fetch(:prefix) { '' }
55
+
56
+ @cursor = TTY::Cursor
57
+ @pastel = Pastel.new
58
+ @reader = Reader.new(@input, @output)
35
59
  end
36
60
 
37
61
  # Ask a question.
38
62
  #
39
63
  # @example
40
- # shell = TTY::Prompt.new
41
- # shell.ask("What is your name?")
64
+ # propmt = TTY::Prompt.new
65
+ # prompt.ask("What is your name?")
42
66
  #
43
- # @param [String] statement
44
- # string question to be asked
67
+ # @param [String] message
68
+ # the question to be asked
45
69
  #
46
- # @yieldparam [TTY::Question] question
70
+ # @yieldparam [TTY::Prompt::Question] question
47
71
  # further configure the question
48
72
  #
49
73
  # @yield [question]
50
74
  #
51
- # @return [TTY::Question]
75
+ # @return [TTY::Prompt::Question]
52
76
  #
53
77
  # @api public
54
78
  def ask(message, *args, &block)
@@ -58,6 +82,46 @@ module TTY
58
82
  question.call(message, &block)
59
83
  end
60
84
 
85
+ # Ask a question with a keypress answer
86
+ #
87
+ # @see #ask
88
+ #
89
+ # @api public
90
+ def keypress(message, *args, &block)
91
+ options = Utils.extract_options!(args)
92
+ options.merge!(read: :keypress)
93
+ args << options
94
+ ask(message, *args, &block)
95
+ end
96
+
97
+ # Ask a question with a multiline answer
98
+ #
99
+ # @see @ask
100
+ #
101
+ # @api public
102
+ def multiline(message, *args, &block)
103
+ options = Utils.extract_options!(args)
104
+ options.merge!(read: :multiline)
105
+ args << options
106
+ ask(message, *args, &block)
107
+ end
108
+
109
+ # Ask masked question
110
+ #
111
+ # @example
112
+ # propmt = TTY::Prompt.new
113
+ # prompt.mask("What is your secret?")
114
+ #
115
+ # @return [TTY::Prompt::MaskQuestion]
116
+ #
117
+ # @api public
118
+ def mask(message, *args, &block)
119
+ options = Utils.extract_options!(args)
120
+
121
+ question = MaskQuestion.new(self, options)
122
+ question.call(message, &block)
123
+ end
124
+
61
125
  # Ask a question with a list of options
62
126
  #
63
127
  # @example
@@ -125,28 +189,39 @@ module TTY
125
189
  # A shortcut method to ask the user positive question and return
126
190
  # true for 'yes' reply, false for 'no'.
127
191
  #
192
+ # @example
193
+ # prompt = TTY::Prompt.new
194
+ # prompt.yes?('Are you human? (Y/n)') # => true
195
+ #
128
196
  # @return [Boolean]
129
197
  #
130
198
  # @api public
131
- def yes?(statement, *args, &block)
132
- ask(statement, {read: :bool}, &block)
199
+ def yes?(question, *args, &block)
200
+ options = Utils.extract_options!(args)
201
+ options.merge!(convert: :bool)
202
+ args << options
203
+ ask(question, *args, &block)
133
204
  end
134
205
 
135
206
  # A shortcut method to ask the user negative question and return
136
207
  # true for 'no' reply.
137
208
  #
209
+ # @example
210
+ # prompt = TTY::Prompt.new
211
+ # prompt.no?('Are you alien? (y/N)') # => true
212
+ #
138
213
  # @return [Boolean]
139
214
  #
140
215
  # @api public
141
- def no?(statement, *args, &block)
142
- !yes?(statement, *args, &block)
216
+ def no?(question, *args, &block)
217
+ !yes?(question, *args, &block)
143
218
  end
144
219
 
145
220
  # Print statement out. If the supplied message ends with a space or
146
221
  # tab character, a new line will not be appended.
147
222
  #
148
223
  # @example
149
- # say("Simple things.")
224
+ # say("Simple things.", color: :red)
150
225
  #
151
226
  # @param [String] message
152
227
  #
@@ -154,25 +229,25 @@ module TTY
154
229
  #
155
230
  # @api public
156
231
  def say(message = '', options = {})
157
- message = message.to_str
232
+ message = message.to_s
158
233
  return unless message.length > 0
159
234
 
160
235
  statement = Statement.new(self, options)
161
- statement.declare message
236
+ statement.call(message)
162
237
  end
163
238
 
164
239
  # Print statement(s) out in red green.
165
240
  #
166
241
  # @example
167
- # shell.confirm "Are you sure?"
168
- # shell.confirm "All is fine!", "This is fine too."
242
+ # prompt.ok "Are you sure?"
243
+ # prompt.ok "All is fine!", "This is fine too."
169
244
  #
170
245
  # @param [Array] messages
171
246
  #
172
247
  # @return [Array] messages
173
248
  #
174
249
  # @api public
175
- def confirm(*args)
250
+ def ok(*args)
176
251
  options = Utils.extract_options!(args)
177
252
  args.each { |message| say message, options.merge(color: :green) }
178
253
  end
@@ -180,8 +255,8 @@ module TTY
180
255
  # Print statement(s) out in yellow color.
181
256
  #
182
257
  # @example
183
- # shell.warn "This action can have dire consequences"
184
- # shell.warn "Carefull young apprentice", "This is potentially dangerous"
258
+ # prompt.warn "This action can have dire consequences"
259
+ # prompt.warn "Carefull young apprentice", "This is potentially dangerous"
185
260
  #
186
261
  # @param [Array] messages
187
262
  #
@@ -196,8 +271,8 @@ module TTY
196
271
  # Print statement(s) out in red color.
197
272
  #
198
273
  # @example
199
- # shell.error "Shutting down all systems!"
200
- # shell.error "Nothing is fine!", "All is broken!"
274
+ # prompt.error "Shutting down all systems!"
275
+ # prompt.error "Nothing is fine!", "All is broken!"
201
276
  #
202
277
  # @param [Array] messages
203
278
  #
@@ -213,7 +288,7 @@ module TTY
213
288
  # matches to suggest an unambigous string
214
289
  #
215
290
  # @example
216
- # shell.suggest('sta', ['status', 'stage', 'commit', 'branch'])
291
+ # prompt.suggest('sta', ['status', 'stage', 'commit', 'branch'])
217
292
  # # => "status, stage"
218
293
  #
219
294
  # @param [String] message
@@ -236,7 +311,7 @@ module TTY
236
311
  say(suggestion.suggest(message, possibilities))
237
312
  end
238
313
 
239
- # Check if outputing to shell
314
+ # Check if outputing to terminal
240
315
  #
241
316
  # @return [Boolean]
242
317
  #
@@ -36,7 +36,7 @@ module TTY
36
36
  def self.from(val)
37
37
  case val
38
38
  when Choice
39
- return val
39
+ val
40
40
  when String, Symbol
41
41
  new(val, val)
42
42
  when Array
@@ -0,0 +1,19 @@
1
+ # encoding: utf-8
2
+
3
+ require 'tty/prompt/converter_registry'
4
+
5
+ module TTY
6
+ class Prompt
7
+ module ConverterDSL
8
+ def self.extended(base)
9
+ attr_reader :converter_registry
10
+
11
+ base.instance_variable_set(:@converter_registry, ConverterRegistry.new)
12
+ end
13
+
14
+ def converter(name, &block)
15
+ converter_registry.register(name, &block)
16
+ end
17
+ end # ConverterDSL
18
+ end # Prompt
19
+ end # TTY
@@ -0,0 +1,56 @@
1
+ # encoding: utf-8
2
+
3
+ module TTY
4
+ class Prompt
5
+ class ConverterRegistry
6
+ def initialize
7
+ @_registry = {}
8
+ end
9
+
10
+ # Register converter
11
+ #
12
+ # @api public
13
+ def register(key, contents = nil, &block)
14
+ if block_given?
15
+ item = block
16
+ else
17
+ item = contents
18
+ end
19
+
20
+ if key?(key)
21
+ fail ArgumentError, "Converter for #{key.inspect} already registered"
22
+ else
23
+ @_registry[key] = item
24
+ end
25
+ self
26
+ end
27
+
28
+ # Check if converter is registered
29
+ #
30
+ # @return [Boolean]
31
+ #
32
+ # @api public
33
+ def key?(key)
34
+ @_registry.key?(key)
35
+ end
36
+
37
+ # Execute converter
38
+ #
39
+ # @api public
40
+ def call(key, input)
41
+ if key.respond_to?(:call)
42
+ converter = key
43
+ else
44
+ converter = @_registry.fetch(key) do
45
+ fail ArgumentError, "#{key.inspect} is not registered"
46
+ end
47
+ end
48
+ converter.call(input)
49
+ end
50
+
51
+ def inspect
52
+ @_registry.inspect
53
+ end
54
+ end # ConverterRegistry
55
+ end # Prompt
56
+ end # TTY
@@ -0,0 +1,77 @@
1
+ # encoding: utf-8
2
+
3
+ require 'pathname'
4
+ require 'necromancer'
5
+ require 'tty/prompt/converter_dsl'
6
+
7
+ module TTY
8
+ class Prompt
9
+ module Converters
10
+ extend ConverterDSL
11
+
12
+ def self.included(base)
13
+ base.class_eval do
14
+ def converter_registry
15
+ Converters.converter_registry
16
+ end
17
+ end
18
+ end
19
+
20
+ converter(:bool) do |input|
21
+ converter = Necromancer.new
22
+ converter.convert(input).to(:boolean, strict: true)
23
+ end
24
+
25
+ converter(:string) do |input|
26
+ String(input).chomp
27
+ end
28
+
29
+ converter(:symbol) do |input|
30
+ input.to_sym
31
+ end
32
+
33
+ converter(:date) do |input|
34
+ converter = Necromancer.new
35
+ converter.convert(input).to(:date)
36
+ end
37
+
38
+ converter(:datetime) do |input|
39
+ converter = Necromancer.new
40
+ converter.convert(input).to(:datetime)
41
+ end
42
+
43
+ converter(:int) do |input|
44
+ converter = Necromancer.new
45
+ converter.convert(input).to(:integer)
46
+ end
47
+
48
+ converter(:float) do |input|
49
+ converter = Necromancer.new
50
+ converter.convert(input).to(:float)
51
+ end
52
+
53
+ converter(:range) do |input|
54
+ converter = Necromancer.new
55
+ converter.convert(input).to(:range, strict: true)
56
+ end
57
+
58
+ converter(:regexp) do |input|
59
+ Regexp.new(input)
60
+ end
61
+
62
+ converter(:file) do |input|
63
+ directory = File.expand_path(File.dirname($0))
64
+ File.open(File.join(directory, input))
65
+ end
66
+
67
+ converter(:path) do |input|
68
+ directory = File.expand_path(File.dirname($0))
69
+ Pathname.new(File.join(directory, input))
70
+ end
71
+
72
+ converter(:char) do |input|
73
+ String(input).chars.to_a[0]
74
+ end
75
+ end # Converters
76
+ end # Prompt
77
+ end # TTY