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
- #restore_cursor ⇒ 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).
-
#save_cursor ⇒ Object
Cursor + charset save/restore, shared by DECSC/DECRC (ESC 7 / ESC 8) and DEC private modes 1048/1049.
- #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
243 244 245 |
# File 'lib/term.rb', line 243 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.
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). |
#charset ⇒ Object
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_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.)
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_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
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))) |
#decaln ⇒ Object
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 |
#delete ⇒ Object
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_cursor ⇒ Object
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 |
#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.
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 |
#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.
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_colours ⇒ Object
246 |
# File 'lib/term.rb', line 246 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.
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 |
#linefeed ⇒ Object
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 |
#origin ⇒ Object
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_cursor ⇒ Object
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_bottom ⇒ Object
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_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).
357 |
# File 'lib/term.rb', line 357 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.
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_cursor ⇒ Object
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_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).
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_cursor ⇒ Object
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_needed ⇒ Object
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 |
#width ⇒ Object
91 |
# File 'lib/term.rb', line 91 def width = @term_width |
#wrap_if_needed ⇒ Object
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 |