Terminal

View Source

TermUI manages low-level terminal operations automatically, but understanding these features helps you build better applications.

Terminal Modes

Cooked Mode (Default)

Normal terminal operation:

  • Line buffering (input sent on Enter)
  • Character echoing
  • Signal handling (Ctrl+C sends SIGINT)

Raw Mode

TermUI's operating mode:

  • Character-by-character input
  • No echoing
  • No signal handling
  • Full control over display

The runtime enables raw mode automatically. It's restored when your app exits.

Alternate Screen

Terminals have two screen buffers:

  • Main screen - The normal scrollback buffer
  • Alternate screen - A separate buffer for full-screen apps

TermUI uses the alternate screen, preserving the user's shell history. When your app exits, the terminal returns to the main screen with history intact.

     
 $ ls                       
 file1.txt                   Your TermUI     
 file2.txt            -->    Application     
 $ my_app                                    
                            
  Main Screen               Alternate Screen   
     
                                     
                                      (exit)
                                     
                            
                             $ ls                
                             file1.txt           
                             file2.txt           
                             $ my_app            
                             $                   
                              Back to Main       
                            

Mouse Tracking

TermUI can capture mouse events.

Tracking Modes

ModeEvents Captured
:clickButton press/release
:dragClick + drag movements
:allAll mouse movement

The runtime enables click tracking by default.

Mouse Coordinates

Mouse positions are 0-indexed:

  • x = column (0 = leftmost)
  • y = row (0 = topmost)
def event_to_msg(%Event.Mouse{action: :click, x: x, y: y}, state) do
  # Check if click is within a region
  if x >= 10 and x < 30 and y >= 5 and y < 10 do
    {:msg, :button_clicked}
  else
    :ignore
  end
end

Scroll Events

Mouse wheel generates scroll events:

def event_to_msg(%Event.Mouse{action: :scroll_up}, _state) do
  {:msg, :scroll_up}
end

def event_to_msg(%Event.Mouse{action: :scroll_down}, _state) do
  {:msg, :scroll_down}
end

Focus Events

Know when the terminal window gains or loses focus:

def event_to_msg(%Event.Focus{action: :gained}, _state) do
  {:msg, :focus_gained}
end

def event_to_msg(%Event.Focus{action: :lost}, _state) do
  {:msg, :focus_lost}
end

def update(:focus_lost, state) do
  # Pause updates, dim display, etc.
  {%{state | paused: true}, []}
end

def update(:focus_gained, state) do
  # Resume updates
  {%{state | paused: false}, []}
end

Note: Focus events require terminal support. They work on most modern terminals (xterm, iTerm2, Alacritty, Kitty, Windows Terminal).

Terminal Size

Getting Size

Query current dimensions:

{:ok, {rows, cols}} = TermUI.Terminal.get_terminal_size()

Handling Resize

Respond to window size changes:

def event_to_msg(%Event.Resize{width: w, height: h}, _state) do
  {:msg, {:resize, w, h}}
end

def update({:resize, width, height}, state) do
  {%{state | width: width, height: height}, []}
end

def view(state) do
  if state.width < 80 do
    render_compact_layout(state)
  else
    render_full_layout(state)
  end
end

Cursor Control

The runtime manages cursor visibility and position. The cursor is hidden during normal operation to avoid flicker.

For text input widgets that need a visible cursor:

# The cursor position is managed by the renderer
# Your TextInput widget indicates where the cursor should be
TextInput.render(
  value: state.text,
  cursor_position: state.cursor_pos,
  focused: true  # Shows cursor
)

Color Support

Detection

TermUI detects terminal color capabilities:

  • 16 colors (basic)
  • 256 colors (extended)
  • True color (24-bit RGB)

Graceful Degradation

Use named colors for maximum compatibility:

# Works everywhere
Style.new(fg: :red)

# Requires 256-color support
Style.new(fg: 196)

# Requires true color support
Style.new(fg: {255, 100, 50})

The renderer automatically degrades colors for less capable terminals.

Clipboard

Paste Events

Bracketed paste mode delivers pasted text as a single event:

def event_to_msg(%Event.Paste{content: text}, _state) do
  {:msg, {:paste, text}}
end

def update({:paste, text}, state) do
  # Insert pasted text at cursor
  new_text = state.text <> text
  {%{state | text: new_text}, []}
end

Without bracketed paste, pasted text would arrive as individual key events, which is slower and may trigger unintended shortcuts.

Terminal Requirements

Minimum Requirements

  • ANSI escape sequence support
  • UTF-8 encoding
  • 80x24 minimum size
  • 256-color or true color support
  • Mouse tracking support
  • Focus event support
  • Unicode box drawing characters

Supported Terminals

Tested and working:

TerminalPlatformNotes
AlacrittyCross-platformFull support
KittyLinux/macOSFull support
iTerm2macOSFull support
WezTermCross-platformFull support
GNOME TerminalLinuxFull support
Windows TerminalWindowsFull support
Terminal.appmacOSLimited mouse
xtermCross-platformFull support

SSH Sessions

TermUI works over SSH when the remote terminal supports required features. The runtime detects terminal capabilities through multiple methods to ensure SSH compatibility.

Error Handling

Terminal Not Available

Handle cases where no terminal is present:

case TermUI.Runtime.start_link(root: MyApp) do
  {:ok, pid} ->
    # Running normally
    pid

  {:error, :not_a_terminal} ->
    IO.puts("Error: Must run in a terminal")
    System.halt(1)
end

Cleanup on Crash

The runtime traps exits and restores terminal state even if your app crashes:

# In Runtime.init/1
Process.flag(:trap_exit, true)

# In Runtime.terminate/2
Terminal.restore()  # Always runs

This ensures users don't get stuck in raw mode with no echo.

Direct Terminal Access

For advanced use cases, access terminal functions directly:

alias TermUI.Terminal

# These are managed by Runtime, but available if needed:
Terminal.enable_raw_mode()
Terminal.disable_raw_mode()
Terminal.enter_alternate_screen()
Terminal.leave_alternate_screen()
Terminal.show_cursor()
Terminal.hide_cursor()
Terminal.clear_screen()
Terminal.set_cursor_position(row, col)

Warning: Direct terminal access can interfere with the runtime. Use only when necessary.

Next Steps