Class: TTY::Command::Window::Emulator

Inherits:
Object
  • Object
show all
Defined in:
lib/tty/command/window/emulator.rb

Overview

A VT100-subset terminal emulator with a fixed-size screen.

The emulator maintains a rows x cols grid of cells (character + SGR attributes), interprets the escape sequences commonly emitted by CLI tools (cursor movement, erase, scroll regions, insert/delete, colors, alternate screen) and keeps a plain-text scrollback of lines that scroll off the top of the main screen.

It is not thread-safe on its own; callers synchronize access.

Constant Summary collapse

BLANK =
[" ", nil].freeze
WIDE_PLACEHOLDER =
["", nil].freeze
MAX_SEQUENCE =
512
TAB_STOP =
8
ASCII_CHARS =

Pre-allocated frozen single-char strings for every printable ASCII codepoint. The ASCII fast path in scan reuses these instead of allocating a new String per printable byte (see P0-3).

(0x20..0x7E).map { |b| b.chr(Encoding::UTF_8).freeze }.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(rows:, cols:, scrollback_limit: DEFAULT_SCROLLBACK, responder: nil) ⇒ Emulator

Returns a new instance of Emulator.

Parameters:

  • rows (Integer)

    screen height in lines

  • cols (Integer)

    screen width in columns

  • scrollback_limit (Integer) (defaults to: DEFAULT_SCROLLBACK)

    max plain-text history lines kept

  • responder (#call, nil) (defaults to: nil)

    receives strings the terminal would send back to the child (cursor position reports etc.)



35
36
37
38
39
40
41
42
43
# File 'lib/tty/command/window/emulator.rb', line 35

def initialize(rows:, cols:, scrollback_limit: DEFAULT_SCROLLBACK, responder: nil)
  @rows = rows
  @cols = [cols, 1].max
  @scrollback_limit = scrollback_limit
  @responder = responder
  @scrollback = []
  @partial_input = (+"").force_encoding(Encoding::BINARY)
  full_reset
end

Instance Attribute Details

#colsObject (readonly)

Returns the value of attribute cols.



28
29
30
# File 'lib/tty/command/window/emulator.rb', line 28

def cols
  @cols
end

#cursor_colObject (readonly)

Returns the value of attribute cursor_col.



28
29
30
# File 'lib/tty/command/window/emulator.rb', line 28

def cursor_col
  @cursor_col
end

#cursor_rowObject (readonly)

Returns the value of attribute cursor_row.



28
29
30
# File 'lib/tty/command/window/emulator.rb', line 28

def cursor_row
  @cursor_row
end

#rowsObject (readonly)

Returns the value of attribute rows.



28
29
30
# File 'lib/tty/command/window/emulator.rb', line 28

def rows
  @rows
end

#scrollbackObject (readonly)

Returns the value of attribute scrollback.



28
29
30
# File 'lib/tty/command/window/emulator.rb', line 28

def scrollback
  @scrollback
end

Instance Method Details

#cursor_visible?Boolean

Returns whether the child requested a visible cursor.

Returns:

  • (Boolean)

    whether the child requested a visible cursor



83
84
85
# File 'lib/tty/command/window/emulator.rb', line 83

def cursor_visible?
  @cursor_visible
end

#feed(data) ⇒ Object

Feed a chunk of raw child output into the emulator. Handles UTF-8 characters and escape sequences split across chunk boundaries.

Parameters:

  • data (String)


49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/tty/command/window/emulator.rb', line 49

def feed(data)
  buffer = @partial_input + data.dup.force_encoding(Encoding::BINARY)
  @partial_input = (+"").force_encoding(Encoding::BINARY)

  tail = incomplete_utf8_tail(buffer)
  if tail.positive?
    @partial_input = buffer.byteslice(-tail, tail)
    buffer = buffer.byteslice(0, buffer.bytesize - tail)
  end

  buffer.force_encoding(Encoding::UTF_8)
  buffer = buffer.scrub("\u{FFFD}") unless buffer.valid_encoding?
  scan(buffer)
  invalidate_render_cache
  nil
end

#full_textString

Returns scrollback plus visible screen as plain text.

Returns:

  • (String)

    scrollback plus visible screen as plain text



120
121
122
# File 'lib/tty/command/window/emulator.rb', line 120

def full_text
  (@scrollback + screen_text).join("\n").rstrip
end

#render_line(index) ⇒ String

Render one screen row as an ANSI string (no trailing newline, no clear-to-eol; trailing unstyled blanks are stripped).

Parameters:

  • index (Integer)

Returns:

  • (String)


92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/tty/command/window/emulator.rb', line 92

def render_line(index)
  row = @grid[index] or return ""
  last = row.rindex { |cell| cell[0] != " " || cell[1] } or return ""

  out = +""
  current = false
  row[0..last].each do |(char, sgr)|
    if sgr != current
      out << (sgr ? "\e[0;#{sgr}m" : "\e[0m")
      current = sgr
    end
    out << char
  end
  out << "\e[0m" if current
  out
end

#render_linesArray<String>

Returns every screen row rendered with ANSI colors.

Returns:

  • (Array<String>)

    every screen row rendered with ANSI colors



110
111
112
# File 'lib/tty/command/window/emulator.rb', line 110

def render_lines
  @render_lines ||= Array.new(@rows) { |i| render_line(i) }
end

#resize(cols:) ⇒ Object

Resize the screen width. Rows are fixed for the window's lifetime.

Parameters:

  • cols (Integer)


69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/tty/command/window/emulator.rb', line 69

def resize(cols:)
  cols = [cols, 1].max
  return if cols == @cols

  @cols = cols
  [@main_grid, @alt_grid].each do |grid|
    grid.each { |row| resize_row(row, cols) }
  end
  @cursor_col = [@cursor_col, cols - 1].min
  @pending_wrap = false
  invalidate_render_cache
end

#screen_textArray<String>

Returns plain-text content of the visible screen.

Returns:

  • (Array<String>)

    plain-text content of the visible screen



115
116
117
# File 'lib/tty/command/window/emulator.rb', line 115

def screen_text
  @grid.map { |row| plain_row(row) }
end