tty-prompt 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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