ruby-progress 1.2.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +76 -154
- data/DEMO_SCRIPTS.md +162 -0
- data/Gemfile.lock +1 -1
- data/README.md +128 -120
- data/Rakefile +7 -0
- data/bin/fill +10 -0
- data/bin/prg +76 -1024
- data/demo_screencast.rb +296 -0
- data/examples/daemon_job_example.sh +25 -0
- data/experimental_terminal.rb +7 -0
- data/lib/ruby-progress/cli/fill_options.rb +215 -0
- data/lib/ruby-progress/cli/job_cli.rb +99 -0
- data/lib/ruby-progress/cli/ripple_cli.rb +211 -0
- data/lib/ruby-progress/cli/ripple_options.rb +158 -0
- data/lib/ruby-progress/cli/twirl_cli.rb +173 -0
- data/lib/ruby-progress/cli/twirl_options.rb +147 -0
- data/lib/ruby-progress/cli/twirl_runner.rb +183 -0
- data/lib/ruby-progress/cli/twirl_spinner.rb +79 -0
- data/lib/ruby-progress/cli/worm_cli.rb +109 -0
- data/lib/ruby-progress/cli/worm_options.rb +173 -0
- data/lib/ruby-progress/cli/worm_runner.rb +282 -0
- data/lib/ruby-progress/daemon.rb +65 -0
- data/lib/ruby-progress/fill.rb +215 -0
- data/lib/ruby-progress/fill_cli.rb +249 -0
- data/lib/ruby-progress/output_capture.rb +136 -0
- data/lib/ruby-progress/ripple.rb +1 -0
- data/lib/ruby-progress/utils.rb +16 -2
- data/lib/ruby-progress/version.rb +8 -4
- data/lib/ruby-progress/worm.rb +2 -177
- data/lib/ruby-progress.rb +1 -0
- data/quick_demo.rb +134 -0
- data/readme_demo.rb +128 -0
- data/ruby-progress.gemspec +40 -0
- data/scripts/run_matrix_mise.fish +41 -0
- data/test_daemon_interruption.rb +2 -0
- data/test_daemon_orphan.rb +1 -0
- metadata +24 -1
|
@@ -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
|