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.



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
87
# File 'lib/term.rb', line 27

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.



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

def cursor
  @cursor
end

#escObject

Returns the value of attribute esc.



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

def esc
  @esc
end

#modeObject

Returns the value of attribute mode.



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

def mode
  @mode
end

#mouse_buttonsObject

Returns the value of attribute mouse_buttons.



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

def mouse_buttons
  @mouse_buttons
end

#mouse_modeObject

Returns the value of attribute mouse_mode.



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

def mouse_mode
  @mouse_mode
end

#mouse_reportingObject

Returns the value of attribute mouse_reporting.



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

def mouse_reporting
  @mouse_reporting
end

#origin_modeObject

Returns the value of attribute origin_mode.



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

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.



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

def responder
  @responder
end

#tabsObject

Returns the value of attribute tabs.



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

def tabs
  @tabs
end

#wraparoundObject

Returns the value of attribute wraparound.



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

def wraparound
  @wraparound
end

#xObject

Returns the value of attribute x.



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

def x
  @x
end

#yObject

Returns the value of attribute y.



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

def y
  @y
end

Instance Method Details

#bgObject



243
244
245
# File 'lib/term.rb', line 243

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.



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

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



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

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

#clamph(i) ⇒ Object



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

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

#clampw(i) ⇒ Object



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

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

#clear_aboveObject



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

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

#clear_belowObject



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

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



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

def clear_cursor = @buffer.clear_cursor

#clear_line(y = nil) ⇒ Object



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

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.



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

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

#clear_to_endObject



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

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

#clear_to_startObject



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

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

#cursor_down(lines) ⇒ Object



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

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?



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

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

#decalnObject



190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/term.rb', line 190

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



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

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.



180
181
182
183
# File 'lib/term.rb', line 180

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



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

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

#draw_cursorObject



308
309
310
311
312
# File 'lib/term.rb', line 308

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



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

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



159
160
161
162
163
164
165
166
167
# File 'lib/term.rb', line 159

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



541
542
543
544
545
546
547
548
549
550
# File 'lib/term.rb', line 541

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.



239
240
241
242
# File 'lib/term.rb', line 239

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

#handle_control(ch) ⇒ Object



632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
# File 'lib/term.rb', line 632

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



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
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
# File 'lib/term.rb', line 450

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



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
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
# File 'lib/term.rb', line 399

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
      when 1048
        # Save (h) / restore (l) cursor, as DECSC / DECRC.
        set ? save_cursor : restore_cursor
      when 1049
        # Alternate screen + cursor save/restore. Like mode 47 this
        # terminal has no separate alt buffer yet, so it just clears; the
        # cursor save (on enter) / restore (on leave) is what apps rely on
        # to land the cursor back where it was before the alt screen.
        set ? save_cursor : restore_cursor
        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



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
630
# File 'lib/term.rb', line 599

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";  save_cursor    # DECSC
  when "8";  restore_cursor # DECRC
  else
    unhandled(:escape, s)
  end

  @esc = nil
end

#heightObject



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

def height = @term_height

#indexObject

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



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

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

#insert_lines(num) ⇒ Object



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

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

#invalidate_coloursObject



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

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.



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

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

#linefeedObject



392
393
394
395
396
397
# File 'lib/term.rb', line 392

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

#originObject



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

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



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

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



553
554
555
556
557
558
559
560
561
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 553

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

    cw = CharWidth.width(ch)
    # Zero-width codepoints (variation selectors like U+FE0F, combining marks,
    # zero-width joiners) modify the preceding glyph. This buffer stores one
    # codepoint per cell and can't compose, so drop them rather than letting
    # them claim a cell of their own — which would paint over the next column.
    return if cw == 0
    # A double-width glyph can't straddle the right margin: if only one
    # column is left, blank it and wrap so the glyph starts the next line.
    if cw == 2 && @wraparound && @x == line_width - 1
      @buffer.set(@x, @y, ' ', fg, bg, @mode)
      @x = line_width
      wrap_if_needed
      scroll_if_needed
    end

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

#redraw_line_from_cursorObject

FIXME: Redrawing full spans would be better.



315
316
317
318
319
# File 'lib/term.rb', line 315

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

#region_bottomObject



358
359
360
361
# File 'lib/term.rb', line 358

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



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

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.



130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/term.rb', line 130

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



203
204
205
206
# File 'lib/term.rb', line 203

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

#restore_cursorObject



123
124
125
# File 'lib/term.rb', line 123

def restore_cursor
  @x, @y, @gl, @gr, @g = @saved || [0, 0, 0, nil, [DefaultCharset, nil, nil, nil]]
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).



383
384
385
386
387
388
389
390
# File 'lib/term.rb', line 383

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

#save_cursorObject

Cursor + charset save/restore, shared by DECSC/DECRC (ESC 7 / ESC 8) and DEC private modes 1048/1049. DECRC with no prior save defaults to the home position and default charsets rather than leaving @x/@y nil (which would crash draw_cursor).



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

def save_cursor    = @saved = [@x, @y, @gl, @gr, @g.dup]

#scroll_if_neededObject



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

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



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

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



325
326
327
328
329
330
331
332
333
334
335
336
337
338
# File 'lib/term.rb', line 325

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



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
273
274
275
276
277
278
279
280
281
282
# File 'lib/term.rb', line 248

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



340
341
342
343
344
345
346
347
# File 'lib/term.rb', line 340

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



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

def width  = @term_width

#wrap_if_neededObject



285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
# File 'lib/term.rb', line 285

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