tty 0.0.10 → 0.0.11

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. data/README.md +75 -19
  2. data/lib/tty.rb +7 -0
  3. data/lib/tty/shell/question.rb +3 -3
  4. data/lib/tty/shell/response.rb +5 -5
  5. data/lib/tty/table.rb +51 -19
  6. data/lib/tty/table/column_set.rb +42 -1
  7. data/lib/tty/table/columns.rb +167 -0
  8. data/lib/tty/table/field.rb +2 -2
  9. data/lib/tty/table/indentation.rb +54 -0
  10. data/lib/tty/table/operation/escape.rb +1 -1
  11. data/lib/tty/table/operation/filter.rb +0 -1
  12. data/lib/tty/table/operation/padding.rb +95 -0
  13. data/lib/tty/table/operation/wrapped.rb +6 -3
  14. data/lib/tty/table/operations.rb +3 -2
  15. data/lib/tty/table/orientation/horizontal.rb +27 -1
  16. data/lib/tty/table/orientation/vertical.rb +17 -1
  17. data/lib/tty/table/padder.rb +142 -0
  18. data/lib/tty/table/renderer/basic.rb +101 -31
  19. data/lib/tty/table/validatable.rb +0 -7
  20. data/lib/tty/terminal/color.rb +36 -20
  21. data/lib/tty/text/truncation.rb +16 -1
  22. data/lib/tty/text/wrapping.rb +31 -10
  23. data/lib/tty/version.rb +1 -1
  24. data/spec/tty/shell/question/argument_spec.rb +1 -1
  25. data/spec/tty/shell/question/modify_spec.rb +2 -2
  26. data/spec/tty/shell/response/read_email_spec.rb +0 -1
  27. data/spec/tty/table/border/ascii/rendering_spec.rb +34 -7
  28. data/spec/tty/table/border/null/rendering_spec.rb +34 -7
  29. data/spec/tty/table/column_set/extract_widths_spec.rb +1 -1
  30. data/spec/tty/table/column_set/widths_from_spec.rb +52 -0
  31. data/spec/tty/table/columns/enforce_spec.rb +68 -0
  32. data/spec/tty/table/columns/widths_spec.rb +33 -0
  33. data/spec/tty/table/indentation/insert_indent_spec.rb +27 -0
  34. data/spec/tty/table/operation/wrapped/call_spec.rb +2 -1
  35. data/spec/tty/table/operation/wrapped/wrap_spec.rb +3 -2
  36. data/spec/tty/table/operations/new_spec.rb +3 -5
  37. data/spec/tty/table/orientation_spec.rb +68 -22
  38. data/spec/tty/table/padder/parse_spec.rb +45 -0
  39. data/spec/tty/table/padding_spec.rb +120 -0
  40. data/spec/tty/table/renderer/ascii/indentation_spec.rb +41 -0
  41. data/spec/tty/table/renderer/ascii/padding_spec.rb +61 -0
  42. data/spec/tty/table/renderer/ascii/resizing_spec.rb +114 -0
  43. data/spec/tty/table/renderer/basic/alignment_spec.rb +18 -19
  44. data/spec/tty/table/renderer/basic/coloring_spec.rb +45 -0
  45. data/spec/tty/table/renderer/basic/indentation_spec.rb +46 -0
  46. data/spec/tty/table/renderer/basic/options_spec.rb +2 -2
  47. data/spec/tty/table/renderer/basic/padding_spec.rb +52 -0
  48. data/spec/tty/table/renderer/basic/resizing_spec.rb +96 -0
  49. data/spec/tty/table/renderer/render_spec.rb +36 -0
  50. data/spec/tty/table/renderer/unicode/indentation_spec.rb +41 -0
  51. data/spec/tty/table/renderer/unicode/padding_spec.rb +61 -0
  52. data/spec/tty/table/renderer_spec.rb +19 -0
  53. data/spec/tty/table/rotate_spec.rb +20 -6
  54. data/spec/tty/text/truncation/truncate_spec.rb +18 -3
  55. data/spec/tty/text/wrapping/wrap_spec.rb +24 -7
  56. metadata +56 -18
data/README.md CHANGED
@@ -16,7 +16,8 @@ Toolbox for developing CLI clients in Ruby. This library provides a fluid interf
16
16
  Jump-start development of your command line app:
17
17
 
18
18
  * Table rendering with an easy-to-use API. [status: In Progress]
19
- * Terminal output colorization, paging. [status: ✔ ]
19
+ * Terminal output colorization. [status: ✔ ]
20
+ * Terminal output paging. [status: ✔ ]
20
21
  * System & command detection utilities. [status: In Progress]
21
22
  * Text manipulation(wrapping/truncation) [status: In Progress]
22
23
  * Shell user interface. [status: In Progress]
@@ -106,22 +107,28 @@ This will use so called `basic` renderer with default options.
106
107
  However, you can include other customization options such as
107
108
 
108
109
  ```ruby
109
- column_widths # array of maximum columns widths
110
- column_aligns # array of cell alignments out of :left, :center and :right, default :left
111
- width # constrain the table total width, otherwise dynamically calculated based on content and terminal size
112
- renderer # enforce display type out of :basic, :color, :unicode, :ascii
113
110
  border # hash of border properties out of :characters, :style, :separator keys
114
111
  border_class # a type of border to use
115
- multiline # if true will wrap text at new line or column width, when false will escape special characters
112
+ column_widths # array of maximum columns widths
113
+ column_aligns # array of cell alignments out of :left, :center and :right, default :left
116
114
  filter # a proc object that is applied to every field in a row
115
+ indent # indentation applied to rendered table
116
+ multiline # if true will wrap text at new line or column width,
117
+ # when false will escape special characters
117
118
  orientation # either :horizontal or :vertical
119
+ padding # array of integers to set table fields padding
120
+ renderer # enforce display type out of :basic, :color, :unicode, :ascii
121
+ resize # if true will expand/shrink table column sizes to match the width,
122
+ # otherwise if false rotate table vertically
123
+ width # constrain the table total width, otherwise dynamically
124
+ # calculated from content and terminal size
118
125
  ```
119
126
 
120
127
  #### Multiline
121
128
 
122
129
  Renderer options may include `multiline` parameter. The `true` value will cause the table fields wrap at their natural line breaks or in case when the column widths are set the content will wrap.
123
130
 
124
- ```
131
+ ```ruby
125
132
  table = TTY::Table.new [ ["First", '1'], ["Multi\nLine\nContent", '2'], ["Third", '3']]
126
133
  table.render :ascii, multiline: true
127
134
  # =>
@@ -184,7 +191,7 @@ Next pass the border class to your table instance `render_with` method
184
191
  ```ruby
185
192
  table = TTY::Table.new ['header1', 'header2'], [['a1', 'a2'], ['b1', 'b2']
186
193
  table.render_with MyBorder
187
-
194
+ # =>
188
195
  $header1$header2$
189
196
  $a1 $a2 $
190
197
  * * *
@@ -200,7 +207,7 @@ table.render do |renderer|
200
207
  mid_mid ' '
201
208
  end
202
209
  end
203
-
210
+ # =>
204
211
  header1 header2
205
212
  ======= =======
206
213
  a1 a2
@@ -214,7 +221,7 @@ table = TTY::Table.new ['header1', 'header2'], [['a1', 'a2'], ['b1', 'b2']]
214
221
  table.render do |renderer|
215
222
  renderer.border.separator = :each_row
216
223
  end
217
-
224
+ # =>
218
225
  +-------+-------+
219
226
  |header1|header2|
220
227
  +-------+-------+
@@ -258,13 +265,38 @@ table = TTY::Table.new do |t|
258
265
  end
259
266
  ```
260
267
 
261
- #### Style
268
+ #### Padding
262
269
 
263
- To format individual fields/cells do
270
+ By default padding is not applied. You can add `padding` to table fields like so
264
271
 
265
272
  ```ruby
266
- table = TTY::Table.new rows: rows
267
- table.render width: 40
273
+ heaer = ['Field', 'Type', 'Null', 'Key', 'Default', 'Extra']
274
+ rows = [['id', 'int(11)', 'YES', 'nil', 'NULL', '']]
275
+ table = TTY::Table.new(header, rows)
276
+ table.render { |renderer| renderer.padding= [0,1,0,1] }
277
+ # =>
278
+ +-------+---------+------+-----+---------+-------+
279
+ | Field | Type | Null | Key | Default | Extra |
280
+ +-------+---------+------+-----+---------+-------+
281
+ | id | int(11) | YES | nil | NULL | |
282
+ +-------+---------+------+-----+---------+-------+
283
+ ```
284
+
285
+ or you can set specific padding using `right`, `left`, `top`, `bottom` helpers. However, when adding top or bottom padding a `multiline` option needs to be set to `true` to allow for rows to span multiple lines. For example
286
+
287
+ ```ruby
288
+ table.render { |renderer|
289
+ renderer.multiline = true
290
+ renderer.padding.top = 1
291
+ }
292
+ # =>
293
+ +-----+-------+----+---+-------+-----+
294
+ | | | | | | |
295
+ |Field|Type |Null|Key|Default|Extra|
296
+ +-----+-------+----+---+-------+-----+
297
+ | | | | | | |
298
+ |id |int(11)|YES |nil|NULL | |
299
+ +-----+-------+----+---+-------+-----+
268
300
  ```
269
301
 
270
302
  #### Filter
@@ -282,7 +314,7 @@ table.render do |renderer|
282
314
  end
283
315
  end
284
316
  end
285
-
317
+ # =>
286
318
  +-------+-------+
287
319
  |header1|header2|
288
320
  +-------+-------+
@@ -292,17 +324,36 @@ end
292
324
  +-------+-------+
293
325
  ```
294
326
 
295
- To add background color to even fields do
327
+ To color even fields red on green background add filter like so
296
328
 
297
329
  ```ruby
298
330
  table.render do |renderer|
299
- renderer.filter = Proc.new do |val, row_index, col_index|
300
- if col_index % 2 == 1
301
- TTY.color.set val, :red, :on_green
331
+ renderer.filter = proc do |val, row_index, col_index|
332
+ col_index % 2 == 1 ? TTY.color.set(val, :red, :on_green) : val
302
333
  end
303
334
  end
304
335
  ```
305
336
 
337
+ #### Width
338
+
339
+ To control table's column sizes pass `width`, `resize` options. By default table's natural column widths are calculated from the content. If the total table width does not fit in terminal window then the table is rotated vertically to preserve content.
340
+
341
+ The `resize` property will force the table to expand/shrink to match the terminal width or custom `width`. On its own the `width` property will not resize table but only enforce table vertical rotation if content overspills.
342
+
343
+ ```ruby
344
+ header = ['h1', 'h2', 'h3']
345
+ rows = [['aaa1', 'aa2', 'aaaaaaa3'], ['b1', 'b2', 'b3']]
346
+ table = TTY::Table.new header, rows
347
+ table.render width: 80, resize: true
348
+ # =>
349
+ +---------+-------+------------+
350
+ |h1 |h2 |h3 |
351
+ +---------+-------+------------+
352
+ |aaa1 |aa2 |aaaaaaa3 |
353
+ |b1 |b2 |b3 |
354
+ +---------+-------+------------+
355
+ ```
356
+
306
357
  ### Terminal
307
358
 
308
359
  To read general terminal properties you can use on of the helpers
@@ -403,15 +454,20 @@ Reading answers and converting them into required types can be done with custom
403
454
 
404
455
  ```ruby
405
456
  read_bool # return true or false for strings such as "Yes", "No"
457
+ read_char # return first character
406
458
  read_date # return date type
407
459
  read_datetime # return datetime type
408
460
  read_email # validate answer against email regex
461
+ read_file # return a File object
409
462
  read_float # return decimal or error if cannot convert
410
463
  read_int # return integer or error if cannot convert
411
464
  read_multiple # return multiple line string
412
465
  read_password # return string with echo turned off
413
466
  read_range # return range type
467
+ read_regex # return regex expression
414
468
  read_string # return string
469
+ read_symbol # return symbol
470
+ read_text # return multiline string
415
471
  ```
416
472
 
417
473
  For example, if we wanted to ask a user for a single digit in given range
data/lib/tty.rb CHANGED
@@ -62,10 +62,13 @@ require 'tty/table/border/null'
62
62
  require 'tty/table/border/row_line'
63
63
 
64
64
  require 'tty/table/column_set'
65
+ require 'tty/table/columns'
65
66
  require 'tty/table/orientation'
66
67
  require 'tty/table/orientation/horizontal'
67
68
  require 'tty/table/orientation/vertical'
68
69
  require 'tty/table/transformation'
70
+ require 'tty/table/indentation'
71
+ require 'tty/table/padder'
69
72
 
70
73
  require 'tty/table/operations'
71
74
  require 'tty/table/operation/alignment_set'
@@ -74,6 +77,7 @@ require 'tty/table/operation/truncation'
74
77
  require 'tty/table/operation/wrapped'
75
78
  require 'tty/table/operation/filter'
76
79
  require 'tty/table/operation/escape'
80
+ require 'tty/table/operation/padding'
77
81
 
78
82
  module TTY
79
83
 
@@ -95,6 +99,9 @@ module TTY
95
99
  # Raised when the table orientation is unkown
96
100
  class InvalidOrientationError < ArgumentError; end
97
101
 
102
+ # Raised when the table cannot be resized
103
+ class ResizeError < ArgumentError; end
104
+
98
105
  # Raised when the passed in validation argument is of wrong type
99
106
  class ValidationCoercion < TypeError; end
100
107
 
@@ -153,7 +153,6 @@ module TTY
153
153
  self
154
154
  end
155
155
 
156
-
157
156
  # Reset question object.
158
157
  #
159
158
  # @api public
@@ -170,7 +169,7 @@ module TTY
170
169
  #
171
170
  # @api public
172
171
  def modify(*rules)
173
- @modifier = Modifier.new *rules
172
+ @modifier = Modifier.new(*rules)
174
173
  self
175
174
  end
176
175
 
@@ -281,6 +280,7 @@ module TTY
281
280
  return default_value if !value && default?
282
281
 
283
282
  check_required value
283
+ return if value.nil?
284
284
  check_valid value unless valid_values.empty?
285
285
  within? value
286
286
  validation.valid_value? value
@@ -293,7 +293,7 @@ module TTY
293
293
  #
294
294
  # @api private
295
295
  def check_required(value)
296
- if required? && !default? && !value
296
+ if required? && !default? && value.nil?
297
297
  raise ArgumentRequired, 'No value provided for required'
298
298
  end
299
299
  end
@@ -46,9 +46,9 @@ module TTY
46
46
  if question.mask? && question.echo?
47
47
  reader.getc(question.mask)
48
48
  else
49
- TTY.terminal.echo(question.echo) {
49
+ TTY.terminal.echo(question.echo) do
50
50
  question.character? ? reader.getc(question.mask) : reader.gets
51
- }
51
+ end
52
52
  end
53
53
  end
54
54
 
@@ -59,7 +59,7 @@ module TTY
59
59
  #
60
60
  # @api public
61
61
  def read_string(error=nil)
62
- question.evaluate_response String(read_input)
62
+ question.evaluate_response(String(read_input).strip)
63
63
  end
64
64
 
65
65
  # Read answer's first character
@@ -165,10 +165,10 @@ module TTY
165
165
  #
166
166
  # @api public
167
167
  def read_multiple
168
- response = ""
168
+ response = ''
169
169
  loop do
170
170
  value = question.evaluate_response read_input
171
- break if !value || value == ""
171
+ break if !value || value == ''
172
172
  next if value !~ /\S/
173
173
  response << value
174
174
  end
data/lib/tty/table.rb CHANGED
@@ -39,6 +39,20 @@ module TTY
39
39
  # @api public
40
40
  attr_reader :orientation
41
41
 
42
+ # The table original row count
43
+ #
44
+ # @reutrn [Integer]
45
+ #
46
+ # @api public
47
+ attr_reader :original_rows
48
+
49
+ # The table original column count
50
+ #
51
+ # @reutrn [Integer]
52
+ #
53
+ # @api public
54
+ attr_reader :original_columns
55
+
42
56
  # Subset of safe methods that both Array and Hash implement
43
57
  def_delegators(:data, :assoc, :flatten, :include?, :index,
44
58
  :length, :select, :to_a, :values_at, :pretty_print, :rassoc)
@@ -100,11 +114,12 @@ module TTY
100
114
  validate_options! options
101
115
  @header = (value = options[:header]) ? Header.new(value) : nil
102
116
  @rows = coerce(options.fetch(:rows) { Row.new([]) })
103
- @orientation = Orientation.coerce(options.fetch(:orientation) { :horizontal })
104
117
  @rotated = false
105
- # TODO: assert that row_size is the same as column widths & aligns
118
+ self.orientation = options.fetch(:orientation) { :horizontal }
119
+
106
120
  assert_row_sizes @rows
107
- @orientation.transform(self)
121
+ orientation.transform(self)
122
+
108
123
  yield_or_eval(&block) if block_given?
109
124
  end
110
125
 
@@ -149,33 +164,37 @@ module TTY
149
164
  #
150
165
  # @api private
151
166
  def rotate_vertical
152
- @rows = ([header].compact + rows).transpose.map { |row| to_row(row) }
153
- @header = [] if header
154
- @rotated = true
167
+ @original_columns = column_size
168
+ @original_rows = row_size
169
+ @rows = orientation.slice(self)
170
+ @header = [] if header
171
+ @rotated = true
155
172
  end
156
173
 
157
174
  # Rotate the table horizontally
158
175
  #
159
176
  # @api private
160
177
  def rotate_horizontal
161
- transposed = rows.transpose
162
- if header && header.empty?
163
- @header = transposed[0]
164
- @rows = transposed[1..-1].map { |row| to_row(row, @header) }
165
- elsif rotated?
166
- @rows = transposed.map { |row| to_row(row) }
178
+ if rotated?
179
+ head, body = orientation.slice(self)
180
+ if header && header.empty?
181
+ @header = head[0]
182
+ @rows = body.map {|row| to_row(row, @header)}
183
+ else
184
+ @rows = body.map { |row| to_row(row) }
185
+ end
167
186
  end
168
187
  end
169
188
 
170
189
  # Lookup element of the table given a row(i) and column(j)
171
190
  #
172
191
  # @api public
173
- def [](i, j=false)
174
- return row(i) unless j
175
- if i >= 0 && j >= 0
176
- rows.fetch(i) { return nil }[j]
192
+ def [](row_index, column_index=false)
193
+ return row(row_index) unless column_index
194
+ if row_index >= 0 && column_index >= 0
195
+ rows.fetch(row_index) { return nil }[column_index]
177
196
  else
178
- raise TTY::Table::TupleMissing.new(i,j)
197
+ raise TTY::Table::TupleMissing.new(row_index, column_index)
179
198
  end
180
199
  end
181
200
  alias at []
@@ -185,8 +204,8 @@ module TTY
185
204
  # Set table value at row(i) and column(j)
186
205
  #
187
206
  # @api private
188
- def []=(i, j, val)
189
- @rows[i][j] = val
207
+ def []=(row_index, column_index, val)
208
+ @rows[row_index][column_index] = val
190
209
  end
191
210
  private :[]=
192
211
 
@@ -313,6 +332,7 @@ module TTY
313
332
  # @return [Integer]
314
333
  #
315
334
  # @api public
335
+ # TODO: renmae to columns_size
316
336
  def column_size
317
337
  return rows[0].size if rows.size > 0
318
338
  return 0
@@ -370,6 +390,18 @@ module TTY
370
390
  render(:basic)
371
391
  end
372
392
 
393
+ # Return renderer for this table
394
+ #
395
+ # @param [Symbol] type
396
+ # the renderer type
397
+ #
398
+ # @param [Hash] options
399
+ # the renderer options
400
+ #
401
+ def renderer(type=:basic, options={})
402
+ @renderer ||= Renderer.select(type).new(self, options)
403
+ end
404
+
373
405
  # Render a given table. This method takes options which will be passed
374
406
  # to the renderer prior to rendering, which allows the caller to set any
375
407
  # table rendering variables.
@@ -37,6 +37,47 @@ module TTY
37
37
  self.class.find_maximas(colcount, data)
38
38
  end
39
39
 
40
+ # Assert data integrity for column widths
41
+ #
42
+ # @param [Array] column_widths
43
+ #
44
+ # @param [Integer] table_widths
45
+ #
46
+ # @raise [TTY::InvalidArgument]
47
+ #
48
+ # @api public
49
+ def self.assert_widths(column_widths, table_widths)
50
+ if column_widths.empty?
51
+ raise InvalidArgument, 'Value for :column_widths must be a non-empty array'
52
+ end
53
+ if column_widths.size != table_widths
54
+ raise InvalidArgument, 'Value for :column_widths must match table column count'
55
+ end
56
+ end
57
+
58
+ # Converts column widths to array format or infers default widths
59
+ #
60
+ # @param [TTY::Table] table
61
+ #
62
+ # @param [Array, Numeric, NilClass] column_widths
63
+ #
64
+ # @return [Array[Integer]]
65
+ #
66
+ # @api public
67
+ def self.widths_from(table, column_widths = nil)
68
+ case column_widths
69
+ when Array
70
+ assert_widths(column_widths, table.column_size)
71
+ Array(column_widths).map(&:to_i)
72
+ when Numeric
73
+ Array.new(table.column_size, column_widths)
74
+ when NilClass
75
+ new(table).extract_widths
76
+ else
77
+ raise TypeError, 'Invalid type for column widths'
78
+ end
79
+ end
80
+
40
81
  private
41
82
 
42
83
  # Find maximum widths for each row and header if present.
@@ -68,7 +109,7 @@ module TTY
68
109
  #
69
110
  # @api private
70
111
  def self.find_maximum(data, index)
71
- data.map { |row| (value=row.call(index)) ? value.length : 0 }.max
112
+ data.map { |row| (value = row.call(index)) ? value.length : 0 }.max
72
113
  end
73
114
 
74
115
  end # ColumnSet