ruby-progress 1.1.3 → 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.
data/bin/prg CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'ruby-progress'
4
+ require_relative '../lib/ruby-progress'
5
5
  require 'fileutils'
6
6
  require 'optparse'
7
7
  require 'json'
@@ -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 ' 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'
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
- puts '== ripple'
97
- puts 'rainbow'
98
- puts 'inverse'
99
- puts 'caps'
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 '== worm'
102
- puts 'circles'
103
- puts 'blocks'
104
- puts 'geometric'
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
- puts '== twirl'
107
- RubyProgress::INDICATORS.each_key do |name|
108
- puts name
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
- OptionParser.new do |opts|
135
- opts.banner = 'Usage: prg ripple [options] [STRING]'
136
- opts.separator ''
137
- opts.separator 'Animation Options:'
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
- opts.on('-s', '--speed SPEED', 'Animation speed (fast/medium/slow or f/m/s)') do |s|
140
- options[:speed] = case s.downcase
141
- when /^f/ then :fast
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
- opts.on('-m', '--message MESSAGE', 'Message to display (alternative to positional argument)') do |msg|
149
- options[:message] = msg
150
- end
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
- 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)
154
- end
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
- opts.on('-d', '--direction DIRECTION', 'Animation direction (forward/bidirectional or f/b)') do |f|
157
- options[:format] = f =~ /^f/i ? :forward_only : :bidirectional
158
- end
307
+ opts.separator ''
308
+ opts.separator 'Command Execution:'
159
309
 
160
- opts.separator ''
161
- opts.separator 'Command Execution:'
310
+ opts.on('-c', '--command COMMAND', 'Run command during animation (optional)') do |command|
311
+ options[:command] = command
312
+ end
162
313
 
163
- opts.on('-c', '--command COMMAND', 'Run command during animation (optional)') do |command|
164
- options[:command] = command
165
- end
314
+ opts.on('--success MESSAGE', 'Success message to display') do |msg|
315
+ options[:success_message] = msg
316
+ end
166
317
 
167
- opts.on('--success MESSAGE', 'Success message to display') do |msg|
168
- options[:success_message] = msg
169
- end
318
+ opts.on('--error MESSAGE', 'Error message to display') do |msg|
319
+ options[:fail_message] = msg
320
+ end
170
321
 
171
- opts.on('--error MESSAGE', 'Error message to display') do |msg|
172
- options[:fail_message] = msg
173
- end
322
+ opts.on('--checkmark', 'Show checkmarks (✅ success, 🛑 failure)') do
323
+ options[:complete_checkmark] = true
324
+ end
174
325
 
175
- opts.on('--checkmark', 'Show checkmarks (✅ success, 🛑 failure)') do
176
- options[:complete_checkmark] = true
177
- end
326
+ opts.on('--stdout', 'Output captured command result to STDOUT') do
327
+ options[:output] = :stdout
328
+ end
178
329
 
179
- opts.on('--stdout', 'Output captured command result to STDOUT') do
180
- options[:output] = :stdout
181
- end
330
+ opts.on('--quiet', 'Suppress all output') do
331
+ options[:output] = :quiet
332
+ end
182
333
 
183
- opts.on('--quiet', 'Suppress all output') do
184
- options[:output] = :quiet
185
- end
334
+ opts.separator ''
335
+ opts.separator 'Daemon Mode:'
186
336
 
187
- opts.separator ''
188
- opts.separator 'Daemon Mode:'
337
+ opts.on('--daemon', 'Run in background daemon mode') do
338
+ options[:daemon] = true
339
+ end
189
340
 
190
- opts.on('--daemon', 'Run in background daemon mode') do
191
- options[:daemon] = true
192
- end
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
- 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
345
+ opts.on('--stop', 'Stop daemon (uses default PID file unless --pid-file specified)') do
346
+ options[:stop] = true
347
+ end
197
348
 
198
- opts.on('--stop', 'Stop daemon (uses default PID file unless --pid-file specified)') do
199
- options[:stop] = true
200
- end
349
+ opts.on('--status', 'Show daemon status (running/not running)') do
350
+ options[:status] = true
351
+ end
201
352
 
202
- opts.on('--status', 'Show daemon status (running/not running)') do
203
- options[:status] = true
204
- end
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
- 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
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
- 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.'
368
+ opts.separator ''
369
+ opts.separator 'General:'
220
370
 
221
- opts.separator ''
222
- opts.separator 'General:'
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
- opts.on('-v', '--version', 'Show version') do
225
- puts "Ripple version #{RubyProgress::VERSION}"
226
- exit
227
- end
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
- opts.on('-h', '--help', 'Show this help') do
230
- puts opts
231
- exit
232
- end
233
- end.parse!
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
- File.delete(pid_file) if File.exist?(pid_file)
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
- File.delete(pid_file) if File.exist?(pid_file)
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
- File.delete(pid_file) if File.exist?(pid_file)
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 --list-styles for options)') do |style|
709
- options[:style] = style.to_sym
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] || :dots
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'