ruby-progress 1.1.4 → 1.1.8
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/.rubocop.yml +5 -0
- data/.rubocop_todo.yml +201 -0
- data/CHANGELOG.md +74 -0
- data/Gemfile.lock +1 -1
- data/Rakefile +3 -8
- data/bin/prg +361 -101
- data/blog-post.md +174 -0
- data/examples/daemon_demo.rb +1 -1
- data/experimental_terminal.rb +55 -0
- data/lib/ruby-progress/daemon.rb +2 -2
- data/lib/ruby-progress/ripple.rb +11 -40
- data/lib/ruby-progress/utils.rb +15 -3
- data/lib/ruby-progress/version.rb +4 -1
- data/lib/ruby-progress/worm.rb +116 -23
- data/test_daemon_interruption.rb +26 -0
- data/test_daemon_orphan.rb +13 -0
- metadata +8 -1
data/bin/prg
CHANGED
|
@@ -25,13 +25,19 @@ module PrgCLI
|
|
|
25
25
|
when 'twirl'
|
|
26
26
|
TwirlCLI.run
|
|
27
27
|
when '--list-styles'
|
|
28
|
+
list_styles
|
|
29
|
+
exit
|
|
30
|
+
when '--show-styles'
|
|
28
31
|
show_styles
|
|
29
32
|
exit
|
|
33
|
+
when '--stop-all'
|
|
34
|
+
success = stop_all_processes
|
|
35
|
+
exit(success ? 0 : 1)
|
|
30
36
|
when '-v', '--version'
|
|
31
37
|
puts "prg version #{RubyProgress::VERSION}"
|
|
32
|
-
puts
|
|
33
|
-
puts
|
|
34
|
-
puts
|
|
38
|
+
puts " ripple - Text ripple animation with color effects (v#{RubyProgress::RIPPLE_VERSION})"
|
|
39
|
+
puts " worm - Unicode wave animation with customizable styles (v#{RubyProgress::WORM_VERSION})"
|
|
40
|
+
puts " twirl - Spinner animation with various indicator styles (v#{RubyProgress::TWIRL_VERSION})"
|
|
35
41
|
exit
|
|
36
42
|
when '-h', '--help', 'help'
|
|
37
43
|
show_help
|
|
@@ -59,6 +65,8 @@ module PrgCLI
|
|
|
59
65
|
-v, --version Show version information
|
|
60
66
|
-h, --help Show this help message
|
|
61
67
|
--list-styles List all available styles for all subcommands
|
|
68
|
+
--show-styles Show visual previews of all styles for all subcommands
|
|
69
|
+
--stop-all Stop all prg processes
|
|
62
70
|
|
|
63
71
|
EXAMPLES:
|
|
64
72
|
prg ripple "Loading..." --style rainbow --speed fast
|
|
@@ -92,21 +100,159 @@ module PrgCLI
|
|
|
92
100
|
# Do not chdir or close stdio: we want to be able to emit completion output
|
|
93
101
|
end
|
|
94
102
|
|
|
103
|
+
def self.list_styles
|
|
104
|
+
puts '== ripple styles'
|
|
105
|
+
puts 'rainbow, inverse, caps, normal'
|
|
106
|
+
puts ''
|
|
107
|
+
|
|
108
|
+
puts '== worm styles'
|
|
109
|
+
worm_styles = RubyProgress::Worm::RIPPLE_STYLES
|
|
110
|
+
style_names = worm_styles.keys.reject { |name| name == 'cirlces_small' }
|
|
111
|
+
puts style_names.join(', ')
|
|
112
|
+
puts ''
|
|
113
|
+
|
|
114
|
+
puts '== twirl styles'
|
|
115
|
+
twirl_names = RubyProgress::INDICATORS.keys.map(&:to_s)
|
|
116
|
+
puts twirl_names.join(', ')
|
|
117
|
+
end
|
|
118
|
+
|
|
95
119
|
def self.show_styles
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
120
|
+
show_ripple_styles
|
|
121
|
+
show_worm_styles
|
|
122
|
+
show_twirl_styles
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def self.show_ripple_styles
|
|
126
|
+
puts '== ripple styles (visual effects for text)'
|
|
127
|
+
|
|
128
|
+
# Create sample text for ripple previews
|
|
129
|
+
sample_text = 'Progress'
|
|
130
|
+
|
|
131
|
+
# Rainbow preview - show with actual rainbow colors
|
|
132
|
+
rainbow_preview = sample_text.chars.map.with_index do |char, i|
|
|
133
|
+
colors = [31, 32, 33, 34, 35, 36] # red, green, yellow, blue, magenta, cyan
|
|
134
|
+
"\e[#{colors[i % colors.length]}m#{char}\e[0m"
|
|
135
|
+
end.join
|
|
136
|
+
puts "rainbow - #{rainbow_preview}"
|
|
137
|
+
|
|
138
|
+
# Inverse preview - show with actual inverse background
|
|
139
|
+
inverse_preview = "\e[7m#{sample_text}\e[0m"
|
|
140
|
+
puts "inverse - #{inverse_preview}"
|
|
141
|
+
|
|
142
|
+
# Caps preview
|
|
143
|
+
caps_preview = sample_text.upcase
|
|
144
|
+
puts "caps - #{caps_preview}"
|
|
145
|
+
|
|
146
|
+
# Normal preview
|
|
147
|
+
puts "normal - #{sample_text}"
|
|
148
|
+
|
|
149
|
+
# Show ripple effect preview with highlighted character in middle
|
|
100
150
|
puts ''
|
|
101
|
-
puts '
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
151
|
+
puts 'Ripple effect preview (character 4 highlighted):'
|
|
152
|
+
ripple_demo = sample_text.chars.map.with_index do |char, i|
|
|
153
|
+
if i == 3 # highlight the 'g' in Progress
|
|
154
|
+
"\e[7m#{char}\e[0m" # inverse highlight
|
|
155
|
+
else
|
|
156
|
+
char
|
|
157
|
+
end
|
|
158
|
+
end.join
|
|
159
|
+
puts "normal - #{ripple_demo}"
|
|
160
|
+
|
|
161
|
+
inverse_ripple_demo = sample_text.chars.map.with_index do |char, i|
|
|
162
|
+
if i == 3 # highlight the 'g' in Progress
|
|
163
|
+
"\e[0m#{char}\e[7m" # normal char in inverse text
|
|
164
|
+
else
|
|
165
|
+
"\e[7m#{char}\e[0m" # inverse char
|
|
166
|
+
end
|
|
167
|
+
end.join
|
|
168
|
+
puts "inverse - \e[7m#{inverse_ripple_demo}\e[0m"
|
|
105
169
|
puts ''
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def self.show_worm_styles
|
|
173
|
+
puts '== worm styles (wave animation patterns)'
|
|
174
|
+
# Load worm styles for preview
|
|
175
|
+
worm_styles = RubyProgress::Worm::RIPPLE_STYLES
|
|
176
|
+
worm_styles.each do |name, chars|
|
|
177
|
+
next if name == 'cirlces_small' # skip typo version
|
|
178
|
+
|
|
179
|
+
baseline = chars[:baseline]
|
|
180
|
+
midline = chars[:midline]
|
|
181
|
+
peak = chars[:peak]
|
|
182
|
+
preview = "#{baseline}#{baseline}#{midline}#{peak}#{midline}#{baseline}#{baseline}"
|
|
183
|
+
puts "#{name.ljust(10)} - #{preview}"
|
|
184
|
+
end
|
|
185
|
+
puts ''
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def self.show_twirl_styles
|
|
189
|
+
puts '== twirl styles (spinner characters)'
|
|
190
|
+
RubyProgress::INDICATORS.each do |name, frames|
|
|
191
|
+
preview = frames.join(' ')
|
|
192
|
+
puts "#{name.to_s.ljust(12)} - #{preview}"
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def self.stop_all_processes
|
|
197
|
+
current_pid = Process.pid
|
|
198
|
+
|
|
199
|
+
# Get list of prg processes excluding current process
|
|
200
|
+
prg_pids = `pgrep -f "ruby.*bin/prg"`.split.map(&:to_i).reject { |pid| pid == current_pid }
|
|
201
|
+
|
|
202
|
+
return false if prg_pids.empty?
|
|
203
|
+
|
|
204
|
+
# First try TERM signal
|
|
205
|
+
prg_pids.each do |pid|
|
|
206
|
+
Process.kill('TERM', pid)
|
|
207
|
+
rescue StandardError
|
|
208
|
+
# Ignore errors if process already terminated
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Give processes a moment to terminate gracefully
|
|
212
|
+
sleep 0.5
|
|
213
|
+
|
|
214
|
+
# Check which processes are still running and force kill them
|
|
215
|
+
still_running = `pgrep -f "ruby.*bin/prg"`.split.map(&:to_i).reject { |pid| pid == current_pid }
|
|
216
|
+
unless still_running.empty?
|
|
217
|
+
still_running.each do |pid|
|
|
218
|
+
Process.kill('KILL', pid)
|
|
219
|
+
rescue StandardError
|
|
220
|
+
# Ignore errors if process already terminated
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
true
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def self.stop_subcommand_processes(subcommand)
|
|
228
|
+
current_pid = Process.pid
|
|
229
|
+
|
|
230
|
+
# Get list of subcommand processes excluding current process
|
|
231
|
+
subcommand_pids = `pgrep -f "ruby.*bin/prg #{subcommand}"`.split.map(&:to_i).reject { |pid| pid == current_pid }
|
|
232
|
+
|
|
233
|
+
return false if subcommand_pids.empty?
|
|
234
|
+
|
|
235
|
+
# First try TERM signal
|
|
236
|
+
subcommand_pids.each do |pid|
|
|
237
|
+
Process.kill('TERM', pid)
|
|
238
|
+
rescue StandardError
|
|
239
|
+
# Ignore errors if process already terminated
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Give processes a moment to terminate gracefully
|
|
243
|
+
sleep 0.5
|
|
244
|
+
|
|
245
|
+
# Check which processes are still running and force kill them
|
|
246
|
+
still_running = `pgrep -f "ruby.*bin/prg #{subcommand}"`.split.map(&:to_i).reject { |pid| pid == current_pid }
|
|
247
|
+
unless still_running.empty?
|
|
248
|
+
still_running.each do |pid|
|
|
249
|
+
Process.kill('KILL', pid)
|
|
250
|
+
rescue StandardError
|
|
251
|
+
# Ignore errors if process already terminated
|
|
252
|
+
end
|
|
109
253
|
end
|
|
254
|
+
|
|
255
|
+
true
|
|
110
256
|
end
|
|
111
257
|
end
|
|
112
258
|
|
|
@@ -131,106 +277,124 @@ module RippleCLI
|
|
|
131
277
|
message: nil # For unified interface
|
|
132
278
|
}
|
|
133
279
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
280
|
+
begin
|
|
281
|
+
OptionParser.new do |opts|
|
|
282
|
+
opts.banner = 'Usage: prg ripple [options] [STRING]'
|
|
283
|
+
opts.separator ''
|
|
284
|
+
opts.separator 'Animation Options:'
|
|
285
|
+
|
|
286
|
+
opts.on('-s', '--speed SPEED', 'Animation speed (fast/medium/slow or f/m/s)') do |s|
|
|
287
|
+
options[:speed] = case s.downcase
|
|
288
|
+
when /^f/ then :fast
|
|
289
|
+
when /^m/ then :medium
|
|
290
|
+
when /^s/ then :slow
|
|
291
|
+
else :medium
|
|
292
|
+
end
|
|
293
|
+
end
|
|
138
294
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
when /^m/ then :medium
|
|
143
|
-
when /^s/ then :slow
|
|
144
|
-
else :medium
|
|
145
|
-
end
|
|
146
|
-
end
|
|
295
|
+
opts.on('-m', '--message MESSAGE', 'Message to display (alternative to positional argument)') do |msg|
|
|
296
|
+
options[:message] = msg
|
|
297
|
+
end
|
|
147
298
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
299
|
+
opts.on('--style STYLES', 'Animation styles (rainbow, inverse, caps - can be comma-separated)') do |styles|
|
|
300
|
+
options[:styles] = styles.split(',').map(&:strip).map(&:to_sym)
|
|
301
|
+
end
|
|
151
302
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
303
|
+
opts.on('-d', '--direction DIRECTION', 'Animation direction (forward/bidirectional or f/b)') do |f|
|
|
304
|
+
options[:format] = f =~ /^f/i ? :forward_only : :bidirectional
|
|
305
|
+
end
|
|
155
306
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
end
|
|
307
|
+
opts.separator ''
|
|
308
|
+
opts.separator 'Command Execution:'
|
|
159
309
|
|
|
160
|
-
|
|
161
|
-
|
|
310
|
+
opts.on('-c', '--command COMMAND', 'Run command during animation (optional)') do |command|
|
|
311
|
+
options[:command] = command
|
|
312
|
+
end
|
|
162
313
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
314
|
+
opts.on('--success MESSAGE', 'Success message to display') do |msg|
|
|
315
|
+
options[:success_message] = msg
|
|
316
|
+
end
|
|
166
317
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
318
|
+
opts.on('--error MESSAGE', 'Error message to display') do |msg|
|
|
319
|
+
options[:fail_message] = msg
|
|
320
|
+
end
|
|
170
321
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
322
|
+
opts.on('--checkmark', 'Show checkmarks (✅ success, 🛑 failure)') do
|
|
323
|
+
options[:complete_checkmark] = true
|
|
324
|
+
end
|
|
174
325
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
326
|
+
opts.on('--stdout', 'Output captured command result to STDOUT') do
|
|
327
|
+
options[:output] = :stdout
|
|
328
|
+
end
|
|
178
329
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
330
|
+
opts.on('--quiet', 'Suppress all output') do
|
|
331
|
+
options[:output] = :quiet
|
|
332
|
+
end
|
|
182
333
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
end
|
|
334
|
+
opts.separator ''
|
|
335
|
+
opts.separator 'Daemon Mode:'
|
|
186
336
|
|
|
187
|
-
|
|
188
|
-
|
|
337
|
+
opts.on('--daemon', 'Run in background daemon mode') do
|
|
338
|
+
options[:daemon] = true
|
|
339
|
+
end
|
|
189
340
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
341
|
+
opts.on('--pid-file FILE', 'Write process ID to file (default: /tmp/ruby-progress/progress.pid)') do |file|
|
|
342
|
+
options[:pid_file] = file
|
|
343
|
+
end
|
|
193
344
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
345
|
+
opts.on('--stop', 'Stop daemon (uses default PID file unless --pid-file specified)') do
|
|
346
|
+
options[:stop] = true
|
|
347
|
+
end
|
|
197
348
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
349
|
+
opts.on('--status', 'Show daemon status (running/not running)') do
|
|
350
|
+
options[:status] = true
|
|
351
|
+
end
|
|
201
352
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
353
|
+
opts.on('--stop-success MESSAGE', 'When stopping, show this success message') do |msg|
|
|
354
|
+
options[:stop_success] = msg
|
|
355
|
+
end
|
|
356
|
+
opts.on('--stop-error MESSAGE', 'When stopping, show this error message') do |msg|
|
|
357
|
+
options[:stop_error] = msg
|
|
358
|
+
end
|
|
359
|
+
opts.on('--stop-checkmark', 'When stopping, include a success/error checkmark') do
|
|
360
|
+
options[:stop_checkmark] = true
|
|
361
|
+
end
|
|
205
362
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
options[:stop_error] = msg
|
|
211
|
-
end
|
|
212
|
-
opts.on('--stop-checkmark', 'When stopping, include a success/error checkmark') do
|
|
213
|
-
options[:stop_checkmark] = true
|
|
214
|
-
end
|
|
363
|
+
opts.separator ''
|
|
364
|
+
opts.separator 'Daemon notes:'
|
|
365
|
+
opts.separator ' - Do not append &; prg detaches itself and returns immediately.'
|
|
366
|
+
opts.separator ' - Use --status/--stop with optional --pid-file to control it.'
|
|
215
367
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
opts.separator ' - Do not append &; prg detaches itself and returns immediately.'
|
|
219
|
-
opts.separator ' - Use --status/--stop with optional --pid-file to control it.'
|
|
368
|
+
opts.separator ''
|
|
369
|
+
opts.separator 'General:'
|
|
220
370
|
|
|
221
|
-
|
|
222
|
-
|
|
371
|
+
opts.on('--show-styles', 'Show available ripple styles with visual previews') do
|
|
372
|
+
PrgCLI.show_ripple_styles
|
|
373
|
+
exit
|
|
374
|
+
end
|
|
223
375
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
376
|
+
opts.on('--stop-all', 'Stop all prg ripple processes') do
|
|
377
|
+
success = PrgCLI.stop_subcommand_processes('ripple')
|
|
378
|
+
exit(success ? 0 : 1)
|
|
379
|
+
end
|
|
228
380
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
381
|
+
opts.on('-v', '--version', 'Show version') do
|
|
382
|
+
puts "Ripple version #{RubyProgress::VERSION}"
|
|
383
|
+
exit
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
opts.on('-h', '--help', 'Show this help') do
|
|
387
|
+
puts opts
|
|
388
|
+
exit
|
|
389
|
+
end
|
|
390
|
+
end.parse!
|
|
391
|
+
rescue OptionParser::InvalidOption => e
|
|
392
|
+
puts "Invalid option: #{e.args.first}"
|
|
393
|
+
puts ''
|
|
394
|
+
puts 'Usage: prg ripple [options] [STRING]'
|
|
395
|
+
puts "Run 'prg ripple --help' for more information."
|
|
396
|
+
exit 1
|
|
397
|
+
end
|
|
234
398
|
|
|
235
399
|
# Daemon/status/stop handling (process these without requiring text)
|
|
236
400
|
if options[:status]
|
|
@@ -353,7 +517,7 @@ module RippleCLI
|
|
|
353
517
|
end
|
|
354
518
|
end
|
|
355
519
|
|
|
356
|
-
|
|
520
|
+
FileUtils.rm_f(pid_file)
|
|
357
521
|
end
|
|
358
522
|
end
|
|
359
523
|
end
|
|
@@ -413,7 +577,7 @@ module WormCLI
|
|
|
413
577
|
)
|
|
414
578
|
ensure
|
|
415
579
|
# Clean up PID file
|
|
416
|
-
|
|
580
|
+
FileUtils.rm_f(pid_file)
|
|
417
581
|
end
|
|
418
582
|
end
|
|
419
583
|
|
|
@@ -514,6 +678,11 @@ module WormCLI
|
|
|
514
678
|
end
|
|
515
679
|
opts.on('--stop-checkmark', 'When stopping, include a success checkmark') { options[:stop_checkmark] = true }
|
|
516
680
|
|
|
681
|
+
opts.on('--stop-all', 'Stop all prg worm processes') do
|
|
682
|
+
success = PrgCLI.stop_subcommand_processes('worm')
|
|
683
|
+
exit(success ? 0 : 1)
|
|
684
|
+
end
|
|
685
|
+
|
|
517
686
|
opts.on('--stop-pid FILE', 'Stop daemon by reading PID from file (deprecated: use --stop [--pid-file])') do |file|
|
|
518
687
|
RubyProgress::Daemon.stop_daemon_by_pid_file(file)
|
|
519
688
|
exit
|
|
@@ -527,6 +696,16 @@ module WormCLI
|
|
|
527
696
|
opts.separator ''
|
|
528
697
|
opts.separator 'General:'
|
|
529
698
|
|
|
699
|
+
opts.on('--show-styles', 'Show available worm styles with visual previews') do
|
|
700
|
+
PrgCLI.show_worm_styles
|
|
701
|
+
exit
|
|
702
|
+
end
|
|
703
|
+
|
|
704
|
+
opts.on('--stop-all', 'Stop all prg worm processes') do
|
|
705
|
+
success = PrgCLI.stop_subcommand_processes('worm')
|
|
706
|
+
exit(success ? 0 : 1)
|
|
707
|
+
end
|
|
708
|
+
|
|
530
709
|
opts.on('-v', '--version', 'Show version') do
|
|
531
710
|
puts "Worm version #{RubyProgress::VERSION}"
|
|
532
711
|
exit
|
|
@@ -539,6 +718,12 @@ module WormCLI
|
|
|
539
718
|
end.parse!
|
|
540
719
|
|
|
541
720
|
options
|
|
721
|
+
rescue OptionParser::InvalidOption => e
|
|
722
|
+
puts "Invalid option: #{e.args.first}"
|
|
723
|
+
puts ''
|
|
724
|
+
puts 'Usage: prg worm [options]'
|
|
725
|
+
puts "Run 'prg worm --help' for more information."
|
|
726
|
+
exit 1
|
|
542
727
|
end
|
|
543
728
|
end
|
|
544
729
|
|
|
@@ -675,7 +860,7 @@ module TwirlCLI
|
|
|
675
860
|
end
|
|
676
861
|
end
|
|
677
862
|
|
|
678
|
-
|
|
863
|
+
FileUtils.rm_f(pid_file)
|
|
679
864
|
end
|
|
680
865
|
end
|
|
681
866
|
|
|
@@ -705,8 +890,8 @@ module TwirlCLI
|
|
|
705
890
|
options[:message] = message
|
|
706
891
|
end
|
|
707
892
|
|
|
708
|
-
opts.on('--style STYLE', 'Spinner style (see --
|
|
709
|
-
options[:style] = style
|
|
893
|
+
opts.on('--style STYLE', 'Spinner style (see --show-styles for options)') do |style|
|
|
894
|
+
options[:style] = style
|
|
710
895
|
end
|
|
711
896
|
|
|
712
897
|
opts.separator ''
|
|
@@ -780,6 +965,16 @@ module TwirlCLI
|
|
|
780
965
|
opts.separator ''
|
|
781
966
|
opts.separator 'General:'
|
|
782
967
|
|
|
968
|
+
opts.on('--show-styles', 'Show available twirl styles with visual previews') do
|
|
969
|
+
PrgCLI.show_twirl_styles
|
|
970
|
+
exit
|
|
971
|
+
end
|
|
972
|
+
|
|
973
|
+
opts.on('--stop-all', 'Stop all prg twirl processes') do
|
|
974
|
+
success = PrgCLI.stop_subcommand_processes('twirl')
|
|
975
|
+
exit(success ? 0 : 1)
|
|
976
|
+
end
|
|
977
|
+
|
|
783
978
|
opts.on('-v', '--version', 'Show version') do
|
|
784
979
|
puts "Twirl version #{RubyProgress::VERSION}"
|
|
785
980
|
exit
|
|
@@ -792,6 +987,12 @@ module TwirlCLI
|
|
|
792
987
|
end.parse!
|
|
793
988
|
|
|
794
989
|
options
|
|
990
|
+
rescue OptionParser::InvalidOption => e
|
|
991
|
+
puts "Invalid option: #{e.args.first}"
|
|
992
|
+
puts ''
|
|
993
|
+
puts 'Usage: prg twirl [options]'
|
|
994
|
+
puts "Run 'prg twirl --help' for more information."
|
|
995
|
+
exit 1
|
|
795
996
|
end
|
|
796
997
|
end
|
|
797
998
|
|
|
@@ -799,7 +1000,7 @@ end
|
|
|
799
1000
|
class TwirlSpinner
|
|
800
1001
|
def initialize(message, options = {})
|
|
801
1002
|
@message = message
|
|
802
|
-
@style = options[:style] ||
|
|
1003
|
+
@style = parse_style(options[:style] || 'dots')
|
|
803
1004
|
@speed = parse_speed(options[:speed] || 'medium')
|
|
804
1005
|
@frames = RubyProgress::INDICATORS[@style] || RubyProgress::INDICATORS[:dots]
|
|
805
1006
|
@index = 0
|
|
@@ -807,16 +1008,75 @@ class TwirlSpinner
|
|
|
807
1008
|
|
|
808
1009
|
def animate
|
|
809
1010
|
if @message && !@message.empty?
|
|
810
|
-
print "\r#{@message} #{@frames[@index]}"
|
|
1011
|
+
$stderr.print "\r\e[2K#{@message} #{@frames[@index]}"
|
|
811
1012
|
else
|
|
812
|
-
print "\r#{@frames[@index]}"
|
|
1013
|
+
$stderr.print "\r\e[2K#{@frames[@index]}"
|
|
813
1014
|
end
|
|
1015
|
+
$stderr.flush
|
|
814
1016
|
@index = (@index + 1) % @frames.length
|
|
815
1017
|
sleep @speed
|
|
816
1018
|
end
|
|
817
1019
|
|
|
818
1020
|
private
|
|
819
1021
|
|
|
1022
|
+
def parse_style(style_input)
|
|
1023
|
+
return :dots unless style_input && !style_input.to_s.strip.empty?
|
|
1024
|
+
|
|
1025
|
+
style_lower = style_input.to_s.downcase.strip
|
|
1026
|
+
|
|
1027
|
+
# First, try exact match (convert string keys to symbols for comparison)
|
|
1028
|
+
indicator_keys = RubyProgress::INDICATORS.keys.map(&:to_s)
|
|
1029
|
+
return style_lower.to_sym if indicator_keys.include?(style_lower)
|
|
1030
|
+
|
|
1031
|
+
# Then try prefix matching - keys that start with the input
|
|
1032
|
+
prefix_matches = indicator_keys.select do |key|
|
|
1033
|
+
key.downcase.start_with?(style_lower)
|
|
1034
|
+
end
|
|
1035
|
+
|
|
1036
|
+
unless prefix_matches.empty?
|
|
1037
|
+
# For prefix matches, return the shortest one
|
|
1038
|
+
best_match = prefix_matches.min_by(&:length)
|
|
1039
|
+
return best_match.to_sym
|
|
1040
|
+
end
|
|
1041
|
+
|
|
1042
|
+
# Try character-by-character fuzzy matching for partial inputs
|
|
1043
|
+
# Find keys where the input characters appear in order (not necessarily contiguous)
|
|
1044
|
+
fuzzy_matches = indicator_keys.select do |key|
|
|
1045
|
+
key_chars = key.downcase.chars
|
|
1046
|
+
input_chars = style_lower.chars
|
|
1047
|
+
|
|
1048
|
+
# Check if all input characters appear in order in the key
|
|
1049
|
+
input_chars.all? do |char|
|
|
1050
|
+
idx = key_chars.index(char)
|
|
1051
|
+
if idx
|
|
1052
|
+
key_chars = key_chars[idx + 1..-1] # Remove matched chars and continue
|
|
1053
|
+
true
|
|
1054
|
+
else
|
|
1055
|
+
false
|
|
1056
|
+
end
|
|
1057
|
+
end
|
|
1058
|
+
end
|
|
1059
|
+
|
|
1060
|
+
unless fuzzy_matches.empty?
|
|
1061
|
+
# Sort by length (prefer shorter keys)
|
|
1062
|
+
best_match = fuzzy_matches.min_by(&:length)
|
|
1063
|
+
return best_match.to_sym
|
|
1064
|
+
end
|
|
1065
|
+
|
|
1066
|
+
# Fallback to substring matching
|
|
1067
|
+
substring_matches = indicator_keys.select do |key|
|
|
1068
|
+
key.downcase.include?(style_lower)
|
|
1069
|
+
end
|
|
1070
|
+
|
|
1071
|
+
unless substring_matches.empty?
|
|
1072
|
+
best_match = substring_matches.min_by(&:length)
|
|
1073
|
+
return best_match.to_sym
|
|
1074
|
+
end
|
|
1075
|
+
|
|
1076
|
+
# Default fallback
|
|
1077
|
+
:dots
|
|
1078
|
+
end
|
|
1079
|
+
|
|
820
1080
|
def parse_speed(speed)
|
|
821
1081
|
case speed.to_s.downcase
|
|
822
1082
|
when /^f/, '1', '2', '3'
|