Class: Rubino::UI::PrinterBase

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

Overview

Shared printing behaviour for terminal-based UI adapters.

Subclasses must implement #color_for(role) returning a Pastel method name (e.g. :cyan, :green) so that message formatting stays here while each adapter controls its own color scheme.

Direct Known Subclasses

CLI

Instance Method Summary collapse

Methods inherited from Base

#ask, #assistant_text, #blocking_human_input?, #body, #box_close, #box_open, #confirm, #confirm_destructive, #hint_row, #input_injected, #interactive?, #note, #panel_line, #queued, #replay_user_input, #select, #separator, #table, #thinking_started, #tool_body, #turn_interrupted

Constructor Details

#initializePrinterBase

Returns a new instance of PrinterBase.



13
14
15
# File 'lib/rubino/ui/printer_base.rb', line 13

def initialize
  @pastel = Pastel.new
end

Instance Method Details

#blank_lineObject



58
# File 'lib/rubino/ui/printer_base.rb', line 58

def blank_line = emit_blank

#compression_finished(metadata, at: nil) ⇒ Object



49
50
51
52
# File 'lib/rubino/ui/printer_base.rb', line 49

def compression_finished(, at: nil)
  saved = [:saved_tokens] || 0
  puts_colored(color_for(:muted), "  ⟳ Context compacted (saved #{saved} tokens)")
end

#compression_started(at: nil) ⇒ Object



45
46
47
# File 'lib/rubino/ui/printer_base.rb', line 45

def compression_started(at: nil)
  puts_colored(color_for(:muted), "  ⟳ Compacting context...")
end

#emit(text, style: nil) ⇒ Object Also known as: emit_line

PATH 1. Untrusted text → strip ALL escapes → apply style → write. style is a semantic Pastel method symbol (:dim, :cyan, :red, …), an Array of them for a compound decoration (e.g. [:red, :bold]), or nil for no colour. The text is treated as hostile; escapes become visible caret notation, and the style is applied AFTER sanitizing so it can only wrap already-inert text.



110
111
112
113
# File 'lib/rubino/ui/printer_base.rb', line 110

def emit(text, style: nil)
  safe = Rubino::Util::Output.sanitize_terminal(text.to_s)
  write_line(style ? @pastel.decorate(safe, *Array(style)) : safe)
end

#emit_blankObject

A blank line. Routed through the funnel so $stdout stays private to it.



125
# File 'lib/rubino/ui/printer_base.rb', line 125

def emit_blank = write_line

#emit_frame(raw) ⇒ Object

A rubino-built CURSOR-CONTROL frame for the live region / status spinner / stream tail (Cat 4 — the hot path). These legitimately carry rubino’s OWN cursor escapes (‘r`, `e[2K`, cursor moves) that a defang would strip, so this writes raw THROUGH the single $stdout seam WITHOUT stripping cursor control, then flushes (transient frames must paint immediately — they are not committed lines).

CONTRACT: the caller has ALREADY defanged every UNTRUSTED span it interpolated (model tail text via #sanitize_terminal at #margined_tail / #show_reasoning_tail; the status label/hint via #safe at build time). Only rubino’s own frame escapes pass here. This exists so even the live/stream writes go through ONE seam — there is no direct $stdout.print left in the render path — without changing the print+flush timing the smooth-cadence measurement depends on.



161
162
163
# File 'lib/rubino/ui/printer_base.rb', line 161

def emit_frame(raw)
  write_raw(raw.to_s)
end

#emit_glyph(prefix, body, style: nil) ⇒ Object

PATH 1 (compose). A TRUSTED rubino-built prefix (a coloured glyph rubino chose, e.g. ‘@pastel.cyan(“●”)`) + an UNTRUSTED body that gets the full PATH-1 defang before its own style wrap. The two are joined and written through the single seam.

Cat 2 of the phase-2 migration: the ‘● <name>` activity/delegation rows interpolate a trusted cyan glyph next to a model-chosen name/preview. A plain #emit(“#glyph #name”) would defang the glyph’s OWN colour (the caret leak); #emit_styled(“#glyph #Rubino::UI::PrinterBase.@pastel@pastel.dim(name)”) would KEEP the untrusted name’s SGR (the injection leak). This composes correctly: the glyph keeps its trusted colour, the body is stripped of every escape and THEN wrapped in style, and the join is written verbatim — the glyph’s SGR and the body style are the only escapes that survive. prefix must be rubino-built (never untrusted); body is always treated as hostile.



141
142
143
144
145
# File 'lib/rubino/ui/printer_base.rb', line 141

def emit_glyph(prefix, body, style: nil)
  safe = Rubino::Util::Output.sanitize_terminal(body.to_s)
  styled = style ? @pastel.decorate(safe, *Array(style)) : safe
  write_line("#{prefix}#{styled}")
end

#emit_styled(prebuilt) ⇒ Object

PATH 2. rubino’s OWN pre-built styled prebuilt → strip dangerous control bytes, KEEP rubino’s SGR colour → write. For markdown render output, the live region, and rows that interpolate a rubino-coloured glyph. NEVER pass untrusted text here.



120
121
122
# File 'lib/rubino/ui/printer_base.rb', line 120

def emit_styled(prebuilt)
  write_line(Rubino::Util::Output.sanitize_terminal_keep_sgr(prebuilt.to_s))
end

#error(message) ⇒ Object



20
# File 'lib/rubino/ui/printer_base.rb', line 20

def error(message)   = puts_colored(color_for(:error),   "#{message}")

#info(message) ⇒ Object



17
# File 'lib/rubino/ui/printer_base.rb', line 17

def info(message)    = puts_colored(color_for(:info), message)

#job_enqueued(_type) ⇒ Object



54
# File 'lib/rubino/ui/printer_base.rb', line 54

def job_enqueued(_type) = nil

#job_finished(_type) ⇒ Object



56
# File 'lib/rubino/ui/printer_base.rb', line 56

def job_finished(_type) = nil

#job_started(_type) ⇒ Object



55
# File 'lib/rubino/ui/printer_base.rb', line 55

def job_started(_type)  = nil

#mode_changed(name, previous: nil) ⇒ Object

Default fallback. CLI overrides to render the ‘┄ HH:MM · mode → plan ┄` free-line variant.



62
63
64
65
# File 'lib/rubino/ui/printer_base.rb', line 62

def mode_changed(name, previous: nil)
  arrow = previous && previous != name ? " #{previous}#{name}" : " #{name}"
  puts_colored(color_for(:muted), "  ⟳ mode#{arrow}")
end

#status(message) ⇒ Object



21
# File 'lib/rubino/ui/printer_base.rb', line 21

def status(message)  = puts_colored(color_for(:status),  message)

#stream(chunk) ⇒ Object



23
24
25
26
27
28
29
30
# File 'lib/rubino/ui/printer_base.rb', line 23

def stream(chunk)
  # The streamed chunk is UNTRUSTED model text printed with NO trailing
  # newline (incremental). Defang it here (Cat 4 contract: the caller
  # neutralizes untrusted spans), then write through #emit_frame's single
  # no-newline + flush seam — so even this base streaming path no longer
  # touches $stdout directly.
  emit_frame(Rubino::Util::Output.sanitize_terminal(chunk[:text].to_s))
end

#stream_endObject



32
33
34
# File 'lib/rubino/ui/printer_base.rb', line 32

def stream_end
  emit_blank
end

#success(message) ⇒ Object



18
# File 'lib/rubino/ui/printer_base.rb', line 18

def success(message) = puts_colored(color_for(:success), "#{message}")

#tool_finished(name, result: nil) ⇒ Object



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

def tool_finished(name, result: nil)
  suffix = result ? " (#{result.truncated_preview})" : ""
  puts_colored(color_for(:tool), "#{name} done#{suffix}")
end

#tool_started(name, arguments: nil, at: nil, call_id: nil) ⇒ Object



36
37
38
# File 'lib/rubino/ui/printer_base.rb', line 36

def tool_started(name, arguments: nil, at: nil, call_id: nil)
  puts_colored(color_for(:tool), "  → Running tool: #{name}")
end

#warning(message) ⇒ Object



19
# File 'lib/rubino/ui/printer_base.rb', line 19

def warning(message) = puts_colored(color_for(:warning), "#{message}")