ruby-progress 1.3.1 → 1.3.4
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 +32 -45
- data/CHANGELOG.md +67 -96
- data/Gemfile +2 -0
- data/Gemfile.lock +7 -1
- data/README.md +48 -0
- data/Rakefile +0 -3
- data/bin/prg +15 -0
- data/demo_screencast.rb +223 -71
- data/lib/ruby-progress/cli/fill_options.rb +57 -1
- data/lib/ruby-progress/cli/job_cli.rb +70 -10
- data/lib/ruby-progress/cli/ripple_cli.rb +34 -11
- data/lib/ruby-progress/cli/ripple_options.rb +16 -0
- data/lib/ruby-progress/cli/twirl_options.rb +22 -0
- data/lib/ruby-progress/cli/twirl_runner.rb +13 -7
- data/lib/ruby-progress/cli/twirl_spinner.rb +18 -2
- data/lib/ruby-progress/cli/worm_cli.rb +14 -5
- data/lib/ruby-progress/cli/worm_options.rb +16 -0
- data/lib/ruby-progress/cli/worm_runner.rb +12 -9
- data/lib/ruby-progress/fill.rb +5 -3
- data/lib/ruby-progress/fill_cli.rb +174 -51
- data/lib/ruby-progress/output_capture.rb +169 -37
- data/lib/ruby-progress/ripple.rb +3 -2
- data/lib/ruby-progress/utils.rb +47 -26
- data/lib/ruby-progress/version.rb +5 -5
- data/lib/ruby-progress/worm.rb +16 -68
- data/screencast +2497 -0
- data/screencast.svg +1 -0
- metadata +31 -2
- data/ruby-progress.gemspec +0 -40
|
@@ -2,11 +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'
|
|
6
8
|
require_relative 'output_capture'
|
|
7
9
|
|
|
8
10
|
module RubyProgress
|
|
9
11
|
# CLI module for Fill command
|
|
12
|
+
# rubocop:disable Metrics/ClassLength
|
|
10
13
|
module FillCLI
|
|
11
14
|
class << self
|
|
12
15
|
def run
|
|
@@ -35,11 +38,14 @@ module RubyProgress
|
|
|
35
38
|
|
|
36
39
|
# Handle daemon control first
|
|
37
40
|
if options[:status] || options[:stop]
|
|
38
|
-
pid_file = options
|
|
41
|
+
pid_file = resolve_pid_file(options, :status_name)
|
|
39
42
|
if options[:status]
|
|
40
43
|
Daemon.show_status(pid_file)
|
|
41
44
|
else
|
|
42
|
-
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?)
|
|
43
49
|
end
|
|
44
50
|
exit
|
|
45
51
|
end
|
|
@@ -48,6 +54,17 @@ module RubyProgress
|
|
|
48
54
|
parsed_style = parse_fill_style(options[:style])
|
|
49
55
|
|
|
50
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
|
+
|
|
51
68
|
run_daemon_mode(options, parsed_style)
|
|
52
69
|
elsif options[:current]
|
|
53
70
|
show_current_percentage(options, parsed_style)
|
|
@@ -62,6 +79,14 @@ module RubyProgress
|
|
|
62
79
|
|
|
63
80
|
private
|
|
64
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
|
+
|
|
65
90
|
def parse_fill_style(style_option)
|
|
66
91
|
case style_option
|
|
67
92
|
when String
|
|
@@ -76,9 +101,6 @@ module RubyProgress
|
|
|
76
101
|
end
|
|
77
102
|
|
|
78
103
|
def run_daemon_mode(options, parsed_style)
|
|
79
|
-
# For daemon mode, detach the process
|
|
80
|
-
PrgCLI.daemonize
|
|
81
|
-
|
|
82
104
|
pid_file = options[:pid_file] || '/tmp/ruby-progress/fill.pid'
|
|
83
105
|
FileUtils.mkdir_p(File.dirname(pid_file))
|
|
84
106
|
File.write(pid_file, Process.pid.to_s)
|
|
@@ -98,6 +120,72 @@ module RubyProgress
|
|
|
98
120
|
begin
|
|
99
121
|
fill_bar.render # Show initial empty bar
|
|
100
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
|
+
stream: options[:stdout_live]
|
|
141
|
+
)
|
|
142
|
+
oc.start
|
|
143
|
+
|
|
144
|
+
fill_bar.instance_variable_set(:@output_capture, oc)
|
|
145
|
+
oc.wait
|
|
146
|
+
captured = oc.lines.join("\n")
|
|
147
|
+
exit_status = oc.exit_status
|
|
148
|
+
fill_bar.instance_variable_set(:@output_capture, nil)
|
|
149
|
+
|
|
150
|
+
success = exit_status.to_i.zero?
|
|
151
|
+
if job['message']
|
|
152
|
+
RubyProgress::Utils.display_completion(
|
|
153
|
+
job['message'],
|
|
154
|
+
success: success,
|
|
155
|
+
show_checkmark: job['checkmark'] || false,
|
|
156
|
+
output_stream: :stdout,
|
|
157
|
+
icons: { success: options[:success_icon], error: options[:error_icon] }
|
|
158
|
+
)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
{ 'exit_status' => exit_status, 'output' => captured, 'log_path' => log_path }
|
|
162
|
+
|
|
163
|
+
elsif job['action']
|
|
164
|
+
case job['action']
|
|
165
|
+
when 'advance'
|
|
166
|
+
fill_bar.advance
|
|
167
|
+
{ 'status' => 'done', 'action' => 'advance' }
|
|
168
|
+
when 'percent'
|
|
169
|
+
val = job['value'] || job['percent'] || 0
|
|
170
|
+
fill_bar.percent = val.to_f
|
|
171
|
+
{ 'status' => 'done', 'action' => 'percent', 'value' => val }
|
|
172
|
+
when 'complete'
|
|
173
|
+
fill_bar.complete
|
|
174
|
+
{ 'status' => 'done', 'action' => 'complete' }
|
|
175
|
+
when 'cancel'
|
|
176
|
+
fill_bar.cancel
|
|
177
|
+
{ 'status' => 'done', 'action' => 'cancel' }
|
|
178
|
+
else
|
|
179
|
+
{ 'status' => 'error', 'error' => 'unknown action' }
|
|
180
|
+
end
|
|
181
|
+
else
|
|
182
|
+
{ 'status' => 'error', 'error' => 'no command or action provided' }
|
|
183
|
+
end
|
|
184
|
+
rescue StandardError
|
|
185
|
+
nil
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
101
189
|
# Set up signal handlers for daemon control
|
|
102
190
|
stop_requested = false
|
|
103
191
|
Signal.trap('INT') { stop_requested = true }
|
|
@@ -107,51 +195,82 @@ module RubyProgress
|
|
|
107
195
|
# Keep daemon alive until stop requested
|
|
108
196
|
sleep(0.1) until stop_requested
|
|
109
197
|
ensure
|
|
198
|
+
# If a control message file exists, print its contents like other CLIs
|
|
199
|
+
cmf = RubyProgress::Daemon.control_message_file(pid_file)
|
|
200
|
+
if File.exist?(cmf)
|
|
201
|
+
begin
|
|
202
|
+
data = JSON.parse(File.read(cmf))
|
|
203
|
+
message = data['message']
|
|
204
|
+
check = if data.key?('checkmark')
|
|
205
|
+
data['checkmark'] ? true : false
|
|
206
|
+
else
|
|
207
|
+
false
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
success_val = if data.key?('success')
|
|
211
|
+
data['success'] ? true : false
|
|
212
|
+
else
|
|
213
|
+
true
|
|
214
|
+
end
|
|
215
|
+
if message
|
|
216
|
+
RubyProgress::Utils.display_completion(
|
|
217
|
+
message,
|
|
218
|
+
success: success_val,
|
|
219
|
+
show_checkmark: check,
|
|
220
|
+
output_stream: :stdout,
|
|
221
|
+
icons: { success: options[:success_icon], error: options[:error_icon] }
|
|
222
|
+
)
|
|
223
|
+
end
|
|
224
|
+
rescue StandardError
|
|
225
|
+
# ignore
|
|
226
|
+
ensure
|
|
227
|
+
begin
|
|
228
|
+
File.delete(cmf)
|
|
229
|
+
rescue StandardError
|
|
230
|
+
nil
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
110
235
|
Fill.show_cursor
|
|
111
236
|
FileUtils.rm_f(pid_file)
|
|
112
237
|
end
|
|
113
238
|
end
|
|
114
239
|
|
|
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
240
|
def show_progress_report(options, parsed_style)
|
|
122
|
-
#
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
#
|
|
135
|
-
|
|
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]}"
|
|
241
|
+
# Produce a simple scripting-friendly report to stdout
|
|
242
|
+
length = options[:length] || 20
|
|
243
|
+
percent = (options[:percent] || 50.0).to_f
|
|
244
|
+
style = parsed_style
|
|
245
|
+
|
|
246
|
+
fill = Fill.new(style: style, length: length)
|
|
247
|
+
fill.percent = percent
|
|
248
|
+
|
|
249
|
+
report = fill.report
|
|
250
|
+
puts 'Progress Report:'
|
|
251
|
+
puts "Progress: #{report[:progress][0]}/#{report[:progress][1]}"
|
|
252
|
+
puts "Percent: #{report[:percent]}%"
|
|
253
|
+
puts "Completed: #{report[:completed] ? 'Yes' : 'No'}"
|
|
254
|
+
puts "Style: #{report[:style].inspect}"
|
|
255
|
+
exit(0)
|
|
144
256
|
end
|
|
145
257
|
|
|
146
258
|
def handle_progress_commands(_options, _parsed_style)
|
|
147
|
-
# For progress commands
|
|
148
|
-
#
|
|
149
|
-
# we'd need IPC to communicate with the daemon
|
|
259
|
+
# For now the progress commands are only supported in daemon mode.
|
|
260
|
+
# Return a clear error to the caller (specs assert this message exists).
|
|
150
261
|
warn 'Progress commands require daemon mode implementation'
|
|
151
|
-
|
|
152
|
-
|
|
262
|
+
exit(1)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def show_current_percentage(options, _parsed_style)
|
|
266
|
+
# For scripting and tests: print the current percentage to stdout and exit.
|
|
267
|
+
# If no explicit percent was provided, default to 50.0
|
|
268
|
+
percent = (options[:percent] || 50.0).to_f
|
|
269
|
+
$stdout.print("#{percent}\n")
|
|
270
|
+
exit(0)
|
|
153
271
|
end
|
|
154
272
|
|
|
273
|
+
# Foreground / auto-advance / command mode when not daemonizing
|
|
155
274
|
def run_auto_advance_mode(options, parsed_style)
|
|
156
275
|
fill_options = {
|
|
157
276
|
style: parsed_style,
|
|
@@ -161,20 +280,10 @@ module RubyProgress
|
|
|
161
280
|
error: options[:error_message]
|
|
162
281
|
}
|
|
163
282
|
|
|
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
283
|
fill_bar = Fill.new(fill_options)
|
|
176
284
|
Fill.hide_cursor
|
|
177
285
|
|
|
286
|
+
oc = nil
|
|
178
287
|
begin
|
|
179
288
|
if options[:percent]
|
|
180
289
|
# Set to specific percentage
|
|
@@ -185,7 +294,20 @@ module RubyProgress
|
|
|
185
294
|
sleep(0.1)
|
|
186
295
|
end
|
|
187
296
|
elsif options[:command]
|
|
188
|
-
#
|
|
297
|
+
# Run the command with OutputCapture
|
|
298
|
+
oc = RubyProgress::OutputCapture.new(
|
|
299
|
+
command: options[:command],
|
|
300
|
+
lines: options[:output_lines] || 3,
|
|
301
|
+
position: options[:output_position] || :above,
|
|
302
|
+
log_path: nil,
|
|
303
|
+
stream: options[:stdout] || options[:stdout_live]
|
|
304
|
+
)
|
|
305
|
+
oc.start
|
|
306
|
+
|
|
307
|
+
# Attach capture to the live fill instance so it can render output
|
|
308
|
+
fill_bar.instance_variable_set(:@output_capture, oc)
|
|
309
|
+
|
|
310
|
+
# While the command runs, keep redrawing the bar
|
|
189
311
|
sleep_time = case options[:speed]
|
|
190
312
|
when :fast then 0.1
|
|
191
313
|
when :medium, nil then 0.2
|
|
@@ -195,11 +317,11 @@ module RubyProgress
|
|
|
195
317
|
end
|
|
196
318
|
|
|
197
319
|
fill_bar.render
|
|
198
|
-
# Loop until the OutputCapture reader has finished
|
|
199
320
|
while oc.alive?
|
|
200
321
|
sleep(sleep_time)
|
|
201
322
|
fill_bar.render
|
|
202
323
|
end
|
|
324
|
+
fill_bar.instance_variable_set(:@output_capture, nil)
|
|
203
325
|
else
|
|
204
326
|
# Auto-advance mode
|
|
205
327
|
sleep_time = case options[:speed]
|
|
@@ -216,6 +338,7 @@ module RubyProgress
|
|
|
216
338
|
fill_bar.advance
|
|
217
339
|
end
|
|
218
340
|
end
|
|
341
|
+
|
|
219
342
|
fill_bar.complete
|
|
220
343
|
rescue Interrupt
|
|
221
344
|
fill_bar.cancel
|
|
@@ -2,27 +2,69 @@
|
|
|
2
2
|
|
|
3
3
|
require 'pty'
|
|
4
4
|
require 'io/console'
|
|
5
|
-
require '
|
|
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,16 @@ 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
|
next unless reader.wait_readable(0.1)
|
|
83
180
|
|
|
84
181
|
chunk = reader.read_nonblock(4096, exception: false)
|
|
85
182
|
next if chunk.nil? || chunk.empty?
|
|
86
183
|
|
|
87
|
-
|
|
184
|
+
debug_log("read chunk=#{chunk.inspect}")
|
|
185
|
+
|
|
88
186
|
if @log_path && !@log_file
|
|
89
187
|
begin
|
|
90
188
|
FileUtils.mkdir_p(File.dirname(@log_path))
|
|
@@ -95,29 +193,26 @@ module RubyProgress
|
|
|
95
193
|
end
|
|
96
194
|
|
|
97
195
|
process_chunk(chunk)
|
|
196
|
+
debug_log("after process_chunk buffer_size=#{@buffer.size}")
|
|
197
|
+
|
|
98
198
|
next unless @log_file
|
|
99
199
|
|
|
100
200
|
begin
|
|
101
201
|
@log_file.write(chunk)
|
|
102
202
|
@log_file.flush
|
|
103
203
|
rescue StandardError
|
|
104
|
-
# ignore
|
|
204
|
+
# ignore
|
|
105
205
|
end
|
|
106
206
|
end
|
|
107
207
|
end
|
|
208
|
+
|
|
108
209
|
begin
|
|
109
210
|
Process.wait(@child_pid) if @child_pid
|
|
110
211
|
@exit_status = $CHILD_STATUS.exitstatus if $CHILD_STATUS
|
|
111
212
|
rescue StandardError
|
|
112
213
|
@exit_status = nil
|
|
113
214
|
ensure
|
|
114
|
-
|
|
115
|
-
begin
|
|
116
|
-
@log_file.close
|
|
117
|
-
rescue StandardError
|
|
118
|
-
nil
|
|
119
|
-
end
|
|
120
|
-
end
|
|
215
|
+
@log_file&.close
|
|
121
216
|
end
|
|
122
217
|
rescue Errno::EIO
|
|
123
218
|
# PTY finished
|
|
@@ -125,12 +220,49 @@ module RubyProgress
|
|
|
125
220
|
|
|
126
221
|
def process_chunk(chunk)
|
|
127
222
|
@buf_mutex.synchronize do
|
|
128
|
-
# split into lines, keep last N lines
|
|
129
223
|
chunk.each_line do |line|
|
|
130
224
|
@buffer << line.chomp
|
|
131
225
|
@buffer.shift while @buffer.size > @lines
|
|
132
226
|
end
|
|
133
227
|
end
|
|
228
|
+
|
|
229
|
+
debug_log("process_chunk: buffer=#{@buffer.inspect}")
|
|
230
|
+
redraw($stderr) if @stream
|
|
231
|
+
rescue StandardError => e
|
|
232
|
+
debug_log("process_chunk error: #{e.class}: #{e.message}")
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def debug_log(msg)
|
|
236
|
+
return unless ENV['RUBY_PROGRESS_DEBUG'] || @debug
|
|
237
|
+
|
|
238
|
+
begin
|
|
239
|
+
File.open(@debug_path, 'a') do |f|
|
|
240
|
+
f.puts("#{Time.now.iso8601} PID=#{Process.pid} #{msg}")
|
|
241
|
+
end
|
|
242
|
+
rescue StandardError
|
|
243
|
+
# swallow logging errors
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def reserve_space(io = $stderr)
|
|
248
|
+
return unless io.tty?
|
|
249
|
+
|
|
250
|
+
debug_log("reserve_space called; position=#{@position.inspect}; lines=#{@lines}")
|
|
251
|
+
|
|
252
|
+
if @position == :above
|
|
253
|
+
# Insert lines above current cursor using CSI n L
|
|
254
|
+
io.print "\e[#{@lines}L"
|
|
255
|
+
debug_log("reserve_space: inserted #{@lines} lines for :above")
|
|
256
|
+
else
|
|
257
|
+
# Print newlines then move cursor back up so animation stays above
|
|
258
|
+
io.print("\n" * @lines)
|
|
259
|
+
io.print "\e[#{@lines}A"
|
|
260
|
+
debug_log("reserve_space: printed #{@lines} newlines and moved up #{@lines} for :below")
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
io.flush
|
|
264
|
+
rescue StandardError => e
|
|
265
|
+
debug_log("reserve_space error: #{e.class}: #{e.message}")
|
|
134
266
|
end
|
|
135
267
|
end
|
|
136
268
|
end
|
data/lib/ruby-progress/ripple.rb
CHANGED
|
@@ -154,7 +154,7 @@ module RubyProgress
|
|
|
154
154
|
RubyProgress::Utils.show_cursor
|
|
155
155
|
end
|
|
156
156
|
|
|
157
|
-
def self.complete(string, message, checkmark, success)
|
|
157
|
+
def self.complete(string, message, checkmark, success, icons: {})
|
|
158
158
|
display_message = message || (checkmark ? string : nil)
|
|
159
159
|
return unless display_message
|
|
160
160
|
|
|
@@ -162,7 +162,8 @@ module RubyProgress
|
|
|
162
162
|
display_message,
|
|
163
163
|
success: success,
|
|
164
164
|
show_checkmark: checkmark,
|
|
165
|
-
output_stream: :warn
|
|
165
|
+
output_stream: :warn,
|
|
166
|
+
icons: icons
|
|
166
167
|
)
|
|
167
168
|
end
|
|
168
169
|
|