Current section
Files
Jump to
Current section
Files
guides/user/04-events.md
# Events
TermUI 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.
```elixir
%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:
```elixir
%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 1
```
**Handling Key Events:**
```elixir
# 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: :ignore
```
### Mouse Events
Mouse clicks, movement, and scrolling.
```elixir
%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:**
```elixir
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}}
end
```
**Mouse Tracking Modes:**
Mouse events require enabling mouse tracking:
```elixir
# 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 movement
```
### Resize Events
Terminal window size changes.
```elixir
%Event.Resize{
width: 120, # New column count
height: 40, # New row count
timestamp: 123456789
}
```
**Handling Resize:**
```elixir
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}, []}
end
```
### Focus Events
Terminal window focus changes.
```elixir
%Event.Focus{
action: :gained, # :gained or :lost
timestamp: 123456789
}
```
**Handling Focus:**
```elixir
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}, []}
end
```
### Paste Events
Text pasted from clipboard (with bracketed paste mode).
```elixir
%Event.Paste{
content: "pasted text",
timestamp: 123456789
}
```
**Handling Paste:**
```elixir
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}, []}
end
```
### Tick Events
Timer-based periodic events.
```elixir
%Event.Tick{
interval: 1000, # Interval in milliseconds
timestamp: 123456789
}
```
These are typically generated by commands rather than received directly.
### Custom Events
Application-defined events.
```elixir
%Event.Custom{
name: :data_loaded,
payload: %{items: [...]},
timestamp: 123456789
}
```
## Event Handling Patterns
### Catch-All Handler
Always include a catch-all to handle unexpected events:
```elixir
def event_to_msg(_, _state), do: :ignore
```
### Conditional Handling
Handle events differently based on state:
```elixir
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}
end
```
### Key Sequences
Track key sequences for shortcuts:
```elixir
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: []}, []}
end
```
### Modal Input
Different handling for different modes:
```elixir
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: :ignore
```
## Event Constructors
Create events programmatically (useful for testing):
```elixir
# 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
```elixir
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
```
## Next Steps
- [Styling](05-styling.md) - Visual styling and themes
- [Commands](09-commands.md) - Timers and side effects
- [Terminal](08-terminal.md) - Low-level terminal control