ruby-progress 1.3.2 → 1.3.5
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_todo.yml +37 -18
- data/CHANGELOG.md +64 -118
- data/DAEMON_MODE.md +127 -0
- data/Gemfile +9 -1
- data/Gemfile.lock +25 -18
- data/JOB_CLI_REFACTOR.md +67 -0
- data/README.md +90 -94
- data/bin/prg +10 -16
- data/demo_screencast.rb +130 -122
- data/examples/daemon_job_example.sh +8 -10
- data/lib/ruby-progress/cli/fill_options.rb +8 -6
- data/lib/ruby-progress/cli/job_cli.rb +170 -131
- data/lib/ruby-progress/cli/ripple_cli.rb +19 -56
- data/lib/ruby-progress/cli/ripple_options.rb +18 -3
- data/lib/ruby-progress/cli/twirl_cli.rb +3 -1
- data/lib/ruby-progress/cli/twirl_options.rb +4 -4
- data/lib/ruby-progress/cli/twirl_runner.rb +3 -37
- data/lib/ruby-progress/cli/worm_cli.rb +2 -50
- data/lib/ruby-progress/cli/worm_options.rb +4 -6
- data/lib/ruby-progress/cli/worm_runner.rb +16 -5
- data/lib/ruby-progress/daemon.rb +2 -64
- data/lib/ruby-progress/fill_cli.rb +4 -72
- data/lib/ruby-progress/output_capture.rb +174 -37
- data/lib/ruby-progress/utils.rb +11 -6
- data/lib/ruby-progress/version.rb +5 -5
- data/lib/ruby-progress/worm.rb +8 -61
- data/ruby-progress.gemspec +41 -0
- data/screencast +2497 -26
- data/screencast.svg +1 -0
- data/scripts/coverage_analysis.rb +49 -0
- data/test_daemon.sh +20 -0
- metadata +31 -11
|
@@ -13,7 +13,6 @@ module WormCLI
|
|
|
13
13
|
output_position: :above,
|
|
14
14
|
output_lines: 3
|
|
15
15
|
}
|
|
16
|
-
# rubocop:disable Metrics/BlockLength
|
|
17
16
|
begin
|
|
18
17
|
OptionParser.new do |opts|
|
|
19
18
|
opts.banner = 'Usage: prg worm [options]'
|
|
@@ -83,6 +82,10 @@ module WormCLI
|
|
|
83
82
|
options[:stdout] = true
|
|
84
83
|
end
|
|
85
84
|
|
|
85
|
+
opts.on('--stdout-live', 'Stream captured output to STDOUT as it arrives (non-blocking)') do
|
|
86
|
+
options[:stdout_live] = true
|
|
87
|
+
end
|
|
88
|
+
|
|
86
89
|
opts.separator ''
|
|
87
90
|
opts.separator 'Daemon Mode:'
|
|
88
91
|
|
|
@@ -101,10 +104,6 @@ module WormCLI
|
|
|
101
104
|
options[:daemon_name] = name
|
|
102
105
|
end
|
|
103
106
|
|
|
104
|
-
opts.on('--no-detach', 'When used with --daemon/--daemon-as: run background child but do not fully detach from the terminal') do
|
|
105
|
-
options[:no_detach] = true
|
|
106
|
-
end
|
|
107
|
-
|
|
108
107
|
opts.on('--pid-file FILE', 'Write process ID to file (default: /tmp/ruby-progress/progress.pid)') do |file|
|
|
109
108
|
options[:pid_file] = file
|
|
110
109
|
end
|
|
@@ -178,7 +177,6 @@ module WormCLI
|
|
|
178
177
|
puts "Run 'prg worm --help' for more information."
|
|
179
178
|
exit 1
|
|
180
179
|
end
|
|
181
|
-
# rubocop:enable Metrics/BlockLength
|
|
182
180
|
options
|
|
183
181
|
end
|
|
184
182
|
end
|
|
@@ -57,11 +57,12 @@ module WormRunner
|
|
|
57
57
|
stdout_content = nil
|
|
58
58
|
|
|
59
59
|
begin
|
|
60
|
-
stdout_content = if $stdout.tty? && @output_stdout
|
|
60
|
+
stdout_content = if $stdout.tty? && (@output_stdout || @output_live)
|
|
61
61
|
oc = RubyProgress::OutputCapture.new(
|
|
62
62
|
command: @command,
|
|
63
63
|
lines: @output_lines || 3,
|
|
64
|
-
position: @output_position || :above
|
|
64
|
+
position: @output_position || :above,
|
|
65
|
+
stream: @output_live || false
|
|
65
66
|
)
|
|
66
67
|
oc.start
|
|
67
68
|
@output_capture = oc
|
|
@@ -69,7 +70,13 @@ module WormRunner
|
|
|
69
70
|
oc.wait
|
|
70
71
|
end
|
|
71
72
|
@output_capture = nil
|
|
72
|
-
|
|
73
|
+
# For non-live capture, flush_to handles the output directly
|
|
74
|
+
if @output_live
|
|
75
|
+
oc.lines.join("\n")
|
|
76
|
+
else
|
|
77
|
+
oc.flush_to($stdout) if @output_stdout
|
|
78
|
+
nil # Don't return content since it's already been flushed
|
|
79
|
+
end
|
|
73
80
|
else
|
|
74
81
|
animate do
|
|
75
82
|
Open3.popen3(@command) do |_stdin, stdout, stderr, wait_thr|
|
|
@@ -233,8 +240,12 @@ module WormRunner
|
|
|
233
240
|
$stderr.print "\r\e[2K"
|
|
234
241
|
|
|
235
242
|
if (frame_count % 10).zero?
|
|
236
|
-
|
|
237
|
-
|
|
243
|
+
# Use ANSI save/restore to clear the previous line without moving the
|
|
244
|
+
# global cursor position. This prevents the animation from erasing
|
|
245
|
+
# reserved output lines that we draw elsewhere.
|
|
246
|
+
$stderr.print "\e7" # save
|
|
247
|
+
$stderr.print "\e[1A\e[2K\r"
|
|
248
|
+
$stderr.print "\e8" # restore
|
|
238
249
|
end
|
|
239
250
|
|
|
240
251
|
$stderr.print "#{message_part}#{generate_dots(position, direction)}"
|
data/lib/ruby-progress/daemon.rb
CHANGED
|
@@ -4,6 +4,8 @@ require 'json'
|
|
|
4
4
|
require 'fileutils'
|
|
5
5
|
|
|
6
6
|
module RubyProgress
|
|
7
|
+
# Daemon helpers for backgrounding progress indicators.
|
|
8
|
+
# Provides minimal daemonization, PID file management, and simple control-message signaling.
|
|
7
9
|
module Daemon
|
|
8
10
|
module_function
|
|
9
11
|
|
|
@@ -15,70 +17,6 @@ module RubyProgress
|
|
|
15
17
|
"#{pid_file}.msg"
|
|
16
18
|
end
|
|
17
19
|
|
|
18
|
-
# Resolve a job directory for the daemon based on pid_file or name.
|
|
19
|
-
# If pid_file is '/tmp/ruby-progress/mytask.pid' -> jobs dir '/tmp/ruby-progress/mytask.jobs'
|
|
20
|
-
def job_dir_for_pid(pid_file)
|
|
21
|
-
base = File.basename(pid_file, '.*')
|
|
22
|
-
File.join(File.dirname(pid_file), "#{base}.jobs")
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
# Process available job files in job_dir. Each job is a JSON file with {"id","command","meta"}.
|
|
26
|
-
# This method polls the directory and yields each parsed job hash to the provided block.
|
|
27
|
-
def process_jobs(job_dir, poll_interval: 0.2)
|
|
28
|
-
FileUtils.mkdir_p(job_dir)
|
|
29
|
-
|
|
30
|
-
loop do
|
|
31
|
-
# Accept any job file ending in .json (UUID filenames are common)
|
|
32
|
-
# Ignore processed-* archives and temporary files (e.g., .tmp)
|
|
33
|
-
files = Dir.children(job_dir).select do |f|
|
|
34
|
-
f.end_with?('.json') && !f.start_with?('processed-')
|
|
35
|
-
end.sort
|
|
36
|
-
|
|
37
|
-
files.each do |f|
|
|
38
|
-
path = File.join(job_dir, f)
|
|
39
|
-
processing = "#{path}.processing"
|
|
40
|
-
|
|
41
|
-
# Claim the file atomically
|
|
42
|
-
begin
|
|
43
|
-
File.rename(path, processing)
|
|
44
|
-
rescue StandardError
|
|
45
|
-
next
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
job = begin
|
|
49
|
-
JSON.parse(File.read(processing))
|
|
50
|
-
rescue StandardError
|
|
51
|
-
FileUtils.rm_f(processing)
|
|
52
|
-
next
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
begin
|
|
56
|
-
yielded = yield(job)
|
|
57
|
-
|
|
58
|
-
# on success, write .result info and merge any returned info
|
|
59
|
-
result = { 'id' => job['id'], 'status' => 'done', 'time' => Time.now.to_i }
|
|
60
|
-
if yielded.is_a?(Hash)
|
|
61
|
-
# ensure string keys
|
|
62
|
-
extra = yielded.transform_keys(&:to_s)
|
|
63
|
-
result.merge!(extra)
|
|
64
|
-
end
|
|
65
|
-
File.write("#{processing}.result", result.to_json)
|
|
66
|
-
rescue StandardError => e
|
|
67
|
-
result = { 'id' => job['id'], 'status' => 'error', 'error' => e.message }
|
|
68
|
-
File.write("#{processing}.result", result.to_json)
|
|
69
|
-
ensure
|
|
70
|
-
begin
|
|
71
|
-
FileUtils.mv(processing, File.join(job_dir, "processed-#{f}"))
|
|
72
|
-
rescue StandardError
|
|
73
|
-
FileUtils.rm_f(processing)
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
sleep(poll_interval)
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
|
|
82
20
|
def show_status(pid_file)
|
|
83
21
|
if File.exist?(pid_file)
|
|
84
22
|
pid = File.read(pid_file).strip
|
|
@@ -58,12 +58,8 @@ module RubyProgress
|
|
|
58
58
|
pid_file = resolve_pid_file(options, :daemon_name)
|
|
59
59
|
options[:pid_file] = pid_file
|
|
60
60
|
|
|
61
|
-
#
|
|
62
|
-
|
|
63
|
-
PrgCLI.backgroundize
|
|
64
|
-
else
|
|
65
|
-
PrgCLI.daemonize
|
|
66
|
-
end
|
|
61
|
+
# Background without detaching so progress bar remains visible in current terminal
|
|
62
|
+
PrgCLI.backgroundize
|
|
67
63
|
|
|
68
64
|
run_daemon_mode(options, parsed_style)
|
|
69
65
|
elsif options[:current]
|
|
@@ -120,71 +116,6 @@ module RubyProgress
|
|
|
120
116
|
begin
|
|
121
117
|
fill_bar.render # Show initial empty bar
|
|
122
118
|
|
|
123
|
-
# Start job processor thread for fill (so daemon can accept jobs)
|
|
124
|
-
job_dir = RubyProgress::Daemon.job_dir_for_pid(pid_file)
|
|
125
|
-
Thread.new do
|
|
126
|
-
RubyProgress::Daemon.process_jobs(job_dir) do |job|
|
|
127
|
-
jid = job['id'] || SecureRandom.uuid
|
|
128
|
-
log_path = begin
|
|
129
|
-
File.join(File.dirname(job_dir), "#{jid}.log")
|
|
130
|
-
rescue StandardError
|
|
131
|
-
nil
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
if job['command']
|
|
135
|
-
oc = RubyProgress::OutputCapture.new(
|
|
136
|
-
command: job['command'],
|
|
137
|
-
lines: options[:output_lines] || 3,
|
|
138
|
-
position: options[:output_position] || :above,
|
|
139
|
-
log_path: log_path
|
|
140
|
-
)
|
|
141
|
-
oc.start
|
|
142
|
-
|
|
143
|
-
fill_bar.instance_variable_set(:@output_capture, oc)
|
|
144
|
-
oc.wait
|
|
145
|
-
captured = oc.lines.join("\n")
|
|
146
|
-
exit_status = oc.exit_status
|
|
147
|
-
fill_bar.instance_variable_set(:@output_capture, nil)
|
|
148
|
-
|
|
149
|
-
success = exit_status.to_i.zero?
|
|
150
|
-
if job['message']
|
|
151
|
-
RubyProgress::Utils.display_completion(
|
|
152
|
-
job['message'],
|
|
153
|
-
success: success,
|
|
154
|
-
show_checkmark: job['checkmark'] || false,
|
|
155
|
-
output_stream: :stdout,
|
|
156
|
-
icons: { success: options[:success_icon], error: options[:error_icon] }
|
|
157
|
-
)
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
{ 'exit_status' => exit_status, 'output' => captured, 'log_path' => log_path }
|
|
161
|
-
|
|
162
|
-
elsif job['action']
|
|
163
|
-
case job['action']
|
|
164
|
-
when 'advance'
|
|
165
|
-
fill_bar.advance
|
|
166
|
-
{ 'status' => 'done', 'action' => 'advance' }
|
|
167
|
-
when 'percent'
|
|
168
|
-
val = job['value'] || job['percent'] || 0
|
|
169
|
-
fill_bar.percent = val.to_f
|
|
170
|
-
{ 'status' => 'done', 'action' => 'percent', 'value' => val }
|
|
171
|
-
when 'complete'
|
|
172
|
-
fill_bar.complete
|
|
173
|
-
{ 'status' => 'done', 'action' => 'complete' }
|
|
174
|
-
when 'cancel'
|
|
175
|
-
fill_bar.cancel
|
|
176
|
-
{ 'status' => 'done', 'action' => 'cancel' }
|
|
177
|
-
else
|
|
178
|
-
{ 'status' => 'error', 'error' => 'unknown action' }
|
|
179
|
-
end
|
|
180
|
-
else
|
|
181
|
-
{ 'status' => 'error', 'error' => 'no command or action provided' }
|
|
182
|
-
end
|
|
183
|
-
rescue StandardError
|
|
184
|
-
nil
|
|
185
|
-
end
|
|
186
|
-
end
|
|
187
|
-
|
|
188
119
|
# Set up signal handlers for daemon control
|
|
189
120
|
stop_requested = false
|
|
190
121
|
Signal.trap('INT') { stop_requested = true }
|
|
@@ -298,7 +229,8 @@ module RubyProgress
|
|
|
298
229
|
command: options[:command],
|
|
299
230
|
lines: options[:output_lines] || 3,
|
|
300
231
|
position: options[:output_position] || :above,
|
|
301
|
-
log_path: nil
|
|
232
|
+
log_path: nil,
|
|
233
|
+
stream: options[:stdout] || options[:stdout_live]
|
|
302
234
|
)
|
|
303
235
|
oc.start
|
|
304
236
|
|
|
@@ -3,26 +3,68 @@
|
|
|
3
3
|
require 'pty'
|
|
4
4
|
require 'io/console'
|
|
5
5
|
require 'English'
|
|
6
|
+
require 'fileutils'
|
|
7
|
+
|
|
8
|
+
begin
|
|
9
|
+
require 'tty-cursor'
|
|
10
|
+
require 'tty-screen'
|
|
11
|
+
rescue LoadError
|
|
12
|
+
# fall back to ANSI sequences when tty gems are not installed
|
|
13
|
+
end
|
|
6
14
|
|
|
7
15
|
module RubyProgress
|
|
8
|
-
#
|
|
9
|
-
# for printing captured output while the animation
|
|
16
|
+
# PTY-based live output capture that reserves a small terminal area
|
|
17
|
+
# for printing captured output while the animation draws elsewhere.
|
|
10
18
|
class OutputCapture
|
|
11
19
|
attr_reader :exit_status
|
|
12
20
|
|
|
13
|
-
def initialize(command:, lines: 3, position: :above, log_path: nil)
|
|
21
|
+
def initialize(command:, lines: 3, position: :above, log_path: nil, stream: false, debug: nil)
|
|
14
22
|
@command = command
|
|
15
|
-
|
|
16
|
-
@
|
|
23
|
+
# Coerce lines into a positive Integer
|
|
24
|
+
@lines = (lines || 3).to_i
|
|
25
|
+
@lines = 1 if @lines < 1
|
|
26
|
+
|
|
27
|
+
# Normalize position (accept :top/:bottom or :above/:below or strings)
|
|
28
|
+
pos = position.respond_to?(:to_sym) ? position.to_sym : position
|
|
29
|
+
@position = case pos
|
|
30
|
+
when :top, 'top' then :above
|
|
31
|
+
when :bottom, 'bottom' then :below
|
|
32
|
+
when :above, 'above' then :above
|
|
33
|
+
when :below, 'below' then :below
|
|
34
|
+
else
|
|
35
|
+
:above
|
|
36
|
+
end
|
|
37
|
+
|
|
17
38
|
@buffer = []
|
|
18
39
|
@buf_mutex = Mutex.new
|
|
19
40
|
@stop = false
|
|
20
41
|
@log_path = log_path
|
|
21
42
|
@log_file = nil
|
|
43
|
+
@stream = stream
|
|
44
|
+
|
|
45
|
+
@debug = if debug.nil?
|
|
46
|
+
ENV.fetch('RUBY_PROGRESS_DEBUG', nil) && ENV['RUBY_PROGRESS_DEBUG'] != '0'
|
|
47
|
+
else
|
|
48
|
+
debug
|
|
49
|
+
end
|
|
50
|
+
@debug_path = '/tmp/ruby-progress-debug.log'
|
|
51
|
+
|
|
52
|
+
if @debug
|
|
53
|
+
begin
|
|
54
|
+
FileUtils.mkdir_p(File.dirname(@debug_path))
|
|
55
|
+
File.open(@debug_path, 'w') { |f| f.puts("debug start: #{Time.now}") }
|
|
56
|
+
rescue StandardError
|
|
57
|
+
@debug = false
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Debug: log init if requested via ENV or explicit debug flag
|
|
62
|
+
debug_log("init: position=#{@position.inspect}; lines=#{@lines}")
|
|
22
63
|
end
|
|
23
64
|
|
|
24
|
-
# Start the child process
|
|
65
|
+
# Start capturing the child process. Returns self.
|
|
25
66
|
def start
|
|
67
|
+
reserve_space($stderr) if @stream
|
|
26
68
|
@reader_thread = Thread.new { spawn_and_read }
|
|
27
69
|
self
|
|
28
70
|
end
|
|
@@ -32,45 +74,98 @@ module RubyProgress
|
|
|
32
74
|
@reader_thread&.join
|
|
33
75
|
end
|
|
34
76
|
|
|
35
|
-
# Wait for the reader thread to complete
|
|
36
77
|
def wait
|
|
37
78
|
@reader_thread&.join
|
|
38
79
|
end
|
|
39
80
|
|
|
40
|
-
# Return snapshot of buffered lines (thread-safe)
|
|
41
81
|
def lines
|
|
42
82
|
@buf_mutex.synchronize { @buffer.dup }
|
|
43
83
|
end
|
|
44
84
|
|
|
45
|
-
# Returns whether the reader thread is still alive
|
|
46
85
|
def alive?
|
|
47
86
|
@reader_thread&.alive? || false
|
|
48
87
|
end
|
|
49
88
|
|
|
50
|
-
# Redraw
|
|
51
|
-
# io - IO object to write to (default $stderr)
|
|
89
|
+
# Redraw the reserved area using the current buffered lines.
|
|
52
90
|
def redraw(io = $stderr)
|
|
53
91
|
buf = lines
|
|
54
|
-
|
|
92
|
+
debug_log("redraw called; buffer=#{buf.size}; lines=#{@lines}; position=#{@position}")
|
|
93
|
+
|
|
94
|
+
# If not streaming live to the terminal, don't redraw during capture.
|
|
95
|
+
return unless @stream
|
|
55
96
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
97
|
+
cols = if defined?(TTY::Screen)
|
|
98
|
+
TTY::Screen.columns
|
|
99
|
+
else
|
|
100
|
+
IO.console.winsize[1]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
display_lines = Array.new(@lines, '')
|
|
104
|
+
if buf.empty?
|
|
105
|
+
# leave display_lines as blanks
|
|
106
|
+
elsif buf.size <= @lines
|
|
107
|
+
buf.each_with_index { |l, i| display_lines[i] = l.to_s }
|
|
108
|
+
else
|
|
109
|
+
buf.last(@lines).each_with_index { |l, i| display_lines[i] = l.to_s }
|
|
61
110
|
end
|
|
62
111
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
112
|
+
if defined?(TTY::Cursor)
|
|
113
|
+
cursor = TTY::Cursor
|
|
114
|
+
io.print cursor.save
|
|
115
|
+
|
|
116
|
+
if @position == :above
|
|
117
|
+
io.print cursor.up(@lines)
|
|
118
|
+
else
|
|
119
|
+
io.print cursor.down(1)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
display_lines.each_with_index do |line, idx|
|
|
123
|
+
io.print cursor.clear_line
|
|
124
|
+
io.print line[0, cols]
|
|
125
|
+
io.print cursor.down(1) unless idx == display_lines.length - 1
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
io.print cursor.restore
|
|
129
|
+
debug_log('redraw finished (TTY)')
|
|
130
|
+
else
|
|
131
|
+
io.print "\e7"
|
|
132
|
+
|
|
133
|
+
if @position == :above
|
|
134
|
+
io.print "\e[#{@lines}A"
|
|
135
|
+
else
|
|
136
|
+
io.print "\e[1B"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
display_lines.each_with_index do |line, idx|
|
|
140
|
+
io.print "\e[2K\r"
|
|
141
|
+
io.print line[0, cols]
|
|
142
|
+
io.print "\e[1B" unless idx == display_lines.length - 1
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
io.print "\e8"
|
|
146
|
+
debug_log('redraw finished (ANSI)')
|
|
71
147
|
end
|
|
72
|
-
|
|
148
|
+
|
|
73
149
|
io.flush
|
|
150
|
+
rescue StandardError => e
|
|
151
|
+
debug_log("redraw error: #{e.class}: #{e.message}")
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Flush the buffered lines to the given IO (defaults to STDOUT).
|
|
155
|
+
# This is used when capturing non-live output: capture silently during
|
|
156
|
+
# the run and emit all captured output at the end.
|
|
157
|
+
def flush_to(io = $stdout)
|
|
158
|
+
buf = lines
|
|
159
|
+
return if buf.empty?
|
|
160
|
+
|
|
161
|
+
begin
|
|
162
|
+
buf.each do |line|
|
|
163
|
+
io.puts(line)
|
|
164
|
+
end
|
|
165
|
+
io.flush
|
|
166
|
+
rescue StandardError => e
|
|
167
|
+
debug_log("flush_to error: #{e.class}: #{e.message}")
|
|
168
|
+
end
|
|
74
169
|
end
|
|
75
170
|
|
|
76
171
|
private
|
|
@@ -78,13 +173,21 @@ module RubyProgress
|
|
|
78
173
|
def spawn_and_read
|
|
79
174
|
PTY.spawn(@command) do |reader, _writer, pid|
|
|
80
175
|
@child_pid = pid
|
|
176
|
+
debug_log("spawned pid=#{pid} cmd=#{@command}")
|
|
177
|
+
|
|
81
178
|
until reader.eof? || @stop
|
|
82
|
-
|
|
179
|
+
ready = if reader.respond_to?(:wait_readable)
|
|
180
|
+
reader.wait_readable(0.1)
|
|
181
|
+
else
|
|
182
|
+
IO.select([reader], nil, nil, 0.1)
|
|
183
|
+
end
|
|
184
|
+
next unless ready
|
|
83
185
|
|
|
84
186
|
chunk = reader.read_nonblock(4096, exception: false)
|
|
85
187
|
next if chunk.nil? || chunk.empty?
|
|
86
188
|
|
|
87
|
-
|
|
189
|
+
debug_log("read chunk=#{chunk.inspect}")
|
|
190
|
+
|
|
88
191
|
if @log_path && !@log_file
|
|
89
192
|
begin
|
|
90
193
|
FileUtils.mkdir_p(File.dirname(@log_path))
|
|
@@ -95,29 +198,26 @@ module RubyProgress
|
|
|
95
198
|
end
|
|
96
199
|
|
|
97
200
|
process_chunk(chunk)
|
|
201
|
+
debug_log("after process_chunk buffer_size=#{@buffer.size}")
|
|
202
|
+
|
|
98
203
|
next unless @log_file
|
|
99
204
|
|
|
100
205
|
begin
|
|
101
206
|
@log_file.write(chunk)
|
|
102
207
|
@log_file.flush
|
|
103
208
|
rescue StandardError
|
|
104
|
-
# ignore
|
|
209
|
+
# ignore
|
|
105
210
|
end
|
|
106
211
|
end
|
|
107
212
|
end
|
|
213
|
+
|
|
108
214
|
begin
|
|
109
215
|
Process.wait(@child_pid) if @child_pid
|
|
110
216
|
@exit_status = $CHILD_STATUS.exitstatus if $CHILD_STATUS
|
|
111
217
|
rescue StandardError
|
|
112
218
|
@exit_status = nil
|
|
113
219
|
ensure
|
|
114
|
-
|
|
115
|
-
begin
|
|
116
|
-
@log_file.close
|
|
117
|
-
rescue StandardError
|
|
118
|
-
nil
|
|
119
|
-
end
|
|
120
|
-
end
|
|
220
|
+
@log_file&.close
|
|
121
221
|
end
|
|
122
222
|
rescue Errno::EIO
|
|
123
223
|
# PTY finished
|
|
@@ -125,12 +225,49 @@ module RubyProgress
|
|
|
125
225
|
|
|
126
226
|
def process_chunk(chunk)
|
|
127
227
|
@buf_mutex.synchronize do
|
|
128
|
-
# split into lines, keep last N lines
|
|
129
228
|
chunk.each_line do |line|
|
|
130
229
|
@buffer << line.chomp
|
|
131
230
|
@buffer.shift while @buffer.size > @lines
|
|
132
231
|
end
|
|
133
232
|
end
|
|
233
|
+
|
|
234
|
+
debug_log("process_chunk: buffer=#{@buffer.inspect}")
|
|
235
|
+
redraw($stderr) if @stream
|
|
236
|
+
rescue StandardError => e
|
|
237
|
+
debug_log("process_chunk error: #{e.class}: #{e.message}")
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def debug_log(msg)
|
|
241
|
+
return unless ENV['RUBY_PROGRESS_DEBUG'] || @debug
|
|
242
|
+
|
|
243
|
+
begin
|
|
244
|
+
File.open(@debug_path, 'a') do |f|
|
|
245
|
+
f.puts("#{Time.now.iso8601} PID=#{Process.pid} #{msg}")
|
|
246
|
+
end
|
|
247
|
+
rescue StandardError
|
|
248
|
+
# swallow logging errors
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def reserve_space(io = $stderr)
|
|
253
|
+
return unless io.tty?
|
|
254
|
+
|
|
255
|
+
debug_log("reserve_space called; position=#{@position.inspect}; lines=#{@lines}")
|
|
256
|
+
|
|
257
|
+
if @position == :above
|
|
258
|
+
# Insert lines above current cursor using CSI n L
|
|
259
|
+
io.print "\e[#{@lines}L"
|
|
260
|
+
debug_log("reserve_space: inserted #{@lines} lines for :above")
|
|
261
|
+
else
|
|
262
|
+
# Print newlines then move cursor back up so animation stays above
|
|
263
|
+
io.print("\n" * @lines)
|
|
264
|
+
io.print "\e[#{@lines}A"
|
|
265
|
+
debug_log("reserve_space: printed #{@lines} newlines and moved up #{@lines} for :below")
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
io.flush
|
|
269
|
+
rescue StandardError => e
|
|
270
|
+
debug_log("reserve_space error: #{e.class}: #{e.message}")
|
|
134
271
|
end
|
|
135
272
|
end
|
|
136
273
|
end
|
data/lib/ruby-progress/utils.rb
CHANGED
|
@@ -41,12 +41,17 @@ module RubyProgress
|
|
|
41
41
|
def self.display_completion(message, success: true, show_checkmark: false, output_stream: :warn, icons: {})
|
|
42
42
|
return unless message
|
|
43
43
|
|
|
44
|
-
mark
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
# Determine the mark to show. If checkmarks are enabled, prefer the
|
|
45
|
+
# default icons but allow overrides via icons hash. If checkmarks are not
|
|
46
|
+
# enabled, still show a custom icon when provided via CLI options.
|
|
47
|
+
mark = ''
|
|
48
|
+
if show_checkmark
|
|
49
|
+
icon = success ? (icons[:success] || '✅') : (icons[:error] || '🛑')
|
|
50
|
+
mark = "#{icon} "
|
|
51
|
+
else
|
|
52
|
+
custom_icon = success ? icons[:success] : icons[:error]
|
|
53
|
+
mark = custom_icon ? "#{custom_icon} " : ''
|
|
54
|
+
end
|
|
50
55
|
|
|
51
56
|
formatted_message = "#{mark}#{message}"
|
|
52
57
|
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
module RubyProgress
|
|
4
4
|
# Main gem version
|
|
5
|
-
VERSION = '1.3.
|
|
5
|
+
VERSION = '1.3.5'
|
|
6
6
|
|
|
7
7
|
# Component-specific versions (patch bumps)
|
|
8
|
-
WORM_VERSION = '1.1.
|
|
9
|
-
TWIRL_VERSION = '1.1.
|
|
10
|
-
RIPPLE_VERSION = '1.1.
|
|
11
|
-
FILL_VERSION = '1.0.
|
|
8
|
+
WORM_VERSION = '1.1.5'
|
|
9
|
+
TWIRL_VERSION = '1.1.5'
|
|
10
|
+
RIPPLE_VERSION = '1.1.5'
|
|
11
|
+
FILL_VERSION = '1.0.5'
|
|
12
12
|
end
|
data/lib/ruby-progress/worm.rb
CHANGED
|
@@ -65,6 +65,9 @@ module RubyProgress
|
|
|
65
65
|
@error_text = options[:error]
|
|
66
66
|
@show_checkmark = options[:checkmark] || false
|
|
67
67
|
@output_stdout = options[:stdout] || false
|
|
68
|
+
@output_lines = options[:output_lines]
|
|
69
|
+
@output_position = options[:output_position]
|
|
70
|
+
@output_live = options[:stdout_live] || false
|
|
68
71
|
@direction_mode = options[:direction] || :bidirectional
|
|
69
72
|
@start_chars, @end_chars = RubyProgress::Utils.parse_ends(options[:ends])
|
|
70
73
|
@running = false
|
|
@@ -197,67 +200,11 @@ module RubyProgress
|
|
|
197
200
|
}
|
|
198
201
|
end
|
|
199
202
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
message_part = @message && !@message.empty? ? "#{@message} " : ''
|
|
206
|
-
# Enhanced line clearing for better daemon mode behavior
|
|
207
|
-
$stderr.print "\r\e[2K#{@start_chars}#{message_part}#{generate_dots(position, direction)}#{@end_chars}"
|
|
208
|
-
$stderr.flush
|
|
209
|
-
|
|
210
|
-
sleep @speed
|
|
211
|
-
|
|
212
|
-
position += direction
|
|
213
|
-
if position >= @length - 1
|
|
214
|
-
if @direction_mode == :forward_only
|
|
215
|
-
position = 0
|
|
216
|
-
else
|
|
217
|
-
direction = -1
|
|
218
|
-
end
|
|
219
|
-
elsif position <= 0
|
|
220
|
-
direction = 1
|
|
221
|
-
end
|
|
222
|
-
end
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
# Enhanced animation loop for daemon mode with aggressive line clearing
|
|
226
|
-
def animation_loop_daemon_mode(stop_requested_proc: -> { false })
|
|
227
|
-
position = 0
|
|
228
|
-
direction = 1
|
|
229
|
-
frame_count = 0
|
|
230
|
-
|
|
231
|
-
while @running && !stop_requested_proc.call
|
|
232
|
-
message_part = @message && !@message.empty? ? "#{@message} " : ''
|
|
233
|
-
|
|
234
|
-
# Always clear current line
|
|
235
|
-
$stderr.print "\r\e[2K"
|
|
236
|
-
|
|
237
|
-
# Every few frames, use aggressive clearing to handle interruptions
|
|
238
|
-
if (frame_count % 10).zero?
|
|
239
|
-
$stderr.print "\e[1A\e[2K" # Move up and clear that line too (in case of interruption)
|
|
240
|
-
$stderr.print "\r" # Return to start
|
|
241
|
-
end
|
|
242
|
-
|
|
243
|
-
$stderr.print "#{message_part}#{generate_dots(position, direction)}"
|
|
244
|
-
$stderr.flush
|
|
245
|
-
|
|
246
|
-
sleep @speed
|
|
247
|
-
frame_count += 1
|
|
248
|
-
|
|
249
|
-
position += direction
|
|
250
|
-
if position >= @length - 1
|
|
251
|
-
if @direction_mode == :forward_only
|
|
252
|
-
position = 0
|
|
253
|
-
else
|
|
254
|
-
direction = -1
|
|
255
|
-
end
|
|
256
|
-
elsif position <= 0
|
|
257
|
-
direction = 1
|
|
258
|
-
end
|
|
259
|
-
end
|
|
260
|
-
end
|
|
203
|
+
# animation_loop and animation_loop_daemon_mode are implemented in
|
|
204
|
+
# the WormRunner module so they can share the redraw behavior that
|
|
205
|
+
# integrates with RubyProgress::OutputCapture. Do not redefine them
|
|
206
|
+
# here, otherwise the module implementations (which call
|
|
207
|
+
# @output_capture&.redraw) will be overridden.
|
|
261
208
|
|
|
262
209
|
def generate_dots(ripple_position, direction)
|
|
263
210
|
dots = Array.new(@length) { @style[:baseline] }
|