Class: TermBuffer

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

Overview

The screen buffer: a grid of styled cells + scrollback + scroll region.

Storage is COLUMNAR. A row is not an array of [ch,fg,bg,flags] cell objects; it is split across three parallel arrays of tagged immediates, held per row:

@chars[y][x] - codepoint Integer (nil = unset/blank cell)
@style[y][x] - packed Integer: fg(24) | bg(24)<<24 | flags<<48
@gen[y][x]   - per-cell generation Integer (the damage primitive)

Integers in the fixnum range and nil are stored in the VALUE word with no heap allocation, so writing a cell allocates nothing (vs. an Array per glyph before). fg/bg are 24-bit and flags < 2^12, so a cell’s attributes pack into one fixnum. See docs/architecture-review.md §8.

actually changes. It is the damage primitive a backend consumes to know which cells to repaint; the harness’s markers check reads it too (generation_at), but it is genuine production state, not debug-only weight. Because @gen is stored and moved alongside @chars/@style, the generation follows a cell through scrolls and line/char insert+delete for free - no separate bookkeeping.

Scrollback lines reuse the exact columnar representation: a scrolled-off line is the pair [chars_row, style_row], moved straight into history (no per-cell objects, ~40x fewer retained objects than the old form).

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeTermBuffer

Returns a new instance of TermBuffer.



45
46
47
48
49
50
51
52
# File 'lib/termbuffer.rb', line 45

def initialize
  @w = nil
  @h = nil
  @generation = 0
  clear
  @scroll_start = nil
  @scroll_end = nil
end

Instance Attribute Details

#generationObject (readonly)

Returns the value of attribute generation.



43
44
45
# File 'lib/termbuffer.rb', line 43

def generation
  @generation
end

#scroll_endObject

Returns the value of attribute scroll_end.



42
43
44
# File 'lib/termbuffer.rb', line 42

def scroll_end
  @scroll_end
end

#scroll_startObject

Returns the value of attribute scroll_start.



42
43
44
# File 'lib/termbuffer.rb', line 42

def scroll_start
  @scroll_start
end

#scrollback_bufferObject (readonly)

Returns the value of attribute scrollback_buffer.



43
44
45
# File 'lib/termbuffer.rb', line 43

def scrollback_buffer
  @scrollback_buffer
end

#scrollback_lineattrsObject (readonly)

Returns the value of attribute scrollback_lineattrs.



43
44
45
# File 'lib/termbuffer.rb', line 43

def scrollback_lineattrs
  @scrollback_lineattrs
end

#wObject (readonly)

Returns the value of attribute w.



43
44
45
# File 'lib/termbuffer.rb', line 43

def w
  @w
end

Instance Method Details

#blinkyObject



68
# File 'lib/termbuffer.rb', line 68

def blinky          = @blinky

#cell(ch, style) ⇒ Object



83
84
85
# File 'lib/termbuffer.rb', line 83

def cell(ch, style)
  [ch, style & 0xFFFFFF, (style >> 24) & 0xFFFFFF, style >> 48]
end

#cell_eq?(x, y, ch, fg, bg, flags) ⇒ Boolean

True if (x,y) currently holds exactly this content. Lets the draw path skip identical repaints without reconstructing a cell Array (the prior ‘new == get(x,y)` allocated one per character).

Returns:

  • (Boolean)


180
181
182
183
# File 'lib/termbuffer.rb', line 180

def cell_eq?(x, y, ch, fg, bg, flags)
  chars = @chars[y] or return false
  chars[x] == ch && @style[y][x] == pack_style(fg, bg, flags)
end

#clearObject



54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/termbuffer.rb', line 54

def clear
  @chars     = []
  @style     = []
  @gen       = []
  @lineattrs = []
  @scrollback_buffer = []
  @scrollback_lineattrs = []
  @blinky = Set.new
  @row_dirty = [] # rows changed since the last each_damaged walk
  # NB: @generation is deliberately NOT reset - it stays monotonic across
  # clears so a redrawn-after-clear cell never collides with a stale gen.
end

#clear_line(y, start_x = 0, end_x = nil) ⇒ Object



302
303
304
305
306
307
308
309
310
311
312
313
314
# File 'lib/termbuffer.rb', line 302

def clear_line(y, start_x = 0, end_x = nil)
  if !end_x
    # Clear to end of line: truncate the row at start_x. Dropped cells
    # become unset (gen nil), so a stale on-screen tail is detectable.
    if @chars[y]
      @chars[y] = @chars[y][0...start_x]
      @style[y] = (@style[y] || [])[0...start_x]
      @gen[y]   = (@gen[y]   || [])[0...start_x]
    end
  else
    (start_x..end_x).each { |x| set(x, y, ' ') }
  end
end

#delete_chars(x, y, num) ⇒ Object

DCH: delete num cells at (x,y), shifting the remainder left (gens follow their cells). Vacated cells at the right become blank.



292
293
294
295
296
297
298
299
300
# File 'lib/termbuffer.rb', line 292

def delete_chars(x, y, num)
  chars = @chars[y] or return
  num.times do
    break if x >= chars.length
    chars.delete_at(x)
    @style[y].delete_at(x)
    @gen[y].delete_at(x)
  end
end

#delete_line(y) ⇒ Object



333
334
335
336
337
338
# File 'lib/termbuffer.rb', line 333

def delete_line(y)
  raw_delete_line(y)
  # In a scroll region, deleting a line shifts the region up and inserts a
  # blank line at the bottom (scroll_end), not at the top.
  raw_insert_line(@scroll_end) if @scroll_start
end

#each_character(scrollback_offset = 0) ⇒ Object

Yields [x, y, cell] for every set cell, scrollback (if offset>0) first at the top, then the live grid below it.



193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/termbuffer.rb', line 193

def each_character(scrollback_offset = 0)
  used = 0
  if scrollback_offset > 0 && !@scrollback_buffer.empty?
    offset = [@scrollback_buffer.size, scrollback_offset].min
    if offset > 0
      lines = @scrollback_buffer[-offset..-1] || []
      lines.each_with_index do |packed, idx|
        chars, styles = packed
        chars.each_with_index do |ch, x|
          yield x, idx, cell(ch, styles[x]) if ch
        end
      end
      used = lines.size
    end
  end

  # +1 mirrors the historical off-by-one (draw one extra row).
  remaining = @h ? (@h - used + 1) : @chars.size
  @chars.each_with_index do |chars, y|
    next if !chars || y >= remaining
    styles = @style[y]
    chars.each_with_index do |ch, x|
      yield x, y + used, cell(ch, styles[x]) if ch
    end
  end
end

#each_character_between(spos, epos) ⇒ Object



220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/termbuffer.rb', line 220

def each_character_between(spos, epos)
  if spos.end > epos.end
    spos, epos = epos, spos
  elsif spos.end == epos.end && spos.first > epos.first
    spos, epos = epos, spos
  end

  x = spos.first
  xend, ymax = epos.first, epos.end
  (spos.end..ymax).each do |y|
    line = line_at(y) || ""
    xmax = y == ymax ? xend + 1 : line.length - 1
    xmax = [xmax, line.length - 1].min
    xmax = 0 if xmax < 0
    while x <= xmax
      yield(x, y, line[x])
      x += 1
    end
    x = 0
  end
end

#each_damaged(since_gen) ⇒ Object

Yield [x, y, ch, fg, bg, flags] for every cell whose content changed after since_gen - the damage since the last flush - as scalars, no cell Array allocated. A damage-driven renderer walks this instead of being told to draw eagerly on every #set. Returns the current generation so the caller can advance its watermark. (Walks all rows; row-level dirty tracking is a later optimisation.)



159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/termbuffer.rb', line 159

def each_damaged(since_gen)
  @row_dirty.each_index do |y|
    next unless @row_dirty[y]
    @row_dirty[y] = false
    gens = @gen[y] or next
    chars = @chars[y]
    styles = @style[y]
    gens.each_index do |x|
      g = gens[x]
      next if !g || g <= since_gen
      ch = chars[x] or next
      s = styles[x]
      yield x, y, ch, s & 0xFFFFFF, (s >> 24) & 0xFFFFFF, s >> 48
    end
  end
  @generation
end

#enforce_heightObject



358
359
360
361
362
363
364
# File 'lib/termbuffer.rb', line 358

def enforce_height
  return unless @h
  @chars.slice!(@h..)
  @style.slice!(@h..)
  @gen.slice!(@h..)
  @lineattrs.slice!(@h..)
end

#generation_at(x, y) ⇒ Object

The damage primitive: the generation at which (x,y) last changed, or nil for an unset cell. Scrollback is not damage-tracked.



148
149
150
151
# File 'lib/termbuffer.rb', line 148

def generation_at(x, y)
  return nil if y < 0
  g = @gen[y] and g[x]
end

#get(x, y) ⇒ Object

# Reads



110
111
112
113
114
115
116
117
118
# File 'lib/termbuffer.rb', line 110

def get(x, y)
  if y < 0
    row = line_at(y)
    return row && row[x]
  end
  chars = @chars[y] or return nil
  ch = chars[x] or return nil
  cell(ch, @style[y][x])
end

#getline(y) ⇒ Object

Whole row of cells. Negative rows come from scrollback.



121
122
123
124
# File 'lib/termbuffer.rb', line 121

def getline(y)
  return line_at(y) if y < 0
  reconstruct_row(y) || []
end

#insert(x, y, num, cell) ⇒ Object

ICH / IRM: open a gap of num cells at x by inserting cell, shifting the rest of the line right; cells pushed past the right margin are discarded (the line never grows beyond width). Inserted cells carry no generation (they did not go through #set); they are blanks the caller repaints.



274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
# File 'lib/termbuffer.rb', line 274

def insert(x, y, num, cell)
  ensure_row(y)
  ch = cell[0]
  style = pack_style(cell[1], cell[2], cell[3])
  num.times do
    @chars[y].insert(x, ch)
    @style[y].insert(x, style)
    @gen[y].insert(x, nil)
  end
  if @w && @chars[y].length > @w
    @chars[y].slice!(@w..)
    @style[y].slice!(@w..)
    @gen[y].slice!(@w..)
  end
end

#insert_line(y) ⇒ Object



340
341
342
343
344
345
# File 'lib/termbuffer.rb', line 340

def insert_line(y)
  raw_insert_line(y)
  # Inserting pushes the region down; discard the line that falls just
  # past the bottom of the region.
  raw_delete_line(@scroll_end + 1) if @scroll_end
end

#line_at(y) ⇒ Object

Like #getline but non-vivifying and nil (not []) for an absent row, and mapping negative rows into the (unpacked) scrollback. Safe for read-only traversal such as selection extraction across scrollback.



129
130
131
132
133
134
135
# File 'lib/termbuffer.rb', line 129

def line_at(y)
  if y < 0 && !@scrollback_buffer.empty?
    i = @scrollback_buffer.size + y
    return i >= 0 ? unpack_line(@scrollback_buffer[i]) : nil
  end
  reconstruct_row(y)
end

#lineattrs(y) ⇒ Object



137
138
139
140
141
142
143
144
# File 'lib/termbuffer.rb', line 137

def lineattrs(y)
  y = y.to_i
  if y < 0 && !@scrollback_lineattrs.empty?
    i = @scrollback_lineattrs.size + y
    return i >= 0 ? @scrollback_lineattrs[i] : 0
  end
  @lineattrs[y]
end

#on_resize(w, h) ⇒ Object Also known as: resize



70
71
72
73
74
# File 'lib/termbuffer.rb', line 70

def on_resize(w, h)
  raise if !h
  @w, @h = w, h
  enforce_height
end

#pack_style(fg, bg, flags) ⇒ Object

# Packing helpers



79
80
81
# File 'lib/termbuffer.rb', line 79

def pack_style(fg, bg, flags)
  (fg.to_i & 0xFFFFFF) | ((bg.to_i & 0xFFFFFF) << 24) | (flags.to_i << 48)
end

#scroll_upObject



347
348
349
350
351
352
353
354
355
356
# File 'lib/termbuffer.rb', line 347

def scroll_up
  # Move the top line of the region into scrollback - the columnar
  # [chars, style] arrays ARE the packed scrollback form, so this is a
  # straight handoff (delete_line then drops the live references). The
  # gen row is discarded (scrollback is not damage-tracked).
  y = @scroll_start.to_i
  @scrollback_buffer.push([@chars[y] || [], @style[y] || []])
  @scrollback_lineattrs.push(lineattrs(y))
  delete_line(y)
end

#scrollback_sizeObject



67
# File 'lib/termbuffer.rb', line 67

def scrollback_size = @scrollback_buffer.size

#set(x, y, ch, fg = 0, bg = 0, flags = 0) ⇒ Object

# Writes



244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
# File 'lib/termbuffer.rb', line 244

def set(x, y, ch, fg = 0, bg = 0, flags = 0)
  ch = ch.ord
  if flags.anybits?(BLINK | RAPID_BLINK)
    @blinky << [x, y]
  elsif !@blinky.empty?
    # Only bother removing (and allocating the [x,y] key) when something
    # actually blinks. The overwhelmingly common case is no blinking cells
    # at all, so this skips a per-character array alloc + Set#delete.
    @blinky.delete([x, y])
  end
  ensure_row(y)
  style = pack_style(fg, bg, flags)
  # Bump the generation only on an actual content change (identical
  # rewrites keep their gen, so a cell that didn't change isn't seen as
  # damaged).
  if @chars[y][x] != ch || @style[y][x] != style
    @gen[y][x] = (@generation += 1)
    @row_dirty[y] = true
  end
  @chars[y][x] = ch
  @style[y][x] = style
end

#set_lineattrs(y, v) ⇒ Object



267
# File 'lib/termbuffer.rb', line 267

def set_lineattrs(y, v) = (@lineattrs[y] = v)

#unpack_line(packed) ⇒ Object

Reconstruct a [chars, styles] scrollback pair into an array of cells.



88
89
90
91
# File 'lib/termbuffer.rb', line 88

def unpack_line(packed)
  chars, styles = packed
  chars.map.with_index { |ch, x| ch && cell(ch, styles[x]) }
end

#unset?(x, y) ⇒ Boolean

True if (x,y) has never been written (blank).

Returns:

  • (Boolean)


186
187
188
189
# File 'lib/termbuffer.rb', line 186

def unset?(x, y)
  chars = @chars[y]
  !chars || chars[x].nil?
end