highline 0.5.0 → 0.6.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.
data/CHANGELOG CHANGED
@@ -2,6 +2,29 @@
2
2
 
3
3
  Below is a complete listing of changes for each revision of HighLine.
4
4
 
5
+ == 0.6.0
6
+
7
+ * Implemented HighLine.choose() for menu handling.
8
+ * Provided shortcut <tt>choose(item1, item2, ...)</tt> for simple menus.
9
+ * Allowed Ruby code to be attached to each menu item, to create a complete
10
+ menu solution.
11
+ * Provided for total customization of the menu layout.
12
+ * Allowed for menu selection by index, name or both.
13
+ * Added a _shell_ mode to allow menu selection with additional details
14
+ following the name.
15
+ * Added a list() utility method that can be invoked just like color(). It can
16
+ layout Arrays for you in any output in the modes <tt>:columns_across</tt>,
17
+ <tt>:columns_down</tt>, <tt>:inline</tt> and <tt>:rows</tt>
18
+ * Added support for <tt>echo = "*"</tt> style settings. User code can now
19
+ choose the echo character this way.
20
+ * Modified HighLine to user the "termios" library for character input, if
21
+ available. Will return to old behavior (using "stty"), if "termios" cannot be
22
+ loaded.
23
+ * Improved "stty" state restoring code.
24
+ * Fixed "stty" code to handle interrupt signals.
25
+ * Improved the default auto-complete error message and exposed this message
26
+ through the +responses+ interface as <tt>:no_completion</tt>.
27
+
5
28
  == 0.5.0
6
29
 
7
30
  * Implemented <tt>echo = false</tt> for HighLine::Question objects, primarily to
data/Rakefile CHANGED
@@ -22,13 +22,15 @@ Rake::RDocTask.new do |rdoc|
22
22
  end
23
23
 
24
24
  task :upload_docs => [:rdoc] do
25
- sh "scp -r doc/html/* " +
25
+ sh "scp -r site/* " +
26
26
  "bbazzarrakk@rubyforge.org:/var/www/gforge-projects/highline/"
27
+ sh "scp -r doc/html/* " +
28
+ "bbazzarrakk@rubyforge.org:/var/www/gforge-projects/highline/doc/"
27
29
  end
28
30
 
29
31
  spec = Gem::Specification.new do |spec|
30
32
  spec.name = "highline"
31
- spec.version = "0.5.0"
33
+ spec.version = "0.6.0"
32
34
  spec.platform = Gem::Platform::RUBY
33
35
  spec.summary = "HighLine is a high-level line oriented console interface."
34
36
  spec.files = Dir.glob("{examples,lib,test}/**/*.rb").
@@ -39,6 +41,7 @@ spec = Gem::Specification.new do |spec|
39
41
  spec.extra_rdoc_files = %w{README INSTALL TODO CHANGELOG LICENSE}
40
42
  spec.rdoc_options << '--title' << 'HighLine Documentation' <<
41
43
  '--main' << 'README'
44
+ spec.add_dependency("termios", ">= 0.9.4")
42
45
  spec.require_path = 'lib'
43
46
  spec.autorequire = "highline"
44
47
  spec.author = "James Edward Gray II"
data/TODO CHANGED
@@ -7,5 +7,5 @@ order.
7
7
  <tt>ask(..., lambda { |arr| arr.split(",") })</tt> or similar.
8
8
  * Support <tt>ask(..., Hash)</tt>.
9
9
  * Support <tt>ask(..., File)</tt>.
10
- * Implement choose() for simple menu handling.
11
10
  * Add readline support for history and editing.
11
+ * Add an easy-access help system for menus.
@@ -3,8 +3,9 @@
3
3
  require "rubygems"
4
4
  require "highline/import"
5
5
 
6
+ # The old way, using ask() and say()...
6
7
  choices = %w{ruby python perl}
7
-
8
+ say("This is the old way using ask() and say()...")
8
9
  say("Please choose your favorite programming language:")
9
10
  say(choices.map { |c| " #{c}\n" }.join)
10
11
 
@@ -14,3 +15,52 @@ when "ruby"
14
15
  else
15
16
  say("Not from around here, are you?")
16
17
  end
18
+
19
+ # The new and improved choose()...
20
+ say("\nThis is the new mode (default)...")
21
+ choose do |menu|
22
+ menu.prompt = "Please choose your favorite programming language? "
23
+
24
+ menu.choice :ruby do say("Good choice!") end
25
+ menu.choices(:python, :perl) do say("Not from around here, are you?") end
26
+ end
27
+
28
+ say("\nThis is letter indexing...")
29
+ choose do |menu|
30
+ menu.index = :letter
31
+ menu.index_suffix = ") "
32
+
33
+ menu.prompt = "Please choose your favorite programming language? "
34
+
35
+ menu.choice :ruby do say("Good choice!") end
36
+ menu.choices(:python, :perl) do say("Not from around here, are you?") end
37
+ end
38
+
39
+ say("\nThis is with a different layout...")
40
+ choose do |menu|
41
+ menu.layout = :one_line
42
+
43
+ menu.header = "Languages"
44
+ menu.prompt = "Favorite? "
45
+
46
+ menu.choice :ruby do say("Good choice!") end
47
+ menu.choices(:python, :perl) do say("Not from around here, are you?") end
48
+ end
49
+
50
+ say("\nYou can even build shells...")
51
+ loop do
52
+ choose do |menu|
53
+ menu.layout = :menu_only
54
+
55
+ menu.shell = true
56
+ menu.case = :capitalize
57
+
58
+ menu.choice :Load do |command, details|
59
+ say("Loading file with options: #{details}...")
60
+ end
61
+ menu.choice :Save do |command, details|
62
+ say("Saving file with options: #{details}...")
63
+ end
64
+ menu.choice(:Quit) { exit }
65
+ end
66
+ end
@@ -8,7 +8,9 @@
8
8
  # See HighLine for documentation.
9
9
 
10
10
  require "highline/question"
11
+ require "highline/menu"
11
12
  require "erb"
13
+ require "optparse"
12
14
 
13
15
  #
14
16
  # A HighLine object is a "high-level line oriented" shell over an input and an
@@ -126,9 +128,13 @@ class HighLine
126
128
  # handled. See HighLine.say() for details on the format of _question_, and
127
129
  # HighLine::Question for more information about _answer_type_ and what's
128
130
  # valid in the code block.
131
+ #
132
+ # If <tt>@question</tt> is set before ask() is called, parameters are
133
+ # ignored and that object (must be a HighLine::Question) is used to drive
134
+ # the process instead.
129
135
  #
130
- def ask( question, answer_type = String, &details ) # :yields: question
131
- @question = Question.new(question, answer_type, &details)
136
+ def ask(question, answer_type = String, &details) # :yields: question
137
+ @question ||= Question.new(question, answer_type, &details)
132
138
 
133
139
  say(@question)
134
140
  begin
@@ -170,10 +176,64 @@ class HighLine
170
176
  rescue ArgumentError
171
177
  explain_error(:invalid_type)
172
178
  retry
179
+ rescue Question::NoAutoCompleteMatch
180
+ explain_error(:no_completion)
181
+ retry
173
182
  rescue NameError
174
183
  raise if $!.is_a?(NoMethodError)
175
184
  explain_error(:ambiguous_completion)
176
185
  retry
186
+ ensure
187
+ @question = nil # Reset Question object.
188
+ end
189
+ end
190
+
191
+ #
192
+ # This method is HighLine's menu handler. For simple usage, you can just
193
+ # pass all the menu items you wish to display. At that point, choose() will
194
+ # build and display a menu, walk the user through selection, and return
195
+ # their choice amoung the provided items. You might use this in a case
196
+ # statement for quick and dirty menus.
197
+ #
198
+ # However, choose() is capable of much more. If provided, a block will be
199
+ # passed a HighLine::Menu object to configure. Using this method, you can
200
+ # customize all the details of menu handling from index display, to building
201
+ # a complete shell-like menuing system. See HighLine::Menu for all the
202
+ # methods it responds to.
203
+ #
204
+ def choose( *items, &details )
205
+ @menu = @question = Menu.new(&details)
206
+ @menu.choices(*items) unless items.empty?
207
+
208
+ # Set _answer_type_ so we can double as the Question for ask().
209
+ @menu.answer_type = if @menu.shell
210
+ lambda do |command| # shell-style selection
211
+ first_word = command.split.first
212
+
213
+ options = @menu.options
214
+ options.extend(OptionParser::Completion)
215
+ answer = options.complete(first_word)
216
+
217
+ if answer.nil?
218
+ raise Question::NoAutoCompleteMatch
219
+ end
220
+
221
+ [answer.last, command.sub(/^\s*#{first_word}\s*/, "")]
222
+ end
223
+ else
224
+ @menu.options # normal menu selection, by index or name
225
+ end
226
+
227
+ # Provide hooks for ERb layouts.
228
+ @header = @menu.header
229
+ @prompt = @menu.prompt
230
+
231
+ if @menu.shell
232
+ selected = ask("Ignored", @menu.answer_type)
233
+ @menu.select(*selected)
234
+ else
235
+ selected = ask("Ignored", @menu.answer_type)
236
+ @menu.select(selected)
177
237
  end
178
238
  end
179
239
 
@@ -196,6 +256,83 @@ class HighLine
196
256
  "#{colors.join}#{string}#{CLEAR}"
197
257
  end
198
258
 
259
+ #
260
+ # This method is a utility for quickly and easily laying out lists. It can
261
+ # be accessed within ERb replacments of any text that will be sent to the
262
+ # user.
263
+ #
264
+ # The only required parameter is _items_, which should be the Array of items
265
+ # to list. A specified _mode_ controls how that list is formed and _option_
266
+ # has different effects, depending on the _mode_. Recognized modes are:
267
+ #
268
+ # <tt>:columns_across</tt>:: _items_ will be placed in columns, flowing
269
+ # from left to right. If given, _option_ is the
270
+ # number of columns to be used. When absent,
271
+ # columns will be determined based on _wrap_at_
272
+ # or a default of 80 characters.
273
+ # <tt>:columns_down</tt>:: Indentical to <tt>:columns_across</tt>, save
274
+ # flow goes down.
275
+ # <tt>:inline</tt>:: All _items_ are placed on a single line. The
276
+ # last two _items_ are separated by _option_ or
277
+ # a default of " or ". All other _items_ are
278
+ # separated by ", ".
279
+ # <tt>:rows</tt>:: The default mode. Each of the _items_ is
280
+ # placed on it's own line. The _option_
281
+ # parameter is ignored in this mode.
282
+ #
283
+ def list( items, mode = :rows, option = nil )
284
+ items = items.to_ary
285
+
286
+ case mode
287
+ when :inline
288
+ option = " or " if option.nil?
289
+
290
+ case items.size
291
+ when 0
292
+ ""
293
+ when 1
294
+ items.first
295
+ when 2
296
+ "#{items.first}#{option}#{items.last}"
297
+ else
298
+ items[0..-2].join(", ") + "#{option}#{items.last}"
299
+ end
300
+ when :columns_across, :columns_down
301
+ if option.nil?
302
+ limit = @wrap_at || 80
303
+ max_length = items.max { |a, b| a.length <=> b.length }.length
304
+ option = (limit + 2) / (max_length + 2)
305
+ end
306
+
307
+ max_length = items.max { |a, b| a.length <=> b.length }.length
308
+ items = items.map { |item| "%-#{max_length}s" % item }
309
+ row_count = (items.size / option.to_f).ceil
310
+
311
+ if mode == :columns_across
312
+ rows = Array.new(row_count) { Array.new }
313
+ items.each_with_index do |item, index|
314
+ rows[index / option] << item
315
+ end
316
+
317
+ rows.map { |row| row.join(" ") + "\n" }.join
318
+ else
319
+ columns = Array.new(option) { Array.new }
320
+ items.each_with_index do |item, index|
321
+ columns[index / row_count] << item
322
+ end
323
+
324
+ list = ""
325
+ columns.first.size.times do |index|
326
+ list << columns.map { |column| column[index] }.
327
+ compact.join(" ") + "\n"
328
+ end
329
+ list
330
+ end
331
+ else
332
+ items.map { |i| "#{i}\n" }.join
333
+ end
334
+ end
335
+
199
336
  #
200
337
  # The basic output method for HighLine objects. If the provided _statement_
201
338
  # ends with a space or tab character, a newline will not be appended (output
@@ -207,7 +344,7 @@ class HighLine
207
344
  # and the HighLine.color() method.
208
345
  #
209
346
  def say( statement )
210
- statement = statement.to_s
347
+ statement = statement.to_str
211
348
  return unless statement.length > 0
212
349
 
213
350
  template = ERB.new(statement, nil, "%")
@@ -239,33 +376,70 @@ class HighLine
239
376
  end
240
377
  end
241
378
 
379
+ #
380
+ # This section builds a character reading function to suit the proper
381
+ # platform we're running on. Be warned: Here be dragons!
382
+ #
242
383
  begin
243
- require "Win32API"
384
+ require "Win32API" # See if we're on Windows.
385
+
386
+ CHARACTER_MODE = "Win32API" # For Debugging purposes only.
244
387
 
245
- #
388
+ #
246
389
  # Windows savvy getc().
247
390
  #
248
- # WARNING: This method ignores @input and reads one character
249
- # from STDIN!
391
+ # *WARNING*: This method ignores <tt>@input</tt> and reads one
392
+ # character from +STDIN+!
250
393
  #
251
394
  def get_character
252
- Win32API.new("crtdll", "_getch", [], "L").Call
253
- end
254
- rescue LoadError
255
- #
256
- # Unix savvy getc().
257
- #
258
- # WARNING: This method requires the external "stty" program!
259
- #
260
- def get_character
261
- system "stty raw -echo"
262
- @input.getc
263
- ensure
264
- system "stty -raw echo"
395
+ Win32API.new("crtdll", "_getch", [ ], "L").Call
265
396
  end
397
+ rescue LoadError # If we're not on Windows try...
398
+ begin
399
+ require "termios" # Unix, first choice.
400
+
401
+ CHARACTER_MODE = "termios" # For Debugging purposes only.
402
+
403
+ #
404
+ # Unix savvy getc(). (First choice.)
405
+ #
406
+ # *WARNING*: This method requires the "termios" library!
407
+ #
408
+ def get_character
409
+ old_settings = Termios.getattr(@input)
410
+
411
+ new_settings = old_settings.dup
412
+ new_settings.c_lflag &= ~(Termios::ECHO | Termios::ICANON)
413
+
414
+ begin
415
+ Termios.setattr(@input, Termios::TCSANOW, new_settings)
416
+ @input.getc
417
+ ensure
418
+ Termios.setattr(@input, Termios::TCSANOW, old_settings)
419
+ end
420
+ end
421
+ rescue LoadError # If our first choice fails, default.
422
+ CHARACTER_MODE = "stty" # For Debugging purposes only.
423
+
424
+ #
425
+ # Unix savvy getc(). (Second choice.)
426
+ #
427
+ # *WARNING*: This method requires the external "stty" program!
428
+ #
429
+ def get_character
430
+ state = `stty -g`
431
+
432
+ begin
433
+ system "stty raw -echo cbreak"
434
+ @input.getc
435
+ ensure
436
+ system "stty #{state}"
437
+ end
438
+ end
439
+ end
266
440
  end
267
441
 
268
- #
442
+ #
269
443
  # Read a line of input from the input stream and process whitespace as
270
444
  # requested by the Question object.
271
445
  #
@@ -280,14 +454,16 @@ class HighLine
280
454
  #
281
455
  def get_response( )
282
456
  if @question.character.nil?
283
- if @question.echo
457
+ if @question.echo == true
284
458
  get_line
285
459
  else
286
460
  line = ""
287
461
  while character = get_character
288
462
  line << character.chr
289
-
290
- break if character == 13
463
+ # looking for carriage return (decimal 13) or
464
+ # newline (decimal 10) in raw input
465
+ break if character == 13 or character == 10
466
+ @output.print(@question.echo) if @question.echo != false
291
467
  end
292
468
  say("\n")
293
469
  @question.change_case(@question.remove_whitespace(line))
@@ -296,7 +472,14 @@ class HighLine
296
472
  @question.change_case(@input.getc.chr)
297
473
  else
298
474
  response = get_character.chr
299
- say("#{if @question.echo then response else '' end}\n")
475
+ echo = if @question.echo == true
476
+ response
477
+ elsif @question.echo != false
478
+ @question.echo
479
+ else
480
+ ""
481
+ end
482
+ say("#{echo}\n")
300
483
  @question.change_case(response)
301
484
  end
302
485
  end
@@ -12,14 +12,14 @@ $terminal = HighLine.new
12
12
 
13
13
  #
14
14
  # <tt>require "highline/import"</tt> adds shorcut methods to Kernel, making
15
- # agree(), ask(), and say() globally available. This is handy for quick and
16
- # dirty input and output. These methods use the HighLine object in the global
17
- # variable <tt>$terminal</tt>, which is initialized to used <tt>$stdin</tt> and
18
- # <tt>$stdout</tt> (you are free to change this). Otherwise, these methods are
19
- # identical to their HighLine counterparts, see that class for detailed
20
- # explinations.
15
+ # agree(), ask(), choose() and say() globally available. This is handy for
16
+ # quick and dirty input and output. These methods use the HighLine object in
17
+ # the global variable <tt>$terminal</tt>, which is initialized to used
18
+ # <tt>$stdin</tt> and <tt>$stdout</tt> (you are free to change this).
19
+ # Otherwise, these methods areidentical to their HighLine counterparts, see that
20
+ # class for detailed explinations.
21
21
  #
22
22
  module Kernel
23
23
  extend Forwardable
24
- def_delegators :$terminal, :agree, :ask, :say
24
+ def_delegators :$terminal, :agree, :ask, :choose, :say
25
25
  end