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