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.
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!' --fail-icon")
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.0 - Making terminal progress beautiful! 🚀')
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.0 Feature Demonstration#{@colors[:reset]}"
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
- puts "#{@colors[:command]}$ #{cmd}#{@colors[:reset]}"
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 = 3)
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