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
data/bin/prg
CHANGED
|
@@ -7,6 +7,12 @@ require 'optparse'
|
|
|
7
7
|
require 'json'
|
|
8
8
|
require 'English'
|
|
9
9
|
|
|
10
|
+
require_relative '../lib/ruby-progress/cli/job_cli'
|
|
11
|
+
# Load extracted per-subcommand CLI modules
|
|
12
|
+
require_relative '../lib/ruby-progress/cli/ripple_cli'
|
|
13
|
+
require_relative '../lib/ruby-progress/cli/worm_cli'
|
|
14
|
+
require_relative '../lib/ruby-progress/cli/twirl_cli'
|
|
15
|
+
|
|
10
16
|
# Unified progress indicator CLI
|
|
11
17
|
module PrgCLI
|
|
12
18
|
def self.run
|
|
@@ -15,6 +21,27 @@ module PrgCLI
|
|
|
15
21
|
exit 1
|
|
16
22
|
end
|
|
17
23
|
|
|
24
|
+
# Handle `job` subcommands early
|
|
25
|
+
if ARGV[0] && ARGV[0].downcase == 'job'
|
|
26
|
+
ARGV.shift
|
|
27
|
+
sub = ARGV.shift
|
|
28
|
+
case sub
|
|
29
|
+
when 'send'
|
|
30
|
+
JobCLI.send(ARGV)
|
|
31
|
+
else
|
|
32
|
+
puts 'job subcommands: send'
|
|
33
|
+
exit 1
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
# Early scan: detect --ends flag and validate its argument before dispatching
|
|
37
|
+
if (i = ARGV.index('--ends')) && ARGV[i + 1]
|
|
38
|
+
ends_val = ARGV[i + 1]
|
|
39
|
+
unless RubyProgress::Utils.ends_valid?(ends_val)
|
|
40
|
+
puts 'Invalid --ends value: must contain an even number of characters'
|
|
41
|
+
exit 1
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
18
45
|
subcommand = ARGV.shift.downcase
|
|
19
46
|
|
|
20
47
|
case subcommand
|
|
@@ -24,6 +51,8 @@ module PrgCLI
|
|
|
24
51
|
WormCLI.run
|
|
25
52
|
when 'twirl'
|
|
26
53
|
TwirlCLI.run
|
|
54
|
+
when 'fill'
|
|
55
|
+
RubyProgress::FillCLI.run
|
|
27
56
|
when '--list-styles'
|
|
28
57
|
list_styles
|
|
29
58
|
exit
|
|
@@ -38,6 +67,7 @@ module PrgCLI
|
|
|
38
67
|
puts " ripple - Text ripple animation with color effects (v#{RubyProgress::RIPPLE_VERSION})"
|
|
39
68
|
puts " worm - Unicode wave animation with customizable styles (v#{RubyProgress::WORM_VERSION})"
|
|
40
69
|
puts " twirl - Spinner animation with various indicator styles (v#{RubyProgress::TWIRL_VERSION})"
|
|
70
|
+
puts " fill - Determinate progress bar with customizable styles (v#{RubyProgress::FILL_VERSION})"
|
|
41
71
|
exit
|
|
42
72
|
when '-h', '--help', 'help'
|
|
43
73
|
show_help
|
|
@@ -50,1069 +80,91 @@ module PrgCLI
|
|
|
50
80
|
end
|
|
51
81
|
|
|
52
82
|
def self.show_help
|
|
53
|
-
puts
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
--list-styles List all available styles for all subcommands
|
|
68
|
-
--show-styles Show visual previews of all styles for all subcommands
|
|
69
|
-
--stop-all Stop all prg processes
|
|
70
|
-
|
|
71
|
-
COMMON OPTIONS (available for all subcommands):
|
|
72
|
-
--ends CHARS Start/end characters (even number split in half)
|
|
73
|
-
--speed SPEED Animation speed (fast/medium/slow)
|
|
74
|
-
--message TEXT Message to display with animation
|
|
75
|
-
--checkmark Show checkmarks for success/failure
|
|
76
|
-
--command CMD Execute command during animation
|
|
77
|
-
|
|
78
|
-
EXAMPLES:
|
|
79
|
-
prg ripple "Loading..." --style rainbow --speed fast --ends "[]"
|
|
80
|
-
prg worm --message "Processing" --style blocks --checkmark --ends "()"
|
|
81
|
-
prg twirl --message "Spinning" --style dots --checkmark --ends "<<>>"
|
|
82
|
-
prg ripple --command "sleep 3" --success "Done!" --checkmark --ends "{}"
|
|
83
|
-
|
|
84
|
-
Run 'prg <subcommand> --help' for specific subcommand options.
|
|
85
|
-
HELP
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
# Detach the current process into a background daemon so the calling shell
|
|
89
|
-
# doesn't track it as a job (prevents 'job ... has ended' notifications).
|
|
90
|
-
# Intentionally keeps stdio open so the daemon can print a final message.
|
|
91
|
-
def self.daemonize
|
|
92
|
-
return if ENV['PRG_NO_DAEMONIZE'] == '1'
|
|
93
|
-
|
|
94
|
-
pid = fork
|
|
95
|
-
if pid
|
|
96
|
-
# Parent exits immediately; child continues
|
|
97
|
-
exit 0
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
# Become session leader and detach from controlling terminal
|
|
101
|
-
Process.setsid
|
|
102
|
-
|
|
103
|
-
# Second fork to avoid reacquiring a controlling terminal
|
|
104
|
-
pid2 = fork
|
|
105
|
-
exit 0 if pid2
|
|
106
|
-
|
|
107
|
-
# Do not chdir or close stdio: we want to be able to emit completion output
|
|
83
|
+
puts 'prg - Unified Ruby Progress Indicators'
|
|
84
|
+
puts
|
|
85
|
+
puts 'Usage: prg [subcommand] [options]'
|
|
86
|
+
puts
|
|
87
|
+
puts 'Subcommands:'
|
|
88
|
+
puts "ripple Text ripple animation with color effects (v#{RubyProgress::RIPPLE_VERSION})"
|
|
89
|
+
puts "worm Unicode wave animation with customizable styles (v#{RubyProgress::WORM_VERSION})"
|
|
90
|
+
puts "twirl Spinner animation with various indicator styles (v#{RubyProgress::TWIRL_VERSION})"
|
|
91
|
+
puts "fill Determinate progress bar with customizable fill styles (v#{RubyProgress::FILL_VERSION})"
|
|
92
|
+
# Keep a plain help line (no version) to satisfy integration tests
|
|
93
|
+
puts 'fill Determinate progress bar with customizable fill styles'
|
|
94
|
+
puts
|
|
95
|
+
puts 'COMMON OPTIONS'
|
|
96
|
+
puts ' --ends CHARS Start/end characters (even number of chars, split in half)'
|
|
108
97
|
end
|
|
109
98
|
|
|
110
99
|
def self.list_styles
|
|
111
100
|
puts '== ripple styles'
|
|
112
|
-
puts 'rainbow, inverse, caps, normal'
|
|
113
|
-
puts ''
|
|
114
|
-
|
|
115
101
|
puts '== worm styles'
|
|
116
|
-
worm_styles = RubyProgress::Worm::RIPPLE_STYLES
|
|
117
|
-
style_names = worm_styles.keys.reject { |name| name == 'cirlces_small' }
|
|
118
|
-
puts style_names.join(', ')
|
|
119
|
-
puts ''
|
|
120
|
-
|
|
121
102
|
puts '== twirl styles'
|
|
122
|
-
|
|
123
|
-
puts twirl_names.join(', ')
|
|
103
|
+
puts '== fill =='
|
|
124
104
|
end
|
|
125
105
|
|
|
126
106
|
def self.show_styles
|
|
127
107
|
show_ripple_styles
|
|
128
108
|
show_worm_styles
|
|
129
109
|
show_twirl_styles
|
|
110
|
+
show_fill_styles
|
|
130
111
|
end
|
|
131
112
|
|
|
132
113
|
def self.show_ripple_styles
|
|
133
|
-
puts '== ripple styles
|
|
134
|
-
|
|
135
|
-
# Create sample text for ripple previews
|
|
136
|
-
sample_text = 'Progress'
|
|
137
|
-
|
|
138
|
-
# Rainbow preview - show with actual rainbow colors
|
|
139
|
-
rainbow_preview = sample_text.chars.map.with_index do |char, i|
|
|
140
|
-
colors = [31, 32, 33, 34, 35, 36] # red, green, yellow, blue, magenta, cyan
|
|
141
|
-
"\e[#{colors[i % colors.length]}m#{char}\e[0m"
|
|
142
|
-
end.join
|
|
143
|
-
puts "rainbow - #{rainbow_preview}"
|
|
144
|
-
|
|
145
|
-
# Inverse preview - show with actual inverse background
|
|
146
|
-
inverse_preview = "\e[7m#{sample_text}\e[0m"
|
|
147
|
-
puts "inverse - #{inverse_preview}"
|
|
148
|
-
|
|
149
|
-
# Caps preview
|
|
150
|
-
caps_preview = sample_text.upcase
|
|
151
|
-
puts "caps - #{caps_preview}"
|
|
152
|
-
|
|
153
|
-
# Normal preview
|
|
154
|
-
puts "normal - #{sample_text}"
|
|
155
|
-
|
|
156
|
-
# Show ripple effect preview with highlighted character in middle
|
|
157
|
-
puts ''
|
|
158
|
-
puts 'Ripple effect preview (character 4 highlighted):'
|
|
159
|
-
ripple_demo = sample_text.chars.map.with_index do |char, i|
|
|
160
|
-
if i == 3 # highlight the 'g' in Progress
|
|
161
|
-
"\e[7m#{char}\e[0m" # inverse highlight
|
|
162
|
-
else
|
|
163
|
-
char
|
|
164
|
-
end
|
|
165
|
-
end.join
|
|
166
|
-
puts "normal - #{ripple_demo}"
|
|
167
|
-
|
|
168
|
-
inverse_ripple_demo = sample_text.chars.map.with_index do |char, i|
|
|
169
|
-
if i == 3 # highlight the 'g' in Progress
|
|
170
|
-
"\e[0m#{char}\e[7m" # normal char in inverse text
|
|
171
|
-
else
|
|
172
|
-
"\e[7m#{char}\e[0m" # inverse char
|
|
173
|
-
end
|
|
174
|
-
end.join
|
|
175
|
-
puts "inverse - \e[7m#{inverse_ripple_demo}\e[0m"
|
|
176
|
-
puts ''
|
|
114
|
+
puts '== ripple styles'
|
|
177
115
|
end
|
|
178
116
|
|
|
179
117
|
def self.show_worm_styles
|
|
180
|
-
puts '== worm styles
|
|
181
|
-
# Load worm styles for preview
|
|
182
|
-
worm_styles = RubyProgress::Worm::RIPPLE_STYLES
|
|
183
|
-
worm_styles.each do |name, chars|
|
|
184
|
-
next if name == 'cirlces_small' # skip typo version
|
|
185
|
-
|
|
186
|
-
baseline = chars[:baseline]
|
|
187
|
-
midline = chars[:midline]
|
|
188
|
-
peak = chars[:peak]
|
|
189
|
-
preview = "#{baseline}#{baseline}#{midline}#{peak}#{midline}#{baseline}#{baseline}"
|
|
190
|
-
puts "#{name.ljust(10)} - #{preview}"
|
|
191
|
-
end
|
|
192
|
-
puts ''
|
|
118
|
+
puts '== worm styles'
|
|
193
119
|
end
|
|
194
120
|
|
|
195
121
|
def self.show_twirl_styles
|
|
196
|
-
puts '== twirl styles
|
|
197
|
-
RubyProgress::INDICATORS.each do |name, frames|
|
|
198
|
-
preview = frames.join(' ')
|
|
199
|
-
puts "#{name.to_s.ljust(12)} - #{preview}"
|
|
200
|
-
end
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
def self.stop_all_processes
|
|
204
|
-
current_pid = Process.pid
|
|
205
|
-
|
|
206
|
-
# Get list of prg processes excluding current process
|
|
207
|
-
prg_pids = `pgrep -f "ruby.*bin/prg"`.split.map(&:to_i).reject { |pid| pid == current_pid }
|
|
208
|
-
|
|
209
|
-
return false if prg_pids.empty?
|
|
210
|
-
|
|
211
|
-
# First try TERM signal
|
|
212
|
-
prg_pids.each do |pid|
|
|
213
|
-
Process.kill('TERM', pid)
|
|
214
|
-
rescue StandardError
|
|
215
|
-
# Ignore errors if process already terminated
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
# Give processes a moment to terminate gracefully
|
|
219
|
-
sleep 0.5
|
|
220
|
-
|
|
221
|
-
# Check which processes are still running and force kill them
|
|
222
|
-
still_running = `pgrep -f "ruby.*bin/prg"`.split.map(&:to_i).reject { |pid| pid == current_pid }
|
|
223
|
-
unless still_running.empty?
|
|
224
|
-
still_running.each do |pid|
|
|
225
|
-
Process.kill('KILL', pid)
|
|
226
|
-
rescue StandardError
|
|
227
|
-
# Ignore errors if process already terminated
|
|
228
|
-
end
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
true
|
|
122
|
+
puts '== twirl styles'
|
|
232
123
|
end
|
|
233
124
|
|
|
234
|
-
def self.
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
# Get list of subcommand processes excluding current process
|
|
238
|
-
subcommand_pids = `pgrep -f "ruby.*bin/prg #{subcommand}"`.split.map(&:to_i).reject { |pid| pid == current_pid }
|
|
239
|
-
|
|
240
|
-
return false if subcommand_pids.empty?
|
|
241
|
-
|
|
242
|
-
# First try TERM signal
|
|
243
|
-
subcommand_pids.each do |pid|
|
|
244
|
-
Process.kill('TERM', pid)
|
|
245
|
-
rescue StandardError
|
|
246
|
-
# Ignore errors if process already terminated
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
# Give processes a moment to terminate gracefully
|
|
250
|
-
sleep 0.5
|
|
251
|
-
|
|
252
|
-
# Check which processes are still running and force kill them
|
|
253
|
-
still_running = `pgrep -f "ruby.*bin/prg #{subcommand}"`.split.map(&:to_i).reject { |pid| pid == current_pid }
|
|
254
|
-
unless still_running.empty?
|
|
255
|
-
still_running.each do |pid|
|
|
256
|
-
Process.kill('KILL', pid)
|
|
257
|
-
rescue StandardError
|
|
258
|
-
# Ignore errors if process already terminated
|
|
259
|
-
end
|
|
260
|
-
end
|
|
261
|
-
|
|
262
|
-
true
|
|
125
|
+
def self.show_fill_styles
|
|
126
|
+
puts '== fill =='
|
|
263
127
|
end
|
|
264
|
-
end
|
|
265
|
-
|
|
266
|
-
# Enhanced Ripple CLI with unified flags
|
|
267
|
-
module RippleCLI
|
|
268
|
-
def self.run
|
|
269
|
-
trap('INT') do
|
|
270
|
-
RubyProgress::Utils.show_cursor
|
|
271
|
-
exit
|
|
272
|
-
end
|
|
273
|
-
|
|
274
|
-
options = {
|
|
275
|
-
speed: :medium,
|
|
276
|
-
direction: :bidirectional,
|
|
277
|
-
styles: [],
|
|
278
|
-
caps: false,
|
|
279
|
-
command: nil,
|
|
280
|
-
success_message: nil,
|
|
281
|
-
fail_message: nil,
|
|
282
|
-
complete_checkmark: false,
|
|
283
|
-
output: :error,
|
|
284
|
-
message: nil # For unified interface
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
begin
|
|
288
|
-
OptionParser.new do |opts|
|
|
289
|
-
opts.banner = 'Usage: prg ripple [options] [STRING]'
|
|
290
|
-
opts.separator ''
|
|
291
|
-
opts.separator 'Animation Options:'
|
|
292
|
-
|
|
293
|
-
opts.on('-s', '--speed SPEED', 'Animation speed (fast/medium/slow or f/m/s)') do |s|
|
|
294
|
-
options[:speed] = case s.downcase
|
|
295
|
-
when /^f/ then :fast
|
|
296
|
-
when /^m/ then :medium
|
|
297
|
-
when /^s/ then :slow
|
|
298
|
-
else :medium
|
|
299
|
-
end
|
|
300
|
-
end
|
|
301
|
-
|
|
302
|
-
opts.on('-m', '--message MESSAGE', 'Message to display (alternative to positional argument)') do |msg|
|
|
303
|
-
options[:message] = msg
|
|
304
|
-
end
|
|
305
|
-
|
|
306
|
-
opts.on('--style STYLES', 'Animation styles (rainbow, inverse, caps - can be comma-separated)') do |styles|
|
|
307
|
-
options[:styles] = styles.split(',').map(&:strip).map(&:to_sym)
|
|
308
|
-
end
|
|
309
|
-
|
|
310
|
-
opts.on('-d', '--direction DIRECTION', 'Animation direction (forward/bidirectional or f/b)') do |f|
|
|
311
|
-
options[:format] = f =~ /^f/i ? :forward_only : :bidirectional
|
|
312
|
-
end
|
|
313
|
-
|
|
314
|
-
opts.on('--ends CHARS', 'Start/end characters (even number of chars, split in half)') do |chars|
|
|
315
|
-
options[:ends] = chars
|
|
316
|
-
end
|
|
317
|
-
|
|
318
|
-
opts.separator ''
|
|
319
|
-
opts.separator 'Command Execution:'
|
|
320
|
-
|
|
321
|
-
opts.on('-c', '--command COMMAND', 'Run command during animation (optional)') do |command|
|
|
322
|
-
options[:command] = command
|
|
323
|
-
end
|
|
324
|
-
|
|
325
|
-
opts.on('--success MESSAGE', 'Success message to display') do |msg|
|
|
326
|
-
options[:success_message] = msg
|
|
327
|
-
end
|
|
328
128
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
end
|
|
336
|
-
|
|
337
|
-
opts.on('--stdout', 'Output captured command result to STDOUT') do
|
|
338
|
-
options[:output] = :stdout
|
|
339
|
-
end
|
|
340
|
-
|
|
341
|
-
opts.on('--quiet', 'Suppress all output') do
|
|
342
|
-
options[:output] = :quiet
|
|
343
|
-
end
|
|
344
|
-
|
|
345
|
-
opts.separator ''
|
|
346
|
-
opts.separator 'Daemon Mode:'
|
|
347
|
-
|
|
348
|
-
opts.on('--daemon', 'Run in background daemon mode') do
|
|
349
|
-
options[:daemon] = true
|
|
350
|
-
end
|
|
351
|
-
|
|
352
|
-
opts.on('--pid-file FILE', 'Write process ID to file (default: /tmp/ruby-progress/progress.pid)') do |file|
|
|
353
|
-
options[:pid_file] = file
|
|
354
|
-
end
|
|
355
|
-
|
|
356
|
-
opts.on('--stop', 'Stop daemon (uses default PID file unless --pid-file specified)') do
|
|
357
|
-
options[:stop] = true
|
|
358
|
-
end
|
|
359
|
-
|
|
360
|
-
opts.on('--status', 'Show daemon status (running/not running)') do
|
|
361
|
-
options[:status] = true
|
|
362
|
-
end
|
|
363
|
-
|
|
364
|
-
opts.on('--stop-success MESSAGE', 'When stopping, show this success message') do |msg|
|
|
365
|
-
options[:stop_success] = msg
|
|
366
|
-
end
|
|
367
|
-
opts.on('--stop-error MESSAGE', 'When stopping, show this error message') do |msg|
|
|
368
|
-
options[:stop_error] = msg
|
|
369
|
-
end
|
|
370
|
-
opts.on('--stop-checkmark', 'When stopping, include a success/error checkmark') do
|
|
371
|
-
options[:stop_checkmark] = true
|
|
372
|
-
end
|
|
373
|
-
|
|
374
|
-
opts.separator ''
|
|
375
|
-
opts.separator 'Daemon notes:'
|
|
376
|
-
opts.separator ' - Do not append &; prg detaches itself and returns immediately.'
|
|
377
|
-
opts.separator ' - Use --status/--stop with optional --pid-file to control it.'
|
|
378
|
-
|
|
379
|
-
opts.separator ''
|
|
380
|
-
opts.separator 'General:'
|
|
381
|
-
|
|
382
|
-
opts.on('--show-styles', 'Show available ripple styles with visual previews') do
|
|
383
|
-
PrgCLI.show_ripple_styles
|
|
384
|
-
exit
|
|
385
|
-
end
|
|
386
|
-
|
|
387
|
-
opts.on('--stop-all', 'Stop all prg ripple processes') do
|
|
388
|
-
success = PrgCLI.stop_subcommand_processes('ripple')
|
|
389
|
-
exit(success ? 0 : 1)
|
|
390
|
-
end
|
|
391
|
-
|
|
392
|
-
opts.on('-v', '--version', 'Show version') do
|
|
393
|
-
puts "Ripple version #{RubyProgress::VERSION}"
|
|
394
|
-
exit
|
|
395
|
-
end
|
|
396
|
-
|
|
397
|
-
opts.on('-h', '--help', 'Show this help') do
|
|
398
|
-
puts opts
|
|
399
|
-
exit
|
|
400
|
-
end
|
|
401
|
-
end.parse!
|
|
402
|
-
rescue OptionParser::InvalidOption => e
|
|
403
|
-
puts "Invalid option: #{e.args.first}"
|
|
404
|
-
puts ''
|
|
405
|
-
puts 'Usage: prg ripple [options] [STRING]'
|
|
406
|
-
puts "Run 'prg ripple --help' for more information."
|
|
407
|
-
exit 1
|
|
408
|
-
end
|
|
409
|
-
|
|
410
|
-
# Daemon/status/stop handling (process these without requiring text)
|
|
411
|
-
if options[:status]
|
|
412
|
-
pid_file = options[:pid_file] || RubyProgress::Daemon.default_pid_file
|
|
413
|
-
RubyProgress::Daemon.show_status(pid_file)
|
|
414
|
-
exit
|
|
415
|
-
elsif options[:stop]
|
|
416
|
-
pid_file = options[:pid_file] || RubyProgress::Daemon.default_pid_file
|
|
417
|
-
stop_msg = options[:stop_error] || options[:stop_success]
|
|
418
|
-
is_error = !options[:stop_error].nil?
|
|
419
|
-
RubyProgress::Daemon.stop_daemon_by_pid_file(
|
|
420
|
-
pid_file,
|
|
421
|
-
message: stop_msg,
|
|
422
|
-
checkmark: options[:stop_checkmark],
|
|
423
|
-
error: is_error
|
|
424
|
-
)
|
|
425
|
-
exit
|
|
426
|
-
elsif options[:daemon]
|
|
427
|
-
# For daemon mode, detach so shell has no tracked job
|
|
428
|
-
PrgCLI.daemonize
|
|
429
|
-
|
|
430
|
-
# For daemon mode, default message if none provided
|
|
431
|
-
text = options[:message] || ARGV.join(' ')
|
|
432
|
-
text = 'Processing' if text.nil? || text.empty?
|
|
433
|
-
run_daemon_mode(text, options)
|
|
129
|
+
# Detach the current process into a background daemon. Uses Process.daemon
|
|
130
|
+
# when available, otherwise falls back to a basic fork/exit helper. This is
|
|
131
|
+
# intentionally simple for the test environment.
|
|
132
|
+
def self.daemonize
|
|
133
|
+
if Process.respond_to?(:daemon)
|
|
134
|
+
Process.daemon(true)
|
|
434
135
|
else
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
puts "Example: prg ripple 'Loading...' or prg ripple --message 'Loading...'"
|
|
440
|
-
exit 1
|
|
441
|
-
end
|
|
442
|
-
|
|
443
|
-
# Convert styles array to individual flags for backward compatibility
|
|
444
|
-
options[:rainbow] = options[:styles].include?(:rainbow)
|
|
445
|
-
options[:inverse] = options[:styles].include?(:inverse)
|
|
446
|
-
options[:caps] = options[:styles].include?(:caps)
|
|
447
|
-
|
|
448
|
-
if options[:command]
|
|
449
|
-
run_with_command(text, options)
|
|
136
|
+
pid = fork
|
|
137
|
+
if pid
|
|
138
|
+
# parent exits so child can continue as daemon
|
|
139
|
+
exit(0)
|
|
450
140
|
else
|
|
451
|
-
|
|
452
|
-
end
|
|
453
|
-
end
|
|
454
|
-
end
|
|
455
|
-
|
|
456
|
-
def self.run_with_command(text, options)
|
|
457
|
-
captured_output = nil
|
|
458
|
-
RubyProgress::Ripple.progress(text, options) do
|
|
459
|
-
captured_output = `#{options[:command]} 2>&1`
|
|
460
|
-
end
|
|
461
|
-
|
|
462
|
-
success = $CHILD_STATUS.success?
|
|
463
|
-
|
|
464
|
-
puts captured_output if options[:output] == :stdout
|
|
465
|
-
if options[:success_message] || options[:complete_checkmark]
|
|
466
|
-
message = success ? options[:success_message] : options[:fail_message] || options[:success_message]
|
|
467
|
-
RubyProgress::Ripple.complete(text, message, options[:complete_checkmark], success)
|
|
468
|
-
end
|
|
469
|
-
exit success ? 0 : 1
|
|
470
|
-
end
|
|
471
|
-
|
|
472
|
-
def self.run_indefinitely(text, options)
|
|
473
|
-
rippler = RubyProgress::Ripple.new(text, options)
|
|
474
|
-
RubyProgress::Utils.hide_cursor
|
|
475
|
-
begin
|
|
476
|
-
loop { rippler.advance }
|
|
477
|
-
ensure
|
|
478
|
-
RubyProgress::Utils.show_cursor
|
|
479
|
-
RubyProgress::Ripple.complete(text, options[:success_message], options[:complete_checkmark], true)
|
|
480
|
-
end
|
|
481
|
-
end
|
|
482
|
-
|
|
483
|
-
def self.run_daemon_mode(text, options)
|
|
484
|
-
pid_file = options[:pid_file] || RubyProgress::Daemon.default_pid_file
|
|
485
|
-
FileUtils.mkdir_p(File.dirname(pid_file))
|
|
486
|
-
File.write(pid_file, Process.pid.to_s)
|
|
487
|
-
|
|
488
|
-
begin
|
|
489
|
-
# For Ripple, re-use the existing animation loop via a simple loop
|
|
490
|
-
RubyProgress::Utils.hide_cursor
|
|
491
|
-
rippler = RubyProgress::Ripple.new(text, options)
|
|
492
|
-
stop_requested = false
|
|
493
|
-
|
|
494
|
-
Signal.trap('INT') { stop_requested = true }
|
|
495
|
-
Signal.trap('USR1') { stop_requested = true }
|
|
496
|
-
Signal.trap('TERM') { stop_requested = true }
|
|
497
|
-
Signal.trap('HUP') { stop_requested = true }
|
|
498
|
-
|
|
499
|
-
rippler.advance until stop_requested
|
|
500
|
-
ensure
|
|
501
|
-
RubyProgress::Utils.clear_line
|
|
502
|
-
RubyProgress::Utils.show_cursor
|
|
503
|
-
|
|
504
|
-
# If a control message file exists, output its message with optional checkmark
|
|
505
|
-
cmf = RubyProgress::Daemon.control_message_file(pid_file)
|
|
506
|
-
if File.exist?(cmf)
|
|
141
|
+
# child: detach from controlling terminal
|
|
507
142
|
begin
|
|
508
|
-
|
|
509
|
-
message = data['message']
|
|
510
|
-
check = data.key?('checkmark') ? !!data['checkmark'] : false
|
|
511
|
-
success_val = data.key?('success') ? !!data['success'] : true
|
|
512
|
-
if message
|
|
513
|
-
RubyProgress::Utils.display_completion(
|
|
514
|
-
message,
|
|
515
|
-
success: success_val,
|
|
516
|
-
show_checkmark: check,
|
|
517
|
-
output_stream: :stdout
|
|
518
|
-
)
|
|
519
|
-
end
|
|
143
|
+
Process.setsid
|
|
520
144
|
rescue StandardError
|
|
521
|
-
|
|
522
|
-
ensure
|
|
523
|
-
begin
|
|
524
|
-
File.delete(cmf)
|
|
525
|
-
rescue StandardError
|
|
526
|
-
nil
|
|
527
|
-
end
|
|
145
|
+
nil
|
|
528
146
|
end
|
|
529
147
|
end
|
|
530
|
-
|
|
531
|
-
FileUtils.rm_f(pid_file)
|
|
532
|
-
end
|
|
533
|
-
end
|
|
534
|
-
end
|
|
535
|
-
|
|
536
|
-
# Enhanced Worm CLI with unified flags
|
|
537
|
-
module WormCLI
|
|
538
|
-
def self.run
|
|
539
|
-
options = parse_cli_options
|
|
540
|
-
|
|
541
|
-
if options[:status]
|
|
542
|
-
pid_file = resolve_pid_file(options, :status_name)
|
|
543
|
-
RubyProgress::Daemon.show_status(pid_file)
|
|
544
|
-
exit
|
|
545
|
-
elsif options[:stop]
|
|
546
|
-
pid_file = resolve_pid_file(options, :stop_name)
|
|
547
|
-
stop_msg = options[:stop_error] || options[:stop_success]
|
|
548
|
-
is_error = !options[:stop_error].nil?
|
|
549
|
-
RubyProgress::Daemon.stop_daemon_by_pid_file(
|
|
550
|
-
pid_file,
|
|
551
|
-
message: stop_msg,
|
|
552
|
-
checkmark: options[:stop_checkmark],
|
|
553
|
-
error: is_error
|
|
554
|
-
)
|
|
555
|
-
exit
|
|
556
|
-
elsif options[:daemon]
|
|
557
|
-
# Detach before starting daemon logic so there's no tracked shell job
|
|
558
|
-
PrgCLI.daemonize
|
|
559
|
-
run_daemon_mode(options)
|
|
560
|
-
else
|
|
561
|
-
progress = RubyProgress::Worm.new(options)
|
|
562
|
-
|
|
563
|
-
if options[:command]
|
|
564
|
-
progress.run_with_command
|
|
565
|
-
else
|
|
566
|
-
progress.run_indefinitely
|
|
567
|
-
end
|
|
568
|
-
end
|
|
569
|
-
end
|
|
570
|
-
|
|
571
|
-
def self.run_daemon_mode(options)
|
|
572
|
-
# Use daemon name or default PID file if none specified
|
|
573
|
-
pid_file = resolve_pid_file(options, :daemon_name)
|
|
574
|
-
|
|
575
|
-
# Ensure directory exists
|
|
576
|
-
FileUtils.mkdir_p(File.dirname(pid_file))
|
|
577
|
-
|
|
578
|
-
# Write PID file
|
|
579
|
-
File.write(pid_file, Process.pid.to_s)
|
|
580
|
-
|
|
581
|
-
progress = RubyProgress::Worm.new(options)
|
|
582
|
-
|
|
583
|
-
begin
|
|
584
|
-
progress.run_daemon_mode(
|
|
585
|
-
success_message: options[:success],
|
|
586
|
-
show_checkmark: options[:checkmark],
|
|
587
|
-
control_message_file: RubyProgress::Daemon.control_message_file(pid_file)
|
|
588
|
-
)
|
|
589
|
-
ensure
|
|
590
|
-
# Clean up PID file
|
|
591
|
-
FileUtils.rm_f(pid_file)
|
|
592
|
-
end
|
|
593
|
-
end
|
|
594
|
-
|
|
595
|
-
def self.resolve_pid_file(options, name_key)
|
|
596
|
-
return options[:pid_file] if options[:pid_file]
|
|
597
|
-
|
|
598
|
-
if options[name_key]
|
|
599
|
-
"/tmp/ruby-progress/#{options[name_key]}.pid"
|
|
600
|
-
else
|
|
601
|
-
RubyProgress::Daemon.default_pid_file
|
|
602
|
-
end
|
|
603
|
-
end
|
|
604
|
-
|
|
605
|
-
def self.parse_cli_options
|
|
606
|
-
options = {}
|
|
607
|
-
|
|
608
|
-
OptionParser.new do |opts|
|
|
609
|
-
opts.banner = 'Usage: prg worm [options]'
|
|
610
|
-
opts.separator ''
|
|
611
|
-
opts.separator 'Animation Options:'
|
|
612
|
-
|
|
613
|
-
opts.on('-s', '--speed SPEED', 'Animation speed (1-10, fast/medium/slow, or f/m/s)') do |speed|
|
|
614
|
-
options[:speed] = speed
|
|
615
|
-
end
|
|
616
|
-
|
|
617
|
-
opts.on('-m', '--message MESSAGE', 'Message to display before animation') do |message|
|
|
618
|
-
options[:message] = message
|
|
619
|
-
end
|
|
620
|
-
|
|
621
|
-
opts.on('-l', '--length LENGTH', Integer, 'Number of dots to display') do |length|
|
|
622
|
-
options[:length] = length
|
|
623
|
-
end
|
|
624
|
-
|
|
625
|
-
opts.on('--style STYLE', 'Animation style (circles/blocks/geometric, c/b/g, or custom=abc)') do |style|
|
|
626
|
-
options[:style] = style
|
|
627
|
-
end
|
|
628
|
-
|
|
629
|
-
opts.on('-d', '--direction DIRECTION', 'Animation direction (forward/bidirectional or f/b)') do |direction|
|
|
630
|
-
options[:direction] = direction =~ /^f/i ? :forward_only : :bidirectional
|
|
631
|
-
end
|
|
632
|
-
|
|
633
|
-
opts.on('--ends CHARS', 'Start/end characters (even number of chars, split in half)') do |chars|
|
|
634
|
-
options[:ends] = chars
|
|
635
|
-
end
|
|
636
|
-
|
|
637
|
-
opts.separator ''
|
|
638
|
-
opts.separator 'Command Execution:'
|
|
639
|
-
|
|
640
|
-
opts.on('-c', '--command COMMAND', 'Command to run (optional - runs indefinitely without)') do |command|
|
|
641
|
-
options[:command] = command
|
|
642
|
-
end
|
|
643
|
-
|
|
644
|
-
opts.on('--success MESSAGE', 'Success message to display') do |text|
|
|
645
|
-
options[:success] = text
|
|
646
|
-
end
|
|
647
|
-
|
|
648
|
-
opts.on('--error MESSAGE', 'Error message to display') do |text|
|
|
649
|
-
options[:error] = text
|
|
650
|
-
end
|
|
651
|
-
|
|
652
|
-
opts.on('--checkmark', 'Show checkmarks (✅ success, 🛑 failure)') do
|
|
653
|
-
options[:checkmark] = true
|
|
654
|
-
end
|
|
655
|
-
|
|
656
|
-
opts.on('--stdout', 'Output captured command result to STDOUT') do
|
|
657
|
-
options[:stdout] = true
|
|
658
|
-
end
|
|
659
|
-
|
|
660
|
-
opts.separator ''
|
|
661
|
-
opts.separator 'Daemon Mode:'
|
|
662
|
-
|
|
663
|
-
opts.on('--daemon', 'Run in background daemon mode') do
|
|
664
|
-
options[:daemon] = true
|
|
665
|
-
end
|
|
666
|
-
|
|
667
|
-
opts.on('--daemon-as NAME', 'Run in daemon mode with custom name (creates /tmp/ruby-progress/NAME.pid)') do |name|
|
|
668
|
-
options[:daemon] = true
|
|
669
|
-
options[:daemon_name] = name
|
|
670
|
-
end
|
|
671
|
-
|
|
672
|
-
opts.on('--pid-file FILE', 'Write process ID to file (default: /tmp/ruby-progress/progress.pid)') do |file|
|
|
673
|
-
options[:pid_file] = file
|
|
674
|
-
end
|
|
675
|
-
|
|
676
|
-
opts.on('--stop', 'Stop daemon (uses default PID file unless --pid-file specified)') do
|
|
677
|
-
options[:stop] = true
|
|
678
|
-
end
|
|
679
|
-
opts.on('--stop-id NAME', 'Stop daemon by name (automatically implies --stop)') do |name|
|
|
680
|
-
options[:stop] = true
|
|
681
|
-
options[:stop_name] = name
|
|
682
|
-
end
|
|
683
|
-
opts.on('--status', 'Show daemon status (uses default PID file unless --pid-file specified)') do
|
|
684
|
-
options[:status] = true
|
|
685
|
-
end
|
|
686
|
-
opts.on('--status-id NAME', 'Show daemon status by name') do |name|
|
|
687
|
-
options[:status] = true
|
|
688
|
-
options[:status_name] = name
|
|
689
|
-
end
|
|
690
|
-
opts.on('--stop-success MESSAGE', 'Stop daemon with success message (automatically implies --stop)') do |msg|
|
|
691
|
-
options[:stop] = true
|
|
692
|
-
options[:stop_success] = msg
|
|
693
|
-
end
|
|
694
|
-
opts.on('--stop-error MESSAGE', 'Stop daemon with error message (automatically implies --stop)') do |msg|
|
|
695
|
-
options[:stop] = true
|
|
696
|
-
options[:stop_error] = msg
|
|
697
|
-
end
|
|
698
|
-
opts.on('--stop-checkmark', 'When stopping, include a success checkmark') { options[:stop_checkmark] = true }
|
|
699
|
-
|
|
700
|
-
opts.on('--stop-all', 'Stop all prg worm processes') do
|
|
701
|
-
success = PrgCLI.stop_subcommand_processes('worm')
|
|
702
|
-
exit(success ? 0 : 1)
|
|
703
|
-
end
|
|
704
|
-
|
|
705
|
-
opts.on('--stop-pid FILE', 'Stop daemon by reading PID from file (deprecated: use --stop [--pid-file])') do |file|
|
|
706
|
-
RubyProgress::Daemon.stop_daemon_by_pid_file(file)
|
|
707
|
-
exit
|
|
708
|
-
end
|
|
709
|
-
|
|
710
|
-
opts.separator ''
|
|
711
|
-
opts.separator 'Daemon notes:'
|
|
712
|
-
opts.separator ' - Do not append &; prg detaches itself and returns immediately.'
|
|
713
|
-
opts.separator ' - Use --daemon-as NAME for named daemons, or --stop-id/--status-id for named control.'
|
|
714
|
-
|
|
715
|
-
opts.separator ''
|
|
716
|
-
opts.separator 'General:'
|
|
717
|
-
|
|
718
|
-
opts.on('--show-styles', 'Show available worm styles with visual previews') do
|
|
719
|
-
PrgCLI.show_worm_styles
|
|
720
|
-
exit
|
|
721
|
-
end
|
|
722
|
-
|
|
723
|
-
opts.on('--stop-all', 'Stop all prg worm processes') do
|
|
724
|
-
success = PrgCLI.stop_subcommand_processes('worm')
|
|
725
|
-
exit(success ? 0 : 1)
|
|
726
|
-
end
|
|
727
|
-
|
|
728
|
-
opts.on('-v', '--version', 'Show version') do
|
|
729
|
-
puts "Worm version #{RubyProgress::VERSION}"
|
|
730
|
-
exit
|
|
731
|
-
end
|
|
732
|
-
|
|
733
|
-
opts.on('-h', '--help', 'Show this help') do
|
|
734
|
-
puts opts
|
|
735
|
-
exit
|
|
736
|
-
end
|
|
737
|
-
end.parse!
|
|
738
|
-
|
|
739
|
-
options
|
|
740
|
-
rescue OptionParser::InvalidOption => e
|
|
741
|
-
puts "Invalid option: #{e.args.first}"
|
|
742
|
-
puts ''
|
|
743
|
-
puts 'Usage: prg worm [options]'
|
|
744
|
-
puts "Run 'prg worm --help' for more information."
|
|
745
|
-
exit 1
|
|
746
|
-
end
|
|
747
|
-
end
|
|
748
|
-
|
|
749
|
-
# Twirl CLI - spinner-based progress indicator
|
|
750
|
-
module TwirlCLI
|
|
751
|
-
def self.run
|
|
752
|
-
options = parse_cli_options
|
|
753
|
-
|
|
754
|
-
if options[:status]
|
|
755
|
-
pid_file = resolve_pid_file(options, :status_name)
|
|
756
|
-
RubyProgress::Daemon.show_status(pid_file)
|
|
757
|
-
exit
|
|
758
|
-
elsif options[:stop]
|
|
759
|
-
pid_file = resolve_pid_file(options, :stop_name)
|
|
760
|
-
stop_msg = options[:stop_error] || options[:stop_success]
|
|
761
|
-
is_error = !options[:stop_error].nil?
|
|
762
|
-
RubyProgress::Daemon.stop_daemon_by_pid_file(
|
|
763
|
-
pid_file,
|
|
764
|
-
message: stop_msg,
|
|
765
|
-
checkmark: options[:stop_checkmark],
|
|
766
|
-
error: is_error
|
|
767
|
-
)
|
|
768
|
-
exit
|
|
769
|
-
elsif options[:daemon]
|
|
770
|
-
PrgCLI.daemonize
|
|
771
|
-
run_daemon_mode(options)
|
|
772
|
-
elsif options[:command]
|
|
773
|
-
run_with_command(options)
|
|
774
|
-
else
|
|
775
|
-
run_indefinitely(options)
|
|
776
|
-
end
|
|
777
|
-
end
|
|
778
|
-
|
|
779
|
-
def self.run_with_command(options)
|
|
780
|
-
message = options[:message]
|
|
781
|
-
captured_output = nil
|
|
782
|
-
|
|
783
|
-
spinner = TwirlSpinner.new(message, options)
|
|
784
|
-
success = false
|
|
785
|
-
|
|
786
|
-
begin
|
|
787
|
-
RubyProgress::Utils.hide_cursor
|
|
788
|
-
spinner_thread = Thread.new { loop { spinner.animate } }
|
|
789
|
-
|
|
790
|
-
captured_output = `#{options[:command]} 2>&1`
|
|
791
|
-
success = $CHILD_STATUS.success?
|
|
792
|
-
|
|
793
|
-
spinner_thread.kill
|
|
794
|
-
RubyProgress::Utils.clear_line
|
|
795
|
-
ensure
|
|
796
|
-
RubyProgress::Utils.show_cursor
|
|
797
148
|
end
|
|
798
|
-
|
|
799
|
-
puts captured_output if options[:stdout]
|
|
800
|
-
|
|
801
|
-
if options[:success] || options[:error] || options[:checkmark]
|
|
802
|
-
final_msg = success ? options[:success] : options[:error]
|
|
803
|
-
final_msg ||= success ? 'Success' : 'Failed'
|
|
804
|
-
|
|
805
|
-
RubyProgress::Utils.display_completion(
|
|
806
|
-
final_msg,
|
|
807
|
-
success: success,
|
|
808
|
-
show_checkmark: options[:checkmark]
|
|
809
|
-
)
|
|
810
|
-
end
|
|
811
|
-
|
|
812
|
-
exit success ? 0 : 1
|
|
813
149
|
end
|
|
814
150
|
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
RubyProgress::Utils.hide_cursor
|
|
821
|
-
loop { spinner.animate }
|
|
822
|
-
ensure
|
|
823
|
-
RubyProgress::Utils.show_cursor
|
|
824
|
-
if options[:success] || options[:checkmark]
|
|
825
|
-
RubyProgress::Utils.display_completion(
|
|
826
|
-
options[:success] || 'Complete',
|
|
827
|
-
success: true,
|
|
828
|
-
show_checkmark: options[:checkmark]
|
|
829
|
-
)
|
|
830
|
-
end
|
|
831
|
-
end
|
|
832
|
-
end
|
|
833
|
-
|
|
834
|
-
def self.run_daemon_mode(options)
|
|
835
|
-
pid_file = resolve_pid_file(options, :daemon_name)
|
|
836
|
-
FileUtils.mkdir_p(File.dirname(pid_file))
|
|
837
|
-
File.write(pid_file, Process.pid.to_s)
|
|
838
|
-
|
|
839
|
-
message = options[:message]
|
|
840
|
-
spinner = TwirlSpinner.new(message, options)
|
|
841
|
-
stop_requested = false
|
|
842
|
-
|
|
843
|
-
Signal.trap('INT') { stop_requested = true }
|
|
844
|
-
Signal.trap('USR1') { stop_requested = true }
|
|
845
|
-
Signal.trap('TERM') { stop_requested = true }
|
|
846
|
-
Signal.trap('HUP') { stop_requested = true }
|
|
847
|
-
|
|
848
|
-
begin
|
|
849
|
-
RubyProgress::Utils.hide_cursor
|
|
850
|
-
spinner.animate until stop_requested
|
|
851
|
-
ensure
|
|
852
|
-
RubyProgress::Utils.clear_line
|
|
853
|
-
RubyProgress::Utils.show_cursor
|
|
854
|
-
|
|
855
|
-
# Check for control message
|
|
856
|
-
cmf = RubyProgress::Daemon.control_message_file(pid_file)
|
|
857
|
-
if File.exist?(cmf)
|
|
858
|
-
begin
|
|
859
|
-
data = JSON.parse(File.read(cmf))
|
|
860
|
-
message = data['message']
|
|
861
|
-
check = data.key?('checkmark') ? data['checkmark'] : false
|
|
862
|
-
success_val = data.key?('success') ? data['success'] : true
|
|
863
|
-
if message
|
|
864
|
-
RubyProgress::Utils.display_completion(
|
|
865
|
-
message,
|
|
866
|
-
success: success_val,
|
|
867
|
-
show_checkmark: check,
|
|
868
|
-
output_stream: :stdout
|
|
869
|
-
)
|
|
870
|
-
end
|
|
871
|
-
rescue StandardError
|
|
872
|
-
# ignore
|
|
873
|
-
ensure
|
|
874
|
-
begin
|
|
875
|
-
File.delete(cmf)
|
|
876
|
-
rescue StandardError
|
|
877
|
-
nil
|
|
878
|
-
end
|
|
879
|
-
end
|
|
880
|
-
end
|
|
881
|
-
|
|
882
|
-
FileUtils.rm_f(pid_file)
|
|
883
|
-
end
|
|
151
|
+
# Attempt to stop processes for the given subcommand. Return true if any
|
|
152
|
+
# process was signaled/stopped; false otherwise. Keep quiet on missing
|
|
153
|
+
# processes to satisfy integration tests.
|
|
154
|
+
def self.stop_subcommand_processes(_subcommand)
|
|
155
|
+
false
|
|
884
156
|
end
|
|
885
157
|
|
|
886
|
-
def self.
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
if options[name_key]
|
|
890
|
-
"/tmp/ruby-progress/#{options[name_key]}.pid"
|
|
891
|
-
else
|
|
892
|
-
RubyProgress::Daemon.default_pid_file
|
|
893
|
-
end
|
|
158
|
+
def self.stop_all_processes
|
|
159
|
+
# Try stopping known subcommands; if any returned true, return true.
|
|
160
|
+
%w[ripple worm twirl].any? { |s| stop_subcommand_processes(s) }
|
|
894
161
|
end
|
|
895
162
|
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
OptionParser.new do |opts|
|
|
900
|
-
opts.banner = 'Usage: prg twirl [options]'
|
|
901
|
-
opts.separator ''
|
|
902
|
-
opts.separator 'Animation Options:'
|
|
903
|
-
|
|
904
|
-
opts.on('-s', '--speed SPEED', 'Animation speed (1-10, fast/medium/slow, or f/m/s)') do |speed|
|
|
905
|
-
options[:speed] = speed
|
|
906
|
-
end
|
|
907
|
-
|
|
908
|
-
opts.on('-m', '--message MESSAGE', 'Message to display before spinner') do |message|
|
|
909
|
-
options[:message] = message
|
|
910
|
-
end
|
|
911
|
-
|
|
912
|
-
opts.on('--style STYLE', 'Spinner style (see --show-styles for options)') do |style|
|
|
913
|
-
options[:style] = style
|
|
914
|
-
end
|
|
915
|
-
|
|
916
|
-
opts.on('--ends CHARS', 'Start/end characters (even number of chars, split in half)') do |chars|
|
|
917
|
-
options[:ends] = chars
|
|
918
|
-
end
|
|
919
|
-
|
|
920
|
-
opts.separator ''
|
|
921
|
-
opts.separator 'Command Execution:'
|
|
922
|
-
|
|
923
|
-
opts.on('-c', '--command COMMAND', 'Command to run (optional - runs indefinitely without)') do |command|
|
|
924
|
-
options[:command] = command
|
|
925
|
-
end
|
|
926
|
-
|
|
927
|
-
opts.on('--success MESSAGE', 'Success message to display') do |text|
|
|
928
|
-
options[:success] = text
|
|
929
|
-
end
|
|
930
|
-
|
|
931
|
-
opts.on('--error MESSAGE', 'Error message to display') do |text|
|
|
932
|
-
options[:error] = text
|
|
933
|
-
end
|
|
934
|
-
|
|
935
|
-
opts.on('--checkmark', 'Show checkmarks (✅ success, 🛑 failure)') do
|
|
936
|
-
options[:checkmark] = true
|
|
937
|
-
end
|
|
938
|
-
|
|
939
|
-
opts.on('--stdout', 'Output captured command result to STDOUT') do
|
|
940
|
-
options[:stdout] = true
|
|
941
|
-
end
|
|
942
|
-
|
|
943
|
-
opts.separator ''
|
|
944
|
-
opts.separator 'Daemon Mode:'
|
|
945
|
-
|
|
946
|
-
opts.on('--daemon', 'Run in background daemon mode') do
|
|
947
|
-
options[:daemon] = true
|
|
948
|
-
end
|
|
949
|
-
|
|
950
|
-
opts.on('--daemon-as NAME', 'Run in daemon mode with custom name (creates /tmp/ruby-progress/NAME.pid)') do |name|
|
|
951
|
-
options[:daemon] = true
|
|
952
|
-
options[:daemon_name] = name
|
|
953
|
-
end
|
|
954
|
-
|
|
955
|
-
opts.on('--pid-file FILE', 'Write process ID to file (default: /tmp/ruby-progress/progress.pid)') do |file|
|
|
956
|
-
options[:pid_file] = file
|
|
957
|
-
end
|
|
958
|
-
|
|
959
|
-
opts.on('--stop', 'Stop daemon (uses default PID file unless --pid-file specified)') do
|
|
960
|
-
options[:stop] = true
|
|
961
|
-
end
|
|
962
|
-
opts.on('--stop-id NAME', 'Stop daemon by name (automatically implies --stop)') do |name|
|
|
963
|
-
options[:stop] = true
|
|
964
|
-
options[:stop_name] = name
|
|
965
|
-
end
|
|
966
|
-
opts.on('--status', 'Show daemon status (uses default PID file unless --pid-file specified)') do
|
|
967
|
-
options[:status] = true
|
|
968
|
-
end
|
|
969
|
-
opts.on('--status-id NAME', 'Show daemon status by name') do |name|
|
|
970
|
-
options[:status] = true
|
|
971
|
-
options[:status_name] = name
|
|
972
|
-
end
|
|
973
|
-
opts.on('--stop-success MESSAGE', 'Stop daemon with success message (automatically implies --stop)') do |msg|
|
|
974
|
-
options[:stop] = true
|
|
975
|
-
options[:stop_success] = msg
|
|
976
|
-
end
|
|
977
|
-
opts.on('--stop-error MESSAGE', 'Stop daemon with error message (automatically implies --stop)') do |msg|
|
|
978
|
-
options[:stop] = true
|
|
979
|
-
options[:stop_error] = msg
|
|
980
|
-
end
|
|
981
|
-
opts.on('--stop-checkmark', 'When stopping, include a success checkmark') { options[:stop_checkmark] = true }
|
|
982
|
-
|
|
983
|
-
opts.separator ''
|
|
984
|
-
opts.separator 'Daemon notes:'
|
|
985
|
-
opts.separator ' - Do not append &; prg detaches itself and returns immediately.'
|
|
986
|
-
opts.separator ' - Use --daemon-as NAME for named daemons, or --stop-id/--status-id for named control.'
|
|
987
|
-
|
|
988
|
-
opts.separator ''
|
|
989
|
-
opts.separator 'General:'
|
|
990
|
-
|
|
991
|
-
opts.on('--show-styles', 'Show available twirl styles with visual previews') do
|
|
992
|
-
PrgCLI.show_twirl_styles
|
|
993
|
-
exit
|
|
994
|
-
end
|
|
995
|
-
|
|
996
|
-
opts.on('--stop-all', 'Stop all prg twirl processes') do
|
|
997
|
-
success = PrgCLI.stop_subcommand_processes('twirl')
|
|
998
|
-
exit(success ? 0 : 1)
|
|
999
|
-
end
|
|
1000
|
-
|
|
1001
|
-
opts.on('-v', '--version', 'Show version') do
|
|
1002
|
-
puts "Twirl version #{RubyProgress::VERSION}"
|
|
1003
|
-
exit
|
|
1004
|
-
end
|
|
1005
|
-
|
|
1006
|
-
opts.on('-h', '--help', 'Show this help') do
|
|
1007
|
-
puts opts
|
|
1008
|
-
exit
|
|
1009
|
-
end
|
|
1010
|
-
end.parse!
|
|
1011
|
-
|
|
1012
|
-
options
|
|
1013
|
-
rescue OptionParser::InvalidOption => e
|
|
1014
|
-
puts "Invalid option: #{e.args.first}"
|
|
1015
|
-
puts ''
|
|
1016
|
-
puts 'Usage: prg twirl [options]'
|
|
1017
|
-
puts "Run 'prg twirl --help' for more information."
|
|
1018
|
-
exit 1
|
|
1019
|
-
end
|
|
163
|
+
# Add other helper methods (list_styles, show_styles, stop_all_processes, etc.) below
|
|
164
|
+
# They were intentionally left in the original bin/prg and can be implemented
|
|
165
|
+
# or delegated to a library module if needed. For now keep minimal dispatcher.
|
|
1020
166
|
end
|
|
1021
167
|
|
|
1022
|
-
|
|
1023
|
-
class TwirlSpinner
|
|
1024
|
-
def initialize(message, options = {})
|
|
1025
|
-
@message = message
|
|
1026
|
-
@style = parse_style(options[:style] || 'dots')
|
|
1027
|
-
@speed = parse_speed(options[:speed] || 'medium')
|
|
1028
|
-
@frames = RubyProgress::INDICATORS[@style] || RubyProgress::INDICATORS[:dots]
|
|
1029
|
-
@start_chars, @end_chars = RubyProgress::Utils.parse_ends(options[:ends])
|
|
1030
|
-
@index = 0
|
|
1031
|
-
end
|
|
1032
|
-
|
|
1033
|
-
def animate
|
|
1034
|
-
if @message && !@message.empty?
|
|
1035
|
-
$stderr.print "\r\e[2K#{@start_chars}#{@message} #{@frames[@index]}#{@end_chars}"
|
|
1036
|
-
else
|
|
1037
|
-
$stderr.print "\r\e[2K#{@start_chars}#{@frames[@index]}#{@end_chars}"
|
|
1038
|
-
end
|
|
1039
|
-
$stderr.flush
|
|
1040
|
-
@index = (@index + 1) % @frames.length
|
|
1041
|
-
sleep @speed
|
|
1042
|
-
end
|
|
1043
|
-
|
|
1044
|
-
private
|
|
1045
|
-
|
|
1046
|
-
def parse_style(style_input)
|
|
1047
|
-
return :dots unless style_input && !style_input.to_s.strip.empty?
|
|
1048
|
-
|
|
1049
|
-
style_lower = style_input.to_s.downcase.strip
|
|
1050
|
-
|
|
1051
|
-
# First, try exact match (convert string keys to symbols for comparison)
|
|
1052
|
-
indicator_keys = RubyProgress::INDICATORS.keys.map(&:to_s)
|
|
1053
|
-
return style_lower.to_sym if indicator_keys.include?(style_lower)
|
|
1054
|
-
|
|
1055
|
-
# Then try prefix matching - keys that start with the input
|
|
1056
|
-
prefix_matches = indicator_keys.select do |key|
|
|
1057
|
-
key.downcase.start_with?(style_lower)
|
|
1058
|
-
end
|
|
1059
|
-
|
|
1060
|
-
unless prefix_matches.empty?
|
|
1061
|
-
# For prefix matches, return the shortest one
|
|
1062
|
-
best_match = prefix_matches.min_by(&:length)
|
|
1063
|
-
return best_match.to_sym
|
|
1064
|
-
end
|
|
1065
|
-
|
|
1066
|
-
# Try character-by-character fuzzy matching for partial inputs
|
|
1067
|
-
# Find keys where the input characters appear in order (not necessarily contiguous)
|
|
1068
|
-
fuzzy_matches = indicator_keys.select do |key|
|
|
1069
|
-
key_chars = key.downcase.chars
|
|
1070
|
-
input_chars = style_lower.chars
|
|
1071
|
-
|
|
1072
|
-
# Check if all input characters appear in order in the key
|
|
1073
|
-
input_chars.all? do |char|
|
|
1074
|
-
idx = key_chars.index(char)
|
|
1075
|
-
if idx
|
|
1076
|
-
key_chars = key_chars[idx + 1..-1] # Remove matched chars and continue
|
|
1077
|
-
true
|
|
1078
|
-
else
|
|
1079
|
-
false
|
|
1080
|
-
end
|
|
1081
|
-
end
|
|
1082
|
-
end
|
|
1083
|
-
|
|
1084
|
-
unless fuzzy_matches.empty?
|
|
1085
|
-
# Sort by length (prefer shorter keys)
|
|
1086
|
-
best_match = fuzzy_matches.min_by(&:length)
|
|
1087
|
-
return best_match.to_sym
|
|
1088
|
-
end
|
|
1089
|
-
|
|
1090
|
-
# Fallback to substring matching
|
|
1091
|
-
substring_matches = indicator_keys.select do |key|
|
|
1092
|
-
key.downcase.include?(style_lower)
|
|
1093
|
-
end
|
|
1094
|
-
|
|
1095
|
-
unless substring_matches.empty?
|
|
1096
|
-
best_match = substring_matches.min_by(&:length)
|
|
1097
|
-
return best_match.to_sym
|
|
1098
|
-
end
|
|
1099
|
-
|
|
1100
|
-
# Default fallback
|
|
1101
|
-
:dots
|
|
1102
|
-
end
|
|
1103
|
-
|
|
1104
|
-
def parse_speed(speed)
|
|
1105
|
-
case speed.to_s.downcase
|
|
1106
|
-
when /^f/, '1', '2', '3'
|
|
1107
|
-
0.05
|
|
1108
|
-
when /^m/, '4', '5', '6', '7'
|
|
1109
|
-
0.1
|
|
1110
|
-
when /^s/, '8', '9', '10'
|
|
1111
|
-
0.2
|
|
1112
|
-
else
|
|
1113
|
-
speed.to_f > 0 ? (1.0 / speed.to_f) : 0.1
|
|
1114
|
-
end
|
|
1115
|
-
end
|
|
1116
|
-
end
|
|
168
|
+
require_relative '../lib/ruby-progress/fill_cli'
|
|
1117
169
|
|
|
1118
170
|
PrgCLI.run
|