highline 1.0.1 → 1.0.2
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 +8 -0
- data/README +1 -1
- data/Rakefile +42 -41
- data/TODO +1 -1
- data/examples/ansi_colors.rb +15 -9
- data/examples/asking_for_arrays.rb +1 -1
- data/examples/basic_usage.rb +50 -50
- data/examples/menus.rb +28 -28
- data/examples/trapping_eof.rb +22 -0
- data/examples/using_readline.rb +5 -5
- data/lib/highline.rb +596 -569
- data/lib/highline/import.rb +2 -2
- data/lib/highline/menu.rb +356 -358
- data/lib/highline/question.rb +406 -409
- data/test/tc_highline.rb +739 -727
- data/test/tc_import.rb +12 -12
- data/test/tc_menu.rb +343 -343
- metadata +49 -45
data/CHANGELOG
CHANGED
@@ -2,6 +2,14 @@
|
|
2
2
|
|
3
3
|
Below is a complete listing of changes for each revision of HighLine.
|
4
4
|
|
5
|
+
== 1.0.2
|
6
|
+
|
7
|
+
* Removed old and broken help tests.
|
8
|
+
* Fixed test case typo found by David A. Black.
|
9
|
+
* Added ERb escapes processing to lists, for coloring list items. Color escapes
|
10
|
+
Do nod add to list element size.
|
11
|
+
* HighLine now throws EOFError when input is exhausted.
|
12
|
+
|
5
13
|
== 1.0.1
|
6
14
|
|
7
15
|
* Minor bug fix: Moved help initialization to before response building, so help
|
data/README
CHANGED
data/Rakefile
CHANGED
@@ -7,53 +7,54 @@ require "rubygems"
|
|
7
7
|
task :default => [:test]
|
8
8
|
|
9
9
|
Rake::TestTask.new do |test|
|
10
|
-
|
11
|
-
|
12
|
-
|
10
|
+
test.libs << "test"
|
11
|
+
test.test_files = [ "test/ts_all.rb" ]
|
12
|
+
test.verbose = true
|
13
13
|
end
|
14
14
|
|
15
15
|
Rake::RDocTask.new do |rdoc|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
16
|
+
rdoc.rdoc_files.include( "README", "INSTALL",
|
17
|
+
"TODO", "CHANGELOG",
|
18
|
+
"AUTHORS", "COPYING",
|
19
|
+
"LICENSE", "lib/" )
|
20
|
+
rdoc.main = "README"
|
21
|
+
rdoc.rdoc_dir = "doc/html"
|
22
|
+
rdoc.title = "HighLine Documentation"
|
23
23
|
end
|
24
24
|
|
25
25
|
desc "Upload current documentation to Rubyforge"
|
26
26
|
task :upload_docs => [:rdoc] do
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
27
|
+
sh "scp -r site/* " +
|
28
|
+
"bbazzarrakk@rubyforge.org:/var/www/gforge-projects/highline/"
|
29
|
+
sh "scp -r doc/html/* " +
|
30
|
+
"bbazzarrakk@rubyforge.org:/var/www/gforge-projects/highline/doc/"
|
31
31
|
end
|
32
32
|
|
33
33
|
spec = Gem::Specification.new do |spec|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
spec.test_suite_file = "test/ts_all.rb"
|
42
|
-
spec.has_rdoc = true
|
43
|
-
spec.extra_rdoc_files = %w{README INSTALL TODO CHANGELOG LICENSE}
|
44
|
-
spec.rdoc_options << '--title' << 'HighLine Documentation' <<
|
45
|
-
'--main' << 'README'
|
34
|
+
spec.name = "highline"
|
35
|
+
spec.version = "1.0.2"
|
36
|
+
spec.platform = Gem::Platform::RUBY
|
37
|
+
spec.summary = "HighLine is a high-level line oriented console interface."
|
38
|
+
spec.files = Dir.glob("{examples,lib,test}/**/*.rb").
|
39
|
+
delete_if { |item| item.include?("CVS") } +
|
40
|
+
["Rakefile", "setup.rb"]
|
46
41
|
|
47
|
-
|
48
|
-
|
42
|
+
spec.test_suite_file = "test/ts_all.rb"
|
43
|
+
spec.has_rdoc = true
|
44
|
+
spec.extra_rdoc_files = %w{README INSTALL TODO CHANGELOG LICENSE}
|
45
|
+
spec.rdoc_options << '--title' << 'HighLine Documentation' <<
|
46
|
+
'--main' << 'README'
|
49
47
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
48
|
+
### Removed due to Windows' install problems ###
|
49
|
+
# spec.add_dependency("termios", ">= 0.9.4")
|
50
|
+
|
51
|
+
spec.require_path = 'lib'
|
52
|
+
spec.autorequire = "highline"
|
53
|
+
spec.author = "James Edward Gray II"
|
54
|
+
spec.email = "james@grayproductions.net"
|
55
|
+
spec.rubyforge_project = "highline"
|
56
|
+
spec.homepage = "http://highline.rubyforge.org"
|
57
|
+
spec.description = <<END_DESC
|
57
58
|
A "high-level line oriented" input/output library that grew out of my solution
|
58
59
|
to Ruby Quiz #29. This library attempts to make standard console input and
|
59
60
|
output robust and painless.
|
@@ -61,14 +62,14 @@ END_DESC
|
|
61
62
|
end
|
62
63
|
|
63
64
|
Rake::GemPackageTask.new(spec) do |pkg|
|
64
|
-
|
65
|
-
|
65
|
+
pkg.need_zip = true
|
66
|
+
pkg.need_tar = true
|
66
67
|
end
|
67
68
|
|
68
69
|
desc "Show library's code statistics"
|
69
70
|
task :stats do
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
71
|
+
require 'code_statistics'
|
72
|
+
CodeStatistics.new( ["HighLine", "lib"],
|
73
|
+
["Functionals", "examples"],
|
74
|
+
["Units", "test"] ).to_s
|
74
75
|
end
|
data/TODO
CHANGED
data/examples/ansi_colors.rb
CHANGED
@@ -13,15 +13,15 @@ colors = %w{black red green yellow blue magenta cyan white}
|
|
13
13
|
|
14
14
|
# Using color() with symbols.
|
15
15
|
colors.each_with_index do |c, i|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
16
|
+
say("This should be <%= color('#{c}', :#{c}) %>!")
|
17
|
+
if i == 0
|
18
|
+
say( "This should be " +
|
19
|
+
"<%= color('white on #{c}', :white, :on_#{c}) %>!")
|
20
|
+
else
|
21
|
+
say( "This should be " +
|
22
|
+
"<%= color( '#{colors[i - 1]} on #{c}',
|
23
|
+
:#{colors[i - 1]}, :on_#{c} ) %>!")
|
24
|
+
end
|
25
25
|
end
|
26
26
|
|
27
27
|
# Using color with constants.
|
@@ -30,3 +30,9 @@ say("This should be <%= color('underlined', UNDERLINE) %>!")
|
|
30
30
|
|
31
31
|
# Using constants only.
|
32
32
|
say("This might even <%= BLINK %>blink<%= CLEAR %>!")
|
33
|
+
|
34
|
+
# It even works with list wrapping.
|
35
|
+
erb_digits = %w{Zero One Two Three Four} +
|
36
|
+
["<%= color('Five', :blue) %%>"] +
|
37
|
+
%w{Six Seven Eight Nine}
|
38
|
+
say("<%= list(#{erb_digits.inspect}, :columns_down, 3) %>")
|
data/examples/basic_usage.rb
CHANGED
@@ -12,64 +12,64 @@ require "yaml"
|
|
12
12
|
contacts = [ ]
|
13
13
|
|
14
14
|
class NameClass
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
15
|
+
def self.parse( string )
|
16
|
+
if string =~ /^\s*(\w+),\s*(\w+)\s*$/
|
17
|
+
self.new($2, $1)
|
18
|
+
else
|
19
|
+
raise ArgumentError, "Invalid name format."
|
20
|
+
end
|
21
|
+
end
|
22
22
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
23
|
+
def initialize(first, last)
|
24
|
+
@first, @last = first, last
|
25
|
+
end
|
26
|
+
|
27
|
+
attr_reader :first, :last
|
28
28
|
end
|
29
29
|
|
30
30
|
begin
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
31
|
+
entry = Hash.new
|
32
|
+
|
33
|
+
# basic output
|
34
|
+
say("Enter a contact:")
|
35
35
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
36
|
+
# basic input
|
37
|
+
entry[:name] = ask("Name? (last, first) ", NameClass) do |q|
|
38
|
+
q.validate = /\A\w+, ?\w+\Z/
|
39
|
+
end
|
40
|
+
entry[:company] = ask("Company? ") { |q| q.default = "none" }
|
41
|
+
entry[:address] = ask("Address? ")
|
42
|
+
entry[:city] = ask("City? ")
|
43
|
+
entry[:state] = ask("State? ") do |q|
|
44
|
+
q.case = :up
|
45
|
+
q.validate = /\A[A-Z]{2}\Z/
|
46
|
+
end
|
47
|
+
entry[:zip] = ask("Zip? ") do |q|
|
48
|
+
q.validate = /\A\d{5}(?:-?\d{4})?\Z/
|
49
|
+
end
|
50
|
+
entry[:phone] = ask( "Phone? ",
|
51
|
+
lambda { |p| p.delete("^0-9").
|
52
|
+
sub(/\A(\d{3})/, '(\1) ').
|
53
|
+
sub(/(\d{4})\Z/, '-\1') } ) do |q|
|
54
|
+
q.validate = lambda { |p| p.delete("^0-9").length == 10 }
|
55
|
+
q.responses[:not_valid] = "Enter a phone numer with area code."
|
56
|
+
end
|
57
|
+
entry[:age] = ask("Age? ", Integer) { |q| q.in = 0..105 }
|
58
|
+
entry[:birthday] = ask("Birthday? ", Date)
|
59
|
+
entry[:interests] = ask( "Interests? (comma separated list) ",
|
60
|
+
lambda { |str| str.split(/,\s*/) } )
|
61
|
+
entry[:description] = ask("Enter a description for this contact.") do |q|
|
62
|
+
q.whitespace = :strip_and_collapse
|
63
|
+
end
|
64
64
|
|
65
|
-
|
65
|
+
contacts << entry
|
66
66
|
# shortcut for yes and no questions
|
67
67
|
end while agree("Enter another contact? ", true)
|
68
68
|
|
69
69
|
if agree("Save these contacts? ", true)
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
70
|
+
file_name = ask("Enter a file name: ") do |q|
|
71
|
+
q.validate = /\A\w+\Z/
|
72
|
+
q.confirm = true
|
73
|
+
end
|
74
|
+
File.open("#{file_name}.yaml", "w") { |file| YAML.dump(contacts, file) }
|
75
75
|
end
|
data/examples/menus.rb
CHANGED
@@ -11,55 +11,55 @@ say(choices.map { |c| " #{c}\n" }.join)
|
|
11
11
|
|
12
12
|
case ask("? ", choices)
|
13
13
|
when "ruby"
|
14
|
-
|
14
|
+
say("Good choice!")
|
15
15
|
else
|
16
|
-
|
16
|
+
say("Not from around here, are you?")
|
17
17
|
end
|
18
18
|
|
19
19
|
# The new and improved choose()...
|
20
20
|
say("\nThis is the new mode (default)...")
|
21
21
|
choose do |menu|
|
22
|
-
|
22
|
+
menu.prompt = "Please choose your favorite programming language? "
|
23
23
|
|
24
|
-
|
25
|
-
|
24
|
+
menu.choice :ruby do say("Good choice!") end
|
25
|
+
menu.choices(:python, :perl) do say("Not from around here, are you?") end
|
26
26
|
end
|
27
27
|
|
28
28
|
say("\nThis is letter indexing...")
|
29
29
|
choose do |menu|
|
30
|
-
|
31
|
-
|
30
|
+
menu.index = :letter
|
31
|
+
menu.index_suffix = ") "
|
32
32
|
|
33
|
-
|
33
|
+
menu.prompt = "Please choose your favorite programming language? "
|
34
34
|
|
35
|
-
|
36
|
-
|
35
|
+
menu.choice :ruby do say("Good choice!") end
|
36
|
+
menu.choices(:python, :perl) do say("Not from around here, are you?") end
|
37
37
|
end
|
38
38
|
|
39
39
|
say("\nThis is with a different layout...")
|
40
40
|
choose do |menu|
|
41
|
-
|
41
|
+
menu.layout = :one_line
|
42
42
|
|
43
|
-
|
44
|
-
|
43
|
+
menu.header = "Languages"
|
44
|
+
menu.prompt = "Favorite? "
|
45
45
|
|
46
|
-
|
47
|
-
|
46
|
+
menu.choice :ruby do say("Good choice!") end
|
47
|
+
menu.choices(:python, :perl) do say("Not from around here, are you?") end
|
48
48
|
end
|
49
49
|
|
50
50
|
say("\nYou can even build shells...")
|
51
51
|
loop do
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
52
|
+
choose do |menu|
|
53
|
+
menu.layout = :menu_only
|
54
|
+
|
55
|
+
menu.shell = true
|
56
|
+
|
57
|
+
menu.choice(:load, "Load a file.") do |command, details|
|
58
|
+
say("Loading file with options: #{details}...")
|
59
|
+
end
|
60
|
+
menu.choice(:save, "Save a file.") do |command, details|
|
61
|
+
say("Saving file with options: #{details}...")
|
62
|
+
end
|
63
|
+
menu.choice(:quit, "Exit program.") { exit }
|
64
|
+
end
|
65
65
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
#!/usr/local/bin/ruby -w
|
2
|
+
|
3
|
+
# trapping_eof.rb
|
4
|
+
#
|
5
|
+
# Created by James Edward Gray II on 2006-02-20.
|
6
|
+
# Copyright 2006 Gray Productions. All rights reserved.
|
7
|
+
|
8
|
+
require "rubygems"
|
9
|
+
require "highline/import"
|
10
|
+
|
11
|
+
loop do
|
12
|
+
begin
|
13
|
+
name = ask("What's your name?")
|
14
|
+
break if name == "exit"
|
15
|
+
puts "Hello, #{name}!"
|
16
|
+
rescue EOFError # HighLine throws this if @input.eof?
|
17
|
+
break
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
puts "Goodbye, dear friend."
|
22
|
+
exit
|
data/examples/using_readline.rb
CHANGED
@@ -9,9 +9,9 @@ require "rubygems"
|
|
9
9
|
require "highline/import"
|
10
10
|
|
11
11
|
loop do
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
12
|
+
cmd = ask("Enter command: ", %w{save load reset quit}) do |q|
|
13
|
+
q.readline = true
|
14
|
+
end
|
15
|
+
say("Executing \"#{cmd}\"...")
|
16
|
+
break if cmd == "quit"
|
17
17
|
end
|
data/lib/highline.rb
CHANGED
@@ -24,594 +24,621 @@ require "abbrev"
|
|
24
24
|
# checking, convert types, etc.
|
25
25
|
#
|
26
26
|
class HighLine
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
27
|
+
# An internal HighLine error. User code does not need to trap this.
|
28
|
+
class QuestionError < StandardError
|
29
|
+
# do nothing, just creating a unique error type
|
30
|
+
end
|
31
31
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
32
|
+
#
|
33
|
+
# Embed in a String to clear all previous ANSI sequences. This *MUST* be
|
34
|
+
# done before the program exits!
|
35
|
+
#
|
36
|
+
CLEAR = "\e[0m"
|
37
|
+
# An alias for CLEAR.
|
38
|
+
RESET = CLEAR
|
39
|
+
# The start of an ANSI bold sequence.
|
40
|
+
BOLD = "\e[1m"
|
41
|
+
# The start of an ANSI dark sequence. (Terminal support uncommon.)
|
42
|
+
DARK = "\e[2m"
|
43
|
+
# The start of an ANSI underline sequence.
|
44
|
+
UNDERLINE = "\e[4m"
|
45
|
+
# An alias for UNDERLINE.
|
46
|
+
UNDERSCORE = UNDERLINE
|
47
|
+
# The start of an ANSI blink sequence. (Terminal support uncommon.)
|
48
|
+
BLINK = "\e[5m"
|
49
|
+
# The start of an ANSI reverse sequence.
|
50
|
+
REVERSE = "\e[7m"
|
51
|
+
# The start of an ANSI concealed sequence. (Terminal support uncommon.)
|
52
|
+
CONCEALED = "\e[8m"
|
53
53
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
54
|
+
# Set the terminal's foreground ANSI color to black.
|
55
|
+
BLACK = "\e[30m"
|
56
|
+
# Set the terminal's foreground ANSI color to red.
|
57
|
+
RED = "\e[31m"
|
58
|
+
# Set the terminal's foreground ANSI color to green.
|
59
|
+
GREEN = "\e[32m"
|
60
|
+
# Set the terminal's foreground ANSI color to yellow.
|
61
|
+
YELLOW = "\e[33m"
|
62
|
+
# Set the terminal's foreground ANSI color to blue.
|
63
|
+
BLUE = "\e[34m"
|
64
|
+
# Set the terminal's foreground ANSI color to magenta.
|
65
|
+
MAGENTA = "\e[35m"
|
66
|
+
# Set the terminal's foreground ANSI color to cyan.
|
67
|
+
CYAN = "\e[36m"
|
68
|
+
# Set the terminal's foreground ANSI color to white.
|
69
|
+
WHITE = "\e[37m"
|
70
70
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
71
|
+
# Set the terminal's background ANSI color to black.
|
72
|
+
ON_BLACK = "\e[40m"
|
73
|
+
# Set the terminal's background ANSI color to red.
|
74
|
+
ON_RED = "\e[41m"
|
75
|
+
# Set the terminal's background ANSI color to green.
|
76
|
+
ON_GREEN = "\e[42m"
|
77
|
+
# Set the terminal's background ANSI color to yellow.
|
78
|
+
ON_YELLOW = "\e[43m"
|
79
|
+
# Set the terminal's background ANSI color to blue.
|
80
|
+
ON_BLUE = "\e[44m"
|
81
|
+
# Set the terminal's background ANSI color to magenta.
|
82
|
+
ON_MAGENTA = "\e[45m"
|
83
|
+
# Set the terminal's background ANSI color to cyan.
|
84
|
+
ON_CYAN = "\e[46m"
|
85
|
+
# Set the terminal's background ANSI color to white.
|
86
|
+
ON_WHITE = "\e[47m"
|
87
87
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
88
|
+
#
|
89
|
+
# Create an instance of HighLine, connected to the streams _input_
|
90
|
+
# and _output_.
|
91
|
+
#
|
92
|
+
def initialize( input = $stdin, output = $stdout,
|
93
|
+
wrap_at = nil, page_at = nil )
|
94
|
+
@input = input
|
95
|
+
@output = output
|
96
|
+
@wrap_at = wrap_at
|
97
|
+
@page_at = page_at
|
98
|
+
|
99
|
+
@question = nil
|
100
|
+
@answer = nil
|
101
|
+
@menu = nil
|
102
|
+
@header = nil
|
103
|
+
@prompt = nil
|
104
|
+
@gather = nil
|
105
|
+
@answers = nil
|
106
|
+
@key = nil
|
107
|
+
end
|
108
|
+
|
109
|
+
#
|
110
|
+
# Set to an integer value to cause HighLine to wrap output lines at the
|
111
|
+
# indicated character limit. When +nil+, the default, no wrapping occurs.
|
112
|
+
#
|
113
|
+
attr_accessor :wrap_at
|
114
|
+
#
|
115
|
+
# Set to an integer value to cause HighLine to page output lines over the
|
116
|
+
# indicated line limit. When +nil+, the default, no paging occurs.
|
117
|
+
#
|
118
|
+
attr_accessor :page_at
|
119
|
+
|
120
|
+
#
|
121
|
+
# A shortcut to HighLine.ask() a question that only accepts "yes" or "no"
|
122
|
+
# answers ("y" and "n" are allowed) and returns +true+ or +false+
|
123
|
+
# (+true+ for "yes"). If provided a +true+ value, _character_ will cause
|
124
|
+
# HighLine to fetch a single character response.
|
125
|
+
#
|
126
|
+
# Raises EOFError if input is exhausted.
|
127
|
+
#
|
128
|
+
def agree( yes_or_no_question, character = nil )
|
129
|
+
ask(yes_or_no_question, lambda { |yn| yn.downcase[0] == ?y}) do |q|
|
130
|
+
q.validate = /\Ay(?:es)?|no?\Z/i
|
131
|
+
q.responses[:not_valid] = 'Please enter "yes" or "no".'
|
132
|
+
q.responses[:ask_on_error] = :question
|
133
|
+
q.character = character
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
#
|
138
|
+
# This method is the primary interface for user input. Just provide a
|
139
|
+
# _question_ to ask the user, the _answer_type_ you want returned, and
|
140
|
+
# optionally a code block setting up details of how you want the question
|
141
|
+
# handled. See HighLine.say() for details on the format of _question_, and
|
142
|
+
# HighLine::Question for more information about _answer_type_ and what's
|
143
|
+
# valid in the code block.
|
144
|
+
#
|
145
|
+
# If <tt>@question</tt> is set before ask() is called, parameters are
|
146
|
+
# ignored and that object (must be a HighLine::Question) is used to drive
|
147
|
+
# the process instead.
|
148
|
+
#
|
149
|
+
# Raises EOFError if input is exhausted.
|
150
|
+
#
|
151
|
+
def ask( question, answer_type = String, &details ) # :yields: question
|
152
|
+
@question ||= Question.new(question, answer_type, &details)
|
153
|
+
|
154
|
+
return gather if @question.gather
|
155
|
+
|
156
|
+
# readline() needs to handle it's own output
|
157
|
+
say(@question) unless @question.readline
|
158
|
+
begin
|
159
|
+
@answer = @question.answer_or_default(get_response)
|
160
|
+
unless @question.valid_answer?(@answer)
|
161
|
+
explain_error(:not_valid)
|
162
|
+
raise QuestionError
|
163
|
+
end
|
164
|
+
|
165
|
+
@answer = @question.convert(@answer)
|
166
|
+
|
167
|
+
if @question.in_range?(@answer)
|
168
|
+
if @question.confirm
|
169
|
+
# need to add a layer of scope to ask a question inside a
|
170
|
+
# question, without destroying instance data
|
171
|
+
context_change = self.class.new( @input, @output,
|
172
|
+
@wrap_at, @page_at )
|
173
|
+
if @question.confirm == true
|
174
|
+
confirm_question = "Are you sure? "
|
175
|
+
else
|
176
|
+
# evaluate ERb under initial scope, so it will have
|
177
|
+
# access to @question and @answer
|
178
|
+
template = ERB.new(@question.confirm, nil, "%")
|
179
|
+
confirm_question = template.result(binding)
|
180
|
+
end
|
181
|
+
unless context_change.agree(confirm_question)
|
182
|
+
explain_error(nil)
|
183
|
+
raise QuestionError
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
@answer
|
188
|
+
else
|
189
|
+
explain_error(:not_in_range)
|
190
|
+
raise QuestionError
|
191
|
+
end
|
192
|
+
rescue QuestionError
|
193
|
+
retry
|
194
|
+
rescue ArgumentError
|
195
|
+
explain_error(:invalid_type)
|
196
|
+
retry
|
197
|
+
rescue Question::NoAutoCompleteMatch
|
198
|
+
explain_error(:no_completion)
|
199
|
+
retry
|
200
|
+
rescue NameError
|
201
|
+
raise if $!.is_a?(NoMethodError)
|
202
|
+
explain_error(:ambiguous_completion)
|
203
|
+
retry
|
204
|
+
ensure
|
205
|
+
@question = nil # Reset Question object.
|
206
|
+
end
|
207
|
+
end
|
204
208
|
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
209
|
+
#
|
210
|
+
# This method is HighLine's menu handler. For simple usage, you can just
|
211
|
+
# pass all the menu items you wish to display. At that point, choose() will
|
212
|
+
# build and display a menu, walk the user through selection, and return
|
213
|
+
# their choice amoung the provided items. You might use this in a case
|
214
|
+
# statement for quick and dirty menus.
|
215
|
+
#
|
216
|
+
# However, choose() is capable of much more. If provided, a block will be
|
217
|
+
# passed a HighLine::Menu object to configure. Using this method, you can
|
218
|
+
# customize all the details of menu handling from index display, to building
|
219
|
+
# a complete shell-like menuing system. See HighLine::Menu for all the
|
220
|
+
# methods it responds to.
|
221
|
+
#
|
222
|
+
# Raises EOFError if input is exhausted.
|
223
|
+
#
|
224
|
+
def choose( *items, &details )
|
225
|
+
@menu = @question = Menu.new(&details)
|
226
|
+
@menu.choices(*items) unless items.empty?
|
227
|
+
|
228
|
+
# Set _answer_type_ so we can double as the Question for ask().
|
229
|
+
@menu.answer_type = if @menu.shell
|
230
|
+
lambda do |command| # shell-style selection
|
231
|
+
first_word = command.split.first
|
226
232
|
|
227
|
-
|
228
|
-
|
229
|
-
|
233
|
+
options = @menu.options
|
234
|
+
options.extend(OptionParser::Completion)
|
235
|
+
answer = options.complete(first_word)
|
230
236
|
|
231
|
-
|
232
|
-
|
233
|
-
|
237
|
+
if answer.nil?
|
238
|
+
raise Question::NoAutoCompleteMatch
|
239
|
+
end
|
234
240
|
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
241
|
+
[answer.last, command.sub(/^\s*#{first_word}\s*/, "")]
|
242
|
+
end
|
243
|
+
else
|
244
|
+
@menu.options # normal menu selection, by index or name
|
245
|
+
end
|
246
|
+
|
247
|
+
# Provide hooks for ERb layouts.
|
248
|
+
@header = @menu.header
|
249
|
+
@prompt = @menu.prompt
|
250
|
+
|
251
|
+
if @menu.shell
|
252
|
+
selected = ask("Ignored", @menu.answer_type)
|
253
|
+
@menu.select(self, *selected)
|
254
|
+
else
|
255
|
+
selected = ask("Ignored", @menu.answer_type)
|
256
|
+
@menu.select(self, selected)
|
257
|
+
end
|
258
|
+
end
|
253
259
|
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
260
|
+
#
|
261
|
+
# This method provides easy access to ANSI color sequences, without the user
|
262
|
+
# needing to remember to CLEAR at the end of each sequence. Just pass the
|
263
|
+
# _string_ to color, followed by a list of _colors_ you would like it to be
|
264
|
+
# affected by. The _colors_ can be HighLine class constants, or symbols
|
265
|
+
# (:blue for BLUE, for example). A CLEAR will automatically be embedded to
|
266
|
+
# the end of the returned String.
|
267
|
+
#
|
268
|
+
def color( string, *colors )
|
269
|
+
colors.map! do |c|
|
270
|
+
if c.is_a?(Symbol)
|
271
|
+
self.class.const_get(c.to_s.upcase)
|
272
|
+
else
|
273
|
+
c
|
274
|
+
end
|
275
|
+
end
|
276
|
+
"#{colors.join}#{string}#{CLEAR}"
|
277
|
+
end
|
278
|
+
|
279
|
+
#
|
280
|
+
# This method is a utility for quickly and easily laying out lists. It can
|
281
|
+
# be accessed within ERb replacements of any text that will be sent to the
|
282
|
+
# user.
|
283
|
+
#
|
284
|
+
# The only required parameter is _items_, which should be the Array of items
|
285
|
+
# to list. A specified _mode_ controls how that list is formed and _option_
|
286
|
+
# has different effects, depending on the _mode_. Recognized modes are:
|
287
|
+
#
|
288
|
+
# <tt>:columns_across</tt>:: _items_ will be placed in columns, flowing
|
289
|
+
# from left to right. If given, _option_ is the
|
290
|
+
# number of columns to be used. When absent,
|
291
|
+
# columns will be determined based on _wrap_at_
|
292
|
+
# or a default of 80 characters.
|
293
|
+
# <tt>:columns_down</tt>:: Identical to <tt>:columns_across</tt>, save
|
294
|
+
# flow goes down.
|
295
|
+
# <tt>:inline</tt>:: All _items_ are placed on a single line. The
|
296
|
+
# last two _items_ are separated by _option_ or
|
297
|
+
# a default of " or ". All other _items_ are
|
298
|
+
# separated by ", ".
|
299
|
+
# <tt>:rows</tt>:: The default mode. Each of the _items_ is
|
300
|
+
# placed on it's own line. The _option_
|
301
|
+
# parameter is ignored in this mode.
|
302
|
+
#
|
303
|
+
# Each member of the _items_ Array is passed through ERb and thus can contain
|
304
|
+
# their own expansions. Color escape expansions do not contribute to the
|
305
|
+
# final field width.
|
306
|
+
#
|
307
|
+
def list( items, mode = :rows, option = nil )
|
308
|
+
items = items.to_ary.map do |item|
|
309
|
+
ERB.new(item, nil, "%").result(binding)
|
310
|
+
end
|
311
|
+
|
312
|
+
case mode
|
313
|
+
when :inline
|
314
|
+
option = " or " if option.nil?
|
315
|
+
|
316
|
+
case items.size
|
317
|
+
when 0
|
318
|
+
""
|
319
|
+
when 1
|
320
|
+
items.first
|
321
|
+
when 2
|
322
|
+
"#{items.first}#{option}#{items.last}"
|
323
|
+
else
|
324
|
+
items[0..-2].join(", ") + "#{option}#{items.last}"
|
325
|
+
end
|
326
|
+
when :columns_across, :columns_down
|
327
|
+
max_length = actual_length(
|
328
|
+
items.max { |a, b| actual_length(a) <=> actual_length(b) }
|
329
|
+
)
|
320
330
|
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
if mode == :columns_across
|
326
|
-
rows = Array.new(row_count) { Array.new }
|
327
|
-
items.each_with_index do |item, index|
|
328
|
-
rows[index / option] << item
|
329
|
-
end
|
331
|
+
if option.nil?
|
332
|
+
limit = @wrap_at || 80
|
333
|
+
option = (limit + 2) / (max_length + 2)
|
334
|
+
end
|
330
335
|
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
end
|
343
|
-
list
|
344
|
-
end
|
345
|
-
else
|
346
|
-
items.map { |i| "#{i}\n" }.join
|
347
|
-
end
|
348
|
-
end
|
349
|
-
|
350
|
-
#
|
351
|
-
# The basic output method for HighLine objects. If the provided _statement_
|
352
|
-
# ends with a space or tab character, a newline will not be appended (output
|
353
|
-
# will be flush()ed). All other cases are passed straight to Kernel.puts().
|
354
|
-
#
|
355
|
-
# The _statement_ parameter is processed as an ERb template, supporting
|
356
|
-
# embedded Ruby code. The template is evaluated with a binding inside
|
357
|
-
# the HighLine instance, providing easy access to the ANSI color constants
|
358
|
-
# and the HighLine.color() method.
|
359
|
-
#
|
360
|
-
def say( statement )
|
361
|
-
statement = statement.to_str
|
362
|
-
return unless statement.length > 0
|
363
|
-
|
364
|
-
template = ERB.new(statement, nil, "%")
|
365
|
-
statement = template.result(binding)
|
366
|
-
|
367
|
-
statement = wrap(statement) unless @wrap_at.nil?
|
368
|
-
statement = page_print(statement) unless @page_at.nil?
|
369
|
-
|
370
|
-
if statement[-1, 1] == " " or statement[-1, 1] == "\t"
|
371
|
-
@output.print(statement)
|
372
|
-
@output.flush
|
373
|
-
else
|
374
|
-
@output.puts(statement)
|
375
|
-
end
|
376
|
-
end
|
377
|
-
|
378
|
-
private
|
379
|
-
|
380
|
-
#
|
381
|
-
# A helper method for sending the output stream and error and repeat
|
382
|
-
# of the question.
|
383
|
-
#
|
384
|
-
def explain_error( error )
|
385
|
-
say(@question.responses[error]) unless error.nil?
|
386
|
-
if @question.responses[:ask_on_error] == :question
|
387
|
-
say(@question)
|
388
|
-
elsif @question.responses[:ask_on_error]
|
389
|
-
say(@question.responses[:ask_on_error])
|
390
|
-
end
|
391
|
-
end
|
392
|
-
|
393
|
-
#
|
394
|
-
# Collects an Array/Hash full of answers as described in
|
395
|
-
# HighLine::Question.gather().
|
396
|
-
#
|
397
|
-
def gather( )
|
398
|
-
@gather = @question.gather
|
399
|
-
@answers = [ ]
|
400
|
-
original_question = @question
|
401
|
-
|
402
|
-
@question.gather = false
|
403
|
-
|
404
|
-
case @gather
|
405
|
-
when Integer
|
406
|
-
@answers << ask(@question)
|
407
|
-
@gather -= 1
|
336
|
+
items = items.map do |item|
|
337
|
+
pad = max_length + (item.length - actual_length(item))
|
338
|
+
"%-#{pad}s" % item
|
339
|
+
end
|
340
|
+
row_count = (items.size / option.to_f).ceil
|
341
|
+
|
342
|
+
if mode == :columns_across
|
343
|
+
rows = Array.new(row_count) { Array.new }
|
344
|
+
items.each_with_index do |item, index|
|
345
|
+
rows[index / option] << item
|
346
|
+
end
|
408
347
|
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
348
|
+
rows.map { |row| row.join(" ") + "\n" }.join
|
349
|
+
else
|
350
|
+
columns = Array.new(option) { Array.new }
|
351
|
+
items.each_with_index do |item, index|
|
352
|
+
columns[index / row_count] << item
|
353
|
+
end
|
354
|
+
|
355
|
+
list = ""
|
356
|
+
columns.first.size.times do |index|
|
357
|
+
list << columns.map { |column| column[index] }.
|
358
|
+
compact.join(" ") + "\n"
|
359
|
+
end
|
360
|
+
list
|
361
|
+
end
|
362
|
+
else
|
363
|
+
items.map { |i| "#{i}\n" }.join
|
364
|
+
end
|
365
|
+
end
|
366
|
+
|
367
|
+
#
|
368
|
+
# The basic output method for HighLine objects. If the provided _statement_
|
369
|
+
# ends with a space or tab character, a newline will not be appended (output
|
370
|
+
# will be flush()ed). All other cases are passed straight to Kernel.puts().
|
371
|
+
#
|
372
|
+
# The _statement_ parameter is processed as an ERb template, supporting
|
373
|
+
# embedded Ruby code. The template is evaluated with a binding inside
|
374
|
+
# the HighLine instance, providing easy access to the ANSI color constants
|
375
|
+
# and the HighLine.color() method.
|
376
|
+
#
|
377
|
+
def say( statement )
|
378
|
+
statement = statement.to_str
|
379
|
+
return unless statement.length > 0
|
380
|
+
|
381
|
+
template = ERB.new(statement, nil, "%")
|
382
|
+
statement = template.result(binding)
|
383
|
+
|
384
|
+
statement = wrap(statement) unless @wrap_at.nil?
|
385
|
+
statement = page_print(statement) unless @page_at.nil?
|
386
|
+
|
387
|
+
if statement[-1, 1] == " " or statement[-1, 1] == "\t"
|
388
|
+
@output.print(statement)
|
389
|
+
@output.flush
|
390
|
+
else
|
391
|
+
@output.puts(statement)
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
private
|
396
|
+
|
397
|
+
#
|
398
|
+
# A helper method for sending the output stream and error and repeat
|
399
|
+
# of the question.
|
400
|
+
#
|
401
|
+
def explain_error( error )
|
402
|
+
say(@question.responses[error]) unless error.nil?
|
403
|
+
if @question.responses[:ask_on_error] == :question
|
404
|
+
say(@question)
|
405
|
+
elsif @question.responses[:ask_on_error]
|
406
|
+
say(@question.responses[:ask_on_error])
|
407
|
+
end
|
408
|
+
end
|
409
|
+
|
410
|
+
#
|
411
|
+
# Collects an Array/Hash full of answers as described in
|
412
|
+
# HighLine::Question.gather().
|
413
|
+
#
|
414
|
+
# Raises EOFError if input is exhausted.
|
415
|
+
#
|
416
|
+
def gather( )
|
417
|
+
@gather = @question.gather
|
418
|
+
@answers = [ ]
|
419
|
+
original_question = @question
|
420
|
+
|
421
|
+
@question.gather = false
|
422
|
+
|
423
|
+
case @gather
|
424
|
+
when Integer
|
425
|
+
@answers << ask(@question)
|
426
|
+
@gather -= 1
|
417
427
|
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
when Hash
|
427
|
-
@answers = { }
|
428
|
-
@gather.keys.sort.each do |key|
|
429
|
-
@question = original_question
|
430
|
-
@key = key
|
431
|
-
@answers[key] = ask(@question)
|
432
|
-
end
|
433
|
-
end
|
434
|
-
|
435
|
-
@answers
|
436
|
-
end
|
437
|
-
|
438
|
-
#
|
439
|
-
# This section builds a character reading function to suit the proper
|
440
|
-
# platform we're running on. Be warned: Here be dragons!
|
441
|
-
#
|
442
|
-
begin
|
443
|
-
require "Win32API" # See if we're on Windows.
|
428
|
+
original_question.question = ""
|
429
|
+
until @gather.zero?
|
430
|
+
@question = original_question
|
431
|
+
@answers << ask(@question)
|
432
|
+
@gather -= 1
|
433
|
+
end
|
434
|
+
when String, Regexp
|
435
|
+
@answers << ask(@question)
|
444
436
|
|
445
|
-
|
437
|
+
original_question.question = ""
|
438
|
+
until (@gather.is_a?(String) and @answers.last.to_s == @gather) or
|
439
|
+
(@gather.is_a?(Regexp) and @answers.last.to_s =~ @gather)
|
440
|
+
@question = original_question
|
441
|
+
@answers << ask(@question)
|
442
|
+
end
|
443
|
+
|
444
|
+
@answers.pop
|
445
|
+
when Hash
|
446
|
+
@answers = { }
|
447
|
+
@gather.keys.sort.each do |key|
|
448
|
+
@question = original_question
|
449
|
+
@key = key
|
450
|
+
@answers[key] = ask(@question)
|
451
|
+
end
|
452
|
+
end
|
453
|
+
|
454
|
+
@answers
|
455
|
+
end
|
456
|
+
|
457
|
+
#
|
458
|
+
# This section builds a character reading function to suit the proper
|
459
|
+
# platform we're running on. Be warned: Here be dragons!
|
460
|
+
#
|
461
|
+
begin
|
462
|
+
require "Win32API" # See if we're on Windows.
|
446
463
|
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
464
|
+
CHARACTER_MODE = "Win32API" # For Debugging purposes only.
|
465
|
+
|
466
|
+
#
|
467
|
+
# Windows savvy getc().
|
468
|
+
#
|
469
|
+
# *WARNING*: This method ignores <tt>@input</tt> and reads one
|
470
|
+
# character from +STDIN+!
|
471
|
+
#
|
472
|
+
def get_character
|
473
|
+
Win32API.new("crtdll", "_getch", [ ], "L").Call
|
474
|
+
end
|
475
|
+
rescue LoadError # If we're not on Windows try...
|
476
|
+
begin
|
477
|
+
require "termios" # Unix, first choice.
|
478
|
+
|
479
|
+
CHARACTER_MODE = "termios" # For Debugging purposes only.
|
461
480
|
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
new_settings = old_settings.dup
|
471
|
-
new_settings.c_lflag &= ~(Termios::ECHO | Termios::ICANON)
|
472
|
-
|
473
|
-
begin
|
474
|
-
Termios.setattr(@input, Termios::TCSANOW, new_settings)
|
475
|
-
@input.getc
|
476
|
-
ensure
|
477
|
-
Termios.setattr(@input, Termios::TCSANOW, old_settings)
|
478
|
-
end
|
479
|
-
end
|
480
|
-
rescue LoadError # If our first choice fails, default.
|
481
|
-
CHARACTER_MODE = "stty" # For Debugging purposes only.
|
481
|
+
#
|
482
|
+
# Unix savvy getc(). (First choice.)
|
483
|
+
#
|
484
|
+
# *WARNING*: This method requires the "termios" library!
|
485
|
+
#
|
486
|
+
def get_character
|
487
|
+
old_settings = Termios.getattr(@input)
|
482
488
|
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
489
|
+
new_settings = old_settings.dup
|
490
|
+
new_settings.c_lflag &= ~(Termios::ECHO | Termios::ICANON)
|
491
|
+
|
492
|
+
begin
|
493
|
+
Termios.setattr(@input, Termios::TCSANOW, new_settings)
|
494
|
+
@input.getc
|
495
|
+
ensure
|
496
|
+
Termios.setattr(@input, Termios::TCSANOW, old_settings)
|
497
|
+
end
|
498
|
+
end
|
499
|
+
rescue LoadError # If our first choice fails, default.
|
500
|
+
CHARACTER_MODE = "stty" # For Debugging purposes only.
|
501
|
+
|
502
|
+
#
|
503
|
+
# Unix savvy getc(). (Second choice.)
|
504
|
+
#
|
505
|
+
# *WARNING*: This method requires the external "stty" program!
|
506
|
+
#
|
507
|
+
def get_character
|
508
|
+
state = `stty -g`
|
509
|
+
|
510
|
+
begin
|
511
|
+
system "stty raw -echo cbreak"
|
512
|
+
@input.getc
|
513
|
+
ensure
|
514
|
+
system "stty #{state}"
|
515
|
+
end
|
516
|
+
end
|
499
517
|
end
|
518
|
+
end
|
500
519
|
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
520
|
+
#
|
521
|
+
# Read a line of input from the input stream and process whitespace as
|
522
|
+
# requested by the Question object.
|
523
|
+
#
|
524
|
+
# If Question's _readline_ property is set, that library will be used to
|
525
|
+
# fetch input. *WARNING*: This ignores the currently set input stream.
|
526
|
+
#
|
527
|
+
def get_line( )
|
528
|
+
if @question.readline
|
529
|
+
require "readline" # load only if needed
|
511
530
|
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
531
|
+
# capture say()'s work in a String to feed to readline()
|
532
|
+
old_output = @output
|
533
|
+
@output = StringIO.new
|
534
|
+
say(@question)
|
535
|
+
question = @output.string
|
536
|
+
@output = old_output
|
537
|
+
|
538
|
+
# prep auto-completion
|
539
|
+
completions = @question.selection.abbrev
|
540
|
+
Readline.completion_proc = lambda { |string| completions[string] }
|
541
|
+
|
542
|
+
# work-around ugly readline() warnings
|
543
|
+
old_verbose = $VERBOSE
|
544
|
+
$VERBOSE = nil
|
545
|
+
answer = @question.change_case(
|
546
|
+
@question.remove_whitespace(
|
547
|
+
Readline.readline(question, true) ) )
|
548
|
+
$VERBOSE = old_verbose
|
530
549
|
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
550
|
+
answer
|
551
|
+
else
|
552
|
+
@question.change_case(@question.remove_whitespace(@input.gets))
|
553
|
+
end
|
554
|
+
end
|
555
|
+
|
556
|
+
#
|
557
|
+
# Return a line or character of input, as requested for this question.
|
558
|
+
# Character input will be returned as a single character String,
|
559
|
+
# not an Integer.
|
560
|
+
#
|
561
|
+
# Raises EOFError if input is exhausted.
|
562
|
+
#
|
563
|
+
def get_response( )
|
564
|
+
raise EOFError, "The input stream is exhausted." if @input.eof?
|
565
|
+
|
566
|
+
if @question.character.nil?
|
567
|
+
if @question.echo == true and @question.limit.nil?
|
568
|
+
get_line
|
569
|
+
else
|
570
|
+
line = ""
|
571
|
+
while character = get_character
|
572
|
+
line << character.chr
|
573
|
+
# looking for carriage return (decimal 13) or
|
574
|
+
# newline (decimal 10) in raw input
|
575
|
+
break if character == 13 or character == 10 or
|
576
|
+
(@question.limit and line.size == @question.limit)
|
577
|
+
@output.print(@question.echo) if @question.echo != false
|
578
|
+
end
|
579
|
+
say("\n")
|
580
|
+
@question.change_case(@question.remove_whitespace(line))
|
581
|
+
end
|
582
|
+
elsif @question.character == :getc
|
583
|
+
@question.change_case(@input.getc.chr)
|
584
|
+
else
|
585
|
+
response = get_character.chr
|
586
|
+
echo = if @question.echo == true
|
587
|
+
response
|
588
|
+
elsif @question.echo != false
|
589
|
+
@question.echo
|
590
|
+
else
|
591
|
+
""
|
592
|
+
end
|
593
|
+
say("#{echo}\n")
|
594
|
+
@question.change_case(response)
|
595
|
+
end
|
596
|
+
end
|
597
|
+
|
598
|
+
#
|
599
|
+
# Page print a series of at most _page_at_ lines for _output_. After each
|
600
|
+
# page is printed, HighLine will pause until the user presses enter/return
|
601
|
+
# then display the next page of data.
|
602
|
+
#
|
603
|
+
# Note that the final page of _output_ is *not* printed, but returned
|
604
|
+
# instead. This is to support any special handling for the final sequence.
|
605
|
+
#
|
606
|
+
def page_print( output )
|
607
|
+
lines = output.scan(/[^\n]*\n?/)
|
608
|
+
while lines.size > @page_at
|
609
|
+
@output.puts lines.slice!(0...@page_at).join
|
610
|
+
@output.puts
|
611
|
+
ask("-- press enter/return to continue -- ")
|
612
|
+
@output.puts
|
613
|
+
end
|
614
|
+
return lines.join
|
615
|
+
end
|
616
|
+
|
617
|
+
#
|
618
|
+
# Wrap a sequence of _lines_ at _wrap_at_ characters per line. Existing
|
619
|
+
# newlines will not be affected by this process, but additional newlines
|
620
|
+
# may be added.
|
621
|
+
#
|
622
|
+
def wrap( lines )
|
623
|
+
wrapped = [ ]
|
624
|
+
lines.each do |line|
|
625
|
+
while line =~ /([^\n]{#{@wrap_at + 1},})/
|
626
|
+
search = $1.dup
|
627
|
+
replace = $1.dup
|
628
|
+
if index = replace.rindex(" ", @wrap_at)
|
629
|
+
replace[index, 1] = "\n"
|
630
|
+
replace.sub!(/\n[ \t]+/, "\n")
|
631
|
+
line.sub!(search, replace)
|
632
|
+
else
|
633
|
+
line[@wrap_at, 0] = "\n"
|
634
|
+
end
|
635
|
+
end
|
636
|
+
wrapped << line
|
637
|
+
end
|
638
|
+
return wrapped.join
|
639
|
+
end
|
640
|
+
|
641
|
+
def actual_length( string_with_escapes )
|
642
|
+
string_with_escapes.gsub(/\e\[\d{1,2}m/, "").length
|
643
|
+
end
|
617
644
|
end
|