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/utf8decoder.rb
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
|
|
2
|
+
class UTF8Decoder
|
|
3
|
+
attr_reader :buffer
|
|
4
|
+
|
|
5
|
+
def initialize
|
|
6
|
+
@buffer = "".b
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def <<(str) = @buffer << str.b
|
|
10
|
+
|
|
11
|
+
# Yields each complete character as a String (the original contract).
|
|
12
|
+
def each(&block)
|
|
13
|
+
decode { |complete| complete.each_char(&block) }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Yields each complete character as an Integer codepoint. The hot path
|
|
17
|
+
# (Term#feed) wants codepoints, not 1-char Strings: on a valid chunk
|
|
18
|
+
# String#each_codepoint avoids allocating a String per character and the
|
|
19
|
+
# per-character valid_encoding?/ord that #feed used to do. Validity is
|
|
20
|
+
# checked once per chunk; only a chunk that actually contains bad bytes
|
|
21
|
+
# falls back to the slower per-character path (rendering them as U+FFFD).
|
|
22
|
+
def each_codepoint(&block)
|
|
23
|
+
decode do |complete|
|
|
24
|
+
if complete.valid_encoding?
|
|
25
|
+
complete.each_codepoint(&block)
|
|
26
|
+
else
|
|
27
|
+
complete.each_char { |c| block.call(c.valid_encoding? ? c.ord : 0xFFFD) }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Split @buffer into a complete-sequence prefix and a saved leftover,
|
|
33
|
+
# then yield the prefix (encoding-tagged) to the caller's per-character
|
|
34
|
+
# iterator.
|
|
35
|
+
private def decode
|
|
36
|
+
# We acknowledge that @buffer can contain
|
|
37
|
+
# sequences that are invalid UTF8, and we will
|
|
38
|
+
# do our best with them *unless*:
|
|
39
|
+
# * @buffer[-1] starts any multibyte sequence
|
|
40
|
+
# * @buffer[-2] starts a 3 or 4 byte sequence
|
|
41
|
+
# * @buffer[-3] starts a 4 byte sequence.
|
|
42
|
+
# In those cases, and those cases only, we
|
|
43
|
+
# save those bytes for next time.
|
|
44
|
+
|
|
45
|
+
str = @buffer
|
|
46
|
+
return nil if !str || str.empty?
|
|
47
|
+
last = str.length-1
|
|
48
|
+
|
|
49
|
+
if str[-1].ord >= 0x80
|
|
50
|
+
# -1 is part of a multibyte sequence (a continuation byte 0x80-0xBF
|
|
51
|
+
# or a lead byte 0xC0+). NB: this must be >= 0x80, not > 0x80 - a
|
|
52
|
+
# trailing 0x80 is the second byte of e.g. an em-dash (E2 80 94)
|
|
53
|
+
# split across a pty read; treating it as complete dropped the
|
|
54
|
+
# lead bytes and orphaned the final byte into the next chunk.
|
|
55
|
+
if str.length == 1
|
|
56
|
+
# Single byte that starts a multibyte sequence
|
|
57
|
+
last = -2 # Process nothing, save everything
|
|
58
|
+
elsif str[-2] && str[-2].ord & 0xe0 == 0xc0 # -2..-1 is a 2 byte sequence; we're good.
|
|
59
|
+
elsif str[-2] && str[-2].ord & 0xe0 == 0xe0 # Start of a 3 or 4 byte sequence
|
|
60
|
+
last = str.length-3
|
|
61
|
+
else # -2 is *part of a 3 or 4 byte sequence
|
|
62
|
+
if str[-3] && str[-3].ord & 0xf0 == 0xe0 # -3 Starts a 3 byte sequence
|
|
63
|
+
elsif str[-3] && str[-3].ord & 0xf8 == 0xf0 # -3 starts a 4 byte sequence
|
|
64
|
+
last = str.length-4
|
|
65
|
+
else # -2 must be the final byte of something, so we only chop the last
|
|
66
|
+
last = str.length-2
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
@leftover = str[last+1..-1].b
|
|
71
|
+
complete = str[0..last].force_encoding("UTF-8")
|
|
72
|
+
yield complete
|
|
73
|
+
@buffer = @leftover
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
end
|
|
77
|
+
|
data/lib/window.rb
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
|
|
2
|
+
# Encapsulate the X backend and
|
|
3
|
+
# operations on the window
|
|
4
|
+
|
|
5
|
+
require 'skrift'
|
|
6
|
+
require 'skrift/x11'
|
|
7
|
+
#require 'pry'
|
|
8
|
+
|
|
9
|
+
class Window
|
|
10
|
+
attr_reader :dpy, :wid, :scrollback_count # FIXME
|
|
11
|
+
attr_accessor :width, :height
|
|
12
|
+
|
|
13
|
+
# Get scrollback status
|
|
14
|
+
def scrollback_mode
|
|
15
|
+
@scrollback_count > 0
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Set buffer reference to access scrollback size
|
|
19
|
+
def set_buffer(buffer)
|
|
20
|
+
@buffer = buffer
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Get scrollback buffer size
|
|
24
|
+
def scrollback_buffer_size
|
|
25
|
+
return 0 unless @buffer
|
|
26
|
+
@buffer.scrollback_size
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Increase scrollback counter
|
|
30
|
+
def scrollback_page_up
|
|
31
|
+
# Get current scrollback buffer size
|
|
32
|
+
max_scrollback = scrollback_buffer_size
|
|
33
|
+
return if max_scrollback == 0
|
|
34
|
+
|
|
35
|
+
# Store previous count for calculating how many new lines to clear
|
|
36
|
+
previous_count = @scrollback_count
|
|
37
|
+
|
|
38
|
+
# Limit scrollback count to available lines
|
|
39
|
+
@scrollback_count += 10
|
|
40
|
+
if @scrollback_count > max_scrollback
|
|
41
|
+
@scrollback_count = max_scrollback
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Calculate how many new lines we've scrolled and clear them
|
|
45
|
+
new_lines = @scrollback_count - previous_count
|
|
46
|
+
if new_lines > 0
|
|
47
|
+
# Clear the area where new scrollback lines will appear
|
|
48
|
+
clear(0, 0, @width, new_lines * char_h)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
draw_scrollback_indicator if @scrollback_count <= 10
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Snap straight back to the live screen (bottom of scrollback). Returns
|
|
55
|
+
# true if we were scrolled back, so the caller knows a redraw is needed.
|
|
56
|
+
def scrollback_reset
|
|
57
|
+
return false if @scrollback_count <= 0
|
|
58
|
+
@scrollback_count = 0
|
|
59
|
+
@dirty = true
|
|
60
|
+
true
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Keep the viewport anchored to the same history lines when a new line is
|
|
64
|
+
# pushed into scrollback while we're scrolled back. Without this the
|
|
65
|
+
# displayed (frozen) lines and the selection->buffer mapping would drift
|
|
66
|
+
# apart as output streams.
|
|
67
|
+
def scrollback_anchor
|
|
68
|
+
return if @scrollback_count <= 0
|
|
69
|
+
@scrollback_count += 1
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Decrease scrollback counter
|
|
73
|
+
def scrollback_page_down
|
|
74
|
+
return false if @scrollback_count <= 0
|
|
75
|
+
|
|
76
|
+
# Remember previous count
|
|
77
|
+
previous_count = @scrollback_count
|
|
78
|
+
|
|
79
|
+
@scrollback_count -= 10
|
|
80
|
+
if @scrollback_count <= 0
|
|
81
|
+
# Exiting scrollback mode
|
|
82
|
+
@scrollback_count = 0
|
|
83
|
+
@dirty = true
|
|
84
|
+
|
|
85
|
+
# Clear entire scrollback area that was showing
|
|
86
|
+
clear(0, 0, @width, previous_count * char_h)
|
|
87
|
+
return true
|
|
88
|
+
else
|
|
89
|
+
# Clear area that was occupied by lines no longer in scrollback
|
|
90
|
+
lines_removed = previous_count - @scrollback_count
|
|
91
|
+
if lines_removed > 0
|
|
92
|
+
clear(0, 0, @width, lines_removed * char_h)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
false
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def initialize(**opts)
|
|
99
|
+
@scrollback_count = 0
|
|
100
|
+
|
|
101
|
+
@dpy = X11::Display.new
|
|
102
|
+
@screen = @dpy.screens.first
|
|
103
|
+
|
|
104
|
+
@alpha = 0x80 << 24
|
|
105
|
+
@opaque = 0xff << 24
|
|
106
|
+
|
|
107
|
+
eventmask = X11::Form::StructureNotifyMask | # ConfigureNotify for our own resize
|
|
108
|
+
X11::Form::SubstructureNotifyMask |
|
|
109
|
+
X11::Form::ButtonReleaseMask |
|
|
110
|
+
X11::Form::Button1MotionMask |
|
|
111
|
+
X11::Form::ExposureMask |
|
|
112
|
+
X11::Form::KeyPressMask |
|
|
113
|
+
X11::Form::ButtonPressMask
|
|
114
|
+
|
|
115
|
+
@visual = @dpy.find_visual(0, 32).visual_id
|
|
116
|
+
|
|
117
|
+
@width, @height = 1000, 600
|
|
118
|
+
|
|
119
|
+
@wid = @dpy.create_window(
|
|
120
|
+
0, 0, @width, @height,
|
|
121
|
+
visual: @visual,
|
|
122
|
+
values: {
|
|
123
|
+
X11::Form::CWBackPixel => 0x00 | @alpha, # ARGB background; transparency
|
|
124
|
+
X11::Form::CWBorderPixel => 0,
|
|
125
|
+
# Bit gravity NorthWest (1): on resize the server retains the
|
|
126
|
+
# existing pixels (anchored top-left) instead of discarding them
|
|
127
|
+
# to the background. The default ForgetGravity blanks the whole
|
|
128
|
+
# window every resize before we repaint -- a visible flash.
|
|
129
|
+
X11::Form::CWBitGravity => 1,
|
|
130
|
+
X11::Form::CWEventMask => eventmask,
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
#@gc2 = @dpy.create_gc(@wid, foreground: 0xffffff, background: 0x80000000)
|
|
135
|
+
|
|
136
|
+
# Create pixmap buffer with enough space for the window
|
|
137
|
+
create_buffer
|
|
138
|
+
|
|
139
|
+
#@buf = @wid
|
|
140
|
+
|
|
141
|
+
@scale = opts[:fontsize] || 16
|
|
142
|
+
# Horizontal font scale, decoupled from @scale so DECCOLM (80/132) can
|
|
143
|
+
# narrow/widen the glyph cell while keeping the row height (and thus the
|
|
144
|
+
# row count) constant. Defaults to @scale (square cell).
|
|
145
|
+
@col_scale = @scale
|
|
146
|
+
@fontset = opts[:fonts]
|
|
147
|
+
setup_fonts
|
|
148
|
+
|
|
149
|
+
@dirty = false
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def map_window = @dpy.map_window(@wid)
|
|
153
|
+
|
|
154
|
+
def dirty! = (@dirty = true)
|
|
155
|
+
|
|
156
|
+
# Create a buffer to back the terminal window
|
|
157
|
+
def create_buffer
|
|
158
|
+
# Free the old resources if they exist
|
|
159
|
+
if defined?(@pic) && @pic
|
|
160
|
+
@dpy.render_free_picture(@pic)
|
|
161
|
+
@pic = nil
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
if defined?(@buf) && @buf
|
|
165
|
+
@dpy.free_pixmap(@buf)
|
|
166
|
+
@buf = nil
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Create new buffer with dimensions matching the window
|
|
170
|
+
# Add extra space for possible future window growth
|
|
171
|
+
buffer_width = [@width * 2, 1920].max
|
|
172
|
+
buffer_height = [@height * 2, 1080].max
|
|
173
|
+
|
|
174
|
+
@buf = @dpy.create_pixmap(32, @wid, buffer_width, buffer_height)
|
|
175
|
+
@buf_width = buffer_width
|
|
176
|
+
@buf_height = buffer_height
|
|
177
|
+
|
|
178
|
+
# Clear the entire buffer
|
|
179
|
+
clear(0, 0, buffer_width, buffer_height)
|
|
180
|
+
|
|
181
|
+
# Create the picture
|
|
182
|
+
fmt = @dpy.render_find_visual_format(@visual)
|
|
183
|
+
@pic = @dpy.render_create_picture(@buf, fmt)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def on_resize(w,h)
|
|
187
|
+
ow,oh=@width,@height
|
|
188
|
+
@width, @height = w,h
|
|
189
|
+
|
|
190
|
+
# If the window dimensions exceed the buffer size, recreate the buffer
|
|
191
|
+
if w > @buf_width || h > @buf_height
|
|
192
|
+
# Free old resources and create new buffer
|
|
193
|
+
create_buffer
|
|
194
|
+
|
|
195
|
+
# Signal that a full redraw is needed
|
|
196
|
+
@dirty = true
|
|
197
|
+
else
|
|
198
|
+
# Clear newly visible areas
|
|
199
|
+
clear(ow, 0, w-ow, [oh,h].min) if w > ow
|
|
200
|
+
clear(0, oh, w, h-oh) if h > oh
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
copy_buffer
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def setup_fonts
|
|
207
|
+
xs = @col_scale || @scale
|
|
208
|
+
# fit: scale oversized glyphs (e.g. wide spinner/symbol chars) down into
|
|
209
|
+
# the fixed cell instead of letting them overflow/clamp at the edge.
|
|
210
|
+
@skr = Skrift::X11::Glyphs.new(@dpy, fontset: @fontset, x_scale: xs, y_scale: @scale, fixed: true, fit: true)
|
|
211
|
+
# FIXME: Maybe instantiate these as needed.
|
|
212
|
+
@skr_dblheight = Skrift::X11::Glyphs.new(@dpy, fontset: @fontset, x_scale: xs*2, y_scale: @scale*2, fixed: true)
|
|
213
|
+
@skr_dblwidth = Skrift::X11::Glyphs.new(@dpy, fontset: @fontset, x_scale: xs*2, y_scale: @scale, fixed: true)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def adjust_fontsize(adj)
|
|
217
|
+
@scale += adj
|
|
218
|
+
@scale = @scale.clamp(5, 100)
|
|
219
|
+
@col_scale = @scale # back to a square cell on manual zoom
|
|
220
|
+
@char_w = nil
|
|
221
|
+
@char_h = nil
|
|
222
|
+
setup_fonts
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# DECCOLM font mode: scale the glyph cell so +cols+ columns fit within
|
|
226
|
+
# +pixel_width+, keeping the row height (and row count) constant. The
|
|
227
|
+
# window is not resized; the font scales (down for 132, up for a wide
|
|
228
|
+
# window) to fill it as best integer cells allow.
|
|
229
|
+
#
|
|
230
|
+
# char_w is ceil(scaled advance), so we UNDERSHOOT: aim for
|
|
231
|
+
# char_w <= floor(pixel_width / cols), otherwise cols * char_w exceeds the
|
|
232
|
+
# window and the rightmost columns fall off the right edge. Integer cells
|
|
233
|
+
# mean a small right margin can remain; that is accepted (we don't do
|
|
234
|
+
# sub-pixel cell placement).
|
|
235
|
+
def fit_columns(cols, pixel_width)
|
|
236
|
+
return if cols <= 0 || pixel_width <= 0
|
|
237
|
+
target = pixel_width / cols # integer floor: cols*target <= width
|
|
238
|
+
return if target < 1
|
|
239
|
+
# Proportional first guess from the current cell width.
|
|
240
|
+
@col_scale = (@col_scale.to_f * target / [char_w, 1].max).clamp(2.0, 400.0)
|
|
241
|
+
reset_font!
|
|
242
|
+
# Shrink until the columns actually fit (char_w may have rounded up).
|
|
243
|
+
while char_w > target && @col_scale > 2.0
|
|
244
|
+
@col_scale *= 0.96
|
|
245
|
+
reset_font!
|
|
246
|
+
end
|
|
247
|
+
# Grow back toward the target as long as we still fit, for the widest
|
|
248
|
+
# cell that doesn't overflow.
|
|
249
|
+
while @col_scale < 400.0
|
|
250
|
+
@col_scale *= 1.02
|
|
251
|
+
reset_font!
|
|
252
|
+
if char_w > target
|
|
253
|
+
@col_scale /= 1.02
|
|
254
|
+
reset_font!
|
|
255
|
+
break
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def reset_font!
|
|
261
|
+
@char_w = @char_h = nil
|
|
262
|
+
setup_fonts
|
|
263
|
+
end
|
|
264
|
+
private :reset_font!
|
|
265
|
+
|
|
266
|
+
# DECCOLM window mode: ask X to resize the window to the given pixel size
|
|
267
|
+
# (the WM may or may not honour it; the resulting ConfigureNotify drives
|
|
268
|
+
# the normal resize path).
|
|
269
|
+
def request_pixel_size(w, h)
|
|
270
|
+
@dpy.configure_window(@wid, width: w, height: h)
|
|
271
|
+
@dpy.flush if @dpy.respond_to?(:flush)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def char_w
|
|
275
|
+
@char_w ||= @skr.fixed_width
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def char_h
|
|
279
|
+
return @char_h if @char_h
|
|
280
|
+
lm = @skr.lm
|
|
281
|
+
@char_h = (lm.ascender - lm.descender + lm.line_gap).floor
|
|
282
|
+
@skr.maxheight = @char_h + 1
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def fillrect(x,y,w,h,fg)
|
|
286
|
+
# FIXME: Consider if I want this opaque (as gc_for_col does currently) or
|
|
287
|
+
# not, or maybe *less* transparent but not fully opaque
|
|
288
|
+
@dpy.poly_fill_rectangle(@buf, gc_for_col(fg,0x0), [x, y, w, h])
|
|
289
|
+
@dirty = true
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def clear(x,y,w,h)
|
|
293
|
+
# gc_for_col makes the foreground opaque
|
|
294
|
+
@cleargc ||= @dpy.create_gc(@buf, foreground: 0x0|@alpha, background: 0)
|
|
295
|
+
@dpy.poly_fill_rectangle(@buf, @cleargc, [x, y, w, h])
|
|
296
|
+
@dirty = true
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def gc_for_col(fg,bg)
|
|
300
|
+
@gcs ||= {}
|
|
301
|
+
key = "#{fg},#{bg}"
|
|
302
|
+
return @gcs[key] if @gcs[key]
|
|
303
|
+
bg |= @alpha
|
|
304
|
+
fg |= @opaque
|
|
305
|
+
gc = @dpy.create_gc(@buf, foreground: fg, background: bg)
|
|
306
|
+
@gcs[key]=gc
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# FIXME: Line draw, not rect
|
|
310
|
+
def draw_line(x,y,w,fg) = fillrect(x,y,w,1,fg)
|
|
311
|
+
|
|
312
|
+
def draw(x,y, c, fg, bg, lineattrs)
|
|
313
|
+
case lineattrs
|
|
314
|
+
# On double-width/height lines each cell is twice as wide, so column N
|
|
315
|
+
# sits at pixel 2*N*char_w. The incoming x is the single-width pixel
|
|
316
|
+
# position (col*char_w); double it so a run that does not start at
|
|
317
|
+
# column 0 still lands in the right place.
|
|
318
|
+
when :dbl_upper
|
|
319
|
+
# FIXME: Clipping
|
|
320
|
+
fillrect(x*2,y,c.length*char_w*2,char_h*2,bg)
|
|
321
|
+
@skr_dblheight.render_str(@pic, fg, x*2, y, c)
|
|
322
|
+
when :dbl_lower
|
|
323
|
+
# FIXME: Clipping
|
|
324
|
+
fillrect(x*2,y,c.length*char_w*2,char_h*2,bg)
|
|
325
|
+
@skr_dblheight.render_str(@pic, fg, x*2, y-char_h, c)
|
|
326
|
+
when :dbl_single
|
|
327
|
+
fillrect(x*2,y,c.length*char_w*2,char_h,bg)
|
|
328
|
+
@skr_dblwidth.render_str(@pic, fg, x*2, y, c)
|
|
329
|
+
else
|
|
330
|
+
fillrect(x,y,c.length*char_w,char_h,bg)
|
|
331
|
+
c.rstrip!
|
|
332
|
+
@skr.render_str(@pic,fg, x, y, c)
|
|
333
|
+
end
|
|
334
|
+
return
|
|
335
|
+
#DEBUG
|
|
336
|
+
#p c
|
|
337
|
+
c.each_char do |_|
|
|
338
|
+
draw_line(x,y,char_w, 0x2f0000)
|
|
339
|
+
draw_line(x,y+char_h,char_w, 0x002f00)
|
|
340
|
+
fillrect(x,y,1, char_h, 0x00002f)
|
|
341
|
+
x+=char_w
|
|
342
|
+
end
|
|
343
|
+
@dirty = true
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def draw_scrollback_indicator
|
|
347
|
+
if @scrollback_count > 0
|
|
348
|
+
# First clear the top line to avoid overlapping text
|
|
349
|
+
clear(0, 0, @width, char_h)
|
|
350
|
+
|
|
351
|
+
indicator = "---scrollback---"
|
|
352
|
+
x = (@width - indicator.length * char_w) / 2
|
|
353
|
+
@skr.render_str(@pic, 0xff0000, x, 0, indicator)
|
|
354
|
+
@dirty = true
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def copy_buffer
|
|
359
|
+
@dirty = false
|
|
360
|
+
@flushgc ||= @dpy.create_gc(@buf, foreground: @alpha, background: @alpha,
|
|
361
|
+
graphics_exposures: false
|
|
362
|
+
)
|
|
363
|
+
@dpy.copy_area(@buf, @wid, @flushgc, 0, 0, 0,0,@width, @height)
|
|
364
|
+
|
|
365
|
+
# Draw scrollback indicator after copying buffer if in scrollback mode
|
|
366
|
+
draw_scrollback_indicator if @scrollback_count > 0
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def flush
|
|
370
|
+
if @dirty
|
|
371
|
+
# FIXME: Keep track of dirty region
|
|
372
|
+
@dirty = false
|
|
373
|
+
copy_buffer
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def scroll_up(srcy, w, h, step)
|
|
378
|
+
@dpy.copy_area(@buf,@buf,gc_for_col(0xffffff,0), 0, srcy, 0, srcy-step, w, h)
|
|
379
|
+
@dirty = true
|
|
380
|
+
|
|
381
|
+
if @debug
|
|
382
|
+
$step||= 16
|
|
383
|
+
fillrect(0,srcy+h-step, w, step, $step)
|
|
384
|
+
$step += 16
|
|
385
|
+
else
|
|
386
|
+
# Use the full window width to ensure we clear the entire line width,
|
|
387
|
+
# not just the content width that was passed in
|
|
388
|
+
clear(0, srcy+h-step, @width, step+1)
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# Redraw the scrollback indicator if needed
|
|
392
|
+
draw_scrollback_indicator if @scrollback_count > 0
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def scroll_down(srcy, w, h, step)
|
|
396
|
+
# if srcy+h >
|
|
397
|
+
@dpy.copy_area(@buf,@buf,gc_for_col(0xffffff,0), 0, srcy, 0, srcy+step, w, h)
|
|
398
|
+
@dirty = true
|
|
399
|
+
if @debug
|
|
400
|
+
$step||= 16
|
|
401
|
+
fillrect(0,srcy, w, step, $step)
|
|
402
|
+
$step += 16
|
|
403
|
+
else
|
|
404
|
+
clear(0,srcy, w, step)
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# Redraw the scrollback indicator if needed
|
|
408
|
+
draw_scrollback_indicator if @scrollback_count > 0
|
|
409
|
+
end
|
|
410
|
+
end
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
# #Design change:
|
|
4
|
+
#
|
|
5
|
+
# Terminal writes to the buffer.
|
|
6
|
+
# Buffer batches up updates to an output adapter (the window, but could be a terminal...)
|
|
7
|
+
#
|
|
8
|
+
# But for now, the now stupidly misnamed TrackChanges class just bifurcates buffer changes
|
|
9
|
+
# and passes them to both the buffer *and* the screen, and "nothing" should talk to
|
|
10
|
+
# the window directly. Once all window updates are moved to TrackChanges/WindowAdapter
|
|
11
|
+
# We want to make it smarter.
|
|
12
|
+
#
|
|
13
|
+
class WindowAdapter
|
|
14
|
+
def initialize window, term
|
|
15
|
+
@window = window
|
|
16
|
+
@term = term
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def char_w = @window.char_w
|
|
20
|
+
def char_h = @window.char_h
|
|
21
|
+
def clear = @window.clear(0,0,@window.width,@window.height)
|
|
22
|
+
|
|
23
|
+
# True while the view is scrolled back into history. Live (pty-driven)
|
|
24
|
+
# screen writes are suppressed in this state so output doesn't paint over
|
|
25
|
+
# the scrolled-back display; the buffer is still updated underneath.
|
|
26
|
+
def scrollback_mode = @window.scrollback_mode
|
|
27
|
+
|
|
28
|
+
# A line has entered history while scrolled back: keep the same absolute
|
|
29
|
+
# lines in view (and the selection mapping consistent) by deepening the
|
|
30
|
+
# offset instead of letting the viewport drift.
|
|
31
|
+
def scrollback_anchor = @window.scrollback_anchor
|
|
32
|
+
|
|
33
|
+
# DECCOLM: the terminal asked to switch to +cols+ columns (80/132). The
|
|
34
|
+
# orchestrator (RubyTerm) owns the window pixels, font scale, config and
|
|
35
|
+
# the pty size report, so delegate to it. The headless harness "term"
|
|
36
|
+
# does not implement this (the virtual grid is fixed), so it no-ops.
|
|
37
|
+
def set_columns(cols)
|
|
38
|
+
@term.set_columns(cols) if @term.respond_to?(:set_columns)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def dim(col) #FIXME
|
|
42
|
+
[col].pack("l").each_byte.map{|b| b.ord*0.4 }.pack("C*").unpack("l")[0]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def brighten(col, bg)
|
|
46
|
+
# FIXME. Should bring it towards bg
|
|
47
|
+
[col].pack("l").each_byte.map{|b| (b.ord+128).clamp(0,255) }.pack("C*").unpack("l")[0]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def clear_area(x,y,w,h) = @window.clear(x*char_w,y*char_h,w,h)
|
|
51
|
+
def clear_cells(x,y,w,h) = clear_area(x,y, w * char_w, h * char_h)
|
|
52
|
+
|
|
53
|
+
def clear_line y, from_x, to_x = nil
|
|
54
|
+
if to_x
|
|
55
|
+
# to_x is an INCLUSIVE end column, matching TermBuffer#clear_line
|
|
56
|
+
# (EL mode 1 clears [0..cursor] inclusive). Clearing to_x-from_x
|
|
57
|
+
# cells left the cursor column itself stale on screen.
|
|
58
|
+
clear_cells(from_x, y, to_x - from_x + 1, 1)
|
|
59
|
+
else
|
|
60
|
+
clear_cells(from_x, y, @term.term_width - from_x, 1)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def insert_lines(y, num, maxy)
|
|
65
|
+
# Inserting more lines than fit between y and the region bottom just
|
|
66
|
+
# blanks the whole [y..maxy] span; clamp so the geometry below never
|
|
67
|
+
# goes negative (which would clear rows above the region - see DL).
|
|
68
|
+
num = [num, maxy - y + 1].min
|
|
69
|
+
return if num <= 0
|
|
70
|
+
# Move the rows from the insertion point down to the region bottom -
|
|
71
|
+
# [y .. maxy-num] - down by num rows, into [y+num .. maxy];
|
|
72
|
+
# Window#scroll_down clears the vacated rows at the top (the inserted
|
|
73
|
+
# blanks). The old code scrolled from screen row 0 and ignored y, so an
|
|
74
|
+
# IL anywhere below the top dragged every line above it (and outside the
|
|
75
|
+
# scroll region) down, blanking row 0.
|
|
76
|
+
@window.scroll_down(y * char_h, @term.term_width * char_w,
|
|
77
|
+
(maxy - y - num + 1) * char_h, num * char_h)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def delete_lines(y, num, maxy)
|
|
81
|
+
# Deleting more lines than the region holds clears [y..maxy] entirely.
|
|
82
|
+
# Without this clamp a large num gave Window#scroll_up a negative height
|
|
83
|
+
# and a negative clear origin that clamped to row 0, wiping lines above
|
|
84
|
+
# the scroll region.
|
|
85
|
+
num = [num, maxy - y + 1].min
|
|
86
|
+
return if num <= 0
|
|
87
|
+
# Move the rows below the deleted block - [y+num .. maxy] - up by num
|
|
88
|
+
# rows, into [y .. maxy-num]; Window#scroll_up clears the vacated rows
|
|
89
|
+
# at the bottom of the region.
|
|
90
|
+
@window.scroll_up((y + num) * char_h, @term.term_width * char_w,
|
|
91
|
+
(maxy - (y + num) + 1) * char_h, num * char_h)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# # Migrating draw_flush
|
|
96
|
+
|
|
97
|
+
def draw_flag_lines(flags, x,y, len, fg)
|
|
98
|
+
x *= char_w
|
|
99
|
+
y *= char_h
|
|
100
|
+
w = len * char_w
|
|
101
|
+
if flags.allbits?(OVERLINE)
|
|
102
|
+
@window.draw_line(x,y,w,fg)
|
|
103
|
+
end
|
|
104
|
+
if flags.allbits?(CROSSED_OUT)
|
|
105
|
+
@window.draw_line(x,y+char_h/2+2, w, fg)
|
|
106
|
+
end
|
|
107
|
+
if flags.anybits?(UNDERLINE | DBL_UNDERLINE)
|
|
108
|
+
@window.draw_line(x,y+char_h-3, w, fg)
|
|
109
|
+
if flags.allbits?(DBL_UNDERLINE)
|
|
110
|
+
@window.draw_line(x,y+char_h-1, w, fg)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def draw(x,y,c,fg,bg,flags,lineattrs)
|
|
116
|
+
inverse = flags.allbits?(INVERSE)
|
|
117
|
+
if inverse
|
|
118
|
+
fg,bg=bg,fg
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
if flags.allbits?(FAINT)
|
|
122
|
+
fg = dim(fg)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
if flags.anybits?(BLINK) && @term.blink_state
|
|
126
|
+
fg = inverse ? brighten(fg,bg) : dim(fg)
|
|
127
|
+
elsif flags.anybits?(RAPID_BLINK) && @term.rblink_state
|
|
128
|
+
fg = inverse ? brighten(fg,bg) : dim(fg)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
if x.nil?
|
|
132
|
+
# @BUG
|
|
133
|
+
STDERR.puts "\e[35m@BUG\[0m: x.nil? @windowadapter#draw"
|
|
134
|
+
return
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
@window.draw(x*char_w, y*char_h, c, fg, bg, lineattrs)
|
|
138
|
+
# FIXME: Take into account lineattrs
|
|
139
|
+
draw_flag_lines(flags, x, y, c.length, fg)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Force a complete redraw of the window contents
|
|
143
|
+
def redraw_all
|
|
144
|
+
# First clear the entire window
|
|
145
|
+
@window.clear(0, 0, @window.width, @window.height)
|
|
146
|
+
|
|
147
|
+
# Force the window to flush and update its display
|
|
148
|
+
@window.dirty!
|
|
149
|
+
@window.flush
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def scroll_up(scroll_start, scroll_end)
|
|
154
|
+
@window.scroll_up(
|
|
155
|
+
char_h*((scroll_start||0)+1),
|
|
156
|
+
@window.width,
|
|
157
|
+
(scroll_end-scroll_start)*char_h,
|
|
158
|
+
char_h
|
|
159
|
+
)
|
|
160
|
+
end
|
|
161
|
+
end
|