TermUI

Hex.pmDocsLicense

A direct-mode Terminal UI framework for Elixir/BEAM, inspired by BubbleTea (Go) and Ratatui (Rust).

TermUI combines The Elm Architecture with the BEAM process model and supervision primitives to build robust terminal applications.

Blue Theme    Yellow Theme

Features

Platform support

TermUI 1.0's local Raw and TTY backends target Unix-style terminals and are supported on Linux and macOS. The SSH backend renders independent sessions to OTP SSH channel devices.

Native Windows console support is experimental. ANSI output can work in a terminal where virtual-terminal processing is already enabled, but TermUI does not yet configure Win32 console modes or provide native raw input and resize handling. On Windows, prefer WSL and verify keyboard, resize, paste, and cleanup behavior for the terminal you deploy with. Mouse tracking is intentionally disabled under WSL/ConPTY because disabling sequences are not handled reliably.

OTP 26's signal API does not expose SIGWINCH to application handlers. The minimum-version TTY backend still supports size queries, but automatic local resize events require a newer OTP; Raw mode already requires OTP 28 or later.

IEx Compatibility

TermUI applications work directly in IEx with no code changes. This is perfect for:

Running in IEx

# In your IEx session
iex> TermUI.Runtime.run(root: MyApp.Counter)
# Use arrow keys, press Q to quit, returns to IEx prompt

How It Works

TermUI keeps the active IEx shell in cooked mode and reads through its IO server from a dedicated input process. This avoids replacing the shell while still allowing TermUI to parse navigation keys and return cleanly to the IEx prompt. Depending on the shell and terminal driver, cooked input may be delivered immediately or buffered until Enter is pressed.

Detection and Configuration

You can detect if your application is running in IEx:

iex> TermUI.iex_mode?()
true
iex> TermUI.running_mode()
:iex

Force IEx-compatible mode via configuration:

# config/config.exs
config :term_ui,
iex_compatible: true

Or via environment variable:

export TERM_UI_IEX_MODE=true

Important Notes

Widgets

WidgetDescription
GaugeProgress bar with color zones
SparklineCompact inline trend graph
TableScrollable data table with selection and sorting
MenuHierarchical menu with submenus
TextInputSingle-line and multi-line text input
DialogModal dialog with buttons
PickListModal selection with type-ahead filtering
TabsTabbed interface for switchable panels
AlertDialogModal dialog for confirmations with standard button configurations
ContextMenuRight-click context menu with keyboard and mouse support
ToastTick-driven notifications with stacking and dismissal
ViewportScrollable view with keyboard and mouse support
SplitPaneResizable multi-pane layouts for IDE-style interfaces
TreeViewHierarchical data display with expand/collapse
FormBuilderStructured forms with validation and multiple field types
CommandPaletteSearchable command discovery with substring filtering
BarChartHorizontal/vertical bar charts for categorical data
LineChartLine charts using Braille characters for sub-character resolution
CanvasDirect drawing surface for custom visualizations
LogViewerHigh-performance log viewer with virtual scrolling and filtering
StreamWidgetBounded stream widget with a GenStage consumer adapter
ProcessMonitorLive BEAM process inspection with sorting and filtering
SupervisionTreeViewerOTP supervision hierarchy visualization
ClusterDashboardDistributed Erlang cluster monitoring

Installation

Add term_ui to your dependencies in mix.exs:

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

Quick Start

defmodule Counter do
use TermUI.Elm
alias TermUI.Event
alias TermUI.Renderer.Style
def init(_opts), do: %{count: 0}
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(%Event.Key{key: "q"}, _state), do: {:msg, :quit}
def event_to_msg(_, _), do: :ignore
def update(:increment, state), do: {%{state | count: state.count + 1}, []}
def update(:decrement, state), do: {%{state | count: state.count - 1}, []}
def update(:quit, state), do: {state, [TermUI.Command.quit()]}
def view(state) do
stack(:vertical, [
text("Counter Example", Style.new(fg: :cyan, attrs: [:bold])),
text("", nil),
text("Count: #{state.count}", nil),
text("", nil),
text("↑/↓ to change, Q to quit", Style.new(fg: :bright_black))
])
end
end
# Run the application
TermUI.Runtime.run(root: Counter)

Documentation

User Guides

GuideDescription
OverviewIntroduction to TermUI concepts
Getting StartedFirst steps and setup
Elm ArchitectureUnderstanding init/update/view
EventsHandling keyboard and mouse input
StylingColors, attributes, and themes
LayoutArranging components on screen
WidgetsUsing built-in widgets
TerminalTerminal capabilities and modes
CommandsSide effects and async operations
Advanced WidgetsNavigation, visualization, streaming, and BEAM introspection widgets

Developer Guides

GuideDescription
Architecture OverviewSystem layers and design
Runtime InternalsGenServer event loop and state
Rendering PipelineView to terminal output stages
Event SystemInput parsing and dispatch
Buffer ManagementETS double buffering
Terminal LayerRaw mode and ANSI sequences
Elm ImplementationElm Architecture for OTP
Creating WidgetsHow to build and contribute widgets
Testing FrameworkComponent and widget testing

Examples

The examples/ directory contains standalone applications demonstrating each widget:

ExampleDescription
alert_dialogConfirmation dialogs with standard buttons
bar_chartHorizontal and vertical bar charts
canvasFree-form drawing with box/braille characters
cluster_dashboardDistributed Erlang cluster monitoring
command_paletteVS Code-style command discovery
context_menuRight-click context menus
dashboardSystem monitoring dashboard with multiple widgets
dialogModal dialogs with buttons
form_builderStructured forms with validation
gaugeProgress bars and percentage indicators
iex_counterMinimal counter designed for IEx/TTY mode
line_chartBraille-based line charts
log_viewerReal-time log display with filtering
markdown_viewerScrollable Markdown rendering
menuNested menus with keyboard navigation
multi_rendererBackend selection and capability degradation
pick_listModal selection with type-ahead
process_monitorLive BEAM process inspection
sparklineInline data visualization
split_paneResizable multi-pane layouts
stream_widgetGenStage consumer integration with a bounded display buffer
supervision_tree_viewerOTP supervision hierarchy
tableScrollable data tables with selection
tabsTab-based navigation
text_inputSingle and multi-line text input
toastTick-driven notification dismissal
tree_viewHierarchical data with expand/collapse
viewportScrollable content areas
# Run any example
cd examples/dashboard
mix deps.get
mix termui.run

Requirements

License

MIT License - see LICENSE for details.