# Configuration (`tix.toml`)
`tix.toml` (at the repository root) defines the issues directory, the
milestones directory, optional `[export]` targets, and optional cycles legacy
tables. TUI overrides live separately in optional `tix_keybindings.toml`.
**Boards are records in the file store**: each board is one
`docs/boards/<name>.md` file whose frontmatter
carries its columns and display settings (`tix init` writes
`docs/boards/default.md`). Card display defaults to wrapped three-line titles
sorted newest-first; these settings can be changed in the TUI
[Config mode](config.md) (**F12**). See [navigation](navigation.md) for the
metadata stores, and [key bindings](../key-bindings.md) for the complete key
reference.
```toml
columns = ["todo", "in progress", "done", "mismatch"]
issues_dir = "docs/issues"
[export]
tokens = { gitlab = "/path/to/token" }
project = { gitlab = "", linear = "" }
```
Tokens are never serialized back out after configuration.
Two optional top-level keys are not board settings. `editor` overrides the
full-file editor `tix edit` launches, and `viewer` overrides the read-only viewer
the TUI's **v** key launches — its default, `auto`, uses the first of `mdterm`,
`batcat`, `most`, `less`, `more` found on your `PATH`.
The mode-switch keys are configurable under `[shell]` in
`tix_keybindings.toml` — the
function-key scheme is the default, not a fixed set. Each entry takes a list of
chords (aliases); a mode rebound off the F-row, or bound to a letter, moves both
the key and the rail's chord label, and clearing a list leaves the mode reachable
by click only:
```toml
# tix_keybindings.toml
[shell]
switch_to_new_issue = ["f1"]
switch_to_triage = ["f2"]
switch_to_board = ["f3"]
switch_to_cycle = ["f4"]
switch_to_milestones = ["f5"]
switch_to_config = ["f12"]
toggle_navigation = ["ctrl+n"]
```
An unknown action name or an unparsable chord is a load error naming the key.
If the file is absent, every default remains active; if it is present, only the
named actions change. A keybinding table left in `tix.toml` is refused with a
migration error.
See [`docs/key-bindings.md`](../key-bindings.md#mode-shell) for the full reference.
## `tix.toml` reference
Boards live in `docs/boards/<name>.md`; the board named `default` is the one
used by every command that takes no `--board`. A project that has no
`docs/boards/` directory still reads its legacy `[kanban.<name>]` tables in
`tix.toml`, so an existing repository keeps working unchanged; once the
directory exists, its files win.
| `columns` | `string[]` | `triage, todo, in progress, in review, done, wontfix` (what `tix init` writes); `todo, in progress, in review, done` as the bare fallback | The column names. `status:` holds one of these. |
| `done_column` | `string` | `done` if present, else the last column | Which column means finished, for commit clearing. A board whose `category_map` places `completed` elsewhere gets its completion answer from the **category**, not from this key. |
| `category_map` | `table` | absent | `<column> = "<category>"`. Every value must be one of the six vocabulary keys. A key naming a column the board does not configure is a load error. |
| `show_mismatch` | `bool` | `false` | Render a `Mismatched` column for issues in unconfigured columns. |
| `order_by` | `string` | `created` | Card ordering within a column. |
| `title_lines` | `integer` | `3` | Card title height. |
| `wrap_titles` | `bool` | `true` | Wrap card titles. |
| `hidden_columns` | `string[]` | `[]` | Columns collapsed in the TUI; still reachable by filter and search. |
Every key above except `columns` is omitted from `tix.toml` when it equals the
value shown (`done_column` is omitted when it is `done`, the value the loader
supplies for a missing key), and an empty `[export]` table is omitted too. A
legacy `[kanban.<name>]` table therefore saves byte-identically instead of
gaining keys it never had.
**`[categories]`** (top level, optional)
| `order` | `string[]` | all six keys | The vocabulary, in the project's preferred presentation order. It may be a **subset**, and it may not contain a word outside the global six — adding a key here is an `unknown_category` error, not a new category. |
| `default` | `string` | `unstarted` | The category an unmapped column resolves to. Must be in `order`, and must **not** be `completed` — otherwise every unmapped column would silently count as finished. |
**`[triage]`** (top level, optional)
`tix init` writes one, so a new repository has an inbox from the start; absent — a
config written before the feature, or one that removed the section — there is no
inbox and every `tix triage` command refuses, naming the section. Every named column
is validated against the active board through the category service, so a typo is a
load error naming the column rather than an empty queue. See
[`../triage.md`](../triage.md).
| `entry_status` | `string` | required when the section exists | The column that places an issue in the inbox. |
| `declined_status` | `string` | required when the section exists | Where a decline lands. Must resolve to the `canceled` category. |
| `accepted_status` | `string` | absent | The default target for an accept; `--to` overrides it per decision. With neither, an accept is refused `missing_target` rather than guessed. |
| `deferred_status` | `string` | `entry_status` | The column an issue sits in while deferred. |
**`[transitions]`** (top level, optional)
| `terminal_locked` | `bool` | `true` | Whether leaving a terminal category needs a reason. Only consulted on a board that declares a `category_map`. |
All three sections are **omitted from disk when untouched**, so an existing
`tix.toml` round-trips without acquiring the keys it never had.
**`[layout]`** (top level, optional)
| `directory` | `string` | `{assignee}/{type}/{status}` | Directory template, relative to `issues_dir`. Tokens: `{assignee}` `{type}` `{status}` `{priority}` `{cycle}` `{milestone}`. |
| `filename` | `string` | `{date}-{id}-{slug}.md` | Filename template; must end in `.md`. Tokens: `{id}` `{date}` `{slug}` `{number}` `{title}`. |
| `move_on_change` | `bool` | `true` | Relocate a file when a templated value (status, assignee, type) changes. |
**`[projects.<project-id>]`** (top level, optional)
First-class delivery containers. The **table key is the project's stable id**
(never the display name), so renaming a project changes nothing but its label and
never breaks an issue association — an issue's frontmatter `projects:` list holds
those ids. The section is omitted entirely when there are no projects, so a legacy
`tix.toml` round-trips byte-untouched, and declaration order is preserved
(`tix project list` and the TUI `F10` Portfolio page read it).
| `name` | `string` | required | The mutable **display label**; unique case-folded across the registry, but never the identity. |
| `description` | `string` | absent | Free-form context. |
| `status` | `string` | absent | Project metadata — **not** the issue's board column and not a workflow category. |
| `lead` | `string` | absent | An identity string in the same shape `assignee` tolerates (`@alice` or `Name <email>`); not an account or a permission. |
| `start_date` | `string` | absent | `YYYY-MM-DD`. |
| `due_date` | `string` | absent | `YYYY-MM-DD`, on or after `start_date`. |
A project id is allocated once and **never reused**, even after the project is
deleted. Deleting a project removes this registry entry only; the issues keep
their `projects:` references as diagnosable `missing_project_reference` findings
(`tix lint`), never silently dropped. Unknown keys in the section are rejected by
the same `deny_unknown_fields` convention every other table follows.
**`[epics.<epic-id>]`** (top level, optional)
Cross-cutting delivery containers. The **table key is the epic's stable id**
(never the display name), so renaming an epic changes nothing but its label and
never breaks a reference — an issue's frontmatter `epics:` list, and a child
epic's `parent`, hold those ids. The section is omitted entirely when there are
no epics, so a legacy `tix.toml` round-trips byte-untouched, and declaration
order is preserved (`tix epic list` and the TUI `F10` Portfolio page's Epics tab
read it).
| `name` | `string` | required | The mutable **display label**; unique case-folded across the registry, but never the identity. |
| `description` | `string` | absent | Free-form context. |
| `status` | `string` | absent | Epic metadata — **not** the issue's board column and not a workflow category. |
| `owner` | `string` | absent | An identity string in the same shape `assignee` tolerates; not an account or a permission. |
| `start_date` | `string` | absent | `YYYY-MM-DD`. |
| `due_date` | `string` | absent | `YYYY-MM-DD`, on or after `start_date`. |
| `weight` | `integer` | absent | A positive relative size used by the epic rollup. `Issue.estimate` stays the free-form string it is; no typed size is introduced. |
| `parent` | `string` | absent | Another epic's **id**. At most one parent; a self-parent or a cycle is refused before any write. |
| `projects` | `string[]` | absent | Project **ids** this epic belongs to. |
| `milestone` | `string` | absent | A milestone **name**, keyed the way `Issue.milestones` already is. |
| `cycle` | `string` | absent | A cycle **name**, keyed the way `Issue.cycles` already is. |
The milestone/cycle **name reference** versus the parent/projects **id reference**
is a deliberate asymmetry: it matches how the existing registries already work.
An epic id is allocated once and **never reused**, even after the epic is
deleted. Deleting an epic removes this registry entry only; the issues and child
epics keep their references as diagnosable `missing_epic_reference` findings
(`tix lint`), never silently dropped, and deleting an epic never deletes an issue
or a child epic. Unknown keys in the section are rejected by the same
`deny_unknown_fields` convention every other table follows.
`lint_foreign_repositories` (top level, `bool`, default `true`) makes `tix lint`
also lint the related repositories reachable from `related_issues` and merge
their findings; `tix lint --no-foreign` overrides it for one run. The `[layout]`
section is omitted from disk when it is the default, so a config that never set
it round-trips unchanged.
`tips_on_startup` (top level, `bool`, default `true`) controls the TUI's "did you
know?" modal, which shows one tip from the compiled-in tip list (`src/templates/tips/`) each time
a session reaches the TUI. `tix init` writes the key explicitly; the modal's `d`
key writes `false`, and setting it back to `true` (or deleting the key) brings the
dialog back. Like `lint_foreign_repositories`, a `true` value is omitted on save,
so a config that does not mention it round-trips unchanged.
`custom_types` (top level, `string[]`) is the vocabulary `tix lint` checks an
issue's `type:` against. `tix init` writes a default set — `bug`, `feature`,
`task`, `improvement`, `documentation`, `question`, `duplicate`, `chore`, `open`
— and a `type:` outside the list is a lint **warning** whose message names
`custom_types` as the remedy. When the key is absent, the same defaults apply.
`labels` (top level, `string[]`) declares **extra** known labels beyond the
builtins. `decision-pending` — the label behind the `DECISION:` convention, an
open question a human must settle before the issue can move — is a **builtin**:
`tix` recognises it in every repository, `tix init` writes no key for it (a
brand-new key would be rejected on load by an older binary, whose config denies
unknown fields), and it is always resolved alongside any `labels` you set.
Unlike `custom_types`, the list is **not** a closed vocabulary: `tix lint`
accepts any label, because labels are free-form across trackers and imports, and
a hard allow-list would flag every mirrored one. An untouched file gains no key
on save.