Class: RubyProgress::OutputCapture

Inherits:
Object
  • Object
show all
Defined in:
lib/ruby-progress/output_capture.rb

Overview

PTY-based live output capture that reserves a small terminal area for printing captured output while the animation draws elsewhere.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(command:, lines: 3, position: :above, log_path: nil, stream: false, debug: nil) ⇒ OutputCapture

Create a new OutputCapture instance.

Examples:

oc = RubyProgress::OutputCapture.new(command: 'bundle exec rspec', lines: 4, position: :below)
oc.start
oc.wait
oc.flush_to($stdout)

Parameters:

  • command (String)

    the shell command to spawn and capture via PTY

  • lines (Integer) (defaults to: 3)

    number of reserved lines to keep for captured output (minimum 1)

  • position (Symbol, String) (defaults to: :above)

    :above/:below (or :top/:bottom) to place the reserved area

  • log_path (String, nil) (defaults to: nil)

    optional path to append raw captured output

  • stream (Boolean) (defaults to: false)

    when true, redraw captured output live into the terminal area

  • debug (Boolean, nil) (defaults to: nil)

    enable debug logging when true; nil will consult ENV



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/ruby-progress/output_capture.rb', line 74

def initialize(command:, lines: 3, position: :above, log_path: nil, stream: false, debug: nil)
  @command = command
  # Coerce lines into a positive Integer
  @lines = (lines || 3).to_i
  @lines = 1 if @lines < 1

  # Normalize position (accept :top/:bottom or :above/:below or strings)
  pos = position.respond_to?(:to_sym) ? position.to_sym : position
  @position = case pos
              when :top, 'top' then :above
              when :bottom, 'bottom' then :below
              when :above, 'above' then :above
              when :below, 'below' then :below
              else
                :above
              end

  @buffer = []
  @buf_mutex = Mutex.new
  @stop = false
  @log_path = log_path
  @log_file = nil
  @stream = stream

  @debug = if debug.nil?
             ENV.fetch('RUBY_PROGRESS_DEBUG', nil) && ENV['RUBY_PROGRESS_DEBUG'] != '0'
           else
             debug
           end
  @debug_path = '/tmp/ruby-progress-debug.log'

  if @debug
    begin
      FileUtils.mkdir_p(File.dirname(@debug_path))
      File.open(@debug_path, 'w') { |f| f.puts("debug start: #{Time.now}") }
    rescue StandardError
      @debug = false
    end
  end

  # Debug: log init if requested via ENV or explicit debug flag
  debug_log("init: position=#{@position.inspect}; lines=#{@lines}")
end

Instance Attribute Details

#exit_statusObject (readonly)

Returns the value of attribute exit_status.



58
59
60
# File 'lib/ruby-progress/output_capture.rb', line 58

def exit_status
  @exit_status
end

Instance Method Details

#alive?Boolean

Return true when the background reader thread is alive.

Returns:

  • (Boolean)


154
155
156
# File 'lib/ruby-progress/output_capture.rb', line 154

def alive?
  @reader_thread&.alive? || false
end

#flush_to(io = $stdout) ⇒ void

This method returns an undefined value.

Flush the buffered lines to the given IO (defaults to STDOUT). This is used when capturing non-live output: capture silently during the run and emit all captured output at the end.

Parameters:

  • io (IO) (defaults to: $stdout)

    the IO to write captured lines to (defaults to STDOUT)



232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/ruby-progress/output_capture.rb', line 232

def flush_to(io = $stdout)
  buf = lines
  return if buf.empty?

  begin
    buf.each do |line|
      io.puts(line)
    end
    io.flush
  rescue StandardError => e
    debug_log("flush_to error: #{e.class}: #{e.message}")
  end
end

#linesArray<String>

Return a snapshot of the currently buffered lines.

Returns:



148
149
150
# File 'lib/ruby-progress/output_capture.rb', line 148

def lines
  @buf_mutex.synchronize { @buffer.dup }
end

#redraw(io = $stderr) ⇒ void

This method returns an undefined value.

Redraw the reserved area using the current buffered lines.

Parameters:

  • io (IO) (defaults to: $stderr)

    the IO stream to draw into (defaults to $stderr)



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/ruby-progress/output_capture.rb', line 162

def redraw(io = $stderr)
  buf = lines
  debug_log("redraw called; buffer=#{buf.size}; lines=#{@lines}; position=#{@position}")

  # If not streaming live to the terminal, don't redraw during capture.
  return unless @stream

  cols = if defined?(TTY::Screen)
           TTY::Screen.columns
         else
           IO.console.winsize[1]
         end

  display_lines = Array.new(@lines, '')
  if buf.empty?
    # leave display_lines as blanks
  elsif buf.size <= @lines
    buf.each_with_index { |l, i| display_lines[i] = l.to_s }
  else
    buf.last(@lines).each_with_index { |l, i| display_lines[i] = l.to_s }
  end

  if defined?(TTY::Cursor)
    cursor = TTY::Cursor
    io.print cursor.save

    if @position == :above
      io.print cursor.up(@lines)
    else
      io.print cursor.down(1)
    end

    display_lines.each_with_index do |line, idx|
      io.print cursor.clear_line
      io.print line[0, cols]
      io.print cursor.down(1) unless idx == display_lines.length - 1
    end

    io.print cursor.restore
    debug_log('redraw finished (TTY)')
  else
    io.print "\e7"

    if @position == :above
      io.print "\e[#{@lines}A"
    else
      io.print "\e[1B"
    end

    display_lines.each_with_index do |line, idx|
      io.print "\e[2K\r"
      io.print line[0, cols]
      io.print "\e[1B" unless idx == display_lines.length - 1
    end

    io.print "\e8"
    debug_log('redraw finished (ANSI)')
  end

  io.flush
rescue StandardError => e
  debug_log("redraw error: #{e.class}: #{e.message}")
end

#startObject

Start capturing the child process. Returns self.

This spawns the configured command in a PTY and begins a background reader thread which buffers the most recent lines. When stream is true the captured lines are redrawn into the terminal area reserved by #reserve_space.



124
125
126
127
128
# File 'lib/ruby-progress/output_capture.rb', line 124

def start
  OutputUI.reserve_space($stderr, @position, @lines) if @stream
  @reader_thread = Thread.new { spawn_and_read }
  self
end

#stopvoid

This method returns an undefined value.

Signal the reader thread to stop and wait for it to finish.



132
133
134
135
# File 'lib/ruby-progress/output_capture.rb', line 132

def stop
  @stop = true
  @reader_thread&.join
end

#waitThread?

Wait for the background reader thread to finish and return control to the caller. This is a simple join wrapper used by callers that need to block until the captured command completes.

Returns:

  • (Thread, nil)

    the joined thread or nil if not started



142
143
144
# File 'lib/ruby-progress/output_capture.rb', line 142

def wait
  @reader_thread&.join
end