hsume2-hirb 0.6.0.beta.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gemspec +21 -0
- data/CHANGELOG.rdoc +144 -0
- data/LICENSE.txt +22 -0
- data/README.rdoc +194 -0
- data/Rakefile +35 -0
- data/lib/bond/completions/hirb.rb +15 -0
- data/lib/hirb/console.rb +43 -0
- data/lib/hirb/dynamic_view.rb +113 -0
- data/lib/hirb/formatter.rb +126 -0
- data/lib/hirb/helpers/auto_table.rb +24 -0
- data/lib/hirb/helpers/object_table.rb +14 -0
- data/lib/hirb/helpers/parent_child_tree.rb +24 -0
- data/lib/hirb/helpers/tab_table.rb +24 -0
- data/lib/hirb/helpers/table/filters.rb +10 -0
- data/lib/hirb/helpers/table/resizer.rb +82 -0
- data/lib/hirb/helpers/table.rb +349 -0
- data/lib/hirb/helpers/tree.rb +181 -0
- data/lib/hirb/helpers/unicode_table.rb +15 -0
- data/lib/hirb/helpers/vertical_table.rb +37 -0
- data/lib/hirb/helpers.rb +18 -0
- data/lib/hirb/import_object.rb +10 -0
- data/lib/hirb/menu.rb +238 -0
- data/lib/hirb/pager.rb +105 -0
- data/lib/hirb/string.rb +44 -0
- data/lib/hirb/util.rb +96 -0
- data/lib/hirb/version.rb +3 -0
- data/lib/hirb/view.rb +270 -0
- data/lib/hirb/views/couch_db.rb +11 -0
- data/lib/hirb/views/misc_db.rb +15 -0
- data/lib/hirb/views/mongo_db.rb +14 -0
- data/lib/hirb/views/orm.rb +11 -0
- data/lib/hirb/views/rails.rb +19 -0
- data/lib/hirb/views.rb +8 -0
- data/lib/hirb.rb +82 -0
- data/lib/ripl/hirb.rb +15 -0
- data/test/auto_table_test.rb +30 -0
- data/test/console_test.rb +27 -0
- data/test/deps.rip +4 -0
- data/test/dynamic_view_test.rb +94 -0
- data/test/formatter_test.rb +176 -0
- data/test/hirb_test.rb +39 -0
- data/test/import_test.rb +9 -0
- data/test/menu_test.rb +255 -0
- data/test/object_table_test.rb +79 -0
- data/test/pager_test.rb +162 -0
- data/test/resizer_test.rb +62 -0
- data/test/table_test.rb +630 -0
- data/test/test_helper.rb +61 -0
- data/test/tree_test.rb +184 -0
- data/test/util_test.rb +59 -0
- data/test/view_test.rb +165 -0
- data/test/views_test.rb +13 -0
- metadata +184 -0
@@ -0,0 +1,181 @@
|
|
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 <<self
|
38
|
+
# Main method which renders a tree.
|
39
|
+
# ==== Options:
|
40
|
+
# [:type] Type of tree. Either :basic, :directory or :number. Default is :basic.
|
41
|
+
# [:validate] Boolean to validate tree. Checks to see if all nodes have parents. Raises ParentlessNodeError if
|
42
|
+
# an invalid node is found. Default is false.
|
43
|
+
# [:indent] Number of spaces to indent between levels for basic + number trees. Default is 4.
|
44
|
+
# [:limit] Limits the level or depth of a tree that is displayed. Root node is level 0.
|
45
|
+
# [:description] Displays brief description about tree ie how many nodes it has.
|
46
|
+
# [:multi_line_nodes] Handles multi-lined nodes by indenting their newlines. Default is false.
|
47
|
+
# Examples:
|
48
|
+
# Hirb::Helpers::Tree.render([[0, 'root'], [1, 'child']], :type=>: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
|
@@ -0,0 +1,15 @@
|
|
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
|
@@ -0,0 +1,37 @@
|
|
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
|
data/lib/hirb/helpers.rb
ADDED
@@ -0,0 +1,18 @@
|
|
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
|
+
unicode_table tab_table}.each do |e|
|
17
|
+
require "hirb/helpers/#{e}"
|
18
|
+
end
|
data/lib/hirb/menu.rb
ADDED
@@ -0,0 +1,238 @@
|
|
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
|
+
ALL_ARG = '*'
|
15
|
+
DIRECTIONS = "Specify individual choices (4,7), range of choices (1-3) or all choices (*)."
|
16
|
+
|
17
|
+
|
18
|
+
# This method will return an array unless it's exited by simply pressing return, which returns nil.
|
19
|
+
# If given a block, the block will yield if and with any menu items are chosen.
|
20
|
+
# All options except for the ones below are passed to render the menu.
|
21
|
+
#
|
22
|
+
# ==== Options:
|
23
|
+
# [*:helper_class*] Helper class to render menu. Helper class is expected to implement numbering given a :number option.
|
24
|
+
# To use a very basic menu, set this to false. Defaults to Hirb::Helpers::AutoTable.
|
25
|
+
# [*:prompt*] String for menu prompt. Defaults to "Choose: ".
|
26
|
+
# [*:ask*] Always ask for input, even if there is only one choice. Default is true.
|
27
|
+
# [*:directions*] Display directions before prompt. Default is true.
|
28
|
+
# [*:readline*] Use readline to get user input if available. Input strings are added to readline history. Default is false.
|
29
|
+
# [*:two_d*] Turn menu into a 2 dimensional (2D) menu by allowing user to pick values from table cells. Default is false.
|
30
|
+
# [*:default_field*] Default field for a 2D menu. Defaults to first field in a table.
|
31
|
+
# [*:action*] Turn menu into an action menu by letting user pass menu choices as an argument to a method/command.
|
32
|
+
# A menu choice's place amongst other arguments is preserved. Default is false.
|
33
|
+
# [*:multi_action*] Execute action menu multiple times iterating over the menu choices. Default is false.
|
34
|
+
# [*:action_object*] Object that takes method/command calls. Default is main.
|
35
|
+
# [*:command*] Default method/command to call when no command given.
|
36
|
+
# [*:reopen*] Reopens $stdin with given file or with /dev/tty when set to true. Use when
|
37
|
+
# $stdin is already reading in piped data.
|
38
|
+
# Examples:
|
39
|
+
# >> extend Hirb::Console
|
40
|
+
# => self
|
41
|
+
# >> menu [1,2,3], :prompt=> "So many choices, so little time: "
|
42
|
+
# >> menu [{:a=>1, :b=>2}, {:a=>3, :b=>4}], :fields=>[:a,b], :two_d=>true)
|
43
|
+
def self.render(output, options={}, &block)
|
44
|
+
new(options).render(output, &block)
|
45
|
+
rescue Error=>e
|
46
|
+
$stderr.puts "Error: #{e.message}"
|
47
|
+
end
|
48
|
+
|
49
|
+
#:stopdoc:
|
50
|
+
def initialize(options={})
|
51
|
+
@options = {:helper_class=>Hirb::Helpers::AutoTable, :prompt=>"Choose: ", :ask=>true,
|
52
|
+
:directions=>true}.merge options
|
53
|
+
@options[:reopen] = '/dev/tty' if @options[:reopen] == true
|
54
|
+
end
|
55
|
+
|
56
|
+
def render(output, &block)
|
57
|
+
@output = Array(output)
|
58
|
+
return [] if @output.size.zero?
|
59
|
+
chosen = choose_from_menu
|
60
|
+
block.call(chosen) if block && chosen.size > 0
|
61
|
+
@options[:action] ? execute_action(chosen) : chosen
|
62
|
+
end
|
63
|
+
|
64
|
+
def get_input
|
65
|
+
prompt = pre_prompt + @options[:prompt]
|
66
|
+
prompt = DIRECTIONS+"\n"+prompt if @options[:directions]
|
67
|
+
$stdin.reopen @options[:reopen] if @options[:reopen]
|
68
|
+
|
69
|
+
if @options[:readline] && readline_loads?
|
70
|
+
get_readline_input(prompt)
|
71
|
+
else
|
72
|
+
print prompt
|
73
|
+
$stdin.gets.chomp.strip
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def get_readline_input(prompt)
|
78
|
+
input = Readline.readline prompt
|
79
|
+
Readline::HISTORY << input
|
80
|
+
input
|
81
|
+
end
|
82
|
+
|
83
|
+
def pre_prompt
|
84
|
+
prompt = ''
|
85
|
+
prompt << "Default field: #{default_field}\n" if @options[:two_d] && default_field
|
86
|
+
prompt << "Default command: #{@options[:command]}\n" if @options[:action] && @options[:command]
|
87
|
+
prompt
|
88
|
+
end
|
89
|
+
|
90
|
+
def choose_from_menu
|
91
|
+
return unasked_choice if @output.size == 1 && !@options[:ask]
|
92
|
+
|
93
|
+
if (helper_class = Util.any_const_get(@options[:helper_class]))
|
94
|
+
View.render_output(@output, :class=>@options[:helper_class], :options=>@options.merge(:number=>true))
|
95
|
+
else
|
96
|
+
@output.each_with_index {|e,i| puts "#{i+1}: #{e}" }
|
97
|
+
end
|
98
|
+
|
99
|
+
parse_input get_input
|
100
|
+
end
|
101
|
+
|
102
|
+
def unasked_choice
|
103
|
+
return @output unless @options[:action]
|
104
|
+
raise(Error, "Default command and field required for unasked action menu") unless default_field && @options[:command]
|
105
|
+
@new_args = [@options[:command], CHOSEN_ARG]
|
106
|
+
map_tokens([[@output, default_field]])
|
107
|
+
end
|
108
|
+
|
109
|
+
def execute_action(chosen)
|
110
|
+
return nil if chosen.size.zero?
|
111
|
+
if @options[:multi_action]
|
112
|
+
chosen.each {|e| invoke command, add_chosen_to_args(e) }
|
113
|
+
else
|
114
|
+
invoke command, add_chosen_to_args(chosen)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def invoke(cmd, args)
|
119
|
+
action_object.send(cmd, *args)
|
120
|
+
end
|
121
|
+
|
122
|
+
def parse_input(input)
|
123
|
+
if (@options[:two_d] || @options[:action])
|
124
|
+
tokens = input_to_tokens(input)
|
125
|
+
map_tokens(tokens)
|
126
|
+
else
|
127
|
+
Util.choose_from_array(@output, input)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def map_tokens(tokens)
|
132
|
+
if return_cell_values?
|
133
|
+
@output[0].is_a?(Hash) ? map_hash_of_tokens(tokens) : map_array_of_tokens(tokens)
|
134
|
+
else
|
135
|
+
map_all_args(tokens) { |arr, f| arr[0] }
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def return_cell_values?
|
140
|
+
@options[:two_d]
|
141
|
+
end
|
142
|
+
|
143
|
+
def map_all_args(tokens)
|
144
|
+
tokens.map { |t| t == ALL_ARG ? @output : t }
|
145
|
+
tokens.map { |arr,f|
|
146
|
+
arr == ALL_ARG ? @output : yield(arr, f)
|
147
|
+
}.flatten
|
148
|
+
end
|
149
|
+
|
150
|
+
def map_hash_of_tokens(tokens)
|
151
|
+
map_all_args(tokens) { |arr,f| arr.map {|e| e[f]} }
|
152
|
+
end
|
153
|
+
|
154
|
+
def map_array_of_tokens(tokens)
|
155
|
+
map_all_args(tokens) { |arr,f| arr.map {|e| e.is_a?(Array) && f.is_a?(Integer) ? e[f] : e.send(f) } }
|
156
|
+
end
|
157
|
+
|
158
|
+
def input_to_tokens(input)
|
159
|
+
@new_args = []
|
160
|
+
tokens = (@args = split_input_args(input)).map {|word| parse_word(word) }.compact
|
161
|
+
cleanup_new_args
|
162
|
+
tokens
|
163
|
+
end
|
164
|
+
|
165
|
+
def parse_word(word)
|
166
|
+
if word[CHOSEN_REGEXP]
|
167
|
+
@new_args << CHOSEN_ARG
|
168
|
+
field = $3 ? unalias_field($3) : default_field ||
|
169
|
+
raise(Error, "No default field/column found. Fields must be explicitly picked.")
|
170
|
+
[Util.choose_from_array(@output, word), field ]
|
171
|
+
elsif word.index(ALL_ARG)
|
172
|
+
@new_args << CHOSEN_ARG
|
173
|
+
ALL_ARG
|
174
|
+
else
|
175
|
+
@new_args << word
|
176
|
+
nil
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def cleanup_new_args
|
181
|
+
if @new_args.all? {|e| e == CHOSEN_ARG }
|
182
|
+
@new_args = [CHOSEN_ARG]
|
183
|
+
elsif @new_args.index(ALL_ARG)
|
184
|
+
else
|
185
|
+
i = @new_args.index(CHOSEN_ARG) || raise(Error, "No rows chosen")
|
186
|
+
@new_args.delete(CHOSEN_ARG)
|
187
|
+
@new_args.insert(i, CHOSEN_ARG)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def add_chosen_to_args(items)
|
192
|
+
args = @new_args.dup
|
193
|
+
args[args.index(CHOSEN_ARG)] = items
|
194
|
+
args
|
195
|
+
end
|
196
|
+
|
197
|
+
def command
|
198
|
+
@command ||= begin
|
199
|
+
cmd = (@new_args == [CHOSEN_ARG]) ? nil : @new_args.shift
|
200
|
+
cmd ||= @options[:command] || raise(Error, "No command given for action menu")
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def action_object
|
205
|
+
@options[:action_object] || eval("self", TOPLEVEL_BINDING)
|
206
|
+
end
|
207
|
+
|
208
|
+
def split_input_args(input)
|
209
|
+
input.split(/\s+/)
|
210
|
+
end
|
211
|
+
|
212
|
+
def default_field
|
213
|
+
@default_field ||= @options[:default_field] || fields[0]
|
214
|
+
end
|
215
|
+
|
216
|
+
# Has to be called after displaying menu
|
217
|
+
def fields
|
218
|
+
@fields ||= @options[:fields] || (@options[:ask] && table_helper_class? && Helpers::Table.last_table ?
|
219
|
+
Helpers::Table.last_table.fields[1..-1] : [])
|
220
|
+
end
|
221
|
+
|
222
|
+
def table_helper_class?
|
223
|
+
@options[:helper_class].is_a?(Class) && @options[:helper_class] < Helpers::Table
|
224
|
+
end
|
225
|
+
|
226
|
+
def unalias_field(field)
|
227
|
+
fields.sort_by {|e| e.to_s }.find {|e| e.to_s[/^#{field}/] } || raise(Error, "Invalid field '#{field}'")
|
228
|
+
end
|
229
|
+
|
230
|
+
def readline_loads?
|
231
|
+
require 'readline'
|
232
|
+
true
|
233
|
+
rescue LoadError
|
234
|
+
false
|
235
|
+
end
|
236
|
+
#:startdoc:
|
237
|
+
end
|
238
|
+
end
|
data/lib/hirb/pager.rb
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
module Hirb
|
2
|
+
# This class provides class methods for paging and an object which can conditionally page given a terminal size that is exceeded.
|
3
|
+
class Pager
|
4
|
+
class<<self
|
5
|
+
# Pages using a configured or detected shell command.
|
6
|
+
def command_pager(output, options={})
|
7
|
+
basic_pager(output) if valid_pager_command?(options[:pager_command])
|
8
|
+
end
|
9
|
+
|
10
|
+
def pager_command(*commands) #:nodoc:
|
11
|
+
@pager_command = (!@pager_command.nil? && commands.empty?) ? @pager_command :
|
12
|
+
begin
|
13
|
+
commands = [ENV['PAGER'], 'less', 'more', 'pager'] if commands.empty?
|
14
|
+
commands.compact.uniq.find {|e| Util.command_exists?(e[/\w+/]) }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Pages with a ruby-only pager which either pages or quits.
|
19
|
+
def default_pager(output, options={})
|
20
|
+
pager = new(options[:width], options[:height])
|
21
|
+
while pager.activated_by?(output, options[:inspect])
|
22
|
+
puts pager.slice!(output, options[:inspect])
|
23
|
+
return unless continue_paging?
|
24
|
+
end
|
25
|
+
puts output
|
26
|
+
puts "=== Pager finished. ==="
|
27
|
+
end
|
28
|
+
|
29
|
+
#:stopdoc:
|
30
|
+
def valid_pager_command?(cmd)
|
31
|
+
cmd ? pager_command(cmd) : pager_command
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
def basic_pager(output)
|
36
|
+
pager = IO.popen(pager_command, "w")
|
37
|
+
begin
|
38
|
+
save_stdout = STDOUT.clone
|
39
|
+
STDOUT.reopen(pager)
|
40
|
+
STDOUT.puts output
|
41
|
+
rescue Errno::EPIPE
|
42
|
+
ensure
|
43
|
+
STDOUT.reopen(save_stdout)
|
44
|
+
save_stdout.close
|
45
|
+
pager.close
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def continue_paging?
|
50
|
+
puts "=== Press enter/return to continue or q to quit: ==="
|
51
|
+
!$stdin.gets.chomp[/q/i]
|
52
|
+
end
|
53
|
+
#:startdoc:
|
54
|
+
end
|
55
|
+
|
56
|
+
attr_reader :width, :height
|
57
|
+
|
58
|
+
def initialize(width, height, options={})
|
59
|
+
resize(width, height)
|
60
|
+
@pager_command = options[:pager_command] if options[:pager_command]
|
61
|
+
end
|
62
|
+
|
63
|
+
# Pages given string using configured pager.
|
64
|
+
def page(string, inspect_mode)
|
65
|
+
if self.class.valid_pager_command?(@pager_command)
|
66
|
+
self.class.command_pager(string, :pager_command=>@pager_command)
|
67
|
+
else
|
68
|
+
self.class.default_pager(string, :width=>@width, :height=>@height, :inspect=>inspect_mode)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def slice!(output, inspect_mode=false) #:nodoc:
|
73
|
+
effective_height = @height - 2 # takes into account pager prompt
|
74
|
+
if inspect_mode
|
75
|
+
sliced_output = String.slice(output, 0, @width * effective_height)
|
76
|
+
output.replace String.slice(output, char_count(sliced_output), String.size(output))
|
77
|
+
sliced_output
|
78
|
+
else
|
79
|
+
# could use output.scan(/[^\n]*\n?/) instead of split
|
80
|
+
sliced_output = output.split("\n").slice(0, effective_height).join("\n")
|
81
|
+
output.replace output.split("\n").slice(effective_height..-1).join("\n")
|
82
|
+
sliced_output
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Determines if string should be paged based on configured width and height.
|
87
|
+
def activated_by?(string_to_page, inspect_mode=false)
|
88
|
+
inspect_mode ? (String.size(string_to_page) > @height * @width) : (string_to_page.count("\n") > @height)
|
89
|
+
end
|
90
|
+
|
91
|
+
if String.method_defined? :chars
|
92
|
+
def char_count(string) #:nodoc:
|
93
|
+
string.chars.count
|
94
|
+
end
|
95
|
+
else
|
96
|
+
def char_count(string) #:nodoc:
|
97
|
+
String.size(string)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def resize(width, height) #:nodoc:
|
102
|
+
@width, @height = View.determine_terminal_size(width, height)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
data/lib/hirb/string.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
module Hirb
|
2
|
+
# Provides string helpers to deal with UTF-8 and ruby 1.8.x
|
3
|
+
module String
|
4
|
+
extend self
|
5
|
+
# :stopdoc:
|
6
|
+
if RUBY_VERSION < '1.9'
|
7
|
+
def size(string)
|
8
|
+
string.scan(/./).length
|
9
|
+
end
|
10
|
+
|
11
|
+
def ljust(string, desired_length)
|
12
|
+
leftover = desired_length - size(string)
|
13
|
+
leftover > 0 ? string + " " * leftover : string
|
14
|
+
end
|
15
|
+
|
16
|
+
def rjust(string, desired_length)
|
17
|
+
leftover = desired_length - size(string)
|
18
|
+
leftover > 0 ? " " * leftover + string : string
|
19
|
+
end
|
20
|
+
|
21
|
+
def slice(string, start, finish)
|
22
|
+
string.scan(/./).slice(start, finish).join('')
|
23
|
+
end
|
24
|
+
else
|
25
|
+
def size(string)
|
26
|
+
string.length
|
27
|
+
end
|
28
|
+
|
29
|
+
def ljust(string, desired_length)
|
30
|
+
string.ljust(desired_length)
|
31
|
+
end
|
32
|
+
|
33
|
+
def rjust(string, desired_length)
|
34
|
+
string.rjust(desired_length)
|
35
|
+
end
|
36
|
+
|
37
|
+
def slice(*args)
|
38
|
+
string = args.shift
|
39
|
+
string.slice(*args)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
#:startdoc:
|
43
|
+
end
|
44
|
+
end
|