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,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