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.
@@ -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