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
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'fileutils'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'securerandom'
|
|
4
6
|
require_relative 'ripple_options'
|
|
7
|
+
require_relative '../output_capture'
|
|
5
8
|
|
|
6
9
|
# Enhanced Ripple CLI with unified flags (extracted from bin/prg)
|
|
7
10
|
module RippleCLI
|
|
@@ -30,8 +33,13 @@ module RippleCLI
|
|
|
30
33
|
)
|
|
31
34
|
exit
|
|
32
35
|
elsif options[:daemon]
|
|
33
|
-
# For daemon mode, detach so shell has no tracked job
|
|
34
|
-
|
|
36
|
+
# For daemon mode, detach so shell has no tracked job unless the user
|
|
37
|
+
# requested a non-detaching background child via --no-detach.
|
|
38
|
+
if options[:no_detach]
|
|
39
|
+
PrgCLI.backgroundize
|
|
40
|
+
else
|
|
41
|
+
PrgCLI.daemonize
|
|
42
|
+
end
|
|
35
43
|
|
|
36
44
|
# For daemon mode, default message if none provided
|
|
37
45
|
text = options[:message] || ARGV.join(' ')
|
|
@@ -60,17 +68,42 @@ module RippleCLI
|
|
|
60
68
|
end
|
|
61
69
|
|
|
62
70
|
def self.run_with_command(text, options)
|
|
63
|
-
|
|
64
|
-
|
|
71
|
+
if $stdout.tty?
|
|
72
|
+
# Interactive TTY: use PTY-based capture so the animation can run while the
|
|
73
|
+
# command executes. We only print captured stdout if options[:output] == :stdout.
|
|
74
|
+
oc = RubyProgress::OutputCapture.new(command: options[:command], lines: options[:output_lines] || 3, position: options[:output_position] || :above)
|
|
75
|
+
oc.start
|
|
76
|
+
|
|
77
|
+
# Create rippler. Attach output capture only when the user requested
|
|
78
|
+
# live stdout display via --stdout; otherwise start the PTY reader so
|
|
79
|
+
# we can collect the child's exit status but do not call redraw.
|
|
80
|
+
rippler = RubyProgress::Ripple.new(text, options)
|
|
81
|
+
rippler.instance_variable_set(:@output_capture, oc) if options[:output] == :stdout
|
|
82
|
+
|
|
83
|
+
thread = Thread.new { loop { rippler.advance } }
|
|
84
|
+
oc.wait
|
|
85
|
+
thread.kill
|
|
86
|
+
|
|
87
|
+
captured_lines = oc.lines
|
|
88
|
+
captured_output = captured_lines.join("\n")
|
|
89
|
+
success = oc.exit_status.nil? || oc.exit_status.zero?
|
|
90
|
+
else
|
|
91
|
+
# Non-interactive / CI: fallback to legacy synchronous capture
|
|
65
92
|
captured_output = `#{options[:command]} 2>&1`
|
|
93
|
+
success = $CHILD_STATUS.success?
|
|
66
94
|
end
|
|
67
95
|
|
|
68
|
-
success = $CHILD_STATUS.success?
|
|
69
|
-
|
|
70
96
|
puts captured_output if options[:output] == :stdout
|
|
97
|
+
|
|
71
98
|
if options[:success_message] || options[:complete_checkmark]
|
|
72
99
|
message = success ? options[:success_message] : options[:fail_message] || options[:success_message]
|
|
73
|
-
RubyProgress::Ripple.complete(
|
|
100
|
+
RubyProgress::Ripple.complete(
|
|
101
|
+
text,
|
|
102
|
+
message,
|
|
103
|
+
options[:complete_checkmark],
|
|
104
|
+
success,
|
|
105
|
+
icons: { success: options[:success_icon], error: options[:error_icon] }
|
|
106
|
+
)
|
|
74
107
|
end
|
|
75
108
|
exit success ? 0 : 1
|
|
76
109
|
end
|
|
@@ -90,7 +123,6 @@ module RippleCLI
|
|
|
90
123
|
pid_file = options[:pid_file] || RubyProgress::Daemon.default_pid_file
|
|
91
124
|
FileUtils.mkdir_p(File.dirname(pid_file))
|
|
92
125
|
File.write(pid_file, Process.pid.to_s)
|
|
93
|
-
|
|
94
126
|
begin
|
|
95
127
|
# For Ripple, re-use the existing animation loop via a simple loop
|
|
96
128
|
RubyProgress::Utils.hide_cursor
|
|
@@ -102,6 +134,9 @@ module RippleCLI
|
|
|
102
134
|
Signal.trap('TERM') { stop_requested = true }
|
|
103
135
|
Signal.trap('HUP') { stop_requested = true }
|
|
104
136
|
|
|
137
|
+
job_dir = RubyProgress::Daemon.job_dir_for_pid(pid_file)
|
|
138
|
+
job_thread = Thread.new { process_daemon_jobs_for_rippler(job_dir, rippler, options) }
|
|
139
|
+
|
|
105
140
|
rippler.advance until stop_requested
|
|
106
141
|
ensure
|
|
107
142
|
RubyProgress::Utils.clear_line
|
|
@@ -128,7 +163,8 @@ module RippleCLI
|
|
|
128
163
|
message,
|
|
129
164
|
success: success_val,
|
|
130
165
|
show_checkmark: check,
|
|
131
|
-
output_stream: :stdout
|
|
166
|
+
output_stream: :stdout,
|
|
167
|
+
icons: { success: options[:success_icon], error: options[:error_icon] }
|
|
132
168
|
)
|
|
133
169
|
end
|
|
134
170
|
rescue StandardError
|
|
@@ -142,9 +178,52 @@ module RippleCLI
|
|
|
142
178
|
end
|
|
143
179
|
end
|
|
144
180
|
|
|
181
|
+
# stop job thread and cleanup
|
|
182
|
+
job_thread&.kill
|
|
145
183
|
FileUtils.rm_f(pid_file)
|
|
146
184
|
end
|
|
147
185
|
end
|
|
148
186
|
|
|
187
|
+
def self.process_daemon_jobs_for_rippler(job_dir, rippler, options)
|
|
188
|
+
RubyProgress::Daemon.process_jobs(job_dir) do |job|
|
|
189
|
+
jid = job['id'] || SecureRandom.uuid
|
|
190
|
+
log_path = begin
|
|
191
|
+
File.join(File.dirname(job_dir), "#{jid}.log")
|
|
192
|
+
rescue StandardError
|
|
193
|
+
nil
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
oc = RubyProgress::OutputCapture.new(
|
|
197
|
+
command: job['command'],
|
|
198
|
+
lines: options[:output_lines] || 3,
|
|
199
|
+
position: options[:output_position] || :above,
|
|
200
|
+
log_path: log_path
|
|
201
|
+
)
|
|
202
|
+
oc.start
|
|
203
|
+
|
|
204
|
+
rippler.instance_variable_set(:@output_capture, oc)
|
|
205
|
+
oc.wait
|
|
206
|
+
captured = oc.lines.join("\n")
|
|
207
|
+
exit_status = oc.exit_status
|
|
208
|
+
rippler.instance_variable_set(:@output_capture, nil)
|
|
209
|
+
|
|
210
|
+
success = exit_status.to_i.zero?
|
|
211
|
+
if job['message']
|
|
212
|
+
RubyProgress::Utils.display_completion(
|
|
213
|
+
job['message'],
|
|
214
|
+
success: success,
|
|
215
|
+
show_checkmark: job['checkmark'] || false,
|
|
216
|
+
output_stream: :stdout,
|
|
217
|
+
icons: { success: options[:success_icon], error: options[:error_icon] }
|
|
218
|
+
)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
{ 'exit_status' => exit_status, 'output' => captured, 'log_path' => log_path }
|
|
222
|
+
rescue StandardError
|
|
223
|
+
# ignore per-job errors; process_jobs will write result
|
|
224
|
+
nil
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
149
228
|
# Options parsing moved to ripple_options.rb
|
|
150
229
|
end
|
|
@@ -17,6 +17,8 @@ module RippleCLI
|
|
|
17
17
|
fail_message: nil,
|
|
18
18
|
complete_checkmark: false,
|
|
19
19
|
output: :error,
|
|
20
|
+
output_position: :above,
|
|
21
|
+
output_lines: 3,
|
|
20
22
|
message: nil
|
|
21
23
|
}
|
|
22
24
|
|
|
@@ -57,10 +59,26 @@ module RippleCLI
|
|
|
57
59
|
options[:command] = command
|
|
58
60
|
end
|
|
59
61
|
|
|
62
|
+
opts.on('--output-position POSITION', 'Position to render captured output: above or below (default: above)') do |pos|
|
|
63
|
+
options[:output_position] = pos.to_sym
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
opts.on('--output-lines N', Integer, 'Number of output lines to reserve for captured output (default: 3)') do |n|
|
|
67
|
+
options[:output_lines] = n
|
|
68
|
+
end
|
|
69
|
+
|
|
60
70
|
opts.on('--success MESSAGE', 'Success message to display') do |msg|
|
|
61
71
|
options[:success_message] = msg
|
|
62
72
|
end
|
|
63
73
|
|
|
74
|
+
opts.on('--success-icon ICON', 'Custom success icon to show with completion messages') do |ic|
|
|
75
|
+
options[:success_icon] = ic
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
opts.on('--error-icon ICON', 'Custom error icon to show with failure messages') do |ic|
|
|
79
|
+
options[:error_icon] = ic
|
|
80
|
+
end
|
|
81
|
+
|
|
64
82
|
opts.on('--error MESSAGE', 'Error message to display') do |msg|
|
|
65
83
|
options[:fail_message] = msg
|
|
66
84
|
end
|
|
@@ -84,6 +102,10 @@ module RippleCLI
|
|
|
84
102
|
options[:daemon] = true
|
|
85
103
|
end
|
|
86
104
|
|
|
105
|
+
opts.on('--no-detach', 'When used with --daemon: run background child but do not fully detach from the terminal') do
|
|
106
|
+
options[:no_detach] = true
|
|
107
|
+
end
|
|
108
|
+
|
|
87
109
|
opts.on('--pid-file FILE', 'Write process ID to file (default: /tmp/ruby-progress/progress.pid)') do |file|
|
|
88
110
|
options[:pid_file] = file
|
|
89
111
|
end
|
|
@@ -10,7 +10,10 @@ module TwirlCLI
|
|
|
10
10
|
# so the `TwirlCLI` module stays small and focused on dispatching.
|
|
11
11
|
module Options
|
|
12
12
|
def self.parse_cli_options
|
|
13
|
-
options = {
|
|
13
|
+
options = {
|
|
14
|
+
output_position: :above,
|
|
15
|
+
output_lines: 3
|
|
16
|
+
}
|
|
14
17
|
|
|
15
18
|
OptionParser.new do |opts|
|
|
16
19
|
opts.banner = 'Usage: prg twirl [options]'
|
|
@@ -29,6 +32,12 @@ module TwirlCLI
|
|
|
29
32
|
options[:style] = style
|
|
30
33
|
end
|
|
31
34
|
|
|
35
|
+
opts.on('-d', '--direction DIRECTION', 'Animation direction (forward/bidirectional or f/b)') do |direction|
|
|
36
|
+
# Twirl is a spinner and doesn't visibly change with direction, but accept the
|
|
37
|
+
# flag for parity with other subcommands (Worm/Ripple) so scripts can use it.
|
|
38
|
+
options[:direction] = direction =~ /^f/i ? :forward_only : :bidirectional
|
|
39
|
+
end
|
|
40
|
+
|
|
32
41
|
opts.on('--ends CHARS', 'Start/end characters (even number of chars, split in half)') do |chars|
|
|
33
42
|
options[:ends] = chars
|
|
34
43
|
end
|
|
@@ -40,10 +49,26 @@ module TwirlCLI
|
|
|
40
49
|
options[:command] = command
|
|
41
50
|
end
|
|
42
51
|
|
|
52
|
+
opts.on('--output-position POSITION', 'Position to render captured output: above or below (default: above)') do |pos|
|
|
53
|
+
options[:output_position] = pos.to_sym
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
opts.on('--output-lines N', Integer, 'Number of output lines to reserve for captured output (default: 3)') do |n|
|
|
57
|
+
options[:output_lines] = n
|
|
58
|
+
end
|
|
59
|
+
|
|
43
60
|
opts.on('--success MESSAGE', 'Success message to display') do |text|
|
|
44
61
|
options[:success] = text
|
|
45
62
|
end
|
|
46
63
|
|
|
64
|
+
opts.on('--success-icon ICON', 'Custom success icon to show with completion messages') do |ic|
|
|
65
|
+
options[:success_icon] = ic
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
opts.on('--error-icon ICON', 'Custom error icon to show with failure messages') do |ic|
|
|
69
|
+
options[:error_icon] = ic
|
|
70
|
+
end
|
|
71
|
+
|
|
47
72
|
opts.on('--error MESSAGE', 'Error message to display') do |text|
|
|
48
73
|
options[:error] = text
|
|
49
74
|
end
|
|
@@ -63,6 +88,10 @@ module TwirlCLI
|
|
|
63
88
|
options[:daemon] = true
|
|
64
89
|
end
|
|
65
90
|
|
|
91
|
+
opts.on('--no-detach', 'When used with --daemon/--daemon-as: run background child but do not fully detach from the terminal') do
|
|
92
|
+
options[:no_detach] = true
|
|
93
|
+
end
|
|
94
|
+
|
|
66
95
|
opts.on('--daemon-as NAME', 'Run in daemon mode with custom name (creates /tmp/ruby-progress/NAME.pid)') do |name|
|
|
67
96
|
options[:daemon] = true
|
|
68
97
|
options[:daemon_name] = name
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'fileutils'
|
|
4
4
|
require 'json'
|
|
5
5
|
require_relative 'twirl_spinner'
|
|
6
|
+
require_relative '../output_capture'
|
|
6
7
|
|
|
7
8
|
# Top-level runtime helper module for the Twirl CLI.
|
|
8
9
|
#
|
|
@@ -22,8 +23,25 @@ module TwirlRunner
|
|
|
22
23
|
RubyProgress::Utils.hide_cursor
|
|
23
24
|
spinner_thread = Thread.new { loop { spinner.animate } }
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
if $stdout.tty? && options[:stdout]
|
|
27
|
+
oc = RubyProgress::OutputCapture.new(
|
|
28
|
+
command: options[:command],
|
|
29
|
+
lines: options[:output_lines] || 3,
|
|
30
|
+
position: options[:output_position] || :above
|
|
31
|
+
)
|
|
32
|
+
oc.start
|
|
33
|
+
|
|
34
|
+
spinner.instance_variable_set(:@output_capture, oc)
|
|
35
|
+
|
|
36
|
+
# wait for command while spinner thread runs
|
|
37
|
+
oc.wait
|
|
38
|
+
captured_lines = oc.lines
|
|
39
|
+
captured_output = captured_lines.join("\n")
|
|
40
|
+
success = true
|
|
41
|
+
else
|
|
42
|
+
captured_output = `#{options[:command]} 2>&1`
|
|
43
|
+
success = $CHILD_STATUS.success?
|
|
44
|
+
end
|
|
27
45
|
|
|
28
46
|
spinner_thread.kill
|
|
29
47
|
RubyProgress::Utils.clear_line
|
|
@@ -40,7 +58,8 @@ module TwirlRunner
|
|
|
40
58
|
RubyProgress::Utils.display_completion(
|
|
41
59
|
final_msg,
|
|
42
60
|
success: success,
|
|
43
|
-
show_checkmark: options[:checkmark]
|
|
61
|
+
show_checkmark: options[:checkmark],
|
|
62
|
+
icons: { success: options[:success_icon], error: options[:error_icon] }
|
|
44
63
|
)
|
|
45
64
|
end
|
|
46
65
|
|
|
@@ -60,7 +79,8 @@ module TwirlRunner
|
|
|
60
79
|
RubyProgress::Utils.display_completion(
|
|
61
80
|
options[:success] || 'Complete',
|
|
62
81
|
success: true,
|
|
63
|
-
show_checkmark: options[:checkmark]
|
|
82
|
+
show_checkmark: options[:checkmark],
|
|
83
|
+
icons: { success: options[:success_icon], error: options[:error_icon] }
|
|
64
84
|
)
|
|
65
85
|
end
|
|
66
86
|
end
|
|
@@ -82,6 +102,41 @@ module TwirlRunner
|
|
|
82
102
|
|
|
83
103
|
begin
|
|
84
104
|
RubyProgress::Utils.hide_cursor
|
|
105
|
+
|
|
106
|
+
# Start job processor thread for twirl
|
|
107
|
+
job_dir = RubyProgress::Daemon.job_dir_for_pid(pid_file)
|
|
108
|
+
job_thread = Thread.new do
|
|
109
|
+
RubyProgress::Daemon.process_jobs(job_dir) do |job|
|
|
110
|
+
oc = RubyProgress::OutputCapture.new(
|
|
111
|
+
command: job['command'],
|
|
112
|
+
lines: options[:output_lines] || 3,
|
|
113
|
+
position: options[:output_position] || :above
|
|
114
|
+
)
|
|
115
|
+
oc.start
|
|
116
|
+
|
|
117
|
+
spinner.instance_variable_set(:@output_capture, oc)
|
|
118
|
+
oc.wait
|
|
119
|
+
captured = oc.lines.join("\n")
|
|
120
|
+
exit_status = oc.exit_status
|
|
121
|
+
spinner.instance_variable_set(:@output_capture, nil)
|
|
122
|
+
|
|
123
|
+
success = exit_status.to_i.zero?
|
|
124
|
+
if job['message']
|
|
125
|
+
RubyProgress::Utils.display_completion(
|
|
126
|
+
job['message'],
|
|
127
|
+
success: success,
|
|
128
|
+
show_checkmark: job['checkmark'] || false,
|
|
129
|
+
output_stream: :stdout,
|
|
130
|
+
icons: { success: options[:success_icon], error: options[:error_icon] }
|
|
131
|
+
)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
{ 'exit_status' => exit_status, 'output' => captured }
|
|
135
|
+
rescue StandardError
|
|
136
|
+
# ignore
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
85
140
|
spinner.animate until stop_requested
|
|
86
141
|
ensure
|
|
87
142
|
RubyProgress::Utils.clear_line
|
|
@@ -100,7 +155,8 @@ module TwirlRunner
|
|
|
100
155
|
message,
|
|
101
156
|
success: success_val,
|
|
102
157
|
show_checkmark: check,
|
|
103
|
-
output_stream: :stdout
|
|
158
|
+
output_stream: :stdout,
|
|
159
|
+
icons: { success: options[:success_icon], error: options[:error_icon] }
|
|
104
160
|
)
|
|
105
161
|
end
|
|
106
162
|
rescue StandardError
|
|
@@ -114,6 +170,7 @@ module TwirlRunner
|
|
|
114
170
|
end
|
|
115
171
|
end
|
|
116
172
|
|
|
173
|
+
job_thread&.kill
|
|
117
174
|
FileUtils.rm_f(pid_file)
|
|
118
175
|
end
|
|
119
176
|
end
|
|
@@ -10,14 +10,29 @@ require_relative '../utils'
|
|
|
10
10
|
class TwirlSpinner
|
|
11
11
|
def initialize(message, options = {})
|
|
12
12
|
@message = message
|
|
13
|
-
@style = parse_style(options[:style] || 'dots')
|
|
14
13
|
@speed = parse_speed(options[:speed] || 'medium')
|
|
15
|
-
|
|
14
|
+
|
|
15
|
+
style_opt = options[:style].to_s
|
|
16
|
+
if style_opt.start_with?('custom=')
|
|
17
|
+
chars = style_opt.sub('custom=', '')
|
|
18
|
+
@frames = if chars.length >= 3
|
|
19
|
+
chars.chars
|
|
20
|
+
elsif chars.length == 2
|
|
21
|
+
[chars[0], chars[1], chars[1]]
|
|
22
|
+
else
|
|
23
|
+
[chars, chars, chars]
|
|
24
|
+
end
|
|
25
|
+
@style = :custom
|
|
26
|
+
else
|
|
27
|
+
@style = parse_style(style_opt.empty? ? 'dots' : style_opt)
|
|
28
|
+
@frames = RubyProgress::INDICATORS[@style] || RubyProgress::INDICATORS[:dots]
|
|
29
|
+
end
|
|
16
30
|
@start_chars, @end_chars = RubyProgress::Utils.parse_ends(options[:ends])
|
|
17
31
|
@index = 0
|
|
18
32
|
end
|
|
19
33
|
|
|
20
34
|
def animate
|
|
35
|
+
@output_capture&.redraw($stderr)
|
|
21
36
|
if @message && !@message.empty?
|
|
22
37
|
$stderr.print "\r\e[2K#{@start_chars}#{@message} #{@frames[@index]}#{@end_chars}"
|
|
23
38
|
else
|
|
@@ -35,6 +50,8 @@ class TwirlSpinner
|
|
|
35
50
|
|
|
36
51
|
style_lower = style_input.to_s.downcase.strip
|
|
37
52
|
|
|
53
|
+
# parse_style returns a symbol key for RubyProgress::INDICATORS
|
|
54
|
+
|
|
38
55
|
indicator_keys = RubyProgress::INDICATORS.keys.map(&:to_s)
|
|
39
56
|
return style_lower.to_sym if indicator_keys.include?(style_lower)
|
|
40
57
|
|
|
@@ -6,6 +6,14 @@ require_relative 'worm_options'
|
|
|
6
6
|
|
|
7
7
|
# Enhanced Worm CLI (extracted from bin/prg)
|
|
8
8
|
module WormCLI
|
|
9
|
+
def self.resolve_pid_file(options, name_key = :daemon_name)
|
|
10
|
+
return options[:pid_file] if options[:pid_file]
|
|
11
|
+
|
|
12
|
+
return "/tmp/ruby-progress/#{options[name_key]}.pid" if options[name_key]
|
|
13
|
+
|
|
14
|
+
RubyProgress::Daemon.default_pid_file
|
|
15
|
+
end
|
|
16
|
+
|
|
9
17
|
def self.run
|
|
10
18
|
options = WormCLI::Options.parse_cli_options
|
|
11
19
|
|
|
@@ -25,8 +33,14 @@ module WormCLI
|
|
|
25
33
|
)
|
|
26
34
|
exit
|
|
27
35
|
elsif options[:daemon]
|
|
28
|
-
# Detach before starting daemon logic
|
|
29
|
-
|
|
36
|
+
# Detach (or background without detaching) before starting daemon logic
|
|
37
|
+
# so the invoking shell/script continues immediately.
|
|
38
|
+
if options[:no_detach]
|
|
39
|
+
PrgCLI.backgroundize
|
|
40
|
+
else
|
|
41
|
+
PrgCLI.daemonize
|
|
42
|
+
end
|
|
43
|
+
|
|
30
44
|
run_daemon_mode(options)
|
|
31
45
|
else
|
|
32
46
|
progress = RubyProgress::Worm.new(options)
|
|
@@ -47,29 +61,57 @@ module WormCLI
|
|
|
47
61
|
progress = RubyProgress::Worm.new(options)
|
|
48
62
|
|
|
49
63
|
begin
|
|
64
|
+
# Start job processor thread for worm
|
|
65
|
+
job_dir = RubyProgress::Daemon.job_dir_for_pid(pid_file)
|
|
66
|
+
job_thread = Thread.new do
|
|
67
|
+
RubyProgress::Daemon.process_jobs(job_dir) do |job|
|
|
68
|
+
jid = job['id'] || SecureRandom.uuid
|
|
69
|
+
log_path = begin
|
|
70
|
+
File.join(File.dirname(job_dir), "#{jid}.log")
|
|
71
|
+
rescue StandardError
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
oc = RubyProgress::OutputCapture.new(
|
|
76
|
+
command: job['command'],
|
|
77
|
+
lines: options[:output_lines] || 3,
|
|
78
|
+
position: options[:output_position] || :above,
|
|
79
|
+
log_path: log_path
|
|
80
|
+
)
|
|
81
|
+
oc.start
|
|
82
|
+
|
|
83
|
+
progress.instance_variable_set(:@output_capture, oc)
|
|
84
|
+
oc.wait
|
|
85
|
+
captured = oc.lines.join("\n")
|
|
86
|
+
exit_status = oc.exit_status
|
|
87
|
+
progress.instance_variable_set(:@output_capture, nil)
|
|
88
|
+
|
|
89
|
+
success = exit_status.to_i.zero?
|
|
90
|
+
if job['message']
|
|
91
|
+
RubyProgress::Utils.display_completion(
|
|
92
|
+
job['message'],
|
|
93
|
+
success: success,
|
|
94
|
+
show_checkmark: job['checkmark'] || false,
|
|
95
|
+
output_stream: :stdout,
|
|
96
|
+
icons: { success: options[:success_icon], error: options[:error_icon] }
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
{ 'exit_status' => exit_status, 'output' => captured, 'log_path' => log_path }
|
|
101
|
+
rescue StandardError
|
|
102
|
+
# ignore per-job errors
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
50
106
|
progress.run_daemon_mode(
|
|
51
107
|
success_message: options[:success],
|
|
52
108
|
show_checkmark: options[:checkmark],
|
|
53
|
-
control_message_file: RubyProgress::Daemon.control_message_file(pid_file)
|
|
109
|
+
control_message_file: RubyProgress::Daemon.control_message_file(pid_file),
|
|
110
|
+
icons: { success: options[:success_icon], error: options[:error_icon] }
|
|
54
111
|
)
|
|
55
112
|
ensure
|
|
113
|
+
job_thread&.kill
|
|
56
114
|
FileUtils.rm_f(pid_file)
|
|
57
115
|
end
|
|
58
116
|
end
|
|
59
|
-
|
|
60
|
-
def self.resolve_pid_file(options, name_key)
|
|
61
|
-
return options[:pid_file] if options[:pid_file]
|
|
62
|
-
|
|
63
|
-
if options[name_key]
|
|
64
|
-
"/tmp/ruby-progress/#{options[name_key]}.pid"
|
|
65
|
-
else
|
|
66
|
-
RubyProgress::Daemon.default_pid_file
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
# parse_cli_options is intentionally kept together; further extraction possible
|
|
71
|
-
# parse_cli_options is intentionally delegated to WormCLI::Options
|
|
72
|
-
def self.parse_cli_options
|
|
73
|
-
WormCLI::Options.parse_cli_options
|
|
74
|
-
end
|
|
75
117
|
end
|
|
@@ -9,8 +9,11 @@ module WormCLI
|
|
|
9
9
|
# the main dispatcher to keep the CLI module small and focused.
|
|
10
10
|
module Options
|
|
11
11
|
def self.parse_cli_options
|
|
12
|
-
options = {
|
|
13
|
-
|
|
12
|
+
options = {
|
|
13
|
+
output_position: :above,
|
|
14
|
+
output_lines: 3
|
|
15
|
+
}
|
|
16
|
+
# rubocop:disable Metrics/BlockLength
|
|
14
17
|
begin
|
|
15
18
|
OptionParser.new do |opts|
|
|
16
19
|
opts.banner = 'Usage: prg worm [options]'
|
|
@@ -48,10 +51,26 @@ module WormCLI
|
|
|
48
51
|
options[:command] = command
|
|
49
52
|
end
|
|
50
53
|
|
|
54
|
+
opts.on('--output-position POSITION', 'Position to render captured output: above or below (default: above)') do |pos|
|
|
55
|
+
options[:output_position] = pos.to_sym
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
opts.on('--output-lines N', Integer, 'Number of output lines to reserve for captured output (default: 3)') do |n|
|
|
59
|
+
options[:output_lines] = n
|
|
60
|
+
end
|
|
61
|
+
|
|
51
62
|
opts.on('--success MESSAGE', 'Success message to display') do |text|
|
|
52
63
|
options[:success] = text
|
|
53
64
|
end
|
|
54
65
|
|
|
66
|
+
opts.on('--success-icon ICON', 'Custom success icon to show with completion messages') do |ic|
|
|
67
|
+
options[:success_icon] = ic
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
opts.on('--error-icon ICON', 'Custom error icon to show with failure messages') do |ic|
|
|
71
|
+
options[:error_icon] = ic
|
|
72
|
+
end
|
|
73
|
+
|
|
55
74
|
opts.on('--error MESSAGE', 'Error message to display') do |text|
|
|
56
75
|
options[:error] = text
|
|
57
76
|
end
|
|
@@ -76,6 +95,16 @@ module WormCLI
|
|
|
76
95
|
options[:daemon_name] = name
|
|
77
96
|
end
|
|
78
97
|
|
|
98
|
+
# Accept --daemon-name as an alias for --daemon-as for compatibility
|
|
99
|
+
opts.on('--daemon-name NAME', 'Alias for --daemon-as (compat)') do |name|
|
|
100
|
+
options[:daemon] = true
|
|
101
|
+
options[:daemon_name] = name
|
|
102
|
+
end
|
|
103
|
+
|
|
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
|
+
|
|
79
108
|
opts.on('--pid-file FILE', 'Write process ID to file (default: /tmp/ruby-progress/progress.pid)') do |file|
|
|
80
109
|
options[:pid_file] = file
|
|
81
110
|
end
|
|
@@ -149,7 +178,7 @@ module WormCLI
|
|
|
149
178
|
puts "Run 'prg worm --help' for more information."
|
|
150
179
|
exit 1
|
|
151
180
|
end
|
|
152
|
-
|
|
181
|
+
# rubocop:enable Metrics/BlockLength
|
|
153
182
|
options
|
|
154
183
|
end
|
|
155
184
|
end
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
require 'open3'
|
|
5
5
|
require 'json'
|
|
6
6
|
require_relative '../utils'
|
|
7
|
+
require_relative '../output_capture'
|
|
7
8
|
|
|
8
9
|
# Runtime helper methods for RubyProgress::Worm
|
|
9
10
|
#
|
|
@@ -56,21 +57,36 @@ module WormRunner
|
|
|
56
57
|
stdout_content = nil
|
|
57
58
|
|
|
58
59
|
begin
|
|
59
|
-
stdout_content =
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
60
|
+
stdout_content = if $stdout.tty? && @output_stdout
|
|
61
|
+
oc = RubyProgress::OutputCapture.new(
|
|
62
|
+
command: @command,
|
|
63
|
+
lines: @output_lines || 3,
|
|
64
|
+
position: @output_position || :above
|
|
65
|
+
)
|
|
66
|
+
oc.start
|
|
67
|
+
@output_capture = oc
|
|
68
|
+
animate do
|
|
69
|
+
oc.wait
|
|
70
|
+
end
|
|
71
|
+
@output_capture = nil
|
|
72
|
+
oc.lines.join("\n")
|
|
73
|
+
else
|
|
74
|
+
animate do
|
|
75
|
+
Open3.popen3(@command) do |_stdin, stdout, stderr, wait_thr|
|
|
76
|
+
captured_stdout = stdout.read
|
|
77
|
+
stderr_content = stderr.read
|
|
78
|
+
exit_code = wait_thr.value.exitstatus
|
|
79
|
+
|
|
80
|
+
unless wait_thr.value.success?
|
|
81
|
+
error_msg = @error_text || "Command failed with exit code #{exit_code}"
|
|
82
|
+
error_msg += ": #{stderr_content.strip}" if stderr_content && !stderr_content.empty?
|
|
83
|
+
raise StandardError, error_msg
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
captured_stdout
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
74
90
|
|
|
75
91
|
puts stdout_content if @output_stdout && stdout_content
|
|
76
92
|
rescue StandardError
|
|
@@ -103,7 +119,7 @@ module WormRunner
|
|
|
103
119
|
@running = false
|
|
104
120
|
end
|
|
105
121
|
|
|
106
|
-
def run_daemon_mode(success_message: nil, show_checkmark: false, control_message_file: nil)
|
|
122
|
+
def run_daemon_mode(success_message: nil, show_checkmark: false, control_message_file: nil, icons: {})
|
|
107
123
|
@running = true
|
|
108
124
|
stop_requested = false
|
|
109
125
|
|
|
@@ -146,7 +162,8 @@ module WormRunner
|
|
|
146
162
|
final_message,
|
|
147
163
|
success: final_success,
|
|
148
164
|
show_checkmark: final_checkmark,
|
|
149
|
-
output_stream: :stdout
|
|
165
|
+
output_stream: :stdout,
|
|
166
|
+
icons: icons
|
|
150
167
|
)
|
|
151
168
|
end
|
|
152
169
|
|
|
@@ -161,6 +178,7 @@ module WormRunner
|
|
|
161
178
|
@direction ||= 1
|
|
162
179
|
|
|
163
180
|
message_part = @message && !@message.empty? ? "#{@message} " : ''
|
|
181
|
+
@output_capture&.redraw($stderr)
|
|
164
182
|
$stderr.print "\r\e[2K#{@start_chars}#{message_part}#{generate_dots(@position, @direction)}#{@end_chars}"
|
|
165
183
|
$stderr.flush
|
|
166
184
|
|
|
@@ -184,6 +202,7 @@ module WormRunner
|
|
|
184
202
|
|
|
185
203
|
while @running
|
|
186
204
|
message_part = @message && !@message.empty? ? "#{@message} " : ''
|
|
205
|
+
@output_capture&.redraw($stderr)
|
|
187
206
|
$stderr.print "\r\e[2K#{@start_chars}#{message_part}#{generate_dots(position, direction)}#{@end_chars}"
|
|
188
207
|
$stderr.flush
|
|
189
208
|
|
|
@@ -209,6 +228,7 @@ module WormRunner
|
|
|
209
228
|
|
|
210
229
|
while @running && !stop_requested_proc.call
|
|
211
230
|
message_part = @message && !@message.empty? ? "#{@message} " : ''
|
|
231
|
+
@output_capture&.redraw($stderr)
|
|
212
232
|
|
|
213
233
|
$stderr.print "\r\e[2K"
|
|
214
234
|
|