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.
@@ -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