highline 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,333 @@
1
+ #!/usr/local/bin/ruby -w
2
+
3
+ # menu.rb
4
+ #
5
+ # Created by Gregory Thomas Brown on 2005-05-10.
6
+ # Copyright 2005 smtose.org. All rights reserved.
7
+
8
+ require "highline/question"
9
+
10
+ class HighLine
11
+ #
12
+ # Menu objects encapsulate all the details of a call to HighLine.choose().
13
+ # Using the accessors and Menu.choice() and Menu.choices(), the block passed
14
+ # to HighLine.choose() can detail all aspects of menu display and control.
15
+ #
16
+ class Menu < Question
17
+ #
18
+ # Create an instance of HighLine::Menu. All customization is done
19
+ # through the passed block, which should call accessors and choice() and
20
+ # choices() as needed to define the Menu. Note that Menus are also
21
+ # Questions, so all that functionality is available to the block as
22
+ # well.
23
+ #
24
+ def initialize( )
25
+ #
26
+ # Initialize Question objects with ignored valued, we'll
27
+ # adjust ours as needed.
28
+ #
29
+ super("Ignored", [ ], &nil) # avoiding passing to block along
30
+
31
+ @items = [ ]
32
+
33
+ @index = :number
34
+ @index_suffix = ". "
35
+ @select_by = :index_or_name
36
+ @flow = :rows
37
+ @list_option = nil
38
+ @header = nil
39
+ @prompt = "? "
40
+ @layout = :list
41
+ @shell = false
42
+ @nil_on_handled = false
43
+
44
+ # Override Questions repsonses, we'll set our own.
45
+ @responses = { }
46
+
47
+ yield self if block_given?
48
+
49
+ update_responses # rebuild responses based on our settings
50
+ end
51
+
52
+ #
53
+ # An _index_ to append to each menu item in display. See
54
+ # Menu.index=() for details.
55
+ #
56
+ attr_reader :index
57
+ #
58
+ # The String placed between an _index_ and a menu item. Defaults to
59
+ # ". ". Switches to " ", when _index_ is set to a String (like "-").
60
+ #
61
+ attr_accessor :index_suffix
62
+ #
63
+ # The _select_by_ attribute controls how the user is allowed to pick a
64
+ # menu item. The available choices are:
65
+ #
66
+ # <tt>:index</tt>:: The user is allowed to type the numerical
67
+ # or alphetical index for their selection.
68
+ # <tt>:index_or_name</tt>:: Allows both methods from the
69
+ # <tt>:index</tt> option and the
70
+ # <tt>:name</tt> option.
71
+ # <tt>:name</tt>:: Menu items are selected by typing a portion
72
+ # of the item name that will be
73
+ # auto-completed.
74
+ #
75
+ attr_accessor :select_by
76
+ #
77
+ # This attribute is passed directly on as the mode to HighLine.list() by
78
+ # all the preset layouts. See that method for appropriate settings.
79
+ #
80
+ attr_accessor :flow
81
+ #
82
+ # This setting is passed on as the third parameter to HighLine.list()
83
+ # by all the preset layouts. See that method for details of its
84
+ # effects. Defaults to +nil+.
85
+ #
86
+ attr_accessor :list_option
87
+ #
88
+ # Used by all the preset layouts to display title and/or introductory
89
+ # information, when set. Defaults to +nil+.
90
+ #
91
+ attr_accessor :header
92
+ #
93
+ # Used by all the preset layouts to ask the actual question to fetch a
94
+ # menu selection from the user. Defaults to "? ".
95
+ #
96
+ attr_accessor :prompt
97
+ #
98
+ # An ERb _layout_ to use when displaying this Menu object. See
99
+ # Menu.layout=() for details.
100
+ #
101
+ attr_reader :layout
102
+ #
103
+ # When set to +true+, responses are allowed to be an entire line of
104
+ # input, including details beyond the command itself. Only the first
105
+ # "word" of input will be matched against the menu choices, but both the
106
+ # command selected and the rest of the line will be passed to provided
107
+ # action blocks. Defaults to +false+.
108
+ #
109
+ attr_accessor :shell
110
+ #
111
+ # When +true+, any selected item handled by provided action code, will
112
+ # return +nil+, instead of the results to the action code. This may
113
+ # prove handy when dealing with mixed menus where only the names of
114
+ # items without any code (and +nil+, of course) will be returned.
115
+ # Defaults to +false+.
116
+ #
117
+ attr_accessor :nil_on_handled
118
+
119
+ #
120
+ # Adds _name_ to the list of available menu items. Menu items will be
121
+ # displayed in the order they are added.
122
+ #
123
+ # An optional _action_ can be associated with this name and if provided,
124
+ # it will be called if the item is selected. The result of the method
125
+ # will be returned, unless _nil_on_handled_ is set (when you would get
126
+ # +nil+ instead). In _shell_ mode, a provided block will be passed the
127
+ # command chosen and any details that followed the command. Otherwise,
128
+ # just the command is passed.
129
+ #
130
+ def choice( name, &action )
131
+ @items << [name, action]
132
+ end
133
+
134
+ #
135
+ # A shortcut for multiple calls to the sister method choice(). <b>Be
136
+ # warned:</b> An _action_ set here will apply to *all* provided
137
+ # _names_. This is considered to be a feature, so you can easily
138
+ # hand-off interface processing to a different chunk of code.
139
+ #
140
+ def choices( *names, &action )
141
+ names.each { |n| choice(n, &action) }
142
+ end
143
+
144
+ #
145
+ # Sets the indexing style for this Menu object. Indexes are appended to
146
+ # menu items, when displayed in list form. The available settings are:
147
+ #
148
+ # <tt>:number</tt>:: Menu items will be indexed numerically, starting
149
+ # with 1. This is the default method of indexing.
150
+ # <tt>:letter</tt>:: Items will be indexed alphabetically, starting
151
+ # with a.
152
+ # <tt>:none</tt>:: No index will be appended to menu items.
153
+ # <i>any String</i>:: Will be used as the literal _index_.
154
+ #
155
+ # Setting the _index_ to <tt>:none</tt> a literal String, also adjusts
156
+ # _index_suffix_ to a single space and _select_by_ to <tt>:none</tt>.
157
+ # Because of this, you should make a habit of setting the _index_ first.
158
+ #
159
+ def index=( style )
160
+ @index = style
161
+
162
+ # Default settings.
163
+ if @index == :none or @index.is_a?(String)
164
+ @index_suffix = " "
165
+ @select_by = :name
166
+ end
167
+ end
168
+
169
+ #
170
+ # Setting a _layout_ with this method also adjusts some other attributes
171
+ # of the Menu object, to ideal defaults for the chosen _layout_. To
172
+ # account for that, you probably want to set a _layout_ first in your
173
+ # configuration block, if needed.
174
+ #
175
+ # Accepted settings for _layout_ are:
176
+ #
177
+ # <tt>:list</tt>:: The default _layout_. The _header_ if set
178
+ # will appear at the top on its own line with
179
+ # a trailing colon. Then the list of menu
180
+ # items will follow. Finally, the _prompt_
181
+ # will be used as the ask()-like question.
182
+ # <tt>:one_line</tt>:: A shorter _layout_ that fits on one line.
183
+ # The _header_ comes first followed by a
184
+ # colon and spaces, then the _prompt_ with menu
185
+ # items between trailing parenthesis.
186
+ # <tt>:menu_only</tt>:: Just the menu items, followed up by a likely
187
+ # short _prompt_.
188
+ # <i>any ERb String</i>:: Will be taken as the literal _layout_. This
189
+ # String can access <tt>@header</tt>,
190
+ # <tt>@menu</tt> and <tt>@prompt</tt>, but is
191
+ # otherwise evaluated in the typical HighLine
192
+ # context, to provide access to utilities like
193
+ # HighLine.list() primarily.
194
+ #
195
+ # If set to either <tt>:one_line</tt>, or <tt>:menu_only</tt>, _index_
196
+ # will default to <tt>:none</tt> and _flow_ will default to
197
+ # <tt>:inline</tt>.
198
+ #
199
+ def layout=( new_layout )
200
+ @layout = new_layout
201
+
202
+ # Default settings.
203
+ case @layout
204
+ when :one_line, :menu_only
205
+ self.index = :none
206
+ @flow = :inline
207
+ end
208
+ end
209
+
210
+ #
211
+ # This method returns all possible options for auto-completion, based
212
+ # on the settings of _index_ and _select_by_.
213
+ #
214
+ def options( )
215
+ by_index = if @index == :letter
216
+ l_index = "`"
217
+ @items.map { "#{l_index.succ!}" }
218
+ else
219
+ (1 .. @items.size).collect { |s| String(s) }
220
+ end
221
+ by_name = @items.collect { |c| c.first }
222
+
223
+ case @select_by
224
+ when :index then
225
+ by_index
226
+ when :name
227
+ by_name
228
+ else
229
+ by_index + by_name
230
+ end
231
+ end
232
+
233
+ #
234
+ # This method processes the auto-completed user selection, based on the
235
+ # rules for this Menu object. It an action was provided for the
236
+ # selection, it will be executed as described in Menu.choice().
237
+ #
238
+ def select( selection, details = nil )
239
+ # Find the selected action.
240
+ name, action = if selection =~ /^\d+$/
241
+ @items[selection.to_i - 1]
242
+ else
243
+ l_index = "`"
244
+ index = @items.map { "#{l_index.succ!}" }.index(selection)
245
+ @items.find { |c| c.first == selection } or @items[index]
246
+ end
247
+
248
+ # Run or return it.
249
+ if not @nil_on_handled and not action.nil?
250
+ if @shell
251
+ action.call(name, details)
252
+ else
253
+ action.call(name)
254
+ end
255
+ elsif action.nil?
256
+ name
257
+ else
258
+ nil
259
+ end
260
+ end
261
+
262
+ #
263
+ # Allows Menu objects to pass as Arrays, for use with HighLine.list().
264
+ # This method returns all menu items to be displayed, complete with
265
+ # indexes.
266
+ #
267
+ def to_ary( )
268
+ case @index
269
+ when :number
270
+ @items.map do |c|
271
+ "#{@items.index(c) + 1}#{@index_suffix}#{c.first}"
272
+ end
273
+ when :letter
274
+ l_index = "`"
275
+ @items.map { |c| "#{l_index.succ!}#{@index_suffix}#{c.first}" }
276
+ when :none
277
+ @items.map { |c| "#{c.first}" }
278
+ else
279
+ @items.map { |c| "#{index}#{@index_suffix}#{c.first}" }
280
+ end
281
+ end
282
+
283
+ #
284
+ # Allows Menu to behave as a String, just like Question. Returns the
285
+ # _layout_ to be rendered, which is used by HighLine.say().
286
+ #
287
+ def to_str( )
288
+ case @layout
289
+ when :list
290
+ '<%= if @header.nil? then '' else "#{@header}:\n" end %>' +
291
+ "<%= list( @menu, #{@flow.inspect},
292
+ #{@list_option.inspect} ) %>" +
293
+ "<%= @prompt %>"
294
+ when :one_line
295
+ '<%= if @header.nil? then '' else "#{@header}: " end %>' +
296
+ "<%= @prompt %>" +
297
+ "(<%= list( @menu, #{@flow.inspect},
298
+ #{@list_option.inspect} ) %>)" +
299
+ "<%= @prompt[/\s*$/] %>"
300
+ when :menu_only
301
+ "<%= list( @menu, #{@flow.inspect},
302
+ #{@list_option.inspect} ) %><%= @prompt %>"
303
+ else
304
+ @layout
305
+ end
306
+ end
307
+
308
+ #
309
+ # This method will update the intelligent responses to account for
310
+ # Menu specific differences. This overrides the work done by
311
+ # Question.build_responses().
312
+ #
313
+ def update_responses( )
314
+ append_default unless default.nil?
315
+ @responses = { :ambiguous_completion =>
316
+ "Ambiguous choice. " +
317
+ "Please choose one of #{options.inspect}.",
318
+ :ask_on_error =>
319
+ "? ",
320
+ :invalid_type =>
321
+ "You must enter a valid #{options}.",
322
+ :no_completion =>
323
+ "You must choose one of " +
324
+ "#{options.inspect}.",
325
+ :not_in_range =>
326
+ "Your answer isn't within the expected range " +
327
+ "(#{expected_range}).",
328
+ :not_valid =>
329
+ "Your answer isn't valid (must match " +
330
+ "#{@validate.inspect})." }.merge(@responses)
331
+ end
332
+ end
333
+ end
@@ -16,6 +16,11 @@ class HighLine
16
16
  # process is handled according to the users wishes.
17
17
  #
18
18
  class Question
19
+ # An internal HighLine error. User code does not need to trap this.
20
+ class NoAutoCompleteMatch < StandardError
21
+ # do nothing, just creating a unique error type
22
+ end
23
+
19
24
  #
20
25
  # Create an instance of HighLine::Question. Expects a _question_ to ask
21
26
  # (can be <tt>""</tt>) and an _answer_type_ to convert the answer to.
@@ -44,34 +49,26 @@ class HighLine
44
49
  yield self if block_given?
45
50
 
46
51
  # finalize responses based on settings
47
- append_default unless default.nil?
48
- @responses = { :ambiguous_completion =>
49
- "Ambiguous choice. " +
50
- "Please choose one of #{@answer_type.inspect}.",
51
- :ask_on_error =>
52
- "? ",
53
- :invalid_type =>
54
- "You must enter a valid #{@answer_type}.",
55
- :not_in_range =>
56
- "Your answer isn't within the expected range " +
57
- "(#{expected_range}).",
58
- :not_valid =>
59
- "Your answer isn't valid (must match " +
60
- "#{@validate.inspect})." }.merge(@responses)
52
+ build_responses
61
53
  end
62
54
 
63
55
  # The type that will be used to convert this answer.
64
- attr_reader :answer_type
56
+ attr_accessor :answer_type
65
57
  #
66
58
  # Can be set to +true+ to use HighLine's cross-platform character reader
67
59
  # instead of fetching an entire line of input. (Note: HighLine's
68
60
  # character reader *ONLY* supports STDIN on Windows and Unix.) Can also
69
61
  # be set to <tt>:getc</tt> to use that method on the input stream.
62
+ #
63
+ # *WARNING*: The _echo_ attribute for a question is ignored when using
64
+ # thw <tt>:getc</tt> method.
70
65
  #
71
66
  attr_accessor :character
72
67
  #
73
68
  # Can be set to +true+ or +false+ to control whether or not input will
74
- # be echoed back to the user.
69
+ # be echoed back to the user. A setting of +true+ will cause echo to
70
+ # match input, but any other true value will be treated as to String to
71
+ # echo for each character typed.
75
72
  #
76
73
  # This requires HighLine's character reader. See the _character_
77
74
  # attribute for details.
@@ -122,6 +119,9 @@ class HighLine
122
119
  # original question.
123
120
  # <tt>:invalid_type</tt>:: The error message shown when a type
124
121
  # conversion fails.
122
+ # <tt>:no_completion</tt>:: Used to notify the user that their
123
+ # selection does not have a valid
124
+ # auto-completion match.
125
125
  # <tt>:not_in_range</tt>:: Used to notify the user that a
126
126
  # provided answer did not satisfy
127
127
  # the range requirement tests.
@@ -142,6 +142,36 @@ class HighLine
142
142
  end
143
143
  end
144
144
 
145
+ #
146
+ # Called late in the initialization process to build intelligent
147
+ # responses based on the details of this Question object.
148
+ #
149
+ def build_responses( )
150
+ ### WARNING: This code is quasi-duplicated in ###
151
+ ### Menu.update_responses(). Check their too when ###
152
+ ### making changes! ###
153
+ append_default unless default.nil?
154
+ @responses = { :ambiguous_completion =>
155
+ "Ambiguous choice. " +
156
+ "Please choose one of #{@answer_type.inspect}.",
157
+ :ask_on_error =>
158
+ "? ",
159
+ :invalid_type =>
160
+ "You must enter a valid #{@answer_type}.",
161
+ :no_completion =>
162
+ "You must choose one of " +
163
+ "#{@answer_type.inspect}.",
164
+ :not_in_range =>
165
+ "Your answer isn't within the expected range " +
166
+ "(#{expected_range}).",
167
+ :not_valid =>
168
+ "Your answer isn't valid (must match " +
169
+ "#{@validate.inspect})." }.merge(@responses)
170
+ ### WARNING: This code is quasi-duplicated in ###
171
+ ### Menu.update_responses(). Check their too when ###
172
+ ### making changes! ###
173
+ end
174
+
145
175
  #
146
176
  # Returns the provided _answer_string_ after changing character case by
147
177
  # the rules of this Question. Valid settings for whitespace are:
@@ -205,7 +235,7 @@ class HighLine
205
235
  @answer_type.extend(OptionParser::Completion)
206
236
  answer = @answer_type.complete(answer_string)
207
237
  if answer.nil?
208
- raise ArgumentError, "Not matching auto-complete choice."
238
+ raise NoAutoCompleteMatch
209
239
  end
210
240
  answer.last
211
241
  elsif [Date, DateTime].include?(@answer_type) or
@@ -282,7 +312,7 @@ class HighLine
282
312
  end
283
313
 
284
314
  # Stringifies the question to be asked.
285
- def to_s( )
315
+ def to_str( )
286
316
  @question
287
317
  end
288
318
 
@@ -63,6 +63,21 @@ class TestHighLine < Test::Unit::TestCase
63
63
  q.case = :capitalize
64
64
  end
65
65
  assert_equal(languages.last, answer)
66
+
67
+ # poor auto-complete error message
68
+ @input.truncate(@input.rewind)
69
+ @input << "lisp\nruby\n"
70
+ @input.rewind
71
+ @output.truncate(@output.rewind)
72
+
73
+ answer = @terminal.ask( "What is your favorite programming language? ",
74
+ languages ) do |q|
75
+ q.case = :capitalize
76
+ end
77
+ assert_equal(languages.last, answer)
78
+ assert_equal( "What is your favorite programming language? " +
79
+ "You must choose one of [:Perl, :Python, :Ruby].\n" +
80
+ "? ", @output.string )
66
81
  end
67
82
 
68
83
  def test_case_changes
@@ -83,6 +98,30 @@ class TestHighLine < Test::Unit::TestCase
83
98
  end
84
99
  assert_equal("crazy", answer)
85
100
  end
101
+
102
+ def test_character_echo
103
+ @input << "password\r"
104
+ @input.rewind
105
+
106
+ answer = @terminal.ask("Please enter your password: ") do |q|
107
+ q.echo = "*"
108
+ end
109
+ assert_equal("password", answer)
110
+ assert_equal("Please enter your password: ********\n", @output.string)
111
+
112
+ @input.truncate(@input.rewind)
113
+ @input << "2"
114
+ @input.rewind
115
+ @output.truncate(@output.rewind)
116
+
117
+ answer = @terminal.ask( "Select an option (1, 2 or 2): ",
118
+ Integer ) do |q|
119
+ q.echo = "*"
120
+ q.character = true
121
+ end
122
+ assert_equal(2, answer)
123
+ assert_equal("Select an option (1, 2 or 2): *\n", @output.string)
124
+ end
86
125
 
87
126
  def test_character_reading
88
127
  # WARNING: This method does NOT cover Unix and Windows savvy testing!
@@ -106,19 +145,19 @@ class TestHighLine < Test::Unit::TestCase
106
145
  assert_equal( "This should be \e[1m\e[47mbold on white\e[0m!\n",
107
146
  @output.string )
108
147
 
109
- @output.truncate(@output.rewind)
148
+ @output.truncate(@output.rewind)
110
149
 
111
- @terminal.say("This should be <%= color('cyan', CYAN) %>!")
112
- assert_equal("This should be \e[36mcyan\e[0m!\n", @output.string)
150
+ @terminal.say("This should be <%= color('cyan', CYAN) %>!")
151
+ assert_equal("This should be \e[36mcyan\e[0m!\n", @output.string)
113
152
 
114
- @output.truncate(@output.rewind)
153
+ @output.truncate(@output.rewind)
115
154
 
116
- @terminal.say( "This should be " +
155
+ @terminal.say( "This should be " +
117
156
  "<%= color('blinking on red', :blink, :on_red) %>!" )
118
- assert_equal( "This should be \e[5m\e[41mblinking on red\e[0m!\n",
119
- @output.string )
157
+ assert_equal( "This should be \e[5m\e[41mblinking on red\e[0m!\n",
158
+ @output.string )
120
159
  end
121
-
160
+
122
161
  def test_confirm
123
162
  @input << "junk.txt\nno\nsave.txt\ny\n"
124
163
  @input.rewind
@@ -194,6 +233,70 @@ class TestHighLine < Test::Unit::TestCase
194
233
  @output.string )
195
234
  end
196
235
 
236
+ def test_lists
237
+ digits = %w{Zero One Two Three Four Five Six Seven Eight Nine}
238
+
239
+ @terminal.say("<%= list(#{digits.inspect}) %>")
240
+ assert_equal(digits.map { |d| "#{d}\n" }.join, @output.string)
241
+
242
+ @output.truncate(@output.rewind)
243
+
244
+ @terminal.say("<%= list(#{digits.inspect}, :inline) %>")
245
+ assert_equal( digits[0..-2].join(", ") + " or #{digits.last}\n",
246
+ @output.string )
247
+
248
+ @output.truncate(@output.rewind)
249
+
250
+ @terminal.say("<%= list(#{digits.inspect}, :inline, ' and ') %>")
251
+ assert_equal( digits[0..-2].join(", ") + " and #{digits.last}\n",
252
+ @output.string )
253
+
254
+ @output.truncate(@output.rewind)
255
+
256
+ @terminal.say("<%= list(#{digits.inspect}, :columns_down, 3) %>")
257
+ assert_equal( "Zero Four Eight\n" +
258
+ "One Five Nine \n" +
259
+ "Two Six \n" +
260
+ "Three Seven\n",
261
+ @output.string )
262
+
263
+ colums_of_twenty = ["12345678901234567890"] * 5
264
+
265
+ @output.truncate(@output.rewind)
266
+
267
+ @terminal.say("<%= list(#{colums_of_twenty.inspect}, :columns_down) %>")
268
+ assert_equal( "12345678901234567890 12345678901234567890 " +
269
+ "12345678901234567890\n" +
270
+ "12345678901234567890 12345678901234567890\n",
271
+ @output.string )
272
+
273
+ @output.truncate(@output.rewind)
274
+
275
+ @terminal.say("<%= list(#{digits.inspect}, :columns_across, 3) %>")
276
+ assert_equal( "Zero One Two \n" +
277
+ "Three Four Five \n" +
278
+ "Six Seven Eight\n" +
279
+ "Nine \n",
280
+ @output.string )
281
+
282
+ colums_of_twenty.pop
283
+
284
+ @output.truncate(@output.rewind)
285
+
286
+ @terminal.say( "<%= list( #{colums_of_twenty.inspect},
287
+ :columns_across ) %>" )
288
+ assert_equal( "12345678901234567890 12345678901234567890 " +
289
+ "12345678901234567890\n" +
290
+ "12345678901234567890\n",
291
+ @output.string )
292
+ end
293
+
294
+ def test_mode
295
+ # *WARNING*: These tests wil only complete is "stty" mode!
296
+ assert_equal( "stty", HighLine::CHARACTER_MODE,
297
+ "Tests require \"stty\" mode." )
298
+ end
299
+
197
300
  class NameClass
198
301
  def self.parse( string )
199
302
  if string =~ /^\s*(\w+),\s*(\w+)\s+(\w+)\s*$/