9.6 KiB
TUI Keymap Implementation Reference
This document is the long-term implementation reference for Codex TUI keybindings. It describes how keymap configuration is resolved at runtime, which safety contracts are intentionally strict, and how to test behavior end to end.
Scope and boundaries
This keymap system is action-based and context-aware. It supports user rebinding for the TUI without requiring source code edits.
Responsibilities:
- Resolve config values into runtime key bindings.
- Apply deterministic precedence.
- Reject ambiguous bindings in dispatch scopes where collisions are unsafe.
- Preserve explicit safety semantics for approval elicitation and onboarding.
Non-responsibilities:
- It does not choose which screen should handle an event.
- It does not persist config.
- It does not guarantee terminal modifier reporting consistency; defaults may include compatibility variants for that reason.
Source-of-truth map
- Runtime resolution:
codex-rs/tui/src/keymap.rs - Onboarding flow-level routing and quit guard:
codex-rs/tui/src/onboarding/onboarding_screen.rs - Approval/MCP elicitation option semantics:
codex-rs/tui/src/bottom_pane/approval_overlay.rs - Generic list popup navigation semantics:
codex-rs/tui/src/bottom_pane/list_selection_view.rs - User-facing default template:
https://github.com/openai/codex/blob/main/docs/default-keymap.toml - User-facing config overview:
docs/config.md
Config contract
[tui.keymap] is action-to-binding mapping by context.
[tui.keymap]
preset = "latest"
[tui.keymap.global]
submit = "enter"
[tui.keymap.composer]
submit = ["enter", "ctrl-j"]
Rules:
- Mapping direction is
action -> key_or_keys. - Values support:
action = "key-spec"action = ["key-spec-1", "key-spec-2"]action = []for explicit unbind.
- Unknown contexts, actions, or key identifiers fail validation.
- Aliases are normalized (for example
escape -> esc,pgdn -> page-down). - Key identifiers are lowercase and use
-separators.
Contexts and actions
Supported contexts:
globalchatcomposereditorvim_normalvim_operatorpagerlistapprovalonboarding
Action inventory by context is documented in docs/config.md and the template
at https://github.com/openai/codex/blob/main/docs/default-keymap.toml.
Presets and compatibility policy
Preset semantics:
latestis an alias to the newest shipped preset.v5is the current baseline;v1,v2,v3, andv4are frozen for historical behavior.- Today,
latest -> v5.
User guidance:
- Pin
preset = "v1"for stable behavior over time. - Use
preset = "latest"to adopt new defaults whenlatestmoves.
Developer policy:
- Do not mutate old preset defaults after release.
- Add a new version (for example
v6) for behavior changes. - Update docs and migration notes whenever
latestchanges.
Migration notes:
v2 restores alt-d as a delete_forward_word alias while preserving
alt-delete from v1.
v3 exposes the Copy shortcut as global.copy = "ctrl-o" so it can be
remapped or unbound through [tui.keymap].
TODO(docs): mirror this preset migration note on developers.openai.com.
Compatibility detail:
Some actions intentionally ship with multiple default bindings because terminals
can report modifier combinations differently. Examples include ? vs shift-?
and certain ctrl chords with optional shift.
Shifted letter bindings are also matched compatibly when terminals report them
as uppercase characters without an explicit shift modifier (for example
shift-i matching I).
Resolution and precedence
Resolution order (highest first):
- Context binding (
tui.keymap.<context>.<action>) - Global fallback (
tui.keymap.global.<action>) for chat/composer fallback actions only - Preset default binding
If no binding matches, normal unhandled-key fallback behavior applies.
Conflict validation model
Validation is dispatch-order aware, not globally uniform.
Current conflict passes in RuntimeKeymap::validate_conflicts enforce:
- App-level uniqueness for app actions and app-level chat controls.
- App/composer shadowing prevention, because app handlers execute before forwarding to composer handlers.
- Composer-local uniqueness for submit/queue/shortcut-toggle.
- Context-local uniqueness in editor, vim_normal, vim_operator, pager, list, approval, and onboarding.
Intentionally allowed:
- Same key across different contexts that are not co-evaluated in a way that can cause unsafe shadowing.
- Shared defaults where runtime context gating keeps semantics unambiguous.
Safety invariants
MCP elicitation cancel semantics
For MCP elicitation prompts, Esc is always treated as cancel.
Implementation contract:
Escis always included in cancel shortcuts.- User-defined
approval.cancelshortcuts are merged into cancel. - Any overlap is removed from
approval.declinein elicitation mode.
Rationale: dismissal must remain a safe abort path and never silently map to "continue without requested info".
Onboarding API-key text-entry guard
During API-key entry, printable onboarding.quit bindings are suppressed only
when the API-key field already has text.
Implementation contract:
- Guard applies only when API-key entry mode is active.
- Guard applies only to printable char keys without control/alt modifiers.
- Guard applies only when input is non-empty.
- Control/alt quit chords are never suppressed by this guard.
Rationale: keep text-entry safe once typing has begun while preserving an intentional printable-quit path on empty input.
Dispatch model and handler boundaries
High-level behavior:
- App-level event handling runs before some lower-level handlers.
- Composer behavior depends on both app routing and composer-local checks.
- Onboarding screen routing applies flow-level rules before delegating to step widgets.
- Approval overlay and list selection use context-specific bindings resolved by
RuntimeKeymap.
When changing dispatch order, re-evaluate conflict validation scopes in
keymap.rs and associated tests.
Diagnostics contract
Validation errors should be actionable and include:
- Problem summary.
- Exact config path.
- Why the value is invalid or ambiguous.
- Concrete remediation step.
Categories currently covered:
- Invalid key specification.
- Unknown action/context mapping.
- Same-scope ambiguity.
- Shadowing collisions in dispatch-coupled scopes.
Debug path
When keybindings do not behave as expected, trace in this order:
- Verify config normalization and schema validation in
codex-rs/core/src/config/tui_keymap.rs. - Verify resolved runtime bindings and conflict checks in
codex-rs/tui/src/keymap.rs(from_config,validate_conflicts). - Verify handler-level dispatch order in:
codex-rs/tui/src/app.rsfor app/chat/composer routing.codex-rs/tui/src/pager_overlay.rsfor pager controls.codex-rs/tui/src/bottom_pane/approval_overlay.rsfor approval safety behavior.codex-rs/tui/src/onboarding/onboarding_screen.rsfor onboarding quit guard behavior.
- Reproduce with explicit bindings in
~/.codex/config.tomland compare against:docs/default-keymap.tomldocs/keymap-action-matrix.md
Testing notes
Commands
Run from codex-rs/:
just fmtcargo test -p codex-tui --lib- Optional full crate run (includes integration tests):
cargo test -p codex-tui - Optional focused runs while iterating:
cargo test -p codex-tui --lib keymap::tests
If cargo test -p codex-tui fails because the codex binary cannot be found
in local target/, run --lib for keymap behavior checks and then validate the
integration target in an environment where workspace binaries are available.
For intentional UI/text output changes in codex-tui:
cargo insta pending-snapshots -p codex-tuicargo insta show -p codex-tui <path/to/snapshot.snap.new>cargo insta accept -p codex-tuionly when the full snapshot set is expected.
Behavior coverage checklist
Use this checklist before landing keymap behavior changes:
- Precedence: context override beats global fallback and preset defaults.
- Unbind behavior:
action = []actually removes the binding. - Conflict rejection:
- Same-context duplicates fail.
- App/composer shadowing fails for submit, queue, and toggle-shortcuts.
- Approval safety:
Escresolves elicitation to cancel.- Decline shortcuts never contain cancel overlaps in elicitation mode.
- Onboarding safety:
- Printable quit key is suppressed when API-key input is active and non-empty.
- Printable quit key is not suppressed when input is empty.
- Control/alt quit chords are not suppressed.
- Footer/help hints continue to reflect effective primary bindings.
https://github.com/openai/codex/blob/main/docs/default-keymap.toml,docs/config.md, anddocs/example-config.mdstay aligned with runtime action names and defaults.
Manual sanity checks
- Start onboarding and enter API-key mode.
- Bind
onboarding.quitto a printable key. - Verify that key quits when input is empty, then types once text exists.
- Verify
ctrl-cor another control quit chord still exits. - Trigger an MCP elicitation request and verify
Esccancels, not declines.
Documentation maintenance
When adding/changing keymap API surface:
- Update runtime definitions and defaults in
codex-rs/tui/src/keymap.rs. - Update
docs/default-keymap.toml. - Update
docs/config.mdanddocs/example-config.mdsnippets. - Update this file with behavioral or safety contract changes.
- Add/update regression tests in
codex-rs/tui.