natty-ui 0.34.0 → 1.0.2

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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +0 -1
  3. data/README.md +6 -6
  4. data/examples/24bit-colors.rb +9 -5
  5. data/examples/3bit-colors.rb +7 -7
  6. data/examples/8bit-colors.rb +5 -5
  7. data/examples/attributes.rb +2 -3
  8. data/examples/elements.rb +9 -6
  9. data/examples/examples.rb +9 -9
  10. data/examples/frames.rb +31 -0
  11. data/examples/hbars.rb +6 -3
  12. data/examples/info.rb +13 -10
  13. data/examples/key-codes.rb +8 -9
  14. data/examples/ls.rb +24 -22
  15. data/examples/named-colors.rb +4 -3
  16. data/examples/sections.rb +27 -17
  17. data/examples/select.rb +28 -0
  18. data/examples/sh.rb +25 -7
  19. data/examples/tables.rb +19 -37
  20. data/examples/tasks.rb +32 -22
  21. data/examples/vbars.rb +5 -3
  22. data/lib/natty-ui/dumb_progress.rb +68 -0
  23. data/lib/natty-ui/element.rb +64 -65
  24. data/lib/natty-ui/features.rb +773 -872
  25. data/lib/natty-ui/frame.rb +87 -0
  26. data/lib/natty-ui/helper/table.rb +1376 -0
  27. data/lib/natty-ui/margin.rb +83 -0
  28. data/lib/natty-ui/progress.rb +116 -149
  29. data/lib/natty-ui/renderer/bars.rb +93 -0
  30. data/lib/natty-ui/renderer/choice.rb +56 -0
  31. data/lib/natty-ui/renderer/dumb_choice.rb +34 -0
  32. data/lib/natty-ui/renderer/dumb_select.rb +60 -0
  33. data/lib/natty-ui/renderer/dumb_shell_runner.rb +19 -0
  34. data/lib/natty-ui/renderer/heading.rb +26 -0
  35. data/lib/natty-ui/renderer/horizontal_rule.rb +32 -0
  36. data/lib/natty-ui/{ls_renderer.rb → renderer/ls.rb} +15 -27
  37. data/lib/natty-ui/renderer/mark.rb +13 -0
  38. data/lib/natty-ui/renderer/quote.rb +13 -0
  39. data/lib/natty-ui/renderer/select.rb +63 -0
  40. data/lib/natty-ui/renderer/shell.rb +15 -0
  41. data/lib/natty-ui/renderer/shell_runner.rb +29 -0
  42. data/lib/natty-ui/renderer/table_renderer.rb +429 -0
  43. data/lib/natty-ui/section.rb +142 -41
  44. data/lib/natty-ui/task.rb +39 -27
  45. data/lib/natty-ui/temporary.rb +27 -14
  46. data/lib/natty-ui/utils/border.rb +139 -0
  47. data/lib/natty-ui/utils/str_const.rb +62 -0
  48. data/lib/natty-ui/utils/utils.rb +47 -0
  49. data/lib/natty-ui/version.rb +1 -1
  50. data/lib/natty-ui.rb +87 -30
  51. metadata +31 -28
  52. data/examples/cols.rb +0 -38
  53. data/examples/illustration.rb +0 -60
  54. data/examples/options.rb +0 -28
  55. data/examples/themes.rb +0 -51
  56. data/lib/natty-ui/attributes.rb +0 -593
  57. data/lib/natty-ui/choice.rb +0 -67
  58. data/lib/natty-ui/dumb_choice.rb +0 -47
  59. data/lib/natty-ui/dumb_options.rb +0 -64
  60. data/lib/natty-ui/framed.rb +0 -51
  61. data/lib/natty-ui/hbars_renderer.rb +0 -66
  62. data/lib/natty-ui/options.rb +0 -78
  63. data/lib/natty-ui/shell_renderer.rb +0 -91
  64. data/lib/natty-ui/table.rb +0 -325
  65. data/lib/natty-ui/table_renderer.rb +0 -165
  66. data/lib/natty-ui/theme.rb +0 -403
  67. data/lib/natty-ui/utils.rb +0 -111
  68. data/lib/natty-ui/vbars_renderer.rb +0 -49
  69. data/lib/natty-ui/width_finder.rb +0 -137
  70. data/natty-ui.gemspec +0 -34
@@ -1,165 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'terminal/text'
4
- require_relative 'width_finder'
5
-
6
- module NattyUI
7
- class TableRenderer
8
- def self.[](table, max_width)
9
- columns = table.columns.map(&:width)
10
- return [] if columns.empty?
11
- att = table.attributes
12
- case att.max_width
13
- when Float
14
- max_width *= att.max_width
15
- when Integer
16
- max_width = [max_width, att.max_width].min
17
- end
18
- unless att.border_chars.nil?
19
- max_width -= (columns.size - 1)
20
- max_width -= 2 if att.border_around
21
- end
22
- return [] if max_width < columns.size
23
- new(columns, table.each.to_a, att, max_width).lines
24
- end
25
-
26
- attr_reader :lines
27
-
28
- private
29
-
30
- def initialize(columns, rows, att, max_width)
31
- @max_width, @columns = WidthFinder.find(columns, max_width)
32
- init_borders(att)
33
- @columns = @columns.each.with_index
34
-
35
- @lines = render(rows.shift)
36
- @lines.unshift(@b_top) if @b_top
37
-
38
- if @b_between
39
- rows.each do |row|
40
- @lines << @b_between
41
- @lines += render(row)
42
- end
43
- else
44
- rows.each { |row| @lines += render(row) }
45
- end
46
-
47
- @lines << @b_bottom if @b_bottom
48
- end
49
-
50
- def render(row)
51
- cells = @columns.map { |cw, i| Cell.new(row[i], cw) }
52
- height = cells.max_by { _1.text.size }.text.size
53
- cells.each { _1.correct_height(height) }
54
- Array.new(height) do |line|
55
- "#{@b_outer}#{cells.map { _1.text[line] }.join(@b_inner)}#{@b_outer}"
56
- end
57
- end
58
-
59
- def init_borders(att)
60
- chars = att.border_chars or return
61
- style = border_style(att)
62
- @b_inner = style[chars[9]]
63
- return if chars[10] == ' '
64
- return init_borders_around(chars, style) if att.border_around
65
- @b_between = chars[10] * (@max_width + @columns.size - 1)
66
- i = -1
67
- @columns[0..-2].each { |w| @b_between[i += w + 1] = chars[4] }
68
- @b_between = style[@b_between]
69
- end
70
-
71
- def init_borders_around(chars, style)
72
- fw = chars[10] * (@max_width + @columns.size - 1)
73
- @b_top = "#{chars[0]}#{fw}#{chars[2]}"
74
- @b_bottom = "#{chars[6]}#{fw}#{chars[8]}"
75
- @b_between = "#{chars[3]}#{fw}#{chars[5]}"
76
- i = 0
77
- @columns[0..-2].each do |w|
78
- @b_top[i += w + 1] = chars[1]
79
- @b_bottom[i] = chars[7]
80
- @b_between[i] = chars[4]
81
- end
82
- @b_top = style[@b_top]
83
- @b_bottom = style[@b_bottom]
84
- @b_between = style[@b_between]
85
- @b_outer = @b_inner
86
- end
87
-
88
- def border_style(att)
89
- style = att.border_style_bbcode
90
- style ? ->(line) { "#{style}#{line}[/]" } : lambda(&:itself)
91
- end
92
-
93
- class Cell
94
- attr_reader :width, :text
95
-
96
- def correct_height(height)
97
- return self if (diff = height - @text.size) <= 0
98
- @text =
99
- case @vertical
100
- when :bottom
101
- [Array.new(diff, @empty), @text]
102
- when :middle
103
- tc = diff / 2
104
- [Array.new(tc, @empty), @text, Array.new(diff - tc, @empty)]
105
- else
106
- [@text, Array.new(diff, @empty)]
107
- end.flatten(1)
108
- self
109
- end
110
-
111
- def initialize(cell, width)
112
- return @text = [] unless cell
113
- att = cell.attributes
114
- @padding = att.padding.dup
115
- @align = att.align
116
- @vertical = att.vertical
117
- @style = att.style_bbcode
118
- @text = width_corrected(cell.text, width, att.eol == false)
119
- end
120
-
121
- private
122
-
123
- def width_corrected(text, width, ignore_newline)
124
- @width, @padding[3], @padding[1] =
125
- WidthFinder.adjust(width, @padding[3], @padding[1])
126
- @empty = @style ? "#{@style}#{' ' * width}[/]" : ' ' * width
127
- [
128
- Array.new(@padding[0], @empty),
129
- Text.each_line_with_size(
130
- *text,
131
- limit: @width,
132
- bbcode: true,
133
- ignore_newline: ignore_newline,
134
- ansi: Terminal.ansi?
135
- ).map(&txt_fmt),
136
- Array.new(@padding[2], @empty)
137
- ].flatten(1)
138
- end
139
-
140
- def txt_fmt
141
- lpad, rpad = pads
142
- case @align
143
- when :right
144
- ->(txt, w) { "#{lpad}#{' ' * (@width - w)}#{txt}#{rpad}" }
145
- when :centered
146
- lambda do |txt, w|
147
- s = @width - w
148
- "#{lpad}#{' ' * (lw = s / 2)}#{txt}#{' ' * (s - lw)}#{rpad}"
149
- end
150
- else
151
- ->(txt, w) { "#{lpad}#{txt}#{' ' * (@width - w)}#{rpad}" }
152
- end
153
- end
154
-
155
- def pads
156
- return ' ' * @padding[3], "[/]#{' ' * @padding[1]}" unless @style
157
- ["#{@style}#{' ' * @padding[3]}", "#{' ' * @padding[1]}[/]"]
158
- end
159
- end
160
-
161
- private_constant :Cell
162
- end
163
-
164
- private_constant :TableRenderer
165
- end
@@ -1,403 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'utils'
4
-
5
- module NattyUI
6
- # @todo This chapter needs more documentation.
7
- #
8
- # A theme defines the style of elements.
9
- #
10
- class Theme
11
- class << self
12
- # Currently used theme
13
- #
14
- # @return [Theme]
15
- attr_reader :current
16
-
17
- # Name of currently used theme
18
- #
19
- # @return [Symbol]
20
- attr_reader :current_name
21
-
22
- # Names of all registered themes
23
- #
24
- # @attribute [r] names
25
- # @return [Array<Symbol>]
26
- def names = @ll.keys.sort!
27
-
28
- # Use a theme
29
- #
30
- # @param name [Symbol]
31
- # @return [Symbol] name of used theme
32
- def use(name)
33
- sel = find(name).last
34
- if sel.is_a?(Proc)
35
- sel[builder = Builder.new]
36
- sel = @ll[name][-1] = builder.build.freeze
37
- end
38
- @current = sel
39
- @current_name = name
40
- end
41
-
42
- # Get the descrition of a theme.
43
- #
44
- # @param name [Symbol]
45
- # @return [String] description of the theme
46
- def description(name) = find(name).first
47
-
48
- # Register a theme
49
- #
50
- # @param name [Symbol] theme name
51
- # @param description [#to_s] theme description
52
- # @yieldparam builder [Builder] theme build helper
53
- # @return [Theme] itself
54
- def register(name, description = nil, &block)
55
- raise(ArgumentError, 'block missing') unless block
56
- @ll[name.to_sym] = [description&.to_s || name.to_s.capitalize, block]
57
- self
58
- end
59
-
60
- private
61
-
62
- def find(name)
63
- @ll.fetch(name) { raise(ArgumentError, "no such theme - #{name}") }
64
- end
65
- end
66
-
67
- # @todo This chapter needs more documentation.
68
- #
69
- # Helper class to define a {Theme}.
70
- #
71
- class Builder
72
- attr_accessor :section_border
73
- attr_reader :mark,
74
- :border,
75
- :heading,
76
- :heading_sytle,
77
- :section_styles,
78
- :task_style,
79
- :choice_current_style,
80
- :choice_style,
81
- :sh_out_style,
82
- :sh_err_style
83
-
84
- def heading_sytle=(value)
85
- @heading_sytle = Utils.style(value)
86
- end
87
-
88
- def task_style=(value)
89
- @task_style = Utils.style(value)
90
- end
91
-
92
- def choice_current_style=(value)
93
- @choice_current_style = Utils.style(value)
94
- end
95
-
96
- def choice_style=(value)
97
- @choice_style = Utils.style(value)
98
- end
99
-
100
- def sh_out_style=(value)
101
- @sh_out_style = Utils.style(value)
102
- end
103
-
104
- def sh_err_style=(value)
105
- @sh_err_style = Utils.style(value)
106
- end
107
-
108
- # @return [Theme] new theme
109
- def build = Theme.new(self)
110
-
111
- def define_marker(**defs)
112
- @mark.merge!(defs)
113
- self
114
- end
115
-
116
- def define_border(**defs)
117
- defs.each_pair do |name, str|
118
- s = str.to_s
119
- case Text.width(s, bbcode: false)
120
- when 1
121
- @border[name.to_sym] = "#{s * 11}  "
122
- when 11
123
- @border[name.to_sym] = "#{s}  "
124
- when 13
125
- @border[name.to_sym] = s
126
- else
127
- raise(
128
- TypeError,
129
- "invalid boder definition for #{name} - #{str.inspect}"
130
- )
131
- end
132
- end
133
- self
134
- end
135
-
136
- def define_heading(*defs)
137
- @heading = defs.flatten.take(6)
138
- @heading += Array.new(6 - @heading.size, @heading[-1])
139
- self
140
- end
141
-
142
- def define_section(**defs)
143
- defs.each_pair do |name, style|
144
- style = Utils.style(style)
145
- @section_styles[name.to_sym] = style if style
146
- end
147
- self
148
- end
149
-
150
- private
151
-
152
- def initialize
153
- define_heading(%w[╸╸╺╸╺━━━ ╴╶╴╶─═══ ╴╶╴╶─── ════ ━━━━ ────])
154
- @mark = {
155
- default: '•',
156
- bullet: '•',
157
- checkmark: '✓',
158
- quote: '▍',
159
- information: '𝒊',
160
- warning: '!',
161
- error: '𝙓',
162
- failed: '𝑭',
163
- current: '➔',
164
- choice: '◦',
165
- current_choice: '◉',
166
- option: '[ ]',
167
- option_selected: '[X]',
168
- sh_out: ':',
169
- sh_err: '𝙓'
170
- }
171
- @border = {
172
- ######### 0123456789012
173
- default: '┌┬┐├┼┤└┴┘│─╶╴',
174
- defaulth: '───────── ─╶╴',
175
- defaultv: '││││││││││ ',
176
- double: '╔╦╗╠╬╣╚╩╝║═',
177
- doubleh: '═════════ ═',
178
- doublev: '║║║║║║║║║║ ',
179
- heavy: '┏┳┓┣╋┫┗┻┛┃━╺╸',
180
- heavyh: '━━━━━━━━━ ━╺╸',
181
- heavyv: '┃┃┃┃┃┃┃┃┃┃ ',
182
- rounded: '╭┬╮├┼┤╰┴╯│─╶╴'
183
- }
184
- @section_border = :rounded
185
- @section_styles = {}
186
- end
187
- end
188
-
189
- attr_reader :task_style,
190
- :choice_current_style,
191
- :choice_style,
192
- :sh_out_style,
193
- :sh_err_style,
194
- :option_states
195
-
196
- def defined_marks = @mark.keys.sort!
197
- def defined_borders = @border.keys.sort!
198
- def heading(index) = @heading[index.to_i.clamp(1, 6) - 1]
199
-
200
- def mark(value)
201
- return @mark[value] if value.is_a?(Symbol)
202
- (element = Str.new(value, true)).empty? ? @mark[:default] : element
203
- end
204
-
205
- def border(value)
206
- return @border[value] if value.is_a?(Symbol)
207
- case Text.width(value = value.to_s, bbcode: false)
208
- when 1
209
- "#{value * 11}  "
210
- when 11
211
- "#{value}  "
212
- when 13
213
- value
214
- else
215
- @border[:default]
216
- end
217
- end
218
-
219
- def section_border(kind)
220
- kind.is_a?(Symbol) ? @sections[kind] : @sections[:default]
221
- end
222
-
223
- def initialize(theme)
224
- @heading = create_heading(theme.heading, theme.heading_sytle).freeze
225
- @border = create_border(theme.border).freeze
226
- @mark = create_mark(theme.mark).freeze
227
- @task_style = as_style(theme.task_style)
228
- @choice_current_style = as_style(theme.choice_current_style)
229
- @choice_style = as_style(theme.choice_style)
230
- @sh_out_style = as_style(theme.sh_out_style)
231
- @sh_err_style = as_style(theme.sh_err_style)
232
- @sections =
233
- create_sections(
234
- SectionBorder.create(border(theme.section_border)),
235
- theme.section_styles.dup.compare_by_identity
236
- )
237
- @option_states = create_option_states
238
- end
239
-
240
- private
241
-
242
- def as_style(value) = (Ansi[*value].freeze if value)
243
-
244
- def create_option_states
245
- # [current?][selected?]
246
- c = @mark[:current_choice]
247
- n = @mark[:none]
248
- uns = @mark[:option]
249
- sel = @mark[:option_selected]
250
- {
251
- false => { false => n + uns, true => n + sel }.compare_by_identity,
252
- true => { false => c + uns, true => c + sel }.compare_by_identity
253
- }.compare_by_identity.freeze
254
- end
255
-
256
- def create_sections(template, styles)
257
- Hash
258
- .new do |h, kind|
259
- h[kind] = SectionBorder.new(*template.parts(styles[kind])).freeze
260
- end
261
- .compare_by_identity
262
- end
263
-
264
- def create_mark(mark)
265
- return {} if mark.empty?
266
- mark = mark.to_h { |n, e| [n.to_sym, Str.new("#{e} ")] }
267
- mark[:none] ||= Str.new('  ', 2)
268
- with_default(mark)
269
- end
270
-
271
- def create_border(border)
272
- return {} if border.empty?
273
- with_default(border.transform_values { _1.dup.freeze })
274
- end
275
-
276
- def create_heading(heading, style)
277
- return create_styled_heading(heading, style) if style
278
- heading.map do |left|
279
- right = " #{left.reverse}"
280
- [left = Str.new("#{left} ", true), Str.new(right, left.width)]
281
- end
282
- end
283
-
284
- def create_styled_heading(heading, style)
285
- heading.map do |left|
286
- right = Ansi.decorate(left.reverse, *style)
287
- [
288
- left = Str.new("#{Ansi.decorate(left, *style)} ", true),
289
- Str.new(" #{right}", left.width)
290
- ]
291
- end
292
- end
293
-
294
- def with_default(map)
295
- map.default = (map[:default] ||= map[map.first.first])
296
- map.compare_by_identity
297
- end
298
-
299
- SectionBorder =
300
- Struct.new(:top, :top_left, :top_right, :bottom, :prefix) do
301
- def self.create(border)
302
- mid = border[10] * 2
303
- mid2 = mid * 2
304
- right = "#{border[11]}#{border[12] * 2}"
305
- new(
306
- border[0] + mid2 + right,
307
- border[0] + mid,
308
- mid + right,
309
- border[6] + mid2 + right,
310
- border[9]
311
- )
312
- end
313
-
314
- def parts(style)
315
- if style
316
- style = Ansi[*style]
317
- reset = Ansi::RESET
318
- end
319
- [
320
- Str.new("#{style}#{top}#{reset}", 6),
321
- Str.new("#{style}#{top_left}#{reset} ", 4),
322
- Str.new(" #{style}#{top_right}#{reset}", 6),
323
- Str.new("#{style}#{bottom}#{reset}", 6),
324
- Str.new("#{style}#{prefix}#{reset} ", 2)
325
- ]
326
- end
327
- end
328
-
329
- private_constant :SectionBorder
330
-
331
- @ll = {}
332
-
333
- register(:mono, 'Monochrome – Non-ANSI fallback', &:itself)
334
-
335
- register(:default, 'Default – uses default colors') do |theme|
336
- theme.heading_sytle = :bright_blue
337
- theme.task_style = %i[bright_green b]
338
- # theme.choice_style =
339
- theme.sh_out_style = :default
340
- theme.sh_err_style = :bright_yellow
341
- theme.choice_current_style = %i[bright_white on_blue b]
342
- theme.define_marker(
343
- bullet: '[bright_white]•[/fg]',
344
- checkmark: '[bright_green]✓[/fg]',
345
- quote: '[bright_blue]▍[/fg]',
346
- information: '[bright_yellow]𝒊[/fg]',
347
- warning: '[bright_yellow]![/fg]',
348
- error: '[red]𝙓[/fg]',
349
- failed: '[bright_red]𝑭[/fg]',
350
- current: '[bright_green]➔[/fg]',
351
- choice: '[bright_white]◦[/fg]',
352
- current_choice: '[bright_green]➔[/fg]',
353
- option: '[dim][ ][/dim]',
354
- option_selected: '[dim][[/dim][green]X[/fg][dim]][/dim]',
355
- sh_out: '[bright_white]:[/fg]',
356
- sh_err: '[red]𝙓[/fg]'
357
- )
358
- theme.define_section(
359
- default: :bright_blue,
360
- message: :bright_blue,
361
- information: :bright_blue,
362
- warning: :bright_yellow,
363
- error: :red,
364
- failed: :bright_red
365
- )
366
- end
367
-
368
- register(:emoji, 'Emoji – emoticons and default colors') do |theme|
369
- theme.heading_sytle = :bright_blue
370
- theme.task_style = %i[bright_green b]
371
- # theme.choice_style =
372
- theme.sh_out_style = :default
373
- theme.sh_err_style = :bright_red
374
- theme.choice_current_style = %i[bright_white on_blue b]
375
- theme.define_marker(
376
- bullet: '▫️',
377
- checkmark: '✅',
378
- quote: '[bright_blue]▍[/fg]',
379
- information: '📌',
380
- warning: '⚠️',
381
- error: '❗️',
382
- failed: '‼️',
383
- current: '➡️',
384
- choice: '[bright_white]•[/fg]',
385
- current_choice: '[bright_green]●[/fg]',
386
- option: '[dim][ ][/dim] ',
387
- option_selected: '[dim][[/dim][green]X[/fg][dim]][/dim]',
388
- sh_out: '[white b]:[/]',
389
- sh_err: '❗️'
390
- )
391
- theme.define_section(
392
- default: :bright_blue,
393
- message: :bright_blue,
394
- information: :bright_blue,
395
- warning: :bright_yellow,
396
- error: :red,
397
- failed: :bright_red
398
- )
399
- end
400
-
401
- use(Terminal.colors == 2 ? :mono : :default)
402
- end
403
- end
@@ -1,111 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module NattyUI
4
- module Utils
5
- class << self
6
- def style(value)
7
- value =
8
- case value
9
- when Array
10
- value.dup
11
- when Enumerable
12
- value.to_a
13
- when Symbol, Integer
14
- [value]
15
- when nil
16
- return
17
- else
18
- value.to_s.delete_prefix('[').delete_suffix(']').split
19
- end
20
- value.uniq!
21
- value.keep_if { Ansi.valid?(_1) }.empty? ? nil : value
22
- end
23
-
24
- def align(value)
25
- POS_ALI.include?(value) ? value : :left
26
- end
27
-
28
- def position(value)
29
- value if POS_ALI.include?(value)
30
- end
31
-
32
- def vertical(value)
33
- VERT.include?(value) ? value : :top
34
- end
35
-
36
- def split_table_attr(values)
37
- [values.slice(*TAB_ATTR), values.except(*TAB_ATTR)]
38
- end
39
-
40
- def padding(*value)
41
- value = value.flatten.take(4).map! { [0, _1.to_i].max }
42
- case value.size
43
- when 0
44
- [0, 0, 0, 0]
45
- when 1
46
- Array.new(4, value[0])
47
- when 2
48
- value * 2
49
- when 3
50
- value << value[1]
51
- else
52
- value
53
- end
54
- end
55
- alias margin padding
56
-
57
- def as_size(range, value)
58
- return range.begin if value == :min
59
- return range.end if value.nil? || value.is_a?(Symbol)
60
- (
61
- if value.is_a?(Numeric)
62
- (value > 0 && value < 1 ? (range.end * value) : value).round
63
- else
64
- value.to_i
65
- end
66
- ).clamp(range)
67
- end
68
- end
69
-
70
- POS_ALI = %i[right centered].freeze
71
- VERT = %i[bottom middle].freeze
72
- TAB_ATTR = %i[border_around border border_style position].freeze
73
- end
74
-
75
- class Str
76
- attr_reader :to_s
77
- alias to_str to_s
78
- def empty? = width == 0
79
- def inspect = @to_s.inspect
80
-
81
- def width
82
- return @width if @width
83
- @width = Text.width(@to_s)
84
- freeze
85
- @width
86
- end
87
-
88
- def +(other)
89
- other = Str.new(other) unless other.is_a?(Str)
90
- Str.new(@to_s + other.to_s, width + other.width)
91
- end
92
-
93
- if Terminal.ansi?
94
- def initialize(str, width = nil)
95
- @to_s = Ansi.bbcode(str).freeze
96
- return unless width
97
- @width = @width.is_a?(Integer) ? width : Text.width(@to_s)
98
- freeze
99
- end
100
- else
101
- def initialize(str, width = nil)
102
- @to_s = Ansi.plain(str).freeze
103
- return unless width
104
- @width = @width.is_a?(Integer) ? width : Text.width(@to_s)
105
- freeze
106
- end
107
- end
108
- end
109
-
110
- private_constant :Utils, :Str
111
- end