cli-ui 2.2.3 → 2.3.1
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 +1 -1
- data/lib/cli/ui/ansi.rb +25 -3
- data/lib/cli/ui/color.rb +3 -0
- data/lib/cli/ui/formatter.rb +1 -0
- data/lib/cli/ui/frame/frame_stack.rb +1 -0
- data/lib/cli/ui/frame/frame_style/box.rb +15 -14
- data/lib/cli/ui/frame/frame_style/bracket.rb +14 -13
- data/lib/cli/ui/frame/frame_style.rb +3 -2
- data/lib/cli/ui/frame.rb +9 -8
- data/lib/cli/ui/glyph.rb +2 -1
- data/lib/cli/ui/os.rb +1 -0
- data/lib/cli/ui/printer.rb +1 -0
- data/lib/cli/ui/progress.rb +36 -8
- data/lib/cli/ui/prompt/interactive_options.rb +33 -12
- data/lib/cli/ui/prompt/options_handler.rb +1 -0
- data/lib/cli/ui/prompt.rb +27 -30
- data/lib/cli/ui/spinner/async.rb +1 -0
- data/lib/cli/ui/spinner/spin_group.rb +182 -51
- data/lib/cli/ui/spinner.rb +11 -5
- data/lib/cli/ui/stdout_router.rb +6 -4
- data/lib/cli/ui/table.rb +87 -0
- data/lib/cli/ui/terminal.rb +1 -0
- data/lib/cli/ui/version.rb +2 -1
- data/lib/cli/ui/widgets/base.rb +1 -0
- data/lib/cli/ui/widgets.rb +2 -1
- data/lib/cli/ui/work_queue.rb +146 -0
- data/lib/cli/ui/wrap.rb +4 -4
- data/lib/cli/ui.rb +44 -11
- metadata +5 -3
@@ -1,10 +1,13 @@
|
|
1
1
|
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative '../work_queue'
|
2
5
|
|
3
6
|
module CLI
|
4
7
|
module UI
|
5
8
|
module Spinner
|
6
9
|
class SpinGroup
|
7
|
-
DEFAULT_FINAL_GLYPH = ->(success) { success ? CLI::UI::Glyph::CHECK
|
10
|
+
DEFAULT_FINAL_GLYPH = ->(success) { success ? CLI::UI::Glyph::CHECK : CLI::UI::Glyph::X }
|
8
11
|
|
9
12
|
class << self
|
10
13
|
extend T::Sig
|
@@ -47,6 +50,11 @@ module CLI
|
|
47
50
|
# ==== Options
|
48
51
|
#
|
49
52
|
# * +:auto_debrief+ - Automatically debrief exceptions or through success_debrief? Default to true
|
53
|
+
# * +:interrupt_debrief+ - Automatically debrief on interrupt. Default to false
|
54
|
+
# * +:max_concurrent+ - Maximum number of concurrent tasks. Default is 0 (effectively unlimited)
|
55
|
+
# * +:work_queue+ - Custom WorkQueue instance. If not provided, a new one will be created
|
56
|
+
# * +:to+ - Target stream, like $stdout or $stderr. Can be anything with print and puts methods,
|
57
|
+
# or under Sorbet, IO or StringIO. Defaults to $stdout
|
50
58
|
#
|
51
59
|
# ==== Example Usage
|
52
60
|
#
|
@@ -59,16 +67,31 @@ module CLI
|
|
59
67
|
#
|
60
68
|
# https://user-images.githubusercontent.com/3074765/33798558-c452fa26-dce8-11e7-9e90-b4b34df21a46.gif
|
61
69
|
#
|
62
|
-
sig
|
63
|
-
|
70
|
+
sig do
|
71
|
+
params(
|
72
|
+
auto_debrief: T::Boolean,
|
73
|
+
interrupt_debrief: T::Boolean,
|
74
|
+
max_concurrent: Integer,
|
75
|
+
work_queue: T.nilable(WorkQueue),
|
76
|
+
to: IOLike,
|
77
|
+
).void
|
78
|
+
end
|
79
|
+
def initialize(auto_debrief: true, interrupt_debrief: false, max_concurrent: 0, work_queue: nil, to: $stdout)
|
64
80
|
@m = Mutex.new
|
65
|
-
@consumed_lines = 0
|
66
81
|
@tasks = []
|
82
|
+
@puts_above = []
|
67
83
|
@auto_debrief = auto_debrief
|
84
|
+
@interrupt_debrief = interrupt_debrief
|
68
85
|
@start = Time.new
|
86
|
+
@stopped = false
|
87
|
+
@internal_work_queue = work_queue.nil?
|
88
|
+
@work_queue = T.let(
|
89
|
+
work_queue || WorkQueue.new(max_concurrent.zero? ? 1024 : max_concurrent),
|
90
|
+
WorkQueue,
|
91
|
+
)
|
69
92
|
if block_given?
|
70
93
|
yield self
|
71
|
-
wait
|
94
|
+
wait(to: to)
|
72
95
|
end
|
73
96
|
end
|
74
97
|
|
@@ -81,6 +104,9 @@ module CLI
|
|
81
104
|
sig { returns(T::Boolean) }
|
82
105
|
attr_reader :success
|
83
106
|
|
107
|
+
sig { returns(T::Boolean) }
|
108
|
+
attr_reader :done
|
109
|
+
|
84
110
|
sig { returns(T.nilable(Exception)) }
|
85
111
|
attr_reader :exception
|
86
112
|
|
@@ -95,17 +121,18 @@ module CLI
|
|
95
121
|
sig do
|
96
122
|
params(
|
97
123
|
title: String,
|
98
|
-
final_glyph: T.proc.params(success: T::Boolean).returns(String),
|
124
|
+
final_glyph: T.proc.params(success: T::Boolean).returns(T.any(Glyph, String)),
|
99
125
|
merged_output: T::Boolean,
|
100
126
|
duplicate_output_to: IO,
|
127
|
+
work_queue: WorkQueue,
|
101
128
|
block: T.proc.params(task: Task).returns(T.untyped),
|
102
129
|
).void
|
103
130
|
end
|
104
|
-
def initialize(title, final_glyph:, merged_output:, duplicate_output_to:, &block)
|
131
|
+
def initialize(title, final_glyph:, merged_output:, duplicate_output_to:, work_queue:, &block)
|
105
132
|
@title = title
|
106
133
|
@final_glyph = final_glyph
|
107
134
|
@always_full_render = title =~ Formatter::SCAN_WIDGET
|
108
|
-
@
|
135
|
+
@future = work_queue.enqueue do
|
109
136
|
cap = CLI::UI::StdoutRouter::Capture.new(
|
110
137
|
merged_output: merged_output, duplicate_output_to: duplicate_output_to,
|
111
138
|
) { block.call(self) }
|
@@ -121,7 +148,12 @@ module CLI
|
|
121
148
|
@force_full_render = false
|
122
149
|
@done = false
|
123
150
|
@exception = nil
|
124
|
-
@success
|
151
|
+
@success = false
|
152
|
+
end
|
153
|
+
|
154
|
+
sig { params(block: T.proc.params(task: Task).void).void }
|
155
|
+
def on_done(&block)
|
156
|
+
@on_done = block
|
125
157
|
end
|
126
158
|
|
127
159
|
# Checks if a task is finished
|
@@ -129,18 +161,20 @@ module CLI
|
|
129
161
|
sig { returns(T::Boolean) }
|
130
162
|
def check
|
131
163
|
return true if @done
|
132
|
-
return false
|
164
|
+
return false unless @future.completed?
|
133
165
|
|
134
166
|
@done = true
|
135
167
|
begin
|
136
|
-
|
137
|
-
@success =
|
138
|
-
@success = false if
|
168
|
+
result = @future.value
|
169
|
+
@success = true
|
170
|
+
@success = false if result == TASK_FAILED
|
139
171
|
rescue => exc
|
140
172
|
@exception = exc
|
141
173
|
@success = false
|
142
174
|
end
|
143
175
|
|
176
|
+
@on_done&.call(self)
|
177
|
+
|
144
178
|
@done
|
145
179
|
end
|
146
180
|
|
@@ -165,7 +199,7 @@ module CLI
|
|
165
199
|
sig { params(index: Integer, force: T::Boolean, width: Integer).returns(String) }
|
166
200
|
def render(index, force = true, width: CLI::UI::Terminal.width)
|
167
201
|
@m.synchronize do
|
168
|
-
if force || @always_full_render || @force_full_render
|
202
|
+
if !CLI::UI.enable_cursor? || force || @always_full_render || @force_full_render
|
169
203
|
full_render(index, width)
|
170
204
|
else
|
171
205
|
partial_render(index)
|
@@ -190,38 +224,51 @@ module CLI
|
|
190
224
|
end
|
191
225
|
end
|
192
226
|
|
193
|
-
sig { void }
|
194
|
-
def interrupt
|
195
|
-
@thread.raise(Interrupt)
|
196
|
-
end
|
197
|
-
|
198
227
|
private
|
199
228
|
|
200
229
|
sig { params(index: Integer, terminal_width: Integer).returns(String) }
|
201
230
|
def full_render(index, terminal_width)
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
231
|
+
o = +''
|
232
|
+
|
233
|
+
o << inset
|
234
|
+
o << glyph(index)
|
235
|
+
o << ' '
|
236
|
+
|
237
|
+
truncation_width = terminal_width - CLI::UI::ANSI.printing_width(o)
|
206
238
|
|
207
|
-
|
239
|
+
o << CLI::UI.resolve_text(title, truncate_to: truncation_width)
|
240
|
+
o << ANSI.clear_to_end_of_line if CLI::UI.enable_cursor?
|
208
241
|
|
209
|
-
|
210
|
-
CLI::UI.resolve_text(title, truncate_to: truncation_width) +
|
211
|
-
"\e[K"
|
242
|
+
o
|
212
243
|
end
|
213
244
|
|
214
245
|
sig { params(index: Integer).returns(String) }
|
215
246
|
def partial_render(index)
|
216
|
-
|
247
|
+
o = +''
|
248
|
+
|
249
|
+
o << CLI::UI::ANSI.cursor_forward(inset_width)
|
250
|
+
o << glyph(index)
|
251
|
+
|
252
|
+
o
|
217
253
|
end
|
218
254
|
|
219
255
|
sig { params(index: Integer).returns(String) }
|
220
256
|
def glyph(index)
|
221
257
|
if @done
|
222
|
-
@final_glyph.call(@success)
|
258
|
+
final_glyph = @final_glyph.call(@success)
|
259
|
+
if final_glyph.is_a?(Glyph)
|
260
|
+
CLI::UI.enable_color? ? final_glyph.to_s : final_glyph.char
|
261
|
+
else
|
262
|
+
final_glyph
|
263
|
+
end
|
264
|
+
elsif CLI::UI.enable_cursor?
|
265
|
+
if !@future.started?
|
266
|
+
CLI::UI.enable_color? ? Glyph::HOURGLASS.to_s : Glyph::HOURGLASS.char
|
267
|
+
else
|
268
|
+
CLI::UI.enable_color? ? GLYPHS[index] : RUNES[index]
|
269
|
+
end
|
223
270
|
else
|
224
|
-
|
271
|
+
Glyph::HOURGLASS.char
|
225
272
|
end
|
226
273
|
end
|
227
274
|
|
@@ -251,7 +298,7 @@ module CLI
|
|
251
298
|
sig do
|
252
299
|
params(
|
253
300
|
title: String,
|
254
|
-
final_glyph: T.proc.params(success: T::Boolean).returns(String),
|
301
|
+
final_glyph: T.proc.params(success: T::Boolean).returns(T.any(Glyph, String)),
|
255
302
|
merged_output: T::Boolean,
|
256
303
|
duplicate_output_to: IO,
|
257
304
|
block: T.proc.params(task: Task).void,
|
@@ -270,23 +317,66 @@ module CLI
|
|
270
317
|
final_glyph: final_glyph,
|
271
318
|
merged_output: merged_output,
|
272
319
|
duplicate_output_to: duplicate_output_to,
|
320
|
+
work_queue: @work_queue,
|
273
321
|
&block
|
274
322
|
)
|
275
323
|
end
|
276
324
|
end
|
277
325
|
|
326
|
+
sig { void }
|
327
|
+
def stop
|
328
|
+
# If we already own the mutex (called from within another synchronized block),
|
329
|
+
# set stopped directly to avoid deadlock
|
330
|
+
if @m.owned?
|
331
|
+
return if @stopped
|
332
|
+
|
333
|
+
@stopped = true
|
334
|
+
else
|
335
|
+
@m.synchronize do
|
336
|
+
return if @stopped
|
337
|
+
|
338
|
+
@stopped = true
|
339
|
+
end
|
340
|
+
end
|
341
|
+
# Interrupt is thread-safe on its own, so we can call it outside the mutex
|
342
|
+
@work_queue.interrupt
|
343
|
+
end
|
344
|
+
|
345
|
+
sig { returns(T::Boolean) }
|
346
|
+
def stopped?
|
347
|
+
if @m.owned?
|
348
|
+
@stopped
|
349
|
+
else
|
350
|
+
@m.synchronize { @stopped }
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
278
354
|
# Tells the group you're done adding tasks and to wait for all of them to finish
|
279
355
|
#
|
356
|
+
# ==== Options
|
357
|
+
#
|
358
|
+
# * +:to+ - Target stream, like $stdout or $stderr. Can be anything with print and puts methods,
|
359
|
+
# or under Sorbet, IO or StringIO. Defaults to $stdout
|
360
|
+
#
|
280
361
|
# ==== Example Usage:
|
281
362
|
# spin_group = CLI::UI::SpinGroup.new
|
282
363
|
# spin_group.add('Title') { |spinner| sleep 1.0 }
|
283
364
|
# spin_group.wait
|
284
365
|
#
|
285
|
-
sig { returns(T::Boolean) }
|
286
|
-
def wait
|
366
|
+
sig { params(to: IOLike).returns(T::Boolean) }
|
367
|
+
def wait(to: $stdout)
|
287
368
|
idx = 0
|
288
369
|
|
370
|
+
consumed_lines = 0
|
371
|
+
|
372
|
+
@work_queue.close if @internal_work_queue
|
373
|
+
|
374
|
+
tasks_seen = @tasks.map { false }
|
375
|
+
tasks_seen_done = @tasks.map { false }
|
376
|
+
|
289
377
|
loop do
|
378
|
+
break if stopped?
|
379
|
+
|
290
380
|
done_count = 0
|
291
381
|
|
292
382
|
width = CLI::UI::Terminal.width
|
@@ -296,21 +386,46 @@ module CLI
|
|
296
386
|
|
297
387
|
@m.synchronize do
|
298
388
|
CLI::UI.raw do
|
389
|
+
force_full_render = false
|
390
|
+
|
391
|
+
unless @puts_above.empty?
|
392
|
+
to.print(CLI::UI::ANSI.cursor_up(consumed_lines)) if CLI::UI.enable_cursor?
|
393
|
+
while (message = @puts_above.shift)
|
394
|
+
to.print(CLI::UI::ANSI.insert_lines(message.lines.count)) if CLI::UI.enable_cursor?
|
395
|
+
message.lines.each do |line|
|
396
|
+
to.print(CLI::UI::Frame.prefix + CLI::UI.fmt(line))
|
397
|
+
end
|
398
|
+
to.print("\n")
|
399
|
+
end
|
400
|
+
# we descend with newlines rather than ANSI.cursor_down as the inserted lines may've
|
401
|
+
# pushed the spinner off the front of the buffer, so we can't move back down below it
|
402
|
+
to.print("\n" * consumed_lines) if CLI::UI.enable_cursor?
|
403
|
+
|
404
|
+
force_full_render = true
|
405
|
+
end
|
406
|
+
|
299
407
|
@tasks.each.with_index do |task, int_index|
|
300
408
|
nat_index = int_index + 1
|
301
409
|
task_done = task.check
|
302
410
|
done_count += 1 if task_done
|
303
411
|
|
304
|
-
if
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
412
|
+
if CLI::UI.enable_cursor?
|
413
|
+
if nat_index > consumed_lines
|
414
|
+
to.print(task.render(idx, true, width: width) + "\n")
|
415
|
+
consumed_lines += 1
|
416
|
+
else
|
417
|
+
offset = consumed_lines - int_index
|
418
|
+
move_to = CLI::UI::ANSI.cursor_up(offset) + "\r"
|
419
|
+
move_from = "\r" + CLI::UI::ANSI.cursor_down(offset)
|
420
|
+
|
421
|
+
to.print(move_to + task.render(idx, idx.zero? || force_full_render, width: width) + move_from)
|
422
|
+
end
|
423
|
+
elsif !tasks_seen[int_index] || (task_done && !tasks_seen_done[int_index])
|
424
|
+
to.print(task.render(idx, true, width: width) + "\n")
|
313
425
|
end
|
426
|
+
|
427
|
+
tasks_seen[int_index] = true
|
428
|
+
tasks_seen_done[int_index] ||= task_done
|
314
429
|
end
|
315
430
|
end
|
316
431
|
end
|
@@ -324,13 +439,21 @@ module CLI
|
|
324
439
|
end
|
325
440
|
|
326
441
|
if @auto_debrief
|
327
|
-
debrief
|
442
|
+
debrief(to: to)
|
328
443
|
else
|
329
444
|
all_succeeded?
|
330
445
|
end
|
331
446
|
rescue Interrupt
|
332
|
-
@
|
333
|
-
|
447
|
+
@work_queue.interrupt
|
448
|
+
debrief(to: to) if @interrupt_debrief
|
449
|
+
stopped? ? false : raise
|
450
|
+
end
|
451
|
+
|
452
|
+
sig { params(message: String).void }
|
453
|
+
def puts_above(message)
|
454
|
+
@m.synchronize do
|
455
|
+
@puts_above << message
|
456
|
+
end
|
334
457
|
end
|
335
458
|
|
336
459
|
# Provide an alternative debriefing for failed tasks
|
@@ -362,10 +485,17 @@ module CLI
|
|
362
485
|
|
363
486
|
# Debriefs failed tasks is +auto_debrief+ is true
|
364
487
|
#
|
365
|
-
|
366
|
-
|
488
|
+
# ==== Options
|
489
|
+
#
|
490
|
+
# * +:to+ - Target stream, like $stdout or $stderr. Can be anything with print and puts methods,
|
491
|
+
# or under Sorbet, IO or StringIO. Defaults to $stdout
|
492
|
+
#
|
493
|
+
sig { params(to: IOLike).returns(T::Boolean) }
|
494
|
+
def debrief(to: $stdout)
|
367
495
|
@m.synchronize do
|
368
496
|
@tasks.each do |task|
|
497
|
+
next unless task.done
|
498
|
+
|
369
499
|
title = task.title
|
370
500
|
out = task.stdout
|
371
501
|
err = task.stderr
|
@@ -374,22 +504,23 @@ module CLI
|
|
374
504
|
next @success_debrief&.call(title, out, err)
|
375
505
|
end
|
376
506
|
|
507
|
+
# exception will not be set if the wait loop is stopped before the task is checked
|
377
508
|
e = task.exception
|
378
509
|
next @failure_debrief.call(title, e, out, err) if @failure_debrief
|
379
510
|
|
380
511
|
CLI::UI::Frame.open('Task Failed: ' + title, color: :red, timing: Time.new - @start) do
|
381
512
|
if e
|
382
|
-
puts
|
383
|
-
puts
|
513
|
+
to.puts("#{e.class}: #{e.message}")
|
514
|
+
to.puts("\tfrom #{e.backtrace.join("\n\tfrom ")}")
|
384
515
|
end
|
385
516
|
|
386
517
|
CLI::UI::Frame.divider('STDOUT')
|
387
518
|
out = '(empty)' if out.nil? || out.strip.empty?
|
388
|
-
puts
|
519
|
+
to.puts(out)
|
389
520
|
|
390
521
|
CLI::UI::Frame.divider('STDERR')
|
391
522
|
err = '(empty)' if err.nil? || err.strip.empty?
|
392
|
-
puts
|
523
|
+
to.puts(err)
|
393
524
|
end
|
394
525
|
end
|
395
526
|
@tasks.all?(&:success)
|
data/lib/cli/ui/spinner.rb
CHANGED
@@ -22,7 +22,7 @@ module CLI
|
|
22
22
|
|
23
23
|
colors = [CLI::UI::Color::CYAN.code] * (RUNES.size / 2).ceil +
|
24
24
|
[CLI::UI::Color::MAGENTA.code] * (RUNES.size / 2).to_i
|
25
|
-
GLYPHS = colors.zip(RUNES).map
|
25
|
+
GLYPHS = colors.zip(RUNES).map { |c, r| c + r + CLI::UI::Color::RESET.code }.freeze
|
26
26
|
|
27
27
|
class << self
|
28
28
|
extend T::Sig
|
@@ -62,6 +62,8 @@ module CLI
|
|
62
62
|
# ==== Options
|
63
63
|
#
|
64
64
|
# * +:auto_debrief+ - Automatically debrief exceptions or through success_debrief? Default to true
|
65
|
+
# * +:to+ - Target stream, like $stdout or $stderr. Can be anything with print and puts methods,
|
66
|
+
# or under Sorbet, IO or StringIO. Defaults to $stdout.
|
65
67
|
#
|
66
68
|
# ==== Block
|
67
69
|
#
|
@@ -72,13 +74,17 @@ module CLI
|
|
72
74
|
# CLI::UI::Spinner.spin('Title') { sleep 1.0 }
|
73
75
|
#
|
74
76
|
sig do
|
75
|
-
params(
|
76
|
-
|
77
|
+
params(
|
78
|
+
title: String,
|
79
|
+
auto_debrief: T::Boolean,
|
80
|
+
to: IOLike,
|
81
|
+
block: T.proc.params(task: SpinGroup::Task).void,
|
82
|
+
).returns(T::Boolean)
|
77
83
|
end
|
78
|
-
def spin(title, auto_debrief: true, &block)
|
84
|
+
def spin(title, auto_debrief: true, to: $stdout, &block)
|
79
85
|
sg = SpinGroup.new(auto_debrief: auto_debrief)
|
80
86
|
sg.add(title, &block)
|
81
|
-
sg.wait
|
87
|
+
sg.wait(to: to)
|
82
88
|
end
|
83
89
|
end
|
84
90
|
end
|
data/lib/cli/ui/stdout_router.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# typed: true
|
2
|
+
# frozen_string_literal: true
|
2
3
|
|
3
4
|
require 'cli/ui'
|
4
5
|
require 'stringio'
|
@@ -18,7 +19,8 @@ module CLI
|
|
18
19
|
|
19
20
|
sig { params(args: Object).returns(Integer) }
|
20
21
|
def write(*args)
|
21
|
-
|
22
|
+
strs = args.map do |obj|
|
23
|
+
str = obj.to_s
|
22
24
|
if auto_frame_inset?
|
23
25
|
str = str.dup # unfreeze
|
24
26
|
str = str.to_s.force_encoding(Encoding::UTF_8)
|
@@ -31,13 +33,13 @@ module CLI
|
|
31
33
|
|
32
34
|
# hook return of false suppresses output.
|
33
35
|
if (hook = Thread.current[:cliui_output_hook])
|
34
|
-
return 0 if hook.call(
|
36
|
+
return 0 if hook.call(strs.join, @name) == false
|
35
37
|
end
|
36
38
|
|
37
|
-
ret = T.unsafe(@stream).write_without_cli_ui(*prepend_id(@stream,
|
39
|
+
ret = T.unsafe(@stream).write_without_cli_ui(*prepend_id(@stream, strs))
|
38
40
|
if (dup = StdoutRouter.duplicate_output_to)
|
39
41
|
begin
|
40
|
-
T.unsafe(dup).write(*prepend_id(dup,
|
42
|
+
T.unsafe(dup).write(*prepend_id(dup, strs))
|
41
43
|
rescue IOError
|
42
44
|
# Ignore
|
43
45
|
end
|
data/lib/cli/ui/table.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
3
|
+
module CLI
|
4
|
+
module UI
|
5
|
+
module Table
|
6
|
+
extend T::Sig
|
7
|
+
|
8
|
+
class << self
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
# Prints a formatted table to the specified output
|
12
|
+
# Automatically pads columns to align based on the longest cell in each column,
|
13
|
+
# ignoring the width of ANSI color codes.
|
14
|
+
#
|
15
|
+
# ==== Attributes
|
16
|
+
#
|
17
|
+
# * +table+ - (required) 2D array of strings representing the table data
|
18
|
+
#
|
19
|
+
# ==== Options
|
20
|
+
#
|
21
|
+
# * +:col_spacing+ - Number of spaces between columns. Defaults to 1
|
22
|
+
# * +:to+ - Target stream, like $stdout or $stderr. Can be anything with print and puts methods,
|
23
|
+
# or under Sorbet, IO or StringIO. Defaults to $stdout
|
24
|
+
#
|
25
|
+
# ==== Example
|
26
|
+
#
|
27
|
+
# CLI::UI::Table.puts_table([
|
28
|
+
# ["{{bold:header_1}}", "{{bold:header_2}}"],
|
29
|
+
# ["really_long_cell", "short"],
|
30
|
+
# ["row2", "row2"]
|
31
|
+
# ])
|
32
|
+
#
|
33
|
+
# Default Output:
|
34
|
+
# header_1 header_2
|
35
|
+
# really_long_cell short
|
36
|
+
# row2 row2
|
37
|
+
#
|
38
|
+
sig { params(table: T::Array[T::Array[String]], col_spacing: Integer, to: IOLike).void }
|
39
|
+
def puts_table(table, col_spacing: 1, to: $stdout)
|
40
|
+
col_sizes = table.transpose.map do |col|
|
41
|
+
col.map { |cell| CLI::UI::ANSI.printing_width(CLI::UI.resolve_text(cell)) }.max
|
42
|
+
end
|
43
|
+
|
44
|
+
table.each do |row|
|
45
|
+
padded_row = row.each_with_index.map do |cell, i|
|
46
|
+
col_size = T.must(col_sizes[i]) # guaranteed to be non-nil
|
47
|
+
cell_size = CLI::UI::ANSI.printing_width(CLI::UI.resolve_text(cell))
|
48
|
+
padded_cell = cell + ' ' * (col_size - cell_size)
|
49
|
+
padded_cell
|
50
|
+
end
|
51
|
+
CLI::UI.puts(padded_row.join(' ' * col_spacing), to: to)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Captures a table's output as an array of strings without printing to the terminal
|
56
|
+
# Can be used to further manipulate or format the table output
|
57
|
+
#
|
58
|
+
# ==== Attributes
|
59
|
+
#
|
60
|
+
# * +table+ - (required) 2D array of strings representing the table data
|
61
|
+
#
|
62
|
+
# ==== Options
|
63
|
+
#
|
64
|
+
# * +:col_spacing+ - Number of spaces between columns. Defaults to 1
|
65
|
+
#
|
66
|
+
# ==== Returns
|
67
|
+
#
|
68
|
+
# * +Array[String]+ - Array of strings, each representing a row of the formatted table
|
69
|
+
#
|
70
|
+
# ==== Example
|
71
|
+
#
|
72
|
+
# CLI::UI::Table.capture_table([
|
73
|
+
# ["{{bold:header_1}}", "{{bold:header_2}}"],
|
74
|
+
# ["really_long_cell", "short"],
|
75
|
+
# ["row2", "row2"]
|
76
|
+
# ])
|
77
|
+
#
|
78
|
+
sig { params(table: T::Array[T::Array[String]], col_spacing: Integer).returns(T::Array[String]) }
|
79
|
+
def capture_table(table, col_spacing: 1)
|
80
|
+
strio = StringIO.new
|
81
|
+
puts_table(table, col_spacing: col_spacing, to: strio)
|
82
|
+
strio.string.lines.map(&:chomp)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
data/lib/cli/ui/terminal.rb
CHANGED
data/lib/cli/ui/version.rb
CHANGED
data/lib/cli/ui/widgets/base.rb
CHANGED
data/lib/cli/ui/widgets.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# typed: true
|
2
|
+
# frozen_string_literal: true
|
2
3
|
|
3
4
|
require('cli/ui')
|
4
5
|
|
@@ -80,7 +81,7 @@ module CLI
|
|
80
81
|
|
81
82
|
sig { params(argstring: String, pattern: Regexp).void }
|
82
83
|
def initialize(argstring, pattern)
|
83
|
-
super
|
84
|
+
super(nil)
|
84
85
|
@argstring = argstring
|
85
86
|
@pattern = pattern
|
86
87
|
end
|