Class: Rubino::UI::StdoutProxy

Inherits:
Object
  • Object
show all
Defined in:
lib/rubino/ui/stdout_proxy.rb

Overview

An IO-shaped shim that routes everything written to it through a BottomComposer#print_above, so the ~30 existing $stdout.print/puts call sites across UI::CLI / PrinterBase need ZERO changes. While a turn is active, the chat command swaps $stdout for one of these (prompt_toolkit’s StdoutProxy model); on turn end it swaps the real IO back.

Line buffering — the critical streaming nuance:

UI::CLI#stream emits PARTIAL tokens with NO trailing newline during model
streaming. A naive "print each write above the prompt" would scroll every
token onto its own row. Instead we hold the in-progress line in
+@partial+ and re-render it (the accumulating line) ABOVE the composer via
{BottomComposer#set_partial} as it grows — a transient row redrawn in
place — committing it to scrollback (via {BottomComposer#print_above})
only when a newline arrives. The way prompt_toolkit buffers and batches:
each newline-terminated segment becomes one committed row; the trailing
partial keeps showing live.

The render mutex lives in the composer, so concurrent writes from the streaming thread and keystroke redraws stay serialized.

Instance Method Summary collapse

Constructor Details

#initialize(composer) ⇒ StdoutProxy

Returns a new instance of StdoutProxy.

Parameters:



26
27
28
29
# File 'lib/rubino/ui/stdout_proxy.rb', line 26

def initialize(composer)
  @composer = composer
  @partial  = +""
end

Instance Method Details

#<<(obj) ⇒ Object



71
72
73
74
# File 'lib/rubino/ui/stdout_proxy.rb', line 71

def <<(obj)
  append(obj.to_s)
  self
end

#closeObject

A faithful IO duck MUST answer #close: stdlib Logger::LogDevice treats a logdev that responds to :write but NOT :close as a FILENAME and does File.open(it) → “no implicit conversion of StdoutProxy into String” if a Logger is ever built against $stdout while we hold the swap. No-op close.



118
# File 'lib/rubino/ui/stdout_proxy.rb', line 118

def close; end

#closed?Boolean

Returns:

  • (Boolean)


119
# File 'lib/rubino/ui/stdout_proxy.rb', line 119

def closed? = false

#filenoObject



112
# File 'lib/rubino/ui/stdout_proxy.rb', line 112

def fileno = nil

#finishObject

Commit any held partial line as a final row. Called when the proxy is torn down so an unterminated last line (e.g. a stream that ended without stream_end) isn’t lost.



100
101
102
103
104
105
106
# File 'lib/rubino/ui/stdout_proxy.rb', line 100

def finish
  return if @partial.empty?

  line = @partial
  @partial = +""
  @composer.print_above(line)
end

#flushObject

Streaming writers call flush after each token. We treat flush as “show what you have now”: re-render the accumulating partial line above the composer so streamed text appears live, without committing it to scrollback (it has no newline yet).



80
81
82
83
# File 'lib/rubino/ui/stdout_proxy.rb', line 80

def flush
  render_partial
  self
end

#isattyObject



110
# File 'lib/rubino/ui/stdout_proxy.rb', line 110

def isatty = false

#live(str) ⇒ Object

REPLACE the live region with str (replace, not accumulate). The normal #append path GROWS @partial — right for token-by-token line buffering, but wrong for the streaming-markdown tail, which is the WHOLE in-progress block re-shown each time it changes. So we reset our own buffer and hand the raw tail straight to the composer’s transient row. Used by UI::CLI#stream to show the incomplete block live while completed blocks commit above it.



91
92
93
94
95
# File 'lib/rubino/ui/stdout_proxy.rb', line 91

def live(str)
  @partial = +""
  @composer.set_partial(str.to_s)
  self
end


40
41
42
43
# File 'lib/rubino/ui/stdout_proxy.rb', line 40

def print(*args)
  args.each { |a| append(a.to_s) }
  nil
end

#printf(format) ⇒ Object



66
67
68
69
# File 'lib/rubino/ui/stdout_proxy.rb', line 66

def printf(format, *)
  append(format(format, *))
  nil
end

#puts(*args) ⇒ Object



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/rubino/ui/stdout_proxy.rb', line 45

def puts(*args)
  if args.empty?
    append("\n")
  else
    args.each do |a|
      if a.is_a?(Array)
        a.each { |e| puts(e) }
      else
        # Append the line and its terminating newline in ONE append so the
        # text commits straight to scrollback. Appending them separately
        # showed the line as a TRANSIENT partial row below the subagent
        # cards for a frame before the commit moved it above them — the
        # user saw the same line twice around the live card block (#153).
        s = a.to_s
        append(s.end_with?("\n") ? s : "#{s}\n")
      end
    end
  end
  nil
end

#syncObject



111
# File 'lib/rubino/ui/stdout_proxy.rb', line 111

def sync   = true

#sync=(_) ⇒ Object



121
122
123
# File 'lib/rubino/ui/stdout_proxy.rb', line 121

def sync=(_)
  true
end

#tty?Boolean

Best-effort IO compatibility for code that probes the stream.

Returns:

  • (Boolean)


109
# File 'lib/rubino/ui/stdout_proxy.rb', line 109

def tty?   = false

#write(*args) ⇒ Object

The two methods UI code actually uses are #print and #puts; #write backs both formattings and is also what e.g. StringIO/IO duck-typers call.



33
34
35
36
37
38
# File 'lib/rubino/ui/stdout_proxy.rb', line 33

def write(*args)
  args.sum do |a|
    append(a.to_s)
    a.to_s.bytesize
  end
end