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 +23 -0
- data/Rakefile +5 -2
- data/TODO +1 -1
- data/examples/menus.rb +51 -1
- data/lib/highline.rb +208 -25
- data/lib/highline/import.rb +7 -7
- data/lib/highline/menu.rb +333 -0
- data/lib/highline/question.rb +48 -18
- data/test/tc_highline.rb +111 -8
- data/test/tc_import.rb +1 -0
- data/test/tc_menu.rb +326 -0
- data/test/ts_all.rb +1 -0
- metadata +15 -3
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
|
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.
|
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.
|
data/examples/menus.rb
CHANGED
@@ -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
|
data/lib/highline.rb
CHANGED
@@ -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(
|
131
|
-
@question
|
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.
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/highline/import.rb
CHANGED
@@ -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
|
16
|
-
# dirty input and output. These methods use the HighLine object in
|
17
|
-
# variable <tt>$terminal</tt>, which is initialized to used
|
18
|
-
# <tt>$stdout</tt> (you are free to change this).
|
19
|
-
#
|
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
|