ruby-progress 1.3.4 → 1.3.6

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.
@@ -15,6 +15,11 @@ module WormCLI
15
15
  end
16
16
 
17
17
  def self.run
18
+ trap('INT') do
19
+ RubyProgress::Utils.show_cursor
20
+ exit
21
+ end
22
+
18
23
  options = WormCLI::Options.parse_cli_options
19
24
 
20
25
  if options[:status]
@@ -33,13 +38,8 @@ module WormCLI
33
38
  )
34
39
  exit
35
40
  elsif options[:daemon]
36
- # Detach (or background without detaching) before starting daemon logic
37
- # so the invoking shell/script continues immediately.
38
- if options[:no_detach]
39
- PrgCLI.backgroundize
40
- else
41
- PrgCLI.daemonize
42
- end
41
+ # Background without detaching so worm remains visible in current terminal
42
+ PrgCLI.backgroundize
43
43
 
44
44
  run_daemon_mode(options)
45
45
  else
@@ -61,49 +61,6 @@ module WormCLI
61
61
  progress = RubyProgress::Worm.new(options)
62
62
 
63
63
  begin
64
- # Start job processor thread for worm
65
- job_dir = RubyProgress::Daemon.job_dir_for_pid(pid_file)
66
- job_thread = Thread.new do
67
- RubyProgress::Daemon.process_jobs(job_dir) do |job|
68
- jid = job['id'] || SecureRandom.uuid
69
- log_path = begin
70
- File.join(File.dirname(job_dir), "#{jid}.log")
71
- rescue StandardError
72
- nil
73
- end
74
-
75
- oc = RubyProgress::OutputCapture.new(
76
- command: job['command'],
77
- lines: options[:output_lines] || 3,
78
- position: options[:output_position] || :above,
79
- log_path: log_path,
80
- stream: options[:stdout] || options[:stdout_live]
81
- )
82
- oc.start
83
-
84
- progress.instance_variable_set(:@output_capture, oc)
85
- oc.wait
86
- captured = oc.lines.join("\n")
87
- exit_status = oc.exit_status
88
- progress.instance_variable_set(:@output_capture, nil)
89
-
90
- success = exit_status.to_i.zero?
91
- if job['message']
92
- RubyProgress::Utils.display_completion(
93
- job['message'],
94
- success: success,
95
- show_checkmark: job['checkmark'] || false,
96
- output_stream: :stdout,
97
- icons: { success: options[:success_icon], error: options[:error_icon] }
98
- )
99
- end
100
-
101
- { 'exit_status' => exit_status, 'output' => captured, 'log_path' => log_path }
102
- rescue StandardError
103
- # ignore per-job errors
104
- end
105
- end
106
-
107
64
  progress.run_daemon_mode(
108
65
  success_message: options[:success],
109
66
  show_checkmark: options[:checkmark],
@@ -111,7 +68,6 @@ module WormCLI
111
68
  icons: { success: options[:success_icon], error: options[:error_icon] }
112
69
  )
113
70
  ensure
114
- job_thread&.kill
115
71
  FileUtils.rm_f(pid_file)
116
72
  end
117
73
  end
@@ -13,7 +13,6 @@ module WormCLI
13
13
  output_position: :above,
14
14
  output_lines: 3
15
15
  }
16
- # rubocop:disable Metrics/BlockLength
17
16
  begin
18
17
  OptionParser.new do |opts|
19
18
  opts.banner = 'Usage: prg worm [options]'
@@ -105,10 +104,6 @@ module WormCLI
105
104
  options[:daemon_name] = name
106
105
  end
107
106
 
108
- opts.on('--no-detach', 'When used with --daemon/--daemon-as: run background child but do not fully detach from the terminal') do
109
- options[:no_detach] = true
110
- end
111
-
112
107
  opts.on('--pid-file FILE', 'Write process ID to file (default: /tmp/ruby-progress/progress.pid)') do |file|
113
108
  options[:pid_file] = file
114
109
  end
@@ -182,7 +177,6 @@ module WormCLI
182
177
  puts "Run 'prg worm --help' for more information."
183
178
  exit 1
184
179
  end
185
- # rubocop:enable Metrics/BlockLength
186
180
  options
187
181
  end
188
182
  end
@@ -70,7 +70,13 @@ module WormRunner
70
70
  oc.wait
71
71
  end
72
72
  @output_capture = nil
73
- oc.lines.join("\n")
73
+ # For non-live capture, flush_to handles the output directly
74
+ if @output_live
75
+ oc.lines.join("\n")
76
+ else
77
+ oc.flush_to($stdout) if @output_stdout
78
+ nil # Don't return content since it's already been flushed
79
+ end
74
80
  else
75
81
  animate do
76
82
  Open3.popen3(@command) do |_stdin, stdout, stderr, wait_thr|
@@ -4,6 +4,8 @@ require 'json'
4
4
  require 'fileutils'
5
5
 
6
6
  module RubyProgress
7
+ # Daemon helpers for backgrounding progress indicators.
8
+ # Provides minimal daemonization, PID file management, and simple control-message signaling.
7
9
  module Daemon
8
10
  module_function
9
11
 
@@ -15,70 +17,6 @@ module RubyProgress
15
17
  "#{pid_file}.msg"
16
18
  end
17
19
 
18
- # Resolve a job directory for the daemon based on pid_file or name.
19
- # If pid_file is '/tmp/ruby-progress/mytask.pid' -> jobs dir '/tmp/ruby-progress/mytask.jobs'
20
- def job_dir_for_pid(pid_file)
21
- base = File.basename(pid_file, '.*')
22
- File.join(File.dirname(pid_file), "#{base}.jobs")
23
- end
24
-
25
- # Process available job files in job_dir. Each job is a JSON file with {"id","command","meta"}.
26
- # This method polls the directory and yields each parsed job hash to the provided block.
27
- def process_jobs(job_dir, poll_interval: 0.2)
28
- FileUtils.mkdir_p(job_dir)
29
-
30
- loop do
31
- # Accept any job file ending in .json (UUID filenames are common)
32
- # Ignore processed-* archives and temporary files (e.g., .tmp)
33
- files = Dir.children(job_dir).select do |f|
34
- f.end_with?('.json') && !f.start_with?('processed-')
35
- end.sort
36
-
37
- files.each do |f|
38
- path = File.join(job_dir, f)
39
- processing = "#{path}.processing"
40
-
41
- # Claim the file atomically
42
- begin
43
- File.rename(path, processing)
44
- rescue StandardError
45
- next
46
- end
47
-
48
- job = begin
49
- JSON.parse(File.read(processing))
50
- rescue StandardError
51
- FileUtils.rm_f(processing)
52
- next
53
- end
54
-
55
- begin
56
- yielded = yield(job)
57
-
58
- # on success, write .result info and merge any returned info
59
- result = { 'id' => job['id'], 'status' => 'done', 'time' => Time.now.to_i }
60
- if yielded.is_a?(Hash)
61
- # ensure string keys
62
- extra = yielded.transform_keys(&:to_s)
63
- result.merge!(extra)
64
- end
65
- File.write("#{processing}.result", result.to_json)
66
- rescue StandardError => e
67
- result = { 'id' => job['id'], 'status' => 'error', 'error' => e.message }
68
- File.write("#{processing}.result", result.to_json)
69
- ensure
70
- begin
71
- FileUtils.mv(processing, File.join(job_dir, "processed-#{f}"))
72
- rescue StandardError
73
- FileUtils.rm_f(processing)
74
- end
75
- end
76
- end
77
-
78
- sleep(poll_interval)
79
- end
80
- end
81
-
82
20
  def show_status(pid_file)
83
21
  if File.exist?(pid_file)
84
22
  pid = File.read(pid_file).strip
@@ -58,12 +58,8 @@ module RubyProgress
58
58
  pid_file = resolve_pid_file(options, :daemon_name)
59
59
  options[:pid_file] = pid_file
60
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
61
+ # Background without detaching so progress bar remains visible in current terminal
62
+ PrgCLI.backgroundize
67
63
 
68
64
  run_daemon_mode(options, parsed_style)
69
65
  elsif options[:current]
@@ -120,72 +116,6 @@ module RubyProgress
120
116
  begin
121
117
  fill_bar.render # Show initial empty bar
122
118
 
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
- stream: options[:stdout_live]
141
- )
142
- oc.start
143
-
144
- fill_bar.instance_variable_set(:@output_capture, oc)
145
- oc.wait
146
- captured = oc.lines.join("\n")
147
- exit_status = oc.exit_status
148
- fill_bar.instance_variable_set(:@output_capture, nil)
149
-
150
- success = exit_status.to_i.zero?
151
- if job['message']
152
- RubyProgress::Utils.display_completion(
153
- job['message'],
154
- success: success,
155
- show_checkmark: job['checkmark'] || false,
156
- output_stream: :stdout,
157
- icons: { success: options[:success_icon], error: options[:error_icon] }
158
- )
159
- end
160
-
161
- { 'exit_status' => exit_status, 'output' => captured, 'log_path' => log_path }
162
-
163
- elsif job['action']
164
- case job['action']
165
- when 'advance'
166
- fill_bar.advance
167
- { 'status' => 'done', 'action' => 'advance' }
168
- when 'percent'
169
- val = job['value'] || job['percent'] || 0
170
- fill_bar.percent = val.to_f
171
- { 'status' => 'done', 'action' => 'percent', 'value' => val }
172
- when 'complete'
173
- fill_bar.complete
174
- { 'status' => 'done', 'action' => 'complete' }
175
- when 'cancel'
176
- fill_bar.cancel
177
- { 'status' => 'done', 'action' => 'cancel' }
178
- else
179
- { 'status' => 'error', 'error' => 'unknown action' }
180
- end
181
- else
182
- { 'status' => 'error', 'error' => 'no command or action provided' }
183
- end
184
- rescue StandardError
185
- nil
186
- end
187
- end
188
-
189
119
  # Set up signal handlers for daemon control
190
120
  stop_requested = false
191
121
  Signal.trap('INT') { stop_requested = true }
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'pty'
4
4
  require 'io/console'
5
- require 'english'
5
+ require 'English'
6
6
  require 'fileutils'
7
7
 
8
8
  begin
@@ -176,7 +176,12 @@ module RubyProgress
176
176
  debug_log("spawned pid=#{pid} cmd=#{@command}")
177
177
 
178
178
  until reader.eof? || @stop
179
- next unless reader.wait_readable(0.1)
179
+ ready = if reader.respond_to?(:wait_readable)
180
+ reader.wait_readable(0.1)
181
+ else
182
+ IO.select([reader], nil, nil, 0.1)
183
+ end
184
+ next unless ready
180
185
 
181
186
  chunk = reader.read_nonblock(4096, exception: false)
182
187
  next if chunk.nil? || chunk.empty?
@@ -41,12 +41,17 @@ module RubyProgress
41
41
  def self.display_completion(message, success: true, show_checkmark: false, output_stream: :warn, icons: {})
42
42
  return unless message
43
43
 
44
- mark = if show_checkmark
45
- icon = success ? (icons[:success] || '✅') : (icons[:error] || '🛑')
46
- "#{icon} "
47
- else
48
- ''
49
- end
44
+ # Determine the mark to show. If checkmarks are enabled, prefer the
45
+ # default icons but allow overrides via icons hash. If checkmarks are not
46
+ # enabled, still show a custom icon when provided via CLI options.
47
+ mark = ''
48
+ if show_checkmark
49
+ icon = success ? (icons[:success] || '✅') : (icons[:error] || '🛑')
50
+ mark = "#{icon} "
51
+ else
52
+ custom_icon = success ? icons[:success] : icons[:error]
53
+ mark = custom_icon ? "#{custom_icon} " : ''
54
+ end
50
55
 
51
56
  formatted_message = "#{mark}#{message}"
52
57
 
@@ -2,11 +2,11 @@
2
2
 
3
3
  module RubyProgress
4
4
  # Main gem version
5
- VERSION = '1.3.4'
5
+ VERSION = '1.3.6'
6
6
 
7
7
  # Component-specific versions (patch bumps)
8
- WORM_VERSION = '1.1.4'
9
- TWIRL_VERSION = '1.1.4'
10
- RIPPLE_VERSION = '1.1.4'
11
- FILL_VERSION = '1.0.4'
8
+ WORM_VERSION = '1.1.6'
9
+ TWIRL_VERSION = '1.1.6'
10
+ RIPPLE_VERSION = '1.1.6'
11
+ FILL_VERSION = '1.0.6'
12
12
  end
@@ -0,0 +1,41 @@
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
+ spec.add_dependency 'tty-cursor', '~> 0.7'
33
+ spec.add_dependency 'tty-screen', '~> 0.8'
34
+
35
+ # Development dependencies
36
+ spec.add_development_dependency 'rake', '~> 13.0'
37
+ spec.add_development_dependency 'rspec', '~> 3.0'
38
+ # rubocop managed in Gemfile with version-specific constraints
39
+ spec.add_development_dependency 'simplecov', '~> 0.21'
40
+ spec.metadata['rubygems_mfa_required'] = 'true'
41
+ end
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'json'
5
+
6
+ # Read the SimpleCov results
7
+ data = JSON.parse(File.read('coverage/.resultset.json'))
8
+ files = data['RSpec']['coverage']
9
+
10
+ results = []
11
+ files.each do |path, coverage|
12
+ next unless path.include?('lib/ruby-progress') && !path.include?('/bin/')
13
+
14
+ lines = coverage['lines'] || []
15
+ total = lines.compact.count
16
+ next if total.zero?
17
+
18
+ covered = lines.count { |l| l&.positive? }
19
+ pct = (covered * 100.0 / total).round(1)
20
+ short_path = path.split('ruby-progress/').last
21
+ results << { path: short_path, pct: pct, covered: covered, total: total }
22
+ end
23
+
24
+ puts "\n=== Coverage Analysis by File ===\n\n"
25
+ puts "Files with lowest coverage (need attention):\n\n"
26
+
27
+ results.sort_by { |r| r[:pct] }.first(10).each do |r|
28
+ color = if r[:pct] < 30
29
+ "\e[31m"
30
+ else
31
+ r[:pct] < 60 ? "\e[33m" : "\e[32m"
32
+ end
33
+ reset = "\e[0m"
34
+ puts format("#{color}%6.1f%%#{reset} (%4d/%4d) %s", r[:pct], r[:covered], r[:total], r[:path])
35
+ end
36
+
37
+ puts "\n\nFiles with highest coverage:\n\n"
38
+ results.sort_by { |r| -r[:pct] }.first(10).each do |r|
39
+ color = "\e[32m"
40
+ reset = "\e[0m"
41
+ puts format("#{color}%6.1f%%#{reset} (%4d/%4d) %s", r[:pct], r[:covered], r[:total], r[:path])
42
+ end
43
+
44
+ total_lines = results.sum { |r| r[:total] }
45
+ total_covered = results.sum { |r| r[:covered] }
46
+ overall_pct = (total_covered * 100.0 / total_lines).round(2)
47
+
48
+ puts "\n\n=== Overall Coverage ===\n"
49
+ puts format('Total: %.2f%% (%d/%d lines)', overall_pct, total_covered, total_lines)
data/test_daemon.sh ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # Test daemon mode
4
+ echo "Starting twirl daemon..."
5
+ ./bin/prg twirl --daemon-as test_twirl &
6
+ sleep 2
7
+
8
+ echo "Checking if daemon is running..."
9
+ ps aux | grep "[p]rg twirl" || echo "No daemon found"
10
+
11
+ echo "Checking PID files..."
12
+ ls -la ~/.prg/*.pid 2>/dev/null || echo "No PID files found"
13
+
14
+ echo "Stopping daemon..."
15
+ ./bin/prg job send --daemon-name test_twirl
16
+ sleep 1
17
+
18
+ echo "Checking if daemon stopped..."
19
+ ps aux | grep "[p]rg twirl" || echo "Daemon stopped successfully"
20
+ ls -la ~/.prg/*.pid 2>/dev/null || echo "PID files cleaned up"
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.4
4
+ version: 1.3.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brett Terpstra
@@ -65,20 +65,6 @@ dependencies:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
67
  version: '3.0'
68
- - !ruby/object:Gem::Dependency
69
- name: rubocop
70
- requirement: !ruby/object:Gem::Requirement
71
- requirements:
72
- - - "~>"
73
- - !ruby/object:Gem::Version
74
- version: '1.21'
75
- type: :development
76
- prerelease: false
77
- version_requirements: !ruby/object:Gem::Requirement
78
- requirements:
79
- - - "~>"
80
- - !ruby/object:Gem::Version
81
- version: '1.21'
82
68
  - !ruby/object:Gem::Dependency
83
69
  name: simplecov
84
70
  requirement: !ruby/object:Gem::Requirement
@@ -112,9 +98,11 @@ files:
112
98
  - ".rubocop.yml"
113
99
  - ".rubocop_todo.yml"
114
100
  - CHANGELOG.md
101
+ - DAEMON_MODE.md
115
102
  - DEMO_SCRIPTS.md
116
103
  - Gemfile
117
104
  - Gemfile.lock
105
+ - JOB_CLI_REFACTOR.md
118
106
  - LICENSE
119
107
  - README.md
120
108
  - Rakefile
@@ -154,9 +142,12 @@ files:
154
142
  - lib/ruby-progress/worm.rb
155
143
  - quick_demo.rb
156
144
  - readme_demo.rb
145
+ - ruby-progress.gemspec
157
146
  - screencast
158
147
  - screencast.svg
148
+ - scripts/coverage_analysis.rb
159
149
  - scripts/run_matrix_mise.fish
150
+ - test_daemon.sh
160
151
  - test_daemon_interruption.rb
161
152
  - test_daemon_orphan.rb
162
153
  - test_worm_flags.rb