tty 0.0.6 → 0.0.7
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +78 -12
- data/benchmarks/shell.rb +26 -0
- data/benchmarks/table.rb +35 -0
- data/lib/tty.rb +23 -1
- data/lib/tty/coercer.rb +13 -0
- data/lib/tty/coercer/boolean.rb +39 -0
- data/lib/tty/coercer/float.rb +23 -0
- data/lib/tty/coercer/integer.rb +23 -0
- data/lib/tty/coercer/range.rb +33 -0
- data/lib/tty/shell.rb +6 -2
- data/lib/tty/shell/question.rb +158 -138
- data/lib/tty/shell/reader.rb +92 -0
- data/lib/tty/shell/response.rb +219 -0
- data/lib/tty/shell/response_delegation.rb +53 -0
- data/lib/tty/table.rb +90 -16
- data/lib/tty/table/border.rb +34 -8
- data/lib/tty/table/border/ascii.rb +16 -25
- data/lib/tty/table/border/null.rb +0 -6
- data/lib/tty/table/border/unicode.rb +16 -25
- data/lib/tty/table/column_set.rb +1 -1
- data/lib/tty/table/error.rb +10 -0
- data/lib/tty/table/operation/wrapped.rb +0 -6
- data/lib/tty/table/orientation.rb +57 -0
- data/lib/tty/table/orientation/horizontal.rb +19 -0
- data/lib/tty/table/orientation/vertical.rb +19 -0
- data/lib/tty/table/renderer.rb +7 -0
- data/lib/tty/table/renderer/ascii.rb +1 -1
- data/lib/tty/table/renderer/basic.rb +2 -2
- data/lib/tty/table/renderer/unicode.rb +1 -1
- data/lib/tty/table/validatable.rb +20 -0
- data/lib/tty/terminal.rb +15 -14
- data/lib/tty/terminal/color.rb +1 -1
- data/lib/tty/terminal/echo.rb +41 -0
- data/lib/tty/terminal/home.rb +31 -0
- data/lib/tty/text.rb +85 -0
- data/lib/tty/text/truncation.rb +83 -0
- data/lib/tty/text/wrapping.rb +96 -0
- data/lib/tty/version.rb +1 -1
- data/spec/tty/coercer/boolean/coerce_spec.rb +113 -0
- data/spec/tty/coercer/float/coerce_spec.rb +32 -0
- data/spec/tty/coercer/integer/coerce_spec.rb +39 -0
- data/spec/tty/coercer/range/coerce_spec.rb +73 -0
- data/spec/tty/shell/ask_spec.rb +14 -1
- data/spec/tty/shell/question/argument_spec.rb +30 -0
- data/spec/tty/shell/question/character_spec.rb +16 -0
- data/spec/tty/shell/question/default_spec.rb +25 -0
- data/spec/tty/shell/question/in_spec.rb +23 -0
- data/spec/tty/shell/question/initialize_spec.rb +11 -211
- data/spec/tty/shell/question/modifier/whitespace_spec.rb +1 -1
- data/spec/tty/shell/question/modify_spec.rb +44 -0
- data/spec/tty/shell/question/valid_spec.rb +46 -0
- data/spec/tty/shell/question/validate_spec.rb +30 -0
- data/spec/tty/shell/reader/getc_spec.rb +40 -0
- data/spec/tty/shell/response/read_bool_spec.rb +41 -0
- data/spec/tty/shell/response/read_char_spec.rb +17 -0
- data/spec/tty/shell/response/read_date_spec.rb +20 -0
- data/spec/tty/shell/response/read_email_spec.rb +43 -0
- data/spec/tty/shell/response/read_multiple_spec.rb +24 -0
- data/spec/tty/shell/response/read_number_spec.rb +29 -0
- data/spec/tty/shell/response/read_range_spec.rb +29 -0
- data/spec/tty/shell/response/read_spec.rb +68 -0
- data/spec/tty/shell/response/read_string_spec.rb +19 -0
- data/spec/tty/table/access_spec.rb +6 -0
- data/spec/tty/table/border/new_spec.rb +3 -3
- data/spec/tty/table/initialize_spec.rb +17 -1
- data/spec/tty/table/options_spec.rb +7 -1
- data/spec/tty/table/orientation_spec.rb +98 -0
- data/spec/tty/table/renders_with_spec.rb +76 -0
- data/spec/tty/table/rotate_spec.rb +72 -0
- data/spec/tty/table/to_s_spec.rb +13 -1
- data/spec/tty/table/validatable/validate_options_spec.rb +34 -0
- data/spec/tty/terminal/color/remove_spec.rb +34 -1
- data/spec/tty/terminal/echo_spec.rb +22 -0
- data/spec/tty/text/truncate_spec.rb +13 -0
- data/spec/tty/text/truncation/initialize_spec.rb +29 -0
- data/spec/tty/text/truncation/truncate_spec.rb +73 -0
- data/spec/tty/text/wrap_spec.rb +14 -0
- data/spec/tty/text/wrapping/initialize_spec.rb +25 -0
- data/spec/tty/text/wrapping/wrap_spec.rb +80 -0
- data/tty.gemspec +1 -0
- metadata +101 -8
@@ -0,0 +1,92 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
module TTY
|
4
|
+
class Shell
|
5
|
+
|
6
|
+
# A class responsible for reading character input from STDIN
|
7
|
+
class Reader
|
8
|
+
|
9
|
+
# @api private
|
10
|
+
attr_reader :shell
|
11
|
+
private :shell
|
12
|
+
|
13
|
+
# Key input constants for decimal codes
|
14
|
+
CARRIAGE_RETURN = 13.freeze
|
15
|
+
NEWLINE = 10.freeze
|
16
|
+
BACKSPACE = 127.freeze
|
17
|
+
DELETE = 8.freeze
|
18
|
+
|
19
|
+
# Initialize a Reader
|
20
|
+
#
|
21
|
+
# @api public
|
22
|
+
def initialize(shell=nil)
|
23
|
+
@shell = shell || Shell.new
|
24
|
+
end
|
25
|
+
|
26
|
+
# Get input in unbuffered mode.
|
27
|
+
#
|
28
|
+
#
|
29
|
+
# @api bublic
|
30
|
+
def buffer(&block)
|
31
|
+
bufferring = shell.output.sync
|
32
|
+
# Immediately flush output
|
33
|
+
shell.output.sync = true
|
34
|
+
|
35
|
+
value = block.call if block_given?
|
36
|
+
|
37
|
+
shell.output.sync = bufferring
|
38
|
+
value
|
39
|
+
end
|
40
|
+
|
41
|
+
# Get a value from STDIN one key at a time. Each key press is echoed back
|
42
|
+
# to the shell masked with character(if given). The input finishes when
|
43
|
+
# enter key is pressed.
|
44
|
+
#
|
45
|
+
# @param [String] mask
|
46
|
+
# the character to use as mask
|
47
|
+
#
|
48
|
+
# @return [String]
|
49
|
+
#
|
50
|
+
# @api public
|
51
|
+
def getc(mask=(not_set=true))
|
52
|
+
value = ""
|
53
|
+
|
54
|
+
buffer do
|
55
|
+
begin
|
56
|
+
while (char = shell.input.getbyte) and
|
57
|
+
!(char == CARRIAGE_RETURN || char == NEWLINE)
|
58
|
+
|
59
|
+
if (char == BACKSPACE || char == DELETE)
|
60
|
+
value.slice!(-1, 1) unless value.empty?
|
61
|
+
else
|
62
|
+
print_char char, not_set, mask
|
63
|
+
value << char
|
64
|
+
end
|
65
|
+
end
|
66
|
+
ensure
|
67
|
+
TTY.terminal.echo_on
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
value
|
72
|
+
end
|
73
|
+
|
74
|
+
# Get a value from STDIN using line input.
|
75
|
+
#
|
76
|
+
# @api public
|
77
|
+
def gets
|
78
|
+
shell.input.gets
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
# Print out character back to shell STDOUT
|
84
|
+
#
|
85
|
+
# @api private
|
86
|
+
def print_char(char, not_set, mask)
|
87
|
+
shell.output.putc((not_set || !mask) ? char : mask)
|
88
|
+
end
|
89
|
+
|
90
|
+
end # Reader
|
91
|
+
end # Shell
|
92
|
+
end # TTY
|
@@ -0,0 +1,219 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'date'
|
4
|
+
|
5
|
+
module TTY
|
6
|
+
# A class responsible for shell prompt interactions.
|
7
|
+
class Shell
|
8
|
+
|
9
|
+
# A class representing a question.
|
10
|
+
class Response
|
11
|
+
|
12
|
+
VALID_TYPES = [:boolean, :string, :symbol, :integer, :float, :date, :datetime]
|
13
|
+
attr_reader :reader
|
14
|
+
private :reader
|
15
|
+
|
16
|
+
attr_reader :shell
|
17
|
+
private :shell
|
18
|
+
|
19
|
+
attr_reader :question
|
20
|
+
private :question
|
21
|
+
|
22
|
+
# Initialize a Response
|
23
|
+
#
|
24
|
+
# @api public
|
25
|
+
def initialize(question, shell=nil)
|
26
|
+
@question = question
|
27
|
+
@shell = shell || Shell.new
|
28
|
+
@reader = Reader.new(shell)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Read input from STDIN either character or line
|
32
|
+
#
|
33
|
+
# @param [Symbol] type
|
34
|
+
#
|
35
|
+
# @return [undefined]
|
36
|
+
#
|
37
|
+
# @api public
|
38
|
+
def read(type=nil)
|
39
|
+
question.evaluate_response read_input
|
40
|
+
end
|
41
|
+
|
42
|
+
# @api private
|
43
|
+
def read_input
|
44
|
+
reader = Reader.new(shell)
|
45
|
+
|
46
|
+
if question.mask? && question.echo?
|
47
|
+
reader.getc(question.mask)
|
48
|
+
else
|
49
|
+
TTY.terminal.echo(question.echo) {
|
50
|
+
question.character? ? reader.getc(question.mask) : reader.gets
|
51
|
+
}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Read answer and cast to String type
|
56
|
+
#
|
57
|
+
# @param [String] error
|
58
|
+
# error to display on failed conversion to string type
|
59
|
+
#
|
60
|
+
# @api public
|
61
|
+
def read_string(error=nil)
|
62
|
+
question.evaluate_response String(read_input)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Read answer's first character
|
66
|
+
#
|
67
|
+
# @api public
|
68
|
+
def read_char
|
69
|
+
question.character true
|
70
|
+
question.evaluate_response String(read_input).chars.to_a[0]
|
71
|
+
end
|
72
|
+
|
73
|
+
# Read multiple line answer and cast to String type
|
74
|
+
#
|
75
|
+
# @api public
|
76
|
+
def read_text
|
77
|
+
question.evaluate_response String(read_input)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Read ansewr and cast to Symbol type
|
81
|
+
#
|
82
|
+
# @api public
|
83
|
+
def read_symbol(error=nil)
|
84
|
+
question.evaluate_response read_input.to_sym
|
85
|
+
end
|
86
|
+
|
87
|
+
# Read answer from predifined choicse
|
88
|
+
#
|
89
|
+
# @api public
|
90
|
+
def read_choice(type=nil)
|
91
|
+
question.argument(:required) unless question.default?
|
92
|
+
question.evaluate_response read_input
|
93
|
+
end
|
94
|
+
|
95
|
+
# Read integer value
|
96
|
+
#
|
97
|
+
# @api public
|
98
|
+
def read_int(error=nil)
|
99
|
+
question.evaluate_response TTY::Coercer::Integer.coerce(read_input)
|
100
|
+
end
|
101
|
+
|
102
|
+
# Read float value
|
103
|
+
#
|
104
|
+
# @api public
|
105
|
+
def read_float(error=nil)
|
106
|
+
question.evaluate_response TTY::Coercer::Float.coerce(read_input)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Read regular expression
|
110
|
+
#
|
111
|
+
# @api public
|
112
|
+
def read_regex(error=nil)
|
113
|
+
question.evaluate_response Kernel.send(:Regex, read_input)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Read range expression
|
117
|
+
#
|
118
|
+
# @api public
|
119
|
+
def read_range
|
120
|
+
question.evaluate_response TTY::Coercer::Range.coerce(read_input)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Read date
|
124
|
+
#
|
125
|
+
# @api public
|
126
|
+
def read_date
|
127
|
+
question.evaluate_response Date.parse(read_input)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Read datetime
|
131
|
+
#
|
132
|
+
# @api public
|
133
|
+
def read_datetime
|
134
|
+
question.evaluate_response DateTime.parse(read_input)
|
135
|
+
end
|
136
|
+
|
137
|
+
# Read boolean
|
138
|
+
#
|
139
|
+
# @api public
|
140
|
+
def read_bool(error=nil)
|
141
|
+
question.evaluate_response TTY::Coercer::Boolean.coerce read_input
|
142
|
+
end
|
143
|
+
|
144
|
+
# Read file contents
|
145
|
+
#
|
146
|
+
# @api public
|
147
|
+
def read_file(error=nil)
|
148
|
+
question.evaluate_response File.open(File.join(directory, read_input))
|
149
|
+
end
|
150
|
+
|
151
|
+
# Read string answer and validate against email regex
|
152
|
+
#
|
153
|
+
# @return [String]
|
154
|
+
#
|
155
|
+
# @api public
|
156
|
+
def read_email
|
157
|
+
question.validate(/^[a-z0-9._%+-]+@([a-z0-9-]+\.)+[a-z]{2,6}$/i)
|
158
|
+
if question.error
|
159
|
+
question.prompt question.statement
|
160
|
+
end
|
161
|
+
with_exception { read_string }
|
162
|
+
end
|
163
|
+
|
164
|
+
# Read answer provided on multiple lines
|
165
|
+
#
|
166
|
+
# @api public
|
167
|
+
def read_multiple
|
168
|
+
response = ""
|
169
|
+
loop do
|
170
|
+
value = question.evaluate_response read_input
|
171
|
+
break if !value || value == ""
|
172
|
+
next if value !~ /\S/
|
173
|
+
response << value
|
174
|
+
end
|
175
|
+
response
|
176
|
+
end
|
177
|
+
|
178
|
+
# Read password
|
179
|
+
#
|
180
|
+
# @api public
|
181
|
+
def read_password
|
182
|
+
question.echo false
|
183
|
+
question.evaluate_response read_input
|
184
|
+
end
|
185
|
+
|
186
|
+
private
|
187
|
+
|
188
|
+
# Ignore exception
|
189
|
+
#
|
190
|
+
# @api private
|
191
|
+
def with_exception(&block)
|
192
|
+
yield
|
193
|
+
rescue
|
194
|
+
question.error? ? block.call : raise
|
195
|
+
end
|
196
|
+
|
197
|
+
# @param [Symbol] type
|
198
|
+
# :boolean, :string, :numeric, :array
|
199
|
+
#
|
200
|
+
# @api private
|
201
|
+
def read_type(class_or_name)
|
202
|
+
raise TypeError, "Type #{type} is not valid" if type && !valid_type?(type)
|
203
|
+
case type
|
204
|
+
when :string, ::String
|
205
|
+
read_string
|
206
|
+
when :symbol, ::Symbol
|
207
|
+
read_symbol
|
208
|
+
when :float, ::Float
|
209
|
+
read_float
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def valid_type?(type)
|
214
|
+
self.class::VALID_TYPES.include? type.to_sym
|
215
|
+
end
|
216
|
+
|
217
|
+
end # Response
|
218
|
+
end # Shell
|
219
|
+
end # TTY
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
module TTY
|
4
|
+
class Shell
|
5
|
+
module ResponseDelegation
|
6
|
+
extend TTY::Delegatable
|
7
|
+
|
8
|
+
delegatable_method :dispatch, :read
|
9
|
+
|
10
|
+
delegatable_method :dispatch, :read_string
|
11
|
+
|
12
|
+
delegatable_method :dispatch, :read_char
|
13
|
+
|
14
|
+
delegatable_method :dispatch, :read_text
|
15
|
+
|
16
|
+
delegatable_method :dispatch, :read_symbol
|
17
|
+
|
18
|
+
delegatable_method :dispatch, :read_choice
|
19
|
+
|
20
|
+
delegatable_method :dispatch, :read_int
|
21
|
+
|
22
|
+
delegatable_method :dispatch, :read_float
|
23
|
+
|
24
|
+
delegatable_method :dispatch, :read_regex
|
25
|
+
|
26
|
+
delegatable_method :dispatch, :read_range
|
27
|
+
|
28
|
+
delegatable_method :dispatch, :read_date
|
29
|
+
|
30
|
+
delegatable_method :dispatch, :read_datetime
|
31
|
+
|
32
|
+
delegatable_method :dispatch, :read_bool
|
33
|
+
|
34
|
+
delegatable_method :dispatch, :read_file
|
35
|
+
|
36
|
+
delegatable_method :dispatch, :read_email
|
37
|
+
|
38
|
+
delegatable_method :dispatch, :read_multiple
|
39
|
+
|
40
|
+
delegatable_method :dispatch, :read_password
|
41
|
+
|
42
|
+
# Create response instance when question readed is invoked
|
43
|
+
#
|
44
|
+
# @param [TTY::Shell::Response] response
|
45
|
+
#
|
46
|
+
# @api private
|
47
|
+
def dispatch(response = Response.new(self, shell))
|
48
|
+
@response ||= response
|
49
|
+
end
|
50
|
+
|
51
|
+
end # ResponseDelegation
|
52
|
+
end # Shell
|
53
|
+
end # TTY
|
data/lib/tty/table.rb
CHANGED
@@ -40,16 +40,22 @@ module TTY
|
|
40
40
|
# @api private
|
41
41
|
attr_reader :alignments
|
42
42
|
|
43
|
+
# The table border class
|
44
|
+
#
|
45
|
+
# @api private
|
46
|
+
attr_reader :border_class
|
47
|
+
|
48
|
+
# The table orientation out of :horizontal and :vertical
|
49
|
+
#
|
50
|
+
# @reutnr [TTY::Table::Orientation]
|
51
|
+
#
|
52
|
+
# @api public
|
53
|
+
attr_reader :orientation
|
54
|
+
|
43
55
|
# Subset of safe methods that both Array and Hash implement
|
44
56
|
def_delegators(:@rows, :[], :assoc, :flatten, :include?, :index,
|
45
57
|
:length, :select, :to_a, :values_at, :pretty_print, :rassoc)
|
46
58
|
|
47
|
-
# The table orientation
|
48
|
-
#
|
49
|
-
def direction
|
50
|
-
# TODO implement table orientation
|
51
|
-
end
|
52
|
-
|
53
59
|
# Create a new Table where each argument is a row
|
54
60
|
#
|
55
61
|
# @example
|
@@ -74,20 +80,36 @@ module TTY
|
|
74
80
|
# rows = [ ['a1', 'a2'], ['b1', 'b2'] ]
|
75
81
|
# table = Table.new :header => ['Header 1', 'Header 2'], :rows => rows
|
76
82
|
#
|
83
|
+
# @example of parameters passed as hash
|
84
|
+
# Table.new [ {'Header1' => ['a1','a2'], 'Header2' => ['b1', 'b2'] }] }
|
85
|
+
#
|
77
86
|
# @param [Array[Symbol], Hash] *args
|
78
87
|
#
|
79
88
|
# @api public
|
80
89
|
def self.new(*args, &block)
|
81
90
|
options = Utils.extract_options!(args)
|
82
91
|
if args.size.nonzero?
|
83
|
-
|
84
|
-
header = args.size.zero? ? nil : args.first
|
85
|
-
super({:header => header, :rows => rows}.merge(options), &block)
|
92
|
+
super(extract_tuples(args).merge(options), &block)
|
86
93
|
else
|
87
94
|
super(options, &block)
|
88
95
|
end
|
89
96
|
end
|
90
97
|
|
98
|
+
# Extract header and row tuples from arguments
|
99
|
+
#
|
100
|
+
# @param [Array] args
|
101
|
+
#
|
102
|
+
# @api private
|
103
|
+
def self.extract_tuples(args)
|
104
|
+
rows = args.pop
|
105
|
+
header = args.size.zero? ? nil : args.first
|
106
|
+
if rows.first.is_a?(Hash)
|
107
|
+
header = rows.map(&:keys).flatten.uniq
|
108
|
+
rows = rows.inject([]) { |arr, el| arr + el.values }
|
109
|
+
end
|
110
|
+
{ :header => header, :rows => rows }
|
111
|
+
end
|
112
|
+
|
91
113
|
# Initialize a Table
|
92
114
|
#
|
93
115
|
# @param [Hash] options
|
@@ -107,26 +129,78 @@ module TTY
|
|
107
129
|
#
|
108
130
|
# @api private
|
109
131
|
def initialize(options={}, &block)
|
110
|
-
|
111
|
-
|
132
|
+
validate_options! options
|
133
|
+
|
134
|
+
@header = options.fetch(:header) { nil }
|
135
|
+
@rows = coerce options.fetch(:rows) { [] }
|
112
136
|
@renderer = pick_renderer options[:renderer]
|
137
|
+
@orientation = Orientation.coerce options.fetch(:orientation) { :horizontal }
|
113
138
|
# TODO: assert that row_size is the same as column widths & aligns
|
114
|
-
|
115
|
-
@
|
116
|
-
@alignments = Operation::AlignmentSet.new options[:column_aligns] || []
|
139
|
+
@column_widths = Array(options.delete(:column_widths)).map(&:to_i)
|
140
|
+
@alignments = Operation::AlignmentSet.new Array(options.delete(:column_aligns)).map(&:to_sym)
|
117
141
|
|
118
142
|
assert_row_sizes @rows
|
143
|
+
@orientation.transform(self)
|
119
144
|
yield_or_eval &block if block_given?
|
120
145
|
end
|
121
146
|
|
147
|
+
# Sets table orientation
|
148
|
+
#
|
149
|
+
# @param [String,Symbol] value
|
150
|
+
#
|
151
|
+
# @api public
|
152
|
+
def orientation=(value)
|
153
|
+
@orientation = Orientation.coerce value
|
154
|
+
end
|
155
|
+
|
156
|
+
# Marks this table as rotated
|
157
|
+
#
|
158
|
+
# @api public
|
159
|
+
def rotated?
|
160
|
+
@rotated
|
161
|
+
end
|
162
|
+
|
163
|
+
# Rotate the table between vertical and horizontal orientation
|
164
|
+
#
|
165
|
+
# @return [self]
|
166
|
+
#
|
167
|
+
# @api private
|
168
|
+
def rotate
|
169
|
+
orientation.transform(self)
|
170
|
+
self
|
171
|
+
end
|
172
|
+
|
173
|
+
# Rotate the table vertically
|
174
|
+
#
|
175
|
+
# @api private
|
176
|
+
def rotate_vertical
|
177
|
+
@rows = ([header].compact + rows).transpose
|
178
|
+
@header = [] if header
|
179
|
+
@rotated = true
|
180
|
+
end
|
181
|
+
|
182
|
+
# Rotate the table horizontally
|
183
|
+
#
|
184
|
+
# @api private
|
185
|
+
def rotate_horizontal
|
186
|
+
transposed = rows.transpose
|
187
|
+
if header && header.empty?
|
188
|
+
@rows = transposed[1..-1]
|
189
|
+
@header = transposed[0]
|
190
|
+
elsif rotated?
|
191
|
+
@rows = transposed
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
122
195
|
# Lookup element of the table given a row(i) and column(j)
|
123
196
|
#
|
124
197
|
# @api public
|
125
|
-
def [](i, j)
|
198
|
+
def [](i, j=false)
|
199
|
+
return row(i) unless j
|
126
200
|
if i >= 0 && j >= 0
|
127
201
|
rows.fetch(i){return nil}[j]
|
128
202
|
else
|
129
|
-
raise
|
203
|
+
raise TTY::Table::TupleMissing.new(i,j)
|
130
204
|
end
|
131
205
|
end
|
132
206
|
alias at []
|