Class: Term
- Inherits:
-
Object
- Object
- Term
- 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
-
#cursor ⇒ Object
Returns the value of attribute cursor.
-
#esc ⇒ Object
Returns the value of attribute esc.
-
#mode ⇒ Object
Returns the value of attribute mode.
-
#mouse_buttons ⇒ Object
Returns the value of attribute mouse_buttons.
-
#mouse_mode ⇒ Object
Returns the value of attribute mouse_mode.
-
#mouse_reporting ⇒ Object
Returns the value of attribute mouse_reporting.
-
#origin_mode ⇒ Object
Returns the value of attribute origin_mode.
-
#responder ⇒ Object
Object receiving terminal query replies (DSR/DA).
-
#tabs ⇒ Object
Returns the value of attribute tabs.
-
#wraparound ⇒ Object
Returns the value of attribute wraparound.
-
#x ⇒ Object
Returns the value of attribute x.
-
#y ⇒ Object
Returns the value of attribute y.
Instance Method Summary collapse
- #bg ⇒ Object
-
#bottom ⇒ Object
Inclusive bottom row of the active region.
- #charset ⇒ Object
- #clamph(i) ⇒ Object
- #clampw(i) ⇒ Object
- #clear_above ⇒ Object
- #clear_below ⇒ Object
-
#clear_cursor ⇒ Object
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.
- #clear_line(y = nil) ⇒ Object
-
#clear_screen ⇒ Object
Per vt100 user guide: Erase all of the display – all lines are erased, changed to single-width, and the cursor does not move.
- #clear_to_end ⇒ Object
- #clear_to_start ⇒ Object
- #cursor_down(lines) ⇒ Object
-
#cursor_up(lines) ⇒ Object
ESC [ Pn A / B FIXME: Should these not use clamph?.
- #decaln ⇒ Object
- #delete ⇒ Object
-
#delete_chars(num) ⇒ Object
DCH - delete characters at the cursor, shifting the line left.
-
#delete_lines(num) ⇒ Object
FIXME (still broken) Emacs uses this to scroll up.
- #draw_cursor ⇒ Object
-
#erase_in_display(ps = 0) ⇒ Object
ED - Erase In Display - ESC [ Ps J.
-
#erase_in_line(ps = 0) ⇒ Object
EL - Erase In Line - ESC [ Ps K.
-
#feed(str) ⇒ Object
(also: #write)
This is the preferred public interface:.
-
#fg ⇒ Object
Resolved fg/bg are recomputed only when an SGR (or reset) changes @fg/@bg/@mode - not per character.
- #handle_control(ch) ⇒ Object
- #handle_csi(s) ⇒ Object
-
#handle_dec(s) ⇒ Object
CSI ‘?’ -> DEC private modes.
- #handle_escape(ch) ⇒ Object
- #height ⇒ Object
-
#index ⇒ Object
IND - index: down one line; at the bottom margin scroll the region up.
-
#initialize(buffer) ⇒ Term
constructor
A new instance of Term.
- #insert_lines(num) ⇒ Object
- #invalidate_colours ⇒ Object
-
#line_width ⇒ Object
Usable column count of the current line.
- #linefeed ⇒ Object
- #origin ⇒ Object
- #parse_color(codes) ⇒ Object
- #putchar(ch) ⇒ Object
-
#redraw_line_from_cursor ⇒ Object
FIXME: Redrawing full spans would be better.
- #region_bottom ⇒ Object
-
#region_top ⇒ Object
The scroll region margins.
-
#reset ⇒ Object
RIS - Reset to Initial State (ESC c).
- #resize(w, h) ⇒ Object
-
#reverse_index ⇒ Object
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).
- #scroll_if_needed ⇒ Object
- #scroll_up(num = 1) ⇒ Object
-
#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).
- #set_modes(codes) ⇒ Object
- #set_width_and_clear(w) ⇒ Object
- #width ⇒ Object
- #wrap_if_needed ⇒ Object
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
#cursor ⇒ Object
Returns the value of attribute cursor.
17 18 19 |
# File 'lib/term.rb', line 17 def cursor @cursor end |
#esc ⇒ Object
Returns the value of attribute esc.
17 18 19 |
# File 'lib/term.rb', line 17 def esc @esc end |
#mode ⇒ Object
Returns the value of attribute mode.
17 18 19 |
# File 'lib/term.rb', line 17 def mode @mode end |
#mouse_buttons ⇒ Object
Returns the value of attribute mouse_buttons.
17 18 19 |
# File 'lib/term.rb', line 17 def @mouse_buttons end |
#mouse_mode ⇒ Object
Returns the value of attribute mouse_mode.
17 18 19 |
# File 'lib/term.rb', line 17 def mouse_mode @mouse_mode end |
#mouse_reporting ⇒ Object
Returns the value of attribute mouse_reporting.
17 18 19 |
# File 'lib/term.rb', line 17 def mouse_reporting @mouse_reporting end |
#origin_mode ⇒ Object
Returns the value of attribute origin_mode.
17 18 19 |
# File 'lib/term.rb', line 17 def origin_mode @origin_mode end |
#responder ⇒ Object
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 |
#tabs ⇒ Object
Returns the value of attribute tabs.
17 18 19 |
# File 'lib/term.rb', line 17 def tabs @tabs end |
#wraparound ⇒ Object
Returns the value of attribute wraparound.
17 18 19 |
# File 'lib/term.rb', line 17 def wraparound @wraparound end |
#x ⇒ Object
Returns the value of attribute x.
17 18 19 |
# File 'lib/term.rb', line 17 def x @x end |
#y ⇒ Object
Returns the value of attribute y.
17 18 19 |
# File 'lib/term.rb', line 17 def y @y end |
Instance Method Details
#bg ⇒ Object
234 235 236 |
# File 'lib/term.rb', line 234 def bg @bg_resolved ||= @bg.is_a?(String) ? PALETTE_BASIC[@bg.to_i] : @bg end |
#bottom ⇒ Object
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). |
#charset ⇒ Object
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_above ⇒ Object
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_below ⇒ Object
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_cursor ⇒ Object
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_screen ⇒ Object
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_end ⇒ Object
94 |
# File 'lib/term.rb', line 94 def clear_to_end = @buffer.clear_line(@y, @x) |
#clear_to_start ⇒ Object
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))) |
#decaln ⇒ Object
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 |
#delete ⇒ Object
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_cursor ⇒ Object
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 |
#fg ⇒ Object
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 |
#height ⇒ Object
92 |
# File 'lib/term.rb', line 92 def height = @term_height |
#index ⇒ Object
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_colours ⇒ Object
237 |
# File 'lib/term.rb', line 237 def invalidate_colours; @fg_resolved = @bg_resolved = nil; end |
#line_width ⇒ Object
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 |
#linefeed ⇒ Object
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 |
#origin ⇒ Object
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_cursor ⇒ Object
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_bottom ⇒ Object
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_top ⇒ Object
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 |
#reset ⇒ Object
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_index ⇒ Object
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_needed ⇒ Object
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 |
#width ⇒ Object
91 |
# File 'lib/term.rb', line 91 def width = @term_width |
#wrap_if_needed ⇒ Object
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 |