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"

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.



28
29
30
31
32
33
34
35
36
37
# File 'lib/charming/internal/terminal/tty_backend.rb', line 28

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).



162
163
164
# File 'lib/charming/internal/terminal/tty_backend.rb', line 162

def clear
  write_control(@cursor.clear_screen)
end

#disable_mouse_trackingObject

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



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

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_mouse_trackingObject

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



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

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.



142
143
144
# File 'lib/charming/internal/terminal/tty_backend.rb', line 142

def enter_alt_screen
  write_control(ALT_SCREEN_ON)
end

#hide_cursorObject

Hides the terminal cursor.



157
158
159
# File 'lib/charming/internal/terminal/tty_backend.rb', line 157

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.



76
77
78
79
80
# File 'lib/charming/internal/terminal/tty_backend.rb', line 76

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.



70
71
72
# File 'lib/charming/internal/terminal/tty_backend.rb', line 70

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

#leave_alt_screenObject

Leaves the alternate screen buffer.



147
148
149
# File 'lib/charming/internal/terminal/tty_backend.rb', line 147

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)


117
118
119
# File 'lib/charming/internal/terminal/tty_backend.rb', line 117

def mouse_enabled?
  @mouse_enabled
end

#move_cursor(row, column) ⇒ Object

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



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

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).



122
123
124
# File 'lib/charming/internal/terminal/tty_backend.rb', line 122

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. Mouse escape sequences are parsed by MouseParser; other input is normalized via KeyNormalizer. Returns nil on timeout.



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

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 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.



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

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`.



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

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.



152
153
154
# File 'lib/charming/internal/terminal/tty_backend.rb', line 152

def show_cursor
  write_control(@cursor.show)
end

#sizeObject

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



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

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.



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

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.



128
129
130
131
132
# File 'lib/charming/internal/terminal/tty_backend.rb', line 128

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).



135
136
137
138
139
# File 'lib/charming/internal/terminal/tty_backend.rb', line 135

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