skylight 0.0.7 → 0.0.10

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.
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"