Class: Charming::Internal::Terminal::TTYBackend
- Inherits:
-
Object
- Object
- Charming::Internal::Terminal::TTYBackend
- 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
-
#clear ⇒ Object
Clears the terminal screen and moves the cursor to (1, 1).
-
#disable_mouse_tracking ⇒ Object
Emits the ANSI sequences that disable terminal mouse reporting.
-
#enable_mouse_tracking ⇒ Object
Emits the ANSI sequences that enable terminal mouse reporting (press, motion, SGR).
-
#enter_alt_screen ⇒ Object
Enters the alternate screen buffer.
-
#hide_cursor ⇒ Object
Hides the terminal cursor.
-
#initialize(input: $stdin, output: $stdout, reader: nil, cursor: TTY::Cursor) ⇒ TTYBackend
constructor
input and output default to ‘$stdin`/`$stdout` for normal terminal use; tests can inject IO objects.
-
#install_focus_handler ⇒ Object
Installs a SIGINFO handler that marks the terminal as having received focus.
-
#install_resize_handler ⇒ Object
Installs a SIGWINCH handler that sets the internal ‘@resized` flag, returning the previous handler so it can be restored on teardown.
-
#leave_alt_screen ⇒ Object
Leaves the alternate screen buffer.
-
#mouse_enabled? ⇒ Boolean
Returns whether mouse tracking is currently enabled on this backend.
-
#move_cursor(row, column) ⇒ Object
Moves the terminal cursor to the given 1-based (row, column).
-
#notify_resize ⇒ Object
Manually flags the backend as resized (used by tests or external integrations).
-
#read_event(timeout: nil) ⇒ Object
Reads the next event.
-
#restore_focus_handler ⇒ Object
Restores the previous SIGINFO handler.
-
#restore_resize_handler ⇒ Object
Restores the previous SIGWINCH handler captured by ‘install_resize_handler`.
-
#show_cursor ⇒ Object
Shows the terminal cursor.
-
#size ⇒ Object
Returns the current terminal dimensions as [width, height] via TTY::Screen.
-
#with_raw_input ⇒ Object
Keeps terminal input in raw/no-echo mode for the duration of a TUI run.
-
#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.
-
#write_lines(line_changes) ⇒ Object
Writes a partial frame composed of [row, line] tuples (1-based rows).
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
#clear ⇒ Object
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_tracking ⇒ Object
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_tracking ⇒ Object
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_screen ⇒ Object
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_cursor ⇒ Object
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_handler ⇒ Object
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_handler ⇒ Object
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_screen ⇒ Object
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.
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_resize ⇒ Object
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_handler ⇒ Object
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_handler ⇒ Object
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_cursor ⇒ Object
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 |
#size ⇒ Object
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_input ⇒ Object
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 |