natty-ui 0.12.0 → 0.25.0
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/LICENSE +1 -1
- data/README.md +23 -24
- data/examples/24bit-colors.rb +4 -9
- data/examples/3bit-colors.rb +28 -8
- data/examples/8bit-colors.rb +18 -23
- data/examples/attributes.rb +30 -25
- data/examples/cols.rb +40 -0
- data/examples/elements.rb +31 -0
- data/examples/examples.rb +45 -0
- data/examples/illustration.rb +56 -54
- data/examples/ls.rb +16 -18
- data/examples/named-colors.rb +23 -0
- data/examples/sections.rb +29 -0
- data/examples/tables.rb +62 -0
- data/examples/tasks.rb +52 -0
- data/lib/natty-ui/attributes.rb +604 -0
- data/lib/natty-ui/choice.rb +56 -0
- data/lib/natty-ui/dumb_choice.rb +45 -0
- data/lib/natty-ui/element.rb +78 -0
- data/lib/natty-ui/features.rb +798 -0
- data/lib/natty-ui/framed.rb +51 -0
- data/lib/natty-ui/ls_renderer.rb +93 -0
- data/lib/natty-ui/progress.rb +187 -0
- data/lib/natty-ui/section.rb +69 -0
- data/lib/natty-ui/table.rb +241 -0
- data/lib/natty-ui/table_renderer.rb +147 -0
- data/lib/natty-ui/task.rb +44 -0
- data/lib/natty-ui/temporary.rb +38 -0
- data/lib/natty-ui/theme.rb +303 -0
- data/lib/natty-ui/utils.rb +79 -0
- data/lib/natty-ui/version.rb +1 -1
- data/lib/natty-ui/width_finder.rb +125 -0
- data/lib/natty-ui.rb +89 -147
- metadata +47 -56
- data/examples/animate.rb +0 -44
- data/examples/attributes_list.rb +0 -14
- data/examples/demo.rb +0 -53
- data/examples/message.rb +0 -32
- data/examples/progress.rb +0 -68
- data/examples/query.rb +0 -41
- data/examples/read_key.rb +0 -13
- data/examples/table.rb +0 -41
- data/lib/natty-ui/animation/binary.rb +0 -36
- data/lib/natty-ui/animation/default.rb +0 -38
- data/lib/natty-ui/animation/matrix.rb +0 -51
- data/lib/natty-ui/animation/rainbow.rb +0 -28
- data/lib/natty-ui/animation/type_writer.rb +0 -44
- data/lib/natty-ui/animation.rb +0 -69
- data/lib/natty-ui/ansi/constants.rb +0 -75
- data/lib/natty-ui/ansi.rb +0 -521
- data/lib/natty-ui/ansi_wrapper.rb +0 -199
- data/lib/natty-ui/frame.rb +0 -53
- data/lib/natty-ui/glyph.rb +0 -64
- data/lib/natty-ui/key_map.rb +0 -142
- data/lib/natty-ui/preload.rb +0 -12
- data/lib/natty-ui/spinner.rb +0 -120
- data/lib/natty-ui/text/east_asian_width.rb +0 -2529
- data/lib/natty-ui/text.rb +0 -203
- data/lib/natty-ui/wrapper/animate.rb +0 -17
- data/lib/natty-ui/wrapper/ask.rb +0 -78
- data/lib/natty-ui/wrapper/element.rb +0 -79
- data/lib/natty-ui/wrapper/features.rb +0 -21
- data/lib/natty-ui/wrapper/framed.rb +0 -45
- data/lib/natty-ui/wrapper/heading.rb +0 -60
- data/lib/natty-ui/wrapper/horizontal_rule.rb +0 -37
- data/lib/natty-ui/wrapper/list_in_columns.rb +0 -138
- data/lib/natty-ui/wrapper/message.rb +0 -109
- data/lib/natty-ui/wrapper/mixins.rb +0 -67
- data/lib/natty-ui/wrapper/progress.rb +0 -74
- data/lib/natty-ui/wrapper/query.rb +0 -89
- data/lib/natty-ui/wrapper/quote.rb +0 -25
- data/lib/natty-ui/wrapper/request.rb +0 -54
- data/lib/natty-ui/wrapper/section.rb +0 -118
- data/lib/natty-ui/wrapper/table.rb +0 -551
- data/lib/natty-ui/wrapper/task.rb +0 -55
- data/lib/natty-ui/wrapper.rb +0 -230
@@ -0,0 +1,798 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NattyUI
|
4
|
+
# These are all supported features by {NattyUI} or any other sub- element
|
5
|
+
# like {section}, {message}, {task}, ...
|
6
|
+
#
|
7
|
+
# Any printed text can contain *BBCode*-like embedded ANSI attributes which
|
8
|
+
# will be used when the output terminal supports attributes and colors.
|
9
|
+
#
|
10
|
+
module Features
|
11
|
+
#
|
12
|
+
# @!group Printing Methods
|
13
|
+
#
|
14
|
+
|
15
|
+
# Print given text as lines.
|
16
|
+
#
|
17
|
+
# @example Print two lines text, right aligned
|
18
|
+
# ui.puts("Two lines", "of nice text", align: :right)
|
19
|
+
# # => Two lines
|
20
|
+
# # => of nice text
|
21
|
+
#
|
22
|
+
# @example Print two lines text, with a prefix
|
23
|
+
# ui.puts("Two lines", "of nice text", prefix: ': ')
|
24
|
+
# # => : Two lines
|
25
|
+
# # => : of nice text
|
26
|
+
#
|
27
|
+
# @see #pin
|
28
|
+
#
|
29
|
+
# @param text [#to_s]
|
30
|
+
# one or more convertible objects to print line by line
|
31
|
+
# @param options [{Symbol => Object}]
|
32
|
+
# @option options [:left, :right, :centered] :align (:left)
|
33
|
+
# text alignment
|
34
|
+
# @option options [true, false] :ignore_newline (false)
|
35
|
+
# whether to igniore newline characters
|
36
|
+
#
|
37
|
+
# @return [Features]
|
38
|
+
# itself
|
39
|
+
def puts(*text, **options)
|
40
|
+
bbcode = true if (bbcode = options[:bbcode]).nil?
|
41
|
+
max_width = options[:max_width] || Terminal.columns
|
42
|
+
|
43
|
+
prefix_width =
|
44
|
+
if (prefix = options[:prefix])
|
45
|
+
prefix = Ansi.bbcode(prefix) if bbcode
|
46
|
+
options[:prefix_width] || Text.width(prefix, bbcode: false)
|
47
|
+
else
|
48
|
+
0
|
49
|
+
end
|
50
|
+
|
51
|
+
if (first_line = options[:first_line_prefix])
|
52
|
+
first_line = Ansi.bbcode(first_line) if bbcode
|
53
|
+
first_line_width =
|
54
|
+
options[:first_line_prefix_width] ||
|
55
|
+
Text.width(first_line, bbcode: false)
|
56
|
+
|
57
|
+
if prefix_width < first_line_width
|
58
|
+
prefix_next = "#{prefix}#{' ' * (first_line_width - prefix_width)}"
|
59
|
+
prefix = first_line
|
60
|
+
prefix_width = first_line_width
|
61
|
+
else
|
62
|
+
prefix_next = prefix
|
63
|
+
prefix =
|
64
|
+
if first_line_width < prefix_width
|
65
|
+
first_line + (' ' * (prefix_width - first_line_width))
|
66
|
+
else
|
67
|
+
first_line
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
max_width -= prefix_width
|
73
|
+
|
74
|
+
if (suffix = options[:suffix])
|
75
|
+
suffix = Ansi.bbcode(suffix) if bbcode
|
76
|
+
max_width -= options[:suffix_width] || Text.width(suffix, bbcode: false)
|
77
|
+
end
|
78
|
+
|
79
|
+
return self if max_width <= 0
|
80
|
+
|
81
|
+
lines =
|
82
|
+
Text.each_line_with_size(
|
83
|
+
*text,
|
84
|
+
limit: max_width,
|
85
|
+
bbcode: bbcode,
|
86
|
+
ansi: Terminal.ansi?,
|
87
|
+
ignore_newline: options[:ignore_newline]
|
88
|
+
)
|
89
|
+
|
90
|
+
if (align = options[:align]).nil?
|
91
|
+
lines.each do |line|
|
92
|
+
Terminal.print(prefix, line, suffix, EOL__, bbcode: false)
|
93
|
+
@lines_written += 1
|
94
|
+
prefix, prefix_next = prefix_next, nil if prefix_next
|
95
|
+
end
|
96
|
+
return self
|
97
|
+
end
|
98
|
+
|
99
|
+
unless options[:expand]
|
100
|
+
lines = lines.to_a
|
101
|
+
max_width = lines.max_by(&:last).last
|
102
|
+
end
|
103
|
+
|
104
|
+
case align
|
105
|
+
when :right
|
106
|
+
lines.each do |line, width|
|
107
|
+
Terminal.print(
|
108
|
+
prefix,
|
109
|
+
' ' * (max_width - width),
|
110
|
+
line,
|
111
|
+
suffix,
|
112
|
+
EOL__,
|
113
|
+
bbcode: false
|
114
|
+
)
|
115
|
+
@lines_written += 1
|
116
|
+
prefix, prefix_next = prefix_next, nil if prefix_next
|
117
|
+
end
|
118
|
+
when :centered
|
119
|
+
lines.each do |line, width|
|
120
|
+
space = max_width - width
|
121
|
+
Terminal.print(
|
122
|
+
prefix,
|
123
|
+
' ' * (lw = space / 2),
|
124
|
+
line,
|
125
|
+
' ' * (space - lw),
|
126
|
+
suffix,
|
127
|
+
EOL__,
|
128
|
+
bbcode: false
|
129
|
+
)
|
130
|
+
@lines_written += 1
|
131
|
+
prefix, prefix_next = prefix_next, nil if prefix_next
|
132
|
+
end
|
133
|
+
else
|
134
|
+
lines.each do |line, width|
|
135
|
+
Terminal.print(
|
136
|
+
prefix,
|
137
|
+
line,
|
138
|
+
' ' * (max_width - width),
|
139
|
+
suffix,
|
140
|
+
EOL__,
|
141
|
+
bbcode: false
|
142
|
+
)
|
143
|
+
@lines_written += 1
|
144
|
+
prefix, prefix_next = prefix_next, nil if prefix_next
|
145
|
+
end
|
146
|
+
end
|
147
|
+
self
|
148
|
+
end
|
149
|
+
|
150
|
+
# Print given text as lines like {#puts}. Used in elements with temporary
|
151
|
+
# output like {#task} the text will be kept ("pinned").
|
152
|
+
#
|
153
|
+
# It can optionally have a decoration marker in first line like {#mark}.
|
154
|
+
#
|
155
|
+
# @example Print two lines decorated as information which are pinned
|
156
|
+
# ui.task 'Do something important' do |task|
|
157
|
+
# # ...
|
158
|
+
# task.pin("This is text", "which is pinned", mark: :information)
|
159
|
+
# # ...
|
160
|
+
# end
|
161
|
+
# # => ✓ Do something important
|
162
|
+
# # => 𝒊 This is text
|
163
|
+
# # => which is pinned.
|
164
|
+
#
|
165
|
+
# @param (see #puts)
|
166
|
+
# @param mark (see #mark)
|
167
|
+
# @option (see #puts)
|
168
|
+
#
|
169
|
+
# @return (see puts)
|
170
|
+
def pin(*text, mark: nil, **options)
|
171
|
+
options[:pin] = true
|
172
|
+
options[:first_line_prefix] = Theme.current.mark(mark) if mark
|
173
|
+
puts(*text, **options)
|
174
|
+
end
|
175
|
+
|
176
|
+
# Print given text with a decoration mark.
|
177
|
+
#
|
178
|
+
# @param text (see puts)
|
179
|
+
# @param mark [Symbol, #to_s]
|
180
|
+
# marker type
|
181
|
+
#
|
182
|
+
# @return (see puts)
|
183
|
+
def mark(*text, mark: :default)
|
184
|
+
mark = Theme.current.mark(mark)
|
185
|
+
puts(*text, first_line_prefix: mark, first_line_prefix_width: mark.width)
|
186
|
+
end
|
187
|
+
|
188
|
+
# Print given text as a quotation.
|
189
|
+
#
|
190
|
+
# @param text (see puts)
|
191
|
+
#
|
192
|
+
# @return (see puts)
|
193
|
+
def quote(*text)
|
194
|
+
width = columns * 0.75
|
195
|
+
quote = Theme.current.mark(:quote)
|
196
|
+
puts(
|
197
|
+
*text,
|
198
|
+
prefix: quote,
|
199
|
+
prefix_width: quote.width,
|
200
|
+
max_width: width < 20 ? nil : width.to_i
|
201
|
+
)
|
202
|
+
end
|
203
|
+
|
204
|
+
# Print given text as a heading.
|
205
|
+
#
|
206
|
+
# There are specific shortcuts for heading levels:
|
207
|
+
# {#h1}, {#h2}, {#h3}, {#h4}, {#h5}, {#h6}.
|
208
|
+
#
|
209
|
+
# @example Print a level 1 heading
|
210
|
+
# ui.heading(1, 'This is a H1 heading element')
|
211
|
+
# # => ╴╶╴╶─═══ This is a H1 heading element ═══─╴╶╴╶
|
212
|
+
#
|
213
|
+
# @param level [#to_i]
|
214
|
+
# heading level, one of 1..6
|
215
|
+
# @param text (see puts)
|
216
|
+
#
|
217
|
+
# @return (see puts)
|
218
|
+
def heading(level, *text)
|
219
|
+
prefix, suffix = Theme.current.heading(level)
|
220
|
+
puts(
|
221
|
+
*text,
|
222
|
+
max_width: columns,
|
223
|
+
prefix: prefix,
|
224
|
+
prefix_width: prefix.width,
|
225
|
+
suffix: suffix,
|
226
|
+
suffix_width: suffix.width,
|
227
|
+
align: :centered
|
228
|
+
)
|
229
|
+
end
|
230
|
+
|
231
|
+
# Print given text as a H1 {#heading}.
|
232
|
+
#
|
233
|
+
# @param text (see puts)
|
234
|
+
#
|
235
|
+
# @return (see puts)
|
236
|
+
def h1(*text) = heading(1, *text)
|
237
|
+
|
238
|
+
# Print given text as a H2 {#heading}.
|
239
|
+
#
|
240
|
+
# @param text (see puts)
|
241
|
+
#
|
242
|
+
# @return (see puts)
|
243
|
+
def h2(*text) = heading(2, *text)
|
244
|
+
|
245
|
+
# Print given text as a H3 {#heading}.
|
246
|
+
#
|
247
|
+
# @param text (see puts)
|
248
|
+
#
|
249
|
+
# @return (see puts)
|
250
|
+
def h3(*text) = heading(3, *text)
|
251
|
+
|
252
|
+
# Print given text as a H4 {#heading}.
|
253
|
+
#
|
254
|
+
# @param text (see puts)
|
255
|
+
#
|
256
|
+
# @return (see puts)
|
257
|
+
def h4(*text) = heading(4, *text)
|
258
|
+
|
259
|
+
# Print given text as a H5 {#heading}.
|
260
|
+
#
|
261
|
+
# @param text (see puts)
|
262
|
+
#
|
263
|
+
# @return (see puts)
|
264
|
+
def h5(*text) = heading(5, *text)
|
265
|
+
|
266
|
+
# Print given text as a H6 {#heading}.
|
267
|
+
#
|
268
|
+
# @param text (see puts)
|
269
|
+
#
|
270
|
+
# @return (see puts)
|
271
|
+
def h6(*text) = heading(6, *text)
|
272
|
+
|
273
|
+
# Print a horizontal rule.
|
274
|
+
#
|
275
|
+
# @example
|
276
|
+
# ui.hr(:heavy)
|
277
|
+
#
|
278
|
+
# @param type [Symbol]
|
279
|
+
# border type
|
280
|
+
#
|
281
|
+
# @return (see puts)
|
282
|
+
def hr(type = :default)
|
283
|
+
theme = Theme.current
|
284
|
+
bc = theme.border(type)[10]
|
285
|
+
puts("#{theme.heading_sytle}#{bc * columns}")
|
286
|
+
end
|
287
|
+
|
288
|
+
# Print one or more space lines.
|
289
|
+
#
|
290
|
+
# @param count [#to_i]
|
291
|
+
# lines to print
|
292
|
+
#
|
293
|
+
# @return (see puts)
|
294
|
+
def space(count = 1)
|
295
|
+
(count = count.to_i).positive? ? puts(*Array.new(count, "\n")) : self
|
296
|
+
end
|
297
|
+
|
298
|
+
# Print given items as list (like 'ls' command).
|
299
|
+
#
|
300
|
+
# Each list item will optionally be decorated with the given glyph as:
|
301
|
+
#
|
302
|
+
# - `Integer` as the start value for a numbered list
|
303
|
+
# - `Symbol` as the start symbol
|
304
|
+
# - `:hex` to create a hexadecimal numbered list
|
305
|
+
# - any text as prefix
|
306
|
+
#
|
307
|
+
# @example Print all Ruby files as a numbered list
|
308
|
+
# ui.ls(Dir['*/**/*.rb'], glyph: 1)
|
309
|
+
#
|
310
|
+
# @example Print all Ruby files as a bullet point list (with green bullets)
|
311
|
+
# ui.ls(Dir['*/**/*.rb'], glyph: '[green]•[/fg]')
|
312
|
+
#
|
313
|
+
# @param items [#to_s]
|
314
|
+
# one or more convertible objects to list
|
315
|
+
# @param compact [true, false]
|
316
|
+
# whether the compact display format should be used
|
317
|
+
# @param glyph [Integer, :hex, Symbol, #to_s]
|
318
|
+
# glyph to be used as prefix
|
319
|
+
#
|
320
|
+
# @return (see puts)
|
321
|
+
def ls(*items, compact: true, glyph: nil)
|
322
|
+
return self if items.empty?
|
323
|
+
renderer = compact ? CompactLSRenderer : LSRenderer
|
324
|
+
puts(*renderer.lines(items, glyph, columns))
|
325
|
+
end
|
326
|
+
|
327
|
+
# Generate and print a table.
|
328
|
+
# See {Table} for much more details about table generation.
|
329
|
+
#
|
330
|
+
# @example Draw a very simple table with three rows, four columns, complete borders
|
331
|
+
# ui.table(border: :default, border_around: true, padding: [0, 1]) do |table|
|
332
|
+
# table.add 1, 2, 3, 4
|
333
|
+
# table.add 5, 6, 7, 8
|
334
|
+
# table.add 9, 10, 11, 12
|
335
|
+
# end
|
336
|
+
#
|
337
|
+
# @param attributes [{Symbol => Object}]
|
338
|
+
# attributes for the table and default attributes for table cells
|
339
|
+
# @option attributes [Symbol] :border (nil)
|
340
|
+
# kind of border,
|
341
|
+
# see {Attributes::Border}
|
342
|
+
# @option attributes [true, false] :border_around (false)
|
343
|
+
# whether the table should have a border around,
|
344
|
+
# see {Attributes::BorderAround}
|
345
|
+
# @option attributes [Enumerable<Symbol>] :border_style (nil)
|
346
|
+
# style of border,
|
347
|
+
# see {Attributes::BorderStyle}
|
348
|
+
#
|
349
|
+
# @yieldparam table [Table]
|
350
|
+
# helper to define the table layout
|
351
|
+
#
|
352
|
+
# @return (see puts)
|
353
|
+
def table(**attributes)
|
354
|
+
return self unless block_given?
|
355
|
+
yield(table = Table.new(**attributes))
|
356
|
+
puts(*TableRenderer[table, columns])
|
357
|
+
end
|
358
|
+
|
359
|
+
# Print text in columns.
|
360
|
+
# This is a shorthand to define a {Table} with a single row.
|
361
|
+
#
|
362
|
+
# @param columns [#to_s]
|
363
|
+
# two or more convertible objects to print side by side
|
364
|
+
# @param attributes (see table)
|
365
|
+
# @option attributes (see table)
|
366
|
+
# @option attributes [Integer] :width (nil)
|
367
|
+
# width of a column,
|
368
|
+
# see {Attributes::Width}
|
369
|
+
#
|
370
|
+
# @yieldparam row [Table::Row]
|
371
|
+
# helper to define the row layout
|
372
|
+
#
|
373
|
+
# @return (see puts)
|
374
|
+
def cols(*columns, **attributes)
|
375
|
+
table(**attributes) do |table|
|
376
|
+
table.add do |row|
|
377
|
+
columns.each { row.add(_1, **attributes) }
|
378
|
+
yield(row) if block_given?
|
379
|
+
end
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
# Print a text division with attributes.
|
384
|
+
# This is a shorthand to define a {Table} with a single cell.
|
385
|
+
#
|
386
|
+
# @param text (see puts)
|
387
|
+
# @param attributes [{Symbol => Object}]
|
388
|
+
# attributes for the division
|
389
|
+
# @option attributes [:left, :right, :centered] :align (:left)
|
390
|
+
# text alignment,
|
391
|
+
# see {Attributes::Align}
|
392
|
+
# @option attributes [Integer, Enumerable<Integer>] :padding (nil)
|
393
|
+
# text padding,
|
394
|
+
# see {Attributes::Padding}
|
395
|
+
# @option attributes [Enumerable<Symbol>] :style (nil)
|
396
|
+
# text style,
|
397
|
+
# see {Attributes::Style}
|
398
|
+
# @option attributes [Integer] :width (nil)
|
399
|
+
# width of the cell,
|
400
|
+
# see {Attributes::Width}
|
401
|
+
# @option attributes (see table)
|
402
|
+
#
|
403
|
+
# @return (see puts)
|
404
|
+
def div(*text, **attributes)
|
405
|
+
return self if text.empty?
|
406
|
+
table(border_around: true, **attributes) do |table|
|
407
|
+
table.add { _1.add(*text, **attributes) }
|
408
|
+
end
|
409
|
+
end
|
410
|
+
|
411
|
+
# Dynamically display a task progress.
|
412
|
+
# When a `max` parameter is given the progress will be displayed as a
|
413
|
+
# progress bar below the `title`. Otherwise the progress is displayed just
|
414
|
+
# by accumulating dots.
|
415
|
+
#
|
416
|
+
# @example Display a progress bar
|
417
|
+
# ui.progress('Download file', max: 1024) do |progress|
|
418
|
+
# while progress.value < progress.max
|
419
|
+
# # just to simulate the download
|
420
|
+
# sleep(0.1)
|
421
|
+
# bytes_read = rand(10..128)
|
422
|
+
#
|
423
|
+
# # here we actualize the progress
|
424
|
+
# progress.value += bytes_read
|
425
|
+
# end
|
426
|
+
# end
|
427
|
+
#
|
428
|
+
# @example Display simple progress
|
429
|
+
# progress = ui.progress('Check some stuff')
|
430
|
+
# 10.times do
|
431
|
+
# # simulate some work
|
432
|
+
# sleep(0.1)
|
433
|
+
#
|
434
|
+
# # here we actualize the progress
|
435
|
+
# progress.step
|
436
|
+
# end
|
437
|
+
# progress.ok('Stuff checked ok')
|
438
|
+
#
|
439
|
+
# @overload progress(title, max: nil, pin: false)
|
440
|
+
# @param title [#to_s]
|
441
|
+
# title text to display
|
442
|
+
# @param max [#to_f]
|
443
|
+
# expected maximum value
|
444
|
+
# @param pin [true, false]
|
445
|
+
# whether the final progress state should be "pinned" to parent element
|
446
|
+
#
|
447
|
+
# @return [Progress]
|
448
|
+
# itself
|
449
|
+
#
|
450
|
+
# @overload progress(title, max: nil, pin: false, &block)
|
451
|
+
# @param title [#to_s]
|
452
|
+
# title text
|
453
|
+
# @param max [#to_f]
|
454
|
+
# expected maximum value
|
455
|
+
# @param pin [true, false]
|
456
|
+
# whether the final progress state should be "pinned" to parent element
|
457
|
+
#
|
458
|
+
# @yieldparam progress [Progress]
|
459
|
+
# itself
|
460
|
+
#
|
461
|
+
# @return [Object]
|
462
|
+
# the result of the given block
|
463
|
+
def progress(title, max: nil, pin: false, &block)
|
464
|
+
progress =
|
465
|
+
if Terminal.ansi?
|
466
|
+
Progress.new(self, title, max, pin)
|
467
|
+
else
|
468
|
+
DumbProgress.new(self, title, max)
|
469
|
+
end
|
470
|
+
block ? __with(progress, &block) : progress
|
471
|
+
end
|
472
|
+
|
473
|
+
#
|
474
|
+
# @!endgroup
|
475
|
+
#
|
476
|
+
|
477
|
+
#
|
478
|
+
# @!group Sub-Elements
|
479
|
+
#
|
480
|
+
|
481
|
+
# Create a visually separated section for the output of text elements.
|
482
|
+
# Like any other {Element} sections support all {Features}.
|
483
|
+
#
|
484
|
+
# @example
|
485
|
+
# ui.section do |section|
|
486
|
+
# section.h1('About Sections')
|
487
|
+
# section.space
|
488
|
+
# section.puts('Sections are areas of text elements.')
|
489
|
+
# section.puts('You can use any other feature inside such an area.')
|
490
|
+
# end
|
491
|
+
# # => ╭────╶╶╶
|
492
|
+
# # => │ ╴╶╴╶─═══ About Sections ═══─╴╶╴╶
|
493
|
+
# # => │
|
494
|
+
# # => │ Sections are areas of text elements.
|
495
|
+
# # => │ You can use any other feature inside such an area.
|
496
|
+
# # => ╰──── ─╶╶╶
|
497
|
+
#
|
498
|
+
# @param text [#to_s]
|
499
|
+
# convertible objects to print line by line
|
500
|
+
#
|
501
|
+
# @yieldparam section [Section]
|
502
|
+
# itself
|
503
|
+
#
|
504
|
+
# @return [Object]
|
505
|
+
# the result of the given block
|
506
|
+
def section(*text, &block) = __sec(:default, nil, text, &block)
|
507
|
+
|
508
|
+
# @!macro like_msg
|
509
|
+
# @see section
|
510
|
+
# @param title [#to_s]
|
511
|
+
# title to print as section head
|
512
|
+
# @param text (see section)
|
513
|
+
# @yieldparam (see section)
|
514
|
+
# @return (see section)
|
515
|
+
|
516
|
+
# Create a visually separated section with a title for the output of text
|
517
|
+
# elements.
|
518
|
+
#
|
519
|
+
# @macro like_msg
|
520
|
+
def message(title, *text, &block) = __sec(:message, title, text, &block)
|
521
|
+
alias msg message
|
522
|
+
|
523
|
+
# Create a visually separated section marked as informational with a title
|
524
|
+
# for the output of text elements.
|
525
|
+
#
|
526
|
+
# @macro like_msg
|
527
|
+
def information(title, *text, &block)
|
528
|
+
__tsec(:information, title, text, &block)
|
529
|
+
end
|
530
|
+
alias info information
|
531
|
+
|
532
|
+
# Create a visually separated section marked as a warning with a title for
|
533
|
+
# the output of text elements.
|
534
|
+
#
|
535
|
+
# @macro like_msg
|
536
|
+
def warning(title, *text, &block) = __tsec(:warning, title, text, &block)
|
537
|
+
alias warn warning
|
538
|
+
|
539
|
+
# Create a visually separated section marked as an error with a title for
|
540
|
+
# the output of text elements.
|
541
|
+
#
|
542
|
+
# @macro like_msg
|
543
|
+
def error(title, *text, &block) = __tsec(:error, title, text, &block)
|
544
|
+
alias err error
|
545
|
+
|
546
|
+
# Create a visually separated section marked as a failure with a title for
|
547
|
+
# the output of text elements.
|
548
|
+
#
|
549
|
+
# @macro like_msg
|
550
|
+
def failed(title, *text, &block) = __tsec(:failed, title, text, &block)
|
551
|
+
|
552
|
+
# Create a framed section.
|
553
|
+
#
|
554
|
+
# @param text (see section)
|
555
|
+
# @param align [:left, :right, :centered]
|
556
|
+
# text alignment,
|
557
|
+
# see {Attributes::Align}
|
558
|
+
# @param border: [Symbol]
|
559
|
+
# kind of border,
|
560
|
+
# see {Attributes::Border}
|
561
|
+
# @param border_style [Enumerable<Symbol>]
|
562
|
+
# style of border,
|
563
|
+
# see {Attributes::BorderStyle}
|
564
|
+
#
|
565
|
+
# @yieldparam frame [Framed] itself
|
566
|
+
#
|
567
|
+
# @return (see section)
|
568
|
+
def framed(*text, align: :left, border: :default, border_style: nil, &block)
|
569
|
+
__with(
|
570
|
+
Framed.new(
|
571
|
+
self,
|
572
|
+
Utils.align(align),
|
573
|
+
Theme.current.border(border),
|
574
|
+
Utils.style(border_style),
|
575
|
+
text
|
576
|
+
),
|
577
|
+
&block
|
578
|
+
)
|
579
|
+
end
|
580
|
+
|
581
|
+
# Generate a task section.
|
582
|
+
#
|
583
|
+
# @param title [#to_s]
|
584
|
+
# task title text
|
585
|
+
# @param text (see section)
|
586
|
+
# @param pin [true, false] whether to keep text "pinned"
|
587
|
+
#
|
588
|
+
# @yieldparam task [Task] itself
|
589
|
+
#
|
590
|
+
# @return (see section)
|
591
|
+
def task(title, *text, pin: false, &block)
|
592
|
+
__with(Task.new(self, title, text, pin), &block)
|
593
|
+
end
|
594
|
+
|
595
|
+
#
|
596
|
+
# @!endgroup
|
597
|
+
#
|
598
|
+
|
599
|
+
#
|
600
|
+
# @!group User Interaction
|
601
|
+
#
|
602
|
+
|
603
|
+
# Wait for user input.
|
604
|
+
#
|
605
|
+
# @example Wait until user wants to coninue
|
606
|
+
# ui.await { ui.puts '[faint][\\Press ENTER to continue...][/faint]' }
|
607
|
+
#
|
608
|
+
# @example Ask yes/no-question
|
609
|
+
# ui.await(yes: %w[j o t s y d Enter], no: %w[n Esc]) do
|
610
|
+
# ui.puts 'Do you like NayttUI?'
|
611
|
+
# end
|
612
|
+
# # => true, for user's YES
|
613
|
+
# # => false, for user's NO
|
614
|
+
# # Info:
|
615
|
+
# # The keys will work for Afrikaans, Dutch, English, French, German,
|
616
|
+
# # Italian, Polish, Portuguese, Romanian, Spanish and Swedish.
|
617
|
+
#
|
618
|
+
# @overload await(yes: 'Enter', no: 'Esc')
|
619
|
+
# @param yes [String, Enumerable<String>]
|
620
|
+
# key code/s a user can input to return positive result
|
621
|
+
# @param no [String, Enumerable<String>]
|
622
|
+
# key code/s a user can input to return negative resault
|
623
|
+
#
|
624
|
+
# @return [true, false]
|
625
|
+
# wheter the user inputs a positive result
|
626
|
+
#
|
627
|
+
# @overload await(yes: 'Enter', no: 'Esc', &block)
|
628
|
+
# @param yes [String, Enumerable<String>]
|
629
|
+
# key code/s a user can input to return positive result
|
630
|
+
# @param no [String, Enumerable<String>]
|
631
|
+
# key code/s a user can input to return negative resault
|
632
|
+
#
|
633
|
+
# @yieldparam temp [Temporary]
|
634
|
+
# temporary displayed section (section will be erased after input)
|
635
|
+
#
|
636
|
+
# @return [true, false]
|
637
|
+
# wheter the user inputs a positive result
|
638
|
+
#
|
639
|
+
def await(yes: 'Enter', no: 'Esc')
|
640
|
+
temporary do |arg|
|
641
|
+
yield(arg) if block_given?
|
642
|
+
while (key = Terminal.read_key)
|
643
|
+
if (no == key) || (no.is_a?(Enumerable) && no.include?(key))
|
644
|
+
return false
|
645
|
+
end
|
646
|
+
if (yes == key) || (yes.is_a?(Enumerable) && yes.include?(key))
|
647
|
+
return true
|
648
|
+
end
|
649
|
+
end
|
650
|
+
end
|
651
|
+
end
|
652
|
+
|
653
|
+
# Request a user's chocie.
|
654
|
+
#
|
655
|
+
# @overload choice(*choices, abortable: false)
|
656
|
+
# @param [#to_s] choices
|
657
|
+
# one or more alternatives to select from
|
658
|
+
# @param [true, false] abortable
|
659
|
+
# whether the user is allowed to abort with 'Esc' or 'Ctrl+c'
|
660
|
+
#
|
661
|
+
# @return [Integer]
|
662
|
+
# index of selected choice
|
663
|
+
# @return [nil]
|
664
|
+
# when user aborted the selection
|
665
|
+
#
|
666
|
+
# @overload choice(*choices, abortable: false, &block)
|
667
|
+
# @example Request a fruit
|
668
|
+
# ui.choice('Apple', 'Banana', 'Orange') { ui.puts 'What do you prefer?' }
|
669
|
+
# # => 0, when user likes apples
|
670
|
+
# # => 1, when bananas are user's favorite
|
671
|
+
# # => 2, when user is a oranges lover
|
672
|
+
#
|
673
|
+
# @param [#to_s] choices
|
674
|
+
# one or more alternatives to select from
|
675
|
+
# @param [true, false] abortable
|
676
|
+
# whether the user is allowed to abort with 'Esc' or 'Ctrl+c'
|
677
|
+
#
|
678
|
+
# @yieldparam temp [Temporary]
|
679
|
+
# temporary displayed section (section will be erased after input)
|
680
|
+
#
|
681
|
+
# @return [Integer]
|
682
|
+
# index of selected choice
|
683
|
+
# @return [nil]
|
684
|
+
# when user aborted the selection
|
685
|
+
#
|
686
|
+
# @overload choice(**choices, abortable: false)
|
687
|
+
# @param [#to_s] choices
|
688
|
+
# one or more alternatives to select from
|
689
|
+
# @param [true, false] abortable
|
690
|
+
# whether the user is allowed to abort with 'Esc' or 'Ctrl+c'
|
691
|
+
#
|
692
|
+
# @return [Object]
|
693
|
+
# key for selected choice
|
694
|
+
# @return [nil]
|
695
|
+
# when user aborted the selection
|
696
|
+
#
|
697
|
+
# @overload choice(**choices, abortable: false, &block)
|
698
|
+
# @example Request a preference
|
699
|
+
# ui.choice(
|
700
|
+
# k: 'Kitty',
|
701
|
+
# i: 'iTerm2',
|
702
|
+
# g: 'Ghostty',
|
703
|
+
# t: 'Tabby',
|
704
|
+
# r: 'Rio',
|
705
|
+
# abortable: true
|
706
|
+
# ) { ui.puts 'Which terminal emulator do you like?' }
|
707
|
+
# # => wheter the user selected: :k, :i, :g, :t, :r
|
708
|
+
# # => nil, when the user aborted
|
709
|
+
# @param [#to_s] choices
|
710
|
+
# one or more alternatives to select from
|
711
|
+
# @param [true, false] abortable
|
712
|
+
# whether the user is allowed to abort with 'Esc' or 'Ctrl+c'
|
713
|
+
#
|
714
|
+
# @yieldparam temp [Temporary]
|
715
|
+
# temporary displayed section (section will be erased after input)
|
716
|
+
#
|
717
|
+
# @return [Object]
|
718
|
+
# key for selected choice
|
719
|
+
# @return [nil]
|
720
|
+
# when user aborted the selection
|
721
|
+
#
|
722
|
+
def choice(*choices, abortable: false, **kwchoices, &block)
|
723
|
+
return if choices.empty? && kwchoices.empty?
|
724
|
+
choice =
|
725
|
+
case NattyUI.input_mode
|
726
|
+
when :default
|
727
|
+
Choice.new(self, choices, kwchoices, abortable)
|
728
|
+
when :dumb
|
729
|
+
DumbChoice.new(self, choices, kwchoices, abortable)
|
730
|
+
else
|
731
|
+
return
|
732
|
+
end
|
733
|
+
__with(choice) { choice.select(&block) }
|
734
|
+
end
|
735
|
+
|
736
|
+
#
|
737
|
+
# @!endgroup
|
738
|
+
#
|
739
|
+
|
740
|
+
#
|
741
|
+
# @!group Utilities
|
742
|
+
#
|
743
|
+
|
744
|
+
# @!visibility private
|
745
|
+
def columns = Terminal.columns
|
746
|
+
|
747
|
+
# Display some temporary content.
|
748
|
+
# The content displayed in the block will be erased after the block ends.
|
749
|
+
#
|
750
|
+
# @example Show tempoary information
|
751
|
+
# ui.temporary do
|
752
|
+
# ui.info('Information', 'This text will disappear when you pressed ENTER.')
|
753
|
+
# ui.await
|
754
|
+
# end
|
755
|
+
#
|
756
|
+
# @yieldparam temp [Temporary]
|
757
|
+
# itself
|
758
|
+
#
|
759
|
+
# @return (see section)
|
760
|
+
def temporary(&block) = __with(Temporary.new(self), &block)
|
761
|
+
|
762
|
+
#
|
763
|
+
# @!endgroup
|
764
|
+
#
|
765
|
+
|
766
|
+
private
|
767
|
+
|
768
|
+
def __with(element, &block) = NattyUI.__send__(:with, element, &block)
|
769
|
+
|
770
|
+
def __sec(color, title, text, &block)
|
771
|
+
__with(Section.new(self, title, text, color), &block)
|
772
|
+
end
|
773
|
+
|
774
|
+
def __tsec(color, title, text, &block)
|
775
|
+
__sec(color, "#{Theme.current.mark(color)}#{title}", text, &block)
|
776
|
+
end
|
777
|
+
|
778
|
+
EOL__ = Terminal.ansi? ? "\e[m\n" : "\n"
|
779
|
+
private_constant :EOL__
|
780
|
+
end
|
781
|
+
|
782
|
+
dir = __dir__
|
783
|
+
autoload :Choice, "#{dir}/choice.rb"
|
784
|
+
autoload :CompactLSRenderer, "#{dir}/ls_renderer.rb"
|
785
|
+
autoload :DumbChoice, "#{dir}/dumb_choice.rb"
|
786
|
+
autoload :Framed, "#{dir}/framed.rb"
|
787
|
+
autoload :LSRenderer, "#{dir}/ls_renderer.rb"
|
788
|
+
autoload :Progress, "#{dir}/progress.rb"
|
789
|
+
autoload :DumbProgress, "#{dir}/progress.rb"
|
790
|
+
autoload :Section, "#{dir}/section.rb"
|
791
|
+
autoload :Table, "#{dir}/table.rb"
|
792
|
+
autoload :Task, "#{dir}/task.rb"
|
793
|
+
autoload :Temporary, "#{dir}/temporary.rb"
|
794
|
+
autoload :Theme, "#{dir}/theme.rb"
|
795
|
+
autoload :Utils, "#{dir}/utils.rb"
|
796
|
+
|
797
|
+
private_constant :Choice, :DumbChoice, :LSRenderer, :CompactLSRenderer
|
798
|
+
end
|