ruby-progress 1.3.1 → 1.3.2

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.
@@ -2,11 +2,14 @@
2
2
 
3
3
  require 'optparse'
4
4
  require 'fileutils'
5
+ require 'json'
6
+ require 'securerandom'
5
7
  require_relative 'cli/fill_options'
6
8
  require_relative 'output_capture'
7
9
 
8
10
  module RubyProgress
9
11
  # CLI module for Fill command
12
+ # rubocop:disable Metrics/ClassLength
10
13
  module FillCLI
11
14
  class << self
12
15
  def run
@@ -35,11 +38,14 @@ module RubyProgress
35
38
 
36
39
  # Handle daemon control first
37
40
  if options[:status] || options[:stop]
38
- pid_file = options[:pid_file] || '/tmp/ruby-progress/fill.pid'
41
+ pid_file = resolve_pid_file(options, :status_name)
39
42
  if options[:status]
40
43
  Daemon.show_status(pid_file)
41
44
  else
42
- Daemon.stop_daemon_by_pid_file(pid_file)
45
+ Daemon.stop_daemon_by_pid_file(pid_file,
46
+ message: options[:stop_success],
47
+ checkmark: options[:stop_checkmark],
48
+ error: !options[:stop_error].nil?)
43
49
  end
44
50
  exit
45
51
  end
@@ -48,6 +54,17 @@ module RubyProgress
48
54
  parsed_style = parse_fill_style(options[:style])
49
55
 
50
56
  if options[:daemon]
57
+ # Resolve pid file and honor daemon-as/name
58
+ pid_file = resolve_pid_file(options, :daemon_name)
59
+ options[:pid_file] = pid_file
60
+
61
+ # Detach or background without detaching based on --no-detach
62
+ if options[:no_detach]
63
+ PrgCLI.backgroundize
64
+ else
65
+ PrgCLI.daemonize
66
+ end
67
+
51
68
  run_daemon_mode(options, parsed_style)
52
69
  elsif options[:current]
53
70
  show_current_percentage(options, parsed_style)
@@ -62,6 +79,14 @@ module RubyProgress
62
79
 
63
80
  private
64
81
 
82
+ def resolve_pid_file(options, name_key = :daemon_name)
83
+ return options[:pid_file] if options[:pid_file]
84
+
85
+ return "/tmp/ruby-progress/#{options[name_key]}.pid" if options[name_key]
86
+
87
+ '/tmp/ruby-progress/fill.pid'
88
+ end
89
+
65
90
  def parse_fill_style(style_option)
66
91
  case style_option
67
92
  when String
@@ -76,9 +101,6 @@ module RubyProgress
76
101
  end
77
102
 
78
103
  def run_daemon_mode(options, parsed_style)
79
- # For daemon mode, detach the process
80
- PrgCLI.daemonize
81
-
82
104
  pid_file = options[:pid_file] || '/tmp/ruby-progress/fill.pid'
83
105
  FileUtils.mkdir_p(File.dirname(pid_file))
84
106
  File.write(pid_file, Process.pid.to_s)
@@ -98,6 +120,71 @@ module RubyProgress
98
120
  begin
99
121
  fill_bar.render # Show initial empty bar
100
122
 
123
+ # Start job processor thread for fill (so daemon can accept jobs)
124
+ job_dir = RubyProgress::Daemon.job_dir_for_pid(pid_file)
125
+ Thread.new do
126
+ RubyProgress::Daemon.process_jobs(job_dir) do |job|
127
+ jid = job['id'] || SecureRandom.uuid
128
+ log_path = begin
129
+ File.join(File.dirname(job_dir), "#{jid}.log")
130
+ rescue StandardError
131
+ nil
132
+ end
133
+
134
+ if job['command']
135
+ oc = RubyProgress::OutputCapture.new(
136
+ command: job['command'],
137
+ lines: options[:output_lines] || 3,
138
+ position: options[:output_position] || :above,
139
+ log_path: log_path
140
+ )
141
+ oc.start
142
+
143
+ fill_bar.instance_variable_set(:@output_capture, oc)
144
+ oc.wait
145
+ captured = oc.lines.join("\n")
146
+ exit_status = oc.exit_status
147
+ fill_bar.instance_variable_set(:@output_capture, nil)
148
+
149
+ success = exit_status.to_i.zero?
150
+ if job['message']
151
+ RubyProgress::Utils.display_completion(
152
+ job['message'],
153
+ success: success,
154
+ show_checkmark: job['checkmark'] || false,
155
+ output_stream: :stdout,
156
+ icons: { success: options[:success_icon], error: options[:error_icon] }
157
+ )
158
+ end
159
+
160
+ { 'exit_status' => exit_status, 'output' => captured, 'log_path' => log_path }
161
+
162
+ elsif job['action']
163
+ case job['action']
164
+ when 'advance'
165
+ fill_bar.advance
166
+ { 'status' => 'done', 'action' => 'advance' }
167
+ when 'percent'
168
+ val = job['value'] || job['percent'] || 0
169
+ fill_bar.percent = val.to_f
170
+ { 'status' => 'done', 'action' => 'percent', 'value' => val }
171
+ when 'complete'
172
+ fill_bar.complete
173
+ { 'status' => 'done', 'action' => 'complete' }
174
+ when 'cancel'
175
+ fill_bar.cancel
176
+ { 'status' => 'done', 'action' => 'cancel' }
177
+ else
178
+ { 'status' => 'error', 'error' => 'unknown action' }
179
+ end
180
+ else
181
+ { 'status' => 'error', 'error' => 'no command or action provided' }
182
+ end
183
+ rescue StandardError
184
+ nil
185
+ end
186
+ end
187
+
101
188
  # Set up signal handlers for daemon control
102
189
  stop_requested = false
103
190
  Signal.trap('INT') { stop_requested = true }
@@ -107,51 +194,82 @@ module RubyProgress
107
194
  # Keep daemon alive until stop requested
108
195
  sleep(0.1) until stop_requested
109
196
  ensure
197
+ # If a control message file exists, print its contents like other CLIs
198
+ cmf = RubyProgress::Daemon.control_message_file(pid_file)
199
+ if File.exist?(cmf)
200
+ begin
201
+ data = JSON.parse(File.read(cmf))
202
+ message = data['message']
203
+ check = if data.key?('checkmark')
204
+ data['checkmark'] ? true : false
205
+ else
206
+ false
207
+ end
208
+
209
+ success_val = if data.key?('success')
210
+ data['success'] ? true : false
211
+ else
212
+ true
213
+ end
214
+ if message
215
+ RubyProgress::Utils.display_completion(
216
+ message,
217
+ success: success_val,
218
+ show_checkmark: check,
219
+ output_stream: :stdout,
220
+ icons: { success: options[:success_icon], error: options[:error_icon] }
221
+ )
222
+ end
223
+ rescue StandardError
224
+ # ignore
225
+ ensure
226
+ begin
227
+ File.delete(cmf)
228
+ rescue StandardError
229
+ nil
230
+ end
231
+ end
232
+ end
233
+
110
234
  Fill.show_cursor
111
235
  FileUtils.rm_f(pid_file)
112
236
  end
113
237
  end
114
238
 
115
- def show_current_percentage(options, _parsed_style)
116
- # Just output the percentage for scripting (default to 50% for demonstration)
117
- percentage = options[:percent] || 50
118
- puts percentage.to_f
119
- end
120
-
121
239
  def show_progress_report(options, parsed_style)
122
- # Create a fill bar to demonstrate current progress
123
- fill_options = {
124
- style: parsed_style,
125
- length: options[:length],
126
- ends: options[:ends]
127
- }
128
-
129
- fill_bar = Fill.new(fill_options)
130
-
131
- # Set percentage (default to 50% for demonstration)
132
- fill_bar.percent = options[:percent] || 50
133
-
134
- # Get detailed report
135
- report = fill_bar.report
136
-
137
- # Display the current progress bar and detailed status
138
- fill_bar.render
139
- puts "\nProgress Report:"
140
- puts " Progress: #{report[:progress][0]}/#{report[:progress][1]}"
141
- puts " Percent: #{report[:percent]}%"
142
- puts " Completed: #{report[:completed] ? 'Yes' : 'No'}"
143
- puts " Style: #{report[:style]}"
240
+ # Produce a simple scripting-friendly report to stdout
241
+ length = options[:length] || 20
242
+ percent = (options[:percent] || 50.0).to_f
243
+ style = parsed_style
244
+
245
+ fill = Fill.new(style: style, length: length)
246
+ fill.percent = percent
247
+
248
+ report = fill.report
249
+ puts 'Progress Report:'
250
+ puts "Progress: #{report[:progress][0]}/#{report[:progress][1]}"
251
+ puts "Percent: #{report[:percent]}%"
252
+ puts "Completed: #{report[:completed] ? 'Yes' : 'No'}"
253
+ puts "Style: #{report[:style].inspect}"
254
+ exit(0)
144
255
  end
145
256
 
146
257
  def handle_progress_commands(_options, _parsed_style)
147
- # For progress commands, we assume there's a daemon running
148
- # This is a simplified version - in a real implementation,
149
- # we'd need IPC to communicate with the daemon
258
+ # For now the progress commands are only supported in daemon mode.
259
+ # Return a clear error to the caller (specs assert this message exists).
150
260
  warn 'Progress commands require daemon mode implementation'
151
- warn "Run 'prg fill --daemon' first, then use progress commands"
152
- exit 1
261
+ exit(1)
262
+ end
263
+
264
+ def show_current_percentage(options, _parsed_style)
265
+ # For scripting and tests: print the current percentage to stdout and exit.
266
+ # If no explicit percent was provided, default to 50.0
267
+ percent = (options[:percent] || 50.0).to_f
268
+ $stdout.print("#{percent}\n")
269
+ exit(0)
153
270
  end
154
271
 
272
+ # Foreground / auto-advance / command mode when not daemonizing
155
273
  def run_auto_advance_mode(options, parsed_style)
156
274
  fill_options = {
157
275
  style: parsed_style,
@@ -161,20 +279,10 @@ module RubyProgress
161
279
  error: options[:error_message]
162
280
  }
163
281
 
164
- # If a command is provided, capture its output and pass an OutputCapture
165
- if options[:command]
166
- oc = RubyProgress::OutputCapture.new(
167
- command: options[:command],
168
- lines: options[:output_lines] || 3,
169
- position: options[:output_position] || :above
170
- )
171
- oc.start
172
- fill_options[:output_capture] = oc
173
- end
174
-
175
282
  fill_bar = Fill.new(fill_options)
176
283
  Fill.hide_cursor
177
284
 
285
+ oc = nil
178
286
  begin
179
287
  if options[:percent]
180
288
  # Set to specific percentage
@@ -185,7 +293,19 @@ module RubyProgress
185
293
  sleep(0.1)
186
294
  end
187
295
  elsif options[:command]
188
- # While the command runs, keep redrawing the bar (live redraw handled by Fill#render)
296
+ # Run the command with OutputCapture
297
+ oc = RubyProgress::OutputCapture.new(
298
+ command: options[:command],
299
+ lines: options[:output_lines] || 3,
300
+ position: options[:output_position] || :above,
301
+ log_path: nil
302
+ )
303
+ oc.start
304
+
305
+ # Attach capture to the live fill instance so it can render output
306
+ fill_bar.instance_variable_set(:@output_capture, oc)
307
+
308
+ # While the command runs, keep redrawing the bar
189
309
  sleep_time = case options[:speed]
190
310
  when :fast then 0.1
191
311
  when :medium, nil then 0.2
@@ -195,11 +315,11 @@ module RubyProgress
195
315
  end
196
316
 
197
317
  fill_bar.render
198
- # Loop until the OutputCapture reader has finished
199
318
  while oc.alive?
200
319
  sleep(sleep_time)
201
320
  fill_bar.render
202
321
  end
322
+ fill_bar.instance_variable_set(:@output_capture, nil)
203
323
  else
204
324
  # Auto-advance mode
205
325
  sleep_time = case options[:speed]
@@ -216,6 +336,7 @@ module RubyProgress
216
336
  fill_bar.advance
217
337
  end
218
338
  end
339
+
219
340
  fill_bar.complete
220
341
  rescue Interrupt
221
342
  fill_bar.cancel
@@ -154,7 +154,7 @@ module RubyProgress
154
154
  RubyProgress::Utils.show_cursor
155
155
  end
156
156
 
157
- def self.complete(string, message, checkmark, success)
157
+ def self.complete(string, message, checkmark, success, icons: {})
158
158
  display_message = message || (checkmark ? string : nil)
159
159
  return unless display_message
160
160
 
@@ -162,7 +162,8 @@ module RubyProgress
162
162
  display_message,
163
163
  success: success,
164
164
  show_checkmark: checkmark,
165
- output_stream: :warn
165
+ output_stream: :warn,
166
+ icons: icons
166
167
  )
167
168
  end
168
169
 
@@ -13,12 +13,17 @@ module RubyProgress
13
13
  end
14
14
 
15
15
  def self.clear_line(output_stream = :stderr)
16
- case output_stream
17
- when :stdout
18
- $stdout.print "\r\e[K"
19
- else
20
- $stderr.print "\r\e[K"
21
- end
16
+ io = case output_stream
17
+ when :stdout
18
+ $stdout
19
+ when :stderr
20
+ $stderr
21
+ else
22
+ # allow passing an IO-like object (e.g. StringIO) directly
23
+ output_stream.respond_to?(:print) ? output_stream : $stderr
24
+ end
25
+
26
+ io.print "\r\e[K"
22
27
  end
23
28
 
24
29
  # Enhanced line clearing for daemon mode that handles output interruption
@@ -33,39 +38,55 @@ module RubyProgress
33
38
  # @param success [Boolean] Whether this represents success or failure
34
39
  # @param show_checkmark [Boolean] Whether to show checkmark/X symbols
35
40
  # @param output_stream [Symbol] Where to output (:stdout, :stderr, :warn)
36
- def self.display_completion(message, success: true, show_checkmark: false, output_stream: :warn)
41
+ def self.display_completion(message, success: true, show_checkmark: false, output_stream: :warn, icons: {})
37
42
  return unless message
38
43
 
39
- mark = ''
40
- if show_checkmark
41
- mark = success ? '✅ ' : '🛑 '
42
- end
44
+ mark = if show_checkmark
45
+ icon = success ? (icons[:success] || '✅') : (icons[:error] || '🛑')
46
+ "#{icon} "
47
+ else
48
+ ''
49
+ end
43
50
 
44
51
  formatted_message = "#{mark}#{message}"
45
52
 
46
- case output_stream
47
- when :stdout
48
- puts formatted_message
49
- when :stderr
50
- warn formatted_message
51
- when :warn
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
53
+ # Resolve destination IO: support symbols (:stdout/:stderr/:warn) or an IO-like object
54
+ dest_io = case output_stream
55
+ when :stdout
56
+ $stdout
57
+ when :stderr
58
+ $stderr
59
+ when :warn
60
+ $stderr
61
+ else
62
+ output_stream.respond_to?(:print) ? output_stream : $stderr
63
+ end
64
+
65
+ # Only treat explicit :stdout and :stderr as non-clearing requests.
66
+ # For :warn and any other/custom stream, clear the current line first.
67
+ unless %i[stdout stderr].include?(output_stream)
68
+ # Always include a leading carriage return when clearing to match
69
+ # terminal behavior expected by the test-suite.
70
+ dest_io.print "\r\e[2K"
71
+ dest_io.flush if dest_io.respond_to?(:flush)
72
+ end
73
+
74
+ # Emit the message to the resolved destination IO. Use warn/puts when targeting
75
+ # the standard streams to preserve familiar behavior (warn writes to $stderr).
76
+ if dest_io == $stdout
77
+ $stdout.puts formatted_message
78
+ elsif dest_io == $stderr
55
79
  warn formatted_message
56
80
  else
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
81
+ dest_io.puts formatted_message
61
82
  end
62
83
  end
63
84
 
64
85
  # Clear current line and display completion message
65
86
  # Convenience method that combines line clearing with message display
66
- def self.complete_with_clear(message, success: true, show_checkmark: false, output_stream: :warn)
87
+ def self.complete_with_clear(message, success: true, show_checkmark: false, output_stream: :warn, icons: {})
67
88
  clear_line(output_stream) if output_stream != :warn # warn already includes clear in display_completion
68
- display_completion(message, success: success, show_checkmark: show_checkmark, output_stream: output_stream)
89
+ display_completion(message, success: success, show_checkmark: show_checkmark, output_stream: output_stream, icons: icons)
69
90
  end
70
91
 
71
92
  # Parse start/end characters for animation wrapping
@@ -2,11 +2,11 @@
2
2
 
3
3
  module RubyProgress
4
4
  # Main gem version
5
- VERSION = '1.3.1'
5
+ VERSION = '1.3.2'
6
6
 
7
7
  # Component-specific versions (patch bumps)
8
- WORM_VERSION = '1.1.3'
9
- TWIRL_VERSION = '1.1.3'
10
- RIPPLE_VERSION = '1.1.3'
11
- FILL_VERSION = '1.0.3'
8
+ WORM_VERSION = '1.1.4'
9
+ TWIRL_VERSION = '1.1.4'
10
+ RIPPLE_VERSION = '1.1.4'
11
+ FILL_VERSION = '1.0.4'
12
12
  end
@@ -76,13 +76,14 @@ module RubyProgress
76
76
  def display_completion_message(message, success)
77
77
  return unless message
78
78
 
79
- mark = ''
80
- if @show_checkmark
81
- mark = success ? '✅ ' : '🛑 '
82
- end
83
-
84
- # Clear animation line and output completion message on stderr
85
- $stderr.print "\r\e[2K#{mark}#{message}\n"
79
+ # Delegate to Utils.display_completion so carriage-return and clearing
80
+ # behavior is consistent across all indicators and respects TTY state.
81
+ RubyProgress::Utils.display_completion(
82
+ message,
83
+ success: success,
84
+ show_checkmark: @show_checkmark,
85
+ output_stream: :warn
86
+ )
86
87
  end
87
88
 
88
89
  def parse_speed(speed_input)
data/screencast ADDED
@@ -0,0 +1,26 @@
1
+ {"version":2,"width":146,"height":24,"timestamp":1760358387,"duration":4.124379,"env":{"SHELL":"/opt/homebrew/bin/fish","TERM":"xterm-256color"}}
2
+ [0.002162,"o","$ bin/prg worm --command \"sleep 4\"\r\n"]
3
+ [0.052412,"o","\u001b[?25l"]
4
+ [0.054304,"o","\r\u001b[2K⬤··"]
5
+ [0.260053,"o","\r\u001b[2K●⬤·"]
6
+ [0.462807,"o","\r\u001b[2K··⬤"]
7
+ [0.667558,"o","\r\u001b[2K·⬤●"]
8
+ [0.86773,"o","\r\u001b[2K⬤··"]
9
+ [1.06914,"o","\r\u001b[2K●⬤·"]
10
+ [1.273207,"o","\r\u001b[2K··⬤"]
11
+ [1.477782,"o","\r\u001b[2K·⬤●"]
12
+ [1.682846,"o","\r\u001b[2K⬤··"]
13
+ [1.885136,"o","\r\u001b[2K●⬤·"]
14
+ [2.087746,"o","\r\u001b[2K··⬤"]
15
+ [2.290181,"o","\r\u001b[2K·⬤●"]
16
+ [2.494656,"o","\r\u001b[2K⬤··"]
17
+ [2.69979,"o","\r\u001b[2K●⬤·"]
18
+ [2.900129,"o","\r\u001b[2K··⬤"]
19
+ [3.102774,"o","\r\u001b[2K·⬤●"]
20
+ [3.303982,"o","\r\u001b[2K⬤··"]
21
+ [3.507771,"o","\r\u001b[2K●⬤·"]
22
+ [3.711678,"o","\r\u001b[2K··⬤"]
23
+ [3.915598,"o","\r\u001b[2K·⬤●"]
24
+ [4.120702,"o","\r\u001b[2K"]
25
+ [4.121323,"o","\u001b[?25h"]
26
+ [4.124379,"o",""]
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-progress
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.1
4
+ version: 1.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brett Terpstra
@@ -126,7 +126,7 @@ files:
126
126
  - lib/ruby-progress/worm.rb
127
127
  - quick_demo.rb
128
128
  - readme_demo.rb
129
- - ruby-progress.gemspec
129
+ - screencast
130
130
  - scripts/run_matrix_mise.fish
131
131
  - test_daemon_interruption.rb
132
132
  - test_daemon_orphan.rb
@@ -1,40 +0,0 @@
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 = 'Animated progress indicators for Ruby: Ripple (text ripple effects), Worm (Unicode wave animations), and Twirl (spinner indicators)'
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 = %w[prg ripple worm twirl fill]
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
- spec.metadata['rubygems_mfa_required'] = 'true'
40
- end