hirb 0.2.9 → 0.2.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|