natty-ui 0.35.0 → 1.0.3
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 -944
- 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 +446 -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 +61 -0
- data/lib/natty-ui/utils/utils.rb +47 -0
- data/lib/natty-ui/version.rb +1 -1
- data/lib/natty-ui.rb +86 -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,961 @@
|
|
|
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 prefix.is_a?(StrConst)
|
|
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 cprefix.is_a?(StrConst)
|
|
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 suffix.is_a?(StrConst)
|
|
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 csuffix.is_a?(StrConst)
|
|
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
|
-
|
|
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
|
+
|
|
105
|
+
if cprefix || csuffix
|
|
106
|
+
lines.map! do |line|
|
|
107
|
+
line = "#{cprefix}#{line}#{csuffix}"
|
|
108
|
+
cprefix, prefix = prefix, nil if prefix
|
|
109
|
+
csuffix, suffix = suffix, nil if suffix
|
|
110
|
+
line
|
|
167
111
|
end
|
|
168
112
|
end
|
|
113
|
+
Terminal.puts(*lines, bbcode: false)
|
|
114
|
+
@lines_written += lines.size
|
|
169
115
|
self
|
|
170
116
|
end
|
|
171
117
|
|
|
172
|
-
#
|
|
173
|
-
#
|
|
174
|
-
# @
|
|
175
|
-
#
|
|
176
|
-
#
|
|
177
|
-
#
|
|
178
|
-
#
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
puts(*
|
|
118
|
+
# Prints one or more blank lines.
|
|
119
|
+
#
|
|
120
|
+
# @example Print a single blank line
|
|
121
|
+
# ui.space
|
|
122
|
+
#
|
|
123
|
+
# @example Print three blank lines
|
|
124
|
+
# ui.space 3
|
|
125
|
+
#
|
|
126
|
+
# @param count [#to_int] number of blank lines to print
|
|
127
|
+
# @return (see #puts)
|
|
128
|
+
def space(count = 1)
|
|
129
|
+
(count = count.to_int) < 1 ? self : puts(" \n" * count)
|
|
184
130
|
end
|
|
185
131
|
|
|
186
|
-
#
|
|
187
|
-
# output like {#task} the text will be kept ("pinned").
|
|
132
|
+
# Prints text with a leading mark symbol.
|
|
188
133
|
#
|
|
189
|
-
#
|
|
134
|
+
# @example Default mark
|
|
135
|
+
# ui.mark 'Item one'
|
|
190
136
|
#
|
|
191
|
-
# @example
|
|
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.
|
|
137
|
+
# @example Named symbol mark
|
|
138
|
+
# ui.mark 'Done!', mark: :checkmark
|
|
200
139
|
#
|
|
201
|
-
# @
|
|
202
|
-
#
|
|
203
|
-
# @option (see #puts)
|
|
140
|
+
# @example Custom string mark
|
|
141
|
+
# ui.mark 'Warning', mark: '[bright_red]!'
|
|
204
142
|
#
|
|
205
|
-
# @
|
|
206
|
-
|
|
207
|
-
|
|
143
|
+
# @param text (see #puts)
|
|
144
|
+
# @param mark [Symbol, #to_s] mark to use as prefix:
|
|
145
|
+
# - `:default` — blue bullet •
|
|
146
|
+
# - `:checkmark` — green check mark ✓
|
|
147
|
+
# - `:item` — dim bullet •
|
|
148
|
+
# - any other object — converted via `#to_s` and used as the literal prefix
|
|
149
|
+
# @param popts [Hash] a customizable set of print options, see {#puts}
|
|
150
|
+
# @return (see #puts)
|
|
151
|
+
def mark(*text, mark: :default, **popts)
|
|
152
|
+
Mark.render(self, mark, *text, **popts)
|
|
208
153
|
end
|
|
209
154
|
|
|
210
|
-
#
|
|
155
|
+
# Prints text with a green check-mark prefix.
|
|
211
156
|
#
|
|
212
|
-
#
|
|
157
|
+
# Shorthand for `mark(*text, mark: :checkmark, **options)`.
|
|
213
158
|
#
|
|
214
|
-
# @
|
|
215
|
-
|
|
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.
|
|
159
|
+
# @example
|
|
160
|
+
# ui.ok 'All tests passed'
|
|
227
161
|
#
|
|
228
|
-
#
|
|
229
|
-
#
|
|
162
|
+
# @param text (see #puts)
|
|
163
|
+
# @param popts (see #mark)
|
|
164
|
+
# @return (see #puts)
|
|
165
|
+
def ok(*text, **popts) = mark(*text, mark: :checkmark, **popts)
|
|
166
|
+
|
|
167
|
+
# Prints text with a mark that persists after a {Temporary} element closes.
|
|
230
168
|
#
|
|
231
|
-
#
|
|
232
|
-
#
|
|
233
|
-
# # => ╴╶╴╶─═══ This is a H1 heading element ═══─╴╶╴╶
|
|
169
|
+
# Identical to {#mark} but survives when the surrounding {Temporary} element
|
|
170
|
+
# (see {#temporary}, {#task}, {#progress}) is erased.
|
|
234
171
|
#
|
|
235
|
-
# @
|
|
236
|
-
#
|
|
237
|
-
#
|
|
172
|
+
# @example Pin a success note inside a task
|
|
173
|
+
# ui.task 'Deploy' do
|
|
174
|
+
# do_deploy
|
|
175
|
+
# ui.pin 'Deployed to production', mark: :checkmark
|
|
176
|
+
# end
|
|
238
177
|
#
|
|
239
|
-
# @
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
)
|
|
178
|
+
# @param (see mark)
|
|
179
|
+
# @return (see #puts)
|
|
180
|
+
def pin(*text, mark: :default, **popts)
|
|
181
|
+
mark(*text, mark:, pin: true, **popts)
|
|
251
182
|
end
|
|
252
183
|
|
|
253
|
-
#
|
|
184
|
+
# Prints text with a left-side quotation border.
|
|
254
185
|
#
|
|
255
|
-
# @
|
|
186
|
+
# @example
|
|
187
|
+
# ui.quote "To be or not to be,\nthat is the question."
|
|
256
188
|
#
|
|
257
|
-
# @
|
|
258
|
-
|
|
189
|
+
# @param (see #puts)
|
|
190
|
+
# @return (see #puts)
|
|
191
|
+
def quote(*text) = Quote.render(self, *text)
|
|
259
192
|
|
|
260
|
-
#
|
|
193
|
+
# Prints a heading at the given level.
|
|
261
194
|
#
|
|
262
|
-
#
|
|
195
|
+
# Six heading levels are supported (similarly to HTML `<h1>`–`<h6>`).
|
|
196
|
+
# Use the shorthand helpers {#h1}–{#h6} for convenience.
|
|
263
197
|
#
|
|
264
|
-
# @
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
# Print given text as a H3 {#heading}.
|
|
198
|
+
# @example Large heading
|
|
199
|
+
# ui.heading 1, 'Chapter One'
|
|
268
200
|
#
|
|
269
|
-
# @
|
|
201
|
+
# @example Sub-heading
|
|
202
|
+
# ui.heading 3, 'Section [i]Overview[/i]'
|
|
270
203
|
#
|
|
271
|
-
# @
|
|
272
|
-
|
|
204
|
+
# @param level [#to_int] heading level, `1` (largest) to `6` (smallest)
|
|
205
|
+
# @param title [#to_s] heading text
|
|
206
|
+
# @return (see #puts)
|
|
207
|
+
def heading(level, title) = Heading.render(self, level, title)
|
|
273
208
|
|
|
274
|
-
#
|
|
209
|
+
# Prints a level-1 heading.
|
|
275
210
|
#
|
|
276
|
-
# @param
|
|
277
|
-
#
|
|
278
|
-
|
|
279
|
-
def h4(*text) = heading(4, *text)
|
|
211
|
+
# @param title (see #heading)
|
|
212
|
+
# @return (see #puts)
|
|
213
|
+
def h1(title) = heading(1, title)
|
|
280
214
|
|
|
281
|
-
#
|
|
282
|
-
#
|
|
283
|
-
# @param text (see puts)
|
|
215
|
+
# Prints a level-2 heading.
|
|
284
216
|
#
|
|
285
|
-
# @
|
|
286
|
-
|
|
217
|
+
# @param title (see #heading)
|
|
218
|
+
# @return (see #puts)
|
|
219
|
+
def h2(title) = heading(2, title)
|
|
287
220
|
|
|
288
|
-
#
|
|
289
|
-
#
|
|
290
|
-
# @param text (see puts)
|
|
221
|
+
# Prints a level-3 heading.
|
|
291
222
|
#
|
|
292
|
-
# @
|
|
293
|
-
|
|
223
|
+
# @param title (see #heading)
|
|
224
|
+
# @return (see #puts)
|
|
225
|
+
def h3(title) = heading(3, title)
|
|
294
226
|
|
|
295
|
-
#
|
|
296
|
-
#
|
|
297
|
-
# @example Print double line
|
|
298
|
-
# ui.hr :double
|
|
299
|
-
#
|
|
300
|
-
# @param type [Symbol]
|
|
301
|
-
# border type
|
|
227
|
+
# Prints a level-4 heading.
|
|
302
228
|
#
|
|
303
|
-
# @
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
bc = theme.border(type)[10]
|
|
307
|
-
puts("#{theme.heading_sytle}#{bc * columns}")
|
|
308
|
-
end
|
|
229
|
+
# @param title (see #heading)
|
|
230
|
+
# @return (see #puts)
|
|
231
|
+
def h4(title) = heading(4, title)
|
|
309
232
|
|
|
310
|
-
#
|
|
311
|
-
#
|
|
312
|
-
# @param count [#to_i]
|
|
313
|
-
# lines to print
|
|
233
|
+
# Prints a level-5 heading.
|
|
314
234
|
#
|
|
315
|
-
# @
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
end
|
|
235
|
+
# @param title (see #heading)
|
|
236
|
+
# @return (see #puts)
|
|
237
|
+
def h5(title) = heading(5, title)
|
|
319
238
|
|
|
320
|
-
#
|
|
239
|
+
# Prints a level-6 heading.
|
|
321
240
|
#
|
|
322
|
-
#
|
|
323
|
-
#
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
#
|
|
327
|
-
# - any text as prefix
|
|
328
|
-
#
|
|
329
|
-
# @example Print all Ruby files as a numbered list
|
|
330
|
-
# ui.ls Dir['*/**/*.rb'], glyph: 1
|
|
241
|
+
# @param title (see #heading)
|
|
242
|
+
# @return (see #puts)
|
|
243
|
+
def h6(title) = heading(6, title)
|
|
244
|
+
|
|
245
|
+
# Prints a horizontal rule spanning the available width.
|
|
331
246
|
#
|
|
332
|
-
# @example
|
|
333
|
-
# ui.
|
|
247
|
+
# @example Default rule
|
|
248
|
+
# ui.hr
|
|
334
249
|
#
|
|
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
|
|
250
|
+
# @example Named style
|
|
251
|
+
# ui.hr :double
|
|
341
252
|
#
|
|
342
|
-
# @
|
|
253
|
+
# @example Repeated string
|
|
254
|
+
# ui.hr '+-'
|
|
255
|
+
#
|
|
256
|
+
# @param kind [Symbol, #to_s, nil]
|
|
257
|
+
# - `nil` — default style
|
|
258
|
+
# - `Symbol` — named style: `:single`, `:double`, `:heavy`
|
|
259
|
+
# - `#to_s` — string repeated to fill the available width
|
|
260
|
+
# @return (see #puts)
|
|
261
|
+
def hr(kind = nil) = HorizontalRule.render(self, kind)
|
|
262
|
+
|
|
263
|
+
# Prints a multi-column list.
|
|
264
|
+
#
|
|
265
|
+
# Items are arranged in columns to fit the terminal width.
|
|
266
|
+
#
|
|
267
|
+
# @example Plain list
|
|
268
|
+
# ui.ls 'Alice', 'Bob', 'Carol'
|
|
269
|
+
#
|
|
270
|
+
# @example Hex-numbered list
|
|
271
|
+
# ui.ls 'red', 'green', 'blue', glyph: :hex
|
|
272
|
+
#
|
|
273
|
+
# @param items [#to_s, ...] items to list; nested arrays are flattened
|
|
274
|
+
# @param compact [Boolean] `true` = ordered in columns,
|
|
275
|
+
# `false` = ordered left to right per row
|
|
276
|
+
# @param glyph prefix applied to every item:
|
|
277
|
+
# - `nil` / `false` — no prefix
|
|
278
|
+
# - `Integer` — decimal counter starting at the given number
|
|
279
|
+
# - `Symbol` — alphabetic sequence starting at the given symbol
|
|
280
|
+
# (e.g. `:a` → `a`, `b`, `c`, …)
|
|
281
|
+
# - `:hex` — zero-based hex counter (`01`, `02`, …)
|
|
282
|
+
# - a `#to_s` value matching a hex value — hex counter at the given
|
|
283
|
+
# offset (e.g. `"0x0a"` starts at `0a`)
|
|
284
|
+
# - any other `#to_s` value — used as a literal prefix for every item
|
|
285
|
+
# @return (see #puts)
|
|
343
286
|
def ls(*items, compact: true, glyph: nil)
|
|
344
287
|
return self if items.empty?
|
|
345
|
-
|
|
346
|
-
puts(*renderer.lines(items, glyph, columns))
|
|
288
|
+
puts(*(compact ? CompactLS : LS).lines(columns, items, glyph))
|
|
347
289
|
end
|
|
348
290
|
|
|
349
|
-
#
|
|
350
|
-
#
|
|
351
|
-
#
|
|
352
|
-
#
|
|
353
|
-
#
|
|
354
|
-
#
|
|
355
|
-
#
|
|
356
|
-
#
|
|
357
|
-
#
|
|
358
|
-
#
|
|
359
|
-
# @param
|
|
360
|
-
#
|
|
361
|
-
# @
|
|
362
|
-
#
|
|
363
|
-
#
|
|
364
|
-
#
|
|
365
|
-
#
|
|
366
|
-
#
|
|
367
|
-
#
|
|
368
|
-
#
|
|
369
|
-
#
|
|
370
|
-
# @
|
|
371
|
-
#
|
|
372
|
-
#
|
|
373
|
-
#
|
|
374
|
-
#
|
|
375
|
-
#
|
|
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) } }
|
|
439
|
-
end
|
|
440
|
-
|
|
441
|
-
# Dump given values as vertical bars.
|
|
442
|
-
#
|
|
443
|
-
# @example Draw green bars
|
|
444
|
-
# ui.vbars 1..10, style: :green
|
|
445
|
-
#
|
|
446
|
-
# @example Draw very big bars
|
|
447
|
-
# ui.vbars 1..10, bar_width: 5, height: 20
|
|
448
|
-
#
|
|
449
|
-
# @param values [#to_a, Array<Numeric>] values to print
|
|
450
|
-
# @param normalize [true, false] whether the values should be normalized
|
|
451
|
-
# @param height [Integer] output height
|
|
452
|
-
# @param bar_width [:auto, :min, Integer] with of each bar
|
|
453
|
-
# @param style [Symbol, Array<Symbol>, nil] drawing style
|
|
454
|
-
#
|
|
455
|
-
# @raise [ArgumentError] if any value is negative
|
|
456
|
-
#
|
|
457
|
-
# @return (see puts)
|
|
291
|
+
# Prints a vertical bar chart.
|
|
292
|
+
#
|
|
293
|
+
# All values must be non-negative.
|
|
294
|
+
#
|
|
295
|
+
# @example Simple bar chart
|
|
296
|
+
# ui.vbars [3, 7, 2, 9, 5]
|
|
297
|
+
#
|
|
298
|
+
# @example Normalised chart with custom height
|
|
299
|
+
# ui.vbars [10, 40, 25, 60], normalize: true, height: 15
|
|
300
|
+
#
|
|
301
|
+
# @param values [#each]
|
|
302
|
+
# collection of non-negative `Numeric` values
|
|
303
|
+
# @param min [Numeric, nil]
|
|
304
|
+
# lower bound for scaling; `nil` uses the data minimum
|
|
305
|
+
# @param max [Numeric, nil]
|
|
306
|
+
# upper bound for scaling; `nil` uses the data maximum
|
|
307
|
+
# @param normalize [Boolean]
|
|
308
|
+
# when `true` applies min-max normalization;
|
|
309
|
+
# when `false` scales each bar relative to the maximum value
|
|
310
|
+
# @param height [#to_int]
|
|
311
|
+
# chart height in lines (minimum 3; default: 10)
|
|
312
|
+
# @param bar_width [#to_int, :auto]
|
|
313
|
+
# width of each bar in characters; `:auto` fits the graph in available
|
|
314
|
+
# terminal width
|
|
315
|
+
# @param style [Symbol, String, Array<Style>, Array<String>, nil]
|
|
316
|
+
# ANSI style applied to the bars
|
|
317
|
+
# @return (see #puts)
|
|
458
318
|
def vbars(
|
|
459
319
|
values,
|
|
320
|
+
min: nil,
|
|
321
|
+
max: nil,
|
|
460
322
|
normalize: false,
|
|
461
323
|
height: 10,
|
|
462
|
-
bar_width:
|
|
324
|
+
bar_width: 3,
|
|
463
325
|
style: nil
|
|
464
326
|
)
|
|
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
|
-
)
|
|
327
|
+
bars = VBars.new(values, min, max, normalize)
|
|
328
|
+
return self if bars.empty?
|
|
329
|
+
raise(ArgumentError, 'values can not be negative') unless bars.valid?
|
|
330
|
+
puts(*bars.lines(columns, height, bar_width, style), bbcode: false)
|
|
479
331
|
end
|
|
480
332
|
|
|
481
|
-
#
|
|
333
|
+
# Prints a horizontal bar chart.
|
|
482
334
|
#
|
|
483
|
-
#
|
|
484
|
-
# ui.hbars 1..10, style: :green
|
|
335
|
+
# All values must be non-negative.
|
|
485
336
|
#
|
|
486
|
-
# @example
|
|
487
|
-
# ui.hbars
|
|
337
|
+
# @example Simple horizontal chart
|
|
338
|
+
# ui.hbars [3, 7, 2, 9, 5]
|
|
488
339
|
#
|
|
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
|
|
340
|
+
# @example Chart without value labels
|
|
341
|
+
# ui.hbars [10, 40, 25, 60], text: false, normalize: true
|
|
497
342
|
#
|
|
498
|
-
# @
|
|
499
|
-
#
|
|
500
|
-
# @
|
|
343
|
+
# @param values (see #vbars)
|
|
344
|
+
# @param min (see #vbars)
|
|
345
|
+
# @param max (see #vbars)
|
|
346
|
+
# @param normalize (see #vbars)
|
|
347
|
+
# @param style (see #vbars)
|
|
348
|
+
# @param width [#to_int, Float, :auto] maximum bar width in characters;
|
|
349
|
+
# a `Float` is a fraction of available width; `:auto` fills available width
|
|
350
|
+
# @param text [Boolean] when `true` prints the numeric value next to each bar
|
|
351
|
+
# @param text_style [Symbol, String, Array<Style>, Array<String>, nil]
|
|
352
|
+
# ANSI style applied to the value labels
|
|
353
|
+
# @return (see #puts)
|
|
501
354
|
def hbars(
|
|
502
355
|
values,
|
|
503
356
|
min: nil,
|
|
504
357
|
max: nil,
|
|
505
358
|
normalize: false,
|
|
506
|
-
text: true,
|
|
507
359
|
width: :auto,
|
|
508
360
|
style: nil,
|
|
361
|
+
text: true,
|
|
509
362
|
text_style: nil
|
|
510
363
|
)
|
|
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))
|
|
364
|
+
bars = HBars.new(values, min, max, normalize)
|
|
365
|
+
return self if bars.empty?
|
|
366
|
+
raise(ArgumentError, 'values can not be negative') unless bars.valid?
|
|
367
|
+
bars.with_text(text_style) if text
|
|
368
|
+
puts(*bars.lines(columns, width, style), bbcode: false)
|
|
519
369
|
end
|
|
520
370
|
|
|
521
|
-
#
|
|
522
|
-
#
|
|
523
|
-
#
|
|
524
|
-
#
|
|
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
|
|
371
|
+
# Renders a {Table} to the terminal.
|
|
372
|
+
#
|
|
373
|
+
# The method yields a {Table} object for population; if no block is given
|
|
374
|
+
# nothing is rendered.
|
|
537
375
|
#
|
|
538
|
-
#
|
|
539
|
-
#
|
|
540
|
-
#
|
|
541
|
-
# # simulate some work
|
|
542
|
-
# sleep 0.1
|
|
376
|
+
# Border name symbols:
|
|
377
|
+
# `:rounded` (default), `:single`, `:double`, `:heavy`,
|
|
378
|
+
# `:single_double`, `:double_single`, `:single_heavy`, `:heavy_single`
|
|
543
379
|
#
|
|
544
|
-
#
|
|
545
|
-
#
|
|
380
|
+
# @example Simple table with a double outer frame
|
|
381
|
+
# ui.table border_frame: :double do |t|
|
|
382
|
+
# t.add_row '[b]Name', '[b]Score', align: :center
|
|
383
|
+
# t.add_row 'Alice', 42
|
|
384
|
+
# t.add_row 'Bob', 17
|
|
546
385
|
# 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
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
Progress.new(self, title, max, pin)
|
|
577
|
-
else
|
|
578
|
-
DumbProgress.new(self, title, max)
|
|
579
|
-
end,
|
|
580
|
-
&block
|
|
386
|
+
#
|
|
387
|
+
# @option options [:default, :none, :single, :double, :heavy] :border (:default)
|
|
388
|
+
# default border used for all border parts
|
|
389
|
+
# @option options [nil, :none, :single, :double, :heavy] :border_frame
|
|
390
|
+
# outer frame; falls back to `:border`
|
|
391
|
+
# @option options [nil, :none, :single, :double, :heavy] :border_vertical
|
|
392
|
+
# vertical column separators; falls back to `:border`
|
|
393
|
+
# @option options [nil, :none, :single, :double, :heavy] :border_horizontal
|
|
394
|
+
# horizontal row separators; falls back to `:border`
|
|
395
|
+
# @option options [Symbol, String, Array<Style>, Array<String>, nil] :border_style
|
|
396
|
+
# ANSI style applied to all borders
|
|
397
|
+
# @option options [Symbol, Array<Symbol>] :frame (:all)
|
|
398
|
+
# which frame sides to draw: `:all` or an enumerable of
|
|
399
|
+
# `:top`, `:right`, `:bottom`, `:left`
|
|
400
|
+
# @option options [#to_int, Float, :max, nil] :width
|
|
401
|
+
# `:max` expands to the full terminal width;
|
|
402
|
+
# a negative `#to_int` is an offset from the terminal width;
|
|
403
|
+
# a `Float` is a fraction of the terminal width
|
|
404
|
+
# @yield [table] a {Table} instance to populate with rows and cells
|
|
405
|
+
# @yieldparam table [Table] the table
|
|
406
|
+
# @return [Features]
|
|
407
|
+
def table(**options)
|
|
408
|
+
return self unless block_given?
|
|
409
|
+
yield(table = Table.new)
|
|
410
|
+
return self if table.empty?
|
|
411
|
+
puts(
|
|
412
|
+
*TableRenderer.lines(columns, table, **options),
|
|
413
|
+
bbcode: false,
|
|
414
|
+
eol: :no_paragraph
|
|
581
415
|
)
|
|
582
416
|
end
|
|
583
417
|
|
|
584
|
-
# Execute a program.
|
|
585
418
|
#
|
|
586
|
-
#
|
|
587
|
-
# ui.sh 'ls'
|
|
419
|
+
# @!endgroup
|
|
588
420
|
#
|
|
589
|
-
|
|
590
|
-
# ret = ui.sh('curl', '--version')
|
|
591
|
-
# raise('Curl not found') unless ret&.success?
|
|
421
|
+
|
|
592
422
|
#
|
|
593
|
-
#
|
|
594
|
-
# ui.sh "mkdir 'test' && cd 'test'"
|
|
423
|
+
# @!group Section elements
|
|
595
424
|
#
|
|
596
|
-
|
|
597
|
-
#
|
|
425
|
+
|
|
426
|
+
# Creates a {Temporary} element whose output is erased when it closes.
|
|
598
427
|
#
|
|
599
|
-
# @
|
|
428
|
+
# @example Manual close
|
|
429
|
+
# tmp = ui.temporary
|
|
430
|
+
# tmp.puts 'Loading…'
|
|
431
|
+
# sleep 1
|
|
432
|
+
# tmp.end # erases "Loading…"
|
|
600
433
|
#
|
|
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
|
|
434
|
+
# @example Block form
|
|
435
|
+
# ui.temporary do
|
|
436
|
+
# ui.puts 'Thinking…'
|
|
437
|
+
# sleep 2
|
|
438
|
+
# end # "Thinking…" is erased here
|
|
439
|
+
#
|
|
440
|
+
# @yield [temp] the {Temporary} element
|
|
441
|
+
# @yieldparam temp [Temporary]
|
|
442
|
+
# @return [Object] return value of the block
|
|
443
|
+
# @return [Temporary] itself, if no block is specified
|
|
444
|
+
def temporary(&) = __with(Temporary.new(self), &)
|
|
644
445
|
|
|
446
|
+
# Creates a {Margin} element that adds horizontal and vertical whitespace.
|
|
645
447
|
#
|
|
646
|
-
#
|
|
448
|
+
# @example
|
|
449
|
+
# ui.margin 0, 0.25 do
|
|
450
|
+
# ui.puts 'This text has 25% width horizontal margin.'
|
|
451
|
+
# end
|
|
647
452
|
#
|
|
648
|
-
|
|
453
|
+
# @overload margin(value)
|
|
454
|
+
# Margin for all sides.
|
|
455
|
+
# @param value [#to_int, Float] margin of all four sides
|
|
456
|
+
#
|
|
457
|
+
# @overload margin(vertical = 0, horizontal = 1)
|
|
458
|
+
# Seperate vertical and horizontal margin.
|
|
459
|
+
# @param vertical [#to_int] top and bottom margin
|
|
460
|
+
# @param horizontal [#to_int, Float] left and right margin
|
|
461
|
+
#
|
|
462
|
+
# @overload margin(top, horizontal, bottom)
|
|
463
|
+
# Seperate top, bottom and horizontal margin.
|
|
464
|
+
# @param top [#to_int] top margin
|
|
465
|
+
# @param horizontal [#to_int, Float] left and right margin
|
|
466
|
+
# @param bottom [#to_int] bottom margin
|
|
467
|
+
#
|
|
468
|
+
# @overload margin(top, right, bottom, left)
|
|
469
|
+
# Seperate margins.
|
|
470
|
+
# @param top [#to_int] top margin
|
|
471
|
+
# @param right [#to_int, Float] right margin
|
|
472
|
+
# @param bottom [#to_int] bottom margin
|
|
473
|
+
# @param left [#to_int, Float] left margin
|
|
474
|
+
#
|
|
475
|
+
# @overload margin(top: 0, right: 0, bottom: 0, left: 0)
|
|
476
|
+
# Specific margin.
|
|
477
|
+
# @param top [#to_int] top margin
|
|
478
|
+
# @param right [#to_int, Float] right margin
|
|
479
|
+
# @param bottom [#to_int] bottom margin
|
|
480
|
+
# @param left [#to_int, Float] left margin
|
|
481
|
+
#
|
|
482
|
+
# @yield [margin] the {Margin} element
|
|
483
|
+
# @yieldparam margin [Margin]
|
|
484
|
+
# @return [Object] return value of the block
|
|
485
|
+
# @return [Margin] itself, if no block is specified
|
|
486
|
+
def margin(*, &) = __with(Margin.new(self, *), &)
|
|
487
|
+
|
|
488
|
+
# Creates a {Section} element — a bordered container with an optional title.
|
|
489
|
+
#
|
|
490
|
+
# @example Manual close
|
|
491
|
+
# sec = ui.section 'Results'
|
|
492
|
+
# sec.ok 'All good'
|
|
493
|
+
# sec.end
|
|
494
|
+
#
|
|
495
|
+
# @example Block form with title
|
|
496
|
+
# ui.section 'Summary' do
|
|
497
|
+
# ui.puts '3 files processed.'
|
|
498
|
+
# end
|
|
649
499
|
#
|
|
650
|
-
#
|
|
500
|
+
# @example Block form without title
|
|
501
|
+
# ui.section do
|
|
502
|
+
# ui.puts 'Anonymous section content.'
|
|
503
|
+
# end
|
|
651
504
|
#
|
|
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)
|
|
505
|
+
# @param title [#to_s, nil]
|
|
506
|
+
# optional header text
|
|
507
|
+
# @param type [:default, :message, :information, :warning, :error, :fatal]
|
|
508
|
+
# visual style of the section
|
|
509
|
+
# @param border [:default, :rounded, :single, :double, :heavy]
|
|
510
|
+
# border style
|
|
511
|
+
# @yield [section] the {Section} element
|
|
512
|
+
# @yieldparam section [Section]
|
|
513
|
+
# @return [Object] return value of the block
|
|
514
|
+
# @return [Section] itself, if no block is specified
|
|
515
|
+
def section(title = nil, type: :default, border: :default, &)
|
|
516
|
+
__with(Section.new(self, title, type, border), &)
|
|
696
517
|
end
|
|
697
518
|
alias begin section
|
|
698
519
|
|
|
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)
|
|
520
|
+
# Creates a {Section} with `:message` styling.
|
|
521
|
+
#
|
|
522
|
+
# @param title [#to_s] header text
|
|
523
|
+
# @param border (see #section)
|
|
524
|
+
# @yield (see #section)
|
|
525
|
+
# @yieldparam (see #section)
|
|
526
|
+
# @return (see #section)
|
|
527
|
+
def message(title, border: :default, &)
|
|
528
|
+
section(title, border:, type: :message, &)
|
|
725
529
|
end
|
|
726
530
|
alias msg message
|
|
727
531
|
|
|
728
|
-
#
|
|
729
|
-
# the output of text elements.
|
|
532
|
+
# Creates a {Section} with `:information` styling.
|
|
730
533
|
#
|
|
731
|
-
# @
|
|
732
|
-
|
|
733
|
-
|
|
534
|
+
# @param (see #message)
|
|
535
|
+
# @yield (see #message)
|
|
536
|
+
# @yieldparam (see #message)
|
|
537
|
+
# @return (see #message)
|
|
538
|
+
def information(title, border: :default, &)
|
|
539
|
+
section(title, border:, type: :information, &)
|
|
734
540
|
end
|
|
735
541
|
alias info information
|
|
736
542
|
|
|
737
|
-
#
|
|
738
|
-
# the output of text elements.
|
|
543
|
+
# Creates a {Section} with `:warning` styling.
|
|
739
544
|
#
|
|
740
|
-
# @
|
|
741
|
-
|
|
742
|
-
|
|
545
|
+
# @param (see #message)
|
|
546
|
+
# @yield (see #message)
|
|
547
|
+
# @yieldparam (see #message)
|
|
548
|
+
# @return (see #message)
|
|
549
|
+
def warning(title, border: :default, &)
|
|
550
|
+
section(title, border:, type: :warning, &)
|
|
743
551
|
end
|
|
744
552
|
alias warn warning
|
|
745
553
|
|
|
746
|
-
#
|
|
747
|
-
# the output of text elements.
|
|
554
|
+
# Creates a {Section} with `:error` styling.
|
|
748
555
|
#
|
|
749
|
-
# @
|
|
750
|
-
|
|
751
|
-
|
|
556
|
+
# @param (see #message)
|
|
557
|
+
# @yield (see #message)
|
|
558
|
+
# @yieldparam (see #message)
|
|
559
|
+
# @return (see #message)
|
|
560
|
+
def error(title, border: :default, &)
|
|
561
|
+
section(title, border:, type: :error, &)
|
|
752
562
|
end
|
|
753
|
-
alias err error
|
|
754
563
|
|
|
755
|
-
#
|
|
756
|
-
# the output of text elements.
|
|
564
|
+
# Creates a {Section} with `:fatal` styling.
|
|
757
565
|
#
|
|
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
|
-
)
|
|
566
|
+
# @param (see #message)
|
|
567
|
+
# @yield (see #message)
|
|
568
|
+
# @yieldparam (see #message)
|
|
569
|
+
# @return (see #message)
|
|
570
|
+
def fatal(title, border: :default, &)
|
|
571
|
+
section(title, border:, type: :fatal, &)
|
|
813
572
|
end
|
|
814
573
|
|
|
815
|
-
#
|
|
574
|
+
# Creates a {Frame} element — a bordered box with an optional title.
|
|
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 Manual close
|
|
577
|
+
# frm = ui.frame 'Preview'
|
|
578
|
+
# frm.puts 'Content inside the frame.'
|
|
579
|
+
# frm.end
|
|
824
580
|
#
|
|
825
|
-
# @
|
|
581
|
+
# @example Block form with title and custom border
|
|
582
|
+
# ui.frame 'Results', border: :double do
|
|
583
|
+
# ui.puts 'All checks passed.'
|
|
584
|
+
# end
|
|
585
|
+
#
|
|
586
|
+
# @example Block form without a title
|
|
587
|
+
# ui.frame do
|
|
588
|
+
# ui.puts 'Framed content.'
|
|
589
|
+
# end
|
|
826
590
|
#
|
|
827
|
-
# @
|
|
828
|
-
|
|
829
|
-
|
|
591
|
+
# @param title (see #section)
|
|
592
|
+
# @param border (see #section)
|
|
593
|
+
# @param style [Symbol, String, Array<Style>, Array<String>, nil]
|
|
594
|
+
# ANSI style applied to the frame
|
|
595
|
+
# @yield [frame] the {Frame} element
|
|
596
|
+
# @yieldparam frame [Frame]
|
|
597
|
+
# @return [Object] return value of the block
|
|
598
|
+
# @return [Frame] itself, if no block is specified
|
|
599
|
+
def frame(title = nil, border: :default, style: nil, &)
|
|
600
|
+
__with(Frame.new(self, title, border, style), &)
|
|
830
601
|
end
|
|
831
602
|
|
|
603
|
+
# Creates a {Task} element — a labelled step that shows a spinner while
|
|
604
|
+
# running and a check mark on success.
|
|
832
605
|
#
|
|
833
|
-
#
|
|
834
|
-
#
|
|
835
|
-
|
|
606
|
+
# @example Manual close
|
|
607
|
+
# t = ui.task 'Installing dependencies'
|
|
608
|
+
# run_install
|
|
609
|
+
# t.end
|
|
836
610
|
#
|
|
837
|
-
#
|
|
611
|
+
# @example Block form
|
|
612
|
+
# ui.task 'Installing dependencies' do
|
|
613
|
+
# run_install
|
|
614
|
+
# end
|
|
838
615
|
#
|
|
616
|
+
# @param title [#to_s] task description
|
|
617
|
+
# @param pin [Boolean] whether the task title should be pinned
|
|
618
|
+
# @yield [task] the {Task} element
|
|
619
|
+
# @yieldparam task [Task]
|
|
620
|
+
# @return [Object] return value of the block
|
|
621
|
+
# @return [Task] itself, if no block is specified
|
|
622
|
+
def task(title, pin: false, &) = __with(Task.new(self, title, pin), &)
|
|
839
623
|
|
|
840
|
-
#
|
|
624
|
+
# Creates a {Progress} element for tracking incremental work.
|
|
841
625
|
#
|
|
842
|
-
#
|
|
843
|
-
#
|
|
626
|
+
# When `max` is given the progress is displayed as a percentage bar.
|
|
627
|
+
# When `max` is `nil` an open-ended dot animation is shown instead.
|
|
844
628
|
#
|
|
845
|
-
# @example
|
|
846
|
-
# ui.
|
|
847
|
-
#
|
|
629
|
+
# @example Bounded progress
|
|
630
|
+
# ui.progress 'Processing', max: items.size do |bar|
|
|
631
|
+
# items.each { process(it); bar.step }
|
|
848
632
|
# 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
633
|
#
|
|
857
|
-
# @
|
|
858
|
-
#
|
|
859
|
-
#
|
|
860
|
-
#
|
|
861
|
-
# @param yes [String, Enumerable<String>]
|
|
862
|
-
# key code/s a user can input to return positive result
|
|
863
|
-
# @param no [String, Enumerable<String>]
|
|
864
|
-
# key code/s a user can input to return negative resault
|
|
634
|
+
# @example Open-ended progress
|
|
635
|
+
# ui.progress 'Working…' do |bar|
|
|
636
|
+
# loop { bar.step; break if done? }
|
|
637
|
+
# end
|
|
865
638
|
#
|
|
866
|
-
# @
|
|
867
|
-
#
|
|
868
|
-
# @
|
|
869
|
-
#
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
639
|
+
# @param title (see #section)
|
|
640
|
+
# @param max [Numeric, nil] maximum value
|
|
641
|
+
# @param popts (see #mark)
|
|
642
|
+
# @yield [progress] the {Progress} element
|
|
643
|
+
# @yieldparam progress [Progress]
|
|
644
|
+
# @return [Object] return value of the block
|
|
645
|
+
# @return [Progress] itself when no block is given
|
|
646
|
+
def progress(*title, max: nil, **popts, &)
|
|
647
|
+
__with(
|
|
648
|
+
(Terminal.ansi? ? Progress : DumbProgress).new(
|
|
649
|
+
self,
|
|
650
|
+
max,
|
|
651
|
+
*title,
|
|
652
|
+
**popts
|
|
653
|
+
),
|
|
654
|
+
&
|
|
655
|
+
)
|
|
876
656
|
end
|
|
877
657
|
|
|
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
|
|
658
|
+
#
|
|
659
|
+
# @!endgroup
|
|
660
|
+
#
|
|
661
|
+
# @!group User Interaction
|
|
662
|
+
#
|
|
962
663
|
|
|
963
|
-
#
|
|
964
|
-
# All options are returned with their selection status.
|
|
664
|
+
# Waits for the user to press any key.
|
|
965
665
|
#
|
|
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
|
|
666
|
+
# @example Plain wait
|
|
667
|
+
# ui.puts 'Press any key to continue…'
|
|
668
|
+
# ui.await
|
|
972
669
|
#
|
|
973
|
-
# @
|
|
974
|
-
#
|
|
670
|
+
# @example With temporary prompt
|
|
671
|
+
# ui.await { ui.puts '[faint]Press any key to continue…' }
|
|
975
672
|
#
|
|
976
|
-
# @
|
|
977
|
-
#
|
|
673
|
+
# @yield (see #temporary)
|
|
674
|
+
# @yieldparam (see #temporary)
|
|
978
675
|
# @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) }
|
|
676
|
+
def await
|
|
677
|
+
yield(temp = temporary) if block_given?
|
|
678
|
+
Terminal.read_key_event
|
|
679
|
+
nil
|
|
680
|
+
ensure
|
|
681
|
+
temp&.end
|
|
989
682
|
end
|
|
990
683
|
|
|
991
|
-
#
|
|
992
|
-
# The selected options are returned.
|
|
684
|
+
# Waits for a key event and returns information about it.
|
|
993
685
|
#
|
|
994
|
-
#
|
|
995
|
-
#
|
|
996
|
-
# ui.puts '[i]Which terminal applications did you already tested?[/i]'
|
|
997
|
-
# end
|
|
686
|
+
# Key names are strings such as `"a"`, `"Enter"`, `"Esc"`, `"Back"`,
|
|
687
|
+
# `"Shift+Alt+F1"`.
|
|
998
688
|
#
|
|
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)
|
|
689
|
+
# @example
|
|
690
|
+
# answer = ui.query yes: 'y', no: 'n'
|
|
691
|
+
# ui.puts answer == :yes ? 'Confirmed!' : 'Cancelled.'
|
|
1007
692
|
#
|
|
1008
|
-
# @
|
|
1009
|
-
#
|
|
1010
|
-
#
|
|
1011
|
-
#
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
693
|
+
# @example With a temporary prompt
|
|
694
|
+
# answer = ui.query(yes: 'y', no: 'n') do
|
|
695
|
+
# ui.puts 'Continue? ([b]y[/b]/[b]n[/b])'
|
|
696
|
+
# end
|
|
697
|
+
#
|
|
698
|
+
# @param options [Hash<Object => String, #each>]
|
|
699
|
+
# map of return values to key names or enumerables of key names;
|
|
700
|
+
# e.g. `{ yes: 'y', no: %w[n Esc] }`
|
|
701
|
+
# @yield (see #temporary)
|
|
702
|
+
# @yieldparam (see #temporary)
|
|
703
|
+
# @return [Object] matched option key
|
|
704
|
+
def query(**options)
|
|
705
|
+
yield(temp = temporary) if block_given?
|
|
706
|
+
return Terminal.read_key_event.name if options.empty?
|
|
707
|
+
Terminal.on_key_event do |event|
|
|
708
|
+
event = event.name
|
|
709
|
+
found, =
|
|
710
|
+
options.find do |_, value|
|
|
711
|
+
(value.is_a?(Enumerable) && value.include?(event)) || value == event
|
|
712
|
+
end
|
|
713
|
+
break found if found
|
|
1019
714
|
end
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
selected: selected,
|
|
1023
|
-
**choices.to_h { [_1, sel] },
|
|
1024
|
-
&block
|
|
1025
|
-
).filter_map { |key, selected| key if selected }
|
|
715
|
+
ensure
|
|
716
|
+
temp&.end
|
|
1026
717
|
end
|
|
1027
718
|
|
|
719
|
+
# Presents a list of options and returns the one the user selects.
|
|
1028
720
|
#
|
|
1029
|
-
#
|
|
721
|
+
# In ANSI mode the user navigates with arrow keys and confirms with Enter.
|
|
722
|
+
# In dumb mode items are numbered and the user types the item number.
|
|
1030
723
|
#
|
|
1031
|
-
|
|
724
|
+
# @example Positional items
|
|
725
|
+
# answer = ui.choice 'Yes', 'No', 'Cancel'
|
|
1032
726
|
#
|
|
1033
|
-
#
|
|
727
|
+
# @example Positional items
|
|
728
|
+
# answer = ui.choice 'Yes', 'No', abortable: true do
|
|
729
|
+
# ui.puts 'Overwrite the file?'
|
|
730
|
+
# end
|
|
1034
731
|
#
|
|
732
|
+
# @example Keyword pairs
|
|
733
|
+
# action = ui.choice(overwrite: 'Overwrite', skip: 'Skip') do
|
|
734
|
+
# ui.puts 'File already exists.'
|
|
735
|
+
# end
|
|
736
|
+
#
|
|
737
|
+
# @overload choice(*items, abortable: false, selected: nil)
|
|
738
|
+
# Items are passed as positional arguments; the selected item itself is
|
|
739
|
+
# returned.
|
|
740
|
+
# @param items [#to_s, ...] options to choose from
|
|
741
|
+
# @param abortable [Boolean] when `true` the user can press Esc to cancel
|
|
742
|
+
# @param selected pre-selected item value, or `nil`
|
|
743
|
+
#
|
|
744
|
+
# @overload choice(abortable: false, selected: nil, **pairs)
|
|
745
|
+
# Items are passed as keyword pairs `{ return_value => label }`; the
|
|
746
|
+
# matching key is returned.
|
|
747
|
+
# @param abortable [Boolean] when `true` the user can press Esc to cancel
|
|
748
|
+
# @param selected pre-selected return value, or `nil`
|
|
749
|
+
# @param pairs [Hash{Object => #to_s}] map of return values to display labels
|
|
750
|
+
#
|
|
751
|
+
# @yield (see #temporary)
|
|
752
|
+
# @yieldparam (see #temporary)
|
|
753
|
+
# @return [Object] the key of the selected pair, or `nil` if aborted
|
|
754
|
+
def choice(*items, abortable: false, selected: nil, **pairs)
|
|
755
|
+
return if items.empty? && pairs.empty?
|
|
756
|
+
yield(temp = temporary) if block_given?
|
|
757
|
+
(Terminal.ansi? ? Choice : DumbChoice).new(
|
|
758
|
+
self,
|
|
759
|
+
items + pairs.values,
|
|
760
|
+
Array.new(items.size, &:itself) + pairs.keys
|
|
761
|
+
).select(abortable, selected)
|
|
762
|
+
ensure
|
|
763
|
+
temp&.end
|
|
764
|
+
end
|
|
1035
765
|
|
|
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.
|
|
766
|
+
# Presents a list of options and returns all items the user selects.
|
|
1041
767
|
#
|
|
1042
|
-
#
|
|
1043
|
-
#
|
|
1044
|
-
# This is a [i]temporary[/i] displayed text.
|
|
1045
|
-
# It will disappear when you press [b]ENTER[/b].
|
|
1046
|
-
# MSG
|
|
768
|
+
# In ANSI mode the user toggles items with Space and confirms with Enter.
|
|
769
|
+
# In dumb mode items are numbered and the user types item numbers.
|
|
1047
770
|
#
|
|
1048
|
-
# @example
|
|
1049
|
-
# ui.
|
|
1050
|
-
#
|
|
1051
|
-
#
|
|
1052
|
-
#
|
|
1053
|
-
#
|
|
1054
|
-
# ui.await
|
|
771
|
+
# @example Positional items
|
|
772
|
+
# picks = ui.select 'Kitty', 'iTerm2', 'Ghostty'
|
|
773
|
+
#
|
|
774
|
+
# @example Positional items with all pre-selected
|
|
775
|
+
# picks = ui.select 'A', 'B', 'C', selected: :all do
|
|
776
|
+
# ui.puts 'Choose features to enable:'
|
|
1055
777
|
# end
|
|
1056
778
|
#
|
|
1057
|
-
# @
|
|
1058
|
-
#
|
|
779
|
+
# @example Keyword pairs
|
|
780
|
+
# flags = ui.select verbose: 'Verbose', debug: 'Debug', trace: 'Trace'
|
|
781
|
+
#
|
|
782
|
+
# @overload select(*items, abortable: false, selected: nil)
|
|
783
|
+
# Items are passed as positional arguments; the selected items themselves
|
|
784
|
+
# are returned.
|
|
785
|
+
# @param items [#to_s, ...] options to choose from
|
|
786
|
+
# @param abortable [Boolean] when `true` the user can press Esc to cancel
|
|
787
|
+
# @param selected [nil, :all, #each]
|
|
788
|
+
# pre-selected items: `nil` = none, `:all` = all,
|
|
789
|
+
# or an enumerable of item values to pre-select
|
|
790
|
+
#
|
|
791
|
+
# @overload select(abortable: false, selected: nil, **pairs)
|
|
792
|
+
# Items are passed as keyword pairs `{ return_value => label }`; the
|
|
793
|
+
# matching keys are returned.
|
|
794
|
+
# @param abortable [Boolean] when `true` the user can press Esc to cancel
|
|
795
|
+
# @param selected [nil, :all, #each]
|
|
796
|
+
# @param pairs [Hash{Object => #to_s}] map of return values to labels
|
|
797
|
+
#
|
|
798
|
+
# @yield (see #temporary)
|
|
799
|
+
# @yieldparam (see #temporary)
|
|
800
|
+
# @return [Array] keys of selected pairs, or `nil` if aborted
|
|
801
|
+
def select(*items, abortable: false, selected: nil, **pairs)
|
|
802
|
+
return if items.empty? && pairs.empty?
|
|
803
|
+
yield(temp = temporary) if block_given?
|
|
804
|
+
items = items.to_h { [it, it] }.merge!(pairs)
|
|
805
|
+
(Terminal.ansi? ? Select : DumbSelect).new(
|
|
806
|
+
self,
|
|
807
|
+
if selected == :all
|
|
808
|
+
items.map { it << true }
|
|
809
|
+
elsif selected.is_a?(Enumerable)
|
|
810
|
+
items.map { |ret, txt| [ret, txt, selected.include?(ret)] }
|
|
811
|
+
elsif selected
|
|
812
|
+
items.map { |ret, txt| [ret, txt, selected == ret] }
|
|
813
|
+
else
|
|
814
|
+
items.map { it << false }
|
|
815
|
+
end
|
|
816
|
+
).select(abortable)
|
|
817
|
+
ensure
|
|
818
|
+
temp&.end
|
|
819
|
+
end
|
|
820
|
+
|
|
821
|
+
#
|
|
822
|
+
# @!endgroup
|
|
1059
823
|
#
|
|
1060
|
-
#
|
|
1061
|
-
|
|
1062
|
-
|
|
824
|
+
# @!group Utilities
|
|
825
|
+
#
|
|
826
|
+
|
|
827
|
+
# Executes a shell command and prints its output to the terminal.
|
|
828
|
+
#
|
|
829
|
+
# All arguments and options are forwarded to `Terminal.sh`, which in turn
|
|
830
|
+
# uses `Process.spawn`.
|
|
831
|
+
#
|
|
832
|
+
# @example Run a simple command
|
|
833
|
+
# ui.sh 'echo "Hello Ruby!"'
|
|
834
|
+
#
|
|
835
|
+
# @example Pipe a string as stdin
|
|
836
|
+
# ui.sh 'cat', input: 'Hello from stdin'
|
|
837
|
+
#
|
|
838
|
+
# @overload sh(*cmd, env = {}, shell: false, input: nil, **spawn_options)
|
|
839
|
+
# @param cmd [#to_s, ...] command and arguments, same as `Process.spawn`
|
|
840
|
+
# @param env [Hash, nil] additional environment variables
|
|
841
|
+
# @param shell [Boolean] when `true` runs the command through a system shell
|
|
842
|
+
# @param input piped standard input; accepts any object with `#readpartial`,
|
|
843
|
+
# `#to_io`, `#each`, `#to_a`, or anything `IO.write` accepts (e.g. a String)
|
|
844
|
+
# @param spawn_options [Hash] additional options forwarded to `Process.spawn`
|
|
845
|
+
# @return (see #puts)
|
|
846
|
+
def sh(...) = Shell.render(self, ...)
|
|
847
|
+
|
|
848
|
+
# Executes a shell command, captures its output, and returns it.
|
|
849
|
+
#
|
|
850
|
+
# Stdout and stderr lines are displayed in a scrolling region limited to
|
|
851
|
+
# `max_lines`. All other arguments are identical to {#sh}.
|
|
852
|
+
#
|
|
853
|
+
# @example Capture and inspect output
|
|
854
|
+
# status, out, err = ui.run 'ls', '-la'
|
|
855
|
+
# ui.puts "exit #{status.exitstatus}"
|
|
856
|
+
#
|
|
857
|
+
# @example Limit displayed lines and pipe input
|
|
858
|
+
# File.open('data.txt') { |f| ui.run 'wc', '-l', input: f, max_lines: 5 }
|
|
859
|
+
#
|
|
860
|
+
# @overload run(*cmd, env = {}, shell: false, input: nil, max_lines: 10, **spawn_options)
|
|
861
|
+
# @param cmd (see #sh)
|
|
862
|
+
# @param max_lines [#to_int] maximum number of output lines shown at once
|
|
863
|
+
# @param env (see #sh)
|
|
864
|
+
# @param shell (see #sh)
|
|
865
|
+
# @param input (see #sh)
|
|
866
|
+
# @param spawn_options (see #sh)
|
|
867
|
+
# @return [Array(Process::Status, Array<String>, Array<String>)]
|
|
868
|
+
# three-element array of exit status, stdout lines, and stderr lines
|
|
869
|
+
# @return [nil] when the command could not be started
|
|
870
|
+
def run(*, max_lines: 10, **)
|
|
871
|
+
(Terminal.ansi? ? ShellRunner : DumbShellRunner).render(
|
|
872
|
+
self,
|
|
873
|
+
max_lines,
|
|
874
|
+
*,
|
|
875
|
+
**
|
|
876
|
+
)
|
|
1063
877
|
end
|
|
1064
878
|
|
|
1065
879
|
#
|
|
1066
880
|
# @!endgroup
|
|
1067
881
|
#
|
|
1068
882
|
|
|
1069
|
-
private
|
|
883
|
+
# @private
|
|
884
|
+
def columns = Terminal.columns
|
|
1070
885
|
|
|
1071
|
-
|
|
886
|
+
private
|
|
1072
887
|
|
|
1073
|
-
def
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
__with(Section.new(self, color, mark, title), *text, **options, &block)
|
|
888
|
+
def __determine_max_width(value)
|
|
889
|
+
return columns unless value
|
|
890
|
+
return 0 if value == 0
|
|
891
|
+
return value.to_int + columns if value < 0
|
|
892
|
+
return (value * columns).round if value < 1
|
|
893
|
+
[value.to_int, columns].min
|
|
1080
894
|
end
|
|
1081
895
|
|
|
1082
|
-
def
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
return true
|
|
1090
|
-
end
|
|
1091
|
-
true
|
|
896
|
+
def __with(element)
|
|
897
|
+
NattyUI.__send__(:_begin, element)
|
|
898
|
+
return element unless block_given?
|
|
899
|
+
begin
|
|
900
|
+
yield(element)
|
|
901
|
+
ensure
|
|
902
|
+
NattyUI.__send__(:_end, element)
|
|
1092
903
|
end
|
|
1093
904
|
end
|
|
1094
905
|
end
|
|
1095
906
|
|
|
1096
907
|
dir = __dir__
|
|
1097
908
|
|
|
1098
|
-
|
|
909
|
+
# @comment Elements:
|
|
910
|
+
autoload :Frame, "#{dir}/frame.rb"
|
|
911
|
+
autoload :Margin, "#{dir}/margin.rb"
|
|
912
|
+
autoload :Progress, "#{dir}/progress.rb"
|
|
913
|
+
autoload :DumbProgress, "#{dir}/dumb_progress.rb"
|
|
1099
914
|
autoload :Section, "#{dir}/section.rb"
|
|
1100
|
-
autoload :Table, "#{dir}/table.rb"
|
|
1101
915
|
autoload :Task, "#{dir}/task.rb"
|
|
1102
916
|
autoload :Temporary, "#{dir}/temporary.rb"
|
|
1103
|
-
autoload :Theme, "#{dir}/theme.rb"
|
|
1104
|
-
autoload :Utils, "#{dir}/utils.rb"
|
|
1105
|
-
private_constant(:Framed, :Utils)
|
|
1106
917
|
|
|
1107
|
-
|
|
1108
|
-
autoload :
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
autoload :
|
|
1112
|
-
autoload :
|
|
918
|
+
# @comment Helper:
|
|
919
|
+
autoload :Table, "#{dir}/helper/table.rb"
|
|
920
|
+
|
|
921
|
+
# @comment Utils:
|
|
922
|
+
autoload :Utils, "#{dir}/utils/utils.rb"
|
|
923
|
+
autoload :StrConst, "#{dir}/utils/str_const.rb"
|
|
924
|
+
private_constant(:Utils, :StrConst)
|
|
925
|
+
|
|
926
|
+
# @comment Renderer:
|
|
927
|
+
autoload :VBars, "#{dir}/renderer/bars.rb"
|
|
928
|
+
autoload :HBars, "#{dir}/renderer/bars.rb"
|
|
929
|
+
autoload :Choice, "#{dir}/renderer/choice.rb"
|
|
930
|
+
autoload :DumbChoice, "#{dir}/renderer/dumb_choice.rb"
|
|
931
|
+
autoload :Heading, "#{dir}/renderer/heading.rb"
|
|
932
|
+
autoload :HorizontalRule, "#{dir}/renderer/horizontal_rule.rb"
|
|
933
|
+
autoload :LS, "#{dir}/renderer/ls.rb"
|
|
934
|
+
autoload :CompactLS, "#{dir}/renderer/ls.rb"
|
|
935
|
+
autoload :Mark, "#{dir}/renderer/mark.rb"
|
|
936
|
+
autoload :Quote, "#{dir}/renderer/quote.rb"
|
|
937
|
+
autoload :Select, "#{dir}/renderer/select.rb"
|
|
938
|
+
autoload :DumbSelect, "#{dir}/renderer/dumb_select.rb"
|
|
939
|
+
autoload :Shell, "#{dir}/renderer/shell.rb"
|
|
940
|
+
autoload :ShellRunner, "#{dir}/renderer/shell_runner.rb"
|
|
941
|
+
autoload :DumbShellRunner, "#{dir}/renderer/dumb_shell_runner.rb"
|
|
942
|
+
autoload :TableRenderer, "#{dir}/renderer/table_renderer.rb"
|
|
1113
943
|
private_constant(
|
|
944
|
+
:VBars,
|
|
945
|
+
:HBars,
|
|
1114
946
|
:Choice,
|
|
1115
947
|
:DumbChoice,
|
|
1116
|
-
:
|
|
1117
|
-
:
|
|
1118
|
-
:
|
|
1119
|
-
:
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
:CompactLSRenderer,
|
|
1129
|
-
:HBarsRenderer,
|
|
1130
|
-
:LSRenderer,
|
|
1131
|
-
:ShellRenderer,
|
|
1132
|
-
:VBarsRenderer
|
|
948
|
+
:Heading,
|
|
949
|
+
:HorizontalRule,
|
|
950
|
+
:LS,
|
|
951
|
+
:CompactLS,
|
|
952
|
+
:Mark,
|
|
953
|
+
:Quote,
|
|
954
|
+
:Select,
|
|
955
|
+
:DumbSelect,
|
|
956
|
+
:Shell,
|
|
957
|
+
:ShellRunner,
|
|
958
|
+
:DumbShellRunner,
|
|
959
|
+
:TableRenderer
|
|
1133
960
|
)
|
|
1134
961
|
end
|