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



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

def clear
  write_control(@cursor.clear_screen)
end

#disable_mouse_trackingObject

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



92
93
94
95
96
97
98
99
100
# File 'lib/charming/internal/terminal/tty_backend.rb', line 92

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.



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

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.



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

def enter_alt_screen
  write_control(ALT_SCREEN_ON)
end

#hide_cursorObject

Hides the terminal cursor.



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

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.



62
63
64
65
66
# File 'lib/charming/internal/terminal/tty_backend.rb', line 62

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.



56
57
58
# File 'lib/charming/internal/terminal/tty_backend.rb', line 56

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

#leave_alt_screenObject

Leaves the alternate screen buffer.



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

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)


103
104
105
# File 'lib/charming/internal/terminal/tty_backend.rb', line 103

def mouse_enabled?
  @mouse_enabled
end

#move_cursor(row, column) ⇒ Object

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



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

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



108
109
110
# File 'lib/charming/internal/terminal/tty_backend.rb', line 108

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.



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

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



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

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.



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

def show_cursor
  write_control(@cursor.show)
end

#sizeObject

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



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

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

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



114
115
116
117
118
# File 'lib/charming/internal/terminal/tty_backend.rb', line 114

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



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

def write_lines(line_changes, **)
  without_auto_wrap do
    write_control(line_changes.map { |row, line| "\e[#{row};1H\e[2K#{line}" }.join)
  end
end