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.
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
- # Glyph resolution using +CLI::UI::Glyph.lookup+
22
- # Look at the method signature for +Glyph.lookup+ for more details
23
- #
24
- # ==== Attributes
25
- #
26
- # * +handle+ - handle of the glyph to resolve
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
- # Color resolution using +CLI::UI::Color.lookup+
33
- # Will lookup using +Color.lookup+ unless it's already a CLI::UI::Color (or nil)
34
- #
35
- # ==== Attributes
36
- #
37
- # * +input+ - color to resolve
38
- #
39
- def self.resolve_color(input)
40
- case input
41
- when CLI::UI::Color, nil
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
- # Frame style resolution using +CLI::UI::Frame::FrameStyle.lookup+.
49
- # Will lookup using +FrameStyle.lookup+ unless it's already a CLI::UI::Frame::FrameStyle(or nil)
50
- #
51
- # ==== Attributes
52
- #
53
- # * +input+ - frame style to resolve
54
- def self.resolve_style(input)
55
- case input
56
- when CLI::UI::Frame::FrameStyle, nil
57
- input
58
- else
59
- CLI::UI::Frame::FrameStyle.lookup(input)
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
- # Convenience Method for +CLI::UI::Prompt.confirm+
64
- #
65
- # ==== Attributes
66
- #
67
- # * +question+ - question to confirm
68
- #
69
- def self.confirm(question, **kwargs)
70
- CLI::UI::Prompt.confirm(question, **kwargs)
71
- end
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
- # Convenience Method for +CLI::UI::Prompt.ask+
74
- #
75
- # ==== Attributes
76
- #
77
- # * +question+ - question to ask
78
- # * +kwargs+ - arguments for +Prompt.ask+
79
- #
80
- def self.ask(question, **kwargs)
81
- CLI::UI::Prompt.ask(question, **kwargs)
82
- end
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
- # Convenience Method to resolve text using +CLI::UI::Formatter.format+
85
- # Check +CLI::UI::Formatter::SGR_MAP+ for available formatting options
86
- #
87
- # ==== Attributes
88
- #
89
- # * +input+ - input to format
90
- # * +truncate_to+ - number of characters to truncate the string to (or nil)
91
- #
92
- def self.resolve_text(input, truncate_to: nil)
93
- return input if input.nil?
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
- # Convenience Method to format text using +CLI::UI::Formatter.format+
100
- # Check +CLI::UI::Formatter::SGR_MAP+ for available formatting options
101
- #
102
- # https://user-images.githubusercontent.com/3074765/33799827-6d0721a2-dd01-11e7-9ab5-c3d455264afe.png
103
- # https://user-images.githubusercontent.com/3074765/33799847-9ec03fd0-dd01-11e7-93f7-5f5cc540e61e.png
104
- #
105
- # ==== Attributes
106
- #
107
- # * +input+ - input to format
108
- #
109
- # ==== Options
110
- #
111
- # * +enable_color+ - should color be used? default to true unless output is redirected.
112
- #
113
- def self.fmt(input, enable_color: enable_color?)
114
- CLI::UI::Formatter.new(input).format(enable_color: enable_color)
115
- end
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
- def self.wrap(input)
118
- CLI::UI::Wrap.new(input).wrap
119
- end
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
- # Convenience Method for +CLI::UI::Printer.puts+
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
- # Convenience Method for +CLI::UI::Frame.open+
133
- #
134
- # ==== Attributes
135
- #
136
- # * +args+ - arguments for +Frame.open+
137
- # * +block+ - block for +Frame.open+
138
- #
139
- def self.frame(*args, **kwargs, &block)
140
- CLI::UI::Frame.open(*args, **kwargs, &block)
141
- end
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
- # Convenience Method for +CLI::UI::Spinner.spin+
144
- #
145
- # ==== Attributes
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
- # Convenience Method to override frame color using +CLI::UI::Frame.with_frame_color+
155
- #
156
- # ==== Attributes
157
- #
158
- # * +color+ - color to override to
159
- # * +block+ - block for +Frame.with_frame_color_override+
160
- #
161
- def self.with_frame_color(color, &block)
162
- CLI::UI::Frame.with_frame_color_override(color, &block)
163
- end
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
- # Duplicate output to a file path
166
- #
167
- # ==== Attributes
168
- #
169
- # * +path+ - path to duplicate output to
170
- #
171
- def self.log_output_to(path)
172
- if CLI::UI::StdoutRouter.duplicate_output_to
173
- raise 'multiple logs not allowed'
174
- end
175
- CLI::UI::StdoutRouter.duplicate_output_to = File.open(path, 'w')
176
- yield
177
- ensure
178
- if (file_descriptor = CLI::UI::StdoutRouter.duplicate_output_to)
179
- file_descriptor.close
180
- CLI::UI::StdoutRouter.duplicate_output_to = nil
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
- # Disable all framing within a block
185
- #
186
- # ==== Attributes
187
- #
188
- # * +block+ - block in which to disable frames
189
- #
190
- def self.raw
191
- prev = Thread.current[:no_cliui_frame_inset]
192
- Thread.current[:no_cliui_frame_inset] = true
193
- yield
194
- ensure
195
- Thread.current[:no_cliui_frame_inset] = prev
196
- end
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
- # Check whether colour is enabled in Formatter output. By default, colour
199
- # is enabled when STDOUT is a TTY; that is, when output has not been
200
- # redirected to another program or to a file.
201
- #
202
- def self.enable_color?
203
- @enable_color
204
- end
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
- # Turn colour output in Formatter on or off.
207
- #
208
- # ==== Attributes
209
- #
210
- # * +bool+ - true or false; enable or disable colour.
211
- #
212
- def self.enable_color=(bool)
213
- @enable_color = !!bool
214
- end
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
- self.enable_color = $stdout.tty?
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
- # Set the default frame style.
219
- # Convenience method for setting the default frame style with +CLI::UI::Frame.frame_style=+
220
- #
221
- # Raises ArgumentError if +frame_style+ is not valid
222
- #
223
- # ==== Attributes
224
- #
225
- # * +symbol+ - the default frame style to use for frames
226
- #
227
- def self.frame_style=(frame_style)
228
- Frame.frame_style = frame_style.to_sym
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