hirb 0.1.2 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/hirb/helpers.rb CHANGED
@@ -2,6 +2,6 @@ module Hirb
2
2
  module Helpers #:nodoc:
3
3
  end
4
4
  end
5
- %w{table object_table active_record_table auto_table tree parent_child_tree}.each do |e|
5
+ %w{table object_table active_record_table auto_table tree parent_child_tree vertical_table}.each do |e|
6
6
  require "hirb/helpers/#{e}"
7
7
  end
@@ -8,10 +8,19 @@ class Hirb::Helpers::ActiveRecordTable < Hirb::Helpers::ObjectTable
8
8
  rows = [rows] unless rows.is_a?(Array)
9
9
  options[:fields] ||=
10
10
  begin
11
- fields = rows.first.attribute_names
12
- fields.unshift(fields.delete('id')) if fields.include?('id')
11
+ fields = rows.first.class.column_names
13
12
  fields.map {|e| e.to_sym }
14
13
  end
14
+ if query_used_select?(rows)
15
+ selected_columns = rows.first.attributes.keys
16
+ sorted_columns = rows.first.class.column_names.dup.delete_if {|e| !selected_columns.include?(e) }
17
+ sorted_columns += (selected_columns - sorted_columns)
18
+ options[:fields] = sorted_columns.map {|e| e.to_sym}
19
+ end
15
20
  super(rows, options)
16
21
  end
17
- end
22
+
23
+ def self.query_used_select?(rows) #:nodoc:
24
+ rows.first.attributes.keys.sort != rows.first.class.column_names.sort
25
+ end
26
+ end
@@ -1,10 +1,11 @@
1
- # Attempts to autodetect the table class the output represents and delegates rendering to it.
1
+ # Detects the table class the output should use and delegates rendering to it.
2
2
  class Hirb::Helpers::AutoTable
3
3
  # Same options as Hirb::Helpers::Table.render.
4
4
  def self.render(output, options={})
5
+ output = output.to_a if !output.is_a?(Array) && output.respond_to?(:to_a)
5
6
  klass = if ((output.is_a?(Array) && output[0].is_a?(ActiveRecord::Base)) or output.is_a?(ActiveRecord::Base) rescue false)
6
7
  Hirb::Helpers::ActiveRecordTable
7
- elsif options[:fields]
8
+ elsif (output.is_a?(Array) && !(output[0].is_a?(Hash) || output[0].is_a?(Array)))
8
9
  Hirb::Helpers::ObjectTable
9
10
  else
10
11
  Hirb::Helpers::Table
@@ -2,13 +2,13 @@ class Hirb::Helpers::ObjectTable < Hirb::Helpers::Table
2
2
  # Rows are any ruby objects. Takes same options as Hirb::Helpers::Table.render except as noted below.
3
3
  #
4
4
  # Options:
5
- # :fields- Methods of the object which are represented as columns in the table. Required option.
6
- # All method values are converted to strings via to_s.
5
+ # :fields- Methods of the object to represent as columns. Defaults to [:to_s].
7
6
  def self.render(rows, options ={})
8
- raise(ArgumentError, "Option 'fields' is required.") unless options[:fields]
7
+ options[:fields] ||= [:to_s]
8
+ options[:headers] = {:to_s=>'value'} if options[:fields] == [:to_s]
9
9
  rows = [rows] unless rows.is_a?(Array)
10
10
  item_hashes = rows.inject([]) {|t,item|
11
- t << options[:fields].inject({}) {|h,f| h[f] = item.send(f).to_s; h}
11
+ t << options[:fields].inject({}) {|h,f| h[f] = item.send(f); h}
12
12
  }
13
13
  super(item_hashes, options)
14
14
  end
@@ -1,6 +1,6 @@
1
1
  # Base Table class from which other table classes inherit.
2
2
  # By default, a table is constrained to a default width but this can be adjusted
3
- # via options as well as Hirb:Helpers::Table.max_width.
3
+ # via the max_width option or Hirb::View.width.
4
4
  # Rows can be an array of arrays or an array of hashes.
5
5
  #
6
6
  # An array of arrays ie [[1,2], [2,3]], would render:
@@ -25,10 +25,10 @@
25
25
  #--
26
26
  # derived from http://gist.github.com/72234
27
27
  class Hirb::Helpers::Table
28
- DEFAULT_MAX_WIDTH = 150
28
+ BORDER_LENGTH = 3 # " | " and "-+-" are the borders
29
29
  class TooManyFieldsForWidthError < StandardError; end
30
+
30
31
  class << self
31
- attr_accessor :max_width
32
32
 
33
33
  # Main method which returns a formatted table.
34
34
  # ==== Options:
@@ -40,26 +40,40 @@ class Hirb::Helpers::Table
40
40
  # length.
41
41
  # [:max_width] The maximum allowed width of all fields put together. This option is enforced except when the field_lengths option is set.
42
42
  # This doesn't count field borders as part of the total.
43
+ # [:number] When set to true, numbers rows by adding a :hirb_number column as the first column. Default is false.
44
+ # [:filters] A hash of fields and the filters that each row in the field must run through. The filter converts the cell's value by applying
45
+ # a given proc or an array containing a method and optional arguments to it.
46
+ # [:vertical] When set to true, renders a vertical table using Hirb::Helpers::VerticalTable. Default is false.
43
47
  # Examples:
44
48
  # Hirb::Helpers::Table.render [[1,2], [2,3]]
45
49
  # Hirb::Helpers::Table.render [[1,2], [2,3]], :field_lengths=>{0=>10}
46
50
  # Hirb::Helpers::Table.render [{:age=>10, :weight=>100}, {:age=>80, :weight=>500}]
47
51
  # Hirb::Helpers::Table.render [{:age=>10, :weight=>100}, {:age=>80, :weight=>500}], :headers=>{:weight=>"Weight(lbs)"}
52
+ # Hirb::Helpers::Table.render [{:age=>10, :weight=>100}, {:age=>80, :weight=>500}], :filters=>{:age=>[:to_f]}
48
53
  def render(rows, options={})
49
- new(rows,options).render
54
+ options.delete(:vertical) ? Hirb::Helpers::VerticalTable.render(rows, options) : new(rows, options).render
55
+ rescue TooManyFieldsForWidthError
56
+ $stderr.puts "", "** Error: Too many fields for the current width. Configure your width " +
57
+ "and/or fields to avoid this error. Defaulting to a vertical table. **"
58
+ Hirb::Helpers::VerticalTable.render(rows, options)
50
59
  end
51
60
  end
52
61
 
53
62
  #:stopdoc:
54
63
  def initialize(rows, options={})
55
64
  @options = options
56
- @fields = options[:fields] || ((rows[0].is_a?(Hash)) ? rows[0].keys.sort {|a,b| a.to_s <=> b.to_s} :
65
+ @options[:filters] ||= {}
66
+ @fields = @options[:fields] ? @options[:fields].dup : ((rows[0].is_a?(Hash)) ? rows[0].keys.sort {|a,b| a.to_s <=> b.to_s} :
57
67
  rows[0].is_a?(Array) ? (0..rows[0].length - 1).to_a : [])
58
68
  @rows = setup_rows(rows)
59
69
  @headers = @fields.inject({}) {|h,e| h[e] = e.to_s; h}
60
- if options.has_key?(:headers)
61
- @headers = options[:headers].is_a?(Hash) ? @headers.merge(options[:headers]) :
62
- (options[:headers].is_a?(Array) ? array_to_indices_hash(options[:headers]) : options[:headers])
70
+ if @options.has_key?(:headers)
71
+ @headers = @options[:headers].is_a?(Hash) ? @headers.merge(@options[:headers]) :
72
+ (@options[:headers].is_a?(Array) ? array_to_indices_hash(@options[:headers]) : @options[:headers])
73
+ end
74
+ if @options[:number]
75
+ @headers[:hirb_number] = "number"
76
+ @fields.unshift :hirb_number
63
77
  end
64
78
  end
65
79
 
@@ -71,6 +85,8 @@ class Hirb::Helpers::Table
71
85
  new_rows << array_to_indices_hash(row)
72
86
  }
73
87
  end
88
+ rows = filter_values(rows)
89
+ rows.each_with_index {|e,i| e[:hirb_number] = (i + 1).to_s} if @options[:number]
74
90
  validate_values(rows)
75
91
  rows
76
92
  end
@@ -79,15 +95,23 @@ class Hirb::Helpers::Table
79
95
  body = []
80
96
  unless @rows.length == 0
81
97
  setup_field_lengths
82
- body += @headers ? render_header : [render_border]
98
+ body += render_header
83
99
  body += render_rows
84
- body << render_border
100
+ body += render_footer
85
101
  end
86
102
  body << render_table_description
87
103
  body.join("\n")
88
104
  end
89
-
105
+
90
106
  def render_header
107
+ @headers ? render_table_header : [render_border]
108
+ end
109
+
110
+ def render_footer
111
+ [render_border]
112
+ end
113
+
114
+ def render_table_header
91
115
  title_row = '| ' + @fields.map {|f|
92
116
  format_cell(@headers[f], @field_lengths[f])
93
117
  }.join(' | ') + ' |'
@@ -124,26 +148,53 @@ class Hirb::Helpers::Table
124
148
  if @options[:field_lengths]
125
149
  @field_lengths.merge!(@options[:field_lengths])
126
150
  else
127
- table_max_width = Hirb::Helpers::Table.max_width || DEFAULT_MAX_WIDTH
128
- table_max_width = @options[:max_width] if @options.has_key?(:max_width)
129
- restrict_field_lengths(@field_lengths, table_max_width)
151
+ table_max_width = @options.has_key?(:max_width) ? @options[:max_width] : Hirb::View.width
152
+ restrict_field_lengths(@field_lengths, table_max_width) if table_max_width
130
153
  end
131
154
  end
132
155
 
156
+ def restrict_field_lengths(field_lengths, max_width)
157
+ max_width -= @fields.size * BORDER_LENGTH + 1
158
+ original_field_lengths = field_lengths.dup
159
+ @min_field_length = BORDER_LENGTH
160
+ adjust_long_fields(field_lengths, max_width)
161
+ rescue TooManyFieldsForWidthError
162
+ raise
163
+ rescue
164
+ default_restrict_field_lengths(field_lengths, original_field_lengths, max_width)
165
+ end
166
+
133
167
  # Simple algorithm which given a max width, allows smaller fields to be displayed while
134
168
  # restricting longer fields at an average_long_field_length.
135
- def restrict_field_lengths(field_lengths, max_width)
169
+ def adjust_long_fields(field_lengths, max_width)
136
170
  total_length = field_lengths.values.inject {|t,n| t += n}
137
- if max_width && total_length > max_width
138
- min_field_length = 3
139
- raise TooManyFieldsForWidthError if @fields.size > max_width.to_f / min_field_length
171
+ while total_length > max_width
172
+ raise TooManyFieldsForWidthError if @fields.size > max_width.to_f / @min_field_length
140
173
  average_field_length = total_length / @fields.size.to_f
141
174
  long_lengths = field_lengths.values.select {|e| e > average_field_length}
142
- total_long_field_length = (long_lengths.inject {|t,n| t += n}) * max_width/total_length
143
- average_long_field_length = total_long_field_length / long_lengths.size
144
- field_lengths.each {|f,length|
145
- field_lengths[f] = average_long_field_length if length > average_long_field_length
146
- }
175
+ if long_lengths.empty?
176
+ raise "Algorithm didn't work, resort to default"
177
+ else
178
+ total_long_field_length = (long_lengths.inject {|t,n| t += n}) * max_width/total_length
179
+ average_long_field_length = total_long_field_length / long_lengths.size
180
+ field_lengths.each {|f,length|
181
+ field_lengths[f] = average_long_field_length if length > average_long_field_length
182
+ }
183
+ end
184
+ total_length = field_lengths.values.inject {|t,n| t += n}
185
+ end
186
+ end
187
+
188
+ # Produces a field_lengths which meets the max_width requirement
189
+ def default_restrict_field_lengths(field_lengths, original_field_lengths, max_width)
190
+ original_total_length = original_field_lengths.values.inject {|t,n| t += n}
191
+ relative_lengths = original_field_lengths.values.map {|v| (v / original_total_length.to_f * max_width).to_i }
192
+ # set fields by their relative weight to original length
193
+ if relative_lengths.all? {|e| e > @min_field_length} && (relative_lengths.inject {|a,e| a += e} <= max_width)
194
+ original_field_lengths.each {|k,v| field_lengths[k] = (v / original_total_length.to_f * max_width).to_i }
195
+ else
196
+ # set all fields the same if nothing else works
197
+ field_lengths.each {|k,v| field_lengths[k] = max_width / @fields.size}
147
198
  end
148
199
  end
149
200
 
@@ -159,6 +210,21 @@ class Hirb::Helpers::Table
159
210
  field_lengths
160
211
  end
161
212
 
213
+ def filter_values(rows)
214
+ rows.map {|row|
215
+ new_row = {}
216
+ @fields.each {|f|
217
+ if @options[:filters][f]
218
+ new_row[f] = @options[:filters][f].is_a?(Proc) ? @options[:filters][f].call(row[f]) :
219
+ row[f].send(*@options[:filters][f])
220
+ else
221
+ new_row[f] = row[f]
222
+ end
223
+ }
224
+ new_row
225
+ }
226
+ end
227
+
162
228
  def validate_values(rows)
163
229
  rows.each {|row|
164
230
  @fields.each {|f|
@@ -0,0 +1,31 @@
1
+ class Hirb::Helpers::VerticalTable < Hirb::Helpers::Table
2
+
3
+ # Renders a vertical table using the same options as Hirb::Helpers::Table.render except for :field_lengths,
4
+ # :vertical and :max_width which aren't used.
5
+ def self.render(rows, options={})
6
+ new(rows, options).render
7
+ end
8
+
9
+ #:stopdoc:
10
+ def setup_field_lengths
11
+ @field_lengths = default_field_lengths
12
+ end
13
+
14
+ def render_header; []; end
15
+ def render_footer; []; end
16
+
17
+ def render_rows
18
+ i = 0
19
+ longest_header = @headers.values.sort_by {|e| e.length}.last.length
20
+ stars = "*" * [(longest_header + (longest_header / 2)), 3].max
21
+ @rows.map do |row|
22
+ row = "#{stars} #{i+1}. row #{stars}\n" +
23
+ @fields.map {|f|
24
+ "#{@headers[f].rjust(longest_header)}: #{row[f]}"
25
+ }.join("\n")
26
+ i+= 1
27
+ row
28
+ end
29
+ #:startdoc:
30
+ end
31
+ end
data/lib/hirb/menu.rb ADDED
@@ -0,0 +1,47 @@
1
+ module Hirb
2
+ # This class provides a selection menu using Hirb's table helpers by default to display choices.
3
+ class Menu
4
+ # Menu which asks to select from the given array and returns the selected menu items as an array. See Hirb::Util.choose_from_array
5
+ # for the syntax for specifying selections. All options except for the ones below are passed to render the menu.
6
+ #
7
+ # ==== Options:
8
+ # [:helper_class] Helper class to render menu. Helper class is expected to implement numbering given a :number option.
9
+ # To use a very basic menu, set this to false. Defaults to Hirb::Helpers::AutoTable.
10
+ # [:prompt] String for menu prompt. Defaults to "Choose: ".
11
+ # [:validate_one] Validates that only one item in array is chosen and returns just that item. Default is false.
12
+ # [:ask] Always ask for input, even if there is only one choice. Default is true.
13
+ # Examples:
14
+ # extend Hirb::Console
15
+ # menu([1,2,3], :fields=>[:field1, :field2], :validate_one=>true)
16
+ # menu([1,2,3], :helper_class=>Hirb::Helpers::Table)
17
+ def self.render(output, options={})
18
+ default_options = {:helper_class=>Hirb::Helpers::AutoTable, :prompt=>"Choose #{options[:validate_one] ? 'one' : ''}: ", :ask=>true}
19
+ options = default_options.merge(options)
20
+ output = [output] unless output.is_a?(Array)
21
+ chosen = choose_from_menu(output, options)
22
+ yield(chosen) if block_given? && chosen.is_a?(Array) && chosen.size > 0
23
+ chosen
24
+ end
25
+
26
+ def self.choose_from_menu(output, options) #:nodoc:
27
+ return output if output.size == 1 && !options[:ask]
28
+ if (helper_class = Util.any_const_get(options[:helper_class]))
29
+ View.render_output(output, :class=>options[:helper_class], :options=>options.merge(:number=>true))
30
+ else
31
+ output.each_with_index {|e,i| puts "#{i+1}: #{e}" }
32
+ end
33
+ print options[:prompt]
34
+ input = $stdin.gets.chomp.strip
35
+ chosen = Util.choose_from_array(output, input)
36
+ if options[:validate_one]
37
+ if chosen.size != 1
38
+ $stderr.puts "Choose one. You chose #{chosen.size} items."
39
+ return nil
40
+ else
41
+ return chosen[0]
42
+ end
43
+ end
44
+ chosen
45
+ end
46
+ end
47
+ end
data/lib/hirb/pager.rb ADDED
@@ -0,0 +1,94 @@
1
+ module Hirb
2
+ # This class provides class methods for paging and an object which can conditionally page given a terminal size that is exceeded.
3
+ class Pager
4
+ class<<self
5
+ # Pages using a configured or detected shell command.
6
+ def command_pager(output, options={})
7
+ basic_pager(output) if valid_pager_command?(options[:pager_command])
8
+ end
9
+
10
+ def pager_command(*commands) #:nodoc:
11
+ @pager_command = (!@pager_command.nil? && commands.empty?) ? @pager_command :
12
+ begin
13
+ commands = [ENV['PAGER'], 'less', 'more', 'pager'] if commands.empty?
14
+ commands.compact.uniq.find {|e| Util.command_exists?(e[/\w+/]) }
15
+ end
16
+ end
17
+
18
+ # Pages with a ruby-only pager which either pages or quits.
19
+ def default_pager(output, options={})
20
+ pager = new(options[:width], options[:height])
21
+ while pager.activated_by?(output, options[:inspect])
22
+ puts pager.slice!(output, options[:inspect])
23
+ return unless continue_paging?
24
+ end
25
+ puts output
26
+ puts "=== Pager finished. ==="
27
+ end
28
+
29
+ #:stopdoc:
30
+ def valid_pager_command?(cmd)
31
+ cmd ? pager_command(cmd) : pager_command
32
+ end
33
+
34
+ private
35
+ def basic_pager(output)
36
+ pager = IO.popen(pager_command, "w")
37
+ begin
38
+ save_stdout = STDOUT.clone
39
+ STDOUT.reopen(pager)
40
+ STDOUT.puts output
41
+ ensure
42
+ STDOUT.reopen(save_stdout)
43
+ save_stdout.close
44
+ pager.close
45
+ end
46
+ end
47
+
48
+ def continue_paging?
49
+ puts "=== Press enter/return to continue or q to quit: ==="
50
+ !$stdin.gets.chomp[/q/i]
51
+ end
52
+ #:startdoc:
53
+ end
54
+
55
+ attr_reader :width, :height
56
+
57
+ def initialize(width, height, options={})
58
+ resize(width, height)
59
+ @pager_command = options[:pager_command] if options[:pager_command]
60
+ end
61
+
62
+ # Pages given string using configured pager.
63
+ def page(string, inspect_mode)
64
+ if self.class.valid_pager_command?(@pager_command)
65
+ self.class.command_pager(string, :pager_command=>@pager_command)
66
+ else
67
+ self.class.default_pager(string, :width=>@width, :height=>@height, :inspect=>inspect_mode)
68
+ end
69
+ end
70
+
71
+ def slice!(output, inspect_mode=false) #:nodoc:
72
+ effective_height = @height - 2 # takes into account pager prompt
73
+ if inspect_mode
74
+ sliced_output = output.slice(0, @width * effective_height)
75
+ output.replace output.slice(@width * effective_height..-1)
76
+ sliced_output
77
+ else
78
+ # could use output.scan(/[^\n]*\n?/) instead of split
79
+ sliced_output = output.split("\n").slice(0, effective_height).join("\n")
80
+ output.replace output.split("\n").slice(effective_height..-1).join("\n")
81
+ sliced_output
82
+ end
83
+ end
84
+
85
+ # Determines if string should be paged based on configured width and height.
86
+ def activated_by?(string_to_page, inspect_mode=false)
87
+ inspect_mode ? (string_to_page.size > @height * @width) : (string_to_page.count("\n") > @height)
88
+ end
89
+
90
+ def resize(width, height) #:nodoc:
91
+ @width, @height = Hirb::View.determine_terminal_size(width, height)
92
+ end
93
+ end
94
+ end
data/lib/hirb/util.rb CHANGED
@@ -1,7 +1,8 @@
1
1
  module Hirb
2
+ # Group of handy utility functions used throughout Hirb.
2
3
  module Util
3
4
  extend self
4
- # Returns a constant like const_get() no matter what namespace it's nested in.
5
+ # Returns a constant like Module#const_get no matter what namespace it's nested in.
5
6
  # Returns nil if the constant is not found.
6
7
  def any_const_get(name)
7
8
  return name if name.is_a?(Module)
@@ -21,9 +22,59 @@ module Hirb
21
22
  hash1.merge(hash2) {|k,o,n| (o.is_a?(Hash)) ? recursive_hash_merge(o,n) : n}
22
23
  end
23
24
 
24
- # from Rails ActiveSupport
25
+ # From Rails ActiveSupport, converting undescored lowercase to camel uppercase.
25
26
  def camelize(string)
26
27
  string.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
27
28
  end
29
+
30
+ # Used by Hirb::Menu to select items from an array. Array counting starts at 1. Ranges of numbers are specified with a '-' or '..'.
31
+ # Multiple ranges can be comma delimited. Anything that isn't a valid number is ignored. All elements can be returned with a '*'.
32
+ # Examples:
33
+ # 1-3,5-6 -> [1,2,3,5,6]
34
+ # * -> all elements in array
35
+ # '' -> []
36
+ def choose_from_array(array, input, options={})
37
+ options = {:splitter=>","}.merge(options)
38
+ return array if input.strip == '*'
39
+ result = []
40
+ input.split(options[:splitter]).each do |e|
41
+ if e =~ /-|\.\./
42
+ min,max = e.split(/-|\.\./)
43
+ slice_min = min.to_i - 1
44
+ result.push(*array.slice(slice_min, max.to_i - min.to_i + 1))
45
+ elsif e =~ /\s*(\d+)\s*/
46
+ index = $1.to_i - 1
47
+ next if index < 0
48
+ result.push(array[index]) if array[index]
49
+ end
50
+ end
51
+ return result
52
+ end
53
+
54
+ # Determines if a shell command exists by searching for it in ENV['PATH'].
55
+ def command_exists?(command)
56
+ ENV['PATH'].split(File::PATH_SEPARATOR).any? {|d| File.exists? File.join(d, command) }
57
+ end
58
+
59
+ # Returns [width, height] of terminal when detected, nil if not detected.
60
+ # Think of this as a simpler version of Highline's Highline::SystemExtensions.terminal_size()
61
+ def detect_terminal_size
62
+ (ENV['COLUMNS'] =~ /^\d+$/) && (ENV['LINES'] =~ /^\d+$/) ? [ENV['COLUMNS'].to_i, ENV['LINES'].to_i] :
63
+ ( command_exists?('stty') ? `stty size`.scan(/\d+/).map { |s| s.to_i }.reverse : nil )
64
+ rescue
65
+ nil
66
+ end
67
+
68
+ # Captures STDOUT of anything run in its block and returns it as string.
69
+ def capture_stdout(&block)
70
+ original_stdout = $stdout
71
+ $stdout = fake = StringIO.new
72
+ begin
73
+ yield
74
+ ensure
75
+ $stdout = original_stdout
76
+ end
77
+ fake.string
78
+ end
28
79
  end
29
80
  end