cli-ui 1.5.1 → 2.2.3

Sign up to get free protection for your applications and to get access to all the features.
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