skylight 0.0.7 → 0.0.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/bin/skylight +5 -0
  3. data/lib/skylight.rb +2 -0
  4. data/lib/skylight/cli.rb +84 -0
  5. data/lib/skylight/config.rb +53 -6
  6. data/lib/skylight/instrumenter.rb +4 -1
  7. data/lib/skylight/json_proto.rb +0 -3
  8. data/lib/skylight/middleware.rb +1 -0
  9. data/lib/skylight/normalize.rb +30 -5
  10. data/lib/skylight/normalize/process_action.rb +1 -1
  11. data/lib/skylight/normalize/render_collection.rb +2 -6
  12. data/lib/skylight/normalize/render_partial.rb +2 -5
  13. data/lib/skylight/normalize/render_template.rb +2 -5
  14. data/lib/skylight/normalize/sql.rb +2 -4
  15. data/lib/skylight/railtie.rb +30 -33
  16. data/lib/skylight/sanity_checker.rb +73 -0
  17. data/lib/skylight/subscriber.rb +1 -15
  18. data/lib/skylight/trace.rb +30 -5
  19. data/lib/skylight/util/http.rb +97 -0
  20. data/lib/skylight/vendor/highline.rb +1012 -0
  21. data/lib/skylight/vendor/highline/color_scheme.rb +134 -0
  22. data/lib/skylight/vendor/highline/compatibility.rb +16 -0
  23. data/lib/skylight/vendor/highline/import.rb +41 -0
  24. data/lib/skylight/vendor/highline/menu.rb +398 -0
  25. data/lib/skylight/vendor/highline/question.rb +475 -0
  26. data/lib/skylight/vendor/highline/simulate.rb +48 -0
  27. data/lib/skylight/vendor/highline/string_extensions.rb +131 -0
  28. data/lib/skylight/vendor/highline/style.rb +181 -0
  29. data/lib/skylight/vendor/highline/system_extensions.rb +218 -0
  30. data/lib/skylight/vendor/thor.rb +473 -0
  31. data/lib/skylight/vendor/thor/actions.rb +318 -0
  32. data/lib/skylight/vendor/thor/actions/create_file.rb +105 -0
  33. data/lib/skylight/vendor/thor/actions/create_link.rb +60 -0
  34. data/lib/skylight/vendor/thor/actions/directory.rb +119 -0
  35. data/lib/skylight/vendor/thor/actions/empty_directory.rb +137 -0
  36. data/lib/skylight/vendor/thor/actions/file_manipulation.rb +314 -0
  37. data/lib/skylight/vendor/thor/actions/inject_into_file.rb +109 -0
  38. data/lib/skylight/vendor/thor/base.rb +652 -0
  39. data/lib/skylight/vendor/thor/command.rb +136 -0
  40. data/lib/skylight/vendor/thor/core_ext/hash_with_indifferent_access.rb +80 -0
  41. data/lib/skylight/vendor/thor/core_ext/io_binary_read.rb +12 -0
  42. data/lib/skylight/vendor/thor/core_ext/ordered_hash.rb +100 -0
  43. data/lib/skylight/vendor/thor/error.rb +28 -0
  44. data/lib/skylight/vendor/thor/group.rb +282 -0
  45. data/lib/skylight/vendor/thor/invocation.rb +172 -0
  46. data/lib/skylight/vendor/thor/parser.rb +4 -0
  47. data/lib/skylight/vendor/thor/parser/argument.rb +74 -0
  48. data/lib/skylight/vendor/thor/parser/arguments.rb +171 -0
  49. data/lib/skylight/vendor/thor/parser/option.rb +121 -0
  50. data/lib/skylight/vendor/thor/parser/options.rb +218 -0
  51. data/lib/skylight/vendor/thor/rake_compat.rb +72 -0
  52. data/lib/skylight/vendor/thor/runner.rb +322 -0
  53. data/lib/skylight/vendor/thor/shell.rb +88 -0
  54. data/lib/skylight/vendor/thor/shell/basic.rb +393 -0
  55. data/lib/skylight/vendor/thor/shell/color.rb +148 -0
  56. data/lib/skylight/vendor/thor/shell/html.rb +127 -0
  57. data/lib/skylight/vendor/thor/util.rb +270 -0
  58. data/lib/skylight/vendor/thor/version.rb +3 -0
  59. data/lib/skylight/version.rb +1 -1
  60. data/lib/skylight/worker.rb +3 -58
  61. metadata +56 -18
@@ -0,0 +1,73 @@
1
+ require "yaml"
2
+
3
+ module Skylight
4
+ class SanityChecker
5
+ def initialize(file=File)
6
+ @problems = Hash.new { |h,k| h[k] = [] }
7
+ @file = file
8
+ end
9
+
10
+ def needs_credentials?(yaml_file)
11
+ return true if smoke_test(yaml_file)
12
+ config = Config.load_from_yaml(yaml_file)
13
+ return true if sanity_check(config)
14
+ false
15
+ end
16
+
17
+ def smoke_test(yaml_file)
18
+ return @problems unless check_yaml_exists(yaml_file)
19
+ return @problems unless check_yaml_is_hash(yaml_file)
20
+ end
21
+
22
+ def sanity_check(config)
23
+ return @problems unless check_config_contents(config)
24
+ end
25
+
26
+ def user_credentials(filename)
27
+ return @problems unless check_user_credentials(filename)
28
+ end
29
+
30
+ private
31
+ def yaml_contents
32
+ @yaml_contents ||= YAML.load_file(@yaml_file)
33
+ end
34
+
35
+ def check_yaml_exists(yaml_file)
36
+ return true if File.exist?(yaml_file)
37
+
38
+ @problems["skylight.yml"] << "does not exist"
39
+ false
40
+ end
41
+
42
+ def check_yaml_is_hash(yaml_file)
43
+ yaml_contents = YAML.load_file(yaml_file)
44
+ return true if yaml_contents.is_a?(Hash)
45
+
46
+ @problems["skylight.yml"] << "is not in the correct format"
47
+ false
48
+ end
49
+
50
+ def check_config_contents(config)
51
+ unless config.app_id
52
+ @problems["skylight.yml"] << "does not contain an app id - please run `skylight create`"
53
+ return false
54
+ end
55
+
56
+ unless config.authentication_token
57
+ @problems["skylight.yml"] << "does not contain an app token - please run `skylight create`"
58
+ return false
59
+ end
60
+
61
+ true
62
+ end
63
+
64
+ def check_user_credentials(filename)
65
+ path = @file.expand_path(filename)
66
+ exists = @file.exist?(path)
67
+ return true if exists
68
+
69
+ @problems[filename] << "does not exist"
70
+ false
71
+ end
72
+ end
73
+ end
@@ -12,14 +12,7 @@ module Skylight
12
12
  def start(name, id, payload)
13
13
  return unless trace = Trace.current
14
14
 
15
- name, title, desc, payload = Normalize.normalize(trace, name, payload)
16
-
17
- if name != :skip
18
- logger.debug("[SKYLIGHT] START: #{name} (#{title}, \"#{desc}\")")
19
- logger.debug("[SKYLIGHT] > #{payload.inspect}")
20
- else
21
- logger.debug("[SKYLIGHT] START: skipped")
22
- end
15
+ name, title, desc, payload = Normalize.normalize(trace, name, payload, @config.normalizer)
23
16
 
24
17
  trace.start(name, title, desc, payload)
25
18
  end
@@ -36,13 +29,6 @@ module Skylight
36
29
 
37
30
  name, title, desc, payload = Normalize.normalize(trace, name, payload)
38
31
 
39
- if name != :skip
40
- logger.debug("[SKYLIGHT] MEASURE: #{name} (#{title}, \"#{desc}\")")
41
- logger.debug("[SKYLIGHT] > #{payload.inspect}")
42
- else
43
- logger.debug("[SKYLIGHT] MEASURE: skipped")
44
- end
45
-
46
32
  trace.record(name, title, desc, payload)
47
33
  end
48
34
 
@@ -28,7 +28,8 @@ module Skylight
28
28
  attr_reader :endpoint, :ident, :spans
29
29
  attr_writer :endpoint
30
30
 
31
- def initialize(endpoint = "Unknown", ident = nil)
31
+ def initialize(config = Config.new, endpoint = "Unknown", ident = nil)
32
+ @config = config
32
33
  @ident = ident
33
34
  @endpoint = endpoint
34
35
  @spans = []
@@ -38,6 +39,9 @@ module Skylight
38
39
 
39
40
  # Tracks the ID of the current parent
40
41
  @parent = nil
42
+
43
+ # Track the cumulative amount of GC removed from traces
44
+ @cumulative_gc = 0
41
45
  end
42
46
 
43
47
  def from
@@ -64,6 +68,9 @@ module Skylight
64
68
  @stack.push cat
65
69
  return self if cat == :skip
66
70
 
71
+ # TODO: Allocate GC time to all running threads
72
+ update_cumulative_gc
73
+
67
74
  span = build_span(cat, title, desc, annot)
68
75
 
69
76
  @parent = @spans.length
@@ -85,8 +92,9 @@ module Skylight
85
92
 
86
93
  raise "trace unbalanced" unless span
87
94
 
88
- # Set ended_at
89
- span.ended_at = now - @timestamp
95
+ update_cumulative_gc
96
+
97
+ span.ended_at = now - @timestamp - @cumulative_gc
90
98
 
91
99
  # Update the parent
92
100
  @parent = @spans[@parent].parent
@@ -98,8 +106,16 @@ module Skylight
98
106
  def commit
99
107
  raise "trace unbalanced" if @parent
100
108
 
109
+ n = now
110
+
111
+ if @cumulative_gc > 0
112
+ span = Span.new(0, n - @timestamp - @cumulative_gc, "noise.gc", nil, nil, nil)
113
+ span.ended_at = n - @timestamp
114
+ @spans << span
115
+ end
116
+
101
117
  @ident ||= gen_ident
102
- @finish = now
118
+ @finish = n
103
119
 
104
120
  # No more changes should be made
105
121
  freeze
@@ -113,12 +129,21 @@ module Skylight
113
129
  Util.clock.now
114
130
  end
115
131
 
132
+ def convert(secs)
133
+ Util.clock.convert(secs)
134
+ end
135
+
136
+ def update_cumulative_gc
137
+ @cumulative_gc += convert(@config.gc_profiler.total_time)
138
+ @config.gc_profiler.clear
139
+ end
140
+
116
141
  def gen_ident
117
142
  Util::UUID.gen Digest::MD5.digest(@endpoint)[0, 2]
118
143
  end
119
144
 
120
145
  def build_span(cat, title, desc, annot)
121
- n = now
146
+ n = now - @cumulative_gc
122
147
  @timestamp ||= n
123
148
 
124
149
  Span.new(@parent, n - @timestamp, cat, title, desc, annot)
@@ -0,0 +1,97 @@
1
+ require "json"
2
+
3
+ module Skylight
4
+ module Util
5
+ class HTTP
6
+ CONTENT_ENCODING = 'content-encoding'.freeze
7
+ CONTENT_LENGTH = 'content-length'.freeze
8
+ CONTENT_TYPE = 'content-type'.freeze
9
+ ACCEPT = 'Accept'.freeze
10
+ APPLICATION_JSON = 'application/json'.freeze
11
+ AUTHORIZATION = 'authorization'.freeze
12
+ DEFLATE = 'deflate'.freeze
13
+ GZIP = 'gzip'.freeze
14
+
15
+ def initialize(config)
16
+ @config = config
17
+ end
18
+
19
+ def auth(username, password)
20
+ req = Net::HTTP::Get.new("/login")
21
+ req.basic_auth username, password
22
+ response = make_request(req, nil, ACCEPT => APPLICATION_JSON)
23
+ JSON.parse(response)
24
+ end
25
+
26
+ def create_app(user_token, app_name)
27
+ req = Net::HTTP::Post.new("/apps")
28
+ req["Authorization"] = user_token
29
+
30
+ body = JSON.dump(app: { name: app_name })
31
+ headers = { ACCEPT => APPLICATION_JSON }
32
+ headers[CONTENT_TYPE] = APPLICATION_JSON
33
+ headers[CONTENT_ENCODING] = GZIP if @config.deflate?
34
+ response = make_request(req, body, headers)
35
+
36
+ JSON.parse(response)
37
+ end
38
+
39
+ def post(endpoint, body)
40
+ req = request(Net::HTTP::Post, endpoint, body.bytesize)
41
+ make_request(req, body)
42
+ rescue => e
43
+ logger.error "[SKYLIGHT] POST #{@config.host}:#{@config.port}(ssl=#{@config.ssl?}) - #{e.message} - #{e.class} - #{e.backtrace.first}"
44
+ debug(e.backtrace.join("\n"))
45
+ end
46
+
47
+ def request(type, endpoint, length=nil)
48
+ headers = {}
49
+
50
+ headers[CONTENT_LENGTH] = length.to_s if length
51
+ headers[AUTHORIZATION] = @config.authentication_token
52
+ headers[CONTENT_TYPE] = APPLICATION_JSON if length
53
+ headers[ACCEPT] = APPLICATION_JSON
54
+ headers[CONTENT_ENCODING] = GZIP if @config.deflate?
55
+
56
+ type.new(endpoint, headers)
57
+ end
58
+
59
+ private
60
+ def make_request(req, body=nil, headers={})
61
+ if body
62
+ body = Gzip.compress(body) if @config.deflate?
63
+ req.body = body
64
+ end
65
+
66
+ headers.each do |name, value|
67
+ req[name] = value
68
+ end
69
+
70
+ http = Net::HTTP.new @config.host, @config.port
71
+
72
+ if @config.ssl?
73
+ http.use_ssl = true
74
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
75
+ end
76
+
77
+ http.start do |client|
78
+ response = client.request(req)
79
+
80
+ unless response.code == '200'
81
+ debug "Server responded with #{response.code}"
82
+ end
83
+
84
+ return response.body
85
+ end
86
+ end
87
+
88
+ def logger
89
+ @config.logger
90
+ end
91
+
92
+ def debug(msg)
93
+ logger.debug "[SKYLIGHT] #{msg}" if logger.debug?
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,1012 @@
1
+ # highline.rb
2
+ #
3
+ # Created by James Edward Gray II on 2005-04-26.
4
+ # Copyright 2005 Gray Productions. All rights reserved.
5
+ #
6
+ # See HighLine for documentation.
7
+ #
8
+ # This is Free Software. See LICENSE and COPYING for details.
9
+
10
+ require "erb"
11
+ require "optparse"
12
+ require "stringio"
13
+ require "abbrev"
14
+ require "highline/system_extensions"
15
+ require "highline/question"
16
+ require "highline/menu"
17
+ require "highline/color_scheme"
18
+ require "highline/style"
19
+
20
+ #
21
+ # A HighLine object is a "high-level line oriented" shell over an input and an
22
+ # output stream. HighLine simplifies common console interaction, effectively
23
+ # replacing puts() and gets(). User code can simply specify the question to ask
24
+ # and any details about user interaction, then leave the rest of the work to
25
+ # HighLine. When HighLine.ask() returns, you'll have the answer you requested,
26
+ # even if HighLine had to ask many times, validate results, perform range
27
+ # checking, convert types, etc.
28
+ #
29
+ class HighLine
30
+ # The version of the installed library.
31
+ VERSION = "1.6.18".freeze
32
+
33
+ # An internal HighLine error. User code does not need to trap this.
34
+ class QuestionError < StandardError
35
+ # do nothing, just creating a unique error type
36
+ end
37
+
38
+ # The setting used to disable color output.
39
+ @@use_color = true
40
+
41
+ # Pass +false+ to _setting_ to turn off HighLine's color escapes.
42
+ def self.use_color=( setting )
43
+ @@use_color = setting
44
+ end
45
+
46
+ # Returns true if HighLine is currently using color escapes.
47
+ def self.use_color?
48
+ @@use_color
49
+ end
50
+
51
+ # For checking if the current version of HighLine supports RGB colors
52
+ # Usage: HighLine.supports_rgb_color? rescue false # rescue for compatibility with older versions
53
+ # Note: color usage also depends on HighLine.use_color being set
54
+ def self.supports_rgb_color?
55
+ true
56
+ end
57
+
58
+ # The setting used to disable EOF tracking.
59
+ @@track_eof = true
60
+
61
+ # Pass +false+ to _setting_ to turn off HighLine's EOF tracking.
62
+ def self.track_eof=( setting )
63
+ @@track_eof = setting
64
+ end
65
+
66
+ # Returns true if HighLine is currently tracking EOF for input.
67
+ def self.track_eof?
68
+ @@track_eof
69
+ end
70
+
71
+ # The setting used to control color schemes.
72
+ @@color_scheme = nil
73
+
74
+ # Pass ColorScheme to _setting_ to set a HighLine color scheme.
75
+ def self.color_scheme=( setting )
76
+ @@color_scheme = setting
77
+ end
78
+
79
+ # Returns the current color scheme.
80
+ def self.color_scheme
81
+ @@color_scheme
82
+ end
83
+
84
+ # Returns +true+ if HighLine is currently using a color scheme.
85
+ def self.using_color_scheme?
86
+ not @@color_scheme.nil?
87
+ end
88
+
89
+ #
90
+ # Embed in a String to clear all previous ANSI sequences. This *MUST* be
91
+ # done before the program exits!
92
+ #
93
+
94
+ ERASE_LINE_STYLE = Style.new(:name=>:erase_line, :builtin=>true, :code=>"\e[K") # Erase the current line of terminal output
95
+ ERASE_CHAR_STYLE = Style.new(:name=>:erase_char, :builtin=>true, :code=>"\e[P") # Erase the character under the cursor.
96
+ CLEAR_STYLE = Style.new(:name=>:clear, :builtin=>true, :code=>"\e[0m") # Clear color settings
97
+ RESET_STYLE = Style.new(:name=>:reset, :builtin=>true, :code=>"\e[0m") # Alias for CLEAR.
98
+ BOLD_STYLE = Style.new(:name=>:bold, :builtin=>true, :code=>"\e[1m") # Bold; Note: bold + a color works as you'd expect,
99
+ # for example bold black. Bold without a color displays
100
+ # the system-defined bold color (e.g. red on Mac iTerm)
101
+ DARK_STYLE = Style.new(:name=>:dark, :builtin=>true, :code=>"\e[2m") # Dark; support uncommon
102
+ UNDERLINE_STYLE = Style.new(:name=>:underline, :builtin=>true, :code=>"\e[4m") # Underline
103
+ UNDERSCORE_STYLE = Style.new(:name=>:underscore, :builtin=>true, :code=>"\e[4m") # Alias for UNDERLINE
104
+ BLINK_STYLE = Style.new(:name=>:blink, :builtin=>true, :code=>"\e[5m") # Blink; support uncommon
105
+ REVERSE_STYLE = Style.new(:name=>:reverse, :builtin=>true, :code=>"\e[7m") # Reverse foreground and background
106
+ CONCEALED_STYLE = Style.new(:name=>:concealed, :builtin=>true, :code=>"\e[8m") # Concealed; support uncommon
107
+
108
+ STYLES = %w{CLEAR RESET BOLD DARK UNDERLINE UNDERSCORE BLINK REVERSE CONCEALED}
109
+
110
+ # These RGB colors are approximate; see http://en.wikipedia.org/wiki/ANSI_escape_code
111
+ BLACK_STYLE = Style.new(:name=>:black, :builtin=>true, :code=>"\e[30m", :rgb=>[ 0, 0, 0])
112
+ RED_STYLE = Style.new(:name=>:red, :builtin=>true, :code=>"\e[31m", :rgb=>[128, 0, 0])
113
+ GREEN_STYLE = Style.new(:name=>:green, :builtin=>true, :code=>"\e[32m", :rgb=>[ 0,128, 0])
114
+ BLUE_STYLE = Style.new(:name=>:blue, :builtin=>true, :code=>"\e[34m", :rgb=>[ 0, 0,128])
115
+ YELLOW_STYLE = Style.new(:name=>:yellow, :builtin=>true, :code=>"\e[33m", :rgb=>[128,128, 0])
116
+ MAGENTA_STYLE = Style.new(:name=>:magenta, :builtin=>true, :code=>"\e[35m", :rgb=>[128, 0,128])
117
+ CYAN_STYLE = Style.new(:name=>:cyan, :builtin=>true, :code=>"\e[36m", :rgb=>[ 0,128,128])
118
+ # On Mac OSX Terminal, white is actually gray
119
+ WHITE_STYLE = Style.new(:name=>:white, :builtin=>true, :code=>"\e[37m", :rgb=>[192,192,192])
120
+ # Alias for WHITE, since WHITE is actually a light gray on Macs
121
+ GRAY_STYLE = Style.new(:name=>:gray, :builtin=>true, :code=>"\e[37m", :rgb=>[192,192,192])
122
+ # On Mac OSX Terminal, this is black foreground, or bright white background.
123
+ # Also used as base for RGB colors, if available
124
+ NONE_STYLE = Style.new(:name=>:none, :builtin=>true, :code=>"\e[38m", :rgb=>[ 0, 0, 0])
125
+
126
+ BASIC_COLORS = %w{BLACK RED GREEN YELLOW BLUE MAGENTA CYAN WHITE GRAY NONE}
127
+
128
+ colors = BASIC_COLORS.dup
129
+ BASIC_COLORS.each do |color|
130
+ bright_color = "BRIGHT_#{color}"
131
+ colors << bright_color
132
+ const_set bright_color+'_STYLE', const_get(color + '_STYLE').bright
133
+ end
134
+ COLORS = colors
135
+
136
+ colors.each do |color|
137
+ const_set color, const_get("#{color}_STYLE").code
138
+ const_set "ON_#{color}_STYLE", const_get("#{color}_STYLE").on
139
+ const_set "ON_#{color}", const_get("ON_#{color}_STYLE").code
140
+ end
141
+ ON_NONE_STYLE.rgb = [255,255,255] # Override; white background
142
+
143
+ STYLES.each do |style|
144
+ const_set style, const_get("#{style}_STYLE").code
145
+ end
146
+
147
+ # For RGB colors:
148
+ def self.const_missing(name)
149
+ if name.to_s =~ /^(ON_)?(RGB_)([A-F0-9]{6})(_STYLE)?$/ # RGB color
150
+ on = $1
151
+ suffix = $4
152
+ if suffix
153
+ code_name = $1.to_s + $2 + $3
154
+ else
155
+ code_name = name.to_s
156
+ end
157
+ style_name = code_name + '_STYLE'
158
+ style = Style.rgb($3)
159
+ style = style.on if on
160
+ const_set(style_name, style)
161
+ const_set(code_name, style.code)
162
+ if suffix
163
+ style
164
+ else
165
+ style.code
166
+ end
167
+ else
168
+ raise NameError, "Bad color or uninitialized constant #{name}"
169
+ end
170
+ end
171
+
172
+ #
173
+ # Create an instance of HighLine, connected to the streams _input_
174
+ # and _output_.
175
+ #
176
+ def initialize( input = $stdin, output = $stdout,
177
+ wrap_at = nil, page_at = nil, indent_size=3, indent_level=0 )
178
+ @input = input
179
+ @output = output
180
+
181
+ @multi_indent = true
182
+ @indent_size = indent_size
183
+ @indent_level = indent_level
184
+
185
+ self.wrap_at = wrap_at
186
+ self.page_at = page_at
187
+
188
+ @question = nil
189
+ @answer = nil
190
+ @menu = nil
191
+ @header = nil
192
+ @prompt = nil
193
+ @gather = nil
194
+ @answers = nil
195
+ @key = nil
196
+
197
+ initialize_system_extensions if respond_to?(:initialize_system_extensions)
198
+ end
199
+
200
+ include HighLine::SystemExtensions
201
+
202
+ # The current column setting for wrapping output.
203
+ attr_reader :wrap_at
204
+ # The current row setting for paging output.
205
+ attr_reader :page_at
206
+ # Indentation over multiple lines
207
+ attr_accessor :multi_indent
208
+ # The indentation size
209
+ attr_accessor :indent_size
210
+ # The indentation level
211
+ attr_accessor :indent_level
212
+
213
+ #
214
+ # A shortcut to HighLine.ask() a question that only accepts "yes" or "no"
215
+ # answers ("y" and "n" are allowed) and returns +true+ or +false+
216
+ # (+true+ for "yes"). If provided a +true+ value, _character_ will cause
217
+ # HighLine to fetch a single character response. A block can be provided
218
+ # to further configure the question as in HighLine.ask()
219
+ #
220
+ # Raises EOFError if input is exhausted.
221
+ #
222
+ def agree( yes_or_no_question, character = nil )
223
+ ask(yes_or_no_question, lambda { |yn| yn.downcase[0] == ?y}) do |q|
224
+ q.validate = /\Ay(?:es)?|no?\Z/i
225
+ q.responses[:not_valid] = 'Please enter "yes" or "no".'
226
+ q.responses[:ask_on_error] = :question
227
+ q.character = character
228
+
229
+ yield q if block_given?
230
+ end
231
+ end
232
+
233
+ #
234
+ # This method is the primary interface for user input. Just provide a
235
+ # _question_ to ask the user, the _answer_type_ you want returned, and
236
+ # optionally a code block setting up details of how you want the question
237
+ # handled. See HighLine.say() for details on the format of _question_, and
238
+ # HighLine::Question for more information about _answer_type_ and what's
239
+ # valid in the code block.
240
+ #
241
+ # If <tt>@question</tt> is set before ask() is called, parameters are
242
+ # ignored and that object (must be a HighLine::Question) is used to drive
243
+ # the process instead.
244
+ #
245
+ # Raises EOFError if input is exhausted.
246
+ #
247
+ def ask( question, answer_type = String, &details ) # :yields: question
248
+ @question ||= Question.new(question, answer_type, &details)
249
+
250
+ return gather if @question.gather
251
+
252
+ # readline() needs to handle its own output, but readline only supports
253
+ # full line reading. Therefore if @question.echo is anything but true,
254
+ # the prompt will not be issued. And we have to account for that now.
255
+ # Also, JRuby-1.7's ConsoleReader.readLine() needs to be passed the prompt
256
+ # to handle line editing properly.
257
+ say(@question) unless ((JRUBY or @question.readline) and @question.echo == true)
258
+ begin
259
+ @answer = @question.answer_or_default(get_response)
260
+ unless @question.valid_answer?(@answer)
261
+ explain_error(:not_valid)
262
+ raise QuestionError
263
+ end
264
+
265
+ @answer = @question.convert(@answer)
266
+
267
+ if @question.in_range?(@answer)
268
+ if @question.confirm
269
+ # need to add a layer of scope to ask a question inside a
270
+ # question, without destroying instance data
271
+ context_change = self.class.new(@input, @output, @wrap_at, @page_at, @indent_size, @indent_level)
272
+ if @question.confirm == true
273
+ confirm_question = "Are you sure? "
274
+ else
275
+ # evaluate ERb under initial scope, so it will have
276
+ # access to @question and @answer
277
+ template = ERB.new(@question.confirm, nil, "%")
278
+ confirm_question = template.result(binding)
279
+ end
280
+ unless context_change.agree(confirm_question)
281
+ explain_error(nil)
282
+ raise QuestionError
283
+ end
284
+ end
285
+
286
+ @answer
287
+ else
288
+ explain_error(:not_in_range)
289
+ raise QuestionError
290
+ end
291
+ rescue QuestionError
292
+ retry
293
+ rescue ArgumentError, NameError => error
294
+ raise if error.is_a?(NoMethodError)
295
+ if error.message =~ /ambiguous/
296
+ # the assumption here is that OptionParser::Completion#complete
297
+ # (used for ambiguity resolution) throws exceptions containing
298
+ # the word 'ambiguous' whenever resolution fails
299
+ explain_error(:ambiguous_completion)
300
+ else
301
+ explain_error(:invalid_type)
302
+ end
303
+ retry
304
+ rescue Question::NoAutoCompleteMatch
305
+ explain_error(:no_completion)
306
+ retry
307
+ ensure
308
+ @question = nil # Reset Question object.
309
+ end
310
+ end
311
+
312
+ #
313
+ # This method is HighLine's menu handler. For simple usage, you can just
314
+ # pass all the menu items you wish to display. At that point, choose() will
315
+ # build and display a menu, walk the user through selection, and return
316
+ # their choice among the provided items. You might use this in a case
317
+ # statement for quick and dirty menus.
318
+ #
319
+ # However, choose() is capable of much more. If provided, a block will be
320
+ # passed a HighLine::Menu object to configure. Using this method, you can
321
+ # customize all the details of menu handling from index display, to building
322
+ # a complete shell-like menuing system. See HighLine::Menu for all the
323
+ # methods it responds to.
324
+ #
325
+ # Raises EOFError if input is exhausted.
326
+ #
327
+ def choose( *items, &details )
328
+ @menu = @question = Menu.new(&details)
329
+ @menu.choices(*items) unless items.empty?
330
+
331
+ # Set auto-completion
332
+ @menu.completion = @menu.options
333
+ # Set _answer_type_ so we can double as the Question for ask().
334
+ @menu.answer_type = if @menu.shell
335
+ lambda do |command| # shell-style selection
336
+ first_word = command.to_s.split.first || ""
337
+
338
+ options = @menu.options
339
+ options.extend(OptionParser::Completion)
340
+ answer = options.complete(first_word)
341
+
342
+ if answer.nil?
343
+ raise Question::NoAutoCompleteMatch
344
+ end
345
+
346
+ [answer.last, command.sub(/^\s*#{first_word}\s*/, "")]
347
+ end
348
+ else
349
+ @menu.options # normal menu selection, by index or name
350
+ end
351
+
352
+ # Provide hooks for ERb layouts.
353
+ @header = @menu.header
354
+ @prompt = @menu.prompt
355
+
356
+ if @menu.shell
357
+ selected = ask("Ignored", @menu.answer_type)
358
+ @menu.select(self, *selected)
359
+ else
360
+ selected = ask("Ignored", @menu.answer_type)
361
+ @menu.select(self, selected)
362
+ end
363
+ end
364
+
365
+ #
366
+ # This method provides easy access to ANSI color sequences, without the user
367
+ # needing to remember to CLEAR at the end of each sequence. Just pass the
368
+ # _string_ to color, followed by a list of _colors_ you would like it to be
369
+ # affected by. The _colors_ can be HighLine class constants, or symbols
370
+ # (:blue for BLUE, for example). A CLEAR will automatically be embedded to
371
+ # the end of the returned String.
372
+ #
373
+ # This method returns the original _string_ unchanged if HighLine::use_color?
374
+ # is +false+.
375
+ #
376
+ def self.color( string, *colors )
377
+ return string unless self.use_color?
378
+ Style(*colors).color(string)
379
+ end
380
+
381
+ # In case you just want the color code, without the embedding and the CLEAR
382
+ def self.color_code(*colors)
383
+ Style(*colors).code
384
+ end
385
+
386
+ # Works as an instance method, same as the class method
387
+ def color_code(*colors)
388
+ self.class.color_code(*colors)
389
+ end
390
+
391
+ # Works as an instance method, same as the class method
392
+ def color(*args)
393
+ self.class.color(*args)
394
+ end
395
+
396
+ # Remove color codes from a string
397
+ def self.uncolor(string)
398
+ Style.uncolor(string)
399
+ end
400
+
401
+ # Works as an instance method, same as the class method
402
+ def uncolor(string)
403
+ self.class.uncolor(string)
404
+ end
405
+
406
+ #
407
+ # This method is a utility for quickly and easily laying out lists. It can
408
+ # be accessed within ERb replacements of any text that will be sent to the
409
+ # user.
410
+ #
411
+ # The only required parameter is _items_, which should be the Array of items
412
+ # to list. A specified _mode_ controls how that list is formed and _option_
413
+ # has different effects, depending on the _mode_. Recognized modes are:
414
+ #
415
+ # <tt>:columns_across</tt>:: _items_ will be placed in columns,
416
+ # flowing from left to right. If given,
417
+ # _option_ is the number of columns to be
418
+ # used. When absent, columns will be
419
+ # determined based on _wrap_at_ or a
420
+ # default of 80 characters.
421
+ # <tt>:columns_down</tt>:: Identical to <tt>:columns_across</tt>,
422
+ # save flow goes down.
423
+ # <tt>:uneven_columns_across</tt>:: Like <tt>:columns_across</tt> but each
424
+ # column is sized independently.
425
+ # <tt>:uneven_columns_down</tt>:: Like <tt>:columns_down</tt> but each
426
+ # column is sized independently.
427
+ # <tt>:inline</tt>:: All _items_ are placed on a single line.
428
+ # The last two _items_ are separated by
429
+ # _option_ or a default of " or ". All
430
+ # other _items_ are separated by ", ".
431
+ # <tt>:rows</tt>:: The default mode. Each of the _items_ is
432
+ # placed on its own line. The _option_
433
+ # parameter is ignored in this mode.
434
+ #
435
+ # Each member of the _items_ Array is passed through ERb and thus can contain
436
+ # their own expansions. Color escape expansions do not contribute to the
437
+ # final field width.
438
+ #
439
+ def list( items, mode = :rows, option = nil )
440
+ items = items.to_ary.map do |item|
441
+ if item.nil?
442
+ ""
443
+ else
444
+ ERB.new(item, nil, "%").result(binding)
445
+ end
446
+ end
447
+
448
+ if items.empty?
449
+ ""
450
+ else
451
+ case mode
452
+ when :inline
453
+ option = " or " if option.nil?
454
+
455
+ if items.size == 1
456
+ items.first
457
+ else
458
+ items[0..-2].join(", ") + "#{option}#{items.last}"
459
+ end
460
+ when :columns_across, :columns_down
461
+ max_length = actual_length(
462
+ items.max { |a, b| actual_length(a) <=> actual_length(b) }
463
+ )
464
+
465
+ if option.nil?
466
+ limit = @wrap_at || 80
467
+ option = (limit + 2) / (max_length + 2)
468
+ end
469
+
470
+ items = items.map do |item|
471
+ pad = max_length + (item.to_s.length - actual_length(item))
472
+ "%-#{pad}s" % item
473
+ end
474
+ row_count = (items.size / option.to_f).ceil
475
+
476
+ if mode == :columns_across
477
+ rows = Array.new(row_count) { Array.new }
478
+ items.each_with_index do |item, index|
479
+ rows[index / option] << item
480
+ end
481
+
482
+ rows.map { |row| row.join(" ") + "\n" }.join
483
+ else
484
+ columns = Array.new(option) { Array.new }
485
+ items.each_with_index do |item, index|
486
+ columns[index / row_count] << item
487
+ end
488
+
489
+ list = ""
490
+ columns.first.size.times do |index|
491
+ list << columns.map { |column| column[index] }.
492
+ compact.join(" ") + "\n"
493
+ end
494
+ list
495
+ end
496
+ when :uneven_columns_across
497
+ if option.nil?
498
+ limit = @wrap_at || 80
499
+ items.size.downto(1) do |column_count|
500
+ row_count = (items.size / column_count.to_f).ceil
501
+ rows = Array.new(row_count) { Array.new }
502
+ items.each_with_index do |item, index|
503
+ rows[index / column_count] << item
504
+ end
505
+
506
+ widths = Array.new(column_count, 0)
507
+ rows.each do |row|
508
+ row.each_with_index do |field, column|
509
+ size = actual_length(field)
510
+ widths[column] = size if size > widths[column]
511
+ end
512
+ end
513
+
514
+ if column_count == 1 or
515
+ widths.inject(0) { |sum, n| sum + n + 2 } <= limit + 2
516
+ return rows.map { |row|
517
+ row.zip(widths).map { |field, i|
518
+ "%-#{i + (field.to_s.length - actual_length(field))}s" % field
519
+ }.join(" ") + "\n"
520
+ }.join
521
+ end
522
+ end
523
+ else
524
+ row_count = (items.size / option.to_f).ceil
525
+ rows = Array.new(row_count) { Array.new }
526
+ items.each_with_index do |item, index|
527
+ rows[index / option] << item
528
+ end
529
+
530
+ widths = Array.new(option, 0)
531
+ rows.each do |row|
532
+ row.each_with_index do |field, column|
533
+ size = actual_length(field)
534
+ widths[column] = size if size > widths[column]
535
+ end
536
+ end
537
+
538
+ return rows.map { |row|
539
+ row.zip(widths).map { |field, i|
540
+ "%-#{i + (field.to_s.length - actual_length(field))}s" % field
541
+ }.join(" ") + "\n"
542
+ }.join
543
+ end
544
+ when :uneven_columns_down
545
+ if option.nil?
546
+ limit = @wrap_at || 80
547
+ items.size.downto(1) do |column_count|
548
+ row_count = (items.size / column_count.to_f).ceil
549
+ columns = Array.new(column_count) { Array.new }
550
+ items.each_with_index do |item, index|
551
+ columns[index / row_count] << item
552
+ end
553
+
554
+ widths = Array.new(column_count, 0)
555
+ columns.each_with_index do |column, i|
556
+ column.each do |field|
557
+ size = actual_length(field)
558
+ widths[i] = size if size > widths[i]
559
+ end
560
+ end
561
+
562
+ if column_count == 1 or
563
+ widths.inject(0) { |sum, n| sum + n + 2 } <= limit + 2
564
+ list = ""
565
+ columns.first.size.times do |index|
566
+ list << columns.zip(widths).map { |column, width|
567
+ field = column[index]
568
+ "%-#{width + (field.to_s.length - actual_length(field))}s" %
569
+ field
570
+ }.compact.join(" ").strip + "\n"
571
+ end
572
+ return list
573
+ end
574
+ end
575
+ else
576
+ row_count = (items.size / option.to_f).ceil
577
+ columns = Array.new(option) { Array.new }
578
+ items.each_with_index do |item, index|
579
+ columns[index / row_count] << item
580
+ end
581
+
582
+ widths = Array.new(option, 0)
583
+ columns.each_with_index do |column, i|
584
+ column.each do |field|
585
+ size = actual_length(field)
586
+ widths[i] = size if size > widths[i]
587
+ end
588
+ end
589
+
590
+ list = ""
591
+ columns.first.size.times do |index|
592
+ list << columns.zip(widths).map { |column, width|
593
+ field = column[index]
594
+ "%-#{width + (field.to_s.length - actual_length(field))}s" % field
595
+ }.compact.join(" ").strip + "\n"
596
+ end
597
+ return list
598
+ end
599
+ else
600
+ items.map { |i| "#{i}\n" }.join
601
+ end
602
+ end
603
+ end
604
+
605
+ #
606
+ # The basic output method for HighLine objects. If the provided _statement_
607
+ # ends with a space or tab character, a newline will not be appended (output
608
+ # will be flush()ed). All other cases are passed straight to Kernel.puts().
609
+ #
610
+ # The _statement_ parameter is processed as an ERb template, supporting
611
+ # embedded Ruby code. The template is evaluated with a binding inside
612
+ # the HighLine instance, providing easy access to the ANSI color constants
613
+ # and the HighLine.color() method.
614
+ #
615
+ def say( statement )
616
+ statement = statement.to_str
617
+ return unless statement.length > 0
618
+
619
+ # Allow non-ascii menu prompts in ruby > 1.9.2. ERB eval the menu statement
620
+ # with the environment's default encoding(usually utf8)
621
+ statement.force_encoding(Encoding.default_external) if defined?(Encoding) && Encoding.default_external
622
+
623
+ template = ERB.new(statement, nil, "%")
624
+ statement = template.result(binding)
625
+
626
+ statement = wrap(statement) unless @wrap_at.nil?
627
+ statement = page_print(statement) unless @page_at.nil?
628
+
629
+ statement = statement.gsub(/\n(?!$)/,"\n#{indentation}") if @multi_indent
630
+
631
+ # Don't add a newline if statement ends with whitespace, OR
632
+ # if statement ends with whitespace before a color escape code.
633
+ if /[ \t](\e\[\d+(;\d+)*m)?\Z/ =~ statement
634
+ @output.print(indentation+statement)
635
+ @output.flush
636
+ else
637
+ @output.puts(indentation+statement)
638
+ end
639
+ end
640
+
641
+ #
642
+ # Set to an integer value to cause HighLine to wrap output lines at the
643
+ # indicated character limit. When +nil+, the default, no wrapping occurs. If
644
+ # set to <tt>:auto</tt>, HighLine will attempt to determine the columns
645
+ # available for the <tt>@output</tt> or use a sensible default.
646
+ #
647
+ def wrap_at=( setting )
648
+ @wrap_at = setting == :auto ? output_cols : setting
649
+ end
650
+
651
+ #
652
+ # Set to an integer value to cause HighLine to page output lines over the
653
+ # indicated line limit. When +nil+, the default, no paging occurs. If
654
+ # set to <tt>:auto</tt>, HighLine will attempt to determine the rows available
655
+ # for the <tt>@output</tt> or use a sensible default.
656
+ #
657
+ def page_at=( setting )
658
+ @page_at = setting == :auto ? output_rows - 2 : setting
659
+ end
660
+
661
+ #
662
+ # Outputs indentation with current settings
663
+ #
664
+ def indentation
665
+ return ' '*@indent_size*@indent_level
666
+ end
667
+
668
+ #
669
+ # Executes block or outputs statement with indentation
670
+ #
671
+ def indent(increase=1, statement=nil, multiline=nil)
672
+ @indent_level += increase
673
+ multi = @multi_indent
674
+ @multi_indent = multiline unless multiline.nil?
675
+ if block_given?
676
+ yield self
677
+ else
678
+ say(statement)
679
+ end
680
+ @multi_indent = multi
681
+ @indent_level -= increase
682
+ end
683
+
684
+ #
685
+ # Outputs newline
686
+ #
687
+ def newline
688
+ @output.puts
689
+ end
690
+
691
+ #
692
+ # Returns the number of columns for the console, or a default it they cannot
693
+ # be determined.
694
+ #
695
+ def output_cols
696
+ return 80 unless @output.tty?
697
+ terminal_size.first
698
+ rescue
699
+ return 80
700
+ end
701
+
702
+ #
703
+ # Returns the number of rows for the console, or a default if they cannot be
704
+ # determined.
705
+ #
706
+ def output_rows
707
+ return 24 unless @output.tty?
708
+ terminal_size.last
709
+ rescue
710
+ return 24
711
+ end
712
+
713
+ private
714
+
715
+ #
716
+ # A helper method for sending the output stream and error and repeat
717
+ # of the question.
718
+ #
719
+ def explain_error( error )
720
+ say(@question.responses[error]) unless error.nil?
721
+ if @question.responses[:ask_on_error] == :question
722
+ say(@question)
723
+ elsif @question.responses[:ask_on_error]
724
+ say(@question.responses[:ask_on_error])
725
+ end
726
+ end
727
+
728
+ #
729
+ # Collects an Array/Hash full of answers as described in
730
+ # HighLine::Question.gather().
731
+ #
732
+ # Raises EOFError if input is exhausted.
733
+ #
734
+ def gather( )
735
+ original_question = @question
736
+ original_question_string = @question.question
737
+ original_gather = @question.gather
738
+
739
+ verify_match = @question.verify_match
740
+ @question.gather = false
741
+
742
+ begin # when verify_match is set this loop will repeat until unique_answers == 1
743
+ @answers = [ ]
744
+ @gather = original_gather
745
+ original_question.question = original_question_string
746
+
747
+ case @gather
748
+ when Integer
749
+ @answers << ask(@question)
750
+ @gather -= 1
751
+
752
+ original_question.question = ""
753
+ until @gather.zero?
754
+ @question = original_question
755
+ @answers << ask(@question)
756
+ @gather -= 1
757
+ end
758
+ when ::String, Regexp
759
+ @answers << ask(@question)
760
+
761
+ original_question.question = ""
762
+ until (@gather.is_a?(::String) and @answers.last.to_s == @gather) or
763
+ (@gather.is_a?(Regexp) and @answers.last.to_s =~ @gather)
764
+ @question = original_question
765
+ @answers << ask(@question)
766
+ end
767
+
768
+ @answers.pop
769
+ when Hash
770
+ @answers = { }
771
+ @gather.keys.sort.each do |key|
772
+ @question = original_question
773
+ @key = key
774
+ @answers[key] = ask(@question)
775
+ end
776
+ end
777
+
778
+ if verify_match && (unique_answers(@answers).size > 1)
779
+ @question = original_question
780
+ explain_error(:mismatch)
781
+ else
782
+ verify_match = false
783
+ end
784
+
785
+ end while verify_match
786
+
787
+ original_question.verify_match ? @answer : @answers
788
+ end
789
+
790
+ #
791
+ # A helper method used by HighLine::Question.verify_match
792
+ # for finding whether a list of answers match or differ
793
+ # from each other.
794
+ #
795
+ def unique_answers(list = @answers)
796
+ (list.respond_to?(:values) ? list.values : list).uniq
797
+ end
798
+
799
+ #
800
+ # Read a line of input from the input stream and process whitespace as
801
+ # requested by the Question object.
802
+ #
803
+ # If Question's _readline_ property is set, that library will be used to
804
+ # fetch input. *WARNING*: This ignores the currently set input stream.
805
+ #
806
+ # Raises EOFError if input is exhausted.
807
+ #
808
+ def get_line( )
809
+ if @question.readline
810
+ require "readline" # load only if needed
811
+
812
+ # capture say()'s work in a String to feed to readline()
813
+ old_output = @output
814
+ @output = StringIO.new
815
+ say(@question)
816
+ question = @output.string
817
+ @output = old_output
818
+
819
+ # prep auto-completion
820
+ Readline.completion_proc = lambda do |string|
821
+ @question.selection.grep(/\A#{Regexp.escape(string)}/)
822
+ end
823
+
824
+ # work-around ugly readline() warnings
825
+ old_verbose = $VERBOSE
826
+ $VERBOSE = nil
827
+ raw_answer = Readline.readline(question, true)
828
+ if raw_answer.nil?
829
+ if @@track_eof
830
+ raise EOFError, "The input stream is exhausted."
831
+ else
832
+ raw_answer = String.new # Never return nil
833
+ end
834
+ end
835
+ answer = @question.change_case(
836
+ @question.remove_whitespace(raw_answer))
837
+ $VERBOSE = old_verbose
838
+
839
+ answer
840
+ else
841
+ if JRUBY
842
+ raw_answer = @java_console.readLine(@question.question, nil)
843
+ else
844
+ raise EOFError, "The input stream is exhausted." if @@track_eof and
845
+ @input.eof?
846
+ raw_answer = @input.gets
847
+ end
848
+
849
+ @question.change_case(@question.remove_whitespace(raw_answer))
850
+ end
851
+ end
852
+
853
+ #
854
+ # Return a line or character of input, as requested for this question.
855
+ # Character input will be returned as a single character String,
856
+ # not an Integer.
857
+ #
858
+ # This question's _first_answer_ will be returned instead of input, if set.
859
+ #
860
+ # Raises EOFError if input is exhausted.
861
+ #
862
+ def get_response( )
863
+ return @question.first_answer if @question.first_answer?
864
+
865
+ if @question.character.nil?
866
+ if @question.echo == true and @question.limit.nil?
867
+ get_line
868
+ else
869
+ raw_no_echo_mode
870
+
871
+ line = ""
872
+ backspace_limit = 0
873
+ begin
874
+
875
+ while character = get_character(@input)
876
+ # honor backspace and delete
877
+ if character == 127 or character == 8
878
+ line.slice!(-1, 1)
879
+ backspace_limit -= 1
880
+ else
881
+ line << character.chr
882
+ backspace_limit = line.size
883
+ end
884
+ # looking for carriage return (decimal 13) or
885
+ # newline (decimal 10) in raw input
886
+ break if character == 13 or character == 10
887
+ if @question.echo != false
888
+ if character == 127 or character == 8
889
+ # only backspace if we have characters on the line to
890
+ # eliminate, otherwise we'll tromp over the prompt
891
+ if backspace_limit >= 0 then
892
+ @output.print("\b#{HighLine.Style(:erase_char).code}")
893
+ else
894
+ # do nothing
895
+ end
896
+ else
897
+ if @question.echo == true
898
+ @output.print(character.chr)
899
+ else
900
+ @output.print(@question.echo)
901
+ end
902
+ end
903
+ @output.flush
904
+ end
905
+ break if @question.limit and line.size == @question.limit
906
+ end
907
+ ensure
908
+ restore_mode
909
+ end
910
+ if @question.overwrite
911
+ @output.print("\r#{HighLine.Style(:erase_line).code}")
912
+ @output.flush
913
+ else
914
+ say("\n")
915
+ end
916
+
917
+ @question.change_case(@question.remove_whitespace(line))
918
+ end
919
+ else
920
+ raw_no_echo_mode
921
+ begin
922
+ if @question.character == :getc
923
+ response = @input.getbyte.chr
924
+ else
925
+ response = get_character(@input).chr
926
+ if @question.overwrite
927
+ @output.print("\r#{HighLine.Style(:erase_line).code}")
928
+ @output.flush
929
+ else
930
+ echo = if @question.echo == true
931
+ response
932
+ elsif @question.echo != false
933
+ @question.echo
934
+ else
935
+ ""
936
+ end
937
+ say("#{echo}\n")
938
+ end
939
+ end
940
+ ensure
941
+ restore_mode
942
+ end
943
+ @question.change_case(response)
944
+ end
945
+ end
946
+
947
+ #
948
+ # Page print a series of at most _page_at_ lines for _output_. After each
949
+ # page is printed, HighLine will pause until the user presses enter/return
950
+ # then display the next page of data.
951
+ #
952
+ # Note that the final page of _output_ is *not* printed, but returned
953
+ # instead. This is to support any special handling for the final sequence.
954
+ #
955
+ def page_print( output )
956
+ lines = output.scan(/[^\n]*\n?/)
957
+ while lines.size > @page_at
958
+ @output.puts lines.slice!(0...@page_at).join
959
+ @output.puts
960
+ # Return last line if user wants to abort paging
961
+ return (["...\n"] + lines.slice(-2,1)).join unless continue_paging?
962
+ end
963
+ return lines.join
964
+ end
965
+
966
+ #
967
+ # Ask user if they wish to continue paging output. Allows them to type "q" to
968
+ # cancel the paging process.
969
+ #
970
+ def continue_paging?
971
+ command = HighLine.new(@input, @output).ask(
972
+ "-- press enter/return to continue or q to stop -- "
973
+ ) { |q| q.character = true }
974
+ command !~ /\A[qQ]\Z/ # Only continue paging if Q was not hit.
975
+ end
976
+
977
+ #
978
+ # Wrap a sequence of _lines_ at _wrap_at_ characters per line. Existing
979
+ # newlines will not be affected by this process, but additional newlines
980
+ # may be added.
981
+ #
982
+ def wrap( text )
983
+ wrapped = [ ]
984
+ text.each_line do |line|
985
+ # take into account color escape sequences when wrapping
986
+ wrap_at = @wrap_at + (line.length - actual_length(line))
987
+ while line =~ /([^\n]{#{wrap_at + 1},})/
988
+ search = $1.dup
989
+ replace = $1.dup
990
+ if index = replace.rindex(" ", wrap_at)
991
+ replace[index, 1] = "\n"
992
+ replace.sub!(/\n[ \t]+/, "\n")
993
+ line.sub!(search, replace)
994
+ else
995
+ line[$~.begin(1) + wrap_at, 0] = "\n"
996
+ end
997
+ end
998
+ wrapped << line
999
+ end
1000
+ return wrapped.join
1001
+ end
1002
+
1003
+ #
1004
+ # Returns the length of the passed +string_with_escapes+, minus and color
1005
+ # sequence escapes.
1006
+ #
1007
+ def actual_length( string_with_escapes )
1008
+ string_with_escapes.to_s.gsub(/\e\[\d{1,2}m/, "").length
1009
+ end
1010
+ end
1011
+
1012
+ require "highline/string_extensions"