Class: RubyTUI::Buffer

Inherits:
Object
  • Object
show all
Defined in:
lib/rubytui/buffer.rb

Overview

2D grid of styled Cells used for rendering.

Buffer is the core rendering target. Widgets write into a Buffer via #set_string and #set_style. The Terminal then diffs two buffers to send only changed cells to the backend.

For one-shot CLI output, use #to_ansi or #print to convert the buffer contents to an ANSI-styled string without needing a Terminal.

Examples:

Create and render text

buf = Buffer.new(80, 24)
buf.set_string(0, 0, "Hello!", Style.new.fg(Color::GREEN).bold)
puts buf.to_ansi

Instance Attribute Summary collapse

Deprecated Unicode utilities — use the Unicode module instead collapse

Instance Method Summary collapse

Constructor Details

#initialize(width, height) ⇒ Buffer #initialize(area) ⇒ Buffer

Create a new Buffer.

Overloads:

  • #initialize(width, height) ⇒ Buffer

    Shorthand that creates a buffer at origin (0, 0).

    Parameters:

    • width (Integer) —

      buffer width in columns

    • height (Integer) —

      buffer height in rows

  • #initialize(area) ⇒ Buffer

    Parameters:

    • area (Rect) —

      the rectangular bounds



34
35
36
37
38
39
40
41
42
# File 'lib/rubytui/buffer.rb', line 34

def initialize(area_or_width, height = nil)
  @area = if height
    Rect.new(0, 0, area_or_width, height)
  else
    area_or_width
  end
  @cells = Array.new(@area.area) { Cell.new }
  @image_placements = {}
end

Instance Attribute Details

#area ⇒ Rect (readonly)

Returns the rectangular bounds of this buffer.

Returns:

  • (Rect) —

    the rectangular bounds of this buffer



19
20
21
# File 'lib/rubytui/buffer.rb', line 19

def area
  @area
end

#image_placements ⇒ Hash{Array => Hash{Symbol => Object}} (readonly)

Returns image placements keyed by [x, y, content_hash] (or [x, y] when no content hash is given), registered via #set_image by Widgets::Image#render.

Returns:

  • (Hash{Array => Hash{Symbol => Object}}) —

    image placements keyed by [x, y, content_hash] (or [x, y] when no content hash is given), registered via #set_image by Widgets::Image#render



23
24
25
# File 'lib/rubytui/buffer.rb', line 23

def image_placements
  @image_placements
end

Class Method Details

.char_width(ch) ⇒ Integer

Deprecated.

Use Unicode.char_width instead.

Returns 0 for combining marks, 2 for wide characters, 1 otherwise.

Parameters:

  • ch (String) —

    a single character

Returns:

  • (Integer) —

    0 for combining marks, 2 for wide characters, 1 otherwise



83
84
85
86
# File 'lib/rubytui/buffer.rb', line 83

def self.char_width(ch) = Unicode.char_width(ch)
# @deprecated Use {Unicode.combining_mark?} instead.
# @param cp [Integer] Unicode codepoint
# @return [Boolean] true if codepoint is a combining mark (zero-width)

.cluster_width(cluster) ⇒ Integer

Deprecated.

Returns 0, 1, or 2.

Parameters:

  • cluster (String) —

    a grapheme cluster (one or more codepoints)

Returns:

  • (Integer) —

    0, 1, or 2



74
75
76
77
78
# File 'lib/rubytui/buffer.rb', line 74

def self.cluster_width(cluster) = Unicode.cluster_width(cluster)
# @deprecated Use {Unicode.truncate_to_width} instead.
# @param string [String] the string to truncate
# @param max_width [Integer] maximum display width in columns
# @return [String] truncated string

.combining_mark?(cp) ⇒ Boolean

Deprecated.

Returns true if codepoint is a combining mark (zero-width).

Parameters:

  • cp (Integer) —

    Unicode codepoint

Returns:

  • (Boolean) —

    true if codepoint is a combining mark (zero-width)



87
88
89
90
# File 'lib/rubytui/buffer.rb', line 87

def self.combining_mark?(cp) = Unicode.combining_mark?(cp)
# @deprecated Use {Unicode.wide_char?} instead.
# @param cp [Integer] Unicode codepoint
# @return [Boolean] true if codepoint is a wide character (2-column)

.string_width(string) ⇒ Integer

Deprecated.

Returns display width in terminal columns.

Parameters:

  • string (String) —

    the string to measure

Returns:

  • (Integer) —

    display width in terminal columns



70
71
72
73
# File 'lib/rubytui/buffer.rb', line 70

def self.string_width(string) = Unicode.string_width(string)
# @deprecated Use {Unicode.cluster_width} instead.
# @param cluster [String] a grapheme cluster (one or more codepoints)
# @return [Integer] 0, 1, or 2

.truncate_to_width(string, max_width) ⇒ String

Deprecated.

Returns truncated string.

Parameters:

  • string (String) —

    the string to truncate

  • max_width (Integer) —

    maximum display width in columns

Returns:

  • (String) —

    truncated string



79
80
81
82
# File 'lib/rubytui/buffer.rb', line 79

def self.truncate_to_width(string, max_width) = Unicode.truncate_to_width(string, max_width)
# @deprecated Use {Unicode.char_width} instead.
# @param ch [String] a single character
# @return [Integer] 0 for combining marks, 2 for wide characters, 1 otherwise

.wide_char?(cp) ⇒ Boolean

Deprecated.

Use Unicode.wide_char? instead.

Returns true if codepoint is a wide character (2-column).

Parameters:

  • cp (Integer) —

    Unicode codepoint

Returns:

  • (Boolean) —

    true if codepoint is a wide character (2-column)



91
# File 'lib/rubytui/buffer.rb', line 91

def self.wide_char?(cp) = Unicode.wide_char?(cp)

Instance Method Details

#[](x, y) ⇒ Cell?

Get the cell at position (x, y).

Parameters:

  • x (Integer) —

    column position

  • y (Integer) —

    row position

Returns:

  • (Cell, nil) —

    the cell, or nil if out of bounds



49
50
51
52
# File 'lib/rubytui/buffer.rb', line 49

def [](x, y)
  idx = index(x, y)
  idx ? @cells[idx] : nil
end

#[]=(x, y, cell) ⇒ void

This method returns an undefined value.

Set the cell at position (x, y).

Parameters:

  • x (Integer) —

    column position

  • y (Integer) —

    row position

  • cell (Cell) —

    the cell to place



60
61
62
63
# File 'lib/rubytui/buffer.rb', line 60

def []=(x, y, cell)
  idx = index(x, y)
  @cells[idx] = cell if idx
end

#diff(other) ⇒ Array<Array(Integer, Integer, Cell)>

Diff this buffer against another, returning changed cells.

Used by Terminal#draw for minimal screen updates.

Parameters:

  • other (Buffer) —

    the previous buffer to diff against

Returns:

  • (Array<Array(Integer, Integer, Cell)>) —

    changed cells as [x, y, cell]



159
160
161
162
163
164
165
166
167
168
169
# File 'lib/rubytui/buffer.rb', line 159

def diff(other)
  changes = []
  @cells.each_with_index do |cell, i|
    next if cell == other.cells[i]

    x = @area.x + (i % @area.width)
    y = @area.y + (i / @area.width)
    changes << [x, y, cell]
  end
  changes
end

#each_cell_with_position {|x, y, cell| ... } ⇒ void

This method returns an undefined value.

Iterate over all cells with their absolute positions.

Yields:

  • (x, y, cell) —

    for each cell in the buffer

Yield Parameters:

  • x (Integer) —

    column position

  • y (Integer) —

    row position

  • cell (Cell)


251
252
253
254
255
256
257
# File 'lib/rubytui/buffer.rb', line 251

def each_cell_with_position
  @cells.each_with_index do |cell, i|
    x = @area.x + (i % @area.width)
    y = @area.y + (i / @area.width)
    yield x, y, cell
  end
end

#merge(other) ⇒ void

This method returns an undefined value.

Merge another buffer on top of this one (for overlays).

Non-empty cells from the other buffer overwrite cells in this buffer.

Parameters:

  • other (Buffer) —

    the overlay buffer



235
236
237
238
239
240
241
242
# File 'lib/rubytui/buffer.rb', line 235

def merge(other)
  other.each_cell_with_position do |x, y, cell|
    next if cell.empty?

    idx = index(x, y)
    @cells[idx] = cell if idx
  end
end

This method returns an undefined value.

Print the buffer's ANSI output to a stream.

Convenience wrapper around #to_ansi.

Parameters:

  • output (IO) (defaults to: $stdout) —

    output stream (default: $stdout)



336
337
338
# File 'lib/rubytui/buffer.rb', line 336

def print(output = $stdout)
  output.write(to_ansi)
end

#reset! ⇒ void

This method returns an undefined value.

Reset all cells to empty (space character, default style).



174
175
176
177
# File 'lib/rubytui/buffer.rb', line 174

def reset!
  reset_cells!
  @image_placements.clear
end

#reset_cells! ⇒ void

This method returns an undefined value.

Reset cells only, keeping image placements — used by Terminal for full repaints, where placements must still diff against the previous frame to detect deletions.



184
185
186
# File 'lib/rubytui/buffer.rb', line 184

def reset_cells!
  @cells.each(&:reset!)
end

#set_image(x:, y:, cols:, rows:, data:, pixel_width:, pixel_height:, format:, content_hash: nil, src_x: nil, src_y: nil, src_w: nil, src_h: nil) ⇒ void

This method returns an undefined value.

Register an image placement for Kitty graphics protocol rendering.

Called by Widgets::Image#render. The placement is stored and later emitted by the backend during Terminal#draw.

Parameters:

  • x (Integer) —

    column position

  • y (Integer) —

    row position

  • cols (Integer) —

    display width in columns

  • rows (Integer) —

    display height in rows

  • data (String) —

    raw image bytes (PNG or RGBA)

  • pixel_width (Integer) —

    image width in pixels

  • pixel_height (Integer) —

    image height in pixels

  • format (Symbol) —

    :png or :rgba

  • content_hash (String, nil) (defaults to: nil) —

    hash of image content for change detection

  • src_x (Integer, nil) (defaults to: nil) —

    source rectangle X offset in pixels

  • src_y (Integer, nil) (defaults to: nil) —

    source rectangle Y offset in pixels

  • src_w (Integer, nil) (defaults to: nil) —

    source rectangle width in pixels

  • src_h (Integer, nil) (defaults to: nil) —

    source rectangle height in pixels



207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/rubytui/buffer.rb', line 207

def set_image(x:, y:, cols:, rows:, data:, pixel_width:, pixel_height:, format:,
              content_hash: nil, src_x: nil, src_y: nil, src_w: nil, src_h: nil)
  # Key by position + content hash to detect when image changes at same position
  key = content_hash ? [x, y, content_hash] : [x, y]

  placement = {
    x: x, y: y, cols: cols, rows: rows,
    data: data, pixel_width: pixel_width, pixel_height: pixel_height,
    format: format, content_hash: content_hash
  }

  # Add source rectangle if specified
  if src_x && src_y && src_w && src_h
    placement[:src_x] = src_x
    placement[:src_y] = src_y
    placement[:src_w] = src_w
    placement[:src_h] = src_h
  end

  @image_placements[key] = placement
end

#set_string(x, y, string, style = Style::DEFAULT) ⇒ Integer

Write a styled string starting at (x, y).

Iterates by grapheme cluster so that combining sequences (Devanagari matras/virama, emoji ZWJ, Latin diacritics) are stored as a single cell symbol. Wide clusters (2-column) mark the next cell as a spacer. Clips at buffer boundaries.

Parameters:

  • x (Integer) —

    starting column

  • y (Integer) —

    row

  • string (String) —

    text to write

  • style (Style) (defaults to: Style::DEFAULT) —

    style to apply (default: Style::DEFAULT)

Returns:

  • (Integer) —

    number of terminal columns consumed



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
# File 'lib/rubytui/buffer.rb', line 107

def set_string(x, y, string, style = Style::DEFAULT)
  return 0 unless y >= @area.y && y < @area.bottom

  count = 0
  col = x

  string.grapheme_clusters.each do |gc|
    break if col >= @area.right

    w = Unicode.cluster_width(gc)

    if col >= @area.x
      idx = index(col, y)
      if idx
        @cells[idx].symbol = gc
        @cells[idx].style = style
        count += w

        # For wide clusters, mark the next cell as a spacer
        if w == 2 && col + 1 < @area.right
          next_idx = index(col + 1, y)
          if next_idx
            @cells[next_idx].symbol = ""
            @cells[next_idx].style = style
          end
        end
      end
    end
    col += w
  end
  count
end

#set_style(area, style) ⇒ void

This method returns an undefined value.

Apply a style to all cells within the given area.

Uses Style#patch so the applied style merges with existing cell styles.

Parameters:

  • area (Rect) —

    the area to style

  • style (Style) —

    the style to apply



147
148
149
150
151
# File 'lib/rubytui/buffer.rb', line 147

def set_style(area, style)
  each_cell_in(area) do |idx|
    @cells[idx].style = @cells[idx].style.patch(style)
  end
end

#to_ansi ⇒ String

Convert buffer contents to an ANSI-styled string for one-shot printing.

Each row becomes a line separated by \n. Styles are emitted as ANSI SGR escape codes with reset at end of each styled row.

Examples:

buf = Buffer.new(40, 5)
buf.set_string(0, 0, "Hello", Style.new.fg(Color::GREEN))
puts buf.to_ansi

Returns:

  • (String) —

    ANSI-escaped string ready for terminal output



270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
# File 'lib/rubytui/buffer.rb', line 270

def to_ansi
  csi = "\e["
  result = String.new(capacity: @area.area * 2)
  prev_style = nil

  @area.height.times do |row|
    # Build the row content and track the last non-empty column
    row_buf = String.new(capacity: @area.width * 2)
    last_styled_col = -1
    col = 0
    display_col = 0  # track actual terminal column for cursor correction

    while col < @area.width
      cell = @cells[row * @area.width + col]

      # Skip spacer cells from wide characters
      if cell.symbol.empty?
        col += 1
        next
      end

      # Apply style changes
      style = cell.style
      unless style == prev_style
        codes = style_to_sgr_codes(style, prev_style)
        row_buf << "#{csi}#{codes.join(";")}m" unless codes.empty?
        prev_style = style
      end

      row_buf << cell.symbol

      # Track last non-default cell for trimming
      unless cell.empty?
        last_styled_col = col
      end

      w = Unicode.string_width(cell.symbol)
      display_col += w
      col += [w, 1].max

      # After wide characters, emit cursor column to correct any
      # terminal rendering drift (terminals may disagree about emoji width)
      if w >= 2 && col < @area.width
        row_buf << "#{csi}#{display_col + 1}G"
      end
    end

    # Reset at end of row if we had any styling
    if prev_style && prev_style != Style::DEFAULT
      row_buf << "#{csi}0m"
      prev_style = nil
    end

    result << row_buf
    result << "\n" unless row == @area.height - 1
  end

  result
end