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



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

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.



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

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



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

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

#clampw(i) ⇒ Object



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

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



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

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



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

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?



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

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

#decalnObject



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

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



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

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.



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

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



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

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

#draw_cursorObject



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

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



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

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



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

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



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

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.



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

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

#handle_control(ch) ⇒ Object



612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
# File 'lib/term.rb', line 612

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



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

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



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

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



575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
# File 'lib/term.rb', line 575

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



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.



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

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

#insert_lines(num) ⇒ Object



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

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

#invalidate_coloursObject



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

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.



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

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

#linefeedObject



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

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

#originObject



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

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



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

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



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
561
562
563
564
565
566
567
568
569
570
571
572
573
# File 'lib/term.rb', line 534

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



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

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

#region_bottomObject



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

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



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

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.



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

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



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

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



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

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

#scroll_if_neededObject



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

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



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

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



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

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



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

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



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

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



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

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