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/ripple CHANGED
@@ -1,147 +1,10 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require_relative '../lib/ruby-progress'
5
- require 'optparse'
6
-
7
- # Ripple CLI implementation
8
- module RippleCLI
9
- def self.run
10
- trap('INT') do
11
- RubyProgress::Utils.show_cursor
12
- exit
13
- end
14
-
15
- options = {
16
- speed: :medium,
17
- direction: :bidirectional,
18
- rainbow: false,
19
- spinner: false,
20
- spinner_position: :before,
21
- caps: false,
22
- inverse: false,
23
- command: nil,
24
- success_message: nil,
25
- fail_message: nil,
26
- complete_checkmark: false
27
- }
28
-
29
- OptionParser.new do |opts|
30
- opts.banner = 'Usage: ripple [options] STRING'
31
-
32
- opts.on('-s', '--speed SPEED', %i[fast medium slow], 'Set animation speed ((f)ast/(m)edium/(s)low)') do |s|
33
- options[:speed] = case s
34
- when /^f/ then :fast
35
- when /^m/ then :medium
36
- when /^s/ then :slow
37
- else :slow
38
- end
39
- end
40
-
41
- opts.on('-r', '--rainbow', 'Enable rainbow mode') do
42
- options[:rainbow] = true
43
- end
44
-
45
- opts.on('-d', '--direction DIRECTION', 'Set animation format ((f)orward/(b)ack-and-forth)') do |f|
46
- options[:format] = f =~ /^f/ ? :forward_only : :bidirectional
47
- end
48
-
49
- opts.on('-i', '--inverse', 'Enable inverse mode') do
50
- options[:inverse] = true
51
- end
52
-
53
- opts.on('-c', '--command COMMAND', 'Run a command during the animation') do |command|
54
- options[:command] = command
55
- end
56
-
57
- opts.on('--success MESSAGE', 'Message to display on success') do |msg|
58
- options[:success_message] = msg
59
- end
60
-
61
- opts.on('--fail MESSAGE', 'Message to display on error') do |msg|
62
- options[:fail_message] = msg
63
- end
64
-
65
- opts.on('--checkmark') do
66
- options[:complete_checkmark] = true
67
- end
68
-
69
- opts.on('--spinner TYPE', 'Display a rippling spinner with the message') do |type|
70
- options[:spinner] = type.normalize_type
71
- end
72
-
73
- opts.on('--spinner-pos POSITION', 'Display spinner [b]efore or [a]fter message') do |pos|
74
- options[:spinner_position] = pos =~ /^a/ ? :after : :before
75
- end
76
-
77
- opts.on('--list-spinners', 'List available spinners') do
78
- out = "Spinners:\n"
79
- RubyProgress::INDICATORS.each do |k, v|
80
- out += "- #{k}: #{v[2]}\n"
81
- end
82
- puts out
83
- exit
84
- end
85
-
86
- opts.on('--caps') do
87
- options[:caps] = true
88
- end
89
-
90
- opts.on('--stdout', 'Output captured command result to STDOUT') do |_output|
91
- options[:output] = :stdout
92
- end
93
-
94
- opts.on('--quiet', 'Suppress all output') do |_quiet|
95
- options[:output] = :quiet
96
- end
97
-
98
- opts.on('-v', '--version', 'Display the version') do
99
- puts "Ripple version #{RubyProgress::VERSION}"
100
- exit
101
- end
102
-
103
- opts.on('-h', '--help', 'Display this help message') do
104
- puts opts
105
- exit
106
- end
107
- end.parse!
108
-
109
- if ARGV.empty?
110
- puts 'Please provide a string to animate as an argument.'
111
- exit 1
112
- end
113
-
114
- if options[:command]
115
- captured_output = nil
116
- res = RubyProgress::Ripple.progress(ARGV.join(' '), options) do
117
- captured_output = `#{options[:command]} 2>&1`
118
- end
119
-
120
- res = $?.success?
121
-
122
- puts captured_output if options[:output] == :stdout
123
- if options[:success_message]
124
- message = if res
125
- options[:success_message]
126
- else
127
- options[:fail_message] || options[:success_message]
128
- end
129
- RubyProgress::Ripple.complete(ARGV.join(' '), message, options[:complete_checkmark], res)
130
- end
131
- exit res ? 0 : 1
132
- end
133
-
134
- rippler = RubyProgress::Ripple.new(ARGV.join(' '), {
135
- speed: options[:speed],
136
- format: options[:format],
137
- rainbow: options[:rainbow],
138
- inverse: options[:inverse]
139
- })
140
- RubyProgress::Utils.hide_cursor
141
- rippler.advance while true
142
- RubyProgress::Utils.show_cursor
143
- RubyProgress::Ripple.complete(ARGV.join(' '), options[:success_message], options[:complete_checkmark], true)
144
- end
4
+ # Thin shim to delegate to the unified prg CLI
5
+ prg = File.expand_path(File.join(__dir__, 'prg'))
6
+ if File.executable?(prg)
7
+ exec prg, 'ripple', *ARGV
8
+ else
9
+ exec 'prg', 'ripple', *ARGV
145
10
  end
146
-
147
- RippleCLI.run if __FILE__ == $PROGRAM_NAME
data/bin/twirl ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Thin shim to delegate to the unified prg CLI
5
+ prg = File.expand_path(File.join(__dir__, 'prg'))
6
+ if File.executable?(prg)
7
+ exec prg, 'twirl', *ARGV
8
+ else
9
+ exec 'prg', 'twirl', *ARGV
10
+ end
data/bin/worm CHANGED
@@ -1,80 +1,10 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require_relative '../lib/ruby-progress'
5
- require 'optparse'
6
-
7
- # Worm CLI implementation
8
- module WormCLI
9
- def self.run
10
- options = parse_cli_options
11
- progress = RubyProgress::Worm.new(options)
12
-
13
- if options[:command]
14
- progress.run_with_command
15
- else
16
- # Run indefinitely like ripple does when no command is specified
17
- progress.run_indefinitely
18
- end
19
- end
20
-
21
- def self.parse_cli_options
22
- options = {}
23
-
24
- OptionParser.new do |opts|
25
- opts.banner = "Usage: #{$PROGRAM_NAME} [options]"
26
- opts.separator ''
27
- opts.separator 'Options:'
28
-
29
- opts.on('-s', '--speed SPEED', 'Animation speed (1-10, fast, medium, slow, or f/m/s)') do |speed|
30
- options[:speed] = speed
31
- end
32
-
33
- opts.on('-l', '--length LENGTH', Integer, 'Number of dots to display') do |length|
34
- options[:length] = length
35
- end
36
-
37
- opts.on('-m', '--message MESSAGE', 'Message to display before dots') do |message|
38
- options[:message] = message
39
- end
40
-
41
- opts.on('--style STYLE', 'Animation style (blocks, geometric, circles, or b/g/c)') do |style|
42
- options[:style] = style
43
- end
44
-
45
- opts.on('-c', '--command COMMAND', 'Command to run (optional - runs indefinitely without command)') do |command|
46
- options[:command] = command
47
- end
48
-
49
- opts.on('--success TEXT', 'Text to display on successful completion') do |text|
50
- options[:success] = text
51
- end
52
-
53
- opts.on('--error TEXT', 'Text to display on error') do |text|
54
- options[:error] = text
55
- end
56
-
57
- opts.on('--checkmark', 'Show checkmarks (✅/🛑) in completion messages') do
58
- options[:checkmark] = true
59
- end
60
-
61
- opts.on('--stdout', 'Output captured command result to STDOUT') do
62
- options[:stdout] = true
63
- end
64
-
65
- opts.on('-h', '--help', 'Show this help message') do
66
- puts opts
67
- exit
68
- end
69
-
70
- opts.on('-v', '--version', 'Display the version') do
71
- puts "Worm version #{RubyProgress::VERSION}"
72
- exit
73
- end
74
- end.parse!
75
-
76
- options
77
- end
4
+ # Thin shim to delegate to the unified prg CLI
5
+ prg = File.expand_path(File.join(__dir__, 'prg'))
6
+ if File.executable?(prg)
7
+ exec prg, 'worm', *ARGV
8
+ else
9
+ exec 'prg', 'worm', *ARGV
78
10
  end
79
-
80
- WormCLI.run if __FILE__ == $PROGRAM_NAME
@@ -0,0 +1,67 @@
1
+ #!/bin/bash
2
+
3
+ # Example bash script using ruby-progress daemon mode
4
+ # This demonstrates how to use background progress indicators in shell scripts
5
+
6
+ set -e
7
+
8
+ echo "=== Bash Script with Background Progress Demo ==="
9
+ echo
10
+
11
+ # Configuration
12
+ PID_FILE="/tmp/bash_progress.pid"
13
+ PRG_BIN="$(dirname "$0")/../bin/prg"
14
+
15
+ # Clean up function
16
+ cleanup() {
17
+ if [ -f "$PID_FILE" ]; then
18
+ echo "Cleaning up progress indicator..."
19
+ "$PRG_BIN" worm --stop-pid "$PID_FILE" 2>/dev/null || true
20
+ fi
21
+ }
22
+
23
+ # Set up cleanup on exit
24
+ trap cleanup EXIT
25
+
26
+ echo "1. Starting background progress indicator..."
27
+ "$PRG_BIN" worm --daemon --pid-file "$PID_FILE" \
28
+ --message "Processing multiple tasks..." \
29
+ --success "All tasks completed successfully!" \
30
+ --checkmark &
31
+
32
+ # Give daemon a moment to start
33
+ sleep 0.5
34
+
35
+ echo "2. Executing tasks with background progress..."
36
+
37
+ echo " - Downloading files..."
38
+ sleep 2
39
+
40
+ echo " - Processing data..."
41
+ sleep 2
42
+
43
+ echo " - Generating reports..."
44
+ sleep 1
45
+
46
+ echo " - Uploading results..."
47
+ sleep 1
48
+
49
+ echo "3. All tasks complete, stopping progress indicator..."
50
+ cleanup
51
+
52
+ echo
53
+ echo "=== Script Complete ==="
54
+
55
+ # Usage in your own scripts:
56
+ #
57
+ # # Start progress in background
58
+ # prg worm --daemon --pid-file /tmp/my_progress.pid \
59
+ # --message "Working..." --success "Done!" --checkmark &
60
+ #
61
+ # # Do your actual work
62
+ # long_running_command_1
63
+ # long_running_command_2
64
+ # long_running_command_3
65
+ #
66
+ # # Stop progress indicator
67
+ # prg worm --stop-pid /tmp/my_progress.pid
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Daemon mode test script for ruby-progress
5
+
6
+ require 'fileutils'
7
+
8
+ puts '=== Ruby Progress Daemon Mode Demo ==='
9
+ puts
10
+
11
+ # Test directories
12
+ test_dir = '/tmp/progress_test'
13
+ FileUtils.mkdir_p(test_dir)
14
+ pid_file = "#{test_dir}/progress.pid"
15
+
16
+ puts "Test directory: #{test_dir}"
17
+ puts "PID file: #{pid_file}"
18
+ puts
19
+
20
+ # Clean up any existing PID file
21
+ File.delete(pid_file) if File.exist?(pid_file)
22
+
23
+ puts '1. Starting worm progress indicator in daemon mode...'
24
+ bin_path = File.join(File.dirname(__dir__), 'bin', 'prg')
25
+ system("#{bin_path} worm --daemon --pid-file #{pid_file} --message 'Processing in background...' --success 'All tasks completed!' --checkmark &")
26
+
27
+ # Wait a moment for daemon to start
28
+ sleep 1
29
+
30
+ if File.exist?(pid_file)
31
+ pid = File.read(pid_file).strip
32
+ puts " ✅ Daemon started with PID: #{pid}"
33
+ else
34
+ puts ' ❌ Failed to start daemon'
35
+ exit 1
36
+ end
37
+
38
+ puts
39
+ puts '2. Simulating background work (5 seconds)...'
40
+ puts ' - Task 1: Processing data...'
41
+ sleep 2
42
+ puts ' - Task 2: Running analysis...'
43
+ sleep 2
44
+ puts ' - Task 3: Generating report...'
45
+ sleep 1
46
+
47
+ puts
48
+ puts '3. Stopping daemon...'
49
+ system("#{bin_path} worm --stop-pid #{pid_file}")
50
+
51
+ puts
52
+ puts '4. Cleaning up...'
53
+ FileUtils.rm_rf(test_dir)
54
+
55
+ puts
56
+ puts '=== Demo Complete ==='
57
+ puts
58
+ puts 'Usage in bash scripts:'
59
+ puts ' # Start daemon'
60
+ puts " prg worm --daemon --pid-file /tmp/my_progress.pid --message 'Working...' --success 'Done!' &"
61
+ puts ' '
62
+ puts ' # Do your work'
63
+ puts ' my_long_running_task_1'
64
+ puts ' my_long_running_task_2'
65
+ puts ' '
66
+ puts ' # Stop daemon'
67
+ puts ' prg worm --stop-pid /tmp/my_progress.pid'
@@ -4,49 +4,50 @@
4
4
  # Demo of RubyProgress::Utils universal utilities
5
5
  require_relative '../lib/ruby-progress'
6
6
 
7
- puts "=== RubyProgress::Utils Demo ==="
7
+ puts '=== RubyProgress::Utils Demo ==='
8
8
  puts
9
9
 
10
10
  # Test cursor control
11
- puts "Testing cursor control..."
12
- print "Hiding cursor for 2 seconds..."
11
+ puts 'Testing cursor control...'
12
+ print 'Hiding cursor for 2 seconds...'
13
13
  RubyProgress::Utils.hide_cursor
14
14
  sleep 2
15
- print " showing cursor again."
15
+ print ' showing cursor again.'
16
16
  RubyProgress::Utils.show_cursor
17
17
  puts
18
18
  puts
19
19
 
20
20
  # Test line clearing
21
- puts "Testing line clearing..."
22
- print "This line will be cleared..."
21
+ puts 'Testing line clearing...'
22
+ print 'This line will be cleared...'
23
23
  sleep 1
24
24
  RubyProgress::Utils.clear_line
25
- print "New content on the same line!"
25
+ print 'New content on the same line!'
26
26
  puts
27
27
  puts
28
28
 
29
29
  # Test completion messages
30
- puts "Testing completion messages..."
30
+ puts 'Testing completion messages...'
31
31
 
32
- RubyProgress::Utils.display_completion("Basic success message", success: true)
33
- RubyProgress::Utils.display_completion("Basic failure message", success: false)
32
+ RubyProgress::Utils.display_completion('Basic success message', success: true)
33
+ RubyProgress::Utils.display_completion('Basic failure message', success: false)
34
34
 
35
35
  puts
36
- puts "With checkmarks:"
37
- RubyProgress::Utils.display_completion("Success with checkmark", success: true, show_checkmark: true)
38
- RubyProgress::Utils.display_completion("Failure with checkmark", success: false, show_checkmark: true)
36
+ puts 'With checkmarks:'
37
+ RubyProgress::Utils.display_completion('Success with checkmark', success: true, show_checkmark: true)
38
+ RubyProgress::Utils.display_completion('Failure with checkmark', success: false, show_checkmark: true)
39
39
 
40
40
  puts
41
- puts "Different output streams:"
42
- RubyProgress::Utils.display_completion("To STDOUT", success: true, show_checkmark: true, output_stream: :stdout)
43
- RubyProgress::Utils.display_completion("To STDERR", success: false, show_checkmark: true, output_stream: :stderr)
41
+ puts 'Different output streams:'
42
+ RubyProgress::Utils.display_completion('To STDOUT', success: true, show_checkmark: true, output_stream: :stdout)
43
+ RubyProgress::Utils.display_completion('To STDERR', success: false, show_checkmark: true, output_stream: :stderr)
44
44
 
45
45
  puts
46
- puts "Complete with clear:"
47
- print "Content to be cleared and replaced..."
46
+ puts 'Complete with clear:'
47
+ print 'Content to be cleared and replaced...'
48
48
  sleep 1
49
- RubyProgress::Utils.complete_with_clear("Cleared and completed!", success: true, show_checkmark: true, output_stream: :stdout)
49
+ RubyProgress::Utils.complete_with_clear('Cleared and completed!', success: true, show_checkmark: true,
50
+ output_stream: :stdout)
50
51
 
51
52
  puts
52
- puts "=== Demo Complete ==="
53
+ puts '=== Demo Complete ==='
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module RubyProgress
6
+ module Daemon
7
+ module_function
8
+
9
+ def default_pid_file
10
+ '/tmp/ruby-progress/progress.pid'
11
+ end
12
+
13
+ def control_message_file(pid_file)
14
+ "#{pid_file}.msg"
15
+ end
16
+
17
+ def show_status(pid_file)
18
+ if File.exist?(pid_file)
19
+ pid = File.read(pid_file).strip
20
+ running = system("ps -p #{pid} > /dev/null")
21
+ puts(running ? "Daemon running (pid #{pid})" : 'PID file present but process not running')
22
+ exit(running ? 0 : 1)
23
+ else
24
+ puts 'Daemon not running'
25
+ exit 1
26
+ end
27
+ end
28
+
29
+ def stop_daemon_by_pid_file(pid_file, message: nil, checkmark: false, error: false)
30
+ unless File.exist?(pid_file)
31
+ puts "PID file #{pid_file} not found"
32
+ exit 1
33
+ end
34
+
35
+ pid = File.read(pid_file).strip.to_i
36
+
37
+ # Write control message file if provided
38
+ if message || error
39
+ cmf = control_message_file(pid_file)
40
+ payload = { checkmark: checkmark, success: !error }
41
+ payload[:message] = message if message
42
+ File.write(cmf, payload.to_json)
43
+ end
44
+
45
+ begin
46
+ Process.kill('USR1', pid)
47
+ puts "Stop signal sent to process #{pid}"
48
+ sleep 0.5
49
+ File.delete(pid_file) if File.exist?(pid_file)
50
+ rescue Errno::ESRCH
51
+ puts "Process #{pid} not found (may have already stopped)"
52
+ File.delete(pid_file) if File.exist?(pid_file)
53
+ exit 1
54
+ rescue Errno::EPERM
55
+ puts "Permission denied sending signal to process #{pid}"
56
+ exit 1
57
+ end
58
+ end
59
+ end
60
+ end
@@ -35,7 +35,7 @@ module RubyProgress
35
35
  when :stdout
36
36
  puts formatted_message
37
37
  when :stderr
38
- $stderr.puts formatted_message
38
+ warn formatted_message
39
39
  when :warn
40
40
  warn "\e[2K#{formatted_message}"
41
41
  else
@@ -50,4 +50,4 @@ module RubyProgress
50
50
  display_completion(message, success: success, show_checkmark: show_checkmark, output_stream: output_stream)
51
51
  end
52
52
  end
53
- end
53
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyProgress
4
- VERSION = '1.0.1'
4
+ VERSION = '1.1.2'
5
5
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'optparse'
4
4
  require 'open3'
5
+ require 'json'
5
6
 
6
7
  module RubyProgress
7
8
  # Animated progress indicator with ripple effect using Unicode combining characters
@@ -11,7 +12,7 @@ module RubyProgress
11
12
  'circles' => {
12
13
  baseline: '·', # middle dot
13
14
  midline: '●', # black circle
14
- peak: '⬤' # large circle
15
+ peak: '⬤' # large circle
15
16
  },
16
17
  'blocks' => {
17
18
  baseline: '▁', # lower eighth block
@@ -70,7 +71,7 @@ module RubyProgress
70
71
  result = yield
71
72
  @running = false
72
73
  animation_thread.join
73
- display_completion_message(@success_text || 'Done!', true)
74
+ display_completion_message(@success_text, true)
74
75
  result
75
76
  else
76
77
  animation_thread.join
@@ -113,7 +114,6 @@ module RubyProgress
113
114
 
114
115
  # Output to stdout if --stdout flag is set
115
116
  puts stdout_content if @output_stdout && stdout_content
116
-
117
117
  rescue StandardError => e
118
118
  # animate method handles error display, just exit with proper code
119
119
  exit exit_code.nonzero? || 1
@@ -148,6 +148,78 @@ module RubyProgress
148
148
  @running = false
149
149
  end
150
150
 
151
+ def run_daemon_mode(success_message: nil, show_checkmark: false, control_message_file: nil)
152
+ @running = true
153
+ stop_requested = false
154
+
155
+ # Set up signal handlers
156
+ original_int_handler = Signal.trap('INT') { stop_requested = true }
157
+ Signal.trap('USR1') { stop_requested = true }
158
+ Signal.trap('TERM') { stop_requested = true }
159
+ Signal.trap('HUP') { stop_requested = true }
160
+
161
+ RubyProgress::Utils.hide_cursor
162
+
163
+ begin
164
+ animation_loop_step while @running && !stop_requested
165
+ ensure
166
+ RubyProgress::Utils.clear_line
167
+ RubyProgress::Utils.show_cursor
168
+
169
+ # Display stop-time completion message, preferring control file if provided
170
+ final_message = success_message
171
+ final_checkmark = show_checkmark
172
+ final_success = true
173
+ if control_message_file && File.exist?(control_message_file)
174
+ begin
175
+ data = JSON.parse(File.read(control_message_file))
176
+ final_message = data['message'] if data['message']
177
+ final_checkmark = !!data['checkmark'] if data.key?('checkmark')
178
+ final_success = !!data['success'] if data.key?('success')
179
+ rescue StandardError
180
+ # ignore parse errors, fallback to provided message
181
+ ensure
182
+ begin
183
+ File.delete(control_message_file)
184
+ rescue StandardError
185
+ nil
186
+ end
187
+ end
188
+ end
189
+
190
+ if final_message
191
+ RubyProgress::Utils.display_completion(
192
+ final_message,
193
+ success: final_success,
194
+ show_checkmark: final_checkmark,
195
+ output_stream: :stdout
196
+ )
197
+ end
198
+
199
+ Signal.trap('INT', original_int_handler) if original_int_handler
200
+ end
201
+ end
202
+
203
+ def animation_loop_step
204
+ return unless @running
205
+
206
+ @position ||= 0
207
+ @direction ||= 1
208
+
209
+ print "\r#{@message}#{generate_dots(@position, @direction)}"
210
+ $stdout.flush
211
+
212
+ sleep @speed
213
+
214
+ # Update position and direction
215
+ @position += @direction
216
+ if @position >= @length - 1
217
+ @direction = -1
218
+ elsif @position <= 0
219
+ @direction = 1
220
+ end
221
+ end
222
+
151
223
  private
152
224
 
153
225
  def display_completion_message(message, success)
@@ -237,8 +309,8 @@ module RubyProgress
237
309
  # When moving right, midline appears to the left of peak
238
310
  if direction == -1 # moving left
239
311
  dots[i] = @style[:midline] if i > ripple_position
240
- else # moving right
241
- dots[i] = @style[:midline] if i < ripple_position
312
+ elsif i < ripple_position # moving right
313
+ dots[i] = @style[:midline]
242
314
  end
243
315
  else
244
316
  dots[i] = @style[:baseline]
@@ -250,4 +322,4 @@ module RubyProgress
250
322
 
251
323
  # Terminal utilities moved to RubyProgress::Utils
252
324
  end
253
- end
325
+ end
data/lib/ruby-progress.rb CHANGED
@@ -4,6 +4,7 @@ require_relative 'ruby-progress/version'
4
4
  require_relative 'ruby-progress/utils'
5
5
  require_relative 'ruby-progress/ripple'
6
6
  require_relative 'ruby-progress/worm'
7
+ require_relative 'ruby-progress/daemon'
7
8
 
8
9
  module RubyProgress
9
10
  # Main module for Ruby Progress indicators