ruby-progress 1.2.4 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +26 -45
- data/CHANGELOG.md +38 -1
- data/Gemfile.lock +1 -1
- data/README.md +126 -133
- data/Rakefile +0 -3
- data/bin/prg +50 -0
- data/demo_screencast.rb +180 -36
- data/examples/daemon_job_example.sh +25 -0
- data/lib/ruby-progress/cli/fill_options.rb +73 -1
- data/lib/ruby-progress/cli/job_cli.rb +159 -0
- data/lib/ruby-progress/cli/ripple_cli.rb +88 -9
- data/lib/ruby-progress/cli/ripple_options.rb +22 -0
- data/lib/ruby-progress/cli/twirl_options.rb +30 -1
- data/lib/ruby-progress/cli/twirl_runner.rb +62 -5
- data/lib/ruby-progress/cli/twirl_spinner.rb +19 -2
- data/lib/ruby-progress/cli/worm_cli.rb +61 -19
- data/lib/ruby-progress/cli/worm_options.rb +32 -3
- data/lib/ruby-progress/cli/worm_runner.rb +37 -17
- data/lib/ruby-progress/daemon.rb +65 -0
- data/lib/ruby-progress/fill.rb +9 -3
- data/lib/ruby-progress/fill_cli.rb +189 -38
- data/lib/ruby-progress/output_capture.rb +136 -0
- data/lib/ruby-progress/ripple.rb +4 -2
- data/lib/ruby-progress/utils.rb +47 -26
- data/lib/ruby-progress/version.rb +6 -6
- data/lib/ruby-progress/worm.rb +8 -7
- data/screencast +26 -0
- metadata +5 -2
- data/ruby-progress.gemspec +0 -40
data/demo_screencast.rb
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
# and clear visual separation of different features.
|
|
14
14
|
|
|
15
15
|
require 'io/console'
|
|
16
|
+
require 'shellwords'
|
|
17
|
+
require 'tmpdir'
|
|
16
18
|
|
|
17
19
|
# Demo runner that exercises the major features of the ruby-progress gem
|
|
18
20
|
# used by the documentation and screencast recordings.
|
|
@@ -26,6 +28,11 @@ class ProgressDemo
|
|
|
26
28
|
header: "\e[1;36m", # Bright cyan
|
|
27
29
|
command: "\e[1;33m", # Bright yellow
|
|
28
30
|
description: "\e[0;32m", # Green
|
|
31
|
+
exec: "\e[1;33m", # executable (bright yellow)
|
|
32
|
+
tool: "\e[1;35m", # prg/tool (bright magenta)
|
|
33
|
+
flag: "\e[1;36m", # flags (bright cyan)
|
|
34
|
+
value: "\e[1;37m", # flag values (bright white)
|
|
35
|
+
prompt: "\e[2m", # prompt (dim)
|
|
29
36
|
reset: "\e[0m", # Reset
|
|
30
37
|
dim: "\e[2m" # Dim
|
|
31
38
|
}
|
|
@@ -33,12 +40,12 @@ class ProgressDemo
|
|
|
33
40
|
|
|
34
41
|
def run
|
|
35
42
|
clear_screen
|
|
36
|
-
show_title
|
|
43
|
+
# show_title
|
|
37
44
|
|
|
38
45
|
# Introduction
|
|
39
|
-
show_section_header('Ruby Progress Gem Demo')
|
|
40
|
-
show_description('Demonstrating terminal progress indicators with style!')
|
|
41
|
-
pause_for_narration(3)
|
|
46
|
+
# show_section_header('Ruby Progress Gem Demo')
|
|
47
|
+
# show_description('Demonstrating terminal progress indicators with style!')
|
|
48
|
+
# pause_for_narration(3)
|
|
42
49
|
|
|
43
50
|
# Basic examples for each command
|
|
44
51
|
demo_basic_commands
|
|
@@ -63,12 +70,12 @@ class ProgressDemo
|
|
|
63
70
|
|
|
64
71
|
# Ripple - basic expanding circle animation
|
|
65
72
|
show_demo_header('Ripple', 'Expanding circle animation for tasks with unknown duration')
|
|
66
|
-
run_command("#{ruby_cmd} ripple --command 'sleep 4' --success 'Download complete!' --checkmark")
|
|
73
|
+
run_command("#{ruby_cmd} ripple --command 'sleep 4' --success 'Download complete!' --checkmark PROCESSING")
|
|
67
74
|
pause_between_demos
|
|
68
75
|
|
|
69
76
|
# Worm - progress bar animation
|
|
70
77
|
show_demo_header('Worm', 'Animated progress bar for visual feedback')
|
|
71
|
-
run_command("#{ruby_cmd} worm --length 10 --command 'sleep 5' --success 'Processing finished!' --checkmark")
|
|
78
|
+
run_command("#{ruby_cmd} worm --length 10 --command 'sleep 5' --success 'Processing finished!' --checkmark --message 'Loading'")
|
|
72
79
|
pause_between_demos
|
|
73
80
|
|
|
74
81
|
# Twirl - spinning indicator
|
|
@@ -83,31 +90,27 @@ class ProgressDemo
|
|
|
83
90
|
# Ripple styles
|
|
84
91
|
show_demo_header('Ripple Styles', 'Different visual patterns')
|
|
85
92
|
show_command_info('Default ripple style')
|
|
86
|
-
run_command("#{ruby_cmd} ripple --command 'sleep 3' --success 'Default style'")
|
|
93
|
+
run_command("#{ruby_cmd} ripple --command 'sleep 3' --success 'Default style' 'Rippling Progress Default Style'")
|
|
87
94
|
pause_between_demos(2)
|
|
88
95
|
|
|
89
96
|
show_command_info('Pulse style')
|
|
90
|
-
run_command("#{ruby_cmd} ripple --style pulse --command 'sleep 3' --success 'Pulse style'")
|
|
97
|
+
run_command("#{ruby_cmd} ripple --style pulse --command 'sleep 3' --success 'Pulse style' 'Rippling Progress'")
|
|
91
98
|
pause_between_demos
|
|
92
99
|
|
|
93
100
|
# Worm styles
|
|
94
101
|
show_demo_header('Worm Styles', 'Various progress bar animations')
|
|
95
102
|
show_command_info('Classic worm style')
|
|
96
|
-
run_command("#{ruby_cmd} worm --length 10 --style classic --command 'sleep 4' --success 'Classic worm'")
|
|
97
|
-
pause_between_demos(2)
|
|
98
|
-
|
|
99
|
-
show_command_info('Emoji worm style')
|
|
100
|
-
run_command("#{ruby_cmd} worm --length 10 --style emoji --command 'sleep 4' --success 'Emoji worm! 🎉'")
|
|
103
|
+
run_command("#{ruby_cmd} worm --length 10 --style classic --command 'sleep 4' --success 'Classic worm' --message 'Classic'")
|
|
101
104
|
pause_between_demos(2)
|
|
102
105
|
|
|
103
106
|
show_command_info('Blocks worm style')
|
|
104
|
-
run_command("#{ruby_cmd} worm --length 10 --style blocks --command 'sleep 4' --success 'Block worm'")
|
|
107
|
+
run_command("#{ruby_cmd} worm --length 10 --style blocks --command 'sleep 4' --success 'Block worm' --message 'Blocks'")
|
|
105
108
|
pause_between_demos
|
|
106
109
|
|
|
107
110
|
# Twirl styles
|
|
108
111
|
show_demo_header('Twirl Styles', 'Different spinning patterns')
|
|
109
112
|
show_command_info('Classic spinner')
|
|
110
|
-
run_command("#{ruby_cmd} twirl --style classic --command 'sleep 3' --success 'Classic spin'")
|
|
113
|
+
run_command("#{ruby_cmd} twirl --style classic --command 'sleep 3' --success 'Classic spin' --message 'Loading'")
|
|
111
114
|
pause_between_demos(2)
|
|
112
115
|
|
|
113
116
|
show_command_info('Dots spinner')
|
|
@@ -115,7 +118,7 @@ class ProgressDemo
|
|
|
115
118
|
pause_between_demos(2)
|
|
116
119
|
|
|
117
120
|
show_command_info('Arrow spinner')
|
|
118
|
-
run_command("#{ruby_cmd} twirl --style arrow --command 'sleep 3' --success 'Arrow spin'")
|
|
121
|
+
run_command("#{ruby_cmd} twirl --style arrow --command 'sleep 3' --success 'Arrow spin' --message 'Loading'")
|
|
119
122
|
pause_between_demos
|
|
120
123
|
end
|
|
121
124
|
|
|
@@ -125,17 +128,17 @@ class ProgressDemo
|
|
|
125
128
|
# Error handling
|
|
126
129
|
show_demo_header('Error Handling', 'Graceful failure with custom messages')
|
|
127
130
|
show_command_info('Simulating a failed task')
|
|
128
|
-
run_command("#{ruby_cmd} worm --length 10 --command 'sleep 2 && exit 1' --error 'Something went wrong!' --
|
|
131
|
+
run_command("#{ruby_cmd} worm --length 10 --command 'sleep 2 && exit 1' --error 'Something went wrong!' --checkmark")
|
|
129
132
|
pause_between_demos
|
|
130
133
|
|
|
131
134
|
# Custom colors (if supported)
|
|
132
135
|
show_demo_header('Success Messages', 'Custom completion messages')
|
|
133
136
|
show_command_info('Custom success message with checkmark')
|
|
134
|
-
run_command("#{ruby_cmd} ripple --command 'sleep 3' --success 'Data synchronized successfully' --checkmark")
|
|
137
|
+
run_command("#{ruby_cmd} ripple --command 'sleep 3' --success 'Data synchronized successfully' --checkmark --message 'Syncing data...'")
|
|
135
138
|
pause_between_demos(2)
|
|
136
139
|
|
|
137
140
|
show_command_info('Different success icon')
|
|
138
|
-
run_command("#{ruby_cmd} twirl --command 'sleep 3' --success 'Backup completed' --icon '✓'")
|
|
141
|
+
run_command("#{ruby_cmd} twirl --command 'sleep 3' --success 'Backup completed' --success-icon '✓' --checkmark")
|
|
139
142
|
pause_between_demos
|
|
140
143
|
|
|
141
144
|
# No completion message
|
|
@@ -151,7 +154,7 @@ class ProgressDemo
|
|
|
151
154
|
# Universal --ends flag
|
|
152
155
|
show_demo_header('Universal --ends Flag', 'Add decorative start/end characters')
|
|
153
156
|
show_command_info("Ripple with square brackets: --ends '[]'")
|
|
154
|
-
run_command("#{ruby_cmd} ripple --ends '[]' --command 'sleep 4' --success 'Framed ripple!'")
|
|
157
|
+
run_command("#{ruby_cmd} ripple --ends '[]' --command 'sleep 4' --success 'Framed ripple!' 'With a frame'")
|
|
155
158
|
pause_between_demos(2)
|
|
156
159
|
|
|
157
160
|
show_command_info("Worm with angle brackets: --ends '<<>>'")
|
|
@@ -194,11 +197,95 @@ class ProgressDemo
|
|
|
194
197
|
part2 = "--ends '【】' --command 'sleep 5' --success 'Ultimate combo!' --checkmark"
|
|
195
198
|
run_command(part1 + part2)
|
|
196
199
|
pause_between_demos
|
|
200
|
+
|
|
201
|
+
# v1.3.x additions: output capture and job queue demo snippets
|
|
202
|
+
show_section_header('New in v1.3.x - Output Capture & Job Queue')
|
|
203
|
+
|
|
204
|
+
show_demo_header('Fill --command (output capture)', 'Run a command and reserve terminal rows for output while preserving animation')
|
|
205
|
+
show_command_info('Capture command stdout/stderr into reserved rows:')
|
|
206
|
+
part_a = "#{ruby_cmd} fill --command \"bash -lc 'for i in 1 2 3; do echo line:$i; sleep 1; done'\" "
|
|
207
|
+
part_b = "--output-lines 3 --output-position top --success 'Captured!' --checkmark"
|
|
208
|
+
run_command(part_a + part_b)
|
|
209
|
+
pause_between_demos
|
|
210
|
+
|
|
211
|
+
show_demo_header('prg job send (enqueue a job)', 'Send a job to a running daemon using the file-based job queue')
|
|
212
|
+
show_command_info('Example: create a job payload and atomically enqueue it for the daemon')
|
|
213
|
+
show_command_info('Use the bundled helper to enqueue control/action jobs:')
|
|
214
|
+
puts "#{@colors[:command]}$ prg job send --daemon-name demo --advance#{@colors[:reset]}"
|
|
215
|
+
puts "#{@colors[:command]}$ prg job send --daemon-name demo --percent 42#{@colors[:reset]}"
|
|
216
|
+
puts
|
|
217
|
+
show_command_info('Or enqueue a shell command:')
|
|
218
|
+
|
|
219
|
+
# For the demo we run the worker in the foreground so you can see the
|
|
220
|
+
# live animation and completion message inline. Daemon mode (started with
|
|
221
|
+
# `--daemon` or `--daemon-as`) detaches to the background and processes
|
|
222
|
+
# jobs via the file-based queue; `prg job send` targets a background
|
|
223
|
+
# daemon and prints the job result JSON, but won't show the daemon's
|
|
224
|
+
# animation in the foreground.
|
|
225
|
+
show_command_info('Start a named fill daemon that processes percent/action jobs')
|
|
226
|
+
# Start the fill daemon in non-detaching background mode so animation remains visible
|
|
227
|
+
run_command("#{ruby_cmd} fill --daemon-as demo --no-detach --output-lines 3 --output-position top --success 'Captured!' --checkmark")
|
|
228
|
+
pause_between_demos(1)
|
|
229
|
+
|
|
230
|
+
show_command_info('We will enqueue several percent actions via a small shell script')
|
|
231
|
+
demo_script = <<~BASH
|
|
232
|
+
#!/usr/bin/env bash
|
|
233
|
+
set -eu
|
|
234
|
+
echo "Sending percent updates to demo daemon (atomic mktemp+mv writes)"
|
|
235
|
+
|
|
236
|
+
job_dir="/tmp/ruby-progress/demo.jobs"
|
|
237
|
+
mkdir -p "$job_dir"
|
|
238
|
+
|
|
239
|
+
enqueue_percent() {
|
|
240
|
+
percent=$1
|
|
241
|
+
# Build JSON payload
|
|
242
|
+
id=$(uuidgen 2>/dev/null || echo "job-$(date +%s%N)")
|
|
243
|
+
tmp=$(mktemp "$job_dir/${id}.json.tmp.XXXXXX")
|
|
244
|
+
printf '%s\n' '{"id":"'"${id}"'","action":"percent","value":'"${percent}"'}' > "$tmp"
|
|
245
|
+
mv "$tmp" "$job_dir/${id}.json"
|
|
246
|
+
|
|
247
|
+
# Wait for the daemon to process the job (poll for .processing.result)
|
|
248
|
+
result_path="$job_dir/${id}.json.processing.result"
|
|
249
|
+
start=$(date +%s)
|
|
250
|
+
timeout=10
|
|
251
|
+
while [ ! -f "$result_path" ]; do
|
|
252
|
+
sleep 0.1
|
|
253
|
+
now=$(date +%s)
|
|
254
|
+
if [ $((now - start)) -gt $timeout ]; then
|
|
255
|
+
echo "Timed out waiting for result for job ${id}" >&2
|
|
256
|
+
return 2
|
|
257
|
+
fi
|
|
258
|
+
done
|
|
259
|
+
cat "$result_path"
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
enqueue_percent 10
|
|
263
|
+
sleep 1
|
|
264
|
+
enqueue_percent 40
|
|
265
|
+
sleep 1
|
|
266
|
+
enqueue_percent 70
|
|
267
|
+
sleep 1
|
|
268
|
+
enqueue_percent 100
|
|
269
|
+
|
|
270
|
+
# After updates, stop the daemon cleanly (call local bin/prg to avoid global conflicts)
|
|
271
|
+
#{ruby_cmd} fill --stop-id demo --stop-success 'Demo daemon stopped'
|
|
272
|
+
BASH
|
|
273
|
+
|
|
274
|
+
# Show the simulated script contents
|
|
275
|
+
puts "#{@colors[:command]}$ cat demo_percent_updates.sh#{@colors[:reset]}"
|
|
276
|
+
puts demo_script
|
|
277
|
+
|
|
278
|
+
# Write and execute the script (run in a subshell so output doesn't interleave too badly)
|
|
279
|
+
script_path = File.join(Dir.tmpdir, "demo_percent_updates_#{Time.now.to_i}.sh")
|
|
280
|
+
File.write(script_path, demo_script)
|
|
281
|
+
File.chmod(0o755, script_path)
|
|
282
|
+
run_command("bash #{Shellwords.escape(script_path)}")
|
|
283
|
+
pause_between_demos
|
|
197
284
|
end
|
|
198
285
|
|
|
199
286
|
def show_finale
|
|
200
287
|
show_section_header('Demo Complete!')
|
|
201
|
-
show_description('Ruby Progress Gem v1.2
|
|
288
|
+
show_description('Ruby Progress Gem v1.3.2 - Making terminal progress beautiful! 🚀')
|
|
202
289
|
puts
|
|
203
290
|
show_description('Key features demonstrated:')
|
|
204
291
|
puts "#{@colors[:description]} • Three animation types: ripple, worm, twirl#{@colors[:reset]}"
|
|
@@ -217,7 +304,8 @@ class ProgressDemo
|
|
|
217
304
|
# Utility methods
|
|
218
305
|
|
|
219
306
|
def ruby_cmd
|
|
220
|
-
"ruby -I #{@lib_path} #{@gem_path}"
|
|
307
|
+
# "ruby -I #{@lib_path} #{@gem_path}"
|
|
308
|
+
'bin/prg'
|
|
221
309
|
end
|
|
222
310
|
|
|
223
311
|
def clear_screen
|
|
@@ -228,7 +316,7 @@ class ProgressDemo
|
|
|
228
316
|
puts
|
|
229
317
|
puts "#{@colors[:header]}#{'=' * 60}#{@colors[:reset]}"
|
|
230
318
|
puts "#{@colors[:header]} RUBY PROGRESS GEM - DEMO SCREENCAST#{@colors[:reset]}"
|
|
231
|
-
puts "#{@colors[:header]} Version 1.2
|
|
319
|
+
puts "#{@colors[:header]} Version 1.3.2 Feature Demonstration#{@colors[:reset]}"
|
|
232
320
|
puts "#{@colors[:header]}#{'=' * 60}#{@colors[:reset]}"
|
|
233
321
|
puts
|
|
234
322
|
end
|
|
@@ -257,19 +345,75 @@ class ProgressDemo
|
|
|
257
345
|
end
|
|
258
346
|
|
|
259
347
|
def run_command(cmd)
|
|
260
|
-
|
|
348
|
+
# Type the command with syntax highlighting
|
|
349
|
+
type_command("$ #{cmd.sub(%r{^bin/}, '')}")
|
|
261
350
|
puts
|
|
262
351
|
system(cmd)
|
|
263
352
|
puts
|
|
264
353
|
end
|
|
265
354
|
|
|
355
|
+
# Type out a shell command with simple token-based syntax highlighting.
|
|
356
|
+
# - executable (first token) uses @colors[:exec]
|
|
357
|
+
# - flags (tokens starting with '-') use @colors[:flag]
|
|
358
|
+
# - values use @colors[:value]
|
|
359
|
+
# The function prints one character at a time to simulate typing.
|
|
360
|
+
def type_command(line, speed: 0.04)
|
|
361
|
+
tokens = line.scan(/'[^']*'|"[^"]*"|\S+/)
|
|
362
|
+
pos = 0
|
|
363
|
+
|
|
364
|
+
# Determine the executable token position (skip leading prompt '$' tokens)
|
|
365
|
+
exec_pos = tokens.find_index { |t| !t.start_with?('$') }
|
|
366
|
+
subcmd_pos = exec_pos ? exec_pos + 1 : nil
|
|
367
|
+
|
|
368
|
+
tokens.each_with_index do |token, idx|
|
|
369
|
+
# Find token in original line starting at pos (to preserve spacing)
|
|
370
|
+
start_idx = line.index(token, pos) || pos
|
|
371
|
+
|
|
372
|
+
# print any intermediate whitespace
|
|
373
|
+
inter = line[pos...start_idx]
|
|
374
|
+
$stdout.print(inter) if inter && !inter.empty?
|
|
375
|
+
|
|
376
|
+
# Default color for tokens is value
|
|
377
|
+
color = @colors[:value]
|
|
378
|
+
|
|
379
|
+
if token.start_with?('$')
|
|
380
|
+
color = @colors[:prompt]
|
|
381
|
+
elsif idx == exec_pos && token == 'prg'
|
|
382
|
+
# Highlight the bundled tool name in magenta
|
|
383
|
+
color = @colors[:tool]
|
|
384
|
+
elsif idx == exec_pos
|
|
385
|
+
# Executable token (e.g. ruby or other) - keep exec color
|
|
386
|
+
color = @colors[:exec]
|
|
387
|
+
elsif idx == subcmd_pos
|
|
388
|
+
# Subcommand (ripple/twirl/worm/fill etc.) highlighted in yellow
|
|
389
|
+
color = @colors[:command]
|
|
390
|
+
elsif token.start_with?('-')
|
|
391
|
+
color = @colors[:flag]
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# Print the token one char at a time
|
|
395
|
+
token.each_char do |ch|
|
|
396
|
+
$stdout.print("#{color}#{ch}#{@colors[:reset]}")
|
|
397
|
+
$stdout.flush
|
|
398
|
+
sleep(speed)
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
pos = start_idx + token.length
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
# Print any trailing whitespace/newline
|
|
405
|
+
trailing = line[pos..-1]
|
|
406
|
+
$stdout.print(trailing) if trailing
|
|
407
|
+
$stdout.flush
|
|
408
|
+
end
|
|
409
|
+
|
|
266
410
|
def pause_for_narration(seconds = 2)
|
|
267
|
-
puts "#{@colors[:dim]}[Pausing #{seconds}s for narration...]#{@colors[:reset]}"
|
|
268
|
-
sleep(seconds)
|
|
411
|
+
# puts "#{@colors[:dim]}[Pausing #{seconds}s for narration...]#{@colors[:reset]}"
|
|
412
|
+
# sleep(seconds)
|
|
269
413
|
end
|
|
270
414
|
|
|
271
|
-
def pause_between_demos(seconds =
|
|
272
|
-
puts "#{@colors[:dim]}[Pausing #{seconds}s between demos...]#{@colors[:reset]}"
|
|
415
|
+
def pause_between_demos(seconds = 2)
|
|
416
|
+
# puts "#{@colors[:dim]}[Pausing #{seconds}s between demos...]#{@colors[:reset]}"
|
|
273
417
|
sleep(seconds)
|
|
274
418
|
end
|
|
275
419
|
end
|
|
@@ -278,14 +422,14 @@ end
|
|
|
278
422
|
if __FILE__ == $PROGRAM_NAME
|
|
279
423
|
demo = ProgressDemo.new
|
|
280
424
|
|
|
281
|
-
puts 'Ruby Progress Gem Demo Screencast'
|
|
282
|
-
puts '================================='
|
|
283
|
-
puts
|
|
284
|
-
puts 'This script will demonstrate various features of the ruby-progress gem.'
|
|
285
|
-
puts 'Press ENTER to start the demo, or Ctrl+C to exit.'
|
|
286
|
-
puts
|
|
287
|
-
print 'Ready? '
|
|
288
|
-
gets
|
|
425
|
+
# puts 'Ruby Progress Gem Demo Screencast'
|
|
426
|
+
# puts '================================='
|
|
427
|
+
# puts
|
|
428
|
+
# puts 'This script will demonstrate various features of the ruby-progress gem.'
|
|
429
|
+
# puts 'Press ENTER to start the demo, or Ctrl+C to exit.'
|
|
430
|
+
# puts
|
|
431
|
+
# print 'Ready? '
|
|
432
|
+
# gets
|
|
289
433
|
|
|
290
434
|
begin
|
|
291
435
|
demo.run
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
# Example: start a worm daemon, send a job, wait for result, then stop.
|
|
3
|
+
# This script assumes you're running from the project root and have a working
|
|
4
|
+
# `bin/prg` script in the repository.
|
|
5
|
+
|
|
6
|
+
set -eu
|
|
7
|
+
|
|
8
|
+
PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
9
|
+
PRG_BIN="$PROJECT_ROOT/bin/prg"
|
|
10
|
+
|
|
11
|
+
echo "Starting worm daemon (named 'example')..."
|
|
12
|
+
# prg detaches in daemon mode so no & needed
|
|
13
|
+
$PRG_BIN worm --daemon-as example --message "Example daemon"
|
|
14
|
+
|
|
15
|
+
sleep 0.2
|
|
16
|
+
|
|
17
|
+
echo "Sending job and waiting for result..."
|
|
18
|
+
$PRG_BIN job send --daemon-name example --command "echo hello; sleep 0.1" --wait --timeout 10
|
|
19
|
+
|
|
20
|
+
sleep 0.1
|
|
21
|
+
|
|
22
|
+
echo "Stopping daemon with success message..."
|
|
23
|
+
$PRG_BIN worm --stop-success "Example finished" --stop-checkmark --daemon-name example
|
|
24
|
+
|
|
25
|
+
echo "Done."
|
|
@@ -23,9 +23,12 @@ module RubyProgress
|
|
|
23
23
|
stop: false,
|
|
24
24
|
status: false,
|
|
25
25
|
current: false,
|
|
26
|
+
output_position: :above,
|
|
27
|
+
output_lines: 3,
|
|
26
28
|
report: false
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
# rubocop:disable Metrics/BlockLength
|
|
29
32
|
begin
|
|
30
33
|
OptionParser.new do |opts|
|
|
31
34
|
opts.banner = 'Usage: prg fill [options]'
|
|
@@ -44,6 +47,20 @@ module RubyProgress
|
|
|
44
47
|
options[:ends] = chars
|
|
45
48
|
end
|
|
46
49
|
|
|
50
|
+
opts.separator 'Output capture:'
|
|
51
|
+
|
|
52
|
+
opts.on('-c', '--command COMMAND', 'Command to run and capture output (optional)') do |cmd|
|
|
53
|
+
options[:command] = cmd
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
opts.on('--output-position POSITION', 'Position to render captured output: above or below (default: above)') do |pos|
|
|
57
|
+
options[:output_position] = pos.to_sym
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
opts.on('--output-lines N', Integer, 'Number of output lines to reserve for captured output (default: 3)') do |n|
|
|
61
|
+
options[:output_lines] = n
|
|
62
|
+
end
|
|
63
|
+
|
|
47
64
|
opts.separator ''
|
|
48
65
|
opts.separator 'Progress Control:'
|
|
49
66
|
|
|
@@ -90,6 +107,14 @@ module RubyProgress
|
|
|
90
107
|
options[:success_message] = msg
|
|
91
108
|
end
|
|
92
109
|
|
|
110
|
+
opts.on('--success-icon ICON', 'Custom success icon to show with completion messages') do |ic|
|
|
111
|
+
options[:success_icon] = ic
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
opts.on('--error-icon ICON', 'Custom error icon to show with failure messages') do |ic|
|
|
115
|
+
options[:error_icon] = ic
|
|
116
|
+
end
|
|
117
|
+
|
|
93
118
|
opts.on('--error MESSAGE', 'Error message to display on cancellation') do |msg|
|
|
94
119
|
options[:error_message] = msg
|
|
95
120
|
end
|
|
@@ -105,6 +130,21 @@ module RubyProgress
|
|
|
105
130
|
options[:daemon] = true
|
|
106
131
|
end
|
|
107
132
|
|
|
133
|
+
opts.on('--daemon-as NAME', 'Run in daemon mode with custom name (creates /tmp/ruby-progress/NAME.pid)') do |name|
|
|
134
|
+
options[:daemon] = true
|
|
135
|
+
options[:daemon_name] = name
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Accept --daemon-name as alias for --daemon-as
|
|
139
|
+
opts.on('--daemon-name NAME', 'Alias for --daemon-as (compat)') do |name|
|
|
140
|
+
options[:daemon] = true
|
|
141
|
+
options[:daemon_name] = name
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
opts.on('--no-detach', 'When used with --daemon/--daemon-as: run background child but do not fully detach from the terminal') do
|
|
145
|
+
options[:no_detach] = true
|
|
146
|
+
end
|
|
147
|
+
|
|
108
148
|
opts.on('--pid-file FILE', 'PID file location (default: /tmp/ruby-progress/fill.pid)') do |file|
|
|
109
149
|
options[:pid_file] = file
|
|
110
150
|
end
|
|
@@ -113,10 +153,36 @@ module RubyProgress
|
|
|
113
153
|
options[:stop] = true
|
|
114
154
|
end
|
|
115
155
|
|
|
156
|
+
opts.on('--stop-id NAME', 'Stop daemon by name (implies --stop)') do |name|
|
|
157
|
+
# Backwards-compatible shorthand used in demos: set stop flag
|
|
158
|
+
options[:stop] = true
|
|
159
|
+
# Normalize to canonical keys used by FillCLI (daemon_name/status_name)
|
|
160
|
+
options[:stop_name] = name
|
|
161
|
+
options[:daemon_name] = name
|
|
162
|
+
options[:status_name] = name
|
|
163
|
+
end
|
|
164
|
+
|
|
116
165
|
opts.on('--status', 'Show daemon status') do
|
|
117
166
|
options[:status] = true
|
|
118
167
|
end
|
|
119
168
|
|
|
169
|
+
opts.on('--status-id NAME', 'Show daemon status by name') do |name|
|
|
170
|
+
options[:status] = true
|
|
171
|
+
# Normalize to canonical key
|
|
172
|
+
options[:status_name] = name
|
|
173
|
+
options[:daemon_name] = name
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
opts.on('--stop-success MESSAGE', 'Stop daemon with success message (implies --stop)') do |msg|
|
|
177
|
+
options[:stop] = true
|
|
178
|
+
options[:stop_success] = msg
|
|
179
|
+
end
|
|
180
|
+
opts.on('--stop-error MESSAGE', 'Stop daemon with error message (implies --stop)') do |msg|
|
|
181
|
+
options[:stop] = true
|
|
182
|
+
options[:stop_error] = msg
|
|
183
|
+
end
|
|
184
|
+
opts.on('--stop-checkmark', 'When stopping, include a success checkmark') { options[:stop_checkmark] = true }
|
|
185
|
+
|
|
120
186
|
opts.separator ''
|
|
121
187
|
opts.separator 'General:'
|
|
122
188
|
|
|
@@ -132,6 +198,7 @@ module RubyProgress
|
|
|
132
198
|
options[:help] = true
|
|
133
199
|
end
|
|
134
200
|
end.parse!
|
|
201
|
+
# rubocop:enable Metrics/BlockLength
|
|
135
202
|
rescue OptionParser::InvalidOption => e
|
|
136
203
|
warn "Invalid option: #{e.args.first}"
|
|
137
204
|
warn ''
|
|
@@ -139,7 +206,6 @@ module RubyProgress
|
|
|
139
206
|
warn "Run 'prg fill --help' for more information."
|
|
140
207
|
exit 1
|
|
141
208
|
end
|
|
142
|
-
|
|
143
209
|
options
|
|
144
210
|
end
|
|
145
211
|
|
|
@@ -173,6 +239,12 @@ module RubyProgress
|
|
|
173
239
|
opts.on('--error MESSAGE', 'Error message to display on cancellation')
|
|
174
240
|
opts.on('--checkmark', 'Show checkmarks (✅ success, 🛑 failure)')
|
|
175
241
|
|
|
242
|
+
opts.separator ''
|
|
243
|
+
opts.separator ''
|
|
244
|
+
opts.separator 'Output capture:'
|
|
245
|
+
opts.on('--output-position POSITION', 'Position to render captured output: above or below (default: above)')
|
|
246
|
+
opts.on('--output-lines N', Integer, 'Number of output lines to reserve for captured output (default: 3)')
|
|
247
|
+
|
|
176
248
|
opts.separator ''
|
|
177
249
|
opts.separator 'Daemon Mode:'
|
|
178
250
|
opts.on('--daemon', 'Run in background daemon mode')
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# CLI: prg job
|
|
4
|
+
|
|
5
|
+
# Provides the `prg job send` helper to enqueue commands into a daemon's
|
|
6
|
+
# job directory. This file contains a minimal implementation used by tests.
|
|
7
|
+
|
|
8
|
+
require 'optparse'
|
|
9
|
+
require 'json'
|
|
10
|
+
require 'securerandom'
|
|
11
|
+
require 'fileutils'
|
|
12
|
+
require_relative '../daemon'
|
|
13
|
+
|
|
14
|
+
# Job CLI helpers
|
|
15
|
+
#
|
|
16
|
+
# Exposed as `prg job send`.
|
|
17
|
+
module JobCLI
|
|
18
|
+
# JobCLI
|
|
19
|
+
#
|
|
20
|
+
# Small CLI module that exposes `prg job send` for enqueuing jobs into the
|
|
21
|
+
# daemon job directory. This is intentionally minimal: it writes a single
|
|
22
|
+
# JSON file atomically and optionally waits for a result file created by
|
|
23
|
+
# the daemon's job processor.
|
|
24
|
+
# Simple CLI for submitting jobs to a running daemon job directory.
|
|
25
|
+
# Usage: prg job send --pid-file /tmp/... --command "echo hi" [--wait]
|
|
26
|
+
class Options
|
|
27
|
+
def self.parse(argv)
|
|
28
|
+
options = { wait: false }
|
|
29
|
+
opt = OptionParser.new do |o|
|
|
30
|
+
o.banner = 'Usage: prg job send [options]'
|
|
31
|
+
o.on('--pid-file PATH', 'Path to daemon pid file') do |v|
|
|
32
|
+
options[:pid_file] = v
|
|
33
|
+
end
|
|
34
|
+
o.on('--daemon-name NAME', 'Daemon name (maps to /tmp/ruby-progress/NAME.pid)') do |v|
|
|
35
|
+
options[:daemon_name] = v
|
|
36
|
+
end
|
|
37
|
+
o.on('--command CMD', 'Command to run') do |v|
|
|
38
|
+
options[:command] = v
|
|
39
|
+
end
|
|
40
|
+
o.on('--stdin', 'Read command from stdin (overrides --command)') do
|
|
41
|
+
options[:stdin] = true
|
|
42
|
+
end
|
|
43
|
+
o.on('--advance', 'Send an advance action (no value)') do
|
|
44
|
+
options[:action] = 'advance'
|
|
45
|
+
end
|
|
46
|
+
o.on('--percent N', Integer, 'Send a percent action with value N') do |v|
|
|
47
|
+
options[:action] = 'percent'
|
|
48
|
+
options[:value] = v
|
|
49
|
+
end
|
|
50
|
+
o.on('--complete', 'Send a complete action (no value)') do
|
|
51
|
+
options[:action] = 'complete'
|
|
52
|
+
end
|
|
53
|
+
o.on('--cancel', 'Send a cancel action (no value)') do
|
|
54
|
+
options[:action] = 'cancel'
|
|
55
|
+
end
|
|
56
|
+
o.on('--action ACTION', 'Send a custom action name') do |v|
|
|
57
|
+
options[:action] = v
|
|
58
|
+
end
|
|
59
|
+
o.on('--value VAL', 'Value for the action (string or number)') do |v|
|
|
60
|
+
options[:value] = v
|
|
61
|
+
end
|
|
62
|
+
o.on('--wait', 'Wait for result file and print it') do
|
|
63
|
+
options[:wait] = true
|
|
64
|
+
end
|
|
65
|
+
o.on('--timeout SECONDS', Integer, 'Timeout seconds for wait') do |v|
|
|
66
|
+
options[:timeout] = v
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
rest = opt.parse(argv)
|
|
71
|
+
options[:command] ||= rest.join(' ') unless rest.empty?
|
|
72
|
+
options
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def self.send(argv = ARGV)
|
|
77
|
+
opts = Options.parse(argv)
|
|
78
|
+
|
|
79
|
+
# Resolve pid file
|
|
80
|
+
pid_file = if opts[:pid_file]
|
|
81
|
+
opts[:pid_file]
|
|
82
|
+
elsif opts[:daemon_name]
|
|
83
|
+
"/tmp/ruby-progress/#{opts[:daemon_name]}.pid"
|
|
84
|
+
else
|
|
85
|
+
RubyProgress::Daemon.default_pid_file
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
job_dir = RubyProgress::Daemon.job_dir_for_pid(pid_file)
|
|
89
|
+
FileUtils.mkdir_p(job_dir)
|
|
90
|
+
|
|
91
|
+
cmd = if opts[:stdin]
|
|
92
|
+
$stdin.read
|
|
93
|
+
else
|
|
94
|
+
opts[:command]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
is_action = !opts[:action].nil? && opts[:action] != false
|
|
98
|
+
|
|
99
|
+
if is_action
|
|
100
|
+
if cmd && !cmd.strip.empty?
|
|
101
|
+
warn 'Cannot specify both --command/--stdin and an action flag'
|
|
102
|
+
exit 1
|
|
103
|
+
end
|
|
104
|
+
else
|
|
105
|
+
unless cmd && !cmd.strip.empty?
|
|
106
|
+
warn 'No command specified. Use --command, --stdin, or pass an action flag.'
|
|
107
|
+
exit 1
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
job_id = SecureRandom.uuid
|
|
112
|
+
tmp = File.join(job_dir, "#{job_id}.json.tmp")
|
|
113
|
+
final = File.join(job_dir, "#{job_id}.json")
|
|
114
|
+
|
|
115
|
+
payload = build_payload(opts, job_id, cmd)
|
|
116
|
+
|
|
117
|
+
File.write(tmp, JSON.dump(payload))
|
|
118
|
+
FileUtils.mv(tmp, final)
|
|
119
|
+
|
|
120
|
+
if opts[:wait]
|
|
121
|
+
timeout = opts[:timeout] || 10
|
|
122
|
+
start = Time.now
|
|
123
|
+
result_path = "#{final}.processing.result"
|
|
124
|
+
loop do
|
|
125
|
+
if File.exist?(result_path)
|
|
126
|
+
puts File.read(result_path)
|
|
127
|
+
break
|
|
128
|
+
end
|
|
129
|
+
if Time.now - start > timeout
|
|
130
|
+
warn 'Timed out waiting for result'
|
|
131
|
+
exit 2
|
|
132
|
+
end
|
|
133
|
+
sleep 0.1
|
|
134
|
+
end
|
|
135
|
+
else
|
|
136
|
+
puts job_id
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Build the JSON payload for a job based on parsed options.
|
|
141
|
+
def self.build_payload(opts, job_id, cmd)
|
|
142
|
+
payload = { 'id' => job_id }
|
|
143
|
+
|
|
144
|
+
is_action = !opts[:action].nil? && opts[:action] != false
|
|
145
|
+
|
|
146
|
+
if is_action
|
|
147
|
+
payload['action'] = opts[:action]
|
|
148
|
+
if opts.key?(:value)
|
|
149
|
+
val = opts[:value]
|
|
150
|
+
payload['value'] = val.to_i if val.is_a?(String) && val =~ /^\d+$/
|
|
151
|
+
payload['value'] ||= val
|
|
152
|
+
end
|
|
153
|
+
else
|
|
154
|
+
payload['command'] = cmd
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
payload
|
|
158
|
+
end
|
|
159
|
+
end
|