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
data/lib/cli/ui/frame.rb
CHANGED
@@ -1,4 +1,7 @@
|
|
1
1
|
# coding: utf-8
|
2
|
+
|
3
|
+
# typed: true
|
4
|
+
|
2
5
|
require 'cli/ui'
|
3
6
|
require 'cli/ui/frame/frame_stack'
|
4
7
|
require 'cli/ui/frame/frame_style'
|
@@ -7,9 +10,12 @@ module CLI
|
|
7
10
|
module UI
|
8
11
|
module Frame
|
9
12
|
class UnnestedFrameException < StandardError; end
|
13
|
+
DEFAULT_FRAME_COLOR = CLI::UI.resolve_color(:cyan)
|
14
|
+
|
10
15
|
class << self
|
11
|
-
|
16
|
+
extend T::Sig
|
12
17
|
|
18
|
+
sig { returns(FrameStyle) }
|
13
19
|
def frame_style
|
14
20
|
@frame_style ||= FrameStyle::Box
|
15
21
|
end
|
@@ -22,6 +28,7 @@ module CLI
|
|
22
28
|
#
|
23
29
|
# * +symbol+ or +FrameStyle+ - the default frame style to use for frames
|
24
30
|
#
|
31
|
+
sig { params(frame_style: FrameStylable).void }
|
25
32
|
def frame_style=(frame_style)
|
26
33
|
@frame_style = CLI::UI.resolve_style(frame_style)
|
27
34
|
end
|
@@ -67,13 +74,25 @@ module CLI
|
|
67
74
|
# ┏━━ Open ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
68
75
|
#
|
69
76
|
#
|
77
|
+
sig do
|
78
|
+
type_parameters(:T).params(
|
79
|
+
text: String,
|
80
|
+
color: Colorable,
|
81
|
+
failure_text: T.nilable(String),
|
82
|
+
success_text: T.nilable(String),
|
83
|
+
timing: T.any(T::Boolean, Numeric),
|
84
|
+
frame_style: FrameStylable,
|
85
|
+
block: T.nilable(T.proc.returns(T.type_parameter(:T))),
|
86
|
+
).returns(T.nilable(T.type_parameter(:T)))
|
87
|
+
end
|
70
88
|
def open(
|
71
89
|
text,
|
72
90
|
color: DEFAULT_FRAME_COLOR,
|
73
91
|
failure_text: nil,
|
74
92
|
success_text: nil,
|
75
|
-
timing:
|
76
|
-
frame_style:
|
93
|
+
timing: block_given?,
|
94
|
+
frame_style: self.frame_style,
|
95
|
+
&block
|
77
96
|
)
|
78
97
|
frame_style = CLI::UI.resolve_style(frame_style)
|
79
98
|
color = CLI::UI.resolve_color(color)
|
@@ -91,7 +110,7 @@ module CLI
|
|
91
110
|
t_start = Time.now
|
92
111
|
CLI::UI.raw do
|
93
112
|
print(prefix.chop)
|
94
|
-
puts frame_style.
|
113
|
+
puts frame_style.start(text, color: color)
|
95
114
|
end
|
96
115
|
FrameStack.push(color: color, style: frame_style)
|
97
116
|
|
@@ -103,15 +122,15 @@ module CLI
|
|
103
122
|
success = yield
|
104
123
|
rescue
|
105
124
|
closed = true
|
106
|
-
t_diff =
|
125
|
+
t_diff = elapsed(t_start, timing)
|
107
126
|
close(failure_text, color: :red, elapsed: t_diff)
|
108
127
|
raise
|
109
128
|
else
|
110
129
|
success
|
111
130
|
ensure
|
112
131
|
unless closed
|
113
|
-
t_diff =
|
114
|
-
if success != false
|
132
|
+
t_diff = elapsed(t_start, timing)
|
133
|
+
if T.unsafe(success) != false
|
115
134
|
close(success_text, color: color, elapsed: t_diff)
|
116
135
|
else
|
117
136
|
close(failure_text, color: :red, elapsed: t_diff)
|
@@ -145,16 +164,17 @@ module CLI
|
|
145
164
|
#
|
146
165
|
# MUST be inside an open frame or it raises a +UnnestedFrameException+
|
147
166
|
#
|
167
|
+
sig { params(text: T.nilable(String), color: T.nilable(Colorable), frame_style: T.nilable(FrameStylable)).void }
|
148
168
|
def divider(text, color: nil, frame_style: nil)
|
149
169
|
fs_item = FrameStack.pop
|
150
170
|
raise UnnestedFrameException, 'No frame nesting to unnest' unless fs_item
|
151
171
|
|
152
|
-
|
153
|
-
frame_style = CLI::UI.resolve_style(frame_style
|
172
|
+
divider_color = CLI::UI.resolve_color(color || fs_item.color)
|
173
|
+
frame_style = CLI::UI.resolve_style(frame_style || fs_item.frame_style)
|
154
174
|
|
155
175
|
CLI::UI.raw do
|
156
176
|
print(prefix.chop)
|
157
|
-
puts frame_style.divider(text, color:
|
177
|
+
puts frame_style.divider(text.to_s, color: divider_color)
|
158
178
|
end
|
159
179
|
|
160
180
|
FrameStack.push(fs_item)
|
@@ -184,21 +204,25 @@ module CLI
|
|
184
204
|
#
|
185
205
|
# MUST be inside an open frame or it raises a +UnnestedFrameException+
|
186
206
|
#
|
207
|
+
sig do
|
208
|
+
params(
|
209
|
+
text: T.nilable(String),
|
210
|
+
color: T.nilable(Colorable),
|
211
|
+
elapsed: T.nilable(Numeric),
|
212
|
+
frame_style: T.nilable(FrameStylable),
|
213
|
+
).void
|
214
|
+
end
|
187
215
|
def close(text, color: nil, elapsed: nil, frame_style: nil)
|
188
216
|
fs_item = FrameStack.pop
|
189
217
|
raise UnnestedFrameException, 'No frame nesting to unnest' unless fs_item
|
190
218
|
|
191
|
-
|
192
|
-
frame_style = CLI::UI.resolve_style(frame_style
|
193
|
-
|
194
|
-
kwargs = {}
|
195
|
-
if elapsed
|
196
|
-
kwargs[:right_text] = "(#{elapsed.round(2)}s)"
|
197
|
-
end
|
219
|
+
close_color = CLI::UI.resolve_color(color || fs_item.color)
|
220
|
+
frame_style = CLI::UI.resolve_style(frame_style || fs_item.frame_style)
|
221
|
+
elapsed_string = elapsed ? "(#{elapsed.round(2)}s)" : nil
|
198
222
|
|
199
223
|
CLI::UI.raw do
|
200
224
|
print(prefix.chop)
|
201
|
-
puts frame_style.close(text, color:
|
225
|
+
puts frame_style.close(text.to_s, color: close_color, right_text: elapsed_string)
|
202
226
|
end
|
203
227
|
end
|
204
228
|
|
@@ -208,11 +232,12 @@ module CLI
|
|
208
232
|
#
|
209
233
|
# * +:color+ - The color of the prefix. Defaults to +Thread.current[:cliui_frame_color_override]+
|
210
234
|
#
|
235
|
+
sig { params(color: T.nilable(Colorable)).returns(String) }
|
211
236
|
def prefix(color: Thread.current[:cliui_frame_color_override])
|
212
237
|
+''.tap do |output|
|
213
238
|
items = FrameStack.items
|
214
239
|
|
215
|
-
items[0..-2].each do |item|
|
240
|
+
items[0..-2].to_a.each do |item|
|
216
241
|
output << item.color.code << item.frame_style.prefix
|
217
242
|
end
|
218
243
|
|
@@ -227,6 +252,7 @@ module CLI
|
|
227
252
|
end
|
228
253
|
|
229
254
|
# The width of a prefix given the number of Frames in the stack
|
255
|
+
sig { returns(Integer) }
|
230
256
|
def prefix_width
|
231
257
|
w = FrameStack.items.reduce(0) do |width, item|
|
232
258
|
width + item.frame_style.prefix_width
|
@@ -241,7 +267,12 @@ module CLI
|
|
241
267
|
#
|
242
268
|
# * +color+ - The color to override to
|
243
269
|
#
|
244
|
-
|
270
|
+
sig do
|
271
|
+
type_parameters(:T)
|
272
|
+
.params(color: Colorable, block: T.proc.returns(T.type_parameter(:T)))
|
273
|
+
.returns(T.type_parameter(:T))
|
274
|
+
end
|
275
|
+
def with_frame_color_override(color, &block)
|
245
276
|
prev = Thread.current[:cliui_frame_color_override]
|
246
277
|
Thread.current[:cliui_frame_color_override] = color
|
247
278
|
yield
|
@@ -254,13 +285,13 @@ module CLI
|
|
254
285
|
# If timing is:
|
255
286
|
# Numeric: return it
|
256
287
|
# false: return nil
|
257
|
-
# true
|
258
|
-
|
259
|
-
def
|
288
|
+
# true: defaults to Time.new
|
289
|
+
sig { params(start: Time, timing: T.any(Numeric, T::Boolean)).returns(T.nilable(Numeric)) }
|
290
|
+
def elapsed(start, timing)
|
260
291
|
return timing if timing.is_a?(Numeric)
|
261
292
|
return if timing.is_a?(FalseClass)
|
262
293
|
|
263
|
-
timing = Time.new
|
294
|
+
timing = Time.new
|
264
295
|
timing - start
|
265
296
|
end
|
266
297
|
end
|
data/lib/cli/ui/glyph.rb
CHANGED
@@ -1,14 +1,22 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
1
3
|
require 'cli/ui'
|
2
4
|
|
3
5
|
module CLI
|
4
6
|
module UI
|
5
7
|
class Glyph
|
8
|
+
extend T::Sig
|
9
|
+
|
6
10
|
class InvalidGlyphHandle < ArgumentError
|
11
|
+
extend T::Sig
|
12
|
+
|
13
|
+
sig { params(handle: String).void }
|
7
14
|
def initialize(handle)
|
8
15
|
super
|
9
16
|
@handle = handle
|
10
17
|
end
|
11
18
|
|
19
|
+
sig { returns(String) }
|
12
20
|
def message
|
13
21
|
keys = Glyph.available.join(',')
|
14
22
|
"invalid glyph handle: #{@handle} " \
|
@@ -16,7 +24,14 @@ module CLI
|
|
16
24
|
end
|
17
25
|
end
|
18
26
|
|
19
|
-
|
27
|
+
sig { returns(String) }
|
28
|
+
attr_reader :handle, :to_s, :fmt, :char
|
29
|
+
|
30
|
+
sig { returns(T.any(Integer, T::Array[Integer])) }
|
31
|
+
attr_reader :codepoint
|
32
|
+
|
33
|
+
sig { returns(Color) }
|
34
|
+
attr_reader :color
|
20
35
|
|
21
36
|
# Creates a new glyph
|
22
37
|
#
|
@@ -27,26 +42,18 @@ module CLI
|
|
27
42
|
# * +plain+ - A fallback plain string to be used in case glyphs are disabled
|
28
43
|
# * +color+ - What color to output the glyph. Check +CLI::UI::Color+ for options.
|
29
44
|
#
|
45
|
+
sig { params(handle: String, codepoint: T.any(Integer, T::Array[Integer]), plain: String, color: Color).void }
|
30
46
|
def initialize(handle, codepoint, plain, color)
|
31
47
|
@handle = handle
|
32
48
|
@codepoint = codepoint
|
33
49
|
@color = color
|
34
|
-
@
|
35
|
-
@
|
36
|
-
@
|
37
|
-
@fmt = "{{#{color.name}:#{char}}}"
|
50
|
+
@char = CLI::UI::OS.current.use_emoji? ? Array(codepoint).pack('U*') : plain
|
51
|
+
@to_s = color.code + @char + Color::RESET.code
|
52
|
+
@fmt = "{{#{color.name}:#{@char}}}"
|
38
53
|
|
39
54
|
MAP[handle] = self
|
40
55
|
end
|
41
56
|
|
42
|
-
# Fetches the actual character(s) to be displayed for a glyph, based on the current OS support
|
43
|
-
#
|
44
|
-
# ==== Returns
|
45
|
-
# Returns the glyph string
|
46
|
-
def char
|
47
|
-
CLI::UI::OS.current.supports_emoji? ? @char : @plain
|
48
|
-
end
|
49
|
-
|
50
57
|
# Mapping of glyphs to terminal output
|
51
58
|
MAP = {}
|
52
59
|
STAR = new('*', 0x2b51, '*', Color::YELLOW) # YELLOW SMALL STAR (⭑)
|
@@ -59,25 +66,31 @@ module CLI
|
|
59
66
|
HOURGLASS = new('H', [0x231b, 0xfe0e], 'H', Color::BLUE) # HOURGLASS + VARIATION SELECTOR 15 (⌛︎)
|
60
67
|
WARNING = new('!', [0x26a0, 0xfe0f], '!', Color::YELLOW) # WARNING SIGN + VARIATION SELECTOR 16 (⚠️ )
|
61
68
|
|
62
|
-
|
63
|
-
|
64
|
-
# ==== Raises
|
65
|
-
# Raises a InvalidGlyphHandle if the glyph is not available
|
66
|
-
# You likely need to create it with +.new+ or you made a typo
|
67
|
-
#
|
68
|
-
# ==== Returns
|
69
|
-
# Returns a terminal output-capable string
|
70
|
-
#
|
71
|
-
def self.lookup(name)
|
72
|
-
MAP.fetch(name.to_s)
|
73
|
-
rescue KeyError
|
74
|
-
raise InvalidGlyphHandle, name
|
75
|
-
end
|
69
|
+
class << self
|
70
|
+
extend T::Sig
|
76
71
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
72
|
+
# Looks up a glyph by name
|
73
|
+
#
|
74
|
+
# ==== Raises
|
75
|
+
# Raises a InvalidGlyphHandle if the glyph is not available
|
76
|
+
# You likely need to create it with +.new+ or you made a typo
|
77
|
+
#
|
78
|
+
# ==== Returns
|
79
|
+
# Returns a terminal output-capable string
|
80
|
+
#
|
81
|
+
sig { params(name: String).returns(Glyph) }
|
82
|
+
def lookup(name)
|
83
|
+
MAP.fetch(name.to_s)
|
84
|
+
rescue KeyError
|
85
|
+
raise InvalidGlyphHandle, name
|
86
|
+
end
|
87
|
+
|
88
|
+
# All available glyphs by name
|
89
|
+
#
|
90
|
+
sig { returns(T::Array[String]) }
|
91
|
+
def available
|
92
|
+
MAP.keys
|
93
|
+
end
|
81
94
|
end
|
82
95
|
end
|
83
96
|
end
|
data/lib/cli/ui/os.rb
CHANGED
@@ -1,67 +1,63 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
1
3
|
require 'rbconfig'
|
2
4
|
|
3
5
|
module CLI
|
4
6
|
module UI
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
else
|
15
|
-
if RUBY_PLATFORM !~ /cygwin/ && ENV['OS'] == 'Windows_NT'
|
16
|
-
Windows
|
17
|
-
else
|
18
|
-
raise "Could not determine OS from host_os #{RbConfig::CONFIG["host_os"]}"
|
19
|
-
end
|
20
|
-
end
|
7
|
+
class OS
|
8
|
+
extend T::Sig
|
9
|
+
|
10
|
+
sig { params(emoji: T::Boolean, color_prompt: T::Boolean, arrow_keys: T::Boolean, shift_cursor: T::Boolean).void }
|
11
|
+
def initialize(emoji: true, color_prompt: true, arrow_keys: true, shift_cursor: false)
|
12
|
+
@emoji = emoji
|
13
|
+
@color_prompt = color_prompt
|
14
|
+
@arrow_keys = arrow_keys
|
15
|
+
@shift_cursor = shift_cursor
|
21
16
|
end
|
22
17
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
true
|
27
|
-
end
|
28
|
-
|
29
|
-
def supports_color_prompt?
|
30
|
-
true
|
31
|
-
end
|
32
|
-
|
33
|
-
def supports_arrow_keys?
|
34
|
-
true
|
35
|
-
end
|
36
|
-
|
37
|
-
def shift_cursor_on_line_reset?
|
38
|
-
false
|
39
|
-
end
|
40
|
-
end
|
18
|
+
sig { returns(T::Boolean) }
|
19
|
+
def use_emoji?
|
20
|
+
@emoji
|
41
21
|
end
|
42
22
|
|
43
|
-
|
23
|
+
sig { returns(T::Boolean) }
|
24
|
+
def use_color_prompt?
|
25
|
+
@color_prompt
|
44
26
|
end
|
45
27
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
end
|
28
|
+
sig { returns(T::Boolean) }
|
29
|
+
def suggest_arrow_keys?
|
30
|
+
@arrow_keys
|
31
|
+
end
|
51
32
|
|
52
|
-
|
53
|
-
|
54
|
-
|
33
|
+
sig { returns(T::Boolean) }
|
34
|
+
def shift_cursor_back_on_horizontal_absolute?
|
35
|
+
@shift_cursor
|
36
|
+
end
|
55
37
|
|
56
|
-
|
57
|
-
|
58
|
-
end
|
38
|
+
class << self
|
39
|
+
extend T::Sig
|
59
40
|
|
60
|
-
|
61
|
-
|
41
|
+
sig { returns(OS) }
|
42
|
+
def current
|
43
|
+
@current_os ||= case RbConfig::CONFIG['host_os']
|
44
|
+
when /darwin/
|
45
|
+
MAC
|
46
|
+
when /linux/
|
47
|
+
LINUX
|
48
|
+
else
|
49
|
+
if RUBY_PLATFORM !~ /cygwin/ && ENV['OS'] == 'Windows_NT'
|
50
|
+
WINDOWS
|
51
|
+
else
|
52
|
+
raise "Could not determine OS from host_os #{RbConfig::CONFIG["host_os"]}"
|
53
|
+
end
|
62
54
|
end
|
63
55
|
end
|
64
56
|
end
|
57
|
+
|
58
|
+
MAC = OS.new
|
59
|
+
LINUX = OS.new
|
60
|
+
WINDOWS = OS.new(emoji: false, color_prompt: false, arrow_keys: false, shift_cursor: true)
|
65
61
|
end
|
66
62
|
end
|
67
63
|
end
|
data/lib/cli/ui/printer.rb
CHANGED
@@ -1,58 +1,76 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
1
3
|
require 'cli/ui'
|
2
4
|
|
3
5
|
module CLI
|
4
6
|
module UI
|
5
7
|
class Printer
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
# ==== Attributes
|
11
|
-
#
|
12
|
-
# * +msg+ - (required) the string to output. Can be frozen.
|
13
|
-
#
|
14
|
-
# ==== Options
|
15
|
-
#
|
16
|
-
# * +:frame_color+ - Override the frame color. Defaults to nil.
|
17
|
-
# * +:to+ - Target stream, like $stdout or $stderr. Can be anything with a puts method. Defaults to $stdout.
|
18
|
-
# * +:encoding+ - Force the output to be in a certain encoding. Defaults to UTF-8.
|
19
|
-
# * +:format+ - Whether to format the string using CLI::UI.fmt. Defaults to true.
|
20
|
-
# * +:graceful+ - Whether to gracefully ignore common I/O errors. Defaults to true.
|
21
|
-
# * +:wrap+ - Whether to wrap text at word boundaries to terminal width. Defaults to true.
|
22
|
-
#
|
23
|
-
# ==== Returns
|
24
|
-
# Returns whether the message was successfully printed,
|
25
|
-
# which can be useful if +:graceful+ is set to true.
|
26
|
-
#
|
27
|
-
# ==== Example
|
28
|
-
#
|
29
|
-
# CLI::UI::Printer.puts('{{x}} Ouch', to: $stderr)
|
30
|
-
#
|
31
|
-
def self.puts(
|
32
|
-
msg,
|
33
|
-
frame_color:
|
34
|
-
nil,
|
35
|
-
to:
|
36
|
-
$stdout,
|
37
|
-
encoding: Encoding::UTF_8,
|
38
|
-
format: true,
|
39
|
-
graceful: true,
|
40
|
-
wrap: true
|
41
|
-
)
|
42
|
-
msg = (+msg).force_encoding(encoding) if encoding
|
43
|
-
msg = CLI::UI.fmt(msg) if format
|
44
|
-
msg = CLI::UI.wrap(msg) if wrap
|
8
|
+
extend T::Sig
|
9
|
+
|
10
|
+
class << self
|
11
|
+
extend T::Sig
|
45
12
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
13
|
+
# Print a message to a stream with common utilities.
|
14
|
+
# Allows overriding the color, encoding, and target stream.
|
15
|
+
# By default, it formats the string using CLI:UI and rescues common stream errors.
|
16
|
+
#
|
17
|
+
# ==== Attributes
|
18
|
+
#
|
19
|
+
# * +msg+ - (required) the string to output. Can be frozen.
|
20
|
+
#
|
21
|
+
# ==== Options
|
22
|
+
#
|
23
|
+
# * +:frame_color+ - Override the frame color. Defaults to nil.
|
24
|
+
# * +:to+ - Target stream, like $stdout or $stderr. Can be anything with a puts method. Defaults to $stdout.
|
25
|
+
# * +:encoding+ - Force the output to be in a certain encoding. Defaults to UTF-8.
|
26
|
+
# * +:format+ - Whether to format the string using CLI::UI.fmt. Defaults to true.
|
27
|
+
# * +:graceful+ - Whether to gracefully ignore common I/O errors. Defaults to true.
|
28
|
+
# * +:wrap+ - Whether to wrap text at word boundaries to terminal width. Defaults to true.
|
29
|
+
#
|
30
|
+
# ==== Returns
|
31
|
+
# Returns whether the message was successfully printed,
|
32
|
+
# which can be useful if +:graceful+ is set to true.
|
33
|
+
#
|
34
|
+
# ==== Example
|
35
|
+
#
|
36
|
+
# CLI::UI::Printer.puts('{{x}} Ouch', to: $stderr)
|
37
|
+
#
|
38
|
+
sig do
|
39
|
+
params(
|
40
|
+
msg: String,
|
41
|
+
frame_color: T.nilable(Colorable),
|
42
|
+
to: IOLike,
|
43
|
+
encoding: T.nilable(Encoding),
|
44
|
+
format: T::Boolean,
|
45
|
+
graceful: T::Boolean,
|
46
|
+
wrap: T::Boolean,
|
47
|
+
).returns(T::Boolean)
|
50
48
|
end
|
49
|
+
def puts(
|
50
|
+
msg,
|
51
|
+
frame_color: nil,
|
52
|
+
to: $stdout,
|
53
|
+
encoding: Encoding::UTF_8,
|
54
|
+
format: true,
|
55
|
+
graceful: true,
|
56
|
+
wrap: true
|
57
|
+
)
|
58
|
+
msg = (+msg).force_encoding(encoding) if encoding
|
59
|
+
msg = CLI::UI.fmt(msg) if format
|
60
|
+
msg = CLI::UI.wrap(msg) if wrap
|
61
|
+
|
62
|
+
if frame_color
|
63
|
+
CLI::UI::Frame.with_frame_color_override(frame_color) { to.puts(msg) }
|
64
|
+
else
|
65
|
+
to.puts(msg)
|
66
|
+
end
|
51
67
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
68
|
+
true
|
69
|
+
rescue Errno::EIO, Errno::EPIPE, IOError => e
|
70
|
+
raise(e) unless graceful
|
71
|
+
|
72
|
+
false
|
73
|
+
end
|
56
74
|
end
|
57
75
|
end
|
58
76
|
end
|
data/lib/cli/ui/progress.rb
CHANGED
@@ -1,41 +1,54 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
1
3
|
require 'cli/ui'
|
2
4
|
|
3
5
|
module CLI
|
4
6
|
module UI
|
5
7
|
class Progress
|
8
|
+
extend T::Sig
|
9
|
+
|
6
10
|
# A Cyan filled block
|
7
11
|
FILLED_BAR = "\e[46m"
|
8
12
|
# A bright white block
|
9
13
|
UNFILLED_BAR = "\e[1;47m"
|
10
14
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
bar
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
15
|
+
class << self
|
16
|
+
extend T::Sig
|
17
|
+
|
18
|
+
# Add a progress bar to the terminal output
|
19
|
+
#
|
20
|
+
# https://user-images.githubusercontent.com/3074765/33799794-cc4c940e-dd00-11e7-9bdc-90f77ec9167c.gif
|
21
|
+
#
|
22
|
+
# ==== Example Usage:
|
23
|
+
#
|
24
|
+
# Set the percent to X
|
25
|
+
# CLI::UI::Progress.progress do |bar|
|
26
|
+
# bar.tick(set_percent: percent)
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# Increase the percent by 1 percent
|
30
|
+
# CLI::UI::Progress.progress do |bar|
|
31
|
+
# bar.tick
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# Increase the percent by X
|
35
|
+
# CLI::UI::Progress.progress do |bar|
|
36
|
+
# bar.tick(percent: 0.05)
|
37
|
+
# end
|
38
|
+
sig do
|
39
|
+
type_parameters(:T)
|
40
|
+
.params(width: Integer, block: T.proc.params(bar: Progress).returns(T.type_parameter(:T)))
|
41
|
+
.returns(T.type_parameter(:T))
|
42
|
+
end
|
43
|
+
def progress(width: Terminal.width, &block)
|
44
|
+
bar = Progress.new(width: width)
|
45
|
+
print(CLI::UI::ANSI.hide_cursor)
|
46
|
+
yield(bar)
|
47
|
+
ensure
|
48
|
+
puts bar.to_s
|
49
|
+
CLI::UI.raw do
|
50
|
+
print(ANSI.show_cursor)
|
51
|
+
end
|
39
52
|
end
|
40
53
|
end
|
41
54
|
|
@@ -46,8 +59,9 @@ module CLI
|
|
46
59
|
#
|
47
60
|
# * +:width+ - The width of the terminal
|
48
61
|
#
|
62
|
+
sig { params(width: Integer).void }
|
49
63
|
def initialize(width: Terminal.width)
|
50
|
-
@percent_done = 0
|
64
|
+
@percent_done = T.let(0, Numeric)
|
51
65
|
@max_width = width
|
52
66
|
end
|
53
67
|
|
@@ -61,9 +75,11 @@ module CLI
|
|
61
75
|
#
|
62
76
|
# *Note:* The +:percent+ and +:set_percent must be between 0.00 and 1.0
|
63
77
|
#
|
64
|
-
|
65
|
-
|
66
|
-
|
78
|
+
sig { params(percent: T.nilable(Numeric), set_percent: T.nilable(Numeric)).void }
|
79
|
+
def tick(percent: nil, set_percent: nil)
|
80
|
+
raise ArgumentError, 'percent and set_percent cannot both be specified' if percent && set_percent
|
81
|
+
|
82
|
+
@percent_done += percent || 0.01
|
67
83
|
@percent_done = set_percent if set_percent
|
68
84
|
@percent_done = [@percent_done, 1.0].min # Make sure we can't go above 1.0
|
69
85
|
|
@@ -73,6 +89,7 @@ module CLI
|
|
73
89
|
|
74
90
|
# Format the progress bar to be printed to terminal
|
75
91
|
#
|
92
|
+
sig { returns(String) }
|
76
93
|
def to_s
|
77
94
|
suffix = " #{(@percent_done * 100).floor}%".ljust(5)
|
78
95
|
workable_width = @max_width - Frame.prefix_width - suffix.size
|