ruby-progress 1.3.1 → 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.
@@ -28,6 +28,7 @@ module RubyProgress
28
28
  report: false
29
29
  }
30
30
 
31
+ # rubocop:disable Metrics/BlockLength
31
32
  begin
32
33
  OptionParser.new do |opts|
33
34
  opts.banner = 'Usage: prg fill [options]'
@@ -106,6 +107,14 @@ module RubyProgress
106
107
  options[:success_message] = msg
107
108
  end
108
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
+
109
118
  opts.on('--error MESSAGE', 'Error message to display on cancellation') do |msg|
110
119
  options[:error_message] = msg
111
120
  end
@@ -121,6 +130,21 @@ module RubyProgress
121
130
  options[:daemon] = true
122
131
  end
123
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
+
124
148
  opts.on('--pid-file FILE', 'PID file location (default: /tmp/ruby-progress/fill.pid)') do |file|
125
149
  options[:pid_file] = file
126
150
  end
@@ -129,10 +153,36 @@ module RubyProgress
129
153
  options[:stop] = true
130
154
  end
131
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
+
132
165
  opts.on('--status', 'Show daemon status') do
133
166
  options[:status] = true
134
167
  end
135
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
+
136
186
  opts.separator ''
137
187
  opts.separator 'General:'
138
188
 
@@ -148,6 +198,7 @@ module RubyProgress
148
198
  options[:help] = true
149
199
  end
150
200
  end.parse!
201
+ # rubocop:enable Metrics/BlockLength
151
202
  rescue OptionParser::InvalidOption => e
152
203
  warn "Invalid option: #{e.args.first}"
153
204
  warn ''
@@ -155,7 +206,6 @@ module RubyProgress
155
206
  warn "Run 'prg fill --help' for more information."
156
207
  exit 1
157
208
  end
158
-
159
209
  options
160
210
  end
161
211
 
@@ -28,12 +28,43 @@ module JobCLI
28
28
  options = { wait: false }
29
29
  opt = OptionParser.new do |o|
30
30
  o.banner = 'Usage: prg job send [options]'
31
- o.on('--pid-file PATH', 'Path to daemon pid file') { |v| options[:pid_file] = v }
32
- o.on('--daemon-name NAME', 'Daemon name (maps to /tmp/ruby-progress/NAME.pid)') { |v| options[:daemon_name] = v }
33
- o.on('--command CMD', 'Command to run') { |v| options[:command] = v }
34
- o.on('--stdin', 'Read command from stdin (overrides --command)') { options[:stdin] = true }
35
- o.on('--wait', 'Wait for result file and print it') { options[:wait] = true }
36
- o.on('--timeout SECONDS', Integer, 'Timeout seconds for wait') { |v| options[:timeout] = v }
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
37
68
  end
38
69
 
39
70
  rest = opt.parse(argv)
@@ -63,16 +94,25 @@ module JobCLI
63
94
  opts[:command]
64
95
  end
65
96
 
66
- unless cmd && !cmd.strip.empty?
67
- warn 'No command specified. Use --command or --stdin.'
68
- exit 1
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
69
109
  end
70
110
 
71
111
  job_id = SecureRandom.uuid
72
112
  tmp = File.join(job_dir, "#{job_id}.json.tmp")
73
113
  final = File.join(job_dir, "#{job_id}.json")
74
114
 
75
- payload = { 'id' => job_id, 'command' => cmd }
115
+ payload = build_payload(opts, job_id, cmd)
76
116
 
77
117
  File.write(tmp, JSON.dump(payload))
78
118
  FileUtils.mv(tmp, final)
@@ -96,4 +136,24 @@ module JobCLI
96
136
  puts job_id
97
137
  end
98
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
99
159
  end
@@ -33,8 +33,13 @@ module RippleCLI
33
33
  )
34
34
  exit
35
35
  elsif options[:daemon]
36
- # For daemon mode, detach so shell has no tracked job
37
- PrgCLI.daemonize
36
+ # For daemon mode, detach so shell has no tracked job unless the user
37
+ # requested a non-detaching background child via --no-detach.
38
+ if options[:no_detach]
39
+ PrgCLI.backgroundize
40
+ else
41
+ PrgCLI.daemonize
42
+ end
38
43
 
39
44
  # For daemon mode, default message if none provided
40
45
  text = options[:message] || ARGV.join(' ')
@@ -63,13 +68,17 @@ module RippleCLI
63
68
  end
64
69
 
65
70
  def self.run_with_command(text, options)
66
- if $stdout.tty? && options[:output] == :stdout
71
+ if $stdout.tty?
72
+ # Interactive TTY: use PTY-based capture so the animation can run while the
73
+ # command executes. We only print captured stdout if options[:output] == :stdout.
67
74
  oc = RubyProgress::OutputCapture.new(command: options[:command], lines: options[:output_lines] || 3, position: options[:output_position] || :above)
68
75
  oc.start
69
76
 
70
- # Create rippler and attach output capture so redraw occurs each frame
77
+ # Create rippler. Attach output capture only when the user requested
78
+ # live stdout display via --stdout; otherwise start the PTY reader so
79
+ # we can collect the child's exit status but do not call redraw.
71
80
  rippler = RubyProgress::Ripple.new(text, options)
72
- rippler.instance_variable_set(:@output_capture, oc)
81
+ rippler.instance_variable_set(:@output_capture, oc) if options[:output] == :stdout
73
82
 
74
83
  thread = Thread.new { loop { rippler.advance } }
75
84
  oc.wait
@@ -77,17 +86,24 @@ module RippleCLI
77
86
 
78
87
  captured_lines = oc.lines
79
88
  captured_output = captured_lines.join("\n")
80
- success = true
89
+ success = oc.exit_status.nil? || oc.exit_status.zero?
81
90
  else
82
- # Fallback to legacy capture (non-interactive / CI)
91
+ # Non-interactive / CI: fallback to legacy synchronous capture
83
92
  captured_output = `#{options[:command]} 2>&1`
84
93
  success = $CHILD_STATUS.success?
85
94
  end
86
95
 
87
96
  puts captured_output if options[:output] == :stdout
97
+
88
98
  if options[:success_message] || options[:complete_checkmark]
89
99
  message = success ? options[:success_message] : options[:fail_message] || options[:success_message]
90
- RubyProgress::Ripple.complete(text, message, options[:complete_checkmark], success)
100
+ RubyProgress::Ripple.complete(
101
+ text,
102
+ message,
103
+ options[:complete_checkmark],
104
+ success,
105
+ icons: { success: options[:success_icon], error: options[:error_icon] }
106
+ )
91
107
  end
92
108
  exit success ? 0 : 1
93
109
  end
@@ -147,7 +163,8 @@ module RippleCLI
147
163
  message,
148
164
  success: success_val,
149
165
  show_checkmark: check,
150
- output_stream: :stdout
166
+ output_stream: :stdout,
167
+ icons: { success: options[:success_icon], error: options[:error_icon] }
151
168
  )
152
169
  end
153
170
  rescue StandardError
@@ -196,7 +213,8 @@ module RippleCLI
196
213
  job['message'],
197
214
  success: success,
198
215
  show_checkmark: job['checkmark'] || false,
199
- output_stream: :stdout
216
+ output_stream: :stdout,
217
+ icons: { success: options[:success_icon], error: options[:error_icon] }
200
218
  )
201
219
  end
202
220
 
@@ -71,6 +71,14 @@ module RippleCLI
71
71
  options[:success_message] = msg
72
72
  end
73
73
 
74
+ opts.on('--success-icon ICON', 'Custom success icon to show with completion messages') do |ic|
75
+ options[:success_icon] = ic
76
+ end
77
+
78
+ opts.on('--error-icon ICON', 'Custom error icon to show with failure messages') do |ic|
79
+ options[:error_icon] = ic
80
+ end
81
+
74
82
  opts.on('--error MESSAGE', 'Error message to display') do |msg|
75
83
  options[:fail_message] = msg
76
84
  end
@@ -94,6 +102,10 @@ module RippleCLI
94
102
  options[:daemon] = true
95
103
  end
96
104
 
105
+ opts.on('--no-detach', 'When used with --daemon: run background child but do not fully detach from the terminal') do
106
+ options[:no_detach] = true
107
+ end
108
+
97
109
  opts.on('--pid-file FILE', 'Write process ID to file (default: /tmp/ruby-progress/progress.pid)') do |file|
98
110
  options[:pid_file] = file
99
111
  end
@@ -32,6 +32,12 @@ module TwirlCLI
32
32
  options[:style] = style
33
33
  end
34
34
 
35
+ opts.on('-d', '--direction DIRECTION', 'Animation direction (forward/bidirectional or f/b)') do |direction|
36
+ # Twirl is a spinner and doesn't visibly change with direction, but accept the
37
+ # flag for parity with other subcommands (Worm/Ripple) so scripts can use it.
38
+ options[:direction] = direction =~ /^f/i ? :forward_only : :bidirectional
39
+ end
40
+
35
41
  opts.on('--ends CHARS', 'Start/end characters (even number of chars, split in half)') do |chars|
36
42
  options[:ends] = chars
37
43
  end
@@ -55,6 +61,14 @@ module TwirlCLI
55
61
  options[:success] = text
56
62
  end
57
63
 
64
+ opts.on('--success-icon ICON', 'Custom success icon to show with completion messages') do |ic|
65
+ options[:success_icon] = ic
66
+ end
67
+
68
+ opts.on('--error-icon ICON', 'Custom error icon to show with failure messages') do |ic|
69
+ options[:error_icon] = ic
70
+ end
71
+
58
72
  opts.on('--error MESSAGE', 'Error message to display') do |text|
59
73
  options[:error] = text
60
74
  end
@@ -74,6 +88,10 @@ module TwirlCLI
74
88
  options[:daemon] = true
75
89
  end
76
90
 
91
+ opts.on('--no-detach', 'When used with --daemon/--daemon-as: run background child but do not fully detach from the terminal') do
92
+ options[:no_detach] = true
93
+ end
94
+
77
95
  opts.on('--daemon-as NAME', 'Run in daemon mode with custom name (creates /tmp/ruby-progress/NAME.pid)') do |name|
78
96
  options[:daemon] = true
79
97
  options[:daemon_name] = name
@@ -58,7 +58,8 @@ module TwirlRunner
58
58
  RubyProgress::Utils.display_completion(
59
59
  final_msg,
60
60
  success: success,
61
- show_checkmark: options[:checkmark]
61
+ show_checkmark: options[:checkmark],
62
+ icons: { success: options[:success_icon], error: options[:error_icon] }
62
63
  )
63
64
  end
64
65
 
@@ -78,7 +79,8 @@ module TwirlRunner
78
79
  RubyProgress::Utils.display_completion(
79
80
  options[:success] || 'Complete',
80
81
  success: true,
81
- show_checkmark: options[:checkmark]
82
+ show_checkmark: options[:checkmark],
83
+ icons: { success: options[:success_icon], error: options[:error_icon] }
82
84
  )
83
85
  end
84
86
  end
@@ -124,7 +126,8 @@ module TwirlRunner
124
126
  job['message'],
125
127
  success: success,
126
128
  show_checkmark: job['checkmark'] || false,
127
- output_stream: :stdout
129
+ output_stream: :stdout,
130
+ icons: { success: options[:success_icon], error: options[:error_icon] }
128
131
  )
129
132
  end
130
133
 
@@ -152,7 +155,8 @@ module TwirlRunner
152
155
  message,
153
156
  success: success_val,
154
157
  show_checkmark: check,
155
- output_stream: :stdout
158
+ output_stream: :stdout,
159
+ icons: { success: options[:success_icon], error: options[:error_icon] }
156
160
  )
157
161
  end
158
162
  rescue StandardError
@@ -10,9 +10,23 @@ require_relative '../utils'
10
10
  class TwirlSpinner
11
11
  def initialize(message, options = {})
12
12
  @message = message
13
- @style = parse_style(options[:style] || 'dots')
14
13
  @speed = parse_speed(options[:speed] || 'medium')
15
- @frames = RubyProgress::INDICATORS[@style] || RubyProgress::INDICATORS[:dots]
14
+
15
+ style_opt = options[:style].to_s
16
+ if style_opt.start_with?('custom=')
17
+ chars = style_opt.sub('custom=', '')
18
+ @frames = if chars.length >= 3
19
+ chars.chars
20
+ elsif chars.length == 2
21
+ [chars[0], chars[1], chars[1]]
22
+ else
23
+ [chars, chars, chars]
24
+ end
25
+ @style = :custom
26
+ else
27
+ @style = parse_style(style_opt.empty? ? 'dots' : style_opt)
28
+ @frames = RubyProgress::INDICATORS[@style] || RubyProgress::INDICATORS[:dots]
29
+ end
16
30
  @start_chars, @end_chars = RubyProgress::Utils.parse_ends(options[:ends])
17
31
  @index = 0
18
32
  end
@@ -36,6 +50,8 @@ class TwirlSpinner
36
50
 
37
51
  style_lower = style_input.to_s.downcase.strip
38
52
 
53
+ # parse_style returns a symbol key for RubyProgress::INDICATORS
54
+
39
55
  indicator_keys = RubyProgress::INDICATORS.keys.map(&:to_s)
40
56
  return style_lower.to_sym if indicator_keys.include?(style_lower)
41
57
 
@@ -33,8 +33,14 @@ module WormCLI
33
33
  )
34
34
  exit
35
35
  elsif options[:daemon]
36
- # Detach before starting daemon logic so there's no tracked shell job
37
- PrgCLI.daemonize
36
+ # Detach (or background without detaching) before starting daemon logic
37
+ # so the invoking shell/script continues immediately.
38
+ if options[:no_detach]
39
+ PrgCLI.backgroundize
40
+ else
41
+ PrgCLI.daemonize
42
+ end
43
+
38
44
  run_daemon_mode(options)
39
45
  else
40
46
  progress = RubyProgress::Worm.new(options)
@@ -86,7 +92,8 @@ module WormCLI
86
92
  job['message'],
87
93
  success: success,
88
94
  show_checkmark: job['checkmark'] || false,
89
- output_stream: :stdout
95
+ output_stream: :stdout,
96
+ icons: { success: options[:success_icon], error: options[:error_icon] }
90
97
  )
91
98
  end
92
99
 
@@ -99,7 +106,8 @@ module WormCLI
99
106
  progress.run_daemon_mode(
100
107
  success_message: options[:success],
101
108
  show_checkmark: options[:checkmark],
102
- control_message_file: RubyProgress::Daemon.control_message_file(pid_file)
109
+ control_message_file: RubyProgress::Daemon.control_message_file(pid_file),
110
+ icons: { success: options[:success_icon], error: options[:error_icon] }
103
111
  )
104
112
  ensure
105
113
  job_thread&.kill
@@ -63,6 +63,14 @@ module WormCLI
63
63
  options[:success] = text
64
64
  end
65
65
 
66
+ opts.on('--success-icon ICON', 'Custom success icon to show with completion messages') do |ic|
67
+ options[:success_icon] = ic
68
+ end
69
+
70
+ opts.on('--error-icon ICON', 'Custom error icon to show with failure messages') do |ic|
71
+ options[:error_icon] = ic
72
+ end
73
+
66
74
  opts.on('--error MESSAGE', 'Error message to display') do |text|
67
75
  options[:error] = text
68
76
  end
@@ -93,6 +101,10 @@ module WormCLI
93
101
  options[:daemon_name] = name
94
102
  end
95
103
 
104
+ opts.on('--no-detach', 'When used with --daemon/--daemon-as: run background child but do not fully detach from the terminal') do
105
+ options[:no_detach] = true
106
+ end
107
+
96
108
  opts.on('--pid-file FILE', 'Write process ID to file (default: /tmp/ruby-progress/progress.pid)') do |file|
97
109
  options[:pid_file] = file
98
110
  end
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- # rubocop:disable Metrics/ModuleLength
5
4
  require 'open3'
6
5
  require 'json'
7
6
  require_relative '../utils'
@@ -120,7 +119,7 @@ module WormRunner
120
119
  @running = false
121
120
  end
122
121
 
123
- def run_daemon_mode(success_message: nil, show_checkmark: false, control_message_file: nil)
122
+ def run_daemon_mode(success_message: nil, show_checkmark: false, control_message_file: nil, icons: {})
124
123
  @running = true
125
124
  stop_requested = false
126
125
 
@@ -163,7 +162,8 @@ module WormRunner
163
162
  final_message,
164
163
  success: final_success,
165
164
  show_checkmark: final_checkmark,
166
- output_stream: :stdout
165
+ output_stream: :stdout,
166
+ icons: icons
167
167
  )
168
168
  end
169
169
 
@@ -278,5 +278,3 @@ module WormRunner
278
278
  dots.join
279
279
  end
280
280
  end
281
-
282
- # rubocop:enable Metrics/ModuleLength
@@ -95,7 +95,7 @@ module RubyProgress
95
95
  end
96
96
 
97
97
  # Complete the progress bar and show success message
98
- def complete(message = nil)
98
+ def complete(message = nil, icons: {})
99
99
  @current_progress = @length
100
100
  render
101
101
 
@@ -105,7 +105,8 @@ module RubyProgress
105
105
  completion_message,
106
106
  success: true,
107
107
  show_checkmark: true,
108
- output_stream: :warn
108
+ output_stream: :warn,
109
+ icons: icons
109
110
  )
110
111
  else
111
112
  $stderr.puts # Just add a newline if no message
@@ -124,7 +125,8 @@ module RubyProgress
124
125
  error_msg,
125
126
  success: false,
126
127
  show_checkmark: true,
127
- output_stream: :warn
128
+ output_stream: :warn,
129
+ icons: {}
128
130
  )
129
131
  end
130
132