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

Returns a new instance of OutputCapture.



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/ruby-progress/output_capture.rb', line 21

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.



19
20
21
# File 'lib/ruby-progress/output_capture.rb', line 19

def exit_status
  @exit_status
end

Instance Method Details

#alive?Boolean

Returns:

  • (Boolean)


85
86
87
# File 'lib/ruby-progress/output_capture.rb', line 85

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

#flush_to(io = $stdout) ⇒ Object

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.



157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/ruby-progress/output_capture.rb', line 157

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

#linesObject



81
82
83
# File 'lib/ruby-progress/output_capture.rb', line 81

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

#redraw(io = $stderr) ⇒ Object

Redraw the reserved area using the current buffered lines.



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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/ruby-progress/output_capture.rb', line 90

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.



66
67
68
69
70
# File 'lib/ruby-progress/output_capture.rb', line 66

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

#stopObject



72
73
74
75
# File 'lib/ruby-progress/output_capture.rb', line 72

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

#waitObject



77
78
79
# File 'lib/ruby-progress/output_capture.rb', line 77

def wait
  @reader_thread&.join
end