cli-ui 2.2.3 → 2.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 +13 -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 +8 -6
@@ -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
|