cli-ui 1.5.1 → 2.1.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/README.md +23 -17
- data/lib/cli/ui/ansi.rb +157 -129
- data/lib/cli/ui/color.rb +39 -20
- data/lib/cli/ui/formatter.rb +45 -21
- data/lib/cli/ui/frame/frame_stack.rb +32 -13
- data/lib/cli/ui/frame/frame_style/box.rb +15 -4
- data/lib/cli/ui/frame/frame_style/bracket.rb +18 -7
- data/lib/cli/ui/frame/frame_style.rb +84 -87
- data/lib/cli/ui/frame.rb +55 -24
- data/lib/cli/ui/glyph.rb +44 -31
- data/lib/cli/ui/os.rb +44 -48
- data/lib/cli/ui/printer.rb +65 -47
- data/lib/cli/ui/progress.rb +49 -32
- data/lib/cli/ui/prompt/interactive_options.rb +91 -44
- data/lib/cli/ui/prompt/options_handler.rb +8 -0
- data/lib/cli/ui/prompt.rb +84 -31
- data/lib/cli/ui/sorbet_runtime_stub.rb +157 -0
- data/lib/cli/ui/spinner/async.rb +15 -4
- data/lib/cli/ui/spinner/spin_group.rb +83 -15
- data/lib/cli/ui/spinner.rb +48 -28
- data/lib/cli/ui/stdout_router.rb +71 -34
- data/lib/cli/ui/terminal.rb +37 -25
- data/lib/cli/ui/truncater.rb +7 -2
- data/lib/cli/ui/version.rb +3 -1
- data/lib/cli/ui/widgets/base.rb +23 -4
- data/lib/cli/ui/widgets/status.rb +19 -1
- data/lib/cli/ui/widgets.rb +42 -23
- data/lib/cli/ui/wrap.rb +8 -1
- data/lib/cli/ui.rb +325 -188
- metadata +10 -9
@@ -0,0 +1,157 @@
|
|
1
|
+
# typed: ignore
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module T
|
5
|
+
class << self
|
6
|
+
def absurd(value); end
|
7
|
+
def all(type_a, type_b, *types); end
|
8
|
+
def any(type_a, type_b, *types); end
|
9
|
+
def attached_class; end
|
10
|
+
def class_of(klass); end
|
11
|
+
def enum(values); end
|
12
|
+
def nilable(type); end
|
13
|
+
def noreturn; end
|
14
|
+
def self_type; end
|
15
|
+
def type_alias(type = nil, &_blk); end
|
16
|
+
def type_parameter(name); end
|
17
|
+
def untyped; end
|
18
|
+
|
19
|
+
def assert_type!(value, _type, _checked: true)
|
20
|
+
value
|
21
|
+
end
|
22
|
+
|
23
|
+
def cast(value, _type, _checked: true)
|
24
|
+
value
|
25
|
+
end
|
26
|
+
|
27
|
+
def let(value, _type, _checked: true)
|
28
|
+
value
|
29
|
+
end
|
30
|
+
|
31
|
+
def must(arg, _msg = nil)
|
32
|
+
arg
|
33
|
+
end
|
34
|
+
|
35
|
+
def proc
|
36
|
+
T::Proc.new
|
37
|
+
end
|
38
|
+
|
39
|
+
def reveal_type(value)
|
40
|
+
value
|
41
|
+
end
|
42
|
+
|
43
|
+
def unsafe(value)
|
44
|
+
value
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
module Sig
|
49
|
+
def sig(arg0 = nil, &blk); end
|
50
|
+
end
|
51
|
+
|
52
|
+
module Helpers
|
53
|
+
def abstract!; end
|
54
|
+
def interface!; end
|
55
|
+
def final!; end
|
56
|
+
def sealed!; end
|
57
|
+
def mixes_in_class_methods(mod); end
|
58
|
+
end
|
59
|
+
|
60
|
+
module Generic
|
61
|
+
include(T::Helpers)
|
62
|
+
|
63
|
+
def type_parameters(*params); end
|
64
|
+
def type_member(variance = :invariant, fixed: nil, lower: nil, upper: BasicObject); end
|
65
|
+
def type_template(variance = :invariant, fixed: nil, lower: nil, upper: BasicObject); end
|
66
|
+
|
67
|
+
def [](*types)
|
68
|
+
self
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
module Array
|
73
|
+
class << self
|
74
|
+
def [](type); end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
Boolean = Object.new.freeze
|
79
|
+
|
80
|
+
module Configuration
|
81
|
+
class << self
|
82
|
+
def call_validation_error_handler(signature, opts); end
|
83
|
+
def call_validation_error_handler=(value); end
|
84
|
+
def default_checked_level=(default_checked_level); end
|
85
|
+
def enable_checking_for_sigs_marked_checked_tests; end
|
86
|
+
def enable_final_checks_on_hooks; end
|
87
|
+
def enable_legacy_t_enum_migration_mode; end
|
88
|
+
def reset_final_checks_on_hooks; end
|
89
|
+
def hard_assert_handler(str, extra); end
|
90
|
+
def hard_assert_handler=(value); end
|
91
|
+
def inline_type_error_handler(error); end
|
92
|
+
def inline_type_error_handler=(value); end
|
93
|
+
def log_info_handler(str, extra); end
|
94
|
+
def log_info_handler=(value); end
|
95
|
+
def scalar_types; end
|
96
|
+
def scalar_types=(values); end
|
97
|
+
# rubocop:disable Naming/InclusiveLanguage
|
98
|
+
def sealed_violation_whitelist; end
|
99
|
+
def sealed_violation_whitelist=(sealed_violation_whitelist); end
|
100
|
+
# rubocop:enable Naming/InclusiveLanguage
|
101
|
+
def sig_builder_error_handler(error, location); end
|
102
|
+
def sig_builder_error_handler=(value); end
|
103
|
+
def sig_validation_error_handler(error, opts); end
|
104
|
+
def sig_validation_error_handler=(value); end
|
105
|
+
def soft_assert_handler(str, extra); end
|
106
|
+
def soft_assert_handler=(value); end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
module Enumerable
|
111
|
+
class << self
|
112
|
+
def [](type); end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
module Enumerator
|
117
|
+
class << self
|
118
|
+
def [](type); end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
module Hash
|
123
|
+
class << self
|
124
|
+
def [](keys, values); end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
class Proc
|
129
|
+
def bind(*_)
|
130
|
+
self
|
131
|
+
end
|
132
|
+
|
133
|
+
def params(*_param)
|
134
|
+
self
|
135
|
+
end
|
136
|
+
|
137
|
+
def void
|
138
|
+
self
|
139
|
+
end
|
140
|
+
|
141
|
+
def returns(_type)
|
142
|
+
self
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
module Range
|
147
|
+
class << self
|
148
|
+
def [](type); end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
module Set
|
153
|
+
class << self
|
154
|
+
def [](type); end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
data/lib/cli/ui/spinner/async.rb
CHANGED
@@ -1,11 +1,20 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
1
3
|
module CLI
|
2
4
|
module UI
|
3
5
|
module Spinner
|
4
6
|
class Async
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
class << self
|
10
|
+
extend T::Sig
|
11
|
+
|
12
|
+
# Convenience method for +initialize+
|
13
|
+
#
|
14
|
+
sig { params(title: String).returns(Async) }
|
15
|
+
def start(title)
|
16
|
+
new(title)
|
17
|
+
end
|
9
18
|
end
|
10
19
|
|
11
20
|
# Initializes a new asynchronous spinner with no specific end.
|
@@ -19,6 +28,7 @@ module CLI
|
|
19
28
|
#
|
20
29
|
# CLI::UI::Spinner::Async.new('Title')
|
21
30
|
#
|
31
|
+
sig { params(title: String).void }
|
22
32
|
def initialize(title)
|
23
33
|
require 'thread'
|
24
34
|
sg = CLI::UI::Spinner::SpinGroup.new
|
@@ -30,6 +40,7 @@ module CLI
|
|
30
40
|
|
31
41
|
# Stops an asynchronous spinner
|
32
42
|
#
|
43
|
+
sig { returns(T::Boolean) }
|
33
44
|
def stop
|
34
45
|
@m.synchronize { @cv.signal }
|
35
46
|
@t.value
|
@@ -1,35 +1,53 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
1
3
|
module CLI
|
2
4
|
module UI
|
3
5
|
module Spinner
|
4
6
|
class SpinGroup
|
7
|
+
extend T::Sig
|
8
|
+
|
5
9
|
# Initializes a new spin group
|
6
10
|
# This lets you add +Task+ objects to the group to multi-thread work
|
7
11
|
#
|
8
12
|
# ==== Options
|
9
13
|
#
|
10
|
-
# * +:auto_debrief+ - Automatically debrief exceptions? Default to true
|
14
|
+
# * +:auto_debrief+ - Automatically debrief exceptions or through success_debrief? Default to true
|
11
15
|
#
|
12
16
|
# ==== Example Usage
|
13
17
|
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
+
# CLI::UI::SpinGroup.new do |spin_group|
|
19
|
+
# spin_group.add('Title') { |spinner| sleep 3.0 }
|
20
|
+
# spin_group.add('Title 2') { |spinner| sleep 3.0; spinner.update_title('New Title'); sleep 3.0 }
|
21
|
+
# end
|
18
22
|
#
|
19
23
|
# Output:
|
20
24
|
#
|
21
25
|
# https://user-images.githubusercontent.com/3074765/33798558-c452fa26-dce8-11e7-9e90-b4b34df21a46.gif
|
22
26
|
#
|
27
|
+
sig { params(auto_debrief: T::Boolean).void }
|
23
28
|
def initialize(auto_debrief: true)
|
24
29
|
@m = Mutex.new
|
25
30
|
@consumed_lines = 0
|
26
31
|
@tasks = []
|
27
32
|
@auto_debrief = auto_debrief
|
28
33
|
@start = Time.new
|
34
|
+
if block_given?
|
35
|
+
yield self
|
36
|
+
wait
|
37
|
+
end
|
29
38
|
end
|
30
39
|
|
31
40
|
class Task
|
32
|
-
|
41
|
+
extend T::Sig
|
42
|
+
|
43
|
+
sig { returns(String) }
|
44
|
+
attr_reader :title, :stdout, :stderr
|
45
|
+
|
46
|
+
sig { returns(T::Boolean) }
|
47
|
+
attr_reader :success
|
48
|
+
|
49
|
+
sig { returns(T.nilable(Exception)) }
|
50
|
+
attr_reader :exception
|
33
51
|
|
34
52
|
# Initializes a new Task
|
35
53
|
# This is managed entirely internally by +SpinGroup+
|
@@ -39,11 +57,12 @@ module CLI
|
|
39
57
|
# * +title+ - Title of the task
|
40
58
|
# * +block+ - Block for the task, will be provided with an instance of the spinner
|
41
59
|
#
|
60
|
+
sig { params(title: String, block: T.proc.params(task: Task).returns(T.untyped)).void }
|
42
61
|
def initialize(title, &block)
|
43
62
|
@title = title
|
44
63
|
@always_full_render = title =~ Formatter::SCAN_WIDGET
|
45
64
|
@thread = Thread.new do
|
46
|
-
cap = CLI::UI::StdoutRouter::Capture.new(
|
65
|
+
cap = CLI::UI::StdoutRouter::Capture.new(with_frame_inset: false) { block.call(self) }
|
47
66
|
begin
|
48
67
|
cap.run
|
49
68
|
ensure
|
@@ -61,6 +80,7 @@ module CLI
|
|
61
80
|
|
62
81
|
# Checks if a task is finished
|
63
82
|
#
|
83
|
+
sig { returns(T::Boolean) }
|
64
84
|
def check
|
65
85
|
return true if @done
|
66
86
|
return false if @thread.alive?
|
@@ -96,6 +116,7 @@ module CLI
|
|
96
116
|
# * +force+ - force rerender of the task
|
97
117
|
# * +width+ - current terminal width to format for
|
98
118
|
#
|
119
|
+
sig { params(index: Integer, force: T::Boolean, width: Integer).returns(String) }
|
99
120
|
def render(index, force = true, width: CLI::UI::Terminal.width)
|
100
121
|
@m.synchronize do
|
101
122
|
if force || @always_full_render || @force_full_render
|
@@ -114,6 +135,7 @@ module CLI
|
|
114
135
|
#
|
115
136
|
# * +title+ - title to change the spinner to
|
116
137
|
#
|
138
|
+
sig { params(new_title: String).void }
|
117
139
|
def update_title(new_title)
|
118
140
|
@m.synchronize do
|
119
141
|
@always_full_render = new_title =~ Formatter::SCAN_WIDGET
|
@@ -122,8 +144,14 @@ module CLI
|
|
122
144
|
end
|
123
145
|
end
|
124
146
|
|
147
|
+
sig { void }
|
148
|
+
def interrupt
|
149
|
+
@thread.raise(Interrupt)
|
150
|
+
end
|
151
|
+
|
125
152
|
private
|
126
153
|
|
154
|
+
sig { params(index: Integer, terminal_width: Integer).returns(String) }
|
127
155
|
def full_render(index, terminal_width)
|
128
156
|
prefix = inset +
|
129
157
|
glyph(index) +
|
@@ -137,10 +165,12 @@ module CLI
|
|
137
165
|
"\e[K"
|
138
166
|
end
|
139
167
|
|
168
|
+
sig { params(index: Integer).returns(String) }
|
140
169
|
def partial_render(index)
|
141
170
|
CLI::UI::ANSI.cursor_forward(inset_width) + glyph(index) + CLI::UI::Color::RESET.code
|
142
171
|
end
|
143
172
|
|
173
|
+
sig { params(index: Integer).returns(String) }
|
144
174
|
def glyph(index)
|
145
175
|
if @done
|
146
176
|
@success ? CLI::UI::Glyph::CHECK.to_s : CLI::UI::Glyph::X.to_s
|
@@ -149,10 +179,12 @@ module CLI
|
|
149
179
|
end
|
150
180
|
end
|
151
181
|
|
182
|
+
sig { returns(String) }
|
152
183
|
def inset
|
153
184
|
@inset ||= CLI::UI::Frame.prefix
|
154
185
|
end
|
155
186
|
|
187
|
+
sig { returns(Integer) }
|
156
188
|
def inset_width
|
157
189
|
@inset_width ||= CLI::UI::ANSI.printing_width(inset)
|
158
190
|
end
|
@@ -170,6 +202,7 @@ module CLI
|
|
170
202
|
# spin_group.add('Title') { |spinner| sleep 1.0 }
|
171
203
|
# spin_group.wait
|
172
204
|
#
|
205
|
+
sig { params(title: String, block: T.proc.params(task: Task).void).void }
|
173
206
|
def add(title, &block)
|
174
207
|
@m.synchronize do
|
175
208
|
@tasks << Task.new(title, &block)
|
@@ -183,11 +216,12 @@ module CLI
|
|
183
216
|
# spin_group.add('Title') { |spinner| sleep 1.0 }
|
184
217
|
# spin_group.wait
|
185
218
|
#
|
219
|
+
sig { returns(T::Boolean) }
|
186
220
|
def wait
|
187
221
|
idx = 0
|
188
222
|
|
189
223
|
loop do
|
190
|
-
all_done = true
|
224
|
+
all_done = T.let(true, T::Boolean)
|
191
225
|
|
192
226
|
width = CLI::UI::Terminal.width
|
193
227
|
|
@@ -222,24 +256,58 @@ module CLI
|
|
222
256
|
if @auto_debrief
|
223
257
|
debrief
|
224
258
|
else
|
225
|
-
|
226
|
-
|
227
|
-
|
259
|
+
all_succeeded?
|
260
|
+
end
|
261
|
+
rescue Interrupt
|
262
|
+
@tasks.each(&:interrupt)
|
263
|
+
raise
|
264
|
+
end
|
265
|
+
|
266
|
+
# Provide an alternative debriefing for failed tasks
|
267
|
+
sig do
|
268
|
+
params(
|
269
|
+
block: T.proc.params(title: String, exception: T.nilable(Exception), out: String, err: String).void,
|
270
|
+
).void
|
271
|
+
end
|
272
|
+
def failure_debrief(&block)
|
273
|
+
@failure_debrief = block
|
274
|
+
end
|
275
|
+
|
276
|
+
# Provide a debriefing for successful tasks
|
277
|
+
sig do
|
278
|
+
params(
|
279
|
+
block: T.proc.params(title: String, out: String, err: String).void,
|
280
|
+
).void
|
281
|
+
end
|
282
|
+
def success_debrief(&block)
|
283
|
+
@success_debrief = block
|
284
|
+
end
|
285
|
+
|
286
|
+
sig { returns(T::Boolean) }
|
287
|
+
def all_succeeded?
|
288
|
+
@m.synchronize do
|
289
|
+
@tasks.all?(&:success)
|
228
290
|
end
|
229
291
|
end
|
230
292
|
|
231
293
|
# Debriefs failed tasks is +auto_debrief+ is true
|
232
294
|
#
|
295
|
+
sig { returns(T::Boolean) }
|
233
296
|
def debrief
|
234
297
|
@m.synchronize do
|
235
298
|
@tasks.each do |task|
|
236
|
-
|
237
|
-
|
238
|
-
e = task.exception
|
299
|
+
title = task.title
|
239
300
|
out = task.stdout
|
240
301
|
err = task.stderr
|
241
302
|
|
242
|
-
|
303
|
+
if task.success
|
304
|
+
next @success_debrief&.call(title, out, err)
|
305
|
+
end
|
306
|
+
|
307
|
+
e = task.exception
|
308
|
+
next @failure_debrief.call(title, e, out, err) if @failure_debrief
|
309
|
+
|
310
|
+
CLI::UI::Frame.open('Task Failed: ' + title, color: :red, timing: Time.new - @start) do
|
243
311
|
if e
|
244
312
|
puts "#{e.class}: #{e.message}"
|
245
313
|
puts "\tfrom #{e.backtrace.join("\n\tfrom ")}"
|
data/lib/cli/ui/spinner.rb
CHANGED
@@ -1,22 +1,33 @@
|
|
1
|
-
#
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
2
4
|
require 'cli/ui'
|
3
5
|
|
4
6
|
module CLI
|
5
7
|
module UI
|
6
8
|
module Spinner
|
9
|
+
extend T::Sig
|
10
|
+
|
7
11
|
autoload :Async, 'cli/ui/spinner/async'
|
8
12
|
autoload :SpinGroup, 'cli/ui/spinner/spin_group'
|
9
13
|
|
10
14
|
PERIOD = 0.1 # seconds
|
11
15
|
TASK_FAILED = :task_failed
|
12
16
|
|
13
|
-
RUNES = CLI::UI::OS.current.
|
17
|
+
RUNES = if CLI::UI::OS.current.use_emoji?
|
18
|
+
['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'].freeze
|
19
|
+
else
|
20
|
+
['\\', '|', '/', '-', '\\', '|', '/', '-'].freeze
|
21
|
+
end
|
14
22
|
|
15
23
|
colors = [CLI::UI::Color::CYAN.code] * (RUNES.size / 2).ceil +
|
16
24
|
[CLI::UI::Color::MAGENTA.code] * (RUNES.size / 2).to_i
|
17
25
|
GLYPHS = colors.zip(RUNES).map(&:join)
|
18
26
|
|
19
27
|
class << self
|
28
|
+
extend T::Sig
|
29
|
+
|
30
|
+
sig { returns(T.nilable(Integer)) }
|
20
31
|
attr_accessor(:index)
|
21
32
|
|
22
33
|
# We use this from CLI::UI::Widgets::Status to render an additional
|
@@ -29,37 +40,46 @@ module CLI
|
|
29
40
|
# While it would be possible to stitch through some connection between
|
30
41
|
# the SpinGroup and the Widgets included in its title, this is simpler
|
31
42
|
# in practice and seems unlikely to cause issues in practice.
|
43
|
+
sig { returns(String) }
|
32
44
|
def current_rune
|
33
45
|
RUNES[index || 0]
|
34
46
|
end
|
35
47
|
end
|
36
48
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
49
|
+
class << self
|
50
|
+
extend T::Sig
|
51
|
+
|
52
|
+
# Adds a single spinner
|
53
|
+
# Uses an interactive session to allow the user to pick an answer
|
54
|
+
# Can use arrows, y/n, numbers (1/2), and vim bindings to control
|
55
|
+
#
|
56
|
+
# https://user-images.githubusercontent.com/3074765/33798295-d94fd822-dce3-11e7-819b-43e5502d490e.gif
|
57
|
+
#
|
58
|
+
# ==== Attributes
|
59
|
+
#
|
60
|
+
# * +title+ - Title of the spinner to use
|
61
|
+
#
|
62
|
+
# ==== Options
|
63
|
+
#
|
64
|
+
# * +:auto_debrief+ - Automatically debrief exceptions or through success_debrief? Default to true
|
65
|
+
#
|
66
|
+
# ==== Block
|
67
|
+
#
|
68
|
+
# * *spinner+ - Instance of the spinner. Can call +update_title+ to update the user of changes
|
69
|
+
#
|
70
|
+
# ==== Example Usage:
|
71
|
+
#
|
72
|
+
# CLI::UI::Spinner.spin('Title') { sleep 1.0 }
|
73
|
+
#
|
74
|
+
sig do
|
75
|
+
params(title: String, auto_debrief: T::Boolean, block: T.proc.params(task: SpinGroup::Task).void)
|
76
|
+
.returns(T::Boolean)
|
77
|
+
end
|
78
|
+
def spin(title, auto_debrief: true, &block)
|
79
|
+
sg = SpinGroup.new(auto_debrief: auto_debrief)
|
80
|
+
sg.add(title, &block)
|
81
|
+
sg.wait
|
82
|
+
end
|
63
83
|
end
|
64
84
|
end
|
65
85
|
end
|