# TUI key bindings
TUI key bindings are configured under the optional `[keybindings]` table in `tix.toml`. Each mode is independent. Omitted modes and actions inherit the built-in defaults, so existing configurations need no migration.
```toml
[keybindings.board]
new_issue = ["ctrl+n"]
help = ["f1", "?"]
[keybindings.form]
submit = ["ctrl+s"]
cancel = ["esc"]
```
A binding is one terminal key chord, not a key sequence. Every action accepts one or more strings. A user override replaces that action's complete default alias list in that mode.
## Chord grammar
Names are case-insensitive. Supported named keys are `esc`, `enter`, `tab`, `backspace`, `delete`, `insert`, `home`, `end`, `left`, `right`, `up`, `down`, `pgup`, `pgdn`, `f1` through `f12`, and `space`. Use `shift+tab` for BackTab. Supported modifiers are `ctrl` (or `control`), `alt` (or `option`), `shift`, and `super` (or `cmd`). Printable single characters are also accepted, such as `n`, `?`, and `A`. Character bindings do not support modifier combinations.
Bindings must be non-empty. A chord can be assigned to only one action within one mode. The same chord may be reused in different modes. Invalid names, empty lists, and conflicts are rejected when `tix.toml` loads, with the mode and action in the error.
## Text entry and mouse controls
Printable characters typed into forms, comments, assets, filters, planning fields, and inline-create prompts remain text input unless the exact character is configured as an action in that mode. Backspace and other inherent editing behavior are not commands. Mouse controls are not configurable; existing clicks, double-clicks, context menus, scrolling, and drag-and-drop continue to invoke the same logical actions as the keyboard.
## Mode shell
The TUI has six top-level modes: New Issue, Triage, Board, Cycles, Milestones,
and Config. Their switch keys and the navigation-display toggle live in their own
`[keybindings.shell]` table.
| switch to new issue | `F1` | `switch_to_new_issue` |
| switch to triage | `F2` | `switch_to_triage` |
| switch to board | `F3` | `switch_to_board` |
| switch to cycles | `F4` | `switch_to_cycle` |
| switch to milestones | `F5` | `switch_to_milestones` |
| switch to repos | `F6` | `switch_to_repos` |
| switch to config | `F12` | `switch_to_config` |
| toggle navigation | `Ctrl+n` | `toggle_navigation` |
> **Breaking (UI Overhaul V2):** the chords moved. New Issue was `F4` and is now
> `F1`; Board was `F1` and is now `F3`; Cycle was `F3` and is now `F4`. Triage
> (`F2`) and Config (`F12`) are unchanged, and Milestones (`F5`) is new. The
> action *names* are unchanged, so a `[keybindings.shell]` override keyed by name
> keeps working.
A shell chord switches modes only when the active mode does not bind that key
itself. This keeps `F2` as "open picker" inside the form and the date pickers, for
example: the local binding always wins. A key no mode binds is unaffected, so
`F7`–`F11` stay reserved and do nothing. (`F6` opens the Repos mode.)
The vertical navigation is a **tree**: one trunk row per mode, with clickable
limbs indented beneath — one per configured board (Board; the active one bold),
per configured cycle (Cycles), per milestone (Milestones), and per
`[templates.<name>]` (New Issue). An empty group draws nothing. In the compacted
`Small` display the trunk shows its chord and each limb its first two characters
(`Default` → `De`); `Default` shows the full name.
```toml
[keybindings.shell]
switch_to_milestones = ["f5", "M"]
toggle_navigation = ["ctrl+b"]
```
Overriding `switch_to_milestones` to `["f5", "M"]` gives the mode two chords; the
navigation entry shows both. Clearing a list leaves the mode reachable by click
only, with no chord on its row. An unknown action name or an unparsable chord is
a load error naming the key. The display toggle cycles the vertical navigation
`Off` → `Small` → `Default` (`Default` on launch).
## Movement keys
Every browsing mode moves with `j`/`k` and `↑`/`↓`. Modes with a horizontal axis
also move with `h`/`l` and `←`/`→`:
| Board | `j`/`k`, `↑`/`↓` | `h`/`l`, `←`/`→` (columns) |
| Triage | `j`/`k`, `↑`/`↓` | `h`/`l`, `←`/`→` (control bar) |
| Cycles | `j`/`k`, `↑`/`↓` | `h`/`l`, `←`/`→` (group sections) |
| Milestones | `j`/`k`, `↑`/`↓` | — (registry list) |
| New Issue | `Tab`/`Shift+Tab`, `↑`/`↓` (fields) | — (a form: `h`/`l` are text) |
| Config | `j`/`k`, `↑`/`↓` (focused pane) | `h`/`l`, `←`/`→` (panes) |
Every browsing mode's `h`/`l` and `←`/`→` walk that mode's own horizontal
axis, clamped at the ends rather than wrapping:
- the Board's columns;
- the Triage inbox's control bar — queue, status, assignee, new — where `Enter`
activates the focused control and `j`/`k` keep moving the queue cursor
whichever control is focused;
- the Cycle listing's rendered group sections, landing on the first entry of
the previous / next section;
- the Config mode's two panes — the section rail and the body — where `j`/`k`
then move inside the focused pane: the selected section in the rail, the row
cursor in the body.
New Issue is a form with no horizontal axis: `h`/`l` stay typeable text there,
and `Tab`/`Shift+Tab` (or `↑`/`↓`) change fields. The planning surface reached
with `m` uses `h`/`l` and `←`/`→` to switch its Cycles/Milestones segments; the
Config mode reaches those same sections from its rail, with `j`/`k`.
Triage and Cycle reach these keys differently. The Triage inbox's `h`/`l` are
**raw** keys, like its `j`/`k`: the mode accepts typed text (a decline reason, a
duplicate target), so a binding would rewrite the letters a user is typing. The
Cycle mode binds `move_left`/`move_right` to `←`/`h` and `→`/`l` (see the
`cycle` rows below), so its letters canonicalize to the arrows.
The **Milestones** mode (`F5`) is the Config surface pinned to the milestone
registry: `j`/`k` (or `↑`/`↓`) walk the milestones, `Enter`/`e` edit the selected
one, `n` creates, `x` deletes (with a confirmation naming referencing issues),
`m` opens the planning-management surface, `?` opens help, and `q`/`Esc` closes
back to the board. Its rows are the same rows the Config mode's Milestones
section draws, with the same per-milestone progress.
The **Config** mode's rail has five sections — Dashboards, Cycles, Milestones,
Types, and Custom attributes. The last two edit `custom_types` and
`[custom_attributes].keys`: `n` prompts for a new entry, `x` removes the selected
one, and both persist to `tix.toml`.
## Mouse hover
Hovering highlights without changing any selection:
- the vertical mode navigation — trunk rows and their limbs — in every mode;
- the Config mode's section rail;
- on the board, the hovered card **and** its whole column (the column border takes a distinct accent). The column is marked for any position inside it, so an **empty** column lights up too — hovering below the last card, or over a column with no cards at all, outlines that column.
## Board
| move up | `Up / k` | `move_up` |
| move down | `Down / j` | `move_down` |
| previous column | `Left / h` | `move_left` |
| next column | `Right / l` | `move_right` |
| next board | `Tab` | `next_board` |
| previous board | `Shift+Tab` | `previous_board` |
| open detail | `Enter` | `open` |
| edit issue | `e` | `edit` |
| view issue | `v` | `view` |
| open agent | `Shift+A` | `agent` |
| toggle column | `c` | `toggle_column` |
| filter criteria | `f` | `filter_modal` |
| batch select | `Shift+B` | `bulk_modal` |
| triage inbox | `t` | `triage` |
| mark for batch | `Space` | `mark` |
| unmark for batch | `x` | `unmark` |
| move to previous column | `<` | `move_previous` |
| move to next column | `>` | `move_next` |
| scroll up one page | `PgUp` | `scroll_up` |
| scroll down one page | `PgDn` | `scroll_down` |
| new issue | `n` | `new_issue` |
| clone issue | `Shift+C` | `clone_issue` |
| show archived issues | `a` | `toggle_archived` |
| toggle sub-issues | `u` | `toggle_children` |
| delete issue | `d` | `delete` |
| refresh | `r` | `refresh` |
| settings | `s` | `settings` |
| planning | `m / Shift+M` | `planning` |
| new board | `b` | `new_board` |
| help | `?` | `help` |
| quit | `q / Shift+Q / Esc` | `quit` |
| search | `/` | `text_filter` |
| reorder up | `Ctrl+Up` | `reorder_up` |
| reorder down | `Ctrl+Down` | `reorder_down` |
## Detail
| close | `q / Shift+Q / Esc` | `close` |
| edit issue | `e` | `edit` |
| view issue | `v` | `view` |
| scroll up | `PgUp` | `scroll_up` |
| scroll down | `PgDn` | `scroll_down` |
| help | `?` | `help` |
| comment | `c` | `comment` |
| attach asset | `a` | `asset` |
| set/change/clear parent | `Shift+P` | `edit_parent` |
| open related-issues picker | `r` | `related_picker` |
| follow selected related entry | `o` | `follow_related` |
| back to previous issue | `Backspace` | `nav_back` |
## Repos
The F6 Repos mode lists the child repositories this project coordinates (the
`[repos]` table in `tix.toml`) and the parent when one is known (`parent_repo`, or
the `TIX_PARENT_REPO` inherited from a parent that launched this session). `Enter`
runs `tix` inside the selected repository; exiting that session returns here.
| close | `q / Shift+Q / Esc` | `close` |
| move up | `Up / k` | `move_up` |
| move down | `Down / j` | `move_down` |
| run tix in the selected repository | `Enter` | `submit` |
| help | `?` | `help` |
## Forms and prompts
| `form` | cancel | `Esc` | `cancel` |
| `form` | submit | `Enter` | `submit` |
| `form` | next field/row | `Tab / Down` | `next` |
| `form` | previous field/row | `Shift+Tab / Up` | `previous` |
| `form` | open date/picker | `F2` | `open_picker` |
| `form` | open status/priority dropdown | `Enter` / `F2` (Status, Priority rows) | — row-local |
| `form` | toggle target | `Enter` (Target row) | — row-local |
| `form` | select target side | `Left` / `Right` (Target row) | — row-local |
| `form` | open template picker | `Enter` / `F2` (Template row) | — row-local |
| `planning_form` | cancel | `Esc` | `cancel` |
| `planning_form` | submit | `Enter` | `submit` |
| `planning_form` | next field | `Tab / Down` | `next` |
| `planning_form` | previous field | `Shift+Tab / Up` | `previous` |
| `planning_form` | open date picker | `F2` | `open_picker` |
| `filter` | cancel | `Esc` | `cancel` |
| `related_picker` | next | `Down` | `next` |
| `related_picker` | previous | `Up` | `previous` |
| `related_picker` | select | `Enter` | `submit` |
| `related_picker` | cancel | `Esc` | `cancel` |
| `detail` | new relationship | `n` | `new_relationship` |
| `detail` | next relationship | `j / Down` | `next_relationship` |
| `detail` | previous relationship | `k / Up` | `previous_relationship` |
| `detail` | edit relationship | `Shift+E` | `edit_relationship` |
| `detail` | remove relationship | `d` | `remove_relationship` |
| `detail` | set/change/clear parent | `Shift+P` | `edit_parent` |
| `link_prompt` | submit | `Enter` | `submit` |
| `link_prompt` | cancel | `Esc` | `cancel` |
| `filter` | submit | `Enter` | `submit` |
| `move_into_done` | cancel | `Esc` | `cancel` |
| `move_into_done` | submit | `Enter` | `submit` |
| `comment` | cancel | `Esc` | `cancel` |
| `comment` | submit | `Enter` | `submit` |
| `asset` | cancel | `Esc` | `cancel` |
| `asset` | submit | `Enter` | `submit` |
| `settings_prompt` | cancel | `Esc / q / Shift+Q` | `cancel` |
| `settings_prompt` | submit | `Enter / y / Shift+Y` | `submit` |
| `planning_confirm` | confirm | `y / Shift+Y / Enter` | `confirm` |
| `planning_confirm` | cancel | `Esc` | `cancel` |
## Embedded controls
| `rollover` | submit | `Enter` | `submit` |
| `rollover` | toggle choice | `Left / Right / Tab` | `previous` |
| `rollover` | cancel | `Esc` | `cancel` |
| `confirm_delete` | confirm | `y / Shift+Y` | `confirm` |
| `confirm_delete` | cancel | `Esc` | `cancel` |
| `help_search` | cancel (clears the query) | `Esc` | `cancel` |
| `help_search` | submit (keeps the query) | `Enter` | `submit` |
| `help` | search the keybindings | `/` | `text_filter` |
| `help` | cancel | `Esc / q / ?` | `cancel` |
| `help` | next row | `Down / j` | `next` |
| `help` | previous row | `Up / k` | `previous` |
| `help` | scroll down one page | `PgDn` | `scroll_down` |
| `help` | scroll up one page | `PgUp` | `scroll_up` |
| `help` | submit | `Enter` | `submit` |
| `context_menu` | cancel | `Esc / q / ?` | `cancel` |
| `context_menu` | next | `Down` | `next` |
| `context_menu` | previous | `Up` | `previous` |
| `context_menu` | submit | `Enter` | `submit` |
| `form_picker` | cancel | `Esc / F2` | `cancel` |
| `form_picker` | next | `Up` | `next` |
| `form_picker` | previous | `Down` | `previous` |
| `form_picker` | submit | `Enter` | `submit` |
| `form_picker` | inline create | `+ / Insert` | `create` |
| `date_picker` | move up | `Up / k` | `move_up` |
| `date_picker` | move down | `Down / j` | `move_down` |
| `date_picker` | previous day | `Left / h` | `move_left` |
| `date_picker` | next day | `Right / l` | `move_right` |
| `date_picker` | previous month | `PgUp` | `previous_month` |
| `date_picker` | next month | `PgDn` | `next_month` |
| `date_picker` | previous year | `[` | `previous_year` |
| `date_picker` | next year | `]` | `next_year` |
| `date_picker` | submit | `Enter` | `submit` |
| `date_picker` | clear | `Delete / c / Shift+C` | `reset` |
| `date_picker` | cancel | `Esc / q` | `cancel` |
## Modal management modes
| `filter_modal` | next | `Tab / Down` | `next` |
| `filter_modal` | previous | `Shift+Tab / Up` | `previous` |
| `filter_modal` | reset | `r / Shift+R` | `reset` |
| `filter_modal` | submit | `Enter` | `submit` |
| `filter_modal` | cancel | `Esc` | `cancel` |
| `filter_modal` | open date picker | `F2` | `open_picker` |
| `settings` | previous | `Up / k` | `move_up` |
| `settings` | next | `Down / j` | `move_down` |
| `settings` | previous pane | `Left / h` | `move_left` |
| `settings` | next pane | `Right / l` | `move_right` |
| `settings` | next board | `Tab / ]` | `next_board` |
| `settings` | previous board | `Shift+Tab / [` | `previous_board` |
| `settings` | step into the body / activate row | `Enter` | `submit` |
| `settings` | toggle | `Space` | `toggle` |
| `settings` | add column | `a` | `add_column` |
| `settings` | new board / new cycle / new milestone | `n` | `new_board` |
| `settings` | rename board / edit cycle / edit milestone | `e` | `edit_planning_item` |
| `settings` | delete board / delete cycle / delete milestone | `x / Shift+X` | `delete` |
| `settings` | close | `q / Shift+Q / Esc` | `close` |
| `cycle` | previous cycle | `Up / k` | `move_up` |
| `cycle` | next cycle | `Down / j` | `move_down` |
| `cycle` | previous group | `Left / h` | `move_left` |
| `cycle` | next group | `Right / l` | `move_right` |
| `cycle` | planning management | `m` | `planning` |
| `cycle` | help | `?` | `help` |
| `cycle` | close | `q / Shift+Q / Esc` | `close` |
| `planning_management` | previous | `Up / k` | `move_up` |
| `planning_management` | next | `Down / j` | `move_down` |
| `planning_management` | switch segment | `Tab` | `next` |
| `planning_management` | new | `n` | `new_planning_item` |
| `planning_management` | edit | `e` | `edit_planning_item` |
| `planning_management` | delete | `x / Shift+X` | `delete_planning_item` |
| `planning_management` | close | `q / Shift+Q / Esc` | `close` |
| `edit_repair` | apply the proposed fix | `a / y` | `confirm` |
| `edit_repair` | declare the key in tix.toml | `d / Shift+D` | `declare_key` |
| `edit_repair` | reopen the editor on the retained copy | `r / Shift+R` | `reedit` |
| `edit_repair` | acknowledge (keeps the copy) | `i / Shift+I / Enter / Esc` | `ignore` |
| `edit_repair` | discard the edit (asks again) | `x / Shift+X` | `discard` |
| `edit_conflict` | overwrite the file with this edit | `o / Shift+O / y / Shift+Y` | `confirm` |
| `edit_conflict` | keep the on-disk version | `a / Shift+A / Esc` | `ignore` |
| `edit_conflict` | compare in the editor | `c / Shift+C / r / Shift+R` | `reedit` |
The `edit_repair` modal is what an editor round trip raises when the saved file
has a problem — it will not parse, `tix lint` is unhappy about it, or the edit
lost the title. It lists every problem with its diagnostic and a proposal, and
names the retained copy (`tix-edit-<pid>-<number>.md` in the temp directory) so a
problem with no automatic fix can be corrected by hand.
- **`a`** (or `y`, because it is `confirm` everywhere else) applies the first
proposed fix. For an undeclared top-level frontmatter key that fix is the
declaration itself: the key goes into `[custom_attributes]` in `tix.toml` — see
[custom frontmatter keys](for-users/storage.md#custom-frontmatter-keys). A problem with
nothing tix can apply safely says so and closes.
- **`d`** declares the undeclared key explicitly, or says there is none.
- **`r`** reopens the editor on the retained copy; a file that still will not
parse re-enters the prompt with fresh problems.
- **`i`/`Enter`/`Esc`** acknowledges. This **keeps** the retained copy — the
status line names where — so a stray key never destroys an edit.
- **`x`** discards: the first press arms, the second removes the retained copy.
This is the **only** thing that removes it.
Nothing is ever discarded automatically. Applying a fix operates on the retained
copy and re-parses it; a fix that does not make the file valid leaves the file
untouched and re-reports. Before any write that replaces the issue, the previous
content is saved beside it as `<name>.tixbak` and named in the status line, so
the version a write replaced is always recoverable.
The `edit_conflict` modal is what an editor round trip raises when the issue file
**changed while the editor had it open** — an agent writing through MCP, another
session, a repair. Nothing has been written at that point: the newer file is
untouched and the edit is still the `tix-edit-<pid>-<number>.md` copy in the temp
directory (`/tmp`, or `$TMPDIR`) that the prompt names. `o` (or `y`) overwrites
the newer file with the edit, `a`/`Esc` keeps the newer file and discards the
edit, and `c` (or `r`) reopens the editor on the temp copy so the two can be
compared; the config's `editor` command is resolved again for that second look.
**Esc is the safe default**: only the overwrite chord writes, so a concurrent
write is never clobbered by a keypress that was not a decision.
`settings` is the one mode where `Enter` is not a second `toggle` chord. `toggle`
is `Space` alone and `submit` is `Enter`, because the two do different things:
`Enter` steps from the section rail into the body and then activates the row,
while `Space` toggles the row under the cursor. A `[keybindings.settings]`
override that lists `enter` under `toggle` therefore collides with `submit` and
is rejected at load — bind it to `submit` instead.
`filter`, `move_into_done`, `comment`, `asset`, `link_prompt` and `help_search` are
**free-text** modes: they read
arbitrary text into a buffer, including a filesystem path. `cancel` is bound to `Esc`
alone in those six, and not to `q`, so that every printable character can be typed.
A `q` binding there does not merely fail to type — it resolves to the cancel action's
canonical event, which leaves the mode, so everything typed afterwards is applied as
a command in the mode underneath. `q` still closes `help`, `context_menu`,
`settings`, `date_picker`, and the yes/no `blocked_move` confirmation, none of which
accept text. Override the table in `tix.toml` if you want a different trade.