ruby-progress 1.2.0 → 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
@@ -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
 
@@ -77,5 +83,13 @@ module RubyProgress
77
83
 
78
84
  [start_chars, end_chars]
79
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
80
94
  end
81
95
  end
@@ -1,8 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyProgress
4
- VERSION = '1.2.0'
5
- WORM_VERSION = '1.1.0'
6
- TWIRL_VERSION = '1.1.0'
7
- RIPPLE_VERSION = '1.1.0'
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
@@ -68,183 +69,7 @@ module RubyProgress
68
69
  @start_chars, @end_chars = RubyProgress::Utils.parse_ends(options[:ends])
69
70
  @running = false
70
71
  end
71
-
72
- def animate(message: nil, success: nil, error: nil, &block)
73
- @message = message if message
74
- @success_text = success if success
75
- @error_text = error if error
76
- @running = true
77
-
78
- # Set up interrupt handler to ensure cursor is restored
79
- original_int_handler = Signal.trap('INT') do
80
- @running = false
81
- RubyProgress::Utils.clear_line
82
- RubyProgress::Utils.show_cursor
83
- exit 130
84
- end
85
-
86
- # Hide cursor
87
- RubyProgress::Utils.hide_cursor
88
-
89
- animation_thread = Thread.new { animation_loop }
90
-
91
- begin
92
- if block_given?
93
- result = yield
94
- @running = false
95
- animation_thread.join
96
- display_completion_message(@success_text, true)
97
- result
98
- else
99
- animation_thread.join
100
- end
101
- rescue StandardError => e
102
- @running = false
103
- animation_thread.join
104
- display_completion_message(@error_text || "Error: #{e.message}", false)
105
- nil # Return nil instead of re-raising when used as a progress indicator
106
- ensure
107
- # Always clear animation line and restore cursor
108
- $stderr.print "\r\e[2K"
109
- RubyProgress::Utils.show_cursor
110
- Signal.trap('INT', original_int_handler) if original_int_handler
111
- end
112
- end
113
-
114
- def run_with_command
115
- return unless @command
116
-
117
- exit_code = 0
118
- stdout_content = nil
119
-
120
- begin
121
- stdout_content = animate do
122
- # Use popen3 instead of capture3 for better signal handling
123
- Open3.popen3(@command) do |stdin, stdout, stderr, wait_thr|
124
- stdin.close
125
- captured_stdout = stdout.read
126
- stderr_content = stderr.read
127
- exit_code = wait_thr.value.exitstatus
128
-
129
- unless wait_thr.value.success?
130
- error_msg = @error_text || "Command failed with exit code #{exit_code}"
131
- error_msg += ": #{stderr_content.strip}" if stderr_content && !stderr_content.empty?
132
- raise StandardError, error_msg
133
- end
134
- captured_stdout
135
- end
136
- end
137
-
138
- # Output to stdout if --stdout flag is set
139
- puts stdout_content if @output_stdout && stdout_content
140
- rescue StandardError => e
141
- # animate method handles error display, just exit with proper code
142
- exit exit_code.nonzero? || 1
143
- rescue Interrupt
144
- exit 130
145
- end
146
- end
147
-
148
- def run_indefinitely
149
- # Set up interrupt handler to ensure cursor is restored
150
- original_int_handler = Signal.trap('INT') do
151
- @running = false
152
- RubyProgress::Utils.clear_line
153
- RubyProgress::Utils.show_cursor
154
- exit 130
155
- end
156
-
157
- @running = true
158
- RubyProgress::Utils.hide_cursor
159
-
160
- begin
161
- animation_loop
162
- ensure
163
- RubyProgress::Utils.show_cursor
164
- Signal.trap('INT', original_int_handler) if original_int_handler
165
- end
166
- end
167
-
168
- def stop
169
- @running = false
170
- end
171
-
172
- def run_daemon_mode(success_message: nil, show_checkmark: false, control_message_file: nil)
173
- @running = true
174
- stop_requested = false
175
-
176
- # Set up signal handlers
177
- original_int_handler = Signal.trap('INT') { stop_requested = true }
178
- Signal.trap('USR1') { stop_requested = true }
179
- Signal.trap('TERM') { stop_requested = true }
180
- Signal.trap('HUP') { stop_requested = true }
181
-
182
- RubyProgress::Utils.hide_cursor
183
-
184
- begin
185
- animation_loop_daemon_mode(stop_requested_proc: -> { stop_requested })
186
- ensure
187
- RubyProgress::Utils.clear_line
188
- RubyProgress::Utils.show_cursor
189
-
190
- # Display stop-time completion message, preferring control file if provided
191
- final_message = success_message
192
- final_checkmark = show_checkmark
193
- final_success = true
194
- if control_message_file && File.exist?(control_message_file)
195
- begin
196
- data = JSON.parse(File.read(control_message_file))
197
- final_message = data['message'] if data['message']
198
- final_checkmark = !!data['checkmark'] if data.key?('checkmark')
199
- final_success = !!data['success'] if data.key?('success')
200
- rescue StandardError
201
- # ignore parse errors, fallback to provided message
202
- ensure
203
- begin
204
- File.delete(control_message_file)
205
- rescue StandardError
206
- nil
207
- end
208
- end
209
- end
210
-
211
- if final_message
212
- RubyProgress::Utils.display_completion(
213
- final_message,
214
- success: final_success,
215
- show_checkmark: final_checkmark,
216
- output_stream: :stdout
217
- )
218
- end
219
-
220
- Signal.trap('INT', original_int_handler) if original_int_handler
221
- end
222
- end
223
-
224
- def animation_loop_step
225
- return unless @running
226
-
227
- @position ||= 0
228
- @direction ||= 1
229
-
230
- message_part = @message && !@message.empty? ? "#{@message} " : ''
231
- $stderr.print "\r\e[2K#{@start_chars}#{message_part}#{generate_dots(@position, @direction)}#{@end_chars}"
232
- $stderr.flush
233
-
234
- sleep @speed
235
-
236
- # Update position and direction
237
- @position += @direction
238
- if @position >= @length - 1
239
- if @direction_mode == :forward_only
240
- @position = 0
241
- else
242
- @direction = -1
243
- end
244
- elsif @position <= 0
245
- @direction = 1
246
- end
247
- end
72
+ include WormRunner
248
73
 
249
74
  private
250
75
 
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
data/quick_demo.rb ADDED
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Short demo script for interactive/manual testing of ruby-progress features.
5
+ # Focuses on a compact set of examples used during development and quick checks.
6
+ ##
7
+ #
8
+ # A shorter, more focused demo script for quick testing and demonstrations.
9
+ # This version focuses on the most impressive features without long pauses.
10
+ # QuickDemo: small, focused demo runner used for development and CI smoke tests.
11
+ class QuickDemo
12
+ def initialize
13
+ @gem_path = File.expand_path('bin/prg', __dir__)
14
+ @lib_path = File.expand_path('lib', __dir__)
15
+ end
16
+
17
+ def run
18
+ puts "\nšŸš€ Ruby Progress Gem v1.2.2 - Quick Demo\n"
19
+ puts "=========================================\n\n"
20
+
21
+ # Showcase the most impressive features
22
+ showcase_ends_feature
23
+ showcase_custom_styles
24
+ showcase_direction_control
25
+ showcase_error_handling
26
+
27
+ puts "\n✨ Demo complete! Install with: gem install ruby-progress"
28
+ puts "šŸ“š GitHub: https://github.com/ttscoff/ruby-progress\n\n"
29
+ end
30
+
31
+ private
32
+
33
+ def showcase_ends_feature
34
+ puts 'šŸŽÆ Universal --ends Flag (NEW in v1.2.0)'
35
+ puts '-' * 40
36
+
37
+ puts 'Ripple with brackets:'
38
+ run_cmd("ripple 'Loading...' --ends '[]' --command 'sleep 3' --success 'Bracketed!' --checkmark")
39
+
40
+ puts "\nWorm with angles:"
41
+ run_cmd("worm --length 10 --ends '<<>>' --command 'sleep 3' --success 'Angled!' --checkmark")
42
+
43
+ puts "\nTwirl with emojis:"
44
+ run_cmd("twirl --ends 'ā™„ļøšŸ‘' --command 'sleep 2' --success 'Decorated!'")
45
+
46
+ puts "\n"
47
+ end
48
+
49
+ def showcase_custom_styles
50
+ puts 'šŸŽØ Custom Worm Styles (NEW in v1.2.0)'
51
+ puts '-' * 40
52
+
53
+ puts 'ASCII custom pattern:'
54
+ run_cmd("worm --length 10 --style custom=_-= --command 'sleep 3' --success 'Custom ASCII!'")
55
+
56
+ puts "\nEmoji custom pattern:"
57
+ run_cmd("worm --length 10 --style custom=🟦🟨🟄 --command 'sleep 3' --success 'Custom emoji!'")
58
+
59
+ puts "\n"
60
+ end
61
+
62
+ def showcase_direction_control
63
+ puts 'šŸƒ Direction Control (NEW in v1.2.0)'
64
+ puts '-' * 40
65
+
66
+ puts 'Forward-only worm:'
67
+ run_cmd("worm --length 10 --direction forward --command 'sleep 4' --success 'Always forward!'")
68
+
69
+ puts "\nCombined features:"
70
+ run_cmd("worm --length 10 --style custom=.*🟔 --direction forward --ends '怐怑' --command 'sleep 4' --success 'Ultimate combo!'")
71
+
72
+ puts "\n"
73
+ end
74
+
75
+ def showcase_error_handling
76
+ puts 'āŒ Error Handling'
77
+ puts '-' * 40
78
+
79
+ puts 'Graceful failure:'
80
+ run_cmd("ripple 'Processing...' --command 'sleep 2 && exit 1' --error 'Task failed!' --checkmark")
81
+
82
+ puts "\n"
83
+ end
84
+
85
+ def print_command(args)
86
+ # ANSI color codes
87
+ cyan = "\e[36m"
88
+ bright_red = "\e[91m"
89
+ bright_white = "\e[97m"
90
+ bright_green = "\e[92m"
91
+ reset = "\e[0m"
92
+
93
+ # Start with the base command
94
+ colored_cmd = "$ #{cyan}prg#{reset}"
95
+
96
+ # Use regex to colorize the command
97
+ # First capture the subcommand (ripple, worm, twirl)
98
+ colored_args = args.gsub(/^(ripple|worm|twirl)/, "#{bright_red}\\1#{reset}")
99
+
100
+ # Colorize flags (--flag-name) in bright white and their values in bright green
101
+ colored_args = colored_args.gsub(/(--[\w-]+)(\s+|=)('[^']*'|"[^"]*"|\S+)/) do |_match|
102
+ flag = ::Regexp.last_match(1)
103
+ separator = ::Regexp.last_match(2)
104
+ value = ::Regexp.last_match(3)
105
+ "#{bright_white}#{flag}#{reset}#{separator}#{bright_green}#{value}#{reset}"
106
+ end
107
+
108
+ # Colorize short flags (-f) in bright white and their values in bright green
109
+ colored_args = colored_args.gsub(/(-[a-zA-Z])(\s+)('[^']*'|"[^"]*"|\S+)/) do |_match|
110
+ flag = ::Regexp.last_match(1)
111
+ separator = ::Regexp.last_match(2)
112
+ value = ::Regexp.last_match(3)
113
+ "#{bright_white}#{flag}#{reset}#{separator}#{bright_green}#{value}#{reset}"
114
+ end
115
+
116
+ puts "#{colored_cmd} #{colored_args}"
117
+ end
118
+
119
+ def run_cmd(args)
120
+ full_cmd = "ruby -I #{@lib_path} #{@gem_path} #{args}"
121
+ print_command(args)
122
+ system(full_cmd)
123
+ puts
124
+ end
125
+ end
126
+
127
+ # Quick execution
128
+ if __FILE__ == $PROGRAM_NAME
129
+ begin
130
+ QuickDemo.new.run
131
+ rescue Interrupt
132
+ puts "\n\nDemo stopped. Thanks!"
133
+ end
134
+ end