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.
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'json'
5
+ require 'securerandom'
6
+ require_relative 'ripple_options'
7
+ require_relative '../output_capture'
8
+
9
+ # Enhanced Ripple CLI with unified flags (extracted from bin/prg)
10
+ module RippleCLI
11
+ def self.run
12
+ trap('INT') do
13
+ RubyProgress::Utils.show_cursor
14
+ exit
15
+ end
16
+
17
+ options = RippleCLI::Options.parse_cli_options
18
+
19
+ # Daemon/status/stop handling (process these without requiring text)
20
+ if options[:status]
21
+ pid_file = options[:pid_file] || RubyProgress::Daemon.default_pid_file
22
+ RubyProgress::Daemon.show_status(pid_file)
23
+ exit
24
+ elsif options[:stop]
25
+ pid_file = options[:pid_file] || RubyProgress::Daemon.default_pid_file
26
+ stop_msg = options[:stop_error] || options[:stop_success]
27
+ is_error = !options[:stop_error].nil?
28
+ RubyProgress::Daemon.stop_daemon_by_pid_file(
29
+ pid_file,
30
+ message: stop_msg,
31
+ checkmark: options[:stop_checkmark],
32
+ error: is_error
33
+ )
34
+ exit
35
+ elsif options[:daemon]
36
+ # For daemon mode, detach so shell has no tracked job
37
+ PrgCLI.daemonize
38
+
39
+ # For daemon mode, default message if none provided
40
+ text = options[:message] || ARGV.join(' ')
41
+ text = 'Processing' if text.nil? || text.empty?
42
+ run_daemon_mode(text, options)
43
+ else
44
+ # Non-daemon path requires text
45
+ text = options[:message] || ARGV.join(' ')
46
+ if text.empty?
47
+ puts 'Error: Please provide text to animate via argument or --message flag'
48
+ puts "Example: prg ripple 'Loading...' or prg ripple --message 'Loading...'"
49
+ exit 1
50
+ end
51
+
52
+ # Convert styles array to individual flags for backward compatibility
53
+ options[:rainbow] = options[:styles].include?(:rainbow)
54
+ options[:inverse] = options[:styles].include?(:inverse)
55
+ options[:caps] = options[:styles].include?(:caps)
56
+
57
+ if options[:command]
58
+ run_with_command(text, options)
59
+ else
60
+ run_indefinitely(text, options)
61
+ end
62
+ end
63
+ end
64
+
65
+ def self.run_with_command(text, options)
66
+ if $stdout.tty? && options[:output] == :stdout
67
+ oc = RubyProgress::OutputCapture.new(command: options[:command], lines: options[:output_lines] || 3, position: options[:output_position] || :above)
68
+ oc.start
69
+
70
+ # Create rippler and attach output capture so redraw occurs each frame
71
+ rippler = RubyProgress::Ripple.new(text, options)
72
+ rippler.instance_variable_set(:@output_capture, oc)
73
+
74
+ thread = Thread.new { loop { rippler.advance } }
75
+ oc.wait
76
+ thread.kill
77
+
78
+ captured_lines = oc.lines
79
+ captured_output = captured_lines.join("\n")
80
+ success = true
81
+ else
82
+ # Fallback to legacy capture (non-interactive / CI)
83
+ captured_output = `#{options[:command]} 2>&1`
84
+ success = $CHILD_STATUS.success?
85
+ end
86
+
87
+ puts captured_output if options[:output] == :stdout
88
+ if options[:success_message] || options[:complete_checkmark]
89
+ message = success ? options[:success_message] : options[:fail_message] || options[:success_message]
90
+ RubyProgress::Ripple.complete(text, message, options[:complete_checkmark], success)
91
+ end
92
+ exit success ? 0 : 1
93
+ end
94
+
95
+ def self.run_indefinitely(text, options)
96
+ rippler = RubyProgress::Ripple.new(text, options)
97
+ RubyProgress::Utils.hide_cursor
98
+ begin
99
+ loop { rippler.advance }
100
+ ensure
101
+ RubyProgress::Utils.show_cursor
102
+ RubyProgress::Ripple.complete(text, options[:success_message], options[:complete_checkmark], true)
103
+ end
104
+ end
105
+
106
+ def self.run_daemon_mode(text, options)
107
+ pid_file = options[:pid_file] || RubyProgress::Daemon.default_pid_file
108
+ FileUtils.mkdir_p(File.dirname(pid_file))
109
+ File.write(pid_file, Process.pid.to_s)
110
+ begin
111
+ # For Ripple, re-use the existing animation loop via a simple loop
112
+ RubyProgress::Utils.hide_cursor
113
+ rippler = RubyProgress::Ripple.new(text, options)
114
+ stop_requested = false
115
+
116
+ Signal.trap('INT') { stop_requested = true }
117
+ Signal.trap('USR1') { stop_requested = true }
118
+ Signal.trap('TERM') { stop_requested = true }
119
+ Signal.trap('HUP') { stop_requested = true }
120
+
121
+ job_dir = RubyProgress::Daemon.job_dir_for_pid(pid_file)
122
+ job_thread = Thread.new { process_daemon_jobs_for_rippler(job_dir, rippler, options) }
123
+
124
+ rippler.advance until stop_requested
125
+ ensure
126
+ RubyProgress::Utils.clear_line
127
+ RubyProgress::Utils.show_cursor
128
+
129
+ # If a control message file exists, output its message with optional checkmark
130
+ cmf = RubyProgress::Daemon.control_message_file(pid_file)
131
+ if File.exist?(cmf)
132
+ begin
133
+ data = JSON.parse(File.read(cmf))
134
+ message = data['message']
135
+ check = if data.key?('checkmark')
136
+ data['checkmark'] ? true : false
137
+ else
138
+ false
139
+ end
140
+ success_val = if data.key?('success')
141
+ data['success'] ? true : false
142
+ else
143
+ true
144
+ end
145
+ if message
146
+ RubyProgress::Utils.display_completion(
147
+ message,
148
+ success: success_val,
149
+ show_checkmark: check,
150
+ output_stream: :stdout
151
+ )
152
+ end
153
+ rescue StandardError
154
+ # ignore
155
+ ensure
156
+ begin
157
+ File.delete(cmf)
158
+ rescue StandardError
159
+ nil
160
+ end
161
+ end
162
+ end
163
+
164
+ # stop job thread and cleanup
165
+ job_thread&.kill
166
+ FileUtils.rm_f(pid_file)
167
+ end
168
+ end
169
+
170
+ def self.process_daemon_jobs_for_rippler(job_dir, rippler, options)
171
+ RubyProgress::Daemon.process_jobs(job_dir) do |job|
172
+ jid = job['id'] || SecureRandom.uuid
173
+ log_path = begin
174
+ File.join(File.dirname(job_dir), "#{jid}.log")
175
+ rescue StandardError
176
+ nil
177
+ end
178
+
179
+ oc = RubyProgress::OutputCapture.new(
180
+ command: job['command'],
181
+ lines: options[:output_lines] || 3,
182
+ position: options[:output_position] || :above,
183
+ log_path: log_path
184
+ )
185
+ oc.start
186
+
187
+ rippler.instance_variable_set(:@output_capture, oc)
188
+ oc.wait
189
+ captured = oc.lines.join("\n")
190
+ exit_status = oc.exit_status
191
+ rippler.instance_variable_set(:@output_capture, nil)
192
+
193
+ success = exit_status.to_i.zero?
194
+ if job['message']
195
+ RubyProgress::Utils.display_completion(
196
+ job['message'],
197
+ success: success,
198
+ show_checkmark: job['checkmark'] || false,
199
+ output_stream: :stdout
200
+ )
201
+ end
202
+
203
+ { 'exit_status' => exit_status, 'output' => captured, 'log_path' => log_path }
204
+ rescue StandardError
205
+ # ignore per-job errors; process_jobs will write result
206
+ nil
207
+ end
208
+ end
209
+
210
+ # Options parsing moved to ripple_options.rb
211
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse'
4
+ require 'json'
5
+
6
+ module RippleCLI
7
+ # Option parsing extracted to its own file to reduce module size of RippleCLI.
8
+ module Options
9
+ def self.parse_cli_options
10
+ options = {
11
+ speed: :medium,
12
+ direction: :bidirectional,
13
+ styles: [],
14
+ caps: false,
15
+ command: nil,
16
+ success_message: nil,
17
+ fail_message: nil,
18
+ complete_checkmark: false,
19
+ output: :error,
20
+ output_position: :above,
21
+ output_lines: 3,
22
+ message: nil
23
+ }
24
+
25
+ begin
26
+ OptionParser.new do |opts|
27
+ opts.banner = 'Usage: prg ripple [options] [STRING]'
28
+ opts.separator ''
29
+ opts.separator 'Animation Options:'
30
+
31
+ opts.on('-s', '--speed SPEED', 'Animation speed (fast/medium/slow or f/m/s)') do |s|
32
+ options[:speed] = case s.downcase
33
+ when /^f/ then :fast
34
+ when /^s/ then :slow
35
+ else :medium
36
+ end
37
+ end
38
+
39
+ opts.on('-m', '--message MESSAGE', 'Message to display (alternative to positional argument)') do |msg|
40
+ options[:message] = msg
41
+ end
42
+
43
+ opts.on('--style STYLES', 'Animation styles (rainbow, inverse, caps - can be comma-separated)') do |styles|
44
+ options[:styles] = styles.split(',').map(&:strip).map(&:to_sym)
45
+ end
46
+
47
+ opts.on('-d', '--direction DIRECTION', 'Animation direction (forward/bidirectional or f/b)') do |f|
48
+ options[:format] = f =~ /^f/i ? :forward_only : :bidirectional
49
+ end
50
+
51
+ opts.on('--ends CHARS', 'Start/end characters (even number of chars, split in half)') do |chars|
52
+ options[:ends] = chars
53
+ end
54
+
55
+ opts.separator ''
56
+ opts.separator 'Command Execution:'
57
+
58
+ opts.on('-c', '--command COMMAND', 'Run command during animation (optional)') do |command|
59
+ options[:command] = command
60
+ end
61
+
62
+ opts.on('--output-position POSITION', 'Position to render captured output: above or below (default: above)') do |pos|
63
+ options[:output_position] = pos.to_sym
64
+ end
65
+
66
+ opts.on('--output-lines N', Integer, 'Number of output lines to reserve for captured output (default: 3)') do |n|
67
+ options[:output_lines] = n
68
+ end
69
+
70
+ opts.on('--success MESSAGE', 'Success message to display') do |msg|
71
+ options[:success_message] = msg
72
+ end
73
+
74
+ opts.on('--error MESSAGE', 'Error message to display') do |msg|
75
+ options[:fail_message] = msg
76
+ end
77
+
78
+ opts.on('--checkmark', 'Show checkmarks (✅ success, 🛑 failure)') do
79
+ options[:complete_checkmark] = true
80
+ end
81
+
82
+ opts.on('--stdout', 'Output captured command result to STDOUT') do
83
+ options[:output] = :stdout
84
+ end
85
+
86
+ opts.on('--quiet', 'Suppress all output') do
87
+ options[:output] = :quiet
88
+ end
89
+
90
+ opts.separator ''
91
+ opts.separator 'Daemon Mode:'
92
+
93
+ opts.on('--daemon', 'Run in background daemon mode') do
94
+ options[:daemon] = true
95
+ end
96
+
97
+ opts.on('--pid-file FILE', 'Write process ID to file (default: /tmp/ruby-progress/progress.pid)') do |file|
98
+ options[:pid_file] = file
99
+ end
100
+
101
+ opts.on('--stop', 'Stop daemon (uses default PID file unless --pid-file specified)') do
102
+ options[:stop] = true
103
+ end
104
+
105
+ opts.on('--status', 'Show daemon status (running/not running)') do
106
+ options[:status] = true
107
+ end
108
+
109
+ opts.on('--stop-success MESSAGE', 'When stopping, show this success message') do |msg|
110
+ options[:stop_success] = msg
111
+ end
112
+ opts.on('--stop-error MESSAGE', 'When stopping, show this error message') do |msg|
113
+ options[:stop_error] = msg
114
+ end
115
+ opts.on('--stop-checkmark', 'When stopping, include a success/error checkmark') do
116
+ options[:stop_checkmark] = true
117
+ end
118
+
119
+ opts.separator ''
120
+ opts.separator 'Daemon notes:'
121
+ opts.separator ' - Do not append &; prg detaches itself and returns immediately.'
122
+ opts.separator ' - Use --status/--stop with optional --pid-file to control it.'
123
+
124
+ opts.separator ''
125
+ opts.separator 'General:'
126
+
127
+ opts.on('--show-styles', 'Show available ripple styles with visual previews') do
128
+ PrgCLI.show_ripple_styles
129
+ exit
130
+ end
131
+
132
+ opts.on('--stop-all', 'Stop all prg ripple processes') do
133
+ success = PrgCLI.stop_subcommand_processes('ripple')
134
+ exit(success ? 0 : 1)
135
+ end
136
+
137
+ opts.on('-v', '--version', 'Show version') do
138
+ puts "Ripple version #{RubyProgress::VERSION}"
139
+ exit
140
+ end
141
+
142
+ opts.on('-h', '--help', 'Show this help') do
143
+ puts opts
144
+ exit
145
+ end
146
+ end.parse!
147
+ rescue OptionParser::InvalidOption => e
148
+ puts "Invalid option: #{e.args.first}"
149
+ puts ''
150
+ puts 'Usage: prg ripple [options] [STRING]'
151
+ puts "Run 'prg ripple --help' for more information."
152
+ exit 1
153
+ end
154
+
155
+ options
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require_relative 'twirl_options'
5
+ require_relative 'twirl_spinner'
6
+ require_relative 'twirl_runner'
7
+
8
+ # Twirl CLI (extracted from bin/prg)
9
+ module TwirlCLI
10
+ def self.run
11
+ options = TwirlCLI::Options.parse_cli_options
12
+
13
+ if options[:status]
14
+ pid_file = resolve_pid_file(options, :status_name)
15
+ RubyProgress::Daemon.show_status(pid_file)
16
+ exit
17
+ elsif options[:stop]
18
+ pid_file = resolve_pid_file(options, :stop_name)
19
+ stop_msg = options[:stop_error] || options[:stop_success]
20
+ is_error = !options[:stop_error].nil?
21
+ RubyProgress::Daemon.stop_daemon_by_pid_file(
22
+ pid_file,
23
+ message: stop_msg,
24
+ checkmark: options[:stop_checkmark],
25
+ error: is_error
26
+ )
27
+ exit
28
+ elsif options[:daemon]
29
+ PrgCLI.daemonize
30
+ TwirlRunner.run_daemon_mode(options)
31
+ elsif options[:command]
32
+ TwirlRunner.run_with_command(options)
33
+ else
34
+ TwirlRunner.run_indefinitely(options)
35
+ end
36
+ end
37
+
38
+ # runtime methods moved to TwirlRunner
39
+ def self.resolve_pid_file(options, name_key)
40
+ return options[:pid_file] if options[:pid_file]
41
+
42
+ if options[name_key]
43
+ "/tmp/ruby-progress/#{options[name_key]}.pid"
44
+ else
45
+ RubyProgress::Daemon.default_pid_file
46
+ end
47
+ end
48
+
49
+ def self.parse_cli_options
50
+ options = {}
51
+
52
+ OptionParser.new do |opts|
53
+ opts.banner = 'Usage: prg twirl [options]'
54
+ opts.separator ''
55
+ opts.separator 'Animation Options:'
56
+
57
+ opts.on('-s', '--speed SPEED', 'Animation speed (1-10, fast/medium/slow, or f/m/s)') do |speed|
58
+ options[:speed] = speed
59
+ end
60
+
61
+ opts.on('-m', '--message MESSAGE', 'Message to display before spinner') do |message|
62
+ options[:message] = message
63
+ end
64
+
65
+ opts.on('--style STYLE', 'Spinner style (see --show-styles for options)') do |style|
66
+ options[:style] = style
67
+ end
68
+
69
+ opts.on('--ends CHARS', 'Start/end characters (even number of chars, split in half)') do |chars|
70
+ options[:ends] = chars
71
+ end
72
+
73
+ opts.separator ''
74
+ opts.separator 'Command Execution:'
75
+
76
+ opts.on('-c', '--command COMMAND', 'Command to run (optional - runs indefinitely without)') do |command|
77
+ options[:command] = command
78
+ end
79
+
80
+ opts.on('--success MESSAGE', 'Success message to display') do |text|
81
+ options[:success] = text
82
+ end
83
+
84
+ opts.on('--error MESSAGE', 'Error message to display') do |text|
85
+ options[:error] = text
86
+ end
87
+
88
+ opts.on('--checkmark', 'Show checkmarks (✅ success, 🛑 failure)') do
89
+ options[:checkmark] = true
90
+ end
91
+
92
+ opts.on('--stdout', 'Output captured command result to STDOUT') do |_text|
93
+ options[:stdout] = true
94
+ end
95
+
96
+ opts.separator ''
97
+ opts.separator 'Daemon Mode:'
98
+
99
+ opts.on('--daemon', 'Run in background daemon mode') do
100
+ options[:daemon] = true
101
+ end
102
+
103
+ opts.on('--daemon-as NAME', 'Run in daemon mode with custom name (creates /tmp/ruby-progress/NAME.pid)') do |name|
104
+ options[:daemon] = true
105
+ options[:daemon_name] = name
106
+ end
107
+
108
+ opts.on('--pid-file FILE', 'Write process ID to file (default: /tmp/ruby-progress/progress.pid)') do |file|
109
+ options[:pid_file] = file
110
+ end
111
+
112
+ opts.on('--stop', 'Stop daemon (uses default PID file unless --pid-file specified)') do
113
+ options[:stop] = true
114
+ end
115
+ opts.on('--stop-id NAME', 'Stop daemon by name (automatically implies --stop)') do |name|
116
+ options[:stop] = true
117
+ options[:stop_name] = name
118
+ end
119
+ opts.on('--status', 'Show daemon status (uses default PID file unless --pid-file specified)') do
120
+ options[:status] = true
121
+ end
122
+ opts.on('--status-id NAME', 'Show daemon status by name') do |name|
123
+ options[:status] = true
124
+ options[:status_name] = name
125
+ end
126
+ opts.on('--stop-success MESSAGE', 'Stop daemon with success message (automatically implies --stop)') do |msg|
127
+ options[:stop] = true
128
+ options[:stop_success] = msg
129
+ end
130
+ opts.on('--stop-error MESSAGE', 'Stop daemon with error message (automatically implies --stop)') do |msg|
131
+ options[:stop] = true
132
+ options[:stop_error] = msg
133
+ end
134
+ opts.on('--stop-checkmark', 'When stopping, include a success checkmark') { options[:stop_checkmark] = true }
135
+
136
+ opts.separator ''
137
+ opts.separator 'Daemon notes:'
138
+ opts.separator ' - Do not append &; prg detaches itself and returns immediately.'
139
+ opts.separator ' - Use --daemon-as NAME for named daemons, or --stop-id/--status-id for named control.'
140
+
141
+ opts.separator ''
142
+ opts.separator 'General:'
143
+
144
+ opts.on('--show-styles', 'Show available twirl styles with visual previews') do
145
+ PrgCLI.show_twirl_styles
146
+ exit
147
+ end
148
+
149
+ opts.on('--stop-all', 'Stop all prg twirl processes') do
150
+ success = PrgCLI.stop_subcommand_processes('twirl')
151
+ exit(success ? 0 : 1)
152
+ end
153
+
154
+ opts.on('-v', '--version', 'Show version') do
155
+ puts "Twirl version #{RubyProgress::VERSION}"
156
+ exit
157
+ end
158
+
159
+ opts.on('-h', '--help', 'Show this help') do
160
+ puts opts
161
+ exit
162
+ end
163
+ end.parse!
164
+
165
+ options
166
+ rescue OptionParser::InvalidOption => e
167
+ puts "Invalid option: #{e.args.first}"
168
+ puts ''
169
+ puts 'Usage: prg twirl [options]'
170
+ puts "Run 'prg twirl --help' for more information."
171
+ exit 1
172
+ end
173
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse'
4
+ require 'json'
5
+
6
+ module TwirlCLI
7
+ # Option parsing helpers for the Twirl subcommand.
8
+ #
9
+ # Keeps the CLI option definitions extracted from the main dispatcher
10
+ # so the `TwirlCLI` module stays small and focused on dispatching.
11
+ module Options
12
+ def self.parse_cli_options
13
+ options = {
14
+ output_position: :above,
15
+ output_lines: 3
16
+ }
17
+
18
+ OptionParser.new do |opts|
19
+ opts.banner = 'Usage: prg twirl [options]'
20
+ opts.separator ''
21
+ opts.separator 'Animation Options:'
22
+
23
+ opts.on('-s', '--speed SPEED', 'Animation speed (1-10, fast/medium/slow, or f/m/s)') do |speed|
24
+ options[:speed] = speed
25
+ end
26
+
27
+ opts.on('-m', '--message MESSAGE', 'Message to display before spinner') do |message|
28
+ options[:message] = message
29
+ end
30
+
31
+ opts.on('--style STYLE', 'Spinner style (see --show-styles for options)') do |style|
32
+ options[:style] = style
33
+ end
34
+
35
+ opts.on('--ends CHARS', 'Start/end characters (even number of chars, split in half)') do |chars|
36
+ options[:ends] = chars
37
+ end
38
+
39
+ opts.separator ''
40
+ opts.separator 'Command Execution:'
41
+
42
+ opts.on('-c', '--command COMMAND', 'Command to run (optional - runs indefinitely without)') do |command|
43
+ options[:command] = command
44
+ end
45
+
46
+ opts.on('--output-position POSITION', 'Position to render captured output: above or below (default: above)') do |pos|
47
+ options[:output_position] = pos.to_sym
48
+ end
49
+
50
+ opts.on('--output-lines N', Integer, 'Number of output lines to reserve for captured output (default: 3)') do |n|
51
+ options[:output_lines] = n
52
+ end
53
+
54
+ opts.on('--success MESSAGE', 'Success message to display') do |text|
55
+ options[:success] = text
56
+ end
57
+
58
+ opts.on('--error MESSAGE', 'Error message to display') do |text|
59
+ options[:error] = text
60
+ end
61
+
62
+ opts.on('--checkmark', 'Show checkmarks (✅ success, 🛑 failure)') do
63
+ options[:checkmark] = true
64
+ end
65
+
66
+ opts.on('--stdout', 'Output captured command result to STDOUT') do
67
+ options[:stdout] = true
68
+ end
69
+
70
+ opts.separator ''
71
+ opts.separator 'Daemon Mode:'
72
+
73
+ opts.on('--daemon', 'Run in background daemon mode') do
74
+ options[:daemon] = true
75
+ end
76
+
77
+ opts.on('--daemon-as NAME', 'Run in daemon mode with custom name (creates /tmp/ruby-progress/NAME.pid)') do |name|
78
+ options[:daemon] = true
79
+ options[:daemon_name] = name
80
+ end
81
+
82
+ opts.on('--pid-file FILE', 'Write process ID to file (default: /tmp/ruby-progress/progress.pid)') do |file|
83
+ options[:pid_file] = file
84
+ end
85
+
86
+ opts.on('--stop', 'Stop daemon (uses default PID file unless --pid-file specified)') do
87
+ options[:stop] = true
88
+ end
89
+ opts.on('--stop-id NAME', 'Stop daemon by name (automatically implies --stop)') do |name|
90
+ options[:stop] = true
91
+ options[:stop_name] = name
92
+ end
93
+ opts.on('--status', 'Show daemon status (uses default PID file unless --pid-file specified)') do
94
+ options[:status] = true
95
+ end
96
+ opts.on('--status-id NAME', 'Show daemon status by name') do |name|
97
+ options[:status] = true
98
+ options[:status_name] = name
99
+ end
100
+ opts.on('--stop-success MESSAGE', 'Stop daemon with success message (automatically implies --stop)') do |msg|
101
+ options[:stop] = true
102
+ options[:stop_success] = msg
103
+ end
104
+ opts.on('--stop-error MESSAGE', 'Stop daemon with error message (automatically implies --stop)') do |msg|
105
+ options[:stop] = true
106
+ options[:stop_error] = msg
107
+ end
108
+ opts.on('--stop-checkmark', 'When stopping, include a success checkmark') { options[:stop_checkmark] = true }
109
+
110
+ opts.separator ''
111
+ opts.separator 'Daemon notes:'
112
+ opts.separator ' - Do not append &; prg detaches itself and returns immediately.'
113
+ opts.separator ' - Use --daemon-as NAME for named daemons, or --stop-id/--status-id for named control.'
114
+
115
+ opts.separator ''
116
+ opts.separator 'General:'
117
+
118
+ opts.on('--show-styles', 'Show available twirl styles with visual previews') do
119
+ PrgCLI.show_twirl_styles
120
+ exit
121
+ end
122
+
123
+ opts.on('--stop-all', 'Stop all prg twirl processes') do
124
+ success = PrgCLI.stop_subcommand_processes('twirl')
125
+ exit(success ? 0 : 1)
126
+ end
127
+
128
+ opts.on('-v', '--version', 'Show version') do
129
+ puts "Twirl version #{RubyProgress::VERSION}"
130
+ exit
131
+ end
132
+
133
+ opts.on('-h', '--help', 'Show this help') do
134
+ puts opts
135
+ exit
136
+ end
137
+ end.parse!
138
+ options
139
+ rescue OptionParser::InvalidOption => e
140
+ puts "Invalid option: #{e.args.first}"
141
+ puts ''
142
+ puts 'Usage: prg twirl [options]'
143
+ puts "Run 'prg twirl --help' for more information."
144
+ exit 1
145
+ end
146
+ end
147
+ end