planter-cli 0.0.4 → 3.0.2
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/.gitignore +3 -0
- data/.gitmodules +3 -0
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +31 -0
- data/README.md +33 -4
- data/Rakefile +52 -16
- data/bin/plant +2 -2
- data/docker/Dockerfile +2 -4
- data/docker/Dockerfile-2.6 +4 -6
- data/docker/Dockerfile-2.7 +4 -6
- data/docker/Dockerfile-3.0 +4 -5
- data/docker/Dockerfile-3.3 +11 -0
- data/docker/sources.list +11 -0
- data/lib/planter/array.rb +17 -0
- data/lib/planter/fileentry.rb +5 -1
- data/lib/planter/filelist.rb +13 -4
- data/lib/planter/hash.rb +17 -1
- data/lib/planter/plant.rb +5 -3
- data/lib/planter/prompt.rb +5 -5
- data/lib/planter/string.rb +22 -0
- data/lib/planter/tag.rb +91 -0
- data/lib/planter/version.rb +1 -1
- data/lib/planter.rb +34 -18
- data/lib/tty-spinner/.editorconfig +9 -0
- data/lib/tty-spinner/.github/FUNDING.yml +1 -0
- data/lib/tty-spinner/.github/ISSUE_TEMPLATE/BUG_REPORT.md +31 -0
- data/lib/tty-spinner/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md +23 -0
- data/lib/tty-spinner/.github/ISSUE_TEMPLATE/config.yml +5 -0
- data/lib/tty-spinner/.github/PULL_REQUEST_TEMPLATE.md +19 -0
- data/lib/tty-spinner/.github/workflows/ci.yml +59 -0
- data/lib/tty-spinner/.gitignore +14 -0
- data/lib/tty-spinner/.rspec +2 -0
- data/lib/tty-spinner/.rubocop.yml +78 -0
- data/lib/tty-spinner/CHANGELOG.md +151 -0
- data/lib/tty-spinner/CODE_OF_CONDUCT.md +132 -0
- data/lib/tty-spinner/Gemfile +17 -0
- data/lib/tty-spinner/LICENSE.txt +22 -0
- data/lib/tty-spinner/README.md +581 -0
- data/lib/tty-spinner/Rakefile +10 -0
- data/lib/tty-spinner/appveyor.yml +33 -0
- data/lib/tty-spinner/bin/console +14 -0
- data/lib/tty-spinner/bin/setup +8 -0
- data/lib/tty-spinner/demo.gif +0 -0
- data/lib/tty-spinner/examples/auto_spin.rb +10 -0
- data/lib/tty-spinner/examples/basic.rb +10 -0
- data/lib/tty-spinner/examples/clear.rb +11 -0
- data/lib/tty-spinner/examples/color.rb +14 -0
- data/lib/tty-spinner/examples/error.rb +11 -0
- data/lib/tty-spinner/examples/formats.rb +13 -0
- data/lib/tty-spinner/examples/hide_cursor.rb +14 -0
- data/lib/tty-spinner/examples/log.rb +13 -0
- data/lib/tty-spinner/examples/multi/basic.rb +15 -0
- data/lib/tty-spinner/examples/multi/basic_top_level.rb +15 -0
- data/lib/tty-spinner/examples/multi/custom_style.rb +28 -0
- data/lib/tty-spinner/examples/multi/files.rb +16 -0
- data/lib/tty-spinner/examples/multi/jobs.rb +11 -0
- data/lib/tty-spinner/examples/multi/multi.rb +19 -0
- data/lib/tty-spinner/examples/multi/multi_top_level.rb +20 -0
- data/lib/tty-spinner/examples/multi/pause.rb +28 -0
- data/lib/tty-spinner/examples/multi/threaded.rb +30 -0
- data/lib/tty-spinner/examples/pause.rb +24 -0
- data/lib/tty-spinner/examples/run.rb +20 -0
- data/lib/tty-spinner/examples/success.rb +11 -0
- data/lib/tty-spinner/examples/threaded.rb +13 -0
- data/lib/tty-spinner/examples/update.rb +13 -0
- data/lib/tty-spinner/lib/tty/spinner/formats.rb +274 -0
- data/lib/tty-spinner/lib/tty/spinner/multi.rb +352 -0
- data/lib/tty-spinner/lib/tty/spinner/version.rb +7 -0
- data/lib/tty-spinner/lib/tty/spinner.rb +604 -0
- data/lib/tty-spinner/lib/tty-spinner.rb +2 -0
- data/lib/tty-spinner/spec/spec_helper.rb +52 -0
- data/lib/tty-spinner/spec/unit/auto_spin_spec.rb +25 -0
- data/lib/tty-spinner/spec/unit/clear_spec.rb +16 -0
- data/lib/tty-spinner/spec/unit/error_spec.rb +53 -0
- data/lib/tty-spinner/spec/unit/events_spec.rb +35 -0
- data/lib/tty-spinner/spec/unit/formats_spec.rb +9 -0
- data/lib/tty-spinner/spec/unit/frames_spec.rb +31 -0
- data/lib/tty-spinner/spec/unit/hide_cursor_spec.rb +51 -0
- data/lib/tty-spinner/spec/unit/job_spec.rb +12 -0
- data/lib/tty-spinner/spec/unit/join_spec.rb +10 -0
- data/lib/tty-spinner/spec/unit/log_spec.rb +60 -0
- data/lib/tty-spinner/spec/unit/multi/auto_spin_spec.rb +32 -0
- data/lib/tty-spinner/spec/unit/multi/error_spec.rb +107 -0
- data/lib/tty-spinner/spec/unit/multi/line_inset_spec.rb +57 -0
- data/lib/tty-spinner/spec/unit/multi/on_spec.rb +11 -0
- data/lib/tty-spinner/spec/unit/multi/register_spec.rb +46 -0
- data/lib/tty-spinner/spec/unit/multi/spin_spec.rb +101 -0
- data/lib/tty-spinner/spec/unit/multi/stop_spec.rb +95 -0
- data/lib/tty-spinner/spec/unit/multi/success_spec.rb +108 -0
- data/lib/tty-spinner/spec/unit/new_spec.rb +25 -0
- data/lib/tty-spinner/spec/unit/pause_spec.rb +43 -0
- data/lib/tty-spinner/spec/unit/reset_spec.rb +19 -0
- data/lib/tty-spinner/spec/unit/run_spec.rb +30 -0
- data/lib/tty-spinner/spec/unit/spin_spec.rb +117 -0
- data/lib/tty-spinner/spec/unit/stop_spec.rb +88 -0
- data/lib/tty-spinner/spec/unit/success_spec.rb +53 -0
- data/lib/tty-spinner/spec/unit/tty_spec.rb +8 -0
- data/lib/tty-spinner/spec/unit/update_spec.rb +85 -0
- data/lib/tty-spinner/tasks/console.rake +11 -0
- data/lib/tty-spinner/tasks/coverage.rake +11 -0
- data/lib/tty-spinner/tasks/spec.rake +29 -0
- data/lib/tty-spinner/tty-spinner.gemspec +36 -0
- data/planter-cli.gemspec +1 -0
- data/scripts/runtests.sh +1 -1
- data/spec/cli_spec.rb +27 -0
- data/spec/planter/hash_spec.rb +27 -0
- data/spec/planter_spec.rb +15 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/templates/test/%%project:snake%%.rtf +10 -0
- data/spec/templates/test/Rakefile +6 -0
- data/spec/templates/test/_planter.yml +3 -6
- data/spec/templates/test/test.rb +5 -0
- data/src/_README.md +20 -3
- metadata +108 -3
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "monitor"
|
|
4
|
+
require "tty-cursor"
|
|
5
|
+
|
|
6
|
+
require_relative "spinner/version"
|
|
7
|
+
require_relative "spinner/formats"
|
|
8
|
+
|
|
9
|
+
module TTY
|
|
10
|
+
# Used for creating terminal spinner
|
|
11
|
+
#
|
|
12
|
+
# @api public
|
|
13
|
+
class Spinner
|
|
14
|
+
include Formats
|
|
15
|
+
include MonitorMixin
|
|
16
|
+
|
|
17
|
+
# @raised when attempting to join dead thread
|
|
18
|
+
NotSpinningError = Class.new(StandardError)
|
|
19
|
+
|
|
20
|
+
ECMA_CSI = "\x1b["
|
|
21
|
+
|
|
22
|
+
MATCHER = /:spinner/
|
|
23
|
+
TICK = "✔"
|
|
24
|
+
CROSS = "✖"
|
|
25
|
+
|
|
26
|
+
CURSOR_LOCK = Monitor.new
|
|
27
|
+
|
|
28
|
+
# The object that responds to print call defaulting to stderr
|
|
29
|
+
#
|
|
30
|
+
# @api public
|
|
31
|
+
attr_reader :output
|
|
32
|
+
|
|
33
|
+
# The current format type
|
|
34
|
+
#
|
|
35
|
+
# @return [String]
|
|
36
|
+
#
|
|
37
|
+
# @api public
|
|
38
|
+
attr_reader :format
|
|
39
|
+
|
|
40
|
+
# Whether to show or hide cursor
|
|
41
|
+
#
|
|
42
|
+
# @return [Boolean]
|
|
43
|
+
#
|
|
44
|
+
# @api public
|
|
45
|
+
attr_reader :hide_cursor
|
|
46
|
+
|
|
47
|
+
# The message to print before the spinner
|
|
48
|
+
#
|
|
49
|
+
# @return [String]
|
|
50
|
+
# the current message
|
|
51
|
+
#
|
|
52
|
+
# @api public
|
|
53
|
+
attr_reader :message
|
|
54
|
+
|
|
55
|
+
# Tokens for the message
|
|
56
|
+
#
|
|
57
|
+
# @return [Hash[Symbol, Object]]
|
|
58
|
+
# the current tokens
|
|
59
|
+
#
|
|
60
|
+
# @api public
|
|
61
|
+
attr_reader :tokens
|
|
62
|
+
|
|
63
|
+
# The amount of time between frames in auto spinning
|
|
64
|
+
#
|
|
65
|
+
# @api public
|
|
66
|
+
attr_reader :interval
|
|
67
|
+
|
|
68
|
+
# The current row inside the multi spinner
|
|
69
|
+
#
|
|
70
|
+
# @api public
|
|
71
|
+
attr_reader :row
|
|
72
|
+
|
|
73
|
+
# Initialize a spinner
|
|
74
|
+
#
|
|
75
|
+
# @example
|
|
76
|
+
# spinner = TTY::Spinner.new
|
|
77
|
+
#
|
|
78
|
+
# @param [String] message
|
|
79
|
+
# the message to print in front of the spinner
|
|
80
|
+
#
|
|
81
|
+
# @param [Hash] options
|
|
82
|
+
# @option options [String] :format
|
|
83
|
+
# the spinner format type defaulting to :spin_1
|
|
84
|
+
# @option options [Object] :output
|
|
85
|
+
# the object that responds to print call defaulting to stderr
|
|
86
|
+
# @option options [Boolean] :hide_cursor
|
|
87
|
+
# display or hide cursor
|
|
88
|
+
# @option options [Boolean] :clear
|
|
89
|
+
# clear ouptut when finished
|
|
90
|
+
# @option options [Float] :interval
|
|
91
|
+
# the interval for auto spinning
|
|
92
|
+
#
|
|
93
|
+
# @api public
|
|
94
|
+
def initialize(*args)
|
|
95
|
+
super()
|
|
96
|
+
options = args.last.is_a?(::Hash) ? args.pop : {}
|
|
97
|
+
@message = args.empty? ? ":spinner" : args.pop
|
|
98
|
+
@tokens = {}
|
|
99
|
+
|
|
100
|
+
@format = options.fetch(:format) { :classic }
|
|
101
|
+
@output = options.fetch(:output) { $stderr }
|
|
102
|
+
@hide_cursor = options.fetch(:hide_cursor) { false }
|
|
103
|
+
@frames = options.fetch(:frames) do
|
|
104
|
+
fetch_format(@format.to_sym, :frames)
|
|
105
|
+
end
|
|
106
|
+
@clear = options.fetch(:clear) { false }
|
|
107
|
+
@success_mark = options.fetch(:success_mark) { TICK }
|
|
108
|
+
@error_mark = options.fetch(:error_mark) { CROSS }
|
|
109
|
+
@interval = options.fetch(:interval) do
|
|
110
|
+
fetch_format(@format.to_sym, :interval)
|
|
111
|
+
end
|
|
112
|
+
@row = options[:row]
|
|
113
|
+
|
|
114
|
+
@callbacks = Hash.new { |h, k| h[k] = [] }
|
|
115
|
+
@length = @frames.length
|
|
116
|
+
@thread = nil
|
|
117
|
+
@job = nil
|
|
118
|
+
@multispinner = nil
|
|
119
|
+
reset
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Reset the spinner to initial frame
|
|
123
|
+
#
|
|
124
|
+
# @api public
|
|
125
|
+
def reset
|
|
126
|
+
synchronize do
|
|
127
|
+
@current = 0
|
|
128
|
+
@done = false
|
|
129
|
+
@state = :stopped
|
|
130
|
+
@succeeded = false
|
|
131
|
+
@first_run = true
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Notifies the TTY::Spinner that it is running under a multispinner
|
|
136
|
+
#
|
|
137
|
+
# @param [TTY::Spinner::Multi] the multispinner that it is running under
|
|
138
|
+
#
|
|
139
|
+
# @api private
|
|
140
|
+
def attach_to(multispinner)
|
|
141
|
+
@multispinner = multispinner
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Whether the spinner has completed spinning
|
|
145
|
+
#
|
|
146
|
+
# @return [Boolean] whether or not the spinner has finished
|
|
147
|
+
#
|
|
148
|
+
# @api public
|
|
149
|
+
def done?
|
|
150
|
+
@done
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Whether the spinner is spinning
|
|
154
|
+
#
|
|
155
|
+
# @return [Boolean] whether or not the spinner is spinning
|
|
156
|
+
#
|
|
157
|
+
# @api public
|
|
158
|
+
def spinning?
|
|
159
|
+
@state == :spinning
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Whether the spinner is in the success state.
|
|
163
|
+
# When true the spinner is marked with a success mark.
|
|
164
|
+
#
|
|
165
|
+
# @return [Boolean] whether or not the spinner succeeded
|
|
166
|
+
#
|
|
167
|
+
# @api public
|
|
168
|
+
def success?
|
|
169
|
+
@succeeded == :success
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Whether the spinner is in the error state. This is only true
|
|
173
|
+
# temporarily while it is being marked with a failure mark.
|
|
174
|
+
#
|
|
175
|
+
# @return [Boolean] whether or not the spinner is erroring
|
|
176
|
+
#
|
|
177
|
+
# @api public
|
|
178
|
+
def error?
|
|
179
|
+
@succeeded == :error
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Register callback
|
|
183
|
+
#
|
|
184
|
+
# @param [Symbol] name
|
|
185
|
+
# the name for the event to listen for, e.i. :complete
|
|
186
|
+
#
|
|
187
|
+
# @return [self]
|
|
188
|
+
#
|
|
189
|
+
# @api public
|
|
190
|
+
def on(name, &block)
|
|
191
|
+
synchronize do
|
|
192
|
+
@callbacks[name] << block
|
|
193
|
+
end
|
|
194
|
+
self
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Start timer and unlock spinner
|
|
198
|
+
#
|
|
199
|
+
# @api public
|
|
200
|
+
def start
|
|
201
|
+
@started_at = Time.now
|
|
202
|
+
@done = false
|
|
203
|
+
reset
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Add job to this spinner
|
|
207
|
+
#
|
|
208
|
+
# @api public
|
|
209
|
+
def job(&work)
|
|
210
|
+
synchronize do
|
|
211
|
+
if block_given?
|
|
212
|
+
@job = work
|
|
213
|
+
else
|
|
214
|
+
@job
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Execute this spinner job
|
|
220
|
+
#
|
|
221
|
+
# @yield [TTY::Spinner]
|
|
222
|
+
#
|
|
223
|
+
# @api public
|
|
224
|
+
def execute_job
|
|
225
|
+
job.call(self) if job?
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Check if this spinner has a scheduled job
|
|
229
|
+
#
|
|
230
|
+
# @return [Boolean]
|
|
231
|
+
#
|
|
232
|
+
# @api public
|
|
233
|
+
def job?
|
|
234
|
+
!@job.nil?
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Start automatic spinning animation
|
|
238
|
+
#
|
|
239
|
+
# @api public
|
|
240
|
+
def auto_spin
|
|
241
|
+
CURSOR_LOCK.synchronize do
|
|
242
|
+
start
|
|
243
|
+
sleep_time = 1.0 / @interval
|
|
244
|
+
|
|
245
|
+
spin
|
|
246
|
+
@thread = Thread.new do
|
|
247
|
+
sleep(sleep_time)
|
|
248
|
+
while @started_at
|
|
249
|
+
if Thread.current["pause"]
|
|
250
|
+
Thread.stop
|
|
251
|
+
Thread.current["pause"] = false
|
|
252
|
+
end
|
|
253
|
+
spin
|
|
254
|
+
sleep(sleep_time)
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
ensure
|
|
259
|
+
write(TTY::Cursor.show, false) if @hide_cursor
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Checked if current spinner is paused
|
|
263
|
+
#
|
|
264
|
+
# @return [Boolean]
|
|
265
|
+
#
|
|
266
|
+
# @api public
|
|
267
|
+
def paused?
|
|
268
|
+
!!(@thread && @thread["pause"])
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Pause spinner automatic animation
|
|
272
|
+
#
|
|
273
|
+
# @param [String] mark
|
|
274
|
+
# the custom mark to replace the spinner
|
|
275
|
+
#
|
|
276
|
+
# @api public
|
|
277
|
+
def pause(mark: nil)
|
|
278
|
+
return if paused? || done?
|
|
279
|
+
|
|
280
|
+
synchronize do
|
|
281
|
+
data = message.gsub(MATCHER, mark || @frames[@current])
|
|
282
|
+
data = replace_tokens(data)
|
|
283
|
+
write(data, true)
|
|
284
|
+
@thread["pause"] = true if @thread
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# Resume spinner automatic animation
|
|
289
|
+
#
|
|
290
|
+
# @api public
|
|
291
|
+
def resume
|
|
292
|
+
return unless paused?
|
|
293
|
+
|
|
294
|
+
@thread.wakeup if @thread
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Run spinner while executing job
|
|
298
|
+
#
|
|
299
|
+
# @param [String] stop_message
|
|
300
|
+
# the message displayed when block is finished
|
|
301
|
+
#
|
|
302
|
+
# @yield automatically animate and finish spinner
|
|
303
|
+
#
|
|
304
|
+
# @example
|
|
305
|
+
# spinner.run("Migrated DB") { ... }
|
|
306
|
+
#
|
|
307
|
+
# @api public
|
|
308
|
+
def run(stop_message = "", &block)
|
|
309
|
+
job(&block)
|
|
310
|
+
auto_spin
|
|
311
|
+
|
|
312
|
+
@work = Thread.new { execute_job }
|
|
313
|
+
@work.join
|
|
314
|
+
ensure
|
|
315
|
+
stop(stop_message)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Duration of the spinning animation
|
|
319
|
+
#
|
|
320
|
+
# @return [Numeric]
|
|
321
|
+
#
|
|
322
|
+
# @api public
|
|
323
|
+
def duration
|
|
324
|
+
@started_at ? Time.now - @started_at : nil
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# Join running spinner
|
|
328
|
+
#
|
|
329
|
+
# @param [Float] timeout
|
|
330
|
+
# the timeout for join
|
|
331
|
+
#
|
|
332
|
+
# @api public
|
|
333
|
+
def join(timeout = nil)
|
|
334
|
+
unless @thread
|
|
335
|
+
raise(NotSpinningError, "Cannot join spinner that is not running")
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
timeout ? @thread.join(timeout) : @thread.join
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# Kill running spinner
|
|
342
|
+
#
|
|
343
|
+
# @api public
|
|
344
|
+
def kill
|
|
345
|
+
synchronize do
|
|
346
|
+
@thread.kill if @thread
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# Perform a spin
|
|
351
|
+
#
|
|
352
|
+
# @return [String]
|
|
353
|
+
# the printed data
|
|
354
|
+
#
|
|
355
|
+
# @api public
|
|
356
|
+
def spin
|
|
357
|
+
return if done?
|
|
358
|
+
|
|
359
|
+
synchronize do
|
|
360
|
+
emit(:spin)
|
|
361
|
+
render
|
|
362
|
+
@current = (@current + 1) % @length
|
|
363
|
+
@state = :spinning
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# Render spinner to the output
|
|
368
|
+
#
|
|
369
|
+
# @api private
|
|
370
|
+
def render
|
|
371
|
+
return if done?
|
|
372
|
+
|
|
373
|
+
write(TTY::Cursor.hide) if @hide_cursor && !spinning?
|
|
374
|
+
|
|
375
|
+
data = message.gsub(MATCHER, @frames[@current])
|
|
376
|
+
data = replace_tokens(data)
|
|
377
|
+
write(data, true)
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# Redraw the indent for this spinner, if it exists
|
|
381
|
+
#
|
|
382
|
+
# @api private
|
|
383
|
+
def redraw_indent
|
|
384
|
+
write(TTY::Cursor.hide) if @hide_cursor && !spinning?
|
|
385
|
+
|
|
386
|
+
write("", false)
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# Finish spining
|
|
390
|
+
#
|
|
391
|
+
# @param [String] stop_message
|
|
392
|
+
# the stop message to print
|
|
393
|
+
# @param [String] mark
|
|
394
|
+
# the custom mark to replace the spinner
|
|
395
|
+
#
|
|
396
|
+
# @api public
|
|
397
|
+
def stop(stop_message = "", mark: nil)
|
|
398
|
+
mon_enter
|
|
399
|
+
return if done?
|
|
400
|
+
|
|
401
|
+
clear_line
|
|
402
|
+
return if @clear
|
|
403
|
+
|
|
404
|
+
data = message.gsub(MATCHER, mark || next_char)
|
|
405
|
+
data = replace_tokens(data)
|
|
406
|
+
data << (" " + stop_message) unless stop_message.empty?
|
|
407
|
+
|
|
408
|
+
write(data, false)
|
|
409
|
+
write("\n", false) unless @clear || @multispinner
|
|
410
|
+
ensure
|
|
411
|
+
@state = :stopped
|
|
412
|
+
@done = true
|
|
413
|
+
@started_at = nil
|
|
414
|
+
|
|
415
|
+
write(TTY::Cursor.show, false) if @hide_cursor
|
|
416
|
+
|
|
417
|
+
emit(:done)
|
|
418
|
+
kill
|
|
419
|
+
mon_exit
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
# Retrieve next character
|
|
423
|
+
#
|
|
424
|
+
# @return [String]
|
|
425
|
+
#
|
|
426
|
+
# @api private
|
|
427
|
+
def next_char
|
|
428
|
+
if success?
|
|
429
|
+
@success_mark
|
|
430
|
+
elsif error?
|
|
431
|
+
@error_mark
|
|
432
|
+
else
|
|
433
|
+
@frames[@current - 1]
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
# Finish spinning and set state to :success
|
|
438
|
+
#
|
|
439
|
+
# @param [String] stop_message
|
|
440
|
+
# the message to display on success
|
|
441
|
+
# @param [String] mark
|
|
442
|
+
# the custom mark to replace the spinner
|
|
443
|
+
#
|
|
444
|
+
# @api public
|
|
445
|
+
def success(stop_message = "", mark: nil)
|
|
446
|
+
return if done?
|
|
447
|
+
|
|
448
|
+
synchronize do
|
|
449
|
+
@succeeded = :success
|
|
450
|
+
stop(stop_message, mark: mark)
|
|
451
|
+
emit(:success)
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
# Finish spinning and set state to :error
|
|
456
|
+
#
|
|
457
|
+
# @param [String] stop_message
|
|
458
|
+
# the message to display on error
|
|
459
|
+
# @param [String] mark
|
|
460
|
+
# the custom mark to replace the spinner
|
|
461
|
+
#
|
|
462
|
+
# @api public
|
|
463
|
+
def error(stop_message = "", mark: nil)
|
|
464
|
+
return if done?
|
|
465
|
+
|
|
466
|
+
synchronize do
|
|
467
|
+
@succeeded = :error
|
|
468
|
+
stop(stop_message, mark: mark)
|
|
469
|
+
emit(:error)
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
# Clear current line
|
|
474
|
+
#
|
|
475
|
+
# @api public
|
|
476
|
+
def clear_line
|
|
477
|
+
write(ECMA_CSI + "0m" + TTY::Cursor.clear_line)
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
# Update string formatting tokens
|
|
481
|
+
#
|
|
482
|
+
# @param [Hash[Symbol]] tokens
|
|
483
|
+
# the tokens used in formatting string
|
|
484
|
+
#
|
|
485
|
+
# @api public
|
|
486
|
+
def update(tokens)
|
|
487
|
+
synchronize do
|
|
488
|
+
clear_line if spinning?
|
|
489
|
+
@tokens.merge!(tokens)
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
# Log text above the current spinner
|
|
494
|
+
#
|
|
495
|
+
# @param [String] text
|
|
496
|
+
# the message to log out
|
|
497
|
+
#
|
|
498
|
+
# @api public
|
|
499
|
+
def log(text)
|
|
500
|
+
synchronize do
|
|
501
|
+
cleared_text = text.to_s.lines.map do |line|
|
|
502
|
+
TTY::Cursor.clear_line + line
|
|
503
|
+
end.join
|
|
504
|
+
|
|
505
|
+
write("#{cleared_text}#{"\n" unless cleared_text.end_with?("\n")}", false)
|
|
506
|
+
render
|
|
507
|
+
end
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
# Check if IO is attached to a terminal
|
|
511
|
+
#
|
|
512
|
+
# return [Boolean]
|
|
513
|
+
#
|
|
514
|
+
# @api public
|
|
515
|
+
def tty?
|
|
516
|
+
output.respond_to?(:tty?) && output.tty?
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
private
|
|
520
|
+
|
|
521
|
+
# Execute a block on the proper terminal line if the spinner is running
|
|
522
|
+
# under a multispinner. Otherwise, execute the block on the current line.
|
|
523
|
+
#
|
|
524
|
+
# @api private
|
|
525
|
+
def execute_on_line
|
|
526
|
+
if @multispinner
|
|
527
|
+
@multispinner.synchronize do
|
|
528
|
+
if @first_run
|
|
529
|
+
@row ||= @multispinner.next_row
|
|
530
|
+
yield if block_given?
|
|
531
|
+
output.print "\n"
|
|
532
|
+
@first_run = false
|
|
533
|
+
else
|
|
534
|
+
lines_up = (@multispinner.rows + 1) - @row
|
|
535
|
+
output.print TTY::Cursor.save
|
|
536
|
+
output.print TTY::Cursor.up(lines_up)
|
|
537
|
+
yield if block_given?
|
|
538
|
+
output.print TTY::Cursor.restore
|
|
539
|
+
end
|
|
540
|
+
end
|
|
541
|
+
elsif block_given?
|
|
542
|
+
yield
|
|
543
|
+
end
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
# Write data out to output
|
|
547
|
+
#
|
|
548
|
+
# @return [nil]
|
|
549
|
+
#
|
|
550
|
+
# @api private
|
|
551
|
+
def write(data, clear_first = false)
|
|
552
|
+
return unless tty? # write only to terminal
|
|
553
|
+
|
|
554
|
+
execute_on_line do
|
|
555
|
+
output.print(TTY::Cursor.column(1)) if clear_first
|
|
556
|
+
# If there's a top level spinner, print with inset
|
|
557
|
+
characters_in = @multispinner.line_inset(@row) if @multispinner
|
|
558
|
+
output.print("#{characters_in}#{data}")
|
|
559
|
+
output.flush
|
|
560
|
+
end
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
# Emit callback
|
|
564
|
+
#
|
|
565
|
+
# @api private
|
|
566
|
+
def emit(name, *args)
|
|
567
|
+
@callbacks[name].each do |callback|
|
|
568
|
+
callback.call(*args)
|
|
569
|
+
end
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
# Find frames by token name
|
|
573
|
+
#
|
|
574
|
+
# @param [Symbol] token
|
|
575
|
+
# the name for the frames
|
|
576
|
+
#
|
|
577
|
+
# @return [Array, String]
|
|
578
|
+
#
|
|
579
|
+
# @api private
|
|
580
|
+
def fetch_format(token, property)
|
|
581
|
+
unless FORMATS.key?(token)
|
|
582
|
+
raise ArgumentError, "Unknown format token `:#{token}`"
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
FORMATS[token][property]
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
# Replace any token inside string
|
|
589
|
+
#
|
|
590
|
+
# @param [String] string
|
|
591
|
+
# the string containing tokens
|
|
592
|
+
#
|
|
593
|
+
# @return [String]
|
|
594
|
+
#
|
|
595
|
+
# @api private
|
|
596
|
+
def replace_tokens(string)
|
|
597
|
+
data = string.dup
|
|
598
|
+
@tokens.each do |name, val|
|
|
599
|
+
data.gsub!(/:#{name}/, val.to_s)
|
|
600
|
+
end
|
|
601
|
+
data
|
|
602
|
+
end
|
|
603
|
+
end # Spinner
|
|
604
|
+
end # TTY
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
if ENV["COVERAGE"] == "true"
|
|
4
|
+
require "simplecov"
|
|
5
|
+
require "coveralls"
|
|
6
|
+
|
|
7
|
+
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([
|
|
8
|
+
SimpleCov::Formatter::HTMLFormatter,
|
|
9
|
+
Coveralls::SimpleCov::Formatter
|
|
10
|
+
])
|
|
11
|
+
|
|
12
|
+
SimpleCov.start do
|
|
13
|
+
command_name "spec"
|
|
14
|
+
add_filter "spec"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
require "tty-spinner"
|
|
19
|
+
require "stringio"
|
|
20
|
+
|
|
21
|
+
class StringIO
|
|
22
|
+
def tty?
|
|
23
|
+
true
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
RSpec.configure do |config|
|
|
28
|
+
config.expect_with :rspec do |expectations|
|
|
29
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
config.mock_with :rspec do |mocks|
|
|
33
|
+
mocks.verify_partial_doubles = true
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Limits the available syntax to the non-monkey patched syntax that is recommended.
|
|
37
|
+
config.disable_monkey_patching!
|
|
38
|
+
|
|
39
|
+
# This setting enables warnings. It's recommended, but in some cases may
|
|
40
|
+
# be too noisy due to issues in dependencies.
|
|
41
|
+
config.warnings = true
|
|
42
|
+
|
|
43
|
+
if config.files_to_run.one?
|
|
44
|
+
config.default_formatter = "doc"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
config.profile_examples = 2
|
|
48
|
+
|
|
49
|
+
config.order = :random
|
|
50
|
+
|
|
51
|
+
Kernel.srand config.seed
|
|
52
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
RSpec.describe TTY::Spinner, "#auto_spin" do
|
|
2
|
+
let(:output) { StringIO.new("", "w+") }
|
|
3
|
+
|
|
4
|
+
it "starts and auto spins" do
|
|
5
|
+
spinner = TTY::Spinner.new(output: output, interval: 100)
|
|
6
|
+
allow(spinner).to receive(:spin)
|
|
7
|
+
|
|
8
|
+
spinner.auto_spin
|
|
9
|
+
sleep 0.1
|
|
10
|
+
spinner.stop
|
|
11
|
+
|
|
12
|
+
expect(spinner).to have_received(:spin).at_least(5).times
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "restores cursor when erorr is raised" do
|
|
16
|
+
spinner = TTY::Spinner.new(output: output, hide_cursor: true)
|
|
17
|
+
|
|
18
|
+
spinner.auto_spin {
|
|
19
|
+
raise "boom"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
output.rewind
|
|
23
|
+
expect(output.read).to start_with("\e[?25l").and end_with("\e[?25h")
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
RSpec.describe TTY::Spinner, ":clear" do
|
|
2
|
+
let(:output) { StringIO.new("", "w+") }
|
|
3
|
+
|
|
4
|
+
it "clears output when done" do
|
|
5
|
+
spinner = TTY::Spinner.new(clear: true, output: output)
|
|
6
|
+
3.times { spinner.spin }
|
|
7
|
+
spinner.stop("Done!")
|
|
8
|
+
output.rewind
|
|
9
|
+
expect(output.read).to eq([
|
|
10
|
+
"\e[1G|",
|
|
11
|
+
"\e[1G/",
|
|
12
|
+
"\e[1G-",
|
|
13
|
+
"\e[0m\e[2K\e[1G"
|
|
14
|
+
].join)
|
|
15
|
+
end
|
|
16
|
+
end
|