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.rb
CHANGED
@@ -1,5 +1,13 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
3
|
+
unless defined?(T)
|
4
|
+
require('cli/ui/sorbet_runtime_stub')
|
5
|
+
end
|
6
|
+
|
1
7
|
module CLI
|
2
8
|
module UI
|
9
|
+
extend T::Sig
|
10
|
+
|
3
11
|
autoload :ANSI, 'cli/ui/ansi'
|
4
12
|
autoload :Glyph, 'cli/ui/glyph'
|
5
13
|
autoload :Color, 'cli/ui/color'
|
@@ -18,215 +26,355 @@ module CLI
|
|
18
26
|
# Convenience accessor to +CLI::UI::Spinner::SpinGroup+
|
19
27
|
SpinGroup = Spinner::SpinGroup
|
20
28
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
#
|
28
|
-
def self.glyph(handle)
|
29
|
-
CLI::UI::Glyph.lookup(handle)
|
30
|
-
end
|
29
|
+
Colorable = T.type_alias { T.any(Symbol, String, CLI::UI::Color) }
|
30
|
+
FrameStylable = T.type_alias { T.any(Symbol, String, CLI::UI::Frame::FrameStyle) }
|
31
|
+
IOLike = T.type_alias { T.any(IO, StringIO) }
|
32
|
+
|
33
|
+
class << self
|
34
|
+
extend T::Sig
|
31
35
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
input
|
43
|
-
else
|
44
|
-
CLI::UI::Color.lookup(input)
|
36
|
+
# Glyph resolution using +CLI::UI::Glyph.lookup+
|
37
|
+
# Look at the method signature for +Glyph.lookup+ for more details
|
38
|
+
#
|
39
|
+
# ==== Attributes
|
40
|
+
#
|
41
|
+
# * +handle+ - handle of the glyph to resolve
|
42
|
+
#
|
43
|
+
sig { params(handle: String).returns(Glyph) }
|
44
|
+
def glyph(handle)
|
45
|
+
CLI::UI::Glyph.lookup(handle)
|
45
46
|
end
|
46
|
-
end
|
47
47
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
input
|
58
|
-
|
59
|
-
|
48
|
+
# Color resolution using +CLI::UI::Color.lookup+
|
49
|
+
# Will lookup using +Color.lookup+ unless it's already a CLI::UI::Color (or nil)
|
50
|
+
#
|
51
|
+
# ==== Attributes
|
52
|
+
#
|
53
|
+
# * +input+ - color to resolve
|
54
|
+
#
|
55
|
+
sig { params(input: Colorable).returns(CLI::UI::Color) }
|
56
|
+
def resolve_color(input)
|
57
|
+
case input
|
58
|
+
when CLI::UI::Color
|
59
|
+
input
|
60
|
+
else
|
61
|
+
CLI::UI::Color.lookup(input)
|
62
|
+
end
|
60
63
|
end
|
61
|
-
end
|
62
64
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
65
|
+
# Frame style resolution using +CLI::UI::Frame::FrameStyle.lookup+.
|
66
|
+
# Will lookup using +FrameStyle.lookup+ unless it's already a CLI::UI::Frame::FrameStyle(or nil)
|
67
|
+
#
|
68
|
+
# ==== Attributes
|
69
|
+
#
|
70
|
+
# * +input+ - frame style to resolve
|
71
|
+
sig { params(input: FrameStylable).returns(CLI::UI::Frame::FrameStyle) }
|
72
|
+
def resolve_style(input)
|
73
|
+
case input
|
74
|
+
when CLI::UI::Frame::FrameStyle
|
75
|
+
input
|
76
|
+
else
|
77
|
+
CLI::UI::Frame::FrameStyle.lookup(input.to_s)
|
78
|
+
end
|
79
|
+
end
|
72
80
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
81
|
+
# Convenience Method for +CLI::UI::Prompt.confirm+
|
82
|
+
#
|
83
|
+
# ==== Attributes
|
84
|
+
#
|
85
|
+
# * +question+ - question to confirm
|
86
|
+
#
|
87
|
+
sig { params(question: String, default: T::Boolean).returns(T::Boolean) }
|
88
|
+
def confirm(question, default: true)
|
89
|
+
CLI::UI::Prompt.confirm(question, default: default)
|
90
|
+
end
|
83
91
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
formatted = CLI::UI::Formatter.new(input).format
|
95
|
-
return formatted unless truncate_to
|
96
|
-
CLI::UI::Truncater.call(formatted, truncate_to)
|
97
|
-
end
|
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
|
98
102
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
103
|
+
# Convenience Method for +CLI::UI::Prompt.ask+
|
104
|
+
sig do
|
105
|
+
params(
|
106
|
+
question: String,
|
107
|
+
options: T.nilable(T::Array[String]),
|
108
|
+
default: T.nilable(T.any(String, T::Array[String])),
|
109
|
+
is_file: T::Boolean,
|
110
|
+
allow_empty: T::Boolean,
|
111
|
+
multiple: T::Boolean,
|
112
|
+
filter_ui: T::Boolean,
|
113
|
+
select_ui: T::Boolean,
|
114
|
+
options_proc: T.nilable(T.proc.params(handler: Prompt::OptionsHandler).void),
|
115
|
+
).returns(T.any(String, T::Array[String]))
|
116
|
+
end
|
117
|
+
def ask(
|
118
|
+
question,
|
119
|
+
options: nil,
|
120
|
+
default: nil,
|
121
|
+
is_file: false,
|
122
|
+
allow_empty: true,
|
123
|
+
multiple: false,
|
124
|
+
filter_ui: true,
|
125
|
+
select_ui: true,
|
126
|
+
&options_proc
|
127
|
+
)
|
128
|
+
CLI::UI::Prompt.ask(
|
129
|
+
question,
|
130
|
+
options: options,
|
131
|
+
default: default,
|
132
|
+
is_file: is_file,
|
133
|
+
allow_empty: allow_empty,
|
134
|
+
multiple: multiple,
|
135
|
+
filter_ui: filter_ui,
|
136
|
+
select_ui: select_ui,
|
137
|
+
&options_proc
|
138
|
+
)
|
139
|
+
end
|
116
140
|
|
117
|
-
|
118
|
-
CLI::UI::
|
119
|
-
|
141
|
+
# Convenience Method to resolve text using +CLI::UI::Formatter.format+
|
142
|
+
# Check +CLI::UI::Formatter::SGR_MAP+ for available formatting options
|
143
|
+
#
|
144
|
+
# ==== Attributes
|
145
|
+
#
|
146
|
+
# * +input+ - input to format
|
147
|
+
# * +truncate_to+ - number of characters to truncate the string to (or nil)
|
148
|
+
#
|
149
|
+
sig { params(input: String, truncate_to: T.nilable(Integer)).returns(String) }
|
150
|
+
def resolve_text(input, truncate_to: nil)
|
151
|
+
formatted = CLI::UI::Formatter.new(input).format
|
152
|
+
return formatted unless truncate_to
|
120
153
|
|
121
|
-
|
122
|
-
|
123
|
-
# ==== Attributes
|
124
|
-
#
|
125
|
-
# * +msg+ - Message to print
|
126
|
-
# * +kwargs+ - keyword arguments for +Printer.puts+
|
127
|
-
#
|
128
|
-
def self.puts(msg, **kwargs)
|
129
|
-
CLI::UI::Printer.puts(msg, **kwargs)
|
130
|
-
end
|
154
|
+
CLI::UI::Truncater.call(formatted, truncate_to)
|
155
|
+
end
|
131
156
|
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
157
|
+
# Convenience Method to format text using +CLI::UI::Formatter.format+
|
158
|
+
# Check +CLI::UI::Formatter::SGR_MAP+ for available formatting options
|
159
|
+
#
|
160
|
+
# https://user-images.githubusercontent.com/3074765/33799827-6d0721a2-dd01-11e7-9ab5-c3d455264afe.png
|
161
|
+
# https://user-images.githubusercontent.com/3074765/33799847-9ec03fd0-dd01-11e7-93f7-5f5cc540e61e.png
|
162
|
+
#
|
163
|
+
# ==== Attributes
|
164
|
+
#
|
165
|
+
# * +input+ - input to format
|
166
|
+
#
|
167
|
+
# ==== Options
|
168
|
+
#
|
169
|
+
# * +enable_color+ - should color be used? default to true unless output is redirected.
|
170
|
+
#
|
171
|
+
sig { params(input: String, enable_color: T::Boolean).returns(String) }
|
172
|
+
def fmt(input, enable_color: enable_color?)
|
173
|
+
CLI::UI::Formatter.new(input).format(enable_color: enable_color)
|
174
|
+
end
|
142
175
|
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
# * +args+ - arguments for +Spinner.open+
|
148
|
-
# * +block+ - block for +Spinner.open+
|
149
|
-
#
|
150
|
-
def self.spinner(*args, **kwargs, &block)
|
151
|
-
CLI::UI::Spinner.spin(*args, **kwargs, &block)
|
152
|
-
end
|
176
|
+
sig { params(input: String).returns(String) }
|
177
|
+
def wrap(input)
|
178
|
+
CLI::UI::Wrap.new(input).wrap
|
179
|
+
end
|
153
180
|
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
181
|
+
# Convenience Method for +CLI::UI::Printer.puts+
|
182
|
+
#
|
183
|
+
# ==== Attributes
|
184
|
+
#
|
185
|
+
# * +msg+ - Message to print
|
186
|
+
# * +kwargs+ - keyword arguments for +Printer.puts+
|
187
|
+
#
|
188
|
+
sig do
|
189
|
+
params(
|
190
|
+
msg: String,
|
191
|
+
frame_color: T.nilable(Colorable),
|
192
|
+
to: IOLike,
|
193
|
+
encoding: Encoding,
|
194
|
+
format: T::Boolean,
|
195
|
+
graceful: T::Boolean,
|
196
|
+
wrap: T::Boolean,
|
197
|
+
).void
|
198
|
+
end
|
199
|
+
def puts(
|
200
|
+
msg,
|
201
|
+
frame_color: nil,
|
202
|
+
to: $stdout,
|
203
|
+
encoding: Encoding::UTF_8,
|
204
|
+
format: true,
|
205
|
+
graceful: true,
|
206
|
+
wrap: true
|
207
|
+
)
|
208
|
+
CLI::UI::Printer.puts(
|
209
|
+
msg,
|
210
|
+
frame_color: frame_color,
|
211
|
+
to: to,
|
212
|
+
encoding: encoding,
|
213
|
+
format: format,
|
214
|
+
graceful: graceful,
|
215
|
+
wrap: wrap,
|
216
|
+
)
|
217
|
+
end
|
164
218
|
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
219
|
+
# Convenience Method for +CLI::UI::Frame.open+
|
220
|
+
#
|
221
|
+
# ==== Attributes
|
222
|
+
#
|
223
|
+
# * +args+ - arguments for +Frame.open+
|
224
|
+
# * +block+ - block for +Frame.open+
|
225
|
+
#
|
226
|
+
sig do
|
227
|
+
type_parameters(:T).params(
|
228
|
+
text: String,
|
229
|
+
color: T.nilable(Colorable),
|
230
|
+
failure_text: T.nilable(String),
|
231
|
+
success_text: T.nilable(String),
|
232
|
+
timing: T.any(T::Boolean, Numeric),
|
233
|
+
frame_style: FrameStylable,
|
234
|
+
block: T.nilable(T.proc.returns(T.type_parameter(:T))),
|
235
|
+
).returns(T.nilable(T.type_parameter(:T)))
|
236
|
+
end
|
237
|
+
def frame(
|
238
|
+
text,
|
239
|
+
color: Frame::DEFAULT_FRAME_COLOR,
|
240
|
+
failure_text: nil,
|
241
|
+
success_text: nil,
|
242
|
+
timing: block_given?,
|
243
|
+
frame_style: Frame.frame_style,
|
244
|
+
&block
|
245
|
+
)
|
246
|
+
CLI::UI::Frame.open(
|
247
|
+
text,
|
248
|
+
color: color,
|
249
|
+
failure_text: failure_text,
|
250
|
+
success_text: success_text,
|
251
|
+
timing: timing,
|
252
|
+
frame_style: frame_style,
|
253
|
+
&block
|
254
|
+
)
|
181
255
|
end
|
182
|
-
end
|
183
256
|
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
257
|
+
# Convenience Method for +CLI::UI::Spinner.spin+
|
258
|
+
#
|
259
|
+
# ==== Attributes
|
260
|
+
#
|
261
|
+
# * +args+ - arguments for +Spinner.open+
|
262
|
+
# * +block+ - block for +Spinner.open+
|
263
|
+
#
|
264
|
+
sig do
|
265
|
+
params(title: String, auto_debrief: T::Boolean, block: T.proc.params(task: Spinner::SpinGroup::Task).void)
|
266
|
+
.returns(T::Boolean)
|
267
|
+
end
|
268
|
+
def spinner(title, auto_debrief: true, &block)
|
269
|
+
CLI::UI::Spinner.spin(title, auto_debrief: auto_debrief, &block)
|
270
|
+
end
|
197
271
|
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
272
|
+
# Convenience Method to override frame color using +CLI::UI::Frame.with_frame_color+
|
273
|
+
#
|
274
|
+
# ==== Attributes
|
275
|
+
#
|
276
|
+
# * +color+ - color to override to
|
277
|
+
# * +block+ - block for +Frame.with_frame_color_override+
|
278
|
+
#
|
279
|
+
sig do
|
280
|
+
type_parameters(:T)
|
281
|
+
.params(color: Colorable, block: T.proc.returns(T.type_parameter(:T)))
|
282
|
+
.returns(T.type_parameter(:T))
|
283
|
+
end
|
284
|
+
def with_frame_color(color, &block)
|
285
|
+
CLI::UI::Frame.with_frame_color_override(color, &block)
|
286
|
+
end
|
205
287
|
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
288
|
+
# Duplicate output to a file path
|
289
|
+
#
|
290
|
+
# ==== Attributes
|
291
|
+
#
|
292
|
+
# * +path+ - path to duplicate output to
|
293
|
+
#
|
294
|
+
sig do
|
295
|
+
type_parameters(:T)
|
296
|
+
.params(path: String, block: T.proc.returns(T.type_parameter(:T)))
|
297
|
+
.returns(T.type_parameter(:T))
|
298
|
+
end
|
299
|
+
def log_output_to(path, &block)
|
300
|
+
if CLI::UI::StdoutRouter.duplicate_output_to
|
301
|
+
raise 'multiple logs not allowed'
|
302
|
+
end
|
215
303
|
|
216
|
-
|
304
|
+
CLI::UI::StdoutRouter.duplicate_output_to = File.open(path, 'w')
|
305
|
+
yield
|
306
|
+
ensure
|
307
|
+
if (file_descriptor = CLI::UI::StdoutRouter.duplicate_output_to)
|
308
|
+
begin
|
309
|
+
file_descriptor.close
|
310
|
+
rescue IOError
|
311
|
+
nil
|
312
|
+
end
|
313
|
+
CLI::UI::StdoutRouter.duplicate_output_to = nil
|
314
|
+
end
|
315
|
+
end
|
217
316
|
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
317
|
+
# Disable all framing within a block
|
318
|
+
#
|
319
|
+
# ==== Attributes
|
320
|
+
#
|
321
|
+
# * +block+ - block in which to disable frames
|
322
|
+
#
|
323
|
+
sig { type_parameters(:T).params(block: T.proc.returns(T.type_parameter(:T))).returns(T.type_parameter(:T)) }
|
324
|
+
def raw(&block)
|
325
|
+
prev = Thread.current[:no_cliui_frame_inset]
|
326
|
+
Thread.current[:no_cliui_frame_inset] = true
|
327
|
+
yield
|
328
|
+
ensure
|
329
|
+
Thread.current[:no_cliui_frame_inset] = prev
|
330
|
+
end
|
331
|
+
|
332
|
+
# Check whether colour is enabled in Formatter output. By default, colour
|
333
|
+
# is enabled when STDOUT is a TTY; that is, when output has not been
|
334
|
+
# redirected to another program or to a file.
|
335
|
+
#
|
336
|
+
sig { returns(T::Boolean) }
|
337
|
+
def enable_color?
|
338
|
+
@enable_color
|
339
|
+
end
|
340
|
+
|
341
|
+
# Turn colour output in Formatter on or off.
|
342
|
+
#
|
343
|
+
# ==== Attributes
|
344
|
+
#
|
345
|
+
# * +bool+ - true or false; enable or disable colour.
|
346
|
+
#
|
347
|
+
sig { params(bool: T::Boolean).void }
|
348
|
+
def enable_color=(bool)
|
349
|
+
@enable_color = !!bool
|
350
|
+
end
|
351
|
+
|
352
|
+
# Set the default frame style.
|
353
|
+
# Convenience method for setting the default frame style with +CLI::UI::Frame.frame_style=+
|
354
|
+
#
|
355
|
+
# Raises ArgumentError if +frame_style+ is not valid
|
356
|
+
#
|
357
|
+
# ==== Attributes
|
358
|
+
#
|
359
|
+
# * +symbol+ - the default frame style to use for frames
|
360
|
+
#
|
361
|
+
sig { params(frame_style: FrameStylable).void }
|
362
|
+
def frame_style=(frame_style)
|
363
|
+
Frame.frame_style = frame_style
|
364
|
+
end
|
365
|
+
|
366
|
+
# Create a terminal link
|
367
|
+
sig { params(url: String, text: String, format: T::Boolean, blue_underline: T::Boolean).returns(String) }
|
368
|
+
def link(url, text, format: true, blue_underline: format)
|
369
|
+
raise 'cannot use blue_underline without format' if blue_underline && !format
|
370
|
+
|
371
|
+
text = "{{blue:{{underline:#{text}}}}}" if blue_underline
|
372
|
+
text = CLI::UI.fmt(text) if format
|
373
|
+
"\x1b]8;;#{url}\x1b\\#{text}\x1b]8;;\x1b\\"
|
374
|
+
end
|
229
375
|
end
|
376
|
+
|
377
|
+
self.enable_color = $stdout.tty?
|
230
378
|
end
|
231
379
|
end
|
232
380
|
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# Copyright (c) 2014 Boris Bera
|
2
|
+
#
|
3
|
+
# MIT License
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
# a copy of this software and associated documentation files (the
|
7
|
+
# "Software"), to deal in the Software without restriction, including
|
8
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
# the following conditions:
|
12
|
+
#
|
13
|
+
# The above copyright notice and this permission notice shall be
|
14
|
+
# included in all copies or substantial portions of the Software.
|
15
|
+
#
|
16
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
23
|
+
|
24
|
+
# Sourced from https://github.com/dotboris/reentrant_mutex
|
25
|
+
module CLI
|
26
|
+
module UI
|
27
|
+
class ReentrantMutex < Mutex
|
28
|
+
def initialize
|
29
|
+
@count_mutex = Mutex.new
|
30
|
+
@counts = Hash.new(0)
|
31
|
+
|
32
|
+
super
|
33
|
+
end
|
34
|
+
|
35
|
+
def synchronize
|
36
|
+
raise ThreadError, 'Must be called with a block' unless block_given?
|
37
|
+
|
38
|
+
begin
|
39
|
+
lock
|
40
|
+
yield
|
41
|
+
ensure
|
42
|
+
unlock
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def lock
|
47
|
+
c = increase_count Thread.current
|
48
|
+
super if c <= 1
|
49
|
+
end
|
50
|
+
|
51
|
+
def unlock
|
52
|
+
c = decrease_count Thread.current
|
53
|
+
if c <= 0
|
54
|
+
super
|
55
|
+
delete_count Thread.current
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def count
|
60
|
+
@count_mutex.synchronize { @counts[Thread.current] }
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def increase_count(thread)
|
66
|
+
@count_mutex.synchronize { @counts[thread] += 1 }
|
67
|
+
end
|
68
|
+
|
69
|
+
def decrease_count(thread)
|
70
|
+
@count_mutex.synchronize { @counts[thread] -= 1 }
|
71
|
+
end
|
72
|
+
|
73
|
+
def delete_count(thread)
|
74
|
+
@count_mutex.synchronize { @counts.delete(thread) }
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|