hirb 0.2.9 → 0.2.10
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +13 -0
- data/LICENSE.txt +1 -1
- data/README.rdoc +1 -1
- data/VERSION.yml +3 -2
- data/lib/hirb.rb +22 -9
- data/lib/hirb/formatter.rb +1 -5
- data/lib/hirb/helpers/active_record_table.rb +1 -1
- data/lib/hirb/helpers/object_table.rb +2 -3
- data/lib/hirb/helpers/table.rb +104 -90
- data/lib/hirb/helpers/table/filters.rb +10 -0
- data/lib/hirb/helpers/table/resizer.rb +82 -0
- data/lib/hirb/helpers/vertical_table.rb +12 -6
- data/lib/hirb/menu.rb +204 -36
- data/lib/hirb/util.rb +11 -2
- data/lib/hirb/view.rb +29 -10
- data/test/auto_table_test.rb +8 -23
- data/test/formatter_test.rb +2 -2
- data/test/hirb_test.rb +13 -4
- data/test/menu_test.rb +134 -5
- data/test/object_table_test.rb +23 -4
- data/test/pager_test.rb +1 -1
- data/test/resizer_test.rb +61 -0
- data/test/table_test.rb +182 -75
- data/test/util_test.rb +5 -0
- data/test/view_test.rb +36 -4
- metadata +6 -2
@@ -1,9 +1,11 @@
|
|
1
1
|
class Hirb::Helpers::VerticalTable < Hirb::Helpers::Table
|
2
2
|
|
3
|
-
# Renders a vertical table using the same options as Hirb::Helpers::Table.render except for
|
4
|
-
# :vertical and :max_width which aren't used.
|
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.
|
5
7
|
def self.render(rows, options={})
|
6
|
-
new(rows, {:
|
8
|
+
new(rows, {:escape_special_chars=>false, :resize=>false}.merge(options)).render
|
7
9
|
end
|
8
10
|
|
9
11
|
#:stopdoc:
|
@@ -21,11 +23,15 @@ class Hirb::Helpers::VerticalTable < Hirb::Helpers::Table
|
|
21
23
|
@rows.map do |row|
|
22
24
|
row = "#{stars} #{i+1}. row #{stars}\n" +
|
23
25
|
@fields.map {|f|
|
24
|
-
|
25
|
-
|
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")
|
26
32
|
i+= 1
|
27
33
|
row
|
28
34
|
end
|
29
|
-
#:startdoc:
|
30
35
|
end
|
36
|
+
#:startdoc:
|
31
37
|
end
|
data/lib/hirb/menu.rb
CHANGED
@@ -1,47 +1,215 @@
|
|
1
1
|
module Hirb
|
2
|
-
# This class provides a
|
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.
|
3
8
|
class Menu
|
4
|
-
|
5
|
-
|
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.
|
6
20
|
#
|
7
21
|
# ==== Options:
|
8
|
-
# [
|
9
|
-
#
|
10
|
-
# [
|
11
|
-
# [
|
12
|
-
# [
|
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.
|
13
35
|
# Examples:
|
14
|
-
# extend Hirb::Console
|
15
|
-
#
|
16
|
-
# menu
|
17
|
-
|
18
|
-
|
19
|
-
options
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
36
|
+
# >> extend Hirb::Console
|
37
|
+
# => self
|
38
|
+
# >> menu [1,2,3], :prompt=> "So many choices, so little time: "
|
39
|
+
# >> menu [{:a=>1, :b=>2}, {:a=>3, :b=>4}], :fields=>[:a,b], :two_d=>true)
|
40
|
+
def self.render(output, options={}, &block)
|
41
|
+
new(options).render(output, &block)
|
42
|
+
rescue Error=>e
|
43
|
+
$stderr.puts "Error: #{e.message}"
|
44
|
+
end
|
45
|
+
|
46
|
+
#:stopdoc:
|
47
|
+
def initialize(options={})
|
48
|
+
@options = {:helper_class=>Hirb::Helpers::AutoTable, :prompt=>"Choose: ", :ask=>true,
|
49
|
+
:directions=>true}.merge options
|
50
|
+
end
|
51
|
+
|
52
|
+
def render(output, &block)
|
53
|
+
@output = Array(output)
|
54
|
+
return [] if @output.size.zero?
|
55
|
+
chosen = choose_from_menu
|
56
|
+
block.call(chosen) if block && chosen.size > 0
|
57
|
+
@options[:action] ? execute_action(chosen) : chosen
|
58
|
+
end
|
59
|
+
|
60
|
+
def get_input
|
61
|
+
prompt = pre_prompt + @options[:prompt]
|
62
|
+
prompt = DIRECTIONS+"\n"+prompt if @options[:directions]
|
63
|
+
|
64
|
+
if @options[:readline] && readline_loads?
|
65
|
+
get_readline_input(prompt)
|
66
|
+
else
|
67
|
+
print prompt
|
68
|
+
$stdin.gets.chomp.strip
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def get_readline_input(prompt)
|
73
|
+
input = Readline.readline prompt
|
74
|
+
Readline::HISTORY << input
|
75
|
+
input
|
76
|
+
end
|
77
|
+
|
78
|
+
def pre_prompt
|
79
|
+
prompt = ''
|
80
|
+
prompt << "Default field: #{default_field}\n" if @options[:two_d] && default_field
|
81
|
+
prompt << "Default command: #{@options[:command]}\n" if @options[:action] && @options[:command]
|
82
|
+
prompt
|
83
|
+
end
|
84
|
+
|
85
|
+
def choose_from_menu
|
86
|
+
return unasked_choice if @output.size == 1 && !@options[:ask]
|
87
|
+
|
88
|
+
if (helper_class = Util.any_const_get(@options[:helper_class]))
|
89
|
+
View.render_output(@output, :class=>@options[:helper_class], :options=>@options.merge(:number=>true))
|
90
|
+
else
|
91
|
+
@output.each_with_index {|e,i| puts "#{i+1}: #{e}" }
|
92
|
+
end
|
93
|
+
|
94
|
+
parse_input get_input
|
95
|
+
end
|
96
|
+
|
97
|
+
def unasked_choice
|
98
|
+
return @output unless @options[:action]
|
99
|
+
raise(Error, "Default command and field required for unasked action menu") unless default_field && @options[:command]
|
100
|
+
@new_args = [@options[:command], CHOSEN_ARG]
|
101
|
+
map_tokens([[@output, default_field]])
|
102
|
+
end
|
103
|
+
|
104
|
+
def execute_action(chosen)
|
105
|
+
return nil if chosen.size.zero?
|
106
|
+
if @options[:multi_action]
|
107
|
+
chosen.each {|e| invoke command, add_chosen_to_args(e) }
|
108
|
+
else
|
109
|
+
invoke command, add_chosen_to_args(chosen)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def invoke(cmd, args)
|
114
|
+
action_object.send(cmd, *args)
|
115
|
+
end
|
116
|
+
|
117
|
+
def parse_input(input)
|
118
|
+
if (@options[:two_d] || @options[:action])
|
119
|
+
tokens = input_to_tokens(input)
|
120
|
+
map_tokens(tokens)
|
121
|
+
else
|
122
|
+
Util.choose_from_array(@output, input)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def map_tokens(tokens)
|
127
|
+
if return_cell_values?
|
128
|
+
@output[0].is_a?(Hash) ? tokens.map {|arr,f| arr.map {|e| e[f]} }.flatten :
|
129
|
+
tokens.map {|arr,f| arr.map {|e| e.send(f) } }.flatten
|
30
130
|
else
|
31
|
-
|
131
|
+
tokens.map {|e| e[0] }.flatten
|
32
132
|
end
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
133
|
+
end
|
134
|
+
|
135
|
+
def return_cell_values?
|
136
|
+
@options[:two_d]
|
137
|
+
end
|
138
|
+
|
139
|
+
def input_to_tokens(input)
|
140
|
+
@new_args = []
|
141
|
+
tokens = (@args = split_input_args(input)).map {|word| parse_word(word) }.compact
|
142
|
+
cleanup_new_args
|
143
|
+
tokens
|
144
|
+
end
|
145
|
+
|
146
|
+
def parse_word(word)
|
147
|
+
if word[CHOSEN_REGEXP]
|
148
|
+
@new_args << CHOSEN_ARG
|
149
|
+
field = $3 ? unalias_field($3) : default_field ||
|
150
|
+
raise(Error, "No default field/column found. Fields must be explicitly picked.")
|
151
|
+
[Util.choose_from_array(@output, word), field ]
|
152
|
+
else
|
153
|
+
@new_args << word
|
154
|
+
nil
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def cleanup_new_args
|
159
|
+
if @new_args.all? {|e| e == CHOSEN_ARG }
|
160
|
+
@new_args = [CHOSEN_ARG]
|
161
|
+
else
|
162
|
+
i = @new_args.index(CHOSEN_ARG) || raise(Error, "No rows chosen")
|
163
|
+
@new_args.delete(CHOSEN_ARG)
|
164
|
+
@new_args.insert(i, CHOSEN_ARG)
|
43
165
|
end
|
44
|
-
chosen
|
45
166
|
end
|
167
|
+
|
168
|
+
def add_chosen_to_args(items)
|
169
|
+
args = @new_args.dup
|
170
|
+
args[args.index(CHOSEN_ARG)] = items
|
171
|
+
args
|
172
|
+
end
|
173
|
+
|
174
|
+
def command
|
175
|
+
@command ||= begin
|
176
|
+
cmd = (@new_args == [CHOSEN_ARG]) ? nil : @new_args.shift
|
177
|
+
cmd ||= @options[:command] || raise(Error, "No command given for action menu")
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def action_object
|
182
|
+
@options[:action_object] || eval("self", TOPLEVEL_BINDING)
|
183
|
+
end
|
184
|
+
|
185
|
+
def split_input_args(input)
|
186
|
+
input.split(/\s+/)
|
187
|
+
end
|
188
|
+
|
189
|
+
def default_field
|
190
|
+
@default_field ||= @options[:default_field] || fields[0]
|
191
|
+
end
|
192
|
+
|
193
|
+
# Has to be called after displaying menu
|
194
|
+
def fields
|
195
|
+
@fields ||= @options[:fields] || (@options[:ask] && table_helper_class? && Helpers::Table.last_table ?
|
196
|
+
Helpers::Table.last_table.fields[1..-1] : [])
|
197
|
+
end
|
198
|
+
|
199
|
+
def table_helper_class?
|
200
|
+
@options[:helper_class].is_a?(Class) && (@options[:helper_class] < Helpers::Table || @options[:helper_class] == Helpers::AutoTable)
|
201
|
+
end
|
202
|
+
|
203
|
+
def unalias_field(field)
|
204
|
+
fields.sort_by {|e| e.to_s }.find {|e| e.to_s[/^#{field}/] } || raise(Error, "Invalid field '#{field}'")
|
205
|
+
end
|
206
|
+
|
207
|
+
def readline_loads?
|
208
|
+
require 'readline'
|
209
|
+
true
|
210
|
+
rescue LoadError
|
211
|
+
false
|
212
|
+
end
|
213
|
+
#:startdoc:
|
46
214
|
end
|
47
215
|
end
|
data/lib/hirb/util.rb
CHANGED
@@ -48,7 +48,7 @@ module Hirb
|
|
48
48
|
result.push(array[index]) if array[index]
|
49
49
|
end
|
50
50
|
end
|
51
|
-
|
51
|
+
result
|
52
52
|
end
|
53
53
|
|
54
54
|
# Determines if a shell command exists by searching for it in ENV['PATH'].
|
@@ -61,7 +61,7 @@ module Hirb
|
|
61
61
|
def detect_terminal_size
|
62
62
|
if (ENV['COLUMNS'] =~ /^\d+$/) && (ENV['LINES'] =~ /^\d+$/)
|
63
63
|
[ENV['COLUMNS'].to_i, ENV['LINES'].to_i]
|
64
|
-
elsif RUBY_PLATFORM =~ /java/ && command_exists?('tput')
|
64
|
+
elsif (RUBY_PLATFORM =~ /java/ || !STDIN.tty?) && command_exists?('tput')
|
65
65
|
[`tput cols`.to_i, `tput lines`.to_i]
|
66
66
|
else
|
67
67
|
command_exists?('stty') ? `stty size`.scan(/\d+/).map { |s| s.to_i }.reverse : nil
|
@@ -81,5 +81,14 @@ module Hirb
|
|
81
81
|
end
|
82
82
|
fake.string
|
83
83
|
end
|
84
|
+
|
85
|
+
# From Rubygems, determine a user's home.
|
86
|
+
def find_home
|
87
|
+
['HOME', 'USERPROFILE'].each {|e| return ENV[e] if ENV[e] }
|
88
|
+
return "#{ENV['HOMEDRIVE']}#{ENV['HOMEPATH']}" if ENV['HOMEDRIVE'] && ENV['HOMEPATH']
|
89
|
+
File.expand_path("~")
|
90
|
+
rescue
|
91
|
+
File::ALT_SEPARATOR ? "C:/" : "/"
|
92
|
+
end
|
84
93
|
end
|
85
94
|
end
|
data/lib/hirb/view.rb
CHANGED
@@ -9,11 +9,12 @@ module Hirb
|
|
9
9
|
attr_reader :config
|
10
10
|
|
11
11
|
# This activates view functionality i.e. the formatter, pager and size detection. If irb exists, it overrides irb's output
|
12
|
-
# method with Hirb::View.view_output.
|
12
|
+
# method with Hirb::View.view_output. When called multiple times, new configs are merged into the existing config.
|
13
|
+
# If using Wirble, you should call this after it. The view configuration
|
13
14
|
# can be specified in a hash via a config file, as options to this method, as this method's block or any combination of these three.
|
14
15
|
# In addition to the config keys mentioned in Hirb, the options also take the following keys:
|
15
16
|
# ==== Options:
|
16
|
-
# * config_file: Name of config file
|
17
|
+
# * config_file: Name of config file(s) that are merged into existing config
|
17
18
|
# * output_method: Specify an object's class and instance method (separated by a period) to be realiased with
|
18
19
|
# hirb's view system. The instance method should take a string to be output. Default is IRB::Irb.output_value
|
19
20
|
# if using irb.
|
@@ -22,15 +23,15 @@ module Hirb
|
|
22
23
|
# Hirb::View.enable :formatter=>false, :output_method=>"Mini.output"
|
23
24
|
# Hirb::View.enable {|c| c.output = {'String'=>{:class=>'Hirb::Helpers::Table'}} }
|
24
25
|
def enable(options={}, &block)
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
26
|
+
Array(options.delete(:config_file)).each {|e|
|
27
|
+
@new_config_file = true
|
28
|
+
Hirb.config_files << e
|
29
|
+
}
|
30
|
+
enable_output_method(options.delete(:output_method))
|
31
|
+
puts "Using a block with View.enable will be *deprecated* in the next release" if block_given?
|
32
|
+
merge_or_load_config(Util.recursive_hash_merge(options, HashStruct.block_to_hash(block)))
|
31
33
|
resize(config[:width], config[:height])
|
32
|
-
|
33
|
-
true
|
34
|
+
@enabled = true
|
34
35
|
end
|
35
36
|
|
36
37
|
# Indicates if Hirb::View is enabled.
|
@@ -112,6 +113,13 @@ module Hirb
|
|
112
113
|
end
|
113
114
|
|
114
115
|
#:stopdoc:
|
116
|
+
def enable_output_method(meth)
|
117
|
+
if (meth ||= Object.const_defined?(:IRB) ? "IRB::Irb.output_value" : false) && !@output_method
|
118
|
+
@output_method = meth
|
119
|
+
alias_output_method(@output_method)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
115
123
|
def unalias_output_method(output_method)
|
116
124
|
klass, klass_method = output_method.split(".")
|
117
125
|
eval %[
|
@@ -119,6 +127,7 @@ module Hirb
|
|
119
127
|
alias_method :#{klass_method}, :non_hirb_view_output
|
120
128
|
end
|
121
129
|
]
|
130
|
+
@output_method = nil
|
122
131
|
end
|
123
132
|
|
124
133
|
def alias_output_method(output_method)
|
@@ -176,6 +185,16 @@ module Hirb
|
|
176
185
|
|
177
186
|
def formatter=(value); @formatter = value; end
|
178
187
|
|
188
|
+
def merge_or_load_config(additional_config={})
|
189
|
+
if @config && (@new_config_file || !additional_config.empty?)
|
190
|
+
Hirb.config = nil
|
191
|
+
load_config Util.recursive_hash_merge(@config, additional_config)
|
192
|
+
@new_config_file = false
|
193
|
+
elsif !@enabled
|
194
|
+
load_config(additional_config)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
179
198
|
def load_config(additional_config={})
|
180
199
|
@config = Util.recursive_hash_merge default_config, additional_config
|
181
200
|
formatter(true)
|
data/test/auto_table_test.rb
CHANGED
@@ -17,31 +17,16 @@ class Hirb::Helpers::AutoTableTest < Test::Unit::TestCase
|
|
17
17
|
Hirb::Helpers::AutoTable.render(::Set.new([1,2,3])).should == expected_table
|
18
18
|
end
|
19
19
|
|
20
|
-
test "
|
20
|
+
test "renders hash" do
|
21
21
|
expected_table = <<-TABLE.unindent
|
22
|
-
|
23
|
-
| 0 | 1
|
24
|
-
|
25
|
-
|
|
26
|
-
|
27
|
-
|
28
|
-
2 rows in set
|
22
|
+
+---+-------+
|
23
|
+
| 0 | 1 |
|
24
|
+
+---+-------+
|
25
|
+
| a | 12345 |
|
26
|
+
+---+-------+
|
27
|
+
1 row in set
|
29
28
|
TABLE
|
30
|
-
Hirb::Helpers::AutoTable.render({:a=>
|
31
|
-
end
|
32
|
-
|
33
|
-
test "doesn't convert hash with value hashes if filter exists for value" do
|
34
|
-
expected_table = <<-TABLE.unindent
|
35
|
-
+------+-------+
|
36
|
-
| name | value |
|
37
|
-
+------+-------+
|
38
|
-
| c | ok |
|
39
|
-
| a | b1 |
|
40
|
-
+------+-------+
|
41
|
-
2 rows in set
|
42
|
-
TABLE
|
43
|
-
Hirb::Helpers::AutoTable.render({:a=>{:b=>1}, :c=>'ok'}, :change_fields=>['name', 'value'],
|
44
|
-
:filters=>{'value'=>:to_s}).should == expected_table
|
29
|
+
Hirb::Helpers::AutoTable.render({:a=>12345}).should == expected_table
|
45
30
|
end
|
46
31
|
end
|
47
32
|
end
|
data/test/formatter_test.rb
CHANGED
@@ -75,9 +75,9 @@ class FormatterTest < Test::Unit::TestCase
|
|
75
75
|
formatter_config["Blah"].should == {:class=>"Hirb::Views::Blah", :ancestor=>true}
|
76
76
|
end
|
77
77
|
|
78
|
-
test "
|
78
|
+
test "sets formatter config" do
|
79
79
|
class_hash = {"Something::Base"=>{:class=>"BlahBlah"}}
|
80
|
-
Hirb.enable
|
80
|
+
Hirb.enable :output=>class_hash
|
81
81
|
formatter_config['Something::Base'].should == class_hash['Something::Base']
|
82
82
|
end
|
83
83
|
end
|