TermUI.Backend.Raw (TermUI v1.0.0)
View SourceRaw terminal backend providing full terminal control.
The Raw backend is the primary high-fidelity rendering path in TermUI. It provides direct terminal control with immediate keystroke detection, true color support, mouse tracking, and all advanced terminal features.
Requirements
- OTP 28+: Raw mode is activated via
:shell.start_interactive({:noshell, :raw}) - Terminal access: Requires a real terminal (not pipes or redirected I/O)
How It Works
The Raw backend assumes raw mode has already been activated by TermUI.Backend.Selector
before init/1 is called. The selector uses :shell.start_interactive({:noshell, :raw})
to enter raw mode, and on success, routes to this backend.
Important: The init/1 callback does NOT activate raw mode itself. It only performs
terminal setup (alternate screen, cursor hiding, etc.) assuming raw mode is already active.
Features
When raw mode is active, this backend provides:
- Alternate screen buffer: Preserves original terminal content, restored on exit
- Cursor control: Hide/show cursor, precise positioning
- True color rendering: Full 24-bit RGB color support (
{r, g, b}tuples) - 256-color palette: Extended color support (0-255 indices)
- Mouse tracking: Click, drag, and movement detection
- Immediate input: Character-by-character keystroke detection
- Escape sequence handling: Function keys, arrow keys, modifiers
Initialization Flow
1. Selector calls :shell.start_interactive({:noshell, :raw})
└── Returns :ok (raw mode active)
2. Runtime creates Raw backend state
└── Calls Raw.init(opts)
3. Raw.init/1 performs terminal setup:
├── Enter alternate screen buffer (optional)
├── Hide cursor
├── Enable mouse tracking (optional)
└── Clear screenConfiguration Options
The init/1 callback accepts these options:
:alternate_screen- Use alternate screen buffer (default:true):hide_cursor- Hide cursor during rendering (default:true):mouse_tracking- Mouse tracking mode (default::none):none- No mouse tracking:click- Track button clicks only:drag- Track clicks and drag events:all- Track all mouse movement
:size- Explicit terminal dimensions{rows, cols}(default: auto-detect)
Shutdown Behavior
The shutdown/1 callback restores the terminal to its pre-init state:
- Disable mouse tracking (if enabled)
- Show cursor
- Reset all text attributes
- Leave alternate screen (if entered)
- Return to cooked mode via
:shell.start_interactive({:noshell, :cooked})
Shutdown is designed to be error-safe - individual failures don't prevent subsequent cleanup steps from running.
Usage Example
This backend is typically used via the runtime, not directly:
# Automatic backend selection (recommended)
{:ok, runtime} = TermUI.Runtime.start_link(root: MyApp.Root)
# The runtime handles:
# 1. Backend selection via Selector
# 2. Backend initialization
# 3. Rendering via draw_cells/2
# 4. Input polling via poll_event/2
# 5. Clean shutdownMouse Tracking Modes
The Raw backend uses intuitive mode names that map to underlying ANSI protocol modes:
| Raw Backend | ANSI Protocol | Escape Sequence | Description |
|---|---|---|---|
:none | (disabled) | - | No mouse tracking |
:click | Normal (1000) | ESC[?1000h | Button press/release only |
:drag | Button (1002) | ESC[?1002h | Press/release + motion while pressed |
:all | Any (1003) | ESC[?1003h | All mouse motion events |
When mouse tracking is enabled, SGR extended mode (ESC[?1006h) is also activated
for accurate coordinate encoding beyond column 223.
Note: The TermUI.ANSI module uses protocol names (:normal, :button, :all),
while this backend uses user-friendly names (:click, :drag, :all). The mapping
is handled internally when emitting sequences.
Style Delta Optimization
The current_style field in the backend state tracks the last-emitted SGR (Select
Graphic Rendition) attributes. This enables style delta optimization in
draw_cells/2:
Instead of emitting full style sequences for every cell:
ESC[0;38;2;255;0;0;48;2;0;0;0mA <- 25 bytes per cell
ESC[0;38;2;255;0;0;48;2;0;0;0mBWe only emit changes from the previous style:
ESC[38;2;255;0;0;48;2;0;0;0mA <- Full style for first cell
B <- No escape needed, same style!
ESC[38;2;0;255;0mC <- Only foreground changedThis optimization can reduce escape sequence output by 80-90% for typical UIs where adjacent cells share styles (text blocks, borders, backgrounds).
The current_style map tracks:
:fg- Current foreground color:bg- Current background color:attrs- Current text attributes (:bold,:underline,:reverse, etc.)
See Also
TermUI.Backend- Behaviour definitionTermUI.Backend.Selector- Backend selection logicTermUI.Backend.TTY- Fallback backend for non-raw environmentsTermUI.ANSI- Escape sequence generation
Summary
Types
Mouse tracking mode for the terminal.
Current SGR (Select Graphic Rendition) style state.
Internal state for the Raw backend.
Functions
Clears the entire screen and moves cursor to home position.
Disables mouse tracking.
Draws cells to the terminal at specified positions.
Enables mouse tracking with the specified mode.
Flushes pending output to the terminal.
Hides the terminal cursor.
Initializes the Raw backend with terminal setup.
Maps a Raw backend mouse mode to the corresponding ANSI protocol mode.
Moves the cursor to the specified position.
Polls for input events with the specified timeout.
Re-queries terminal dimensions and updates state.
Shows the terminal cursor.
Shuts down the backend and restores terminal state.
Returns the current terminal dimensions.
Checks if a position is valid within the terminal bounds.
Types
@type mouse_mode() :: :none | :click | :drag | :all
Mouse tracking mode for the terminal.
These are user-friendly names that map to ANSI protocol modes internally:
:none- No mouse tracking (disabled):click- Track button press/release only (ANSI "normal" mode, 1000):drag- Track clicks and motion while button pressed (ANSI "button" mode, 1002):all- Track all mouse movement (ANSI "any" mode, 1003)
See the "Mouse Tracking Modes" section in the module documentation for details.
@type style_state() :: %{ fg: TermUI.Backend.color(), bg: TermUI.Backend.color(), attrs: [atom()] }
Current SGR (Select Graphic Rendition) style state.
Tracks the current foreground color, background color, and text attributes to enable style delta optimization - only emitting escape sequences for changed attributes.
Fields
:fg- Current foreground color (seeTermUI.Backend.color()):bg- Current background color (seeTermUI.Backend.color()):attrs- List of active text attributes::bold- Bold/bright text:dim- Dimmed text:italic- Italic text:underline- Underlined text:blink- Blinking text:reverse- Swapped foreground/background:hidden- Hidden text:strikethrough- Struck-through text
See the "Style Delta Optimization" section in the module documentation for how this enables efficient rendering.
@type t() :: %TermUI.Backend.Raw{ alternate_screen: boolean(), current_style: style_state() | nil, cursor_position: {pos_integer(), pos_integer()} | nil, cursor_visible: boolean(), event_queue: [TermUI.Backend.event()], events_dropped: non_neg_integer(), input_buffer: binary(), mouse_mode: mouse_mode(), optimize_cursor: boolean(), size: {pos_integer(), pos_integer()} }
Internal state for the Raw backend.
Tracks all terminal state needed for rendering and input handling.
Fields
:size- Terminal dimensions as{rows, cols}:cursor_visible- Whether cursor is currently visible (default:false):cursor_position- Current cursor position as{row, col}ornil:alternate_screen- Whether alternate screen buffer is active:mouse_mode- Current mouse tracking mode:current_style- Current SGR state for style delta tracking:optimize_cursor- Whether to use cursor movement optimization (default:true):input_buffer- Buffer for partial escape sequences during input parsing:event_queue- Queue of parsed events waiting to be returned
Functions
Clears the entire screen and moves cursor to home position.
Uses ANSI sequences:
ESC[2J- ED (Erase Display) parameter 2: clear entire screenESC[1;1H- CUP (Cursor Position): move to row 1, column 1
State Changes
After clear:
cursor_positionis set to{1, 1}(home position)current_styleis reset tonil(terminal style state is unknown after clear)
All other state fields are preserved.
Idempotency
This operation is idempotent - calling clear/1 multiple times in succession
is safe and will result in the same state each time.
Examples
{:ok, state} = Raw.init(size: {24, 80})
{:ok, moved} = Raw.move_cursor(state, {10, 20})
{:ok, cleared} = Raw.clear(moved)
cleared.cursor_position # => {1, 1}
cleared.current_style # => nilSee Also
move_cursor/2- Move cursor to specific positiondraw_cells/2- Draw content to screen
Disables mouse tracking.
Turns off mouse event reporting, returning the terminal to normal operation where mouse actions are not reported to the application.
Escape Sequences
This function emits:
- Disable SGR extended mode (
ESC[?1006l) - Disable the current tracking mode:
:click→ESC[?1000l:drag→ESC[?1002l:all→ESC[?1003l
Idempotent Behavior
If mouse tracking is already disabled (:none), no escape sequences are
written and the same state is returned.
Returns
{:ok, updated_state}withmouse_modeset to:none
Examples
# Disable after enabling
{:ok, state} = Raw.enable_mouse(state, :click)
{:ok, state} = Raw.disable_mouse(state)
state.mouse_mode # => :none
# Idempotent - safe to call when already disabled
{:ok, same_state} = Raw.disable_mouse(state)See Also
enable_mouse/2- Enable mouse trackingshutdown/1- Automatically disables mouse tracking during cleanup
@spec draw_cells(t(), [{TermUI.Backend.position(), TermUI.Backend.cell()}]) :: {:ok, t()}
Draws cells to the terminal at specified positions.
Cells are rendered with optimized cursor movement and style delta tracking to minimize escape sequence output. See the "Style Delta Optimization" section in the module documentation for details on how this works.
Cell Format
Each cell is a tuple {position, cell_data} where:
positionis{row, col}(1-indexed)cell_datais{char, fg, bg, attrs}
Performance
This function uses several optimizations:
- Style delta tracking (only emit changed attributes)
- Relative cursor movement when cheaper than absolute
- Batched I/O writes
Examples
# Draw a single red "A" at position {1, 1}
cells = [{{1, 1}, {"A", :red, :default, []}}]
{:ok, state} = Raw.draw_cells(state, cells)
# Draw multiple cells with different styles
cells = [
{{1, 1}, {"H", :green, :default, [:bold]}},
{{1, 2}, {"i", :green, :default, [:bold]}},
{{2, 1}, {"!", :yellow, :blue, []}}
]
{:ok, state} = Raw.draw_cells(state, cells)
Enables mouse tracking with the specified mode.
Changes the mouse tracking mode, enabling detection of mouse events. This function can be called after initialization to change the tracking mode.
Parameters
state- Current backend statemode- Mouse tracking mode::click- Track button press/release only (ANSI "normal" mode, 1000):drag- Track clicks and motion while button pressed (ANSI "button" mode, 1002):all- Track all mouse movement (ANSI "any" mode, 1003)
Escape Sequences
This function emits:
- The appropriate mouse tracking mode sequence:
:click→ESC[?1000h:drag→ESC[?1002h:all→ESC[?1003h
- SGR extended mode (
ESC[?1006h) for accurate coordinate encoding
Idempotent Behavior
If the requested mode matches the current mode, no escape sequences are written and the same state is returned.
Returns
{:ok, updated_state}withmouse_modeset to the new mode
Examples
# Enable click tracking
{:ok, state} = Raw.enable_mouse(state, :click)
# Enable all movement tracking
{:ok, state} = Raw.enable_mouse(state, :all)See Also
disable_mouse/1- Disable mouse trackinginit/1- Can set initial mouse tracking mode via:mouse_trackingoption
Flushes pending output to the terminal.
For the Raw backend, this is a no-op because IO.write/1 is synchronous -
output is written directly to the terminal without buffering. The callback
exists for API completeness and compatibility with backends that may use
buffered I/O.
This function is idempotent and safe to call multiple times.
Returns
{:ok, state}- Always succeeds, returning state unchanged
Hides the terminal cursor.
Uses ANSI sequence ESC[?25l (DECTCEM off).
Idempotent Behavior
This operation is idempotent. When the cursor is already hidden:
- No escape sequence is written to the terminal
- The exact same state object is returned unchanged
- Callers cannot distinguish a no-op from an actual state change
This design prevents redundant ANSI writes and allows callers to call without tracking current visibility state.
See Also
show_cursor/1- Show the cursormove_cursor/2- Move cursor to position
Initializes the Raw backend with terminal setup.
Assumes raw mode is already active (started by Selector). Performs terminal configuration including alternate screen, cursor hiding, and mouse tracking.
Options
:alternate_screen- Use alternate screen buffer (default:true):hide_cursor- Hide cursor during rendering (default:true):mouse_tracking- Mouse tracking mode (default::none):size- Explicit dimensions{rows, cols}(default: auto-detect):optimize_cursor- Use cursor movement optimization (default:true)
Returns
{:ok, state}on success{:error, :invalid_size}if size option is malformed{:error, :terminal_setup_failed}if terminal configuration fails{:error, :size_detection_failed}if auto-detect fails and no size provided
Examples
# Default initialization
{:ok, state} = Raw.init([])
# With explicit options
{:ok, state} = Raw.init(
alternate_screen: true,
hide_cursor: true,
mouse_tracking: :click,
size: {24, 80}
)
@spec mouse_mode_to_ansi(mouse_mode()) :: :normal | :button | :all | nil
Maps a Raw backend mouse mode to the corresponding ANSI protocol mode.
This is used internally when emitting mouse tracking escape sequences.
Examples
iex> Raw.mouse_mode_to_ansi(:click)
:normal
iex> Raw.mouse_mode_to_ansi(:drag)
:button
iex> Raw.mouse_mode_to_ansi(:all)
:all
@spec move_cursor(t(), TermUI.Backend.position()) :: {:ok, t()}
Moves the cursor to the specified position.
Position is 1-indexed: {1, 1} is the top-left corner.
Cursor Optimization
When optimize_cursor: true (default), this function uses CursorOptimizer
to select the cheapest movement sequence. This can reduce cursor movement
overhead by 40%+ compared to always using absolute positioning.
Movement options considered:
- Absolute positioning:
ESC[{row};{col}H(6-10 bytes) - Relative moves: up/down/left/right (3-6 bytes)
- Carriage return + vertical (1 + 3-6 bytes)
- Home position:
ESC[H(3 bytes) - Literal spaces for small rightward moves (1 byte each)
Position Validation
Positions must have positive integer coordinates. This function does NOT validate positions against terminal bounds - positions beyond the terminal dimensions are accepted and recorded in state. Most terminals silently clamp out-of-bounds positions, which may cause state-reality divergence.
Callers should validate positions before calling using valid_position?/2:
if Raw.valid_position?(state, position) do
Raw.move_cursor(state, position)
else
{:error, :out_of_bounds}
endThis design allows the renderer layer to handle bounds checking appropriately for its use case (e.g., scrolling, wrapping, or clamping).
See Also
hide_cursor/1- Hide cursor during renderingshow_cursor/1- Show cursor after renderingvalid_position?/2- Check if position is within terminal bounds
Examples
{:ok, state} = Raw.move_cursor(state, {1, 1}) # Top-left
{:ok, state} = Raw.move_cursor(state, {24, 80}) # Bottom-right (80x24)
@spec poll_event(t(), non_neg_integer()) :: {:ok, TermUI.Backend.event(), t()} | {:timeout, t()} | {:error, term(), t()}
Polls for input events with the specified timeout.
In raw mode, input arrives character-by-character enabling real-time
keyboard and mouse event handling. This function uses the EscapeParser
module to parse escape sequences into TermUI.Event structs.
Parameters
state- Current backend statetimeout- Milliseconds to wait (0 for non-blocking)
Returns
{:ok, event, state}- Event received and parsed{:timeout, state}- No input within timeout period{:error, reason, state}- Terminal I/O error occurred
Escape Sequence Handling
Some sequences are ambiguous (ESC alone vs ESC followed by another key). The function buffers partial sequences and uses the timeout to disambiguate. If the buffer contains a partial escape sequence and the timeout expires, the escape key is emitted and remaining bytes are re-parsed.
Examples
# Non-blocking poll (timeout = 0)
{:timeout, state} = Raw.poll_event(state, 0)
# Block up to 100ms for input
case Raw.poll_event(state, 100) do
{:ok, %Event.Key{key: :enter}, state} -> handle_enter(state)
{:ok, %Event.Mouse{action: :click}, state} -> handle_click(state)
{:timeout, state} -> handle_idle(state)
end
@spec refresh_size(t()) :: {:ok, TermUI.Backend.size(), t()} | {:error, :size_detection_failed}
Re-queries terminal dimensions and updates state.
This function queries the terminal for its current size using :io.rows/0
and :io.columns/0, then updates the cached size in state. It should be
called after receiving a SIGWINCH signal to handle terminal resize events.
Return Value
{:ok, {rows, cols}, updated_state}- New dimensions and updated state{:error, :size_detection_failed}- Failed to query terminal dimensions
SIGWINCH Handling
Terminal resize events are delivered via SIGWINCH. Your application should:
- Register a signal handler for SIGWINCH
- Call
refresh_size/1when the signal is received - Trigger a re-render with the new dimensions
Example integration:
def handle_info({:signal, :sigwinch}, state) do
case Raw.refresh_size(state.backend_state) do
{:ok, new_size, new_backend_state} ->
# Update state and trigger re-render
{:noreply, %{state | backend_state: new_backend_state, size: new_size}}
{:error, _reason} ->
# Keep existing size
{:noreply, state}
end
endSize Detection
Uses the same detection logic as init/1:
- Query
:io.rows/0and:io.columns/0 - Fall back to LINES and COLUMNS environment variables
- Return error if all methods fail
See Also
Shows the terminal cursor.
Uses ANSI sequence ESC[?25h (DECTCEM on).
Idempotent Behavior
This operation is idempotent. When the cursor is already visible:
- No escape sequence is written to the terminal
- The exact same state object is returned unchanged
- Callers cannot distinguish a no-op from an actual state change
This design prevents redundant ANSI writes and allows callers to call without tracking current visibility state.
See Also
hide_cursor/1- Hide the cursormove_cursor/2- Move cursor to position
@spec shutdown(t()) :: :ok
Shuts down the backend and restores terminal state.
Performs cleanup in order: disable mouse, show cursor, reset attributes, leave alternate screen, return to cooked mode.
Error Safety
This function is designed to be error-safe:
- Each cleanup step is wrapped in try/rescue
- Individual failures are logged but don't prevent subsequent steps
- Always returns
:okregardless of individual step failures - Idempotent: safe to call multiple times
Cleanup Sequence
- Disable mouse tracking (if enabled)
- Show cursor (ANSI:
ESC[?25h) - Reset all text attributes (ANSI:
ESC[0m) - Leave alternate screen (ANSI:
ESC[?1049l) - Return to cooked mode via
:shell.start_interactive({:noshell, :cooked})
@spec size(t()) :: {:ok, TermUI.Backend.size()}
Returns the current terminal dimensions.
Returns the cached size from state as {rows, cols}. This does not
re-query the terminal - it returns the dimensions captured at init/1
or last updated by refresh_size/1.
Return Value
{:ok, {rows, cols}}- Terminal dimensions (rows first, then columns)
Examples
{:ok, {24, 80}} = Raw.size(state) # Standard 80x24 terminal
{:ok, {50, 120}} = Raw.size(state) # Larger terminalSee Also
refresh_size/1- Re-query terminal dimensions (call after SIGWINCH)init/1- Initial size detection
Checks if a position is valid within the terminal bounds.
Returns true if the position has positive coordinates and is within
the terminal dimensions stored in state.
Examples
iex> state = %Raw{size: {24, 80}}
iex> Raw.valid_position?(state, {1, 1})
true
iex> Raw.valid_position?(state, {24, 80})
true
iex> Raw.valid_position?(state, {25, 1})
false
iex> Raw.valid_position?(state, {0, 1})
false