natty-ui 0.12.0 → 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.
- checksums.yaml +4 -4
- data/LICENSE +1 -1
- data/README.md +23 -24
- data/examples/24bit-colors.rb +4 -9
- data/examples/3bit-colors.rb +28 -8
- data/examples/8bit-colors.rb +18 -23
- data/examples/attributes.rb +30 -25
- data/examples/cols.rb +40 -0
- data/examples/elements.rb +31 -0
- data/examples/examples.rb +45 -0
- data/examples/illustration.rb +56 -54
- data/examples/ls.rb +16 -18
- data/examples/named-colors.rb +23 -0
- data/examples/sections.rb +29 -0
- data/examples/tables.rb +62 -0
- data/examples/tasks.rb +52 -0
- data/lib/natty-ui/attributes.rb +604 -0
- data/lib/natty-ui/choice.rb +56 -0
- data/lib/natty-ui/dumb_choice.rb +45 -0
- data/lib/natty-ui/element.rb +78 -0
- data/lib/natty-ui/features.rb +798 -0
- data/lib/natty-ui/framed.rb +51 -0
- data/lib/natty-ui/ls_renderer.rb +93 -0
- data/lib/natty-ui/progress.rb +187 -0
- data/lib/natty-ui/section.rb +69 -0
- data/lib/natty-ui/table.rb +241 -0
- data/lib/natty-ui/table_renderer.rb +147 -0
- data/lib/natty-ui/task.rb +44 -0
- data/lib/natty-ui/temporary.rb +38 -0
- data/lib/natty-ui/theme.rb +303 -0
- data/lib/natty-ui/utils.rb +79 -0
- data/lib/natty-ui/version.rb +1 -1
- data/lib/natty-ui/width_finder.rb +125 -0
- data/lib/natty-ui.rb +89 -147
- metadata +47 -56
- data/examples/animate.rb +0 -44
- data/examples/attributes_list.rb +0 -14
- data/examples/demo.rb +0 -53
- data/examples/message.rb +0 -32
- data/examples/progress.rb +0 -68
- data/examples/query.rb +0 -41
- data/examples/read_key.rb +0 -13
- data/examples/table.rb +0 -41
- data/lib/natty-ui/animation/binary.rb +0 -36
- data/lib/natty-ui/animation/default.rb +0 -38
- data/lib/natty-ui/animation/matrix.rb +0 -51
- data/lib/natty-ui/animation/rainbow.rb +0 -28
- data/lib/natty-ui/animation/type_writer.rb +0 -44
- data/lib/natty-ui/animation.rb +0 -69
- data/lib/natty-ui/ansi/constants.rb +0 -75
- data/lib/natty-ui/ansi.rb +0 -521
- data/lib/natty-ui/ansi_wrapper.rb +0 -199
- data/lib/natty-ui/frame.rb +0 -53
- data/lib/natty-ui/glyph.rb +0 -64
- data/lib/natty-ui/key_map.rb +0 -142
- data/lib/natty-ui/preload.rb +0 -12
- data/lib/natty-ui/spinner.rb +0 -120
- data/lib/natty-ui/text/east_asian_width.rb +0 -2529
- data/lib/natty-ui/text.rb +0 -203
- data/lib/natty-ui/wrapper/animate.rb +0 -17
- data/lib/natty-ui/wrapper/ask.rb +0 -78
- data/lib/natty-ui/wrapper/element.rb +0 -79
- data/lib/natty-ui/wrapper/features.rb +0 -21
- data/lib/natty-ui/wrapper/framed.rb +0 -45
- data/lib/natty-ui/wrapper/heading.rb +0 -60
- data/lib/natty-ui/wrapper/horizontal_rule.rb +0 -37
- data/lib/natty-ui/wrapper/list_in_columns.rb +0 -138
- data/lib/natty-ui/wrapper/message.rb +0 -109
- data/lib/natty-ui/wrapper/mixins.rb +0 -67
- data/lib/natty-ui/wrapper/progress.rb +0 -74
- data/lib/natty-ui/wrapper/query.rb +0 -89
- data/lib/natty-ui/wrapper/quote.rb +0 -25
- data/lib/natty-ui/wrapper/request.rb +0 -54
- data/lib/natty-ui/wrapper/section.rb +0 -118
- data/lib/natty-ui/wrapper/table.rb +0 -551
- data/lib/natty-ui/wrapper/task.rb +0 -55
- data/lib/natty-ui/wrapper.rb +0 -230
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'element'
|
4
|
+
|
5
|
+
module NattyUI
|
6
|
+
# {Element} with a frame around the content used by {Features.framed}.
|
7
|
+
#
|
8
|
+
class Framed < Element
|
9
|
+
# @!visibility private
|
10
|
+
def closed? = @bottom ? false : true
|
11
|
+
|
12
|
+
# @!visibility private
|
13
|
+
def puts(*objects, **options)
|
14
|
+
return self if closed?
|
15
|
+
options[:align] = @align
|
16
|
+
options[:expand] = true
|
17
|
+
super
|
18
|
+
end
|
19
|
+
|
20
|
+
# @!visibility private
|
21
|
+
def done
|
22
|
+
return if closed?
|
23
|
+
@parent.puts(@bottom)
|
24
|
+
@bottom = nil
|
25
|
+
end
|
26
|
+
|
27
|
+
# @!visibility private
|
28
|
+
def inspect = "#{_to_s.chop} align=#{@align.inspect} closed?=#{closed?}>"
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def initialize(parent, align, chars, style, msg)
|
33
|
+
super(parent)
|
34
|
+
@align = align
|
35
|
+
if style
|
36
|
+
style = Ansi[*style]
|
37
|
+
@prefix = "#{style}#{chars[9]}[/] "
|
38
|
+
@suffix = " #{style}#{chars[9]}[/]"
|
39
|
+
else
|
40
|
+
@prefix = "#{chars[9]} "
|
41
|
+
@suffix = " #{chars[9]}"
|
42
|
+
end
|
43
|
+
@prefix_width = 2
|
44
|
+
@suffix_width = 2
|
45
|
+
line = chars[10] * (parent.columns - 2)
|
46
|
+
parent.puts("#{style}#{chars[0]}#{line}#{chars[2]}")
|
47
|
+
@bottom = "#{style}#{chars[6]}#{line}#{chars[8]}"
|
48
|
+
puts(*msg) unless msg.empty?
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NattyUI
|
4
|
+
class LSRenderer
|
5
|
+
class << self
|
6
|
+
def lines(items, glyph, max_width)
|
7
|
+
items = as_items(items, glyph)
|
8
|
+
lines = []
|
9
|
+
width = items.max_by(&:width).width + 3
|
10
|
+
return lines if (sl_size = max_width / width).zero?
|
11
|
+
items.each_slice(sl_size) do |slice|
|
12
|
+
lines << slice.map { _1.to_s(width) }.join
|
13
|
+
end
|
14
|
+
lines
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def as_items(items, glyph)
|
20
|
+
items.flatten!
|
21
|
+
glyph = as_glyph(glyph, items.size)
|
22
|
+
items.map! { |item| Item.new(item = glyph[item], Text.width(item)) }
|
23
|
+
end
|
24
|
+
|
25
|
+
def as_glyph(glyph, size)
|
26
|
+
case glyph
|
27
|
+
when nil, false
|
28
|
+
lambda(&:itself)
|
29
|
+
when :hex
|
30
|
+
pad = size.to_s(16).size
|
31
|
+
glyph = 0
|
32
|
+
->(s) { "#{(glyph += 1).to_s(16).rjust(pad, '0')} #{s}" }
|
33
|
+
when Integer
|
34
|
+
pad = (glyph + size).to_s.size
|
35
|
+
glyph -= 1
|
36
|
+
->(s) { "#{(glyph += 1).to_s.rjust(pad)} #{s}" }
|
37
|
+
when Symbol
|
38
|
+
lambda do |s|
|
39
|
+
"#{
|
40
|
+
t = glyph
|
41
|
+
glyph = glyph.succ
|
42
|
+
t
|
43
|
+
} #{s}[/]"
|
44
|
+
end
|
45
|
+
else
|
46
|
+
->(s) { "#{glyph} #{s}" }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
Item =
|
51
|
+
Struct.new(:str, :width) do
|
52
|
+
def to_s(in_width) = "#{str}#{' ' * (in_width - width)}"
|
53
|
+
end
|
54
|
+
private_constant :Item
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class CompactLSRenderer < LSRenderer
|
59
|
+
class << self
|
60
|
+
def lines(items, glyph, max_width)
|
61
|
+
items = as_items(items, glyph)
|
62
|
+
return [] if items.empty?
|
63
|
+
found, widths = find_columns(items, max_width)
|
64
|
+
fill(found[-1], found[0].size)
|
65
|
+
found.transpose.map! do |row|
|
66
|
+
row.each_with_index.map { |item, col| item&.to_s(widths[col]) }.join
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def find_columns(items, max_width)
|
73
|
+
found = [items]
|
74
|
+
widths = [items.max_by(&:width).width]
|
75
|
+
1.upto(items.size - 1) do |slice_size|
|
76
|
+
candidate = items.each_slice(items.size / slice_size).to_a
|
77
|
+
cwidths = candidate.map { _1.max_by(&:width).width + 3 }
|
78
|
+
cwidths[-1] -= 3
|
79
|
+
break if cwidths.sum > max_width
|
80
|
+
found = candidate
|
81
|
+
widths = cwidths
|
82
|
+
end
|
83
|
+
[found, widths]
|
84
|
+
end
|
85
|
+
|
86
|
+
def fill(ary, size)
|
87
|
+
(diff = size - ary.size).positive? && ary.fill(nil, ary.size, diff)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
private_constant :LSRenderer, :CompactLSRenderer
|
93
|
+
end
|
@@ -0,0 +1,187 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NattyUI
|
4
|
+
# @todo This chapter needs more documentation.
|
5
|
+
#
|
6
|
+
# Progress indictaor helper used by {Features.progress}.
|
7
|
+
#
|
8
|
+
module ProgressHelper
|
9
|
+
# @return [String]
|
10
|
+
attr_reader :title
|
11
|
+
|
12
|
+
# @attribute [w] title
|
13
|
+
def title=(value)
|
14
|
+
@title = value
|
15
|
+
redraw
|
16
|
+
end
|
17
|
+
|
18
|
+
# @return [Float]
|
19
|
+
attr_reader :value
|
20
|
+
|
21
|
+
# @attribute [w] value
|
22
|
+
def value=(val)
|
23
|
+
@value = val.to_f
|
24
|
+
@value = 0.0 if @value < 0
|
25
|
+
@max = @value if @max&.<(@value)
|
26
|
+
redraw
|
27
|
+
end
|
28
|
+
|
29
|
+
# @return [Float]
|
30
|
+
attr_reader :max
|
31
|
+
|
32
|
+
# @attribute [w] max
|
33
|
+
def max=(val)
|
34
|
+
@max = val.to_f
|
35
|
+
if @max <= 0
|
36
|
+
@max = nil
|
37
|
+
elsif @max < @value
|
38
|
+
@max = @value
|
39
|
+
end
|
40
|
+
redraw
|
41
|
+
end
|
42
|
+
|
43
|
+
def active? = @status.nil?
|
44
|
+
def ok? = @status == :ok
|
45
|
+
def failed? = @status == :failed
|
46
|
+
|
47
|
+
def step(count: 1, title: nil)
|
48
|
+
@title = title if title
|
49
|
+
self.value += count
|
50
|
+
self
|
51
|
+
end
|
52
|
+
|
53
|
+
def done(title = nil) = finish(:ok, title)
|
54
|
+
alias ok done
|
55
|
+
|
56
|
+
def failed(title = nil) = finish(:failed, title)
|
57
|
+
|
58
|
+
alias _to_s to_s
|
59
|
+
private :_to_s
|
60
|
+
|
61
|
+
# @!visibility private
|
62
|
+
def to_s
|
63
|
+
return "#{title}: #{format('%5.2f', @value)}" unless @max
|
64
|
+
"#{@title}: #{
|
65
|
+
format(
|
66
|
+
'%5.2f of %5.2f - %5.2f%%',
|
67
|
+
@value,
|
68
|
+
@max,
|
69
|
+
100.0 * (@value / @max)
|
70
|
+
)
|
71
|
+
}"
|
72
|
+
end
|
73
|
+
|
74
|
+
# @!visibility private
|
75
|
+
def inspect = "#{_to_s.chop} #{self}>"
|
76
|
+
end
|
77
|
+
|
78
|
+
class Progress
|
79
|
+
include ProgressHelper
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def finish(status, title)
|
84
|
+
return if @status
|
85
|
+
@status = status
|
86
|
+
NattyUI.back_to_line(@pin_line)
|
87
|
+
if status == :failed
|
88
|
+
@parent.failed(title || @title)
|
89
|
+
else
|
90
|
+
cm = Theme.current.mark(:checkmark)
|
91
|
+
@parent.puts(
|
92
|
+
title || @title,
|
93
|
+
pin: @pin,
|
94
|
+
first_line_prefix: cm,
|
95
|
+
first_line_prefix_width: cm.width
|
96
|
+
)
|
97
|
+
end
|
98
|
+
self
|
99
|
+
end
|
100
|
+
|
101
|
+
def initialize(parent, title, max, pin)
|
102
|
+
@parent = parent
|
103
|
+
@value = 0.0
|
104
|
+
@title = title
|
105
|
+
@pin = pin
|
106
|
+
@pin_line = NattyUI.lines_written
|
107
|
+
@style = Theme.current.task_style
|
108
|
+
max ? self.max = max : redraw
|
109
|
+
end
|
110
|
+
|
111
|
+
def redraw
|
112
|
+
return if @status
|
113
|
+
bar = @max ? bar(@value / @max) : moving_bar
|
114
|
+
curr = bar ? [@title, bar] : [@title]
|
115
|
+
return if @last == curr
|
116
|
+
@pin_line = NattyUI.back_to_line(@pin_line) if @last
|
117
|
+
@parent.puts(
|
118
|
+
*curr,
|
119
|
+
first_line_prefix: "#{@style}➔ ",
|
120
|
+
first_line_prefix_width: 2
|
121
|
+
)
|
122
|
+
@last = curr
|
123
|
+
end
|
124
|
+
|
125
|
+
def moving_bar
|
126
|
+
"#{@style}#{'·' * @value}" if @value >= 1
|
127
|
+
end
|
128
|
+
|
129
|
+
def bar(diff)
|
130
|
+
size = [@parent.columns, 72].min - 11
|
131
|
+
percent = format('%5.2f', 100.0 * diff)
|
132
|
+
return percent if size < 10
|
133
|
+
return if percent == '100.00'
|
134
|
+
fill = '█' * (size * diff)
|
135
|
+
"#{percent}% [bright_black]┃#{@style}#{fill}[bright_black]#{
|
136
|
+
'░' * (size - fill.size)
|
137
|
+
}┃[/]"
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
class DumbProgress
|
142
|
+
include ProgressHelper
|
143
|
+
|
144
|
+
private
|
145
|
+
|
146
|
+
def finish(status, title)
|
147
|
+
return if @status
|
148
|
+
@status = status
|
149
|
+
if status == :failed
|
150
|
+
@parent.failed(title || @title)
|
151
|
+
else
|
152
|
+
cm = Theme.current.mark(:checkmark)
|
153
|
+
@parent.puts(
|
154
|
+
title || @title,
|
155
|
+
first_line_prefix: cm,
|
156
|
+
first_line_prefix_width: cm.width
|
157
|
+
)
|
158
|
+
end
|
159
|
+
self
|
160
|
+
end
|
161
|
+
|
162
|
+
def initialize(parent, title, max)
|
163
|
+
@parent = parent
|
164
|
+
@value = @last_value = 0.0
|
165
|
+
@title = title
|
166
|
+
max ? self.max = max : redraw
|
167
|
+
end
|
168
|
+
|
169
|
+
def redraw
|
170
|
+
return if @status
|
171
|
+
if @last_title != @title
|
172
|
+
@parent.puts(
|
173
|
+
@last_title = @title,
|
174
|
+
first_line_prefix: '➔ ',
|
175
|
+
first_line_prefix_width: 2
|
176
|
+
)
|
177
|
+
end
|
178
|
+
return if @max.nil? || @value < 1
|
179
|
+
percent = format('%3.0f %%', 100.0 * (@value / @max))
|
180
|
+
return if @last_percent == percent
|
181
|
+
@parent.puts(percent, first_line_prefix: ' ', first_line_prefix_width: 2)
|
182
|
+
@last_percent = percent
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
private_constant :Progress, :DumbProgress
|
187
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'element'
|
4
|
+
|
5
|
+
module NattyUI
|
6
|
+
# {Element} implemting a display section used by
|
7
|
+
#
|
8
|
+
# - {Features.section}
|
9
|
+
# - {Features.message}
|
10
|
+
# - {Features.information}
|
11
|
+
# - {Features.warning}
|
12
|
+
# - {Features.error}
|
13
|
+
# - {Features.failed}
|
14
|
+
#
|
15
|
+
class Section < Element
|
16
|
+
include StateMixin
|
17
|
+
|
18
|
+
# @!visibility private
|
19
|
+
def puts(*objects, **options) = @state ? self : super
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def finish_ok(text)
|
24
|
+
puts(*text) unless text.empty?
|
25
|
+
@state = :ok
|
26
|
+
@parent.puts(@border.bottom)
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
def finish_failed
|
31
|
+
@parent.puts(@border.bottom)
|
32
|
+
super
|
33
|
+
end
|
34
|
+
|
35
|
+
def show_title(title)
|
36
|
+
return @parent.puts(@border.top) unless title
|
37
|
+
prefix = @border.top_left
|
38
|
+
suffix = @border.top_right
|
39
|
+
@parent.puts(
|
40
|
+
title,
|
41
|
+
prefix: prefix,
|
42
|
+
prefix_width: prefix.width,
|
43
|
+
suffix: suffix,
|
44
|
+
suffix_width: suffix.width
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
48
|
+
def initialize(parent, title, msg, kind)
|
49
|
+
super(parent)
|
50
|
+
title, rest = split(title) if title && !title.empty?
|
51
|
+
@border = Theme.current.section_border(kind)
|
52
|
+
show_title(title)
|
53
|
+
@prefix = @border.prefix
|
54
|
+
@prefix_width = @prefix.size
|
55
|
+
puts(*rest) if rest && !rest.empty?
|
56
|
+
puts(*msg) unless msg.empty?
|
57
|
+
end
|
58
|
+
|
59
|
+
def split(title)
|
60
|
+
rest =
|
61
|
+
Text.each_line(
|
62
|
+
title,
|
63
|
+
limit: @parent.columns - 9,
|
64
|
+
ansi: Terminal.ansi?
|
65
|
+
).to_a
|
66
|
+
[rest.shift, rest]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,241 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'attributes'
|
4
|
+
require_relative 'table_renderer'
|
5
|
+
|
6
|
+
module NattyUI
|
7
|
+
# @todo This chapter needs more documentation.
|
8
|
+
#
|
9
|
+
# Collection of rows and columns used by {Features.table}.
|
10
|
+
#
|
11
|
+
class Table
|
12
|
+
class Column < NattyUI::Attributes::Base
|
13
|
+
include NattyUI::Attributes::Width
|
14
|
+
|
15
|
+
# @return [Integer] column index
|
16
|
+
attr_reader :index
|
17
|
+
|
18
|
+
def width
|
19
|
+
@width ||= find_width
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_s = "#{super.chop} @index:#{@index} @width:#{width.inspect}>"
|
23
|
+
alias inspect to_s
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def find_width
|
28
|
+
min = max = nil
|
29
|
+
@parent.each_cell_of(@index) do |cell|
|
30
|
+
next unless cell
|
31
|
+
m = cell.attributes.min_width
|
32
|
+
min = m if m && (min.nil? || m < min)
|
33
|
+
m = cell.attributes.max_width
|
34
|
+
max = m if m && (max.nil? || max < m)
|
35
|
+
end
|
36
|
+
wh_from(min.to_i, max.to_i)
|
37
|
+
end
|
38
|
+
|
39
|
+
def respond_to_missing?(name, _)
|
40
|
+
return suoer unless name.end_with?('=')
|
41
|
+
Cell::Attributes.public_method_defined?(name) || super
|
42
|
+
end
|
43
|
+
|
44
|
+
def method_missing(name, *args, **kwargs)
|
45
|
+
return super unless name.end_with?('=')
|
46
|
+
return super unless Cell::Attributes.public_method_defined?(name)
|
47
|
+
@parent.each_cell_of(@index) { _1.attributes.__send__(name, *args) }
|
48
|
+
args[0]
|
49
|
+
end
|
50
|
+
|
51
|
+
def initialize(parent, index, **attributes)
|
52
|
+
super(**attributes)
|
53
|
+
@parent = parent
|
54
|
+
@index = index
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class ColumnsCollection
|
59
|
+
include Enumerable
|
60
|
+
|
61
|
+
# @return [Integer] count of columns
|
62
|
+
def count = columns.size
|
63
|
+
|
64
|
+
def empty? = columns.empty?
|
65
|
+
|
66
|
+
# @return [Column, nil] column at given index
|
67
|
+
def [](index) = columns[index]
|
68
|
+
|
69
|
+
def each(&block) = columns.each(&block)
|
70
|
+
|
71
|
+
def to_s = "#{super.chop} @columns:#{columns.inspect}>"
|
72
|
+
alias inspect to_s
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def columns
|
77
|
+
cc = @parent.column_count
|
78
|
+
case cc <=> @columns.size
|
79
|
+
when -1
|
80
|
+
@columns = @columns.take(cc)
|
81
|
+
when 1
|
82
|
+
bi = @columns.size
|
83
|
+
@columns +=
|
84
|
+
Array.new(cc - @columns.size) { Column.new(@parent, bi + _1) }
|
85
|
+
else
|
86
|
+
@columns
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def initialize(parent)
|
91
|
+
@parent = parent
|
92
|
+
@columns = []
|
93
|
+
end
|
94
|
+
|
95
|
+
def initialize_copy(*_)
|
96
|
+
super
|
97
|
+
@columns = []
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
class Row
|
102
|
+
include Enumerable
|
103
|
+
|
104
|
+
def empty? = @cells.empty?
|
105
|
+
|
106
|
+
# @return [Integer] count of cells
|
107
|
+
def count = @cells.size
|
108
|
+
|
109
|
+
def each(&block) = @cells.each(&block)
|
110
|
+
|
111
|
+
# @return [Cell, nil] cell at given index
|
112
|
+
def [](index) = @cells[index]
|
113
|
+
|
114
|
+
# @return [Cell] created cell
|
115
|
+
def []=(index, *args)
|
116
|
+
@cells[index] = create_cell(args)
|
117
|
+
@cells.map! { _1 || Cell.new }
|
118
|
+
end
|
119
|
+
|
120
|
+
# Add a new cell to the row with given `text` and `attributes`.
|
121
|
+
# @return [Cell] created cell
|
122
|
+
def add(*text, **attributes)
|
123
|
+
nc = Cell.new(*text, **attributes)
|
124
|
+
@cells << nc
|
125
|
+
block_given? ? yield(nc) : nc
|
126
|
+
end
|
127
|
+
|
128
|
+
def delete(cell)
|
129
|
+
cell.is_a?(Cell) ? @cells.delete(cell) : @cells.delete_at(cell)
|
130
|
+
self
|
131
|
+
end
|
132
|
+
|
133
|
+
# Add a new cell to the row with given `text`.
|
134
|
+
# @return [Row] itself
|
135
|
+
def <<(text)
|
136
|
+
add(text)
|
137
|
+
self
|
138
|
+
end
|
139
|
+
|
140
|
+
private
|
141
|
+
|
142
|
+
def respond_to_missing?(name, _)
|
143
|
+
return super unless name.end_with?('=')
|
144
|
+
Cell::Attributes.public_method_defined?(name) || super
|
145
|
+
end
|
146
|
+
|
147
|
+
def method_missing(name, *args, **_)
|
148
|
+
return super unless name.end_with?('=')
|
149
|
+
return super unless Cell::Attributes.public_method_defined?(name)
|
150
|
+
@cells.each { _1.attributes.__send__(name, *args) }
|
151
|
+
args[0]
|
152
|
+
end
|
153
|
+
|
154
|
+
def initialize
|
155
|
+
@cells = []
|
156
|
+
end
|
157
|
+
|
158
|
+
def initialize_copy(*_)
|
159
|
+
super
|
160
|
+
@cells = @cells.map(&:dup)
|
161
|
+
end
|
162
|
+
|
163
|
+
def create_cell(args)
|
164
|
+
args.flatten!(1)
|
165
|
+
Cell.new(*args)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
class Cell
|
170
|
+
include TextWithAttributes
|
171
|
+
|
172
|
+
class Attributes < NattyUI::Attributes::Base
|
173
|
+
prepend NattyUI::Attributes::Width
|
174
|
+
prepend NattyUI::Attributes::Padding
|
175
|
+
prepend NattyUI::Attributes::Align
|
176
|
+
prepend NattyUI::Attributes::Vertical
|
177
|
+
prepend NattyUI::Attributes::Style
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
class Attributes < NattyUI::Attributes::Base
|
182
|
+
prepend NattyUI::Attributes::Border
|
183
|
+
prepend NattyUI::Attributes::BorderStyle
|
184
|
+
prepend NattyUI::Attributes::BorderAround
|
185
|
+
end
|
186
|
+
|
187
|
+
include Enumerable
|
188
|
+
include WithAttributes
|
189
|
+
|
190
|
+
# @return [ColumnsCollection] table columns
|
191
|
+
attr_reader :columns
|
192
|
+
|
193
|
+
def empty? = @rows.empty?
|
194
|
+
|
195
|
+
# @return [Integer] count of rows
|
196
|
+
def count = @rows.size
|
197
|
+
|
198
|
+
# @return [Integer] count of columns
|
199
|
+
def column_count = @rows.empty? ? 0 : @rows.max_by(&:count).count
|
200
|
+
|
201
|
+
def each(&block) = @rows.each(&block)
|
202
|
+
|
203
|
+
def [](row_index, column_index = nil)
|
204
|
+
row = @rows[row_index] or return
|
205
|
+
column_index ? row[column_index] : row
|
206
|
+
end
|
207
|
+
|
208
|
+
# @return [Row] created row
|
209
|
+
def add(*text, **attributes)
|
210
|
+
nr = Row.new
|
211
|
+
@rows << nr
|
212
|
+
text.each { nr.add(_1, **attributes) }
|
213
|
+
block_given? ? yield(nr) : nr
|
214
|
+
end
|
215
|
+
|
216
|
+
def delete(row)
|
217
|
+
row.is_a?(Row) ? @rows.delete(row) : @rows.delete_at(row)
|
218
|
+
end
|
219
|
+
|
220
|
+
def each_cell_of(column_index)
|
221
|
+
return to_enum(__method__, column_index) unless block_given?
|
222
|
+
return if (column_index = column_index.to_i) >= column_count
|
223
|
+
@rows.each { yield(_1[column_index]) }
|
224
|
+
nil
|
225
|
+
end
|
226
|
+
|
227
|
+
private
|
228
|
+
|
229
|
+
def initialize(**attributes)
|
230
|
+
super
|
231
|
+
@rows = []
|
232
|
+
@columns = ColumnsCollection.new(self)
|
233
|
+
end
|
234
|
+
|
235
|
+
def initialize_copy(*_)
|
236
|
+
super
|
237
|
+
@columns = ColumnsCollection.new(self)
|
238
|
+
@rows = @rows.map(&:dup)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|