hirber 0.8.0

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 (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,106 @@
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
+ env_pager = ENV['PAGER'] ? File.basename(ENV['PAGER']) : nil
14
+ commands = [env_pager, 'less', 'more', 'pager'] if commands.empty?
15
+ commands.compact.uniq.find {|e| Util.command_exists?(e[/\w+/]) }
16
+ end
17
+ end
18
+
19
+ # Pages with a ruby-only pager which either pages or quits.
20
+ def default_pager(output, options={})
21
+ pager = new(options[:width], options[:height])
22
+ while pager.activated_by?(output, options[:inspect])
23
+ puts pager.slice!(output, options[:inspect])
24
+ return unless continue_paging?
25
+ end
26
+ puts output
27
+ puts "=== Pager finished. ==="
28
+ end
29
+
30
+ #:stopdoc:
31
+ def valid_pager_command?(cmd)
32
+ cmd ? pager_command(cmd) : pager_command
33
+ end
34
+
35
+ private
36
+ def basic_pager(output)
37
+ pager = IO.popen(pager_command, "w")
38
+ begin
39
+ save_stdout = STDOUT.clone
40
+ STDOUT.reopen(pager)
41
+ STDOUT.puts output
42
+ rescue Errno::EPIPE
43
+ ensure
44
+ STDOUT.reopen(save_stdout)
45
+ save_stdout.close
46
+ pager.close
47
+ end
48
+ end
49
+
50
+ def continue_paging?
51
+ puts "=== Press enter/return to continue or q to quit: ==="
52
+ !$stdin.gets.chomp[/q/i]
53
+ end
54
+ #:startdoc:
55
+ end
56
+
57
+ attr_reader :width, :height
58
+
59
+ def initialize(width, height, options={})
60
+ resize(width, height)
61
+ @pager_command = options[:pager_command] if options[:pager_command]
62
+ end
63
+
64
+ # Pages given string using configured pager.
65
+ def page(string, inspect_mode)
66
+ if self.class.valid_pager_command?(@pager_command)
67
+ self.class.command_pager(string, :pager_command=>@pager_command)
68
+ else
69
+ self.class.default_pager(string, :width=>@width, :height=>@height, :inspect=>inspect_mode)
70
+ end
71
+ end
72
+
73
+ def slice!(output, inspect_mode=false) #:nodoc:
74
+ effective_height = @height - 2 # takes into account pager prompt
75
+ if inspect_mode
76
+ sliced_output = String.slice(output, 0, @width * effective_height)
77
+ output.replace String.slice(output, char_count(sliced_output), String.size(output))
78
+ sliced_output
79
+ else
80
+ # could use output.scan(/[^\n]*\n?/) instead of split
81
+ sliced_output = output.split("\n").slice(0, effective_height).join("\n")
82
+ output.replace output.split("\n").slice(effective_height..-1).join("\n")
83
+ sliced_output
84
+ end
85
+ end
86
+
87
+ # Determines if string should be paged based on configured width and height.
88
+ def activated_by?(string_to_page, inspect_mode=false)
89
+ inspect_mode ? (String.size(string_to_page) > @height * @width) : (string_to_page.count("\n") > @height)
90
+ end
91
+
92
+ if String.method_defined? :chars
93
+ def char_count(string) #:nodoc:
94
+ string.chars.count
95
+ end
96
+ else
97
+ def char_count(string) #:nodoc:
98
+ String.size(string)
99
+ end
100
+ end
101
+
102
+ def resize(width, height) #:nodoc:
103
+ @width, @height = View.determine_terminal_size(width, height)
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,44 @@
1
+ module Hirb
2
+ # Provides string helpers to deal with UTF-8 and ruby 1.8.x
3
+ module String
4
+ extend self
5
+ # :stopdoc:
6
+ if RUBY_VERSION < '1.9'
7
+ def size(string)
8
+ string.scan(/./).length
9
+ end
10
+
11
+ def ljust(string, desired_length)
12
+ leftover = desired_length - size(string)
13
+ leftover > 0 ? string + " " * leftover : string
14
+ end
15
+
16
+ def rjust(string, desired_length)
17
+ leftover = desired_length - size(string)
18
+ leftover > 0 ? " " * leftover + string : string
19
+ end
20
+
21
+ def slice(string, start, finish)
22
+ string.scan(/./).slice(start, finish).join('')
23
+ end
24
+ else
25
+ def size(string)
26
+ string.length
27
+ end
28
+
29
+ def ljust(string, desired_length)
30
+ string.ljust(desired_length)
31
+ end
32
+
33
+ def rjust(string, desired_length)
34
+ string.rjust(desired_length)
35
+ end
36
+
37
+ def slice(*args)
38
+ string = args.shift
39
+ string.slice(*args)
40
+ end
41
+ end
42
+ #:startdoc:
43
+ end
44
+ end
@@ -0,0 +1,96 @@
1
+ module Hirb
2
+ # Group of handy utility functions used throughout Hirb.
3
+ module Util
4
+ extend self
5
+ # Returns a constant like Module#const_get no matter what namespace it's nested in.
6
+ # Returns nil if the constant is not found.
7
+ def any_const_get(name)
8
+ return name if name.is_a?(Module)
9
+ begin
10
+ klass = Object
11
+ name.split('::').each {|e|
12
+ klass = klass.const_get(e)
13
+ }
14
+ klass
15
+ rescue
16
+ nil
17
+ end
18
+ end
19
+
20
+ # Recursively merge hash1 with hash2.
21
+ def recursive_hash_merge(hash1, hash2)
22
+ hash1.merge(hash2) {|k,o,n| (o.is_a?(Hash)) ? recursive_hash_merge(o,n) : n}
23
+ end
24
+
25
+ # From Rails ActiveSupport, converting undescored lowercase to camel uppercase.
26
+ def camelize(string)
27
+ string.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
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[/^\s*\*/]
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
+ 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.exist? 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
+ if (ENV['COLUMNS'] =~ /^\d+$/) && (ENV['LINES'] =~ /^\d+$/)
63
+ [ENV['COLUMNS'].to_i, ENV['LINES'].to_i]
64
+ elsif (RUBY_PLATFORM =~ /java/ || (!STDIN.tty? && ENV['TERM'])) && command_exists?('tput')
65
+ [`tput cols`.to_i, `tput lines`.to_i]
66
+ elsif STDIN.tty? && command_exists?('stty')
67
+ `stty size`.scan(/\d+/).map { |s| s.to_i }.reverse
68
+ else
69
+ nil
70
+ end
71
+ rescue
72
+ nil
73
+ end
74
+
75
+ # Captures STDOUT of anything run in its block and returns it as string.
76
+ def capture_stdout(&block)
77
+ original_stdout = $stdout
78
+ $stdout = fake = StringIO.new
79
+ begin
80
+ yield
81
+ ensure
82
+ $stdout = original_stdout
83
+ end
84
+ fake.string
85
+ end
86
+
87
+ # From Rubygems, determine a user's home.
88
+ def find_home
89
+ ['HOME', 'USERPROFILE'].each {|e| return ENV[e] if ENV[e] }
90
+ return "#{ENV['HOMEDRIVE']}#{ENV['HOMEPATH']}" if ENV['HOMEDRIVE'] && ENV['HOMEPATH']
91
+ File.expand_path("~")
92
+ rescue
93
+ File::ALT_SEPARATOR ? "C:/" : "/"
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,3 @@
1
+ module Hirb
2
+ VERSION = "0.8.0"
3
+ end
@@ -0,0 +1,272 @@
1
+ module Hirb
2
+ # This class is responsible for managing all view-related functionality.
3
+ #
4
+ # == Create a View
5
+ # Let's create a simple view for Hash objects:
6
+ # $ irb -rubygems
7
+ # >> require 'hirb'
8
+ # =>true
9
+ # >> Hirb.enable
10
+ # =>nil
11
+ # >> require 'yaml'
12
+ # =>true
13
+ #
14
+ # # A view method is the smallest view
15
+ # >> def yaml(output); output.to_yaml; end
16
+ # => nil
17
+ # # Add the view
18
+ # >> Hirb.add_view Hash, :method=>:yaml
19
+ # => true
20
+ #
21
+ # # Hashes now appear as yaml
22
+ # >> {:a=>1, :b=>{:c=>3}}
23
+ # ---
24
+ # :a : 1
25
+ # :b :
26
+ # :c : 3
27
+ # => true
28
+ #
29
+ # Another way of creating a view is a Helper class:
30
+ #
31
+ # # Create yaml view class
32
+ # >> class Hirb::Helpers::Yaml; def self.render(output, options={}); output.to_yaml; end ;end
33
+ # =>nil
34
+ # # Add the view
35
+ # >> Hirb.add_view Hash, :class=>Hirb::Helpers::Yaml
36
+ # =>true
37
+ #
38
+ # # Hashes appear as yaml like above ...
39
+ #
40
+ # == Configure a View
41
+ # To configure the above Helper class as a view, either pass Hirb.enable a hash:
42
+ # # In .irbrc
43
+ # require 'hirb'
44
+ # # View class needs to come before enable()
45
+ # class Hirb::Helpers::Yaml; def self.render(output, options={}); output.to_yaml; end ;end
46
+ # Hirb.enable :output=>{"Hash"=>{:class=>"Hirb::Helpers::Yaml"}}
47
+ #
48
+ # Or create a config file at config/hirb.yml or ~/.hirb.yml:
49
+ # # The config file for the yaml example would look like:
50
+ # # ---
51
+ # # :output :
52
+ # # Hash :
53
+ # # :class : Hirb::Helpers::Yaml
54
+ #
55
+ # # In .irbrc
56
+ # require 'hirb'
57
+ # # View class needs to come before enable()
58
+ # class Hirb::Helpers::Yaml; def self.render(output, options={}); output.to_yaml; end ;end
59
+ # Hirb.enable
60
+ #
61
+ # For more about configuring Hirb, see the Config Files section in Hirb.
62
+ module View
63
+ DEFAULT_WIDTH = 120
64
+ DEFAULT_HEIGHT = 40
65
+ class<<self
66
+ attr_accessor :render_method
67
+ attr_reader :config
68
+
69
+ # This activates view functionality i.e. the formatter, pager and size detection. If irb exists, it overrides irb's output
70
+ # method with Hirb::View.view_output. When called multiple times, new configs are merged into the existing config.
71
+ # If using Wirble, you should call this after it. The view configuration can be specified in a hash via a config file,
72
+ # or as options to this method. In addition to the config keys mentioned in Hirb, options also take the following keys:
73
+ # ==== Options:
74
+ # * config_file: Name of config file(s) that are merged into existing config
75
+ # Examples:
76
+ # Hirb.enable
77
+ # Hirb.enable :formatter=>false
78
+ def enable(options={})
79
+ Array(options.delete(:config_file)).each {|e|
80
+ @new_config_file = true
81
+ Hirb.config_files << e
82
+ }
83
+ enable_output_method unless @output_method
84
+ merge_or_load_config options
85
+ resize(config[:width], config[:height])
86
+ @enabled = true
87
+ end
88
+
89
+ # Indicates if Hirb::View is enabled.
90
+ def enabled?
91
+ @enabled || false
92
+ end
93
+
94
+ # Disable's Hirb's output and revert's irb's output method if irb exists.
95
+ def disable
96
+ @enabled = false
97
+ disable_output_method if @output_method
98
+ false
99
+ end
100
+
101
+ # Toggles pager on or off. The pager only works while Hirb::View is enabled.
102
+ def toggle_pager
103
+ config[:pager] = !config[:pager]
104
+ end
105
+
106
+ # Toggles formatter on or off.
107
+ def toggle_formatter
108
+ config[:formatter] = !config[:formatter]
109
+ end
110
+
111
+ # Resizes the console width and height for use with the table and pager i.e. after having resized the console window. *nix users
112
+ # should only have to call this method. Non-*nix users should call this method with explicit width and height. If you don't know
113
+ # your width and height, in irb play with "a"* width to find width and puts "a\n" * height to find height.
114
+ def resize(width=nil, height=nil)
115
+ config[:width], config[:height] = determine_terminal_size(width, height)
116
+ pager.resize(config[:width], config[:height])
117
+ end
118
+
119
+ # This is the main method of this class. When view is enabled, this method searches for a formatter it can use for the output and if
120
+ # successful renders it using render_method(). The options this method takes are helper config hashes as described in
121
+ # Hirb::Formatter.format_output(). Returns true if successful and false if no formatting is done or if not enabled.
122
+ def view_output(output, options={})
123
+ enabled? && config[:formatter] && render_output(output, options)
124
+ rescue Exception=>e
125
+ if config[:ignore_errors]
126
+ $stderr.puts "Hirb Error: #{e.message}"
127
+ false
128
+ else
129
+ index = (obj = e.backtrace.find {|f| f =~ /^\(eval\)/}) ? e.backtrace.index(obj) : e.backtrace.length
130
+ $stderr.puts "Hirb Error: #{e.message}", e.backtrace.slice(0,index).map {|_e| " " + _e }
131
+ true
132
+ end
133
+ end
134
+
135
+ # Captures STDOUT and renders it using render_method(). The main use case is to conditionally page captured stdout.
136
+ def capture_and_render(&block)
137
+ render_method.call Util.capture_stdout(&block)
138
+ end
139
+
140
+ # A lambda or proc which handles the final formatted object.
141
+ # Although this pages/puts the object by default, it could be set to do other things
142
+ # i.e. write the formatted object to a file.
143
+ undef :render_method
144
+ def render_method
145
+ @render_method ||= default_render_method
146
+ end
147
+
148
+ # Resets render_method back to its default.
149
+ def reset_render_method
150
+ @render_method = default_render_method
151
+ end
152
+
153
+ # Current console width
154
+ def width
155
+ config && config[:width] ? config[:width] : DEFAULT_WIDTH
156
+ end
157
+
158
+ # Current console height
159
+ def height
160
+ config && config[:height] ? config[:height] : DEFAULT_HEIGHT
161
+ end
162
+
163
+ # Current formatter config, storing a hash of all static views
164
+ def formatter_config
165
+ formatter.config
166
+ end
167
+
168
+ # Adds a view when View is enabled. See Formatter.add_view for more details.
169
+ def add(klass, view_config)
170
+ if enabled?
171
+ formatter.add_view(klass, view_config)
172
+ else
173
+ puts "View must be enabled to add a view"
174
+ end
175
+ end
176
+
177
+ #:stopdoc:
178
+ def enable_output_method
179
+ if defined?(Ripl) && Ripl.respond_to?(:started?) && Ripl.started?
180
+ @output_method = true
181
+ require 'ripl/hirb' unless defined? Ripl::Hirb
182
+ elsif defined? IRB::Irb
183
+ @output_method = true
184
+ ::IRB::Irb.class_eval do
185
+ alias_method :non_hirb_view_output, :output_value
186
+ def output_value #:nodoc:
187
+ Hirb::View.view_or_page_output(@context.last_value) || non_hirb_view_output
188
+ end
189
+ end
190
+ end
191
+ end
192
+
193
+ def disable_output_method
194
+ if defined?(IRB::Irb) && !defined? Ripl
195
+ ::IRB::Irb.send :alias_method, :output_value, :non_hirb_view_output
196
+ end
197
+ @output_method = nil
198
+ end
199
+
200
+ def view_or_page_output(str)
201
+ view_output(str) || page_output(str.inspect, true)
202
+ end
203
+
204
+ def render_output(output, options={})
205
+ if (formatted_output = formatter.format_output(output, options))
206
+ render_method.call(formatted_output)
207
+ true
208
+ else
209
+ false
210
+ end
211
+ end
212
+
213
+ def determine_terminal_size(width, height)
214
+ detected = (width.nil? || height.nil?) ? Util.detect_terminal_size || [] : []
215
+ [width || detected[0] || DEFAULT_WIDTH , height || detected[1] || DEFAULT_HEIGHT]
216
+ end
217
+
218
+ def page_output(output, inspect_mode=false)
219
+ if enabled? && config[:pager] && pager.activated_by?(output, inspect_mode)
220
+ pager.page(output, inspect_mode)
221
+ true
222
+ else
223
+ false
224
+ end
225
+ end
226
+
227
+ def pager
228
+ @pager ||= Pager.new(config[:width], config[:height], :pager_command=>config[:pager_command])
229
+ end
230
+
231
+ def pager=(value); @pager = value; end
232
+
233
+ def formatter(reload=false)
234
+ @formatter = reload || @formatter.nil? ? Formatter.new(config[:output]) : @formatter
235
+ end
236
+
237
+ def formatter=(value); @formatter = value; end
238
+
239
+ def merge_or_load_config(additional_config={})
240
+ if @config && (@new_config_file || !additional_config.empty?)
241
+ Hirb.config = nil
242
+ load_config Util.recursive_hash_merge(@config, additional_config)
243
+ @new_config_file = false
244
+ elsif !@enabled
245
+ load_config(additional_config)
246
+ end
247
+ end
248
+
249
+ def load_config(additional_config={})
250
+ @config = Util.recursive_hash_merge default_config, additional_config
251
+ formatter(true)
252
+ true
253
+ end
254
+
255
+ def config_loaded?; !!@config; end
256
+
257
+ undef :config
258
+ def config
259
+ @config
260
+ end
261
+
262
+ def default_render_method
263
+ lambda {|output| page_output(output) || puts(output) }
264
+ end
265
+
266
+ def default_config
267
+ Util.recursive_hash_merge({:pager=>true, :formatter=>true}, Hirb.config || {})
268
+ end
269
+ #:startdoc:
270
+ end
271
+ end
272
+ end