cli-ui 1.5.1 → 2.2.3
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 +28 -17
- data/lib/cli/ui/ansi.rb +172 -129
- data/lib/cli/ui/color.rb +39 -20
- data/lib/cli/ui/formatter.rb +46 -21
- data/lib/cli/ui/frame/frame_stack.rb +30 -50
- data/lib/cli/ui/frame/frame_style/box.rb +16 -5
- data/lib/cli/ui/frame/frame_style/bracket.rb +19 -8
- data/lib/cli/ui/frame/frame_style.rb +84 -87
- data/lib/cli/ui/frame.rb +79 -32
- 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 +50 -33
- data/lib/cli/ui/prompt/interactive_options.rb +114 -68
- data/lib/cli/ui/prompt/options_handler.rb +8 -0
- data/lib/cli/ui/prompt.rb +168 -58
- data/lib/cli/ui/sorbet_runtime_stub.rb +169 -0
- data/lib/cli/ui/spinner/async.rb +15 -4
- data/lib/cli/ui/spinner/spin_group.rb +174 -36
- data/lib/cli/ui/spinner.rb +48 -28
- data/lib/cli/ui/stdout_router.rb +229 -47
- 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 +336 -188
- data/vendor/reentrant_mutex.rb +78 -0
- metadata +11 -9
data/lib/cli/ui/stdout_router.rb
CHANGED
@@ -1,24 +1,27 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
1
3
|
require 'cli/ui'
|
2
4
|
require 'stringio'
|
5
|
+
require_relative '../../../vendor/reentrant_mutex'
|
3
6
|
|
4
7
|
module CLI
|
5
8
|
module UI
|
6
9
|
module StdoutRouter
|
7
|
-
class << self
|
8
|
-
attr_accessor :duplicate_output_to
|
9
|
-
end
|
10
|
-
|
11
10
|
class Writer
|
11
|
+
extend T::Sig
|
12
|
+
|
13
|
+
sig { params(stream: IOLike, name: Symbol).void }
|
12
14
|
def initialize(stream, name)
|
13
15
|
@stream = stream
|
14
16
|
@name = name
|
15
17
|
end
|
16
18
|
|
19
|
+
sig { params(args: Object).returns(Integer) }
|
17
20
|
def write(*args)
|
18
21
|
args = args.map do |str|
|
19
22
|
if auto_frame_inset?
|
20
23
|
str = str.dup # unfreeze
|
21
|
-
str = str.force_encoding(Encoding::UTF_8)
|
24
|
+
str = str.to_s.force_encoding(Encoding::UTF_8)
|
22
25
|
apply_line_prefix(str, CLI::UI::Frame.prefix)
|
23
26
|
else
|
24
27
|
@pending_newline = false
|
@@ -28,37 +31,50 @@ module CLI
|
|
28
31
|
|
29
32
|
# hook return of false suppresses output.
|
30
33
|
if (hook = Thread.current[:cliui_output_hook])
|
31
|
-
return if hook.call(args.map(&:to_s).join, @name) == false
|
34
|
+
return 0 if hook.call(args.map(&:to_s).join, @name) == false
|
32
35
|
end
|
33
36
|
|
34
|
-
@stream.write_without_cli_ui(*prepend_id(@stream, args))
|
37
|
+
ret = T.unsafe(@stream).write_without_cli_ui(*prepend_id(@stream, args))
|
35
38
|
if (dup = StdoutRouter.duplicate_output_to)
|
36
|
-
|
39
|
+
begin
|
40
|
+
T.unsafe(dup).write(*prepend_id(dup, args))
|
41
|
+
rescue IOError
|
42
|
+
# Ignore
|
43
|
+
end
|
37
44
|
end
|
45
|
+
ret
|
38
46
|
end
|
39
47
|
|
40
48
|
private
|
41
49
|
|
50
|
+
sig { params(stream: IOLike, args: T::Array[String]).returns(T::Array[String]) }
|
42
51
|
def prepend_id(stream, args)
|
43
52
|
return args unless prepend_id_for_stream(stream)
|
53
|
+
|
44
54
|
args.map do |a|
|
45
55
|
next a if a.chomp.empty? # allow new lines to be new lines
|
56
|
+
|
46
57
|
"[#{Thread.current[:cliui_output_id][:id]}] #{a}"
|
47
58
|
end
|
48
59
|
end
|
49
60
|
|
61
|
+
sig { params(stream: IOLike).returns(T::Boolean) }
|
50
62
|
def prepend_id_for_stream(stream)
|
51
63
|
return false unless Thread.current[:cliui_output_id]
|
52
64
|
return true if Thread.current[:cliui_output_id][:streams].include?(stream)
|
65
|
+
|
53
66
|
false
|
54
67
|
end
|
55
68
|
|
69
|
+
sig { returns(T::Boolean) }
|
56
70
|
def auto_frame_inset?
|
57
71
|
!Thread.current[:no_cliui_frame_inset]
|
58
72
|
end
|
59
73
|
|
74
|
+
sig { params(str: String, prefix: String).returns(String) }
|
60
75
|
def apply_line_prefix(str, prefix)
|
61
76
|
return '' if str.empty?
|
77
|
+
|
62
78
|
prefixed = +''
|
63
79
|
str.force_encoding(Encoding::UTF_8).lines.each do |line|
|
64
80
|
if @pending_newline
|
@@ -74,46 +90,132 @@ module CLI
|
|
74
90
|
end
|
75
91
|
|
76
92
|
class Capture
|
77
|
-
|
93
|
+
extend T::Sig
|
94
|
+
|
95
|
+
@capture_mutex = Mutex.new
|
96
|
+
@stdin_mutex = CLI::UI::ReentrantMutex.new
|
78
97
|
@active_captures = 0
|
79
98
|
@saved_stdin = nil
|
80
99
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
100
|
+
class << self
|
101
|
+
extend T::Sig
|
102
|
+
|
103
|
+
sig { returns(T.nilable(Capture)) }
|
104
|
+
def current_capture
|
105
|
+
Thread.current[:cliui_current_capture]
|
106
|
+
end
|
107
|
+
|
108
|
+
sig { returns(Capture) }
|
109
|
+
def current_capture!
|
110
|
+
T.must(current_capture)
|
111
|
+
end
|
112
|
+
|
113
|
+
sig { type_parameters(:T).params(block: T.proc.returns(T.type_parameter(:T))).returns(T.type_parameter(:T)) }
|
114
|
+
def in_alternate_screen(&block)
|
115
|
+
stdin_synchronize do
|
116
|
+
previous_print_captured_output = current_capture&.print_captured_output
|
117
|
+
current_capture&.print_captured_output = true
|
118
|
+
Spinner::SpinGroup.pause_spinners do
|
119
|
+
if outermost_uncaptured?
|
120
|
+
begin
|
121
|
+
prev_hook = Thread.current[:cliui_output_hook]
|
122
|
+
Thread.current[:cliui_output_hook] = nil
|
123
|
+
replay = current_capture!.stdout.gsub(ANSI.match_alternate_screen, '')
|
124
|
+
CLI::UI.raw do
|
125
|
+
print("#{ANSI.enter_alternate_screen}#{replay}")
|
126
|
+
end
|
127
|
+
ensure
|
128
|
+
Thread.current[:cliui_output_hook] = prev_hook
|
129
|
+
end
|
130
|
+
end
|
131
|
+
block.call
|
132
|
+
ensure
|
133
|
+
print(ANSI.exit_alternate_screen) if outermost_uncaptured?
|
134
|
+
end
|
135
|
+
ensure
|
136
|
+
current_capture&.print_captured_output = !!previous_print_captured_output
|
88
137
|
end
|
89
|
-
@active_captures += 1
|
90
138
|
end
|
91
139
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
140
|
+
sig { type_parameters(:T).params(block: T.proc.returns(T.type_parameter(:T))).returns(T.type_parameter(:T)) }
|
141
|
+
def stdin_synchronize(&block)
|
142
|
+
@stdin_mutex.synchronize do
|
143
|
+
case $stdin
|
144
|
+
when BlockingInput
|
145
|
+
$stdin.synchronize do
|
146
|
+
block.call
|
147
|
+
end
|
148
|
+
else
|
149
|
+
block.call
|
150
|
+
end
|
98
151
|
end
|
99
152
|
end
|
153
|
+
|
154
|
+
sig { type_parameters(:T).params(block: T.proc.returns(T.type_parameter(:T))).returns(T.type_parameter(:T)) }
|
155
|
+
def with_stdin_masked(&block)
|
156
|
+
@capture_mutex.synchronize do
|
157
|
+
if @active_captures.zero?
|
158
|
+
@stdin_mutex.synchronize do
|
159
|
+
@saved_stdin = $stdin
|
160
|
+
$stdin = BlockingInput.new(@saved_stdin)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
@active_captures += 1
|
164
|
+
end
|
165
|
+
|
166
|
+
yield
|
167
|
+
ensure
|
168
|
+
@capture_mutex.synchronize do
|
169
|
+
@active_captures -= 1
|
170
|
+
if @active_captures.zero?
|
171
|
+
@stdin_mutex.synchronize do
|
172
|
+
$stdin = @saved_stdin
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
private
|
179
|
+
|
180
|
+
sig { returns(T::Boolean) }
|
181
|
+
def outermost_uncaptured?
|
182
|
+
@stdin_mutex.count == 1 && $stdin.is_a?(BlockingInput)
|
183
|
+
end
|
100
184
|
end
|
101
185
|
|
102
|
-
|
186
|
+
sig do
|
187
|
+
params(
|
188
|
+
with_frame_inset: T::Boolean,
|
189
|
+
merged_output: T::Boolean,
|
190
|
+
duplicate_output_to: IO,
|
191
|
+
block: T.proc.void,
|
192
|
+
).void
|
193
|
+
end
|
194
|
+
def initialize(
|
195
|
+
with_frame_inset: true,
|
196
|
+
merged_output: false,
|
197
|
+
duplicate_output_to: File.open(File::NULL, 'w'),
|
198
|
+
&block
|
199
|
+
)
|
103
200
|
@with_frame_inset = with_frame_inset
|
104
|
-
@
|
201
|
+
@merged_output = merged_output
|
202
|
+
@duplicate_output_to = duplicate_output_to
|
105
203
|
@block = block
|
204
|
+
@print_captured_output = false
|
205
|
+
@out = StringIO.new
|
206
|
+
@err = StringIO.new
|
106
207
|
end
|
107
208
|
|
108
|
-
|
209
|
+
sig { returns(T::Boolean) }
|
210
|
+
attr_accessor :print_captured_output
|
109
211
|
|
212
|
+
sig { returns(T.untyped) }
|
110
213
|
def run
|
111
214
|
require 'stringio'
|
112
215
|
|
113
216
|
StdoutRouter.assert_enabled!
|
114
217
|
|
115
|
-
|
116
|
-
err = StringIO.new
|
218
|
+
Thread.current[:cliui_current_capture] = self
|
117
219
|
|
118
220
|
prev_frame_inset = Thread.current[:no_cliui_frame_inset]
|
119
221
|
prev_hook = Thread.current[:cliui_output_hook]
|
@@ -125,61 +227,132 @@ module CLI
|
|
125
227
|
self.class.with_stdin_masked do
|
126
228
|
Thread.current[:no_cliui_frame_inset] = !@with_frame_inset
|
127
229
|
Thread.current[:cliui_output_hook] = ->(data, stream) do
|
230
|
+
stream = :stdout if @merged_output
|
128
231
|
case stream
|
129
|
-
when :stdout
|
130
|
-
|
232
|
+
when :stdout
|
233
|
+
@out.write(data)
|
234
|
+
@duplicate_output_to.write(data)
|
235
|
+
when :stderr
|
236
|
+
@err.write(data)
|
131
237
|
else raise
|
132
238
|
end
|
133
|
-
|
239
|
+
print_captured_output # suppress writing to terminal by default
|
134
240
|
end
|
135
241
|
|
136
|
-
|
137
|
-
@block.call(*@block_args)
|
138
|
-
ensure
|
139
|
-
@stdout = out.string
|
140
|
-
@stderr = err.string
|
141
|
-
end
|
242
|
+
@block.call
|
142
243
|
end
|
143
244
|
ensure
|
144
245
|
Thread.current[:cliui_output_hook] = prev_hook
|
145
246
|
Thread.current[:no_cliui_frame_inset] = prev_frame_inset
|
247
|
+
Thread.current[:cliui_current_capture] = nil
|
248
|
+
end
|
249
|
+
|
250
|
+
sig { returns(String) }
|
251
|
+
def stdout
|
252
|
+
@out.string
|
253
|
+
end
|
254
|
+
|
255
|
+
sig { returns(String) }
|
256
|
+
def stderr
|
257
|
+
@err.string
|
258
|
+
end
|
259
|
+
|
260
|
+
class BlockingInput
|
261
|
+
extend T::Sig
|
262
|
+
|
263
|
+
sig { params(stream: IO).void }
|
264
|
+
def initialize(stream)
|
265
|
+
@stream = stream
|
266
|
+
@m = CLI::UI::ReentrantMutex.new
|
267
|
+
end
|
268
|
+
|
269
|
+
sig { type_parameters(:T).params(block: T.proc.returns(T.type_parameter(:T))).returns(T.type_parameter(:T)) }
|
270
|
+
def synchronize(&block)
|
271
|
+
@m.synchronize do
|
272
|
+
previous_allowed_to_read = Thread.current[:cliui_allowed_to_read]
|
273
|
+
Thread.current[:cliui_allowed_to_read] = true
|
274
|
+
block.call
|
275
|
+
ensure
|
276
|
+
Thread.current[:cliui_allowed_to_read] = previous_allowed_to_read
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
READING_METHODS = [
|
281
|
+
:each,
|
282
|
+
:each_byte,
|
283
|
+
:each_char,
|
284
|
+
:each_codepoint,
|
285
|
+
:each_line,
|
286
|
+
:getbyte,
|
287
|
+
:getc,
|
288
|
+
:getch,
|
289
|
+
:gets,
|
290
|
+
:read,
|
291
|
+
:read_nonblock,
|
292
|
+
:readbyte,
|
293
|
+
:readchar,
|
294
|
+
:readline,
|
295
|
+
:readlines,
|
296
|
+
:readpartial,
|
297
|
+
]
|
298
|
+
|
299
|
+
NON_READING_METHODS = IO.instance_methods(false) - READING_METHODS
|
300
|
+
|
301
|
+
READING_METHODS.each do |method|
|
302
|
+
define_method(method) do |*args, **kwargs, &block|
|
303
|
+
raise(IOError, 'closed stream') unless Thread.current[:cliui_allowed_to_read]
|
304
|
+
|
305
|
+
@stream.send(method, *args, **kwargs, &block)
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
NON_READING_METHODS.each do |method|
|
310
|
+
define_method(method) do |*args, **kwargs, &block|
|
311
|
+
@stream.send(method, *args, **kwargs, &block)
|
312
|
+
end
|
313
|
+
end
|
146
314
|
end
|
147
315
|
end
|
148
316
|
|
149
317
|
class << self
|
318
|
+
extend T::Sig
|
319
|
+
|
150
320
|
WRITE_WITHOUT_CLI_UI = :write_without_cli_ui
|
151
321
|
|
152
322
|
NotEnabled = Class.new(StandardError)
|
153
323
|
|
154
|
-
|
155
|
-
|
156
|
-
raise ArgumentError, <<~EOF
|
157
|
-
on_streams must be an array of objects that respond to `write`
|
158
|
-
These do not respond to write
|
159
|
-
#{on_streams.reject { |s| s.respond_to?(:write) }.map.with_index { |s| s.class.to_s }.join("\n")}
|
160
|
-
EOF
|
161
|
-
end
|
324
|
+
sig { returns(T.nilable(IOLike)) }
|
325
|
+
attr_accessor :duplicate_output_to
|
162
326
|
|
327
|
+
sig do
|
328
|
+
type_parameters(:T)
|
329
|
+
.params(on_streams: T::Array[IOLike], block: T.proc.params(id: String).returns(T.type_parameter(:T)))
|
330
|
+
.returns(T.type_parameter(:T))
|
331
|
+
end
|
332
|
+
def with_id(on_streams:, &block)
|
163
333
|
require 'securerandom'
|
164
334
|
id = format('%05d', rand(10**5))
|
165
335
|
Thread.current[:cliui_output_id] = {
|
166
336
|
id: id,
|
167
|
-
streams: on_streams,
|
337
|
+
streams: on_streams.map { |stream| T.cast(stream, IOLike) },
|
168
338
|
}
|
169
339
|
yield(id)
|
170
340
|
ensure
|
171
341
|
Thread.current[:cliui_output_id] = nil
|
172
342
|
end
|
173
343
|
|
344
|
+
sig { returns(T.nilable(T::Hash[Symbol, T.any(String, IOLike)])) }
|
174
345
|
def current_id
|
175
346
|
Thread.current[:cliui_output_id]
|
176
347
|
end
|
177
348
|
|
349
|
+
sig { void }
|
178
350
|
def assert_enabled!
|
179
351
|
raise NotEnabled unless enabled?
|
180
352
|
end
|
181
353
|
|
182
|
-
|
354
|
+
sig { type_parameters(:T).params(block: T.proc.returns(T.type_parameter(:T))).returns(T.type_parameter(:T)) }
|
355
|
+
def with_enabled(&block)
|
183
356
|
enable
|
184
357
|
yield
|
185
358
|
ensure
|
@@ -187,23 +360,29 @@ module CLI
|
|
187
360
|
end
|
188
361
|
|
189
362
|
# TODO: remove this
|
363
|
+
sig { void }
|
190
364
|
def ensure_activated
|
191
365
|
enable unless enabled?
|
192
366
|
end
|
193
367
|
|
368
|
+
sig { returns(T::Boolean) }
|
194
369
|
def enable
|
195
370
|
return false if enabled?($stdout) || enabled?($stderr)
|
371
|
+
|
196
372
|
activate($stdout, :stdout)
|
197
373
|
activate($stderr, :stderr)
|
198
374
|
true
|
199
375
|
end
|
200
376
|
|
377
|
+
sig { params(stream: IOLike).returns(T::Boolean) }
|
201
378
|
def enabled?(stream = $stdout)
|
202
379
|
stream.respond_to?(WRITE_WITHOUT_CLI_UI)
|
203
380
|
end
|
204
381
|
|
382
|
+
sig { returns(T::Boolean) }
|
205
383
|
def disable
|
206
384
|
return false unless enabled?($stdout) && enabled?($stderr)
|
385
|
+
|
207
386
|
deactivate($stdout)
|
208
387
|
deactivate($stderr)
|
209
388
|
true
|
@@ -211,16 +390,19 @@ module CLI
|
|
211
390
|
|
212
391
|
private
|
213
392
|
|
393
|
+
sig { params(stream: IOLike).void }
|
214
394
|
def deactivate(stream)
|
215
395
|
sc = stream.singleton_class
|
216
396
|
sc.send(:remove_method, :write)
|
217
397
|
sc.send(:alias_method, :write, WRITE_WITHOUT_CLI_UI)
|
218
398
|
end
|
219
399
|
|
400
|
+
sig { params(stream: IOLike, streamname: Symbol).void }
|
220
401
|
def activate(stream, streamname)
|
221
402
|
writer = StdoutRouter::Writer.new(stream, streamname)
|
222
403
|
|
223
404
|
raise if stream.respond_to?(WRITE_WITHOUT_CLI_UI)
|
405
|
+
|
224
406
|
stream.singleton_class.send(:alias_method, WRITE_WITHOUT_CLI_UI, :write)
|
225
407
|
stream.define_singleton_method(:write) do |*args|
|
226
408
|
writer.write(*args)
|
data/lib/cli/ui/terminal.rb
CHANGED
@@ -1,44 +1,56 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
1
3
|
require 'cli/ui'
|
2
4
|
require 'io/console'
|
3
5
|
|
4
6
|
module CLI
|
5
7
|
module UI
|
6
8
|
module Terminal
|
9
|
+
extend T::Sig
|
10
|
+
|
7
11
|
DEFAULT_WIDTH = 80
|
8
12
|
DEFAULT_HEIGHT = 24
|
9
13
|
|
10
|
-
|
11
|
-
|
12
|
-
#
|
13
|
-
def self.width
|
14
|
-
winsize[1]
|
15
|
-
end
|
14
|
+
class << self
|
15
|
+
extend T::Sig
|
16
16
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
17
|
+
# Returns the width of the terminal, if possible
|
18
|
+
# Otherwise will return DEFAULT_WIDTH
|
19
|
+
#
|
20
|
+
sig { returns(Integer) }
|
21
|
+
def width
|
22
|
+
winsize[1]
|
23
|
+
end
|
23
24
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
25
|
+
# Returns the width of the terminal, if possible
|
26
|
+
# Otherwise, will return DEFAULT_HEIGHT
|
27
|
+
#
|
28
|
+
sig { returns(Integer) }
|
29
|
+
def height
|
30
|
+
winsize[0]
|
31
|
+
end
|
32
|
+
|
33
|
+
sig { returns([Integer, Integer]) }
|
34
|
+
def winsize
|
35
|
+
@winsize ||= begin
|
36
|
+
winsize = IO.console.winsize
|
37
|
+
setup_winsize_trap
|
28
38
|
|
29
|
-
|
39
|
+
if winsize.any?(&:zero?)
|
40
|
+
[DEFAULT_HEIGHT, DEFAULT_WIDTH]
|
41
|
+
else
|
42
|
+
winsize
|
43
|
+
end
|
44
|
+
rescue
|
30
45
|
[DEFAULT_HEIGHT, DEFAULT_WIDTH]
|
31
|
-
else
|
32
|
-
winsize
|
33
46
|
end
|
34
|
-
rescue
|
35
|
-
[DEFAULT_HEIGHT, DEFAULT_WIDTH]
|
36
47
|
end
|
37
|
-
end
|
38
48
|
|
39
|
-
|
40
|
-
|
41
|
-
@
|
49
|
+
sig { void }
|
50
|
+
def setup_winsize_trap
|
51
|
+
@winsize_trap ||= Signal.trap('WINCH') do
|
52
|
+
@winsize = nil
|
53
|
+
end
|
42
54
|
end
|
43
55
|
end
|
44
56
|
end
|
data/lib/cli/ui/truncater.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# typed: true
|
1
2
|
# frozen_string_literal: true
|
2
3
|
|
3
4
|
require 'cli/ui'
|
@@ -27,12 +28,15 @@ module CLI
|
|
27
28
|
TRUNCATED = "\x1b[0m…"
|
28
29
|
|
29
30
|
class << self
|
31
|
+
extend T::Sig
|
32
|
+
|
33
|
+
sig { params(text: String, printing_width: Integer).returns(String) }
|
30
34
|
def call(text, printing_width)
|
31
35
|
return text if text.size <= printing_width
|
32
36
|
|
33
37
|
width = 0
|
34
38
|
mode = PARSE_ROOT
|
35
|
-
truncation_index = nil
|
39
|
+
truncation_index = T.let(nil, T.nilable(Integer))
|
36
40
|
|
37
41
|
codepoints = text.codepoints
|
38
42
|
codepoints.each.with_index do |cp, index|
|
@@ -83,11 +87,12 @@ module CLI
|
|
83
87
|
# the end of the string.
|
84
88
|
return text if !truncation_index || width <= printing_width
|
85
89
|
|
86
|
-
codepoints[0...truncation_index].pack('U*') + TRUNCATED
|
90
|
+
T.must(codepoints[0...truncation_index]).pack('U*') + TRUNCATED
|
87
91
|
end
|
88
92
|
|
89
93
|
private
|
90
94
|
|
95
|
+
sig { params(printable_codepoint: Integer).returns(Integer) }
|
91
96
|
def width(printable_codepoint)
|
92
97
|
case printable_codepoint
|
93
98
|
when EMOJI_RANGE
|
data/lib/cli/ui/version.rb
CHANGED
data/lib/cli/ui/widgets/base.rb
CHANGED
@@ -1,26 +1,45 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
1
3
|
require('cli/ui')
|
2
4
|
|
3
5
|
module CLI
|
4
6
|
module UI
|
5
7
|
module Widgets
|
6
8
|
class Base
|
7
|
-
|
8
|
-
|
9
|
+
extend T::Sig
|
10
|
+
extend T::Helpers
|
11
|
+
abstract!
|
12
|
+
|
13
|
+
class << self
|
14
|
+
extend T::Sig
|
15
|
+
|
16
|
+
sig { params(argstring: String).returns(String) }
|
17
|
+
def call(argstring)
|
18
|
+
new(argstring).render
|
19
|
+
end
|
9
20
|
end
|
10
21
|
|
22
|
+
sig { params(argstring: String).void }
|
11
23
|
def initialize(argstring)
|
12
24
|
pat = self.class.argparse_pattern
|
13
25
|
unless (@match_data = pat.match(argstring))
|
14
26
|
raise(Widgets::InvalidWidgetArguments.new(argstring, pat))
|
15
27
|
end
|
28
|
+
|
16
29
|
@match_data.names.each do |name|
|
17
30
|
instance_variable_set(:"@#{name}", @match_data[name])
|
18
31
|
end
|
19
32
|
end
|
20
33
|
|
21
|
-
|
22
|
-
|
34
|
+
class << self
|
35
|
+
extend T::Sig
|
36
|
+
|
37
|
+
sig { abstract.returns(Regexp) }
|
38
|
+
def argparse_pattern; end
|
23
39
|
end
|
40
|
+
|
41
|
+
sig { abstract.returns(String) }
|
42
|
+
def render; end
|
24
43
|
end
|
25
44
|
end
|
26
45
|
end
|
@@ -1,4 +1,6 @@
|
|
1
|
-
#
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
2
4
|
require('cli/ui')
|
3
5
|
|
4
6
|
module CLI
|
@@ -19,6 +21,16 @@ module CLI
|
|
19
21
|
SPINNER_STOPPED = '⠿'
|
20
22
|
EMPTY_SET = '∅'
|
21
23
|
|
24
|
+
class << self
|
25
|
+
extend T::Sig
|
26
|
+
|
27
|
+
sig { override.returns(Regexp) }
|
28
|
+
def argparse_pattern
|
29
|
+
ARGPARSE_PATTERN
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
sig { override.returns(String) }
|
22
34
|
def render
|
23
35
|
if zero?(@succeeded) && zero?(@failed) && zero?(@working) && zero?(@pending)
|
24
36
|
Color::RESET.code + Color::BOLD.code + EMPTY_SET + Color::RESET.code
|
@@ -30,28 +42,34 @@ module CLI
|
|
30
42
|
|
31
43
|
private
|
32
44
|
|
45
|
+
sig { params(num_str: String).returns(T::Boolean) }
|
33
46
|
def zero?(num_str)
|
34
47
|
num_str == '0'
|
35
48
|
end
|
36
49
|
|
50
|
+
sig { params(num_str: String, rune: String, color: Color).returns(String) }
|
37
51
|
def colorize_if_nonzero(num_str, rune, color)
|
38
52
|
color = Color::GRAY if zero?(num_str)
|
39
53
|
color.code + num_str + rune
|
40
54
|
end
|
41
55
|
|
56
|
+
sig { returns(String) }
|
42
57
|
def succeeded_part
|
43
58
|
colorize_if_nonzero(@succeeded, Glyph::CHECK.char, Color::GREEN)
|
44
59
|
end
|
45
60
|
|
61
|
+
sig { returns(String) }
|
46
62
|
def failed_part
|
47
63
|
colorize_if_nonzero(@failed, Glyph::X.char, Color::RED)
|
48
64
|
end
|
49
65
|
|
66
|
+
sig { returns(String) }
|
50
67
|
def working_part
|
51
68
|
rune = zero?(@working) ? SPINNER_STOPPED : Spinner.current_rune
|
52
69
|
colorize_if_nonzero(@working, rune, Color::BLUE)
|
53
70
|
end
|
54
71
|
|
72
|
+
sig { returns(String) }
|
55
73
|
def pending_part
|
56
74
|
colorize_if_nonzero(@pending, Glyph::HOURGLASS.char, Color::WHITE)
|
57
75
|
end
|