ruby-progress 1.2.0 → 1.3.1
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/CHANGELOG.md +76 -154
- data/DEMO_SCRIPTS.md +162 -0
- data/Gemfile.lock +1 -1
- data/README.md +128 -120
- data/Rakefile +7 -0
- data/bin/fill +10 -0
- data/bin/prg +76 -1024
- data/demo_screencast.rb +296 -0
- data/examples/daemon_job_example.sh +25 -0
- data/experimental_terminal.rb +7 -0
- data/lib/ruby-progress/cli/fill_options.rb +215 -0
- data/lib/ruby-progress/cli/job_cli.rb +99 -0
- data/lib/ruby-progress/cli/ripple_cli.rb +211 -0
- data/lib/ruby-progress/cli/ripple_options.rb +158 -0
- data/lib/ruby-progress/cli/twirl_cli.rb +173 -0
- data/lib/ruby-progress/cli/twirl_options.rb +147 -0
- data/lib/ruby-progress/cli/twirl_runner.rb +183 -0
- data/lib/ruby-progress/cli/twirl_spinner.rb +79 -0
- data/lib/ruby-progress/cli/worm_cli.rb +109 -0
- data/lib/ruby-progress/cli/worm_options.rb +173 -0
- data/lib/ruby-progress/cli/worm_runner.rb +282 -0
- data/lib/ruby-progress/daemon.rb +65 -0
- data/lib/ruby-progress/fill.rb +215 -0
- data/lib/ruby-progress/fill_cli.rb +249 -0
- data/lib/ruby-progress/output_capture.rb +136 -0
- data/lib/ruby-progress/ripple.rb +1 -0
- data/lib/ruby-progress/utils.rb +16 -2
- data/lib/ruby-progress/version.rb +8 -4
- data/lib/ruby-progress/worm.rb +2 -177
- data/lib/ruby-progress.rb +1 -0
- data/quick_demo.rb +134 -0
- data/readme_demo.rb +128 -0
- data/ruby-progress.gemspec +40 -0
- data/scripts/run_matrix_mise.fish +41 -0
- data/test_daemon_interruption.rb +2 -0
- data/test_daemon_orphan.rb +1 -0
- metadata +24 -1
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'optparse'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require_relative 'cli/fill_options'
|
|
6
|
+
require_relative 'output_capture'
|
|
7
|
+
|
|
8
|
+
module RubyProgress
|
|
9
|
+
# CLI module for Fill command
|
|
10
|
+
module FillCLI
|
|
11
|
+
class << self
|
|
12
|
+
def run
|
|
13
|
+
trap('INT') do
|
|
14
|
+
Utils.show_cursor
|
|
15
|
+
exit
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
options = RubyProgress::FillCLI::Options.parse_cli_options
|
|
19
|
+
|
|
20
|
+
# Handle basic output flags first
|
|
21
|
+
if options[:help]
|
|
22
|
+
puts RubyProgress::FillCLI::Options.help_text
|
|
23
|
+
exit
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
if options[:version]
|
|
27
|
+
puts "Fill version #{RubyProgress::FILL_VERSION}"
|
|
28
|
+
exit
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
if options[:show_styles]
|
|
32
|
+
show_fill_styles
|
|
33
|
+
exit
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Handle daemon control first
|
|
37
|
+
if options[:status] || options[:stop]
|
|
38
|
+
pid_file = options[:pid_file] || '/tmp/ruby-progress/fill.pid'
|
|
39
|
+
if options[:status]
|
|
40
|
+
Daemon.show_status(pid_file)
|
|
41
|
+
else
|
|
42
|
+
Daemon.stop_daemon_by_pid_file(pid_file)
|
|
43
|
+
end
|
|
44
|
+
exit
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Parse style option
|
|
48
|
+
parsed_style = parse_fill_style(options[:style])
|
|
49
|
+
|
|
50
|
+
if options[:daemon]
|
|
51
|
+
run_daemon_mode(options, parsed_style)
|
|
52
|
+
elsif options[:current]
|
|
53
|
+
show_current_percentage(options, parsed_style)
|
|
54
|
+
elsif options[:report]
|
|
55
|
+
show_progress_report(options, parsed_style)
|
|
56
|
+
elsif options[:advance] || options[:complete] || options[:cancel]
|
|
57
|
+
handle_progress_commands(options, parsed_style)
|
|
58
|
+
else
|
|
59
|
+
run_auto_advance_mode(options, parsed_style)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def parse_fill_style(style_option)
|
|
66
|
+
case style_option
|
|
67
|
+
when String
|
|
68
|
+
if style_option.start_with?('custom=')
|
|
69
|
+
Fill.parse_custom_style(style_option)
|
|
70
|
+
else
|
|
71
|
+
style_option.to_sym
|
|
72
|
+
end
|
|
73
|
+
else
|
|
74
|
+
style_option
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def run_daemon_mode(options, parsed_style)
|
|
79
|
+
# For daemon mode, detach the process
|
|
80
|
+
PrgCLI.daemonize
|
|
81
|
+
|
|
82
|
+
pid_file = options[:pid_file] || '/tmp/ruby-progress/fill.pid'
|
|
83
|
+
FileUtils.mkdir_p(File.dirname(pid_file))
|
|
84
|
+
File.write(pid_file, Process.pid.to_s)
|
|
85
|
+
|
|
86
|
+
# Create the fill bar and show initial empty state
|
|
87
|
+
fill_options = {
|
|
88
|
+
style: parsed_style,
|
|
89
|
+
length: options[:length],
|
|
90
|
+
ends: options[:ends],
|
|
91
|
+
success: options[:success_message],
|
|
92
|
+
error: options[:error_message]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
fill_bar = Fill.new(fill_options)
|
|
96
|
+
Fill.hide_cursor
|
|
97
|
+
|
|
98
|
+
begin
|
|
99
|
+
fill_bar.render # Show initial empty bar
|
|
100
|
+
|
|
101
|
+
# Set up signal handlers for daemon control
|
|
102
|
+
stop_requested = false
|
|
103
|
+
Signal.trap('INT') { stop_requested = true }
|
|
104
|
+
Signal.trap('USR1') { stop_requested = true }
|
|
105
|
+
Signal.trap('TERM') { stop_requested = true }
|
|
106
|
+
|
|
107
|
+
# Keep daemon alive until stop requested
|
|
108
|
+
sleep(0.1) until stop_requested
|
|
109
|
+
ensure
|
|
110
|
+
Fill.show_cursor
|
|
111
|
+
FileUtils.rm_f(pid_file)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def show_current_percentage(options, _parsed_style)
|
|
116
|
+
# Just output the percentage for scripting (default to 50% for demonstration)
|
|
117
|
+
percentage = options[:percent] || 50
|
|
118
|
+
puts percentage.to_f
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def show_progress_report(options, parsed_style)
|
|
122
|
+
# Create a fill bar to demonstrate current progress
|
|
123
|
+
fill_options = {
|
|
124
|
+
style: parsed_style,
|
|
125
|
+
length: options[:length],
|
|
126
|
+
ends: options[:ends]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
fill_bar = Fill.new(fill_options)
|
|
130
|
+
|
|
131
|
+
# Set percentage (default to 50% for demonstration)
|
|
132
|
+
fill_bar.percent = options[:percent] || 50
|
|
133
|
+
|
|
134
|
+
# Get detailed report
|
|
135
|
+
report = fill_bar.report
|
|
136
|
+
|
|
137
|
+
# Display the current progress bar and detailed status
|
|
138
|
+
fill_bar.render
|
|
139
|
+
puts "\nProgress Report:"
|
|
140
|
+
puts " Progress: #{report[:progress][0]}/#{report[:progress][1]}"
|
|
141
|
+
puts " Percent: #{report[:percent]}%"
|
|
142
|
+
puts " Completed: #{report[:completed] ? 'Yes' : 'No'}"
|
|
143
|
+
puts " Style: #{report[:style]}"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def handle_progress_commands(_options, _parsed_style)
|
|
147
|
+
# For progress commands, we assume there's a daemon running
|
|
148
|
+
# This is a simplified version - in a real implementation,
|
|
149
|
+
# we'd need IPC to communicate with the daemon
|
|
150
|
+
warn 'Progress commands require daemon mode implementation'
|
|
151
|
+
warn "Run 'prg fill --daemon' first, then use progress commands"
|
|
152
|
+
exit 1
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def run_auto_advance_mode(options, parsed_style)
|
|
156
|
+
fill_options = {
|
|
157
|
+
style: parsed_style,
|
|
158
|
+
length: options[:length],
|
|
159
|
+
ends: options[:ends],
|
|
160
|
+
success: options[:success_message],
|
|
161
|
+
error: options[:error_message]
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
# If a command is provided, capture its output and pass an OutputCapture
|
|
165
|
+
if options[:command]
|
|
166
|
+
oc = RubyProgress::OutputCapture.new(
|
|
167
|
+
command: options[:command],
|
|
168
|
+
lines: options[:output_lines] || 3,
|
|
169
|
+
position: options[:output_position] || :above
|
|
170
|
+
)
|
|
171
|
+
oc.start
|
|
172
|
+
fill_options[:output_capture] = oc
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
fill_bar = Fill.new(fill_options)
|
|
176
|
+
Fill.hide_cursor
|
|
177
|
+
|
|
178
|
+
begin
|
|
179
|
+
if options[:percent]
|
|
180
|
+
# Set to specific percentage
|
|
181
|
+
fill_bar.percent = options[:percent]
|
|
182
|
+
fill_bar.render
|
|
183
|
+
unless fill_bar.completed?
|
|
184
|
+
# For non-complete percentages, show the result briefly
|
|
185
|
+
sleep(0.1)
|
|
186
|
+
end
|
|
187
|
+
elsif options[:command]
|
|
188
|
+
# While the command runs, keep redrawing the bar (live redraw handled by Fill#render)
|
|
189
|
+
sleep_time = case options[:speed]
|
|
190
|
+
when :fast then 0.1
|
|
191
|
+
when :medium, nil then 0.2
|
|
192
|
+
when :slow then 0.5
|
|
193
|
+
when Numeric then 1.0 / options[:speed]
|
|
194
|
+
else 0.3
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
fill_bar.render
|
|
198
|
+
# Loop until the OutputCapture reader has finished
|
|
199
|
+
while oc.alive?
|
|
200
|
+
sleep(sleep_time)
|
|
201
|
+
fill_bar.render
|
|
202
|
+
end
|
|
203
|
+
else
|
|
204
|
+
# Auto-advance mode
|
|
205
|
+
sleep_time = case options[:speed]
|
|
206
|
+
when :fast then 0.1
|
|
207
|
+
when :medium, nil then 0.2
|
|
208
|
+
when :slow then 0.5
|
|
209
|
+
when Numeric then 1.0 / options[:speed]
|
|
210
|
+
else 0.3
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
fill_bar.render
|
|
214
|
+
(1..options[:length]).each do
|
|
215
|
+
sleep(sleep_time)
|
|
216
|
+
fill_bar.advance
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
fill_bar.complete
|
|
220
|
+
rescue Interrupt
|
|
221
|
+
fill_bar.cancel
|
|
222
|
+
ensure
|
|
223
|
+
# Ensure we wait for capture thread to finish and show cursor
|
|
224
|
+
oc&.wait
|
|
225
|
+
Fill.show_cursor
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def show_fill_styles
|
|
230
|
+
puts "\nAvailable Fill Styles:"
|
|
231
|
+
puts '=' * 50
|
|
232
|
+
|
|
233
|
+
Fill::FILL_STYLES.each do |name, style|
|
|
234
|
+
print "#{name.to_s.ljust(12)} : "
|
|
235
|
+
|
|
236
|
+
# Show a sample progress bar
|
|
237
|
+
filled = style[:full] * 6
|
|
238
|
+
empty = style[:empty] * 4
|
|
239
|
+
puts "[#{filled}#{empty}] (60%)"
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
puts "\nCustom Style:"
|
|
243
|
+
puts "#{'custom=XY'.ljust(12)} : Specify X=empty, Y=full characters"
|
|
244
|
+
puts ' Example: --style custom=.# → [######....] (60%)'
|
|
245
|
+
puts
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pty'
|
|
4
|
+
require 'io/console'
|
|
5
|
+
require 'English'
|
|
6
|
+
|
|
7
|
+
module RubyProgress
|
|
8
|
+
# Simple PTY-based output capture that reserves a small area of the terminal
|
|
9
|
+
# for printing captured output while the animation is drawn separately.
|
|
10
|
+
class OutputCapture
|
|
11
|
+
attr_reader :exit_status
|
|
12
|
+
|
|
13
|
+
def initialize(command:, lines: 3, position: :above, log_path: nil)
|
|
14
|
+
@command = command
|
|
15
|
+
@lines = lines
|
|
16
|
+
@position = position
|
|
17
|
+
@buffer = []
|
|
18
|
+
@buf_mutex = Mutex.new
|
|
19
|
+
@stop = false
|
|
20
|
+
@log_path = log_path
|
|
21
|
+
@log_file = nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Start the child process and return a thread that manages capture.
|
|
25
|
+
def start
|
|
26
|
+
@reader_thread = Thread.new { spawn_and_read }
|
|
27
|
+
self
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def stop
|
|
31
|
+
@stop = true
|
|
32
|
+
@reader_thread&.join
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Wait for the reader thread to complete
|
|
36
|
+
def wait
|
|
37
|
+
@reader_thread&.join
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Return snapshot of buffered lines (thread-safe)
|
|
41
|
+
def lines
|
|
42
|
+
@buf_mutex.synchronize { @buffer.dup }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Returns whether the reader thread is still alive
|
|
46
|
+
def alive?
|
|
47
|
+
@reader_thread&.alive? || false
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Redraw buffered output into the terminal above/below the current cursor
|
|
51
|
+
# io - IO object to write to (default $stderr)
|
|
52
|
+
def redraw(io = $stderr)
|
|
53
|
+
buf = lines
|
|
54
|
+
_rows, cols = IO.console.winsize
|
|
55
|
+
|
|
56
|
+
# Ensure we have exactly @lines entries (pad with empty strings)
|
|
57
|
+
display_lines = Array.new(@lines) { '' }
|
|
58
|
+
start = [0, buf.size - @lines].max
|
|
59
|
+
buf[start, @lines]&.each_with_index do |l, i|
|
|
60
|
+
display_lines[i + (@lines - [buf.size, @lines].min)] = l.to_s
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Save cursor, move up N lines, clear and print buffer, restore cursor
|
|
64
|
+
io.print "\e[s" # save position
|
|
65
|
+
io.print "\e[#{@lines}A" # move up @lines
|
|
66
|
+
display_lines.each do |line|
|
|
67
|
+
io.print "\e[2K" # clear line
|
|
68
|
+
io.print "\r" # move cursor to start of line
|
|
69
|
+
io.print line[0, cols]
|
|
70
|
+
io.print "\n"
|
|
71
|
+
end
|
|
72
|
+
io.print "\e[u" # restore
|
|
73
|
+
io.flush
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def spawn_and_read
|
|
79
|
+
PTY.spawn(@command) do |reader, _writer, pid|
|
|
80
|
+
@child_pid = pid
|
|
81
|
+
until reader.eof? || @stop
|
|
82
|
+
next unless reader.wait_readable(0.1)
|
|
83
|
+
|
|
84
|
+
chunk = reader.read_nonblock(4096, exception: false)
|
|
85
|
+
next if chunk.nil? || chunk.empty?
|
|
86
|
+
|
|
87
|
+
# lazily open log file if requested
|
|
88
|
+
if @log_path && !@log_file
|
|
89
|
+
begin
|
|
90
|
+
FileUtils.mkdir_p(File.dirname(@log_path))
|
|
91
|
+
@log_file = File.open(@log_path, 'a')
|
|
92
|
+
rescue StandardError
|
|
93
|
+
@log_file = nil
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
process_chunk(chunk)
|
|
98
|
+
next unless @log_file
|
|
99
|
+
|
|
100
|
+
begin
|
|
101
|
+
@log_file.write(chunk)
|
|
102
|
+
@log_file.flush
|
|
103
|
+
rescue StandardError
|
|
104
|
+
# ignore logging errors
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
begin
|
|
109
|
+
Process.wait(@child_pid) if @child_pid
|
|
110
|
+
@exit_status = $CHILD_STATUS.exitstatus if $CHILD_STATUS
|
|
111
|
+
rescue StandardError
|
|
112
|
+
@exit_status = nil
|
|
113
|
+
ensure
|
|
114
|
+
if @log_file
|
|
115
|
+
begin
|
|
116
|
+
@log_file.close
|
|
117
|
+
rescue StandardError
|
|
118
|
+
nil
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
rescue Errno::EIO
|
|
123
|
+
# PTY finished
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def process_chunk(chunk)
|
|
127
|
+
@buf_mutex.synchronize do
|
|
128
|
+
# split into lines, keep last N lines
|
|
129
|
+
chunk.each_line do |line|
|
|
130
|
+
@buffer << line.chomp
|
|
131
|
+
@buffer.shift while @buffer.size > @lines
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
data/lib/ruby-progress/ripple.rb
CHANGED
|
@@ -140,6 +140,7 @@ module RubyProgress
|
|
|
140
140
|
char = @rainbow ? char.rainbow(i) : char.extend(StringExtensions).light_white
|
|
141
141
|
post = letters.slice!(0, letters.length).join.extend(StringExtensions).dark_white
|
|
142
142
|
end
|
|
143
|
+
@output_capture&.redraw($stderr)
|
|
143
144
|
$stderr.print "\r\e[2K#{@start_chars}#{pre}#{char}#{post}#{@end_chars}"
|
|
144
145
|
$stderr.flush
|
|
145
146
|
end
|
data/lib/ruby-progress/utils.rb
CHANGED
|
@@ -49,9 +49,15 @@ module RubyProgress
|
|
|
49
49
|
when :stderr
|
|
50
50
|
warn formatted_message
|
|
51
51
|
when :warn
|
|
52
|
-
|
|
52
|
+
# Ensure we're at the beginning of a fresh line, clear it, then display message
|
|
53
|
+
$stderr.print "\r\e[2K"
|
|
54
|
+
$stderr.flush
|
|
55
|
+
warn formatted_message
|
|
53
56
|
else
|
|
54
|
-
|
|
57
|
+
# Ensure we're at the beginning of a fresh line, clear it, then display message
|
|
58
|
+
$stderr.print "\r\e[2K"
|
|
59
|
+
$stderr.flush
|
|
60
|
+
warn formatted_message
|
|
55
61
|
end
|
|
56
62
|
end
|
|
57
63
|
|
|
@@ -77,5 +83,13 @@ module RubyProgress
|
|
|
77
83
|
|
|
78
84
|
[start_chars, end_chars]
|
|
79
85
|
end
|
|
86
|
+
|
|
87
|
+
# Validate ends string: must be non-empty and even-length (handles multi-byte chars)
|
|
88
|
+
def self.ends_valid?(ends_string)
|
|
89
|
+
return false unless ends_string && !ends_string.empty?
|
|
90
|
+
|
|
91
|
+
chars = ends_string.each_char.to_a
|
|
92
|
+
!chars.empty? && (chars.length % 2).zero?
|
|
93
|
+
end
|
|
80
94
|
end
|
|
81
95
|
end
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module RubyProgress
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
# Main gem version
|
|
5
|
+
VERSION = '1.3.1'
|
|
6
|
+
|
|
7
|
+
# Component-specific versions (patch bumps)
|
|
8
|
+
WORM_VERSION = '1.1.3'
|
|
9
|
+
TWIRL_VERSION = '1.1.3'
|
|
10
|
+
RIPPLE_VERSION = '1.1.3'
|
|
11
|
+
FILL_VERSION = '1.0.3'
|
|
8
12
|
end
|
data/lib/ruby-progress/worm.rb
CHANGED
|
@@ -4,6 +4,7 @@ require 'optparse'
|
|
|
4
4
|
require 'open3'
|
|
5
5
|
require 'json'
|
|
6
6
|
require_relative 'utils'
|
|
7
|
+
require_relative 'cli/worm_runner'
|
|
7
8
|
|
|
8
9
|
module RubyProgress
|
|
9
10
|
# Animated progress indicator with ripple effect using Unicode combining characters
|
|
@@ -68,183 +69,7 @@ module RubyProgress
|
|
|
68
69
|
@start_chars, @end_chars = RubyProgress::Utils.parse_ends(options[:ends])
|
|
69
70
|
@running = false
|
|
70
71
|
end
|
|
71
|
-
|
|
72
|
-
def animate(message: nil, success: nil, error: nil, &block)
|
|
73
|
-
@message = message if message
|
|
74
|
-
@success_text = success if success
|
|
75
|
-
@error_text = error if error
|
|
76
|
-
@running = true
|
|
77
|
-
|
|
78
|
-
# Set up interrupt handler to ensure cursor is restored
|
|
79
|
-
original_int_handler = Signal.trap('INT') do
|
|
80
|
-
@running = false
|
|
81
|
-
RubyProgress::Utils.clear_line
|
|
82
|
-
RubyProgress::Utils.show_cursor
|
|
83
|
-
exit 130
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
# Hide cursor
|
|
87
|
-
RubyProgress::Utils.hide_cursor
|
|
88
|
-
|
|
89
|
-
animation_thread = Thread.new { animation_loop }
|
|
90
|
-
|
|
91
|
-
begin
|
|
92
|
-
if block_given?
|
|
93
|
-
result = yield
|
|
94
|
-
@running = false
|
|
95
|
-
animation_thread.join
|
|
96
|
-
display_completion_message(@success_text, true)
|
|
97
|
-
result
|
|
98
|
-
else
|
|
99
|
-
animation_thread.join
|
|
100
|
-
end
|
|
101
|
-
rescue StandardError => e
|
|
102
|
-
@running = false
|
|
103
|
-
animation_thread.join
|
|
104
|
-
display_completion_message(@error_text || "Error: #{e.message}", false)
|
|
105
|
-
nil # Return nil instead of re-raising when used as a progress indicator
|
|
106
|
-
ensure
|
|
107
|
-
# Always clear animation line and restore cursor
|
|
108
|
-
$stderr.print "\r\e[2K"
|
|
109
|
-
RubyProgress::Utils.show_cursor
|
|
110
|
-
Signal.trap('INT', original_int_handler) if original_int_handler
|
|
111
|
-
end
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def run_with_command
|
|
115
|
-
return unless @command
|
|
116
|
-
|
|
117
|
-
exit_code = 0
|
|
118
|
-
stdout_content = nil
|
|
119
|
-
|
|
120
|
-
begin
|
|
121
|
-
stdout_content = animate do
|
|
122
|
-
# Use popen3 instead of capture3 for better signal handling
|
|
123
|
-
Open3.popen3(@command) do |stdin, stdout, stderr, wait_thr|
|
|
124
|
-
stdin.close
|
|
125
|
-
captured_stdout = stdout.read
|
|
126
|
-
stderr_content = stderr.read
|
|
127
|
-
exit_code = wait_thr.value.exitstatus
|
|
128
|
-
|
|
129
|
-
unless wait_thr.value.success?
|
|
130
|
-
error_msg = @error_text || "Command failed with exit code #{exit_code}"
|
|
131
|
-
error_msg += ": #{stderr_content.strip}" if stderr_content && !stderr_content.empty?
|
|
132
|
-
raise StandardError, error_msg
|
|
133
|
-
end
|
|
134
|
-
captured_stdout
|
|
135
|
-
end
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
# Output to stdout if --stdout flag is set
|
|
139
|
-
puts stdout_content if @output_stdout && stdout_content
|
|
140
|
-
rescue StandardError => e
|
|
141
|
-
# animate method handles error display, just exit with proper code
|
|
142
|
-
exit exit_code.nonzero? || 1
|
|
143
|
-
rescue Interrupt
|
|
144
|
-
exit 130
|
|
145
|
-
end
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
def run_indefinitely
|
|
149
|
-
# Set up interrupt handler to ensure cursor is restored
|
|
150
|
-
original_int_handler = Signal.trap('INT') do
|
|
151
|
-
@running = false
|
|
152
|
-
RubyProgress::Utils.clear_line
|
|
153
|
-
RubyProgress::Utils.show_cursor
|
|
154
|
-
exit 130
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
@running = true
|
|
158
|
-
RubyProgress::Utils.hide_cursor
|
|
159
|
-
|
|
160
|
-
begin
|
|
161
|
-
animation_loop
|
|
162
|
-
ensure
|
|
163
|
-
RubyProgress::Utils.show_cursor
|
|
164
|
-
Signal.trap('INT', original_int_handler) if original_int_handler
|
|
165
|
-
end
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
def stop
|
|
169
|
-
@running = false
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def run_daemon_mode(success_message: nil, show_checkmark: false, control_message_file: nil)
|
|
173
|
-
@running = true
|
|
174
|
-
stop_requested = false
|
|
175
|
-
|
|
176
|
-
# Set up signal handlers
|
|
177
|
-
original_int_handler = Signal.trap('INT') { stop_requested = true }
|
|
178
|
-
Signal.trap('USR1') { stop_requested = true }
|
|
179
|
-
Signal.trap('TERM') { stop_requested = true }
|
|
180
|
-
Signal.trap('HUP') { stop_requested = true }
|
|
181
|
-
|
|
182
|
-
RubyProgress::Utils.hide_cursor
|
|
183
|
-
|
|
184
|
-
begin
|
|
185
|
-
animation_loop_daemon_mode(stop_requested_proc: -> { stop_requested })
|
|
186
|
-
ensure
|
|
187
|
-
RubyProgress::Utils.clear_line
|
|
188
|
-
RubyProgress::Utils.show_cursor
|
|
189
|
-
|
|
190
|
-
# Display stop-time completion message, preferring control file if provided
|
|
191
|
-
final_message = success_message
|
|
192
|
-
final_checkmark = show_checkmark
|
|
193
|
-
final_success = true
|
|
194
|
-
if control_message_file && File.exist?(control_message_file)
|
|
195
|
-
begin
|
|
196
|
-
data = JSON.parse(File.read(control_message_file))
|
|
197
|
-
final_message = data['message'] if data['message']
|
|
198
|
-
final_checkmark = !!data['checkmark'] if data.key?('checkmark')
|
|
199
|
-
final_success = !!data['success'] if data.key?('success')
|
|
200
|
-
rescue StandardError
|
|
201
|
-
# ignore parse errors, fallback to provided message
|
|
202
|
-
ensure
|
|
203
|
-
begin
|
|
204
|
-
File.delete(control_message_file)
|
|
205
|
-
rescue StandardError
|
|
206
|
-
nil
|
|
207
|
-
end
|
|
208
|
-
end
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
if final_message
|
|
212
|
-
RubyProgress::Utils.display_completion(
|
|
213
|
-
final_message,
|
|
214
|
-
success: final_success,
|
|
215
|
-
show_checkmark: final_checkmark,
|
|
216
|
-
output_stream: :stdout
|
|
217
|
-
)
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
Signal.trap('INT', original_int_handler) if original_int_handler
|
|
221
|
-
end
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
def animation_loop_step
|
|
225
|
-
return unless @running
|
|
226
|
-
|
|
227
|
-
@position ||= 0
|
|
228
|
-
@direction ||= 1
|
|
229
|
-
|
|
230
|
-
message_part = @message && !@message.empty? ? "#{@message} " : ''
|
|
231
|
-
$stderr.print "\r\e[2K#{@start_chars}#{message_part}#{generate_dots(@position, @direction)}#{@end_chars}"
|
|
232
|
-
$stderr.flush
|
|
233
|
-
|
|
234
|
-
sleep @speed
|
|
235
|
-
|
|
236
|
-
# Update position and direction
|
|
237
|
-
@position += @direction
|
|
238
|
-
if @position >= @length - 1
|
|
239
|
-
if @direction_mode == :forward_only
|
|
240
|
-
@position = 0
|
|
241
|
-
else
|
|
242
|
-
@direction = -1
|
|
243
|
-
end
|
|
244
|
-
elsif @position <= 0
|
|
245
|
-
@direction = 1
|
|
246
|
-
end
|
|
247
|
-
end
|
|
72
|
+
include WormRunner
|
|
248
73
|
|
|
249
74
|
private
|
|
250
75
|
|
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/fill'
|
|
7
8
|
require_relative 'ruby-progress/daemon'
|
|
8
9
|
|
|
9
10
|
module RubyProgress
|