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:
Mouse events require enabling mouse tracking:
# In Terminal setup (done automatically by Runtime)
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 (with bracketed paste mode).
%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
Timer-based periodic events.
%Event.Tick{
interval: 1000, # Interval in milliseconds
timestamp: 123456789
}These are typically generated by commands rather than received directly.
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