natty-ui 0.35.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.
- checksums.yaml +4 -4
- data/README.md +6 -6
- data/examples/24bit-colors.rb +9 -5
- data/examples/3bit-colors.rb +7 -7
- data/examples/8bit-colors.rb +5 -7
- data/examples/attributes.rb +2 -3
- data/examples/elements.rb +9 -6
- data/examples/examples.rb +9 -9
- data/examples/frames.rb +31 -0
- data/examples/hbars.rb +6 -3
- data/examples/info.rb +13 -10
- data/examples/key-codes.rb +8 -9
- data/examples/ls.rb +24 -22
- data/examples/named-colors.rb +4 -3
- data/examples/sections.rb +26 -24
- data/examples/select.rb +28 -0
- data/examples/sh.rb +25 -7
- data/examples/tables.rb +19 -37
- data/examples/tasks.rb +32 -22
- data/examples/vbars.rb +5 -3
- data/lib/natty-ui/dumb_progress.rb +68 -0
- data/lib/natty-ui/element.rb +61 -70
- data/lib/natty-ui/features.rb +771 -949
- data/lib/natty-ui/frame.rb +87 -0
- data/lib/natty-ui/helper/table.rb +1376 -0
- data/lib/natty-ui/margin.rb +83 -0
- data/lib/natty-ui/progress.rb +116 -152
- data/lib/natty-ui/renderer/bars.rb +93 -0
- data/lib/natty-ui/renderer/choice.rb +56 -0
- data/lib/natty-ui/renderer/dumb_choice.rb +34 -0
- data/lib/natty-ui/renderer/dumb_select.rb +60 -0
- data/lib/natty-ui/renderer/dumb_shell_runner.rb +19 -0
- data/lib/natty-ui/renderer/heading.rb +26 -0
- data/lib/natty-ui/renderer/horizontal_rule.rb +32 -0
- data/lib/natty-ui/{ls_renderer.rb → renderer/ls.rb} +15 -27
- data/lib/natty-ui/renderer/mark.rb +13 -0
- data/lib/natty-ui/renderer/quote.rb +13 -0
- data/lib/natty-ui/renderer/select.rb +63 -0
- data/lib/natty-ui/renderer/shell.rb +15 -0
- data/lib/natty-ui/renderer/shell_runner.rb +29 -0
- data/lib/natty-ui/renderer/table_renderer.rb +429 -0
- data/lib/natty-ui/section.rb +144 -32
- data/lib/natty-ui/task.rb +38 -25
- data/lib/natty-ui/temporary.rb +27 -14
- data/lib/natty-ui/utils/border.rb +139 -0
- data/lib/natty-ui/utils/str_const.rb +62 -0
- data/lib/natty-ui/utils/utils.rb +47 -0
- data/lib/natty-ui/version.rb +1 -1
- data/lib/natty-ui.rb +76 -35
- metadata +31 -28
- data/examples/cols.rb +0 -38
- data/examples/illustration.rb +0 -60
- data/examples/options.rb +0 -28
- data/examples/themes.rb +0 -51
- data/lib/natty-ui/attributes.rb +0 -593
- data/lib/natty-ui/choice.rb +0 -67
- data/lib/natty-ui/dumb_choice.rb +0 -47
- data/lib/natty-ui/dumb_options.rb +0 -64
- data/lib/natty-ui/framed.rb +0 -50
- data/lib/natty-ui/hbars_renderer.rb +0 -66
- data/lib/natty-ui/options.rb +0 -78
- data/lib/natty-ui/shell_renderer.rb +0 -91
- data/lib/natty-ui/table.rb +0 -325
- data/lib/natty-ui/table_renderer.rb +0 -165
- data/lib/natty-ui/theme.rb +0 -403
- data/lib/natty-ui/utils.rb +0 -111
- data/lib/natty-ui/vbars_renderer.rb +0 -49
- data/lib/natty-ui/width_finder.rb +0 -137
- data/natty-ui.gemspec +0 -34
data/lib/natty-ui/features.rb
CHANGED
|
@@ -1,1134 +1,956 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module NattyUI
|
|
4
|
-
#
|
|
5
|
-
# like {#section}, {#message}, {#information}, {#warning}, {#error},
|
|
6
|
-
# {#failed}, {#framed}, {#task}, ...
|
|
7
|
-
#
|
|
8
|
-
# Any printed text can contain *BBCode*-like embedded ANSI attributes which
|
|
9
|
-
# will be used when the output terminal supports attributes and colors.
|
|
4
|
+
# Mixin that provides all UI methods to {NattyUI} and every {Element}.
|
|
10
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.
|
|
11
10
|
module Features
|
|
12
11
|
#
|
|
13
|
-
# @!group
|
|
14
|
-
#
|
|
15
|
-
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
# @
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
# @
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
44
62
|
else
|
|
45
|
-
bbcode = true
|
|
46
|
-
|
|
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
|
|
47
66
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if max_width > 0
|
|
52
|
-
max_width *= Terminal.columns
|
|
53
|
-
elsif max_width < 0
|
|
54
|
-
max_width += Terminal.columns
|
|
55
|
-
else
|
|
56
|
-
return self
|
|
57
|
-
end
|
|
67
|
+
unless (prefix = popts.delete(:prefix)).nil?
|
|
68
|
+
prefix = StrConst[prefix] unless StrConst === prefix
|
|
69
|
+
return self if (max_width -= prefix.width) < 1
|
|
58
70
|
end
|
|
59
71
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (first_line = options[:first_line_prefix])
|
|
71
|
-
first_line = Ansi.bbcode(first_line) if bbcode
|
|
72
|
-
first_line_width =
|
|
73
|
-
options[:first_line_prefix_width] ||
|
|
74
|
-
Text.width(first_line, bbcode: false)
|
|
75
|
-
|
|
76
|
-
if prefix_width < first_line_width
|
|
77
|
-
prefix_next = "#{prefix}#{' ' * (first_line_width - prefix_width)}"
|
|
78
|
-
prefix = first_line
|
|
79
|
-
prefix_width = first_line_width
|
|
80
|
-
else
|
|
81
|
-
prefix_next = prefix
|
|
82
|
-
prefix =
|
|
83
|
-
if first_line_width < prefix_width
|
|
84
|
-
first_line + (' ' * (prefix_width - first_line_width))
|
|
85
|
-
else
|
|
86
|
-
first_line
|
|
87
|
-
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)
|
|
88
81
|
end
|
|
89
82
|
end
|
|
90
83
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
suffix = Ansi.bbcode(suffix) if bbcode
|
|
95
|
-
max_width -=
|
|
96
|
-
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
|
|
97
87
|
end
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
return self if max_width <= 0
|
|
101
|
-
|
|
102
|
-
lines =
|
|
103
|
-
Text.each_line_with_size(
|
|
104
|
-
*text,
|
|
105
|
-
limit: max_width,
|
|
106
|
-
bbcode: bbcode,
|
|
107
|
-
ansi: Terminal.ansi?,
|
|
108
|
-
ignore_newline: ignore_newline
|
|
109
|
-
)
|
|
110
|
-
tail = options[:tail] and lines = lines.to_a.last(tail)
|
|
111
|
-
@__eol ||= Terminal.ansi? ? "\e[m\n" : "\n"
|
|
112
88
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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)
|
|
118
97
|
end
|
|
119
|
-
return self
|
|
120
|
-
end
|
|
121
98
|
|
|
122
|
-
|
|
123
|
-
max_width = (lines = lines.to_a).max_by(&:last)[-1]
|
|
99
|
+
popts[:width] = max_width
|
|
124
100
|
end
|
|
125
101
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
@__eol,
|
|
135
|
-
bbcode: false
|
|
136
|
-
)
|
|
137
|
-
@lines_written += 1
|
|
138
|
-
prefix, prefix_next = prefix_next, nil if prefix_next
|
|
139
|
-
end
|
|
140
|
-
when :centered
|
|
141
|
-
lines.each do |line, width|
|
|
142
|
-
space = max_width - width
|
|
143
|
-
Terminal.print(
|
|
144
|
-
prefix,
|
|
145
|
-
' ' * (lw = space / 2),
|
|
146
|
-
line,
|
|
147
|
-
' ' * (space - lw),
|
|
148
|
-
suffix,
|
|
149
|
-
@__eol,
|
|
150
|
-
bbcode: false
|
|
151
|
-
)
|
|
152
|
-
@lines_written += 1
|
|
153
|
-
prefix, prefix_next = prefix_next, nil if prefix_next
|
|
154
|
-
end
|
|
155
|
-
else
|
|
156
|
-
lines.each do |line, width|
|
|
157
|
-
Terminal.print(
|
|
158
|
-
prefix,
|
|
159
|
-
line,
|
|
160
|
-
' ' * (max_width - width),
|
|
161
|
-
suffix,
|
|
162
|
-
@__eol,
|
|
163
|
-
bbcode: false
|
|
164
|
-
)
|
|
165
|
-
@lines_written += 1
|
|
166
|
-
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
|
|
167
110
|
end
|
|
168
111
|
end
|
|
112
|
+
Terminal.puts(*lines, bbcode: false)
|
|
113
|
+
@lines_written += lines.size
|
|
169
114
|
self
|
|
170
115
|
end
|
|
171
116
|
|
|
172
|
-
#
|
|
173
|
-
#
|
|
174
|
-
# @param text (see puts)
|
|
175
|
-
# @param mark [Symbol, #to_s]
|
|
176
|
-
# marker type
|
|
177
|
-
#
|
|
178
|
-
# @return (see puts)
|
|
179
|
-
def mark(*text, mark: :default, **options)
|
|
180
|
-
mark = Theme.current.mark(mark)
|
|
181
|
-
options[:first_line_prefix] = mark
|
|
182
|
-
options[:first_line_prefix_width] = mark.width
|
|
183
|
-
puts(*text, **options)
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
# Print given text as lines like {#puts}. Used in elements with temporary
|
|
187
|
-
# output like {#task} the text will be kept ("pinned").
|
|
188
|
-
#
|
|
189
|
-
# It can optionally have a decoration marker in first line like {#mark}.
|
|
117
|
+
# Prints one or more blank lines.
|
|
190
118
|
#
|
|
191
|
-
# @example Print
|
|
192
|
-
# ui.
|
|
193
|
-
# # ...
|
|
194
|
-
# task.pin("This is text", "which is pinned", mark: :information)
|
|
195
|
-
# # ...
|
|
196
|
-
# end
|
|
197
|
-
# # => ✓ Do something important
|
|
198
|
-
# # => 𝒊 This is text
|
|
199
|
-
# # => which is pinned.
|
|
119
|
+
# @example Print a single blank line
|
|
120
|
+
# ui.space
|
|
200
121
|
#
|
|
201
|
-
# @
|
|
202
|
-
#
|
|
203
|
-
# @option (see #puts)
|
|
122
|
+
# @example Print three blank lines
|
|
123
|
+
# ui.space 3
|
|
204
124
|
#
|
|
205
|
-
# @
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
# Print given text as a quotation.
|
|
211
|
-
#
|
|
212
|
-
# @param text (see puts)
|
|
213
|
-
#
|
|
214
|
-
# @return (see puts)
|
|
215
|
-
def quote(*text)
|
|
216
|
-
width = columns * 0.75
|
|
217
|
-
quote = Theme.current.mark(:quote)
|
|
218
|
-
puts(
|
|
219
|
-
*text,
|
|
220
|
-
prefix: quote,
|
|
221
|
-
prefix_width: quote.width,
|
|
222
|
-
max_width: width < 20 ? nil : width.round
|
|
223
|
-
)
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
# Print given text as a heading.
|
|
227
|
-
#
|
|
228
|
-
# There are specific shortcuts for heading levels:
|
|
229
|
-
# {#h1}, {#h2}, {#h3}, {#h4}, {#h5}, {#h6}.
|
|
230
|
-
#
|
|
231
|
-
# @example Print a level 1 heading
|
|
232
|
-
# ui.heading(1, 'This is a H1 heading element')
|
|
233
|
-
# # => ╴╶╴╶─═══ This is a H1 heading element ═══─╴╶╴╶
|
|
234
|
-
#
|
|
235
|
-
# @param level [#to_i]
|
|
236
|
-
# heading level, one of 1..6
|
|
237
|
-
# @param text (see puts)
|
|
238
|
-
#
|
|
239
|
-
# @return (see puts)
|
|
240
|
-
def heading(level, *text)
|
|
241
|
-
prefix, suffix = Theme.current.heading(level)
|
|
242
|
-
puts(
|
|
243
|
-
*text,
|
|
244
|
-
max_width: columns,
|
|
245
|
-
prefix: prefix,
|
|
246
|
-
prefix_width: prefix.width,
|
|
247
|
-
suffix: suffix,
|
|
248
|
-
suffix_width: suffix.width,
|
|
249
|
-
align: :centered
|
|
250
|
-
)
|
|
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)
|
|
251
129
|
end
|
|
252
130
|
|
|
253
|
-
#
|
|
131
|
+
# Prints text with a leading mark symbol.
|
|
254
132
|
#
|
|
255
|
-
# @
|
|
133
|
+
# @example Default mark
|
|
134
|
+
# ui.mark 'Item one'
|
|
256
135
|
#
|
|
257
|
-
# @
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
# Print given text as a H2 {#heading}.
|
|
136
|
+
# @example Named symbol mark
|
|
137
|
+
# ui.mark 'Done!', mark: :checkmark
|
|
261
138
|
#
|
|
262
|
-
# @
|
|
139
|
+
# @example Custom string mark
|
|
140
|
+
# ui.mark 'Warning', mark: '[bright_red]!'
|
|
263
141
|
#
|
|
264
|
-
# @
|
|
265
|
-
|
|
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
|
|
266
153
|
|
|
267
|
-
#
|
|
154
|
+
# Prints text with a green check-mark prefix.
|
|
155
|
+
#
|
|
156
|
+
# Shorthand for `mark(*text, mark: :checkmark, **options)`.
|
|
268
157
|
#
|
|
269
|
-
# @
|
|
158
|
+
# @example
|
|
159
|
+
# ui.ok 'All tests passed'
|
|
270
160
|
#
|
|
271
|
-
# @
|
|
272
|
-
|
|
161
|
+
# @param text (see #puts)
|
|
162
|
+
# @param popts (see #mark)
|
|
163
|
+
# @return (see #puts)
|
|
164
|
+
def ok(*text, **popts) = mark(*text, mark: :checkmark, **popts)
|
|
273
165
|
|
|
274
|
-
#
|
|
166
|
+
# Prints text with a mark that persists after a {Temporary} element closes.
|
|
275
167
|
#
|
|
276
|
-
#
|
|
168
|
+
# Identical to {#mark} but survives when the surrounding {Temporary} element
|
|
169
|
+
# (see {#temporary}, {#task}, {#progress}) is erased.
|
|
277
170
|
#
|
|
278
|
-
# @
|
|
279
|
-
|
|
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
|
|
176
|
+
#
|
|
177
|
+
# @param (see mark)
|
|
178
|
+
# @return (see #puts)
|
|
179
|
+
def pin(*text, mark: :default, **popts)
|
|
180
|
+
mark(*text, mark:, pin: true, **popts)
|
|
181
|
+
end
|
|
280
182
|
|
|
281
|
-
#
|
|
183
|
+
# Prints text with a left-side quotation border.
|
|
282
184
|
#
|
|
283
|
-
# @
|
|
185
|
+
# @example
|
|
186
|
+
# ui.quote "To be or not to be,\nthat is the question."
|
|
284
187
|
#
|
|
285
|
-
# @
|
|
286
|
-
|
|
188
|
+
# @param (see #puts)
|
|
189
|
+
# @return (see #puts)
|
|
190
|
+
def quote(*text) = Quote.render(self, *text)
|
|
287
191
|
|
|
288
|
-
#
|
|
192
|
+
# Prints a heading at the given level.
|
|
289
193
|
#
|
|
290
|
-
#
|
|
194
|
+
# Six heading levels are supported (similarly to HTML `<h1>`–`<h6>`).
|
|
195
|
+
# Use the shorthand helpers {#h1}–{#h6} for convenience.
|
|
291
196
|
#
|
|
292
|
-
# @
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
# Print a horizontal rule.
|
|
197
|
+
# @example Large heading
|
|
198
|
+
# ui.heading 1, 'Chapter One'
|
|
296
199
|
#
|
|
297
|
-
# @example
|
|
298
|
-
# ui.
|
|
200
|
+
# @example Sub-heading
|
|
201
|
+
# ui.heading 3, 'Section [i]Overview[/i]'
|
|
299
202
|
#
|
|
300
|
-
# @param
|
|
301
|
-
#
|
|
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.
|
|
302
209
|
#
|
|
303
|
-
# @
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
bc = theme.border(type)[10]
|
|
307
|
-
puts("#{theme.heading_sytle}#{bc * columns}")
|
|
308
|
-
end
|
|
210
|
+
# @param title (see #heading)
|
|
211
|
+
# @return (see #puts)
|
|
212
|
+
def h1(title) = heading(1, title)
|
|
309
213
|
|
|
310
|
-
#
|
|
214
|
+
# Prints a level-2 heading.
|
|
311
215
|
#
|
|
312
|
-
# @param
|
|
313
|
-
#
|
|
216
|
+
# @param title (see #heading)
|
|
217
|
+
# @return (see #puts)
|
|
218
|
+
def h2(title) = heading(2, title)
|
|
219
|
+
|
|
220
|
+
# Prints a level-3 heading.
|
|
314
221
|
#
|
|
315
|
-
# @
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
end
|
|
222
|
+
# @param title (see #heading)
|
|
223
|
+
# @return (see #puts)
|
|
224
|
+
def h3(title) = heading(3, title)
|
|
319
225
|
|
|
320
|
-
#
|
|
226
|
+
# Prints a level-4 heading.
|
|
321
227
|
#
|
|
322
|
-
#
|
|
228
|
+
# @param title (see #heading)
|
|
229
|
+
# @return (see #puts)
|
|
230
|
+
def h4(title) = heading(4, title)
|
|
231
|
+
|
|
232
|
+
# Prints a level-5 heading.
|
|
323
233
|
#
|
|
324
|
-
#
|
|
325
|
-
#
|
|
326
|
-
|
|
327
|
-
|
|
234
|
+
# @param title (see #heading)
|
|
235
|
+
# @return (see #puts)
|
|
236
|
+
def h5(title) = heading(5, title)
|
|
237
|
+
|
|
238
|
+
# Prints a level-6 heading.
|
|
328
239
|
#
|
|
329
|
-
# @
|
|
330
|
-
#
|
|
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.
|
|
331
245
|
#
|
|
332
|
-
# @example
|
|
333
|
-
# ui.
|
|
246
|
+
# @example Default rule
|
|
247
|
+
# ui.hr
|
|
334
248
|
#
|
|
335
|
-
# @
|
|
336
|
-
#
|
|
337
|
-
# @param compact [true, false]
|
|
338
|
-
# whether the compact display format should be used
|
|
339
|
-
# @param glyph [Integer, :hex, Symbol, #to_s]
|
|
340
|
-
# glyph to be used as prefix
|
|
249
|
+
# @example Named style
|
|
250
|
+
# ui.hr :double
|
|
341
251
|
#
|
|
342
|
-
# @
|
|
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)
|
|
343
285
|
def ls(*items, compact: true, glyph: nil)
|
|
344
286
|
return self if items.empty?
|
|
345
|
-
|
|
346
|
-
puts(*renderer.lines(items, glyph, columns))
|
|
347
|
-
end
|
|
348
|
-
|
|
349
|
-
# Generate and print a table.
|
|
350
|
-
# See {Table} for much more details about table generation.
|
|
351
|
-
#
|
|
352
|
-
# @example Draw a very simple 3x4 table with complete borders
|
|
353
|
-
# ui.table(border: :default, border_around: true, padding: [0, 1]) do |table|
|
|
354
|
-
# table.add 1, 2, 3, 4
|
|
355
|
-
# table.add 5, 6, 7, 8
|
|
356
|
-
# table.add 9, 10, 11, 12
|
|
357
|
-
# end
|
|
358
|
-
#
|
|
359
|
-
# @param attributes [{Symbol => Object}]
|
|
360
|
-
# attributes for the table and default attributes for table cells
|
|
361
|
-
# @option attributes [Symbol] :border (nil)
|
|
362
|
-
# kind of border,
|
|
363
|
-
# see {Table::Attributes}
|
|
364
|
-
# @option attributes [Enumerable<Symbol>] :border_style (nil)
|
|
365
|
-
# style of border,
|
|
366
|
-
# see {Table::Attributes}
|
|
367
|
-
# @option attributes [true, false] :border_around (false)
|
|
368
|
-
# whether the table should have a border around,
|
|
369
|
-
# see {Table::Attributes}
|
|
370
|
-
# @option attributes [:left, :right, :centered] :position (false)
|
|
371
|
-
# where to align the table,
|
|
372
|
-
# see {Table::Attributes}
|
|
373
|
-
#
|
|
374
|
-
# @yieldparam table [Table]
|
|
375
|
-
# helper to define the table layout
|
|
376
|
-
#
|
|
377
|
-
# @return (see puts)
|
|
378
|
-
def table(**attributes)
|
|
379
|
-
return self unless block_given?
|
|
380
|
-
yield(table = Table.new(**attributes))
|
|
381
|
-
puts(
|
|
382
|
-
*TableRenderer[table, columns],
|
|
383
|
-
align: table.attributes.position,
|
|
384
|
-
expand: true
|
|
385
|
-
)
|
|
386
|
-
end
|
|
387
|
-
|
|
388
|
-
# Print text in columns.
|
|
389
|
-
# This is a shorthand to define a {Table} with a single row.
|
|
390
|
-
#
|
|
391
|
-
# @param columns [#to_s]
|
|
392
|
-
# two or more convertible objects to print side by side
|
|
393
|
-
# @param attributes (see table)
|
|
394
|
-
# @option attributes (see table)
|
|
395
|
-
# @option attributes [Integer] :width (nil)
|
|
396
|
-
# width of a column,
|
|
397
|
-
# see {Attributes::Width}
|
|
398
|
-
#
|
|
399
|
-
# @yieldparam row [Table::Row]
|
|
400
|
-
# helper to define the row layout
|
|
401
|
-
#
|
|
402
|
-
# @return (see puts)
|
|
403
|
-
def cols(*columns, **attributes)
|
|
404
|
-
tab_att, att = Utils.split_table_attr(attributes)
|
|
405
|
-
table(**tab_att) do |table|
|
|
406
|
-
table.add do |row|
|
|
407
|
-
columns.each { row.add(_1, **att) }
|
|
408
|
-
yield(row) if block_given?
|
|
409
|
-
end
|
|
410
|
-
end
|
|
411
|
-
end
|
|
412
|
-
|
|
413
|
-
# Print a text division with attributes.
|
|
414
|
-
# This is a shorthand to define a {Table} with a single cell.
|
|
415
|
-
#
|
|
416
|
-
# @param text (see puts)
|
|
417
|
-
# @param attributes [{Symbol => Object}]
|
|
418
|
-
# attributes for the division
|
|
419
|
-
# @option attributes [:left, :right, :centered] :align (:left)
|
|
420
|
-
# text alignment,
|
|
421
|
-
# see {Attributes::Align}
|
|
422
|
-
# @option attributes [Integer, Enumerable<Integer>] :padding (nil)
|
|
423
|
-
# text padding,
|
|
424
|
-
# see {Attributes::Padding}
|
|
425
|
-
# @option attributes [Enumerable<Symbol>] :style (nil)
|
|
426
|
-
# text style,
|
|
427
|
-
# see {Attributes::Style}
|
|
428
|
-
# @option attributes [Integer] :width (nil)
|
|
429
|
-
# width of the cell,
|
|
430
|
-
# see {Attributes::Width}
|
|
431
|
-
# @option attributes (see table)
|
|
432
|
-
#
|
|
433
|
-
# @return (see puts)
|
|
434
|
-
def div(*text, **attributes)
|
|
435
|
-
return self if text.empty?
|
|
436
|
-
tab_att, att = Utils.split_table_attr(attributes)
|
|
437
|
-
tab_att[:border_around] = true
|
|
438
|
-
table(**tab_att) { |table| table.add { _1.add(*text, **att) } }
|
|
287
|
+
puts(*(compact ? CompactLS : LS).lines(columns, items, glyph))
|
|
439
288
|
end
|
|
440
289
|
|
|
441
|
-
#
|
|
442
|
-
#
|
|
443
|
-
#
|
|
444
|
-
#
|
|
445
|
-
#
|
|
446
|
-
#
|
|
447
|
-
#
|
|
448
|
-
#
|
|
449
|
-
#
|
|
450
|
-
#
|
|
451
|
-
# @param
|
|
452
|
-
#
|
|
453
|
-
# @param
|
|
454
|
-
#
|
|
455
|
-
# @
|
|
456
|
-
#
|
|
457
|
-
# @
|
|
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)
|
|
458
317
|
def vbars(
|
|
459
318
|
values,
|
|
319
|
+
min: nil,
|
|
320
|
+
max: nil,
|
|
460
321
|
normalize: false,
|
|
461
322
|
height: 10,
|
|
462
|
-
bar_width:
|
|
323
|
+
bar_width: 3,
|
|
463
324
|
style: nil
|
|
464
325
|
)
|
|
465
|
-
|
|
466
|
-
if
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
puts(
|
|
470
|
-
*VBarsRenderer.lines(
|
|
471
|
-
values,
|
|
472
|
-
columns,
|
|
473
|
-
height,
|
|
474
|
-
normalize,
|
|
475
|
-
bar_width,
|
|
476
|
-
Terminal.ansi? ? style : nil
|
|
477
|
-
)
|
|
478
|
-
)
|
|
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)
|
|
479
330
|
end
|
|
480
331
|
|
|
481
|
-
#
|
|
482
|
-
#
|
|
483
|
-
# @example Draw green bars
|
|
484
|
-
# ui.hbars 1..10, style: :green
|
|
332
|
+
# Prints a horizontal bar chart.
|
|
485
333
|
#
|
|
486
|
-
#
|
|
487
|
-
# ui.hbars 1..10, style: :blue, width: 0.5
|
|
334
|
+
# All values must be non-negative.
|
|
488
335
|
#
|
|
489
|
-
# @
|
|
490
|
-
#
|
|
491
|
-
# @param max [#to_f] end value
|
|
492
|
-
# @param normalize [true, false] whether the values should be normalized
|
|
493
|
-
# @param text [true, false] whether the values should be printed too
|
|
494
|
-
# @param width [:auto, :min, Integer] with of each bar
|
|
495
|
-
# @param style [Symbol, Array<Symbol>, nil] bar drawing style
|
|
496
|
-
# @param text_style [Symbol, Array<Symbol>, nil] text style
|
|
336
|
+
# @example Simple horizontal chart
|
|
337
|
+
# ui.hbars [3, 7, 2, 9, 5]
|
|
497
338
|
#
|
|
498
|
-
# @
|
|
339
|
+
# @example Chart without value labels
|
|
340
|
+
# ui.hbars [10, 40, 25, 60], text: false, normalize: true
|
|
499
341
|
#
|
|
500
|
-
# @
|
|
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)
|
|
501
353
|
def hbars(
|
|
502
354
|
values,
|
|
503
355
|
min: nil,
|
|
504
356
|
max: nil,
|
|
505
357
|
normalize: false,
|
|
506
|
-
text: true,
|
|
507
358
|
width: :auto,
|
|
508
359
|
style: nil,
|
|
360
|
+
text: true,
|
|
509
361
|
text_style: nil
|
|
510
362
|
)
|
|
511
|
-
|
|
512
|
-
if
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
renderer = HBarsRenderer.new(values, min, max)
|
|
517
|
-
renderer.with_text(text_style) if text
|
|
518
|
-
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)
|
|
519
368
|
end
|
|
520
369
|
|
|
521
|
-
#
|
|
522
|
-
# When a `max` parameter is given the progress will be displayed as a
|
|
523
|
-
# progress bar below the `title`. Otherwise the progress is displayed just
|
|
524
|
-
# by accumulating dots.
|
|
525
|
-
#
|
|
526
|
-
# @example Display a progress bar
|
|
527
|
-
# ui.progress('Download file', max: 1024) do |progress|
|
|
528
|
-
# while progress.value < progress.max
|
|
529
|
-
# # just to simulate the download
|
|
530
|
-
# sleep(0.1)
|
|
531
|
-
# bytes_read = rand(10..128)
|
|
532
|
-
#
|
|
533
|
-
# # here we actualize the progress
|
|
534
|
-
# progress.value += bytes_read
|
|
535
|
-
# end
|
|
536
|
-
# end
|
|
370
|
+
# Renders a {Table} to the terminal.
|
|
537
371
|
#
|
|
538
|
-
#
|
|
539
|
-
#
|
|
540
|
-
# 10.times do
|
|
541
|
-
# # simulate some work
|
|
542
|
-
# sleep 0.1
|
|
372
|
+
# The method yields a {Table} object for population; if no block is given
|
|
373
|
+
# nothing is rendered.
|
|
543
374
|
#
|
|
544
|
-
#
|
|
545
|
-
#
|
|
375
|
+
# Border name symbols:
|
|
376
|
+
# `:rounded` (default), `:single`, `:double`, `:heavy`,
|
|
377
|
+
# `:single_double`, `:double_single`, `:single_heavy`, `:heavy_single`
|
|
378
|
+
#
|
|
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
|
|
546
384
|
# end
|
|
547
|
-
#
|
|
548
|
-
#
|
|
549
|
-
#
|
|
550
|
-
#
|
|
551
|
-
#
|
|
552
|
-
#
|
|
553
|
-
#
|
|
554
|
-
#
|
|
555
|
-
#
|
|
556
|
-
#
|
|
557
|
-
#
|
|
558
|
-
#
|
|
559
|
-
#
|
|
560
|
-
#
|
|
561
|
-
#
|
|
562
|
-
#
|
|
563
|
-
#
|
|
564
|
-
#
|
|
565
|
-
#
|
|
566
|
-
#
|
|
567
|
-
#
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
def progress(title, max: nil, pin: false, &block)
|
|
574
|
-
__with(
|
|
575
|
-
if Terminal.ansi?
|
|
576
|
-
Progress.new(self, title, max, pin)
|
|
577
|
-
else
|
|
578
|
-
DumbProgress.new(self, title, max)
|
|
579
|
-
end,
|
|
580
|
-
&block
|
|
581
|
-
)
|
|
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)
|
|
582
411
|
end
|
|
583
412
|
|
|
584
|
-
# Execute a program.
|
|
585
413
|
#
|
|
586
|
-
#
|
|
587
|
-
# ui.sh 'ls'
|
|
414
|
+
# @!endgroup
|
|
588
415
|
#
|
|
589
|
-
|
|
590
|
-
# ret = ui.sh('curl', '--version')
|
|
591
|
-
# raise('Curl not found') unless ret&.success?
|
|
416
|
+
|
|
592
417
|
#
|
|
593
|
-
#
|
|
594
|
-
# ui.sh "mkdir 'test' && cd 'test'"
|
|
418
|
+
# @!group Section elements
|
|
595
419
|
#
|
|
596
|
-
|
|
597
|
-
#
|
|
420
|
+
|
|
421
|
+
# Creates a {Temporary} element whose output is erased when it closes.
|
|
598
422
|
#
|
|
599
|
-
# @
|
|
423
|
+
# @example Manual close
|
|
424
|
+
# tmp = ui.temporary
|
|
425
|
+
# ui.puts 'Loading…'
|
|
426
|
+
# sleep 1
|
|
427
|
+
# tmp.end # erases "Loading…"
|
|
600
428
|
#
|
|
601
|
-
# @
|
|
602
|
-
#
|
|
603
|
-
#
|
|
604
|
-
#
|
|
605
|
-
#
|
|
606
|
-
#
|
|
607
|
-
#
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
# Execute a shell program and return output. Limit the lines displayed.
|
|
613
|
-
#
|
|
614
|
-
# @example Capture output and error
|
|
615
|
-
# status, out, err = ui.run('ls ./ && ls ./this_does_not_exist')
|
|
616
|
-
# # => #<Process::Status: pid 25562 exit 1>
|
|
617
|
-
# # => [...] # the output of first `ls`
|
|
618
|
-
# # => ["ls: ./this_does_not_exist: No such file or directory"]
|
|
619
|
-
#
|
|
620
|
-
# @see sh
|
|
621
|
-
#
|
|
622
|
-
# @param cmd [String]
|
|
623
|
-
# command and optional arguments
|
|
624
|
-
# @param preserve_spaces [true,false]
|
|
625
|
-
# whether the spaces and tabs of the output should be preserve
|
|
626
|
-
# @param max_lines [Integer]
|
|
627
|
-
# limit of displayed lines
|
|
628
|
-
# @param options [Hash] executions options
|
|
629
|
-
# @return [[Process::Status, Array<String>, Array<String>]]
|
|
630
|
-
# process status, output and error output when command was executed
|
|
631
|
-
# @return [nil]
|
|
632
|
-
# in error case (like command not found)
|
|
633
|
-
def run(*cmd, preserve_spaces: false, max_lines: 10, **options)
|
|
634
|
-
result =
|
|
635
|
-
ShellRenderer.run(
|
|
636
|
-
self,
|
|
637
|
-
cmd,
|
|
638
|
-
options,
|
|
639
|
-
preserve_spaces,
|
|
640
|
-
max_lines.clamp(1, Terminal.rows)
|
|
641
|
-
)
|
|
642
|
-
result if result[0]
|
|
643
|
-
end
|
|
429
|
+
# @example Block form
|
|
430
|
+
# ui.temporary do
|
|
431
|
+
# ui.puts 'Thinking…'
|
|
432
|
+
# sleep 2
|
|
433
|
+
# end # "Thinking…" is erased here
|
|
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), &)
|
|
644
440
|
|
|
441
|
+
# Creates a {Margin} element that adds horizontal and vertical whitespace.
|
|
645
442
|
#
|
|
646
|
-
#
|
|
443
|
+
# @example
|
|
444
|
+
# ui.margin 0, 0.25 do
|
|
445
|
+
# ui.puts 'This text has 25% width horizontal margin.'
|
|
446
|
+
# end
|
|
647
447
|
#
|
|
648
|
-
|
|
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
|
|
649
494
|
#
|
|
650
|
-
#
|
|
495
|
+
# @example Block form without title
|
|
496
|
+
# ui.section do
|
|
497
|
+
# ui.puts 'Anonymous section content.'
|
|
498
|
+
# end
|
|
651
499
|
#
|
|
652
|
-
|
|
653
|
-
#
|
|
654
|
-
#
|
|
655
|
-
#
|
|
656
|
-
#
|
|
657
|
-
#
|
|
658
|
-
#
|
|
659
|
-
# @
|
|
660
|
-
#
|
|
661
|
-
#
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
# section.puts 'Sections are areas of text elements.'
|
|
665
|
-
# section.puts 'You can use any other feature inside such an area.'
|
|
666
|
-
# end
|
|
667
|
-
# # => ╭────╶╶╶
|
|
668
|
-
# # => │ ╴╶╴╶─═══ About Sections ═══─╴╶╴╶
|
|
669
|
-
# # => │
|
|
670
|
-
# # => │ Sections are areas of text elements.
|
|
671
|
-
# # => │ You can use any other feature inside such an area.
|
|
672
|
-
# # => ╰──── ─╶╶╶
|
|
673
|
-
#
|
|
674
|
-
# @yieldparam section [Section]
|
|
675
|
-
# itself
|
|
676
|
-
# @return [Object]
|
|
677
|
-
# the result of the given block
|
|
678
|
-
#
|
|
679
|
-
# @overload section(*text, **options)
|
|
680
|
-
#
|
|
681
|
-
# @example
|
|
682
|
-
# section = ui.section
|
|
683
|
-
# section.h1 'About Sections'
|
|
684
|
-
# section.space
|
|
685
|
-
# section.puts 'Sections are areas of text elements.'
|
|
686
|
-
# section.end # close the section
|
|
687
|
-
#
|
|
688
|
-
# @param text [*#to_s]
|
|
689
|
-
# optional objects to print line by line
|
|
690
|
-
# @param options [{Symbol => Object}]
|
|
691
|
-
# print options – see {puts}
|
|
692
|
-
# @return [Section]
|
|
693
|
-
# itself
|
|
694
|
-
def section(*text, **options, &block)
|
|
695
|
-
__sec(:default, false, nil, text, options, &block)
|
|
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), &)
|
|
696
512
|
end
|
|
697
513
|
alias begin section
|
|
698
514
|
|
|
699
|
-
#
|
|
700
|
-
#
|
|
701
|
-
#
|
|
702
|
-
#
|
|
703
|
-
#
|
|
704
|
-
#
|
|
705
|
-
#
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
# @return [Object]
|
|
709
|
-
# the result of the given block
|
|
710
|
-
# @overload $0(title, *text, **options)
|
|
711
|
-
# @param title [#to_s]
|
|
712
|
-
# section title
|
|
713
|
-
# @param text [*#to_s]
|
|
714
|
-
# optional objects to print line by line
|
|
715
|
-
# @param options [{Symbol => Object}]
|
|
716
|
-
# print options – see {puts}
|
|
717
|
-
# @return [Section]
|
|
718
|
-
# itself
|
|
719
|
-
|
|
720
|
-
# Create a visually separated section with title for the output of text
|
|
721
|
-
# elements.
|
|
722
|
-
# @macro msg_like
|
|
723
|
-
def message(title, *text, **options, &block)
|
|
724
|
-
__sec(:message, false, title, text, options, &block)
|
|
515
|
+
# Creates a {Section} with `:message` styling.
|
|
516
|
+
#
|
|
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, &)
|
|
725
524
|
end
|
|
726
525
|
alias msg message
|
|
727
526
|
|
|
728
|
-
#
|
|
729
|
-
# the output of text elements.
|
|
527
|
+
# Creates a {Section} with `:information` styling.
|
|
730
528
|
#
|
|
731
|
-
# @
|
|
732
|
-
|
|
733
|
-
|
|
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, &)
|
|
734
535
|
end
|
|
735
536
|
alias info information
|
|
736
537
|
|
|
737
|
-
#
|
|
738
|
-
# the output of text elements.
|
|
538
|
+
# Creates a {Section} with `:warning` styling.
|
|
739
539
|
#
|
|
740
|
-
# @
|
|
741
|
-
|
|
742
|
-
|
|
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, &)
|
|
743
546
|
end
|
|
744
547
|
alias warn warning
|
|
745
548
|
|
|
746
|
-
#
|
|
747
|
-
# the output of text elements.
|
|
549
|
+
# Creates a {Section} with `:error` styling.
|
|
748
550
|
#
|
|
749
|
-
# @
|
|
750
|
-
|
|
751
|
-
|
|
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, &)
|
|
752
557
|
end
|
|
753
|
-
alias err error
|
|
754
558
|
|
|
755
|
-
#
|
|
756
|
-
# the output of text elements.
|
|
559
|
+
# Creates a {Section} with `:fatal` styling.
|
|
757
560
|
#
|
|
758
|
-
# @
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
# Like any other {Element} sections support all {Features}.
|
|
765
|
-
#
|
|
766
|
-
# @overload framed(align: :left, border: :default, border_style: nil)
|
|
767
|
-
# @param align [:left, :right, :centered]
|
|
768
|
-
# text alignment,
|
|
769
|
-
# see {Attributes::Align}
|
|
770
|
-
# @param border [Symbol]
|
|
771
|
-
# kind of border,
|
|
772
|
-
# see {Attributes::Border}
|
|
773
|
-
# @param border_style [Enumerable<Symbol>]
|
|
774
|
-
# style of border,
|
|
775
|
-
# see {Attributes::BorderStyle}
|
|
776
|
-
# @yieldparam framed [Element]
|
|
777
|
-
# itself
|
|
778
|
-
# @return [Object] the result of the given block
|
|
779
|
-
# @overload framed(*text, align: :left, border: :default, border_style: nil, **options)
|
|
780
|
-
# @param text [*#to_s]
|
|
781
|
-
# optional objects to print line by line
|
|
782
|
-
# @param align [:left, :right, :centered]
|
|
783
|
-
# text alignment,
|
|
784
|
-
# see {Attributes::Align}
|
|
785
|
-
# @param border [Symbol]
|
|
786
|
-
# kind of border,
|
|
787
|
-
# see {Attributes::Border}
|
|
788
|
-
# @param border_style [Enumerable<Symbol>]
|
|
789
|
-
# style of border,
|
|
790
|
-
# see {Attributes::BorderStyle}
|
|
791
|
-
# @param options [{Symbol => Object}]
|
|
792
|
-
# print options – see {puts}
|
|
793
|
-
# @return [Element] itself
|
|
794
|
-
def framed(
|
|
795
|
-
*text,
|
|
796
|
-
align: :left,
|
|
797
|
-
border: :default,
|
|
798
|
-
border_style: nil,
|
|
799
|
-
**options,
|
|
800
|
-
&block
|
|
801
|
-
)
|
|
802
|
-
__with(
|
|
803
|
-
Framed.new(
|
|
804
|
-
self,
|
|
805
|
-
Utils.align(align),
|
|
806
|
-
Theme.current.border(border),
|
|
807
|
-
Utils.style(border_style)
|
|
808
|
-
),
|
|
809
|
-
*text,
|
|
810
|
-
**options,
|
|
811
|
-
&block
|
|
812
|
-
)
|
|
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, &)
|
|
813
567
|
end
|
|
814
568
|
|
|
815
|
-
#
|
|
569
|
+
# Creates a {Frame} element — a bordered box with an optional title.
|
|
570
|
+
#
|
|
571
|
+
# @example Manual close
|
|
572
|
+
# frm = ui.frame 'Preview'
|
|
573
|
+
# ui.puts 'Content inside the frame.'
|
|
574
|
+
# frm.end
|
|
816
575
|
#
|
|
817
|
-
# @
|
|
818
|
-
#
|
|
819
|
-
#
|
|
820
|
-
#
|
|
821
|
-
# @param pin [true, false] whether to keep text "pinned"
|
|
822
|
-
# @param options [{Symbol => Object}]
|
|
823
|
-
# print options – see {puts}
|
|
576
|
+
# @example Block form with title and custom border
|
|
577
|
+
# ui.frame 'Results', border: :double do
|
|
578
|
+
# ui.puts 'All checks passed.'
|
|
579
|
+
# end
|
|
824
580
|
#
|
|
825
|
-
# @
|
|
581
|
+
# @example Block form without a title
|
|
582
|
+
# ui.frame do
|
|
583
|
+
# ui.puts 'Framed content.'
|
|
584
|
+
# end
|
|
826
585
|
#
|
|
827
|
-
# @
|
|
828
|
-
|
|
829
|
-
|
|
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), &)
|
|
830
596
|
end
|
|
831
597
|
|
|
598
|
+
# Creates a {Task} element — a labelled step that shows a spinner while
|
|
599
|
+
# running and a check mark on success.
|
|
832
600
|
#
|
|
833
|
-
#
|
|
834
|
-
#
|
|
835
|
-
|
|
601
|
+
# @example Manual close
|
|
602
|
+
# t = ui.task 'Installing dependencies'
|
|
603
|
+
# run_install
|
|
604
|
+
# t.end
|
|
836
605
|
#
|
|
837
|
-
#
|
|
606
|
+
# @example Block form
|
|
607
|
+
# ui.task 'Installing dependencies' do
|
|
608
|
+
# run_install
|
|
609
|
+
# end
|
|
838
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), &)
|
|
839
618
|
|
|
840
|
-
#
|
|
619
|
+
# Creates a {Progress} element for tracking incremental work.
|
|
841
620
|
#
|
|
842
|
-
#
|
|
843
|
-
#
|
|
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.
|
|
844
623
|
#
|
|
845
|
-
# @example
|
|
846
|
-
# ui.
|
|
847
|
-
#
|
|
624
|
+
# @example Bounded progress
|
|
625
|
+
# ui.progress 'Processing', max: items.size do |bar|
|
|
626
|
+
# items.each { process(it); bar.step }
|
|
848
627
|
# end
|
|
849
|
-
# # => true, for user's YES
|
|
850
|
-
# # => false, for user's NO
|
|
851
|
-
# # Info:
|
|
852
|
-
# # The keys will work for Afrikaans, Dutch, English, French, German,
|
|
853
|
-
# # Italian, Polish, Portuguese, Romanian, Spanish and Swedish.
|
|
854
|
-
#
|
|
855
|
-
# @overload await(yes: 'Enter', no: 'Esc')
|
|
856
|
-
#
|
|
857
|
-
# @overload await(yes: 'Enter', no: 'Esc', &block)
|
|
858
|
-
# @yieldparam temp [Temporary]
|
|
859
|
-
# temporary displayed section (section will be erased after input)
|
|
860
628
|
#
|
|
861
|
-
# @
|
|
862
|
-
#
|
|
863
|
-
#
|
|
864
|
-
#
|
|
629
|
+
# @example Open-ended progress
|
|
630
|
+
# ui.progress 'Working…' do |bar|
|
|
631
|
+
# loop { bar.step; break if done? }
|
|
632
|
+
# end
|
|
865
633
|
#
|
|
866
|
-
# @
|
|
867
|
-
#
|
|
868
|
-
# @
|
|
869
|
-
#
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
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
|
+
)
|
|
876
651
|
end
|
|
877
652
|
|
|
878
|
-
#
|
|
879
|
-
#
|
|
880
|
-
#
|
|
881
|
-
#
|
|
882
|
-
#
|
|
883
|
-
# one or more alternatives to select from
|
|
884
|
-
# @param abortable [true, false]
|
|
885
|
-
# whether the user is allowed to abort with 'Esc' or 'Ctrl+c'
|
|
886
|
-
#
|
|
887
|
-
# @return [Integer]
|
|
888
|
-
# index of selected choice
|
|
889
|
-
# @return [nil]
|
|
890
|
-
# when user aborted the selection
|
|
891
|
-
#
|
|
892
|
-
# @overload choice(*choices, abortable: false, &block)
|
|
893
|
-
# @example Request a fruit
|
|
894
|
-
# ui.choice('Apple', 'Banana', 'Orange') { ui.puts 'What do you prefer?' }
|
|
895
|
-
# # => 0, when user likes apples
|
|
896
|
-
# # => 1, when bananas are user's favorite
|
|
897
|
-
# # => 2, when user is a oranges lover
|
|
898
|
-
#
|
|
899
|
-
# @param choices[#to_s]
|
|
900
|
-
# one or more alternatives to select from
|
|
901
|
-
# @param abortable[true, false]
|
|
902
|
-
# whether the user is allowed to abort with 'Esc' or 'Ctrl+c'
|
|
903
|
-
#
|
|
904
|
-
# @yieldparam temp [Temporary]
|
|
905
|
-
# temporary displayed section (section will be erased after input)
|
|
906
|
-
#
|
|
907
|
-
# @return [Integer]
|
|
908
|
-
# index of selected choice
|
|
909
|
-
# @return [nil]
|
|
910
|
-
# when user aborted the selection
|
|
911
|
-
#
|
|
912
|
-
# @overload choice(**choices, abortable: false)
|
|
913
|
-
# @param choices [#to_s]
|
|
914
|
-
# one or more alternatives to select from
|
|
915
|
-
# @param abortable [true, false]
|
|
916
|
-
# whether the user is allowed to abort with 'Esc' or 'Ctrl+c'
|
|
917
|
-
# @param selected [#to_s, nil]
|
|
918
|
-
# optionally pre-selected option
|
|
919
|
-
#
|
|
920
|
-
# @return [Object]
|
|
921
|
-
# key for selected choice
|
|
922
|
-
# @return [nil]
|
|
923
|
-
# when user aborted the selection
|
|
924
|
-
#
|
|
925
|
-
# @overload choice(**choices, abortable: false, &block)
|
|
926
|
-
# @example Request a preference
|
|
927
|
-
# ui.choice(
|
|
928
|
-
# k: 'Kitty',
|
|
929
|
-
# i: 'iTerm2',
|
|
930
|
-
# g: 'Ghostty',
|
|
931
|
-
# t: 'Tabby',
|
|
932
|
-
# r: 'Rio',
|
|
933
|
-
# abortable: true
|
|
934
|
-
# ) { ui.puts 'Which terminal emulator do you like?' }
|
|
935
|
-
# # => whether the user selected: :k, :i, :g, :t, :r
|
|
936
|
-
# # => nil, when the user aborted
|
|
937
|
-
# @param choices[#to_s]
|
|
938
|
-
# one or more alternatives to select from
|
|
939
|
-
# @param abortable[true, false]
|
|
940
|
-
# whether the user is allowed to abort with 'Esc' or 'Ctrl+c'
|
|
941
|
-
# @param selected[Integer]
|
|
942
|
-
# pre-selected option index
|
|
943
|
-
#
|
|
944
|
-
# @yieldparam temp [Temporary]
|
|
945
|
-
# temporary displayed section (section will be erased after input)
|
|
946
|
-
#
|
|
947
|
-
# @return [Object]
|
|
948
|
-
# key for selected choice
|
|
949
|
-
# @return [nil]
|
|
950
|
-
# when user aborted the selection
|
|
951
|
-
#
|
|
952
|
-
def choice(*choices, abortable: false, selected: nil, **kwchoices, &block)
|
|
953
|
-
return if choices.empty? && kwchoices.empty?
|
|
954
|
-
choice =
|
|
955
|
-
if Terminal.ansi?
|
|
956
|
-
Choice.new(self, choices, kwchoices, abortable, selected)
|
|
957
|
-
else
|
|
958
|
-
DumbChoice.new(self, choices, kwchoices, abortable)
|
|
959
|
-
end
|
|
960
|
-
__with(choice) { choice.select(&block) }
|
|
961
|
-
end
|
|
653
|
+
#
|
|
654
|
+
# @!endgroup
|
|
655
|
+
#
|
|
656
|
+
# @!group User Interaction
|
|
657
|
+
#
|
|
962
658
|
|
|
963
|
-
#
|
|
964
|
-
# All options are returned with their selection status.
|
|
659
|
+
# Waits for the user to press any key.
|
|
965
660
|
#
|
|
966
|
-
# @
|
|
967
|
-
#
|
|
968
|
-
#
|
|
969
|
-
# whether the user is allowed to abort with 'Esc' or 'Ctrl+c'
|
|
970
|
-
# @param selected [#to_s, nil]
|
|
971
|
-
# optionally pre-selected key
|
|
661
|
+
# @example Plain wait
|
|
662
|
+
# ui.puts 'Press any key to continue…'
|
|
663
|
+
# ui.await
|
|
972
664
|
#
|
|
973
|
-
# @
|
|
974
|
-
#
|
|
665
|
+
# @example With temporary prompt
|
|
666
|
+
# ui.await { ui.puts '[faint]Press any key to continue…' }
|
|
975
667
|
#
|
|
976
|
-
# @
|
|
977
|
-
#
|
|
668
|
+
# @yield (see #temporary)
|
|
669
|
+
# @yieldparam (see #temporary)
|
|
978
670
|
# @return [nil]
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
else
|
|
986
|
-
DumbOptions.new(self, choices, abortable, selected)
|
|
987
|
-
end
|
|
988
|
-
__with(options) { options.select(&block) }
|
|
671
|
+
def await
|
|
672
|
+
yield(temp = temporary) if block_given?
|
|
673
|
+
Terminal.read_key_event
|
|
674
|
+
nil
|
|
675
|
+
ensure
|
|
676
|
+
temp&.end
|
|
989
677
|
end
|
|
990
678
|
|
|
991
|
-
#
|
|
992
|
-
# The selected options are returned.
|
|
679
|
+
# Waits for a key event and returns information about it.
|
|
993
680
|
#
|
|
994
|
-
#
|
|
995
|
-
#
|
|
996
|
-
# ui.puts '[i]Which terminal applications did you already tested?[/i]'
|
|
997
|
-
# end
|
|
681
|
+
# Key names are strings such as `"a"`, `"Enter"`, `"Esc"`, `"Back"`,
|
|
682
|
+
# `"Shift+Alt+F1"`.
|
|
998
683
|
#
|
|
999
|
-
# @
|
|
1000
|
-
#
|
|
1001
|
-
#
|
|
1002
|
-
# whether the user is allowed to abort with 'Esc' or 'Ctrl+c'
|
|
1003
|
-
# @param selected [Integer, :all, nil]
|
|
1004
|
-
# optionally pre-selected option index or `:all` to pre-select all items
|
|
1005
|
-
# @yieldparam temp [Temporary]
|
|
1006
|
-
# 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.'
|
|
1007
687
|
#
|
|
1008
|
-
# @
|
|
1009
|
-
#
|
|
1010
|
-
#
|
|
1011
|
-
#
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
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
|
|
1019
709
|
end
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
selected: selected,
|
|
1023
|
-
**choices.to_h { [_1, sel] },
|
|
1024
|
-
&block
|
|
1025
|
-
).filter_map { |key, selected| key if selected }
|
|
710
|
+
ensure
|
|
711
|
+
temp&.end
|
|
1026
712
|
end
|
|
1027
713
|
|
|
714
|
+
# Presents a list of options and returns the one the user selects.
|
|
1028
715
|
#
|
|
1029
|
-
#
|
|
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.
|
|
1030
718
|
#
|
|
1031
|
-
|
|
719
|
+
# @example Positional items
|
|
720
|
+
# answer = ui.choice 'Yes', 'No', 'Cancel'
|
|
1032
721
|
#
|
|
1033
|
-
#
|
|
722
|
+
# @example Positional items
|
|
723
|
+
# answer = ui.choice 'Yes', 'No', abortable: true do
|
|
724
|
+
# ui.puts 'Overwrite the file?'
|
|
725
|
+
# end
|
|
1034
726
|
#
|
|
727
|
+
# @example Keyword pairs
|
|
728
|
+
# action = ui.choice(overwrite: 'Overwrite', skip: 'Skip') do
|
|
729
|
+
# ui.puts 'File already exists.'
|
|
730
|
+
# end
|
|
731
|
+
#
|
|
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
|
|
759
|
+
end
|
|
1035
760
|
|
|
1036
|
-
#
|
|
1037
|
-
def columns = Terminal.columns
|
|
1038
|
-
|
|
1039
|
-
# Display some temporary content.
|
|
1040
|
-
# The content displayed in the block will be erased after the block ends.
|
|
761
|
+
# Presents a list of options and returns all items the user selects.
|
|
1041
762
|
#
|
|
1042
|
-
#
|
|
1043
|
-
#
|
|
1044
|
-
# This is a [i]temporary[/i] displayed text.
|
|
1045
|
-
# It will disappear when you press [b]ENTER[/b].
|
|
1046
|
-
# MSG
|
|
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.
|
|
1047
765
|
#
|
|
1048
|
-
# @example
|
|
1049
|
-
# ui.
|
|
1050
|
-
#
|
|
1051
|
-
#
|
|
1052
|
-
#
|
|
1053
|
-
#
|
|
1054
|
-
# ui.await
|
|
766
|
+
# @example Positional items
|
|
767
|
+
# picks = ui.select 'Kitty', 'iTerm2', 'Ghostty'
|
|
768
|
+
#
|
|
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:'
|
|
1055
772
|
# end
|
|
1056
773
|
#
|
|
1057
|
-
# @
|
|
1058
|
-
#
|
|
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
|
|
815
|
+
|
|
816
|
+
#
|
|
817
|
+
# @!endgroup
|
|
1059
818
|
#
|
|
1060
|
-
#
|
|
1061
|
-
|
|
1062
|
-
|
|
819
|
+
# @!group Utilities
|
|
820
|
+
#
|
|
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
|
+
)
|
|
1063
872
|
end
|
|
1064
873
|
|
|
1065
874
|
#
|
|
1066
875
|
# @!endgroup
|
|
1067
876
|
#
|
|
1068
877
|
|
|
1069
|
-
private
|
|
878
|
+
# @private
|
|
879
|
+
def columns = Terminal.columns
|
|
1070
880
|
|
|
1071
|
-
|
|
881
|
+
private
|
|
1072
882
|
|
|
1073
|
-
def
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
__with(Section.new(self, color, mark, title), *text, **options, &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
|
|
1080
889
|
end
|
|
1081
890
|
|
|
1082
|
-
def
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
return true
|
|
1090
|
-
end
|
|
1091
|
-
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)
|
|
1092
898
|
end
|
|
1093
899
|
end
|
|
1094
900
|
end
|
|
1095
901
|
|
|
1096
902
|
dir = __dir__
|
|
1097
903
|
|
|
1098
|
-
|
|
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"
|
|
1099
909
|
autoload :Section, "#{dir}/section.rb"
|
|
1100
|
-
autoload :Table, "#{dir}/table.rb"
|
|
1101
910
|
autoload :Task, "#{dir}/task.rb"
|
|
1102
911
|
autoload :Temporary, "#{dir}/temporary.rb"
|
|
1103
|
-
autoload :Theme, "#{dir}/theme.rb"
|
|
1104
|
-
autoload :Utils, "#{dir}/utils.rb"
|
|
1105
|
-
private_constant(:Framed, :Utils)
|
|
1106
912
|
|
|
1107
|
-
|
|
1108
|
-
autoload :
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
autoload :
|
|
1112
|
-
autoload :
|
|
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"
|
|
1113
938
|
private_constant(
|
|
939
|
+
:VBars,
|
|
940
|
+
:HBars,
|
|
1114
941
|
:Choice,
|
|
1115
942
|
:DumbChoice,
|
|
1116
|
-
:
|
|
1117
|
-
:
|
|
1118
|
-
:
|
|
1119
|
-
:
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
:CompactLSRenderer,
|
|
1129
|
-
:HBarsRenderer,
|
|
1130
|
-
:LSRenderer,
|
|
1131
|
-
:ShellRenderer,
|
|
1132
|
-
: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
|
|
1133
955
|
)
|
|
1134
956
|
end
|