Class: Rubino::UI::StdoutProxy
- Inherits:
-
Object
- Object
- Rubino::UI::StdoutProxy
- 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
- #<<(obj) ⇒ Object
-
#close ⇒ Object
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.
- #closed? ⇒ Boolean
- #fileno ⇒ Object
-
#finish ⇒ Object
Commit any held partial line as a final row.
-
#flush ⇒ Object
Streaming writers call flush after each token.
-
#initialize(composer) ⇒ StdoutProxy
constructor
A new instance of StdoutProxy.
- #isatty ⇒ Object
-
#live(str) ⇒ Object
REPLACE the live region with
str(replace, not accumulate). - #print(*args) ⇒ Object
- #printf(format) ⇒ Object
- #puts(*args) ⇒ Object
- #sync ⇒ Object
- #sync=(_) ⇒ Object
-
#tty? ⇒ Boolean
Best-effort IO compatibility for code that probes the stream.
-
#write(*args) ⇒ Object
The two methods UI code actually uses are #print and #puts; #write backs both formattings and is also what e.g.
Constructor Details
#initialize(composer) ⇒ StdoutProxy
Returns a new instance of StdoutProxy.
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 |
#close ⇒ Object
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
119 |
# File 'lib/rubino/ui/stdout_proxy.rb', line 119 def closed? = false |
#fileno ⇒ Object
112 |
# File 'lib/rubino/ui/stdout_proxy.rb', line 112 def fileno = nil |
#finish ⇒ Object
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 |
#flush ⇒ Object
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 |
#isatty ⇒ Object
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 |
#print(*args) ⇒ Object
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 |
#sync ⇒ Object
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.
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 |