#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeybindingSection {
Navigation,
Editing,
Submission,
Modes,
Sessions,
Clipboard,
Help,
}
impl KeybindingSection {
pub fn label(self) -> &'static str {
match self {
Self::Navigation => "Navigation",
Self::Editing => "Input editing",
Self::Submission => "Actions",
Self::Modes => "Modes",
Self::Sessions => "Sessions",
Self::Clipboard => "Clipboard",
Self::Help => "Help",
}
}
pub fn rank(self) -> u8 {
match self {
Self::Navigation => 0,
Self::Editing => 1,
Self::Submission => 2,
Self::Modes => 3,
Self::Sessions => 4,
Self::Clipboard => 5,
Self::Help => 6,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct KeybindingEntry {
pub chord: &'static str,
pub description: &'static str,
pub section: KeybindingSection,
}
pub const KEYBINDINGS: &[KeybindingEntry] = &[
KeybindingEntry {
chord: "↑ / ↓",
description: "Scroll transcript or navigate input history",
section: KeybindingSection::Navigation,
},
KeybindingEntry {
chord: "Ctrl+↑ / Ctrl+↓",
description: "Navigate input history",
section: KeybindingSection::Navigation,
},
KeybindingEntry {
chord: "Alt+↑ / Alt+↓",
description: "Scroll transcript",
section: KeybindingSection::Navigation,
},
KeybindingEntry {
chord: "PgUp / PgDn",
description: "Scroll transcript by page",
section: KeybindingSection::Navigation,
},
KeybindingEntry {
chord: "Home / End",
description: "Jump to top / bottom of transcript",
section: KeybindingSection::Navigation,
},
KeybindingEntry {
chord: "g / G",
description: "Jump to top / bottom (when input is empty)",
section: KeybindingSection::Navigation,
},
KeybindingEntry {
chord: "[ / ]",
description: "Jump between tool output blocks",
section: KeybindingSection::Navigation,
},
KeybindingEntry {
chord: "← / →",
description: "Move cursor in composer",
section: KeybindingSection::Editing,
},
KeybindingEntry {
chord: "Ctrl+A / Ctrl+E",
description: "Jump to start / end of line",
section: KeybindingSection::Editing,
},
KeybindingEntry {
chord: "Backspace / Delete",
description: "Delete character before / after the cursor",
section: KeybindingSection::Editing,
},
KeybindingEntry {
chord: "Ctrl+U",
description: "Clear the current draft",
section: KeybindingSection::Editing,
},
KeybindingEntry {
chord: "Ctrl+J / Alt+Enter",
description: "Insert a newline in the composer",
section: KeybindingSection::Editing,
},
KeybindingEntry {
chord: "Enter",
description: "Send the current draft",
section: KeybindingSection::Submission,
},
KeybindingEntry {
chord: "Esc",
description: "Close menu, cancel request, discard draft, or clear input",
section: KeybindingSection::Submission,
},
KeybindingEntry {
chord: "Ctrl+C",
description: "Cancel request, or exit when nothing is running",
section: KeybindingSection::Submission,
},
KeybindingEntry {
chord: "Ctrl+D",
description: "Exit when input is empty",
section: KeybindingSection::Submission,
},
KeybindingEntry {
chord: "Ctrl+K",
description: "Open the command palette",
section: KeybindingSection::Submission,
},
KeybindingEntry {
chord: "Ctrl+P",
description: "Open the fuzzy file picker (insert @path on Enter)",
section: KeybindingSection::Submission,
},
KeybindingEntry {
chord: "l",
description: "Open pager for the last message (when input is empty)",
section: KeybindingSection::Submission,
},
KeybindingEntry {
chord: "v",
description: "Open details for the selected tool or message (when input is empty)",
section: KeybindingSection::Submission,
},
KeybindingEntry {
chord: "Alt+V",
description: "Open tool-details pager",
section: KeybindingSection::Submission,
},
KeybindingEntry {
chord: "Ctrl+O",
description: "Open thinking pager",
section: KeybindingSection::Submission,
},
KeybindingEntry {
chord: "Ctrl+T",
description: "Open live transcript overlay (sticky-tail auto-scroll)",
section: KeybindingSection::Submission,
},
KeybindingEntry {
chord: "Esc Esc",
description: "Backtrack to a previous user message (Left/Right step, Enter to rewind)",
section: KeybindingSection::Submission,
},
KeybindingEntry {
chord: "Tab / Shift+Tab",
description: "Complete /command or cycle modes (Shift+Tab cycles reasoning effort)",
section: KeybindingSection::Modes,
},
KeybindingEntry {
chord: "Alt+1 / Alt+2 / Alt+3",
description: "Jump directly to Plan / Agent / YOLO mode",
section: KeybindingSection::Modes,
},
KeybindingEntry {
chord: "Alt+P / Alt+A / Alt+Y",
description: "Alternative jump to Plan / Agent / YOLO mode",
section: KeybindingSection::Modes,
},
KeybindingEntry {
chord: "Alt+! / Alt+@ / Alt+# / Alt+$ / Alt+)",
description: "Focus Plan / Todos / Tasks / Agents / Auto sidebar",
section: KeybindingSection::Modes,
},
KeybindingEntry {
chord: "Ctrl+X",
description: "Toggle between Plan and Agent modes",
section: KeybindingSection::Modes,
},
KeybindingEntry {
chord: "Ctrl+R",
description: "Open the session picker",
section: KeybindingSection::Sessions,
},
KeybindingEntry {
chord: "Ctrl+V",
description: "Paste text or attach a clipboard image",
section: KeybindingSection::Clipboard,
},
KeybindingEntry {
chord: "Ctrl+Shift+C",
description: "Copy the current selection (Cmd+C on macOS)",
section: KeybindingSection::Clipboard,
},
KeybindingEntry {
chord: "@path",
description: "Add a local text file or directory to context",
section: KeybindingSection::Clipboard,
},
KeybindingEntry {
chord: "?",
description: "Open this help overlay (when input is empty)",
section: KeybindingSection::Help,
},
KeybindingEntry {
chord: "F1",
description: "Toggle help overlay",
section: KeybindingSection::Help,
},
KeybindingEntry {
chord: "Ctrl+/",
description: "Toggle help overlay",
section: KeybindingSection::Help,
},
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn catalog_is_non_empty_and_sections_have_entries() {
assert!(!KEYBINDINGS.is_empty());
let sections = [
KeybindingSection::Navigation,
KeybindingSection::Editing,
KeybindingSection::Submission,
KeybindingSection::Modes,
KeybindingSection::Sessions,
KeybindingSection::Clipboard,
KeybindingSection::Help,
];
for section in sections {
assert!(
KEYBINDINGS.iter().any(|entry| entry.section == section),
"no entries for section {:?}",
section
);
}
}
#[test]
fn help_section_documents_question_mark() {
assert!(
KEYBINDINGS
.iter()
.any(|entry| entry.chord.contains('?') && entry.section == KeybindingSection::Help),
"`?` must remain documented as the help-toggle chord"
);
}
#[test]
fn section_rank_is_a_total_order() {
let sections = [
KeybindingSection::Navigation,
KeybindingSection::Editing,
KeybindingSection::Submission,
KeybindingSection::Modes,
KeybindingSection::Sessions,
KeybindingSection::Clipboard,
KeybindingSection::Help,
];
let mut ranks: Vec<u8> = sections.iter().map(|s| s.rank()).collect();
ranks.sort_unstable();
ranks.dedup();
assert_eq!(ranks.len(), sections.len(), "ranks must be unique");
}
}