ruby-progress 1.3.1 → 1.3.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.
@@ -33,8 +33,13 @@ module RippleCLI
33
33
  )
34
34
  exit
35
35
  elsif options[:daemon]
36
- # For daemon mode, detach so shell has no tracked job
37
- PrgCLI.daemonize
36
+ # For daemon mode, detach so shell has no tracked job unless the user
37
+ # requested a non-detaching background child via --no-detach.
38
+ if options[:no_detach]
39
+ PrgCLI.backgroundize
40
+ else
41
+ PrgCLI.daemonize
42
+ end
38
43
 
39
44
  # For daemon mode, default message if none provided
40
45
  text = options[:message] || ARGV.join(' ')
@@ -63,13 +68,22 @@ module RippleCLI
63
68
  end
64
69
 
65
70
  def self.run_with_command(text, options)
66
- if $stdout.tty? && options[:output] == :stdout
67
- oc = RubyProgress::OutputCapture.new(command: options[:command], lines: options[:output_lines] || 3, position: options[:output_position] || :above)
71
+ if $stdout.tty?
72
+ # Interactive TTY: use PTY-based capture so the animation can run while the
73
+ # command executes. We only print captured stdout if options[:output] == :stdout.
74
+ oc = RubyProgress::OutputCapture.new(
75
+ command: options[:command],
76
+ lines: options[:output_lines] || 3,
77
+ position: options[:output_position] || :above,
78
+ stream: options[:output] == :stdout || options[:stdout_live]
79
+ )
68
80
  oc.start
69
81
 
70
- # Create rippler and attach output capture so redraw occurs each frame
82
+ # Create rippler. Attach output capture only when the user requested
83
+ # live stdout display via --stdout; otherwise start the PTY reader so
84
+ # we can collect the child's exit status but do not call redraw.
71
85
  rippler = RubyProgress::Ripple.new(text, options)
72
- rippler.instance_variable_set(:@output_capture, oc)
86
+ rippler.instance_variable_set(:@output_capture, oc) if options[:output] == :stdout
73
87
 
74
88
  thread = Thread.new { loop { rippler.advance } }
75
89
  oc.wait
@@ -77,17 +91,24 @@ module RippleCLI
77
91
 
78
92
  captured_lines = oc.lines
79
93
  captured_output = captured_lines.join("\n")
80
- success = true
94
+ success = oc.exit_status.nil? || oc.exit_status.zero?
81
95
  else
82
- # Fallback to legacy capture (non-interactive / CI)
96
+ # Non-interactive / CI: fallback to legacy synchronous capture
83
97
  captured_output = `#{options[:command]} 2>&1`
84
98
  success = $CHILD_STATUS.success?
85
99
  end
86
100
 
87
101
  puts captured_output if options[:output] == :stdout
102
+
88
103
  if options[:success_message] || options[:complete_checkmark]
89
104
  message = success ? options[:success_message] : options[:fail_message] || options[:success_message]
90
- RubyProgress::Ripple.complete(text, message, options[:complete_checkmark], success)
105
+ RubyProgress::Ripple.complete(
106
+ text,
107
+ message,
108
+ options[:complete_checkmark],
109
+ success,
110
+ icons: { success: options[:success_icon], error: options[:error_icon] }
111
+ )
91
112
  end
92
113
  exit success ? 0 : 1
93
114
  end
@@ -147,7 +168,8 @@ module RippleCLI
147
168
  message,
148
169
  success: success_val,
149
170
  show_checkmark: check,
150
- output_stream: :stdout
171
+ output_stream: :stdout,
172
+ icons: { success: options[:success_icon], error: options[:error_icon] }
151
173
  )
152
174
  end
153
175
  rescue StandardError
@@ -196,7 +218,8 @@ module RippleCLI
196
218
  job['message'],
197
219
  success: success,
198
220
  show_checkmark: job['checkmark'] || false,
199
- output_stream: :stdout
221
+ output_stream: :stdout,
222
+ icons: { success: options[:success_icon], error: options[:error_icon] }
200
223
  )
201
224
  end
202
225
 
@@ -71,6 +71,14 @@ module RippleCLI
71
71
  options[:success_message] = msg
72
72
  end
73
73
 
74
+ opts.on('--success-icon ICON', 'Custom success icon to show with completion messages') do |ic|
75
+ options[:success_icon] = ic
76
+ end
77
+
78
+ opts.on('--error-icon ICON', 'Custom error icon to show with failure messages') do |ic|
79
+ options[:error_icon] = ic
80
+ end
81
+
74
82
  opts.on('--error MESSAGE', 'Error message to display') do |msg|
75
83
  options[:fail_message] = msg
76
84
  end
@@ -83,6 +91,10 @@ module RippleCLI
83
91
  options[:output] = :stdout
84
92
  end
85
93
 
94
+ opts.on('--stdout-live', 'Stream captured output to STDOUT as it arrives (non-blocking)') do
95
+ options[:stdout_live] = true
96
+ end
97
+
86
98
  opts.on('--quiet', 'Suppress all output') do
87
99
  options[:output] = :quiet
88
100
  end
@@ -94,6 +106,10 @@ module RippleCLI
94
106
  options[:daemon] = true
95
107
  end
96
108
 
109
+ opts.on('--no-detach', 'When used with --daemon: run background child but do not fully detach from the terminal') do
110
+ options[:no_detach] = true
111
+ end
112
+
97
113
  opts.on('--pid-file FILE', 'Write process ID to file (default: /tmp/ruby-progress/progress.pid)') do |file|
98
114
  options[:pid_file] = file
99
115
  end
@@ -32,6 +32,12 @@ module TwirlCLI
32
32
  options[:style] = style
33
33
  end
34
34
 
35
+ opts.on('-d', '--direction DIRECTION', 'Animation direction (forward/bidirectional or f/b)') do |direction|
36
+ # Twirl is a spinner and doesn't visibly change with direction, but accept the
37
+ # flag for parity with other subcommands (Worm/Ripple) so scripts can use it.
38
+ options[:direction] = direction =~ /^f/i ? :forward_only : :bidirectional
39
+ end
40
+
35
41
  opts.on('--ends CHARS', 'Start/end characters (even number of chars, split in half)') do |chars|
36
42
  options[:ends] = chars
37
43
  end
@@ -55,6 +61,14 @@ module TwirlCLI
55
61
  options[:success] = text
56
62
  end
57
63
 
64
+ opts.on('--success-icon ICON', 'Custom success icon to show with completion messages') do |ic|
65
+ options[:success_icon] = ic
66
+ end
67
+
68
+ opts.on('--error-icon ICON', 'Custom error icon to show with failure messages') do |ic|
69
+ options[:error_icon] = ic
70
+ end
71
+
58
72
  opts.on('--error MESSAGE', 'Error message to display') do |text|
59
73
  options[:error] = text
60
74
  end
@@ -67,6 +81,10 @@ module TwirlCLI
67
81
  options[:stdout] = true
68
82
  end
69
83
 
84
+ opts.on('--stdout-live', 'Stream captured output to STDOUT as it arrives (non-blocking)') do
85
+ options[:stdout_live] = true
86
+ end
87
+
70
88
  opts.separator ''
71
89
  opts.separator 'Daemon Mode:'
72
90
 
@@ -74,6 +92,10 @@ module TwirlCLI
74
92
  options[:daemon] = true
75
93
  end
76
94
 
95
+ opts.on('--no-detach', 'When used with --daemon/--daemon-as: run background child but do not fully detach from the terminal') do
96
+ options[:no_detach] = true
97
+ end
98
+
77
99
  opts.on('--daemon-as NAME', 'Run in daemon mode with custom name (creates /tmp/ruby-progress/NAME.pid)') do |name|
78
100
  options[:daemon] = true
79
101
  options[:daemon_name] = name
@@ -23,11 +23,12 @@ module TwirlRunner
23
23
  RubyProgress::Utils.hide_cursor
24
24
  spinner_thread = Thread.new { loop { spinner.animate } }
25
25
 
26
- if $stdout.tty? && options[:stdout]
26
+ if $stdout.tty? && (options[:stdout] || options[:stdout_live])
27
27
  oc = RubyProgress::OutputCapture.new(
28
28
  command: options[:command],
29
29
  lines: options[:output_lines] || 3,
30
- position: options[:output_position] || :above
30
+ position: options[:output_position] || :above,
31
+ stream: options[:stdout] || options[:stdout_live]
31
32
  )
32
33
  oc.start
33
34
 
@@ -58,7 +59,8 @@ module TwirlRunner
58
59
  RubyProgress::Utils.display_completion(
59
60
  final_msg,
60
61
  success: success,
61
- show_checkmark: options[:checkmark]
62
+ show_checkmark: options[:checkmark],
63
+ icons: { success: options[:success_icon], error: options[:error_icon] }
62
64
  )
63
65
  end
64
66
 
@@ -78,7 +80,8 @@ module TwirlRunner
78
80
  RubyProgress::Utils.display_completion(
79
81
  options[:success] || 'Complete',
80
82
  success: true,
81
- show_checkmark: options[:checkmark]
83
+ show_checkmark: options[:checkmark],
84
+ icons: { success: options[:success_icon], error: options[:error_icon] }
82
85
  )
83
86
  end
84
87
  end
@@ -108,7 +111,8 @@ module TwirlRunner
108
111
  oc = RubyProgress::OutputCapture.new(
109
112
  command: job['command'],
110
113
  lines: options[:output_lines] || 3,
111
- position: options[:output_position] || :above
114
+ position: options[:output_position] || :above,
115
+ stream: options[:stdout] || options[:stdout_live]
112
116
  )
113
117
  oc.start
114
118
 
@@ -124,7 +128,8 @@ module TwirlRunner
124
128
  job['message'],
125
129
  success: success,
126
130
  show_checkmark: job['checkmark'] || false,
127
- output_stream: :stdout
131
+ output_stream: :stdout,
132
+ icons: { success: options[:success_icon], error: options[:error_icon] }
128
133
  )
129
134
  end
130
135
 
@@ -152,7 +157,8 @@ module TwirlRunner
152
157
  message,
153
158
  success: success_val,
154
159
  show_checkmark: check,
155
- output_stream: :stdout
160
+ output_stream: :stdout,
161
+ icons: { success: options[:success_icon], error: options[:error_icon] }
156
162
  )
157
163
  end
158
164
  rescue StandardError
@@ -10,9 +10,23 @@ require_relative '../utils'
10
10
  class TwirlSpinner
11
11
  def initialize(message, options = {})
12
12
  @message = message
13
- @style = parse_style(options[:style] || 'dots')
14
13
  @speed = parse_speed(options[:speed] || 'medium')
15
- @frames = RubyProgress::INDICATORS[@style] || RubyProgress::INDICATORS[:dots]
14
+
15
+ style_opt = options[:style].to_s
16
+ if style_opt.start_with?('custom=')
17
+ chars = style_opt.sub('custom=', '')
18
+ @frames = if chars.length >= 3
19
+ chars.chars
20
+ elsif chars.length == 2
21
+ [chars[0], chars[1], chars[1]]
22
+ else
23
+ [chars, chars, chars]
24
+ end
25
+ @style = :custom
26
+ else
27
+ @style = parse_style(style_opt.empty? ? 'dots' : style_opt)
28
+ @frames = RubyProgress::INDICATORS[@style] || RubyProgress::INDICATORS[:dots]
29
+ end
16
30
  @start_chars, @end_chars = RubyProgress::Utils.parse_ends(options[:ends])
17
31
  @index = 0
18
32
  end
@@ -36,6 +50,8 @@ class TwirlSpinner
36
50
 
37
51
  style_lower = style_input.to_s.downcase.strip
38
52
 
53
+ # parse_style returns a symbol key for RubyProgress::INDICATORS
54
+
39
55
  indicator_keys = RubyProgress::INDICATORS.keys.map(&:to_s)
40
56
  return style_lower.to_sym if indicator_keys.include?(style_lower)
41
57
 
@@ -33,8 +33,14 @@ module WormCLI
33
33
  )
34
34
  exit
35
35
  elsif options[:daemon]
36
- # Detach before starting daemon logic so there's no tracked shell job
37
- PrgCLI.daemonize
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
43
+
38
44
  run_daemon_mode(options)
39
45
  else
40
46
  progress = RubyProgress::Worm.new(options)
@@ -70,7 +76,8 @@ module WormCLI
70
76
  command: job['command'],
71
77
  lines: options[:output_lines] || 3,
72
78
  position: options[:output_position] || :above,
73
- log_path: log_path
79
+ log_path: log_path,
80
+ stream: options[:stdout] || options[:stdout_live]
74
81
  )
75
82
  oc.start
76
83
 
@@ -86,7 +93,8 @@ module WormCLI
86
93
  job['message'],
87
94
  success: success,
88
95
  show_checkmark: job['checkmark'] || false,
89
- output_stream: :stdout
96
+ output_stream: :stdout,
97
+ icons: { success: options[:success_icon], error: options[:error_icon] }
90
98
  )
91
99
  end
92
100
 
@@ -99,7 +107,8 @@ module WormCLI
99
107
  progress.run_daemon_mode(
100
108
  success_message: options[:success],
101
109
  show_checkmark: options[:checkmark],
102
- control_message_file: RubyProgress::Daemon.control_message_file(pid_file)
110
+ control_message_file: RubyProgress::Daemon.control_message_file(pid_file),
111
+ icons: { success: options[:success_icon], error: options[:error_icon] }
103
112
  )
104
113
  ensure
105
114
  job_thread&.kill
@@ -63,6 +63,14 @@ module WormCLI
63
63
  options[:success] = text
64
64
  end
65
65
 
66
+ opts.on('--success-icon ICON', 'Custom success icon to show with completion messages') do |ic|
67
+ options[:success_icon] = ic
68
+ end
69
+
70
+ opts.on('--error-icon ICON', 'Custom error icon to show with failure messages') do |ic|
71
+ options[:error_icon] = ic
72
+ end
73
+
66
74
  opts.on('--error MESSAGE', 'Error message to display') do |text|
67
75
  options[:error] = text
68
76
  end
@@ -75,6 +83,10 @@ module WormCLI
75
83
  options[:stdout] = true
76
84
  end
77
85
 
86
+ opts.on('--stdout-live', 'Stream captured output to STDOUT as it arrives (non-blocking)') do
87
+ options[:stdout_live] = true
88
+ end
89
+
78
90
  opts.separator ''
79
91
  opts.separator 'Daemon Mode:'
80
92
 
@@ -93,6 +105,10 @@ module WormCLI
93
105
  options[:daemon_name] = name
94
106
  end
95
107
 
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
+
96
112
  opts.on('--pid-file FILE', 'Write process ID to file (default: /tmp/ruby-progress/progress.pid)') do |file|
97
113
  options[:pid_file] = file
98
114
  end
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- # rubocop:disable Metrics/ModuleLength
5
4
  require 'open3'
6
5
  require 'json'
7
6
  require_relative '../utils'
@@ -58,11 +57,12 @@ module WormRunner
58
57
  stdout_content = nil
59
58
 
60
59
  begin
61
- stdout_content = if $stdout.tty? && @output_stdout
60
+ stdout_content = if $stdout.tty? && (@output_stdout || @output_live)
62
61
  oc = RubyProgress::OutputCapture.new(
63
62
  command: @command,
64
63
  lines: @output_lines || 3,
65
- position: @output_position || :above
64
+ position: @output_position || :above,
65
+ stream: @output_live || false
66
66
  )
67
67
  oc.start
68
68
  @output_capture = oc
@@ -120,7 +120,7 @@ module WormRunner
120
120
  @running = false
121
121
  end
122
122
 
123
- def run_daemon_mode(success_message: nil, show_checkmark: false, control_message_file: nil)
123
+ def run_daemon_mode(success_message: nil, show_checkmark: false, control_message_file: nil, icons: {})
124
124
  @running = true
125
125
  stop_requested = false
126
126
 
@@ -163,7 +163,8 @@ module WormRunner
163
163
  final_message,
164
164
  success: final_success,
165
165
  show_checkmark: final_checkmark,
166
- output_stream: :stdout
166
+ output_stream: :stdout,
167
+ icons: icons
167
168
  )
168
169
  end
169
170
 
@@ -233,8 +234,12 @@ module WormRunner
233
234
  $stderr.print "\r\e[2K"
234
235
 
235
236
  if (frame_count % 10).zero?
236
- $stderr.print "\e[1A\e[2K"
237
- $stderr.print "\r"
237
+ # Use ANSI save/restore to clear the previous line without moving the
238
+ # global cursor position. This prevents the animation from erasing
239
+ # reserved output lines that we draw elsewhere.
240
+ $stderr.print "\e7" # save
241
+ $stderr.print "\e[1A\e[2K\r"
242
+ $stderr.print "\e8" # restore
238
243
  end
239
244
 
240
245
  $stderr.print "#{message_part}#{generate_dots(position, direction)}"
@@ -278,5 +283,3 @@ module WormRunner
278
283
  dots.join
279
284
  end
280
285
  end
281
-
282
- # rubocop:enable Metrics/ModuleLength
@@ -95,7 +95,7 @@ module RubyProgress
95
95
  end
96
96
 
97
97
  # Complete the progress bar and show success message
98
- def complete(message = nil)
98
+ def complete(message = nil, icons: {})
99
99
  @current_progress = @length
100
100
  render
101
101
 
@@ -105,7 +105,8 @@ module RubyProgress
105
105
  completion_message,
106
106
  success: true,
107
107
  show_checkmark: true,
108
- output_stream: :warn
108
+ output_stream: :warn,
109
+ icons: icons
109
110
  )
110
111
  else
111
112
  $stderr.puts # Just add a newline if no message
@@ -124,7 +125,8 @@ module RubyProgress
124
125
  error_msg,
125
126
  success: false,
126
127
  show_checkmark: true,
127
- output_stream: :warn
128
+ output_stream: :warn,
129
+ icons: {}
128
130
  )
129
131
  end
130
132