Getting Started
View SourceThis 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"}
]
endThen 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:
- Selects TTY directly when IEx is detected, preserving shell ownership
- Otherwise attempts Raw on a supported Unix/OTP combination
- 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?
| Scenario | Recommended Mode |
|---|---|
| Production application | Raw (auto-detected) |
| Development in IEx | TTY (auto-detected) |
| Testing/Debugging | TTY for IEx convenience |
| Local shell reached over SSH | Auto (Raw or TTY according to ownership) |
| Independent OTP SSH channel | Explicit 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
endStep 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
endStep 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
init/1- Called once when the component starts. Returns initial state.event_to_msg/2- Converts terminal events to application messages. Return values:{:msg, message}- Send message toupdate/2:ignore- Discard the event:propagate- Leave unhandled; the single-root 1.0 runtime has no parent, so it is currently discarded
update/2- Handles messages and returns{new_state, commands}. Commands are side effects like timers or quit requests.view/1- Returns a render tree describing what to display.
Render Tree Primitives
text(string)- Plain texttext(string, style)- Styled textstack(:vertical, children)- Vertical layoutstack(: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),
# ...
])
endReset Functionality
Add to event_to_msg/2:
def event_to_msg(%Event.Key{key: key}, _state) when key in ["r", "R"] do
{:msg, :reset}
endAdd to update/2:
def update(:reset, state) do
{%{state | count: 0}, []}
endUsing 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}")
])
endRunning 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
- The Elm Architecture - Learn the pattern in depth
- Events - Handle all types of input
- Styling - Make your app visually appealing
- Widgets - Use pre-built components