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
@@ -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