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,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse'
4
+ require 'fileutils'
5
+ require_relative 'cli/fill_options'
6
+
7
+ module RubyProgress
8
+ # CLI module for Fill command
9
+ module FillCLI
10
+ class << self
11
+ def run
12
+ trap('INT') do
13
+ Utils.show_cursor
14
+ exit
15
+ end
16
+
17
+ options = RubyProgress::FillCLI::Options.parse_cli_options
18
+
19
+ # Handle basic output flags first
20
+ if options[:help]
21
+ puts RubyProgress::FillCLI::Options.help_text
22
+ exit
23
+ end
24
+
25
+ if options[:version]
26
+ puts "Fill version #{RubyProgress::FILL_VERSION}"
27
+ exit
28
+ end
29
+
30
+ if options[:show_styles]
31
+ show_fill_styles
32
+ exit
33
+ end
34
+
35
+ # Handle daemon control first
36
+ if options[:status] || options[:stop]
37
+ pid_file = options[:pid_file] || '/tmp/ruby-progress/fill.pid'
38
+ if options[:status]
39
+ Daemon.show_status(pid_file)
40
+ else
41
+ Daemon.stop_daemon_by_pid_file(pid_file)
42
+ end
43
+ exit
44
+ end
45
+
46
+ # Parse style option
47
+ parsed_style = parse_fill_style(options[:style])
48
+
49
+ if options[:daemon]
50
+ run_daemon_mode(options, parsed_style)
51
+ elsif options[:current]
52
+ show_current_percentage(options, parsed_style)
53
+ elsif options[:report]
54
+ show_progress_report(options, parsed_style)
55
+ elsif options[:advance] || options[:complete] || options[:cancel]
56
+ handle_progress_commands(options, parsed_style)
57
+ else
58
+ run_auto_advance_mode(options, parsed_style)
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def parse_fill_style(style_option)
65
+ case style_option
66
+ when String
67
+ if style_option.start_with?('custom=')
68
+ Fill.parse_custom_style(style_option)
69
+ else
70
+ style_option.to_sym
71
+ end
72
+ else
73
+ style_option
74
+ end
75
+ end
76
+
77
+ def run_daemon_mode(options, parsed_style)
78
+ # For daemon mode, detach the process
79
+ PrgCLI.daemonize
80
+
81
+ pid_file = options[:pid_file] || '/tmp/ruby-progress/fill.pid'
82
+ FileUtils.mkdir_p(File.dirname(pid_file))
83
+ File.write(pid_file, Process.pid.to_s)
84
+
85
+ # Create the fill bar and show initial empty state
86
+ fill_options = {
87
+ style: parsed_style,
88
+ length: options[:length],
89
+ ends: options[:ends],
90
+ success: options[:success_message],
91
+ error: options[:error_message]
92
+ }
93
+
94
+ fill_bar = Fill.new(fill_options)
95
+ Fill.hide_cursor
96
+
97
+ begin
98
+ fill_bar.render # Show initial empty bar
99
+
100
+ # Set up signal handlers for daemon control
101
+ stop_requested = false
102
+ Signal.trap('INT') { stop_requested = true }
103
+ Signal.trap('USR1') { stop_requested = true }
104
+ Signal.trap('TERM') { stop_requested = true }
105
+
106
+ # Keep daemon alive until stop requested
107
+ sleep(0.1) until stop_requested
108
+ ensure
109
+ Fill.show_cursor
110
+ FileUtils.rm_f(pid_file)
111
+ end
112
+ end
113
+
114
+ def show_current_percentage(options, _parsed_style)
115
+ # Just output the percentage for scripting (default to 50% for demonstration)
116
+ percentage = options[:percent] || 50
117
+ puts percentage.to_f
118
+ end
119
+
120
+ def show_progress_report(options, parsed_style)
121
+ # Create a fill bar to demonstrate current progress
122
+ fill_options = {
123
+ style: parsed_style,
124
+ length: options[:length],
125
+ ends: options[:ends]
126
+ }
127
+
128
+ fill_bar = Fill.new(fill_options)
129
+
130
+ # Set percentage (default to 50% for demonstration)
131
+ fill_bar.percent = options[:percent] || 50
132
+
133
+ # Get detailed report
134
+ report = fill_bar.report
135
+
136
+ # Display the current progress bar and detailed status
137
+ fill_bar.render
138
+ puts "\nProgress Report:"
139
+ puts " Progress: #{report[:progress][0]}/#{report[:progress][1]}"
140
+ puts " Percent: #{report[:percent]}%"
141
+ puts " Completed: #{report[:completed] ? 'Yes' : 'No'}"
142
+ puts " Style: #{report[:style]}"
143
+ end
144
+
145
+ def handle_progress_commands(_options, _parsed_style)
146
+ # For progress commands, we assume there's a daemon running
147
+ # This is a simplified version - in a real implementation,
148
+ # we'd need IPC to communicate with the daemon
149
+ warn 'Progress commands require daemon mode implementation'
150
+ warn "Run 'prg fill --daemon' first, then use progress commands"
151
+ exit 1
152
+ end
153
+
154
+ def run_auto_advance_mode(options, parsed_style)
155
+ fill_options = {
156
+ style: parsed_style,
157
+ length: options[:length],
158
+ ends: options[:ends],
159
+ success: options[:success_message],
160
+ error: options[:error_message]
161
+ }
162
+
163
+ fill_bar = Fill.new(fill_options)
164
+ Fill.hide_cursor
165
+
166
+ begin
167
+ if options[:percent]
168
+ # Set to specific percentage
169
+ fill_bar.percent = options[:percent]
170
+ fill_bar.render
171
+ unless fill_bar.completed?
172
+ # For non-complete percentages, show the result briefly
173
+ sleep(0.1)
174
+ end
175
+ else
176
+ # Auto-advance mode
177
+ sleep_time = case options[:speed]
178
+ when :fast then 0.1
179
+ when :medium, nil then 0.2
180
+ when :slow then 0.5
181
+ when Numeric then 1.0 / options[:speed]
182
+ else 0.3
183
+ end
184
+
185
+ fill_bar.render
186
+ (1..options[:length]).each do
187
+ sleep(sleep_time)
188
+ fill_bar.advance
189
+ end
190
+ end
191
+ fill_bar.complete
192
+ rescue Interrupt
193
+ fill_bar.cancel
194
+ ensure
195
+ Fill.show_cursor
196
+ end
197
+ end
198
+
199
+ def show_fill_styles
200
+ puts "\nAvailable Fill Styles:"
201
+ puts '=' * 50
202
+
203
+ Fill::FILL_STYLES.each do |name, style|
204
+ print "#{name.to_s.ljust(12)} : "
205
+
206
+ # Show a sample progress bar
207
+ filled = style[:full] * 6
208
+ empty = style[:empty] * 4
209
+ puts "[#{filled}#{empty}] (60%)"
210
+ end
211
+
212
+ puts "\nCustom Style:"
213
+ puts "#{'custom=XY'.ljust(12)} : Specify X=empty, Y=full characters"
214
+ puts ' Example: --style custom=.# → [######....] (60%)'
215
+ puts
216
+ end
217
+ end
218
+ end
219
+ end
@@ -93,7 +93,8 @@ module RubyProgress
93
93
  spinner_position: false,
94
94
  caps: false,
95
95
  inverse: false,
96
- output: :error
96
+ output: :error,
97
+ ends: nil
97
98
  }
98
99
  @options = defaults.merge(options)
99
100
  @string = string
@@ -104,6 +105,7 @@ module RubyProgress
104
105
  @spinner_position = @options[:spinner_position]
105
106
  @caps = @options[:caps]
106
107
  @inverse = @options[:inverse]
108
+ @start_chars, @end_chars = RubyProgress::Utils.parse_ends(@options[:ends])
107
109
  end
108
110
 
109
111
  def printout
@@ -138,7 +140,7 @@ module RubyProgress
138
140
  char = @rainbow ? char.rainbow(i) : char.extend(StringExtensions).light_white
139
141
  post = letters.slice!(0, letters.length).join.extend(StringExtensions).dark_white
140
142
  end
141
- $stderr.print "\r\e[2K#{pre}#{char}#{post}"
143
+ $stderr.print "\r\e[2K#{@start_chars}#{pre}#{char}#{post}#{@end_chars}"
142
144
  $stderr.flush
143
145
  end
144
146
 
@@ -49,9 +49,15 @@ module RubyProgress
49
49
  when :stderr
50
50
  warn formatted_message
51
51
  when :warn
52
- warn "\e[2K#{formatted_message}"
52
+ # Ensure we're at the beginning of a fresh line, clear it, then display message
53
+ $stderr.print "\r\e[2K"
54
+ $stderr.flush
55
+ warn formatted_message
53
56
  else
54
- warn "\e[2K#{formatted_message}"
57
+ # Ensure we're at the beginning of a fresh line, clear it, then display message
58
+ $stderr.print "\r\e[2K"
59
+ $stderr.flush
60
+ warn formatted_message
55
61
  end
56
62
  end
57
63
 
@@ -61,5 +67,29 @@ module RubyProgress
61
67
  clear_line(output_stream) if output_stream != :warn # warn already includes clear in display_completion
62
68
  display_completion(message, success: success, show_checkmark: show_checkmark, output_stream: output_stream)
63
69
  end
70
+
71
+ # Parse start/end characters for animation wrapping
72
+ # @param ends_string [String] Even-length string to split in half for start/end chars
73
+ # @return [Array<String>] Array with [start_chars, end_chars]
74
+ def self.parse_ends(ends_string)
75
+ return ['', ''] unless ends_string && !ends_string.empty?
76
+
77
+ chars = ends_string.each_char.to_a
78
+ return ['', ''] if chars.length.odd? || chars.empty?
79
+
80
+ mid_point = chars.length / 2
81
+ start_chars = chars[0...mid_point].join
82
+ end_chars = chars[mid_point..-1].join
83
+
84
+ [start_chars, end_chars]
85
+ end
86
+
87
+ # Validate ends string: must be non-empty and even-length (handles multi-byte chars)
88
+ def self.ends_valid?(ends_string)
89
+ return false unless ends_string && !ends_string.empty?
90
+
91
+ chars = ends_string.each_char.to_a
92
+ !chars.empty? && (chars.length % 2).zero?
93
+ end
64
94
  end
65
95
  end
@@ -1,8 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyProgress
4
- VERSION = '1.1.9'
5
- WORM_VERSION = '1.0.4'
6
- TWIRL_VERSION = '1.0.1'
7
- RIPPLE_VERSION = '1.0.5'
4
+ # Main gem version
5
+ VERSION = '1.2.4'
6
+
7
+ # Component-specific versions
8
+ WORM_VERSION = '1.1.2'
9
+ TWIRL_VERSION = '1.1.2'
10
+ RIPPLE_VERSION = '1.1.2'
11
+ FILL_VERSION = '1.0.1'
8
12
  end
@@ -4,6 +4,7 @@ require 'optparse'
4
4
  require 'open3'
5
5
  require 'json'
6
6
  require_relative 'utils'
7
+ require_relative 'cli/worm_runner'
7
8
 
8
9
  module RubyProgress
9
10
  # Animated progress indicator with ripple effect using Unicode combining characters
@@ -64,187 +65,17 @@ module RubyProgress
64
65
  @error_text = options[:error]
65
66
  @show_checkmark = options[:checkmark] || false
66
67
  @output_stdout = options[:stdout] || false
68
+ @direction_mode = options[:direction] || :bidirectional
69
+ @start_chars, @end_chars = RubyProgress::Utils.parse_ends(options[:ends])
67
70
  @running = false
68
71
  end
69
-
70
- def animate(message: nil, success: nil, error: nil, &block)
71
- @message = message if message
72
- @success_text = success if success
73
- @error_text = error if error
74
- @running = true
75
-
76
- # Set up interrupt handler to ensure cursor is restored
77
- original_int_handler = Signal.trap('INT') do
78
- @running = false
79
- RubyProgress::Utils.clear_line
80
- RubyProgress::Utils.show_cursor
81
- exit 130
82
- end
83
-
84
- # Hide cursor
85
- RubyProgress::Utils.hide_cursor
86
-
87
- animation_thread = Thread.new { animation_loop }
88
-
89
- begin
90
- if block_given?
91
- result = yield
92
- @running = false
93
- animation_thread.join
94
- display_completion_message(@success_text, true)
95
- result
96
- else
97
- animation_thread.join
98
- end
99
- rescue StandardError => e
100
- @running = false
101
- animation_thread.join
102
- display_completion_message(@error_text || "Error: #{e.message}", false)
103
- nil # Return nil instead of re-raising when used as a progress indicator
104
- ensure
105
- # Always clear animation line and restore cursor
106
- $stderr.print "\r\e[2K"
107
- RubyProgress::Utils.show_cursor
108
- Signal.trap('INT', original_int_handler) if original_int_handler
109
- end
110
- end
111
-
112
- def run_with_command
113
- return unless @command
114
-
115
- exit_code = 0
116
- stdout_content = nil
117
-
118
- begin
119
- stdout_content = animate do
120
- # Use popen3 instead of capture3 for better signal handling
121
- Open3.popen3(@command) do |stdin, stdout, stderr, wait_thr|
122
- stdin.close
123
- captured_stdout = stdout.read
124
- stderr_content = stderr.read
125
- exit_code = wait_thr.value.exitstatus
126
-
127
- unless wait_thr.value.success?
128
- error_msg = @error_text || "Command failed with exit code #{exit_code}"
129
- error_msg += ": #{stderr_content.strip}" if stderr_content && !stderr_content.empty?
130
- raise StandardError, error_msg
131
- end
132
- captured_stdout
133
- end
134
- end
135
-
136
- # Output to stdout if --stdout flag is set
137
- puts stdout_content if @output_stdout && stdout_content
138
- rescue StandardError => e
139
- # animate method handles error display, just exit with proper code
140
- exit exit_code.nonzero? || 1
141
- rescue Interrupt
142
- exit 130
143
- end
144
- end
145
-
146
- def run_indefinitely
147
- # Set up interrupt handler to ensure cursor is restored
148
- original_int_handler = Signal.trap('INT') do
149
- @running = false
150
- RubyProgress::Utils.clear_line
151
- RubyProgress::Utils.show_cursor
152
- exit 130
153
- end
154
-
155
- @running = true
156
- RubyProgress::Utils.hide_cursor
157
-
158
- begin
159
- animation_loop
160
- ensure
161
- RubyProgress::Utils.show_cursor
162
- Signal.trap('INT', original_int_handler) if original_int_handler
163
- end
164
- end
165
-
166
- def stop
167
- @running = false
168
- end
169
-
170
- def run_daemon_mode(success_message: nil, show_checkmark: false, control_message_file: nil)
171
- @running = true
172
- stop_requested = false
173
-
174
- # Set up signal handlers
175
- original_int_handler = Signal.trap('INT') { stop_requested = true }
176
- Signal.trap('USR1') { stop_requested = true }
177
- Signal.trap('TERM') { stop_requested = true }
178
- Signal.trap('HUP') { stop_requested = true }
179
-
180
- RubyProgress::Utils.hide_cursor
181
-
182
- begin
183
- animation_loop_daemon_mode(stop_requested_proc: -> { stop_requested })
184
- ensure
185
- RubyProgress::Utils.clear_line
186
- RubyProgress::Utils.show_cursor
187
-
188
- # Display stop-time completion message, preferring control file if provided
189
- final_message = success_message
190
- final_checkmark = show_checkmark
191
- final_success = true
192
- if control_message_file && File.exist?(control_message_file)
193
- begin
194
- data = JSON.parse(File.read(control_message_file))
195
- final_message = data['message'] if data['message']
196
- final_checkmark = !!data['checkmark'] if data.key?('checkmark')
197
- final_success = !!data['success'] if data.key?('success')
198
- rescue StandardError
199
- # ignore parse errors, fallback to provided message
200
- ensure
201
- begin
202
- File.delete(control_message_file)
203
- rescue StandardError
204
- nil
205
- end
206
- end
207
- end
208
-
209
- if final_message
210
- RubyProgress::Utils.display_completion(
211
- final_message,
212
- success: final_success,
213
- show_checkmark: final_checkmark,
214
- output_stream: :stdout
215
- )
216
- end
217
-
218
- Signal.trap('INT', original_int_handler) if original_int_handler
219
- end
220
- end
221
-
222
- def animation_loop_step
223
- return unless @running
224
-
225
- @position ||= 0
226
- @direction ||= 1
227
-
228
- message_part = @message && !@message.empty? ? "#{@message} " : ''
229
- $stderr.print "\r\e[2K#{message_part}#{generate_dots(@position, @direction)}"
230
- $stderr.flush
231
-
232
- sleep @speed
233
-
234
- # Update position and direction
235
- @position += @direction
236
- if @position >= @length - 1
237
- @direction = -1
238
- elsif @position <= 0
239
- @direction = 1
240
- end
241
- end
72
+ include WormRunner
242
73
 
243
74
  private
244
75
 
245
76
  def display_completion_message(message, success)
246
77
  return unless message
247
-
78
+
248
79
  mark = ''
249
80
  if @show_checkmark
250
81
  mark = success ? '✅ ' : '🛑 '
@@ -285,7 +116,15 @@ module RubyProgress
285
116
  def parse_style(style_input)
286
117
  return RIPPLE_STYLES['circles'] unless style_input && !style_input.to_s.strip.empty?
287
118
 
288
- style_lower = style_input.to_s.downcase.strip
119
+ style_str = style_input.to_s.strip
120
+
121
+ # Check for custom style format: custom=abc or custom_abc or customXabc
122
+ if style_str.match(/^custom[_=](.+)$/i)
123
+ custom_chars = Regexp.last_match(1)
124
+ return parse_custom_style(custom_chars)
125
+ end
126
+
127
+ style_lower = style_str.downcase
289
128
 
290
129
  # First, try exact match
291
130
  return RIPPLE_STYLES[style_lower] if RIPPLE_STYLES.key?(style_lower)
@@ -339,6 +178,24 @@ module RubyProgress
339
178
  RIPPLE_STYLES['circles']
340
179
  end
341
180
 
181
+ def parse_custom_style(custom_chars)
182
+ # Split into individual characters, properly handling multi-byte characters (emojis)
183
+ chars = custom_chars.each_char.to_a
184
+
185
+ # Ensure we have exactly 3 characters
186
+ if chars.length != 3
187
+ # Fallback to default if not exactly 3 characters
188
+ return RIPPLE_STYLES['circles']
189
+ end
190
+
191
+ # Create custom style hash with baseline, midline, peak
192
+ {
193
+ baseline: chars[0],
194
+ midline: chars[1],
195
+ peak: chars[2]
196
+ }
197
+ end
198
+
342
199
  def animation_loop
343
200
  position = 0
344
201
  direction = 1
@@ -346,14 +203,18 @@ module RubyProgress
346
203
  while @running
347
204
  message_part = @message && !@message.empty? ? "#{@message} " : ''
348
205
  # Enhanced line clearing for better daemon mode behavior
349
- $stderr.print "\r\e[2K#{message_part}#{generate_dots(position, direction)}"
206
+ $stderr.print "\r\e[2K#{@start_chars}#{message_part}#{generate_dots(position, direction)}#{@end_chars}"
350
207
  $stderr.flush
351
208
 
352
209
  sleep @speed
353
210
 
354
211
  position += direction
355
212
  if position >= @length - 1
356
- direction = -1
213
+ if @direction_mode == :forward_only
214
+ position = 0
215
+ else
216
+ direction = -1
217
+ end
357
218
  elsif position <= 0
358
219
  direction = 1
359
220
  end
@@ -386,7 +247,11 @@ module RubyProgress
386
247
 
387
248
  position += direction
388
249
  if position >= @length - 1
389
- direction = -1
250
+ if @direction_mode == :forward_only
251
+ position = 0
252
+ else
253
+ direction = -1
254
+ end
390
255
  elsif position <= 0
391
256
  direction = 1
392
257
  end
data/lib/ruby-progress.rb CHANGED
@@ -4,6 +4,7 @@ require_relative 'ruby-progress/version'
4
4
  require_relative 'ruby-progress/utils'
5
5
  require_relative 'ruby-progress/ripple'
6
6
  require_relative 'ruby-progress/worm'
7
+ require_relative 'ruby-progress/fill'
7
8
  require_relative 'ruby-progress/daemon'
8
9
 
9
10
  module RubyProgress