tty-prompt 0.1.0 → 0.2.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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +0 -2
  3. data/CHANGELOG.md +12 -0
  4. data/README.md +223 -59
  5. data/lib/tty/prompt/choice.rb +83 -0
  6. data/lib/tty/prompt/choices.rb +92 -0
  7. data/lib/tty/prompt/codes.rb +32 -0
  8. data/lib/tty/prompt/cursor.rb +131 -0
  9. data/lib/tty/prompt/list.rb +209 -0
  10. data/lib/tty/prompt/mode/echo.rb +10 -9
  11. data/lib/tty/prompt/mode/raw.rb +10 -9
  12. data/lib/tty/prompt/multi_list.rb +105 -0
  13. data/lib/tty/prompt/question/validation.rb +12 -27
  14. data/lib/tty/prompt/question.rb +58 -107
  15. data/lib/tty/prompt/reader.rb +44 -11
  16. data/lib/tty/prompt/response.rb +31 -36
  17. data/lib/tty/prompt/response_delegation.rb +3 -2
  18. data/lib/tty/prompt/statement.rb +10 -10
  19. data/lib/tty/prompt/test.rb +15 -0
  20. data/lib/tty/prompt/version.rb +3 -3
  21. data/lib/tty/prompt.rb +72 -9
  22. data/lib/tty-prompt.rb +11 -0
  23. data/spec/unit/ask_spec.rb +32 -35
  24. data/spec/unit/choice/eql_spec.rb +24 -0
  25. data/spec/unit/choice/from_spec.rb +25 -0
  26. data/spec/unit/choices/add_spec.rb +14 -0
  27. data/spec/unit/choices/each_spec.rb +15 -0
  28. data/spec/unit/choices/new_spec.rb +12 -0
  29. data/spec/unit/choices/pluck_spec.rb +11 -0
  30. data/spec/unit/cursor/new_spec.rb +74 -0
  31. data/spec/unit/error_spec.rb +4 -8
  32. data/spec/unit/multi_select_spec.rb +163 -0
  33. data/spec/unit/question/character_spec.rb +5 -16
  34. data/spec/unit/question/default_spec.rb +4 -10
  35. data/spec/unit/question/in_spec.rb +15 -12
  36. data/spec/unit/question/initialize_spec.rb +1 -6
  37. data/spec/unit/question/modify_spec.rb +25 -24
  38. data/spec/unit/question/required_spec.rb +31 -0
  39. data/spec/unit/question/validate_spec.rb +25 -17
  40. data/spec/unit/question/validation/call_spec.rb +22 -0
  41. data/spec/unit/response/read_bool_spec.rb +38 -27
  42. data/spec/unit/response/read_char_spec.rb +5 -8
  43. data/spec/unit/response/read_date_spec.rb +8 -12
  44. data/spec/unit/response/read_email_spec.rb +25 -22
  45. data/spec/unit/response/read_multiple_spec.rb +11 -13
  46. data/spec/unit/response/read_number_spec.rb +12 -16
  47. data/spec/unit/response/read_range_spec.rb +10 -13
  48. data/spec/unit/response/read_spec.rb +39 -38
  49. data/spec/unit/response/read_string_spec.rb +7 -12
  50. data/spec/unit/say_spec.rb +10 -14
  51. data/spec/unit/select_spec.rb +192 -0
  52. data/spec/unit/statement/initialize_spec.rb +0 -4
  53. data/spec/unit/suggest_spec.rb +6 -9
  54. data/spec/unit/warn_spec.rb +4 -8
  55. metadata +32 -8
  56. data/spec/unit/question/argument_spec.rb +0 -30
  57. data/spec/unit/question/valid_spec.rb +0 -46
  58. data/spec/unit/question/validation/valid_value_spec.rb +0 -22
@@ -0,0 +1,131 @@
1
+ # encoding: utf-8
2
+
3
+ module TTY
4
+ class Prompt
5
+ class Cursor
6
+ ECMA_CSI = "\e[".freeze
7
+ DEC_RST = 'l'.freeze
8
+ DEC_SET = 'h'.freeze
9
+ DEC_TCEM = '?25'.freeze
10
+ ECMA_CLR = 'K'.freeze
11
+
12
+ attr_reader :shell
13
+
14
+ def initialize(stream = nil, options = {})
15
+ @stream = stream || $stdout
16
+ @shell = options.fetch(:shell, false)
17
+ @hidden = options.fetch(:hidden, false)
18
+ end
19
+
20
+ def print
21
+ self.class.new(@stream, shell: true)
22
+ end
23
+
24
+ def show
25
+ @hidden = false
26
+ ECMA_CSI + DEC_TCEM + DEC_SET
27
+ end
28
+
29
+ def hide
30
+ show if @hidden
31
+ @hidden = true
32
+ ECMA_CSI + DEC_TCEM + DEC_RST
33
+ end
34
+
35
+ # Switch off cursor for the block
36
+ # @api public
37
+ def invisible
38
+ hide
39
+ yield
40
+ ensure
41
+ show
42
+ end
43
+
44
+ # Save current position
45
+ # @api public
46
+ def save
47
+ ECMA_CSI + "s"
48
+ end
49
+
50
+ # Restore cursor position
51
+ # @api public
52
+ def restore
53
+ ECMA_CSI + "u"
54
+ end
55
+
56
+ def current
57
+ ECMA_CSI + "6n"
58
+ end
59
+
60
+ # Move cursor relative to its current position
61
+ # @api public
62
+ def move(x, y)
63
+ end
64
+
65
+ # Move cursor up by number of lines
66
+ #
67
+ # @param [Integer] count
68
+ #
69
+ # @api public
70
+ def move_up(count = nil)
71
+ ECMA_CSI + "#{(count || 1)}A"
72
+ end
73
+
74
+ def move_down(count = nil)
75
+ ECMA_CSI + "#{(count || 1)}B"
76
+ end
77
+
78
+ # Move to start of the line
79
+ #
80
+ # @api public
81
+ def move_start
82
+ ECMA_CSI + '1000D'
83
+ end
84
+
85
+ # @param [Integer] count
86
+ # how far to go left
87
+ # @api public
88
+ def move_left(count = nil)
89
+ ECMA_CSI + "#{count || 1}D"
90
+ end
91
+
92
+ # @api public
93
+ def move_right(count = nil)
94
+ ECMA_CSI + "#{count || 1}C"
95
+ end
96
+
97
+ def next_line
98
+ ECMA_CSI + 'E'
99
+ end
100
+
101
+ def prev_line
102
+ ECMA_CSI + 'F'
103
+ end
104
+
105
+ # @api public
106
+ def clear_line
107
+ move_start + ECMA_CSI + ECMA_CLR
108
+ end
109
+
110
+ # @api public
111
+ def clear_lines(amount, direction = :up)
112
+ amount.times.reduce("") do |acc|
113
+ dir = direction == :up ? move_up : move_down
114
+ acc << dir + clear_line
115
+ end
116
+ end
117
+
118
+ # Clear screen down from current position
119
+ # @api public
120
+ def clear_down
121
+ ECMA_CSI + "J"
122
+ end
123
+
124
+ # Clear screen up from current position
125
+ # @api public
126
+ def clear_up
127
+ ECMA_CSI + "1J"
128
+ end
129
+ end # Cursor
130
+ end # Prompt
131
+ end # TTY
@@ -0,0 +1,209 @@
1
+ # encoding: utf-8
2
+
3
+ module TTY
4
+ class Prompt
5
+ # A class responsible for rendering select list menu
6
+ # Used by {Prompt} to display interactive menu.
7
+ #
8
+ # @api private
9
+ class List
10
+ HELP = '(Use arrow keys, press Enter to select)'.freeze
11
+
12
+ # Create instance of TTY::Prompt::List menu.
13
+ #
14
+ # @param Hash options
15
+ # the configuration options
16
+ # @option options [Symbol] :default
17
+ # the default active choice, defaults to 1
18
+ # @option options [Symbol] :color
19
+ # the color for the selected item, defualts to :green
20
+ # @option options [Symbol] :marker
21
+ # the marker for the selected item
22
+ #
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
+
30
+ @first_render = true
31
+ @done = false
32
+ @default = Array[options.fetch(:default) { 1 }]
33
+ @active = @default.first
34
+ @choices = Choices.new
35
+ @color = options.fetch(:color) { :green }
36
+ @marker = options.fetch(:marker) { Codes::ITEM_SELECTED }
37
+ @help = options.fetch(:help) { HELP }
38
+ end
39
+
40
+ # Set marker
41
+ #
42
+ # @api public
43
+ def marker(value)
44
+ @marker = value
45
+ end
46
+
47
+ # Set default option selected
48
+ #
49
+ # @api public
50
+ def default(*default_values)
51
+ @default = default_values
52
+ end
53
+
54
+ # Add a single choice
55
+ #
56
+ # @api public
57
+ def choice(*value, &block)
58
+ if block
59
+ @choices << (value << block)
60
+ else
61
+ @choices << value
62
+ end
63
+ end
64
+
65
+ # Add multiple choices
66
+ #
67
+ # @param [Array[Object]] values
68
+ # the values to add as choices
69
+ #
70
+ # @api public
71
+ def choices(values)
72
+ values.each { |val| choice(*val) }
73
+ end
74
+
75
+ # Call the list menu by passing question and choices
76
+ #
77
+ # @param [String] question
78
+ #
79
+ # @param
80
+ # @api public
81
+ def call(question, possibilities, &block)
82
+ choices(possibilities)
83
+ @question = question
84
+ block.call(self) if block
85
+ setup_defaults
86
+ render
87
+ end
88
+
89
+ private
90
+
91
+ # Setup default option and active selection
92
+ #
93
+ # @api private
94
+ def setup_defaults
95
+ validate_defaults
96
+ @active = @default.first
97
+ end
98
+
99
+ # Validate default indexes to be within range
100
+ #
101
+ # @api private
102
+ def validate_defaults
103
+ @default.each do |d|
104
+ if d < 1 || d > @choices.size
105
+ fail PromptConfigurationError,
106
+ "default index `#{d}` out of range (1 - #{@choices.size})"
107
+ end
108
+ end
109
+ end
110
+
111
+ # Render a selection list.
112
+ #
113
+ # By default the result is printed out.
114
+ #
115
+ # @return [Object] value
116
+ # return the selected value
117
+ #
118
+ # @api private
119
+ def render
120
+ @prompt.output.print(@cursor.hide)
121
+ until @done
122
+ render_question
123
+ process_input
124
+ refresh
125
+ end
126
+ render_question
127
+ answer = render_answer
128
+ ensure
129
+ @prompt.output.print(@cursor.show)
130
+ answer
131
+ end
132
+
133
+ # Find value for the choice selected
134
+ #
135
+ # @return [nil, Object]
136
+ #
137
+ # @api private
138
+ def render_answer
139
+ @choices[@active - 1].value
140
+ end
141
+
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
+ # Determine area of the screen to clear
160
+ #
161
+ # @api private
162
+ def refresh
163
+ lines = @question.scan("\n").length + @choices.length + 1
164
+ @prompt.output.print(@cursor.clear_lines(lines))
165
+ end
166
+
167
+ # Render question with instructions and menu
168
+ #
169
+ # @api private
170
+ def render_question
171
+ header = @question + Codes::SPACE + render_header
172
+ @prompt.output.puts(header)
173
+ @first_render = false
174
+ render_menu unless @done
175
+ end
176
+
177
+ # Render initial help and selected choice
178
+ #
179
+ # @return [String]
180
+ #
181
+ # @api private
182
+ def render_header
183
+ if @done
184
+ selected_item = "#{@choices[@active - 1].name}"
185
+ @pastel.decorate(selected_item, @color)
186
+ elsif @first_render
187
+ @pastel.decorate(@help, :bright_black)
188
+ else
189
+ ''
190
+ end
191
+ end
192
+
193
+ # Render menu with choices to select from
194
+ #
195
+ # @api private
196
+ def render_menu
197
+ @choices.each_with_index do |choice, index|
198
+ message = if index + 1 == @active
199
+ selected = @marker + Codes::SPACE + choice.name
200
+ @pastel.decorate("#{selected}", @color)
201
+ else
202
+ Codes::SPACE * 2 + choice.name
203
+ end
204
+ @prompt.output.puts(message)
205
+ end
206
+ end
207
+ end # List
208
+ end # Prompt
209
+ end # TTY
@@ -24,15 +24,16 @@ module TTY
24
24
  # @api public
25
25
  def echo(is_on=true, &block)
26
26
  value = nil
27
- begin
28
- off unless is_on
29
- value = block.call if block_given?
30
- on
31
- return value
32
- rescue NoMethodError, Interrupt
33
- on
34
- exit
35
- end
27
+ off unless is_on
28
+ value = block.call if block_given?
29
+ rescue NoMethodError, Interrupt => error
30
+ puts "#{error.class} #{error.message}"
31
+ puts error.backtrace
32
+ on
33
+ exit
34
+ ensure
35
+ on
36
+ return value
36
37
  end
37
38
  end # Echo
38
39
  end # Mode
@@ -24,15 +24,16 @@ module TTY
24
24
  # @api public
25
25
  def raw(is_on=true, &block)
26
26
  value = nil
27
- begin
28
- on if is_on
29
- value = block.call if block_given?
30
- off
31
- return value
32
- rescue NoMethodError, Interrupt
33
- off
34
- exit
35
- end
27
+ on if is_on
28
+ value = block.call if block_given?
29
+ rescue NoMethodError, Interrupt => error
30
+ puts "#{error.class} #{error.message}"
31
+ puts error.backtrace
32
+ off
33
+ exit
34
+ ensure
35
+ off
36
+ return value
36
37
  end
37
38
  end # Raw
38
39
  end # Mode
@@ -0,0 +1,105 @@
1
+ # encoding: utf-8
2
+
3
+ require 'tty/prompt/list'
4
+
5
+ module TTY
6
+ class Prompt
7
+ # A class responsible for rendering multi select list menu.
8
+ # Used by {Prompt} to display interactive choice menu.
9
+ #
10
+ # @api private
11
+ class MultiList < List
12
+
13
+ HELP = '(Use arrow keys, press Space to select and Enter to finish)'.freeze
14
+
15
+ # Create instance of TTY::Prompt::MultiList menu.
16
+ #
17
+ # @param [Prompt] :prompt
18
+ # @param [Hash] options
19
+ #
20
+ # @api public
21
+ def initialize(prompt, options)
22
+ super
23
+ @selected = []
24
+ @help = options.fetch(:help) { HELP }
25
+ @default = options.fetch(:default) { [] }
26
+ end
27
+
28
+ private
29
+
30
+ # Setup default options and active selection
31
+ #
32
+ # @api private
33
+ def setup_defaults
34
+ validate_defaults
35
+ @selected = @choices.values_at(*@default.map { |d| d - 1 })
36
+ @active = @default.last unless @selected.empty?
37
+ end
38
+
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
+ # Render initial help text and then currently selected choices
64
+ #
65
+ # @api private
66
+ def render_header
67
+ if @done
68
+ @pastel.decorate(@selected.map(&:name).join(', '), :green)
69
+ elsif @selected.size.nonzero?
70
+ @selected.map(&:name).join(', ')
71
+ elsif @first_render
72
+ @pastel.decorate(@help, :bright_black)
73
+ else
74
+ ''
75
+ end
76
+ end
77
+
78
+ # All values for the choices selected
79
+ #
80
+ # @return [Array[nil,Object]]
81
+ #
82
+ # @api private
83
+ def render_answer
84
+ @selected.map(&:value)
85
+ end
86
+
87
+ # Render menu with choices to select from
88
+ #
89
+ # @api private
90
+ def render_menu
91
+ @choices.each_with_index do |choice, index|
92
+ indicator = (index + 1 == @active) ? @marker : Codes::SPACE
93
+ indicator += Codes::SPACE
94
+ message = if @selected.include?(choice)
95
+ selected = @pastel.decorate(Codes::RADIO_CHECKED, :green)
96
+ selected + Codes::SPACE + choice.name
97
+ else
98
+ Codes::RADIO_UNCHECKED + Codes::SPACE + choice.name
99
+ end
100
+ @prompt.output.puts(indicator + message)
101
+ end
102
+ end
103
+ end # MultiList
104
+ end # Prompt
105
+ end # TTY
@@ -47,39 +47,24 @@ module TTY
47
47
  !!validation
48
48
  end
49
49
 
50
- # Test if the value matches the validation
50
+ # Test if the input passes the validation
51
51
  #
52
52
  # @example
53
- # validation.valid_value?(value) # => true or false
53
+ # Validation.new
54
+ # validation.valid?(input) # => true
54
55
  #
55
- # @param [Object] value
56
- # the value to validate
56
+ # @param [Object] input
57
+ # the input to validate
57
58
  #
58
- # @return [undefined]
59
+ # @return [Boolean]
59
60
  #
60
61
  # @api public
61
- def valid_value?(value)
62
- check_validation(value)
63
- end
64
-
65
- private
66
-
67
- # Check if provided value passes validation
68
- #
69
- # @param [String] value
70
- #
71
- # @raise [TTY::InvalidArgument] unkown type of argument
72
- #
73
- # @return [undefined]
74
- #
75
- # @api private
76
- def check_validation(value)
77
- if validate? && value
78
- value = value.to_s
79
- if validation.is_a?(Regexp) && validation =~ value
80
- elsif validation.is_a?(Proc) && validation.call(value)
81
- else
82
- fail InvalidArgument, "Invalid input for #{value}"
62
+ def call(input)
63
+ if validate? && input
64
+ input = input.to_s
65
+ if validation.is_a?(Regexp) && validation =~ input
66
+ elsif validation.is_a?(Proc) && validation.call(input)
67
+ else fail InvalidArgument, "Invalid input for #{input}"
83
68
  end
84
69
  true
85
70
  else