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.
- checksums.yaml +4 -4
- data/.rubocop.yml +5 -0
- data/.rubocop_todo.yml +201 -0
- data/CHANGELOG.md +81 -0
- data/Gemfile.lock +1 -1
- data/Rakefile +3 -8
- data/bin/prg +362 -102
- 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 +117 -22
- data/test_daemon_interruption.rb +26 -0
- data/test_daemon_orphan.rb +13 -0
- metadata +8 -2
- data/ruby-progress.gemspec +0 -39
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!*
|
data/examples/daemon_demo.rb
CHANGED
|
@@ -18,7 +18,7 @@ puts "PID file: #{pid_file}"
|
|
|
18
18
|
puts
|
|
19
19
|
|
|
20
20
|
# Clean up any existing PID file
|
|
21
|
-
|
|
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
|
data/lib/ruby-progress/daemon.rb
CHANGED
|
@@ -45,10 +45,10 @@ module RubyProgress
|
|
|
45
45
|
begin
|
|
46
46
|
Process.kill('USR1', pid)
|
|
47
47
|
sleep 0.5
|
|
48
|
-
|
|
48
|
+
FileUtils.rm_f(pid_file)
|
|
49
49
|
rescue Errno::ESRCH
|
|
50
50
|
puts "Process #{pid} not found (may have already stopped)"
|
|
51
|
-
|
|
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}"
|
data/lib/ruby-progress/ripple.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =~ /^#{
|
|
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.
|
|
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}
|
|
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)
|
data/lib/ruby-progress/utils.rb
CHANGED
|
@@ -12,8 +12,20 @@ module RubyProgress
|
|
|
12
12
|
$stderr.print "\e[?25h"
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
def self.clear_line
|
|
16
|
-
|
|
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
|
data/lib/ruby-progress/worm.rb
CHANGED
|
@@ -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
|
|
|
@@ -35,7 +55,7 @@ module RubyProgress
|
|
|
35
55
|
|
|
36
56
|
def initialize(options = {})
|
|
37
57
|
@length = options[:length] || 3
|
|
38
|
-
@message = options[:message]
|
|
58
|
+
@message = options[:message]
|
|
39
59
|
@speed = parse_speed(options[:speed] || 'medium')
|
|
40
60
|
@style = parse_style(options[:style] || 'circles')
|
|
41
61
|
@command = options[:command]
|
|
@@ -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
|
-
|
|
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,8 +223,9 @@ module RubyProgress
|
|
|
206
223
|
@position ||= 0
|
|
207
224
|
@direction ||= 1
|
|
208
225
|
|
|
209
|
-
|
|
210
|
-
$
|
|
226
|
+
message_part = @message && !@message.empty? ? "#{@message} " : ''
|
|
227
|
+
$stderr.print "\r\e[2K#{message_part}#{generate_dots(@position, @direction)}"
|
|
228
|
+
$stderr.flush
|
|
211
229
|
|
|
212
230
|
sleep @speed
|
|
213
231
|
|
|
@@ -239,7 +257,7 @@ module RubyProgress
|
|
|
239
257
|
if speed_input.match?(/^\d+$/)
|
|
240
258
|
# Numeric string (1-10)
|
|
241
259
|
speed_num = speed_input.to_i
|
|
242
|
-
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)
|
|
243
261
|
end
|
|
244
262
|
|
|
245
263
|
# Check for abbreviated forms
|
|
@@ -255,25 +273,67 @@ module RubyProgress
|
|
|
255
273
|
end
|
|
256
274
|
when Numeric
|
|
257
275
|
speed_num = speed_input.to_i
|
|
258
|
-
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']
|
|
259
277
|
else
|
|
260
278
|
SPEED_MAP['medium']
|
|
261
279
|
end
|
|
262
280
|
end
|
|
263
281
|
|
|
264
282
|
def parse_style(style_input)
|
|
265
|
-
return RIPPLE_STYLES['circles'] unless style_input
|
|
266
|
-
|
|
267
|
-
style_lower = style_input.to_s.downcase
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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]
|
|
276
333
|
end
|
|
334
|
+
|
|
335
|
+
# Default fallback
|
|
336
|
+
RIPPLE_STYLES['circles']
|
|
277
337
|
end
|
|
278
338
|
|
|
279
339
|
def animation_loop
|
|
@@ -281,10 +341,45 @@ module RubyProgress
|
|
|
281
341
|
direction = 1
|
|
282
342
|
|
|
283
343
|
while @running
|
|
284
|
-
|
|
285
|
-
|
|
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
|
|
286
380
|
|
|
287
381
|
sleep @speed
|
|
382
|
+
frame_count += 1
|
|
288
383
|
|
|
289
384
|
position += direction
|
|
290
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'
|