ruby-progress 1.1.9 → 1.2.4
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 +63 -122
- data/DEMO_SCRIPTS.md +162 -0
- data/Gemfile.lock +1 -1
- data/README.md +201 -62
- data/Rakefile +7 -0
- data/bin/fill +10 -0
- data/bin/prg +50 -1009
- data/demo_screencast.rb +296 -0
- data/experimental_terminal.rb +7 -0
- data/lib/ruby-progress/cli/fill_options.rb +193 -0
- data/lib/ruby-progress/cli/ripple_cli.rb +150 -0
- data/lib/ruby-progress/cli/ripple_options.rb +148 -0
- data/lib/ruby-progress/cli/twirl_cli.rb +173 -0
- data/lib/ruby-progress/cli/twirl_options.rb +136 -0
- data/lib/ruby-progress/cli/twirl_runner.rb +130 -0
- data/lib/ruby-progress/cli/twirl_spinner.rb +78 -0
- data/lib/ruby-progress/cli/worm_cli.rb +75 -0
- data/lib/ruby-progress/cli/worm_options.rb +156 -0
- data/lib/ruby-progress/cli/worm_runner.rb +260 -0
- data/lib/ruby-progress/fill.rb +211 -0
- data/lib/ruby-progress/fill_cli.rb +219 -0
- data/lib/ruby-progress/ripple.rb +4 -2
- data/lib/ruby-progress/utils.rb +32 -2
- data/lib/ruby-progress/version.rb +8 -4
- data/lib/ruby-progress/worm.rb +43 -178
- 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 +21 -1
|
@@ -0,0 +1,148 @@
|
|
|
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
|
+
message: nil
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
begin
|
|
24
|
+
OptionParser.new do |opts|
|
|
25
|
+
opts.banner = 'Usage: prg ripple [options] [STRING]'
|
|
26
|
+
opts.separator ''
|
|
27
|
+
opts.separator 'Animation Options:'
|
|
28
|
+
|
|
29
|
+
opts.on('-s', '--speed SPEED', 'Animation speed (fast/medium/slow or f/m/s)') do |s|
|
|
30
|
+
options[:speed] = case s.downcase
|
|
31
|
+
when /^f/ then :fast
|
|
32
|
+
when /^s/ then :slow
|
|
33
|
+
else :medium
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
opts.on('-m', '--message MESSAGE', 'Message to display (alternative to positional argument)') do |msg|
|
|
38
|
+
options[:message] = msg
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
opts.on('--style STYLES', 'Animation styles (rainbow, inverse, caps - can be comma-separated)') do |styles|
|
|
42
|
+
options[:styles] = styles.split(',').map(&:strip).map(&:to_sym)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
opts.on('-d', '--direction DIRECTION', 'Animation direction (forward/bidirectional or f/b)') do |f|
|
|
46
|
+
options[:format] = f =~ /^f/i ? :forward_only : :bidirectional
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
opts.on('--ends CHARS', 'Start/end characters (even number of chars, split in half)') do |chars|
|
|
50
|
+
options[:ends] = chars
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
opts.separator ''
|
|
54
|
+
opts.separator 'Command Execution:'
|
|
55
|
+
|
|
56
|
+
opts.on('-c', '--command COMMAND', 'Run command during animation (optional)') do |command|
|
|
57
|
+
options[:command] = command
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
opts.on('--success MESSAGE', 'Success message to display') do |msg|
|
|
61
|
+
options[:success_message] = msg
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
opts.on('--error MESSAGE', 'Error message to display') do |msg|
|
|
65
|
+
options[:fail_message] = msg
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
opts.on('--checkmark', 'Show checkmarks (✅ success, 🛑 failure)') do
|
|
69
|
+
options[:complete_checkmark] = true
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
opts.on('--stdout', 'Output captured command result to STDOUT') do
|
|
73
|
+
options[:output] = :stdout
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
opts.on('--quiet', 'Suppress all output') do
|
|
77
|
+
options[:output] = :quiet
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
opts.separator ''
|
|
81
|
+
opts.separator 'Daemon Mode:'
|
|
82
|
+
|
|
83
|
+
opts.on('--daemon', 'Run in background daemon mode') do
|
|
84
|
+
options[:daemon] = true
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
opts.on('--pid-file FILE', 'Write process ID to file (default: /tmp/ruby-progress/progress.pid)') do |file|
|
|
88
|
+
options[:pid_file] = file
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
opts.on('--stop', 'Stop daemon (uses default PID file unless --pid-file specified)') do
|
|
92
|
+
options[:stop] = true
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
opts.on('--status', 'Show daemon status (running/not running)') do
|
|
96
|
+
options[:status] = true
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
opts.on('--stop-success MESSAGE', 'When stopping, show this success message') do |msg|
|
|
100
|
+
options[:stop_success] = msg
|
|
101
|
+
end
|
|
102
|
+
opts.on('--stop-error MESSAGE', 'When stopping, show this error message') do |msg|
|
|
103
|
+
options[:stop_error] = msg
|
|
104
|
+
end
|
|
105
|
+
opts.on('--stop-checkmark', 'When stopping, include a success/error checkmark') do
|
|
106
|
+
options[:stop_checkmark] = true
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
opts.separator ''
|
|
110
|
+
opts.separator 'Daemon notes:'
|
|
111
|
+
opts.separator ' - Do not append &; prg detaches itself and returns immediately.'
|
|
112
|
+
opts.separator ' - Use --status/--stop with optional --pid-file to control it.'
|
|
113
|
+
|
|
114
|
+
opts.separator ''
|
|
115
|
+
opts.separator 'General:'
|
|
116
|
+
|
|
117
|
+
opts.on('--show-styles', 'Show available ripple styles with visual previews') do
|
|
118
|
+
PrgCLI.show_ripple_styles
|
|
119
|
+
exit
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
opts.on('--stop-all', 'Stop all prg ripple processes') do
|
|
123
|
+
success = PrgCLI.stop_subcommand_processes('ripple')
|
|
124
|
+
exit(success ? 0 : 1)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
opts.on('-v', '--version', 'Show version') do
|
|
128
|
+
puts "Ripple version #{RubyProgress::VERSION}"
|
|
129
|
+
exit
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
opts.on('-h', '--help', 'Show this help') do
|
|
133
|
+
puts opts
|
|
134
|
+
exit
|
|
135
|
+
end
|
|
136
|
+
end.parse!
|
|
137
|
+
rescue OptionParser::InvalidOption => e
|
|
138
|
+
puts "Invalid option: #{e.args.first}"
|
|
139
|
+
puts ''
|
|
140
|
+
puts 'Usage: prg ripple [options] [STRING]'
|
|
141
|
+
puts "Run 'prg ripple --help' for more information."
|
|
142
|
+
exit 1
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
options
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
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,136 @@
|
|
|
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
|
+
|
|
15
|
+
OptionParser.new do |opts|
|
|
16
|
+
opts.banner = 'Usage: prg twirl [options]'
|
|
17
|
+
opts.separator ''
|
|
18
|
+
opts.separator 'Animation Options:'
|
|
19
|
+
|
|
20
|
+
opts.on('-s', '--speed SPEED', 'Animation speed (1-10, fast/medium/slow, or f/m/s)') do |speed|
|
|
21
|
+
options[:speed] = speed
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
opts.on('-m', '--message MESSAGE', 'Message to display before spinner') do |message|
|
|
25
|
+
options[:message] = message
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
opts.on('--style STYLE', 'Spinner style (see --show-styles for options)') do |style|
|
|
29
|
+
options[:style] = style
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
opts.on('--ends CHARS', 'Start/end characters (even number of chars, split in half)') do |chars|
|
|
33
|
+
options[:ends] = chars
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
opts.separator ''
|
|
37
|
+
opts.separator 'Command Execution:'
|
|
38
|
+
|
|
39
|
+
opts.on('-c', '--command COMMAND', 'Command to run (optional - runs indefinitely without)') do |command|
|
|
40
|
+
options[:command] = command
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
opts.on('--success MESSAGE', 'Success message to display') do |text|
|
|
44
|
+
options[:success] = text
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
opts.on('--error MESSAGE', 'Error message to display') do |text|
|
|
48
|
+
options[:error] = text
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
opts.on('--checkmark', 'Show checkmarks (✅ success, 🛑 failure)') do
|
|
52
|
+
options[:checkmark] = true
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
opts.on('--stdout', 'Output captured command result to STDOUT') do
|
|
56
|
+
options[:stdout] = true
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
opts.separator ''
|
|
60
|
+
opts.separator 'Daemon Mode:'
|
|
61
|
+
|
|
62
|
+
opts.on('--daemon', 'Run in background daemon mode') do
|
|
63
|
+
options[:daemon] = true
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
opts.on('--daemon-as NAME', 'Run in daemon mode with custom name (creates /tmp/ruby-progress/NAME.pid)') do |name|
|
|
67
|
+
options[:daemon] = true
|
|
68
|
+
options[:daemon_name] = name
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
opts.on('--pid-file FILE', 'Write process ID to file (default: /tmp/ruby-progress/progress.pid)') do |file|
|
|
72
|
+
options[:pid_file] = file
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
opts.on('--stop', 'Stop daemon (uses default PID file unless --pid-file specified)') do
|
|
76
|
+
options[:stop] = true
|
|
77
|
+
end
|
|
78
|
+
opts.on('--stop-id NAME', 'Stop daemon by name (automatically implies --stop)') do |name|
|
|
79
|
+
options[:stop] = true
|
|
80
|
+
options[:stop_name] = name
|
|
81
|
+
end
|
|
82
|
+
opts.on('--status', 'Show daemon status (uses default PID file unless --pid-file specified)') do
|
|
83
|
+
options[:status] = true
|
|
84
|
+
end
|
|
85
|
+
opts.on('--status-id NAME', 'Show daemon status by name') do |name|
|
|
86
|
+
options[:status] = true
|
|
87
|
+
options[:status_name] = name
|
|
88
|
+
end
|
|
89
|
+
opts.on('--stop-success MESSAGE', 'Stop daemon with success message (automatically implies --stop)') do |msg|
|
|
90
|
+
options[:stop] = true
|
|
91
|
+
options[:stop_success] = msg
|
|
92
|
+
end
|
|
93
|
+
opts.on('--stop-error MESSAGE', 'Stop daemon with error message (automatically implies --stop)') do |msg|
|
|
94
|
+
options[:stop] = true
|
|
95
|
+
options[:stop_error] = msg
|
|
96
|
+
end
|
|
97
|
+
opts.on('--stop-checkmark', 'When stopping, include a success checkmark') { options[:stop_checkmark] = true }
|
|
98
|
+
|
|
99
|
+
opts.separator ''
|
|
100
|
+
opts.separator 'Daemon notes:'
|
|
101
|
+
opts.separator ' - Do not append &; prg detaches itself and returns immediately.'
|
|
102
|
+
opts.separator ' - Use --daemon-as NAME for named daemons, or --stop-id/--status-id for named control.'
|
|
103
|
+
|
|
104
|
+
opts.separator ''
|
|
105
|
+
opts.separator 'General:'
|
|
106
|
+
|
|
107
|
+
opts.on('--show-styles', 'Show available twirl styles with visual previews') do
|
|
108
|
+
PrgCLI.show_twirl_styles
|
|
109
|
+
exit
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
opts.on('--stop-all', 'Stop all prg twirl processes') do
|
|
113
|
+
success = PrgCLI.stop_subcommand_processes('twirl')
|
|
114
|
+
exit(success ? 0 : 1)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
opts.on('-v', '--version', 'Show version') do
|
|
118
|
+
puts "Twirl version #{RubyProgress::VERSION}"
|
|
119
|
+
exit
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
opts.on('-h', '--help', 'Show this help') do
|
|
123
|
+
puts opts
|
|
124
|
+
exit
|
|
125
|
+
end
|
|
126
|
+
end.parse!
|
|
127
|
+
options
|
|
128
|
+
rescue OptionParser::InvalidOption => e
|
|
129
|
+
puts "Invalid option: #{e.args.first}"
|
|
130
|
+
puts ''
|
|
131
|
+
puts 'Usage: prg twirl [options]'
|
|
132
|
+
puts "Run 'prg twirl --help' for more information."
|
|
133
|
+
exit 1
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'json'
|
|
5
|
+
require_relative 'twirl_spinner'
|
|
6
|
+
|
|
7
|
+
# Top-level runtime helper module for the Twirl CLI.
|
|
8
|
+
#
|
|
9
|
+
# Contains helper methods used by the `TwirlCLI` dispatcher. These
|
|
10
|
+
# methods implement the runtime behavior (running a command, running
|
|
11
|
+
# indefinitely, or launching in daemon mode) and were extracted to
|
|
12
|
+
# reduce module size and improve testability.
|
|
13
|
+
module TwirlRunner
|
|
14
|
+
def self.run_with_command(options)
|
|
15
|
+
message = options[:message]
|
|
16
|
+
captured_output = nil
|
|
17
|
+
|
|
18
|
+
spinner = TwirlSpinner.new(message, options)
|
|
19
|
+
success = false
|
|
20
|
+
|
|
21
|
+
begin
|
|
22
|
+
RubyProgress::Utils.hide_cursor
|
|
23
|
+
spinner_thread = Thread.new { loop { spinner.animate } }
|
|
24
|
+
|
|
25
|
+
captured_output = `#{options[:command]} 2>&1`
|
|
26
|
+
success = $CHILD_STATUS.success?
|
|
27
|
+
|
|
28
|
+
spinner_thread.kill
|
|
29
|
+
RubyProgress::Utils.clear_line
|
|
30
|
+
ensure
|
|
31
|
+
RubyProgress::Utils.show_cursor
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
puts captured_output if options[:stdout]
|
|
35
|
+
|
|
36
|
+
if options[:success] || options[:error] || options[:checkmark]
|
|
37
|
+
final_msg = success ? options[:success] : options[:error]
|
|
38
|
+
final_msg ||= success ? 'Success' : 'Failed'
|
|
39
|
+
|
|
40
|
+
RubyProgress::Utils.display_completion(
|
|
41
|
+
final_msg,
|
|
42
|
+
success: success,
|
|
43
|
+
show_checkmark: options[:checkmark]
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
exit success ? 0 : 1
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.run_indefinitely(options)
|
|
51
|
+
message = options[:message]
|
|
52
|
+
spinner = TwirlSpinner.new(message, options)
|
|
53
|
+
|
|
54
|
+
begin
|
|
55
|
+
RubyProgress::Utils.hide_cursor
|
|
56
|
+
loop { spinner.animate }
|
|
57
|
+
ensure
|
|
58
|
+
RubyProgress::Utils.show_cursor
|
|
59
|
+
if options[:success] || options[:checkmark]
|
|
60
|
+
RubyProgress::Utils.display_completion(
|
|
61
|
+
options[:success] || 'Complete',
|
|
62
|
+
success: true,
|
|
63
|
+
show_checkmark: options[:checkmark]
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.run_daemon_mode(options)
|
|
70
|
+
pid_file = resolve_pid_file(options, :daemon_name)
|
|
71
|
+
FileUtils.mkdir_p(File.dirname(pid_file))
|
|
72
|
+
File.write(pid_file, Process.pid.to_s)
|
|
73
|
+
|
|
74
|
+
message = options[:message]
|
|
75
|
+
spinner = TwirlSpinner.new(message, options)
|
|
76
|
+
stop_requested = false
|
|
77
|
+
|
|
78
|
+
Signal.trap('INT') { stop_requested = true }
|
|
79
|
+
Signal.trap('USR1') { stop_requested = true }
|
|
80
|
+
Signal.trap('TERM') { stop_requested = true }
|
|
81
|
+
Signal.trap('HUP') { stop_requested = true }
|
|
82
|
+
|
|
83
|
+
begin
|
|
84
|
+
RubyProgress::Utils.hide_cursor
|
|
85
|
+
spinner.animate until stop_requested
|
|
86
|
+
ensure
|
|
87
|
+
RubyProgress::Utils.clear_line
|
|
88
|
+
RubyProgress::Utils.show_cursor
|
|
89
|
+
|
|
90
|
+
# Check for control message
|
|
91
|
+
cmf = RubyProgress::Daemon.control_message_file(pid_file)
|
|
92
|
+
if File.exist?(cmf)
|
|
93
|
+
begin
|
|
94
|
+
data = JSON.parse(File.read(cmf))
|
|
95
|
+
message = data['message']
|
|
96
|
+
check = data.key?('checkmark') ? data['checkmark'] : false
|
|
97
|
+
success_val = data.key?('success') ? data['success'] : true
|
|
98
|
+
if message
|
|
99
|
+
RubyProgress::Utils.display_completion(
|
|
100
|
+
message,
|
|
101
|
+
success: success_val,
|
|
102
|
+
show_checkmark: check,
|
|
103
|
+
output_stream: :stdout
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
rescue StandardError
|
|
107
|
+
# ignore
|
|
108
|
+
ensure
|
|
109
|
+
begin
|
|
110
|
+
File.delete(cmf)
|
|
111
|
+
rescue StandardError
|
|
112
|
+
nil
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
FileUtils.rm_f(pid_file)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def self.resolve_pid_file(options, name_key)
|
|
122
|
+
return options[:pid_file] if options[:pid_file]
|
|
123
|
+
|
|
124
|
+
if options[name_key]
|
|
125
|
+
"/tmp/ruby-progress/#{options[name_key]}.pid"
|
|
126
|
+
else
|
|
127
|
+
RubyProgress::Daemon.default_pid_file
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../utils'
|
|
4
|
+
|
|
5
|
+
# Minimal spinner implementation used by the Twirl CLI
|
|
6
|
+
#
|
|
7
|
+
# This small class handles animation frame selection and printing
|
|
8
|
+
# for the Twirl command. It was extracted to keep runtime logic
|
|
9
|
+
# out of the CLI dispatcher module.
|
|
10
|
+
class TwirlSpinner
|
|
11
|
+
def initialize(message, options = {})
|
|
12
|
+
@message = message
|
|
13
|
+
@style = parse_style(options[:style] || 'dots')
|
|
14
|
+
@speed = parse_speed(options[:speed] || 'medium')
|
|
15
|
+
@frames = RubyProgress::INDICATORS[@style] || RubyProgress::INDICATORS[:dots]
|
|
16
|
+
@start_chars, @end_chars = RubyProgress::Utils.parse_ends(options[:ends])
|
|
17
|
+
@index = 0
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def animate
|
|
21
|
+
if @message && !@message.empty?
|
|
22
|
+
$stderr.print "\r\e[2K#{@start_chars}#{@message} #{@frames[@index]}#{@end_chars}"
|
|
23
|
+
else
|
|
24
|
+
$stderr.print "\r\e[2K#{@start_chars}#{@frames[@index]}#{@end_chars}"
|
|
25
|
+
end
|
|
26
|
+
$stderr.flush
|
|
27
|
+
@index = (@index + 1) % @frames.length
|
|
28
|
+
sleep @speed
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def parse_style(style_input)
|
|
34
|
+
return :dots unless style_input && !style_input.to_s.strip.empty?
|
|
35
|
+
|
|
36
|
+
style_lower = style_input.to_s.downcase.strip
|
|
37
|
+
|
|
38
|
+
indicator_keys = RubyProgress::INDICATORS.keys.map(&:to_s)
|
|
39
|
+
return style_lower.to_sym if indicator_keys.include?(style_lower)
|
|
40
|
+
|
|
41
|
+
prefix_matches = indicator_keys.select { |key| key.downcase.start_with?(style_lower) }
|
|
42
|
+
return prefix_matches.min_by(&:length).to_sym unless prefix_matches.empty?
|
|
43
|
+
|
|
44
|
+
fuzzy_matches = indicator_keys.select do |key|
|
|
45
|
+
key_chars = key.downcase.chars
|
|
46
|
+
input_chars = style_lower.chars
|
|
47
|
+
input_chars.all? do |char|
|
|
48
|
+
idx = key_chars.index(char)
|
|
49
|
+
if idx
|
|
50
|
+
key_chars = key_chars[idx + 1..-1]
|
|
51
|
+
true
|
|
52
|
+
else
|
|
53
|
+
false
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
return fuzzy_matches.min_by(&:length).to_sym unless fuzzy_matches.empty?
|
|
59
|
+
|
|
60
|
+
substring_matches = indicator_keys.select { |key| key.downcase.include?(style_lower) }
|
|
61
|
+
return substring_matches.min_by(&:length).to_sym unless substring_matches.empty?
|
|
62
|
+
|
|
63
|
+
:dots
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def parse_speed(speed)
|
|
67
|
+
case speed.to_s.downcase
|
|
68
|
+
when /^f/, '1', '2', '3'
|
|
69
|
+
0.05
|
|
70
|
+
when /^m/, '4', '5', '6', '7'
|
|
71
|
+
0.1
|
|
72
|
+
when /^s/, '8', '9', '10'
|
|
73
|
+
0.2
|
|
74
|
+
else
|
|
75
|
+
speed.to_f.positive? ? (1.0 / speed.to_f) : 0.1
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|