cli-ui 2.0.0 → 2.2.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 +11 -0
- data/lib/cli/ui/ansi.rb +15 -0
- data/lib/cli/ui/color.rb +1 -1
- data/lib/cli/ui/formatter.rb +3 -2
- data/lib/cli/ui/frame/frame_stack.rb +3 -42
- data/lib/cli/ui/frame/frame_style/box.rb +1 -1
- data/lib/cli/ui/frame/frame_style/bracket.rb +1 -1
- data/lib/cli/ui/frame.rb +28 -12
- data/lib/cli/ui/progress.rb +2 -2
- data/lib/cli/ui/prompt/interactive_options.rb +26 -27
- data/lib/cli/ui/prompt.rb +109 -52
- data/lib/cli/ui/sorbet_runtime_stub.rb +12 -0
- data/lib/cli/ui/spinner/spin_group.rb +111 -29
- data/lib/cli/ui/spinner.rb +1 -1
- data/lib/cli/ui/stdout_router.rb +170 -26
- data/lib/cli/ui/version.rb +1 -1
- data/lib/cli/ui.rb +11 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8f063229317bef21e8a24f2190dd907c1a1080eefad787871dc8f07b95a3c31b
|
4
|
+
data.tar.gz: 6379c4023ca55081b2d5260ce79ed9615c304c7ab099b9318883a04ef4d11e1e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 07f5f6b944ceeadd42359bc5c3edde469fb034641ad0ea274306097ec2471af1d8b6fb1fbc85f5d30c43ddb1472b6af1ee1688a921deea6404cbf642239d4ffe
|
7
|
+
data.tar.gz: 25c645e1b2c6078e3ace849ef02ff85b727e88df4dedfa4baf200a56760369cf440ca435fe8ca18e3e24f301dfa80e772e48c6ccdf12a5dd9b6e4c2d5b4ae353
|
data/README.md
CHANGED
@@ -52,6 +52,11 @@ For large numbers of options, using `e`, `:`, or `G` will toggle "line select" m
|
|
52
52
|
CLI::UI.ask('What language/framework do you use?', options: %w(rails go ruby python))
|
53
53
|
```
|
54
54
|
|
55
|
+
To set the color of instruction text:
|
56
|
+
```ruby
|
57
|
+
CLI::UI::Prompt.instructions_color = CLI::UI::Color::GRAY
|
58
|
+
```
|
59
|
+
|
55
60
|
Can also assign callbacks to each option
|
56
61
|
|
57
62
|
```ruby
|
@@ -175,6 +180,12 @@ end
|
|
175
180
|
|
176
181
|
---
|
177
182
|
|
183
|
+
## Sorbet
|
184
|
+
|
185
|
+
We make use of [Sorbet](https://sorbet.org/) in cli-ui. We provide stubs for Sorbet so that you can use this gem even
|
186
|
+
if you aren't using Sorbet. We activate these stubs if `T` is undefined when the gem is loaded. For this reason, if you
|
187
|
+
would like to use this gem and your project _does_ use Sorbet, ensure you load Sorbet _before_ loading cli-ui.
|
188
|
+
|
178
189
|
## Example Usage
|
179
190
|
|
180
191
|
The following code makes use of nested-framing, multi-threaded spinners, formatted text, and more.
|
data/lib/cli/ui/ansi.rb
CHANGED
@@ -133,6 +133,21 @@ module CLI
|
|
133
133
|
cmd
|
134
134
|
end
|
135
135
|
|
136
|
+
sig { returns(String) }
|
137
|
+
def enter_alternate_screen
|
138
|
+
control('?1049', 'h')
|
139
|
+
end
|
140
|
+
|
141
|
+
sig { returns(String) }
|
142
|
+
def exit_alternate_screen
|
143
|
+
control('?1049', 'l')
|
144
|
+
end
|
145
|
+
|
146
|
+
sig { returns(Regexp) }
|
147
|
+
def match_alternate_screen
|
148
|
+
/#{Regexp.escape(control('?1049', ''))}[hl]/
|
149
|
+
end
|
150
|
+
|
136
151
|
# Show the cursor
|
137
152
|
#
|
138
153
|
sig { returns(String) }
|
data/lib/cli/ui/color.rb
CHANGED
data/lib/cli/ui/formatter.rb
CHANGED
@@ -116,7 +116,7 @@ module CLI
|
|
116
116
|
return content unless enable_color
|
117
117
|
return content if stack == prev_fmt
|
118
118
|
|
119
|
-
unless stack.empty? && (@nodes.
|
119
|
+
unless stack.empty? && (@nodes.empty? || T.must(@nodes.last)[1].empty?)
|
120
120
|
content << apply_format('', stack, sgr_map)
|
121
121
|
end
|
122
122
|
content
|
@@ -167,7 +167,8 @@ module CLI
|
|
167
167
|
index = sc.pos - 2 # rewind past '}}'
|
168
168
|
raise(FormatError.new(
|
169
169
|
"invalid widget handle at index #{index}: '#{widget_handle}'",
|
170
|
-
@text,
|
170
|
+
@text,
|
171
|
+
index,
|
171
172
|
))
|
172
173
|
end
|
173
174
|
elsif (match = sc.scan(SCAN_FUNCNAME))
|
@@ -4,9 +4,6 @@ module CLI
|
|
4
4
|
module UI
|
5
5
|
module Frame
|
6
6
|
module FrameStack
|
7
|
-
COLOR_ENVVAR = 'CLI_FRAME_STACK'
|
8
|
-
STYLE_ENVVAR = 'CLI_STYLE_STACK'
|
9
|
-
|
10
7
|
class StackItem
|
11
8
|
extend T::Sig
|
12
9
|
|
@@ -32,12 +29,7 @@ module CLI
|
|
32
29
|
# Fetch all items off the frame stack
|
33
30
|
sig { returns(T::Array[StackItem]) }
|
34
31
|
def items
|
35
|
-
|
36
|
-
styles = ENV.fetch(STYLE_ENVVAR, '').split(':').map(&:to_sym)
|
37
|
-
|
38
|
-
colors.each_with_index.map do |color, i|
|
39
|
-
StackItem.new(color, styles[i] || Frame.frame_style)
|
40
|
-
end
|
32
|
+
Thread.current[:cliui_frame_stack] ||= []
|
41
33
|
end
|
42
34
|
|
43
35
|
# Push a new item onto the frame stack.
|
@@ -71,44 +63,13 @@ module CLI
|
|
71
63
|
raise ArgumentError, 'Must give one of item or color: and style:'
|
72
64
|
end
|
73
65
|
|
74
|
-
item
|
75
|
-
|
76
|
-
curr = items
|
77
|
-
curr << item
|
78
|
-
|
79
|
-
serialize(curr)
|
66
|
+
items.push(item || StackItem.new(T.must(color), T.must(style)))
|
80
67
|
end
|
81
68
|
|
82
69
|
# Removes and returns the last stack item off the stack
|
83
70
|
sig { returns(T.nilable(StackItem)) }
|
84
71
|
def pop
|
85
|
-
|
86
|
-
ret = curr.pop
|
87
|
-
|
88
|
-
serialize(curr)
|
89
|
-
|
90
|
-
ret.nil? ? nil : ret
|
91
|
-
end
|
92
|
-
|
93
|
-
private
|
94
|
-
|
95
|
-
# Serializes the item stack into two ENV variables.
|
96
|
-
#
|
97
|
-
# This is done to preserve backward compatibility with earlier versions of cli/ui.
|
98
|
-
# This ensures that any code that relied upon previous stack behavior should continue
|
99
|
-
# to work.
|
100
|
-
sig { params(items: T::Array[StackItem]).void }
|
101
|
-
def serialize(items)
|
102
|
-
colors = []
|
103
|
-
styles = []
|
104
|
-
|
105
|
-
items.each do |item|
|
106
|
-
colors << item.color.name
|
107
|
-
styles << item.frame_style.style_name
|
108
|
-
end
|
109
|
-
|
110
|
-
ENV[COLOR_ENVVAR] = colors.join(':')
|
111
|
-
ENV[STYLE_ENVVAR] = styles.join(':')
|
72
|
+
items.pop
|
112
73
|
end
|
113
74
|
end
|
114
75
|
end
|
@@ -112,7 +112,7 @@ module CLI
|
|
112
112
|
preamble_start = Frame.prefix_width
|
113
113
|
# If prefix_width is non-zero, we need to subtract the width of
|
114
114
|
# the final space, since we're going to write over it.
|
115
|
-
preamble_start -= 1
|
115
|
+
preamble_start -= 1 if preamble_start.nonzero?
|
116
116
|
preamble_end = preamble_start + preamble_width
|
117
117
|
|
118
118
|
suffix_width = CLI::UI::ANSI.printing_width(suffix)
|
@@ -126,7 +126,7 @@ module CLI
|
|
126
126
|
|
127
127
|
# If prefix_width is non-zero, we need to subtract the width of
|
128
128
|
# the final space, since we're going to write over it.
|
129
|
-
preamble_start -= 1
|
129
|
+
preamble_start -= 1 if preamble_start.nonzero?
|
130
130
|
|
131
131
|
# Jumping around the line can cause some unwanted flashes
|
132
132
|
o << CLI::UI::ANSI.hide_cursor
|
data/lib/cli/ui/frame.rb
CHANGED
@@ -54,6 +54,8 @@ module CLI
|
|
54
54
|
# * +:success_text+ - If the block succeeds, what do we output? Defaults to nil
|
55
55
|
# * +:timing+ - How long did the frame content take? Invalid for blockless. Defaults to true for the block form
|
56
56
|
# * +frame_style+ - The frame style to use for this frame
|
57
|
+
# * +:to+ - Target stream, like $stdout or $stderr. Can be anything with print and puts methods,
|
58
|
+
# or under Sorbet, IO or StringIO. Defaults to $stdout.
|
57
59
|
#
|
58
60
|
# ==== Example
|
59
61
|
#
|
@@ -82,6 +84,7 @@ module CLI
|
|
82
84
|
success_text: T.nilable(String),
|
83
85
|
timing: T.any(T::Boolean, Numeric),
|
84
86
|
frame_style: FrameStylable,
|
87
|
+
to: IOLike,
|
85
88
|
block: T.nilable(T.proc.returns(T.type_parameter(:T))),
|
86
89
|
).returns(T.nilable(T.type_parameter(:T)))
|
87
90
|
end
|
@@ -92,6 +95,7 @@ module CLI
|
|
92
95
|
success_text: nil,
|
93
96
|
timing: block_given?,
|
94
97
|
frame_style: self.frame_style,
|
98
|
+
to: $stdout,
|
95
99
|
&block
|
96
100
|
)
|
97
101
|
frame_style = CLI::UI.resolve_style(frame_style)
|
@@ -109,8 +113,8 @@ module CLI
|
|
109
113
|
|
110
114
|
t_start = Time.now
|
111
115
|
CLI::UI.raw do
|
112
|
-
print(prefix.chop)
|
113
|
-
puts
|
116
|
+
to.print(prefix.chop)
|
117
|
+
to.puts(frame_style.start(text, color: color))
|
114
118
|
end
|
115
119
|
FrameStack.push(color: color, style: frame_style)
|
116
120
|
|
@@ -123,7 +127,7 @@ module CLI
|
|
123
127
|
rescue
|
124
128
|
closed = true
|
125
129
|
t_diff = elapsed(t_start, timing)
|
126
|
-
close(failure_text, color: :red, elapsed: t_diff)
|
130
|
+
close(failure_text, color: :red, elapsed: t_diff, to: to)
|
127
131
|
raise
|
128
132
|
else
|
129
133
|
success
|
@@ -131,9 +135,9 @@ module CLI
|
|
131
135
|
unless closed
|
132
136
|
t_diff = elapsed(t_start, timing)
|
133
137
|
if T.unsafe(success) != false
|
134
|
-
close(success_text, color: color, elapsed: t_diff)
|
138
|
+
close(success_text, color: color, elapsed: t_diff, to: to)
|
135
139
|
else
|
136
|
-
close(failure_text, color: :red, elapsed: t_diff)
|
140
|
+
close(failure_text, color: :red, elapsed: t_diff, to: to)
|
137
141
|
end
|
138
142
|
end
|
139
143
|
end
|
@@ -150,6 +154,8 @@ module CLI
|
|
150
154
|
#
|
151
155
|
# * +:color+ - The color of the frame. Defaults to +DEFAULT_FRAME_COLOR+
|
152
156
|
# * +frame_style+ - The frame style to use for this frame
|
157
|
+
# * +:to+ - Target stream, like $stdout or $stderr. Can be anything with print and puts methods,
|
158
|
+
# or under Sorbet, IO or StringIO. Defaults to $stdout.
|
153
159
|
#
|
154
160
|
# ==== Example
|
155
161
|
#
|
@@ -164,8 +170,15 @@ module CLI
|
|
164
170
|
#
|
165
171
|
# MUST be inside an open frame or it raises a +UnnestedFrameException+
|
166
172
|
#
|
167
|
-
sig
|
168
|
-
|
173
|
+
sig do
|
174
|
+
params(
|
175
|
+
text: T.nilable(String),
|
176
|
+
color: T.nilable(Colorable),
|
177
|
+
frame_style: T.nilable(FrameStylable),
|
178
|
+
to: IOLike,
|
179
|
+
).void
|
180
|
+
end
|
181
|
+
def divider(text, color: nil, frame_style: nil, to: $stdout)
|
169
182
|
fs_item = FrameStack.pop
|
170
183
|
raise UnnestedFrameException, 'No frame nesting to unnest' unless fs_item
|
171
184
|
|
@@ -173,8 +186,8 @@ module CLI
|
|
173
186
|
frame_style = CLI::UI.resolve_style(frame_style || fs_item.frame_style)
|
174
187
|
|
175
188
|
CLI::UI.raw do
|
176
|
-
print(prefix.chop)
|
177
|
-
puts
|
189
|
+
to.print(prefix.chop)
|
190
|
+
to.puts(frame_style.divider(text.to_s, color: divider_color))
|
178
191
|
end
|
179
192
|
|
180
193
|
FrameStack.push(fs_item)
|
@@ -192,6 +205,8 @@ module CLI
|
|
192
205
|
# * +:color+ - The color of the frame. Defaults to nil
|
193
206
|
# * +:elapsed+ - How long did the frame take? Defaults to nil
|
194
207
|
# * +frame_style+ - The frame style to use for this frame. Defaults to nil
|
208
|
+
# * +:to+ - Target stream, like $stdout or $stderr. Can be anything with print and puts methods,
|
209
|
+
# or under Sorbet, IO or StringIO. Defaults to $stdout.
|
195
210
|
#
|
196
211
|
# ==== Example
|
197
212
|
#
|
@@ -210,9 +225,10 @@ module CLI
|
|
210
225
|
color: T.nilable(Colorable),
|
211
226
|
elapsed: T.nilable(Numeric),
|
212
227
|
frame_style: T.nilable(FrameStylable),
|
228
|
+
to: IOLike,
|
213
229
|
).void
|
214
230
|
end
|
215
|
-
def close(text, color: nil, elapsed: nil, frame_style: nil)
|
231
|
+
def close(text, color: nil, elapsed: nil, frame_style: nil, to: $stdout)
|
216
232
|
fs_item = FrameStack.pop
|
217
233
|
raise UnnestedFrameException, 'No frame nesting to unnest' unless fs_item
|
218
234
|
|
@@ -221,8 +237,8 @@ module CLI
|
|
221
237
|
elapsed_string = elapsed ? "(#{elapsed.round(2)}s)" : nil
|
222
238
|
|
223
239
|
CLI::UI.raw do
|
224
|
-
print(prefix.chop)
|
225
|
-
puts
|
240
|
+
to.print(prefix.chop)
|
241
|
+
to.puts(frame_style.close(text.to_s, color: close_color, right_text: elapsed_string))
|
226
242
|
end
|
227
243
|
end
|
228
244
|
|
data/lib/cli/ui/progress.rb
CHANGED
@@ -45,7 +45,7 @@ module CLI
|
|
45
45
|
print(CLI::UI::ANSI.hide_cursor)
|
46
46
|
yield(bar)
|
47
47
|
ensure
|
48
|
-
puts
|
48
|
+
puts(bar)
|
49
49
|
CLI::UI.raw do
|
50
50
|
print(ANSI.show_cursor)
|
51
51
|
end
|
@@ -83,7 +83,7 @@ module CLI
|
|
83
83
|
@percent_done = set_percent if set_percent
|
84
84
|
@percent_done = [@percent_done, 1.0].min # Make sure we can't go above 1.0
|
85
85
|
|
86
|
-
print(
|
86
|
+
print(self)
|
87
87
|
print(CLI::UI::ANSI.previous_line + "\n")
|
88
88
|
end
|
89
89
|
|
@@ -111,21 +111,29 @@ module CLI
|
|
111
111
|
# so to get the # of lines, you need to join then split
|
112
112
|
|
113
113
|
# since lines may be longer than the terminal is wide, we need to
|
114
|
-
# determine how many extra lines would be taken up by them
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
114
|
+
# determine how many extra lines would be taken up by them.
|
115
|
+
#
|
116
|
+
# To accomplish this we split the string by new lines and add the
|
117
|
+
# extra characters to the first line.
|
118
|
+
# Then we calculate how many lines would be needed to render the string
|
119
|
+
# based on the terminal width
|
120
|
+
# 3 = space before the number, the . after the number, the space after the .
|
121
|
+
# multiple check is for the space for the checkbox, if rendered
|
122
|
+
# options.count.to_s.size gets us the max size of the number we will display
|
123
|
+
extra_chars = @marker.length + 3 + @options.count.to_s.size + (@multiple ? 1 : 0)
|
120
124
|
|
121
125
|
@option_lengths = @options.map do |text|
|
122
|
-
|
123
|
-
width ||= text
|
124
|
-
.split("\n")
|
125
|
-
.reject(&:empty?)
|
126
|
-
.sum { |l| (CLI::UI.fmt(l, enable_color: false).length / max_width).ceil }
|
126
|
+
next 1 if text.empty?
|
127
127
|
|
128
|
-
|
128
|
+
# Find the length of all the lines in this string
|
129
|
+
non_empty_line_lengths = text.split("\n").reject(&:empty?).map do |line|
|
130
|
+
CLI::UI.fmt(line, enable_color: false).length
|
131
|
+
end
|
132
|
+
# The first line has the marker and number, so we add that so we can take it into account
|
133
|
+
non_empty_line_lengths[0] += extra_chars
|
134
|
+
# Finally, we need to calculate how many lines each one will take. We can do that by dividing each one
|
135
|
+
# by the width of the terminal, rounding up to the nearest natural number
|
136
|
+
non_empty_line_lengths.sum { |length| (length.to_f / @terminal_width_at_calculation_time).ceil }
|
129
137
|
end
|
130
138
|
end
|
131
139
|
|
@@ -285,7 +293,7 @@ module CLI
|
|
285
293
|
# rubocop:disable Style/WhenThen,Layout/SpaceBeforeSemicolon,Style/Semicolon
|
286
294
|
sig { void }
|
287
295
|
def wait_for_user_input
|
288
|
-
char = read_char
|
296
|
+
char = Prompt.read_char
|
289
297
|
@last_char = char
|
290
298
|
|
291
299
|
case char
|
@@ -375,17 +383,6 @@ module CLI
|
|
375
383
|
@redraw = true
|
376
384
|
end
|
377
385
|
|
378
|
-
sig { returns(T.nilable(String)) }
|
379
|
-
def read_char
|
380
|
-
if $stdin.tty? && !ENV['TEST']
|
381
|
-
$stdin.getch # raw mode for tty
|
382
|
-
else
|
383
|
-
$stdin.getc # returns nil at end of input
|
384
|
-
end
|
385
|
-
rescue Errno::EIO, Errno::EPIPE, IOError
|
386
|
-
"\e"
|
387
|
-
end
|
388
|
-
|
389
386
|
sig { params(recalculate: T::Boolean).returns(T::Array[[String, T.nilable(Integer)]]) }
|
390
387
|
def presented_options(recalculate: false)
|
391
388
|
return @presented_options unless recalculate
|
@@ -497,8 +494,10 @@ module CLI
|
|
497
494
|
message = " #{num}#{num ? "." : " "}#{padding}"
|
498
495
|
|
499
496
|
format = '%s'
|
500
|
-
# If multiple, bold
|
501
|
-
|
497
|
+
# If multiple, bold selected. If not multiple, do not bold any options.
|
498
|
+
# Bolding options can cause confusion as some users may perceive bold white (default color) as selected
|
499
|
+
# rather than the actual selected color.
|
500
|
+
format = "{{bold:#{format}}}" if @multiple && is_chosen
|
502
501
|
format = "{{cyan:#{format}}}" if @multiple && is_chosen && num != @active
|
503
502
|
format = " #{format}"
|
504
503
|
|
@@ -508,7 +507,7 @@ module CLI
|
|
508
507
|
if num == @active
|
509
508
|
|
510
509
|
color = filtering? || selecting? ? 'green' : 'blue'
|
511
|
-
message = message.split("\n").map { |l| "{{#{color}
|
510
|
+
message = message.split("\n").map { |l| "{{#{color}:#{@marker} #{l.strip}}}" }.join("\n")
|
512
511
|
end
|
513
512
|
|
514
513
|
puts CLI::UI.fmt(message)
|
data/lib/cli/ui/prompt.rb
CHANGED
@@ -29,6 +29,22 @@ module CLI
|
|
29
29
|
class << self
|
30
30
|
extend T::Sig
|
31
31
|
|
32
|
+
sig { returns(Color) }
|
33
|
+
def instructions_color
|
34
|
+
@instructions_color ||= Color::YELLOW
|
35
|
+
end
|
36
|
+
|
37
|
+
# Set the instructions color.
|
38
|
+
#
|
39
|
+
# ==== Attributes
|
40
|
+
#
|
41
|
+
# * +color+ - the color to use for prompt instructions
|
42
|
+
#
|
43
|
+
sig { params(color: Colorable).void }
|
44
|
+
def instructions_color=(color)
|
45
|
+
@instructions_color = CLI::UI.resolve_color(color)
|
46
|
+
end
|
47
|
+
|
32
48
|
# Ask a user a question with either free form answer or a set of answers (multiple choice)
|
33
49
|
# Can use arrows, y/n, numbers (1/2), and vim bindings to control multiple choice selection
|
34
50
|
# Do not use this method for yes/no questions. Use +confirm+
|
@@ -167,19 +183,21 @@ module CLI
|
|
167
183
|
def ask_password(question)
|
168
184
|
require 'io/console'
|
169
185
|
|
170
|
-
|
186
|
+
CLI::UI::StdoutRouter::Capture.in_alternate_screen do
|
187
|
+
$stdout.print(CLI::UI.fmt('{{?}} ' + question)) # Do not use puts_question to avoid the new line.
|
171
188
|
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
189
|
+
# noecho interacts poorly with Readline under system Ruby, so do a manual `gets` here.
|
190
|
+
# No fancy Readline integration (like echoing back) is required for a password prompt anyway.
|
191
|
+
password = $stdin.noecho do
|
192
|
+
# Chomp will remove the one new line character added by `gets`, without touching potential extra spaces:
|
193
|
+
# " 123 \n".chomp => " 123 "
|
194
|
+
$stdin.gets.to_s.chomp
|
195
|
+
end
|
179
196
|
|
180
|
-
|
197
|
+
$stdout.puts # Complete the line
|
181
198
|
|
182
|
-
|
199
|
+
password
|
200
|
+
end
|
183
201
|
end
|
184
202
|
|
185
203
|
# Asks the user a yes/no question.
|
@@ -197,6 +215,36 @@ module CLI
|
|
197
215
|
ask_interactive(question, default ? ['yes', 'no'] : ['no', 'yes'], filter_ui: false) == 'yes'
|
198
216
|
end
|
199
217
|
|
218
|
+
# Present the user with a message and wait for any key to be pressed, returning the pressed key.
|
219
|
+
#
|
220
|
+
# ==== Example Usage:
|
221
|
+
#
|
222
|
+
# CLI::UI::Prompt.any_key # Press any key to continue...
|
223
|
+
#
|
224
|
+
# CLI::UI::Prompt.any_key('Press RETURN to continue...') # Then check if that's what they pressed
|
225
|
+
sig { params(prompt: String).returns(T.nilable(String)) }
|
226
|
+
def any_key(prompt = 'Press any key to continue...')
|
227
|
+
CLI::UI::StdoutRouter::Capture.in_alternate_screen do
|
228
|
+
puts_question(prompt)
|
229
|
+
read_char
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# Wait for any key to be pressed, returning the pressed key.
|
234
|
+
sig { returns(T.nilable(String)) }
|
235
|
+
def read_char
|
236
|
+
CLI::UI::StdoutRouter::Capture.in_alternate_screen do
|
237
|
+
if $stdin.tty? && !ENV['TEST']
|
238
|
+
require 'io/console'
|
239
|
+
$stdin.getch # raw mode for tty
|
240
|
+
else
|
241
|
+
$stdin.getc # returns nil at end of input
|
242
|
+
end
|
243
|
+
end
|
244
|
+
rescue Errno::EIO, Errno::EPIPE, IOError
|
245
|
+
"\e"
|
246
|
+
end
|
247
|
+
|
200
248
|
private
|
201
249
|
|
202
250
|
sig do
|
@@ -208,23 +256,25 @@ module CLI
|
|
208
256
|
raise(ArgumentError, 'conflicting arguments: default enabled but allow_empty is false')
|
209
257
|
end
|
210
258
|
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
259
|
+
CLI::UI::StdoutRouter::Capture.in_alternate_screen do
|
260
|
+
if default
|
261
|
+
puts_question("#{question} (empty = #{default})")
|
262
|
+
else
|
263
|
+
puts_question(question)
|
264
|
+
end
|
216
265
|
|
217
|
-
|
218
|
-
|
219
|
-
|
266
|
+
# Ask a free form question
|
267
|
+
loop do
|
268
|
+
line = readline(is_file: is_file)
|
220
269
|
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
270
|
+
if line.empty? && default
|
271
|
+
write_default_over_empty_input(default)
|
272
|
+
return default
|
273
|
+
end
|
225
274
|
|
226
|
-
|
227
|
-
|
275
|
+
if !line.empty? || allow_empty
|
276
|
+
return line
|
277
|
+
end
|
228
278
|
end
|
229
279
|
end
|
230
280
|
end
|
@@ -259,33 +309,38 @@ module CLI
|
|
259
309
|
instructions = (multiple ? 'Toggle options. ' : '') + navigate_text
|
260
310
|
instructions += ", filter with 'f'" if filter_ui
|
261
311
|
instructions += ", enter option with 'e'" if select_ui && (options.size > 9)
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
case resp
|
274
|
-
when
|
275
|
-
|
276
|
-
|
277
|
-
|
312
|
+
|
313
|
+
CLI::UI::StdoutRouter::Capture.in_alternate_screen do
|
314
|
+
puts_question("#{question} " + instructions_color.code + "(#{instructions})" + Color::RESET.code)
|
315
|
+
resp = interactive_prompt(options, multiple: multiple, default: default)
|
316
|
+
|
317
|
+
# Clear the line
|
318
|
+
print(ANSI.previous_line + ANSI.clear_to_end_of_line)
|
319
|
+
# Force StdoutRouter to prefix
|
320
|
+
print(ANSI.previous_line + "\n")
|
321
|
+
|
322
|
+
# reset the question to include the answer
|
323
|
+
resp_text = case resp
|
324
|
+
when Array
|
325
|
+
case resp.size
|
326
|
+
when 0
|
327
|
+
'<nothing>'
|
328
|
+
when 1..2
|
329
|
+
resp.join(' and ')
|
330
|
+
else
|
331
|
+
"#{resp.size} items"
|
332
|
+
end
|
278
333
|
else
|
279
|
-
|
334
|
+
resp
|
280
335
|
end
|
281
|
-
|
282
|
-
resp
|
283
|
-
end
|
284
|
-
puts_question("#{question} (You chose: {{italic:#{resp_text}}})")
|
285
|
-
|
286
|
-
return T.must(handler).call(resp) if block_given?
|
336
|
+
puts_question("#{question} (You chose: {{italic:#{resp_text}}})")
|
287
337
|
|
288
|
-
|
338
|
+
if block_given?
|
339
|
+
T.must(handler).call(resp)
|
340
|
+
else
|
341
|
+
resp
|
342
|
+
end
|
343
|
+
end
|
289
344
|
end
|
290
345
|
|
291
346
|
# Useful for stubbing in tests
|
@@ -294,13 +349,15 @@ module CLI
|
|
294
349
|
.returns(T.any(T::Array[String], String))
|
295
350
|
end
|
296
351
|
def interactive_prompt(options, multiple: false, default: nil)
|
297
|
-
|
352
|
+
CLI::UI::StdoutRouter::Capture.in_alternate_screen do
|
353
|
+
InteractiveOptions.call(options, multiple: multiple, default: default)
|
354
|
+
end
|
298
355
|
end
|
299
356
|
|
300
357
|
sig { params(default: String).void }
|
301
358
|
def write_default_over_empty_input(default)
|
302
359
|
CLI::UI.raw do
|
303
|
-
|
360
|
+
$stderr.puts(
|
304
361
|
CLI::UI::ANSI.cursor_up(1) +
|
305
362
|
"\r" +
|
306
363
|
CLI::UI::ANSI.cursor_forward(4) + # TODO: width
|
@@ -312,7 +369,7 @@ module CLI
|
|
312
369
|
|
313
370
|
sig { params(str: String).void }
|
314
371
|
def puts_question(str)
|
315
|
-
|
372
|
+
$stdout.puts(CLI::UI.fmt('{{?}} ' + str))
|
316
373
|
end
|
317
374
|
|
318
375
|
sig { params(is_file: T::Boolean).returns(String) }
|
@@ -340,7 +397,7 @@ module CLI
|
|
340
397
|
print(CLI::UI::Color::RESET.code)
|
341
398
|
line.to_s.chomp
|
342
399
|
rescue Interrupt
|
343
|
-
CLI::UI.raw {
|
400
|
+
CLI::UI.raw { $stderr.puts('^C' + CLI::UI::Color::RESET.code) }
|
344
401
|
raise
|
345
402
|
end
|
346
403
|
end
|
@@ -154,4 +154,16 @@ module T
|
|
154
154
|
def [](type); end
|
155
155
|
end
|
156
156
|
end
|
157
|
+
|
158
|
+
class << self
|
159
|
+
def const_added(name)
|
160
|
+
super
|
161
|
+
raise 'When using both cli-ui and sorbet, you must require sorbet before cli-ui'
|
162
|
+
end
|
163
|
+
|
164
|
+
def method_added(name)
|
165
|
+
super
|
166
|
+
raise 'When using both cli-ui and sorbet, you must require sorbet before cli-ui'
|
167
|
+
end
|
168
|
+
end
|
157
169
|
end
|
@@ -4,6 +4,41 @@ module CLI
|
|
4
4
|
module UI
|
5
5
|
module Spinner
|
6
6
|
class SpinGroup
|
7
|
+
DEFAULT_FINAL_GLYPH = ->(success) { success ? CLI::UI::Glyph::CHECK.to_s : CLI::UI::Glyph::X.to_s }
|
8
|
+
|
9
|
+
class << self
|
10
|
+
extend T::Sig
|
11
|
+
|
12
|
+
sig { returns(Mutex) }
|
13
|
+
attr_reader :pause_mutex
|
14
|
+
|
15
|
+
sig { returns(T::Boolean) }
|
16
|
+
def paused?
|
17
|
+
@paused
|
18
|
+
end
|
19
|
+
|
20
|
+
sig do
|
21
|
+
type_parameters(:T)
|
22
|
+
.params(block: T.proc.returns(T.type_parameter(:T)))
|
23
|
+
.returns(T.type_parameter(:T))
|
24
|
+
end
|
25
|
+
def pause_spinners(&block)
|
26
|
+
previous_paused = T.let(nil, T.nilable(T::Boolean))
|
27
|
+
@pause_mutex.synchronize do
|
28
|
+
previous_paused = @paused
|
29
|
+
@paused = true
|
30
|
+
end
|
31
|
+
block.call
|
32
|
+
ensure
|
33
|
+
@pause_mutex.synchronize do
|
34
|
+
@paused = previous_paused
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
@pause_mutex = Mutex.new
|
40
|
+
@paused = false
|
41
|
+
|
7
42
|
extend T::Sig
|
8
43
|
|
9
44
|
# Initializes a new spin group
|
@@ -11,7 +46,7 @@ module CLI
|
|
11
46
|
#
|
12
47
|
# ==== Options
|
13
48
|
#
|
14
|
-
# * +:auto_debrief+ - Automatically debrief exceptions? Default to true
|
49
|
+
# * +:auto_debrief+ - Automatically debrief exceptions or through success_debrief? Default to true
|
15
50
|
#
|
16
51
|
# ==== Example Usage
|
17
52
|
#
|
@@ -57,12 +92,23 @@ module CLI
|
|
57
92
|
# * +title+ - Title of the task
|
58
93
|
# * +block+ - Block for the task, will be provided with an instance of the spinner
|
59
94
|
#
|
60
|
-
sig
|
61
|
-
|
95
|
+
sig do
|
96
|
+
params(
|
97
|
+
title: String,
|
98
|
+
final_glyph: T.proc.params(success: T::Boolean).returns(String),
|
99
|
+
merged_output: T::Boolean,
|
100
|
+
duplicate_output_to: IO,
|
101
|
+
block: T.proc.params(task: Task).returns(T.untyped),
|
102
|
+
).void
|
103
|
+
end
|
104
|
+
def initialize(title, final_glyph:, merged_output:, duplicate_output_to:, &block)
|
62
105
|
@title = title
|
106
|
+
@final_glyph = final_glyph
|
63
107
|
@always_full_render = title =~ Formatter::SCAN_WIDGET
|
64
108
|
@thread = Thread.new do
|
65
|
-
cap = CLI::UI::StdoutRouter::Capture.new(
|
109
|
+
cap = CLI::UI::StdoutRouter::Capture.new(
|
110
|
+
merged_output: merged_output, duplicate_output_to: duplicate_output_to,
|
111
|
+
) { block.call(self) }
|
66
112
|
begin
|
67
113
|
cap.run
|
68
114
|
ensure
|
@@ -173,7 +219,7 @@ module CLI
|
|
173
219
|
sig { params(index: Integer).returns(String) }
|
174
220
|
def glyph(index)
|
175
221
|
if @done
|
176
|
-
@success
|
222
|
+
@final_glyph.call(@success)
|
177
223
|
else
|
178
224
|
GLYPHS[index]
|
179
225
|
end
|
@@ -202,10 +248,30 @@ module CLI
|
|
202
248
|
# spin_group.add('Title') { |spinner| sleep 1.0 }
|
203
249
|
# spin_group.wait
|
204
250
|
#
|
205
|
-
sig
|
206
|
-
|
251
|
+
sig do
|
252
|
+
params(
|
253
|
+
title: String,
|
254
|
+
final_glyph: T.proc.params(success: T::Boolean).returns(String),
|
255
|
+
merged_output: T::Boolean,
|
256
|
+
duplicate_output_to: IO,
|
257
|
+
block: T.proc.params(task: Task).void,
|
258
|
+
).void
|
259
|
+
end
|
260
|
+
def add(
|
261
|
+
title,
|
262
|
+
final_glyph: DEFAULT_FINAL_GLYPH,
|
263
|
+
merged_output: false,
|
264
|
+
duplicate_output_to: File.new(File::NULL, 'w'),
|
265
|
+
&block
|
266
|
+
)
|
207
267
|
@m.synchronize do
|
208
|
-
@tasks << Task.new(
|
268
|
+
@tasks << Task.new(
|
269
|
+
title,
|
270
|
+
final_glyph: final_glyph,
|
271
|
+
merged_output: merged_output,
|
272
|
+
duplicate_output_to: duplicate_output_to,
|
273
|
+
&block
|
274
|
+
)
|
209
275
|
end
|
210
276
|
end
|
211
277
|
|
@@ -221,32 +287,36 @@ module CLI
|
|
221
287
|
idx = 0
|
222
288
|
|
223
289
|
loop do
|
224
|
-
|
290
|
+
done_count = 0
|
225
291
|
|
226
292
|
width = CLI::UI::Terminal.width
|
227
293
|
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
294
|
+
self.class.pause_mutex.synchronize do
|
295
|
+
next if self.class.paused?
|
296
|
+
|
297
|
+
@m.synchronize do
|
298
|
+
CLI::UI.raw do
|
299
|
+
@tasks.each.with_index do |task, int_index|
|
300
|
+
nat_index = int_index + 1
|
301
|
+
task_done = task.check
|
302
|
+
done_count += 1 if task_done
|
303
|
+
|
304
|
+
if nat_index > @consumed_lines
|
305
|
+
print(task.render(idx, true, width: width) + "\n")
|
306
|
+
@consumed_lines += 1
|
307
|
+
else
|
308
|
+
offset = @consumed_lines - int_index
|
309
|
+
move_to = CLI::UI::ANSI.cursor_up(offset) + "\r"
|
310
|
+
move_from = "\r" + CLI::UI::ANSI.cursor_down(offset)
|
311
|
+
|
312
|
+
print(move_to + task.render(idx, idx.zero?, width: width) + move_from)
|
313
|
+
end
|
244
314
|
end
|
245
315
|
end
|
246
316
|
end
|
247
317
|
end
|
248
318
|
|
249
|
-
break if
|
319
|
+
break if done_count == @tasks.size
|
250
320
|
|
251
321
|
idx = (idx + 1) % GLYPHS.size
|
252
322
|
Spinner.index = idx
|
@@ -273,6 +343,16 @@ module CLI
|
|
273
343
|
@failure_debrief = block
|
274
344
|
end
|
275
345
|
|
346
|
+
# Provide a debriefing for successful tasks
|
347
|
+
sig do
|
348
|
+
params(
|
349
|
+
block: T.proc.params(title: String, out: String, err: String).void,
|
350
|
+
).void
|
351
|
+
end
|
352
|
+
def success_debrief(&block)
|
353
|
+
@success_debrief = block
|
354
|
+
end
|
355
|
+
|
276
356
|
sig { returns(T::Boolean) }
|
277
357
|
def all_succeeded?
|
278
358
|
@m.synchronize do
|
@@ -286,13 +366,15 @@ module CLI
|
|
286
366
|
def debrief
|
287
367
|
@m.synchronize do
|
288
368
|
@tasks.each do |task|
|
289
|
-
next if task.success
|
290
|
-
|
291
369
|
title = task.title
|
292
|
-
e = task.exception
|
293
370
|
out = task.stdout
|
294
371
|
err = task.stderr
|
295
372
|
|
373
|
+
if task.success
|
374
|
+
next @success_debrief&.call(title, out, err)
|
375
|
+
end
|
376
|
+
|
377
|
+
e = task.exception
|
296
378
|
next @failure_debrief.call(title, e, out, err) if @failure_debrief
|
297
379
|
|
298
380
|
CLI::UI::Frame.open('Task Failed: ' + title, color: :red, timing: Time.new - @start) do
|
data/lib/cli/ui/spinner.rb
CHANGED
data/lib/cli/ui/stdout_router.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require 'cli/ui'
|
4
4
|
require 'stringio'
|
5
|
+
require_relative '../../../vendor/reentrant_mutex'
|
5
6
|
|
6
7
|
module CLI
|
7
8
|
module UI
|
@@ -35,7 +36,11 @@ module CLI
|
|
35
36
|
|
36
37
|
T.unsafe(@stream).write_without_cli_ui(*prepend_id(@stream, args))
|
37
38
|
if (dup = StdoutRouter.duplicate_output_to)
|
38
|
-
|
39
|
+
begin
|
40
|
+
T.unsafe(dup).write(*prepend_id(dup, args))
|
41
|
+
rescue IOError
|
42
|
+
# Ignore
|
43
|
+
end
|
39
44
|
end
|
40
45
|
end
|
41
46
|
|
@@ -86,48 +91,122 @@ module CLI
|
|
86
91
|
class Capture
|
87
92
|
extend T::Sig
|
88
93
|
|
89
|
-
@
|
94
|
+
@capture_mutex = Mutex.new
|
95
|
+
@stdin_mutex = ReentrantMutex.new
|
90
96
|
@active_captures = 0
|
91
97
|
@saved_stdin = nil
|
92
98
|
|
93
99
|
class << self
|
94
100
|
extend T::Sig
|
95
101
|
|
102
|
+
sig { returns(T.nilable(Capture)) }
|
103
|
+
def current_capture
|
104
|
+
Thread.current[:cliui_current_capture]
|
105
|
+
end
|
106
|
+
|
107
|
+
sig { returns(Capture) }
|
108
|
+
def current_capture!
|
109
|
+
T.must(current_capture)
|
110
|
+
end
|
111
|
+
|
112
|
+
sig { type_parameters(:T).params(block: T.proc.returns(T.type_parameter(:T))).returns(T.type_parameter(:T)) }
|
113
|
+
def in_alternate_screen(&block)
|
114
|
+
stdin_synchronize do
|
115
|
+
previous_print_captured_output = current_capture&.print_captured_output
|
116
|
+
current_capture&.print_captured_output = true
|
117
|
+
Spinner::SpinGroup.pause_spinners do
|
118
|
+
if outermost_uncaptured?
|
119
|
+
begin
|
120
|
+
prev_hook = Thread.current[:cliui_output_hook]
|
121
|
+
Thread.current[:cliui_output_hook] = nil
|
122
|
+
replay = current_capture!.stdout.gsub(ANSI.match_alternate_screen, '')
|
123
|
+
CLI::UI.raw do
|
124
|
+
print("#{ANSI.enter_alternate_screen}#{replay}")
|
125
|
+
end
|
126
|
+
ensure
|
127
|
+
Thread.current[:cliui_output_hook] = prev_hook
|
128
|
+
end
|
129
|
+
end
|
130
|
+
block.call
|
131
|
+
ensure
|
132
|
+
print(ANSI.exit_alternate_screen) if outermost_uncaptured?
|
133
|
+
end
|
134
|
+
ensure
|
135
|
+
current_capture&.print_captured_output = !!previous_print_captured_output
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
sig { type_parameters(:T).params(block: T.proc.returns(T.type_parameter(:T))).returns(T.type_parameter(:T)) }
|
140
|
+
def stdin_synchronize(&block)
|
141
|
+
@stdin_mutex.synchronize do
|
142
|
+
case $stdin
|
143
|
+
when BlockingInput
|
144
|
+
$stdin.synchronize do
|
145
|
+
block.call
|
146
|
+
end
|
147
|
+
else
|
148
|
+
block.call
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
96
153
|
sig { type_parameters(:T).params(block: T.proc.returns(T.type_parameter(:T))).returns(T.type_parameter(:T)) }
|
97
154
|
def with_stdin_masked(&block)
|
98
|
-
@
|
155
|
+
@capture_mutex.synchronize do
|
99
156
|
if @active_captures.zero?
|
100
|
-
@
|
101
|
-
|
102
|
-
|
103
|
-
|
157
|
+
@stdin_mutex.synchronize do
|
158
|
+
@saved_stdin = $stdin
|
159
|
+
$stdin = BlockingInput.new(@saved_stdin)
|
160
|
+
end
|
104
161
|
end
|
105
162
|
@active_captures += 1
|
106
163
|
end
|
107
164
|
|
108
165
|
yield
|
109
166
|
ensure
|
110
|
-
@
|
167
|
+
@capture_mutex.synchronize do
|
111
168
|
@active_captures -= 1
|
112
169
|
if @active_captures.zero?
|
113
|
-
|
170
|
+
@stdin_mutex.synchronize do
|
171
|
+
$stdin = @saved_stdin
|
172
|
+
end
|
114
173
|
end
|
115
174
|
end
|
116
175
|
end
|
176
|
+
|
177
|
+
private
|
178
|
+
|
179
|
+
sig { returns(T::Boolean) }
|
180
|
+
def outermost_uncaptured?
|
181
|
+
@stdin_mutex.count == 1 && $stdin.is_a?(BlockingInput)
|
182
|
+
end
|
117
183
|
end
|
118
184
|
|
119
185
|
sig do
|
120
|
-
params(
|
186
|
+
params(
|
187
|
+
with_frame_inset: T::Boolean,
|
188
|
+
merged_output: T::Boolean,
|
189
|
+
duplicate_output_to: IO,
|
190
|
+
block: T.proc.void,
|
191
|
+
).void
|
121
192
|
end
|
122
|
-
def initialize(
|
193
|
+
def initialize(
|
194
|
+
with_frame_inset: true,
|
195
|
+
merged_output: false,
|
196
|
+
duplicate_output_to: File.open(File::NULL, 'w'),
|
197
|
+
&block
|
198
|
+
)
|
123
199
|
@with_frame_inset = with_frame_inset
|
200
|
+
@merged_output = merged_output
|
201
|
+
@duplicate_output_to = duplicate_output_to
|
124
202
|
@block = block
|
125
|
-
@
|
126
|
-
@
|
203
|
+
@print_captured_output = false
|
204
|
+
@out = StringIO.new
|
205
|
+
@err = StringIO.new
|
127
206
|
end
|
128
207
|
|
129
|
-
sig { returns(
|
130
|
-
|
208
|
+
sig { returns(T::Boolean) }
|
209
|
+
attr_accessor :print_captured_output
|
131
210
|
|
132
211
|
sig { returns(T.untyped) }
|
133
212
|
def run
|
@@ -135,8 +214,7 @@ module CLI
|
|
135
214
|
|
136
215
|
StdoutRouter.assert_enabled!
|
137
216
|
|
138
|
-
|
139
|
-
err = StringIO.new
|
217
|
+
Thread.current[:cliui_current_capture] = self
|
140
218
|
|
141
219
|
prev_frame_inset = Thread.current[:no_cliui_frame_inset]
|
142
220
|
prev_hook = Thread.current[:cliui_output_hook]
|
@@ -148,24 +226,90 @@ module CLI
|
|
148
226
|
self.class.with_stdin_masked do
|
149
227
|
Thread.current[:no_cliui_frame_inset] = !@with_frame_inset
|
150
228
|
Thread.current[:cliui_output_hook] = ->(data, stream) do
|
229
|
+
stream = :stdout if @merged_output
|
151
230
|
case stream
|
152
|
-
when :stdout
|
153
|
-
|
231
|
+
when :stdout
|
232
|
+
@out.write(data)
|
233
|
+
@duplicate_output_to.write(data)
|
234
|
+
when :stderr
|
235
|
+
@err.write(data)
|
154
236
|
else raise
|
155
237
|
end
|
156
|
-
|
238
|
+
print_captured_output # suppress writing to terminal by default
|
157
239
|
end
|
158
240
|
|
159
|
-
|
160
|
-
@block.call
|
161
|
-
ensure
|
162
|
-
@stdout = out.string
|
163
|
-
@stderr = err.string
|
164
|
-
end
|
241
|
+
@block.call
|
165
242
|
end
|
166
243
|
ensure
|
167
244
|
Thread.current[:cliui_output_hook] = prev_hook
|
168
245
|
Thread.current[:no_cliui_frame_inset] = prev_frame_inset
|
246
|
+
Thread.current[:cliui_current_capture] = nil
|
247
|
+
end
|
248
|
+
|
249
|
+
sig { returns(String) }
|
250
|
+
def stdout
|
251
|
+
@out.string
|
252
|
+
end
|
253
|
+
|
254
|
+
sig { returns(String) }
|
255
|
+
def stderr
|
256
|
+
@err.string
|
257
|
+
end
|
258
|
+
|
259
|
+
class BlockingInput
|
260
|
+
extend T::Sig
|
261
|
+
|
262
|
+
sig { params(stream: IO).void }
|
263
|
+
def initialize(stream)
|
264
|
+
@stream = stream
|
265
|
+
@m = ReentrantMutex.new
|
266
|
+
end
|
267
|
+
|
268
|
+
sig { type_parameters(:T).params(block: T.proc.returns(T.type_parameter(:T))).returns(T.type_parameter(:T)) }
|
269
|
+
def synchronize(&block)
|
270
|
+
@m.synchronize do
|
271
|
+
previous_allowed_to_read = Thread.current[:cliui_allowed_to_read]
|
272
|
+
Thread.current[:cliui_allowed_to_read] = true
|
273
|
+
block.call
|
274
|
+
ensure
|
275
|
+
Thread.current[:cliui_allowed_to_read] = previous_allowed_to_read
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
READING_METHODS = [
|
280
|
+
:each,
|
281
|
+
:each_byte,
|
282
|
+
:each_char,
|
283
|
+
:each_codepoint,
|
284
|
+
:each_line,
|
285
|
+
:getbyte,
|
286
|
+
:getc,
|
287
|
+
:getch,
|
288
|
+
:gets,
|
289
|
+
:read,
|
290
|
+
:read_nonblock,
|
291
|
+
:readbyte,
|
292
|
+
:readchar,
|
293
|
+
:readline,
|
294
|
+
:readlines,
|
295
|
+
:readpartial,
|
296
|
+
]
|
297
|
+
|
298
|
+
NON_READING_METHODS = IO.instance_methods(false) - READING_METHODS
|
299
|
+
|
300
|
+
READING_METHODS.each do |method|
|
301
|
+
define_method(method) do |*args, **kwargs, &block|
|
302
|
+
raise(IOError, 'closed stream') unless Thread.current[:cliui_allowed_to_read]
|
303
|
+
|
304
|
+
@stream.send(method, *args, **kwargs, &block)
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
NON_READING_METHODS.each do |method|
|
309
|
+
define_method(method) do |*args, **kwargs, &block|
|
310
|
+
@stream.send(method, *args, **kwargs, &block)
|
311
|
+
end
|
312
|
+
end
|
169
313
|
end
|
170
314
|
end
|
171
315
|
|
data/lib/cli/ui/version.rb
CHANGED
data/lib/cli/ui.rb
CHANGED
@@ -89,6 +89,17 @@ module CLI
|
|
89
89
|
CLI::UI::Prompt.confirm(question, default: default)
|
90
90
|
end
|
91
91
|
|
92
|
+
# Convenience Method for +CLI::UI::Prompt.any_key+
|
93
|
+
#
|
94
|
+
# ==== Attributes
|
95
|
+
#
|
96
|
+
# * +prompt+ - prompt to present
|
97
|
+
#
|
98
|
+
sig { params(prompt: String).returns(T.nilable(String)) }
|
99
|
+
def any_key(prompt = 'Press any key to continue')
|
100
|
+
CLI::UI::Prompt.any_key(prompt)
|
101
|
+
end
|
102
|
+
|
92
103
|
# Convenience Method for +CLI::UI::Prompt.ask+
|
93
104
|
sig do
|
94
105
|
params(
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cli-ui
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Burke Libbey
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: exe
|
12
12
|
cert_chain: []
|
13
|
-
date:
|
13
|
+
date: 2023-04-17 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: minitest
|