hirber 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/.gemspec +21 -0
  3. data/.travis.yml +11 -0
  4. data/CHANGELOG.rdoc +165 -0
  5. data/CONTRIBUTING.md +1 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.rdoc +205 -0
  8. data/Rakefile +35 -0
  9. data/lib/bond/completions/hirb.rb +15 -0
  10. data/lib/hirb.rb +84 -0
  11. data/lib/hirb/console.rb +43 -0
  12. data/lib/hirb/dynamic_view.rb +113 -0
  13. data/lib/hirb/formatter.rb +126 -0
  14. data/lib/hirb/helpers.rb +18 -0
  15. data/lib/hirb/helpers/auto_table.rb +24 -0
  16. data/lib/hirb/helpers/markdown_table.rb +14 -0
  17. data/lib/hirb/helpers/object_table.rb +14 -0
  18. data/lib/hirb/helpers/parent_child_tree.rb +24 -0
  19. data/lib/hirb/helpers/tab_table.rb +24 -0
  20. data/lib/hirb/helpers/table.rb +376 -0
  21. data/lib/hirb/helpers/table/filters.rb +10 -0
  22. data/lib/hirb/helpers/table/resizer.rb +82 -0
  23. data/lib/hirb/helpers/tree.rb +181 -0
  24. data/lib/hirb/helpers/unicode_table.rb +15 -0
  25. data/lib/hirb/helpers/vertical_table.rb +37 -0
  26. data/lib/hirb/import_object.rb +10 -0
  27. data/lib/hirb/menu.rb +226 -0
  28. data/lib/hirb/pager.rb +106 -0
  29. data/lib/hirb/string.rb +44 -0
  30. data/lib/hirb/util.rb +96 -0
  31. data/lib/hirb/version.rb +3 -0
  32. data/lib/hirb/view.rb +272 -0
  33. data/lib/hirb/views.rb +8 -0
  34. data/lib/hirb/views/couch_db.rb +11 -0
  35. data/lib/hirb/views/misc_db.rb +15 -0
  36. data/lib/hirb/views/mongo_db.rb +17 -0
  37. data/lib/hirb/views/orm.rb +11 -0
  38. data/lib/hirb/views/rails.rb +19 -0
  39. data/lib/ripl/hirb.rb +15 -0
  40. data/test/auto_table_test.rb +33 -0
  41. data/test/console_test.rb +27 -0
  42. data/test/dynamic_view_test.rb +94 -0
  43. data/test/formatter_test.rb +176 -0
  44. data/test/hirb_test.rb +39 -0
  45. data/test/import_test.rb +9 -0
  46. data/test/menu_test.rb +272 -0
  47. data/test/object_table_test.rb +79 -0
  48. data/test/pager_test.rb +162 -0
  49. data/test/resizer_test.rb +62 -0
  50. data/test/table_test.rb +667 -0
  51. data/test/test_helper.rb +60 -0
  52. data/test/tree_test.rb +184 -0
  53. data/test/util_test.rb +59 -0
  54. data/test/view_test.rb +178 -0
  55. data/test/views_test.rb +22 -0
  56. metadata +164 -0
@@ -0,0 +1,181 @@
1
+ # Base tree class which given an array of nodes produces different types of trees.
2
+ # The types of trees currently are:
3
+ # * basic:
4
+ # 0
5
+ # 1
6
+ # 2
7
+ # 3
8
+ # 4
9
+ #
10
+ # * directory:
11
+ # 0
12
+ # |-- 1
13
+ # | |-- 2
14
+ # | `-- 3
15
+ # `-- 4
16
+ #
17
+ # * number:
18
+ # 1. 0
19
+ # 1. 1
20
+ # 1. 2
21
+ # 2. 3
22
+ # 2. 4
23
+ #
24
+ # Tree nodes can be given as an array of arrays or an array of hashes.
25
+ # To render the above basic tree with an array of hashes:
26
+ # Hirb::Helpers::Tree.render([{:value=>0, :level=>0}, {:value=>1, :level=>1}, {:value=>2, :level=>2},
27
+ # {:value=>3, :level=>2}, {:value=>4, :level=>1}])
28
+ # Note from the hash keys that :level refers to the depth of the tree while :value refers to the text displayed
29
+ # for a node.
30
+ #
31
+ # To render the above basic tree with an array of arrays:
32
+ # Hirb::Helpers::Tree.render([[0,0], [1,1], [2,2], [2,3], [1,4]])
33
+ # Note that the each array pair consists of the level and the value for the node.
34
+ class Hirb::Helpers::Tree
35
+ class ParentlessNodeError < StandardError; end
36
+
37
+ class <<self
38
+ # Main method which renders a tree.
39
+ # ==== Options:
40
+ # [:type] Type of tree. Either :basic, :directory or :number. Default is :basic.
41
+ # [:validate] Boolean to validate tree. Checks to see if all nodes have parents. Raises ParentlessNodeError if
42
+ # an invalid node is found. Default is false.
43
+ # [:indent] Number of spaces to indent between levels for basic + number trees. Default is 4.
44
+ # [:limit] Limits the level or depth of a tree that is displayed. Root node is level 0.
45
+ # [:description] Displays brief description about tree ie how many nodes it has.
46
+ # [:multi_line_nodes] Handles multi-lined nodes by indenting their newlines. Default is false.
47
+ # Examples:
48
+ # Hirb::Helpers::Tree.render([[0, 'root'], [1, 'child']], :type=>:directory)
49
+ def render(nodes, options={})
50
+ new(nodes, options).render
51
+ end
52
+ end
53
+
54
+ # :stopdoc:
55
+ attr_accessor :nodes
56
+
57
+ def initialize(input_nodes, options={})
58
+ @options = options
59
+ @type = options[:type] || :basic
60
+ if input_nodes[0].is_a?(Array)
61
+ @nodes = input_nodes.map {|e| Node.new(:level=>e[0], :value=>e[1]) }
62
+ else
63
+ @nodes = input_nodes.map {|e| Node.new(e)}
64
+ end
65
+ @nodes.each_with_index {|e,i| e.merge!(:tree=>self, :index=>i)}
66
+ @nodes.each {|e| e[:value] = e[:value].to_s }
67
+ validate_nodes if options[:validate]
68
+ self
69
+ end
70
+
71
+ def render
72
+ body = render_tree
73
+ body += render_description if @options[:description]
74
+ body
75
+ end
76
+
77
+ def render_description
78
+ "\n\n#{@nodes.length} #{@nodes.length == 1 ? 'node' : 'nodes'} in tree"
79
+ end
80
+
81
+ def render_tree
82
+ @indent = ' ' * (@options[:indent] || 4 )
83
+ @nodes = @nodes.select {|e| e[:level] <= @options[:limit] } if @options[:limit]
84
+ case @type.to_s
85
+ when 'directory' then render_directory
86
+ when 'number' then render_number
87
+ else render_basic
88
+ end
89
+ end
90
+
91
+ def render_nodes
92
+ value_indent = @options[:multi_line_nodes] ? @indent : nil
93
+ @nodes.map {|e| yield(e) + e.value(value_indent) }.join("\n")
94
+ end
95
+
96
+ def render_directory
97
+ mark_last_nodes_per_level
98
+ render_nodes {|e|
99
+ value = ''
100
+ unless e.root?
101
+ value << e.render_parent_characters
102
+ value << (e[:last_node] ? "`-- " : "|-- ")
103
+ end
104
+ value
105
+ }
106
+ end
107
+
108
+ def render_number
109
+ counter = {}
110
+ @nodes.each {|e|
111
+ parent_level_key = "#{(e.parent ||{})[:index]}.#{e[:level]}"
112
+ counter[parent_level_key] ||= 0
113
+ counter[parent_level_key] += 1
114
+ e[:pre_value] = "#{counter[parent_level_key]}. "
115
+ }
116
+ render_nodes {|e| @indent * e[:level] + e[:pre_value] }
117
+ end
118
+
119
+ def render_basic
120
+ render_nodes {|e| @indent * e[:level] }
121
+ end
122
+
123
+ def validate_nodes
124
+ @nodes.each do |e|
125
+ raise ParentlessNodeError if (e[:level] > e.previous[:level]) && (e[:level] - e.previous[:level]) > 1
126
+ end
127
+ end
128
+
129
+ # walks tree accumulating last nodes per unique parent+level
130
+ def mark_last_nodes_per_level
131
+ @nodes.each {|e| e.delete(:last_node)}
132
+ last_node_hash = @nodes.inject({}) {|h,e|
133
+ h["#{(e.parent ||{})[:index]}.#{e[:level]}"] = e; h
134
+ }
135
+ last_node_hash.values.uniq.each {|e| e[:last_node] = true}
136
+ end
137
+ #:startdoc:
138
+ class Node < ::Hash #:nodoc:
139
+ class MissingLevelError < StandardError; end
140
+ class MissingValueError < StandardError; end
141
+
142
+ def initialize(hash)
143
+ super
144
+ raise MissingLevelError unless hash.has_key?(:level)
145
+ raise MissingValueError unless hash.has_key?(:value)
146
+ replace(hash)
147
+ end
148
+
149
+ def value(indent=nil)
150
+ indent ? self[:value].gsub("\n", "\n#{indent * self[:level]}") : self[:value]
151
+ end
152
+
153
+ def parent
154
+ self[:tree].nodes.slice(0 .. self[:index]).reverse.detect {|e| e[:level] < self[:level]}
155
+ end
156
+
157
+ def next
158
+ self[:tree].nodes[self[:index] + 1]
159
+ end
160
+
161
+ def previous
162
+ self[:tree].nodes[self[:index] - 1]
163
+ end
164
+
165
+ def root?; self[:level] == 0; end
166
+
167
+ # refers to characters which connect parent nodes
168
+ def render_parent_characters
169
+ parent_chars = []
170
+ get_parents_character(parent_chars)
171
+ parent_chars.reverse.map {|level| level + ' ' * 3 }.join('')
172
+ end
173
+
174
+ def get_parents_character(parent_chars)
175
+ if self.parent
176
+ parent_chars << (self.parent[:last_node] ? ' ' : '|') unless self.parent.root?
177
+ self.parent.get_parents_character(parent_chars)
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,15 @@
1
+ # -*- encoding : utf-8 -*-
2
+ class Hirb::Helpers::UnicodeTable < Hirb::Helpers::Table
3
+ CHARS = {
4
+ :top => {:left => '┌', :center => '┬', :right => '┐', :horizontal => '─',
5
+ :vertical => {:outside => '│', :inside => '│'} },
6
+ :middle => {:left => '├', :center => '┼', :right => '┤', :horizontal => '─'},
7
+ :bottom => {:left => '└', :center => '┴', :right => '┘', :horizontal => '─',
8
+ :vertical => {:outside => '│', :inside => '╎'} }
9
+ }
10
+
11
+ # Renders a unicode table
12
+ def self.render(rows, options={})
13
+ new(rows, options).render
14
+ end
15
+ end
@@ -0,0 +1,37 @@
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 the ones below
4
+ # and :max_fields, :vertical and :max_width which aren't used.
5
+ # ==== Options:
6
+ # [:hide_empty] Boolean which hides empty values (nil or '') from being displayed. Default is false.
7
+ def self.render(rows, options={})
8
+ new(rows, {:escape_special_chars=>false, :resize=>false}.merge(options)).render
9
+ end
10
+
11
+ #:stopdoc:
12
+ def setup_field_lengths
13
+ @field_lengths = default_field_lengths
14
+ end
15
+
16
+ def render_header; []; end
17
+ def render_footer; []; end
18
+
19
+ def render_rows
20
+ i = 0
21
+ longest_header = Hirb::String.size @headers.values.sort_by {|e| Hirb::String.size(e) }.last
22
+ stars = "*" * [(longest_header + (longest_header / 2)), 3].max
23
+ @rows.map do |row|
24
+ row = "#{stars} #{i+1}. row #{stars}\n" +
25
+ @fields.map {|f|
26
+ if !@options[:hide_empty] || (@options[:hide_empty] && !row[f].empty?)
27
+ "#{Hirb::String.rjust(@headers[f], longest_header)}: #{row[f]}"
28
+ else
29
+ nil
30
+ end
31
+ }.compact.join("\n")
32
+ i+= 1
33
+ row
34
+ end
35
+ end
36
+ #:startdoc:
37
+ end
@@ -0,0 +1,10 @@
1
+ module Hirb
2
+ module ObjectMethods
3
+ # Takes same options as Hirb::View.render_output.
4
+ def view(*args)
5
+ Hirb::Console.render_output(*(args.unshift(self)))
6
+ end
7
+ end
8
+ end
9
+
10
+ Object.send :include, Hirb::ObjectMethods
@@ -0,0 +1,226 @@
1
+ module Hirb
2
+ # This class provides a menu using Hirb's table helpers by default to display choices.
3
+ # Menu choices (syntax at Hirb::Util.choose_from_array) refer to rows. However, when in
4
+ # two_d mode, choices refer to specific cells by appending a ':field' to a choice.
5
+ # A field name can be an abbreviated. Menus can also have an action mode, which turns the
6
+ # menu prompt into a commandline that executes the choices as arguments and uses methods as
7
+ # actions/commands.
8
+ class Menu
9
+ class Error < StandardError; end
10
+
11
+ # Detects valid choices and optional field/column
12
+ CHOSEN_REGEXP = /^(\d([^:]+)?|\*)(?::)?(\S+)?/
13
+ CHOSEN_ARG = '%s'
14
+ DIRECTIONS = "Specify individual choices (4,7), range of choices (1-3) or all choices (*)."
15
+
16
+
17
+ # This method will return an array unless it's exited by simply pressing return, which returns nil.
18
+ # If given a block, the block will yield if and with any menu items are chosen.
19
+ # All options except for the ones below are passed to render the menu.
20
+ #
21
+ # ==== Options:
22
+ # [*:helper_class*] Helper class to render menu. Helper class is expected to implement numbering given a :number option.
23
+ # To use a very basic menu, set this to false. Defaults to Hirb::Helpers::AutoTable.
24
+ # [*:prompt*] String for menu prompt. Defaults to "Choose: ".
25
+ # [*:ask*] Always ask for input, even if there is only one choice. Default is true.
26
+ # [*:directions*] Display directions before prompt. Default is true.
27
+ # [*:readline*] Use readline to get user input if available. Input strings are added to readline history. Default is false.
28
+ # [*:two_d*] Turn menu into a 2 dimensional (2D) menu by allowing user to pick values from table cells. Default is false.
29
+ # [*:default_field*] Default field for a 2D menu. Defaults to first field in a table.
30
+ # [*:action*] Turn menu into an action menu by letting user pass menu choices as an argument to a method/command.
31
+ # A menu choice's place amongst other arguments is preserved. Default is false.
32
+ # [*:multi_action*] Execute action menu multiple times iterating over the menu choices. Default is false.
33
+ # [*:action_object*] Object that takes method/command calls. Default is main.
34
+ # [*:command*] Default method/command to call when no command given.
35
+ # [*:reopen*] Reopens $stdin with given file or with /dev/tty when set to true. Use when
36
+ # $stdin is already reading in piped data.
37
+ # Examples:
38
+ # >> extend Hirb::Console
39
+ # => self
40
+ # >> menu [1,2,3], :prompt=> "So many choices, so little time: "
41
+ # >> menu [{:a=>1, :b=>2}, {:a=>3, :b=>4}], :fields=>[:a,b], :two_d=>true)
42
+ def self.render(output, options={}, &block)
43
+ new(options).render(output, &block)
44
+ rescue Error=>e
45
+ $stderr.puts "Error: #{e.message}"
46
+ end
47
+
48
+ #:stopdoc:
49
+ def initialize(options={})
50
+ @options = {:helper_class=>Hirb::Helpers::AutoTable, :prompt=>"Choose: ", :ask=>true,
51
+ :directions=>true}.merge options
52
+ @options[:reopen] = '/dev/tty' if @options[:reopen] == true
53
+ end
54
+
55
+ def render(output, &block)
56
+ @output = Array(output)
57
+ return [] if @output.size.zero?
58
+ chosen = choose_from_menu
59
+ block.call(chosen) if block && chosen.size > 0
60
+ @options[:action] ? execute_action(chosen) : chosen
61
+ end
62
+
63
+ def get_input
64
+ prompt = pre_prompt + @options[:prompt]
65
+ prompt = DIRECTIONS+"\n"+prompt if @options[:directions]
66
+ $stdin.reopen @options[:reopen] if @options[:reopen]
67
+
68
+ if @options[:readline] && readline_loads?
69
+ get_readline_input(prompt)
70
+ else
71
+ print prompt
72
+ $stdin.gets.chomp.strip
73
+ end
74
+ end
75
+
76
+ def get_readline_input(prompt)
77
+ input = Readline.readline prompt
78
+ Readline::HISTORY << input
79
+ input
80
+ end
81
+
82
+ def pre_prompt
83
+ prompt = ''
84
+ prompt << "Default field: #{default_field}\n" if @options[:two_d] && default_field
85
+ prompt << "Default command: #{@options[:command]}\n" if @options[:action] && @options[:command]
86
+ prompt
87
+ end
88
+
89
+ def choose_from_menu
90
+ return unasked_choice if @output.size == 1 && !@options[:ask]
91
+
92
+ if (Util.any_const_get(@options[:helper_class]))
93
+ View.render_output(@output, :class=>@options[:helper_class], :options=>@options.merge(:number=>true))
94
+ else
95
+ @output.each_with_index {|e,i| puts "#{i+1}: #{e}" }
96
+ end
97
+
98
+ parse_input get_input
99
+ end
100
+
101
+ def unasked_choice
102
+ return @output unless @options[:action]
103
+ raise(Error, "Default command and field required for unasked action menu") unless default_field && @options[:command]
104
+ @new_args = [@options[:command], CHOSEN_ARG]
105
+ map_tokens([[@output, default_field]])
106
+ end
107
+
108
+ def execute_action(chosen)
109
+ return nil if chosen.size.zero?
110
+ if @options[:multi_action]
111
+ chosen.each {|e| invoke command, add_chosen_to_args(e) }
112
+ else
113
+ invoke command, add_chosen_to_args(chosen)
114
+ end
115
+ end
116
+
117
+ def invoke(cmd, args)
118
+ action_object.send(cmd, *args)
119
+ end
120
+
121
+ def parse_input(input)
122
+ if (@options[:two_d] || @options[:action])
123
+ tokens = input_to_tokens(input)
124
+ map_tokens(tokens)
125
+ else
126
+ Util.choose_from_array(@output, input)
127
+ end
128
+ end
129
+
130
+ def map_tokens(tokens)
131
+ values = if return_cell_values?
132
+ @output[0].is_a?(Hash) ?
133
+ tokens.map {|arr,f| arr.map {|e| e[f]} } :
134
+ tokens.map {|arr,f|
135
+ arr.map {|e| e.is_a?(Array) && f.is_a?(Integer) ? e[f] : e.send(f) }
136
+ }
137
+ else
138
+ tokens.map {|arr, f| arr[0] }
139
+ end
140
+ values.flatten
141
+ end
142
+
143
+ def return_cell_values?
144
+ @options[:two_d]
145
+ end
146
+
147
+ def input_to_tokens(input)
148
+ @new_args = []
149
+ tokens = (@args = split_input_args(input)).map {|word| parse_word(word) }.compact
150
+ cleanup_new_args
151
+ tokens
152
+ end
153
+
154
+ def parse_word(word)
155
+ if word[CHOSEN_REGEXP]
156
+ @new_args << CHOSEN_ARG
157
+ field = $3 ? unalias_field($3) : default_field ||
158
+ raise(Error, "No default field/column found. Fields must be explicitly picked.")
159
+
160
+ token = Util.choose_from_array(@output, word)
161
+ token = [token] if word[/\*|-|\.\.|,/] && !return_cell_values?
162
+ [token, field]
163
+ else
164
+ @new_args << word
165
+ nil
166
+ end
167
+ end
168
+
169
+ def cleanup_new_args
170
+ if @new_args.all? {|e| e == CHOSEN_ARG }
171
+ @new_args = [CHOSEN_ARG]
172
+ else
173
+ i = @new_args.index(CHOSEN_ARG) || raise(Error, "No rows chosen")
174
+ @new_args.delete(CHOSEN_ARG)
175
+ @new_args.insert(i, CHOSEN_ARG)
176
+ end
177
+ end
178
+
179
+ def add_chosen_to_args(items)
180
+ args = @new_args.dup
181
+ args[args.index(CHOSEN_ARG)] = items
182
+ args
183
+ end
184
+
185
+ def command
186
+ @command ||= begin
187
+ cmd = (@new_args == [CHOSEN_ARG]) ? nil : @new_args.shift
188
+ cmd ||= @options[:command] || raise(Error, "No command given for action menu")
189
+ end
190
+ end
191
+
192
+ def action_object
193
+ @options[:action_object] || eval("self", TOPLEVEL_BINDING)
194
+ end
195
+
196
+ def split_input_args(input)
197
+ input.split(/\s+/)
198
+ end
199
+
200
+ def default_field
201
+ @default_field ||= @options[:default_field] || fields[0]
202
+ end
203
+
204
+ # Has to be called after displaying menu
205
+ def fields
206
+ @fields ||= @options[:fields] || (@options[:ask] && table_helper_class? && Helpers::Table.last_table ?
207
+ Helpers::Table.last_table.fields[1..-1] : [])
208
+ end
209
+
210
+ def table_helper_class?
211
+ @options[:helper_class].is_a?(Class) && @options[:helper_class] < Helpers::Table
212
+ end
213
+
214
+ def unalias_field(field)
215
+ fields.sort_by {|e| e.to_s }.find {|e| e.to_s[/^#{field}/] } || raise(Error, "Invalid field '#{field}'")
216
+ end
217
+
218
+ def readline_loads?
219
+ require 'readline'
220
+ true
221
+ rescue LoadError
222
+ false
223
+ end
224
+ #:startdoc:
225
+ end
226
+ end