ruby-progress 1.0.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 +7 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +39 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +74 -0
- data/LICENSE +21 -0
- data/README.md +353 -0
- data/Rakefile +84 -0
- data/bin/prg +311 -0
- data/bin/ripple +147 -0
- data/bin/worm +80 -0
- data/demo_gem.rb +56 -0
- data/demo_worm_infinite.rb +21 -0
- data/examples/utils_demo.rb +52 -0
- data/lib/ruby-progress/ripple.rb +265 -0
- data/lib/ruby-progress/utils.rb +53 -0
- data/lib/ruby-progress/version.rb +5 -0
- data/lib/ruby-progress/worm.rb +253 -0
- data/lib/ruby-progress.rb +11 -0
- data/ruby-progress.gemspec +39 -0
- data/test_worm_flags.rb +24 -0
- metadata +123 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyProgress
|
|
4
|
+
# Color definitions for terminal output
|
|
5
|
+
COLORS = {
|
|
6
|
+
'red' => "\e[31m",
|
|
7
|
+
'green' => "\e[32m",
|
|
8
|
+
'yellow' => "\e[33m",
|
|
9
|
+
'blue' => "\e[34m",
|
|
10
|
+
'magenta' => "\e[35m",
|
|
11
|
+
'cyan' => "\e[36m",
|
|
12
|
+
'white' => "\e[37m",
|
|
13
|
+
'dark_red' => "\e[31;1m",
|
|
14
|
+
'dark_green' => "\e[32;1m",
|
|
15
|
+
'dark_yellow' => "\e[33;1m",
|
|
16
|
+
'dark_blue' => "\e[34;1m",
|
|
17
|
+
'dark_magenta' => "\e[35;1m",
|
|
18
|
+
'dark_cyan' => "\e[36;1m",
|
|
19
|
+
'dark_white' => "\e[37;1m",
|
|
20
|
+
'light_red' => "\e[31;2m",
|
|
21
|
+
'light_green' => "\e[32;2m",
|
|
22
|
+
'light_yellow' => "\e[33;2m",
|
|
23
|
+
'light_blue' => "\e[34;2m",
|
|
24
|
+
'light_magenta' => "\e[35;2m",
|
|
25
|
+
'light_cyan' => "\e[36;2m",
|
|
26
|
+
'light_white' => "\e[37;2m",
|
|
27
|
+
'reset' => "\e[0m"
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
# Spinner indicator definitions
|
|
31
|
+
INDICATORS = {
|
|
32
|
+
arc: %w[◜ ◠ ◝ ◞ ◡ ◟],
|
|
33
|
+
arrow: %w[← ↖ ↑ ↗ → ↘ ↓ ↙],
|
|
34
|
+
arrow_pulse: [
|
|
35
|
+
'▹▹▹▹▹',
|
|
36
|
+
'▸▹▹▹▹',
|
|
37
|
+
'▹▸▹▹▹',
|
|
38
|
+
'▹▹▸▹▹',
|
|
39
|
+
'▹▹▹▸▹',
|
|
40
|
+
'▹▹▹▹▸'
|
|
41
|
+
],
|
|
42
|
+
balloon: %w[. o O ° O o .],
|
|
43
|
+
block_1: %w[▖▖▖ ▘▖▖ ▖▘▖ ▖▖▘],
|
|
44
|
+
block_2: %w[▌ ▀ ▐ ▄],
|
|
45
|
+
bounce: [
|
|
46
|
+
'[ ]',
|
|
47
|
+
'[= ]',
|
|
48
|
+
'[== ]',
|
|
49
|
+
'[=== ]',
|
|
50
|
+
'[====]'
|
|
51
|
+
],
|
|
52
|
+
circle: %w[○○○ ●○○ ○●○ ○○●],
|
|
53
|
+
classic: ['|', '/', '—', '\\'],
|
|
54
|
+
dots: %w[⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏],
|
|
55
|
+
dots_2: %w[⣾ ⣽ ⣻ ⢿ ⡿ ⣟ ⣯ ⣷],
|
|
56
|
+
dots_3: %w[⠋ ⠙ ⠚ ⠞ ⠖ ⠦ ⠴ ⠲ ⠳ ⠓],
|
|
57
|
+
dots_4: %w[⠄ ⠆ ⠇ ⠋ ⠙ ⠸ ⠰ ⠠ ⠰ ⠸ ⠙ ⠋ ⠇ ⠆],
|
|
58
|
+
dots_5: %w[⠋ ⠙ ⠚ ⠒ ⠂ ⠂ ⠒ ⠲ ⠴ ⠦ ⠖ ⠒ ⠐ ⠐ ⠒ ⠓ ⠋],
|
|
59
|
+
dots_6: %w[⠁ ⠉ ⠙ ⠚ ⠒ ⠂ ⠂ ⠒ ⠲ ⠴ ⠤ ⠄ ⠄ ⠤ ⠴ ⠲ ⠒ ⠂ ⠂ ⠒ ⠚ ⠙ ⠉ ⠁],
|
|
60
|
+
dots_7: %w[⠈ ⠉ ⠋ ⠓ ⠒ ⠐ ⠐ ⠒ ⠖ ⠦ ⠤ ⠠ ⠠ ⠤ ⠦ ⠖ ⠒ ⠐ ⠐ ⠒ ⠓ ⠋ ⠉ ⠈],
|
|
61
|
+
dots_8: %w[⠁ ⠁ ⠉ ⠙ ⠚ ⠒ ⠂ ⠂ ⠒ ⠲ ⠴ ⠤ ⠄ ⠄ ⠤ ⠠ ⠠ ⠤ ⠦ ⠖ ⠒ ⠐ ⠐ ⠒ ⠓ ⠋ ⠉ ⠈ ⠈],
|
|
62
|
+
dots_9: %w[⢹ ⢺ ⢼ ⣸ ⣇ ⡧ ⡗ ⡏],
|
|
63
|
+
dots_10: %w[⢄ ⢂ ⢁ ⡁ ⡈ ⡐ ⡠],
|
|
64
|
+
dots_11: %w[⠁ ⠂ ⠄ ⡀ ⢀ ⠠ ⠐ ⠈],
|
|
65
|
+
ellipsis: ['. ', '.. ', '... ', '....'],
|
|
66
|
+
lighthouse: ['∙∙∙', '●∙∙', '∙●∙', '∙∙●'],
|
|
67
|
+
o: %w[Ooo oOo ooO],
|
|
68
|
+
pipe: %w[┤ ┘ ┴ └ ├ ┌ ┬ ┐],
|
|
69
|
+
pulse: %w[⎺ ⎻ ⎼ ⎽ ⎼ ⎻],
|
|
70
|
+
pulse_2: %w[▁ ▃ ▅ ▆ ▇ █ ▇ ▆ ▅ ▃],
|
|
71
|
+
pulse_3: %w[▉ ▊ ▋ ▌ ▍ ▎ ▏ ▎ ▍ ▌ ▋ ▊ ▉],
|
|
72
|
+
pulse_4: %w[- = ≡ = -],
|
|
73
|
+
push: [
|
|
74
|
+
'[> ]',
|
|
75
|
+
'[=> ]',
|
|
76
|
+
'[==> ]',
|
|
77
|
+
'[===> ]',
|
|
78
|
+
'[====>]'
|
|
79
|
+
],
|
|
80
|
+
spin: %w[◴ ◷ ◶ ◵],
|
|
81
|
+
spin_2: %w[◐ ◓ ◑ ◒],
|
|
82
|
+
spin_3: %w[◰ ◳ ◲ ◱],
|
|
83
|
+
toggle: %w[■ □ ▪ ▫],
|
|
84
|
+
triangle: %w[◢ ◣ ◤ ◥]
|
|
85
|
+
}.freeze
|
|
86
|
+
|
|
87
|
+
# String extensions for color support
|
|
88
|
+
module StringExtensions
|
|
89
|
+
COLORS.each do |color_name, color_code|
|
|
90
|
+
define_method(color_name) do
|
|
91
|
+
"#{color_code}#{self}#{COLORS['reset']}"
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def rainbow(index = 0)
|
|
96
|
+
chars = split('')
|
|
97
|
+
colored_chars = chars.map.with_index do |char, idx|
|
|
98
|
+
color = COLORS.values[(idx + index) % COLORS.size]
|
|
99
|
+
"#{color}#{char}#{COLORS['reset']}"
|
|
100
|
+
end
|
|
101
|
+
colored_chars.join
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def normalize_type
|
|
105
|
+
spinner_type = :classic
|
|
106
|
+
INDICATORS.each do |spinner, _v|
|
|
107
|
+
spinner_type = spinner if spinner =~ /^#{split('').join('.*?')}/i
|
|
108
|
+
end
|
|
109
|
+
spinner_type
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Text ripple animation class
|
|
114
|
+
class Ripple
|
|
115
|
+
attr_accessor :index, :string, :speed, :format, :inverse, :rainbow, :spinner, :spinner_position, :caps
|
|
116
|
+
|
|
117
|
+
def initialize(string, options = {})
|
|
118
|
+
defaults = {
|
|
119
|
+
speed: :medium,
|
|
120
|
+
format: :bidirectional,
|
|
121
|
+
rainbow: false,
|
|
122
|
+
spinner: false,
|
|
123
|
+
spinner_position: false,
|
|
124
|
+
caps: false,
|
|
125
|
+
inverse: false,
|
|
126
|
+
output: :error
|
|
127
|
+
}
|
|
128
|
+
@options = defaults.merge(options)
|
|
129
|
+
@string = string
|
|
130
|
+
@index = 0
|
|
131
|
+
@direction = :forward
|
|
132
|
+
@rainbow = @options[:rainbow]
|
|
133
|
+
@spinner = @options[:spinner]
|
|
134
|
+
@spinner_position = @options[:spinner_position]
|
|
135
|
+
@caps = @options[:caps]
|
|
136
|
+
@inverse = @options[:inverse]
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def printout
|
|
140
|
+
letters = @string.dup.split('')
|
|
141
|
+
i = @index
|
|
142
|
+
if @spinner
|
|
143
|
+
case @spinner_position
|
|
144
|
+
when :before
|
|
145
|
+
pre = "#{INDICATORS[@spinner][i]} "
|
|
146
|
+
post = @string
|
|
147
|
+
else
|
|
148
|
+
pre = "#{@string} "
|
|
149
|
+
post = INDICATORS[@spinner][i]
|
|
150
|
+
end
|
|
151
|
+
elsif @caps
|
|
152
|
+
pre = letters.slice!(0, i).join
|
|
153
|
+
char = letters.slice!(0, 2).join
|
|
154
|
+
post = letters.slice!(0, letters.length).join
|
|
155
|
+
pre = @inverse ? pre.upcase : pre.downcase
|
|
156
|
+
char = @inverse ? char.downcase : char.upcase
|
|
157
|
+
post = @inverse ? post.upcase : post.downcase
|
|
158
|
+
elsif @inverse
|
|
159
|
+
pre = letters.slice!(0, i).join
|
|
160
|
+
pre = @rainbow ? pre.rainbow : pre.extend(StringExtensions).light_white
|
|
161
|
+
char = letters.slice!(0, 2).join
|
|
162
|
+
char = char.extend(StringExtensions).dark_white
|
|
163
|
+
post = letters.slice!(0, letters.length).join
|
|
164
|
+
post = @rainbow ? post.rainbow : post.extend(StringExtensions).light_white
|
|
165
|
+
else
|
|
166
|
+
pre = letters.slice!(0, i).join.extend(StringExtensions).dark_white
|
|
167
|
+
char = letters.slice!(0, 2).join
|
|
168
|
+
char = @rainbow ? char.rainbow(i) : char.extend(StringExtensions).light_white
|
|
169
|
+
post = letters.slice!(0, letters.length).join.extend(StringExtensions).dark_white
|
|
170
|
+
end
|
|
171
|
+
$stderr.print "\e[2K#{pre}#{char}#{post}\r"
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Hide or show the cursor (delegated to Utils)
|
|
175
|
+
def self.hide_cursor
|
|
176
|
+
RubyProgress::Utils.hide_cursor
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def self.show_cursor
|
|
180
|
+
RubyProgress::Utils.show_cursor
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def self.complete(string, message, checkmark, success)
|
|
184
|
+
display_message = message || (checkmark ? string : nil)
|
|
185
|
+
return unless display_message
|
|
186
|
+
|
|
187
|
+
RubyProgress::Utils.display_completion(
|
|
188
|
+
display_message,
|
|
189
|
+
success: success,
|
|
190
|
+
show_checkmark: checkmark,
|
|
191
|
+
output_stream: :warn
|
|
192
|
+
)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def advance
|
|
196
|
+
max = @spinner ? (INDICATORS[@spinner].count - 1) : (@string.length - 1)
|
|
197
|
+
advance = true
|
|
198
|
+
|
|
199
|
+
if @index == max && @options[:format] != :forward_only
|
|
200
|
+
@direction = :backward
|
|
201
|
+
elsif @index == max && @options[:format] == :forward_only
|
|
202
|
+
@index = 0
|
|
203
|
+
advance = false
|
|
204
|
+
elsif @index == 0
|
|
205
|
+
@direction = :forward
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
if advance
|
|
209
|
+
@index = @direction == :backward ? @index - 1 : @index + 1
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
printout
|
|
213
|
+
|
|
214
|
+
case @options[:speed]
|
|
215
|
+
when :fast
|
|
216
|
+
sleep 0.05
|
|
217
|
+
when :medium
|
|
218
|
+
sleep 0.1
|
|
219
|
+
else
|
|
220
|
+
sleep 0.2
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def self.progress(string, options = {})
|
|
225
|
+
Signal.trap('INT') do
|
|
226
|
+
Thread.current.kill
|
|
227
|
+
nil
|
|
228
|
+
end
|
|
229
|
+
defaults = { speed: :medium,
|
|
230
|
+
format: :bidirectional,
|
|
231
|
+
rainbow: false,
|
|
232
|
+
inverse: false,
|
|
233
|
+
output: :error }
|
|
234
|
+
options = defaults.merge(options)
|
|
235
|
+
|
|
236
|
+
rippler = new(string, options)
|
|
237
|
+
Ripple.hide_cursor
|
|
238
|
+
begin
|
|
239
|
+
thread = Thread.new do
|
|
240
|
+
rippler.advance while true
|
|
241
|
+
end
|
|
242
|
+
result = yield if block_given?
|
|
243
|
+
thread.kill
|
|
244
|
+
|
|
245
|
+
if @options[:output] == :error
|
|
246
|
+
$?.exitstatus.zero?
|
|
247
|
+
elsif @options[:output] == :stdout
|
|
248
|
+
result
|
|
249
|
+
else
|
|
250
|
+
nil
|
|
251
|
+
end
|
|
252
|
+
rescue StandardError
|
|
253
|
+
thread&.kill
|
|
254
|
+
nil
|
|
255
|
+
ensure
|
|
256
|
+
Ripple.show_cursor
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Extend String class with color methods
|
|
263
|
+
class String
|
|
264
|
+
include RubyProgress::StringExtensions
|
|
265
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyProgress
|
|
4
|
+
# Universal terminal utilities shared between progress indicators
|
|
5
|
+
module Utils
|
|
6
|
+
# Terminal cursor control
|
|
7
|
+
def self.hide_cursor
|
|
8
|
+
$stderr.print "\e[?25l"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.show_cursor
|
|
12
|
+
$stderr.print "\e[?25h"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.clear_line
|
|
16
|
+
print "\r\e[K"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Universal completion message display
|
|
20
|
+
# @param message [String] The message to display
|
|
21
|
+
# @param success [Boolean] Whether this represents success or failure
|
|
22
|
+
# @param show_checkmark [Boolean] Whether to show checkmark/X symbols
|
|
23
|
+
# @param output_stream [Symbol] Where to output (:stdout, :stderr, :warn)
|
|
24
|
+
def self.display_completion(message, success: true, show_checkmark: false, output_stream: :warn)
|
|
25
|
+
return unless message
|
|
26
|
+
|
|
27
|
+
mark = ''
|
|
28
|
+
if show_checkmark
|
|
29
|
+
mark = success ? '✅ ' : '🛑 '
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
formatted_message = "#{mark}#{message}"
|
|
33
|
+
|
|
34
|
+
case output_stream
|
|
35
|
+
when :stdout
|
|
36
|
+
puts formatted_message
|
|
37
|
+
when :stderr
|
|
38
|
+
$stderr.puts formatted_message
|
|
39
|
+
when :warn
|
|
40
|
+
warn "\e[2K#{formatted_message}"
|
|
41
|
+
else
|
|
42
|
+
warn "\e[2K#{formatted_message}"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Clear current line and display completion message
|
|
47
|
+
# Convenience method that combines line clearing with message display
|
|
48
|
+
def self.complete_with_clear(message, success: true, show_checkmark: false, output_stream: :warn)
|
|
49
|
+
clear_line if output_stream != :warn # warn already includes clear in display_completion
|
|
50
|
+
display_completion(message, success: success, show_checkmark: show_checkmark, output_stream: output_stream)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'optparse'
|
|
4
|
+
require 'open3'
|
|
5
|
+
|
|
6
|
+
module RubyProgress
|
|
7
|
+
# Animated progress indicator with ripple effect using Unicode combining characters
|
|
8
|
+
class Worm
|
|
9
|
+
# Ripple effect styles
|
|
10
|
+
RIPPLE_STYLES = {
|
|
11
|
+
'circles' => {
|
|
12
|
+
baseline: '·', # middle dot
|
|
13
|
+
midline: '●', # black circle
|
|
14
|
+
peak: '⬤' # large circle
|
|
15
|
+
},
|
|
16
|
+
'blocks' => {
|
|
17
|
+
baseline: '▁', # lower eighth block
|
|
18
|
+
midline: '▄', # lower half block
|
|
19
|
+
peak: '█' # full block
|
|
20
|
+
},
|
|
21
|
+
'geometric' => {
|
|
22
|
+
baseline: '▪', # small black square
|
|
23
|
+
midline: '▫', # small white square
|
|
24
|
+
peak: '■' # large black square
|
|
25
|
+
}
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
# Speed mappings
|
|
29
|
+
SPEED_MAP = {
|
|
30
|
+
'slow' => 0.5,
|
|
31
|
+
'medium' => 0.2,
|
|
32
|
+
'fast' => 0.1
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
def initialize(options = {})
|
|
36
|
+
@length = options[:length] || 3
|
|
37
|
+
@message = options[:message] || 'Processing'
|
|
38
|
+
@speed = parse_speed(options[:speed] || 'medium')
|
|
39
|
+
@style = parse_style(options[:style] || 'circles')
|
|
40
|
+
@command = options[:command]
|
|
41
|
+
@success_text = options[:success]
|
|
42
|
+
@error_text = options[:error]
|
|
43
|
+
@show_checkmark = options[:checkmark] || false
|
|
44
|
+
@output_stdout = options[:stdout] || false
|
|
45
|
+
@running = false
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def animate(message: nil, success: nil, error: nil, &block)
|
|
49
|
+
@message = message if message
|
|
50
|
+
@success_text = success if success
|
|
51
|
+
@error_text = error if error
|
|
52
|
+
@running = true
|
|
53
|
+
|
|
54
|
+
# Set up interrupt handler to ensure cursor is restored
|
|
55
|
+
original_int_handler = Signal.trap('INT') do
|
|
56
|
+
@running = false
|
|
57
|
+
RubyProgress::Utils.clear_line
|
|
58
|
+
RubyProgress::Utils.show_cursor
|
|
59
|
+
puts "\nInterrupted!"
|
|
60
|
+
exit 130
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Hide cursor
|
|
64
|
+
RubyProgress::Utils.hide_cursor
|
|
65
|
+
|
|
66
|
+
animation_thread = Thread.new { animation_loop }
|
|
67
|
+
|
|
68
|
+
begin
|
|
69
|
+
if block_given?
|
|
70
|
+
result = yield
|
|
71
|
+
@running = false
|
|
72
|
+
animation_thread.join
|
|
73
|
+
display_completion_message(@success_text || 'Done!', true)
|
|
74
|
+
result
|
|
75
|
+
else
|
|
76
|
+
animation_thread.join
|
|
77
|
+
end
|
|
78
|
+
rescue StandardError => e
|
|
79
|
+
@running = false
|
|
80
|
+
animation_thread.join
|
|
81
|
+
display_completion_message(@error_text || "Error: #{e.message}", false)
|
|
82
|
+
nil # Return nil instead of re-raising when used as a progress indicator
|
|
83
|
+
ensure
|
|
84
|
+
# Always restore cursor and signal handler
|
|
85
|
+
RubyProgress::Utils.show_cursor
|
|
86
|
+
Signal.trap('INT', original_int_handler) if original_int_handler
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def run_with_command
|
|
91
|
+
return unless @command
|
|
92
|
+
|
|
93
|
+
exit_code = 0
|
|
94
|
+
stdout_content = nil
|
|
95
|
+
|
|
96
|
+
begin
|
|
97
|
+
stdout_content = animate do
|
|
98
|
+
# Use popen3 instead of capture3 for better signal handling
|
|
99
|
+
Open3.popen3(@command) do |stdin, stdout, stderr, wait_thr|
|
|
100
|
+
stdin.close
|
|
101
|
+
captured_stdout = stdout.read
|
|
102
|
+
stderr_content = stderr.read
|
|
103
|
+
exit_code = wait_thr.value.exitstatus
|
|
104
|
+
|
|
105
|
+
unless wait_thr.value.success?
|
|
106
|
+
error_msg = @error_text || "Command failed with exit code #{exit_code}"
|
|
107
|
+
error_msg += ": #{stderr_content.strip}" if stderr_content && !stderr_content.empty?
|
|
108
|
+
raise StandardError, error_msg
|
|
109
|
+
end
|
|
110
|
+
captured_stdout
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Output to stdout if --stdout flag is set
|
|
115
|
+
puts stdout_content if @output_stdout && stdout_content
|
|
116
|
+
|
|
117
|
+
rescue StandardError => e
|
|
118
|
+
# animate method handles error display, just exit with proper code
|
|
119
|
+
exit exit_code.nonzero? || 1
|
|
120
|
+
rescue Interrupt
|
|
121
|
+
puts "\nInterrupted!"
|
|
122
|
+
exit 130
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def run_indefinitely
|
|
127
|
+
# Set up interrupt handler to ensure cursor is restored
|
|
128
|
+
original_int_handler = Signal.trap('INT') do
|
|
129
|
+
@running = false
|
|
130
|
+
RubyProgress::Utils.clear_line
|
|
131
|
+
RubyProgress::Utils.show_cursor
|
|
132
|
+
puts "\nInterrupted!"
|
|
133
|
+
exit 130
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
@running = true
|
|
137
|
+
RubyProgress::Utils.hide_cursor
|
|
138
|
+
|
|
139
|
+
begin
|
|
140
|
+
animation_loop
|
|
141
|
+
ensure
|
|
142
|
+
RubyProgress::Utils.show_cursor
|
|
143
|
+
Signal.trap('INT', original_int_handler) if original_int_handler
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def stop
|
|
148
|
+
@running = false
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
private
|
|
152
|
+
|
|
153
|
+
def display_completion_message(message, success)
|
|
154
|
+
return unless message
|
|
155
|
+
|
|
156
|
+
mark = ''
|
|
157
|
+
if @show_checkmark
|
|
158
|
+
mark = success ? '✅ ' : '🛑 '
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
puts "#{mark}#{message}"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def parse_speed(speed_input)
|
|
165
|
+
case speed_input
|
|
166
|
+
when String
|
|
167
|
+
if speed_input.match?(/^\d+$/)
|
|
168
|
+
# Numeric string (1-10)
|
|
169
|
+
speed_num = speed_input.to_i
|
|
170
|
+
return 0.6 - (speed_num - 1) * 0.05 if speed_num.between?(1, 10)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Check for abbreviated forms
|
|
174
|
+
speed_lower = speed_input.downcase
|
|
175
|
+
if speed_lower.start_with?('f')
|
|
176
|
+
SPEED_MAP['fast']
|
|
177
|
+
elsif speed_lower.start_with?('m')
|
|
178
|
+
SPEED_MAP['medium']
|
|
179
|
+
elsif speed_lower.start_with?('s')
|
|
180
|
+
SPEED_MAP['slow']
|
|
181
|
+
else
|
|
182
|
+
SPEED_MAP['medium']
|
|
183
|
+
end
|
|
184
|
+
when Numeric
|
|
185
|
+
speed_num = speed_input.to_i
|
|
186
|
+
speed_num.between?(1, 10) ? 0.6 - (speed_num - 1) * 0.05 : SPEED_MAP['medium']
|
|
187
|
+
else
|
|
188
|
+
SPEED_MAP['medium']
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def parse_style(style_input)
|
|
193
|
+
return RIPPLE_STYLES['circles'] unless style_input
|
|
194
|
+
|
|
195
|
+
style_lower = style_input.to_s.downcase
|
|
196
|
+
if style_lower.start_with?('b')
|
|
197
|
+
RIPPLE_STYLES['blocks']
|
|
198
|
+
elsif style_lower.start_with?('g')
|
|
199
|
+
RIPPLE_STYLES['geometric']
|
|
200
|
+
elsif style_lower.start_with?('c')
|
|
201
|
+
RIPPLE_STYLES['circles']
|
|
202
|
+
else
|
|
203
|
+
RIPPLE_STYLES['circles'] # default
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def animation_loop
|
|
208
|
+
position = 0
|
|
209
|
+
direction = 1
|
|
210
|
+
|
|
211
|
+
while @running
|
|
212
|
+
print "\r#{@message}#{generate_dots(position, direction)}"
|
|
213
|
+
$stdout.flush
|
|
214
|
+
|
|
215
|
+
sleep @speed
|
|
216
|
+
|
|
217
|
+
position += direction
|
|
218
|
+
if position >= @length - 1
|
|
219
|
+
direction = -1
|
|
220
|
+
elsif position <= 0
|
|
221
|
+
direction = 1
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def generate_dots(ripple_position, direction)
|
|
227
|
+
dots = Array.new(@length) { @style[:baseline] }
|
|
228
|
+
|
|
229
|
+
# Apply ripple effect
|
|
230
|
+
(0...@length).each do |i|
|
|
231
|
+
distance = (i - ripple_position).abs
|
|
232
|
+
case distance
|
|
233
|
+
when 0
|
|
234
|
+
dots[i] = @style[:peak]
|
|
235
|
+
when 1
|
|
236
|
+
# When moving left, midline appears to the right of peak
|
|
237
|
+
# When moving right, midline appears to the left of peak
|
|
238
|
+
if direction == -1 # moving left
|
|
239
|
+
dots[i] = @style[:midline] if i > ripple_position
|
|
240
|
+
else # moving right
|
|
241
|
+
dots[i] = @style[:midline] if i < ripple_position
|
|
242
|
+
end
|
|
243
|
+
else
|
|
244
|
+
dots[i] = @style[:baseline]
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
dots.join
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Terminal utilities moved to RubyProgress::Utils
|
|
252
|
+
end
|
|
253
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'ruby-progress/version'
|
|
4
|
+
require_relative 'ruby-progress/utils'
|
|
5
|
+
require_relative 'ruby-progress/ripple'
|
|
6
|
+
require_relative 'ruby-progress/worm'
|
|
7
|
+
|
|
8
|
+
module RubyProgress
|
|
9
|
+
# Main module for Ruby Progress indicators
|
|
10
|
+
# Contains both Ripple and Worm classes for different types of progress animations
|
|
11
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/ruby-progress/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'ruby-progress'
|
|
7
|
+
spec.version = RubyProgress::VERSION
|
|
8
|
+
spec.authors = ['Brett Terpstra']
|
|
9
|
+
spec.email = ['me@brettterpstra.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'Animated terminal progress indicators'
|
|
12
|
+
spec.description = 'Two different animated progress indicators for Ruby: Ripple (text ripple effects) and Worm (Unicode wave animations)'
|
|
13
|
+
spec.homepage = 'https://github.com/ttscoff/ruby-progress'
|
|
14
|
+
spec.license = 'MIT'
|
|
15
|
+
spec.required_ruby_version = '>= 2.5.0'
|
|
16
|
+
|
|
17
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
|
18
|
+
spec.metadata['source_code_uri'] = spec.homepage
|
|
19
|
+
spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/main/CHANGELOG.md"
|
|
20
|
+
|
|
21
|
+
# Specify which files should be added to the gem when it is released.
|
|
22
|
+
spec.files = Dir.chdir(__dir__) do
|
|
23
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
24
|
+
(f == __FILE__) || f.match(%r{\A(?:(?:test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
spec.bindir = 'bin'
|
|
28
|
+
spec.executables = ['prg', 'ripple', 'worm']
|
|
29
|
+
spec.require_paths = ['lib']
|
|
30
|
+
|
|
31
|
+
# Runtime dependencies
|
|
32
|
+
# None required - uses only standard library
|
|
33
|
+
|
|
34
|
+
# Development dependencies
|
|
35
|
+
spec.add_development_dependency 'rake', '~> 13.0'
|
|
36
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
|
37
|
+
spec.add_development_dependency 'rubocop', '~> 1.21'
|
|
38
|
+
spec.add_development_dependency 'simplecov', '~> 0.21'
|
|
39
|
+
end
|
data/test_worm_flags.rb
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
# Test script to demonstrate the new --stdout and --checkmark flags in worm.rb
|
|
4
|
+
|
|
5
|
+
puts "Testing worm.rb with new flags:"
|
|
6
|
+
puts
|
|
7
|
+
|
|
8
|
+
puts "1. Testing --checkmark flag with success:"
|
|
9
|
+
system("ruby worm.rb --command 'echo Hello World' --message 'Running test' --success 'Test passed!' --checkmark")
|
|
10
|
+
puts
|
|
11
|
+
|
|
12
|
+
puts "2. Testing --stdout flag:"
|
|
13
|
+
system("ruby worm.rb --command 'echo This output will be displayed' --message 'Capturing output' --stdout")
|
|
14
|
+
puts
|
|
15
|
+
|
|
16
|
+
puts "3. Testing both --checkmark and --stdout together:"
|
|
17
|
+
system("ruby worm.rb --command 'echo Combined test output' --message 'Testing both flags' --success 'Both flags work!' --checkmark --stdout")
|
|
18
|
+
puts
|
|
19
|
+
|
|
20
|
+
puts "4. Testing --checkmark with failure:"
|
|
21
|
+
system("ruby worm.rb --command 'exit 1' --message 'Testing failure' --error 'Test failed!' --checkmark")
|
|
22
|
+
puts
|
|
23
|
+
|
|
24
|
+
puts "All tests completed!"
|