cli-ui 2.0.0 → 2.2.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 +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
|