in codex-rs/tui/src/keymap.rs [1164:1688]
fn validate_conflicts(&self) -> Result<(), String> {
validate_unique(
"app",
[
("open_transcript", self.app.open_transcript.as_slice()),
(
"open_external_editor",
self.app.open_external_editor.as_slice(),
),
("copy", self.app.copy.as_slice()),
("clear_terminal", self.app.clear_terminal.as_slice()),
("toggle_vim_mode", self.app.toggle_vim_mode.as_slice()),
("toggle_fast_mode", self.app.toggle_fast_mode.as_slice()),
("toggle_raw_output", self.app.toggle_raw_output.as_slice()),
("chat.interrupt_turn", self.chat.interrupt_turn.as_slice()),
(
"chat.decrease_reasoning_effort",
self.chat.decrease_reasoning_effort.as_slice(),
),
(
"chat.increase_reasoning_effort",
self.chat.increase_reasoning_effort.as_slice(),
),
(
"chat.edit_queued_message",
self.chat.edit_queued_message.as_slice(),
),
("composer.submit", self.composer.submit.as_slice()),
("composer.queue", self.composer.queue.as_slice()),
(
"composer.toggle_shortcuts",
self.composer.toggle_shortcuts.as_slice(),
),
(
"composer.history_search_previous",
self.composer.history_search_previous.as_slice(),
),
(
"composer.history_search_next",
self.composer.history_search_next.as_slice(),
),
],
)?;
validate_no_reserved(
"main",
[
("open_transcript", self.app.open_transcript.as_slice()),
(
"open_external_editor",
self.app.open_external_editor.as_slice(),
),
("copy", self.app.copy.as_slice()),
("clear_terminal", self.app.clear_terminal.as_slice()),
("toggle_vim_mode", self.app.toggle_vim_mode.as_slice()),
("toggle_fast_mode", self.app.toggle_fast_mode.as_slice()),
("toggle_raw_output", self.app.toggle_raw_output.as_slice()),
("chat.interrupt_turn", self.chat.interrupt_turn.as_slice()),
(
"chat.decrease_reasoning_effort",
self.chat.decrease_reasoning_effort.as_slice(),
),
(
"chat.increase_reasoning_effort",
self.chat.increase_reasoning_effort.as_slice(),
),
(
"chat.edit_queued_message",
self.chat.edit_queued_message.as_slice(),
),
("composer.submit", self.composer.submit.as_slice()),
("composer.queue", self.composer.queue.as_slice()),
(
"composer.toggle_shortcuts",
self.composer.toggle_shortcuts.as_slice(),
),
(
"composer.history_search_previous",
self.composer.history_search_previous.as_slice(),
),
(
"composer.history_search_next",
self.composer.history_search_next.as_slice(),
),
],
MAIN_RESERVED_BINDINGS,
[(
"chat.interrupt_turn",
"fixed.backtrack",
key_hint::plain(KeyCode::Esc),
)],
)?;
validate_no_shadow_with_allowed_overlaps(
"app",
[
("open_transcript", self.app.open_transcript.as_slice()),
(
"open_external_editor",
self.app.open_external_editor.as_slice(),
),
("copy", self.app.copy.as_slice()),
("clear_terminal", self.app.clear_terminal.as_slice()),
("toggle_vim_mode", self.app.toggle_vim_mode.as_slice()),
("toggle_fast_mode", self.app.toggle_fast_mode.as_slice()),
("toggle_raw_output", self.app.toggle_raw_output.as_slice()),
],
[
("list.move_up", self.list.move_up.as_slice()),
("list.move_down", self.list.move_down.as_slice()),
("list.move_left", self.list.move_left.as_slice()),
("list.move_right", self.list.move_right.as_slice()),
("list.page_up", self.list.page_up.as_slice()),
("list.page_down", self.list.page_down.as_slice()),
("list.jump_top", self.list.jump_top.as_slice()),
("list.jump_bottom", self.list.jump_bottom.as_slice()),
("list.accept", self.list.accept.as_slice()),
("list.cancel", self.list.cancel.as_slice()),
(
"approval.open_fullscreen",
self.approval.open_fullscreen.as_slice(),
),
("approval.open_thread", self.approval.open_thread.as_slice()),
("approval.approve", self.approval.approve.as_slice()),
(
"approval.approve_for_session",
self.approval.approve_for_session.as_slice(),
),
(
"approval.approve_for_prefix",
self.approval.approve_for_prefix.as_slice(),
),
("approval.deny", self.approval.deny.as_slice()),
("approval.decline", self.approval.decline.as_slice()),
("approval.cancel", self.approval.cancel.as_slice()),
],
[(
"clear_terminal",
"list.move_right",
key_hint::ctrl(KeyCode::Char('l')),
)],
)?;
// The request-user-input overlay consumes turn interruption before
// configurable question navigation reaches its list handler.
validate_no_shadow_with_allowed_overlaps(
"request_user_input",
[("chat.interrupt_turn", self.chat.interrupt_turn.as_slice())],
[
("list.move_left", self.list.move_left.as_slice()),
("list.move_right", self.list.move_right.as_slice()),
],
[],
)?;
// While the composer is focused, these main-surface handlers always
// consume matching keys before the event reaches the textarea editor.
validate_no_shadow_with_allowed_overlaps(
"main",
[
("open_transcript", self.app.open_transcript.as_slice()),
(
"open_external_editor",
self.app.open_external_editor.as_slice(),
),
("copy", self.app.copy.as_slice()),
("clear_terminal", self.app.clear_terminal.as_slice()),
("chat.interrupt_turn", self.chat.interrupt_turn.as_slice()),
(
"chat.decrease_reasoning_effort",
self.chat.decrease_reasoning_effort.as_slice(),
),
(
"chat.increase_reasoning_effort",
self.chat.increase_reasoning_effort.as_slice(),
),
("composer.submit", self.composer.submit.as_slice()),
("toggle_vim_mode", self.app.toggle_vim_mode.as_slice()),
("toggle_fast_mode", self.app.toggle_fast_mode.as_slice()),
("toggle_raw_output", self.app.toggle_raw_output.as_slice()),
(
"composer.history_search_previous",
self.composer.history_search_previous.as_slice(),
),
],
[
(
"editor.insert_newline",
self.editor.insert_newline.as_slice(),
),
("editor.move_left", self.editor.move_left.as_slice()),
("editor.move_right", self.editor.move_right.as_slice()),
("editor.move_up", self.editor.move_up.as_slice()),
("editor.move_down", self.editor.move_down.as_slice()),
(
"editor.move_word_left",
self.editor.move_word_left.as_slice(),
),
(
"editor.move_word_right",
self.editor.move_word_right.as_slice(),
),
(
"editor.move_line_start",
self.editor.move_line_start.as_slice(),
),
("editor.move_line_end", self.editor.move_line_end.as_slice()),
(
"editor.delete_backward",
self.editor.delete_backward.as_slice(),
),
(
"editor.delete_forward",
self.editor.delete_forward.as_slice(),
),
(
"editor.delete_backward_word",
self.editor.delete_backward_word.as_slice(),
),
(
"editor.delete_forward_word",
self.editor.delete_forward_word.as_slice(),
),
(
"editor.kill_line_start",
self.editor.kill_line_start.as_slice(),
),
(
"editor.kill_whole_line",
self.editor.kill_whole_line.as_slice(),
),
("editor.kill_line_end", self.editor.kill_line_end.as_slice()),
("editor.yank", self.editor.yank.as_slice()),
],
[(
"composer.submit",
"editor.insert_newline",
key_hint::plain(KeyCode::Enter),
)],
)?;
validate_unique(
"editor",
[
("insert_newline", self.editor.insert_newline.as_slice()),
("move_left", self.editor.move_left.as_slice()),
("move_right", self.editor.move_right.as_slice()),
("move_up", self.editor.move_up.as_slice()),
("move_down", self.editor.move_down.as_slice()),
("move_word_left", self.editor.move_word_left.as_slice()),
("move_word_right", self.editor.move_word_right.as_slice()),
("move_line_start", self.editor.move_line_start.as_slice()),
("move_line_end", self.editor.move_line_end.as_slice()),
("delete_backward", self.editor.delete_backward.as_slice()),
("delete_forward", self.editor.delete_forward.as_slice()),
(
"delete_backward_word",
self.editor.delete_backward_word.as_slice(),
),
(
"delete_forward_word",
self.editor.delete_forward_word.as_slice(),
),
("kill_line_start", self.editor.kill_line_start.as_slice()),
("kill_whole_line", self.editor.kill_whole_line.as_slice()),
("kill_line_end", self.editor.kill_line_end.as_slice()),
("yank", self.editor.yank.as_slice()),
],
)?;
validate_unique(
"vim_normal",
[
("enter_insert", self.vim_normal.enter_insert.as_slice()),
(
"append_after_cursor",
self.vim_normal.append_after_cursor.as_slice(),
),
(
"append_line_end",
self.vim_normal.append_line_end.as_slice(),
),
(
"insert_line_start",
self.vim_normal.insert_line_start.as_slice(),
),
(
"open_line_below",
self.vim_normal.open_line_below.as_slice(),
),
(
"open_line_above",
self.vim_normal.open_line_above.as_slice(),
),
("move_left", self.vim_normal.move_left.as_slice()),
("move_right", self.vim_normal.move_right.as_slice()),
("move_up", self.vim_normal.move_up.as_slice()),
("move_down", self.vim_normal.move_down.as_slice()),
(
"move_word_forward",
self.vim_normal.move_word_forward.as_slice(),
),
(
"move_word_backward",
self.vim_normal.move_word_backward.as_slice(),
),
("move_word_end", self.vim_normal.move_word_end.as_slice()),
(
"move_line_start",
self.vim_normal.move_line_start.as_slice(),
),
("move_line_end", self.vim_normal.move_line_end.as_slice()),
("delete_char", self.vim_normal.delete_char.as_slice()),
(
"substitute_char",
self.vim_normal.substitute_char.as_slice(),
),
(
"delete_to_line_end",
self.vim_normal.delete_to_line_end.as_slice(),
),
(
"change_to_line_end",
self.vim_normal.change_to_line_end.as_slice(),
),
("yank_line", self.vim_normal.yank_line.as_slice()),
("paste_after", self.vim_normal.paste_after.as_slice()),
(
"start_delete_operator",
self.vim_normal.start_delete_operator.as_slice(),
),
(
"start_yank_operator",
self.vim_normal.start_yank_operator.as_slice(),
),
(
"start_change_operator",
self.vim_normal.start_change_operator.as_slice(),
),
(
"cancel_operator",
self.vim_normal.cancel_operator.as_slice(),
),
],
)?;
validate_unique(
"vim_operator",
[
("delete_line", self.vim_operator.delete_line.as_slice()),
("yank_line", self.vim_operator.yank_line.as_slice()),
("motion_left", self.vim_operator.motion_left.as_slice()),
("motion_right", self.vim_operator.motion_right.as_slice()),
("motion_up", self.vim_operator.motion_up.as_slice()),
("motion_down", self.vim_operator.motion_down.as_slice()),
(
"motion_word_forward",
self.vim_operator.motion_word_forward.as_slice(),
),
(
"motion_word_backward",
self.vim_operator.motion_word_backward.as_slice(),
),
(
"motion_word_end",
self.vim_operator.motion_word_end.as_slice(),
),
(
"motion_line_start",
self.vim_operator.motion_line_start.as_slice(),
),
(
"motion_line_end",
self.vim_operator.motion_line_end.as_slice(),
),
(
"select_inner_text_object",
self.vim_operator.select_inner_text_object.as_slice(),
),
(
"select_around_text_object",
self.vim_operator.select_around_text_object.as_slice(),
),
("cancel", self.vim_operator.cancel.as_slice()),
],
)?;
validate_unique(
"vim_text_object",
[
("word", self.vim_text_object.word.as_slice()),
("big_word", self.vim_text_object.big_word.as_slice()),
("parentheses", self.vim_text_object.parentheses.as_slice()),
("brackets", self.vim_text_object.brackets.as_slice()),
("braces", self.vim_text_object.braces.as_slice()),
("double_quote", self.vim_text_object.double_quote.as_slice()),
("single_quote", self.vim_text_object.single_quote.as_slice()),
("backtick", self.vim_text_object.backtick.as_slice()),
("cancel", self.vim_text_object.cancel.as_slice()),
],
)?;
validate_unique(
"pager",
[
("scroll_up", self.pager.scroll_up.as_slice()),
("scroll_down", self.pager.scroll_down.as_slice()),
("page_up", self.pager.page_up.as_slice()),
("page_down", self.pager.page_down.as_slice()),
("half_page_up", self.pager.half_page_up.as_slice()),
("half_page_down", self.pager.half_page_down.as_slice()),
("jump_top", self.pager.jump_top.as_slice()),
("jump_bottom", self.pager.jump_bottom.as_slice()),
("close", self.pager.close.as_slice()),
("close_transcript", self.pager.close_transcript.as_slice()),
],
)?;
validate_no_reserved(
"pager",
[
("scroll_up", self.pager.scroll_up.as_slice()),
("scroll_down", self.pager.scroll_down.as_slice()),
("page_up", self.pager.page_up.as_slice()),
("page_down", self.pager.page_down.as_slice()),
("half_page_up", self.pager.half_page_up.as_slice()),
("half_page_down", self.pager.half_page_down.as_slice()),
("jump_top", self.pager.jump_top.as_slice()),
("jump_bottom", self.pager.jump_bottom.as_slice()),
("close", self.pager.close.as_slice()),
("close_transcript", self.pager.close_transcript.as_slice()),
],
TRANSCRIPT_BACKTRACK_RESERVED_BINDINGS,
[],
)?;
validate_unique(
"list",
[
("move_up", self.list.move_up.as_slice()),
("move_down", self.list.move_down.as_slice()),
("move_left", self.list.move_left.as_slice()),
("move_right", self.list.move_right.as_slice()),
("page_up", self.list.page_up.as_slice()),
("page_down", self.list.page_down.as_slice()),
("jump_top", self.list.jump_top.as_slice()),
("jump_bottom", self.list.jump_bottom.as_slice()),
("accept", self.list.accept.as_slice()),
("cancel", self.list.cancel.as_slice()),
],
)?;
validate_unique(
"approval",
[
("open_fullscreen", self.approval.open_fullscreen.as_slice()),
("open_thread", self.approval.open_thread.as_slice()),
("approve", self.approval.approve.as_slice()),
(
"approve_for_session",
self.approval.approve_for_session.as_slice(),
),
(
"approve_for_prefix",
self.approval.approve_for_prefix.as_slice(),
),
("deny", self.approval.deny.as_slice()),
("decline", self.approval.decline.as_slice()),
("cancel", self.approval.cancel.as_slice()),
],
)?;
let mut seen: HashMap<(KeyCode, KeyModifiers), &'static str> = HashMap::new();
for (action, bindings) in [
("list.move_up", self.list.move_up.as_slice()),
("list.move_down", self.list.move_down.as_slice()),
("list.move_left", self.list.move_left.as_slice()),
("list.move_right", self.list.move_right.as_slice()),
("list.page_up", self.list.page_up.as_slice()),
("list.page_down", self.list.page_down.as_slice()),
("list.jump_top", self.list.jump_top.as_slice()),
("list.jump_bottom", self.list.jump_bottom.as_slice()),
("list.accept", self.list.accept.as_slice()),
("list.cancel", self.list.cancel.as_slice()),
(
"approval.open_fullscreen",
self.approval.open_fullscreen.as_slice(),
),
("approval.open_thread", self.approval.open_thread.as_slice()),
("approval.approve", self.approval.approve.as_slice()),
(
"approval.approve_for_session",
self.approval.approve_for_session.as_slice(),
),
(
"approval.approve_for_prefix",
self.approval.approve_for_prefix.as_slice(),
),
("approval.deny", self.approval.deny.as_slice()),
("approval.decline", self.approval.decline.as_slice()),
("approval.cancel", self.approval.cancel.as_slice()),
] {
for binding in bindings {
let key = binding.parts();
if let Some(previous) = seen.insert(key, action) {
// Approval overlays intentionally reserve Esc as a stable
// cancellation path even though decline options may also
// display it in contexts where that is safe.
if previous == "list.cancel"
&& action == "approval.decline"
&& key == (KeyCode::Esc, KeyModifiers::NONE)
{
continue;
}
return Err(format!(
"Ambiguous approval overlay keymap bindings: `{previous}` and `{action}` use the same key. \
Set unique keys in `~/.codex/config.toml` and retry. \
See the Codex keymap documentation for supported actions and examples."
));
}
}
}
Ok(())
}