├── lib ├── hirb │ ├── version.rb │ ├── views.rb │ ├── import_object.rb │ ├── views │ │ ├── orm.rb │ │ ├── couch_db.rb │ │ ├── misc_db.rb │ │ ├── mongo_db.rb │ │ └── rails.rb │ ├── helpers │ │ ├── table │ │ │ ├── filters.rb │ │ │ └── resizer.rb │ │ ├── tab_table.rb │ │ ├── markdown_table.rb │ │ ├── unicode_table.rb │ │ ├── object_table.rb │ │ ├── auto_table.rb │ │ ├── parent_child_tree.rb │ │ ├── vertical_table.rb │ │ ├── tree.rb │ │ └── table.rb │ ├── helpers.rb │ ├── string.rb │ ├── console.rb │ ├── util.rb │ ├── pager.rb │ ├── dynamic_view.rb │ ├── formatter.rb │ ├── menu.rb │ └── view.rb ├── ripl │ └── hirb.rb ├── bond │ └── completions │ │ └── hirb.rb └── hirb.rb ├── CONTRIBUTING.md ├── .travis.yml ├── test ├── import_test.rb ├── console_test.rb ├── auto_table_test.rb ├── views_test.rb ├── test_helper.rb ├── hirb_test.rb ├── object_table_test.rb ├── util_test.rb ├── resizer_test.rb ├── dynamic_view_test.rb ├── tree_test.rb ├── view_test.rb ├── pager_test.rb ├── formatter_test.rb ├── menu_test.rb └── table_test.rb ├── Rakefile ├── LICENSE.txt ├── .gemspec ├── CHANGELOG.rdoc └── README.rdoc /lib/hirb/version.rb: -------------------------------------------------------------------------------- 1 | module Hirb 2 | VERSION = '0.7.3' 3 | end 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Thanks for trying out this project! [See here for contribution guidelines.](http://tagaholic.me/contributing.html) 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | before_install: bundle init --gemspec=.gemspec 2 | script: bacon -q -Ilib -I. test/*_test.rb 3 | rvm: 4 | - 1.8.7 5 | - 1.9.2 6 | - 1.9.3 7 | - 2.0 8 | - 2.1 9 | - 2.2 10 | - jruby 11 | - rbx 12 | -------------------------------------------------------------------------------- /lib/hirb/views.rb: -------------------------------------------------------------------------------- 1 | module Hirb 2 | # Namespace for Helpers defining multiple views in a module i.e. via DynamicView. 3 | module Views 4 | module Single #:nodoc: 5 | end 6 | end 7 | end 8 | %w{rails orm mongo_db couch_db misc_db}.each {|e| require "hirb/views/#{e}" } -------------------------------------------------------------------------------- /lib/hirb/import_object.rb: -------------------------------------------------------------------------------- 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 11 | -------------------------------------------------------------------------------- /lib/hirb/views/orm.rb: -------------------------------------------------------------------------------- 1 | module Hirb::Views::ORM #:nodoc: 2 | def data_mapper__resource_view(obj) 3 | {:fields=>obj.class.properties.map {|e| e.name }} 4 | end 5 | 6 | def sequel__model_view(obj) 7 | {:fields=>obj.class.columns} 8 | end 9 | end 10 | 11 | Hirb::DynamicView.add Hirb::Views::ORM, :helper=>:auto_table -------------------------------------------------------------------------------- /lib/ripl/hirb.rb: -------------------------------------------------------------------------------- 1 | require 'hirb' 2 | 3 | module Ripl::Hirb 4 | def before_loop 5 | super 6 | Hirb.enable(Ripl.config[:hirb] || {}) 7 | end 8 | 9 | def format_result(result) 10 | return super if !Hirb::View.enabled? 11 | Hirb::View.view_or_page_output(result) || super 12 | end 13 | end 14 | 15 | Ripl::Shell.include Ripl::Hirb 16 | -------------------------------------------------------------------------------- /lib/hirb/helpers/table/filters.rb: -------------------------------------------------------------------------------- 1 | class Hirb::Helpers::Table 2 | # Contains filter methods used by :filters option. To define a custom filter, simply open this module and create a method 3 | # that take one argument, the value you will be filtering. 4 | module Filters 5 | extend self 6 | def comma_join(arr) #:nodoc: 7 | arr.join(', ') 8 | end 9 | end 10 | end -------------------------------------------------------------------------------- /test/import_test.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'test_helper') 2 | 3 | describe "import" do 4 | it "require import_object extends Object" do 5 | Object.ancestors.map {|e| e.to_s}.include?("Hirb::ObjectMethods").should == false 6 | require 'hirb/import_object' 7 | Object.ancestors.map {|e| e.to_s}.include?("Hirb::ObjectMethods").should == true 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/hirb/views/couch_db.rb: -------------------------------------------------------------------------------- 1 | module Hirb::Views::CouchDb #:nodoc: 2 | def default_couch(obj) 3 | {:fields=>([:_id] + obj.class.properties.map {|e| e.name }) } 4 | end 5 | 6 | alias_method :couch_rest__extended_document_view, :default_couch 7 | alias_method :couch_foo__base_view, :default_couch 8 | alias_method :couch_potato__persistence_view, :default_couch 9 | end 10 | 11 | Hirb::DynamicView.add Hirb::Views::CouchDb, :helper=>:auto_table -------------------------------------------------------------------------------- /lib/hirb/views/misc_db.rb: -------------------------------------------------------------------------------- 1 | module Hirb::Views::MiscDb #:nodoc: 2 | def friendly__document_view(obj) 3 | {:fields=>obj.class.attributes.keys - [:id]} 4 | end 5 | 6 | def ripple__document_view(obj) 7 | {:fields=>obj.class.properties.keys} 8 | end 9 | 10 | def d_b_i__row_view(obj) 11 | {:fields=>obj.column_names, :table_class=>Hirb::Helpers::Table} 12 | end 13 | end 14 | 15 | Hirb::DynamicView.add Hirb::Views::MiscDb, :helper=>:auto_table -------------------------------------------------------------------------------- /lib/hirb/views/mongo_db.rb: -------------------------------------------------------------------------------- 1 | module Hirb::Views::MongoDb #:nodoc: 2 | def mongoid__document_view(obj) 3 | fields = obj.class.fields.keys 4 | fields.delete('_id') 5 | fields.unshift('_id') 6 | {:fields=>fields} 7 | end 8 | 9 | def mongo_mapper__document_view(obj) 10 | fields = obj.class.column_names 11 | fields.delete('_id') && fields.unshift('_id') 12 | {:fields=>fields} 13 | end 14 | alias_method :mongo_mapper__embedded_document_view, :mongo_mapper__document_view 15 | end 16 | 17 | Hirb::DynamicView.add Hirb::Views::MongoDb, :helper=>:auto_table 18 | -------------------------------------------------------------------------------- /lib/hirb/helpers/tab_table.rb: -------------------------------------------------------------------------------- 1 | class Hirb::Helpers::TabTable < Hirb::Helpers::Table 2 | DELIM = "\t" 3 | 4 | # Renders a tab-delimited table 5 | def self.render(rows, options={}) 6 | new(rows, {:description => false}.merge(options)).render 7 | end 8 | 9 | def render_header 10 | @headers ? render_table_header : [] 11 | end 12 | 13 | def render_table_header 14 | [ format_values(@headers).join(DELIM) ] 15 | end 16 | 17 | def render_rows 18 | @rows.map { |row| format_values(row).join(DELIM) } 19 | end 20 | 21 | def render_footer 22 | [] 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/hirb/helpers.rb: -------------------------------------------------------------------------------- 1 | module Hirb 2 | module Helpers #:nodoc: 3 | @helper_classes ||= {} 4 | def self.helper_class(klass) 5 | @helper_classes[klass.to_s] ||= begin 6 | if (helper_class = constants.find {|e| e.to_s == Util.camelize(klass.to_s)}) 7 | klass = "Hirb::Helpers::#{helper_class}" 8 | end 9 | Util.any_const_get(klass) 10 | end 11 | end 12 | end 13 | end 14 | 15 | %w{table object_table auto_table tree parent_child_tree vertical_table 16 | markdown_table unicode_table tab_table}.each do |e| 17 | require "hirb/helpers/#{e}" 18 | end 19 | -------------------------------------------------------------------------------- /lib/hirb/helpers/markdown_table.rb: -------------------------------------------------------------------------------- 1 | # Creates a markdown table for github 2 | class Hirb::Helpers::MarkdownTable < 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 | def self.render(rows, options={}) 12 | new(rows, options).render 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/hirb/helpers/unicode_table.rb: -------------------------------------------------------------------------------- 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 16 | -------------------------------------------------------------------------------- /lib/hirb/helpers/object_table.rb: -------------------------------------------------------------------------------- 1 | class Hirb::Helpers::ObjectTable < Hirb::Helpers::Table 2 | # Rows are any ruby objects. Takes same options as Hirb::Helpers::Table.render except as noted below. 3 | # 4 | # ==== Options: 5 | # [:fields] Methods of the object to represent as columns. Defaults to [:to_s]. 6 | def self.render(rows, options ={}) 7 | options[:fields] ||= [:to_s] 8 | options[:headers] ||= {:to_s=>'value'} if options[:fields] == [:to_s] 9 | item_hashes = options[:fields].empty? ? [] : Array(rows).inject([]) {|t,item| 10 | t << options[:fields].inject({}) {|h,f| h[f] = item.__send__(f); h} 11 | } 12 | super(item_hashes, options) 13 | end 14 | end -------------------------------------------------------------------------------- /test/console_test.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'test_helper') 2 | 3 | describe "Console" do 4 | it "#table is called without Hirb enabled" do 5 | extend Hirb::Console 6 | reset_config 7 | expected_table = <<-TABLE.unindent 8 | +-------+ 9 | | value | 10 | +-------+ 11 | | 5 | 12 | | 3 | 13 | +-------+ 14 | 2 rows in set 15 | TABLE 16 | capture_stdout { 17 | table([5,3], :fields=>[:to_s]) 18 | }.should == expected_table +"\n" 19 | end 20 | 21 | it ".render_output sets config if it wasn't before" do 22 | reset_config 23 | View.expects(:render_output) 24 | Console.render_output('blah') 25 | View.config.is_a?(Hash).should == true 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/hirb/views/rails.rb: -------------------------------------------------------------------------------- 1 | module Hirb::Views::Rails #:nodoc: 2 | def active_record__base_view(obj) 3 | {:fields=>get_active_record_fields(obj)} 4 | end 5 | 6 | def get_active_record_fields(obj) 7 | fields = obj.class.column_names.map {|e| e.to_sym } 8 | # if query used select 9 | if obj.attributes.keys.compact.sort != obj.class.column_names.sort 10 | selected_columns = obj.attributes.keys.compact 11 | sorted_columns = obj.class.column_names.dup.delete_if {|e| !selected_columns.include?(e) } 12 | sorted_columns += (selected_columns - sorted_columns) 13 | fields = sorted_columns.map {|e| e.to_sym} 14 | end 15 | fields 16 | end 17 | end 18 | 19 | Hirb::DynamicView.add Hirb::Views::Rails, :helper=>:auto_table 20 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'fileutils' 3 | 4 | def gemspec 5 | @gemspec ||= eval(File.read('.gemspec'), binding, '.gemspec') 6 | end 7 | 8 | desc "Build the gem" 9 | task :gem=>:gemspec do 10 | sh "gem build .gemspec" 11 | FileUtils.mkdir_p 'pkg' 12 | FileUtils.mv "#{gemspec.name}-#{gemspec.version}.gem", 'pkg' 13 | end 14 | 15 | desc "Install the gem locally" 16 | task :install => :gem do 17 | sh %{gem install pkg/#{gemspec.name}-#{gemspec.version}} 18 | end 19 | 20 | desc "Generate the gemspec" 21 | task :generate do 22 | puts gemspec.to_ruby 23 | end 24 | 25 | desc "Validate the gemspec" 26 | task :gemspec do 27 | gemspec.validate 28 | end 29 | 30 | desc 'Run tests' 31 | task :test do |t| 32 | sh 'bacon -q -Ilib -I. test/*_test.rb' 33 | end 34 | 35 | task :default => :test 36 | -------------------------------------------------------------------------------- /lib/bond/completions/hirb.rb: -------------------------------------------------------------------------------- 1 | complete(:methods=>%w{Hirb::View.enable Hirb.enable}) { 2 | %w{config_file output_method output width height formatter pager pager_command} 3 | } 4 | complete(:methods=>%w{Hirb::Helpers::Table.render table}) { 5 | %w{fields headers max_fields max_width resize number change_fields}+ 6 | %w{filters header_filter filter_any filter_classes vertical all_fields}+ 7 | %w{description escape_special_chars table_class hide_empty unicode grep_fields} 8 | } 9 | complete(:method=>"Hirb::Helpers::Tree.render") { 10 | %w{type validate indent limit description multi_line_nodes value_method children_method} 11 | } 12 | complete(:methods=>%w{Hirb::Menu.render menu}) { 13 | %w{helper_class prompt ask directions readline two_d default_field action multi_action} + 14 | %w{action_object command reopen} 15 | } 16 | -------------------------------------------------------------------------------- /test/auto_table_test.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'test_helper') 2 | 3 | describe "auto table" do 4 | it "converts nonarrays to arrays and renders" do 5 | require 'set' 6 | # rubinius sorts Set#to_a differently 7 | arr = RUBY_DESCRIPTION.include?('rubinius') ? Set.new([1,2,3]).to_a : [1,2,3] 8 | 9 | expected_table = <<-TABLE.unindent 10 | +-------+ 11 | | value | 12 | +-------+ 13 | | #{arr[0]} | 14 | | #{arr[1]} | 15 | | #{arr[2]} | 16 | +-------+ 17 | 3 rows in set 18 | TABLE 19 | Helpers::AutoTable.render(::Set.new([1,2,3])).should == expected_table 20 | end 21 | 22 | it "renders hash" do 23 | expected_table = <<-TABLE.unindent 24 | +---+-------+ 25 | | 0 | 1 | 26 | +---+-------+ 27 | | a | 12345 | 28 | +---+-------+ 29 | 1 row in set 30 | TABLE 31 | Helpers::AutoTable.render({:a=>12345}).should == expected_table 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/views_test.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'test_helper') 2 | 3 | describe "activerecord table" do 4 | it "with no select gets default options" do 5 | pet = stub(:name=>'rufus', :age=>7, :attributes=>{"name"=>'rufus', 'age'=>7}, :class=>stub(:column_names=>%w{age name})) 6 | Helpers::AutoTable.active_record__base_view(pet).should == {:fields=>[:age, :name]} 7 | end 8 | 9 | it "with select gets default options" do 10 | pet = stub(:name=>'rufus', :age=>7, :attributes=>{'name'=>'rufus'}, :class=>stub(:column_names=>%w{age name})) 11 | Helpers::AutoTable.active_record__base_view(pet).should == {:fields=>[:name]} 12 | end 13 | end 14 | 15 | describe "mongoid table" do 16 | it "only has one _id" do 17 | fields = {'_id' => 'x0f0x', 'name' => 'blah'} 18 | mongoid_stub = stub(:class => stub(:fields => fields)) 19 | Helpers::AutoTable.mongoid__document_view(mongoid_stub).should == 20 | {:fields => fields.keys.sort} 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT LICENSE 2 | 3 | Copyright (c) 2010 Gabriel Horner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /lib/hirb/string.rb: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /lib/hirb/helpers/auto_table.rb: -------------------------------------------------------------------------------- 1 | # This helper wraps around the other table helpers i.e. Hirb::Helpers::Table while 2 | # providing default helper options via Hirb::DynamicView. Using these default options, this 3 | # helper supports views for the following modules/classes: 4 | # ActiveRecord::Base, CouchFoo::Base, CouchPotato::Persistence, CouchRest::ExtendedDocument, 5 | # DBI::Row, DataMapper::Resource, Friendly::Document, MongoMapper::Document, MongoMapper::EmbeddedDocument, 6 | # Mongoid::Document, Ripple::Document, Sequel::Model. 7 | class Hirb::Helpers::AutoTable < Hirb::Helpers::Table 8 | extend Hirb::DynamicView 9 | 10 | # Takes same options as Hirb::Helpers::Table.render except as noted below. 11 | # 12 | # ==== Options: 13 | # [:table_class] Explicit table class to use for rendering. Defaults to 14 | # Hirb::Helpers::ObjectTable if output is not an Array or Hash. Otherwise 15 | # defaults to Hirb::Helpers::Table. 16 | def self.render(output, options={}) 17 | output = Array(output) 18 | (defaults = dynamic_options(output[0])) && (options = defaults.merge(options)) 19 | klass = options.delete(:table_class) || ( 20 | !(output[0].is_a?(Hash) || output[0].is_a?(Array)) ? 21 | Hirb::Helpers::ObjectTable : Hirb::Helpers::Table) 22 | klass.render(output, options) 23 | end 24 | end -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'bacon' 2 | require 'bacon/bits' 3 | require 'mocha-on-bacon' 4 | require 'hirb' 5 | include Hirb 6 | 7 | module TestHelpers 8 | # set these to avoid invoking stty multiple times which doubles test suite running time 9 | ENV["LINES"] = ENV["COLUMNS"] = "20" 10 | def reset_terminal_size 11 | ENV["LINES"] = ENV["COLUMNS"] = "20" 12 | end 13 | 14 | def capture_stdout(&block) 15 | original_stdout = $stdout 16 | $stdout = fake = StringIO.new 17 | begin 18 | yield 19 | ensure 20 | $stdout = original_stdout 21 | end 22 | fake.string 23 | end 24 | 25 | def capture_stderr(&block) 26 | original_stderr = $stderr 27 | $stderr = fake = StringIO.new 28 | begin 29 | yield 30 | ensure 31 | $stderr = original_stderr 32 | end 33 | fake.string 34 | end 35 | 36 | def reset_config 37 | View.instance_eval "@config = nil" 38 | end 39 | end 40 | 41 | class Bacon::Context 42 | include TestHelpers 43 | end 44 | 45 | class String 46 | def unindent(num=nil) 47 | regex = num ? /^\s{#{num}}/ : /^\s*/ 48 | gsub(regex, '').chomp 49 | end 50 | end 51 | 52 | # mocks IRB for View + Pager 53 | module ::IRB 54 | class Irb 55 | def initialize(context) 56 | @context = context 57 | end 58 | def output_value; end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/hirb/helpers/parent_child_tree.rb: -------------------------------------------------------------------------------- 1 | class Hirb::Helpers::ParentChildTree < Hirb::Helpers::Tree 2 | class <@value_method.call(node), :level=>level} 21 | @children_method.call(node).each {|e| build_node(e, level + 1)} 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/hirb/helpers/vertical_table.rb: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /test/hirb_test.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'test_helper') 2 | 3 | describe "Hirb" do 4 | before_all { Hirb.config_files = nil } 5 | before { Hirb.config = nil } 6 | 7 | it "config converts yaml when config file exists" do 8 | yaml_data = {:blah=>'blah'} 9 | File.stubs('exist?').returns(true) 10 | Hirb.config_files = ['ok'] 11 | YAML.expects(:load_file).returns(yaml_data) 12 | Hirb.config.should == yaml_data 13 | end 14 | 15 | it "config defaults to hash when no config file" do 16 | File.stubs('exist?').returns(false) 17 | Hirb.config.should == {} 18 | end 19 | 20 | it "config reloads if given explicit reload" do 21 | Hirb.config 22 | Hirb.expects(:read_config_file).returns({}) 23 | Hirb.config(true) 24 | end 25 | 26 | it "config reads multiple config files and merges them" do 27 | Hirb.config_files = %w{one two} 28 | Hirb.expects(:read_config_file).times(2).returns({:output=>{"String"=>:auto_table}}, {:output=>{"Array"=>:auto_table}}) 29 | Hirb.config.should == {:output=>{"Array"=>:auto_table, "String"=>:auto_table}} 30 | Hirb.config_files = nil 31 | end 32 | 33 | it "config_file sets correctly when no ENV['HOME']" do 34 | Hirb.config_files = nil 35 | home = ENV.delete('HOME') 36 | Hirb.config_files[0].class.should == String 37 | ENV["HOME"] = home 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require 'rubygems' unless Object.const_defined?(:Gem) 3 | require File.dirname(__FILE__) + "/lib/hirb/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "hirb" 7 | s.version = Hirb::VERSION 8 | s.authors = ["Gabriel Horner"] 9 | s.email = "gabriel.horner@gmail.com" 10 | s.homepage = "http://tagaholic.me/hirb/" 11 | s.summary = "A mini view framework for console/irb that's easy to use, even while under its influence." 12 | s.description = "Hirb provides a mini view framework for console applications and uses it to improve ripl(irb)'s default inspect output. Given an object or array of objects, hirb renders a view based on the object's class and/or ancestry. Hirb offers reusable views in the form of helper classes. The two main helpers, Hirb::Helpers::Table and Hirb::Helpers::Tree, provide several options for generating ascii tables and trees. Using Hirb::Helpers::AutoTable, hirb has useful default views for at least ten popular database gems i.e. Rails' ActiveRecord::Base. Other than views, hirb offers a smart pager and a console menu. The smart pager only pages when the output exceeds the current screen size. The menu is used in conjunction with tables to offer two dimensional menus." 13 | s.required_rubygems_version = ">= 1.3.5" 14 | s.add_development_dependency 'bacon', '>= 1.1.0' 15 | s.add_development_dependency 'mocha', '~> 0.12.1' 16 | s.add_development_dependency 'mocha-on-bacon', '~> 0.2.1' 17 | s.add_development_dependency 'bacon-bits' 18 | s.files = Dir.glob(%w[{lib,test}/**/*.rb bin/* [A-Z]*.{txt,rdoc,md} ext/**/*.{rb,c}]) + %w{Rakefile .gemspec .travis.yml} 19 | s.extra_rdoc_files = ["README.rdoc", "LICENSE.txt"] 20 | s.license = 'MIT' 21 | end 22 | -------------------------------------------------------------------------------- /lib/hirb/console.rb: -------------------------------------------------------------------------------- 1 | module Hirb 2 | # This module is meant to be extended to provide methods for use in a console/irb shell. 3 | # For example: 4 | # >> extend Hirb::Console 5 | # >> view 'some string', :class=>Some::String::Formatter 6 | # >> table [[:row1], [:row2]] 7 | module Console 8 | class<:tree :type=>:directory 13 | # # is the same as: 14 | # render_output output, :class=>:tree, :options=> {:type=>:directory} 15 | # 16 | def render_output(output, options={}) 17 | View.load_config unless View.config_loaded? 18 | View.render_output(output, options.merge(:console=>true)) 19 | end 20 | 21 | # Takes same arguments and options as render_output() but returns formatted output instead of rendering it. 22 | def format_output(output, options={}, &block) 23 | View.load_config unless View.config_loaded? 24 | View.formatter.format_output(output, options.merge(:console=>true), &block) 25 | end 26 | end 27 | 28 | # Renders a table for the given object. Takes same options as Hirb::Helpers::Table.render. 29 | def table(output, options={}) 30 | Console.render_output(output, options.merge(:class=>"Hirb::Helpers::AutoTable")) 31 | end 32 | 33 | # Renders any specified view for the given object. Takes same options as Hirb::View.render_output. 34 | def view(output, options={}) 35 | Console.render_output(output, options) 36 | end 37 | 38 | # Renders a menu given an array using Hirb::Menu.render. 39 | def menu(output, options={}, &block) 40 | Console.format_output(output, options.merge(:class=>"Hirb::Menu"), &block) 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/object_table_test.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'test_helper') 2 | 3 | describe "object table" do 4 | def table(*args) 5 | Helpers::ObjectTable.render(*args) 6 | end 7 | 8 | before_all { 9 | @pets = [stub(:name=>'rufus', :age=>7, :to_s=>'rufus'), stub(:name=>'alf', :age=>101, :to_s=>'alf')] 10 | } 11 | it "renders" do 12 | expected_table = <<-TABLE.unindent 13 | +-------+-----+ 14 | | name | age | 15 | +-------+-----+ 16 | | rufus | 7 | 17 | | alf | 101 | 18 | +-------+-----+ 19 | 2 rows in set 20 | TABLE 21 | table(@pets, :fields=>[:name, :age]).should == expected_table 22 | end 23 | 24 | it "with no options defaults to to_s field" do 25 | expected_table = <<-TABLE.unindent 26 | +-------+ 27 | | value | 28 | +-------+ 29 | | rufus | 30 | | alf | 31 | +-------+ 32 | 2 rows in set 33 | TABLE 34 | table(@pets).should == expected_table 35 | end 36 | 37 | it "renders simple arrays" do 38 | expected_table = <<-TABLE.unindent 39 | +-------+ 40 | | value | 41 | +-------+ 42 | | 1 | 43 | | 2 | 44 | | 3 | 45 | | 4 | 46 | +-------+ 47 | 4 rows in set 48 | TABLE 49 | table([1,2,3,4]).should == expected_table 50 | end 51 | 52 | it "renders simple arrays with custom header" do 53 | expected_table = <<-TABLE.unindent 54 | +-----+ 55 | | num | 56 | +-----+ 57 | | 1 | 58 | | 2 | 59 | | 3 | 60 | | 4 | 61 | +-----+ 62 | 4 rows in set 63 | TABLE 64 | table([1,2,3,4], :headers=>{:to_s=>'num'}).should == expected_table 65 | end 66 | 67 | it "with empty fields" do 68 | expected_table = <<-TABLE.unindent 69 | 0 rows in set 70 | TABLE 71 | table(@pets, :fields => []).should == expected_table 72 | end 73 | 74 | it "doesn't raise error for objects that don't have :send defined" do 75 | object = Object.new 76 | class<[:to_s]) } 78 | end 79 | end -------------------------------------------------------------------------------- /test/util_test.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'test_helper') 2 | 3 | describe "Util" do 4 | it "camelize converts underscore lowercase to camelcase" do 5 | Util.camelize('hirb/util').should == "Hirb::Util" 6 | Util.camelize('hirb_hash').should == "HirbHash" 7 | end 8 | 9 | it "any_const_get returns nested class" do 10 | Util.any_const_get("Hirb::Helpers").should == Hirb::Helpers 11 | end 12 | 13 | it "any_const_get returns nil for invalid class" do 14 | Util.any_const_get("Basdfr").should == nil 15 | end 16 | 17 | it "any_const_get returns class when given class" do 18 | Util.any_const_get(String).should == String 19 | end 20 | 21 | it "recursive_hash_merge merges" do 22 | expected_hash = {:output=>{:fields=>["f1", "f2"], :method=>"blah"}, :key1=>"hash1", :key2=>"hash2"} 23 | Util.recursive_hash_merge({:output=>{:fields=>%w{f1 f2}}, :key1=>'hash1'}, 24 | {:output=>{:method=>'blah'}, :key2=>'hash2'}).should == expected_hash 25 | end 26 | 27 | it "choose_from_array specifies range with -" do 28 | Util.choose_from_array([1,2,3,4], '1-2,4').should == [1,2,4] 29 | end 30 | 31 | it "choose_from_array specifies range with .." do 32 | Util.choose_from_array([1,2,3,4], '1 .. 2,4').should == [1,2,4] 33 | end 34 | 35 | it "choose_from_array chooses all with *" do 36 | Util.choose_from_array([1,2,3,4], '*').should == [1,2,3,4] 37 | end 38 | 39 | it "choose_from_array ignores non-numerical input" do 40 | Util.choose_from_array([1,2,3,4], 'a,2').should == [2] 41 | end 42 | 43 | it "choose_from_array ignores 0" do 44 | Util.choose_from_array([1,2,3,4], '0,2').should == [2] 45 | end 46 | 47 | it "choose_from_array returns empty when empty input" do 48 | Util.choose_from_array([1,2,3,4], "\n").should == [] 49 | end 50 | 51 | it "choose_from_array returns empty with an invalid range" do 52 | Util.choose_from_array([1,2,3,4], "5").should == [] 53 | end 54 | 55 | it "capture_stdout" do 56 | string = "sweetness man" 57 | Util.capture_stdout { puts string }.should == string + "\n" 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /test/resizer_test.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'test_helper') 2 | 3 | describe "Resizer" do 4 | def table(options) 5 | @table = Helpers::Table.new [options[:field_lengths].keys.inject({}) {|t,e| t[e] = '1'; t}] 6 | @table.field_lengths = options[:field_lengths] 7 | @table.width = options[:width] 8 | @table.max_fields = options[:max_fields] if options[:max_fields] 9 | @width, @field_lengths = @table.width, @table.field_lengths 10 | @table 11 | end 12 | 13 | it "resize ensures columns total doesn't exceed max width" do 14 | table :field_lengths=>{:f1=>135, :f2=>45, :f3=>4, :f4=>55}, :width=>195 15 | Helpers::Table::Resizer.resize!(@table) 16 | @field_lengths.values.inject {|a,e| a+=e}.should <= @width 17 | end 18 | 19 | it "resize sets columns by relative lengths" do 20 | table :field_lengths=>{:a=>30, :b=>30, :c=>40}, :width=>60 21 | Helpers::Table::Resizer.resize!(@table) 22 | @field_lengths.values.inject {|a,e| a+=e}.should <= @width 23 | @field_lengths.values.uniq.size.should.not == 1 24 | end 25 | 26 | it "resize sets all columns roughly equal when adusting long fields don't work" do 27 | table :field_lengths=>{:field1=>10, :field2=>15, :field3=>100}, :width=>20 28 | Helpers::Table::Resizer.resize!(@table) 29 | @field_lengths.values.inject {|a,e| a+=e}.should <= @width 30 | @field_lengths.values.each {|e| e.should <= 4 } 31 | end 32 | 33 | describe "add_extra_width and max_fields" do 34 | def table_and_resize(options={}) 35 | defaults = {:field_lengths=>{:f1=>135, :f2=>30, :f3=>4, :f4=>100}, :width=>195, :max_fields=>{:f1=>80, :f4=>30} } 36 | @table = table defaults.merge(options) 37 | # repeated from table since instance variables aren't copied b/n contexts 38 | @width, @field_lengths = @table.width, @table.field_lengths 39 | Helpers::Table::Resizer.resize! @table 40 | end 41 | 42 | it "doesn't add to already maxed out field" do 43 | table_and_resize 44 | @field_lengths[:f3].should == 4 45 | end 46 | 47 | it "restricted before adding width" do 48 | table_and_resize 49 | @field_lengths[:f4].should <= 30 50 | end 51 | 52 | it "adds to restricted field" do 53 | table_and_resize 54 | @field_lengths[:f1].should <= 80 55 | end 56 | 57 | it "adds to unrestricted field" do 58 | table_and_resize :field_lengths=>{:f1=>135, :f2=>70, :f3=>4, :f4=>100} 59 | @field_lengths[:f2].should == 70 60 | end 61 | end 62 | end -------------------------------------------------------------------------------- /lib/hirb.rb: -------------------------------------------------------------------------------- 1 | # Needed by Hirb::String to handle multibyte characters 2 | $KCODE = 'u' if RUBY_VERSION < '1.9' 3 | 4 | require 'yaml' 5 | require 'hirb/util' 6 | require 'hirb/string' 7 | require 'hirb/formatter' # must come before helpers/auto_table 8 | require 'hirb/dynamic_view' 9 | require 'hirb/helpers' 10 | require 'hirb/views' 11 | require 'hirb/view' 12 | require 'hirb/console' 13 | require 'hirb/pager' 14 | require 'hirb/menu' 15 | require 'hirb/version' 16 | 17 | # Most of Hirb's functionality is in Hirb::View. 18 | # For a tutorial on configuring and creating views see Hirb::View. For a tutorial on dynamic views see Hirb::DynamicView. 19 | # 20 | # == Config Files 21 | # Hirb can have multiple config files defined by config_files(). These config files 22 | # have the following top level keys: 23 | # [*:output*] This hash is used by the formatter object. See Hirb::Formatter.config for its format. 24 | # [*:width*] Width of the terminal/console. Defaults to Hirb::View::DEFAULT_WIDTH or possibly autodetected when Hirb is enabled. 25 | # [*:height*] Height of the terminal/console. Defaults to Hirb::View::DEFAULT_HEIGHT or possibly autodetected when Hirb is enabled. 26 | # [*:formatter*] Boolean which determines if the formatter is enabled. Defaults to true. 27 | # [*:pager*] Boolean which determines if the pager is enabled. Defaults to true. 28 | # [*:pager_command*] Command to be used for paging. Command can have options after it i.e. 'less -r'. 29 | # Defaults to common pagers i.e. less and more if detected. 30 | # [*:ignore_errors*] Boolean which ignores internal view errors and continues with original view 31 | # (i.e. #inspect for irb). Defaults to false. 32 | module Hirb 33 | class < @width 29 | average_field = total_length / @field_size.to_f 30 | long_lengths = @field_lengths.values.select {|e| e > average_field } 31 | return false if long_lengths.empty? 32 | 33 | # adjusts average long field by ratio with @width 34 | average_long_field = sum(long_lengths)/long_lengths.size * @width/total_length 35 | @field_lengths.each {|f,length| 36 | @field_lengths[f] = average_long_field if length > average_long_field 37 | } 38 | end 39 | true 40 | end 41 | 42 | # Produces a field_lengths which meets the @width requirement 43 | def default_restrict_field_lengths 44 | original_total_length = sum @original_field_lengths.values 45 | # set fields by their relative weight to original length 46 | new_lengths = @original_field_lengths.inject({}) {|t,(k,v)| 47 | t[k] = (v / original_total_length.to_f * @width).to_i; t } 48 | 49 | # set all fields the same if relative doesn't work 50 | unless new_lengths.values.all? {|e| e > MIN_FIELD_LENGTH} && (sum(new_lengths.values) <= @width) 51 | new_lengths = @field_lengths.inject({}) {|t,(k,_v)| t[k] = @width / @field_size; t } 52 | end 53 | @field_lengths.each {|k,v| @field_lengths[k] = new_lengths[k] } 54 | end 55 | 56 | def add_extra_width 57 | added_width = 0 58 | extra_width = @width - sum(@field_lengths.values) 59 | unmaxed_fields = @field_lengths.keys.select {|f| !remaining_width(f).zero? } 60 | # order can affect which one gets the remainder so let's keep it consistent 61 | unmaxed_fields = unmaxed_fields.sort_by {|e| e.to_s} 62 | 63 | unmaxed_fields.each_with_index do |f, i| 64 | extra_per_field = (extra_width - added_width) / (unmaxed_fields.size - i) 65 | add_to_field = remaining_width(f) < extra_per_field ? remaining_width(f) : extra_per_field 66 | added_width += add_to_field 67 | @field_lengths[f] += add_to_field 68 | end 69 | end 70 | 71 | def remaining_width(field) 72 | (@remaining_width ||= {})[field] ||= begin 73 | (@table.max_fields[field] || @original_field_lengths[field]) - @field_lengths[field] 74 | end 75 | end 76 | 77 | def sum(arr) 78 | arr.inject {|t,e| t += e } || 0 79 | end 80 | #:startdoc: 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /test/dynamic_view_test.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'test_helper') 2 | 3 | describe "DynamicView" do 4 | def output_expects(output, expects) 5 | Helpers::ObjectTable.expects(:render).with(output, expects) 6 | Helpers::AutoTable.render(output) 7 | end 8 | 9 | describe "add" do 10 | before_all { View.load_config } 11 | 12 | it "raises error if no :helper option" do 13 | lambda { Hirb.add_dynamic_view 'Blah', {} }.should.raise(ArgumentError). 14 | message.should =~ /:helper.*required/ 15 | end 16 | 17 | it "raises error if :helper option not a dynamic_view module" do 18 | lambda { Hirb.add_dynamic_view('Blah', :helper=>:table) {|obj| } }. 19 | should.raise(ArgumentError).message.should =~ /:helper.*must/ 20 | end 21 | 22 | it "raises error if views module not a module" do 23 | lambda { Hirb.add_dynamic_view 'Blah', :helper=>:auto_table }.should.raise(ArgumentError). 24 | message.should =~ /must be a module/ 25 | end 26 | 27 | it "adds a view with block" do 28 | Hirb.add_dynamic_view('Date', :helper=>:auto_table) do |obj| 29 | {:fields=>obj.class::DAYNAMES} 30 | end 31 | output_expects [Date.new], :fields=>Date::DAYNAMES 32 | end 33 | 34 | it "when adding views with a block, second view for same class overrides first one" do 35 | Hirb.add_dynamic_view('Date', :helper=>:auto_table) do |obj| 36 | {:fields=>obj.class::DAYNAMES} 37 | end 38 | Hirb.add_dynamic_view('Date', :helper=>:auto_table) do |obj| 39 | {:fields=>[:blah]} 40 | end 41 | output_expects [Date.new], :fields=>[:blah] 42 | end 43 | end 44 | 45 | it "class_to_method and method_to_class convert to each other" do 46 | ["DBI::Row", "Hirb::View"].each do |e| 47 | Helpers::AutoTable.method_to_class(DynamicView.class_to_method(e).downcase).should == e 48 | end 49 | end 50 | 51 | it "class_to_method converts correctly" do 52 | DynamicView.class_to_method("DBI::Row").should == 'd_b_i__row_view' 53 | end 54 | 55 | describe "dynamic_view" do 56 | def define_view(mod_name= :Blah, &block) 57 | mod = Views.const_set(mod_name, Module.new) 58 | mod_block = block_given? ? block : lambda {|obj| {:fields=>obj.class::DAYNAMES}} 59 | mod.send(:define_method, :date_view, mod_block) 60 | Hirb.add_dynamic_view mod, :helper=>:auto_table 61 | end 62 | 63 | before_all { View.load_config } 64 | before { Formatter.dynamic_config = {} } 65 | after { Views.send(:remove_const, :Blah) } 66 | 67 | it "sets a view's options" do 68 | define_view 69 | output_expects [Date.new], :fields=>Date::DAYNAMES 70 | end 71 | 72 | it "does override existing formatter dynamic_config" do 73 | Formatter.dynamic_config["Date"] = {:class=>Helpers::Table} 74 | define_view 75 | Formatter.dynamic_config["Date"].should == {:class=>Hirb::Helpers::AutoTable, :ancestor=>true} 76 | end 77 | 78 | it "raises a readable error when error occurs in a view" do 79 | define_view {|obj| raise 'blah' } 80 | lambda { Helpers::AutoTable.render([Date.new]) }.should.raise(RuntimeError). 81 | message.should =~ /'Date'.*date_view.*\nblah/ 82 | end 83 | 84 | it "another view can reuse an old view's options" do 85 | define_view 86 | define_view(:Blah2) do |obj| 87 | {:fields=>obj.class::DAYNAMES + ['blah']} 88 | end 89 | output_expects [Date.new], :fields=>(Date::DAYNAMES + ['blah']) 90 | end 91 | after_all { reset_config } 92 | end 93 | after_all { Formatter.dynamic_config = {} } 94 | end -------------------------------------------------------------------------------- /lib/hirb/util.rb: -------------------------------------------------------------------------------- 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 97 | -------------------------------------------------------------------------------- /lib/hirb/pager.rb: -------------------------------------------------------------------------------- 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<@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 107 | -------------------------------------------------------------------------------- /lib/hirb/dynamic_view.rb: -------------------------------------------------------------------------------- 1 | module Hirb 2 | # This module extends a Helper with the ability to have dynamic views for configured output classes. 3 | # After a Helper has extended this module, it can use it within a render() by calling 4 | # dynamic_options() to get dynamically generated options for the object it's rendering. See Hirb::Helpers::AutoTable as an example. 5 | # 6 | # == Dynamic Views 7 | # Whereas normal views are generated from helpers with static helper options, dynamic views are generated from helpers and 8 | # dynamically generated helper options. Let's look at an example for Rails' ActiveRecord classes: 9 | # 10 | # Hirb.add_dynamic_view("ActiveRecord::Base", :helper=>:auto_table) {|obj| 11 | # {:fields=>obj.class.column_names} } 12 | # 13 | # From this dynamic view definition, _any_ ActiveRecord model class will render a table with the correct fields, since the fields 14 | # are extracted from the output object's class at runtime. Note that dynamic view definitions should return a hash of helper options. 15 | # 16 | # To define multiple dynamic views, create a Views module where each method ending in '\_view' maps to a class/module: 17 | # 18 | # module Hirb::Views::ORM 19 | # def data_mapper__resource_view(obj) 20 | # {:fields=>obj.class.properties.map {|e| e.name }} 21 | # end 22 | # 23 | # def sequel__model_view(obj) 24 | # {:fields=>obj.class.columns} 25 | # end 26 | # end 27 | # 28 | # Hirb.add_dynamic_view Hirb::Views::ORM, :helper=>:auto_table 29 | # 30 | # In this example, 'data_mapper__resource_view' maps to DataMapper::Resource and 'sequel__model_view' maps to Sequel::Model. 31 | # Note that when mapping method names to class names, '__' maps to '::' and '_' signals the next letter to be capitalized. 32 | module DynamicView 33 | # Add dynamic views to output class(es) for a given helper. If defining one view, the first argument is the output class 34 | # and a block defines the dynamic view. If defining multiple views, the first argument should be a Views::* module where 35 | # each method in the module ending in _view defines a view for an output class. To map output classes to method names in 36 | # a Views module, translate'::' to '__' and a capital letter translates to a '_' and a lowercase letter. 37 | # ==== Options: 38 | # [*:helper*] Required option. Helper class that view(s) use to format. Hirb::Helpers::AutoTable is the only valid 39 | # helper among default helpers. Can be given in aliased form i.e. :auto_table -> Hirb::Helpers::AutoTable. 40 | # 41 | # Examples: 42 | # Hirb.add_dynamic_view Hirb::Views::ORM, :helper=>:auto_table 43 | # Hirb.add_dynamic_view("ActiveRecord::Base", :helper=>:auto_table) {|obj| {:fields=>obj.class.column_names} } 44 | def self.add(view, options, &block) 45 | raise ArgumentError, ":helper option is required" unless options[:helper] 46 | helper = Helpers.helper_class options[:helper] 47 | unless helper.is_a?(Module) && class << helper; self.ancestors; end.include?(self) 48 | raise ArgumentError, ":helper option must be a helper that has extended DynamicView" 49 | end 50 | mod = block ? generate_single_view_module(view, &block) : view 51 | raise ArgumentError, "'#{mod}' must be a module" unless mod.is_a?(Module) 52 | helper.add_module mod 53 | end 54 | 55 | def self.generate_single_view_module(output_mod, &block) #:nodoc: 56 | meth = class_to_method output_mod.to_s 57 | view_mod = meth.capitalize 58 | Views::Single.send(:remove_const, view_mod) if Views::Single.const_defined?(view_mod) 59 | mod = Views::Single.const_set(view_mod, Module.new) 60 | mod.send(:define_method, meth, block) 61 | mod 62 | end 63 | 64 | def self.class_to_method(mod) #:nodoc: 65 | mod.gsub(/(?!^)([A-Z])/) {|e| '_'+e }.gsub('::_', '__').downcase + '_view' 66 | end 67 | 68 | # Returns a hash of options based on dynamic views defined for the object's ancestry. If no config is found returns nil. 69 | def dynamic_options(obj) 70 | view_methods.each do |meth| 71 | if obj.class.ancestors.map {|e| e.to_s }.include?(method_to_class(meth)) 72 | begin 73 | return send(meth, obj) 74 | rescue 75 | raise "View failed to generate for '#{method_to_class(meth)}' "+ 76 | "while in '#{meth}' with error:\n#{$!.message}" 77 | end 78 | end 79 | end 80 | nil 81 | end 82 | 83 | #:stopdoc: 84 | def add_module(mod) 85 | new_methods = mod.instance_methods.select {|e| e.to_s =~ /_view$/ }.map {|e| e.to_s} 86 | return if new_methods.empty? 87 | extend mod 88 | view_methods.replace(view_methods + new_methods).uniq! 89 | update_config(new_methods) 90 | end 91 | 92 | def update_config(meths) 93 | output_config = meths.inject({}) {|t,e| 94 | t[method_to_class(e)] = {:class=>self, :ancestor=>true}; t 95 | } 96 | Formatter.dynamic_config.merge! output_config 97 | end 98 | 99 | def method_to_class(meth) 100 | view_method_classes[meth] ||= Util.camelize meth.sub(/_view$/, '').gsub('__', '/') 101 | end 102 | 103 | def view_method_classes 104 | @view_method_classes ||= {} 105 | end 106 | #:startdoc: 107 | 108 | # Stores view methods that a Helper has been given via DynamicView.add 109 | def view_methods 110 | @view_methods ||= [] 111 | end 112 | end 113 | end -------------------------------------------------------------------------------- /CHANGELOG.rdoc: -------------------------------------------------------------------------------- 1 | == 0.7.3 2 | * Remove warnings 3 | 4 | == 0.7.2 5 | * Allow full paths on $PAGER 6 | * Fix AR tables with no id column 7 | 8 | == 0.7.1 9 | * Add :style option 10 | * Fix mocha dep issue 11 | 12 | == 0.7.0 13 | * Add github markdown table 14 | 15 | == 0.6.2 16 | * Add * support to 1d/2d menus 17 | 18 | == 0.6.1 19 | * Fix for mongoid view 20 | * Fix tests on rubinius + jruby 21 | 22 | == 0.6.0 23 | * Add tab table 24 | * Tests pass in 1.9.3 25 | 26 | == 0.5.0 27 | * Add :grep_fields option to Table 28 | 29 | == 0.4.5 30 | * Fix the fix 31 | 32 | == 0.4.4 33 | * Fix bundler messing with ripl plugin 34 | 35 | == 0.4.3 36 | * Remove Formatter::TO_A_EXCEPTIONS and replace with Formatter.to_a_classes 37 | 38 | == 0.4.2 39 | * Fix bug with Tempfile and to_a_exceptions 40 | 41 | == 0.4.1 42 | * Fix bug with rendering empty hash 43 | * Add missing yaml require 44 | 45 | == 0.4.0 46 | * Add unicode table helper thanks to janlelis 47 | * Make pager compatible with full width characters 48 | 49 | == 0.3.6 50 | * Tweak ripl support 51 | * Allow override of :hirb_number thanks to asanghi. 52 | * Fix Hirb.add_view to work with class which inherits Hash 53 | 54 | == 0.3.5 55 | * Add ripl support 56 | * Fix Formatter#determine_output_class for IO and Hash 57 | * Remove :output_method option for Hirb.enable 58 | * Allow rubygems 1.3.5 59 | 60 | == 0.3.4 61 | * Added auto format of array-like objects i.e. ActiveRecord::Relation and Set. 62 | * Fixed bug when Hirb::Console#table is used without Hirb enabled. 63 | * Fixed bug when hirb is running within cron and uses tput. 64 | 65 | == 0.3.3 66 | * Added ignore_errors option to ignore view errors and continue with original view. 67 | * Added support for array menu items. 68 | * Added support to ObjectTable for objects with an undefined :send method. 69 | 70 | == 0.3.2 71 | * Added irb autocompletions for bond. 72 | * Fixed tests for ruby 1.9. 73 | * Changed tests to use bacon. 74 | * Removed jeweler in Rakefile and pointless $LOAD_PATH manipulation. 75 | 76 | == 0.3.1 77 | * Bug fix on DynamicView.class_to_method to allow overrides of default views. 78 | * Modified mongo_mapper view to have _id first. 79 | 80 | == 0.3.0 81 | * Added dynamic views. 82 | * Added default table views for the following database classes/modules: 83 | CouchFoo::Base, CouchPotato::Persistence, CouchRest::ExtendedDocument, 84 | DBI::Row, DataMapper::Resource, Friendly::Document, MongoMapper::Document, MongoMapper::EmbeddedDocument, 85 | Mongoid::Document, Ripple::Document and Sequel::Model. 86 | * Added Hirb.add_view and Hirb.add_dynamic_view for easier view manipulation. 87 | * Added :multi_line_nodes option for Tree. 88 | * Fixed :change_fields option bug in Table. 89 | * Fixed no headers and nil fields bug in Table. 90 | * Removed deprecations in Hirb.config_file + View.enable. 91 | * Removed Views classes and View.format_class. 92 | * Removed :return_rows option for Table. 93 | 94 | == 0.2.10 95 | * Added multiple options to Menu, most importantly :two_d and :action. 96 | * Improved table resizing algorithm. 97 | * Added merging of configs for multiple Hirb.enable calls. 98 | * Added :max_fields, :hide_empty, :delete_callbacks, :resize, :header_filter 99 | and :return_rows options to Table. 100 | * Added escaping for \t and \r in Table. 101 | * Renamed Table's :no_newlines option to :escape_special_chars. 102 | * Removed Table's :field_lengths option. 103 | * Removed Menu's :validate_one option. 104 | * Bug fix for table header of a basic array. 105 | * Deprecating Hirb.config_file + View.enable in next release. 106 | 107 | == 0.2.9 108 | * Added newline filtering and :no_newlines option for table helper. 109 | * Added default filters for hashes that have hash values. 110 | * Bug fix for deprecated to_a call. 111 | 112 | == 0.2.8 113 | * Added callbacks to Hirb::Helpers::Table. 114 | * Added :change_fields option to Hirb::Helpers::Table. 115 | * Added terminal size detection for jruby. 116 | * Bug fix for paging long outputs. 117 | * Bug fix to make unexpected hirb rendering errors more clear. 118 | 119 | == 0.2.7 120 | * 2 ruby 1.9 bug fixes. 121 | * Bug fix in :fields of Hirb::Helpers::ObjectTable. 122 | * Made :class option in Hirb::Formatter friendlier to external apps. 123 | 124 | == 0.2.6 125 | * Added :description option and added proc ability to :children_method option for helpers. 126 | * Bug fix for no ENV['HOME'] on Windows. 127 | * Bug fix on unaliasing output_method. 128 | * Bug fix on multiple renders of vertical table. 129 | 130 | == 0.2.5 131 | * Added ability to use Hirb.enable with non-irb ruby shells. 132 | * Helper configs now recursively merge when inheriting from others via :ancestor option. 133 | 134 | == 0.2.4 135 | * Bug fix on UTF-8 support. 136 | 137 | == 0.2.3 138 | * Added UTF-8 support for Ruby 1.8.x 139 | * Added :all_fields option to Table helper. 140 | 141 | == 0.2.2 142 | * Added a friendlier default (a vertical table) to incorrectly configured tables. 143 | * Added vertical table helper thanks to chrononaut. 144 | * Added detection of :select option from ActiveRecord queries in ActiveRecordTable helper. 145 | * Added handling anything that responds to :to_a in AutoTable helper. 146 | 147 | == 0.2.1 148 | * Fixed typo in Hirb::Console.view 149 | 150 | == 0.2.0 151 | * Major refactoring with bug fixes and better tests. 152 | * Improved table algorithm to ensure that tables don't wrap. 153 | * Added a pager which detects if output should be paged, Hirb::Pager. 154 | * Added a selection menu, Hirb::Menu 155 | * Following API changes: Hirb::Helpers::Table.max_width removed and config files don't use 156 | the :view key anymore. 157 | == 0.1.2 158 | * Added tree views. 159 | * Added output_method option to Hirb::View.render_output. 160 | 161 | == 0.1.1 162 | * Fixed bug when rendering table with many fields. 163 | 164 | == 0.1.0 165 | * Initial release 166 | -------------------------------------------------------------------------------- /test/tree_test.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'test_helper') 2 | 3 | describe "Tree helpers:" do 4 | def tree(*args) 5 | Helpers::Tree.render(*args) 6 | end 7 | 8 | describe "basic tree" do 9 | it "with hash nodes renders" do 10 | expected_tree = <<-TREE.unindent(6) 11 | 0.0 12 | 1.1 13 | 2.2 14 | 3.2 15 | 4.1 16 | TREE 17 | tree([{:level=>0, :value=>'0.0'}, {:level=>1, :value=>'1.1'}, {:level=>2, :value=>'2.2'},{:level=>2, :value=>'3.2'}, 18 | {:level=>1, :value=>'4.1'}]).should == expected_tree 19 | end 20 | 21 | it "with array nodes renders" do 22 | expected_tree = <<-TREE.unindent(6) 23 | 0.0 24 | 1.1 25 | 2.2 26 | 3.2 27 | 4.1 28 | TREE 29 | tree([[0, "0.0"], [1, "1.1"], [2, "2.2"], [2, "3.2"], [1, "4.1"]]).should == expected_tree 30 | end 31 | 32 | it "with non-string values renders" do 33 | expected_tree = <<-TREE.unindent(6) 34 | 0.0 35 | 1.1 36 | 2.2 37 | 3.2 38 | 4.1 39 | TREE 40 | tree([[0,0.0],[1,1.1],[2,2.2],[2,3.2],[1,4.1]]).should == expected_tree 41 | end 42 | 43 | it "with indent option renders" do 44 | expected_tree = <<-TREE.unindent(6) 45 | 0.0 46 | 1.1 47 | 2.2 48 | 3.2 49 | 4.1 50 | TREE 51 | tree([[0, "0.0"], [1, "1.1"], [2, "2.2"], [2, "3.2"], [1, "4.1"]], :indent=>2).should == expected_tree 52 | end 53 | 54 | it "with limit option renders" do 55 | expected_tree = <<-TREE.unindent(6) 56 | 0.0 57 | 1.1 58 | 4.1 59 | TREE 60 | tree([[0, "0.0"], [1, "1.1"], [2, "2.2"], [2, "3.2"], [1, "4.1"]], :limit=>1).should == expected_tree 61 | end 62 | 63 | it "with description option renders" do 64 | expected_tree = <<-TREE.unindent(6) 65 | 0.0 66 | 1.1 67 | 2.2 68 | 3.2 69 | 4.1 70 | 71 | 5 nodes in tree 72 | TREE 73 | tree([[0, "0.0"], [1, "1.1"], [2, "2.2"], [2, "3.2"], [1, "4.1"]], :description=>true).should == expected_tree 74 | end 75 | 76 | it "with type directory renders" do 77 | expected_tree = <<-TREE.unindent 78 | 0.0 79 | |-- 1.1 80 | | |-- 2.2 81 | | `-- 3.2 82 | `-- 4.1 83 | TREE 84 | tree([[0, "0.0"], [1, "1.1"], [2, "2.2"], [2, "3.2"], [1, "4.1"]], :type=>:directory).should == expected_tree 85 | end 86 | 87 | it "with type directory and multiple children per level renders" do 88 | expected_tree = <<-TREE.unindent 89 | 0.0 90 | |-- 1.1 91 | | |-- 2.2 92 | | | `-- 3.3 93 | | `-- 4.2 94 | | `-- 5.3 95 | `-- 6.1 96 | TREE 97 | tree([[0,'0.0'], [1,'1.1'], [2,'2.2'],[3,'3.3'],[2,'4.2'],[3,'5.3'],[1,'6.1']], :type=>:directory).should == expected_tree 98 | end 99 | 100 | it "with type number renders" do 101 | expected_tree = <<-TREE.unindent(6) 102 | 1. 0 103 | 1. 1 104 | 1. 2 105 | 2. 3 106 | 2. 4 107 | TREE 108 | tree([[0,'0'],[1,'1'],[2,'2'],[2,'3'],[1,'4']], :type=>:number).should == expected_tree 109 | end 110 | 111 | it "with multi-line nodes option renders" do 112 | expected_tree = <<-TREE.unindent(6) 113 | parent 114 | +-------+ 115 | | value | 116 | +-------+ 117 | | 1 | 118 | | 2 | 119 | | 3 | 120 | +-------+ 121 | indented 122 | stuff 123 | TREE 124 | node1 = "+-------+\n| value |\n+-------+\n| 1 |\n| 2 |\n| 3 |\n+-------+" 125 | tree([ [0, 'parent'],[1, node1],[2, "indented\nstuff"]], :multi_line_nodes=>true).should == expected_tree 126 | end 127 | end 128 | 129 | def mock_node(value, value_method) 130 | children = [] 131 | value,children = *value if value.is_a?(Array) 132 | mock(value_method=>value, :children=>children.map {|e| mock_node(e, value_method)}) 133 | end 134 | 135 | describe "parent_child_tree" do 136 | it "with name value renders" do 137 | expected_tree = <<-TREE.unindent 138 | 0.0 139 | |-- 1.1 140 | |-- 2.1 141 | | `-- 3.2 142 | `-- 4.1 143 | TREE 144 | root = mock_node(['0.0', ['1.1', ['2.1', ['3.2']], '4.1']], :name) 145 | Helpers::ParentChildTree.render(root, :type=>:directory).should == expected_tree 146 | end 147 | 148 | it "with value_method option renders" do 149 | expected_tree = <<-TREE.unindent 150 | 0.0 151 | |-- 1.1 152 | |-- 2.1 153 | | `-- 3.2 154 | `-- 4.1 155 | TREE 156 | root = mock_node(['0.0', ['1.1', ['2.1', ['3.2']], '4.1']], :blah) 157 | Helpers::ParentChildTree.render(root, :type=>:directory, :value_method=>:blah).should == expected_tree 158 | end 159 | 160 | it "with children_method proc option renders" do 161 | expected_tree = <<-TREE.unindent 162 | 1 163 | |-- 2 164 | |-- 3 165 | |-- 4 166 | `-- 5 167 | TREE 168 | Helpers::ParentChildTree.render(1, :type=>:directory, 169 | :children_method=>lambda {|e| e == 1 ? (2..5).to_a : []}, :value_method=>:to_s).should == expected_tree 170 | end 171 | end 172 | 173 | it "tree with parentless nodes renders ParentlessNodeError" do 174 | lambda { tree([[0, "0.0"], [2, '1.2']], :validate=>true) }.should.raise(Helpers::Tree::ParentlessNodeError) 175 | end 176 | 177 | it "tree with hash nodes missing level raises MissingLevelError" do 178 | lambda { tree([{:value=>'ok'}]) }.should.raise(Helpers::Tree::Node::MissingLevelError) 179 | end 180 | 181 | it "tree with hash nodes missing level raises MissingValueError" do 182 | lambda { tree([{:level=>0}]) }.should.raise(Helpers::Tree::Node::MissingValueError) 183 | end 184 | end -------------------------------------------------------------------------------- /lib/hirb/formatter.rb: -------------------------------------------------------------------------------- 1 | module Hirb 2 | # A Formatter object formats an output object (using Formatter.format_output) into a string based on the views defined 3 | # for its class and/or ancestry. 4 | class Formatter 5 | class<{:class=>'Hirb::Helpers::ObjectTable', :ancestor=>true, :options=>{:max_width=>180}}} 35 | # {'Date'=>{:class=>:auto_table, :ancestor=>true}} 36 | # {'Hash'=>{:method=>:puts}} 37 | def config 38 | @config 39 | end 40 | 41 | # Adds the view for the given class and view hash config. See Formatter.config for valid keys for view hash. 42 | def add_view(klass, view_config) 43 | @klass_config.delete(klass) 44 | @config[klass.to_s] = view_config 45 | true 46 | end 47 | 48 | # This method looks for an output object's view in Formatter.config and then Formatter.dynamic_config. 49 | # If a view is found, a stringified view is returned based on the object. If no view is found, nil is returned. The options this 50 | # class takes are a view hash as described in Formatter.config. These options will be merged with any existing helper 51 | # config hash an output class has in Formatter.config. Any block given is passed along to a helper class. 52 | def format_output(output, options={}, &block) 53 | output_class = determine_output_class(output) 54 | options = parse_console_options(options) if options.delete(:console) 55 | options = Util.recursive_hash_merge(klass_config(output_class), options) 56 | _format_output(output, options, &block) 57 | end 58 | 59 | #:stopdoc: 60 | def to_a_classes 61 | @to_a_classes ||= self.class.to_a_classes.map {|e| Util.any_const_get(e) }.compact 62 | end 63 | 64 | def _format_output(output, options, &block) 65 | output = options[:output_method] ? (output.is_a?(Array) ? 66 | output.map {|e| call_output_method(options[:output_method], e) } : 67 | call_output_method(options[:output_method], output) ) : output 68 | args = [output] 69 | args << options[:options] if options[:options] && !options[:options].empty? 70 | if options[:method] 71 | send(options[:method],*args) 72 | elsif options[:class] && (helper_class = Helpers.helper_class(options[:class])) 73 | helper_class.render(*args, &block) 74 | elsif options[:output_method] 75 | output 76 | end 77 | end 78 | 79 | def parse_console_options(options) #:nodoc: 80 | real_options = [:method, :class, :output_method].inject({}) do |h, e| 81 | h[e] = options.delete(e) if options[e]; h 82 | end 83 | real_options.merge! :options=>options 84 | end 85 | 86 | def determine_output_class(output) 87 | output.respond_to?(:to_a) && to_a_classes.any? {|e| output.is_a?(e) } ? 88 | Array(output)[0].class : output.class 89 | end 90 | 91 | def call_output_method(output_method, output) 92 | output_method.is_a?(Proc) ? output_method.call(output) : output.send(output_method) 93 | end 94 | 95 | # Internal view options built from user-defined ones. Options are built by recursively merging options from oldest 96 | # ancestors to the most recent ones. 97 | def klass_config(output_class) 98 | @klass_config[output_class] ||= build_klass_config(output_class) 99 | end 100 | 101 | def build_klass_config(output_class) 102 | output_ancestors = output_class.ancestors.map {|e| e.to_s}.reverse 103 | output_ancestors.pop 104 | hash = output_ancestors.inject({}) {|h, ancestor_klass| 105 | add_klass_config_if_true(h, ancestor_klass) {|c, klass| c[klass] && c[klass][:ancestor] } 106 | } 107 | add_klass_config_if_true(hash, output_class.to_s) {|c, klass| c[klass] } 108 | end 109 | 110 | def add_klass_config_if_true(hash, klass) 111 | if yield(@config, klass) 112 | Util.recursive_hash_merge hash, @config[klass] 113 | elsif yield(self.class.dynamic_config, klass) 114 | @config[klass] = self.class.dynamic_config[klass].dup # copy to local 115 | Util.recursive_hash_merge hash, self.class.dynamic_config[klass] 116 | else 117 | hash 118 | end 119 | end 120 | 121 | def reset_klass_config 122 | @klass_config = {} 123 | end 124 | #:startdoc: 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /test/view_test.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'test_helper') 2 | 3 | describe "View" do 4 | def formatter_config 5 | View.formatter.config 6 | end 7 | 8 | it "page_output pages when view is enabled" do 9 | Hirb.enable 10 | View.pager.stubs(:activated_by?).returns(true) 11 | View.pager.expects(:page) 12 | View.page_output('blah').should == true 13 | Hirb.disable 14 | end 15 | 16 | it "page_output doesn't page when view is disabled" do 17 | Hirb.enable 18 | Hirb.disable 19 | View.pager.stubs(:activated_by?).returns(true) 20 | View.pager.expects(:page).never 21 | View.page_output('blah').should == false 22 | end 23 | 24 | it "view_output catches unexpected errors and prints them" do 25 | reset_config 26 | Hirb.enable 27 | View.expects(:render_output).raises('blah') 28 | capture_stderr { View.view_output([1,2,3]) }.should =~ /Hirb Error: blah/ 29 | Hirb.disable 30 | end 31 | 32 | describe "enable" do 33 | before { reset_config } 34 | after { Hirb.disable } 35 | it "redefines irb output_value" do 36 | View.expects(:render_output).once 37 | Hirb.enable 38 | context_stub = stub(:last_value=>'') 39 | ::IRB::Irb.new(context_stub).output_value 40 | end 41 | 42 | it "is enabled?" do 43 | Hirb.enable 44 | View.enabled?.should == true 45 | end 46 | 47 | def output_class_config(klass) 48 | { :output=>{klass=>{:class=>:auto_table}} } 49 | end 50 | 51 | it "sets formatter config" do 52 | class_hash = {"Something::Base"=>{:class=>"BlahBlah"}} 53 | Hirb.enable :output=>class_hash 54 | View.formatter_config['Something::Base'].should == class_hash['Something::Base'] 55 | end 56 | 57 | it "when called multiple times merges configs" do 58 | Hirb.config = nil 59 | # default config + config_file 60 | Hirb.expects(:read_config_file).returns(output_class_config('Regexp')) 61 | Hirb.enable output_class_config('String') 62 | 63 | # add config file and explicit config 64 | [{:config_file=>'ok'}, output_class_config('Struct')].each do |config| 65 | Hirb.expects(:read_config_file).times(2).returns( 66 | output_class_config('ActiveRecord::Base'), output_class_config('Array')) 67 | Hirb.enable config 68 | end 69 | 70 | Hirb.config_files.include?('ok').should == true 71 | output_keys = %w{ActiveRecord::Base Array Regexp String Struct} 72 | View.config[:output].keys.sort.should == output_keys 73 | end 74 | 75 | it "when called multiple times without config doesn't affect config" do 76 | Hirb.enable 77 | old_config = View.config 78 | Hirb.expects(:read_config_file).never 79 | View.expects(:load_config).never 80 | Hirb.enable 81 | View.config.should == old_config 82 | end 83 | 84 | it "works without irb" do 85 | Object.stubs(:const_defined?).with(:IRB).returns(false) 86 | Hirb.enable 87 | formatter_config.size.should.be > 0 88 | end 89 | 90 | it "with config_file option adds to config_file" do 91 | Hirb.enable :config_file=> 'test_file' 92 | Hirb.config_files.include?('test_file').should == true 93 | end 94 | 95 | it "with ignore_errors enable option" do 96 | Hirb.enable :ignore_errors => true 97 | View.stubs(:render_output).raises(Exception, "Ex mesg") 98 | capture_stderr { View.view_output("").should == false }.should =~ /Error: Ex mesg/ 99 | end 100 | end 101 | 102 | describe "resize" do 103 | def pager; View.pager; end 104 | before do 105 | View.pager = nil; reset_config; Hirb.enable 106 | end 107 | 108 | after { Hirb.disable} 109 | it "changes width and height with stty" do 110 | if RUBY_PLATFORM[/java/] 111 | Util.expects(:command_exists?).with('tput').returns(false) 112 | end 113 | # stub tty? since running with rake sets 114 | STDIN.stubs(:tty?).returns(true) 115 | Util.expects(:command_exists?).with('stty').returns(true) 116 | ENV['COLUMNS'] = ENV['LINES'] = nil # bypasses env usage 117 | 118 | capture_stderr { View.resize } 119 | 120 | pager.width.should.not == 10 121 | pager.height.should.not == 10 122 | reset_terminal_size 123 | end 124 | 125 | it "changes width and height with ENV" do 126 | ENV['COLUMNS'] = ENV['LINES'] = '10' # simulates resizing 127 | View.resize 128 | pager.width.should == 10 129 | pager.height.should == 10 130 | end 131 | 132 | it "with no environment or stty still has valid width and height" do 133 | View.config[:width] = View.config[:height] = nil 134 | unless RUBY_PLATFORM[/java/] 135 | Util.expects(:command_exists?).with('stty').returns(false) 136 | end 137 | ENV['COLUMNS'] = ENV['LINES'] = nil 138 | 139 | View.resize 140 | pager.width.is_a?(Integer).should == true 141 | pager.height.is_a?(Integer).should == true 142 | reset_terminal_size 143 | end 144 | end 145 | 146 | it "disable points output_value back to original output_value" do 147 | View.expects(:render_output).never 148 | Hirb.enable 149 | Hirb.disable 150 | context_stub = stub(:last_value=>'') 151 | ::IRB::Irb.new(context_stub).output_value 152 | end 153 | 154 | it "disable works without irb defined" do 155 | Object.stubs(:const_defined?).with(:IRB).returns(false) 156 | Hirb.enable 157 | Hirb.disable 158 | View.enabled?.should == false 159 | end 160 | 161 | it "capture_and_render" do 162 | string = 'no waaaay' 163 | View.render_method.expects(:call).with(string) 164 | View.capture_and_render { print string } 165 | end 166 | 167 | it "state is toggled by toggle_pager" do 168 | previous_state = View.config[:pager] 169 | View.toggle_pager 170 | View.config[:pager].should == !previous_state 171 | end 172 | 173 | it "state is toggled by toggle_formatter" do 174 | previous_state = View.config[:formatter] 175 | View.toggle_formatter 176 | View.config[:formatter].should == !previous_state 177 | end 178 | end 179 | -------------------------------------------------------------------------------- /lib/hirb/helpers/tree.rb: -------------------------------------------------------------------------------- 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 <: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 182 | -------------------------------------------------------------------------------- /test/pager_test.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'test_helper') 2 | 3 | describe "Pager" do 4 | def pager; View.pager; end 5 | 6 | def create_pageable_string(inspect_mode=false, size={}) 7 | size = {:width=>pager.width, :height=>pager.height}.merge(size) 8 | seed = inspect_mode ? "a" : "a\n" 9 | if inspect_mode 10 | seed * (size[:width] * size[:height] + 1) 11 | else 12 | seed * (size[:height] + 1) 13 | end 14 | end 15 | 16 | it "command_pager sets pager_command when command exists" do 17 | Util.expects(:command_exists?).returns(true) 18 | Pager.expects(:basic_pager) 19 | Pager.command_pager 'blah', :pager_command=>'less' 20 | end 21 | 22 | it "command_pager doesn't set pager_command when command doesn't exist" do 23 | Util.expects(:command_exists?).returns(false) 24 | Pager.expects(:basic_pager).never 25 | Pager.command_pager 'blah', :pager_command=>'moreless' 26 | end 27 | 28 | describe "default_pager" do 29 | before_all { reset_config; Hirb.enable :pager=>true } 30 | before { View.pager = nil; Pager.stubs(:pager_command).returns(nil) } 31 | 32 | it "pages once in normal mode" do 33 | $stdin.expects(:gets).returns("\n") 34 | output = capture_stdout { pager.page(create_pageable_string, false) } 35 | output.include?('quit').should == true 36 | output.include?('finished').should == true 37 | end 38 | 39 | it "doesn't page in normal mode" do 40 | $stdin.expects(:gets).never 41 | output = capture_stdout { pager.page("a\n", false) } 42 | output.include?("a\n=== Pager finished. ===\n").should == true 43 | end 44 | 45 | it "pages once in inspect mode" do 46 | $stdin.expects(:gets).returns("\n") 47 | output = capture_stdout { pager.page(create_pageable_string(true), true) } 48 | output.include?('quit').should == true 49 | output.include?('finished').should == true 50 | end 51 | 52 | it "doesn't page in inspect mode" do 53 | $stdin.expects(:gets).never 54 | output = capture_stdout { pager.page("a", true) } 55 | output.include?("a\n=== Pager finished. ===\n").should == true 56 | end 57 | after_all { Hirb.disable } 58 | end 59 | 60 | describe "pager" do 61 | before_all { reset_config; Hirb.enable } 62 | before { View.pager = nil; View.formatter = nil } 63 | 64 | def irb_eval(string) 65 | context_stub = stub(:last_value=>string) 66 | ::IRB::Irb.new(context_stub).output_value 67 | end 68 | 69 | # this mode is called within @irb.output_value 70 | describe "in inspect_mode" do 71 | it "activates when output is wide enough" do 72 | output = create_pageable_string(true) 73 | pager.expects(:page).with(output.inspect, true) 74 | View.expects(:render_output).returns(false) 75 | irb_eval output 76 | end 77 | 78 | it "doesn't activate when output isn't wide enough" do 79 | pager.expects(:page).never 80 | View.expects(:render_output).returns(false) 81 | irb_eval("a") 82 | end 83 | 84 | it "activates with an explicit width" do 85 | View.config[:width] = 10 86 | output = create_pageable_string true, :width=>10 87 | pager.expects(:page).with(output.inspect, true) 88 | View.expects(:render_output).returns(false) 89 | irb_eval output 90 | end 91 | 92 | it "activates default_pager when pager command is invalid" do 93 | Pager.expects(:pager_command).returns(nil) 94 | output = create_pageable_string(true) 95 | Pager.expects(:default_pager).with(output.inspect, anything) 96 | View.expects(:render_output).returns(false) 97 | capture_stdout { irb_eval output } 98 | end 99 | end 100 | 101 | # this mode is called within View.render_output 102 | describe "in normal mode" do 103 | it "activates when output is long enough" do 104 | output = create_pageable_string 105 | View.formatter.expects(:format_output).returns(output) 106 | pager.expects(:page).with(output, false) 107 | irb_eval(output) 108 | end 109 | 110 | it "doesn't activate when output isn't long enough" do 111 | output = "a\n" 112 | View.formatter.expects(:format_output).returns(output) 113 | pager.expects(:page).never 114 | capture_stdout { irb_eval(output) } 115 | end 116 | 117 | it "activates with an explicit height" do 118 | View.config[:height] = 100 119 | output = create_pageable_string false, :height=>100 120 | View.formatter.expects(:format_output).returns(output) 121 | pager.expects(:page).with(output, false) 122 | irb_eval(output) 123 | end 124 | 125 | it "activates default_pager when pager_command is invalid" do 126 | Pager.expects(:pager_command).returns(nil) 127 | output = create_pageable_string 128 | Pager.expects(:default_pager).with(output, anything) 129 | View.formatter.expects(:format_output).returns(output) 130 | capture_stdout { irb_eval output } 131 | end 132 | end 133 | 134 | it "activates pager_command with valid pager_command option" do 135 | View.config[:pager_command] = "less" 136 | View.expects(:render_output).returns(false) 137 | Util.expects(:command_exists?).returns(true) 138 | Pager.expects(:command_pager) 139 | irb_eval create_pageable_string(true) 140 | View.config[:pager_command] = nil 141 | end 142 | 143 | it "activates pager_command with pager_command option that has command options" do 144 | View.config[:pager_command] = "less -r" 145 | View.expects(:render_output).returns(false) 146 | Util.expects(:command_exists?).with('less').returns(true) 147 | Pager.expects(:command_pager) 148 | irb_eval create_pageable_string(true) 149 | View.config[:pager_command] = nil 150 | end 151 | 152 | it "doesn't activate pager_command with invalid pager_command option" do 153 | View.config[:pager_command] = "moreless" 154 | View.expects(:render_output).returns(false) 155 | Util.expects(:command_exists?).returns(false) 156 | Pager.expects(:default_pager) 157 | irb_eval create_pageable_string(true) 158 | View.config[:pager_command] = nil 159 | end 160 | end 161 | after_all { Hirb.disable } 162 | end -------------------------------------------------------------------------------- /test/formatter_test.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'test_helper') 2 | 3 | describe "Formatter" do 4 | def set_formatter(hash={}) 5 | @formatter = Formatter.new(hash) 6 | end 7 | 8 | describe "klass_config" do 9 | it "recursively merges ancestor options" do 10 | @formatter = set_formatter "String"=>{:args=>[1,2], :options=>{:fields=>[:to_s]}}, 11 | "Object"=>{:method=>:object_output, :ancestor=>true, :options=>{:vertical=>true}}, 12 | "Kernel"=>{:method=>:default_output} 13 | expected_result = {:method=>:object_output, :args=>[1, 2], :ancestor=>true, :options=>{:fields=>[:to_s], :vertical=>true}} 14 | @formatter.klass_config(::String).should == expected_result 15 | end 16 | 17 | it "doesn't merge ancestor options" do 18 | @formatter = set_formatter "String"=>{:args=>[1,2]}, "Object"=>{:method=>:object_output}, 19 | "Kernel"=>{:method=>:default_output} 20 | @formatter.klass_config(::String).should == {:args=>[1, 2]} 21 | end 22 | 23 | it "returns hash when nothing found" do 24 | set_formatter.klass_config(::String).should == {} 25 | end 26 | 27 | describe "with dynamic_config" do 28 | def set_formatter(hash={}) 29 | @formatter = Formatter.new(hash) 30 | end 31 | after { Formatter.dynamic_config = {}} 32 | 33 | it "merges ancestor options and sets local config" do 34 | Formatter.dynamic_config = {"Object"=>{:method=>:blah}, "Kernel"=>{:args=>[1,2], :ancestor=>true}} 35 | set_formatter.klass_config(::String).should == {:args=>[1,2], :ancestor=>true} 36 | @formatter.config['Kernel'].should == {:args=>[1,2], :ancestor=>true} 37 | end 38 | 39 | it "uses local config over dynamic_config" do 40 | Formatter.dynamic_config = {"String"=>{:method=>:blah}} 41 | set_formatter "String"=>{:args=>[1,2]} 42 | @formatter.klass_config(::String).should == {:args=>[1,2]} 43 | end 44 | 45 | it "uses dynamic_config and sets local config" do 46 | Formatter.dynamic_config = {"String"=>{:method=>:blah}} 47 | set_formatter.klass_config(::String).should == {:method=>:blah} 48 | @formatter.config['String'].should == {:method=>:blah} 49 | end 50 | end 51 | end 52 | 53 | describe "formatter methods:" do 54 | before_all { eval "module ::Dooda; end" } 55 | 56 | it "#add_view sets formatter config" do 57 | @formatter = set_formatter 58 | @formatter.add_view ::Dooda, :class=>"DoodaView" 59 | @formatter.klass_config(::Dooda).should == {:class=>"DoodaView"} 60 | end 61 | 62 | it "#add_view overwrites existing formatter config" do 63 | @formatter = set_formatter "Dooda"=>{:class=>"DoodaView"} 64 | @formatter.add_view ::Dooda, :class=>"DoodaView2" 65 | @formatter.klass_config(::Dooda).should == {:class=>"DoodaView2"} 66 | end 67 | 68 | it "#parse_console_options passes all options except for formatter options into :options" do 69 | @formatter = set_formatter 70 | options = {:class=>'blah', :method=>'blah', :output_method=>'blah', :blah=>'blah'} 71 | expected_options = {:class=>'blah', :method=>'blah', :output_method=>'blah', :options=>{:blah=>'blah'}} 72 | @formatter.parse_console_options(options).should == expected_options 73 | end 74 | 75 | it "#determine_output_class recognizes subclasses of to_a classes" do 76 | class Array2 < Array; end 77 | @formatter.determine_output_class(Array2.new(%w{ok dude})).should == String 78 | end 79 | end 80 | 81 | describe "format_output" do 82 | def view_output(*args, &block); View.view_output(*args, &block); end 83 | def render_method(*args); View.render_method(*args); end 84 | 85 | def enable_with_output(value) 86 | Hirb.enable :output=>value 87 | end 88 | 89 | before_all { 90 | eval %[module ::Commify 91 | def self.render(strings) 92 | strings = Array(strings) 93 | strings.map {|e| e.split('').join(',')}.join("\n") 94 | end 95 | end] 96 | reset_config 97 | } 98 | before { View.formatter = nil; reset_config } 99 | after { Hirb.disable } 100 | 101 | it "formats with method option" do 102 | eval "module ::Kernel; def commify(string); string.split('').join(','); end; end" 103 | enable_with_output "String"=>{:method=>:commify} 104 | render_method.expects(:call).with('d,u,d,e') 105 | view_output('dude') 106 | end 107 | 108 | it "formats with class option" do 109 | enable_with_output "String"=>{:class=>"Commify"} 110 | render_method.expects(:call).with('d,u,d,e') 111 | view_output('dude') 112 | end 113 | 114 | it "formats with class option as symbol" do 115 | enable_with_output "String"=>{:class=>:auto_table} 116 | Helpers::AutoTable.expects(:render) 117 | view_output('dude') 118 | end 119 | 120 | it "formats arrays" do 121 | enable_with_output "String"=>{:class=>"Commify"} 122 | render_method.expects(:call).with('d,u,d,e') 123 | view_output(['dude']) 124 | end 125 | 126 | it "formats array-like objects" do 127 | enable_with_output "String"=>{:class=>"Commify"} 128 | render_method.expects(:call).with('d,u,d,e') 129 | require 'set' 130 | view_output Set.new(['dude']) 131 | end 132 | 133 | it "formats with options option" do 134 | eval "module ::Blahify; def self.render(*args); end; end" 135 | enable_with_output "String"=>{:class=>"Blahify", :options=>{:fields=>%w{a b}}} 136 | Blahify.expects(:render).with('dude', :fields=>%w{a b}) 137 | view_output('dude') 138 | end 139 | 140 | it "doesn't format and returns false when no format method found" do 141 | Hirb.enable 142 | render_method.expects(:call).never 143 | view_output(Date.today).should == false 144 | end 145 | 146 | it "formats with output_method option as method" do 147 | enable_with_output 'String'=>{:class=>"Commify", :output_method=>:chop} 148 | render_method.expects(:call).with('d,u,d') 149 | view_output('dude') 150 | end 151 | 152 | it "formats with output_method option as proc" do 153 | enable_with_output 'String'=>{:class=>"Commify", :output_method=>lambda {|e| e.chop}} 154 | render_method.expects(:call).with('d,u,d') 155 | view_output('dude') 156 | end 157 | 158 | it "formats output array with output_method option" do 159 | enable_with_output 'String'=>{:class=>"Commify", :output_method=>:chop} 160 | render_method.expects(:call).with("d,u,d\nm,a") 161 | view_output(['dude', 'man']) 162 | end 163 | 164 | it "formats with explicit class option" do 165 | enable_with_output 'String'=>{:class=>"Blahify"} 166 | render_method.expects(:call).with('d,u,d,e') 167 | view_output('dude', :class=>"Commify") 168 | end 169 | 170 | it "formats with explicit options option merges with existing options" do 171 | enable_with_output "String"=>{:class=>"Commify", :options=>{:fields=>%w{f1 f2}}} 172 | Commify.expects(:render).with('dude', :max_width=>10, :fields=>%w{f1 f2}) 173 | view_output('dude', :options=>{:max_width=>10}) 174 | end 175 | end 176 | end 177 | -------------------------------------------------------------------------------- /lib/hirb/menu.rb: -------------------------------------------------------------------------------- 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 227 | -------------------------------------------------------------------------------- /test/menu_test.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'test_helper') 2 | 3 | describe "Menu" do 4 | before_all { View.instance_variable_set("@config", :width=>Hirb::View::DEFAULT_WIDTH) } 5 | 6 | def menu(*args, &block) 7 | # testing via menu's main use case (through console) instead of Menu.render 8 | @console ||= Object.new.extend(Hirb::Console) 9 | @console.menu(*args, &block) 10 | end 11 | 12 | def basic_menu(*args, &block) 13 | menu_input('1') 14 | capture_stdout { menu(*args, &block).should == [1] } 15 | end 16 | 17 | def menu_input(input='') 18 | $stdin.expects(:gets).returns(input) 19 | end 20 | 21 | describe "menu" do 22 | it "by default renders table menu" do 23 | expected_menu = <<-MENU.unindent 24 | +--------+-------+ 25 | | number | value | 26 | +--------+-------+ 27 | | 1 | 1 | 28 | | 2 | 2 | 29 | | 3 | 3 | 30 | +--------+-------+ 31 | 3 rows in set 32 | MENU 33 | basic_menu([1,2,3]).include?(expected_menu).should == true 34 | end 35 | 36 | it "with block renders" do 37 | menu_input "1,2" 38 | expected_result = [1,2] 39 | capture_stdout { 40 | menu([1,2,3]) {|e| e.should == expected_result }.should == expected_result 41 | } 42 | end 43 | 44 | it "with block and no chosen doesn't call block" do 45 | menu_input "" 46 | block = lambda {|e| @called = true } 47 | capture_stdout { 48 | menu([1,2,3], &block).should == [] 49 | } 50 | assert !@called 51 | end 52 | 53 | it "with valid helper_class option renders" do 54 | Helpers::Table.expects(:render) 55 | basic_menu [1,2,3], :helper_class=>"Hirb::Helpers::Table" 56 | end 57 | 58 | it "with invalid helper_class option renders default menu" do 59 | expected_menu = <<-MENU.unindent 60 | 1: 1 61 | 2: 2 62 | 3: 3 63 | MENU 64 | basic_menu([1,2,3], :helper_class=>"SomeHelper").include?(expected_menu).should == true 65 | end 66 | 67 | it "with false helper_class option renders default menu" do 68 | expected_menu = <<-MENU.unindent 69 | 1: 1 70 | 2: 2 71 | 3: 3 72 | MENU 73 | basic_menu([1,2,3], :helper_class=>false).include?(expected_menu).should == true 74 | end 75 | 76 | it "prints prompt option" do 77 | prompt = "Input or else ..." 78 | basic_menu([1,2,3], :prompt=>prompt).include?(prompt).should == true 79 | end 80 | 81 | it "converts non-array inputs to array" do 82 | Helpers::AutoTable.expects(:render).with([1], anything) 83 | basic_menu 1 84 | end 85 | 86 | it "with false ask option returns one choice without asking" do 87 | $stdin.expects(:gets).never 88 | menu([1], :ask=>false).should == [1] 89 | end 90 | 91 | it "with no items to choose from always return without asking" do 92 | $stdin.expects(:gets).never 93 | menu([], :ask=>false).should == [] 94 | menu([], :ask=>true).should == [] 95 | end 96 | 97 | it "with directions option turns off directions" do 98 | menu_input('blah') 99 | capture_stdout { menu([1], :directions=>false) }.should.not =~ /range.*all/ 100 | end 101 | 102 | it "with true reopen option reopens" do 103 | $stdin.expects(:reopen).with('/dev/tty') 104 | basic_menu [1], :reopen=>true 105 | end 106 | 107 | it "with string reopen option reopens" do 108 | $stdin.expects(:reopen).with('/dev/blah') 109 | basic_menu [1], :reopen=>'/dev/blah' 110 | end 111 | end 112 | 113 | def two_d_menu(options={}) 114 | if options[:invokes] || options[:invoke] 115 | cmd = options[:command] || 'p' 116 | (options[:invokes] || [options[:invoke]]).each {|e| 117 | Menu.any_instance.expects(:invoke).with(cmd, e) 118 | } 119 | end 120 | 121 | capture_stdout { 122 | return menu(options[:output] || [{:a=>1, :bro=>2}, {:a=>3, :bro=>4}], 123 | {:two_d=>true}.merge(options)) 124 | } 125 | end 126 | 127 | describe "2d menu" do 128 | it "with default field from last_table renders" do 129 | menu_input "1" 130 | two_d_menu.should == [1] 131 | end 132 | 133 | it "with default field from fields option renders" do 134 | menu_input "1" 135 | two_d_menu(:fields=>[:bro, :a]).should == [2] 136 | end 137 | 138 | it "with default field option renders" do 139 | menu_input "1" 140 | two_d_menu(:default_field=>:bro).should == [2] 141 | end 142 | 143 | it "with non-table helper class renders" do 144 | menu_input "1" 145 | two_d_menu(:helper_class=>false, :fields=>[:a,:bro]).should == [1] 146 | end 147 | 148 | it "with no default field prints error" do 149 | menu_input "1" 150 | capture_stderr { two_d_menu(:fields=>[]) }.should =~ /No default.*found/ 151 | end 152 | 153 | it "with invalid field prints error" do 154 | menu_input "1:z" 155 | capture_stderr { two_d_menu }.should =~ /Invalid.*'z'/ 156 | end 157 | 158 | it "with choice from abbreviated field" do 159 | menu_input "2:b" 160 | two_d_menu.should == [4] 161 | end 162 | 163 | it "with choices from multiple fields renders" do 164 | menu_input "1 2:bro" 165 | two_d_menu.should == [1,4] 166 | end 167 | end 168 | 169 | describe "action menu" do 170 | it "invokes" do 171 | menu_input "p 1 2:bro" 172 | two_d_menu(:action=>true, :invoke=>[[1,4]]) 173 | end 174 | 175 | it "with 1d invokes" do 176 | menu_input "p 1" 177 | two_d_menu(:action=>true, :two_d=>nil, :invoke=>[[{:a=>1, :bro=>2}]]) 178 | end 179 | 180 | it "with 1d invokes on range of choices" do 181 | menu_input "p 1,2 1-2 1..2" 182 | choices = [{:a => 1, :bro => 2}, {:a => 3, :bro => 4}] 183 | two_d_menu(:action=>true, :two_d=>nil, :invoke=>[Array.new(3, choices).flatten]) 184 | end 185 | 186 | it "with 1d and all choices" do 187 | menu_input "p *" 188 | two_d_menu(:action=>true, :two_d => nil, :invoke=>[[{:a => 1, :bro => 2}, {:a => 3, :bro => 4}]]) 189 | end 190 | 191 | it "with non-choice arguments invokes" do 192 | menu_input "p arg1 1" 193 | two_d_menu :action=>true, :invoke=>['arg1', [1]] 194 | end 195 | 196 | it "with multiple choice arguments flattens them into arg" do 197 | menu_input "p arg1 1 2:bro arg2" 198 | two_d_menu :action=>true, :invoke=>['arg1', [1,4], 'arg2'] 199 | end 200 | 201 | it "with nothing chosen prints error" do 202 | menu_input "cmd" 203 | capture_stderr { two_d_menu(:action=>true) }.should =~ /No rows chosen/ 204 | end 205 | 206 | it "with range of choices" do 207 | menu_input "p 1,2:a 1-2:a 1..2:a" 208 | choices = [1,3] 209 | two_d_menu(:action=>true, :invoke=>[Array.new(3, choices).flatten]) 210 | end 211 | 212 | it "with multiple all choices" do 213 | menu_input "p * * 2:bro" 214 | two_d_menu(:action=>true, :invoke=>[[1,3,1,3,4]]) 215 | end 216 | 217 | it "with all choices with field" do 218 | menu_input "p *:bro" 219 | two_d_menu(:action=>true, :invoke=>[[2, 4]]) 220 | end 221 | 222 | it "with no command given prints error" do 223 | menu_input "1" 224 | capture_stderr { two_d_menu(:action=>true) }.should =~ /No command given/ 225 | end 226 | 227 | it "with array menu items" do 228 | menu_input "p 1" 229 | two_d_menu :action=>true, :output=>[['some', 'choice'], ['and', 'another']], 230 | :invokes=>[[['some']]] 231 | end 232 | 233 | it "with array menu items and all choices" do 234 | menu_input "p 1 *" 235 | two_d_menu :action=>true, :output=>[['some', 'choice'], ['and', 'another']], 236 | :invokes=>[[['some', 'some', 'and']]] 237 | end 238 | 239 | it "with multi_action option invokes" do 240 | menu_input "p 1 2:bro" 241 | two_d_menu(:action=>true, :multi_action=>true, :invokes=>[[1], [4]]) 242 | end 243 | 244 | it "with command option invokes" do 245 | menu_input "1" 246 | two_d_menu(:action=>true, :command=>'p', :invoke=>[[1]]) 247 | end 248 | 249 | it "with command option and empty input doesn't invoke action and exists silently" do 250 | Menu.any_instance.expects(:invoke).never 251 | menu_input "" 252 | two_d_menu(:action=>true, :command=>'p').should == nil 253 | end 254 | 255 | it "with action_object option invokes" do 256 | obj = mock(:blah=>true) 257 | menu_input "blah 1" 258 | two_d_menu(:action=>true, :action_object=>obj) 259 | end 260 | 261 | it "with ask false and defaults invokes" do 262 | two_d_menu(:output=>[{:a=>1, :bro=>2}], :action=>true, :ask=>false, :default_field=>:a, 263 | :command=>'p', :invoke=>[[1]]) 264 | end 265 | 266 | it "with ask false and no defaults prints error" do 267 | capture_stderr { 268 | two_d_menu(:output=>[{:a=>1, :bro=>2}], :action=>true, :ask=>false, :command=>'p') 269 | }.should =~ /Default.*required/ 270 | end 271 | end 272 | end 273 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | To read a linked version of this README, {click here}[http://tagaholic.me/hirb/doc/]. 2 | 3 | == Description 4 | 5 | Hirb provides a mini view framework for console applications and uses it to improve ripl(irb)'s default inspect output. 6 | Given an object or array of objects, hirb renders a view based on the object's class and/or ancestry. Hirb offers reusable 7 | views in the form of helper classes. The two main helpers, Hirb::Helpers::Table and Hirb::Helpers::Tree, provide several options 8 | for generating ascii tables and trees. Using Hirb::Helpers::AutoTable, hirb has useful default views for at least ten popular database gems 9 | i.e. Rails' ActiveRecord::Base. Other than views, hirb offers a smart pager and a console menu. The smart pager 10 | only pages when the output exceeds the current screen size. The menu is used in conjunction with tables to offer 11 | {two dimensional menus}[http://tagaholic.me/2010/02/16/two-dimensional-console-menus-with-hirb.html]. 12 | 13 | == Install 14 | 15 | Install the gem with: 16 | 17 | gem install hirb 18 | 19 | For people using full-width unicode characters, install {hirb-unicode}[https://github.com/miaout17/hirb-unicode]: 20 | 21 | gem install hirb-unicode 22 | 23 | == View Tutorials 24 | 25 | * To create and configure views, see Hirb::View or {here if on the web}[http://tagaholic.me/hirb/doc/classes/Hirb/View.html]. 26 | * To create dynamic views, see Hirb::DynamicView or {here if on the web}[http://tagaholic.me/hirb/doc/classes/Hirb/DynamicView.html]. 27 | 28 | == Printing Ascii Tables 29 | 30 | To print ascii tables from an array of arrays, hashes or any objects: 31 | 32 | puts Hirb::Helpers::AutoTable.render(ARRAY_OF_OBJECTS) 33 | 34 | Hirb will intelligently pick up on field names from an array of hashes and create properly-aligned 35 | fields from an array of arrays. See 36 | {here}[http://tagaholic.me/2009/10/15/boson-and-hirb-interactions.html#hirbs_handy_tables] for examples. 37 | 38 | == Rails Example 39 | 40 | Let's load and enable the view framework: 41 | $ rails console 42 | Loading local environment (Rails 3.0.3) 43 | >> require 'hirb' 44 | => true 45 | >> Hirb.enable 46 | => nil 47 | 48 | The default configuration provides table views for ActiveRecord::Base descendants. 49 | If a class isn't configured, Hirb reverts to irb's default echo mode. 50 | >> Hirb::Formatter.dynamic_config['ActiveRecord::Base'] 51 | => {:class=>Hirb::Helpers::AutoTable, :ancestor=>true} 52 | 53 | # Tag is a model class and descendant of ActiveRecord::Base 54 | >> Tag.last 55 | +-----+-------------------------+-------------+---------------+-----------+-----------+-------+ 56 | | id | created_at | description | name | namespace | predicate | value | 57 | +-----+-------------------------+-------------+---------------+-----------+-----------+-------+ 58 | | 907 | 2009-03-06 21:10:41 UTC | | gem:tags=yaml | gem | tags | yaml | 59 | +-----+-------------------------+-------------+---------------+-----------+-----------+-------+ 60 | 1 row in set 61 | 62 | >> Hirb::Formatter.dynamic_config['String'] 63 | => nil 64 | >> 'plain ol irb' 65 | => 'plain ol irb' 66 | >> Hirb::Formatter.dynamic_config['Symbol'] 67 | => nil 68 | >> :blah 69 | => :blah 70 | 71 | From above you can see there are no views configured for a String or a Symbol so Hirb defaults to 72 | irb's echo mode. On the other hand, Tag has a view thanks to being a descendant of ActiveRecord::Base 73 | and there being an :ancestor option. 74 | 75 | Having seen hirb display views based on an output object's class, let's see it handle an array of objects: 76 | 77 | >> Tag.all :limit=>3, :order=>"id DESC" 78 | +-----+-------------------------+-------------+-------------------+-----------+-----------+----------+ 79 | | id | created_at | description | name | namespace | predicate | value | 80 | +-----+-------------------------+-------------+-------------------+-----------+-----------+----------+ 81 | | 907 | 2009-03-06 21:10:41 UTC | | gem:tags=yaml | gem | tags | yaml | 82 | | 906 | 2009-03-06 08:47:04 UTC | | gem:tags=nomonkey | gem | tags | nomonkey | 83 | | 905 | 2009-03-04 00:30:10 UTC | | article:tags=ruby | article | tags | ruby | 84 | +-----+-------------------------+-------------+-------------------+-----------+-----------+----------+ 85 | 3 rows in set 86 | 87 | At any time you can disable Hirb if you really like irb's lovely echo mode: 88 | >> Hirb.disable 89 | => nil 90 | >> Tag.all :limit=>3, :order=>"id DESC" 91 | => [#, #, #] 96 | 97 | == Views: Anytime, Anywhere 98 | While preconfigured tables are great for database records, sometimes you just want to create 99 | tables/views for any output object: 100 | 101 | #These examples don't need to have Hirb::View enabled. 102 | >> Hirb.disable 103 | => nil 104 | 105 | # Imports table() and view() 106 | >> extend Hirb::Console 107 | => main 108 | 109 | # Create a unicode table 110 | >> table [[:a, :b, :c]], :unicode => true 111 | ┌───┬───┬───┐ 112 | │ 0 │ 1 │ 2 │ 113 | ├───┼───┼───┤ 114 | │ a ╎ b ╎ c │ 115 | └───┴───┴───┘ 116 | 1 row in set 117 | 118 | # Creates github-markdown 119 | >> table [[:a, :b, :c]], :markdown => true 120 | | 0 | 1 | 2 | 121 | |---|---|---| 122 | | a | b | c | 123 | 124 | # Create a table of Dates comparing them with different formats. 125 | >> table [Date.today, Date.today.next_month], :fields=>[:to_s, :ld, :ajd, :amjd, :asctime] 126 | +------------+--------+-----------+-------+--------------------------+ 127 | | to_s | ld | ajd | amjd | asctime | 128 | +------------+--------+-----------+-------+--------------------------+ 129 | | 2009-03-11 | 155742 | 4909803/2 | 54901 | Wed Mar 11 00:00:00 2009 | 130 | | 2009-04-11 | 155773 | 4909865/2 | 54932 | Sat Apr 11 00:00:00 2009 | 131 | +------------+--------+-----------+-------+--------------------------+ 132 | 2 rows in set 133 | 134 | # Same table as the previous method. However view() will be able to call any helper. 135 | >> view [Date.today, Date.today.next_month], :class=>:object_table, 136 | :fields=>[:to_s, :ld, :ajd, :amjd, :asctime] 137 | 138 | If these console methods weren't convenient enough, try: 139 | 140 | # Imports view() to all objects. 141 | >> require 'hirb/import_object' 142 | => true 143 | # Yields same table as above examples. 144 | >> [Date.today, Date.today.next_month].view :class=>:object_table, 145 | :fields=>[:to_s, :ld, :ajd, :amjd, :asctime] 146 | 147 | Although views by default are printed to STDOUT, they can be easily modified to write anywhere: 148 | # Setup views to write to file 'console.log'. 149 | >> Hirb::View.render_method = lambda {|output| File.open("console.log", 'w') {|f| f.write(output) } } 150 | 151 | # Writes to file with same table output as above example. 152 | >> view [Date.today, Date.today.next_month], :class=>:object_table, 153 | :fields=>[:to_s, :ld, :ajd, :amjd, :asctime] 154 | 155 | # Doesn't write to file because Symbol doesn't have a view and thus defaults to irb's echo mode. 156 | >> :blah 157 | => :blah 158 | 159 | # Go back to printing Hirb views to STDOUT. 160 | >> Hirb::View.reset_render_method 161 | 162 | == Pager 163 | 164 | Hirb has both pager and formatter functionality enabled by default. Note - if you copy and paste 165 | into your ruby console and think that one of your lines will kick off the pager, be aware that subsequent 166 | characters will go to your pager and could cause side effects like saving a file. 167 | 168 | If you want to turn off the functionality of either pager or formatter, pass that in at startup: 169 | 170 | Hirb.enable :pager=>false 171 | Hirb.enable :formatter=>false 172 | 173 | or toggle their state at runtime: 174 | 175 | Hirb::View.toggle_pager 176 | Hirb::View.toggle_formatter 177 | 178 | == Sharing Helpers and Views 179 | If you have tested helpers you'd like to share, fork Hirb and put them under lib/hirb/helpers. To share 180 | views for certain classes, put them under lib/hirb/views. Please submit views for gems that have a nontrivial 181 | number of users. 182 | 183 | == Limitations 184 | If using Wirble and irb, you should call Hirb after it since they both override irb's default output. 185 | 186 | == Motivation 187 | Table code from http://gist.github.com/72234 and {my console app's needs}[http://github.com/cldwalker/tag-tree]. 188 | 189 | == Credits 190 | * Chrononaut for vertical table helper. 191 | * janlelis for unicode table helper. 192 | * technogeeky and FND for markdown table helper. 193 | * hsume2, crafterm, spastorino, xaviershay, bogdan, asanghi, vwall, maxmeyer, jimjh, ddoherty03, 194 | rochefort and joshua for patches. 195 | 196 | == Bugs/Issues 197 | Please report them {on github}[http://github.com/cldwalker/hirb/issues]. 198 | 199 | == Contributing 200 | {See here}[http://tagaholic.me/contributing.html] 201 | 202 | == Links 203 | * http://tagaholic.me/2009/03/13/hirb-irb-on-the-good-stuff.html 204 | * http://tagaholic.me/2009/03/18/ruby-class-trees-rails-plugin-trees-with-hirb.html 205 | * http://tagaholic.me/2009/06/19/page-irb-output-and-improve-ri-with-hirb.html 206 | -------------------------------------------------------------------------------- /lib/hirb/view.rb: -------------------------------------------------------------------------------- 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<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 273 | -------------------------------------------------------------------------------- /lib/hirb/helpers/table.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding : utf-8 -*- 2 | require 'hirb/helpers/table/filters' 3 | require 'hirb/helpers/table/resizer' 4 | 5 | module Hirb 6 | # Base Table class from which other table classes inherit. 7 | # By default, a table is constrained to a default width but this can be adjusted 8 | # via the max_width option or Hirb::View.width. 9 | # Rows can be an array of arrays or an array of hashes. 10 | # 11 | # An array of arrays ie [[1,2], [2,3]], would render: 12 | # +---+---+ 13 | # | 0 | 1 | 14 | # +---+---+ 15 | # | 1 | 2 | 16 | # | 2 | 3 | 17 | # +---+---+ 18 | # 19 | # By default, the fields/columns are the numerical indices of the array. 20 | # 21 | # An array of hashes ie [{:age=>10, :weight=>100}, {:age=>80, :weight=>500}], would render: 22 | # +-----+--------+ 23 | # | age | weight | 24 | # +-----+--------+ 25 | # | 10 | 100 | 26 | # | 80 | 500 | 27 | # +-----+--------+ 28 | # 29 | # By default, the fields/columns are the keys of the first hash. 30 | # 31 | # === Custom Callbacks 32 | # Callback methods can be defined to add your own options that modify rows right before they are rendered. 33 | # Here's an example that allows for searching with a :query option: 34 | # module Query 35 | # # Searches fields given a query hash 36 | # def query_callback(rows, options) 37 | # return rows unless options[:query] 38 | # options[:query].map {|field,query| 39 | # rows.select {|e| e[field].to_s =~ /#{query}/i } 40 | # }.flatten.uniq 41 | # end 42 | # end 43 | # Hirb::Helpers::Table.send :include, Query 44 | # 45 | # >> puts Hirb::Helpers::Table.render [{:name=>'batman'}, {:name=>'robin'}], :query=>{:name=>'rob'} 46 | # +-------+ 47 | # | name | 48 | # +-------+ 49 | # | robin | 50 | # +-------+ 51 | # 1 row in set 52 | # 53 | # Callback methods: 54 | # * must be defined in Helpers::Table and end in '_callback'. 55 | # * should expect rows and a hash of render options. Rows will be an array of hashes. 56 | # * are expected to return an array of hashes. 57 | # * are invoked in alphabetical order. 58 | # For a thorough example, see {Boson::Pipe}[http://github.com/cldwalker/boson/blob/master/lib/boson/pipe.rb]. 59 | #-- 60 | # derived from http://gist.github.com/72234 61 | class Helpers::Table 62 | BORDER_LENGTH = 3 # " | " and "-+-" are the borders 63 | MIN_FIELD_LENGTH = 3 64 | class TooManyFieldsForWidthError < StandardError; end 65 | 66 | CHARS = { 67 | :top => {:left => '+', :center => '+', :right => '+', :horizontal => '-', 68 | :vertical => {:outside => '|', :inside => '|'} }, 69 | :middle => {:left => '+', :center => '+', :right => '+', :horizontal => '-'}, 70 | :bottom => {:left => '+', :center => '+', :right => '+', :horizontal => '-', 71 | :vertical => {:outside => '|', :inside => '|'} } 72 | } 73 | 74 | class << self 75 | 76 | # Main method which returns a formatted table. 77 | # ==== Options: 78 | # [*:fields*] An array which overrides the default fields and can be used to indicate field order. 79 | # [*:headers*] A hash of fields and their header names. Fields that aren't specified here default to their name. 80 | # When set to false, headers are hidden. Can also be an array but only for array rows. 81 | # [*:max_fields*] A hash of fields and their maximum allowed lengths. Maximum length can also be a percentage of the total width 82 | # (decimal less than one). When a field exceeds it's maximum then it's 83 | # truncated and has a ... appended to it. Fields that aren't specified have no maximum. 84 | # [*:max_width*] The maximum allowed width of all fields put together including field borders. Only valid when :resize is true. 85 | # Default is Hirb::View.width. 86 | # [*:resize*] Resizes table to display all columns in allowed :max_width. Default is true. Setting this false will display the full 87 | # length of each field. 88 | # [*:number*] When set to true, numbers rows by adding a :hirb_number column as the first column. Default is false. 89 | # [*:change_fields*] A hash to change old field names to new field names. This can also be an array of new names but only for array rows. 90 | # This is useful when wanting to change auto-generated keys to more user-friendly names i.e. for array rows. 91 | # [*:grep_fields*] A regexp that selects which fields to display. By default this is not set and applied. 92 | # [*:filters*] A hash of fields and their filters, applied to every row in a field. A filter can be a proc, an instance method 93 | # applied to the field value or a Filters method. Also see the filter_classes attribute below. 94 | # [*:header_filter*] A filter, like one in :filters, that is applied to all headers after the :headers option. 95 | # [*:filter_any*] When set to true, any cell defaults to being filtered by its class in :filter_classes. 96 | # Default Hirb::Helpers::Table.filter_any(). 97 | # [*:filter_classes*] Hash which maps classes to filters. Default is Hirb::Helpers::Table.filter_classes(). 98 | # [*:all_fields*] When set to true, renders fields in all rows. Valid only in rows that are hashes. Default is false. 99 | # [*:description*] When set to true, renders row count description at bottom. Default is true. 100 | # [*:escape_special_chars*] When set to true, escapes special characters \n,\t,\r so they don't disrupt tables. Default is false for 101 | # vertical tables and true for anything else. 102 | # [*:vertical*] When set to true, renders a vertical table using Hirb::Helpers::VerticalTable. Default is false. 103 | # [*:unicode*] When set to true, renders a unicode table using Hirb::Helpers::UnicodeTable. Default is false. 104 | # [*:tab*] When set to true, renders a tab-delimited table using Hirb::Helpers::TabTable. Default is false. 105 | # [*:style*] Choose style of table: :simple, :vertical, :unicode, :tab or :markdown. :simple 106 | # just uses the default render. Other values map to a capitalized namespace in format 107 | # Hirb::Helpers::OptionValTable. 108 | # 109 | # Examples: 110 | # Hirb::Helpers::Table.render [[1,2], [2,3]] 111 | # Hirb::Helpers::Table.render [[1,2], [2,3]], :max_fields=>{0=>10}, :header_filter=>:capitalize 112 | # Hirb::Helpers::Table.render [['a',1], ['b',2]], :change_fields=>%w{letters numbers}, :max_fields=>{'numbers'=>0.4} 113 | # Hirb::Helpers::Table.render [{:age=>10, :weight=>100}, {:age=>80, :weight=>500}] 114 | # Hirb::Helpers::Table.render [{:age=>10, :weight=>100}, {:age=>80, :weight=>500}], :headers=>{:weight=>"Weight(lbs)"} 115 | # Hirb::Helpers::Table.render [{:age=>10, :weight=>100}, {:age=>80, :weight=>500}], :filters=>{:age=>[:to_f]} 116 | # Hirb::Helpers::Table.render [{:age=>10, :weight=>100}, {:age=>80, :weight=>500}], :style=> :simple} 117 | def render(rows, options={}) 118 | choose_style(rows, options) 119 | rescue TooManyFieldsForWidthError 120 | $stderr.puts "", "** Hirb Warning: Too many fields for the current width. Configure your width " + 121 | "and/or fields to avoid this error. Defaulting to a vertical table. **" 122 | Helpers::VerticalTable.render(rows, options) 123 | end 124 | 125 | def choose_style(rows, options) 126 | case options[:style] 127 | when :vertical 128 | Helpers::VerticalTable.render(rows, options) 129 | when :unicode 130 | Helpers::UnicodeTable.render(rows, options) 131 | when :tab 132 | Helpers::TabTable.render(rows, options) 133 | when :markdown 134 | Helpers::MarkdownTable.render(rows, options) 135 | when :simple 136 | new(rows, options).render 137 | else 138 | options[:vertical] ? Helpers::VerticalTable.render(rows, options) : 139 | options[:unicode] ? Helpers::UnicodeTable.render(rows, options) : 140 | options[:tab] ? Helpers::TabTable.render(rows, options) : 141 | options[:markdown] ? Helpers::MarkdownTable.render(rows, options) : 142 | new(rows, options).render 143 | end 144 | end 145 | private :choose_style 146 | 147 | # A hash which maps a cell value's class to a filter. This serves to set a default filter per field if all of its 148 | # values are a class in this hash. By default, Array values are comma joined and Hashes are inspected. 149 | # See the :filter_any option to apply this filter per value. 150 | attr_accessor :filter_classes 151 | # Boolean which sets the default for :filter_any option. 152 | attr_accessor :filter_any 153 | # Holds last table object created 154 | attr_accessor :last_table 155 | end 156 | self.filter_classes = { Array=>:comma_join, Hash=>:inspect } 157 | 158 | 159 | def chars 160 | self.class.const_get(:CHARS) 161 | end 162 | 163 | #:stopdoc: 164 | attr_accessor :width, :max_fields, :field_lengths, :fields 165 | def initialize(rows, options={}) 166 | raise ArgumentError, "Table must be an array of hashes or array of arrays" unless rows.is_a?(Array) && 167 | (rows[0].is_a?(Hash) or rows[0].is_a?(Array) or rows.empty?) 168 | @options = {:description=>true, :filters=>{}, :change_fields=>{}, :escape_special_chars=>true, 169 | :filter_any=>Helpers::Table.filter_any, :resize=>true}.merge(options) 170 | @fields = set_fields(rows) 171 | @fields = @fields.select {|e| e.to_s[@options[:grep_fields]] } if @options[:grep_fields] 172 | @rows = set_rows(rows) 173 | @headers = set_headers 174 | if @options[:number] 175 | @headers[:hirb_number] ||= "number" 176 | @fields.unshift :hirb_number 177 | end 178 | Helpers::Table.last_table = self 179 | end 180 | 181 | def set_fields(rows) 182 | @options[:change_fields] = array_to_indices_hash(@options[:change_fields]) if @options[:change_fields].is_a?(Array) 183 | return @options[:fields].dup if @options[:fields] 184 | 185 | fields = if rows[0].is_a?(Hash) 186 | keys = @options[:all_fields] ? rows.map {|e| e.keys}.flatten.uniq : rows[0].keys 187 | keys.sort {|a,b| a.to_s <=> b.to_s} 188 | else 189 | rows[0].is_a?(Array) ? (0..rows[0].length - 1).to_a : [] 190 | end 191 | 192 | @options[:change_fields].each do |oldf, newf| 193 | (index = fields.index(oldf)) && fields[index] = newf 194 | end 195 | fields 196 | end 197 | 198 | def set_rows(rows) 199 | rows = Array(rows) 200 | if rows[0].is_a?(Array) 201 | rows = rows.inject([]) {|new_rows, row| 202 | new_rows << array_to_indices_hash(row) 203 | } 204 | end 205 | @options[:change_fields].each do |oldf, newf| 206 | rows.each {|e| e[newf] = e.delete(oldf) if e.key?(oldf) } 207 | end 208 | rows = filter_values(rows) 209 | rows.each_with_index {|e,i| e[:hirb_number] = (i + 1).to_s} if @options[:number] 210 | deleted_callbacks = Array(@options[:delete_callbacks]).map {|e| "#{e}_callback" } 211 | (methods.grep(/_callback$/).map {|e| e.to_s} - deleted_callbacks).sort.each do |meth| 212 | rows = send(meth, rows, @options.dup) 213 | end 214 | validate_values(rows) 215 | rows 216 | end 217 | 218 | def set_headers 219 | headers = @fields.inject({}) {|h,e| h[e] = e.to_s; h} 220 | if @options.has_key?(:headers) 221 | headers = @options[:headers].is_a?(Hash) ? headers.merge(@options[:headers]) : 222 | (@options[:headers].is_a?(Array) ? array_to_indices_hash(@options[:headers]) : @options[:headers]) 223 | end 224 | if @options[:header_filter] 225 | headers.each {|k,v| 226 | headers[k] = call_filter(@options[:header_filter], v) 227 | } 228 | end 229 | headers 230 | end 231 | 232 | def render 233 | body = [] 234 | unless @rows.length == 0 235 | setup_field_lengths 236 | body += render_header 237 | body += render_rows 238 | body += render_footer 239 | end 240 | body << render_table_description if @options[:description] 241 | body.join("\n") 242 | end 243 | 244 | def render_header 245 | @headers ? render_table_header : [render_border(:top)] 246 | end 247 | 248 | def render_footer 249 | [render_border(:bottom)] 250 | end 251 | 252 | def render_table_header 253 | title_row = chars[:top][:vertical][:outside] + ' ' + 254 | format_values(@headers).join(' ' + chars[:top][:vertical][:inside] +' ') + 255 | ' ' + chars[:top][:vertical][:outside] 256 | [render_border(:top), title_row, render_border(:middle)] 257 | end 258 | 259 | def render_border(which) 260 | chars[which][:left] + chars[which][:horizontal] + 261 | @fields.map {|f| chars[which][:horizontal] * @field_lengths[f] }. 262 | join(chars[which][:horizontal] + chars[which][:center] + chars[which][:horizontal]) + 263 | chars[which][:horizontal] + chars[which][:right] 264 | end 265 | 266 | def format_values(values) 267 | @fields.map {|field| format_cell(values[field], @field_lengths[field]) } 268 | end 269 | 270 | def format_cell(value, cell_width) 271 | text = String.size(value) > cell_width ? 272 | ( 273 | (cell_width < 5) ? String.slice(value, 0, cell_width) : String.slice(value, 0, cell_width - 3) + '...' 274 | ) : value 275 | String.ljust(text, cell_width) 276 | end 277 | 278 | def render_rows 279 | @rows.map do |row| 280 | chars[:bottom][:vertical][:outside] + ' ' + 281 | format_values(row).join(' ' + chars[:bottom][:vertical][:inside] + ' ') + 282 | ' ' + chars[:bottom][:vertical][:outside] 283 | end 284 | end 285 | 286 | def render_table_description 287 | (@rows.length == 0) ? "0 rows in set" : 288 | "#{@rows.length} #{@rows.length == 1 ? 'row' : 'rows'} in set" 289 | end 290 | 291 | def setup_field_lengths 292 | @field_lengths = default_field_lengths 293 | if @options[:resize] 294 | raise TooManyFieldsForWidthError if @fields.size > self.actual_width.to_f / MIN_FIELD_LENGTH 295 | Resizer.resize!(self) 296 | else 297 | enforce_field_constraints 298 | end 299 | end 300 | 301 | def enforce_field_constraints 302 | max_fields.each {|k,max| @field_lengths[k] = max if @field_lengths[k].to_i > max } 303 | end 304 | 305 | undef :max_fields 306 | def max_fields 307 | @max_fields ||= (@options[:max_fields] ||= {}).each {|k,v| 308 | @options[:max_fields][k] = (actual_width * v.to_f.abs).floor if v.to_f.abs < 1 309 | } 310 | end 311 | 312 | def actual_width 313 | @actual_width ||= self.width - (@fields.size * BORDER_LENGTH + 1) 314 | end 315 | 316 | undef :width 317 | def width 318 | @width ||= @options[:max_width] || View.width 319 | end 320 | 321 | # find max length for each field; start with the headers 322 | def default_field_lengths 323 | field_lengths = @headers ? @headers.inject({}) {|h,(k,v)| h[k] = String.size(v); h} : 324 | @fields.inject({}) {|h,e| h[e] = 1; h } 325 | @rows.each do |row| 326 | @fields.each do |field| 327 | len = String.size(row[field]) 328 | field_lengths[field] = len if len > field_lengths[field].to_i 329 | end 330 | end 331 | field_lengths 332 | end 333 | 334 | def set_filter_defaults(rows) 335 | @filter_classes.each do |klass, filter| 336 | @fields.each {|field| 337 | if rows.all? {|r| r[field].class == klass } 338 | @options[:filters][field] ||= filter 339 | end 340 | } 341 | end 342 | end 343 | 344 | def filter_values(rows) 345 | @filter_classes = Helpers::Table.filter_classes.merge @options[:filter_classes] || {} 346 | set_filter_defaults(rows) unless @options[:filter_any] 347 | rows.map {|row| 348 | @fields.inject({}) {|new_row,f| 349 | (filter = @options[:filters][f]) || (@options[:filter_any] && (filter = @filter_classes[row[f].class])) 350 | new_row[f] = filter ? call_filter(filter, row[f]) : row[f] 351 | new_row 352 | } 353 | } 354 | end 355 | 356 | def call_filter(filter, val) 357 | filter.is_a?(Proc) ? filter.call(val) : 358 | val.respond_to?(Array(filter)[0]) ? val.send(*filter) : Filters.send(filter, val) 359 | end 360 | 361 | def validate_values(rows) 362 | rows.each {|row| 363 | @fields.each {|f| 364 | row[f] = row[f].to_s || '' 365 | row[f] = row[f].gsub(/(\t|\r|\n)/) {|e| e.dump.gsub('"','') } if @options[:escape_special_chars] 366 | } 367 | } 368 | end 369 | 370 | # Converts an array to a hash mapping a numerical index to its array value. 371 | def array_to_indices_hash(array) 372 | array.inject({}) {|hash,e| hash[hash.size] = e; hash } 373 | end 374 | #:startdoc: 375 | end 376 | end 377 | -------------------------------------------------------------------------------- /test/table_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | require File.join(File.dirname(__FILE__), 'test_helper') 3 | 4 | describe "Table" do 5 | def table(*args) 6 | Helpers::Table.render(*args) 7 | end 8 | before_all { reset_config } 9 | 10 | describe "basic table" do 11 | it "renders" do 12 | expected_table = <<-TABLE.unindent 13 | +---+---+ 14 | | a | b | 15 | +---+---+ 16 | | 1 | 2 | 17 | | 3 | 4 | 18 | +---+---+ 19 | 2 rows in set 20 | TABLE 21 | table([{:a=>1, :b=>2}, {:a=>3, :b=>4}]).should == expected_table 22 | end 23 | 24 | it "also renders to the same table with :simple style given" do 25 | expected_table = <<-TABLE.unindent 26 | +---+---+ 27 | | a | b | 28 | +---+---+ 29 | | 1 | 2 | 30 | | 3 | 4 | 31 | +---+---+ 32 | 2 rows in set 33 | TABLE 34 | table([{:a=>1, :b=>2}, {:a=>3, :b=>4}], :style => :simple).should == expected_table 35 | end 36 | 37 | it "with no headers renders" do 38 | expected_table = <<-TABLE.unindent 39 | +---+---+ 40 | | 1 | 2 | 41 | +---+---+ 42 | 1 row in set 43 | TABLE 44 | table([{:a=>1, :b=>2}], :headers=>false).should == expected_table 45 | end 46 | 47 | it "with no headers and nil fields renders" do 48 | expected_table = <<-TABLE.unindent 49 | +---+---+ 50 | | 1 | | 51 | +---+---+ 52 | 1 row in set 53 | TABLE 54 | table([{:a=>1, :b=>nil}], :headers=>false).should == expected_table 55 | end 56 | 57 | it "with string keys renders" do 58 | expected_table = <<-TABLE.unindent 59 | +---+---+ 60 | | a | b | 61 | +---+---+ 62 | | 1 | 2 | 63 | | 3 | 4 | 64 | +---+---+ 65 | 2 rows in set 66 | TABLE 67 | table([{'a'=>1, 'b'=>2}, {'a'=>3, 'b'=>4}]).should == expected_table 68 | end 69 | 70 | it "with no keys renders" do 71 | expected_table = <<-TABLE.unindent 72 | +--+ 73 | | | 74 | +--+ 75 | | | 76 | +--+ 77 | 1 row in set 78 | TABLE 79 | table([{}]).should == expected_table 80 | end 81 | 82 | it "with array only rows renders" do 83 | expected_table = <<-TABLE.unindent 84 | +---+---+ 85 | | 0 | 1 | 86 | +---+---+ 87 | | 1 | 2 | 88 | | 3 | 4 | 89 | +---+---+ 90 | 2 rows in set 91 | TABLE 92 | table([[1,2], [3,4]]).should == expected_table 93 | end 94 | 95 | it "with too many fields defaults to vertical table" do 96 | rows = [Array.new(25, "A"* 10)] 97 | Helpers::VerticalTable.expects(:render).with(rows, anything) 98 | capture_stderr { table(rows)}.should =~ /Warning:/ 99 | end 100 | 101 | it "with no rows renders" do 102 | table([]).should == "0 rows in set" 103 | end 104 | 105 | it "with invalid rows raises an argumenterror" do 106 | lambda { table(:a=>1) }.should.raise(ArgumentError).message.should =~ /Table must/ 107 | end 108 | 109 | it "renders utf8" do 110 | expected_table = <<-TABLE.unindent 111 | +--------------------+ 112 | | name | 113 | +--------------------+ 114 | | アイウエオカキ | 115 | | クケコサシスセソタチツテ | 116 | | Tata l'asticote | 117 | | toto létoile PAOLI | 118 | +--------------------+ 119 | 4 rows in set 120 | TABLE 121 | table([{:name=>"アイウエオカキ"}, {:name=>"クケコサシスセソタチツテ"}, {:name=>"Tata l'asticote"}, {:name=>"toto létoile PAOLI"}]).should == expected_table 122 | end 123 | 124 | it "stringifies newlines and tabs and renders" do 125 | expected_table = <<-TABLE.unindent 126 | +-----+---+ 127 | | a | b | 128 | +-----+---+ 129 | | 1#{'\n'} | 2 | 130 | | 3#{'\t'} | 4 | 131 | +-----+---+ 132 | 2 rows in set 133 | TABLE 134 | value = [{'a'=>"1\n", 'b'=>2}, {'a'=>"3\t", 'b'=>4}] 135 | table(value).should == expected_table 136 | value.should == [{'a'=>"1\n", 'b'=>2}, {'a'=>"3\t", 'b'=>4}] 137 | end 138 | 139 | it "with a field of only array values renders values comma joined" do 140 | expected_table = <<-TABLE.unindent 141 | +----+------+ 142 | | a | b | 143 | +----+------+ 144 | | 1 | 1, 2 | 145 | | ok | 3, 4 | 146 | +----+------+ 147 | 2 rows in set 148 | TABLE 149 | table([{:a=>1, :b=>[1,2]}, {:a=>'ok', :b=>[3,4]}]).should == expected_table 150 | end 151 | 152 | it "with filter class default doesn't override explicit filters" do 153 | expected_table = <<-TABLE.unindent 154 | +------+-------+ 155 | | name | value | 156 | +------+-------+ 157 | | a | 1 | 158 | +------+-------+ 159 | 1 row in set 160 | TABLE 161 | table([{:name=>'a', :value=>{:b=>1}}], :filters=>{:value=>:size}).should == expected_table 162 | end 163 | end 164 | 165 | describe "table with" do 166 | it "fields option renders" do 167 | expected_table = <<-TABLE.unindent 168 | +---+---+ 169 | | b | a | 170 | +---+---+ 171 | | 2 | 1 | 172 | | 4 | 3 | 173 | +---+---+ 174 | 2 rows in set 175 | TABLE 176 | table([{:a=>1, :b=>2}, {:a=>3, :b=>4}], :fields=>[:b, :a]).should == expected_table 177 | end 178 | 179 | it "fields option and array only rows" do 180 | expected_table = <<-TABLE.unindent 181 | +---+---+ 182 | | 0 | 2 | 183 | +---+---+ 184 | | 1 | 3 | 185 | +---+---+ 186 | 1 row in set 187 | TABLE 188 | table([[1,2,3]], :fields=>[0,2]).should == expected_table 189 | end 190 | 191 | it "fields and number options copies fields option and does not modify it" do 192 | options = {:fields=>[:f1], :number=>true} 193 | table([{:f1=>1, :f2=>2}], options) 194 | options[:fields].should == [:f1] 195 | end 196 | 197 | it "invalid fields option renders empty columns" do 198 | expected_table = <<-TABLE.unindent 199 | +---+---+ 200 | | b | c | 201 | +---+---+ 202 | | 2 | | 203 | | 4 | | 204 | +---+---+ 205 | 2 rows in set 206 | TABLE 207 | table([{:a=>1, :b=>2}, {:a=>3, :b=>4}], :fields=>[:b, :c]).should == expected_table 208 | end 209 | 210 | it "grep_fields option and symbol fields" do 211 | expected_table = <<-TABLE.unindent 212 | +----+----+ 213 | | f1 | f2 | 214 | +----+----+ 215 | | 1 | 2 | 216 | +----+----+ 217 | 1 row in set 218 | TABLE 219 | table([{:f1 => 1, :f2 => 2, :gf1 => 3}], :grep_fields => /^f/).should == expected_table 220 | end 221 | 222 | it "grep_fields option and non-symbol fields" do 223 | expected_table = <<-TABLE.unindent 224 | +---+ 225 | | 1 | 226 | +---+ 227 | | 2 | 228 | +---+ 229 | 1 row in set 230 | TABLE 231 | table([[1,2,3]], :grep_fields => /1/).should == expected_table 232 | end 233 | 234 | it "invalid field in max_fields option renders" do 235 | expected_table = <<-TABLE.unindent 236 | +------------+---+ 237 | | a | b | 238 | +------------+---+ 239 | | AAAAAAA... | 2 | 240 | +------------+---+ 241 | 1 row in set 242 | TABLE 243 | table([{:a=> "A" * 50, :b=>2}], :max_fields=>{:a=>10,:c=>10}).should == expected_table 244 | end 245 | 246 | it "max_fields option with fields less than 3 characters renders" do 247 | expected_table = <<-TABLE.unindent 248 | +----+---+ 249 | | a | b | 250 | +----+---+ 251 | | AA | 2 | 252 | +----+---+ 253 | 1 row in set 254 | TABLE 255 | table([{:a=> "A" * 50, :b=>2}], :max_fields=>{:a=>2}, :resize=>false).should == expected_table 256 | end 257 | 258 | it "max_fields option without resize renders" do 259 | expected_table = <<-TABLE.unindent 260 | +------------+---+ 261 | | a | b | 262 | +------------+---+ 263 | | AAAAAAA... | 2 | 264 | +------------+---+ 265 | 1 row in set 266 | TABLE 267 | table([{:a=> "A" * 50, :b=>2}], :max_fields=>{:a=>10}, :resize=>false).should == expected_table 268 | end 269 | 270 | it "max_fields option with percentage renders" do 271 | expected_table = <<-TABLE.unindent 272 | +------------------+---+ 273 | | a | b | 274 | +------------------+---+ 275 | | AAAAAAAAAAAAA... | 2 | 276 | +------------------+---+ 277 | 1 row in set 278 | TABLE 279 | table([{:a=> "A" * 50, :b=>2}], :max_fields=>{:a=>'0.15'}).should == expected_table 280 | end 281 | 282 | it "max_width option renders" do 283 | expected_table = <<-TABLE.unindent 284 | +-----------+---+------------+ 285 | | a | b | c | 286 | +-----------+---+------------+ 287 | | AAAAAA... | 2 | CCCCCCCCCC | 288 | +-----------+---+------------+ 289 | 1 row in set 290 | TABLE 291 | table([{:a=> "A" * 50, :b=>2, :c=>"C"*10}], :max_width=>30).should == expected_table 292 | end 293 | 294 | it "resize option false renders full table" do 295 | expected_table = <<-TABLE.unindent 296 | +----------------------------------------------------+---+------------+ 297 | | a | b | c | 298 | +----------------------------------------------------+---+------------+ 299 | | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA | 2 | CCCCCCCCCC | 300 | +----------------------------------------------------+---+------------+ 301 | 1 row in set 302 | TABLE 303 | table([{:a=> "A" * 50, :b=>2, :c=>"C"*10}], :resize=>false).should == expected_table 304 | end 305 | 306 | it "global width renders" do 307 | expected_table = <<-TABLE.unindent 308 | +-----------+---+------------+ 309 | | a | b | c | 310 | +-----------+---+------------+ 311 | | AAAAAA... | 2 | CCCCCCCCCC | 312 | +-----------+---+------------+ 313 | 1 row in set 314 | TABLE 315 | View.load_config 316 | View.resize(30) 317 | table([{:a=> "A" * 50, :b=>2, :c=>"C"*10}]).should == expected_table 318 | reset_config 319 | end 320 | 321 | it "headers option and headers longer than fields renders" do 322 | expected_table = <<-TABLE.unindent 323 | +---+---------+---------+ 324 | | a | field B | field C | 325 | +---+---------+---------+ 326 | | A | 2 | C | 327 | +---+---------+---------+ 328 | 1 row in set 329 | TABLE 330 | table([{:a=> "A", :b=>2, :c=>"C"}], :headers=>{:b=>"field B", :c=>"field C"}).should == expected_table 331 | end 332 | 333 | it "headers option and headers shortened by max_fields renders" do 334 | expected_table = <<-TABLE.unindent 335 | +-------+---+ 336 | | fi... | b | 337 | +-------+---+ 338 | | A | 2 | 339 | +-------+---+ 340 | 1 row in set 341 | TABLE 342 | table([{:a=> "A", :b=>2}], :headers=>{:a=>"field A"}, :max_fields=>{:a=>5}, :resize=>false).should == expected_table 343 | end 344 | 345 | it "headers option as an array renders" do 346 | expected_table = <<-TABLE.unindent 347 | +---+---+ 348 | | A | B | 349 | +---+---+ 350 | | 1 | 2 | 351 | | 3 | 4 | 352 | +---+---+ 353 | 2 rows in set 354 | TABLE 355 | table([[1,2], [3,4]], :headers=>['A', 'B']).should == expected_table 356 | end 357 | 358 | it "header_filter option renders" do 359 | expected_table = <<-TABLE.unindent 360 | +---+---+ 361 | | A | B | 362 | +---+---+ 363 | | 2 | 3 | 364 | +---+---+ 365 | 1 row in set 366 | TABLE 367 | table([{:a=> 2, :b=>3}], :header_filter=>:capitalize).should == expected_table 368 | end 369 | 370 | it "filters option renders" do 371 | expected_table = <<-TABLE.unindent 372 | +-----------+---+ 373 | | 0 | 1 | 374 | +-----------+---+ 375 | | s,o,m,e | 2 | 376 | | t,h,i,n,g | 1 | 377 | +-----------+---+ 378 | 2 rows in set 379 | TABLE 380 | table([['some', {:num=>2}], ['thing', {:num=>1}]], :filters=>{0=>lambda {|e| e.split("").join(",")}, 381 | 1=>[:[], :num]}).should == expected_table 382 | end 383 | 384 | it "filters option calls Filters method and renders" do 385 | module ::Hirb::Helpers::Table::Filters 386 | def semicolon_join(arr); arr.join('; '); end 387 | end 388 | 389 | expected_table = <<-TABLE.unindent 390 | +------+------------------------------+ 391 | | 0 | 1 | 392 | +------+------------------------------+ 393 | | some | unsightly; unreadable; array | 394 | +------+------------------------------+ 395 | 1 row in set 396 | TABLE 397 | table([[['some'], %w{unsightly unreadable array}]], :filters=>{1=>:semicolon_join}).should == expected_table 398 | end 399 | 400 | it "number option renders" do 401 | expected_table = <<-TABLE.unindent 402 | +--------+---+---+ 403 | | number | 0 | 1 | 404 | +--------+---+---+ 405 | | 1 | a | b | 406 | | 2 | c | d | 407 | +--------+---+---+ 408 | 2 rows in set 409 | TABLE 410 | table([['a','b'], ['c', 'd']], :number=>true).should == expected_table 411 | end 412 | 413 | it "number option renders with header that can be overridden" do 414 | expected_table = <<-TABLE.unindent 415 | +----+---+---+ 416 | | SR | 0 | 1 | 417 | +----+---+---+ 418 | | 1 | a | b | 419 | | 2 | c | d | 420 | +----+---+---+ 421 | 2 rows in set 422 | TABLE 423 | table([['a','b'], ['c', 'd']], :number=>true, :headers => {:hirb_number => "SR"}).should == expected_table 424 | end 425 | 426 | it "description option false renders" do 427 | expected_table = <<-TABLE.unindent 428 | +---+---+ 429 | | 0 | 1 | 430 | +---+---+ 431 | | a | b | 432 | | c | d | 433 | +---+---+ 434 | TABLE 435 | table([['a','b'], ['c', 'd']], :description=>false).should == expected_table 436 | end 437 | 438 | it "vertical option renders vertical table" do 439 | expected_table = <<-TABLE.unindent 440 | *** 1. row *** 441 | a: 1 442 | b: 2 443 | *** 2. row *** 444 | a: 3 445 | b: 4 446 | 2 rows in set 447 | TABLE 448 | table([{:a=>1, :b=>2}, {:a=>3, :b=>4}], :vertical=>true).should == expected_table 449 | end 450 | 451 | it "vertical option renders vertical table with newlines" do 452 | expected_table = <<-TABLE.unindent 453 | *** 1. row *** 454 | a: 1 455 | b: 2 456 | *** 2. row *** 457 | a: 3 458 | b: 4 459 | and one 460 | 2 rows in set 461 | TABLE 462 | table([{:a=>1, :b=>2}, {:a=>3, :b=>"4\nand one"}], :vertical=>true).should == expected_table 463 | end 464 | 465 | it "vertical option renders vertical table successively" do 466 | expected_table = <<-TABLE.unindent 467 | *** 1. row *** 468 | a: 1 469 | b: 2 470 | *** 2. row *** 471 | a: 3 472 | b: 4 473 | 2 rows in set 474 | TABLE 475 | options = {:vertical=>true} 476 | table([{:a=>1, :b=>2}, {:a=>3, :b=>4}], options).should == expected_table 477 | table([{:a=>1, :b=>2}, {:a=>3, :b=>4}], options).should == expected_table 478 | end 479 | 480 | it "hide_empty and vertical options renders" do 481 | expected_table = <<-TABLE.unindent 482 | *** 1. row *** 483 | b: 2 484 | *** 2. row *** 485 | a: 3 486 | 2 rows in set 487 | TABLE 488 | table([{:a=>'', :b=>2}, {:a=>3, :b=>nil}], :hide_empty=>true, :vertical=>true).should == expected_table 489 | end 490 | 491 | it "unicode option renders" do 492 | expected_table = <<-TABLE.unindent 493 | ┌───┬───┐ 494 | │ a │ b │ 495 | ├───┼───┤ 496 | │ 1 ╎ 2 │ 497 | │ 3 ╎ 4 │ 498 | └───┴───┘ 499 | 2 rows in set 500 | TABLE 501 | table([{:a=>1, :b=>2}, {:a=>3, :b=>4}], :unicode => true).should == expected_table 502 | end 503 | 504 | it "tab option renders" do 505 | expected_table = <<-TABLE.unindent 506 | a b 507 | 1 2 508 | 3 4 509 | TABLE 510 | table([{:a=>1, :b=>2}, {:a=>3, :b=>4}], :tab => true).should == expected_table 511 | end 512 | 513 | it "tab option with no headers renders" do 514 | expected_table = <<-TABLE.unindent 515 | 1 2 516 | 3 4 517 | TABLE 518 | table([{:a=>1, :b=>2}, {:a=>3, :b=>4}], :tab => true, :headers => false). 519 | should == expected_table 520 | end 521 | 522 | it "markdown option renders" do 523 | expected_table = <<-TABLE.chomp 524 | | a | b | 525 | |--- | ---| 526 | | 1 | 2 | 527 | | 3 | 4 | 528 | 529 | 2 rows in set 530 | TABLE 531 | table([{:a=>1, :b=>2}, {:a=>3, :b=>4}], :markdown => true). 532 | should == "\n#{expected_table}" 533 | end 534 | 535 | it "markdown option with no headers renders" do 536 | expected_table = <<-TABLE.chomp 537 | | 1 | 2 | 538 | | 3 | 4 | 539 | 540 | 2 rows in set 541 | TABLE 542 | table([{:a=>1, :b=>2}, {:a=>3, :b=>4}], :markdown => true, :headers => false). 543 | should == "\n#{expected_table}" 544 | end 545 | 546 | it "all_fields option renders all fields" do 547 | expected_table = <<-TABLE.unindent 548 | +---+---+---+ 549 | | a | b | c | 550 | +---+---+---+ 551 | | 1 | 2 | | 552 | | 3 | | 4 | 553 | +---+---+---+ 554 | 2 rows in set 555 | TABLE 556 | table([{:a=>1, :b=>2}, {:a=>3, :c=>4}], :all_fields=>true).should == expected_table 557 | end 558 | 559 | it "change_fields option renders" do 560 | expected_table = <<-TABLE.unindent 561 | +------+-------+ 562 | | name | value | 563 | +------+-------+ 564 | | 1 | 2 | 565 | | 2 | 3 | 566 | +------+-------+ 567 | 2 rows in set 568 | TABLE 569 | table([[1,2],[2,3]], :change_fields=>{0=>'name', 1=>'value'}).should == expected_table 570 | table([[1,2],[2,3]], :change_fields=>['name', 'value']).should == expected_table 571 | end 572 | 573 | it "change_fields and fields option renders" do 574 | expected_table = <<-TABLE.unindent 575 | +------+ 576 | | name | 577 | +------+ 578 | | 1 | 579 | | 2 | 580 | +------+ 581 | 2 rows in set 582 | TABLE 583 | table([[1,2],[2,3]], :change_fields=>['name', 'value'], :fields=>['name']).should == expected_table 584 | end 585 | 586 | it "invalid fields in change_fields options are ignored" do 587 | expected_table = <<-TABLE.unindent 588 | +------+-------+ 589 | | name | value | 590 | +------+-------+ 591 | | 1 | 2 | 592 | | 2 | 3 | 593 | +------+-------+ 594 | 2 rows in set 595 | TABLE 596 | table([{:a=>1,:b=>2}, {:a=>2,:b=>3}], :change_fields=>{:a=>'name', :b=>'value', :c=>'time'}).should == expected_table 597 | table([[1,2],[2,3]], :change_fields=>['name', 'value','time']).should == expected_table 598 | end 599 | 600 | it "filter_any option filters any value" do 601 | expected_table = <<-TABLE.unindent 602 | +---------+ 603 | | a | 604 | +---------+ 605 | | {:b=>1} | 606 | | 2 | 607 | +---------+ 608 | 2 rows in set 609 | TABLE 610 | table([{:a=>{:b=>1}}, {:a=>2}], :filter_any=>true).should == expected_table 611 | end 612 | 613 | it "filter_classes option overrides class-wide filter_classes" do 614 | expected_table = <<-TABLE.unindent 615 | +---+ 616 | | a | 617 | +---+ 618 | | 1 | 619 | +---+ 620 | 1 row in set 621 | TABLE 622 | table([{:a=>{:b=>1}}], :filter_classes=>{Hash=>:size}).should == expected_table 623 | end 624 | end 625 | 626 | describe "table with callbacks" do 627 | before_all { 628 | Helpers::Table.send(:define_method, :and_one_callback) do |obj, opt| 629 | obj.each {|row| row.each {|k,v| row[k] += opt[:add] } } 630 | obj 631 | end 632 | } 633 | it "detects and runs them" do 634 | expected_table = <<-TABLE.unindent 635 | +---+---+ 636 | | a | b | 637 | +---+---+ 638 | | 2 | 3 | 639 | | 4 | 5 | 640 | +---+---+ 641 | 2 rows in set 642 | TABLE 643 | table([{'a'=>1, 'b'=>2}, {'a'=>3, 'b'=>4}], :add=>1).should == expected_table 644 | end 645 | 646 | it "doesn't run callbacks in delete_callbacks option" do 647 | Helpers::Table.send(:define_method, :and_two_callback) do |obj, opt| 648 | obj.each {|row| row.each {|k,v| row[k] = row[k] * 2 } } 649 | obj 650 | end 651 | 652 | expected_table = <<-TABLE.unindent 653 | +---+---+ 654 | | a | b | 655 | +---+---+ 656 | | 2 | 3 | 657 | | 4 | 5 | 658 | +---+---+ 659 | 2 rows in set 660 | TABLE 661 | table([{'a'=>1, 'b'=>2}, {'a'=>3, 'b'=>4}], :add=>1, :delete_callbacks=>[:and_two]).should == expected_table 662 | 663 | Helpers::Table.send(:remove_method, :and_two_callback) 664 | end 665 | after_all { Helpers::Table.send(:remove_method, :and_one_callback) } 666 | end 667 | end 668 | --------------------------------------------------------------------------------