Class: Term

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

Overview

The escape/control interpreter: it turns a byte stream into operations on a buffer, and knows nothing about X11, windows, or how its buffer is rendered. It talks only to its buffer (a TrackChanges, which owns the render backend). The same interpreter therefore drives a terminal that renders to an X11 window or to a terminal (AnsiBackend), or for a multiplexer / TUI library, depending only on the backend behind the buffer. It carries no pixel/colour constants and no rendering: even the cursor is just a position it reports (#draw_cursor) for the buffer to render as an overlay.

Constant Summary collapse

CSI_MAP =
{
  "J" => :erase_in_display,
  "K" => :erase_in_line
}

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(buffer) ⇒ Term

Returns a new instance of Term.



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/term.rb', line 26

def initialize(buffer)
  @buffer = buffer

  # FIXME: I should consider whether to change origin to match the terminal handling
  # as it might be easier.
  @x = 0; @y = 0

  # Initial size only; the host resizes to the real geometry before use.
  @term_width  = 80
  @term_height = 24

  @tabs = 40.times.map {|i| i * 8}

  # EscapeParser instance when an escape code is being parsed.
  # FIXME: Clearing this each time is probably slow.
  @esc = nil 


  @bg = BG
  @fg = FG
  @mode = 0

  @wraparound = true

  # Show cursor?
  @cursor = true

  # DECOM - Origin Mode - https://vt100.net/docs/vt510-rm/DECOM.html
  # FIXME: Origin mode is currently only partially respected.
  @origin_mode = false

  # LNM - Line Feed / New Line Mode - See https://vt100.net/docs/vt100-ug/chapter3.html
  # LNM (line feed/new line mode). When reset (the VT100 default), LF/VT/FF
  # only move down; when set, they also return to column 0. The pty's
  # ONLCR already turns a program's "\n" into "\r\n", so off is both
  # correct and what tmux/xterm do.
  @lnm = false
  # IRM (insert/replace mode). When set, printed characters are inserted
  # at the cursor, shifting the rest of the line right, rather than
  # overwriting. Default replace (off).
  @irm = false

  # :x10, :v200, :v200_highlight, :btn_event, :any_event
  # FIXME: Only :btn_event_mouse supported so far
  # See: https://www.xfree86.org/current/ctlseqs.html ("Mouse tracking")
  @mouse_mode = nil
  # Mouse reporting format. nil == x10, :multibyte (NOT SUPPORTED), :digits,
  # :urxvt (NOT SUPPORTED, probably never)
  @mouse_reporting = nil
  # Currently pressed mouse buttons.
  @mouse_buttons = nil

  # Character set state (GL/GR designation per vt100/vt220).
  @gl = 0
  @gr = nil # FIXME: GR shifting not implemented
  @g = [DefaultCharset, nil, nil, nil]
  @saved = nil # Saved cursor state (ESC 7 / ESC 8)

  @decoder = UTF8Decoder.new
  @responder = nil
end

Instance Attribute Details

#cursorObject

Returns the value of attribute cursor.



16
17
18
# File 'lib/term.rb', line 16

def cursor
  @cursor
end

#escObject

Returns the value of attribute esc.



16
17
18
# File 'lib/term.rb', line 16

def esc
  @esc
end

#modeObject

Returns the value of attribute mode.



16
17
18
# File 'lib/term.rb', line 16

def mode
  @mode
end

#mouse_buttonsObject

Returns the value of attribute mouse_buttons.



16
17
18
# File 'lib/term.rb', line 16

def mouse_buttons
  @mouse_buttons
end

#mouse_modeObject

Returns the value of attribute mouse_mode.



16
17
18
# File 'lib/term.rb', line 16

def mouse_mode
  @mouse_mode
end

#mouse_reportingObject

Returns the value of attribute mouse_reporting.



16
17
18
# File 'lib/term.rb', line 16

def mouse_reporting
  @mouse_reporting
end

#origin_modeObject

Returns the value of attribute origin_mode.



16
17
18
# File 'lib/term.rb', line 16

def origin_mode
  @origin_mode
end

#responderObject

Object receiving terminal query replies (DSR/DA). Must respond to #report_position(x,y) and the three Device Attributes replies #device_attr_primary / #device_attr_secondary / #device_attr_tertiary. In the live terminal this is the Controller (which writes to the pty); in the test harness it is a Session capturing responses.



24
25
26
# File 'lib/term.rb', line 24

def responder
  @responder
end

#tabsObject

Returns the value of attribute tabs.



16
17
18
# File 'lib/term.rb', line 16

def tabs
  @tabs
end

#wraparoundObject

Returns the value of attribute wraparound.



16
17
18
# File 'lib/term.rb', line 16

def wraparound
  @wraparound
end

#xObject

Returns the value of attribute x.



16
17
18
# File 'lib/term.rb', line 16

def x
  @x
end

#yObject

Returns the value of attribute y.



16
17
18
# File 'lib/term.rb', line 16

def y
  @y
end

Instance Method Details

#bgObject



233
234
235
# File 'lib/term.rb', line 233

def bg
  @bg_resolved ||= @bg.is_a?(String) ? PALETTE_BASIC[@bg.to_i] : @bg
end

#bottomObject

Inclusive bottom row of the active region. CSI scroll_end is already stored as an inclusive index; when unset the active region is the whole screen, so the last valid row is height - 1.



343
344
345
346
# File 'lib/term.rb', line 343

def bottom =  @origin_mode ? (@buffer.scroll_end || height - 1) : height - 1
# The scroll region margins. Unlike #origin/#bottom these are NOT gated
# on origin mode: DECSTBM governs IND/RI/LF scrolling whether or not
# DECOM is set (origin mode only affects cursor addressing).

#charsetObject



88
# File 'lib/term.rb', line 88

def charset = @g[@gl] || DefaultCharset

#clamph(i) ⇒ Object



359
# File 'lib/term.rb', line 359

def clamph(i) = i.clamp(origin,bottom)

#clampw(i) ⇒ Object



358
# File 'lib/term.rb', line 358

def clampw(i) = i.clamp(0,line_width-1)

#clear_aboveObject



96
97
98
99
# File 'lib/term.rb', line 96

def clear_above
  (0...y).each {|y| clear_line(y) }
  clear_to_start
end

#clear_belowObject



101
102
103
104
# File 'lib/term.rb', line 101

def clear_below
  clear_to_end
  (y+1..height).each {|y| clear_line(y)}
end

#clear_cursorObject

The cursor is a render-time overlay, not interpreter state: the interpreter just reports where the cursor is (and whether it’s shown); the buffer/backend renders it. (See docs/architecture-review.md Phase 4.)



296
# File 'lib/term.rb', line 296

def clear_cursor = @buffer.clear_cursor

#clear_line(y = nil) ⇒ Object



95
# File 'lib/term.rb', line 95

def clear_line(y=nil) = @buffer.clear_line(y||@y, 0)

#clear_screenObject

Per vt100 user guide: Erase all of the display –all lines are erased, changed to single-width, and the cursor does not move.



111
112
113
114
115
# File 'lib/term.rb', line 111

def clear_screen
  @buffer.scroll_start = nil
  @buffer.scroll_end   = nil
  @buffer.clear   # the buffer (TrackChanges) also clears the backend
end

#clear_to_endObject



93
# File 'lib/term.rb', line 93

def clear_to_end      = @buffer.clear_line(@y, @x)

#clear_to_startObject



94
# File 'lib/term.rb', line 94

def clear_to_start    = @buffer.clear_line(@y, 0, @x)

#cursor_down(lines) ⇒ Object



178
# File 'lib/term.rb', line 178

def cursor_down(lines) = (@y = clamph(@y + lines.to_i.clamp(1,height)))

#cursor_up(lines) ⇒ Object

ESC [ Pn A / B FIXME: Should these not use clamph?



177
# File 'lib/term.rb', line 177

def cursor_up(lines)   = (@y = clamph(@y - lines.to_i.clamp(1,height)))

#decalnObject



180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/term.rb', line 180

def decaln
  # DEC alignment; only purpose served here is for vttest
  # so doesn't need to be efficient
  @buffer.scroll_start = nil
  @buffer.scroll_end   = nil
  width.times.each do |x|
    height.times.each do |y|
      @buffer.set(x,y,'E',fg,bg,0)
    end
  end
  @buffer.draw_flush
end

#deleteObject



161
162
163
164
# File 'lib/term.rb', line 161

def delete
  @x = clampw(@x - 1)
  @buffer.set(@x,@y, 66, fg, bg, @mode)
end

#delete_chars(num) ⇒ Object

DCH - delete characters at the cursor, shifting the line left.



170
171
172
173
# File 'lib/term.rb', line 170

def delete_chars(num)
  @buffer.delete_chars(@x, @y, num || 1)
  redraw_line_from_cursor
end

#delete_lines(num) ⇒ Object

FIXME (still broken) Emacs uses this to scroll up



167
# File 'lib/term.rb', line 167

def delete_lines(num) = @buffer.delete_lines(@y, num, height)

#draw_cursorObject



298
299
300
301
302
# File 'lib/term.rb', line 298

def draw_cursor
  x, y = @x, @y
  y += 1 if x >= width   # pending-wrap: the cursor shows on the next row
  @buffer.draw_cursor(x, y, @cursor)
end

#erase_in_display(ps = 0) ⇒ Object

ED - Erase In Display - ESC [ Ps J



139
140
141
142
143
144
145
146
# File 'lib/term.rb', line 139

def erase_in_display(ps = 0)
  case ps.to_i
  when 0 then clear_below
  when 1 then clear_above
  when 2 then clear_screen
  when 3 then @buffer.clear  # FIXME: Not in VT100. Where is this from?
  end
end

#erase_in_line(ps = 0) ⇒ Object

EL - Erase In Line - ESC [ Ps K



149
150
151
152
153
154
155
156
157
# File 'lib/term.rb', line 149

def erase_in_line(ps = 0)
  case ps.to_i
  when 0 then clear_to_end
  when 1 then clear_to_start
  when 2 then clear_line
  else
    unhandled(:erase_in_line, ps)
  end
end

#feed(str) ⇒ Object Also known as: write

This is the preferred public interface:

Feed raw bytes (as read from the pty) into the terminal. Handles UTF-8 decoding, control characters and escape sequences. This is synchronous: when it returns, all bytes have been interpreted and the buffer updated (rendering may still be batched in TrackChanges until #draw_flush is called on the buffer).



521
522
523
524
525
526
527
528
529
530
# File 'lib/term.rb', line 521

def feed(str)
  @decoder << str
  # each_codepoint yields Integer codepoints directly (the decoder already
  # maps bytes it can't decode to U+FFFD), so the hot path allocates no
  # per-character String and does no per-character valid_encoding?/ord.
  @decoder.each_codepoint { |cp| putchar(cp) }
rescue StandardError
  # Last-resort guard so a malformed sequence can't take down the input
  # thread; stays silent (no debug output to the pane/stderr).
end

#fgObject

Resolved fg/bg are recomputed only when an SGR (or reset) changes @fg/@bg/@mode - not per character. putchar resolves the colour for every printable glyph, so doing the String/palette dance each time showed up in the profile; memoise and invalidate via #invalidate_colours.



229
230
231
232
# File 'lib/term.rb', line 229

def fg
  @fg_resolved ||=
    @fg.is_a?(String) ? PALETTE_BASIC[@fg.to_i + (@mode.allbits?(BOLD) ? 8:0)] : @fg
end

#handle_control(ch) ⇒ Object



599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
# File 'lib/term.rb', line 599

def handle_control(ch)
  case ch
  when 1,2;
  when 7; unhandled(:bell)
  when 8;
    # Backspace. From the pending-wrap state (cursor parked past the last
    # column after printing there) BS clears the pending wrap and stays on
    # the last column, rather than stepping back two.
    if    @x >= line_width then @x = line_width - 1
    elsif @x > 0           then @x -= 1
    end
  when 9
    if i = @tabs.index {|t| t > @x}
      # FIXME: This is only right behaviour if wrap is off, is it not?
      @x = clampw(@tabs[i])
    else
      # No tab stop to the right (e.g. all stops cleared via TBC): HT
      # advances to the last column, per VT100.
      @x = line_width - 1
    end
  when 10, 11
    linefeed
  when 12; @x = 0; @y = 0
  when 13; @x = 0
  when 14; @gl = 1
  when 15; @gl = 0
  when 16..26;
  when 27; @esc = EscapeParser.new # FIXME: Is this right if !@esc.nil? ?
  when 28..31;
  end
end

#handle_csi(s) ⇒ Object



430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
# File 'lib/term.rb', line 430

def handle_csi(s)
  return handle_dec(s) if s[1] == "?"
  args = s[1..-2].split(/[:;]/).map{|i| i.empty? ? nil : i.to_i }


  cmd = s[-1]
  if CSI_MAP[cmd]
    return send(CSI_MAP[cmd], *args)
  end

  case s[-1]
  when "@";
    @buffer.insert(@x,@y,args[0] || 1,[32,0,0,0])
    redraw_line_from_cursor
  when "A" then cursor_up(args[0])
  when "B" then cursor_down(args[0])
  when "C"
    @x = clampw(@x + args[0].to_i.clamp(1,width))
  when "D"
    @x = clampw(@x - args[0].to_i.clamp(1,width))
  when "G"
    @x = clampw((args[0]||1)-1)
  when "H", "f"
    @y = (origin + args[0].to_i.clamp(1,99999))-1
    @x = (args[1]||1)-1
  #when "J" then erase_in_display(args[0])
  #when "K" then erase_in_line(args[0])
  when "L" then insert_lines(args[0])
  when "M" then delete_lines(args[0]||1)
  when "P" then delete_chars(args[0]||1)
  when "S" then scroll_up(args[0]||1)
  when "T"; # Scroll down
  when "c"
    # Device Attributes. The reply type depends on the private prefix;
    # answering DA1/DA2 with the wrong kind (e.g. a DA3 DCS) means a
    # host like tmux fails to recognise it and leaks it to the pane.
    #   CSI c   / CSI 0 c  -> primary   (DA1), reply CSI ? ... c
    #   CSI > c / CSI > 0 c-> secondary (DA2), reply CSI > ... c
    #   CSI = c            -> tertiary  (DA3), reply DCS ! | ... ST
    if block_given?
      case s[1]
      when ">" then yield(:device_attr_secondary)
      when "=" then yield(:device_attr_tertiary)
      else          yield(:device_attr_primary)
      end
    end
  when "d"
    @y = clamph(origin+(args[0]||1) - 1) # FIXME: Should these be clamped
  when "g"
    case args[0].to_i
    when 0 then @tabs.delete(@x)
    when 3 then @tabs = []
    end
  when "h", "l"
    # Standard (non-DEC-private) modes - SM/RM. 4 = IRM, 20 = LNM.
    set = s[-1] == "h"
    args.each do |code|
      case code
      when 4  then @irm = set
      when 20 then @lnm = set
      end
    end
  when "m"; set_modes(args.empty? ? [0] : args)
  when "n"
    case args[0]
    when 6
      yield(:report_position) if block_given?
    else
      unhandled(:dsr, args)
    end
  when "r"
    @buffer.scroll_start = (args[0] || 1)-1
    @buffer.scroll_end   = (args[1] || height)-1
    # DECSTBM homes the cursor: to the origin (region top) in origin
    # mode, otherwise to screen home.
    @x = 0
    @y = origin
  else
    unhandled(:csi, s)
  end
  nil
end

#handle_dec(s) ⇒ Object

CSI ‘?’ -> DEC private modes



389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
# File 'lib/term.rb', line 389

def handle_dec(s) # CSI '?' -> DEC private modes
  args = s[2..-2].split(/[:;]/).map{|i| i.empty? ? nil : i.to_i }
  case s[-1]
  when "h","l"
    set = s[-1] == "h"
    args.each do |code|
      case code
      when 3  then set_width_and_clear(set ? 132 : 80)
      when 6  then @origin_mode = set
      when 7  then @wraparound  = set
      when 9
        # FIXME: Unsupported X10 mouse reporting mode
      when 20 then @lnm = set
      when 25
        @cursor = set
        clear_cursor if !set
      when 47;
        # Start/end alternate screen mode
        # FIXME: Save/restore
        # FIXME: Scrollback should be disabled/enabled.
        clear_screen

      # Extended mouse modes
      # See https://terminalguide.namepad.de/mouse/
      when 1000 then @mouse_mode = set ? :vt200 : nil
      when 1001 then @mouse_mode = set ? :vt200_highlight : nil
      when 1002 then @mouse_mode = set ? :btn_event : nil
      when 1003 then @mouse_mode = set ? :any_event : nil
      when 1006 then @mouse_reporting = set ? :digits : nil
      when 2004
        # FIXME: Bracketed paste.
      end
    end
  end
end

#handle_escape(ch) ⇒ Object



562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
# File 'lib/term.rb', line 562

def handle_escape(ch)
  return false if !@esc.complete?
  s = @esc.str
  if s[0] == ?[
    handle_csi(s) {|op| respond(op) }
    @esc = nil
    return
  end

  case s
  when "D"; index            # IND
  when "E"; index; @x = 0     # NEL
  when "H"; @tabs = (@tabs << @x).sort.uniq
  when "M"; reverse_index     # RI
  when "#3"; set_line_attrs(:dbl_upper)   # DECDHL top half
  when "#4"; set_line_attrs(:dbl_lower)   # DECDHL bottom half
  when "#5"; set_line_attrs(0)            # DECSWL single width
  when "#6"; set_line_attrs(:dbl_single)  # DECDWL double width
  when "c"; reset            # RIS
  when "#8"; decaln
  when "(B"; @g[0] = DefaultCharset
  when ")B"; @g[1] = DefaultCharset
  when "(0"; @g[0] = GraphicsCharset
  when ")0"; @g[1] = GraphicsCharset
  when "7";  @saved = [@x,@y,@gl,@gr,@g.dup]
  when "8"
    # DECRC with no prior DECSC: default to home position and default
    # charsets rather than leaving @x/@y nil (which crashes draw_cursor).
    sx, sy, sgl, sgr, sg = @saved || [0, 0, 0, nil, [DefaultCharset, nil, nil, nil]]
    @x, @y, @gl, @gr, @g = sx, sy, sgl, sgr, sg
  else
    unhandled(:escape, s)
  end

  @esc = nil
end

#heightObject



91
# File 'lib/term.rb', line 91

def height = @term_height

#indexObject

IND - index: down one line; at the bottom margin scroll the region up.



362
363
364
365
366
367
368
369
# File 'lib/term.rb', line 362

def index
  if @y >= region_bottom
    @y = region_bottom
    scroll_up(1)
  else
    @y += 1
  end
end

#insert_lines(num) ⇒ Object



159
# File 'lib/term.rb', line 159

def insert_lines(num) = @buffer.insert_lines(@y, num||1, height)

#invalidate_coloursObject



236
# File 'lib/term.rb', line 236

def invalidate_colours; @fg_resolved = @bg_resolved = nil; end

#line_widthObject

Usable column count of the current line. Double-width/height lines show each cell twice as wide, so only width/2 columns fit; the cursor’s last valid column is therefore line_width-1.



352
353
354
355
356
357
# File 'lib/term.rb', line 352

def line_width
  case @buffer.lineattrs(@y)
  when :dbl_upper, :dbl_lower, :dbl_single then width / 2
  else width
  end
end

#linefeedObject



382
383
384
385
386
387
# File 'lib/term.rb', line 382

def linefeed
  @x = 0 if @lnm
  @buffer.draw_flush
  @y = clamph(@y) + 1
  scroll_if_needed
end

#originObject



339
340
341
342
# File 'lib/term.rb', line 339

def origin =  @origin_mode ? (@buffer.scroll_start || 0) : 0
# Inclusive bottom row of the active region.  CSI scroll_end is already
# stored as an inclusive index; when unset the active region is the
# whole screen, so the last valid row is height - 1.

#parse_color(codes) ⇒ Object



216
217
218
219
220
221
222
# File 'lib/term.rb', line 216

def parse_color(codes)
  case c = codes.shift
  when 5; PALETTE256[codes.shift]
  when 2; codes.shift << 16 | codes.shift << 8 | codes.shift
  else;   BG
  end
end

#putchar(ch) ⇒ Object



533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
# File 'lib/term.rb', line 533

def putchar(ch)
  if ch.is_a?(String)
    STDERR.puts "WARNING: Should be int"
    ch = ch.ord
  end
  if @esc&.put(ch)
    handle_escape(ch)
  elsif ch < 32
    handle_control(ch)
  else
    wrap_if_needed
    scroll_if_needed
    return if ch == 127   # DEL is ignored in the data stream

    # IRM (insert mode): shift the rest of the line right and repaint it,
    # then drop the new glyph into the gap.
    if @irm
      @buffer.insert(@x, @y, 1, [32,0,0,0])
      @buffer.set(@x, @y, charset[ch], fg, bg, @mode)
      redraw_line_from_cursor
    else
      @buffer.set(@x, @y, charset[ch], fg, bg, @mode)
    end
    @y = clamph(@y)
    @x += 1
    scroll_if_needed
  end
end

#redraw_line_from_cursorObject

FIXME: Redrawing full spans would be better.



305
306
307
308
309
# File 'lib/term.rb', line 305

def redraw_line_from_cursor
  clear_cursor
  (@x..width).each {|x| @buffer.redraw(x,@y) }
  draw_cursor
end

#region_bottomObject



348
349
350
351
# File 'lib/term.rb', line 348

def region_bottom = @buffer.scroll_end   || height - 1
# Usable column count of the current line. Double-width/height lines show
# each cell twice as wide, so only width/2 columns fit; the cursor's last
# valid column is therefore line_width-1.

#region_topObject

The scroll region margins. Unlike #origin/#bottom these are NOT gated on origin mode: DECSTBM governs IND/RI/LF scrolling whether or not DECOM is set (origin mode only affects cursor addressing).



347
# File 'lib/term.rb', line 347

def region_top    = @buffer.scroll_start || 0

#resetObject

RIS - Reset to Initial State (ESC c). Full reset: restore margins, modes, charsets, tab stops and attributes to their defaults, home the cursor and clear the screen.



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/term.rb', line 120

def reset
  @x = @y = 0
  @tabs = 40.times.map {|i| i * 8}
  @fg = FG; @bg = BG; @mode = 0
  invalidate_colours
  @wraparound  = true
  @cursor      = true
  @origin_mode = false
  @lnm = false
  @irm = false
  @mouse_mode = nil
  @mouse_reporting = nil
  @gl = 0; @gr = nil
  @g  = [DefaultCharset, nil, nil, nil]
  @saved = nil
  clear_screen   # also resets the scroll region and clears
end

#resize(w, h) ⇒ Object



193
194
195
196
# File 'lib/term.rb', line 193

def resize(w,h)
  @term_width = w
  @term_height = h
end

#reverse_indexObject

RI - reverse index: up one line; at the top margin scroll the region down (insert a blank line at the top, discarding the region’s last).



373
374
375
376
377
378
379
380
# File 'lib/term.rb', line 373

def reverse_index
  if @y <= region_top
    @y = region_top
    insert_lines(1)
  else
    @y -= 1
  end
end

#scroll_if_neededObject



205
206
207
208
209
210
211
212
213
214
# File 'lib/term.rb', line 205

def scroll_if_needed
  # Scroll at the scroll-region bottom margin, which (like IND/RI)
  # applies regardless of origin mode - LF and wrap both scroll the
  # region, not the whole screen, when a region is set.
  dy = @y - region_bottom
  if dy > 0
    scroll_up(dy)
    @y -= dy
  end
end

#scroll_up(num = 1) ⇒ Object



198
199
200
201
202
203
# File 'lib/term.rb', line 198

def scroll_up(num=1)
  # The buffer (TrackChanges) drives the backend scroll - the blit and the
  # scrolled-back-view handling are rendering concerns, not interpreter
  # ones.
  num.times { @buffer.scroll_up }
end

#set_line_attrs(attr) ⇒ Object

Set a line’s width/height attribute (DECDHL/DECDWL/DECSWL) and re-render the whole line: a single/double switch changes the size of glyphs already on the line, so the cells written before the attribute must be repainted (vttest sets the attribute after writing the text).



315
316
317
318
319
320
321
322
323
324
325
326
327
328
# File 'lib/term.rb', line 315

def set_line_attrs(attr)
  @buffer.set_lineattrs(@y, attr)
  clear_cursor
  # Repaint this line and its neighbours from their own attributes. A
  # (previous) double-height attribute on this line draws into the row
  # above/below; switching to a shorter attribute must clear those
  # spilled halves, which only repainting the neighbour rows does.
  [@y - 1, @y, @y + 1].each do |row|
    next if row < 0 || row >= height
    (0...width).each {|x| @buffer.redraw(x, row) }
  end
  @buffer.draw_flush
  draw_cursor
end

#set_modes(codes) ⇒ Object



238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# File 'lib/term.rb', line 238

def set_modes(codes)
  while c = codes.shift
    case c
    when 0;       @mode = 0; @fg = FG; @bg = BG
    when 1;       @mode |= BOLD
    when 2;       @mode |= FAINT
    when 3;       @mode |= ITALICS # FIXME
    when 4;       @mode |= UNDERLINE
    when 5;       @mode |= BLINK
    when 6;       @mode |= RAPID_BLINK
    when 7;       @mode |= INVERSE
    when 8;       @mode |= INVISIBLE
    when 9;       @mode |= CROSSED_OUT
    when 21;      @mode |= DBL_UNDERLINE
    when 22;      @mode &= ~BOLD & ~FAINT
    when 23;      @mode &= ~ITALICS
    when 24;      @mode &= ~UNDERLINE & ~DBL_UNDERLINE
    when 25;      @mode &= ~BLINK & ~RAPID_BLINK
    when 27;      @mode &= ~INVERSE
    when 28;      @mode &= ~INVISIBLE
    when 29;      @mode &= ~CROSSED_OUT
    when 30..37;  @fg = (c-30).to_s # FIXME: Hack
    when 38;      @fg = parse_color(codes)
    when 39;      @fg = FG
    when 40..47;  @bg = (c-40).to_s # FIXME: Hack
    when 48;      @bg = parse_color(codes)
    when 49;      @bg = BG
    when 53;      @mode |= OVERLINE
    when 55;      @mode &= ~OVERLINE
    else return unhandled(:sgr, c)
    end
  end
ensure
  invalidate_colours
end

#set_width_and_clear(w) ⇒ Object



330
331
332
333
334
335
336
337
# File 'lib/term.rb', line 330

def set_width_and_clear(w)
  # DECCOLM. Set the logical width, then let the display layer actually
  # realise the column change - by rescaling the font or resizing the
  # window (RubyTerm#set_columns); a no-op in the headless harness.
  resize(w, height)
  @buffer.set_columns(w)
  clear_screen
end

#widthObject



90
# File 'lib/term.rb', line 90

def width  = @term_width

#wrap_if_neededObject



275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
# File 'lib/term.rb', line 275

def wrap_if_needed
  if @x >= line_width
    if @wraparound
      @x = 0
      @y += 1
    else
      @x = line_width-1
    end
  elsif @x < 0
    if @wraparound
      @y -= 1
      @x = line_width-1
    else
      @x = 0
    end
  end
end