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.
- checksums.yaml +4 -4
- data/.yardopts +0 -1
- 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 -5
- 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 +27 -17
- 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 +64 -65
- data/lib/natty-ui/features.rb +773 -872
- 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 -149
- 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 +142 -41
- data/lib/natty-ui/task.rb +39 -27
- 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 +87 -30
- 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 -51
- 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,1055 +1,956 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module NattyUI
|
|
4
|
-
#
|
|
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
|
|
13
|
-
#
|
|
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
|
-
|
|
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
|
|
45
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
122
|
-
max_width = (lines = lines.to_a).max_by(&:last)[-1]
|
|
99
|
+
popts[:width] = max_width
|
|
123
100
|
end
|
|
124
101
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
#
|
|
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
|
|
191
|
-
# ui.
|
|
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
|
-
# @
|
|
201
|
-
#
|
|
202
|
-
# @option (see #puts)
|
|
122
|
+
# @example Print three blank lines
|
|
123
|
+
# ui.space 3
|
|
203
124
|
#
|
|
204
|
-
# @
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
#
|
|
131
|
+
# Prints text with a leading mark symbol.
|
|
253
132
|
#
|
|
254
|
-
# @
|
|
133
|
+
# @example Default mark
|
|
134
|
+
# ui.mark 'Item one'
|
|
255
135
|
#
|
|
256
|
-
# @
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
# Print given text as a H2 {#heading}.
|
|
136
|
+
# @example Named symbol mark
|
|
137
|
+
# ui.mark 'Done!', mark: :checkmark
|
|
260
138
|
#
|
|
261
|
-
# @
|
|
139
|
+
# @example Custom string mark
|
|
140
|
+
# ui.mark 'Warning', mark: '[bright_red]!'
|
|
262
141
|
#
|
|
263
|
-
# @
|
|
264
|
-
|
|
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
|
-
#
|
|
154
|
+
# Prints text with a green check-mark prefix.
|
|
155
|
+
#
|
|
156
|
+
# Shorthand for `mark(*text, mark: :checkmark, **options)`.
|
|
267
157
|
#
|
|
268
|
-
# @
|
|
158
|
+
# @example
|
|
159
|
+
# ui.ok 'All tests passed'
|
|
269
160
|
#
|
|
270
|
-
# @
|
|
271
|
-
|
|
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
|
-
#
|
|
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
|
-
# @
|
|
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
|
-
# @
|
|
278
|
-
|
|
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
|
-
#
|
|
183
|
+
# Prints text with a left-side quotation border.
|
|
281
184
|
#
|
|
282
|
-
# @
|
|
185
|
+
# @example
|
|
186
|
+
# ui.quote "To be or not to be,\nthat is the question."
|
|
283
187
|
#
|
|
284
|
-
# @
|
|
285
|
-
|
|
188
|
+
# @param (see #puts)
|
|
189
|
+
# @return (see #puts)
|
|
190
|
+
def quote(*text) = Quote.render(self, *text)
|
|
286
191
|
|
|
287
|
-
#
|
|
192
|
+
# Prints a heading at the given level.
|
|
288
193
|
#
|
|
289
|
-
#
|
|
194
|
+
# Six heading levels are supported (similarly to HTML `<h1>`–`<h6>`).
|
|
195
|
+
# Use the shorthand helpers {#h1}–{#h6} for convenience.
|
|
290
196
|
#
|
|
291
|
-
# @
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
# Print a horizontal rule.
|
|
197
|
+
# @example Large heading
|
|
198
|
+
# ui.heading 1, 'Chapter One'
|
|
295
199
|
#
|
|
296
|
-
# @example
|
|
297
|
-
# ui.
|
|
200
|
+
# @example Sub-heading
|
|
201
|
+
# ui.heading 3, 'Section [i]Overview[/i]'
|
|
298
202
|
#
|
|
299
|
-
# @param
|
|
300
|
-
#
|
|
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
|
-
# @
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
#
|
|
214
|
+
# Prints a level-2 heading.
|
|
310
215
|
#
|
|
311
|
-
# @param
|
|
312
|
-
#
|
|
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
|
-
# @
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
end
|
|
222
|
+
# @param title (see #heading)
|
|
223
|
+
# @return (see #puts)
|
|
224
|
+
def h3(title) = heading(3, title)
|
|
318
225
|
|
|
319
|
-
#
|
|
226
|
+
# Prints a level-4 heading.
|
|
320
227
|
#
|
|
321
|
-
#
|
|
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
|
-
#
|
|
324
|
-
#
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
# @
|
|
329
|
-
#
|
|
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
|
|
332
|
-
# ui.
|
|
246
|
+
# @example Default rule
|
|
247
|
+
# ui.hr
|
|
333
248
|
#
|
|
334
|
-
# @
|
|
335
|
-
#
|
|
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
|
-
# @
|
|
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
|
-
|
|
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
|
-
#
|
|
413
|
-
#
|
|
414
|
-
#
|
|
415
|
-
#
|
|
416
|
-
# @
|
|
417
|
-
#
|
|
418
|
-
#
|
|
419
|
-
#
|
|
420
|
-
#
|
|
421
|
-
#
|
|
422
|
-
#
|
|
423
|
-
#
|
|
424
|
-
# @
|
|
425
|
-
#
|
|
426
|
-
#
|
|
427
|
-
#
|
|
428
|
-
#
|
|
429
|
-
#
|
|
430
|
-
#
|
|
431
|
-
#
|
|
432
|
-
#
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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:
|
|
323
|
+
bar_width: 3,
|
|
462
324
|
style: nil
|
|
463
325
|
)
|
|
464
|
-
|
|
465
|
-
if
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
#
|
|
481
|
-
#
|
|
482
|
-
# @example Draw green bars
|
|
483
|
-
# ui.hbars 1..10, style: :green
|
|
332
|
+
# Prints a horizontal bar chart.
|
|
484
333
|
#
|
|
485
|
-
#
|
|
486
|
-
# ui.hbars 1..10, style: :blue, width: 0.5
|
|
334
|
+
# All values must be non-negative.
|
|
487
335
|
#
|
|
488
|
-
# @
|
|
489
|
-
#
|
|
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
|
-
# @
|
|
339
|
+
# @example Chart without value labels
|
|
340
|
+
# ui.hbars [10, 40, 25, 60], text: false, normalize: true
|
|
498
341
|
#
|
|
499
|
-
# @
|
|
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
|
-
|
|
511
|
-
if
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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
|
-
#
|
|
521
|
-
#
|
|
522
|
-
#
|
|
523
|
-
#
|
|
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
|
-
#
|
|
538
|
-
#
|
|
539
|
-
#
|
|
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
|
-
#
|
|
544
|
-
#
|
|
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
|
-
#
|
|
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
|
-
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
|
-
#
|
|
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
|
-
# @!
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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.
|
|
656
|
-
#
|
|
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
|
-
# @
|
|
669
|
-
#
|
|
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
|
-
# @
|
|
672
|
-
#
|
|
495
|
+
# @example Block form without title
|
|
496
|
+
# ui.section do
|
|
497
|
+
# ui.puts 'Anonymous section content.'
|
|
498
|
+
# end
|
|
673
499
|
#
|
|
674
|
-
# @
|
|
675
|
-
#
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
#
|
|
679
|
-
#
|
|
680
|
-
#
|
|
681
|
-
#
|
|
682
|
-
#
|
|
683
|
-
#
|
|
684
|
-
|
|
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
|
-
#
|
|
687
|
-
# elements.
|
|
515
|
+
# Creates a {Section} with `:message` styling.
|
|
688
516
|
#
|
|
689
|
-
# @
|
|
690
|
-
|
|
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
|
-
#
|
|
694
|
-
# for the output of text elements.
|
|
527
|
+
# Creates a {Section} with `:information` styling.
|
|
695
528
|
#
|
|
696
|
-
# @
|
|
697
|
-
|
|
698
|
-
|
|
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
|
-
#
|
|
703
|
-
# the output of text elements.
|
|
538
|
+
# Creates a {Section} with `:warning` styling.
|
|
704
539
|
#
|
|
705
|
-
# @
|
|
706
|
-
|
|
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
|
-
#
|
|
710
|
-
# the output of text elements.
|
|
549
|
+
# Creates a {Section} with `:error` styling.
|
|
711
550
|
#
|
|
712
|
-
# @
|
|
713
|
-
|
|
714
|
-
|
|
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
|
-
#
|
|
717
|
-
# the output of text elements.
|
|
559
|
+
# Creates a {Section} with `:fatal` styling.
|
|
718
560
|
#
|
|
719
|
-
# @
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
#
|
|
723
|
-
|
|
724
|
-
|
|
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
|
-
#
|
|
569
|
+
# Creates a {Frame} element — a bordered box with an optional title.
|
|
752
570
|
#
|
|
753
|
-
# @
|
|
754
|
-
#
|
|
755
|
-
#
|
|
756
|
-
#
|
|
571
|
+
# @example Manual close
|
|
572
|
+
# frm = ui.frame 'Preview'
|
|
573
|
+
# ui.puts 'Content inside the frame.'
|
|
574
|
+
# frm.end
|
|
757
575
|
#
|
|
758
|
-
# @
|
|
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
|
-
# @
|
|
761
|
-
|
|
762
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
776
|
-
# ui.await { ui.puts '[faint][\\Press ENTER to continue...][/faint]' }
|
|
654
|
+
# @!endgroup
|
|
777
655
|
#
|
|
778
|
-
#
|
|
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
|
-
|
|
658
|
+
|
|
659
|
+
# Waits for the user to press any key.
|
|
789
660
|
#
|
|
790
|
-
# @
|
|
791
|
-
#
|
|
792
|
-
#
|
|
661
|
+
# @example Plain wait
|
|
662
|
+
# ui.puts 'Press any key to continue…'
|
|
663
|
+
# ui.await
|
|
793
664
|
#
|
|
794
|
-
# @
|
|
795
|
-
#
|
|
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
|
-
# @
|
|
800
|
-
#
|
|
668
|
+
# @yield (see #temporary)
|
|
669
|
+
# @yieldparam (see #temporary)
|
|
801
670
|
# @return [nil]
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
900
|
-
#
|
|
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
|
-
# @
|
|
907
|
-
#
|
|
684
|
+
# @example
|
|
685
|
+
# answer = ui.query yes: 'y', no: 'n'
|
|
686
|
+
# ui.puts answer == :yes ? 'Confirmed!' : 'Cancelled.'
|
|
908
687
|
#
|
|
909
|
-
# @
|
|
910
|
-
#
|
|
911
|
-
#
|
|
912
|
-
#
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
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
|
-
#
|
|
925
|
-
# The selected options are returned.
|
|
714
|
+
# Presents a list of options and returns the one the user selects.
|
|
926
715
|
#
|
|
927
|
-
#
|
|
928
|
-
#
|
|
929
|
-
#
|
|
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
|
-
# @
|
|
933
|
-
#
|
|
934
|
-
#
|
|
935
|
-
#
|
|
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
|
-
# @
|
|
942
|
-
# selected
|
|
943
|
-
#
|
|
944
|
-
#
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
982
|
-
# itself
|
|
819
|
+
# @!group Utilities
|
|
983
820
|
#
|
|
984
|
-
|
|
985
|
-
|
|
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
|
-
|
|
996
|
-
__with(Section.new(self, title, text, color), &block)
|
|
997
|
-
end
|
|
881
|
+
private
|
|
998
882
|
|
|
999
|
-
def
|
|
1000
|
-
|
|
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
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
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__
|
|
902
|
+
dir = __dir__
|
|
1018
903
|
|
|
1019
|
-
|
|
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
|
-
|
|
1029
|
-
autoload :
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
autoload :
|
|
1033
|
-
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"
|
|
1034
938
|
private_constant(
|
|
939
|
+
:VBars,
|
|
940
|
+
:HBars,
|
|
1035
941
|
:Choice,
|
|
1036
942
|
:DumbChoice,
|
|
1037
|
-
:
|
|
1038
|
-
:
|
|
1039
|
-
:
|
|
1040
|
-
:
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
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
|