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,282 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# rubocop:disable Metrics/ModuleLength
|
|
5
|
+
require 'open3'
|
|
6
|
+
require 'json'
|
|
7
|
+
require_relative '../utils'
|
|
8
|
+
require_relative '../output_capture'
|
|
9
|
+
|
|
10
|
+
# Runtime helper methods for RubyProgress::Worm
|
|
11
|
+
#
|
|
12
|
+
# These methods implement the interactive runtime behavior (animation
|
|
13
|
+
# loop, command execution, daemon mode, etc.) and were extracted from
|
|
14
|
+
# RubyProgress::Worm to reduce class length and improve readability.
|
|
15
|
+
module WormRunner
|
|
16
|
+
def animate(message: nil, success: nil, error: nil)
|
|
17
|
+
@message = message if message
|
|
18
|
+
@success_text = success if success
|
|
19
|
+
@error_text = error if error
|
|
20
|
+
@running = true
|
|
21
|
+
|
|
22
|
+
original_int_handler = Signal.trap('INT') do
|
|
23
|
+
@running = false
|
|
24
|
+
RubyProgress::Utils.clear_line
|
|
25
|
+
RubyProgress::Utils.show_cursor
|
|
26
|
+
exit 130
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
RubyProgress::Utils.hide_cursor
|
|
30
|
+
animation_thread = Thread.new { animation_loop }
|
|
31
|
+
|
|
32
|
+
begin
|
|
33
|
+
if block_given?
|
|
34
|
+
result = yield
|
|
35
|
+
@running = false
|
|
36
|
+
animation_thread.join
|
|
37
|
+
display_completion_message(@success_text, true)
|
|
38
|
+
result
|
|
39
|
+
else
|
|
40
|
+
animation_thread.join
|
|
41
|
+
end
|
|
42
|
+
rescue StandardError => e
|
|
43
|
+
@running = false
|
|
44
|
+
animation_thread.join
|
|
45
|
+
display_completion_message(@error_text || "Error: #{e.message}", false)
|
|
46
|
+
nil
|
|
47
|
+
ensure
|
|
48
|
+
$stderr.print "\r\e[2K"
|
|
49
|
+
RubyProgress::Utils.show_cursor
|
|
50
|
+
Signal.trap('INT', original_int_handler) if original_int_handler
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def run_with_command
|
|
55
|
+
return unless @command
|
|
56
|
+
|
|
57
|
+
exit_code = 0
|
|
58
|
+
stdout_content = nil
|
|
59
|
+
|
|
60
|
+
begin
|
|
61
|
+
stdout_content = if $stdout.tty? && @output_stdout
|
|
62
|
+
oc = RubyProgress::OutputCapture.new(
|
|
63
|
+
command: @command,
|
|
64
|
+
lines: @output_lines || 3,
|
|
65
|
+
position: @output_position || :above
|
|
66
|
+
)
|
|
67
|
+
oc.start
|
|
68
|
+
@output_capture = oc
|
|
69
|
+
animate do
|
|
70
|
+
oc.wait
|
|
71
|
+
end
|
|
72
|
+
@output_capture = nil
|
|
73
|
+
oc.lines.join("\n")
|
|
74
|
+
else
|
|
75
|
+
animate do
|
|
76
|
+
Open3.popen3(@command) do |_stdin, stdout, stderr, wait_thr|
|
|
77
|
+
captured_stdout = stdout.read
|
|
78
|
+
stderr_content = stderr.read
|
|
79
|
+
exit_code = wait_thr.value.exitstatus
|
|
80
|
+
|
|
81
|
+
unless wait_thr.value.success?
|
|
82
|
+
error_msg = @error_text || "Command failed with exit code #{exit_code}"
|
|
83
|
+
error_msg += ": #{stderr_content.strip}" if stderr_content && !stderr_content.empty?
|
|
84
|
+
raise StandardError, error_msg
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
captured_stdout
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
puts stdout_content if @output_stdout && stdout_content
|
|
93
|
+
rescue StandardError
|
|
94
|
+
exit exit_code.nonzero? || 1
|
|
95
|
+
rescue Interrupt
|
|
96
|
+
exit 130
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def run_indefinitely
|
|
101
|
+
original_int_handler = Signal.trap('INT') do
|
|
102
|
+
@running = false
|
|
103
|
+
RubyProgress::Utils.clear_line
|
|
104
|
+
RubyProgress::Utils.show_cursor
|
|
105
|
+
exit 130
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
@running = true
|
|
109
|
+
RubyProgress::Utils.hide_cursor
|
|
110
|
+
|
|
111
|
+
begin
|
|
112
|
+
animation_loop
|
|
113
|
+
ensure
|
|
114
|
+
RubyProgress::Utils.show_cursor
|
|
115
|
+
Signal.trap('INT', original_int_handler) if original_int_handler
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def stop
|
|
120
|
+
@running = false
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def run_daemon_mode(success_message: nil, show_checkmark: false, control_message_file: nil)
|
|
124
|
+
@running = true
|
|
125
|
+
stop_requested = false
|
|
126
|
+
|
|
127
|
+
original_int_handler = Signal.trap('INT') { stop_requested = true }
|
|
128
|
+
Signal.trap('USR1') { stop_requested = true }
|
|
129
|
+
Signal.trap('TERM') { stop_requested = true }
|
|
130
|
+
Signal.trap('HUP') { stop_requested = true }
|
|
131
|
+
|
|
132
|
+
RubyProgress::Utils.hide_cursor
|
|
133
|
+
|
|
134
|
+
begin
|
|
135
|
+
animation_loop_daemon_mode(stop_requested_proc: -> { stop_requested })
|
|
136
|
+
ensure
|
|
137
|
+
RubyProgress::Utils.clear_line
|
|
138
|
+
RubyProgress::Utils.show_cursor
|
|
139
|
+
|
|
140
|
+
final_message = success_message
|
|
141
|
+
final_checkmark = show_checkmark ? true : false
|
|
142
|
+
final_success = true
|
|
143
|
+
|
|
144
|
+
if control_message_file && File.exist?(control_message_file)
|
|
145
|
+
begin
|
|
146
|
+
data = JSON.parse(File.read(control_message_file))
|
|
147
|
+
final_message = data['message'] if data['message']
|
|
148
|
+
final_checkmark = data['checkmark'] if data.key?('checkmark')
|
|
149
|
+
final_success = data['success'] if data.key?('success')
|
|
150
|
+
rescue StandardError
|
|
151
|
+
# ignore parse errors
|
|
152
|
+
ensure
|
|
153
|
+
begin
|
|
154
|
+
File.delete(control_message_file)
|
|
155
|
+
rescue StandardError
|
|
156
|
+
nil
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
if final_message
|
|
162
|
+
RubyProgress::Utils.display_completion(
|
|
163
|
+
final_message,
|
|
164
|
+
success: final_success,
|
|
165
|
+
show_checkmark: final_checkmark,
|
|
166
|
+
output_stream: :stdout
|
|
167
|
+
)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
Signal.trap('INT', original_int_handler) if original_int_handler
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def animation_loop_step
|
|
175
|
+
return unless @running
|
|
176
|
+
|
|
177
|
+
@position ||= 0
|
|
178
|
+
@direction ||= 1
|
|
179
|
+
|
|
180
|
+
message_part = @message && !@message.empty? ? "#{@message} " : ''
|
|
181
|
+
@output_capture&.redraw($stderr)
|
|
182
|
+
$stderr.print "\r\e[2K#{@start_chars}#{message_part}#{generate_dots(@position, @direction)}#{@end_chars}"
|
|
183
|
+
$stderr.flush
|
|
184
|
+
|
|
185
|
+
sleep @speed
|
|
186
|
+
|
|
187
|
+
@position += @direction
|
|
188
|
+
if @position >= @length - 1
|
|
189
|
+
if @direction_mode == :forward_only
|
|
190
|
+
@position = 0
|
|
191
|
+
else
|
|
192
|
+
@direction = -1
|
|
193
|
+
end
|
|
194
|
+
elsif @position <= 0
|
|
195
|
+
@direction = 1
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def animation_loop
|
|
200
|
+
position = 0
|
|
201
|
+
direction = 1
|
|
202
|
+
|
|
203
|
+
while @running
|
|
204
|
+
message_part = @message && !@message.empty? ? "#{@message} " : ''
|
|
205
|
+
@output_capture&.redraw($stderr)
|
|
206
|
+
$stderr.print "\r\e[2K#{@start_chars}#{message_part}#{generate_dots(position, direction)}#{@end_chars}"
|
|
207
|
+
$stderr.flush
|
|
208
|
+
|
|
209
|
+
sleep @speed
|
|
210
|
+
|
|
211
|
+
position += direction
|
|
212
|
+
if position >= @length - 1
|
|
213
|
+
if @direction_mode == :forward_only
|
|
214
|
+
position = 0
|
|
215
|
+
else
|
|
216
|
+
direction = -1
|
|
217
|
+
end
|
|
218
|
+
elsif position <= 0
|
|
219
|
+
direction = 1
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def animation_loop_daemon_mode(stop_requested_proc: -> { false })
|
|
225
|
+
position = 0
|
|
226
|
+
direction = 1
|
|
227
|
+
frame_count = 0
|
|
228
|
+
|
|
229
|
+
while @running && !stop_requested_proc.call
|
|
230
|
+
message_part = @message && !@message.empty? ? "#{@message} " : ''
|
|
231
|
+
@output_capture&.redraw($stderr)
|
|
232
|
+
|
|
233
|
+
$stderr.print "\r\e[2K"
|
|
234
|
+
|
|
235
|
+
if (frame_count % 10).zero?
|
|
236
|
+
$stderr.print "\e[1A\e[2K"
|
|
237
|
+
$stderr.print "\r"
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
$stderr.print "#{message_part}#{generate_dots(position, direction)}"
|
|
241
|
+
$stderr.flush
|
|
242
|
+
|
|
243
|
+
sleep @speed
|
|
244
|
+
frame_count += 1
|
|
245
|
+
|
|
246
|
+
position += direction
|
|
247
|
+
if position >= @length - 1
|
|
248
|
+
if @direction_mode == :forward_only
|
|
249
|
+
position = 0
|
|
250
|
+
else
|
|
251
|
+
direction = -1
|
|
252
|
+
end
|
|
253
|
+
elsif position <= 0
|
|
254
|
+
direction = 1
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def generate_dots(ripple_position, direction)
|
|
260
|
+
dots = Array.new(@length) { @style[:baseline] }
|
|
261
|
+
|
|
262
|
+
(0...@length).each do |i|
|
|
263
|
+
distance = (i - ripple_position).abs
|
|
264
|
+
case distance
|
|
265
|
+
when 0
|
|
266
|
+
dots[i] = @style[:peak]
|
|
267
|
+
when 1
|
|
268
|
+
if direction == -1
|
|
269
|
+
dots[i] = @style[:midline] if i > ripple_position
|
|
270
|
+
elsif i < ripple_position
|
|
271
|
+
dots[i] = @style[:midline]
|
|
272
|
+
end
|
|
273
|
+
else
|
|
274
|
+
dots[i] = @style[:baseline]
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
dots.join
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# rubocop:enable Metrics/ModuleLength
|
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
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyProgress
|
|
4
|
+
# Determinate progress bar with customizable fill styles
|
|
5
|
+
class Fill
|
|
6
|
+
# Built-in fill styles with empty and full characters
|
|
7
|
+
FILL_STYLES = {
|
|
8
|
+
blocks: { empty: '▱', full: '▰' },
|
|
9
|
+
classic: { empty: '-', full: '=' },
|
|
10
|
+
dots: { empty: '·', full: '●' },
|
|
11
|
+
squares: { empty: '□', full: '■' },
|
|
12
|
+
circles: { empty: '○', full: '●' },
|
|
13
|
+
ascii: { empty: '.', full: '#' },
|
|
14
|
+
bars: { empty: '░', full: '█' },
|
|
15
|
+
arrows: { empty: '▷', full: '▶' },
|
|
16
|
+
stars: { empty: '☆', full: '★' }
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
attr_reader :length, :style, :current_progress, :start_chars, :end_chars
|
|
20
|
+
attr_accessor :success_message, :error_message
|
|
21
|
+
|
|
22
|
+
def initialize(options = {})
|
|
23
|
+
@length = options[:length] || 20
|
|
24
|
+
@style = parse_style(options[:style] || :blocks)
|
|
25
|
+
@current_progress = 0
|
|
26
|
+
@success_message = options[:success]
|
|
27
|
+
@error_message = options[:error]
|
|
28
|
+
@output_capture = options[:output_capture]
|
|
29
|
+
|
|
30
|
+
# Parse --ends characters
|
|
31
|
+
if options[:ends]
|
|
32
|
+
@start_chars, @end_chars = RubyProgress::Utils.parse_ends(options[:ends])
|
|
33
|
+
else
|
|
34
|
+
@start_chars = ''
|
|
35
|
+
@end_chars = ''
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Advance the progress bar by one step or specified increment
|
|
40
|
+
def advance(increment: 1, percent: nil)
|
|
41
|
+
@current_progress = if percent
|
|
42
|
+
[@length * percent / 100.0, @length].min.round
|
|
43
|
+
else
|
|
44
|
+
[@current_progress + increment, @length].min
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
render
|
|
48
|
+
completed?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Set progress to specific percentage (0-100)
|
|
52
|
+
def percent=(percent)
|
|
53
|
+
percent = percent.clamp(0, 100) # Clamp between 0-100
|
|
54
|
+
@current_progress = (@length * percent / 100.0).round
|
|
55
|
+
render
|
|
56
|
+
completed?
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Check if progress bar is complete
|
|
60
|
+
def completed?
|
|
61
|
+
@current_progress >= @length
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Get current progress as percentage
|
|
65
|
+
def percent
|
|
66
|
+
(@current_progress.to_f / @length * 100).round(1)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Get current progress as float (0.0-100.0) - for scripting
|
|
70
|
+
def current
|
|
71
|
+
(@current_progress.to_f / @length * 100).round(1)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Get detailed progress status information
|
|
75
|
+
def report
|
|
76
|
+
{
|
|
77
|
+
progress: [@current_progress, @length],
|
|
78
|
+
percent: current,
|
|
79
|
+
completed: completed?,
|
|
80
|
+
style: @style
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Render the current progress bar to stderr
|
|
85
|
+
def render
|
|
86
|
+
# First redraw captured output (if any) so it appears above/below the bar
|
|
87
|
+
@output_capture&.redraw($stderr)
|
|
88
|
+
|
|
89
|
+
filled = @style[:full] * @current_progress
|
|
90
|
+
empty = @style[:empty] * (@length - @current_progress)
|
|
91
|
+
bar = "#{@start_chars}#{filled}#{empty}#{@end_chars}"
|
|
92
|
+
|
|
93
|
+
$stderr.print "\r\e[2K#{bar}"
|
|
94
|
+
$stderr.flush
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Complete the progress bar and show success message
|
|
98
|
+
def complete(message = nil)
|
|
99
|
+
@current_progress = @length
|
|
100
|
+
render
|
|
101
|
+
|
|
102
|
+
completion_message = message || @success_message
|
|
103
|
+
if completion_message
|
|
104
|
+
RubyProgress::Utils.display_completion(
|
|
105
|
+
completion_message,
|
|
106
|
+
success: true,
|
|
107
|
+
show_checkmark: true,
|
|
108
|
+
output_stream: :warn
|
|
109
|
+
)
|
|
110
|
+
else
|
|
111
|
+
$stderr.puts # Just add a newline if no message
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Cancel the progress bar and show error message
|
|
116
|
+
def cancel(message = nil)
|
|
117
|
+
$stderr.print "\r\e[2K" # Clear the progress bar
|
|
118
|
+
$stderr.flush
|
|
119
|
+
|
|
120
|
+
error_msg = message || @error_message
|
|
121
|
+
return unless error_msg
|
|
122
|
+
|
|
123
|
+
RubyProgress::Utils.display_completion(
|
|
124
|
+
error_msg,
|
|
125
|
+
success: false,
|
|
126
|
+
show_checkmark: true,
|
|
127
|
+
output_stream: :warn
|
|
128
|
+
)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Hide or show the cursor (delegated to Utils)
|
|
132
|
+
def self.hide_cursor
|
|
133
|
+
RubyProgress::Utils.hide_cursor
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def self.show_cursor
|
|
137
|
+
RubyProgress::Utils.show_cursor
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Progress with block interface for library usage
|
|
141
|
+
def self.progress(options = {}, &block)
|
|
142
|
+
return unless block_given?
|
|
143
|
+
|
|
144
|
+
fill_bar = new(options)
|
|
145
|
+
Fill.hide_cursor
|
|
146
|
+
|
|
147
|
+
begin
|
|
148
|
+
fill_bar.render # Show initial empty bar
|
|
149
|
+
|
|
150
|
+
# Call the block with the fill bar instance
|
|
151
|
+
result = block.call(fill_bar)
|
|
152
|
+
|
|
153
|
+
# Handle completion based on block result or bar state
|
|
154
|
+
if fill_bar.completed? || result == true || result.nil?
|
|
155
|
+
fill_bar.complete
|
|
156
|
+
elsif result == false
|
|
157
|
+
fill_bar.cancel
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
result
|
|
161
|
+
rescue StandardError => e
|
|
162
|
+
fill_bar.cancel("Error: #{e.message}")
|
|
163
|
+
raise
|
|
164
|
+
ensure
|
|
165
|
+
Fill.show_cursor
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
private
|
|
170
|
+
|
|
171
|
+
# Parse style option into empty/full character hash
|
|
172
|
+
def parse_style(style_option)
|
|
173
|
+
case style_option
|
|
174
|
+
when Symbol, String
|
|
175
|
+
style_name = style_option.to_sym
|
|
176
|
+
if FILL_STYLES.key?(style_name)
|
|
177
|
+
FILL_STYLES[style_name]
|
|
178
|
+
else
|
|
179
|
+
FILL_STYLES[:blocks] # Default fallback
|
|
180
|
+
end
|
|
181
|
+
when Hash
|
|
182
|
+
# Allow direct hash specification: { empty: '.', full: '#' }
|
|
183
|
+
{
|
|
184
|
+
empty: style_option[:empty] || '.',
|
|
185
|
+
full: style_option[:full] || '#'
|
|
186
|
+
}
|
|
187
|
+
else
|
|
188
|
+
FILL_STYLES[:blocks] # Default fallback
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
class << self
|
|
193
|
+
# Parse custom style string like "custom=.#"
|
|
194
|
+
def parse_custom_style(style_string)
|
|
195
|
+
if style_string.start_with?('custom=')
|
|
196
|
+
chars = style_string.sub('custom=', '')
|
|
197
|
+
|
|
198
|
+
# Handle multi-byte characters properly
|
|
199
|
+
char_array = chars.chars
|
|
200
|
+
|
|
201
|
+
if char_array.length == 2
|
|
202
|
+
{ empty: char_array[0], full: char_array[1] }
|
|
203
|
+
else
|
|
204
|
+
# Invalid custom style, return default
|
|
205
|
+
FILL_STYLES[:blocks]
|
|
206
|
+
end
|
|
207
|
+
else
|
|
208
|
+
# Try to find built-in style
|
|
209
|
+
style_name = style_string.to_sym
|
|
210
|
+
FILL_STYLES[style_name] || FILL_STYLES[:blocks]
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|