natty-ui 0.9.4 → 0.11.0

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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -11
  3. data/examples/24bit-colors.rb +8 -16
  4. data/examples/3bit-colors.rb +1 -1
  5. data/examples/8bit-colors.rb +15 -11
  6. data/examples/animate.rb +31 -11
  7. data/examples/attributes.rb +16 -14
  8. data/examples/attributes_list.rb +14 -0
  9. data/examples/demo.rb +4 -4
  10. data/examples/illustration.rb +52 -24
  11. data/examples/ls.rb +4 -4
  12. data/examples/message.rb +11 -9
  13. data/examples/progress.rb +2 -2
  14. data/examples/query.rb +1 -1
  15. data/examples/table.rb +23 -23
  16. data/lib/natty-ui/animation/binary.rb +37 -0
  17. data/lib/natty-ui/animation/default.rb +42 -0
  18. data/lib/natty-ui/animation/matrix.rb +55 -0
  19. data/lib/natty-ui/animation/rainbow.rb +28 -0
  20. data/lib/natty-ui/animation/type_writer.rb +44 -0
  21. data/lib/natty-ui/animation.rb +69 -0
  22. data/lib/natty-ui/ansi/constants.rb +1 -1
  23. data/lib/natty-ui/ansi.rb +46 -26
  24. data/lib/natty-ui/ansi_wrapper.rb +47 -51
  25. data/lib/natty-ui/key_map.rb +72 -49
  26. data/lib/natty-ui/preload.rb +1 -1
  27. data/lib/natty-ui/text.rb +75 -85
  28. data/lib/natty-ui/version.rb +1 -1
  29. data/lib/natty-ui/wrapper/animate.rb +1 -1
  30. data/lib/natty-ui/wrapper/ask.rb +1 -1
  31. data/lib/natty-ui/wrapper/element.rb +18 -10
  32. data/lib/natty-ui/wrapper/features.rb +0 -8
  33. data/lib/natty-ui/wrapper/framed.rb +7 -27
  34. data/lib/natty-ui/wrapper/message.rb +1 -1
  35. data/lib/natty-ui/wrapper/request.rb +1 -1
  36. data/lib/natty-ui/wrapper/section.rb +18 -18
  37. data/lib/natty-ui/wrapper/table.rb +432 -168
  38. data/lib/natty-ui/wrapper.rb +45 -48
  39. data/lib/natty-ui.rb +65 -2
  40. metadata +10 -8
  41. data/lib/natty-ui/line_animation/default.rb +0 -35
  42. data/lib/natty-ui/line_animation/matrix.rb +0 -28
  43. data/lib/natty-ui/line_animation/rainbow.rb +0 -30
  44. data/lib/natty-ui/line_animation/type_writer.rb +0 -44
  45. data/lib/natty-ui/line_animation.rb +0 -48
@@ -7,43 +7,15 @@ module NattyUI
7
7
  #
8
8
  # Table view of data.
9
9
  #
10
- # @note Tables do not support text attributes yet and are still under
11
- # construction. This means table features are not complete defined and
12
- # may change in near future.
13
- #
14
10
  # Defined values for `type` are
15
11
  # :double, :heavy, :semi, :simple
16
12
  #
17
- # @overload table(type: simple)
18
- # Construct and display a table.
19
- #
20
- # @param [Symbol] type frame type
21
- # @yieldparam [Table] table construction helper
22
- # @return [Wrapper::Section, Wrapper] it's parent object
23
- #
24
- # @example
25
- # ui.table do |table|
26
- # table.add('name', 'price', 'origin')
27
- # table.add('apple', '1$', 'California')
28
- # table.add('banana', '2$', 'Brasil')
29
- # table.add('kiwi', '1.5$', 'Newzeeland')
30
- # end
31
- #
32
- # # output:
33
- # # name │ price │ origin
34
- # # ───────┼───────┼───────────
35
- # # apple │ 1$ │ California
36
- # # ───────┼───────┼───────────
37
- # # banana │ 2$ │ Brasil
38
- # # ───────┼───────┼───────────
39
- # # kiwi │ 1.5$ │ Newzeeland
40
- #
41
- # @overload table(*args, type: simple)
13
+ # @overload table(*args, type: simple, expand: false)
42
14
  # Display the given arrays as rows of a table.
43
15
  #
44
- # @param [Array<#to_s>] args one or more arrays representing rows of the table
16
+ # @param [#map<#map<#to_s>>] args one or more arrays representing rows of the table
45
17
  # @param [Symbol] type frame type
46
- # @return [Wrapper::Section, Wrapper] it's parent object
18
+ # @param [false, true. :equal] expand
47
19
  #
48
20
  # @example
49
21
  # ui.table(
@@ -52,10 +24,45 @@ module NattyUI
52
24
  # %w[banana 2$ Brasil],
53
25
  # %w[kiwi 1.5$ Newzeeland]
54
26
  # )
55
- def table(*table, type: :simple)
56
- return _element(:Table, table, type) unless block_given?
57
- yield(table = Table.new(table))
58
- _element(:Table, table.rows, type)
27
+ #
28
+ # # name │ price origin
29
+ # # ───────┼───────┼───────────
30
+ # # apple │ 1$ │ California
31
+ # # ───────┼───────┼───────────
32
+ # # banana │ 2$ │ Brasil
33
+ # # ───────┼───────┼───────────
34
+ # # kiwi │ 1.5$ │ Newzeeland
35
+ #
36
+ # @overload table(type: simple, expand: false)
37
+ # Construct and display a table.
38
+ #
39
+ # @param [Symbol] type frame type
40
+ # @param [false, true. :equal] expand
41
+ #
42
+ # @example
43
+ # ui.table(type: :heavy, expand: true) do |table|
44
+ # table.add('name', 'price', 'origin', style: 'bold green')
45
+ # table.add('apple', '1$', 'California')
46
+ # table.add('banana', '2$', 'Brasil')
47
+ # table.add('kiwi', '1.5$', 'Newzeeland')
48
+ # table.align_column(0, :right).align_row(0, :center)
49
+ # end
50
+ #
51
+ # # name ┃ price ┃ origin
52
+ # # ━━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━
53
+ # # apple ┃ 1$ ┃ California
54
+ # # ━━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━
55
+ # # banana ┃ 2$ ┃ Brasil
56
+ # # ━━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━
57
+ # # kiwi ┃ 1.5$ ┃ Newzeeland
58
+ #
59
+ # @yield [Table] table construction helper
60
+ # @return [Wrapper::Section, Wrapper] it's parent object
61
+ def table(*table, type: :simple, expand: false)
62
+ type = NattyUI.frame(type)
63
+ table = Table.create(*table)
64
+ yield(table) if block_given?
65
+ _element(:Table, table, type, expand)
59
66
  end
60
67
 
61
68
  #
@@ -74,28 +81,280 @@ module NattyUI
74
81
  # # kiwi: 1.5$
75
82
  #
76
83
  def pairs(seperator = ': ', **kwargs)
77
- _element(:Pairs, kwargs.to_a, seperator)
84
+ kwargs.empty? ? self : _element(:Pairs, kwargs, seperator)
78
85
  end
79
86
 
87
+ #
88
+ # Helper class to define a table layout.
89
+ # @see #table
90
+ #
80
91
  class Table
81
- attr_reader :rows
92
+ # @!visibility private
93
+ def self.create(*table)
94
+ table = table[0] if table.size == 1 && table[0].respond_to?(:each)
95
+ return table if table.is_a?(self.class)
96
+ ret = new
97
+ table.each { ret.add_row(*_1) }
98
+ ret
99
+ end
100
+
101
+ # @!visibility private
102
+ def initialize = @rows = []
103
+
104
+ # @return [Integer] count of rows
105
+ def row_count = @rows.size
82
106
 
83
- def add_row(*columns)
84
- @rows << columns
107
+ # @return [Integer] count of columns
108
+ def col_count = @rows.max_by { _1&.size || 0 }&.size || 0
109
+
110
+ # Get the {Cell} at a table position.
111
+ #
112
+ # @param [Integer] row row index
113
+ # @param [Integer] col column index
114
+ # @return [Cell] at row/column
115
+ # @return [nil] if no cell defined at row/column
116
+ def [](row, col) = @rows.dig(row, col)
117
+
118
+ # Change {Cell} or (text) value at a table position.
119
+ #
120
+ # @example change {Cell} at row 2, column 3
121
+ # table[2, 3] = table.cell('Hello World', align: right, style: 'bold')
122
+ # @example change text at row 2, column 3
123
+ # table[2, 3] = 'Hello Ruby!'
124
+ # @example delete {Cell} at row 2, column 3
125
+ # table[2, 3] = nil
126
+ #
127
+ # @param [Integer] row row index
128
+ # @param [Integer] col column index
129
+ # @param [Cell, #to_s, nil] value Cell or text to use at specified position
130
+ # @return [Cell, #to_s, nil] the value
131
+ def []=(row, col, value)
132
+ row = (@rows[row] ||= [])
133
+ if value.nil? || value.is_a?(Cell)
134
+ row[col] = value
135
+ else
136
+ cell = row[col]
137
+ cell ? cell.value = value : row[col] = Cell.new(value, nil, nil)
138
+ end
139
+ end
140
+
141
+ # Create a new cell.
142
+ #
143
+ # @example create a {Cell} with right aligned bold text "Hello World"
144
+ # table[2, 3] = table.cell('Hello World', align: right, style: 'bold')
145
+ #
146
+ # @param [#to_s] value text value
147
+ # @param [:left, :right, :center] align text alignment
148
+ # @param [String] style text style; see {Ansi.try_convert}
149
+ # @return [Cell] a new cell
150
+ def cell(value, align: :left, style: nil) = Cell.new(value, align, style)
151
+
152
+ # Add a new row to the table.
153
+ #
154
+ # @example add a row with three right-aligned columns
155
+ # table.add_row('One', 'Two', 'Three', align: :right)
156
+ #
157
+ # @param [#map] columns Enumerable-like object containing column texts
158
+ # @param [:left, :right, :center] align text alignment
159
+ # @param [String] style text style; see {Ansi.try_convert}
160
+ # @return [Table] itself
161
+ def add_row(*columns, align: nil, style: nil)
162
+ if columns.size == 1 && columns[0].respond_to?(:map)
163
+ columns = columns[0]
164
+ end
165
+ columns =
166
+ columns.map { |cell| as_cell(cell, align, style) if cell }.to_a
167
+ @rows << (columns.empty? ? nil : columns)
85
168
  self
86
169
  end
87
170
  alias add add_row
88
171
 
89
- def add_col(*columns)
90
- columns.each_with_index do |col, row_idx|
91
- (@rows[row_idx] ||= []) << col
172
+ # Add a new column to the table.
173
+ #
174
+ # @example add a column of three rows with bold styled text
175
+ # table.add_column('One', 'Two', 'Three', style: :bold)
176
+ #
177
+ # @param [#map] rows Enumerable-like object containing texts for each row
178
+ # @param [:left, :right, :center] align text alignment
179
+ # @param [String] style text style; see {Ansi.try_convert}
180
+ # @return [Table] itself
181
+ def add_column(*rows, align: nil, style: nil)
182
+ rows = rows[0] if rows.size == 1 && rows[0].respond_to?(:map)
183
+ row_idx = -1
184
+ rows.each do |cell|
185
+ (@rows[row_idx += 1] ||= []) << as_cell(cell, align, style)
186
+ end
187
+ end
188
+
189
+ # Change style of one or more rows.
190
+ #
191
+ # @example define bold red text style for the first row
192
+ # table.style_row(0, 'bold red')
193
+ # @example define yellow text style for the first three rows
194
+ # table.style_row(0..2, 'yellow')
195
+ # @example define green text style for rows 3, 4 and 7
196
+ # table.style_row([3, 4, 7], 'green')
197
+ #
198
+ # @param [Integer, Enumerable<Integer>] row index of row(s) to change
199
+ # @param [String, nil] style text style; see {Ansi.try_convert}
200
+ # @return [Table] itself
201
+ def style_row(row, style)
202
+ if row.is_a?(Integer)
203
+ row = [row]
204
+ elsif !row.is_a?(Enumerable)
205
+ raise(TypeError, "invalid row value - #{row}")
206
+ end
207
+ row.each { |r| @rows[r]&.each { _1&.style = style } }
208
+ self
209
+ end
210
+
211
+ # Change style of one or more columns.
212
+ #
213
+ # @example define bold red text style for the first column
214
+ # table.style_column(0, 'bold red')
215
+ # @example define yellow text style for the first three columns
216
+ # table.style_column(0..2, 'yellow')
217
+ # @example define green text style for columns with index 3, 4 and 7
218
+ # table.style_column([3, 4, 7], 'green')
219
+ #
220
+ # @param [Integer, Enumerable<Integer>] column index of column(s) to change
221
+ # @param [String, nil] style text style; see {Ansi.try_convert}
222
+ # @return [Table] itself
223
+ def style_column(column, style)
224
+ if column.is_a?(Integer)
225
+ column = [column]
226
+ elsif !column.is_a?(Enumerable)
227
+ raise(TypeError, "invalid column value - #{column}")
228
+ end
229
+ @rows.each { |row| column.each { row[_1]&.style = style } }
230
+ self
231
+ end
232
+
233
+ # Change text alignment of one or more rows.
234
+ #
235
+ # @example align first row right
236
+ # table.align_row(0, :right)
237
+ # @example center first three rows
238
+ # table.align_row(0..2, :center)
239
+ # @example center the rows with index 3, 4 and 7
240
+ # table.align_row([3, 4, 7], :center)
241
+ #
242
+ # @param [Integer, Enumerable<Integer>] row index of row(s) to change
243
+ # @param [:left, :right, :center] alignment
244
+ # @return [Table] itself
245
+ def align_row(row, alignment)
246
+ if row.is_a?(Integer)
247
+ row = [row]
248
+ elsif !row.is_a?(Enumerable)
249
+ raise(TypeError, "invalid row value - #{row}")
92
250
  end
251
+ row.each { |r| @rows[r]&.each { _1&.align = alignment } }
93
252
  self
94
253
  end
95
254
 
96
- def initialize(rows) = (@rows = rows)
255
+ # Change text alignment of one or more column.
256
+ #
257
+ # @example align first column right
258
+ # table.align_column(0, :right)
259
+ # @example center first three columns
260
+ # table.align_column(0..2, :center)
261
+ # @example center the columns with index 3, 4 and 7
262
+ # table.align_column([3, 4, 7], :center)
263
+ #
264
+ # @param [Integer, Enumerable<Integer>] column index of column(s) to change
265
+ # @param [:left, :right, :center] alignment
266
+ # @return [Table] itself
267
+ def align_column(column, alignment)
268
+ if column.is_a?(Integer)
269
+ column = [column]
270
+ elsif !column.is_a?(Enumerable)
271
+ raise(TypeError, "invalid column value - #{column}")
272
+ end
273
+ @rows.each { |row| column.each { row[_1]&.align = alignment } }
274
+ self
275
+ end
276
+
277
+ # Convert the table to the compactest (two-dimensional) array
278
+ # representation.
279
+ #
280
+ # @return [Array<Array<Cell>>]
281
+ def to_a
282
+ ret = []
283
+ ridx = -1
284
+ @rows.each do |row|
285
+ ridx += 1
286
+ next unless row
287
+ count = 0
288
+ row =
289
+ row.map do |cell|
290
+ next unless cell
291
+ next if cell.value.empty?
292
+ count += 1
293
+ cell.dup
294
+ end
295
+ ret[ridx] = row if count.positive?
296
+ end
297
+ ret
298
+ end
299
+
300
+ private
301
+
302
+ def as_cell(value, align, style)
303
+ return Cell.new(value, align, style) unless value.is_a?(Cell)
304
+ cell = value.dup
305
+ cell.align = align if align
306
+ cell.style = style if style
307
+ cell
308
+ end
309
+
310
+ def initialize_copy(*)
311
+ super
312
+ @rows = to_a
313
+ end
314
+
315
+ class Cell
316
+ # @return [String, nil] text value of the cell
317
+ attr_reader :value
318
+
319
+ attr_writer :align, :style
320
+
321
+ # @!visibility private
322
+ attr_accessor :tag
323
+
324
+ # @!visibility private
325
+ def initialize(value, align, style)
326
+ @value = value.to_s
327
+ @align = align
328
+ @style = style
329
+ end
330
+
331
+ # @attribute [r] align
332
+ # @return [:left, :right, :center] text alignment
333
+ def align = ALIGNMENT[@align]
334
+
335
+ # @attribute [r] style
336
+ # @return [String, nil] text style; see {Ansi.try_convert}
337
+ def style
338
+ @style_ ||= Ansi.try_convert(@style)
339
+ end
340
+
341
+ def value=(value)
342
+ @value = value.to_s
343
+ end
344
+
345
+ private
346
+
347
+ def initialize_copy(*)
348
+ super
349
+ @value = @value.dup
350
+ @tag = nil
351
+ end
352
+
353
+ alignment = { left: :left, right: :right, center: :center }
354
+ alignment.default = :left
355
+ ALIGNMENT = alignment.compare_by_identity.freeze
356
+ end
97
357
  end
98
- private_constant :Table
99
358
  end
100
359
 
101
360
  class Wrapper
@@ -105,27 +364,15 @@ module NattyUI
105
364
  class Table < Element
106
365
  protected
107
366
 
108
- def call(rows, type)
109
- TableGenerator.each_line(
110
- rows,
367
+ def call(table, frame, enlarge)
368
+ TableGen.each_line(
369
+ table.to_a,
111
370
  @parent.available_width - 1,
112
- ORNAMENTS[type] ||
113
- raise(ArgumentError, "invalid table type - #{type.inspect}"),
114
- Ansi[39],
115
- Ansi::RESET
116
- ) { @parent.puts(_1) }
371
+ frame,
372
+ enlarge
373
+ ) { |line| @parent.puts(line) }
117
374
  @parent
118
375
  end
119
-
120
- def coloring = [nil, nil]
121
-
122
- ORNAMENTS = {
123
- rounded: '│─┼',
124
- simple: '│─┼',
125
- heavy: '┃━╋',
126
- double: '║═╬',
127
- semi: '║╴╫'
128
- }.compare_by_identity.freeze
129
376
  end
130
377
 
131
378
  # An {Element} to print key/value pairs.
@@ -134,157 +381,174 @@ module NattyUI
134
381
  class Pairs < Element
135
382
  protected
136
383
 
137
- def call(rows, seperator)
138
- TableGenerator.each_simple_line(
139
- rows,
384
+ def call(kwargs, seperator)
385
+ TableGen.each_line_simple(
386
+ kwargs.map do |k, v|
387
+ [
388
+ Features::Table::Cell.new(k, :right, nil),
389
+ Features::Table::Cell.new(v, :left, nil)
390
+ ]
391
+ end,
140
392
  @parent.available_width - 1,
141
- seperator,
142
- Text.plain(seperator)[-1] == ' '
393
+ seperator
143
394
  ) { @parent.puts(_1) }
144
395
  @parent
145
396
  end
146
397
  end
147
398
 
148
- class TableGenerator
149
- def self.each_line(rows, max_width, ornament, opref, osuff)
150
- return if rows.empty?
151
- gen = new(rows, max_width, 3)
399
+ class TableGen
400
+ COLOR = Ansi[39]
401
+
402
+ def self.each_line(table, max_width, frame, expand)
403
+ gen = new(table, max_width, 3, expand)
152
404
  return unless gen.ok?
153
- last_row = 0
154
- col_div = " #{opref}#{ornament[0]}#{osuff} "
155
- row_div = "#{ornament[1]}#{ornament[2]}#{ornament[1]}"
405
+ col_div = "#{Ansi::RESET} #{COLOR}#{frame[4]}#{Ansi::RESET} "
406
+ row_div = "#{frame[5]}#{frame[6]}#{frame[5]}"
156
407
  row_div =
157
- "#{opref}#{gen.widths.map { ornament[1] * _1 }.join(row_div)}#{osuff}"
158
- gen.each do |line, number|
408
+ "#{COLOR}#{
409
+ gen.widths.map { frame[5] * _1 }.join(row_div)
410
+ }#{Ansi::RESET}"
411
+ last_row = 0
412
+ gen.each do |number, line|
159
413
  if last_row != number
160
414
  last_row = number
161
415
  yield(row_div)
162
416
  end
163
- yield(line.join(col_div))
417
+ yield(line.join(col_div) + Ansi::RESET)
164
418
  end
165
419
  end
166
420
 
167
- def self.each_simple_line(rows, max_width, col_div, first_right)
168
- return if rows.empty?
169
- gen = new(rows, max_width, Text.width(col_div))
421
+ def self.each_line_simple(table, max_width, seperator)
422
+ gen = new(table, max_width, Text.width(seperator), false)
170
423
  return unless gen.ok?
171
- gen.aligns[0] = :right if first_right
172
- gen.each { yield(_1.join(col_div)) }
424
+ col_div = "#{COLOR}#{seperator}#{Ansi::RESET}"
425
+ gen.each { yield(_2.join(col_div)) }
173
426
  end
174
427
 
175
- attr_reader :widths, :aligns
428
+ attr_reader :widths
176
429
 
177
- def initialize(rows, max_width, col_div_size)
178
- @rows =
179
- rows.map do |row|
180
- row.map do |col|
181
- col = Text.embellish(col).each_line(chomp: true).to_a
182
- col.empty? ? col << '' : col
183
- end
184
- end
430
+ def initialize(table, max_width, coldiv_size, expand)
431
+ return if coldiv_size > max_width # wtf
432
+ @table = table
433
+ @col_count = table.max_by { _1&.size || 0 }&.size || 0
185
434
  @max_width = max_width
186
- @col_div_size = col_div_size
187
- @widths = create_widths.freeze
188
- @aligns = Array.new(@widths.size, :left)
435
+ @widths = determine_widths
436
+ sum = @widths.sum
437
+ space = @max_width - ((@col_count - 1) * coldiv_size)
438
+ if space < sum
439
+ @widths = reduce_widths(sum, space, coldiv_size)
440
+ adjust!
441
+ elsif expand && sum < space
442
+ if expand == :equal
443
+ equal_widths(space)
444
+ else
445
+ enlarge_widths(sum, space)
446
+ end
447
+ adjust!
448
+ end
449
+ @empties = @widths.map { ' ' * _1 }
189
450
  end
190
451
 
191
- def ok? = (@widths != nil)
452
+ def ok? = @empties != nil
192
453
 
193
454
  def each
194
- return unless @widths
195
- col_empty = @widths.map { ' ' * _1 }
196
- @rows.each_with_index do |row, row_idx|
197
- row
198
- .max_by(&:size)
199
- .size
200
- .times do |line_nr|
201
- col_idx = -1
202
- yield(
203
- @widths.map do |col_width|
204
- cell = row[col_idx += 1] or next col_empty[col_idx]
205
- next col_empty[col_idx] if (line = cell[line_nr]).nil?
206
- align(line.to_s, col_width, @aligns[col_idx])
207
- end,
208
- row_idx
209
- )
210
- end
455
+ ridx = -1
456
+ @table.each do |row|
457
+ ridx += 1
458
+ next yield(ridx, @empties) unless row
459
+ line_count = row.map { _1 ? _1.tag.lines.size : 0 }.max
460
+ line_count.times do |lidx|
461
+ cidx = -1
462
+ yield(
463
+ ridx,
464
+ @empties.map do |spacer|
465
+ cell = row[cidx += 1] or next spacer
466
+ str, str_size = cell.tag.lines[lidx]
467
+ next spacer unless str_size
468
+ str_fmt(cell.align, cell.style, str, str_size, spacer.size)
469
+ end
470
+ )
471
+ end
211
472
  end
473
+ nil
212
474
  end
213
475
 
214
476
  private
215
477
 
216
- def align(str, width, alignment)
217
- return str unless (width -= Text.width(str)).positive?
218
- return str + (' ' * width) if alignment == :left
219
- (' ' * width) << str
478
+ def str_fmt(align, style, str, str_size, size)
479
+ return "#{style}#{str}" unless (size -= str_size).positive?
480
+ return "#{style}#{' ' * size}#{str}" if align == :right
481
+ if align == :center
482
+ right = size / 2
483
+ return "#{style}#{' ' * (size - right)}#{str}#{' ' * right}"
484
+ end
485
+ "#{style}#{str}#{' ' * size}"
220
486
  end
221
487
 
222
- def create_widths
223
- matrix = create_matrix
224
- col_widths = find_col_widths(matrix)
225
- adjusted = adjusted_widths(col_widths)
226
- return if adjusted.empty? # nothing to draw
227
- return adjusted if col_widths == adjusted # all fine
228
- if (size = adjusted.size) != col_widths.size
229
- @rows.map! { _1.take(size) }
230
- matrix.map! { _1.take(size) }
231
- col_widths = col_widths.take(size)
232
- end
233
- diff = diff(col_widths, adjusted)
234
- @rows.each_with_index do |row, row_idx|
235
- diff.each do |col_idx|
236
- adjust_to = adjusted[col_idx]
237
- next if matrix[row_idx][col_idx] <= adjust_to
238
- row[col_idx] = Text.as_lines(row[col_idx], adjust_to)
488
+ def adjust!
489
+ @table.each do |row|
490
+ next unless row
491
+ cidx = -1
492
+ row.each do |cell|
493
+ cidx += 1
494
+ next unless cell
495
+ width = @widths[cidx]
496
+ next if cell.tag.value <= width
497
+ cell.tag.lines = Text.as_lines([cell.value], width)
239
498
  end
240
499
  end
241
- adjusted
242
500
  end
243
501
 
244
- def create_matrix
245
- ret =
246
- @rows.map { |row| row.map { |col| col.map { Text.width(_1) }.max } }
247
- cc = ret.max_by(&:size).size
248
- ret.each { (add = cc - _1.size).nonzero? and _1.fill(0, _1.size, add) }
502
+ def enlarge_widths(sum, space)
503
+ @widths.map! { ((((100.0 * _1) / sum) * space) / 100).round }
504
+ return if (diff = space - @widths.sum).zero?
505
+ @widths[
506
+ @widths.index(diff.negative? ? @widths.max : @widths.min)
507
+ ] += diff
249
508
  end
250
509
 
251
- def find_col_widths(matrix)
252
- ret = nil
253
- matrix.each do |row|
254
- next ret = row.dup unless ret
255
- row.each_with_index do |size, idx|
256
- hs = ret[idx]
257
- ret[idx] = size if hs < size
258
- end
259
- end
260
- ret
510
+ def equal_widths(space)
511
+ @widths = Array.new(@widths.size, space / @widths.size)
512
+ @widths[-1] += space - @widths.sum
261
513
  end
262
514
 
263
- def adjusted_widths(col_widths)
264
- ret = col_widths.dup
265
- left = @max_width - (@col_div_size * (col_widths.size - 1))
266
- return ret if ret.sum <= left
267
- indexed = ret.each_with_index.to_a
268
- # TODO: optimize this!
269
- until ret.sum <= left
270
- indexed.sort! { |b, a| (a[0] <=> b[0]).nonzero? || (a[1] <=> b[1]) }
271
- pair = indexed[0]
272
- next ret[pair[1]] = pair[0] if (pair[0] -= 1).nonzero?
273
- indexed.shift
274
- return [] if indexed.empty?
275
- ret.pop
515
+ def reduce_widths(sum, space, coldiv_size)
516
+ ws = @widths.dup
517
+ until sum <= space
518
+ max = ws.max
519
+ if max == 1
520
+ ws = @widths.take(ws.size - 1)
521
+ sum = ws.sum
522
+ space += coldiv_size
523
+ else
524
+ while (idx = ws.rindex(max)) && (sum > space)
525
+ ws[idx] -= 1
526
+ sum -= 1
527
+ end
528
+ end
276
529
  end
277
- ret
530
+ ws
278
531
  end
279
532
 
280
- def diff(col_widths, adjusted)
281
- ret = []
282
- col_widths.each_with_index do |val, idx|
283
- ret << idx if val != adjusted[idx]
533
+ def determine_widths
534
+ ret = Array.new(@col_count, 1)
535
+ @table.each do |row|
536
+ next unless row
537
+ cidx = -1
538
+ row.each do |cell|
539
+ cidx += 1
540
+ next unless cell
541
+ cell.tag = Tag.new(Text.as_lines([cell.value], @max_width))
542
+ width = cell.tag.value = cell.tag.lines.max_by(&:last).last
543
+ ret[cidx] = width if ret[cidx] < width
544
+ end
284
545
  end
285
546
  ret
286
547
  end
548
+
549
+ Tag = Struct.new(:lines, :value)
287
550
  end
288
- private_constant :TableGenerator
551
+
552
+ private_constant :TableGen
289
553
  end
290
554
  end