Class: RubyTUI::AnsiBackend
- Inherits:
-
Object
- Object
- RubyTUI::AnsiBackend
- Includes:
- Backend
- Defined in:
- lib/rubytui/backends/ansi_backend.rb
Overview
Backend that drives a real terminal with ANSI escape sequences.
Output accumulates in an internal string buffer and is written to the output IO only by #flush. #enter_alternate_screen, #leave_alternate_screen, #enable_mouse, #disable_mouse and #clear_all_images! flush immediately.
Constant Summary collapse
- CSI =
Control Sequence Introducer (ESC [) that prefixes escape sequences.
"\e["
Instance Attribute Summary collapse
-
#cell_pixel_size ⇒ Array(Integer, Integer)?
readonly
Cached cell dimensions [width_px, height_px], or nil until #query_cell_pixel_size has run.
Instance Method Summary collapse
-
#clear ⇒ void
Append the clear-screen sequence (CSI 2J) to the output buffer.
-
#clear_all_images! ⇒ void
Delete all Kitty graphics protocol images.
-
#delete_kitty_image(placement) ⇒ void
Delete a Kitty graphics protocol image by ID.
-
#disable_mouse ⇒ void
Disable mouse event reporting.
-
#disable_raw_mode ⇒ void
Restore the input's terminal state saved by #enable_raw_mode.
-
#draw(changes) ⇒ void
Append escape sequences for the changed cells to the output buffer.
-
#draw_kitty_image(placement) ⇒ void
Emit a Kitty graphics protocol image at the given placement.
-
#enable_mouse ⇒ void
Enable SGR extended mouse mode (button events + drag + scroll).
-
#enable_raw_mode ⇒ void
Put the input into raw mode, saving its terminal state for #disable_raw_mode.
-
#enter_alternate_screen ⇒ void
Switch to the alternate screen buffer (CSI ?1049h) and flush.
-
#flush ⇒ void
Write the buffered output to the output IO and flush it.
-
#hide_cursor ⇒ void
Append the hide-cursor sequence (CSI ?25l) to the output buffer.
-
#initialize(output: $stdout, input: $stdin) ⇒ AnsiBackend
constructor
A new instance of AnsiBackend.
-
#leave_alternate_screen ⇒ void
Switch back from the alternate screen buffer (CSI ?1049l) and flush.
-
#move_cursor(x, y) ⇒ void
Append a cursor-position sequence to the output buffer.
-
#query_cell_pixel_size ⇒ Array(Integer, Integer)
Query the terminal for cell pixel dimensions.
-
#show_cursor ⇒ void
Append the show-cursor sequence (CSI ?25h) to the output buffer.
-
#size ⇒ Array(Integer, Integer)
Get the output terminal size.
Constructor Details
#initialize(output: $stdout, input: $stdin) ⇒ AnsiBackend
Returns a new instance of AnsiBackend.
23 24 25 26 27 28 29 30 31 32 |
# File 'lib/rubytui/backends/ansi_backend.rb', line 23 def initialize(output: $stdout, input: $stdin) @output = output @input = input @buffer = String.new(capacity: 4096) @prev_style = nil @original_state = nil @next_image_id = 1 @image_id_map = {} @cell_pixel_size = nil end |
Instance Attribute Details
#cell_pixel_size ⇒ Array(Integer, Integer)? (readonly)
Returns cached cell dimensions [width_px, height_px], or nil until #query_cell_pixel_size has run.
240 241 242 |
# File 'lib/rubytui/backends/ansi_backend.rb', line 240 def cell_pixel_size @cell_pixel_size end |
Instance Method Details
#clear ⇒ void
This method returns an undefined value.
Append the clear-screen sequence (CSI 2J) to the output buffer.
103 104 105 |
# File 'lib/rubytui/backends/ansi_backend.rb', line 103 def clear @buffer << "#{CSI}2J" end |
#clear_all_images! ⇒ void
This method returns an undefined value.
Delete all Kitty graphics protocol images.
Nuclear option for clearing all image overlays. Useful during transitions or when precise tracking isn't possible. Forgets all tracked image IDs and flushes immediately.
316 317 318 319 320 |
# File 'lib/rubytui/backends/ansi_backend.rb', line 316 def clear_all_images! @buffer << "\e_Ga=d,d=A\e\\" @image_id_map.clear flush end |
#delete_kitty_image(placement) ⇒ void
This method returns an undefined value.
Delete a Kitty graphics protocol image by ID.
Sends a delete command targeting the specific image ID, ensuring precise removal without affecting other images. Appends to the output buffer; does not flush.
296 297 298 299 300 301 302 303 304 305 306 307 |
# File 'lib/rubytui/backends/ansi_backend.rb', line 296 def delete_kitty_image(placement) placement_key = [placement[:x], placement[:y], placement[:content_hash]] image_id = @image_id_map.delete(placement_key) if image_id # Delete by image ID — precise and reliable @buffer << "\e_Ga=d,d=i,i=#{image_id}\e\\" else # Fallback: delete images whose cells were overwritten @buffer << "\e_Ga=d,d=C\e\\" end end |
#disable_mouse ⇒ void
This method returns an undefined value.
Disable mouse event reporting
Turns off modes 1006, 1002 and 1000, then flushes.
140 141 142 143 144 145 |
# File 'lib/rubytui/backends/ansi_backend.rb', line 140 def disable_mouse @buffer << "#{CSI}?1006l" @buffer << "#{CSI}?1002l" @buffer << "#{CSI}?1000l" flush end |
#disable_raw_mode ⇒ void
This method returns an undefined value.
Restore the input's terminal state saved by #enable_raw_mode.
Runs stty with the saved state on the input. Falls back to
cooked! (when the input supports it) if no state was saved or
stty raises.
174 175 176 177 178 179 180 181 182 183 184 185 |
# File 'lib/rubytui/backends/ansi_backend.rb', line 174 def disable_raw_mode if @original_state begin system("stty", @original_state, in: @input, err: File::NULL) rescue StandardError @input.cooked! if @input.respond_to?(:cooked!) end @original_state = nil elsif @input.respond_to?(:cooked!) @input.cooked! end end |
#draw(changes) ⇒ void
This method returns an undefined value.
Append escape sequences for the changed cells to the output buffer.
Emits a cursor move only where a cell does not directly follow the
previous one, skips wide-character spacer cells (empty symbol), and
appends an SGR reset at the end. Does nothing if changes is empty.
Does not flush.
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
# File 'lib/rubytui/backends/ansi_backend.rb', line 43 def draw(changes) return if changes.empty? last_x = -1 last_y = -1 changes.each do |x, y, cell| # Skip spacer cells from wide characters if cell.symbol.empty? next end # Move cursor if not at expected position unless x == last_x + 1 && y == last_y @buffer << "#{CSI}#{y + 1};#{x + 1}H" end # Apply style apply_style(cell.style) # Write character @buffer << cell.symbol # Wide characters advance terminal cursor by 2 w = cell.symbol.length == 1 ? Buffer.char_width(cell.symbol) : 1 last_x = x + w - 1 last_y = y end # Reset style at end @buffer << "#{CSI}0m" @prev_style = nil end |
#draw_kitty_image(placement) ⇒ void
This method returns an undefined value.
Emit a Kitty graphics protocol image at the given placement.
Transmits the image data as chunked base64 in APC escape sequences. The terminal decodes and renders the image at the cursor position. Each image gets a unique ID for precise deletion. Appends to the output buffer; does not flush.
251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 |
# File 'lib/rubytui/backends/ansi_backend.rb', line 251 def draw_kitty_image(placement) # Generate a unique image ID for this placement placement_key = [placement[:x], placement[:y], placement[:content_hash]] image_id = @image_id_map[placement_key] ||= (@next_image_id += 1) # Position cursor at image origin @buffer << "#{CSI}#{placement[:y] + 1};#{placement[:x] + 1}H" encoded = [placement[:data]].pack("m0") # strict base64, no newlines format_code = placement[:format] == :png ? 100 : 32 # Chunk into 4096-byte pieces (safe for all Kitty-compatible terminals) chunks = encoded.scan(/.{1,4096}/m) chunks.each_with_index do |chunk, i| more = i < chunks.length - 1 ? 1 : 0 if i == 0 # C=1: do not move the cursor after displaying — keeps the # terminal's cursor position in sync with the backend's model params = +"a=T,f=#{format_code},i=#{image_id},C=1" params << ",s=#{placement[:pixel_width]},v=#{placement[:pixel_height]}" if format_code == 32 # Source rectangle for cropping/scrolling (pixel coordinates within image) if placement[:src_x] && placement[:src_y] params << ",x=#{placement[:src_x]},y=#{placement[:src_y]}" params << ",w=#{placement[:src_w]},h=#{placement[:src_h]}" if placement[:src_w] && placement[:src_h] end params << ",c=#{placement[:cols]},r=#{placement[:rows]},m=#{more}" @buffer << "\e_G#{params};#{chunk}\e\\" else @buffer << "\e_Gm=#{more};#{chunk}\e\\" end end end |
#enable_mouse ⇒ void
This method returns an undefined value.
Enable SGR extended mouse mode (button events + drag + scroll)
Turns on modes 1000, 1002 and 1006, then flushes.
128 129 130 131 132 133 |
# File 'lib/rubytui/backends/ansi_backend.rb', line 128 def enable_mouse @buffer << "#{CSI}?1000h" # Enable button event tracking @buffer << "#{CSI}?1002h" # Enable button-event tracking with drag @buffer << "#{CSI}?1006h" # Enable SGR extended mode (for coords > 223) flush end |
#enable_raw_mode ⇒ void
This method returns an undefined value.
Put the input into raw mode, saving its terminal state for #disable_raw_mode.
Save the termios state of the SAME fd we are about to raw! — a backtick stty -g reads the process's stdin, which is the wrong fd (and often not a tty at all) when a custom input: IO such as File.open("/dev/tty") is used, e.g. a TUI launched from fzf execute() with output captured. A failed stty yields "" which is truthy, so it must collapse to nil or restore would run stty "" and leave the terminal stuck in raw mode.
157 158 159 160 161 162 163 164 165 |
# File 'lib/rubytui/backends/ansi_backend.rb', line 157 def enable_raw_mode @original_state = begin state = IO.popen(["stty", "-g"], in: @input, err: File::NULL, &:read).chomp state.empty? ? nil : state rescue StandardError nil end @input.raw! if @input.respond_to?(:raw!) end |
#enter_alternate_screen ⇒ void
This method returns an undefined value.
Switch to the alternate screen buffer (CSI ?1049h) and flush.
110 111 112 113 |
# File 'lib/rubytui/backends/ansi_backend.rb', line 110 def enter_alternate_screen @buffer << "#{CSI}?1049h" flush end |
#flush ⇒ void
This method returns an undefined value.
Write the buffered output to the output IO and flush it.
Does nothing if the buffer is empty.
327 328 329 330 331 332 333 |
# File 'lib/rubytui/backends/ansi_backend.rb', line 327 def flush return if @buffer.empty? @output.write(@buffer) @output.flush @buffer.clear end |
#hide_cursor ⇒ void
This method returns an undefined value.
Append the hide-cursor sequence (CSI ?25l) to the output buffer.
80 81 82 |
# File 'lib/rubytui/backends/ansi_backend.rb', line 80 def hide_cursor @buffer << "#{CSI}?25l" end |
#leave_alternate_screen ⇒ void
This method returns an undefined value.
Switch back from the alternate screen buffer (CSI ?1049l) and flush.
118 119 120 121 |
# File 'lib/rubytui/backends/ansi_backend.rb', line 118 def leave_alternate_screen @buffer << "#{CSI}?1049l" flush end |
#move_cursor(x, y) ⇒ void
This method returns an undefined value.
Append a cursor-position sequence to the output buffer.
96 97 98 |
# File 'lib/rubytui/backends/ansi_backend.rb', line 96 def move_cursor(x, y) @buffer << "#{CSI}#{y + 1};#{x + 1}H" end |
#query_cell_pixel_size ⇒ Array(Integer, Integer)
Query the terminal for cell pixel dimensions.
Sends CSI 16 t and parses the response. Must be called before
entering raw mode or while in raw mode with careful stdin handling.
Falls back to [9, 18] when the terminal does not answer within 100 ms
or the query raises IOError, Errno::EIO or Errno::ENOTTY; the
result, including the fallback, is cached.
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 |
# File 'lib/rubytui/backends/ansi_backend.rb', line 207 def query_cell_pixel_size return @cell_pixel_size if @cell_pixel_size begin @input.raw do @output.write("\e[16t") @output.flush response = +"" deadline = Time.now + 0.1 while Time.now < deadline if IO.select([@input], nil, nil, 0.01) char = @input.read_nonblock(1, exception: false) break if char == :wait_readable || char.nil? response << char break if char == "t" end end if response =~ /\e\[6;(\d+);(\d+)t/ @cell_pixel_size = [$2.to_i, $1.to_i] end end rescue IOError, Errno::EIO, Errno::ENOTTY # Terminal doesn't support query end @cell_pixel_size ||= [9, 18] end |
#show_cursor ⇒ void
This method returns an undefined value.
Append the show-cursor sequence (CSI ?25h) to the output buffer.
87 88 89 |
# File 'lib/rubytui/backends/ansi_backend.rb', line 87 def show_cursor @buffer << "#{CSI}?25h" end |
#size ⇒ Array(Integer, Integer)
Get the output terminal size.
191 192 193 194 195 196 |
# File 'lib/rubytui/backends/ansi_backend.rb', line 191 def size rows, cols = @output.winsize [cols, rows] rescue Errno::ENOTTY, Errno::ENODEV, NoMethodError [80, 24] end |