Getting Started

View Source

This guide walks you through creating your first TermUI application.

Installation

Add TermUI to your dependencies in mix.exs:

def deps do
  [
    {:term_ui, "~> 1.0"}
  ]
end

Then fetch dependencies:

mix deps.get

Understanding Backends: Raw vs TTY

TermUI automatically selects between two local terminal backends based on your environment. Independent OTP SSH channels use the explicit SSH backend described below.

Raw Mode (Full TUI Experience)

Raw mode provides complete terminal control:

  • Alternate screen buffer - Preserves your shell history
  • Character-by-character input - No line buffering
  • Mouse support - Click, drag, and scroll events, except under WSL/ConPTY
  • Live UI updates - Dirty rendering capped at roughly 60 FPS

When it's used:

  • Running from command line (mix run, mix termui.run)
  • Terminal supports raw mode (OTP 28+)
  • No other shell is running

TTY Mode (IEx Compatible)

TTY mode works inside IEx and other constrained environments:

  • Alternate screen - The runtime enables it, just as it does in Raw mode
  • Shell-compatible input - Uses a dedicated reader without replacing the active IEx shell; some terminals buffer input until Enter
  • Reduced feature set - Mouse support may be limited
  • Works in IEx - Perfect for development and debugging

When it's used:

  • Running inside IEx
  • A shell is already running
  • Raw mode activation fails

Automatic Backend Selection

TermUI automatically selects the appropriate local backend:

  1. Selects TTY directly when IEx is detected, preserving shell ownership
  2. Otherwise attempts Raw on a supported Unix/OTP combination
  3. Falls back to TTY when Raw mode is unavailable

You can also force a specific mode:

# Force raw mode
TermUI.Runtime.run(root: MyApp.Counter, backend: :raw)

# Force TTY mode
TermUI.Runtime.run(root: MyApp.Counter, backend: :tty)

Which Should You Use?

ScenarioRecommended Mode
Production applicationRaw (auto-detected)
Development in IExTTY (auto-detected)
Testing/DebuggingTTY for IEx convenience
Local shell reached over SSHAuto (Raw or TTY according to ownership)
Independent OTP SSH channelExplicit TermUI.Backend.SSH

The same root component runs in both local modes. Account for cooked TTY input possibly arriving only after Enter and for mouse reporting not being enabled in TTY mode.

Your First Application

Let's build a simple counter that responds to keyboard input.

Step 1: Create the Component

Create lib/my_app/counter.ex:

defmodule MyApp.Counter do
  @moduledoc """
  A simple counter component demonstrating TermUI basics.
  """

  use TermUI.Elm

  alias TermUI.Event
  alias TermUI.Renderer.Style

  # Initialize state
  def init(_opts) do
    %{count: 0}
  end

  # Convert events to messages
  def event_to_msg(%Event.Key{key: key}, _state) when key in ["q", "Q"] do
    {:msg, :quit}
  end

  def event_to_msg(%Event.Key{key: :up}, _state), do: {:msg, :increment}
  def event_to_msg(%Event.Key{key: :down}, _state), do: {:msg, :decrement}
  def event_to_msg(_, _state), do: :ignore

  # Update state based on messages
  def update(:quit, state) do
    {state, [TermUI.Command.quit()]}
  end

  def update(:increment, state) do
    {%{state | count: state.count + 1}, []}
  end

  def update(:decrement, state) do
    {%{state | count: state.count - 1}, []}
  end

  # Render the view
  def view(state) do
    stack(:vertical, [
      text("Simple Counter", Style.new(fg: :cyan, attrs: [:bold])),
      text(""),
      text("Count: #{state.count}", Style.new(fg: :white)),
      text(""),
      text("[↑] Increment  [↓] Decrement  [Q] Quit", Style.new(fg: :bright_black))
    ])
  end
end

Step 2: Create the Entry Point

Create lib/my_app.ex:

defmodule MyApp do
  @moduledoc """
  Entry point for the counter application.
  """

  def run do
    TermUI.Runtime.run(root: MyApp.Counter)
  end

  def start do
    TermUI.Runtime.start_link(root: MyApp.Counter)
  end
end

Step 3: Run the Application

mix termui.run

The mix termui.run command will automatically discover and run your root module (MyApp in this case).

You should see your counter application. Press to increment, to decrement, and Q to quit.

Understanding the Code

The use TermUI.Elm Macro

This sets up your module as an Elm Architecture component, importing necessary functions like text/1, text/2, and stack/2.

The Four Callbacks

  1. init/1 - Called once when the component starts. Returns initial state.

  2. event_to_msg/2 - Converts terminal events to application messages. Return values:

    • {:msg, message} - Send message to update/2
    • :ignore - Discard the event
    • :propagate - Leave unhandled; the single-root 1.0 runtime has no parent, so it is currently discarded
  3. update/2 - Handles messages and returns {new_state, commands}. Commands are side effects like timers or quit requests.

  4. view/1 - Returns a render tree describing what to display.

Render Tree Primitives

  • text(string) - Plain text
  • text(string, style) - Styled text
  • stack(:vertical, children) - Vertical layout
  • stack(:horizontal, children) - Horizontal layout

Adding More Features

Color Based on Value

def view(state) do
  count_style = cond do
    state.count > 0 -> Style.new(fg: :green)
    state.count < 0 -> Style.new(fg: :red)
    true -> Style.new(fg: :white)
  end

  stack(:vertical, [
    text("Count: #{state.count}", count_style),
    # ...
  ])
end

Reset Functionality

Add to event_to_msg/2:

def event_to_msg(%Event.Key{key: key}, _state) when key in ["r", "R"] do
  {:msg, :reset}
end

Add to update/2:

def update(:reset, state) do
  {%{state | count: 0}, []}
end

Using Widgets

alias TermUI.Widgets.Gauge

def view(state) do
  # Normalize count to 0-100 range for gauge
  gauge_value = max(0, min(100, state.count + 50))

  stack(:vertical, [
    text("Counter with Gauge"),
    text(""),
    Gauge.render(value: gauge_value, width: 30),
    text(""),
    text("Count: #{state.count}")
  ])
end

Running in IEx

For development and debugging, you can run your app in IEx using TTY mode:

iex -S mix

Then in IEx:

iex> MyApp.run()

The app will run in TTY mode, which:

  • Works inside IEx without taking over the shell completely
  • Supports the same normalized key events after the shell delivers them; some cooked-mode terminals require Enter
  • Uses the runtime-managed alternate screen and restores it on exit

For immediate character input, automatic Raw mouse setup, and differential output, run from the command line instead:

mix termui.run

or

mix run -e "MyApp.run()" --no-halt

Next Steps