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.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +23 -24
  4. data/examples/24bit-colors.rb +4 -9
  5. data/examples/3bit-colors.rb +28 -8
  6. data/examples/8bit-colors.rb +18 -23
  7. data/examples/attributes.rb +30 -25
  8. data/examples/cols.rb +40 -0
  9. data/examples/elements.rb +31 -0
  10. data/examples/examples.rb +45 -0
  11. data/examples/illustration.rb +56 -54
  12. data/examples/ls.rb +16 -18
  13. data/examples/named-colors.rb +23 -0
  14. data/examples/sections.rb +29 -0
  15. data/examples/tables.rb +62 -0
  16. data/examples/tasks.rb +52 -0
  17. data/lib/natty-ui/attributes.rb +604 -0
  18. data/lib/natty-ui/choice.rb +56 -0
  19. data/lib/natty-ui/dumb_choice.rb +45 -0
  20. data/lib/natty-ui/element.rb +78 -0
  21. data/lib/natty-ui/features.rb +798 -0
  22. data/lib/natty-ui/framed.rb +51 -0
  23. data/lib/natty-ui/ls_renderer.rb +93 -0
  24. data/lib/natty-ui/progress.rb +187 -0
  25. data/lib/natty-ui/section.rb +69 -0
  26. data/lib/natty-ui/table.rb +241 -0
  27. data/lib/natty-ui/table_renderer.rb +147 -0
  28. data/lib/natty-ui/task.rb +44 -0
  29. data/lib/natty-ui/temporary.rb +38 -0
  30. data/lib/natty-ui/theme.rb +303 -0
  31. data/lib/natty-ui/utils.rb +79 -0
  32. data/lib/natty-ui/version.rb +1 -1
  33. data/lib/natty-ui/width_finder.rb +125 -0
  34. data/lib/natty-ui.rb +89 -147
  35. metadata +47 -56
  36. data/examples/animate.rb +0 -44
  37. data/examples/attributes_list.rb +0 -14
  38. data/examples/demo.rb +0 -53
  39. data/examples/message.rb +0 -32
  40. data/examples/progress.rb +0 -68
  41. data/examples/query.rb +0 -41
  42. data/examples/read_key.rb +0 -13
  43. data/examples/table.rb +0 -41
  44. data/lib/natty-ui/animation/binary.rb +0 -36
  45. data/lib/natty-ui/animation/default.rb +0 -38
  46. data/lib/natty-ui/animation/matrix.rb +0 -51
  47. data/lib/natty-ui/animation/rainbow.rb +0 -28
  48. data/lib/natty-ui/animation/type_writer.rb +0 -44
  49. data/lib/natty-ui/animation.rb +0 -69
  50. data/lib/natty-ui/ansi/constants.rb +0 -75
  51. data/lib/natty-ui/ansi.rb +0 -521
  52. data/lib/natty-ui/ansi_wrapper.rb +0 -199
  53. data/lib/natty-ui/frame.rb +0 -53
  54. data/lib/natty-ui/glyph.rb +0 -64
  55. data/lib/natty-ui/key_map.rb +0 -142
  56. data/lib/natty-ui/preload.rb +0 -12
  57. data/lib/natty-ui/spinner.rb +0 -120
  58. data/lib/natty-ui/text/east_asian_width.rb +0 -2529
  59. data/lib/natty-ui/text.rb +0 -203
  60. data/lib/natty-ui/wrapper/animate.rb +0 -17
  61. data/lib/natty-ui/wrapper/ask.rb +0 -78
  62. data/lib/natty-ui/wrapper/element.rb +0 -79
  63. data/lib/natty-ui/wrapper/features.rb +0 -21
  64. data/lib/natty-ui/wrapper/framed.rb +0 -45
  65. data/lib/natty-ui/wrapper/heading.rb +0 -60
  66. data/lib/natty-ui/wrapper/horizontal_rule.rb +0 -37
  67. data/lib/natty-ui/wrapper/list_in_columns.rb +0 -138
  68. data/lib/natty-ui/wrapper/message.rb +0 -109
  69. data/lib/natty-ui/wrapper/mixins.rb +0 -67
  70. data/lib/natty-ui/wrapper/progress.rb +0 -74
  71. data/lib/natty-ui/wrapper/query.rb +0 -89
  72. data/lib/natty-ui/wrapper/quote.rb +0 -25
  73. data/lib/natty-ui/wrapper/request.rb +0 -54
  74. data/lib/natty-ui/wrapper/section.rb +0 -118
  75. data/lib/natty-ui/wrapper/table.rb +0 -551
  76. data/lib/natty-ui/wrapper/task.rb +0 -55
  77. 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