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,1055 +1,956 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NattyUI
4
- # These are all supported features by {NattyUI} or any other sub- element
5
- # like {section}, {message}, {task}, ...
6
- #
7
- # Any printed text can contain *BBCode*-like embedded ANSI attributes which
8
- # will be used when the output terminal supports attributes and colors.
4
+ # Mixin that provides all UI methods to {NattyUI} and every {Element}.
9
5
  #
6
+ # You never include or extend this module directly. It is already available
7
+ # on {NattyUI} (via `extend`) and on every {Element} subclass (via
8
+ # `include`). The {Kernel#ui} helper always returns an object that responds
9
+ # to every method defined here.
10
10
  module Features
11
11
  #
12
- # @!group Printing Methods
13
- #
14
-
15
- # Print given text as lines.
16
- #
17
- # @example Print two lines text, right aligned
18
- # ui.puts "Two lines", "of nice text", align: :right
19
- # # => Two lines
20
- # # => of nice text
21
- #
22
- # @example Print two lines text, with a prefix
23
- # ui.puts "Two lines", "of nice text", prefix: ': '
24
- # # => : Two lines
25
- # # => : of nice text
26
- #
27
- # @see #pin
28
- #
29
- # @param text [#to_s]
30
- # one or more convertible objects to print line by line
31
- # @param options [{Symbol => Object}]
32
- # @option options [:left, :right, :centered] :align (:left)
33
- # text alignment
34
- # @option options [true, false] :eol (true)
35
- # whether to respect newline characters
36
- #
37
- # @return [Features]
38
- # itself
39
- def puts(*text, **options)
40
- if options.empty?
41
- bbcode = true
42
- max_width = Terminal.columns
12
+ # @!group Output methods
13
+ #
14
+
15
+ # Formats and prints text to the terminal.
16
+ #
17
+ # Text is word-wrapped to fit the available column width. BBCode-like
18
+ # markup is interpreted by default (e.g. `[b]bold[/b]`, `[red]text[/fg]`).
19
+ #
20
+ # @example Simple output with BBCode markup
21
+ # ui.puts 'Hello, [b]world[/b]!'
22
+ #
23
+ # @example Centred text with a prefix
24
+ # ui.puts 'Step 1 of 3', 'Connect to server',
25
+ # align: :center, prefix: '[cyan]→ '
26
+ #
27
+ # @example Suppress line breaks and compact spaces
28
+ # ui.puts <<~TEXT, eol: false, spaces: false
29
+ # Line one
30
+ # Line two here
31
+ # TEXT
32
+ # # => Line one Line two here
33
+ #
34
+ # @param text [#to_s, ...] one or more text values to print
35
+ # @param popts [Hash] a customizable set of print options
36
+ # @option popts [Boolean] :bbcode (true)
37
+ # interpret BBCode-like markup in the text
38
+ # @option popts [Boolean] :eol (true)
39
+ # when `false`, line breaks inside the text are collapsed to spaces
40
+ # @option popts [Boolean] :spaces (true)
41
+ # when `false`, runs of whitespace are compacted to a single space
42
+ # @option popts [:left, :center, :right, nil] :align
43
+ # horizontal text alignment within the available width
44
+ # @option popts [#to_s, nil] :prefix
45
+ # string prepended to the first output line
46
+ # @option popts [#to_s, nil] :suffix
47
+ # string appended to the last output line
48
+ # @option popts [#to_int, Float, nil] :max_width
49
+ # maximum line width in characters; `nil` uses the full terminal width;
50
+ # a negative `#to_int` value is an offset from the terminal width;
51
+ # a `Float` in `0.0..1.0` is a fraction of the terminal width
52
+ # @return [Features] itself
53
+ def puts(*text, **popts)
54
+ return if text.empty?
55
+
56
+ popts.delete(:pin) # ignore!
57
+
58
+ if popts.empty?
59
+ popts[:bbcode] = true
60
+ popts[:width] = max_width = columns
61
+ return self if max_width < 1
43
62
  else
44
- bbcode = true if (bbcode = options[:bbcode]).nil?
45
- ignore_newline = options[:eol] == false || options[:ignore_newline]
63
+ popts[:bbcode] = true unless popts.key?(:bbcode)
64
+ max_width = __determine_max_width(popts.delete(:max_width))
65
+ return self if max_width < 1
46
66
 
47
- if (max_width = options[:max_width]).nil?
48
- max_width = Terminal.columns
49
- elsif max_width < 1
50
- if max_width > 0
51
- max_width *= Terminal.columns
52
- elsif max_width < 0
53
- max_width += Terminal.columns
54
- else
55
- return self
56
- end
67
+ unless (prefix = popts.delete(:prefix)).nil?
68
+ prefix = StrConst[prefix] unless StrConst === prefix
69
+ return self if (max_width -= prefix.width) < 1
57
70
  end
58
71
 
59
- return self if max_width <= 0
60
-
61
- prefix_width =
62
- if (prefix = options[:prefix])
63
- prefix = Ansi.bbcode(prefix) if bbcode
64
- options[:prefix_width] || Text.width(prefix, bbcode: false)
65
- else
66
- 0
67
- end
68
-
69
- if (first_line = options[:first_line_prefix])
70
- first_line = Ansi.bbcode(first_line) if bbcode
71
- first_line_width =
72
- options[:first_line_prefix_width] ||
73
- Text.width(first_line, bbcode: false)
74
-
75
- if prefix_width < first_line_width
76
- prefix_next = "#{prefix}#{' ' * (first_line_width - prefix_width)}"
77
- prefix = first_line
78
- prefix_width = first_line_width
79
- else
80
- prefix_next = prefix
81
- prefix =
82
- if first_line_width < prefix_width
83
- first_line + (' ' * (prefix_width - first_line_width))
84
- else
85
- first_line
86
- end
72
+ if (cprefix = popts.delete(:cprefix)).nil?
73
+ cprefix = prefix
74
+ elsif cprefix == true
75
+ prefix, cprefix = StrConst.spacer(prefix.width), prefix if prefix
76
+ else
77
+ cprefix = StrConst[cprefix] unless StrConst === cprefix
78
+ unless prefix
79
+ return self if (max_width -= cprefix.width) < 1
80
+ prefix = StrConst.spacer(cprefix.width)
87
81
  end
88
82
  end
89
83
 
90
- max_width -= prefix_width
91
-
92
- if (suffix = options[:suffix])
93
- suffix = Ansi.bbcode(suffix) if bbcode
94
- max_width -=
95
- options[:suffix_width] || Text.width(suffix, bbcode: false)
84
+ unless (suffix = popts.delete(:suffix)).nil?
85
+ suffix = StrConst[suffix] unless StrConst === suffix
86
+ return self if (max_width -= suffix.width) < 1
96
87
  end
97
- end
98
-
99
- return self if max_width <= 0
100
88
 
101
- lines =
102
- Text.each_line_with_size(
103
- *text,
104
- limit: max_width,
105
- bbcode: bbcode,
106
- ansi: Terminal.ansi?,
107
- ignore_newline: ignore_newline
108
- )
109
- tail = options[:tail] and lines = lines.to_a.last(tail)
110
- @__eol ||= Terminal.ansi? ? "\e[m\n" : "\n"
111
-
112
- if (align = options[:align]).nil?
113
- lines.each do |line, _|
114
- Terminal.print(prefix, line, suffix, @__eol, bbcode: false)
115
- @lines_written += 1
116
- prefix, prefix_next = prefix_next, nil if prefix_next
89
+ if (csuffix = popts.delete(:csuffix)).nil?
90
+ csuffix = suffix
91
+ elsif csuffix == true
92
+ suffix, csuffix = StrConst.spacer(suffix.width), suffix if suffix
93
+ else
94
+ csuffix = StrConst[csuffix] unless StrConst === csuffix
95
+ return self if (max_width -= csuffix.width) < 1
96
+ suffix = StrConst.spacer(csuffix.width)
117
97
  end
118
- return self
119
- end
120
98
 
121
- unless options[:expand]
122
- max_width = (lines = lines.to_a).max_by(&:last)[-1]
99
+ popts[:width] = max_width
123
100
  end
124
101
 
125
- case align
126
- when :right
127
- lines.each do |line, width|
128
- Terminal.print(
129
- prefix,
130
- ' ' * (max_width - width),
131
- line,
132
- suffix,
133
- @__eol,
134
- bbcode: false
135
- )
136
- @lines_written += 1
137
- prefix, prefix_next = prefix_next, nil if prefix_next
138
- end
139
- when :centered
140
- lines.each do |line, width|
141
- space = max_width - width
142
- Terminal.print(
143
- prefix,
144
- ' ' * (lw = space / 2),
145
- line,
146
- ' ' * (space - lw),
147
- suffix,
148
- @__eol,
149
- bbcode: false
150
- )
151
- @lines_written += 1
152
- prefix, prefix_next = prefix_next, nil if prefix_next
153
- end
154
- else
155
- lines.each do |line, width|
156
- Terminal.print(
157
- prefix,
158
- line,
159
- ' ' * (max_width - width),
160
- suffix,
161
- @__eol,
162
- bbcode: false
163
- )
164
- @lines_written += 1
165
- prefix, prefix_next = prefix_next, nil if prefix_next
102
+ popts[:ansi] = Terminal.ansi?
103
+ lines = Text.format(*text, **popts)
104
+ if cprefix || csuffix
105
+ lines.map! do |line|
106
+ line = "#{cprefix}#{line}#{csuffix}"
107
+ cprefix, prefix = prefix, nil if prefix
108
+ csuffix, suffix = suffix, nil if suffix
109
+ line
166
110
  end
167
111
  end
112
+ Terminal.puts(*lines, bbcode: false)
113
+ @lines_written += lines.size
168
114
  self
169
115
  end
170
116
 
171
- # Print given text with a decoration mark.
172
- #
173
- # @param text (see puts)
174
- # @param mark [Symbol, #to_s]
175
- # marker type
176
- #
177
- # @return (see puts)
178
- def mark(*text, mark: :default, **options)
179
- mark = Theme.current.mark(mark)
180
- options[:first_line_prefix] = mark
181
- options[:first_line_prefix_width] = mark.width
182
- puts(*text, **options)
183
- end
184
-
185
- # Print given text as lines like {#puts}. Used in elements with temporary
186
- # output like {#task} the text will be kept ("pinned").
187
- #
188
- # It can optionally have a decoration marker in first line like {#mark}.
117
+ # Prints one or more blank lines.
189
118
  #
190
- # @example Print two lines decorated as information which are pinned
191
- # ui.task 'Do something important' do |task|
192
- # # ...
193
- # task.pin("This is text", "which is pinned", mark: :information)
194
- # # ...
195
- # end
196
- # # => ✓ Do something important
197
- # # => 𝒊 This is text
198
- # # => which is pinned.
119
+ # @example Print a single blank line
120
+ # ui.space
199
121
  #
200
- # @param (see #puts)
201
- # @param mark (see #mark)
202
- # @option (see #puts)
122
+ # @example Print three blank lines
123
+ # ui.space 3
203
124
  #
204
- # @return (see puts)
205
- def pin(*text, mark: nil, **options)
206
- mark(*text, mark: mark, pin: true, **options)
207
- end
208
-
209
- # Print given text as a quotation.
210
- #
211
- # @param text (see puts)
212
- #
213
- # @return (see puts)
214
- def quote(*text)
215
- width = columns * 0.75
216
- quote = Theme.current.mark(:quote)
217
- puts(
218
- *text,
219
- prefix: quote,
220
- prefix_width: quote.width,
221
- max_width: width < 20 ? nil : width.round
222
- )
223
- end
224
-
225
- # Print given text as a heading.
226
- #
227
- # There are specific shortcuts for heading levels:
228
- # {#h1}, {#h2}, {#h3}, {#h4}, {#h5}, {#h6}.
229
- #
230
- # @example Print a level 1 heading
231
- # ui.heading(1, 'This is a H1 heading element')
232
- # # => ╴╶╴╶─═══ This is a H1 heading element ═══─╴╶╴╶
233
- #
234
- # @param level [#to_i]
235
- # heading level, one of 1..6
236
- # @param text (see puts)
237
- #
238
- # @return (see puts)
239
- def heading(level, *text)
240
- prefix, suffix = Theme.current.heading(level)
241
- puts(
242
- *text,
243
- max_width: columns,
244
- prefix: prefix,
245
- prefix_width: prefix.width,
246
- suffix: suffix,
247
- suffix_width: suffix.width,
248
- align: :centered
249
- )
125
+ # @param count [#to_int] number of blank lines to print
126
+ # @return (see #puts)
127
+ def space(count = 1)
128
+ (count = count.to_int) < 1 ? self : puts(" \n" * count)
250
129
  end
251
130
 
252
- # Print given text as a H1 {#heading}.
131
+ # Prints text with a leading mark symbol.
253
132
  #
254
- # @param text (see puts)
133
+ # @example Default mark
134
+ # ui.mark 'Item one'
255
135
  #
256
- # @return (see puts)
257
- def h1(*text) = heading(1, *text)
258
-
259
- # Print given text as a H2 {#heading}.
136
+ # @example Named symbol mark
137
+ # ui.mark 'Done!', mark: :checkmark
260
138
  #
261
- # @param text (see puts)
139
+ # @example Custom string mark
140
+ # ui.mark 'Warning', mark: '[bright_red]!'
262
141
  #
263
- # @return (see puts)
264
- def h2(*text) = heading(2, *text)
142
+ # @param text (see #puts)
143
+ # @param mark [Symbol, #to_s] mark to use as prefix:
144
+ # - `:default` — blue bullet •
145
+ # - `:checkmark` — green check mark ✓
146
+ # - `:item` — dim bullet •
147
+ # - any other object — converted via `#to_s` and used as the literal prefix
148
+ # @param popts [Hash] a customizable set of print options, see {#puts}
149
+ # @return (see #puts)
150
+ def mark(*text, mark: :default, **popts)
151
+ Mark.render(self, mark, *text, **popts)
152
+ end
265
153
 
266
- # Print given text as a H3 {#heading}.
154
+ # Prints text with a green check-mark prefix.
155
+ #
156
+ # Shorthand for `mark(*text, mark: :checkmark, **options)`.
267
157
  #
268
- # @param text (see puts)
158
+ # @example
159
+ # ui.ok 'All tests passed'
269
160
  #
270
- # @return (see puts)
271
- def h3(*text) = heading(3, *text)
161
+ # @param text (see #puts)
162
+ # @param popts (see #mark)
163
+ # @return (see #puts)
164
+ def ok(*text, **popts) = mark(*text, mark: :checkmark, **popts)
272
165
 
273
- # Print given text as a H4 {#heading}.
166
+ # Prints text with a mark that persists after a {Temporary} element closes.
167
+ #
168
+ # Identical to {#mark} but survives when the surrounding {Temporary} element
169
+ # (see {#temporary}, {#task}, {#progress}) is erased.
274
170
  #
275
- # @param text (see puts)
171
+ # @example Pin a success note inside a task
172
+ # ui.task 'Deploy' do
173
+ # do_deploy
174
+ # ui.pin 'Deployed to production', mark: :checkmark
175
+ # end
276
176
  #
277
- # @return (see puts)
278
- def h4(*text) = heading(4, *text)
177
+ # @param (see mark)
178
+ # @return (see #puts)
179
+ def pin(*text, mark: :default, **popts)
180
+ mark(*text, mark:, pin: true, **popts)
181
+ end
279
182
 
280
- # Print given text as a H5 {#heading}.
183
+ # Prints text with a left-side quotation border.
281
184
  #
282
- # @param text (see puts)
185
+ # @example
186
+ # ui.quote "To be or not to be,\nthat is the question."
283
187
  #
284
- # @return (see puts)
285
- def h5(*text) = heading(5, *text)
188
+ # @param (see #puts)
189
+ # @return (see #puts)
190
+ def quote(*text) = Quote.render(self, *text)
286
191
 
287
- # Print given text as a H6 {#heading}.
192
+ # Prints a heading at the given level.
288
193
  #
289
- # @param text (see puts)
194
+ # Six heading levels are supported (similarly to HTML `<h1>`–`<h6>`).
195
+ # Use the shorthand helpers {#h1}–{#h6} for convenience.
290
196
  #
291
- # @return (see puts)
292
- def h6(*text) = heading(6, *text)
293
-
294
- # Print a horizontal rule.
197
+ # @example Large heading
198
+ # ui.heading 1, 'Chapter One'
295
199
  #
296
- # @example Print double line
297
- # ui.hr :double
200
+ # @example Sub-heading
201
+ # ui.heading 3, 'Section [i]Overview[/i]'
298
202
  #
299
- # @param type [Symbol]
300
- # border type
203
+ # @param level [#to_int] heading level, `1` (largest) to `6` (smallest)
204
+ # @param title [#to_s] heading text
205
+ # @return (see #puts)
206
+ def heading(level, title) = Heading.render(self, level, title)
207
+
208
+ # Prints a level-1 heading.
301
209
  #
302
- # @return (see puts)
303
- def hr(type = :default)
304
- theme = Theme.current
305
- bc = theme.border(type)[10]
306
- puts("#{theme.heading_sytle}#{bc * columns}")
307
- end
210
+ # @param title (see #heading)
211
+ # @return (see #puts)
212
+ def h1(title) = heading(1, title)
308
213
 
309
- # Print one or more space lines.
214
+ # Prints a level-2 heading.
310
215
  #
311
- # @param count [#to_i]
312
- # lines to print
216
+ # @param title (see #heading)
217
+ # @return (see #puts)
218
+ def h2(title) = heading(2, title)
219
+
220
+ # Prints a level-3 heading.
313
221
  #
314
- # @return (see puts)
315
- def space(count = 1)
316
- (count = count.to_i).positive? ? puts("\n" * count) : self
317
- end
222
+ # @param title (see #heading)
223
+ # @return (see #puts)
224
+ def h3(title) = heading(3, title)
318
225
 
319
- # Print given items as list (like 'ls' command).
226
+ # Prints a level-4 heading.
320
227
  #
321
- # Each list item will optionally be decorated with the given glyph as:
228
+ # @param title (see #heading)
229
+ # @return (see #puts)
230
+ def h4(title) = heading(4, title)
231
+
232
+ # Prints a level-5 heading.
322
233
  #
323
- # - `Integer` as the start value for a numbered list
324
- # - `Symbol` as the start symbol
325
- # - `:hex` to create a hexadecimal numbered list
326
- # - any text as prefix
234
+ # @param title (see #heading)
235
+ # @return (see #puts)
236
+ def h5(title) = heading(5, title)
237
+
238
+ # Prints a level-6 heading.
327
239
  #
328
- # @example Print all Ruby files as a numbered list
329
- # ui.ls Dir['*/**/*.rb'], glyph: 1
240
+ # @param title (see #heading)
241
+ # @return (see #puts)
242
+ def h6(title) = heading(6, title)
243
+
244
+ # Prints a horizontal rule spanning the available width.
330
245
  #
331
- # @example Print all Ruby files as a bullet point list (with green bullets)
332
- # ui.ls Dir['*/**/*.rb'], glyph: '[green]•[/fg]'
246
+ # @example Default rule
247
+ # ui.hr
333
248
  #
334
- # @param items [#to_s]
335
- # one or more convertible objects to list
336
- # @param compact [true, false]
337
- # whether the compact display format should be used
338
- # @param glyph [Integer, :hex, Symbol, #to_s]
339
- # glyph to be used as prefix
249
+ # @example Named style
250
+ # ui.hr :double
340
251
  #
341
- # @return (see puts)
252
+ # @example Repeated string
253
+ # ui.hr '+-'
254
+ #
255
+ # @param kind [Symbol, #to_s, nil]
256
+ # - `nil` — default style
257
+ # - `Symbol` — named style: `:single`, `:double`, `:heavy`
258
+ # - `#to_s` — string repeated to fill the available width
259
+ # @return (see #puts)
260
+ def hr(kind = nil) = HorizontalRule.render(self, kind)
261
+
262
+ # Prints a multi-column list.
263
+ #
264
+ # Items are arranged in columns to fit the terminal width.
265
+ #
266
+ # @example Plain list
267
+ # ui.ls 'Alice', 'Bob', 'Carol'
268
+ #
269
+ # @example Hex-numbered list
270
+ # ui.ls 'red', 'green', 'blue', glyph: :hex
271
+ #
272
+ # @param items [#to_s, ...] items to list; nested arrays are flattened
273
+ # @param compact [Boolean] `true` = ordered in columns,
274
+ # `false` = ordered left to right per row
275
+ # @param glyph prefix applied to every item:
276
+ # - `nil` / `false` — no prefix
277
+ # - `Integer` — decimal counter starting at the given number
278
+ # - `Symbol` — alphabetic sequence starting at the given symbol
279
+ # (e.g. `:a` → `a`, `b`, `c`, …)
280
+ # - `:hex` — zero-based hex counter (`01`, `02`, …)
281
+ # - a `#to_s` value matching a hex value — hex counter at the given
282
+ # offset (e.g. `"0x0a"` starts at `0a`)
283
+ # - any other `#to_s` value — used as a literal prefix for every item
284
+ # @return (see #puts)
342
285
  def ls(*items, compact: true, glyph: nil)
343
286
  return self if items.empty?
344
- renderer = compact ? CompactLSRenderer : LSRenderer
345
- puts(*renderer.lines(items, glyph, columns))
346
- end
347
-
348
- # Generate and print a table.
349
- # See {Table} for much more details about table generation.
350
- #
351
- # @example Draw a very simple 3x4 table with complete borders
352
- # ui.table(border: :default, border_around: true, padding: [0, 1]) do |table|
353
- # table.add 1, 2, 3, 4
354
- # table.add 5, 6, 7, 8
355
- # table.add 9, 10, 11, 12
356
- # end
357
- #
358
- # @param attributes [{Symbol => Object}]
359
- # attributes for the table and default attributes for table cells
360
- # @option attributes [Symbol] :border (nil)
361
- # kind of border,
362
- # see {Table::Attributes}
363
- # @option attributes [Enumerable<Symbol>] :border_style (nil)
364
- # style of border,
365
- # see {Table::Attributes}
366
- # @option attributes [true, false] :border_around (false)
367
- # whether the table should have a border around,
368
- # see {Table::Attributes}
369
- # @option attributes [:left, :right, :centered] :position (false)
370
- # where to align the table,
371
- # see {Table::Attributes}
372
- #
373
- # @yieldparam table [Table]
374
- # helper to define the table layout
375
- #
376
- # @return (see puts)
377
- def table(**attributes)
378
- return self unless block_given?
379
- yield(table = Table.new(**attributes))
380
- puts(
381
- *TableRenderer[table, columns],
382
- align: table.attributes.position,
383
- expand: true
384
- )
385
- end
386
-
387
- # Print text in columns.
388
- # This is a shorthand to define a {Table} with a single row.
389
- #
390
- # @param columns [#to_s]
391
- # two or more convertible objects to print side by side
392
- # @param attributes (see table)
393
- # @option attributes (see table)
394
- # @option attributes [Integer] :width (nil)
395
- # width of a column,
396
- # see {Attributes::Width}
397
- #
398
- # @yieldparam row [Table::Row]
399
- # helper to define the row layout
400
- #
401
- # @return (see puts)
402
- def cols(*columns, **attributes)
403
- tab_att, att = Utils.split_table_attr(attributes)
404
- table(**tab_att) do |table|
405
- table.add do |row|
406
- columns.each { row.add(_1, **att) }
407
- yield(row) if block_given?
408
- end
409
- end
287
+ puts(*(compact ? CompactLS : LS).lines(columns, items, glyph))
410
288
  end
411
289
 
412
- # Print a text division with attributes.
413
- # This is a shorthand to define a {Table} with a single cell.
414
- #
415
- # @param text (see puts)
416
- # @param attributes [{Symbol => Object}]
417
- # attributes for the division
418
- # @option attributes [:left, :right, :centered] :align (:left)
419
- # text alignment,
420
- # see {Attributes::Align}
421
- # @option attributes [Integer, Enumerable<Integer>] :padding (nil)
422
- # text padding,
423
- # see {Attributes::Padding}
424
- # @option attributes [Enumerable<Symbol>] :style (nil)
425
- # text style,
426
- # see {Attributes::Style}
427
- # @option attributes [Integer] :width (nil)
428
- # width of the cell,
429
- # see {Attributes::Width}
430
- # @option attributes (see table)
431
- #
432
- # @return (see puts)
433
- def div(*text, **attributes)
434
- return self if text.empty?
435
- tab_att, att = Utils.split_table_attr(attributes)
436
- tab_att[:border_around] = true
437
- table(**tab_att) { |table| table.add { _1.add(*text, **att) } }
438
- end
439
-
440
- # Dump given values as vertical bars.
441
- #
442
- # @example Draw green bars
443
- # ui.vbars 1..10, style: :green
444
- #
445
- # @example Draw very big bars
446
- # ui.vbars 1..10, bar_width: 5, height: 20
447
- #
448
- # @param values [#to_a, Array<Numeric>] values to print
449
- # @param normalize [true, false] whether the values should be normalized
450
- # @param height [Integer] output height
451
- # @param bar_width [:auto, :min, Integer] with of each bar
452
- # @param style [Symbol, Array<Symbol>, nil] drawing style
453
- #
454
- # @raise [ArgumentError] if any value is negative
455
- #
456
- # @return (see puts)
290
+ # Prints a vertical bar chart.
291
+ #
292
+ # All values must be non-negative.
293
+ #
294
+ # @example Simple bar chart
295
+ # ui.vbars [3, 7, 2, 9, 5]
296
+ #
297
+ # @example Normalised chart with custom height
298
+ # ui.vbars [10, 40, 25, 60], normalize: true, height: 15
299
+ #
300
+ # @param values [#each]
301
+ # collection of non-negative `Numeric` values
302
+ # @param min [Numeric, nil]
303
+ # lower bound for scaling; `nil` uses the data minimum
304
+ # @param max [Numeric, nil]
305
+ # upper bound for scaling; `nil` uses the data maximum
306
+ # @param normalize [Boolean]
307
+ # when `true` applies min-max normalization;
308
+ # when `false` scales each bar relative to the maximum value
309
+ # @param height [#to_int]
310
+ # chart height in lines (minimum 3; default: 10)
311
+ # @param bar_width [#to_int, :auto]
312
+ # width of each bar in characters; `:auto` fits the graph in available
313
+ # terminal width
314
+ # @param style [Symbol, String, Array<Style>, Array<String>, nil]
315
+ # ANSI style applied to the bars
316
+ # @return (see #puts)
457
317
  def vbars(
458
318
  values,
319
+ min: nil,
320
+ max: nil,
459
321
  normalize: false,
460
322
  height: 10,
461
- bar_width: :auto,
323
+ bar_width: 3,
462
324
  style: nil
463
325
  )
464
- return self if (values = values.to_a).empty?
465
- if values.any?(&:negative?)
466
- raise(ArgumentError, 'values can not be negative')
467
- end
468
- puts(
469
- *VBarsRenderer.lines(
470
- values,
471
- columns,
472
- height,
473
- normalize,
474
- bar_width,
475
- Terminal.ansi? ? style : nil
476
- )
477
- )
326
+ bars = VBars.new(values, min, max, normalize)
327
+ return self if bars.empty?
328
+ raise(ArgumentError, 'values can not be negative') unless bars.valid?
329
+ puts(*bars.lines(columns, height, bar_width, style), bbcode: false)
478
330
  end
479
331
 
480
- # Dump given values as horizontal bars.
481
- #
482
- # @example Draw green bars
483
- # ui.hbars 1..10, style: :green
332
+ # Prints a horizontal bar chart.
484
333
  #
485
- # @example Draw bars in half sreen width
486
- # ui.hbars 1..10, style: :blue, width: 0.5
334
+ # All values must be non-negative.
487
335
  #
488
- # @param values [#to_a, Array<Numeric>] values to print
489
- # @param min [#to_f] start value
490
- # @param max [#to_f] end value
491
- # @param normalize [true, false] whether the values should be normalized
492
- # @param text [true, false] whether the values should be printed too
493
- # @param width [:auto, :min, Integer] with of each bar
494
- # @param style [Symbol, Array<Symbol>, nil] bar drawing style
495
- # @param text_style [Symbol, Array<Symbol>, nil] text style
336
+ # @example Simple horizontal chart
337
+ # ui.hbars [3, 7, 2, 9, 5]
496
338
  #
497
- # @raise [ArgumentError] if any value is negative
339
+ # @example Chart without value labels
340
+ # ui.hbars [10, 40, 25, 60], text: false, normalize: true
498
341
  #
499
- # @return (see puts)
342
+ # @param values (see #vbars)
343
+ # @param min (see #vbars)
344
+ # @param max (see #vbars)
345
+ # @param normalize (see #vbars)
346
+ # @param style (see #vbars)
347
+ # @param width [#to_int, Float, :auto] maximum bar width in characters;
348
+ # a `Float` is a fraction of available width; `:auto` fills available width
349
+ # @param text [Boolean] when `true` prints the numeric value next to each bar
350
+ # @param text_style [Symbol, String, Array<Style>, Array<String>, nil]
351
+ # ANSI style applied to the value labels
352
+ # @return (see #puts)
500
353
  def hbars(
501
354
  values,
502
355
  min: nil,
503
356
  max: nil,
504
357
  normalize: false,
505
- text: true,
506
358
  width: :auto,
507
359
  style: nil,
360
+ text: true,
508
361
  text_style: nil
509
362
  )
510
- return self if (values = values.to_a).empty?
511
- if values.any?(&:negative?)
512
- raise(ArgumentError, 'values can not be negative')
513
- end
514
- style = text_style = nil unless Terminal.ansi?
515
- renderer = HBarsRenderer.new(values, min, max)
516
- renderer.with_text(text_style) if text
517
- puts(*renderer.lines(Utils.as_size(3..columns, width), style, normalize))
363
+ bars = HBars.new(values, min, max, normalize)
364
+ return self if bars.empty?
365
+ raise(ArgumentError, 'values can not be negative') unless bars.valid?
366
+ bars.with_text(text_style) if text
367
+ puts(*bars.lines(columns, width, style), bbcode: false)
518
368
  end
519
369
 
520
- # Dynamically display a task progress.
521
- # When a `max` parameter is given the progress will be displayed as a
522
- # progress bar below the `title`. Otherwise the progress is displayed just
523
- # by accumulating dots.
524
- #
525
- # @example Display a progress bar
526
- # ui.progress('Download file', max: 1024) do |progress|
527
- # while progress.value < progress.max
528
- # # just to simulate the download
529
- # sleep(0.1)
530
- # bytes_read = rand(10..128)
531
- #
532
- # # here we actualize the progress
533
- # progress.value += bytes_read
534
- # end
535
- # end
370
+ # Renders a {Table} to the terminal.
371
+ #
372
+ # The method yields a {Table} object for population; if no block is given
373
+ # nothing is rendered.
536
374
  #
537
- # @example Display simple progress
538
- # progress = ui.progress 'Check some stuff'
539
- # 10.times do
540
- # # simulate some work
541
- # sleep 0.1
375
+ # Border name symbols:
376
+ # `:rounded` (default), `:single`, `:double`, `:heavy`,
377
+ # `:single_double`, `:double_single`, `:single_heavy`, `:heavy_single`
542
378
  #
543
- # # here we actualize the progress
544
- # progress.step
379
+ # @example Simple table with a double outer frame
380
+ # ui.table border_frame: :double do |t|
381
+ # t.add_row '[b]Name', '[b]Score', align: :center
382
+ # t.add_row 'Alice', 42
383
+ # t.add_row 'Bob', 17
545
384
  # end
546
- # progress.ok 'Stuff checked ok'
547
- #
548
- # @overload progress(title, max: nil, pin: false)
549
- # @param title [#to_s]
550
- # title text to display
551
- # @param max [#to_f]
552
- # expected maximum value
553
- # @param pin [true, false]
554
- # whether the final progress state should be "pinned" to parent element
555
- #
556
- # @return [ProgressHelper]
557
- # itself
558
- #
559
- # @overload progress(title, max: nil, pin: false, &block)
560
- # @param title [#to_s]
561
- # title text
562
- # @param max [#to_f]
563
- # expected maximum value
564
- # @param pin [true, false]
565
- # whether the final progress state should be "pinned" to parent element
566
- #
567
- # @yieldparam progress [ProgressHelper]
568
- # itself
569
- #
570
- # @return [Object]
571
- # the result of the given block
572
- def progress(title, max: nil, pin: false, &block)
573
- progress =
574
- if Terminal.ansi?
575
- Progress.new(self, title, max, pin)
576
- else
577
- DumbProgress.new(self, title, max)
578
- end
579
- block ? __with(progress, &block) : progress
385
+ #
386
+ # @option options [:default, :none, :single, :double, :heavy] :border (:default)
387
+ # default border used for all border parts
388
+ # @option options [nil, :none, :single, :double, :heavy] :border_frame
389
+ # outer frame; falls back to `:border`
390
+ # @option options [nil, :none, :single, :double, :heavy] :border_vertical
391
+ # vertical column separators; falls back to `:border`
392
+ # @option options [nil, :none, :single, :double, :heavy] :border_horizontal
393
+ # horizontal row separators; falls back to `:border`
394
+ # @option options [Symbol, String, Array<Style>, Array<String>, nil] :border_style
395
+ # ANSI style applied to all borders
396
+ # @option options [Symbol, Array<Symbol>] :frame (:all)
397
+ # which frame sides to draw: `:all` or an enumerable of
398
+ # `:top`, `:right`, `:bottom`, `:left`
399
+ # @option options [#to_int, Float, :max, nil] :width
400
+ # `:max` expands to the full terminal width;
401
+ # a negative `#to_int` is an offset from the terminal width;
402
+ # a `Float` is a fraction of the terminal width
403
+ # @yield [table] a {Table} instance to populate with rows and cells
404
+ # @yieldparam table [Table] the table
405
+ # @return [Features]
406
+ def table(**options)
407
+ return self unless block_given?
408
+ yield(table = Table.new)
409
+ return self if table.empty?
410
+ puts(*TableRenderer.lines(columns, table, **options), bbcode: false)
580
411
  end
581
412
 
582
- # Execute a program.
583
- #
584
- # @example Execute a simple command
585
- # ui.sh 'ls'
586
- #
587
- # @example Execute a command wih arguments
588
- # ret = ui.sh('curl', '--version')
589
- # raise('Curl not found') unless ret&.success?
590
413
  #
591
- # @example Execute shell commands
592
- # ui.sh "mkdir 'test' && cd 'test'"
593
- #
594
- # @example Execute a command with environment variables
595
- # ui.sh({'ENV' => 'test'}, 'rails')
596
- #
597
- # @see run
414
+ # @!endgroup
598
415
  #
599
- # @param cmd (see run)
600
- # @param preserve_spaces (see run)
601
- # @param options (see run)
602
- # @return [Process::Status]
603
- # when command was executed
604
- # @return [nil]
605
- # in error case (like command not found)
606
- def sh(*cmd, preserve_spaces: false, **options)
607
- ShellRenderer.sh(self, cmd, options, preserve_spaces)
608
- end
609
-
610
- # Execute a shell program and return output. Limit the lines displayed.
611
- #
612
- # @example Capture output and error
613
- # status, out, err = ui.run('ls ./ && ls ./this_does_not_exist')
614
- # # => #<Process::Status: pid 25562 exit 1>
615
- # # => [...] # the output of first `ls`
616
- # # => ["ls: ./this_does_not_exist: No such file or directory"]
617
- #
618
- # @see sh
619
- #
620
- # @param cmd [String]
621
- # command and optional arguments
622
- # @param preserve_spaces [true,false]
623
- # whether the spaces and tabs of the output should be preserve
624
- # @param max_lines [Integer]
625
- # limit of displayed lines
626
- # @param options [Hash] executions options
627
- # @return [[Process::Status, Array<String>, Array<String>]]
628
- # process status, output and error output when command was executed
629
- # @return [nil]
630
- # in error case (like command not found)
631
- def run(*cmd, preserve_spaces: false, max_lines: 10, **options)
632
- result =
633
- ShellRenderer.run(
634
- self,
635
- cmd,
636
- options,
637
- preserve_spaces,
638
- max_lines.clamp(1, Terminal.rows)
639
- )
640
- result if result[0]
641
- end
642
416
 
643
417
  #
644
- # @!endgroup
418
+ # @!group Section elements
645
419
  #
646
420
 
421
+ # Creates a {Temporary} element whose output is erased when it closes.
422
+ #
423
+ # @example Manual close
424
+ # tmp = ui.temporary
425
+ # ui.puts 'Loading…'
426
+ # sleep 1
427
+ # tmp.end # erases "Loading…"
647
428
  #
648
- # @!group Sub-Elements
429
+ # @example Block form
430
+ # ui.temporary do
431
+ # ui.puts 'Thinking…'
432
+ # sleep 2
433
+ # end # "Thinking…" is erased here
649
434
  #
435
+ # @yield [temp] the {Temporary} element
436
+ # @yieldparam temp [Temporary]
437
+ # @return [Object] return value of the block
438
+ # @return [Temporary] itself, if no block is specified
439
+ def temporary(&) = __with(Temporary.new(self), &)
650
440
 
651
- # Create a visually separated section for the output of text elements.
652
- # Like any other {Element} sections support all {Features}.
441
+ # Creates a {Margin} element that adds horizontal and vertical whitespace.
653
442
  #
654
443
  # @example
655
- # ui.section do |section|
656
- # section.h1 'About Sections'
657
- # section.space
658
- # section.puts 'Sections are areas of text elements.'
659
- # section.puts 'You can use any other feature inside such an area.'
444
+ # ui.margin 0, 0.25 do
445
+ # ui.puts 'This text has 25% width horizontal margin.'
660
446
  # end
661
- # # => ╭────╶╶╶
662
- # # => │ ╴╶╴╶─═══ About Sections ═══─╴╶╴╶
663
- # # => │
664
- # # => │ Sections are areas of text elements.
665
- # # => │ You can use any other feature inside such an area.
666
- # # => ╰──── ─╶╶╶
667
447
  #
668
- # @param text [#to_s]
669
- # convertible objects to print line by line
448
+ # @overload margin(value)
449
+ # Margin for all sides.
450
+ # @param value [#to_int, Float] margin of all four sides
451
+ #
452
+ # @overload margin(vertical = 0, horizontal = 1)
453
+ # Seperate vertical and horizontal margin.
454
+ # @param vertical [#to_int] top and bottom margin
455
+ # @param horizontal [#to_int, Float] left and right margin
456
+ #
457
+ # @overload margin(top, horizontal, bottom)
458
+ # Seperate top, bottom and horizontal margin.
459
+ # @param top [#to_int] top margin
460
+ # @param horizontal [#to_int, Float] left and right margin
461
+ # @param bottom [#to_int] bottom margin
462
+ #
463
+ # @overload margin(top, right, bottom, left)
464
+ # Seperate margins.
465
+ # @param top [#to_int] top margin
466
+ # @param right [#to_int, Float] right margin
467
+ # @param bottom [#to_int] bottom margin
468
+ # @param left [#to_int, Float] left margin
469
+ #
470
+ # @overload margin(top: 0, right: 0, bottom: 0, left: 0)
471
+ # Specific margin.
472
+ # @param top [#to_int] top margin
473
+ # @param right [#to_int, Float] right margin
474
+ # @param bottom [#to_int] bottom margin
475
+ # @param left [#to_int, Float] left margin
476
+ #
477
+ # @yield [margin] the {Margin} element
478
+ # @yieldparam margin [Margin]
479
+ # @return [Object] return value of the block
480
+ # @return [Margin] itself, if no block is specified
481
+ def margin(*, &) = __with(Margin.new(self, *), &)
482
+
483
+ # Creates a {Section} element — a bordered container with an optional title.
484
+ #
485
+ # @example Manual close
486
+ # sec = ui.section 'Results'
487
+ # ui.ok 'All good'
488
+ # sec.end
489
+ #
490
+ # @example Block form with title
491
+ # ui.section 'Summary' do
492
+ # ui.puts '3 files processed.'
493
+ # end
670
494
  #
671
- # @yieldparam section [Section]
672
- # itself
495
+ # @example Block form without title
496
+ # ui.section do
497
+ # ui.puts 'Anonymous section content.'
498
+ # end
673
499
  #
674
- # @return [Object]
675
- # the result of the given block
676
- def section(*text, &block) = __sec(:default, nil, text, &block)
677
-
678
- # @!macro like_msg
679
- # @see section
680
- # @param title [#to_s]
681
- # title to print as section head
682
- # @param text (see section)
683
- # @yieldparam (see section)
684
- # @return (see section)
500
+ # @param title [#to_s, nil]
501
+ # optional header text
502
+ # @param type [:default, :message, :information, :warning, :error, :fatal]
503
+ # visual style of the section
504
+ # @param border [:default, :rounded, :single, :double, :heavy]
505
+ # border style
506
+ # @yield [section] the {Section} element
507
+ # @yieldparam section [Section]
508
+ # @return [Object] return value of the block
509
+ # @return [Section] itself, if no block is specified
510
+ def section(title = nil, type: :default, border: :default, &)
511
+ __with(Section.new(self, title, type, border), &)
512
+ end
513
+ alias begin section
685
514
 
686
- # Create a visually separated section with a title for the output of text
687
- # elements.
515
+ # Creates a {Section} with `:message` styling.
688
516
  #
689
- # @macro like_msg
690
- def message(title, *text, &block) = __sec(:message, title, text, &block)
517
+ # @param title [#to_s] header text
518
+ # @param border (see #section)
519
+ # @yield (see #section)
520
+ # @yieldparam (see #section)
521
+ # @return (see #section)
522
+ def message(title, border: :default, &)
523
+ section(title, border:, type: :message, &)
524
+ end
691
525
  alias msg message
692
526
 
693
- # Create a visually separated section marked as informational with a title
694
- # for the output of text elements.
527
+ # Creates a {Section} with `:information` styling.
695
528
  #
696
- # @macro like_msg
697
- def information(title, *text, &block)
698
- __tsec(:information, title, text, &block)
529
+ # @param (see #message)
530
+ # @yield (see #message)
531
+ # @yieldparam (see #message)
532
+ # @return (see #message)
533
+ def information(title, border: :default, &)
534
+ section(title, border:, type: :information, &)
699
535
  end
700
536
  alias info information
701
537
 
702
- # Create a visually separated section marked as a warning with a title for
703
- # the output of text elements.
538
+ # Creates a {Section} with `:warning` styling.
704
539
  #
705
- # @macro like_msg
706
- def warning(title, *text, &block) = __tsec(:warning, title, text, &block)
540
+ # @param (see #message)
541
+ # @yield (see #message)
542
+ # @yieldparam (see #message)
543
+ # @return (see #message)
544
+ def warning(title, border: :default, &)
545
+ section(title, border:, type: :warning, &)
546
+ end
707
547
  alias warn warning
708
548
 
709
- # Create a visually separated section marked as an error with a title for
710
- # the output of text elements.
549
+ # Creates a {Section} with `:error` styling.
711
550
  #
712
- # @macro like_msg
713
- def error(title, *text, &block) = __tsec(:error, title, text, &block)
714
- alias err error
551
+ # @param (see #message)
552
+ # @yield (see #message)
553
+ # @yieldparam (see #message)
554
+ # @return (see #message)
555
+ def error(title, border: :default, &)
556
+ section(title, border:, type: :error, &)
557
+ end
715
558
 
716
- # Create a visually separated section marked as a failure with a title for
717
- # the output of text elements.
559
+ # Creates a {Section} with `:fatal` styling.
718
560
  #
719
- # @macro like_msg
720
- def failed(title, *text, &block) = __tsec(:failed, title, text, &block)
721
-
722
- # Create a framed section.
723
- #
724
- # @param text (see section)
725
- # @param align [:left, :right, :centered]
726
- # text alignment,
727
- # see {Attributes::Align}
728
- # @param border [Symbol]
729
- # kind of border,
730
- # see {Attributes::Border}
731
- # @param border_style [Enumerable<Symbol>]
732
- # style of border,
733
- # see {Attributes::BorderStyle}
734
- #
735
- # @yieldparam frame [Framed] itself
736
- #
737
- # @return (see section)
738
- def framed(*text, align: :left, border: :default, border_style: nil, &block)
739
- __with(
740
- Framed.new(
741
- self,
742
- Utils.align(align),
743
- Theme.current.border(border),
744
- Utils.style(border_style),
745
- text
746
- ),
747
- &block
748
- )
561
+ # @param (see #message)
562
+ # @yield (see #message)
563
+ # @yieldparam (see #message)
564
+ # @return (see #message)
565
+ def fatal(title, border: :default, &)
566
+ section(title, border:, type: :fatal, &)
749
567
  end
750
568
 
751
- # Generate a task section.
569
+ # Creates a {Frame} element — a bordered box with an optional title.
752
570
  #
753
- # @param title [#to_s]
754
- # task title text
755
- # @param text (see section)
756
- # @param pin [true, false] whether to keep text "pinned"
571
+ # @example Manual close
572
+ # frm = ui.frame 'Preview'
573
+ # ui.puts 'Content inside the frame.'
574
+ # frm.end
757
575
  #
758
- # @yieldparam task [Task] itself
576
+ # @example Block form with title and custom border
577
+ # ui.frame 'Results', border: :double do
578
+ # ui.puts 'All checks passed.'
579
+ # end
580
+ #
581
+ # @example Block form without a title
582
+ # ui.frame do
583
+ # ui.puts 'Framed content.'
584
+ # end
759
585
  #
760
- # @return (see section)
761
- def task(title, *text, pin: false, &block)
762
- __with(Task.new(self, title, text, pin), &block)
586
+ # @param title (see #section)
587
+ # @param border (see #section)
588
+ # @param style [Symbol, String, Array<Style>, Array<String>, nil]
589
+ # ANSI style applied to the frame
590
+ # @yield [frame] the {Frame} element
591
+ # @yieldparam frame [Frame]
592
+ # @return [Object] return value of the block
593
+ # @return [Frame] itself, if no block is specified
594
+ def frame(title = nil, border: :default, style: nil, &)
595
+ __with(Frame.new(self, title, border, style), &)
763
596
  end
764
597
 
598
+ # Creates a {Task} element — a labelled step that shows a spinner while
599
+ # running and a check mark on success.
765
600
  #
766
- # @!endgroup
601
+ # @example Manual close
602
+ # t = ui.task 'Installing dependencies'
603
+ # run_install
604
+ # t.end
605
+ #
606
+ # @example Block form
607
+ # ui.task 'Installing dependencies' do
608
+ # run_install
609
+ # end
767
610
  #
611
+ # @param title [#to_s] task description
612
+ # @param pin [Boolean] whether the task title should be pinned
613
+ # @yield [task] the {Task} element
614
+ # @yieldparam task [Task]
615
+ # @return [Object] return value of the block
616
+ # @return [Task] itself, if no block is specified
617
+ def task(title, pin: false, &) = __with(Task.new(self, title, pin), &)
768
618
 
619
+ # Creates a {Progress} element for tracking incremental work.
769
620
  #
770
- # @!group User Interaction
621
+ # When `max` is given the progress is displayed as a percentage bar.
622
+ # When `max` is `nil` an open-ended dot animation is shown instead.
623
+ #
624
+ # @example Bounded progress
625
+ # ui.progress 'Processing', max: items.size do |bar|
626
+ # items.each { process(it); bar.step }
627
+ # end
771
628
  #
629
+ # @example Open-ended progress
630
+ # ui.progress 'Working…' do |bar|
631
+ # loop { bar.step; break if done? }
632
+ # end
633
+ #
634
+ # @param title (see #section)
635
+ # @param max [Numeric, nil] maximum value
636
+ # @param popts (see #mark)
637
+ # @yield [progress] the {Progress} element
638
+ # @yieldparam progress [Progress]
639
+ # @return [Object] return value of the block
640
+ # @return [Progress] itself when no block is given
641
+ def progress(*title, max: nil, **popts, &)
642
+ __with(
643
+ (Terminal.ansi? ? Progress : DumbProgress).new(
644
+ self,
645
+ max,
646
+ *title,
647
+ **popts
648
+ ),
649
+ &
650
+ )
651
+ end
772
652
 
773
- # Wait for user input.
774
653
  #
775
- # @example Wait until user wants to coninue
776
- # ui.await { ui.puts '[faint][\\Press ENTER to continue...][/faint]' }
654
+ # @!endgroup
777
655
  #
778
- # @example Ask yes/no-question
779
- # ui.await(yes: %w[j o t s y d Enter], no: %w[n Esc]) do
780
- # ui.puts 'Do you like NayttUI?'
781
- # end
782
- # # => true, for user's YES
783
- # # => false, for user's NO
784
- # # Info:
785
- # # The keys will work for Afrikaans, Dutch, English, French, German,
786
- # # Italian, Polish, Portuguese, Romanian, Spanish and Swedish.
656
+ # @!group User Interaction
787
657
  #
788
- # @overload await(yes: 'Enter', no: 'Esc')
658
+
659
+ # Waits for the user to press any key.
789
660
  #
790
- # @overload await(yes: 'Enter', no: 'Esc', &block)
791
- # @yieldparam temp [Temporary]
792
- # temporary displayed section (section will be erased after input)
661
+ # @example Plain wait
662
+ # ui.puts 'Press any key to continue…'
663
+ # ui.await
793
664
  #
794
- # @param yes [String, Enumerable<String>]
795
- # key code/s a user can input to return positive result
796
- # @param no [String, Enumerable<String>]
797
- # key code/s a user can input to return negative resault
665
+ # @example With temporary prompt
666
+ # ui.await { ui.puts '[faint]Press any key to continue…' }
798
667
  #
799
- # @return [true, false]
800
- # whether the user inputs a positive result
668
+ # @yield (see #temporary)
669
+ # @yieldparam (see #temporary)
801
670
  # @return [nil]
802
- # in error case
803
- def await(yes: 'Enter', no: 'Esc')
804
- return __await(yes, no) unless block_given?
805
- temporary do |temp|
806
- yield(temp)
807
- __await(yes, no)
808
- end
671
+ def await
672
+ yield(temp = temporary) if block_given?
673
+ Terminal.read_key_event
674
+ nil
675
+ ensure
676
+ temp&.end
809
677
  end
810
678
 
811
- # Allows the user to select an option from a selection.
812
- # The selected option is returned.
813
- #
814
- # @overload choice(*choices, abortable: false)
815
- # @param choices [#to_s]
816
- # one or more alternatives to select from
817
- # @param abortable [true, false]
818
- # whether the user is allowed to abort with 'Esc' or 'Ctrl+c'
819
- #
820
- # @return [Integer]
821
- # index of selected choice
822
- # @return [nil]
823
- # when user aborted the selection
824
- #
825
- # @overload choice(*choices, abortable: false, &block)
826
- # @example Request a fruit
827
- # ui.choice('Apple', 'Banana', 'Orange') { ui.puts 'What do you prefer?' }
828
- # # => 0, when user likes apples
829
- # # => 1, when bananas are user's favorite
830
- # # => 2, when user is a oranges lover
831
- #
832
- # @param choices[#to_s]
833
- # one or more alternatives to select from
834
- # @param abortable[true, false]
835
- # whether the user is allowed to abort with 'Esc' or 'Ctrl+c'
836
- #
837
- # @yieldparam temp [Temporary]
838
- # temporary displayed section (section will be erased after input)
839
- #
840
- # @return [Integer]
841
- # index of selected choice
842
- # @return [nil]
843
- # when user aborted the selection
844
- #
845
- # @overload choice(**choices, abortable: false)
846
- # @param choices [#to_s]
847
- # one or more alternatives to select from
848
- # @param abortable [true, false]
849
- # whether the user is allowed to abort with 'Esc' or 'Ctrl+c'
850
- # @param selected [#to_s, nil]
851
- # optionally pre-selected option
852
- #
853
- # @return [Object]
854
- # key for selected choice
855
- # @return [nil]
856
- # when user aborted the selection
857
- #
858
- # @overload choice(**choices, abortable: false, &block)
859
- # @example Request a preference
860
- # ui.choice(
861
- # k: 'Kitty',
862
- # i: 'iTerm2',
863
- # g: 'Ghostty',
864
- # t: 'Tabby',
865
- # r: 'Rio',
866
- # abortable: true
867
- # ) { ui.puts 'Which terminal emulator do you like?' }
868
- # # => whether the user selected: :k, :i, :g, :t, :r
869
- # # => nil, when the user aborted
870
- # @param choices[#to_s]
871
- # one or more alternatives to select from
872
- # @param abortable[true, false]
873
- # whether the user is allowed to abort with 'Esc' or 'Ctrl+c'
874
- # @param selected[Integer]
875
- # pre-selected option index
876
- #
877
- # @yieldparam temp [Temporary]
878
- # temporary displayed section (section will be erased after input)
879
- #
880
- # @return [Object]
881
- # key for selected choice
882
- # @return [nil]
883
- # when user aborted the selection
884
- #
885
- def choice(*choices, abortable: false, selected: nil, **kwchoices, &block)
886
- return if choices.empty? && kwchoices.empty?
887
- choice =
888
- if Terminal.ansi?
889
- Choice.new(self, choices, kwchoices, abortable, selected)
890
- else
891
- DumbChoice.new(self, choices, kwchoices, abortable)
892
- end
893
- __with(choice) { choice.select(&block) }
894
- end
895
-
896
- # Allows the user to select from several options.
897
- # All options are returned with their selection status.
679
+ # Waits for a key event and returns information about it.
898
680
  #
899
- # @param choices [{#to_s => [true,false]}]
900
- # Hash of options and their selection state
901
- # @param abortable [true, false]
902
- # whether the user is allowed to abort with 'Esc' or 'Ctrl+c'
903
- # @param selected [#to_s, nil]
904
- # optionally pre-selected key
681
+ # Key names are strings such as `"a"`, `"Enter"`, `"Esc"`, `"Back"`,
682
+ # `"Shift+Alt+F1"`.
905
683
  #
906
- # @yieldparam temp [Temporary]
907
- # temporary displayed section (section will be erased after input)
684
+ # @example
685
+ # answer = ui.query yes: 'y', no: 'n'
686
+ # ui.puts answer == :yes ? 'Confirmed!' : 'Cancelled.'
908
687
  #
909
- # @return [{#to_s => [true,false]}]
910
- # Hash of options and their selection state
911
- # @return [nil]
912
- # when user aborted the selection
913
- def options(abortable: false, selected: nil, **choices, &block)
914
- return {} if choices.empty?
915
- options =
916
- if Terminal.ansi?
917
- Options.new(self, choices, abortable, selected)
918
- else
919
- DumbOptions.new(self, choices, abortable, selected)
920
- end
921
- __with(options) { options.select(&block) }
688
+ # @example With a temporary prompt
689
+ # answer = ui.query(yes: 'y', no: 'n') do
690
+ # ui.puts 'Continue? ([b]y[/b]/[b]n[/b])'
691
+ # end
692
+ #
693
+ # @param options [Hash<Object => String, #each>]
694
+ # map of return values to key names or enumerables of key names;
695
+ # e.g. `{ yes: 'y', no: %w[n Esc] }`
696
+ # @yield (see #temporary)
697
+ # @yieldparam (see #temporary)
698
+ # @return [Object] matched option key
699
+ def query(**options)
700
+ yield(temp = temporary) if block_given?
701
+ return Terminal.read_key_event.name if options.empty?
702
+ Terminal.on_key_event do |event|
703
+ event = event.name
704
+ found, =
705
+ options.find do |_, value|
706
+ (value.is_a?(Enumerable) && value.include?(event)) || value == event
707
+ end
708
+ break found if found
709
+ end
710
+ ensure
711
+ temp&.end
922
712
  end
923
713
 
924
- # Allows the user to select from several options.
925
- # The selected options are returned.
714
+ # Presents a list of options and returns the one the user selects.
926
715
  #
927
- # @example Select a terminal
928
- # ui.select %w[Kitty iTerm2 Ghostty Tabby Rio] do
929
- # ui.puts '[i]Which terminal applications did you already tested?[/i]'
716
+ # In ANSI mode the user navigates with arrow keys and confirms with Enter.
717
+ # In dumb mode items are numbered and the user types the item number.
718
+ #
719
+ # @example Positional items
720
+ # answer = ui.choice 'Yes', 'No', 'Cancel'
721
+ #
722
+ # @example Positional items
723
+ # answer = ui.choice 'Yes', 'No', abortable: true do
724
+ # ui.puts 'Overwrite the file?'
930
725
  # end
931
726
  #
932
- # @param choices [Array<#to_s>]
933
- # selectable options
934
- # @param abortable [true, false]
935
- # whether the user is allowed to abort with 'Esc' or 'Ctrl+c'
936
- # @param selected [Integer, :all, nil]
937
- # optionally pre-selected option index or `:all` to pre-select all items
938
- # @yieldparam temp [Temporary]
939
- # temporary displayed section (section will be erased after input)
727
+ # @example Keyword pairs
728
+ # action = ui.choice(overwrite: 'Overwrite', skip: 'Skip') do
729
+ # ui.puts 'File already exists.'
730
+ # end
940
731
  #
941
- # @return [Array<#to_s>]
942
- # selected options
943
- # @return [nil]
944
- # when user aborted the selection
945
- def select(*choices, abortable: false, selected: nil, &block)
946
- return [] if choices.empty?
947
- choices = choices[0] if choices.size == 1 && choices[0].is_a?(Enumerable)
948
- if selected == :all
949
- sel = true
950
- elsif selected
951
- selected = choices[selected.to_i]
952
- end
953
- options(
954
- abortable: abortable,
955
- selected: selected,
956
- **choices.to_h { [_1, sel] },
957
- &block
958
- ).filter_map { |key, selected| key if selected }
732
+ # @overload choice(*items, abortable: false, selected: nil)
733
+ # Items are passed as positional arguments; the selected item itself is
734
+ # returned.
735
+ # @param items [#to_s, ...] options to choose from
736
+ # @param abortable [Boolean] when `true` the user can press Esc to cancel
737
+ # @param selected pre-selected item value, or `nil`
738
+ #
739
+ # @overload choice(abortable: false, selected: nil, **pairs)
740
+ # Items are passed as keyword pairs `{ return_value => label }`; the
741
+ # matching key is returned.
742
+ # @param abortable [Boolean] when `true` the user can press Esc to cancel
743
+ # @param selected pre-selected return value, or `nil`
744
+ # @param pairs [Hash{Object => #to_s}] map of return values to display labels
745
+ #
746
+ # @yield (see #temporary)
747
+ # @yieldparam (see #temporary)
748
+ # @return [Object] the key of the selected pair, or `nil` if aborted
749
+ def choice(*items, abortable: false, selected: nil, **pairs)
750
+ return if items.empty? && pairs.empty?
751
+ yield(temp = temporary) if block_given?
752
+ (Terminal.ansi? ? Choice : DumbChoice).new(
753
+ self,
754
+ items + pairs.values,
755
+ Array.new(items.size, &:itself) + pairs.keys
756
+ ).select(abortable, selected)
757
+ ensure
758
+ temp&.end
959
759
  end
960
760
 
761
+ # Presents a list of options and returns all items the user selects.
961
762
  #
962
- # @!endgroup
763
+ # In ANSI mode the user toggles items with Space and confirms with Enter.
764
+ # In dumb mode items are numbered and the user types item numbers.
963
765
  #
964
-
766
+ # @example Positional items
767
+ # picks = ui.select 'Kitty', 'iTerm2', 'Ghostty'
965
768
  #
966
- # @!group Utilities
769
+ # @example Positional items with all pre-selected
770
+ # picks = ui.select 'A', 'B', 'C', selected: :all do
771
+ # ui.puts 'Choose features to enable:'
772
+ # end
967
773
  #
774
+ # @example Keyword pairs
775
+ # flags = ui.select verbose: 'Verbose', debug: 'Debug', trace: 'Trace'
776
+ #
777
+ # @overload select(*items, abortable: false, selected: nil)
778
+ # Items are passed as positional arguments; the selected items themselves
779
+ # are returned.
780
+ # @param items [#to_s, ...] options to choose from
781
+ # @param abortable [Boolean] when `true` the user can press Esc to cancel
782
+ # @param selected [nil, :all, #each]
783
+ # pre-selected items: `nil` = none, `:all` = all,
784
+ # or an enumerable of item values to pre-select
785
+ #
786
+ # @overload select(abortable: false, selected: nil, **pairs)
787
+ # Items are passed as keyword pairs `{ return_value => label }`; the
788
+ # matching keys are returned.
789
+ # @param abortable [Boolean] when `true` the user can press Esc to cancel
790
+ # @param selected [nil, :all, #each]
791
+ # @param pairs [Hash{Object => #to_s}] map of return values to labels
792
+ #
793
+ # @yield (see #temporary)
794
+ # @yieldparam (see #temporary)
795
+ # @return [Array] keys of selected pairs, or `nil` if aborted
796
+ def select(*items, abortable: false, selected: nil, **pairs)
797
+ return if items.empty? && pairs.empty?
798
+ yield(temp = temporary) if block_given?
799
+ items = items.to_h { [it, it] }.merge!(pairs)
800
+ (Terminal.ansi? ? Select : DumbSelect).new(
801
+ self,
802
+ if selected == :all
803
+ items.map { it << true }
804
+ elsif selected.is_a?(Enumerable)
805
+ items.map { |ret, txt| [ret, txt, selected.include?(ret)] }
806
+ elsif selected
807
+ items.map { |ret, txt| [ret, txt, selected == ret] }
808
+ else
809
+ items.map { it << false }
810
+ end
811
+ ).select(abortable)
812
+ ensure
813
+ temp&.end
814
+ end
968
815
 
969
- # @private
970
- def columns = Terminal.columns
971
-
972
- # Display some temporary content.
973
- # The content displayed in the block will be erased after the block ends.
974
816
  #
975
- # @example Show tempoary information
976
- # ui.temporary do
977
- # ui.info 'Information', 'This text will disappear when you pressed ENTER.'
978
- # ui.await
979
- # end
817
+ # @!endgroup
980
818
  #
981
- # @yieldparam temp [Temporary]
982
- # itself
819
+ # @!group Utilities
983
820
  #
984
- # @return (see section)
985
- def temporary(&block) = __with(Temporary.new(self), &block)
821
+
822
+ # Executes a shell command and prints its output to the terminal.
823
+ #
824
+ # All arguments and options are forwarded to `Terminal.sh`, which in turn
825
+ # uses `Process.spawn`.
826
+ #
827
+ # @example Run a simple command
828
+ # ui.sh 'echo "Hello Ruby!"'
829
+ #
830
+ # @example Pipe a string as stdin
831
+ # ui.sh 'cat', input: 'Hello from stdin'
832
+ #
833
+ # @overload sh(*cmd, env = {}, shell: false, input: nil, **spawn_options)
834
+ # @param cmd [#to_s, ...] command and arguments, same as `Process.spawn`
835
+ # @param env [Hash, nil] additional environment variables
836
+ # @param shell [Boolean] when `true` runs the command through a system shell
837
+ # @param input piped standard input; accepts any object with `#readpartial`,
838
+ # `#to_io`, `#each`, `#to_a`, or anything `IO.write` accepts (e.g. a String)
839
+ # @param spawn_options [Hash] additional options forwarded to `Process.spawn`
840
+ # @return (see #puts)
841
+ def sh(...) = Shell.render(self, ...)
842
+
843
+ # Executes a shell command, captures its output, and returns it.
844
+ #
845
+ # Stdout and stderr lines are displayed in a scrolling region limited to
846
+ # `max_lines`. All other arguments are identical to {#sh}.
847
+ #
848
+ # @example Capture and inspect output
849
+ # status, out, err = ui.run 'ls', '-la'
850
+ # ui.puts "exit #{status.exitstatus}"
851
+ #
852
+ # @example Limit displayed lines and pipe input
853
+ # File.open('data.txt') { |f| ui.run 'wc', '-l', input: f, max_lines: 5 }
854
+ #
855
+ # @overload run(*cmd, env = {}, shell: false, input: nil, max_lines: 10, **spawn_options)
856
+ # @param cmd (see #sh)
857
+ # @param max_lines [#to_int] maximum number of output lines shown at once
858
+ # @param env (see #sh)
859
+ # @param shell (see #sh)
860
+ # @param input (see #sh)
861
+ # @param spawn_options (see #sh)
862
+ # @return [Array(Process::Status, Array<String>, Array<String>)]
863
+ # three-element array of exit status, stdout lines, and stderr lines
864
+ # @return [nil] when the command could not be started
865
+ def run(*, max_lines: 10, **)
866
+ (Terminal.ansi? ? ShellRunner : DumbShellRunner).render(
867
+ self,
868
+ max_lines,
869
+ *,
870
+ **
871
+ )
872
+ end
986
873
 
987
874
  #
988
875
  # @!endgroup
989
876
  #
990
877
 
991
- private
992
-
993
- def __with(element, &block) = NattyUI.__send__(:with, element, &block)
878
+ # @private
879
+ def columns = Terminal.columns
994
880
 
995
- def __sec(color, title, text, &block)
996
- __with(Section.new(self, title, text, color), &block)
997
- end
881
+ private
998
882
 
999
- def __tsec(color, title, text, &block)
1000
- __sec(color, "#{Theme.current.mark(color)}#{title}", text, &block)
883
+ def __determine_max_width(value)
884
+ return columns unless value
885
+ return 0 if value == 0
886
+ return value.to_int + columns if value < 0
887
+ return (value * columns).round if value < 1
888
+ [value.to_int, columns].min
1001
889
  end
1002
890
 
1003
- def __await(yes, no)
1004
- Terminal.on_key_event do |event|
1005
- event = event.name
1006
- if (no == event) || (no.is_a?(Enumerable) && no.include?(event))
1007
- return false
1008
- end
1009
- if (yes == event) || (yes.is_a?(Enumerable) && yes.include?(event))
1010
- return true
1011
- end
1012
- true
891
+ def __with(element)
892
+ NattyUI.__send__(:_begin, element)
893
+ return element unless block_given?
894
+ begin
895
+ yield(element)
896
+ ensure
897
+ NattyUI.__send__(:_end, element)
1013
898
  end
1014
899
  end
1015
900
  end
1016
901
 
1017
- dir = __dir__ # call the function once
902
+ dir = __dir__
1018
903
 
1019
- autoload :Framed, "#{dir}/framed.rb"
904
+ # @comment Elements:
905
+ autoload :Frame, "#{dir}/frame.rb"
906
+ autoload :Margin, "#{dir}/margin.rb"
907
+ autoload :Progress, "#{dir}/progress.rb"
908
+ autoload :DumbProgress, "#{dir}/dumb_progress.rb"
1020
909
  autoload :Section, "#{dir}/section.rb"
1021
- autoload :Table, "#{dir}/table.rb"
1022
910
  autoload :Task, "#{dir}/task.rb"
1023
911
  autoload :Temporary, "#{dir}/temporary.rb"
1024
- autoload :Theme, "#{dir}/theme.rb"
1025
- autoload :Utils, "#{dir}/utils.rb"
1026
- private_constant(:Framed, :Utils)
1027
912
 
1028
- autoload :Choice, "#{dir}/choice.rb"
1029
- autoload :DumbChoice, "#{dir}/dumb_choice.rb"
1030
- autoload :Options, "#{dir}/options.rb"
1031
- autoload :DumbOptions, "#{dir}/dumb_options.rb"
1032
- autoload :Progress, "#{dir}/progress.rb"
1033
- autoload :DumbProgress, "#{dir}/progress.rb"
913
+ # @comment Helper:
914
+ autoload :Table, "#{dir}/helper/table.rb"
915
+
916
+ # @comment Utils:
917
+ autoload :Utils, "#{dir}/utils/utils.rb"
918
+ autoload :StrConst, "#{dir}/utils/str_const.rb"
919
+ private_constant(:Utils, :StrConst)
920
+
921
+ # @comment Renderer:
922
+ autoload :VBars, "#{dir}/renderer/bars.rb"
923
+ autoload :HBars, "#{dir}/renderer/bars.rb"
924
+ autoload :Choice, "#{dir}/renderer/choice.rb"
925
+ autoload :DumbChoice, "#{dir}/renderer/dumb_choice.rb"
926
+ autoload :Heading, "#{dir}/renderer/heading.rb"
927
+ autoload :HorizontalRule, "#{dir}/renderer/horizontal_rule.rb"
928
+ autoload :LS, "#{dir}/renderer/ls.rb"
929
+ autoload :CompactLS, "#{dir}/renderer/ls.rb"
930
+ autoload :Mark, "#{dir}/renderer/mark.rb"
931
+ autoload :Quote, "#{dir}/renderer/quote.rb"
932
+ autoload :Select, "#{dir}/renderer/select.rb"
933
+ autoload :DumbSelect, "#{dir}/renderer/dumb_select.rb"
934
+ autoload :Shell, "#{dir}/renderer/shell.rb"
935
+ autoload :ShellRunner, "#{dir}/renderer/shell_runner.rb"
936
+ autoload :DumbShellRunner, "#{dir}/renderer/dumb_shell_runner.rb"
937
+ autoload :TableRenderer, "#{dir}/renderer/table_renderer.rb"
1034
938
  private_constant(
939
+ :VBars,
940
+ :HBars,
1035
941
  :Choice,
1036
942
  :DumbChoice,
1037
- :Options,
1038
- :DumbOptions,
1039
- :Progress,
1040
- :DumbProgress
1041
- )
1042
-
1043
- autoload :CompactLSRenderer, "#{dir}/ls_renderer.rb"
1044
- autoload :HBarsRenderer, "#{dir}/hbars_renderer.rb"
1045
- autoload :LSRenderer, "#{dir}/ls_renderer.rb"
1046
- autoload :ShellRenderer, "#{dir}/shell_renderer.rb"
1047
- autoload :VBarsRenderer, "#{dir}/vbars_renderer.rb"
1048
- private_constant(
1049
- :CompactLSRenderer,
1050
- :HBarsRenderer,
1051
- :LSRenderer,
1052
- :ShellRenderer,
1053
- :VBarsRenderer
943
+ :Heading,
944
+ :HorizontalRule,
945
+ :LS,
946
+ :CompactLS,
947
+ :Mark,
948
+ :Quote,
949
+ :Select,
950
+ :DumbSelect,
951
+ :Shell,
952
+ :ShellRunner,
953
+ :DumbShellRunner,
954
+ :TableRenderer
1054
955
  )
1055
956
  end