hirb 0.1.2 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
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