highline 0.5.0 → 0.6.0

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