ruby-progress 1.2.4 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +26 -45
- data/CHANGELOG.md +38 -1
- data/Gemfile.lock +1 -1
- data/README.md +126 -133
- data/Rakefile +0 -3
- data/bin/prg +50 -0
- data/demo_screencast.rb +180 -36
- data/examples/daemon_job_example.sh +25 -0
- data/lib/ruby-progress/cli/fill_options.rb +73 -1
- data/lib/ruby-progress/cli/job_cli.rb +159 -0
- data/lib/ruby-progress/cli/ripple_cli.rb +88 -9
- data/lib/ruby-progress/cli/ripple_options.rb +22 -0
- data/lib/ruby-progress/cli/twirl_options.rb +30 -1
- data/lib/ruby-progress/cli/twirl_runner.rb +62 -5
- data/lib/ruby-progress/cli/twirl_spinner.rb +19 -2
- data/lib/ruby-progress/cli/worm_cli.rb +61 -19
- data/lib/ruby-progress/cli/worm_options.rb +32 -3
- data/lib/ruby-progress/cli/worm_runner.rb +37 -17
- data/lib/ruby-progress/daemon.rb +65 -0
- data/lib/ruby-progress/fill.rb +9 -3
- data/lib/ruby-progress/fill_cli.rb +189 -38
- data/lib/ruby-progress/output_capture.rb +136 -0
- data/lib/ruby-progress/ripple.rb +4 -2
- data/lib/ruby-progress/utils.rb +47 -26
- data/lib/ruby-progress/version.rb +6 -6
- data/lib/ruby-progress/worm.rb +8 -7
- data/screencast +26 -0
- metadata +5 -2
- data/ruby-progress.gemspec +0 -40
data/lib/ruby-progress/daemon.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
|
+
require 'fileutils'
|
|
4
5
|
|
|
5
6
|
module RubyProgress
|
|
6
7
|
module Daemon
|
|
@@ -14,6 +15,70 @@ module RubyProgress
|
|
|
14
15
|
"#{pid_file}.msg"
|
|
15
16
|
end
|
|
16
17
|
|
|
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
|
+
|
|
17
82
|
def show_status(pid_file)
|
|
18
83
|
if File.exist?(pid_file)
|
|
19
84
|
pid = File.read(pid_file).strip
|
data/lib/ruby-progress/fill.rb
CHANGED
|
@@ -25,6 +25,7 @@ module RubyProgress
|
|
|
25
25
|
@current_progress = 0
|
|
26
26
|
@success_message = options[:success]
|
|
27
27
|
@error_message = options[:error]
|
|
28
|
+
@output_capture = options[:output_capture]
|
|
28
29
|
|
|
29
30
|
# Parse --ends characters
|
|
30
31
|
if options[:ends]
|
|
@@ -82,6 +83,9 @@ module RubyProgress
|
|
|
82
83
|
|
|
83
84
|
# Render the current progress bar to stderr
|
|
84
85
|
def render
|
|
86
|
+
# First redraw captured output (if any) so it appears above/below the bar
|
|
87
|
+
@output_capture&.redraw($stderr)
|
|
88
|
+
|
|
85
89
|
filled = @style[:full] * @current_progress
|
|
86
90
|
empty = @style[:empty] * (@length - @current_progress)
|
|
87
91
|
bar = "#{@start_chars}#{filled}#{empty}#{@end_chars}"
|
|
@@ -91,7 +95,7 @@ module RubyProgress
|
|
|
91
95
|
end
|
|
92
96
|
|
|
93
97
|
# Complete the progress bar and show success message
|
|
94
|
-
def complete(message = nil)
|
|
98
|
+
def complete(message = nil, icons: {})
|
|
95
99
|
@current_progress = @length
|
|
96
100
|
render
|
|
97
101
|
|
|
@@ -101,7 +105,8 @@ module RubyProgress
|
|
|
101
105
|
completion_message,
|
|
102
106
|
success: true,
|
|
103
107
|
show_checkmark: true,
|
|
104
|
-
output_stream: :warn
|
|
108
|
+
output_stream: :warn,
|
|
109
|
+
icons: icons
|
|
105
110
|
)
|
|
106
111
|
else
|
|
107
112
|
$stderr.puts # Just add a newline if no message
|
|
@@ -120,7 +125,8 @@ module RubyProgress
|
|
|
120
125
|
error_msg,
|
|
121
126
|
success: false,
|
|
122
127
|
show_checkmark: true,
|
|
123
|
-
output_stream: :warn
|
|
128
|
+
output_stream: :warn,
|
|
129
|
+
icons: {}
|
|
124
130
|
)
|
|
125
131
|
end
|
|
126
132
|
|
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
require 'optparse'
|
|
4
4
|
require 'fileutils'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'securerandom'
|
|
5
7
|
require_relative 'cli/fill_options'
|
|
8
|
+
require_relative 'output_capture'
|
|
6
9
|
|
|
7
10
|
module RubyProgress
|
|
8
11
|
# CLI module for Fill command
|
|
12
|
+
# rubocop:disable Metrics/ClassLength
|
|
9
13
|
module FillCLI
|
|
10
14
|
class << self
|
|
11
15
|
def run
|
|
@@ -34,11 +38,14 @@ module RubyProgress
|
|
|
34
38
|
|
|
35
39
|
# Handle daemon control first
|
|
36
40
|
if options[:status] || options[:stop]
|
|
37
|
-
pid_file = options
|
|
41
|
+
pid_file = resolve_pid_file(options, :status_name)
|
|
38
42
|
if options[:status]
|
|
39
43
|
Daemon.show_status(pid_file)
|
|
40
44
|
else
|
|
41
|
-
Daemon.stop_daemon_by_pid_file(pid_file
|
|
45
|
+
Daemon.stop_daemon_by_pid_file(pid_file,
|
|
46
|
+
message: options[:stop_success],
|
|
47
|
+
checkmark: options[:stop_checkmark],
|
|
48
|
+
error: !options[:stop_error].nil?)
|
|
42
49
|
end
|
|
43
50
|
exit
|
|
44
51
|
end
|
|
@@ -47,6 +54,17 @@ module RubyProgress
|
|
|
47
54
|
parsed_style = parse_fill_style(options[:style])
|
|
48
55
|
|
|
49
56
|
if options[:daemon]
|
|
57
|
+
# Resolve pid file and honor daemon-as/name
|
|
58
|
+
pid_file = resolve_pid_file(options, :daemon_name)
|
|
59
|
+
options[:pid_file] = pid_file
|
|
60
|
+
|
|
61
|
+
# Detach or background without detaching based on --no-detach
|
|
62
|
+
if options[:no_detach]
|
|
63
|
+
PrgCLI.backgroundize
|
|
64
|
+
else
|
|
65
|
+
PrgCLI.daemonize
|
|
66
|
+
end
|
|
67
|
+
|
|
50
68
|
run_daemon_mode(options, parsed_style)
|
|
51
69
|
elsif options[:current]
|
|
52
70
|
show_current_percentage(options, parsed_style)
|
|
@@ -61,6 +79,14 @@ module RubyProgress
|
|
|
61
79
|
|
|
62
80
|
private
|
|
63
81
|
|
|
82
|
+
def resolve_pid_file(options, name_key = :daemon_name)
|
|
83
|
+
return options[:pid_file] if options[:pid_file]
|
|
84
|
+
|
|
85
|
+
return "/tmp/ruby-progress/#{options[name_key]}.pid" if options[name_key]
|
|
86
|
+
|
|
87
|
+
'/tmp/ruby-progress/fill.pid'
|
|
88
|
+
end
|
|
89
|
+
|
|
64
90
|
def parse_fill_style(style_option)
|
|
65
91
|
case style_option
|
|
66
92
|
when String
|
|
@@ -75,9 +101,6 @@ module RubyProgress
|
|
|
75
101
|
end
|
|
76
102
|
|
|
77
103
|
def run_daemon_mode(options, parsed_style)
|
|
78
|
-
# For daemon mode, detach the process
|
|
79
|
-
PrgCLI.daemonize
|
|
80
|
-
|
|
81
104
|
pid_file = options[:pid_file] || '/tmp/ruby-progress/fill.pid'
|
|
82
105
|
FileUtils.mkdir_p(File.dirname(pid_file))
|
|
83
106
|
File.write(pid_file, Process.pid.to_s)
|
|
@@ -97,6 +120,71 @@ module RubyProgress
|
|
|
97
120
|
begin
|
|
98
121
|
fill_bar.render # Show initial empty bar
|
|
99
122
|
|
|
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
|
+
|
|
100
188
|
# Set up signal handlers for daemon control
|
|
101
189
|
stop_requested = false
|
|
102
190
|
Signal.trap('INT') { stop_requested = true }
|
|
@@ -106,51 +194,82 @@ module RubyProgress
|
|
|
106
194
|
# Keep daemon alive until stop requested
|
|
107
195
|
sleep(0.1) until stop_requested
|
|
108
196
|
ensure
|
|
197
|
+
# If a control message file exists, print its contents like other CLIs
|
|
198
|
+
cmf = RubyProgress::Daemon.control_message_file(pid_file)
|
|
199
|
+
if File.exist?(cmf)
|
|
200
|
+
begin
|
|
201
|
+
data = JSON.parse(File.read(cmf))
|
|
202
|
+
message = data['message']
|
|
203
|
+
check = if data.key?('checkmark')
|
|
204
|
+
data['checkmark'] ? true : false
|
|
205
|
+
else
|
|
206
|
+
false
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
success_val = if data.key?('success')
|
|
210
|
+
data['success'] ? true : false
|
|
211
|
+
else
|
|
212
|
+
true
|
|
213
|
+
end
|
|
214
|
+
if message
|
|
215
|
+
RubyProgress::Utils.display_completion(
|
|
216
|
+
message,
|
|
217
|
+
success: success_val,
|
|
218
|
+
show_checkmark: check,
|
|
219
|
+
output_stream: :stdout,
|
|
220
|
+
icons: { success: options[:success_icon], error: options[:error_icon] }
|
|
221
|
+
)
|
|
222
|
+
end
|
|
223
|
+
rescue StandardError
|
|
224
|
+
# ignore
|
|
225
|
+
ensure
|
|
226
|
+
begin
|
|
227
|
+
File.delete(cmf)
|
|
228
|
+
rescue StandardError
|
|
229
|
+
nil
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
109
234
|
Fill.show_cursor
|
|
110
235
|
FileUtils.rm_f(pid_file)
|
|
111
236
|
end
|
|
112
237
|
end
|
|
113
238
|
|
|
114
|
-
def show_current_percentage(options, _parsed_style)
|
|
115
|
-
# Just output the percentage for scripting (default to 50% for demonstration)
|
|
116
|
-
percentage = options[:percent] || 50
|
|
117
|
-
puts percentage.to_f
|
|
118
|
-
end
|
|
119
|
-
|
|
120
239
|
def show_progress_report(options, parsed_style)
|
|
121
|
-
#
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
#
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
# Display the current progress bar and detailed status
|
|
137
|
-
fill_bar.render
|
|
138
|
-
puts "\nProgress Report:"
|
|
139
|
-
puts " Progress: #{report[:progress][0]}/#{report[:progress][1]}"
|
|
140
|
-
puts " Percent: #{report[:percent]}%"
|
|
141
|
-
puts " Completed: #{report[:completed] ? 'Yes' : 'No'}"
|
|
142
|
-
puts " Style: #{report[:style]}"
|
|
240
|
+
# Produce a simple scripting-friendly report to stdout
|
|
241
|
+
length = options[:length] || 20
|
|
242
|
+
percent = (options[:percent] || 50.0).to_f
|
|
243
|
+
style = parsed_style
|
|
244
|
+
|
|
245
|
+
fill = Fill.new(style: style, length: length)
|
|
246
|
+
fill.percent = percent
|
|
247
|
+
|
|
248
|
+
report = fill.report
|
|
249
|
+
puts 'Progress Report:'
|
|
250
|
+
puts "Progress: #{report[:progress][0]}/#{report[:progress][1]}"
|
|
251
|
+
puts "Percent: #{report[:percent]}%"
|
|
252
|
+
puts "Completed: #{report[:completed] ? 'Yes' : 'No'}"
|
|
253
|
+
puts "Style: #{report[:style].inspect}"
|
|
254
|
+
exit(0)
|
|
143
255
|
end
|
|
144
256
|
|
|
145
257
|
def handle_progress_commands(_options, _parsed_style)
|
|
146
|
-
# For progress commands
|
|
147
|
-
#
|
|
148
|
-
# we'd need IPC to communicate with the daemon
|
|
258
|
+
# For now the progress commands are only supported in daemon mode.
|
|
259
|
+
# Return a clear error to the caller (specs assert this message exists).
|
|
149
260
|
warn 'Progress commands require daemon mode implementation'
|
|
150
|
-
|
|
151
|
-
exit 1
|
|
261
|
+
exit(1)
|
|
152
262
|
end
|
|
153
263
|
|
|
264
|
+
def show_current_percentage(options, _parsed_style)
|
|
265
|
+
# For scripting and tests: print the current percentage to stdout and exit.
|
|
266
|
+
# If no explicit percent was provided, default to 50.0
|
|
267
|
+
percent = (options[:percent] || 50.0).to_f
|
|
268
|
+
$stdout.print("#{percent}\n")
|
|
269
|
+
exit(0)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Foreground / auto-advance / command mode when not daemonizing
|
|
154
273
|
def run_auto_advance_mode(options, parsed_style)
|
|
155
274
|
fill_options = {
|
|
156
275
|
style: parsed_style,
|
|
@@ -163,6 +282,7 @@ module RubyProgress
|
|
|
163
282
|
fill_bar = Fill.new(fill_options)
|
|
164
283
|
Fill.hide_cursor
|
|
165
284
|
|
|
285
|
+
oc = nil
|
|
166
286
|
begin
|
|
167
287
|
if options[:percent]
|
|
168
288
|
# Set to specific percentage
|
|
@@ -172,6 +292,34 @@ module RubyProgress
|
|
|
172
292
|
# For non-complete percentages, show the result briefly
|
|
173
293
|
sleep(0.1)
|
|
174
294
|
end
|
|
295
|
+
elsif options[:command]
|
|
296
|
+
# Run the command with OutputCapture
|
|
297
|
+
oc = RubyProgress::OutputCapture.new(
|
|
298
|
+
command: options[:command],
|
|
299
|
+
lines: options[:output_lines] || 3,
|
|
300
|
+
position: options[:output_position] || :above,
|
|
301
|
+
log_path: nil
|
|
302
|
+
)
|
|
303
|
+
oc.start
|
|
304
|
+
|
|
305
|
+
# Attach capture to the live fill instance so it can render output
|
|
306
|
+
fill_bar.instance_variable_set(:@output_capture, oc)
|
|
307
|
+
|
|
308
|
+
# While the command runs, keep redrawing the bar
|
|
309
|
+
sleep_time = case options[:speed]
|
|
310
|
+
when :fast then 0.1
|
|
311
|
+
when :medium, nil then 0.2
|
|
312
|
+
when :slow then 0.5
|
|
313
|
+
when Numeric then 1.0 / options[:speed]
|
|
314
|
+
else 0.3
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
fill_bar.render
|
|
318
|
+
while oc.alive?
|
|
319
|
+
sleep(sleep_time)
|
|
320
|
+
fill_bar.render
|
|
321
|
+
end
|
|
322
|
+
fill_bar.instance_variable_set(:@output_capture, nil)
|
|
175
323
|
else
|
|
176
324
|
# Auto-advance mode
|
|
177
325
|
sleep_time = case options[:speed]
|
|
@@ -188,10 +336,13 @@ module RubyProgress
|
|
|
188
336
|
fill_bar.advance
|
|
189
337
|
end
|
|
190
338
|
end
|
|
339
|
+
|
|
191
340
|
fill_bar.complete
|
|
192
341
|
rescue Interrupt
|
|
193
342
|
fill_bar.cancel
|
|
194
343
|
ensure
|
|
344
|
+
# Ensure we wait for capture thread to finish and show cursor
|
|
345
|
+
oc&.wait
|
|
195
346
|
Fill.show_cursor
|
|
196
347
|
end
|
|
197
348
|
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
|
|
@@ -153,7 +154,7 @@ module RubyProgress
|
|
|
153
154
|
RubyProgress::Utils.show_cursor
|
|
154
155
|
end
|
|
155
156
|
|
|
156
|
-
def self.complete(string, message, checkmark, success)
|
|
157
|
+
def self.complete(string, message, checkmark, success, icons: {})
|
|
157
158
|
display_message = message || (checkmark ? string : nil)
|
|
158
159
|
return unless display_message
|
|
159
160
|
|
|
@@ -161,7 +162,8 @@ module RubyProgress
|
|
|
161
162
|
display_message,
|
|
162
163
|
success: success,
|
|
163
164
|
show_checkmark: checkmark,
|
|
164
|
-
output_stream: :warn
|
|
165
|
+
output_stream: :warn,
|
|
166
|
+
icons: icons
|
|
165
167
|
)
|
|
166
168
|
end
|
|
167
169
|
|