Class: AnsiBackend

Inherits:
Object
  • Object
show all
Defined in:
lib/ansibackend.rb

Overview

A rendering backend that emits ANSI escape sequences instead of painting pixels: a drop-in for WindowAdapter (it satisfies the same draw / scroll / clear / clear_line / insert_lines / delete_lines interface the run-batcher in TrackChanges drives, plus the cell-metric and scrollback hooks Term queries). It turns the terminal’s damage stream - changed runs of cells, plus scroll/clear ops - into the minimal escape sequences that reproduce the screen on a real terminal. This is the “render economically to a terminal, like Emacs” backend, and the basis for letting a TUI app render to a terminal OR an X11 window from the same code.

Colours arrive already resolved to 24-bit RGB (Term#fg/#bg), so they are emitted as truecolor SGR. The run-batcher only hands us cells that changed, so only changed runs are emitted (CUP + minimal SGR + text). The cursor overlay (a cell drawn with the CURSOR background) is recognised and turned into a real cursor position rather than a coloured cell.

Constant Summary collapse

CURSOR_BG =

must match Term::CURSOR

0xff00ff
SGR_FLAGS =

flag bit -> SGR set-code

{
  BOLD => 1, FAINT => 2, ITALICS => 3, UNDERLINE => 4, BLINK => 5,
  RAPID_BLINK => 6, INVERSE => 7, INVISIBLE => 8, CROSSED_OUT => 9,
  DBL_UNDERLINE => 21, OVERLINE => 53,
}.freeze

Instance Method Summary collapse

Constructor Details

#initialize(cols, rows, origin_row: 0, origin_col: 0) ⇒ AnsiBackend

origin_row/origin_col place the rendered screen at an offset on the real terminal, so the same Term core can be drawn into a sub-window (a terminal-in-a-terminal / multiplexer pane). With a non-zero origin, full-screen erase becomes a per-row erase of just the sub-window.



32
33
34
35
36
37
# File 'lib/ansibackend.rb', line 32

def initialize(cols, rows, origin_row: 0, origin_col: 0)
  @cols, @rows = cols, rows
  @origin_row, @origin_col = origin_row, origin_col
  @out = +"".b
  reset_state
end

Instance Method Details

#char_hObject



48
# File 'lib/ansibackend.rb', line 48

def char_h = 1

#char_wObject

# cell metrics: a text cell is one character



47
# File 'lib/ansibackend.rb', line 47

def char_w = 1

#clearObject



83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/ansibackend.rb', line 83

def clear
  reset_state
  if @origin_row.zero? && @origin_col.zero?
    @out << "\e[H\e[2J"
    @cx = @cy = 0 # \e[H homes the cursor
  else
    # Erase only this sub-window's rows, not the whole real screen. This
    # leaves the cursor at the last erased row, so @cx/@cy stay nil
    # (reset_state) and the next draw re-issues a CUP.
    @rows.times { |y| @out << cup(y, 0) << "\e[2K" }
  end
end

#clear_line(y, from_x, to_x = nil) ⇒ Object



96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/ansibackend.rb', line 96

def clear_line(y, from_x, to_x = nil)
  if to_x
    # Erase to start (from_x is 0 in practice): emit EL-1 rather than
    # synthesising spaces, so the replay's clear_to_start reproduces the
    # exact same cells (raw default attributes, not a resolved \e[0m).
    move(y, to_x)
    @out << "\e[1K"
  else
    move(y, from_x)
    @out << "\e[0K" # erase to end of line (buffer truncates the row)
  end
end

#delete_lines(y, num, maxy) ⇒ Object



120
121
122
123
124
# File 'lib/ansibackend.rb', line 120

def delete_lines(y, num, maxy)
  set_region(@region ? @region[0] : 0, maxy)
  move(y, 0)
  @out << "\e[#{num}M"
end

#draw(x, y, str, fg, bg, flags, _lineattrs = nil) ⇒ Object



71
72
73
74
75
76
77
78
79
80
81
# File 'lib/ansibackend.rb', line 71

def draw(x, y, str, fg, bg, flags, _lineattrs = nil)
  if bg == CURSOR_BG
    @cursor_pos = [x, y] # cursor overlay - the real cursor shows position
    return
  end
  move(y, x)
  @out << sgr(fg, bg, flags)
  @out << str
  @cx = x + str.length
  @cy = y
end

#insert_lines(y, num, maxy) ⇒ Object



114
115
116
117
118
# File 'lib/ansibackend.rb', line 114

def insert_lines(y, num, maxy)
  set_region(@region ? @region[0] : 0, maxy)
  move(y, 0)
  @out << "\e[#{num}L"
end

#outputObject

The escape sequence produced so far, with a trailing reposition to the cursor overlay if one was seen. Non-destructive.



55
56
57
# File 'lib/ansibackend.rb', line 55

def output
  @cursor_pos ? @out + cup(@cursor_pos[1], @cursor_pos[0]) : @out.dup
end

#reset_stateObject



39
40
41
42
43
44
# File 'lib/ansibackend.rb', line 39

def reset_state
  @cx = @cy = nil          # tracked cursor (nil = unknown -> force CUP)
  @fg = @bg = @flags = nil # tracked SGR (nil = unknown)
  @region = nil
  @cursor_pos = nil
end

#scroll_up(scroll_start, scroll_end) ⇒ Object



109
110
111
112
# File 'lib/ansibackend.rb', line 109

def scroll_up(scroll_start, scroll_end)
  set_region(scroll_start, scroll_end)
  @out << "\e[S" # scroll the region up one line
end

#scrollback_anchorObject



50
# File 'lib/ansibackend.rb', line 50

def scrollback_anchor; end

#scrollback_modeObject



49
# File 'lib/ansibackend.rb', line 49

def scrollback_mode = false

#set_columns(_) ⇒ Object



51
# File 'lib/ansibackend.rb', line 51

def set_columns(_); end

#takeObject

Take the output and reset the buffer for the next frame. The trailing cursor reposition moved the real terminal cursor, so forget the tracked position (the next frame’s first draw must re-issue a CUP). SGR/region state persists - the real terminal keeps it between frames.



63
64
65
66
67
68
69
# File 'lib/ansibackend.rb', line 63

def take
  s = output
  @out = +"".b
  @cursor_pos = nil
  @cx = @cy = nil
  s
end