Events
View SourceTermUI delivers terminal input as structured events to your components. This guide covers all event types and how to handle them.
Event Types
Key Events
Keyboard input including regular characters, special keys, and modifier combinations.
%Event.Key{
key: :enter, # Atom for special keys, string for characters
char: nil, # Character string (nil for special keys)
modifiers: [:ctrl], # List of :ctrl, :alt, :shift
timestamp: 123456789 # Monotonic time in milliseconds
}Special Keys:
| Key | Atom |
|---|---|
| Enter | :enter |
| Escape | :escape |
| Tab | :tab |
| Backspace | :backspace |
| Delete | :delete |
| Insert | :insert |
| Home | :home |
| End | :end |
| Page Up | :page_up |
| Page Down | :page_down |
| Arrow Up | :up |
| Arrow Down | :down |
| Arrow Left | :left |
| Arrow Right | :right |
| F1-F12 | :f1 through :f12 |
Character Keys:
Regular characters are delivered as strings:
%Event.Key{key: "a", char: "a"} # Lowercase a
%Event.Key{key: "A", char: "A"} # Uppercase A (shift held)
%Event.Key{key: " ", char: " "} # Space
%Event.Key{key: "1", char: "1"} # Number 1Handling Key Events:
# Match special keys
def event_to_msg(%Event.Key{key: :enter}, _state), do: {:msg, :submit}
def event_to_msg(%Event.Key{key: :escape}, _state), do: {:msg, :cancel}
# Match characters (case-insensitive)
def event_to_msg(%Event.Key{key: key}, _state) when key in ["q", "Q"] do
{:msg, :quit}
end
# Match with modifiers
def event_to_msg(%Event.Key{key: "s", modifiers: [:ctrl]}, _state) do
{:msg, :save}
end
# Match any character for text input
def event_to_msg(%Event.Key{char: char}, state) when is_binary(char) do
{:msg, {:char_input, char}}
end
# Ignore unhandled keys
def event_to_msg(%Event.Key{}, _state), do: :ignoreMouse Events
Mouse clicks, movement, and scrolling.
%Event.Mouse{
action: :click, # :click, :double_click, :press, :release, :drag, :move
button: :left, # :left, :middle, :right, or nil
x: 10, # Column (0-indexed)
y: 5, # Row (0-indexed)
modifiers: [], # :ctrl, :alt, :shift
timestamp: 123456789
}Mouse Actions:
| Action | Description |
|---|---|
:press | Button pressed down |
:release | Button released |
:click | Press and release |
:double_click | Two clicks in quick succession |
:drag | Movement with button held |
:move | Movement without button |
:scroll_up | Scroll wheel up |
:scroll_down | Scroll wheel down |
Handling Mouse Events:
def event_to_msg(%Event.Mouse{action: :click, x: x, y: y}, _state) do
{:msg, {:click, x, y}}
end
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
def event_to_msg(%Event.Mouse{action: :drag, x: x, y: y}, _state) do
{:msg, {:drag, x, y}}
endMouse Tracking Modes:
Raw runtime startup enables all mouse tracking modes except on WSL/ConPTY. The TTY runtime does not enable mouse reporting. For a standalone Raw terminal integration, the low-level API is:
# Direct Raw setup (Runtime already owns this when using Raw)
Terminal.enable_mouse_tracking(:click) # Click events only
Terminal.enable_mouse_tracking(:drag) # Click and drag
Terminal.enable_mouse_tracking(:all) # All movementResize Events
Terminal window size changes.
%Event.Resize{
width: 120, # New column count
height: 40, # New row count
timestamp: 123456789
}Handling Resize:
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}, []}
endFocus Events
Terminal window focus changes.
%Event.Focus{
action: :gained, # :gained or :lost
timestamp: 123456789
}Handling 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 animations, save state, etc.
{%{state | paused: true}, []}
endPaste Events
Text pasted from clipboard when the terminal/host has bracketed paste mode enabled. The 1.0 runtime parser supports these sequences but does not enable the reporting mode automatically.
%Event.Paste{
content: "pasted text",
timestamp: 123456789
}Handling Paste:
def event_to_msg(%Event.Paste{content: text}, _state) do
{:msg, {:paste, text}}
end
def update({:paste, text}, state) do
{%{state | input: state.input <> text}, []}
endTick Events
Host-created timing metadata events.
%Event.Tick{
interval: 1000, # Interval in milliseconds
timestamp: 123456789
}Event.Tick can be constructed and injected by a host. Command.timer/2 and
Command.interval/2 deliver their configured messages directly; they do not
create %Event.Tick{} values.
Custom Events
Application-defined events.
%Event.Custom{
name: :data_loaded,
payload: %{items: [...]},
timestamp: 123456789
}Event Handling Patterns
Catch-All Handler
Always include a catch-all to handle unexpected events:
def event_to_msg(_, _state), do: :ignoreConditional Handling
Handle events differently based on state:
def event_to_msg(%Event.Key{key: :enter}, %{mode: :edit}) do
{:msg, :confirm_edit}
end
def event_to_msg(%Event.Key{key: :enter}, %{mode: :view}) do
{:msg, :start_edit}
endKey Sequences
Track key sequences for shortcuts:
def init(_opts) do
%{key_buffer: []}
end
def event_to_msg(%Event.Key{key: "g"}, %{key_buffer: ["g"]}) do
{:msg, :go_to_top} # gg command
end
def event_to_msg(%Event.Key{key: key}, _state) when is_binary(key) do
{:msg, {:key_pressed, key}}
end
def update({:key_pressed, key}, state) do
buffer = [key | state.key_buffer] |> Enum.take(2)
{%{state | key_buffer: buffer}, [Command.timer(500, :clear_buffer)]}
end
def update(:clear_buffer, state) do
{%{state | key_buffer: []}, []}
endModal Input
Different handling for different modes:
def event_to_msg(event, %{mode: :normal} = state) do
handle_normal_mode(event, state)
end
def event_to_msg(event, %{mode: :insert} = state) do
handle_insert_mode(event, state)
end
defp handle_normal_mode(%Event.Key{key: "i"}, _state), do: {:msg, :enter_insert}
defp handle_normal_mode(%Event.Key{key: "j"}, _state), do: {:msg, :move_down}
defp handle_normal_mode(%Event.Key{key: "k"}, _state), do: {:msg, :move_up}
defp handle_normal_mode(_, _), do: :ignore
defp handle_insert_mode(%Event.Key{key: :escape}, _state), do: {:msg, :exit_insert}
defp handle_insert_mode(%Event.Key{char: char}, _state) when is_binary(char) do
{:msg, {:insert_char, char}}
end
defp handle_insert_mode(_, _), do: :ignoreEvent Constructors
Create events programmatically (useful for testing):
# Key events
Event.key(:enter)
Event.key("a")
Event.key("s", modifiers: [:ctrl])
# Mouse events
Event.mouse(:click, :left, 10, 5)
Event.mouse(:scroll_up, nil, 10, 5)
# Other events
Event.Resize.new(120, 40)
Event.Focus.new(:gained)
Event.Paste.new("text")Testing Events
defmodule MyApp.ComponentTest do
use ExUnit.Case
alias TermUI.Event
test "enter key submits form" do
state = %{input: "test"}
event = Event.key(:enter)
assert {:msg, :submit} = MyApp.Component.event_to_msg(event, state)
end
test "ctrl+s saves" do
event = Event.key("s", modifiers: [:ctrl])
assert {:msg, :save} = MyApp.Component.event_to_msg(event, %{})
end
test "click selects item" do
event = Event.mouse(:click, :left, 5, 10)
assert {:msg, {:select, 5, 10}} = MyApp.Component.event_to_msg(event, %{})
end
end