Class: Charming::Internal::Terminal::TTYBackend

Inherits:
Object
  • Object
show all
Includes:
Adapter
Defined in:
lib/charming/internal/terminal/tty_backend.rb

Overview

TTYBackend is the production terminal backend. It reads key and mouse events from a TTY::Reader, normalizes them via KeyNormalizer and MouseParser, and writes output frames using TTY::Cursor and TTY::Screen. It also installs SIGWINCH and SIGINFO handlers so the runtime can react to terminal resize and focus changes.

Constant Summary collapse

ALT_SCREEN_ON =

Escape sequences for entering/leaving the alternate screen buffer.

"\e[?1049h"
ALT_SCREEN_OFF =
"\e[?1049l"
AUTO_WRAP_OFF =

Escape sequences for disabling/enabling automatic line wrapping during frame writes.

"\e[?7l"
AUTO_WRAP_ON =
"\e[?7h"
BRACKETED_PASTE_ON =

Escape sequences for enabling/disabling bracketed-paste mode, and the markers the terminal wraps around pasted text.

"\e[?2004h"
BRACKETED_PASTE_OFF =
"\e[?2004l"
PASTE_START =
"\e[200~"
PASTE_END =
"\e[201~"
FOCUS_REPORTING_ON =

Escape sequences for terminal focus reporting and the focus-in/out markers.

"\e[?1004h"
FOCUS_REPORTING_OFF =
"\e[?1004l"
FOCUS_IN =
"\e[I"
FOCUS_OUT =
"\e[O"

Instance Method Summary collapse

Constructor Details

#initialize(input: $stdin, output: $stdout, reader: nil, cursor: TTY::Cursor) ⇒ TTYBackend

input and output default to ‘$stdin`/`$stdout` for normal terminal use; tests can inject IO objects. reader is a TTY::Reader instance (created from input/output when nil). cursor is the TTY::Cursor class used for cursor control.



41
42
43
44
45
46
47
48
49
50
# File 'lib/charming/internal/terminal/tty_backend.rb', line 41

def initialize(input: $stdin, output: $stdout, reader: nil, cursor: TTY::Cursor)
  @input = input
  @output = output
  @reader = reader || TTY::Reader.new(input: input, output: output)
  @cursor = cursor
  @key_normalizer = KeyNormalizer.new(@reader)
  @resized = false
  @previous_winch_handler = nil
  @mouse_enabled = false
end

Instance Method Details

#clearObject

Clears the terminal screen and moves the cursor to (1, 1).



211
212
213
# File 'lib/charming/internal/terminal/tty_backend.rb', line 211

def clear
  write_control(@cursor.clear_screen)
end

#disable_bracketed_pasteObject

Emits the ANSI sequence disabling bracketed-paste mode. Idempotent.



96
97
98
99
100
101
# File 'lib/charming/internal/terminal/tty_backend.rb', line 96

def disable_bracketed_paste
  return unless @bracketed_paste

  write_control(BRACKETED_PASTE_OFF)
  @bracketed_paste = false
end

#disable_focus_reportingObject

Emits the ANSI sequence disabling terminal focus reporting. Idempotent.



80
81
82
83
84
85
# File 'lib/charming/internal/terminal/tty_backend.rb', line 80

def disable_focus_reporting
  return unless @focus_reporting

  write_control(FOCUS_REPORTING_OFF)
  @focus_reporting = false
end

#disable_mouse_trackingObject

Emits the ANSI sequences that disable terminal mouse reporting. Idempotent.



155
156
157
158
159
160
161
162
163
# File 'lib/charming/internal/terminal/tty_backend.rb', line 155

def disable_mouse_tracking
  return unless @mouse_enabled

  write_control("\e[?1000l")
  write_control("\e[?1002l")
  write_control("\e[?1003l")
  write_control("\e[?1006l")
  @mouse_enabled = false
end

#enable_bracketed_pasteObject

Emits the ANSI sequence enabling bracketed-paste mode. Idempotent.



88
89
90
91
92
93
# File 'lib/charming/internal/terminal/tty_backend.rb', line 88

def enable_bracketed_paste
  return if @bracketed_paste

  write_control(BRACKETED_PASTE_ON)
  @bracketed_paste = true
end

#enable_focus_reportingObject

Emits the ANSI sequence enabling terminal focus reporting. Idempotent.



72
73
74
75
76
77
# File 'lib/charming/internal/terminal/tty_backend.rb', line 72

def enable_focus_reporting
  return if @focus_reporting

  write_control(FOCUS_REPORTING_ON)
  @focus_reporting = true
end

#enable_mouse_trackingObject

Emits the ANSI sequences that enable terminal mouse reporting (press, motion, SGR). Idempotent: skipped when mouse tracking is already enabled.



145
146
147
148
149
150
151
152
# File 'lib/charming/internal/terminal/tty_backend.rb', line 145

def enable_mouse_tracking
  return if @mouse_enabled

  write_control("\e[?1000h")
  write_control("\e[?1002h")
  write_control("\e[?1006h")
  @mouse_enabled = true
end

#enter_alt_screenObject

Enters the alternate screen buffer.



191
192
193
# File 'lib/charming/internal/terminal/tty_backend.rb', line 191

def enter_alt_screen
  write_control(ALT_SCREEN_ON)
end

#hide_cursorObject

Hides the terminal cursor.



206
207
208
# File 'lib/charming/internal/terminal/tty_backend.rb', line 206

def hide_cursor
  write_control(@cursor.hide)
end

#install_focus_handlerObject

Installs a SIGINFO handler that marks the terminal as having received focus. SIGINFO is sent by some terminals (notably macOS Terminal.app) on focus changes.



125
126
127
128
129
# File 'lib/charming/internal/terminal/tty_backend.rb', line 125

def install_focus_handler
  # Terminal focus change: some terminals send a special sequence
  # when focus changes. We use this to throttle rendering.
  @previous_focus_handler = Signal.trap("INFO") { @focused = true }
end

#install_resize_handlerObject

Installs a SIGWINCH handler that sets the internal ‘@resized` flag, returning the previous handler so it can be restored on teardown.



119
120
121
# File 'lib/charming/internal/terminal/tty_backend.rb', line 119

def install_resize_handler
  @previous_winch_handler = Signal.trap("WINCH") { @resized = true }
end

#leave_alt_screenObject

Leaves the alternate screen buffer.



196
197
198
# File 'lib/charming/internal/terminal/tty_backend.rb', line 196

def leave_alt_screen
  write_control(ALT_SCREEN_OFF)
end

#mouse_enabled?Boolean

Returns whether mouse tracking is currently enabled on this backend.

Returns:

  • (Boolean)


166
167
168
# File 'lib/charming/internal/terminal/tty_backend.rb', line 166

def mouse_enabled?
  @mouse_enabled
end

#move_cursor(row, column) ⇒ Object

Moves the terminal cursor to the given 1-based (row, column).



216
217
218
# File 'lib/charming/internal/terminal/tty_backend.rb', line 216

def move_cursor(row, column)
  write_control(@cursor.move_to(column - 1, row - 1))
end

#notify_resizeObject

Manually flags the backend as resized (used by tests or external integrations).



171
172
173
# File 'lib/charming/internal/terminal/tty_backend.rb', line 171

def notify_resize
  @resized = true
end

#read_event(timeout: nil) ⇒ Object

Reads the next event. If a SIGWINCH was received, returns a ResizeEvent with the current terminal dimensions. Bracketed pastes return a PasteEvent; mouse escape sequences are parsed by MouseParser; other input is normalized via KeyNormalizer. Returns nil on timeout.



56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/charming/internal/terminal/tty_backend.rb', line 56

def read_event(timeout: nil)
  return resize_event if resized?

  raw = @reader.read_keypress(echo: false, raw: true, nonblock: timeout)
  return nil unless raw
  return Events::FocusEvent.new(focused: true) if raw == FOCUS_IN
  return Events::FocusEvent.new(focused: false) if raw == FOCUS_OUT
  return paste_event(raw) if raw.start_with?(PASTE_START)
  return MouseParser.parse(raw) if MouseParser.sequence?(raw)

  @key_normalizer.normalize(raw)
rescue Errno::EAGAIN, IO::WaitReadable
  nil
end

#restore_focus_handlerObject

Restores the previous SIGINFO handler.



132
133
134
135
# File 'lib/charming/internal/terminal/tty_backend.rb', line 132

def restore_focus_handler
  Signal.trap("INFO", @previous_focus_handler) if @previous_focus_handler
  @previous_focus_handler = nil
end

#restore_resize_handlerObject

Restores the previous SIGWINCH handler captured by ‘install_resize_handler`.



138
139
140
141
# File 'lib/charming/internal/terminal/tty_backend.rb', line 138

def restore_resize_handler
  Signal.trap("WINCH", @previous_winch_handler) if @previous_winch_handler
  @previous_winch_handler = nil
end

#show_cursorObject

Shows the terminal cursor.



201
202
203
# File 'lib/charming/internal/terminal/tty_backend.rb', line 201

def show_cursor
  write_control(@cursor.show)
end

#sizeObject

Returns the current terminal dimensions as [width, height] via TTY::Screen.



221
# File 'lib/charming/internal/terminal/tty_backend.rb', line 221

def size = [TTY::Screen.width, TTY::Screen.height]

#with_raw_inputObject

Keeps terminal input in raw/no-echo mode for the duration of a TUI run. Reading a single keypress in raw mode is not enough: keys pressed while rendering or dispatching events can otherwise be echoed into the alternate screen before the next read.



106
107
108
109
110
111
112
113
114
115
# File 'lib/charming/internal/terminal/tty_backend.rb', line 106

def with_raw_input
  return yield unless @input.respond_to?(:tty?) && @input.tty?
  return yield unless @input.respond_to?(:raw) && @input.respond_to?(:noecho)

  @input.raw do
    @input.noecho do
      yield
    end
  end
end

#write_frame(frame) ⇒ Object

Writes a full multi-line frame to the terminal, disabling auto-wrap during the write so overlong lines don’t disturb the screen layout.



177
178
179
180
181
# File 'lib/charming/internal/terminal/tty_backend.rb', line 177

def write_frame(frame)
  without_auto_wrap do
    write_positioned_lines(frame.to_s.lines(chomp: true))
  end
end

#write_lines(line_changes) ⇒ Object

Writes a partial frame composed of [row, line] tuples (1-based rows).



184
185
186
187
188
# File 'lib/charming/internal/terminal/tty_backend.rb', line 184

def write_lines(line_changes, **)
  without_auto_wrap do
    write_control(line_changes.map { |row, line| positioned_line(row, line) }.join)
  end
end