Class: TermBuffer
- Inherits:
-
Object
- Object
- TermBuffer
- 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
-
#generation ⇒ Object
readonly
Returns the value of attribute generation.
-
#scroll_end ⇒ Object
Returns the value of attribute scroll_end.
-
#scroll_start ⇒ Object
Returns the value of attribute scroll_start.
-
#scrollback_buffer ⇒ Object
readonly
Returns the value of attribute scrollback_buffer.
-
#scrollback_lineattrs ⇒ Object
readonly
Returns the value of attribute scrollback_lineattrs.
-
#w ⇒ Object
readonly
Returns the value of attribute w.
Instance Method Summary collapse
- #blinky ⇒ Object
- #cell(ch, style) ⇒ Object
-
#cell_eq?(x, y, ch, fg, bg, flags) ⇒ Boolean
True if (x,y) currently holds exactly this content.
- #clear ⇒ Object
- #clear_line(y, start_x = 0, end_x = nil) ⇒ Object
-
#delete_chars(x, y, num) ⇒ Object
DCH: delete
numcells at (x,y), shifting the remainder left (gens follow their cells). - #delete_line(y) ⇒ Object
-
#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.
- #each_character_between(spos, epos) ⇒ Object
-
#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. - #enforce_height ⇒ Object
-
#generation_at(x, y) ⇒ Object
The damage primitive: the generation at which (x,y) last changed, or nil for an unset cell.
-
#get(x, y) ⇒ Object
# Reads.
-
#getline(y) ⇒ Object
Whole row of cells.
-
#initialize ⇒ TermBuffer
constructor
A new instance of TermBuffer.
-
#insert(x, y, num, cell) ⇒ Object
ICH / IRM: open a gap of
numcells at x by insertingcell, shifting the rest of the line right; cells pushed past the right margin are discarded (the line never grows beyond width). - #insert_line(y) ⇒ Object
-
#line_at(y) ⇒ Object
Like #getline but non-vivifying and nil (not []) for an absent row, and mapping negative rows into the (unpacked) scrollback.
- #lineattrs(y) ⇒ Object
- #on_resize(w, h) ⇒ Object (also: #resize)
-
#pack_style(fg, bg, flags) ⇒ Object
# Packing helpers.
- #scroll_up ⇒ Object
- #scrollback_size ⇒ Object
-
#set(x, y, ch, fg = 0, bg = 0, flags = 0) ⇒ Object
# Writes.
- #set_lineattrs(y, v) ⇒ Object
-
#unpack_line(packed) ⇒ Object
Reconstruct a [chars, styles] scrollback pair into an array of cells.
-
#unset?(x, y) ⇒ Boolean
True if (x,y) has never been written (blank).
Constructor Details
#initialize ⇒ TermBuffer
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
#generation ⇒ Object (readonly)
Returns the value of attribute generation.
43 44 45 |
# File 'lib/termbuffer.rb', line 43 def generation @generation end |
#scroll_end ⇒ Object
Returns the value of attribute scroll_end.
42 43 44 |
# File 'lib/termbuffer.rb', line 42 def scroll_end @scroll_end end |
#scroll_start ⇒ Object
Returns the value of attribute scroll_start.
42 43 44 |
# File 'lib/termbuffer.rb', line 42 def scroll_start @scroll_start end |
#scrollback_buffer ⇒ Object (readonly)
Returns the value of attribute scrollback_buffer.
43 44 45 |
# File 'lib/termbuffer.rb', line 43 def scrollback_buffer @scrollback_buffer end |
#scrollback_lineattrs ⇒ Object (readonly)
Returns the value of attribute scrollback_lineattrs.
43 44 45 |
# File 'lib/termbuffer.rb', line 43 def scrollback_lineattrs @scrollback_lineattrs end |
#w ⇒ Object (readonly)
Returns the value of attribute w.
43 44 45 |
# File 'lib/termbuffer.rb', line 43 def w @w end |
Instance Method Details
#blinky ⇒ Object
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).
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 |
#clear ⇒ Object
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_height ⇒ Object
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_up ⇒ Object
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_size ⇒ Object
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).
186 187 188 189 |
# File 'lib/termbuffer.rb', line 186 def unset?(x, y) chars = @chars[y] !chars || chars[x].nil? end |