ExRatatui.Focus (ExRatatui v0.8.2)

Copy Markdown View Source

Focus management for multi-panel apps.

Focus is a tiny state machine over an ordered ring of focusable IDs. You declare the IDs up front, feed every key event through handle_key/2, and pattern-match on current/1 to decide which widget receives the keystroke. handle_key/2 consumes Tab / Shift+Tab (or your overrides) and passes everything else through unchanged.

There is no process, no macro, no protocol — just a struct you keep in your reducer state or ExRatatui.App model.

Caller pattern

def handle_event(%Event.Key{} = key, state) do
  {focus, key} = Focus.handle_key(state.focus, key)
  state = %{state | focus: focus}

  case key do
    nil ->
      # consumed by Focus (Tab / Shift+Tab); nothing more to do
      state

    key ->
      case Focus.current(focus) do
        :search  -> update_search(state, key)
        :results -> update_results(state, key)
        :details -> update_details(state, key)
      end
  end
end

Styling the focused widget

Focus never touches widget structs. Use focused?/2 to decide the style yourself:

border_style =
  if Focus.focused?(focus, :search),
    do: %Style{fg: :yellow},
    else: %Style{fg: :gray}

%TextInput{
  state: search_state,
  block: %Block{borders: :all, border_style: border_style}
}

Custom keys

Pass :next_keys / :prev_keys to new/2 as lists of %ExRatatui.Event.Key{} structs. Only :code and :modifiers matter — :kind is ignored, and :modifiers is compared as a set (order-independent).

Focus.new([:a, :b, :c],
  next_keys: [%Event.Key{code: "tab"}, %Event.Key{code: "right", modifiers: ["ctrl"]}],
  prev_keys: [%Event.Key{code: "left", modifiers: ["ctrl"]}]
)

Summary

Functions

Returns the currently focused ID.

Jumps focus to a specific ID.

Returns true when id is the currently focused ID.

Routes a key event through the focus ring.

Builds a focus ring from an ordered list of IDs.

Advances focus to the next ID, wrapping from the last back to the first.

Retreats focus to the previous ID, wrapping from the first back to the last.

Types

id()

@type id() :: atom()

t()

@type t() :: %ExRatatui.Focus{
  ids: [id(), ...],
  index: non_neg_integer(),
  next_keys: [ExRatatui.Event.Key.t()],
  prev_keys: [ExRatatui.Event.Key.t()]
}

Functions

current(focus)

@spec current(t()) :: id()

Returns the currently focused ID.

Examples

iex> ExRatatui.Focus.new([:a, :b, :c]) |> ExRatatui.Focus.current()
:a

iex> ExRatatui.Focus.new([:a, :b, :c], initial: :b) |> ExRatatui.Focus.current()
:b

focus(focus, id)

@spec focus(t(), id()) :: t()

Jumps focus to a specific ID.

Raises ArgumentError if id is not in the ring.

focused?(focus, id)

@spec focused?(t(), id()) :: boolean()

Returns true when id is the currently focused ID.

Examples

iex> focus = ExRatatui.Focus.new([:a, :b, :c])
iex> ExRatatui.Focus.focused?(focus, :a)
true
iex> ExRatatui.Focus.focused?(focus, :b)
false

handle_key(focus, event)

@spec handle_key(t(), ExRatatui.Event.Key.t()) :: {t(), ExRatatui.Event.Key.t() | nil}

Routes a key event through the focus ring.

Returns {focus, nil} when the event matched a :next_keys or :prev_keys entry (focus moved, event consumed). Returns {focus, event} unchanged otherwise so the caller can forward it to the currently focused widget.

Matching compares :code and :modifiers (as a set). :kind is ignored.

new(ids, opts \\ [])

@spec new(
  [id(), ...],
  keyword()
) :: t()

Builds a focus ring from an ordered list of IDs.

Options

  • :initial — ID to start focused on (defaults to the first entry).
  • :next_keys — list of %ExRatatui.Event.Key{} that advance focus (defaults to Tab).
  • :prev_keys — list of %ExRatatui.Event.Key{} that retreat focus (defaults to Shift+Tab and back_tab).

Raises ArgumentError for an empty list, duplicate IDs, non-atom entries, or an :initial that is not in ids.

next(focus)

@spec next(t()) :: t()

Advances focus to the next ID, wrapping from the last back to the first.

prev(focus)

@spec prev(t()) :: t()

Retreats focus to the previous ID, wrapping from the first back to the last.