ruby-progress 1.0.1 → 1.1.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.
data/bin/prg CHANGED
@@ -1,8 +1,11 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require_relative '../lib/ruby-progress'
4
+ require 'ruby-progress'
5
+ require 'fileutils'
5
6
  require 'optparse'
7
+ require 'json'
8
+ require 'English'
6
9
 
7
10
  # Unified progress indicator CLI
8
11
  module PrgCLI
@@ -13,16 +16,22 @@ module PrgCLI
13
16
  end
14
17
 
15
18
  subcommand = ARGV.shift.downcase
16
-
19
+
17
20
  case subcommand
18
21
  when 'ripple'
19
22
  RippleCLI.run
20
23
  when 'worm'
21
24
  WormCLI.run
25
+ when 'twirl'
26
+ TwirlCLI.run
27
+ when '--list-styles'
28
+ show_styles
29
+ exit
22
30
  when '-v', '--version'
23
31
  puts "prg version #{RubyProgress::VERSION}"
24
- puts " ripple - Text ripple animation with spinners and effects"
25
- puts " worm - Unicode wave animation with customizable styles"
32
+ puts ' ripple - Text ripple animation with color effects'
33
+ puts ' worm - Unicode wave animation with customizable styles'
34
+ puts ' twirl - Spinner animation with various indicator styles'
26
35
  exit
27
36
  when '-h', '--help', 'help'
28
37
  show_help
@@ -42,21 +51,63 @@ module PrgCLI
42
51
  prg <subcommand> [options]
43
52
 
44
53
  SUBCOMMANDS:
45
- ripple Text ripple animation with spinners and color effects
54
+ ripple Text ripple animation with color effects
46
55
  worm Unicode wave animation with customizable dot styles
56
+ twirl Spinner-based animation with various indicator styles
47
57
 
48
58
  GLOBAL OPTIONS:
49
59
  -v, --version Show version information
50
60
  -h, --help Show this help message
61
+ --list-styles List all available styles for all subcommands
51
62
 
52
63
  EXAMPLES:
53
- prg ripple "Loading..." --rainbow --speed fast
64
+ prg ripple "Loading..." --style rainbow --speed fast
54
65
  prg worm --message "Processing" --style blocks --checkmark
66
+ prg twirl --message "Spinning" --style dots --checkmark
55
67
  prg ripple --command "sleep 3" --success "Done!" --checkmark
56
68
 
57
69
  Run 'prg <subcommand> --help' for specific subcommand options.
58
70
  HELP
59
71
  end
72
+
73
+ # Detach the current process into a background daemon so the calling shell
74
+ # doesn't track it as a job (prevents 'job ... has ended' notifications).
75
+ # Intentionally keeps stdio open so the daemon can print a final message.
76
+ def self.daemonize
77
+ return if ENV['PRG_NO_DAEMONIZE'] == '1'
78
+
79
+ pid = fork
80
+ if pid
81
+ # Parent exits immediately; child continues
82
+ exit 0
83
+ end
84
+
85
+ # Become session leader and detach from controlling terminal
86
+ Process.setsid
87
+
88
+ # Second fork to avoid reacquiring a controlling terminal
89
+ pid2 = fork
90
+ exit 0 if pid2
91
+
92
+ # Do not chdir or close stdio: we want to be able to emit completion output
93
+ end
94
+
95
+ def self.show_styles
96
+ puts '== ripple'
97
+ puts 'rainbow'
98
+ puts 'inverse'
99
+ puts 'caps'
100
+ puts ''
101
+ puts '== worm'
102
+ puts 'circles'
103
+ puts 'blocks'
104
+ puts 'geometric'
105
+ puts ''
106
+ puts '== twirl'
107
+ RubyProgress::INDICATORS.each_key do |name|
108
+ puts name
109
+ end
110
+ end
60
111
  end
61
112
 
62
113
  # Enhanced Ripple CLI with unified flags
@@ -70,11 +121,8 @@ module RippleCLI
70
121
  options = {
71
122
  speed: :medium,
72
123
  direction: :bidirectional,
73
- rainbow: false,
74
- spinner: false,
75
- spinner_position: :before,
124
+ styles: [],
76
125
  caps: false,
77
- inverse: false,
78
126
  command: nil,
79
127
  success_message: nil,
80
128
  fail_message: nil,
@@ -101,38 +149,14 @@ module RippleCLI
101
149
  options[:message] = msg
102
150
  end
103
151
 
104
- opts.on('-r', '--rainbow', 'Enable rainbow color mode') do
105
- options[:rainbow] = true
152
+ opts.on('--style STYLES', 'Animation styles (rainbow, inverse, caps - can be comma-separated)') do |styles|
153
+ options[:styles] = styles.split(',').map(&:strip).map(&:to_sym)
106
154
  end
107
155
 
108
156
  opts.on('-d', '--direction DIRECTION', 'Animation direction (forward/bidirectional or f/b)') do |f|
109
157
  options[:format] = f =~ /^f/i ? :forward_only : :bidirectional
110
158
  end
111
159
 
112
- opts.on('-i', '--inverse', 'Enable inverse highlighting mode') do
113
- options[:inverse] = true
114
- end
115
-
116
- opts.separator ''
117
- opts.separator 'Spinner Options:'
118
-
119
- opts.on('--spinner TYPE', 'Use spinner animation instead of text ripple') do |type|
120
- options[:spinner] = type.normalize_type
121
- end
122
-
123
- opts.on('--spinner-pos POSITION', 'Spinner position (before/after or b/a)') do |pos|
124
- options[:spinner_position] = pos =~ /^a/i ? :after : :before
125
- end
126
-
127
- opts.on('--caps', 'Enable case transformation mode') do
128
- options[:caps] = true
129
- end
130
-
131
- opts.on('--list-spinners', 'List all available spinner types') do
132
- show_spinners
133
- exit
134
- end
135
-
136
160
  opts.separator ''
137
161
  opts.separator 'Command Execution:'
138
162
 
@@ -160,6 +184,40 @@ module RippleCLI
160
184
  options[:output] = :quiet
161
185
  end
162
186
 
187
+ opts.separator ''
188
+ opts.separator 'Daemon Mode:'
189
+
190
+ opts.on('--daemon', 'Run in background daemon mode') do
191
+ options[:daemon] = true
192
+ end
193
+
194
+ opts.on('--pid-file FILE', 'Write process ID to file (default: /tmp/ruby-progress/progress.pid)') do |file|
195
+ options[:pid_file] = file
196
+ end
197
+
198
+ opts.on('--stop', 'Stop daemon (uses default PID file unless --pid-file specified)') do
199
+ options[:stop] = true
200
+ end
201
+
202
+ opts.on('--status', 'Show daemon status (running/not running)') do
203
+ options[:status] = true
204
+ end
205
+
206
+ opts.on('--stop-success MESSAGE', 'When stopping, show this success message') do |msg|
207
+ options[:stop_success] = msg
208
+ end
209
+ opts.on('--stop-error MESSAGE', 'When stopping, show this error message') do |msg|
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
215
+
216
+ opts.separator ''
217
+ opts.separator 'Daemon notes:'
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.'
220
+
163
221
  opts.separator ''
164
222
  opts.separator 'General:'
165
223
 
@@ -174,26 +232,49 @@ module RippleCLI
174
232
  end
175
233
  end.parse!
176
234
 
177
- # Get text from positional argument or --message flag
178
- text = options[:message] || ARGV.join(' ')
179
-
180
- if text.empty?
181
- puts 'Error: Please provide text to animate via argument or --message flag'
182
- puts "Example: prg ripple 'Loading...' or prg ripple --message 'Loading...'"
183
- exit 1
184
- end
185
-
186
- if options[:command]
187
- run_with_command(text, options)
235
+ # Daemon/status/stop handling (process these without requiring text)
236
+ if options[:status]
237
+ pid_file = options[:pid_file] || RubyProgress::Daemon.default_pid_file
238
+ RubyProgress::Daemon.show_status(pid_file)
239
+ exit
240
+ elsif options[:stop]
241
+ pid_file = options[:pid_file] || RubyProgress::Daemon.default_pid_file
242
+ stop_msg = options[:stop_error] || options[:stop_success]
243
+ is_error = !options[:stop_error].nil?
244
+ RubyProgress::Daemon.stop_daemon_by_pid_file(
245
+ pid_file,
246
+ message: stop_msg,
247
+ checkmark: options[:stop_checkmark],
248
+ error: is_error
249
+ )
250
+ exit
251
+ elsif options[:daemon]
252
+ # For daemon mode, detach so shell has no tracked job
253
+ PrgCLI.daemonize
254
+
255
+ # For daemon mode, default message if none provided
256
+ text = options[:message] || ARGV.join(' ')
257
+ text = 'Processing' if text.nil? || text.empty?
258
+ run_daemon_mode(text, options)
188
259
  else
189
- run_indefinitely(text, options)
190
- end
191
- end
260
+ # Non-daemon path requires text
261
+ text = options[:message] || ARGV.join(' ')
262
+ if text.empty?
263
+ puts 'Error: Please provide text to animate via argument or --message flag'
264
+ puts "Example: prg ripple 'Loading...' or prg ripple --message 'Loading...'"
265
+ exit 1
266
+ end
192
267
 
193
- def self.show_spinners
194
- puts "Available spinners:"
195
- RubyProgress::INDICATORS.each do |name, chars|
196
- puts " #{name.to_s.ljust(15)} #{chars[0..2].join(' ')}"
268
+ # Convert styles array to individual flags for backward compatibility
269
+ options[:rainbow] = options[:styles].include?(:rainbow)
270
+ options[:inverse] = options[:styles].include?(:inverse)
271
+ options[:caps] = options[:styles].include?(:caps)
272
+
273
+ if options[:command]
274
+ run_with_command(text, options)
275
+ else
276
+ run_indefinitely(text, options)
277
+ end
197
278
  end
198
279
  end
199
280
 
@@ -203,11 +284,9 @@ module RippleCLI
203
284
  captured_output = `#{options[:command]} 2>&1`
204
285
  end
205
286
 
206
- success = $?.success?
287
+ success = $CHILD_STATUS.success?
207
288
 
208
- if options[:output] == :stdout
209
- puts captured_output
210
- end
289
+ puts captured_output if options[:output] == :stdout
211
290
  if options[:success_message] || options[:complete_checkmark]
212
291
  message = success ? options[:success_message] : options[:fail_message] || options[:success_message]
213
292
  RubyProgress::Ripple.complete(text, message, options[:complete_checkmark], success)
@@ -219,14 +298,64 @@ module RippleCLI
219
298
  rippler = RubyProgress::Ripple.new(text, options)
220
299
  RubyProgress::Utils.hide_cursor
221
300
  begin
222
- while true
223
- rippler.advance
224
- end
301
+ loop { rippler.advance }
225
302
  ensure
226
303
  RubyProgress::Utils.show_cursor
227
304
  RubyProgress::Ripple.complete(text, options[:success_message], options[:complete_checkmark], true)
228
305
  end
229
306
  end
307
+
308
+ def self.run_daemon_mode(text, options)
309
+ pid_file = options[:pid_file] || RubyProgress::Daemon.default_pid_file
310
+ FileUtils.mkdir_p(File.dirname(pid_file))
311
+ File.write(pid_file, Process.pid.to_s)
312
+
313
+ begin
314
+ # For Ripple, re-use the existing animation loop via a simple loop
315
+ RubyProgress::Utils.hide_cursor
316
+ rippler = RubyProgress::Ripple.new(text, options)
317
+ stop_requested = false
318
+
319
+ Signal.trap('INT') { stop_requested = true }
320
+ Signal.trap('USR1') { stop_requested = true }
321
+ Signal.trap('TERM') { stop_requested = true }
322
+ Signal.trap('HUP') { stop_requested = true }
323
+
324
+ rippler.advance until stop_requested
325
+ ensure
326
+ RubyProgress::Utils.clear_line
327
+ RubyProgress::Utils.show_cursor
328
+
329
+ # If a control message file exists, output its message with optional checkmark
330
+ cmf = RubyProgress::Daemon.control_message_file(pid_file)
331
+ if File.exist?(cmf)
332
+ begin
333
+ data = JSON.parse(File.read(cmf))
334
+ message = data['message']
335
+ check = data.key?('checkmark') ? !!data['checkmark'] : false
336
+ success_val = data.key?('success') ? !!data['success'] : true
337
+ if message
338
+ RubyProgress::Utils.display_completion(
339
+ message,
340
+ success: success_val,
341
+ show_checkmark: check,
342
+ output_stream: :stdout
343
+ )
344
+ end
345
+ rescue StandardError
346
+ # ignore
347
+ ensure
348
+ begin
349
+ File.delete(cmf)
350
+ rescue StandardError
351
+ nil
352
+ end
353
+ end
354
+ end
355
+
356
+ File.delete(pid_file) if File.exist?(pid_file)
357
+ end
358
+ end
230
359
  end
231
360
 
232
361
  # Enhanced Worm CLI with unified flags
@@ -234,12 +363,67 @@ module WormCLI
234
363
  def self.run
235
364
  options = parse_cli_options
236
365
 
366
+ if options[:status]
367
+ pid_file = resolve_pid_file(options, :status_name)
368
+ RubyProgress::Daemon.show_status(pid_file)
369
+ exit
370
+ elsif options[:stop]
371
+ pid_file = resolve_pid_file(options, :stop_name)
372
+ stop_msg = options[:stop_error] || options[:stop_success]
373
+ is_error = !options[:stop_error].nil?
374
+ RubyProgress::Daemon.stop_daemon_by_pid_file(
375
+ pid_file,
376
+ message: stop_msg,
377
+ checkmark: options[:stop_checkmark],
378
+ error: is_error
379
+ )
380
+ exit
381
+ elsif options[:daemon]
382
+ # Detach before starting daemon logic so there's no tracked shell job
383
+ PrgCLI.daemonize
384
+ run_daemon_mode(options)
385
+ else
386
+ progress = RubyProgress::Worm.new(options)
387
+
388
+ if options[:command]
389
+ progress.run_with_command
390
+ else
391
+ progress.run_indefinitely
392
+ end
393
+ end
394
+ end
395
+
396
+ def self.run_daemon_mode(options)
397
+ # Use daemon name or default PID file if none specified
398
+ pid_file = resolve_pid_file(options, :daemon_name)
399
+
400
+ # Ensure directory exists
401
+ FileUtils.mkdir_p(File.dirname(pid_file))
402
+
403
+ # Write PID file
404
+ File.write(pid_file, Process.pid.to_s)
405
+
237
406
  progress = RubyProgress::Worm.new(options)
238
407
 
239
- if options[:command]
240
- progress.run_with_command
408
+ begin
409
+ progress.run_daemon_mode(
410
+ success_message: options[:success],
411
+ show_checkmark: options[:checkmark],
412
+ control_message_file: RubyProgress::Daemon.control_message_file(pid_file)
413
+ )
414
+ ensure
415
+ # Clean up PID file
416
+ File.delete(pid_file) if File.exist?(pid_file)
417
+ end
418
+ end
419
+
420
+ def self.resolve_pid_file(options, name_key)
421
+ return options[:pid_file] if options[:pid_file]
422
+
423
+ if options[name_key]
424
+ "/tmp/ruby-progress/#{options[name_key]}.pid"
241
425
  else
242
- progress.run_indefinitely
426
+ RubyProgress::Daemon.default_pid_file
243
427
  end
244
428
  end
245
429
 
@@ -290,6 +474,56 @@ module WormCLI
290
474
  options[:stdout] = true
291
475
  end
292
476
 
477
+ opts.separator ''
478
+ opts.separator 'Daemon Mode:'
479
+
480
+ opts.on('--daemon', 'Run in background daemon mode') do
481
+ options[:daemon] = true
482
+ end
483
+
484
+ opts.on('--daemon-as NAME', 'Run in daemon mode with custom name (creates /tmp/ruby-progress/NAME.pid)') do |name|
485
+ options[:daemon] = true
486
+ options[:daemon_name] = name
487
+ end
488
+
489
+ opts.on('--pid-file FILE', 'Write process ID to file (default: /tmp/ruby-progress/progress.pid)') do |file|
490
+ options[:pid_file] = file
491
+ end
492
+
493
+ opts.on('--stop', 'Stop daemon (uses default PID file unless --pid-file specified)') do
494
+ options[:stop] = true
495
+ end
496
+ opts.on('--stop-id NAME', 'Stop daemon by name (automatically implies --stop)') do |name|
497
+ options[:stop] = true
498
+ options[:stop_name] = name
499
+ end
500
+ opts.on('--status', 'Show daemon status (uses default PID file unless --pid-file specified)') do
501
+ options[:status] = true
502
+ end
503
+ opts.on('--status-id NAME', 'Show daemon status by name') do |name|
504
+ options[:status] = true
505
+ options[:status_name] = name
506
+ end
507
+ opts.on('--stop-success MESSAGE', 'Stop daemon with success message (automatically implies --stop)') do |msg|
508
+ options[:stop] = true
509
+ options[:stop_success] = msg
510
+ end
511
+ opts.on('--stop-error MESSAGE', 'Stop daemon with error message (automatically implies --stop)') do |msg|
512
+ options[:stop] = true
513
+ options[:stop_error] = msg
514
+ end
515
+ opts.on('--stop-checkmark', 'When stopping, include a success checkmark') { options[:stop_checkmark] = true }
516
+
517
+ opts.on('--stop-pid FILE', 'Stop daemon by reading PID from file (deprecated: use --stop [--pid-file])') do |file|
518
+ RubyProgress::Daemon.stop_daemon_by_pid_file(file)
519
+ exit
520
+ end
521
+
522
+ opts.separator ''
523
+ opts.separator 'Daemon notes:'
524
+ opts.separator ' - Do not append &; prg detaches itself and returns immediately.'
525
+ opts.separator ' - Use --daemon-as NAME for named daemons, or --stop-id/--status-id for named control.'
526
+
293
527
  opts.separator ''
294
528
  opts.separator 'General:'
295
529
 
@@ -308,4 +542,289 @@ module WormCLI
308
542
  end
309
543
  end
310
544
 
311
- PrgCLI.run if __FILE__ == $PROGRAM_NAME
545
+ # Twirl CLI - spinner-based progress indicator
546
+ module TwirlCLI
547
+ def self.run
548
+ options = parse_cli_options
549
+
550
+ if options[:status]
551
+ pid_file = resolve_pid_file(options, :status_name)
552
+ RubyProgress::Daemon.show_status(pid_file)
553
+ exit
554
+ elsif options[:stop]
555
+ pid_file = resolve_pid_file(options, :stop_name)
556
+ stop_msg = options[:stop_error] || options[:stop_success]
557
+ is_error = !options[:stop_error].nil?
558
+ RubyProgress::Daemon.stop_daemon_by_pid_file(
559
+ pid_file,
560
+ message: stop_msg,
561
+ checkmark: options[:stop_checkmark],
562
+ error: is_error
563
+ )
564
+ exit
565
+ elsif options[:daemon]
566
+ PrgCLI.daemonize
567
+ run_daemon_mode(options)
568
+ elsif options[:command]
569
+ run_with_command(options)
570
+ else
571
+ run_indefinitely(options)
572
+ end
573
+ end
574
+
575
+ def self.run_with_command(options)
576
+ message = options[:message] || 'Processing'
577
+ captured_output = nil
578
+
579
+ spinner = TwirlSpinner.new(message, options)
580
+ success = false
581
+
582
+ begin
583
+ RubyProgress::Utils.hide_cursor
584
+ spinner_thread = Thread.new { spinner.animate }
585
+
586
+ captured_output = `#{options[:command]} 2>&1`
587
+ success = $CHILD_STATUS.success?
588
+
589
+ spinner_thread.kill
590
+ RubyProgress::Utils.clear_line
591
+ ensure
592
+ RubyProgress::Utils.show_cursor
593
+ end
594
+
595
+ puts captured_output if options[:stdout]
596
+
597
+ if options[:success] || options[:error] || options[:checkmark]
598
+ final_msg = success ? options[:success] : options[:error]
599
+ final_msg ||= success ? 'Success' : 'Failed'
600
+
601
+ RubyProgress::Utils.display_completion(
602
+ final_msg,
603
+ success: success,
604
+ show_checkmark: options[:checkmark]
605
+ )
606
+ end
607
+
608
+ exit success ? 0 : 1
609
+ end
610
+
611
+ def self.run_indefinitely(options)
612
+ message = options[:message] || 'Processing'
613
+ spinner = TwirlSpinner.new(message, options)
614
+
615
+ begin
616
+ RubyProgress::Utils.hide_cursor
617
+ loop { spinner.animate }
618
+ ensure
619
+ RubyProgress::Utils.show_cursor
620
+ if options[:success] || options[:checkmark]
621
+ RubyProgress::Utils.display_completion(
622
+ options[:success] || 'Complete',
623
+ success: true,
624
+ show_checkmark: options[:checkmark]
625
+ )
626
+ end
627
+ end
628
+ end
629
+
630
+ def self.run_daemon_mode(options)
631
+ pid_file = resolve_pid_file(options, :daemon_name)
632
+ FileUtils.mkdir_p(File.dirname(pid_file))
633
+ File.write(pid_file, Process.pid.to_s)
634
+
635
+ message = options[:message] || 'Processing'
636
+ spinner = TwirlSpinner.new(message, options)
637
+ stop_requested = false
638
+
639
+ Signal.trap('INT') { stop_requested = true }
640
+ Signal.trap('USR1') { stop_requested = true }
641
+ Signal.trap('TERM') { stop_requested = true }
642
+ Signal.trap('HUP') { stop_requested = true }
643
+
644
+ begin
645
+ RubyProgress::Utils.hide_cursor
646
+ spinner.animate until stop_requested
647
+ ensure
648
+ RubyProgress::Utils.clear_line
649
+ RubyProgress::Utils.show_cursor
650
+
651
+ # Check for control message
652
+ cmf = RubyProgress::Daemon.control_message_file(pid_file)
653
+ if File.exist?(cmf)
654
+ begin
655
+ data = JSON.parse(File.read(cmf))
656
+ message = data['message']
657
+ check = data.key?('checkmark') ? data['checkmark'] : false
658
+ success_val = data.key?('success') ? data['success'] : true
659
+ if message
660
+ RubyProgress::Utils.display_completion(
661
+ message,
662
+ success: success_val,
663
+ show_checkmark: check,
664
+ output_stream: :stdout
665
+ )
666
+ end
667
+ rescue StandardError
668
+ # ignore
669
+ ensure
670
+ begin
671
+ File.delete(cmf)
672
+ rescue StandardError
673
+ nil
674
+ end
675
+ end
676
+ end
677
+
678
+ File.delete(pid_file) if File.exist?(pid_file)
679
+ end
680
+ end
681
+
682
+ def self.resolve_pid_file(options, name_key)
683
+ return options[:pid_file] if options[:pid_file]
684
+
685
+ if options[name_key]
686
+ "/tmp/ruby-progress/#{options[name_key]}.pid"
687
+ else
688
+ RubyProgress::Daemon.default_pid_file
689
+ end
690
+ end
691
+
692
+ def self.parse_cli_options
693
+ options = {}
694
+
695
+ OptionParser.new do |opts|
696
+ opts.banner = 'Usage: prg twirl [options]'
697
+ opts.separator ''
698
+ opts.separator 'Animation Options:'
699
+
700
+ opts.on('-s', '--speed SPEED', 'Animation speed (1-10, fast/medium/slow, or f/m/s)') do |speed|
701
+ options[:speed] = speed
702
+ end
703
+
704
+ opts.on('-m', '--message MESSAGE', 'Message to display before spinner') do |message|
705
+ options[:message] = message
706
+ end
707
+
708
+ opts.on('--style STYLE', 'Spinner style (see --list-styles for options)') do |style|
709
+ options[:style] = style.to_sym
710
+ end
711
+
712
+ opts.separator ''
713
+ opts.separator 'Command Execution:'
714
+
715
+ opts.on('-c', '--command COMMAND', 'Command to run (optional - runs indefinitely without)') do |command|
716
+ options[:command] = command
717
+ end
718
+
719
+ opts.on('--success MESSAGE', 'Success message to display') do |text|
720
+ options[:success] = text
721
+ end
722
+
723
+ opts.on('--error MESSAGE', 'Error message to display') do |text|
724
+ options[:error] = text
725
+ end
726
+
727
+ opts.on('--checkmark', 'Show checkmarks (✅ success, 🛑 failure)') do
728
+ options[:checkmark] = true
729
+ end
730
+
731
+ opts.on('--stdout', 'Output captured command result to STDOUT') do
732
+ options[:stdout] = true
733
+ end
734
+
735
+ opts.separator ''
736
+ opts.separator 'Daemon Mode:'
737
+
738
+ opts.on('--daemon', 'Run in background daemon mode') do
739
+ options[:daemon] = true
740
+ end
741
+
742
+ opts.on('--daemon-as NAME', 'Run in daemon mode with custom name (creates /tmp/ruby-progress/NAME.pid)') do |name|
743
+ options[:daemon] = true
744
+ options[:daemon_name] = name
745
+ end
746
+
747
+ opts.on('--pid-file FILE', 'Write process ID to file (default: /tmp/ruby-progress/progress.pid)') do |file|
748
+ options[:pid_file] = file
749
+ end
750
+
751
+ opts.on('--stop', 'Stop daemon (uses default PID file unless --pid-file specified)') do
752
+ options[:stop] = true
753
+ end
754
+ opts.on('--stop-id NAME', 'Stop daemon by name (automatically implies --stop)') do |name|
755
+ options[:stop] = true
756
+ options[:stop_name] = name
757
+ end
758
+ opts.on('--status', 'Show daemon status (uses default PID file unless --pid-file specified)') do
759
+ options[:status] = true
760
+ end
761
+ opts.on('--status-id NAME', 'Show daemon status by name') do |name|
762
+ options[:status] = true
763
+ options[:status_name] = name
764
+ end
765
+ opts.on('--stop-success MESSAGE', 'Stop daemon with success message (automatically implies --stop)') do |msg|
766
+ options[:stop] = true
767
+ options[:stop_success] = msg
768
+ end
769
+ opts.on('--stop-error MESSAGE', 'Stop daemon with error message (automatically implies --stop)') do |msg|
770
+ options[:stop] = true
771
+ options[:stop_error] = msg
772
+ end
773
+ opts.on('--stop-checkmark', 'When stopping, include a success checkmark') { options[:stop_checkmark] = true }
774
+
775
+ opts.separator ''
776
+ opts.separator 'Daemon notes:'
777
+ opts.separator ' - Do not append &; prg detaches itself and returns immediately.'
778
+ opts.separator ' - Use --daemon-as NAME for named daemons, or --stop-id/--status-id for named control.'
779
+
780
+ opts.separator ''
781
+ opts.separator 'General:'
782
+
783
+ opts.on('-v', '--version', 'Show version') do
784
+ puts "Twirl version #{RubyProgress::VERSION}"
785
+ exit
786
+ end
787
+
788
+ opts.on('-h', '--help', 'Show this help') do
789
+ puts opts
790
+ exit
791
+ end
792
+ end.parse!
793
+
794
+ options
795
+ end
796
+ end
797
+
798
+ # Simple spinner class for Twirl
799
+ class TwirlSpinner
800
+ def initialize(message, options = {})
801
+ @message = message
802
+ @style = options[:style] || :dots
803
+ @speed = parse_speed(options[:speed] || 'medium')
804
+ @frames = RubyProgress::INDICATORS[@style] || RubyProgress::INDICATORS[:dots]
805
+ @index = 0
806
+ end
807
+
808
+ def animate
809
+ print "\r#{@message} #{@frames[@index]}"
810
+ @index = (@index + 1) % @frames.length
811
+ sleep @speed
812
+ end
813
+
814
+ private
815
+
816
+ def parse_speed(speed)
817
+ case speed.to_s.downcase
818
+ when /^f/, '1', '2', '3'
819
+ 0.05
820
+ when /^m/, '4', '5', '6', '7'
821
+ 0.1
822
+ when /^s/, '8', '9', '10'
823
+ 0.2
824
+ else
825
+ speed.to_f > 0 ? (1.0 / speed.to_f) : 0.1
826
+ end
827
+ end
828
+ end
829
+
830
+ PrgCLI.run