tty-prompt 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,29 @@
1
+ # encoding: utf-8
2
+
3
+ require 'tty/prompt/result'
4
+
5
+ module TTY
6
+ class Prompt
7
+ # Evaluates provided parameters and stops if any of them fails
8
+ # @api private
9
+ class Evaluator
10
+ attr_reader :results
11
+
12
+ def initialize(question, &block)
13
+ @question = question
14
+ @results = []
15
+ instance_eval(&block) if block
16
+ end
17
+
18
+ def call(initial)
19
+ seed = Result::Success.new(@question, initial)
20
+ results.reduce(seed, &:with)
21
+ end
22
+
23
+ def check(proc = nil, &block)
24
+ results << (proc || block)
25
+ end
26
+ alias_method :<<, :check
27
+ end # Evaluator
28
+ end # Prompt
29
+ end # TTY
@@ -21,20 +21,18 @@ module TTY
21
21
  # the marker for the selected item
22
22
  #
23
23
  # @api public
24
- def initialize(prompt, options)
25
- @prompt = prompt
26
- @reader = Reader.new(@prompt)
27
- @pastel = Pastel.new
28
- @cursor = Cursor.new
29
-
24
+ def initialize(prompt, options = {})
25
+ @prompt = prompt
30
26
  @first_render = true
31
27
  @done = false
32
28
  @default = Array[options.fetch(:default) { 1 }]
33
29
  @active = @default.first
34
30
  @choices = Choices.new
35
31
  @color = options.fetch(:color) { :green }
36
- @marker = options.fetch(:marker) { Codes::ITEM_SELECTED }
32
+ @marker = options.fetch(:marker) { Symbols::ITEM_SELECTED }
37
33
  @help = options.fetch(:help) { HELP }
34
+
35
+ @prompt.subscribe(self)
38
36
  end
39
37
 
40
38
  # Set marker
@@ -86,6 +84,26 @@ module TTY
86
84
  render
87
85
  end
88
86
 
87
+ def keyescape(event)
88
+ exit 130
89
+ end
90
+
91
+ def keyspace(event)
92
+ @done = true
93
+ end
94
+
95
+ def keyreturn(event)
96
+ @done = true
97
+ end
98
+
99
+ def keyup(event)
100
+ @active = (@active == 1) ? @choices.length : @active - 1
101
+ end
102
+
103
+ def keydown(event)
104
+ @active = (@active == @choices.length) ? 1 : @active + 1
105
+ end
106
+
89
107
  private
90
108
 
91
109
  # Setup default option and active selection
@@ -117,16 +135,16 @@ module TTY
117
135
  #
118
136
  # @api private
119
137
  def render
120
- @prompt.output.print(@cursor.hide)
138
+ @prompt.print(@prompt.hide)
121
139
  until @done
122
140
  render_question
123
- process_input
141
+ @prompt.read_keypress
124
142
  refresh
125
143
  end
126
144
  render_question
127
145
  answer = render_answer
128
146
  ensure
129
- @prompt.output.print(@cursor.show)
147
+ @prompt.print(@prompt.show)
130
148
  answer
131
149
  end
132
150
 
@@ -139,37 +157,20 @@ module TTY
139
157
  @choices[@active - 1].value
140
158
  end
141
159
 
142
- # Process keyboard input
143
- #
144
- # @api private
145
- def process_input
146
- chars = @reader.read_keypress
147
- case chars
148
- when Codes::SIGINT, Codes::ESCAPE
149
- exit 130
150
- when Codes::RETURN, Codes::SPACE
151
- @done = true
152
- when Codes::KEY_UP, Codes::CTRL_K, Codes::CTRL_P
153
- @active = (@active == 1) ? @choices.length : @active - 1
154
- when Codes::KEY_DOWN, Codes::CTRL_J, Codes::CTRL_N
155
- @active = (@active == @choices.length) ? 1 : @active + 1
156
- end
157
- end
158
-
159
160
  # Determine area of the screen to clear
160
161
  #
161
162
  # @api private
162
163
  def refresh
163
164
  lines = @question.scan("\n").length + @choices.length + 1
164
- @prompt.output.print(@cursor.clear_lines(lines))
165
+ @prompt.print(@prompt.clear_lines(lines))
165
166
  end
166
167
 
167
168
  # Render question with instructions and menu
168
169
  #
169
170
  # @api private
170
171
  def render_question
171
- header = @question + Codes::SPACE + render_header
172
- @prompt.output.puts(header)
172
+ header = "#{@prompt.prefix}#{@question} #{render_header}"
173
+ @prompt.puts(header)
173
174
  @first_render = false
174
175
  render_menu unless @done
175
176
  end
@@ -182,9 +183,9 @@ module TTY
182
183
  def render_header
183
184
  if @done
184
185
  selected_item = "#{@choices[@active - 1].name}"
185
- @pastel.decorate(selected_item, @color)
186
+ @prompt.decorate(selected_item, @color)
186
187
  elsif @first_render
187
- @pastel.decorate(@help, :bright_black)
188
+ @prompt.decorate(@help, :bright_black)
188
189
  else
189
190
  ''
190
191
  end
@@ -196,12 +197,13 @@ module TTY
196
197
  def render_menu
197
198
  @choices.each_with_index do |choice, index|
198
199
  message = if index + 1 == @active
199
- selected = @marker + Codes::SPACE + choice.name
200
- @pastel.decorate("#{selected}", @color)
200
+ selected = @marker + Symbols::SPACE + choice.name
201
+ @prompt.decorate("#{selected}", @color)
201
202
  else
202
- Codes::SPACE * 2 + choice.name
203
+ Symbols::SPACE * 2 + choice.name
203
204
  end
204
- @prompt.output.puts(message)
205
+ newline = (index == @choices.length - 1) ? '' : "\n"
206
+ @prompt.print(message + newline)
205
207
  end
206
208
  end
207
209
  end # List
@@ -0,0 +1,85 @@
1
+ # encoding: utf-8
2
+
3
+ module TTY
4
+ class Prompt
5
+ class MaskQuestion < Question
6
+ # Create masked question
7
+ #
8
+ # @param [Hash] options
9
+ # @option options [String] :mask
10
+ #
11
+ # @api public
12
+ def initialize(prompt, options = {})
13
+ super
14
+ @mask = options.fetch(:mask) { Symbols::ITEM_SECURE }
15
+ @done_masked = false
16
+ @failure = false
17
+ @prompt.subscribe(self)
18
+ end
19
+
20
+ # Set character for masking the STDIN input
21
+ #
22
+ # @param [String] char
23
+ #
24
+ # @return [self]
25
+ #
26
+ # @api public
27
+ def mask(char = (not_set = true))
28
+ return @mask if not_set
29
+ @mask = char
30
+ end
31
+
32
+ def keyreturn(event)
33
+ @done_masked = true
34
+ end
35
+
36
+ def keyenter(event)
37
+ @done_masked = true
38
+ end
39
+
40
+ def keypress(event)
41
+ if [:backspace, :delete].include?(event.key.name)
42
+ @input.chop! unless @input.empty?
43
+ elsif event.value =~ /^[^\e\n\r]/
44
+ @input += event.value
45
+ end
46
+ end
47
+
48
+ # Render question and input replaced with masked character
49
+ #
50
+ # @api private
51
+ def render_question
52
+ header = "#{prompt.prefix}#{message} "
53
+ if echo?
54
+ masked = "#{@mask * "#{@input}".length}"
55
+ if @done_masked && !@failure
56
+ masked = @prompt.decorate(masked, @color)
57
+ end
58
+ header += masked
59
+ end
60
+ @prompt.print(header)
61
+ @prompt.print("\n") if @done
62
+ end
63
+
64
+ def render_error_or_finish(result)
65
+ @failure = result.failure?
66
+ super
67
+ end
68
+
69
+ # Read input from user masked by character
70
+ #
71
+ # @private
72
+ def read_input
73
+ @done_masked = false
74
+ @input = ''
75
+ until @done_masked
76
+ @prompt.read_keypress(echo?)
77
+ @prompt.print(@prompt.clear_line)
78
+ render_question
79
+ end
80
+ @prompt.print("\n")
81
+ @input
82
+ end
83
+ end # MaskQuestion
84
+ end # Prompt
85
+ end # TTY
@@ -25,6 +25,18 @@ module TTY
25
25
  @default = options.fetch(:default) { [] }
26
26
  end
27
27
 
28
+ # Callback fired when space key is pressed
29
+ #
30
+ # @api private
31
+ def keyspace(event)
32
+ active_choice = @choices[@active - 1]
33
+ if @selected.include?(active_choice)
34
+ @selected.delete(active_choice)
35
+ else
36
+ @selected << active_choice
37
+ end
38
+ end
39
+
28
40
  private
29
41
 
30
42
  # Setup default options and active selection
@@ -36,40 +48,16 @@ module TTY
36
48
  @active = @default.last unless @selected.empty?
37
49
  end
38
50
 
39
- # Process keyboard input and maintain selected choices
40
- #
41
- # @api private
42
- def process_input
43
- chars = @reader.read_keypress
44
- case chars
45
- when Codes::SIGINT, Codes::ESCAPE
46
- exit 130
47
- when Codes::RETURN
48
- @done = true
49
- when Codes::KEY_UP, Codes::CTRL_K, Codes::CTRL_P
50
- @active = (@active == 1) ? @choices.length : @active - 1
51
- when Codes::KEY_DOWN, Codes::CTRL_J, Codes::CTRL_N
52
- @active = (@active == @choices.length) ? 1 : @active + 1
53
- when Codes::SPACE
54
- active_choice = @choices[@active - 1]
55
- if @selected.include?(active_choice)
56
- @selected.delete(active_choice)
57
- else
58
- @selected << active_choice
59
- end
60
- end
61
- end
62
-
63
51
  # Render initial help text and then currently selected choices
64
52
  #
65
53
  # @api private
66
54
  def render_header
67
55
  if @done
68
- @pastel.decorate(@selected.map(&:name).join(', '), :green)
56
+ @prompt.decorate(@selected.map(&:name).join(', '), :green)
69
57
  elsif @selected.size.nonzero?
70
58
  @selected.map(&:name).join(', ')
71
59
  elsif @first_render
72
- @pastel.decorate(@help, :bright_black)
60
+ @prompt.decorate(@help, :bright_black)
73
61
  else
74
62
  ''
75
63
  end
@@ -89,15 +77,16 @@ module TTY
89
77
  # @api private
90
78
  def render_menu
91
79
  @choices.each_with_index do |choice, index|
92
- indicator = (index + 1 == @active) ? @marker : Codes::SPACE
93
- indicator += Codes::SPACE
80
+ indicator = (index + 1 == @active) ? @marker : Symbols::SPACE
81
+ indicator += Symbols::SPACE
94
82
  message = if @selected.include?(choice)
95
- selected = @pastel.decorate(Codes::RADIO_CHECKED, :green)
96
- selected + Codes::SPACE + choice.name
83
+ selected = @prompt.decorate(Symbols::RADIO_CHECKED, :green)
84
+ selected + Symbols::SPACE + choice.name
97
85
  else
98
- Codes::RADIO_UNCHECKED + Codes::SPACE + choice.name
86
+ Symbols::RADIO_UNCHECKED + Symbols::SPACE + choice.name
99
87
  end
100
- @prompt.output.puts(indicator + message)
88
+ newline = (index == @choices.length - 1) ? '' : "\n"
89
+ @prompt.print(indicator + message + newline)
101
90
  end
102
91
  end
103
92
  end # MultiList
@@ -2,61 +2,61 @@
2
2
 
3
3
  require 'tty/prompt/question/modifier'
4
4
  require 'tty/prompt/question/validation'
5
- require 'tty/prompt/response_delegation'
5
+ require 'tty/prompt/question/checks'
6
+ require 'tty/prompt/converter_dsl'
7
+ require 'tty/prompt/converters'
6
8
 
7
9
  module TTY
8
10
  # A class responsible for shell prompt interactions.
9
11
  class Prompt
10
- # A class representing a command line question
12
+ # A class responsible for gathering user input
13
+ #
14
+ # @api public
11
15
  class Question
12
- include ResponseDelegation
16
+ include Checks
17
+ include Converters
18
+
19
+ BLANK_REGEX = /\A[[:space:]]*\z/o.freeze
20
+
21
+ UndefinedSetting = Module.new
13
22
 
14
23
  # Store question message
15
24
  # @api public
16
25
  attr_reader :message
17
26
 
18
- # Store default value.
19
- #
20
- # @api private
21
- attr_reader :default_value
22
-
23
- attr_reader :validation
24
-
25
- # Controls character processing of the answer
26
- #
27
- # @api public
28
27
  attr_reader :modifier
29
28
 
30
- attr_reader :error
31
-
32
- # Returns character mode
33
- #
34
- # @api public
35
- attr_reader :character
36
-
37
- # @api private
38
29
  attr_reader :prompt
39
30
 
31
+ attr_reader :validation
32
+
40
33
  # Initialize a Question
41
34
  #
42
35
  # @api public
43
36
  def initialize(prompt, options = {})
44
- @prompt = prompt || Prompt.new
45
- @required = options.fetch(:required) { false }
46
- @echo = options.fetch(:echo) { true }
47
- @raw = options.fetch(:raw) { false }
48
- @mask = options.fetch(:mask) { false }
49
- @character = options.fetch(:character) { false }
50
- @in = options.fetch(:in) { false }
51
- @modifier = Modifier.new options.fetch(:modifier) { [] }
52
- @validation = Validation.new(options.fetch(:validation) { nil })
53
- @default = options.fetch(:default) { nil }
54
- @error = false
55
- @converter = Necromancer.new
56
- @read = options.fetch(:read) { nil }
37
+ @prompt = prompt
38
+ @default = options.fetch(:default) { UndefinedSetting }
39
+ @required = options.fetch(:required) { false }
40
+ @echo = options.fetch(:echo) { true }
41
+ @in = options.fetch(:in) { UndefinedSetting }
42
+ @modifier = options.fetch(:modifier) { [] }
43
+ @validation = options.fetch(:validation) { UndefinedSetting }
44
+ @read = options.fetch(:read) { UndefinedSetting }
45
+ @convert = options.fetch(:convert) { UndefinedSetting }
46
+ @color = options.fetch(:color) { :green }
47
+ @done = false
48
+ @input = nil
49
+
50
+ @evaluator = Evaluator.new(self)
51
+
52
+ @evaluator << CheckRequired
53
+ @evaluator << CheckDefault
54
+ @evaluator << CheckRange
55
+ @evaluator << CheckValidation
56
+ @evaluator << CheckModifier
57
57
  end
58
58
 
59
- # Call the quesiton
59
+ # Call the question
60
60
  #
61
61
  # @param [String] message
62
62
  #
@@ -64,160 +64,222 @@ module TTY
64
64
  #
65
65
  # @api public
66
66
  def call(message, &block)
67
+ return if blank?(message)
67
68
  @message = message
68
69
  block.call(self) if block
69
- prompt.output.print("#{prompt.prefix}#{message}")
70
70
  render
71
71
  end
72
72
 
73
- # Reader answer and convert to type
73
+ # Read answer and convert to type
74
74
  #
75
75
  # @api private
76
76
  def render
77
- dispatch.read_type(@read)
77
+ until @done
78
+ render_question
79
+ result = process_input
80
+ errors = result.errors
81
+ render_error_or_finish(result)
82
+ refresh(errors.count)
83
+ end
84
+ render_question
85
+ convert_result(result.value)
78
86
  end
79
87
 
80
- # Set default value.
88
+ # Render question
81
89
  #
82
- # @api public
83
- def default(value)
84
- return @default unless value
85
- @default = value
90
+ # @api private
91
+ def render_question
92
+ header = "#{prompt.prefix}#{message} "
93
+ if @convert == :bool && !@done
94
+ header += @prompt.decorate('(Y/n)', :bright_black) + ' '
95
+ elsif !echo?
96
+ header
97
+ elsif @done
98
+ header += @prompt.decorate("#{@input}", @color)
99
+ elsif default?
100
+ header += @prompt.decorate("(#{default})", :bright_black) + ' '
101
+ end
102
+ @prompt.print(header)
103
+ @prompt.print("\n") if @done
86
104
  end
87
105
 
88
- # Check if default value is set
106
+ # Decide how to handle input from user
89
107
  #
90
- # @return [Boolean]
91
- #
92
- # @api public
93
- def default?
94
- !!@default
108
+ # @api private
109
+ def process_input
110
+ @input = read_input
111
+ if blank?(@input)
112
+ @input = default? ? default : nil
113
+ end
114
+ @evaluator.(@input)
95
115
  end
96
116
 
97
- # Ensure that passed argument is present or not
98
- #
99
- # @return [Boolean]
117
+ # Process input
100
118
  #
101
- # @api public
102
- def required(value)
103
- @required = value
119
+ # @api private
120
+ def read_input
121
+ case @read
122
+ when :keypress
123
+ @prompt.read_keypress
124
+ when :multiline
125
+ @prompt.read_multiline
126
+ else
127
+ @prompt.read_line(echo)
128
+ end
104
129
  end
105
130
 
106
- # Set validation rule for an argument
131
+ # Handle error condition
107
132
  #
108
- # @param [Object] value
133
+ # @api private
134
+ def render_error_or_finish(result)
135
+ if result.failure?
136
+ result.errors.each do |err|
137
+ @prompt.print(@prompt.clear_line)
138
+ @prompt.print(@prompt.decorate('>>', :red) + ' ' + err)
139
+ end
140
+ @prompt.print(@prompt.cursor.up(result.errors.count))
141
+ else
142
+ @done = true
143
+ if result.errors.count.nonzero?
144
+ @prompt.print(@prompt.cursor.down(result.errors.count))
145
+ end
146
+ end
147
+ end
148
+
149
+ # Determine area of the screen to clear
109
150
  #
110
- # @return [Question]
151
+ # @param [Integer] errors
111
152
  #
112
- # @api public
113
- def validate(value = nil, &block)
114
- @validation = Validation.new(value || block)
153
+ # @api private
154
+ def refresh(errors = nil)
155
+ lines = @message.scan("\n").length
156
+ lines += ((!echo? || errors.nonzero?) ? 1 : 2) # clear user enter
157
+
158
+ if errors.nonzero? && @done
159
+ lines += errors
160
+ end
161
+
162
+ @prompt.print(@prompt.clear_lines(lines))
115
163
  end
116
164
 
117
- # Modify string according to the rule given.
165
+ # Convert value to expected type
118
166
  #
119
- # @param [Symbol] rule
167
+ # @param [Object] value
120
168
  #
121
- # @api public
122
- def modify(*rules)
123
- @modifier = Modifier.new(*rules)
169
+ # @api private
170
+ def convert_result(value)
171
+ if convert? & !blank?(value)
172
+ converter_registry.(@convert, value)
173
+ else
174
+ value
175
+ end
124
176
  end
125
177
 
126
- # Setup behaviour when error(s) occur
178
+ # Set reader type
127
179
  #
128
180
  # @api public
129
- def on_error(action = nil)
130
- @error = action
181
+ def read(value)
182
+ @read = value
131
183
  end
132
184
 
133
- # Check if error behaviour is set
185
+ # Specify answer conversion
134
186
  #
135
187
  # @api public
136
- def error?
137
- !!@error
188
+ def convert(value)
189
+ @convert = value
138
190
  end
139
191
 
140
- # Turn terminal echo on or off. This is used to secure the display so
141
- # that the entered characters are not echoed back to the screen.
192
+ # Check if conversion is set
193
+ #
194
+ # @return [Boolean]
142
195
  #
143
196
  # @api public
144
- def echo(value = nil)
145
- return @echo if value.nil?
146
- @echo = value
197
+ def convert?
198
+ @convert != UndefinedSetting
147
199
  end
148
200
 
149
- # Chec if echo is set
201
+ # Set default value.
150
202
  #
151
203
  # @api public
152
- def echo?
153
- !!@echo
204
+ def default(value = (not_set = true))
205
+ return @default if not_set
206
+ @default = value
154
207
  end
155
208
 
156
- # Turn raw mode on or off. This enables character-based input.
209
+ # Check if default value is set
210
+ #
211
+ # @return [Boolean]
157
212
  #
158
213
  # @api public
159
- def raw(value = nil)
160
- return @raw if value.nil?
161
- @raw = value
214
+ def default?
215
+ @default != UndefinedSetting
162
216
  end
163
217
 
164
- # Check if raw mode is set
218
+ # Ensure that passed argument is present or not
219
+ #
220
+ # @return [Boolean]
165
221
  #
166
222
  # @api public
167
- def raw?
168
- !!@raw
223
+ def required(value = (not_set = true))
224
+ return @required if not_set
225
+ @required = value
169
226
  end
227
+ alias_method :required?, :required
170
228
 
171
- # Set character for masking the STDIN input
229
+ # Set validation rule for an argument
172
230
  #
173
- # @param [String] character
231
+ # @param [Object] value
174
232
  #
175
- # @return [self]
233
+ # @return [Question]
176
234
  #
177
235
  # @api public
178
- def mask(char = nil)
179
- return @mask if char.nil?
180
- @mask = char
236
+ def validate(value = nil, &block)
237
+ @validation = (value || block)
181
238
  end
182
239
 
183
- # Check if character mask is set
240
+ def validation?
241
+ @validation != UndefinedSetting
242
+ end
243
+
244
+ # Modify string according to the rule given.
184
245
  #
185
- # @return [Boolean]
246
+ # @param [Symbol] rule
186
247
  #
187
248
  # @api public
188
- def mask?
189
- !!@mask
249
+ def modify(*rules)
250
+ @modifier = rules
190
251
  end
191
252
 
192
- # Set if the input is character based or not
193
- #
194
- # @param [Boolean] value
195
- #
196
- # @return [self]
253
+ # Turn terminal echo on or off. This is used to secure the display so
254
+ # that the entered characters are not echoed back to the screen.
197
255
  #
198
256
  # @api public
199
- def char(value = nil)
200
- return @character if value.nil?
201
- @character = value
257
+ def echo(value = nil)
258
+ return @echo if value.nil?
259
+ @echo = value
202
260
  end
261
+ alias_method :echo?, :echo
203
262
 
204
- # Check if character intput is set
205
- #
206
- # @return [Boolean]
263
+ # Turn raw mode on or off. This enables character-based input.
207
264
  #
208
265
  # @api public
209
- def character?
210
- !!@character
266
+ def raw(value = nil)
267
+ return @raw if value.nil?
268
+ @raw = value
211
269
  end
270
+ alias_method :raw?, :raw
212
271
 
213
- # Set expect range of values
272
+ # Set expected range of values
214
273
  #
215
274
  # @param [String] value
216
275
  #
217
276
  # @api public
218
- def in(value = nil)
219
- return @in if value.nil?
220
- @in = @converter.convert(value).to(:range, strict: true)
277
+ def in(value = (not_set = true))
278
+ if in? && !@in.is_a?(Range)
279
+ @in = converter_registry.(:range, @in)
280
+ end
281
+ return @in if not_set
282
+ @in = converter_registry.(:range, value)
221
283
  end
222
284
 
223
285
  # Check if range is set
@@ -226,63 +288,23 @@ module TTY
226
288
  #
227
289
  # @api public
228
290
  def in?
229
- !!@in
291
+ @in != UndefinedSetting
230
292
  end
231
293
 
232
- # Check if response matches all the requirements set by the question
233
- #
234
- # @param [Object] value
235
- #
236
- # @return [Object]
237
- #
238
- # @api private
239
- def evaluate_response(input)
240
- return @default if !input && default?
241
- check_required(input)
242
- return if input.nil?
243
-
244
- within?(input)
245
- validation.(input)
246
- modifier.apply_to(input)
247
- end
248
-
249
- # Reset question object.
250
- #
251
- # @api public
252
- def clean
253
- @message = nil
254
- @default = nil
255
- @required = false
256
- @modifier = nil
294
+ def blank?(value)
295
+ value.nil? ||
296
+ value.respond_to?(:empty?) && value.empty? ||
297
+ BLANK_REGEX === value
257
298
  end
258
299
 
259
300
  def to_s
260
301
  "#{message}"
261
302
  end
262
303
 
304
+ # String representation of this question
305
+ # @api public
263
306
  def inspect
264
- "#<Question @message=#{message}>"
265
- end
266
-
267
- private
268
-
269
- # Check if value is present
270
- #
271
- # @api private
272
- def check_required(value)
273
- if @required && !default? && value.nil?
274
- fail ArgumentRequired, 'No value provided for required'
275
- end
276
- end
277
-
278
- # Check if value is within expected range
279
- #
280
- # @api private
281
- def within?(value)
282
- if in? && value
283
- @in.include?(value) || fail(InvalidArgument,
284
- "Value #{value} is not included in the range #{@in}")
285
- end
307
+ "#<#{self.class.name} @message=#{message}, @input=#{@input}>"
286
308
  end
287
309
  end # Question
288
310
  end # Prompt