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.
data/blog-post.md ADDED
@@ -0,0 +1,174 @@
1
+ ---
2
+ title: "More CLI Progress Indicators: The ruby-progress Gem"
3
+ date: 2025-10-10
4
+ categories: [Ruby, CLI, Development Tools]
5
+ tags: [ruby, cli, progress, animation, terminal]
6
+ ---
7
+
8
+ [ripple]: https://brettterpstra.com/2025/06/30/ripple-an-indeterminate-progress-indicator/
9
+
10
+ # More CLI Progress Indicators: The ruby-progress Gem
11
+
12
+ The `ruby-progress` gem expands my previous work on [Ripple], adding 2 more indicator types and a new daemon mode to make it more useful in non-Ruby shell scripts.
13
+
14
+ ## What is ruby-progress?
15
+
16
+ Ruby-progress provides animated progress indicators for command-line applications. Unlike traditional progress bars showing completion percentage, this gem focuses on continuous animations (indeterminate) that indicate activity - perfect for tasks with unpredictable completion times.
17
+
18
+ Three animation styles are available:
19
+
20
+ - **Ripple**: Wave-like effect across text
21
+ - **Worm**: Moving dot pattern that crawls across the screen
22
+ - **Twirl**: Classic spinning indicators with various symbols
23
+
24
+ ## Quick Start
25
+
26
+ ```bash
27
+ # Install
28
+ gem install ruby-progress
29
+
30
+ # Basic usage
31
+ ripple "Processing files..."
32
+ worm --message "Loading..." --speed fast
33
+ twirl --spinner dots
34
+ ```
35
+
36
+ ## Ruby Integration
37
+
38
+ ```ruby
39
+ require 'ruby-progress'
40
+
41
+ # Block syntax with automatic cleanup
42
+ RubyProgress::Ripple.progress("Processing files...") do
43
+ process_files
44
+ upload_results
45
+ end
46
+
47
+ # Manual control
48
+ worm = RubyProgress::Worm.new(
49
+ message: "Custom task",
50
+ style: 'blocks',
51
+ speed: 'medium'
52
+ )
53
+
54
+ worm.animate { heavy_computation }
55
+ ```
56
+
57
+ ## Daemon Mode - The Killer Feature
58
+
59
+ Run progress indicators in the background while your scripts execute:
60
+
61
+ ```bash
62
+ # Start background indicator
63
+ worm --daemon --pid-file /tmp/progress.pid --message "Deploying..."
64
+
65
+ # Run your actual work
66
+ ./deploy.sh
67
+ kubectl apply -f manifests/
68
+
69
+ # Stop with success message
70
+ worm --stop /tmp/progress.pid --message "Deploy complete!" --checkmark
71
+ ```
72
+
73
+ This is incredibly useful for complex deployment scripts where you want continuous visual feedback without interrupting the main process.
74
+
75
+ There's a unified binary called `prg` that takes the three types as subcommands, e.g. `prg twirl --checkmark`. You can use any of them either way.
76
+
77
+ ## Advanced Features
78
+
79
+ ### Error Handling
80
+ ```ruby
81
+ RubyProgress::Ripple.progress("Risky operation") do
82
+ might_fail_operation
83
+ rescue StandardError => e
84
+ # Automatically stops and shows error state
85
+ puts "Failed: #{e.message}"
86
+ end
87
+ ```
88
+
89
+ ### Command Integration
90
+ ```ruby
91
+ # Execute shell commands with progress
92
+ RubyProgress::Worm.new(
93
+ command: "pg_dump mydb > backup.sql",
94
+ success: "Backup complete!",
95
+ error: "Backup failed"
96
+ ).run_with_command
97
+ ```
98
+
99
+ ### Visual Customization
100
+ ```ruby
101
+ # Rainbow colors and custom styling
102
+ RubyProgress::Ripple.progress("Colorful task",
103
+ rainbow: true,
104
+ speed: :fast,
105
+ format: :forward_only
106
+ ) do
107
+ process_with_style
108
+ end
109
+ ```
110
+
111
+ ## Real-World Examples
112
+
113
+ ### Deployment Script
114
+
115
+ ```bash
116
+ #!/bin/bash
117
+ worm --daemon-as deploy --message "Deploying..." &
118
+
119
+ git push production main
120
+ kubectl rollout status deployment/app
121
+
122
+ worm --stop-id deploy --stop-success "Success!" --checkmark
123
+ ```
124
+
125
+ ### Data Processing
126
+ ```ruby
127
+ def import_large_csv(file)
128
+ RubyProgress::Ripple.progress("Importing #{File.basename(file)}...") do
129
+ CSV.foreach(file, headers: true) { |row| User.create!(row.to_h) }
130
+ end
131
+ end
132
+ ```
133
+
134
+ ### Rake Tasks
135
+ ```ruby
136
+ task :backup do
137
+ RubyProgress::Worm.new(
138
+ command: "tar -czf backup.tar.gz app/",
139
+ success: "Backup created successfully!"
140
+ ).run_with_command
141
+ end
142
+ ```
143
+
144
+ ## Why ruby-progress?
145
+
146
+ - **Simple API**: Works in Ruby code and shell scripts
147
+ - **Visual Appeal**: Engaging animations beyond basic spinners
148
+ - **Unique Daemon Mode**: Background progress indicators
149
+ - **Production Ready**: 84.55% test coverage, 113 test examples, zero failures
150
+ - **Cross-Platform**: Linux, macOS, Windows support
151
+ - **Reliable**: Comprehensive error handling and edge case coverage
152
+
153
+ ## Installation & First Steps
154
+
155
+ 1. **Install**: `gem install ruby-progress`
156
+ 2. **Test CLI**: `ripple "Hello World!"`
157
+ 3. **Try in Ruby**:
158
+ ```ruby
159
+ require 'ruby-progress'
160
+ RubyProgress::Ripple.progress("Testing...") { sleep 2 }
161
+ ```
162
+
163
+ ## Conclusion
164
+
165
+ Ruby-progress transforms boring CLI applications into engaging user experiences. Whether building deployment scripts, data processing tools, or utility commands, it provides the visual feedback users expect from modern command-line tools.
166
+
167
+ The daemon mode alone makes it worth trying - no other progress library offers this level of flexibility for complex workflows.
168
+
169
+ **Links:**
170
+ - [GitHub](https://github.com/ttscoff/ruby-progress)
171
+ - [RubyGems](https://rubygems.org/gems/ruby-progress)
172
+ - [Latest Release](https://github.com/ttscoff/ruby-progress/releases/latest)
173
+
174
+ *Make your CLI tools feel alive with ruby-progress!*
@@ -18,7 +18,7 @@ puts "PID file: #{pid_file}"
18
18
  puts
19
19
 
20
20
  # Clean up any existing PID file
21
- File.delete(pid_file) if File.exist?(pid_file)
21
+ FileUtils.rm_f(pid_file)
22
22
 
23
23
  puts '1. Starting worm progress indicator in daemon mode...'
24
24
  bin_path = File.join(File.dirname(__dir__), 'bin', 'prg')
@@ -0,0 +1,55 @@
1
+ # Experimental terminal line protection for daemon mode
2
+ # This demonstrates potential solutions for the daemon output interruption problem
3
+
4
+ module RubyProgress
5
+ module SmartTerminal
6
+ # Save current cursor position
7
+ def self.save_cursor_position
8
+ $stderr.print "\e[s" # Save cursor position
9
+ $stderr.flush
10
+ end
11
+
12
+ # Restore cursor to saved position
13
+ def self.restore_cursor_position
14
+ $stderr.print "\e[u" # Restore cursor position
15
+ $stderr.flush
16
+ end
17
+
18
+ # Get current cursor position (requires terminal interaction)
19
+ def self.get_cursor_position
20
+ # This is complex and requires reading from stdin
21
+ # which may not work reliably in daemon mode
22
+ $stderr.print "\e[6n" # Request cursor position
23
+ $stderr.flush
24
+ # Would need to read response: "\e[{row};{col}R"
25
+ # But this requires terminal input capability
26
+ end
27
+
28
+ # Alternative: Use absolute positioning
29
+ def self.position_cursor_absolute(row, col)
30
+ $stderr.print "\e[#{row};#{col}H"
31
+ $stderr.flush
32
+ end
33
+
34
+ # Enhanced line clearing that works from any position
35
+ def self.clear_current_line_and_reposition
36
+ $stderr.print "\r" # Move to start of line
37
+ $stderr.print "\e[2K" # Clear entire line
38
+ $stderr.print "\e[1A" # Move up one line (in case we're on a new line)
39
+ $stderr.print "\r" # Move to start again
40
+ $stderr.print "\e[2K" # Clear that line too
41
+ $stderr.flush
42
+ end
43
+
44
+ # Experimental: Try to "reclaim" the animation line
45
+ def self.reclaim_animation_line(animation_text)
46
+ # Strategy: Clear multiple lines and reposition
47
+ $stderr.print "\r" # Go to start of current line
48
+ $stderr.print "\e[2K" # Clear current line
49
+ $stderr.print "\e[1A" # Move up one line
50
+ $stderr.print "\e[2K" # Clear that line
51
+ $stderr.print animation_text # Print our animation
52
+ $stderr.flush
53
+ end
54
+ end
55
+ end
@@ -45,10 +45,10 @@ module RubyProgress
45
45
  begin
46
46
  Process.kill('USR1', pid)
47
47
  sleep 0.5
48
- File.delete(pid_file) if File.exist?(pid_file)
48
+ FileUtils.rm_f(pid_file)
49
49
  rescue Errno::ESRCH
50
50
  puts "Process #{pid} not found (may have already stopped)"
51
- File.delete(pid_file) if File.exist?(pid_file)
51
+ FileUtils.rm_f(pid_file)
52
52
  exit 1
53
53
  rescue Errno::EPERM
54
54
  puts "Permission denied sending signal to process #{pid}"
@@ -31,57 +31,27 @@ module RubyProgress
31
31
  INDICATORS = {
32
32
  arc: %w[◜ ◠ ◝ ◞ ◡ ◟],
33
33
  arrow: %w[← ↖ ↑ ↗ → ↘ ↓ ↙],
34
- arrow_pulse: [
35
- '▹▹▹▹▹',
36
- '▸▹▹▹▹',
37
- '▹▸▹▹▹',
38
- '▹▹▸▹▹',
39
- '▹▹▹▸▹',
40
- '▹▹▹▹▸'
41
- ],
42
- balloon: %w[. o O ° O o .],
43
- block_1: %w[▖▖▖ ▘▖▖ ▖▘▖ ▖▖▘],
44
34
  block_2: %w[▌ ▀ ▐ ▄],
45
- bounce: [
46
- '[ ]',
47
- '[= ]',
48
- '[== ]',
49
- '[=== ]',
50
- '[====]'
51
- ],
52
- circle: %w[○○○ ●○○ ○●○ ○○●],
35
+ block_1: %w[▖▖▖ ▘▖▖ ▖▘▖ ▖▖▘],
36
+ bounce: %w[⠁ ⠂ ⠄ ⡀ ⢀ ⠠ ⠐ ⠈],
53
37
  classic: ['|', '/', '—', '\\'],
54
38
  dots: %w[⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏],
55
39
  dots_2: %w[⣾ ⣽ ⣻ ⢿ ⡿ ⣟ ⣯ ⣷],
56
40
  dots_3: %w[⠋ ⠙ ⠚ ⠞ ⠖ ⠦ ⠴ ⠲ ⠳ ⠓],
57
- dots_4: %w[ ⠠ ⠰ ⠸ ⠙ ⠋ ⠇ ⠆],
58
- dots_5: %w[⠋ ⠙ ⠚ ⠒ ⠂ ⠂ ⠒ ⠲ ⠴ ⠦ ⠖ ⠒ ⠐ ⠐ ⠒ ⠓ ⠋],
59
- dots_6: %w[⠁ ⠉ ⠙ ⠚ ⠒ ⠂ ⠂ ⠒ ⠲ ⠴ ⠤ ⠄ ⠄ ⠤ ⠴ ⠲ ⠒ ⠂ ⠂ ⠒ ⠚ ⠙ ⠉ ⠁],
60
- dots_7: %w[⠈ ⠉ ⠋ ⠓ ⠒ ⠐ ⠐ ⠒ ⠖ ⠦ ⠤ ⠠ ⠠ ⠤ ⠦ ⠖ ⠒ ⠐ ⠐ ⠒ ⠓ ⠋ ⠉ ⠈],
61
- dots_8: %w[⠁ ⠁ ⠉ ⠙ ⠚ ⠒ ⠂ ⠂ ⠒ ⠲ ⠴ ⠤ ⠄ ⠄ ⠤ ⠠ ⠠ ⠤ ⠦ ⠖ ⠒ ⠐ ⠐ ⠒ ⠓ ⠋ ⠉ ⠈ ⠈],
62
- dots_9: %w[⢹ ⢺ ⢼ ⣸ ⣇ ⡧ ⡗ ⡏],
63
- dots_10: %w[⢄ ⢂ ⢁ ⡁ ⡈ ⡐ ⡠],
64
- dots_11: %w[⠁ ⠂ ⠄ ⡀ ⢀ ⠠ ⠐ ⠈],
41
+ dots_4: %w[ ],
65
42
  ellipsis: ['. ', '.. ', '... ', '....'],
66
- lighthouse: ['∙∙∙', '●∙∙', '∙●∙', '∙∙●'],
67
- o: %w[Ooo oOo ooO],
68
43
  pipe: %w[┤ ┘ ┴ └ ├ ┌ ┬ ┐],
69
44
  pulse: %w[⎺ ⎻ ⎼ ⎽ ⎼ ⎻],
70
45
  pulse_2: %w[▁ ▃ ▅ ▆ ▇ █ ▇ ▆ ▅ ▃],
71
46
  pulse_3: %w[▉ ▊ ▋ ▌ ▍ ▎ ▏ ▎ ▍ ▌ ▋ ▊ ▉],
72
47
  pulse_4: %w[- = ≡ = -],
73
- push: [
74
- '[> ]',
75
- '[=> ]',
76
- '[==> ]',
77
- '[===> ]',
78
- '[====>]'
79
- ],
48
+ o: %w[Ooo oOo ooO],
80
49
  spin: %w[◴ ◷ ◶ ◵],
81
50
  spin_2: %w[◐ ◓ ◑ ◒],
82
51
  spin_3: %w[◰ ◳ ◲ ◱],
83
52
  toggle: %w[■ □ ▪ ▫],
84
- triangle: %w[◢ ◣ ◤ ◥]
53
+ triangle: %w[◢ ◣ ◤ ◥],
54
+ twinkle: %w[⢄ ⢂ ⢁ ⡁ ⡈ ⡐ ⡠]
85
55
  }.freeze
86
56
 
87
57
  # String extensions for color support
@@ -93,7 +63,7 @@ module RubyProgress
93
63
  end
94
64
 
95
65
  def rainbow(index = 0)
96
- chars = split('')
66
+ chars = self.chars
97
67
  colored_chars = chars.map.with_index do |char, idx|
98
68
  color = COLORS.values[(idx + index) % COLORS.size]
99
69
  "#{color}#{char}#{COLORS['reset']}"
@@ -104,7 +74,7 @@ module RubyProgress
104
74
  def normalize_type
105
75
  spinner_type = :classic
106
76
  INDICATORS.each do |spinner, _v|
107
- spinner_type = spinner if spinner =~ /^#{split('').join('.*?')}/i
77
+ spinner_type = spinner if spinner =~ /^#{chars.join('.*?')}/i
108
78
  end
109
79
  spinner_type
110
80
  end
@@ -137,7 +107,7 @@ module RubyProgress
137
107
  end
138
108
 
139
109
  def printout
140
- letters = @string.dup.split('')
110
+ letters = @string.dup.chars
141
111
  i = @index
142
112
  if @spinner
143
113
  case @spinner_position
@@ -168,7 +138,8 @@ module RubyProgress
168
138
  char = @rainbow ? char.rainbow(i) : char.extend(StringExtensions).light_white
169
139
  post = letters.slice!(0, letters.length).join.extend(StringExtensions).dark_white
170
140
  end
171
- $stderr.print "\e[2K#{pre}#{char}#{post}\r"
141
+ $stderr.print "\r\e[2K#{pre}#{char}#{post}"
142
+ $stderr.flush
172
143
  end
173
144
 
174
145
  # Hide or show the cursor (delegated to Utils)
@@ -12,8 +12,20 @@ module RubyProgress
12
12
  $stderr.print "\e[?25h"
13
13
  end
14
14
 
15
- def self.clear_line
16
- print "\r\e[K"
15
+ def self.clear_line(output_stream = :stderr)
16
+ case output_stream
17
+ when :stdout
18
+ $stdout.print "\r\e[K"
19
+ else
20
+ $stderr.print "\r\e[K"
21
+ end
22
+ end
23
+
24
+ # Enhanced line clearing for daemon mode that handles output interruption
25
+ def self.clear_line_aggressive
26
+ $stderr.print "\r\e[2K" # Clear entire current line
27
+ $stderr.print "\e[1A\e[2K" # Move up one line and clear it too
28
+ $stderr.print "\r" # Return to start of line
17
29
  end
18
30
 
19
31
  # Universal completion message display
@@ -46,7 +58,7 @@ module RubyProgress
46
58
  # Clear current line and display completion message
47
59
  # Convenience method that combines line clearing with message display
48
60
  def self.complete_with_clear(message, success: true, show_checkmark: false, output_stream: :warn)
49
- clear_line if output_stream != :warn # warn already includes clear in display_completion
61
+ clear_line(output_stream) if output_stream != :warn # warn already includes clear in display_completion
50
62
  display_completion(message, success: success, show_checkmark: show_checkmark, output_stream: output_stream)
51
63
  end
52
64
  end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyProgress
4
- VERSION = '1.1.4'
4
+ VERSION = '1.1.8'
5
+ WORM_VERSION = '1.0.3'
6
+ TWIRL_VERSION = '1.0.1'
7
+ RIPPLE_VERSION = '1.0.5'
5
8
  end
@@ -23,6 +23,26 @@ module RubyProgress
23
23
  baseline: '▪', # small black square
24
24
  midline: '▫', # small white square
25
25
  peak: '■' # large black square
26
+ },
27
+ 'cirlces_small' => {
28
+ baseline: '∙',
29
+ midline: '∙',
30
+ peak: '●'
31
+ },
32
+ 'arrow' => {
33
+ baseline: '▹',
34
+ midline: '▸',
35
+ peak: '▶'
36
+ },
37
+ 'balloon' => {
38
+ baseline: '.',
39
+ midline: 'o',
40
+ peak: '°'
41
+ },
42
+ 'circle_open' => {
43
+ baseline: '○',
44
+ midline: '●',
45
+ peak: '○'
26
46
  }
27
47
  }.freeze
28
48
 
@@ -57,7 +77,6 @@ module RubyProgress
57
77
  @running = false
58
78
  RubyProgress::Utils.clear_line
59
79
  RubyProgress::Utils.show_cursor
60
- puts "\nInterrupted!"
61
80
  exit 130
62
81
  end
63
82
 
@@ -118,7 +137,6 @@ module RubyProgress
118
137
  # animate method handles error display, just exit with proper code
119
138
  exit exit_code.nonzero? || 1
120
139
  rescue Interrupt
121
- puts "\nInterrupted!"
122
140
  exit 130
123
141
  end
124
142
  end
@@ -129,7 +147,6 @@ module RubyProgress
129
147
  @running = false
130
148
  RubyProgress::Utils.clear_line
131
149
  RubyProgress::Utils.show_cursor
132
- puts "\nInterrupted!"
133
150
  exit 130
134
151
  end
135
152
 
@@ -161,7 +178,7 @@ module RubyProgress
161
178
  RubyProgress::Utils.hide_cursor
162
179
 
163
180
  begin
164
- animation_loop_step while @running && !stop_requested
181
+ animation_loop_daemon_mode(stop_requested_proc: -> { stop_requested })
165
182
  ensure
166
183
  RubyProgress::Utils.clear_line
167
184
  RubyProgress::Utils.show_cursor
@@ -206,9 +223,9 @@ module RubyProgress
206
223
  @position ||= 0
207
224
  @direction ||= 1
208
225
 
209
- message_part = @message && !@message.empty? ? @message : ''
210
- print "\r#{message_part}#{generate_dots(@position, @direction)}"
211
- $stdout.flush
226
+ message_part = @message && !@message.empty? ? "#{@message} " : ''
227
+ $stderr.print "\r\e[2K#{message_part}#{generate_dots(@position, @direction)}"
228
+ $stderr.flush
212
229
 
213
230
  sleep @speed
214
231
 
@@ -240,7 +257,7 @@ module RubyProgress
240
257
  if speed_input.match?(/^\d+$/)
241
258
  # Numeric string (1-10)
242
259
  speed_num = speed_input.to_i
243
- return 0.6 - (speed_num - 1) * 0.05 if speed_num.between?(1, 10)
260
+ return 0.6 - ((speed_num - 1) * 0.05) if speed_num.between?(1, 10)
244
261
  end
245
262
 
246
263
  # Check for abbreviated forms
@@ -256,25 +273,67 @@ module RubyProgress
256
273
  end
257
274
  when Numeric
258
275
  speed_num = speed_input.to_i
259
- speed_num.between?(1, 10) ? 0.6 - (speed_num - 1) * 0.05 : SPEED_MAP['medium']
276
+ speed_num.between?(1, 10) ? 0.6 - ((speed_num - 1) * 0.05) : SPEED_MAP['medium']
260
277
  else
261
278
  SPEED_MAP['medium']
262
279
  end
263
280
  end
264
281
 
265
282
  def parse_style(style_input)
266
- return RIPPLE_STYLES['circles'] unless style_input
267
-
268
- style_lower = style_input.to_s.downcase
269
- if style_lower.start_with?('b')
270
- RIPPLE_STYLES['blocks']
271
- elsif style_lower.start_with?('g')
272
- RIPPLE_STYLES['geometric']
273
- elsif style_lower.start_with?('c')
274
- RIPPLE_STYLES['circles']
275
- else
276
- RIPPLE_STYLES['circles'] # default
283
+ return RIPPLE_STYLES['circles'] unless style_input && !style_input.to_s.strip.empty?
284
+
285
+ style_lower = style_input.to_s.downcase.strip
286
+
287
+ # First, try exact match
288
+ return RIPPLE_STYLES[style_lower] if RIPPLE_STYLES.key?(style_lower)
289
+
290
+ # Then try prefix matching - keys that start with the input
291
+ prefix_matches = RIPPLE_STYLES.keys.select do |key|
292
+ key.downcase.start_with?(style_lower)
293
+ end
294
+
295
+ unless prefix_matches.empty?
296
+ # For prefix matches, return the shortest one
297
+ best_match = prefix_matches.min_by(&:length)
298
+ return RIPPLE_STYLES[best_match]
299
+ end
300
+
301
+ # Try character-by-character fuzzy matching for partial inputs
302
+ # Find keys where the input characters appear in order (not necessarily contiguous)
303
+ fuzzy_matches = RIPPLE_STYLES.keys.select do |key|
304
+ key_chars = key.downcase.chars
305
+ input_chars = style_lower.chars
306
+
307
+ # Check if all input characters appear in order in the key
308
+ input_chars.all? do |char|
309
+ idx = key_chars.index(char)
310
+ if idx
311
+ key_chars = key_chars[idx + 1..-1] # Remove matched chars and continue
312
+ true
313
+ else
314
+ false
315
+ end
316
+ end
317
+ end
318
+
319
+ unless fuzzy_matches.empty?
320
+ # Sort by length (prefer shorter keys)
321
+ best_match = fuzzy_matches.min_by(&:length)
322
+ return RIPPLE_STYLES[best_match]
323
+ end
324
+
325
+ # Fallback to substring matching
326
+ substring_matches = RIPPLE_STYLES.keys.select do |key|
327
+ key.downcase.include?(style_lower)
328
+ end
329
+
330
+ unless substring_matches.empty?
331
+ best_match = substring_matches.min_by(&:length)
332
+ return RIPPLE_STYLES[best_match]
277
333
  end
334
+
335
+ # Default fallback
336
+ RIPPLE_STYLES['circles']
278
337
  end
279
338
 
280
339
  def animation_loop
@@ -282,11 +341,45 @@ module RubyProgress
282
341
  direction = 1
283
342
 
284
343
  while @running
285
- message_part = @message && !@message.empty? ? @message : ''
286
- print "\r#{message_part}#{generate_dots(position, direction)}"
287
- $stdout.flush
344
+ message_part = @message && !@message.empty? ? "#{@message} " : ''
345
+ # Enhanced line clearing for better daemon mode behavior
346
+ $stderr.print "\r\e[2K#{message_part}#{generate_dots(position, direction)}"
347
+ $stderr.flush
348
+
349
+ sleep @speed
350
+
351
+ position += direction
352
+ if position >= @length - 1
353
+ direction = -1
354
+ elsif position <= 0
355
+ direction = 1
356
+ end
357
+ end
358
+ end
359
+
360
+ # Enhanced animation loop for daemon mode with aggressive line clearing
361
+ def animation_loop_daemon_mode(stop_requested_proc: -> { false })
362
+ position = 0
363
+ direction = 1
364
+ frame_count = 0
365
+
366
+ while @running && !stop_requested_proc.call
367
+ message_part = @message && !@message.empty? ? "#{@message} " : ''
368
+
369
+ # Always clear current line
370
+ $stderr.print "\r\e[2K"
371
+
372
+ # Every few frames, use aggressive clearing to handle interruptions
373
+ if (frame_count % 10).zero?
374
+ $stderr.print "\e[1A\e[2K" # Move up and clear that line too (in case of interruption)
375
+ $stderr.print "\r" # Return to start
376
+ end
377
+
378
+ $stderr.print "#{message_part}#{generate_dots(position, direction)}"
379
+ $stderr.flush
288
380
 
289
381
  sleep @speed
382
+ frame_count += 1
290
383
 
291
384
  position += direction
292
385
  if position >= @length - 1
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env ruby
2
+ # Test script to explore terminal cursor behavior with daemon output interruption
3
+
4
+ require_relative 'lib/ruby-progress'
5
+
6
+ puts 'Testing daemon output interruption behavior...'
7
+ puts 'This will start a daemon and then output some text to see what happens.'
8
+
9
+ # Start a daemon
10
+ system("ruby -I lib bin/prg worm --daemon --message 'Testing daemon animation' &")
11
+
12
+ sleep 1
13
+
14
+ puts 'This output should interrupt the animation'
15
+ puts 'And this is a second line of output'
16
+
17
+ sleep 2
18
+
19
+ puts 'More interrupting output after animation has been running'
20
+
21
+ sleep 2
22
+
23
+ # Stop the daemon
24
+ system('ruby -I lib bin/prg --stop-all')
25
+
26
+ puts 'Test completed'
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Test script to reproduce daemon orphaning issue
4
+ puts 'Starting daemon...'
5
+ system("./bin/prg worm --daemon-as test 'Working on something...'")
6
+
7
+ puts 'Daemon started, sleeping for 3 seconds...'
8
+ puts 'Cancel this script with ^C during the sleep to reproduce the orphan issue'
9
+ sleep 3
10
+
11
+ puts 'Stopping daemon normally...'
12
+ system('./bin/prg --stop-id test')
13
+ puts 'Done'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-progress
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.4
4
+ version: 1.1.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brett Terpstra
@@ -80,6 +80,8 @@ files:
80
80
  - ".editorconfig"
81
81
  - ".markdownlint.json"
82
82
  - ".rspec"
83
+ - ".rubocop.yml"
84
+ - ".rubocop_todo.yml"
83
85
  - CHANGELOG.md
84
86
  - Gemfile
85
87
  - Gemfile.lock
@@ -90,17 +92,21 @@ files:
90
92
  - bin/ripple
91
93
  - bin/twirl
92
94
  - bin/worm
95
+ - blog-post.md
93
96
  - demo_gem.rb
94
97
  - demo_worm_infinite.rb
95
98
  - examples/bash_daemon_demo.sh
96
99
  - examples/daemon_demo.rb
97
100
  - examples/utils_demo.rb
101
+ - experimental_terminal.rb
98
102
  - lib/ruby-progress.rb
99
103
  - lib/ruby-progress/daemon.rb
100
104
  - lib/ruby-progress/ripple.rb
101
105
  - lib/ruby-progress/utils.rb
102
106
  - lib/ruby-progress/version.rb
103
107
  - lib/ruby-progress/worm.rb
108
+ - test_daemon_interruption.rb
109
+ - test_daemon_orphan.rb
104
110
  - test_worm_flags.rb
105
111
  - worm.rb
106
112
  homepage: https://github.com/ttscoff/ruby-progress
@@ -110,6 +116,7 @@ metadata:
110
116
  homepage_uri: https://github.com/ttscoff/ruby-progress
111
117
  source_code_uri: https://github.com/ttscoff/ruby-progress
112
118
  changelog_uri: https://github.com/ttscoff/ruby-progress/blob/main/CHANGELOG.md
119
+ rubygems_mfa_required: 'true'
113
120
  rdoc_options: []
114
121
  require_paths:
115
122
  - lib