cli-ui 1.5.1 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,157 @@
1
+ # typed: ignore
2
+ # frozen_string_literal: true
3
+
4
+ module T
5
+ class << self
6
+ def absurd(value); end
7
+ def all(type_a, type_b, *types); end
8
+ def any(type_a, type_b, *types); end
9
+ def attached_class; end
10
+ def class_of(klass); end
11
+ def enum(values); end
12
+ def nilable(type); end
13
+ def noreturn; end
14
+ def self_type; end
15
+ def type_alias(type = nil, &_blk); end
16
+ def type_parameter(name); end
17
+ def untyped; end
18
+
19
+ def assert_type!(value, _type, _checked: true)
20
+ value
21
+ end
22
+
23
+ def cast(value, _type, _checked: true)
24
+ value
25
+ end
26
+
27
+ def let(value, _type, _checked: true)
28
+ value
29
+ end
30
+
31
+ def must(arg, _msg = nil)
32
+ arg
33
+ end
34
+
35
+ def proc
36
+ T::Proc.new
37
+ end
38
+
39
+ def reveal_type(value)
40
+ value
41
+ end
42
+
43
+ def unsafe(value)
44
+ value
45
+ end
46
+ end
47
+
48
+ module Sig
49
+ def sig(arg0 = nil, &blk); end
50
+ end
51
+
52
+ module Helpers
53
+ def abstract!; end
54
+ def interface!; end
55
+ def final!; end
56
+ def sealed!; end
57
+ def mixes_in_class_methods(mod); end
58
+ end
59
+
60
+ module Generic
61
+ include(T::Helpers)
62
+
63
+ def type_parameters(*params); end
64
+ def type_member(variance = :invariant, fixed: nil, lower: nil, upper: BasicObject); end
65
+ def type_template(variance = :invariant, fixed: nil, lower: nil, upper: BasicObject); end
66
+
67
+ def [](*types)
68
+ self
69
+ end
70
+ end
71
+
72
+ module Array
73
+ class << self
74
+ def [](type); end
75
+ end
76
+ end
77
+
78
+ Boolean = Object.new.freeze
79
+
80
+ module Configuration
81
+ class << self
82
+ def call_validation_error_handler(signature, opts); end
83
+ def call_validation_error_handler=(value); end
84
+ def default_checked_level=(default_checked_level); end
85
+ def enable_checking_for_sigs_marked_checked_tests; end
86
+ def enable_final_checks_on_hooks; end
87
+ def enable_legacy_t_enum_migration_mode; end
88
+ def reset_final_checks_on_hooks; end
89
+ def hard_assert_handler(str, extra); end
90
+ def hard_assert_handler=(value); end
91
+ def inline_type_error_handler(error); end
92
+ def inline_type_error_handler=(value); end
93
+ def log_info_handler(str, extra); end
94
+ def log_info_handler=(value); end
95
+ def scalar_types; end
96
+ def scalar_types=(values); end
97
+ # rubocop:disable Naming/InclusiveLanguage
98
+ def sealed_violation_whitelist; end
99
+ def sealed_violation_whitelist=(sealed_violation_whitelist); end
100
+ # rubocop:enable Naming/InclusiveLanguage
101
+ def sig_builder_error_handler(error, location); end
102
+ def sig_builder_error_handler=(value); end
103
+ def sig_validation_error_handler(error, opts); end
104
+ def sig_validation_error_handler=(value); end
105
+ def soft_assert_handler(str, extra); end
106
+ def soft_assert_handler=(value); end
107
+ end
108
+ end
109
+
110
+ module Enumerable
111
+ class << self
112
+ def [](type); end
113
+ end
114
+ end
115
+
116
+ module Enumerator
117
+ class << self
118
+ def [](type); end
119
+ end
120
+ end
121
+
122
+ module Hash
123
+ class << self
124
+ def [](keys, values); end
125
+ end
126
+ end
127
+
128
+ class Proc
129
+ def bind(*_)
130
+ self
131
+ end
132
+
133
+ def params(*_param)
134
+ self
135
+ end
136
+
137
+ def void
138
+ self
139
+ end
140
+
141
+ def returns(_type)
142
+ self
143
+ end
144
+ end
145
+
146
+ module Range
147
+ class << self
148
+ def [](type); end
149
+ end
150
+ end
151
+
152
+ module Set
153
+ class << self
154
+ def [](type); end
155
+ end
156
+ end
157
+ end
@@ -1,11 +1,20 @@
1
+ # typed: true
2
+
1
3
  module CLI
2
4
  module UI
3
5
  module Spinner
4
6
  class Async
5
- # Convenience method for +initialize+
6
- #
7
- def self.start(title)
8
- new(title)
7
+ extend T::Sig
8
+
9
+ class << self
10
+ extend T::Sig
11
+
12
+ # Convenience method for +initialize+
13
+ #
14
+ sig { params(title: String).returns(Async) }
15
+ def start(title)
16
+ new(title)
17
+ end
9
18
  end
10
19
 
11
20
  # Initializes a new asynchronous spinner with no specific end.
@@ -19,6 +28,7 @@ module CLI
19
28
  #
20
29
  # CLI::UI::Spinner::Async.new('Title')
21
30
  #
31
+ sig { params(title: String).void }
22
32
  def initialize(title)
23
33
  require 'thread'
24
34
  sg = CLI::UI::Spinner::SpinGroup.new
@@ -30,6 +40,7 @@ module CLI
30
40
 
31
41
  # Stops an asynchronous spinner
32
42
  #
43
+ sig { returns(T::Boolean) }
33
44
  def stop
34
45
  @m.synchronize { @cv.signal }
35
46
  @t.value
@@ -1,35 +1,53 @@
1
+ # typed: true
2
+
1
3
  module CLI
2
4
  module UI
3
5
  module Spinner
4
6
  class SpinGroup
7
+ extend T::Sig
8
+
5
9
  # Initializes a new spin group
6
10
  # This lets you add +Task+ objects to the group to multi-thread work
7
11
  #
8
12
  # ==== Options
9
13
  #
10
- # * +:auto_debrief+ - Automatically debrief exceptions? Default to true
14
+ # * +:auto_debrief+ - Automatically debrief exceptions or through success_debrief? Default to true
11
15
  #
12
16
  # ==== Example Usage
13
17
  #
14
- # spin_group = CLI::UI::SpinGroup.new
15
- # spin_group.add('Title') { |spinner| sleep 3.0 }
16
- # spin_group.add('Title 2') { |spinner| sleep 3.0; spinner.update_title('New Title'); sleep 3.0 }
17
- # spin_group.wait
18
+ # CLI::UI::SpinGroup.new do |spin_group|
19
+ # spin_group.add('Title') { |spinner| sleep 3.0 }
20
+ # spin_group.add('Title 2') { |spinner| sleep 3.0; spinner.update_title('New Title'); sleep 3.0 }
21
+ # end
18
22
  #
19
23
  # Output:
20
24
  #
21
25
  # https://user-images.githubusercontent.com/3074765/33798558-c452fa26-dce8-11e7-9e90-b4b34df21a46.gif
22
26
  #
27
+ sig { params(auto_debrief: T::Boolean).void }
23
28
  def initialize(auto_debrief: true)
24
29
  @m = Mutex.new
25
30
  @consumed_lines = 0
26
31
  @tasks = []
27
32
  @auto_debrief = auto_debrief
28
33
  @start = Time.new
34
+ if block_given?
35
+ yield self
36
+ wait
37
+ end
29
38
  end
30
39
 
31
40
  class Task
32
- attr_reader :title, :exception, :success, :stdout, :stderr
41
+ extend T::Sig
42
+
43
+ sig { returns(String) }
44
+ attr_reader :title, :stdout, :stderr
45
+
46
+ sig { returns(T::Boolean) }
47
+ attr_reader :success
48
+
49
+ sig { returns(T.nilable(Exception)) }
50
+ attr_reader :exception
33
51
 
34
52
  # Initializes a new Task
35
53
  # This is managed entirely internally by +SpinGroup+
@@ -39,11 +57,12 @@ module CLI
39
57
  # * +title+ - Title of the task
40
58
  # * +block+ - Block for the task, will be provided with an instance of the spinner
41
59
  #
60
+ sig { params(title: String, block: T.proc.params(task: Task).returns(T.untyped)).void }
42
61
  def initialize(title, &block)
43
62
  @title = title
44
63
  @always_full_render = title =~ Formatter::SCAN_WIDGET
45
64
  @thread = Thread.new do
46
- cap = CLI::UI::StdoutRouter::Capture.new(self, with_frame_inset: false, &block)
65
+ cap = CLI::UI::StdoutRouter::Capture.new(with_frame_inset: false) { block.call(self) }
47
66
  begin
48
67
  cap.run
49
68
  ensure
@@ -61,6 +80,7 @@ module CLI
61
80
 
62
81
  # Checks if a task is finished
63
82
  #
83
+ sig { returns(T::Boolean) }
64
84
  def check
65
85
  return true if @done
66
86
  return false if @thread.alive?
@@ -96,6 +116,7 @@ module CLI
96
116
  # * +force+ - force rerender of the task
97
117
  # * +width+ - current terminal width to format for
98
118
  #
119
+ sig { params(index: Integer, force: T::Boolean, width: Integer).returns(String) }
99
120
  def render(index, force = true, width: CLI::UI::Terminal.width)
100
121
  @m.synchronize do
101
122
  if force || @always_full_render || @force_full_render
@@ -114,6 +135,7 @@ module CLI
114
135
  #
115
136
  # * +title+ - title to change the spinner to
116
137
  #
138
+ sig { params(new_title: String).void }
117
139
  def update_title(new_title)
118
140
  @m.synchronize do
119
141
  @always_full_render = new_title =~ Formatter::SCAN_WIDGET
@@ -122,8 +144,14 @@ module CLI
122
144
  end
123
145
  end
124
146
 
147
+ sig { void }
148
+ def interrupt
149
+ @thread.raise(Interrupt)
150
+ end
151
+
125
152
  private
126
153
 
154
+ sig { params(index: Integer, terminal_width: Integer).returns(String) }
127
155
  def full_render(index, terminal_width)
128
156
  prefix = inset +
129
157
  glyph(index) +
@@ -137,10 +165,12 @@ module CLI
137
165
  "\e[K"
138
166
  end
139
167
 
168
+ sig { params(index: Integer).returns(String) }
140
169
  def partial_render(index)
141
170
  CLI::UI::ANSI.cursor_forward(inset_width) + glyph(index) + CLI::UI::Color::RESET.code
142
171
  end
143
172
 
173
+ sig { params(index: Integer).returns(String) }
144
174
  def glyph(index)
145
175
  if @done
146
176
  @success ? CLI::UI::Glyph::CHECK.to_s : CLI::UI::Glyph::X.to_s
@@ -149,10 +179,12 @@ module CLI
149
179
  end
150
180
  end
151
181
 
182
+ sig { returns(String) }
152
183
  def inset
153
184
  @inset ||= CLI::UI::Frame.prefix
154
185
  end
155
186
 
187
+ sig { returns(Integer) }
156
188
  def inset_width
157
189
  @inset_width ||= CLI::UI::ANSI.printing_width(inset)
158
190
  end
@@ -170,6 +202,7 @@ module CLI
170
202
  # spin_group.add('Title') { |spinner| sleep 1.0 }
171
203
  # spin_group.wait
172
204
  #
205
+ sig { params(title: String, block: T.proc.params(task: Task).void).void }
173
206
  def add(title, &block)
174
207
  @m.synchronize do
175
208
  @tasks << Task.new(title, &block)
@@ -183,11 +216,12 @@ module CLI
183
216
  # spin_group.add('Title') { |spinner| sleep 1.0 }
184
217
  # spin_group.wait
185
218
  #
219
+ sig { returns(T::Boolean) }
186
220
  def wait
187
221
  idx = 0
188
222
 
189
223
  loop do
190
- all_done = true
224
+ all_done = T.let(true, T::Boolean)
191
225
 
192
226
  width = CLI::UI::Terminal.width
193
227
 
@@ -222,24 +256,58 @@ module CLI
222
256
  if @auto_debrief
223
257
  debrief
224
258
  else
225
- @m.synchronize do
226
- @tasks.all?(&:success)
227
- end
259
+ all_succeeded?
260
+ end
261
+ rescue Interrupt
262
+ @tasks.each(&:interrupt)
263
+ raise
264
+ end
265
+
266
+ # Provide an alternative debriefing for failed tasks
267
+ sig do
268
+ params(
269
+ block: T.proc.params(title: String, exception: T.nilable(Exception), out: String, err: String).void,
270
+ ).void
271
+ end
272
+ def failure_debrief(&block)
273
+ @failure_debrief = block
274
+ end
275
+
276
+ # Provide a debriefing for successful tasks
277
+ sig do
278
+ params(
279
+ block: T.proc.params(title: String, out: String, err: String).void,
280
+ ).void
281
+ end
282
+ def success_debrief(&block)
283
+ @success_debrief = block
284
+ end
285
+
286
+ sig { returns(T::Boolean) }
287
+ def all_succeeded?
288
+ @m.synchronize do
289
+ @tasks.all?(&:success)
228
290
  end
229
291
  end
230
292
 
231
293
  # Debriefs failed tasks is +auto_debrief+ is true
232
294
  #
295
+ sig { returns(T::Boolean) }
233
296
  def debrief
234
297
  @m.synchronize do
235
298
  @tasks.each do |task|
236
- next if task.success
237
-
238
- e = task.exception
299
+ title = task.title
239
300
  out = task.stdout
240
301
  err = task.stderr
241
302
 
242
- CLI::UI::Frame.open('Task Failed: ' + task.title, color: :red, timing: Time.new - @start) do
303
+ if task.success
304
+ next @success_debrief&.call(title, out, err)
305
+ end
306
+
307
+ e = task.exception
308
+ next @failure_debrief.call(title, e, out, err) if @failure_debrief
309
+
310
+ CLI::UI::Frame.open('Task Failed: ' + title, color: :red, timing: Time.new - @start) do
243
311
  if e
244
312
  puts "#{e.class}: #{e.message}"
245
313
  puts "\tfrom #{e.backtrace.join("\n\tfrom ")}"
@@ -1,22 +1,33 @@
1
- # frozen-string-literal: true
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
2
4
  require 'cli/ui'
3
5
 
4
6
  module CLI
5
7
  module UI
6
8
  module Spinner
9
+ extend T::Sig
10
+
7
11
  autoload :Async, 'cli/ui/spinner/async'
8
12
  autoload :SpinGroup, 'cli/ui/spinner/spin_group'
9
13
 
10
14
  PERIOD = 0.1 # seconds
11
15
  TASK_FAILED = :task_failed
12
16
 
13
- RUNES = CLI::UI::OS.current.supports_emoji? ? %w(⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏).freeze : %w(\\ | / - \\ | / -).freeze
17
+ RUNES = if CLI::UI::OS.current.use_emoji?
18
+ ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'].freeze
19
+ else
20
+ ['\\', '|', '/', '-', '\\', '|', '/', '-'].freeze
21
+ end
14
22
 
15
23
  colors = [CLI::UI::Color::CYAN.code] * (RUNES.size / 2).ceil +
16
24
  [CLI::UI::Color::MAGENTA.code] * (RUNES.size / 2).to_i
17
25
  GLYPHS = colors.zip(RUNES).map(&:join)
18
26
 
19
27
  class << self
28
+ extend T::Sig
29
+
30
+ sig { returns(T.nilable(Integer)) }
20
31
  attr_accessor(:index)
21
32
 
22
33
  # We use this from CLI::UI::Widgets::Status to render an additional
@@ -29,37 +40,46 @@ module CLI
29
40
  # While it would be possible to stitch through some connection between
30
41
  # the SpinGroup and the Widgets included in its title, this is simpler
31
42
  # in practice and seems unlikely to cause issues in practice.
43
+ sig { returns(String) }
32
44
  def current_rune
33
45
  RUNES[index || 0]
34
46
  end
35
47
  end
36
48
 
37
- # Adds a single spinner
38
- # Uses an interactive session to allow the user to pick an answer
39
- # Can use arrows, y/n, numbers (1/2), and vim bindings to control
40
- #
41
- # https://user-images.githubusercontent.com/3074765/33798295-d94fd822-dce3-11e7-819b-43e5502d490e.gif
42
- #
43
- # ==== Attributes
44
- #
45
- # * +title+ - Title of the spinner to use
46
- #
47
- # ==== Options
48
- #
49
- # * +:auto_debrief+ - Automatically debrief exceptions? Default to true
50
- #
51
- # ==== Block
52
- #
53
- # * *spinner+ - Instance of the spinner. Can call +update_title+ to update the user of changes
54
- #
55
- # ==== Example Usage:
56
- #
57
- # CLI::UI::Spinner.spin('Title') { sleep 1.0 }
58
- #
59
- def self.spin(title, auto_debrief: true, &block)
60
- sg = SpinGroup.new(auto_debrief: auto_debrief)
61
- sg.add(title, &block)
62
- sg.wait
49
+ class << self
50
+ extend T::Sig
51
+
52
+ # Adds a single spinner
53
+ # Uses an interactive session to allow the user to pick an answer
54
+ # Can use arrows, y/n, numbers (1/2), and vim bindings to control
55
+ #
56
+ # https://user-images.githubusercontent.com/3074765/33798295-d94fd822-dce3-11e7-819b-43e5502d490e.gif
57
+ #
58
+ # ==== Attributes
59
+ #
60
+ # * +title+ - Title of the spinner to use
61
+ #
62
+ # ==== Options
63
+ #
64
+ # * +:auto_debrief+ - Automatically debrief exceptions or through success_debrief? Default to true
65
+ #
66
+ # ==== Block
67
+ #
68
+ # * *spinner+ - Instance of the spinner. Can call +update_title+ to update the user of changes
69
+ #
70
+ # ==== Example Usage:
71
+ #
72
+ # CLI::UI::Spinner.spin('Title') { sleep 1.0 }
73
+ #
74
+ sig do
75
+ params(title: String, auto_debrief: T::Boolean, block: T.proc.params(task: SpinGroup::Task).void)
76
+ .returns(T::Boolean)
77
+ end
78
+ def spin(title, auto_debrief: true, &block)
79
+ sg = SpinGroup.new(auto_debrief: auto_debrief)
80
+ sg.add(title, &block)
81
+ sg.wait
82
+ end
63
83
  end
64
84
  end
65
85
  end