# `Minga.Keymap.Bindings`
[🔗](https://github.com/jsmestad/minga/blob/main/lib/minga/keymap/bindings.ex#L1)

Prefix tree (trie) for key sequence → command bindings.

Each node in the trie can represent either an intermediate step in a
multi-key sequence (a prefix node) or a terminal binding (a command node).
Nodes may simultaneously be a prefix and a command (e.g. `g` could be a
command and also prefix for `gg`).

## Key representation

A key is a `{codepoint, modifiers}` tuple where `codepoint` is the Unicode
codepoint of the key and `modifiers` is a bitmask of modifier keys:

* `0x01` — Shift
* `0x02` — Ctrl
* `0x04` — Alt
* `0x08` — Super

## Usage

    trie = Minga.Keymap.Bindings.new()
    trie = Minga.Keymap.Bindings.bind(trie, [{?j, 0}], :move_down, "Move cursor down")
    trie = Minga.Keymap.Bindings.bind(trie, [{?g, 0}, {?g, 0}], :file_start, "Go to first line")

    {:command, :move_down} = Minga.Keymap.Bindings.lookup(trie, {?j, 0})
    {:prefix, node}        = Minga.Keymap.Bindings.lookup(trie, {?g, 0})

# `key`

```elixir
@type key() :: {codepoint :: non_neg_integer(), modifiers :: non_neg_integer()}
```

A single key event: `{codepoint, modifiers}`.

`codepoint` is the Unicode codepoint (e.g. `?j` = 106).
`modifiers` is a bitmask: Shift=0x01, Ctrl=0x02, Alt=0x04, Super=0x08.

# `node_t`

```elixir
@type node_t() :: Minga.Keymap.Bindings.Node.t()
```

A trie node.

# `bind`

```elixir
@spec bind(node_t(), [key()], atom() | tuple(), String.t()) :: node_t()
```

Binds a key sequence to a command in the trie.

Returns an updated trie root. Intermediate nodes are created as needed.
Rebinding an existing sequence overwrites the previous binding.

## Parameters

* `root`        — the trie root node
* `keys`        — non-empty list of `t:key/0` values representing the sequence
* `command`     — atom name of the command to bind
* `description` — human-readable description for which-key display

## Examples

    iex> trie = Minga.Keymap.Bindings.new()
    iex> trie = Minga.Keymap.Bindings.bind(trie, [{?j, 0}], :move_down, "Move cursor down")
    iex> Minga.Keymap.Bindings.lookup(trie, {?j, 0})
    {:command, :move_down}
    iex> Minga.Keymap.Bindings.lookup(trie, {?k, 0})
    :not_found

# `bind_prefix`

```elixir
@spec bind_prefix(node_t(), [key()], String.t()) :: node_t()
```

Sets a human-readable description on an intermediate (prefix) node without
binding a command. Useful for labelling leader-key groups like `f → "+file"`.

Creates intermediate nodes as needed.

# `children`

```elixir
@spec children(node_t()) :: [{key(), String.t() | atom()}]
```

Returns the direct children of a trie node for which-key display.

Each entry is a `{key, label}` tuple where `label` is either the
description string (for a terminal binding) or the command atom (for a
prefix or unnamed node).

## Examples

    iex> trie = Minga.Keymap.Bindings.new()
    iex> trie = Minga.Keymap.Bindings.bind(trie, [{?j, 0}], :move_down, "Move cursor down")
    iex> Minga.Keymap.Bindings.children(trie)
    [{{106, 0}, "Move cursor down"}]

# `format_key`

```elixir
@spec format_key(key()) :: String.t()
```

Formats a single `t:key/0` tuple into a human-readable string.

## Examples

    iex> Minga.Keymap.Bindings.format_key({32, 0})
    "SPC"

    iex> Minga.Keymap.Bindings.format_key({?s, 0x02})
    "C-s"

    iex> Minga.Keymap.Bindings.format_key({?j, 0x00})
    "j"

# `lookup`

```elixir
@spec lookup(node_t(), key()) ::
  {:command, atom() | tuple()} | {:prefix, node_t()} | :not_found
```

Looks up a single key in the trie.

Returns one of:

* `{:command, atom()}` — the key sequence is complete and maps to a command
* `{:prefix, node_t()}` — the key is a valid prefix; the returned node can
  be used as the new root for the next key
* `:not_found` — the key does not exist in this trie node

## Examples

    iex> trie = Minga.Keymap.Bindings.new()
    iex> trie = Minga.Keymap.Bindings.bind(trie, [{?g, 0}, {?g, 0}], :document_start, "Go to first line")
    iex> match?({:prefix, _}, Minga.Keymap.Bindings.lookup(trie, {?g, 0}))
    true
    iex> Minga.Keymap.Bindings.lookup(trie, {?z, 0})
    :not_found

# `lookup_sequence`

```elixir
@spec lookup_sequence(node_t(), [key()]) ::
  {:command, atom(), String.t()} | {:prefix, node_t()} | :not_found
```

Looks up a full key sequence in the trie, walking node by node.

Returns one of:

* `{:command, atom(), String.t()}` — the sequence maps to a command with its description
* `{:prefix, node_t()}` — the sequence is a valid prefix (more keys needed)
* `:not_found` — no match at some point in the sequence

## Examples

    iex> trie = Minga.Keymap.Bindings.new()
    iex> trie = Minga.Keymap.Bindings.bind(trie, [{?g, 0}, {?g, 0}], :document_start, "Go to first line")
    iex> Minga.Keymap.Bindings.lookup_sequence(trie, [{?g, 0}, {?g, 0}])
    {:command, :document_start, "Go to first line"}
    iex> Minga.Keymap.Bindings.lookup_sequence(trie, [{?g, 0}])
    {:prefix, %Minga.Keymap.Bindings.Node{children: %{{103, 0} => %Minga.Keymap.Bindings.Node{children: %{}, command: :document_start, description: "Go to first line"}}, command: nil, description: nil}}
    iex> Minga.Keymap.Bindings.lookup_sequence(trie, [{?z, 0}])
    :not_found

# `merge_bindings`

```elixir
@spec merge_bindings(node_t(), [{[key()], atom() | tuple(), String.t()}]) :: node_t()
```

Merges a list of binding tuples into a trie.

Each binding is a `{key_sequence, command, description}` tuple. Bindings
are applied in order, so later entries override earlier ones on conflict.

This is the bulk registration helper for shared binding groups. Scope
modules call this to include a group's bindings, then apply scope-specific
bindings on top (which override group bindings on conflict).

## Examples

    iex> bindings = [
    ...>   {[{?j, 0}], :move_down, "Move down"},
    ...>   {[{?k, 0}], :move_up, "Move up"}
    ...> ]
    iex> trie = Minga.Keymap.Bindings.merge_bindings(Minga.Keymap.Bindings.new(), bindings)
    iex> {:command, :move_down} = Minga.Keymap.Bindings.lookup(trie, {?j, 0})
    iex> {:command, :move_up} = Minga.Keymap.Bindings.lookup(trie, {?k, 0})

# `merge_bindings`

```elixir
@spec merge_bindings(node_t(), [{[key()], atom() | tuple(), String.t()}], keyword()) ::
  node_t()
```

Merges a list of binding tuples into a trie, excluding specific commands.

Same as `merge_bindings/2` but skips any binding whose command atom
appears in the `exclude` list. Use this when a scope includes a shared
group but needs to override specific commands with different semantics.

## Examples

    iex> bindings = [
    ...>   {[{?j, 0}], :move_down, "Move down"},
    ...>   {[{?q, 0}], :quit_editor, "Quit"}
    ...> ]
    iex> trie = Minga.Keymap.Bindings.merge_bindings(Minga.Keymap.Bindings.new(), bindings, exclude: [:quit_editor])
    iex> {:command, :move_down} = Minga.Keymap.Bindings.lookup(trie, {?j, 0})
    iex> :not_found = Minga.Keymap.Bindings.lookup(trie, {?q, 0})

# `merge_group`

```elixir
@spec merge_group(node_t(), atom()) :: node_t()
```

Merges a named shared group into a trie.

Convenience wrapper that calls `SharedGroups.get/1` and `merge_bindings/2`.

## Examples

    trie = Bindings.new()
    |> Bindings.merge_group(:cua_navigation)
    |> Bindings.bind([{?q, 0}], :quit, "Quit")

# `merge_group`

```elixir
@spec merge_group(node_t(), atom(), keyword()) :: node_t()
```

Merges a named shared group into a trie with exclusions.

## Examples

    trie = Bindings.new()
    |> Bindings.merge_group(:cua_navigation, exclude: [:move_up])

# `new`

```elixir
@spec new() :: node_t()
```

Creates a new, empty trie root node.

## Examples

    iex> trie = Minga.Keymap.Bindings.new()
    iex> Minga.Keymap.Bindings.lookup(trie, {?j, 0})
    :not_found

# `unbind`

```elixir
@spec unbind(node_t(), [key()]) :: node_t()
```

Removes a key sequence binding from the trie.

Clears the command and description on the terminal node. Prunes empty
intermediate nodes (nodes with no command and no children) on the way
back up so the trie doesn't accumulate dead branches.

Returns the updated trie. No-op if the sequence doesn't exist.

## Examples

    iex> trie = Minga.Keymap.Bindings.new()
    iex> trie = Minga.Keymap.Bindings.bind(trie, [{?j, 0}], :move_down, "Move cursor down")
    iex> trie = Minga.Keymap.Bindings.unbind(trie, [{?j, 0}])
    iex> Minga.Keymap.Bindings.lookup(trie, {?j, 0})
    :not_found

---

*Consult [api-reference.md](api-reference.md) for complete listing*
