cli-ui 1.5.1 → 2.0.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 +17 -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 +67 -11
- 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,7 +1,11 @@
|
|
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
|
#
|
@@ -11,25 +15,39 @@ module CLI
|
|
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,46 @@ 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
|
+
sig { returns(T::Boolean) }
|
277
|
+
def all_succeeded?
|
278
|
+
@m.synchronize do
|
279
|
+
@tasks.all?(&:success)
|
228
280
|
end
|
229
281
|
end
|
230
282
|
|
231
283
|
# Debriefs failed tasks is +auto_debrief+ is true
|
232
284
|
#
|
285
|
+
sig { returns(T::Boolean) }
|
233
286
|
def debrief
|
234
287
|
@m.synchronize do
|
235
288
|
@tasks.each do |task|
|
236
289
|
next if task.success
|
237
290
|
|
291
|
+
title = task.title
|
238
292
|
e = task.exception
|
239
293
|
out = task.stdout
|
240
294
|
err = task.stderr
|
241
295
|
|
242
|
-
|
296
|
+
next @failure_debrief.call(title, e, out, err) if @failure_debrief
|
297
|
+
|
298
|
+
CLI::UI::Frame.open('Task Failed: ' + title, color: :red, timing: Time.new - @start) do
|
243
299
|
if e
|
244
300
|
puts "#{e.class}: #{e.message}"
|
245
301
|
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? 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
|