2 # Class used to return formatted output to the +Debugger+ for display.
3 # Command implementations should not output anything directly, but should
4 # instead return either a string, or an instance of this class if the output
5 # needs to be formatted in some way.
9 # Class for defining columns of output.
12 FORMAT_RE = /%([|-])?(0)?(\d*|\*)([sd])/
14 # Defines a new Columns block for an output.
15 # Takes the following arguments:
16 # - formats: Either a fixnum specifying the number of columns, or an array
17 # of format specifications (one per column). If an array is passed, the
18 # items of the array may be either a format specification as handled by
19 # String#%, or an array containing the column header and a format
21 # - An optional column separator string that will be inserted between
22 # columns when converting a row of cells to a string; defaults to a
24 # Column widths will be fixed at whatever size is specified in the format,
25 # or if none is specified, to whatever the necessary width is requried to
26 # output all values in that column. The special width specification * can
27 # be used to indicate that all remaining space on the line should be set
28 # as the column width.
29 def initialize(formats, col_sep=' ')
30 if formats.kind_of? Array
33 formats.each_with_index do |fmt,i|
35 @headers = [] unless @headers
36 @headers[i] = fmt.first if fmt.size > 1
37 @formats[i] = fmt.last
42 elsif formats.kind_of? Fixnum
43 @formats = Array.new(formats, '%-s')
46 raise ArgumentError, "The formats arg must be an Array or a Fixnum (got #{formats.class})"
48 @col_separator = col_sep
50 @fixed_widths = Array.new(@formats.size)
51 @widths = Array.new(@formats.size, 0)
53 # Use widths specified in format string
54 @formats.each_with_index do |fmt, i|
56 raise ArgumentError, "Invalid format specification" unless $4
57 if $3 and $3.length > 0
59 @fixed_widths[i] = $3.to_i + $`.length + $'.length
60 @widths[i] = @fixed_widths[i]
62 @fixed_widths[i] = '*'
67 # Initialise column widths to column header widths
69 @headers.each_with_index do |hdr, i|
70 @widths[i] = hdr.length if hdr and hdr.length > @widths[i]
75 attr_reader :fixed_widths, :widths
77 # Update the column widths required based on the row content
78 def update_widths(cells)
79 cells.each_with_index do |cell,col|
81 @formats[col] =~ FORMAT_RE
82 str = "#{$`}%#{$2}#{$3 unless $3 == '*'}#{$4}#{$'}" % cell
83 @widths[col] = str.length if str.length > @widths[col]
88 # Redistributes the calculated widths, ensuring the overall line width is
89 # no greater than the specified page width. Reductions are made in the
91 # - first, all columns that have a width specification are reduced to that
92 # width (if they exceed it)
93 # - next, any columns with a variable width specification are reduced
94 # (from largest to smallest) proportionately based on the needed reduction
95 def redistribute_widths(page_width, indent=0)
97 # Reduce page_wdith by any requirements for indentation and column separators
98 page_width -= indent + (@widths.size-1) * @col_separator.length
99 raise ArgumentError, "Page width is insufficient to display any content" if page_width < 1
101 # Determine the fixed and variable width columns
106 @widths.each_with_index do |width,i|
107 cum_width += @widths[i]
109 if fixed_widths[i] == '*'
111 var_width += @widths[i]
113 fixed_width += @fixed_widths[i]
117 return true if cum_width <= page_width
119 # Need to squeeze - first up, ensure fixed width columns don't exceed
121 @fixed_widths.each_with_index do |fw,i|
124 cum_width -= @widths[i] - fw
129 return true if cum_width <= page_width
131 if variable_widths.size > 0 and (fixed_width + variable_widths.size) < page_width
132 # Next, reduce variable widths proportionately to needs
134 variable_widths.sort.each do |i|
135 adj = (@widths[i].to_f / var_width * (cum_width - page_width)).ceil
138 break if cum_width - cum_adj <= page_width
142 return true if cum_width <= page_width
144 if cum_width > page_width
145 # TODO: Full squeeze - squeeze all columns to fit
153 # Returns true if the column specification has headers
158 # Returns a count of the number of columns defined
163 # Returns a formatted string containing the column headers.
164 # Takes two optional parameters:
165 # - indent: specifies the number of characters to indent the line by
167 # - page_width: specifies a page width to which the output should be made
168 # to fit. If nil (the default), output is not forced to fit any width.
169 def format_header_str(indent=0)
172 hdr.concat format_str(@headers, indent-1, Array.new(@formats.size, '%|s'))
173 str = ' ' * (indent-1) + '+' if indent > 0
174 @widths.each do |width|
175 str << '-' * width + '+'
182 # Format an array of cells into a string
183 def format_str(row, indent=0, formats=@formats)
185 formats.each_with_index do |fmt, i|
187 # Format cell ignoring width and alignment, wrapping if necessary
189 cell = "#{$`}%#{$4}#{$'}" % row[i]
192 when '|' then :center
196 lines = wrap(cell, @widths[i], align, pad)
203 line, last_line = 0, 1
205 while line < last_line do
207 cells.each_with_index do |cell, i|
208 last_line = cell.size if line == 0 and cell.size > last_line
210 line_cells << cell[line]
212 # Cell does not wrap onto current line, so just output spaces
213 line_cells << ' ' * @widths[i]
216 str << ' ' * indent + line_cells.join(@col_separator)
222 # Splits the supplied string at logical breaks to ensure that no line is
223 # longer than the spcecified width. Returns an array of lines.
224 def wrap(str, width, align=:none, pad=' ')
225 raise ArgumentError, "Invalid wrap length specified (#{width})" if width < 0
227 return [nil] unless str and width > 0
231 until str.length <= width do
232 if pos = str[0, width].rindex(/[\s\-,\/_]/)
233 # Found a break on whitespace or dash
234 line, str = str[0..pos].rstrip, str[pos+1..-1].strip
235 elsif pos = str[0, width-1].rindex(/[^\w]/) and pos > 0
236 # Found a non-word character to break on
237 line, str = str[0...pos].rstrip, str[pos..-1].strip
239 # Force break at width
240 line, str = str[0...width].rstrip, str[width..-1].strip
243 # Pad with spaces to requested width if an alignment is specified
244 lines << align(line, width, align, pad)
246 lines << align(str, width, align, pad) if str
251 def align(line, width, align, pad=' ')
254 line = line + pad * (width - line.length)
256 line = pad * (width - line.length) + line
258 line = line.center(width, pad)
266 # Class for colorizing output lines
269 def initialize(color=:clear)
311 # Class for marking a line in some way
314 def initialize(marker='=> ')
327 # Convenience method for creating an Output of type :info
328 def self.info(msg, color=nil)
330 out.set_color color if color
332 out.set_color :clear if color
336 # Convenience method for creating an Output of type :none
337 def self.none(msg, color=nil)
339 out.set_color color if color
341 out.set_color :clear if color
345 # Convenience method for creating an Output of type :error
346 def self.error(msg, color=nil)
348 out.set_color color if color
350 out.set_color :clear if color
354 # Initializes a new output instance.
355 # Output may be of different types, with the default being :info.
356 # The output type is not used by the Output class, but may be significant
357 # to the interface that displays the output. Commonly used output types
359 # - :info, the default for informational output
360 # - :none, to indicate there is no output of the requested type
361 # - :error, to indicate a command error
362 def initialize(output_type=:info)
363 @output_type = output_type
367 # Clears all output settings
374 attr_reader :output, :current_cols, :current_color
376 # Adds a new row of data to the output
377 # The item to be added may be an object of several different types:
378 # - An array is regarded as a series of cells that make up a row in a table.
379 # - A string is regarded as a complete row of data.
380 # - A Columns object defines the columnar aspects of subsequent rows of
381 # data, which are typically arrays.
382 # - A Color object defines a color change that remains in force until it is
384 # - LineMarker object specifies a marker to appear to the left of the next
389 # Line contains multiple columns of text
390 unless @current_cols and item.size == @current_cols.count
391 # Normally, a command will explicitly specify column formats via a
392 # call to set_columns; however, if the output stream receives an array
393 # of objects with a different column count to previous lines, a new
394 # Columns instance is auto-created.
395 @current_cols = Columns.new(item.size)
396 @output << @current_cols
398 @current_cols.update_widths(item)
402 @current_color = item
404 @marker_width = item.length if @marker_width < item.length
409 # Convenience method to set a new column structure
410 def set_columns(formats, col_sep=' ')
411 self << Columns.new(formats, col_sep)
414 # Convenience method to set a new row color
416 self << Color.new(color) unless @current_color and @current_color.color == color
419 # Sets a marker to be displayed next to the next line
420 def set_line_marker(marker='=> ')
421 self << LineMarker.new(marker)
424 # Convert this output stream to a string
429 # Return the output as an array of strings, one per line. If an output item
430 # wraps to more than one line, each line will be a seperate entry in the
432 # Takes an optional parameter +page_width+ that specifies the width of the
433 # page on which the output will be displayed.
434 def lines(page_width=nil)
439 @output.each do |item|
443 str << color.escape if color
445 str << color.clear if color
450 # Check page_width is sufficient for marker and column headers
451 width = page_width-1 - (@marker_width + 2)
452 column.redistribute_widths(page_width-1, @marker_width + 2) if width > 0
454 if column.has_headers?
455 lines.concat column.format_header_str(@marker_width+2)
459 str << color.escape if color
460 str << output_marker(marker)
462 l = column.format_str(item, @marker_width+2)
463 l.first[0, @marker_width] = ''
464 l.first.insert 0, str
465 l.last << color.clear if color
468 if item.color == :clear
480 def output_marker(marker)
486 str = ' ' * @marker_width