rubyterm 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +144 -0
- data/bin/record +3 -0
- data/bin/rubyterm +11 -0
- data/example-config.toml +61 -0
- data/lib/ansibackend.rb +155 -0
- data/lib/bitmapwindow.rb +176 -0
- data/lib/charsets.rb +52 -0
- data/lib/controller.rb +80 -0
- data/lib/escapeparser.rb +71 -0
- data/lib/keymap.rb +112 -0
- data/lib/palette.rb +14 -0
- data/lib/rubyterm/app.rb +580 -0
- data/lib/rubyterm/version.rb +5 -0
- data/lib/rubyterm.rb +47 -0
- data/lib/term.rb +657 -0
- data/lib/termbuffer.rb +365 -0
- data/lib/trackchanges.rb +319 -0
- data/lib/utf8decoder.rb +77 -0
- data/lib/window.rb +410 -0
- data/lib/windowadapter.rb +161 -0
- metadata +127 -0
data/lib/termbuffer.rb
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
require 'set'
|
|
2
|
+
|
|
3
|
+
BOLD = 0x002
|
|
4
|
+
FAINT = 0x004
|
|
5
|
+
ITALICS = 0x008
|
|
6
|
+
UNDERLINE = 0x010
|
|
7
|
+
BLINK = 0x020
|
|
8
|
+
RAPID_BLINK = 0x040
|
|
9
|
+
INVERSE = 0x080
|
|
10
|
+
INVISIBLE = 0x100
|
|
11
|
+
CROSSED_OUT = 0x200
|
|
12
|
+
DBL_UNDERLINE = 0x400
|
|
13
|
+
OVERLINE = 0x800
|
|
14
|
+
|
|
15
|
+
# The screen buffer: a grid of styled cells + scrollback + scroll region.
|
|
16
|
+
#
|
|
17
|
+
# Storage is COLUMNAR. A row is not an array of [ch,fg,bg,flags] cell
|
|
18
|
+
# objects; it is split across three parallel arrays of tagged immediates,
|
|
19
|
+
# held per row:
|
|
20
|
+
#
|
|
21
|
+
# @chars[y][x] - codepoint Integer (nil = unset/blank cell)
|
|
22
|
+
# @style[y][x] - packed Integer: fg(24) | bg(24)<<24 | flags<<48
|
|
23
|
+
# @gen[y][x] - per-cell generation Integer (the damage primitive)
|
|
24
|
+
#
|
|
25
|
+
# Integers in the fixnum range and nil are stored in the VALUE word with no
|
|
26
|
+
# heap allocation, so writing a cell allocates nothing (vs. an Array per
|
|
27
|
+
# glyph before). fg/bg are 24-bit and flags < 2^12, so a cell's attributes
|
|
28
|
+
# pack into one fixnum. See docs/architecture-review.md §8.
|
|
29
|
+
#
|
|
30
|
+
# @gen is a monotonic per-cell version, bumped only when a cell's content
|
|
31
|
+
# actually changes. It is the damage primitive a backend consumes to know
|
|
32
|
+
# which cells to repaint; the harness's markers check reads it too
|
|
33
|
+
# (generation_at), but it is genuine production state, not debug-only
|
|
34
|
+
# weight. Because @gen is stored and moved alongside @chars/@style, the
|
|
35
|
+
# generation follows a cell through scrolls and line/char insert+delete for
|
|
36
|
+
# free - no separate bookkeeping.
|
|
37
|
+
#
|
|
38
|
+
# Scrollback lines reuse the exact columnar representation: a scrolled-off
|
|
39
|
+
# line is the pair [chars_row, style_row], moved straight into history (no
|
|
40
|
+
# per-cell objects, ~40x fewer retained objects than the old form).
|
|
41
|
+
class TermBuffer
|
|
42
|
+
attr_accessor :scroll_start, :scroll_end
|
|
43
|
+
attr_reader :w, :scrollback_buffer, :scrollback_lineattrs, :generation
|
|
44
|
+
|
|
45
|
+
def initialize
|
|
46
|
+
@w = nil
|
|
47
|
+
@h = nil
|
|
48
|
+
@generation = 0
|
|
49
|
+
clear
|
|
50
|
+
@scroll_start = nil
|
|
51
|
+
@scroll_end = nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def clear
|
|
55
|
+
@chars = []
|
|
56
|
+
@style = []
|
|
57
|
+
@gen = []
|
|
58
|
+
@lineattrs = []
|
|
59
|
+
@scrollback_buffer = []
|
|
60
|
+
@scrollback_lineattrs = []
|
|
61
|
+
@blinky = Set.new
|
|
62
|
+
@row_dirty = [] # rows changed since the last each_damaged walk
|
|
63
|
+
# NB: @generation is deliberately NOT reset - it stays monotonic across
|
|
64
|
+
# clears so a redrawn-after-clear cell never collides with a stale gen.
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def scrollback_size = @scrollback_buffer.size
|
|
68
|
+
def blinky = @blinky
|
|
69
|
+
|
|
70
|
+
def on_resize(w, h)
|
|
71
|
+
raise if !h
|
|
72
|
+
@w, @h = w, h
|
|
73
|
+
enforce_height
|
|
74
|
+
end
|
|
75
|
+
alias resize on_resize
|
|
76
|
+
|
|
77
|
+
# # Packing helpers
|
|
78
|
+
|
|
79
|
+
def pack_style(fg, bg, flags)
|
|
80
|
+
(fg.to_i & 0xFFFFFF) | ((bg.to_i & 0xFFFFFF) << 24) | (flags.to_i << 48)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def cell(ch, style)
|
|
84
|
+
[ch, style & 0xFFFFFF, (style >> 24) & 0xFFFFFF, style >> 48]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Reconstruct a [chars, styles] scrollback pair into an array of cells.
|
|
88
|
+
def unpack_line(packed)
|
|
89
|
+
chars, styles = packed
|
|
90
|
+
chars.map.with_index { |ch, x| ch && cell(ch, styles[x]) }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private def ensure_row(y)
|
|
94
|
+
@chars[y] ||= []
|
|
95
|
+
@style[y] ||= []
|
|
96
|
+
@gen[y] ||= []
|
|
97
|
+
@lineattrs[y] ||= 0
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Rebuild row y as an array of cells (nil for unset positions), or nil if
|
|
101
|
+
# the row has never been written. Non-mutating.
|
|
102
|
+
private def reconstruct_row(y)
|
|
103
|
+
chars = @chars[y] or return nil
|
|
104
|
+
styles = @style[y]
|
|
105
|
+
chars.map.with_index { |ch, x| ch && cell(ch, styles[x]) }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# # Reads
|
|
109
|
+
|
|
110
|
+
def get(x, y)
|
|
111
|
+
if y < 0
|
|
112
|
+
row = line_at(y)
|
|
113
|
+
return row && row[x]
|
|
114
|
+
end
|
|
115
|
+
chars = @chars[y] or return nil
|
|
116
|
+
ch = chars[x] or return nil
|
|
117
|
+
cell(ch, @style[y][x])
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Whole row of cells. Negative rows come from scrollback.
|
|
121
|
+
def getline(y)
|
|
122
|
+
return line_at(y) if y < 0
|
|
123
|
+
reconstruct_row(y) || []
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Like #getline but non-vivifying and nil (not []) for an absent row, and
|
|
127
|
+
# mapping negative rows into the (unpacked) scrollback. Safe for read-only
|
|
128
|
+
# traversal such as selection extraction across scrollback.
|
|
129
|
+
def line_at(y)
|
|
130
|
+
if y < 0 && !@scrollback_buffer.empty?
|
|
131
|
+
i = @scrollback_buffer.size + y
|
|
132
|
+
return i >= 0 ? unpack_line(@scrollback_buffer[i]) : nil
|
|
133
|
+
end
|
|
134
|
+
reconstruct_row(y)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def lineattrs(y)
|
|
138
|
+
y = y.to_i
|
|
139
|
+
if y < 0 && !@scrollback_lineattrs.empty?
|
|
140
|
+
i = @scrollback_lineattrs.size + y
|
|
141
|
+
return i >= 0 ? @scrollback_lineattrs[i] : 0
|
|
142
|
+
end
|
|
143
|
+
@lineattrs[y]
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# The damage primitive: the generation at which (x,y) last changed, or
|
|
147
|
+
# nil for an unset cell. Scrollback is not damage-tracked.
|
|
148
|
+
def generation_at(x, y)
|
|
149
|
+
return nil if y < 0
|
|
150
|
+
g = @gen[y] and g[x]
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Yield [x, y, ch, fg, bg, flags] for every cell whose content changed
|
|
154
|
+
# after +since_gen+ - the damage since the last flush - as scalars, no
|
|
155
|
+
# cell Array allocated. A damage-driven renderer walks this instead of
|
|
156
|
+
# being told to draw eagerly on every #set. Returns the current
|
|
157
|
+
# generation so the caller can advance its watermark. (Walks all rows;
|
|
158
|
+
# row-level dirty tracking is a later optimisation.)
|
|
159
|
+
def each_damaged(since_gen)
|
|
160
|
+
@row_dirty.each_index do |y|
|
|
161
|
+
next unless @row_dirty[y]
|
|
162
|
+
@row_dirty[y] = false
|
|
163
|
+
gens = @gen[y] or next
|
|
164
|
+
chars = @chars[y]
|
|
165
|
+
styles = @style[y]
|
|
166
|
+
gens.each_index do |x|
|
|
167
|
+
g = gens[x]
|
|
168
|
+
next if !g || g <= since_gen
|
|
169
|
+
ch = chars[x] or next
|
|
170
|
+
s = styles[x]
|
|
171
|
+
yield x, y, ch, s & 0xFFFFFF, (s >> 24) & 0xFFFFFF, s >> 48
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
@generation
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# True if (x,y) currently holds exactly this content. Lets the draw path
|
|
178
|
+
# skip identical repaints without reconstructing a cell Array (the prior
|
|
179
|
+
# `new == get(x,y)` allocated one per character).
|
|
180
|
+
def cell_eq?(x, y, ch, fg, bg, flags)
|
|
181
|
+
chars = @chars[y] or return false
|
|
182
|
+
chars[x] == ch && @style[y][x] == pack_style(fg, bg, flags)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# True if (x,y) has never been written (blank).
|
|
186
|
+
def unset?(x, y)
|
|
187
|
+
chars = @chars[y]
|
|
188
|
+
!chars || chars[x].nil?
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Yields [x, y, cell] for every *set* cell, scrollback (if offset>0) first
|
|
192
|
+
# at the top, then the live grid below it.
|
|
193
|
+
def each_character(scrollback_offset = 0)
|
|
194
|
+
used = 0
|
|
195
|
+
if scrollback_offset > 0 && !@scrollback_buffer.empty?
|
|
196
|
+
offset = [@scrollback_buffer.size, scrollback_offset].min
|
|
197
|
+
if offset > 0
|
|
198
|
+
lines = @scrollback_buffer[-offset..-1] || []
|
|
199
|
+
lines.each_with_index do |packed, idx|
|
|
200
|
+
chars, styles = packed
|
|
201
|
+
chars.each_with_index do |ch, x|
|
|
202
|
+
yield x, idx, cell(ch, styles[x]) if ch
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
used = lines.size
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# +1 mirrors the historical off-by-one (draw one extra row).
|
|
210
|
+
remaining = @h ? (@h - used + 1) : @chars.size
|
|
211
|
+
@chars.each_with_index do |chars, y|
|
|
212
|
+
next if !chars || y >= remaining
|
|
213
|
+
styles = @style[y]
|
|
214
|
+
chars.each_with_index do |ch, x|
|
|
215
|
+
yield x, y + used, cell(ch, styles[x]) if ch
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def each_character_between(spos, epos)
|
|
221
|
+
if spos.end > epos.end
|
|
222
|
+
spos, epos = epos, spos
|
|
223
|
+
elsif spos.end == epos.end && spos.first > epos.first
|
|
224
|
+
spos, epos = epos, spos
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
x = spos.first
|
|
228
|
+
xend, ymax = epos.first, epos.end
|
|
229
|
+
(spos.end..ymax).each do |y|
|
|
230
|
+
line = line_at(y) || ""
|
|
231
|
+
xmax = y == ymax ? xend + 1 : line.length - 1
|
|
232
|
+
xmax = [xmax, line.length - 1].min
|
|
233
|
+
xmax = 0 if xmax < 0
|
|
234
|
+
while x <= xmax
|
|
235
|
+
yield(x, y, line[x])
|
|
236
|
+
x += 1
|
|
237
|
+
end
|
|
238
|
+
x = 0
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# # Writes
|
|
243
|
+
|
|
244
|
+
def set(x, y, ch, fg = 0, bg = 0, flags = 0)
|
|
245
|
+
ch = ch.ord
|
|
246
|
+
if flags.anybits?(BLINK | RAPID_BLINK)
|
|
247
|
+
@blinky << [x, y]
|
|
248
|
+
elsif !@blinky.empty?
|
|
249
|
+
# Only bother removing (and allocating the [x,y] key) when something
|
|
250
|
+
# actually blinks. The overwhelmingly common case is no blinking cells
|
|
251
|
+
# at all, so this skips a per-character array alloc + Set#delete.
|
|
252
|
+
@blinky.delete([x, y])
|
|
253
|
+
end
|
|
254
|
+
ensure_row(y)
|
|
255
|
+
style = pack_style(fg, bg, flags)
|
|
256
|
+
# Bump the generation only on an actual content change (identical
|
|
257
|
+
# rewrites keep their gen, so a cell that didn't change isn't seen as
|
|
258
|
+
# damaged).
|
|
259
|
+
if @chars[y][x] != ch || @style[y][x] != style
|
|
260
|
+
@gen[y][x] = (@generation += 1)
|
|
261
|
+
@row_dirty[y] = true
|
|
262
|
+
end
|
|
263
|
+
@chars[y][x] = ch
|
|
264
|
+
@style[y][x] = style
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def set_lineattrs(y, v) = (@lineattrs[y] = v)
|
|
268
|
+
|
|
269
|
+
# ICH / IRM: open a gap of +num+ cells at x by inserting +cell+, shifting
|
|
270
|
+
# the rest of the line right; cells pushed past the right margin are
|
|
271
|
+
# discarded (the line never grows beyond width). Inserted cells carry no
|
|
272
|
+
# generation (they did not go through #set); they are blanks the caller
|
|
273
|
+
# repaints.
|
|
274
|
+
def insert(x, y, num, cell)
|
|
275
|
+
ensure_row(y)
|
|
276
|
+
ch = cell[0]
|
|
277
|
+
style = pack_style(cell[1], cell[2], cell[3])
|
|
278
|
+
num.times do
|
|
279
|
+
@chars[y].insert(x, ch)
|
|
280
|
+
@style[y].insert(x, style)
|
|
281
|
+
@gen[y].insert(x, nil)
|
|
282
|
+
end
|
|
283
|
+
if @w && @chars[y].length > @w
|
|
284
|
+
@chars[y].slice!(@w..)
|
|
285
|
+
@style[y].slice!(@w..)
|
|
286
|
+
@gen[y].slice!(@w..)
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# DCH: delete +num+ cells at (x,y), shifting the remainder left (gens
|
|
291
|
+
# follow their cells). Vacated cells at the right become blank.
|
|
292
|
+
def delete_chars(x, y, num)
|
|
293
|
+
chars = @chars[y] or return
|
|
294
|
+
num.times do
|
|
295
|
+
break if x >= chars.length
|
|
296
|
+
chars.delete_at(x)
|
|
297
|
+
@style[y].delete_at(x)
|
|
298
|
+
@gen[y].delete_at(x)
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def clear_line(y, start_x = 0, end_x = nil)
|
|
303
|
+
if !end_x
|
|
304
|
+
# Clear to end of line: truncate the row at start_x. Dropped cells
|
|
305
|
+
# become unset (gen nil), so a stale on-screen tail is detectable.
|
|
306
|
+
if @chars[y]
|
|
307
|
+
@chars[y] = @chars[y][0...start_x]
|
|
308
|
+
@style[y] = (@style[y] || [])[0...start_x]
|
|
309
|
+
@gen[y] = (@gen[y] || [])[0...start_x]
|
|
310
|
+
end
|
|
311
|
+
else
|
|
312
|
+
(start_x..end_x).each { |x| set(x, y, ' ') }
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# # Line operations (region-aware)
|
|
317
|
+
|
|
318
|
+
private def raw_delete_line(y)
|
|
319
|
+
@chars.slice!(y)
|
|
320
|
+
@style.slice!(y)
|
|
321
|
+
@gen.slice!(y)
|
|
322
|
+
@lineattrs.slice!(y)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
private def raw_insert_line(y)
|
|
326
|
+
@chars.insert(y, nil)
|
|
327
|
+
@style.insert(y, nil)
|
|
328
|
+
@gen.insert(y, nil)
|
|
329
|
+
@lineattrs.insert(y, 0)
|
|
330
|
+
enforce_height
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def delete_line(y)
|
|
334
|
+
raw_delete_line(y)
|
|
335
|
+
# In a scroll region, deleting a line shifts the region up and inserts a
|
|
336
|
+
# blank line at the bottom (scroll_end), not at the top.
|
|
337
|
+
raw_insert_line(@scroll_end) if @scroll_start
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def insert_line(y)
|
|
341
|
+
raw_insert_line(y)
|
|
342
|
+
# Inserting pushes the region down; discard the line that falls just
|
|
343
|
+
# past the bottom of the region.
|
|
344
|
+
raw_delete_line(@scroll_end + 1) if @scroll_end
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def scroll_up
|
|
348
|
+
# Move the top line of the region into scrollback - the columnar
|
|
349
|
+
# [chars, style] arrays ARE the packed scrollback form, so this is a
|
|
350
|
+
# straight handoff (delete_line then drops the live references). The
|
|
351
|
+
# gen row is discarded (scrollback is not damage-tracked).
|
|
352
|
+
y = @scroll_start.to_i
|
|
353
|
+
@scrollback_buffer.push([@chars[y] || [], @style[y] || []])
|
|
354
|
+
@scrollback_lineattrs.push(lineattrs(y))
|
|
355
|
+
delete_line(y)
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def enforce_height
|
|
359
|
+
return unless @h
|
|
360
|
+
@chars.slice!(@h..)
|
|
361
|
+
@style.slice!(@h..)
|
|
362
|
+
@gen.slice!(@h..)
|
|
363
|
+
@lineattrs.slice!(@h..)
|
|
364
|
+
end
|
|
365
|
+
end
|
data/lib/trackchanges.rb
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
|
|
2
|
+
# FIXME: Roll this into the actual buffer.
|
|
3
|
+
class TrackChanges
|
|
4
|
+
# The cursor is rendered as an overlay - a cell repainted with this
|
|
5
|
+
# background. The AnsiBackend recognises it and turns it into a real
|
|
6
|
+
# terminal cursor; the X11 backend paints the block.
|
|
7
|
+
CURSOR = 0xff00ff
|
|
8
|
+
|
|
9
|
+
def initialize buffer, adapter
|
|
10
|
+
@buffer = buffer
|
|
11
|
+
@adapter = adapter
|
|
12
|
+
@cursor_pos = nil # where the cursor overlay was last painted
|
|
13
|
+
# When true, #set only mutates the buffer; rendering is deferred to the
|
|
14
|
+
# next #draw_flush, which walks the buffer's damage (generation) instead
|
|
15
|
+
# of drawing eagerly per cell. Default off (the proven eager path) while
|
|
16
|
+
# the damage-driven path is validated against it; see test_damage.rb.
|
|
17
|
+
@defer = false
|
|
18
|
+
@last_flush_gen = 0
|
|
19
|
+
@rows = 24 # overwritten by on_resize before use
|
|
20
|
+
# When true, ALL rendering is suppressed (the buffer still mutates):
|
|
21
|
+
# used to jump-scroll a flood of output by interpreting many chunks and
|
|
22
|
+
# then doing ONE full redraw of the final screen, skipping the
|
|
23
|
+
# intermediate frames the user would never see. The model (incl.
|
|
24
|
+
# scrollback) stays correct; only the framebuffer is batched.
|
|
25
|
+
@suspend = false
|
|
26
|
+
clear
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
attr_reader :buffer
|
|
30
|
+
attr_accessor :defer, :suspend
|
|
31
|
+
|
|
32
|
+
# Rendering is off either because we're viewing scrollback history or
|
|
33
|
+
# because output is being jump-scrolled.
|
|
34
|
+
def suppressed? = @suspend || @adapter.scrollback_mode
|
|
35
|
+
|
|
36
|
+
def clear
|
|
37
|
+
# Flush any batched text first: otherwise pending draws are emitted to
|
|
38
|
+
# the screen AFTER the clear and survive it (stale content; the buffer
|
|
39
|
+
# is already correct, so only the incremental render diverges).
|
|
40
|
+
draw_flush
|
|
41
|
+
@buffer.clear
|
|
42
|
+
@adapter.clear unless suppressed?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Methods that does not alter the buffer
|
|
46
|
+
def lineattrs(y) = @buffer.lineattrs(y)
|
|
47
|
+
def get(x,y) = @buffer.get(x,y)
|
|
48
|
+
def scroll_start = @buffer.scroll_start
|
|
49
|
+
def scroll_end = @buffer.scroll_end
|
|
50
|
+
def blinky = @buffer.blinky
|
|
51
|
+
# Backend-facing queries the interpreter routes through the buffer rather
|
|
52
|
+
# than reaching the adapter directly (so Term talks only to its buffer).
|
|
53
|
+
def scrollback_mode = @adapter.scrollback_mode
|
|
54
|
+
def set_columns(cols) = @adapter.set_columns(cols)
|
|
55
|
+
def each_character(scrollback_offset = 0, &block)
|
|
56
|
+
@buffer.each_character(scrollback_offset, &block)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# # Mutation
|
|
60
|
+
#
|
|
61
|
+
# Scroll the region up one line: draw pending damage, scroll the model,
|
|
62
|
+
# then drive the backend - a blit, or (when scrolled back) just anchor the
|
|
63
|
+
# viewport so the frozen history lines stay in place. The blit's inclusive
|
|
64
|
+
# bottom row is the scroll region's, or the last screen row when unset.
|
|
65
|
+
def scroll_up
|
|
66
|
+
draw_flush
|
|
67
|
+
start = @buffer.scroll_start.to_i
|
|
68
|
+
bottom = @buffer.scroll_end || (@rows - 1)
|
|
69
|
+
@buffer.scroll_up
|
|
70
|
+
return if @suspend
|
|
71
|
+
if @adapter.scrollback_mode
|
|
72
|
+
@adapter.scrollback_anchor
|
|
73
|
+
else
|
|
74
|
+
@adapter.scroll_up(start, bottom)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def delete_lines(y, num, maxy)
|
|
79
|
+
draw_flush
|
|
80
|
+
# Delete repeatedly at the SAME row: each delete shifts the rows below
|
|
81
|
+
# up into y, so deleting at y+i would skip every other line.
|
|
82
|
+
num.times { @buffer.delete_line(y) }
|
|
83
|
+
@adapter.delete_lines(y, num, @buffer.scroll_end||maxy) unless suppressed?
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def insert_lines(y, num, maxy)
|
|
87
|
+
draw_flush
|
|
88
|
+
num.times.each {|i| @buffer.insert_line(y+i) }
|
|
89
|
+
@adapter.insert_lines(y, num, @buffer.scroll_end || maxy) unless suppressed?
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def clear_line(*args)
|
|
93
|
+
draw_flush
|
|
94
|
+
@buffer.clear_line(*args)
|
|
95
|
+
@adapter.clear_line(*args) unless suppressed?
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def set(x,y,c,fg,bg,mode)
|
|
99
|
+
# MUST be before the @buffer.set below, as draw_buffered compares
|
|
100
|
+
# against the buffer's *current* content to avoid redundant redraws.
|
|
101
|
+
# Skipped while scrolled back so live output does not paint over the
|
|
102
|
+
# scrolled-back view (the buffer still updates).
|
|
103
|
+
#
|
|
104
|
+
# draw_buffered reads the cell's four fields synchronously and never
|
|
105
|
+
# retains the array, so we reuse a per-instance scratch cell instead of
|
|
106
|
+
# allocating [c,fg,bg,mode] per character. Safe: a single processing
|
|
107
|
+
# thread, with no re-entrancy back into #set.
|
|
108
|
+
unless @defer || @adapter.scrollback_mode
|
|
109
|
+
s = (@scratch ||= [])
|
|
110
|
+
s[0], s[1], s[2], s[3] = c, fg, bg, mode
|
|
111
|
+
draw_buffered(x, y, s)
|
|
112
|
+
end
|
|
113
|
+
@buffer.set(x,y,c,fg,bg,mode)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def on_resize(w,h)
|
|
117
|
+
raise if !h
|
|
118
|
+
@rows = h # used as the default scroll-region bottom in scroll_up
|
|
119
|
+
# FIXME: Window is currently resized separately.
|
|
120
|
+
@buffer.on_resize(w,h)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Explicit delegations to the underlying buffer, replacing a catch-all
|
|
124
|
+
# method_missing so the buffer's surface through TrackChanges is
|
|
125
|
+
# knowable. Each is a model-only mutation whose on-screen redraw the
|
|
126
|
+
# caller drives separately (Term#redraw_line_from_cursor after
|
|
127
|
+
# insert/delete_chars; Term#set_line_attrs after set_lineattrs) or a
|
|
128
|
+
# read-only query - none of them paint, which is why they bypass the
|
|
129
|
+
# adapter. (The scroll_start/scroll_end getters are defined above; only
|
|
130
|
+
# the setters delegate.)
|
|
131
|
+
def insert(*args) = @buffer.insert(*args)
|
|
132
|
+
def delete_chars(*args) = @buffer.delete_chars(*args)
|
|
133
|
+
def set_lineattrs(*args) = @buffer.set_lineattrs(*args)
|
|
134
|
+
def scroll_start=(v); @buffer.scroll_start = v; end
|
|
135
|
+
def scroll_end=(v); @buffer.scroll_end = v; end
|
|
136
|
+
def scrollback_size = @buffer.scrollback_size
|
|
137
|
+
def each_character_between(*args, &block) = @buffer.each_character_between(*args, &block)
|
|
138
|
+
|
|
139
|
+
def redraw_blink
|
|
140
|
+
return nil if suppressed?
|
|
141
|
+
b = @buffer.blinky
|
|
142
|
+
return nil if b.empty?
|
|
143
|
+
b.each { |x,y| redraw(x,y) }
|
|
144
|
+
draw_flush
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def redraw(x,y) = draw_buffered(x,y, @buffer.get(x,y), true)
|
|
148
|
+
|
|
149
|
+
# Render the cursor overlay at (x,y) if +visible+, after restoring the
|
|
150
|
+
# cell under its previous position. A no-op while scrolled back, so the
|
|
151
|
+
# live cursor doesn't paint over the frozen history view.
|
|
152
|
+
def draw_cursor(x, y, visible)
|
|
153
|
+
return if suppressed?
|
|
154
|
+
clear_cursor
|
|
155
|
+
return unless visible
|
|
156
|
+
redraw_with(x, y, bg: CURSOR)
|
|
157
|
+
@cursor_pos = [x, y]
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def clear_cursor
|
|
161
|
+
return if suppressed?
|
|
162
|
+
return unless @cursor_pos
|
|
163
|
+
redraw(*@cursor_pos)
|
|
164
|
+
@cursor_pos = nil
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def redraw_all(scrollback_offset = 0)
|
|
168
|
+
@buffer.each_character(scrollback_offset) { |*args| draw_buffered(*args, true) }
|
|
169
|
+
# We just force-drew every cell, so nothing is damaged relative to now:
|
|
170
|
+
# advance the watermark before flushing so the damage walk doesn't redraw
|
|
171
|
+
# it all again (and so the next incremental flush only sees new changes).
|
|
172
|
+
@last_flush_gen = @buffer.generation
|
|
173
|
+
draw_flush
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def redraw_with(x,y, fg: nil, bg: nil)
|
|
177
|
+
cell = Array(@buffer.get(x,y)).dup
|
|
178
|
+
cell[0] ||= " "
|
|
179
|
+
cell[1] = fg if fg
|
|
180
|
+
cell[2] = bg if bg
|
|
181
|
+
draw_buffered(x,y, cell, true)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Draw an already-resolved cell at a *screen* position, optionally
|
|
185
|
+
# overriding fg/bg. Used when the cell's buffer row and its screen row
|
|
186
|
+
# differ (selection highlighting while scrolled back into scrollback).
|
|
187
|
+
def redraw_cell_at(screen_x, screen_y, cell, fg: nil, bg: nil)
|
|
188
|
+
cell = Array(cell).dup
|
|
189
|
+
cell[0] ||= " "
|
|
190
|
+
cell[1] = fg if fg
|
|
191
|
+
cell[2] = bg if bg
|
|
192
|
+
draw_buffered(screen_x, screen_y, cell, true)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Repaint whatever is currently displayed at a screen position, given the
|
|
196
|
+
# active scrollback offset (so scrollback rows repaint their scrolled-off
|
|
197
|
+
# content rather than the live buffer's).
|
|
198
|
+
def redraw_display(screen_x, screen_y, scrollback_offset = 0)
|
|
199
|
+
buffer_y = screen_y - scrollback_offset
|
|
200
|
+
draw_buffered(screen_x, screen_y, @buffer.get(screen_x, buffer_y), true)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Public flush point. In the default (eager) mode draws already happened
|
|
204
|
+
# on #set, so this just emits the pending run. In damage-driven (defer)
|
|
205
|
+
# mode #set only mutates, so a flush first walks the buffer's damage and
|
|
206
|
+
# draws the changed cells (run-batched) before emitting. Either way it
|
|
207
|
+
# then emits the run buffer, which also carries force-redraws (cursor,
|
|
208
|
+
# ICH/DCH, blink, selection).
|
|
209
|
+
def draw_flush
|
|
210
|
+
return if @suspend # jump-scrolling: defer all rendering to the redraw
|
|
211
|
+
if @defer && !@adapter.scrollback_mode
|
|
212
|
+
@buffer.each_damaged(@last_flush_gen) do |x, y, ch, fg, bg, flags|
|
|
213
|
+
s = (@scratch ||= [])
|
|
214
|
+
s[0], s[1], s[2], s[3] = ch, fg, bg, flags
|
|
215
|
+
draw_buffered(x, y, s, true)
|
|
216
|
+
end
|
|
217
|
+
@last_flush_gen = @buffer.generation
|
|
218
|
+
end
|
|
219
|
+
flush_buf
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Emit the batched run and reset the batch. Internal: draw_buffered calls
|
|
223
|
+
# this on a run break, so it must NOT re-enter the damage walk above.
|
|
224
|
+
def flush_buf
|
|
225
|
+
if @bufx && @buf && @buf[0] && !@buf[0].empty?
|
|
226
|
+
c = @buf[0]
|
|
227
|
+
fg = @buf[1] || PALETTE_BASIC[7]
|
|
228
|
+
bg = @buf[2] || PALETTE_BASIC[0]
|
|
229
|
+
if c == " " && fg == 0 && bg == 0 # Why?
|
|
230
|
+
else
|
|
231
|
+
lineattrs = @buffer.lineattrs(@bufy)
|
|
232
|
+
flags = @buf[3].to_i
|
|
233
|
+
#p [:flush, fg, c]
|
|
234
|
+
@adapter.draw(@bufx, @bufy, c, fg, bg, flags, lineattrs)
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
@buf = []
|
|
238
|
+
@bufx = nil
|
|
239
|
+
@bufy = nil
|
|
240
|
+
@last_x = -2
|
|
241
|
+
@last_y = -2
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# This is a hack
|
|
246
|
+
def draw_buffered(x,y,cell, force=false)
|
|
247
|
+
@last_x ||= -255
|
|
248
|
+
@last_y ||= -255
|
|
249
|
+
@buf ||= ["",PALETTE_BASIC[7], PALETTE_BASIC[0],0]
|
|
250
|
+
cell ||= [" "]
|
|
251
|
+
|
|
252
|
+
#p [:buffered, x, y, cell, @bufx, @bufy, @buf, force]
|
|
253
|
+
if @buf[0] && @buf[0].length > 160
|
|
254
|
+
flush_buf
|
|
255
|
+
elsif @last_y != y || @last_x + 1 != x
|
|
256
|
+
flush_buf
|
|
257
|
+
elsif (@buf[1] != cell[1]) or (@buf[2] != cell[2]) or (@buf[3] != cell[3])
|
|
258
|
+
flush_buf
|
|
259
|
+
else
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# FIXME: It is possible this is called from multiple threads.
|
|
263
|
+
# Uh oh. That *will* be trouble. Either changes must be serialized -
|
|
264
|
+
# they certainly must be for the backend screen buffer - or
|
|
265
|
+
# this must be made thread local.
|
|
266
|
+
#
|
|
267
|
+
@buf[0] ||= ""
|
|
268
|
+
|
|
269
|
+
# This is to get better performance out of applications that
|
|
270
|
+
# carelessly prints far more than they ought to.
|
|
271
|
+
# *cough* my editor *cough*
|
|
272
|
+
if force
|
|
273
|
+
match = false
|
|
274
|
+
else
|
|
275
|
+
# Skip the draw if the buffer already holds this exact cell, or if
|
|
276
|
+
# we're writing a default-background space over an unset cell.
|
|
277
|
+
# Compared against the buffer's columnar storage directly, so no cell
|
|
278
|
+
# Array is reconstructed per character.
|
|
279
|
+
# FIXME: Make this more deliberate about *background* attributes.
|
|
280
|
+
match = (cell[0] == 32 && cell[2] == BG && @buffer.unset?(x, y)) ||
|
|
281
|
+
@buffer.cell_eq?(x, y, cell[0], cell[1], cell[2], cell[3])
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# FIXME: The #to_s here is a workaround for thread sync issues.
|
|
285
|
+
if @buf[0].to_s.empty?
|
|
286
|
+
if match
|
|
287
|
+
return
|
|
288
|
+
else
|
|
289
|
+
#p [:diff, x,y, cell, bcell]
|
|
290
|
+
end
|
|
291
|
+
elsif match
|
|
292
|
+
# This heuristic could probably be better:
|
|
293
|
+
# * Keep a count, and trigger on the *number of matches*
|
|
294
|
+
# instead of on the number of characters. This to e.g. prevent a
|
|
295
|
+
# single coinciding character from splitting up the rendering into
|
|
296
|
+
# 8-char chunks
|
|
297
|
+
#p [:match_non_empty, @buf[0].length]
|
|
298
|
+
if @buf[0]&.length.to_i > 8
|
|
299
|
+
# If flushing here, chop the buffer down to the point of the first
|
|
300
|
+
# match.
|
|
301
|
+
flush_buf
|
|
302
|
+
return
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
c = cell[0]
|
|
307
|
+
|
|
308
|
+
@buf[1] ||= cell[1]
|
|
309
|
+
@buf[2] ||= cell[2]
|
|
310
|
+
@buf[3] ||= cell[3]
|
|
311
|
+
@buf[0] ||= ""
|
|
312
|
+
@buf[0] << (c || "")
|
|
313
|
+
@bufx ||= x
|
|
314
|
+
@bufy ||= y
|
|
315
|
+
@last_x = x
|
|
316
|
+
@last_y = y
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
end
|