use std::borrow::Cow;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeybindingSection {
Navigation,
Editing,
Submission,
Modes,
Sessions,
Clipboard,
Help,
}
impl KeybindingSection {
pub fn label(self, locale: crate::localization::Locale) -> Cow<'static, str> {
use crate::localization::{MessageId, tr};
let id = match self {
Self::Navigation => MessageId::HelpSectionNavigation,
Self::Editing => MessageId::HelpSectionEditing,
Self::Submission => MessageId::HelpSectionActions,
Self::Modes => MessageId::HelpSectionModes,
Self::Sessions => MessageId::HelpSectionSessions,
Self::Clipboard => MessageId::HelpSectionClipboard,
Self::Help => MessageId::HelpSectionHelp,
};
tr(locale, id)
}
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_id: crate::localization::MessageId,
pub section: KeybindingSection,
}
pub const KEYBINDINGS: &[KeybindingEntry] = &[
KeybindingEntry {
chord: "↑ / ↓",
description_id: crate::localization::MessageId::KbScrollTranscript,
section: KeybindingSection::Navigation,
},
KeybindingEntry {
chord: "Alt+↑ / Alt+↓",
description_id: crate::localization::MessageId::KbScrollTranscriptAlt,
section: KeybindingSection::Navigation,
},
KeybindingEntry {
chord: "Shift+↑ / Shift+↓",
description_id: crate::localization::MessageId::KbBrowseHistory,
section: KeybindingSection::Navigation,
},
KeybindingEntry {
chord: "PgUp / PgDn",
description_id: crate::localization::MessageId::KbScrollPage,
section: KeybindingSection::Navigation,
},
KeybindingEntry {
chord: "Ctrl+Home / Ctrl+End",
description_id: crate::localization::MessageId::KbJumpTopBottom,
section: KeybindingSection::Navigation,
},
KeybindingEntry {
chord: "Alt+G / Alt+Shift+G",
description_id: crate::localization::MessageId::KbJumpTopBottomEmpty,
section: KeybindingSection::Navigation,
},
KeybindingEntry {
chord: "Alt+[ / Alt+]",
description_id: crate::localization::MessageId::KbJumpToolBlocks,
section: KeybindingSection::Navigation,
},
KeybindingEntry {
chord: "← / → / Ctrl+←/→ / Alt+←/→",
description_id: crate::localization::MessageId::KbMoveCursor,
section: KeybindingSection::Editing,
},
KeybindingEntry {
chord: "Home / End",
description_id: crate::localization::MessageId::KbJumpLineStartEnd,
section: KeybindingSection::Editing,
},
KeybindingEntry {
chord: "Ctrl+A / Ctrl+E",
description_id: crate::localization::MessageId::KbJumpLineStartEnd,
section: KeybindingSection::Editing,
},
KeybindingEntry {
chord: "Backspace / Delete",
description_id: crate::localization::MessageId::KbDeleteChar,
section: KeybindingSection::Editing,
},
KeybindingEntry {
chord: "Ctrl+W / Ctrl+Backspace / Alt+Backspace",
description_id: crate::localization::MessageId::KbDeleteWord,
section: KeybindingSection::Editing,
},
KeybindingEntry {
chord: "Ctrl+Y",
description_id: crate::localization::MessageId::KbYank,
section: KeybindingSection::Editing,
},
KeybindingEntry {
chord: "Ctrl+Shift+E / Cmd+Shift+E",
description_id: crate::localization::MessageId::KbToggleFileTree,
section: KeybindingSection::Navigation,
},
KeybindingEntry {
chord: "Shift+←/→ / Shift+Home/End / Ctrl+Shift+←/→ / Alt+Shift+←/→ / Ctrl+Shift+Home/End",
description_id: crate::localization::MessageId::KbSelectText,
section: KeybindingSection::Editing,
},
KeybindingEntry {
chord: "Ctrl+Shift+A / Cmd+A",
description_id: crate::localization::MessageId::KbSelectAllDraft,
section: KeybindingSection::Editing,
},
KeybindingEntry {
chord: "Ctrl+U",
description_id: crate::localization::MessageId::KbClearDraft,
section: KeybindingSection::Editing,
},
KeybindingEntry {
chord: "Ctrl+Z",
description_id: crate::localization::MessageId::KbRestoreClearedDraft,
section: KeybindingSection::Editing,
},
KeybindingEntry {
chord: "Ctrl+G / Ctrl+S",
description_id: crate::localization::MessageId::KbStashDraft,
section: KeybindingSection::Editing,
},
KeybindingEntry {
chord: "Alt+R",
description_id: crate::localization::MessageId::KbSearchHistory,
section: KeybindingSection::Editing,
},
KeybindingEntry {
chord: "Ctrl+J / Alt+Enter / Shift+Enter",
description_id: crate::localization::MessageId::KbInsertNewline,
section: KeybindingSection::Editing,
},
KeybindingEntry {
chord: "Enter",
description_id: crate::localization::MessageId::KbSendDraft,
section: KeybindingSection::Submission,
},
KeybindingEntry {
chord: "Esc",
description_id: crate::localization::MessageId::KbCloseMenu,
section: KeybindingSection::Submission,
},
KeybindingEntry {
chord: "Ctrl+C",
description_id: crate::localization::MessageId::KbCancelOrExit,
section: KeybindingSection::Submission,
},
KeybindingEntry {
chord: "Ctrl+B",
description_id: crate::localization::MessageId::KbShellControls,
section: KeybindingSection::Submission,
},
KeybindingEntry {
chord: "Ctrl+D",
description_id: crate::localization::MessageId::KbExitEmpty,
section: KeybindingSection::Submission,
},
KeybindingEntry {
chord: "Ctrl+K",
description_id: crate::localization::MessageId::KbCommandPalette,
section: KeybindingSection::Submission,
},
KeybindingEntry {
chord: "F2",
description_id: crate::localization::MessageId::KbSettings,
section: KeybindingSection::Submission,
},
KeybindingEntry {
chord: "Ctrl+X (Activity sidebar)",
description_id: crate::localization::MessageId::KbCancelBackgroundShellJobs,
section: KeybindingSection::Submission,
},
KeybindingEntry {
chord: "Ctrl+P",
description_id: crate::localization::MessageId::KbFuzzyFilePicker,
section: KeybindingSection::Submission,
},
KeybindingEntry {
chord: "/context",
description_id: crate::localization::MessageId::KbCompactInspector,
section: KeybindingSection::Submission,
},
KeybindingEntry {
chord: "Alt+L",
description_id: crate::localization::MessageId::KbLastMessagePager,
section: KeybindingSection::Submission,
},
KeybindingEntry {
chord: "Alt+V",
description_id: crate::localization::MessageId::KbSelectedDetails,
section: KeybindingSection::Submission,
},
KeybindingEntry {
chord: "Ctrl+O",
description_id: crate::localization::MessageId::KbReasoningDetail,
section: KeybindingSection::Submission,
},
KeybindingEntry {
chord: "Ctrl+Alt+O",
description_id: crate::localization::MessageId::KbTurnInspector,
section: KeybindingSection::Submission,
},
KeybindingEntry {
chord: "Ctrl+Shift+O / F4",
description_id: crate::localization::MessageId::KbExternalEditor,
section: KeybindingSection::Editing,
},
KeybindingEntry {
chord: "/transcript / Ctrl+Shift+T",
description_id: crate::localization::MessageId::KbLiveTranscript,
section: KeybindingSection::Submission,
},
KeybindingEntry {
chord: "Ctrl+T",
description_id: crate::localization::MessageId::KbCycleThinking,
section: KeybindingSection::Modes,
},
KeybindingEntry {
chord: "Esc Esc",
description_id: crate::localization::MessageId::KbBacktrackMessage,
section: KeybindingSection::Submission,
},
KeybindingEntry {
chord: "Tab",
description_id: crate::localization::MessageId::KbCompleteCycleModes,
section: KeybindingSection::Modes,
},
KeybindingEntry {
chord: "Shift+Tab",
description_id: crate::localization::MessageId::KbCyclePermissions,
section: KeybindingSection::Modes,
},
KeybindingEntry {
chord: "Alt+1-8",
description_id: crate::localization::MessageId::KbJumpPlanAgentYolo,
section: KeybindingSection::Modes,
},
KeybindingEntry {
chord: "Alt+P / Alt+A / Alt+Y",
description_id: crate::localization::MessageId::KbAltJumpPlanAgentYolo,
section: KeybindingSection::Modes,
},
KeybindingEntry {
chord: "Alt+! / Alt+@ / Alt+# / Alt+$ / Alt+0 / Ctrl+Alt+0",
description_id: crate::localization::MessageId::KbFocusSidebar,
section: KeybindingSection::Modes,
},
KeybindingEntry {
chord: "Ctrl+R",
description_id: crate::localization::MessageId::KbSessionPicker,
section: KeybindingSection::Sessions,
},
KeybindingEntry {
chord: "Ctrl+L",
description_id: crate::localization::MessageId::KbCompactContext,
section: KeybindingSection::Sessions,
},
KeybindingEntry {
chord: "Cmd+V / Ctrl+Shift+V",
description_id: crate::localization::MessageId::KbTerminalPaste,
section: KeybindingSection::Clipboard,
},
KeybindingEntry {
chord: "Ctrl+V",
description_id: crate::localization::MessageId::KbPasteAttach,
section: KeybindingSection::Clipboard,
},
KeybindingEntry {
chord: "Ctrl+C (selection)",
description_id: crate::localization::MessageId::KbCopySelection,
section: KeybindingSection::Clipboard,
},
KeybindingEntry {
chord: "Right click",
description_id: crate::localization::MessageId::KbContextMenu,
section: KeybindingSection::Clipboard,
},
KeybindingEntry {
chord: "@path",
description_id: crate::localization::MessageId::KbAttachPath,
section: KeybindingSection::Clipboard,
},
KeybindingEntry {
chord: "F1 / Ctrl+/",
description_id: crate::localization::MessageId::KbHelpOverlay,
section: KeybindingSection::Help,
},
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn catalog_is_non_empty_and_sections_have_entries() {
assert!(KEYBINDINGS.iter().any(|entry| !entry.chord.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_advertises_f1_and_ctrl_slash_never_alt_question() {
assert!(
KEYBINDINGS.iter().any(|entry| {
entry.section == KeybindingSection::Help
&& entry.chord.contains("F1")
&& entry.chord.contains("Ctrl+/")
}),
"help must document F1 with the Ctrl+/ fallback"
);
assert!(
KEYBINDINGS
.iter()
.all(|entry| !entry.chord.contains("Alt+?")),
"Alt+? must not be advertised in the help catalog"
);
}
#[test]
fn composer_catalog_assigns_one_stable_role_to_each_chord() {
let chord_for = |id| {
KEYBINDINGS
.iter()
.find(|entry| entry.description_id == id)
.expect("composer binding should be documented")
.chord
};
assert_eq!(
chord_for(crate::localization::MessageId::KbInsertNewline),
"Ctrl+J / Alt+Enter / Shift+Enter"
);
assert!(
KEYBINDINGS
.iter()
.all(|entry| !entry.chord.contains("Ctrl+Enter")
&& !entry.chord.contains("Cmd+Enter"))
);
assert_eq!(
chord_for(crate::localization::MessageId::KbStashDraft),
"Ctrl+G / Ctrl+S"
);
assert_eq!(
chord_for(crate::localization::MessageId::KbSendDraft),
"Enter"
);
let tab_copy = crate::localization::tr(
crate::localization::Locale::En,
crate::localization::MessageId::KbCompleteCycleModes,
);
assert!(!tab_copy.to_ascii_lowercase().contains("queue"));
let stash_copy = crate::localization::tr(
crate::localization::Locale::En,
crate::localization::MessageId::KbStashDraft,
);
assert!(!stash_copy.to_ascii_lowercase().contains("send"));
}
#[test]
fn clipboard_help_distinguishes_terminal_text_graphical_image_and_in_app_copy() {
let terminal_paste = KEYBINDINGS
.iter()
.find(|entry| entry.description_id == crate::localization::MessageId::KbTerminalPaste)
.expect("terminal paste binding should be documented");
let graphical_paste = KEYBINDINGS
.iter()
.find(|entry| entry.description_id == crate::localization::MessageId::KbPasteAttach)
.expect("graphical paste binding should be documented");
let copy = KEYBINDINGS
.iter()
.find(|entry| entry.description_id == crate::localization::MessageId::KbCopySelection)
.expect("copy binding should be documented");
assert!(terminal_paste.chord.contains("Cmd+V"));
assert!(terminal_paste.chord.contains("Ctrl+Shift+V"));
assert_eq!(graphical_paste.chord, "Ctrl+V");
let terminal_description = crate::localization::tr(
crate::localization::Locale::En,
crate::localization::MessageId::KbTerminalPaste,
);
let graphical_description = crate::localization::tr(
crate::localization::Locale::En,
crate::localization::MessageId::KbPasteAttach,
);
assert!(!terminal_description.to_ascii_lowercase().contains("image"));
assert!(graphical_description.to_ascii_lowercase().contains("image"));
assert_eq!(copy.chord, "Ctrl+C (selection)");
assert!(!copy.chord.contains("Cmd+C"));
assert!(!copy.chord.contains("Ctrl+Shift+C"));
}
#[test]
fn transcript_navigation_catalog_does_not_advertise_bare_typing_keys() {
for stale in [
"g / G",
"[ / ]",
"l",
"?",
"Ctrl+↑ / Ctrl+↓",
"v",
"v / Alt+V",
] {
assert!(
KEYBINDINGS.iter().all(|entry| entry.chord != stale),
"stale handler-free chord remains documented: {stale}"
);
}
for wired in ["Alt+G / Alt+Shift+G", "Alt+[ / Alt+]", "Alt+L", "Alt+V"] {
assert!(
KEYBINDINGS.iter().any(|entry| entry.chord == wired),
"wired transcript shortcut missing from help: {wired}"
);
}
}
#[test]
fn live_transcript_documents_command_before_shaky_chord() {
let transcript = KEYBINDINGS
.iter()
.find(|entry| entry.description_id == crate::localization::MessageId::KbLiveTranscript)
.expect("live transcript entry should be documented");
assert_eq!(transcript.chord, "/transcript / Ctrl+Shift+T");
}
#[test]
fn shell_binding_source_matches_help_catalog_chords() {
use crate::tui::shell_key_routing::{ShellBindingId, binding};
assert_eq!(binding(ShellBindingId::ToolDetails).catalog_chord, "Alt+V");
assert_eq!(
binding(ShellBindingId::ContextInspector).catalog_chord,
"/context"
);
assert_eq!(binding(ShellBindingId::Help).catalog_chord, "F1 / Ctrl+/");
for id in [
ShellBindingId::ToolDetails,
ShellBindingId::ContextInspector,
ShellBindingId::Help,
] {
let chord = binding(id).catalog_chord;
assert!(
KEYBINDINGS
.iter()
.any(|entry| entry.chord == chord || entry.chord.contains(chord)),
"shell binding {id:?} chord missing from help catalog: {chord}"
);
}
}
#[test]
fn ctrl_o_and_ctrl_alt_o_help_copy_match_split_surfaces() {
let ctrl_o = KEYBINDINGS
.iter()
.find(|entry| entry.chord == "Ctrl+O")
.expect("Ctrl+O keybinding should be documented");
assert_eq!(
ctrl_o.description_id,
crate::localization::MessageId::KbReasoningDetail
);
assert_eq!(
crate::localization::tr(crate::localization::Locale::En, ctrl_o.description_id,),
"Open reasoning detail for the selected or current turn"
);
let ctrl_alt_o = KEYBINDINGS
.iter()
.find(|entry| entry.chord == "Ctrl+Alt+O")
.expect("Ctrl+Alt+O keybinding should be documented");
assert_eq!(
ctrl_alt_o.description_id,
crate::localization::MessageId::KbTurnInspector
);
assert_eq!(
crate::localization::tr(crate::localization::Locale::En, ctrl_alt_o.description_id,),
"Open Turn Inspector"
);
let editor = KEYBINDINGS
.iter()
.find(|entry| entry.chord == "Ctrl+Shift+O / F4")
.expect("external-editor keybinding should be documented");
assert_eq!(
crate::localization::tr(crate::localization::Locale::En, editor.description_id,),
"Open composer draft in external editor"
);
}
#[test]
fn ctrl_x_activity_sidebar_cancel_all_is_documented() {
let ctrl_x_activity = KEYBINDINGS
.iter()
.find(|entry| entry.chord == "Ctrl+X (Activity sidebar)")
.expect("Ctrl+X Activity sidebar keybinding should be documented");
assert_eq!(
ctrl_x_activity.description_id,
crate::localization::MessageId::KbCancelBackgroundShellJobs
);
}
#[test]
fn tool_details_documents_alt_v_only_never_bare_v() {
let selected_details = KEYBINDINGS
.iter()
.filter(|entry| {
entry.description_id == crate::localization::MessageId::KbSelectedDetails
})
.map(|entry| entry.chord)
.collect::<Vec<_>>();
assert_eq!(selected_details, vec!["Alt+V"]);
assert!(
KEYBINDINGS
.iter()
.all(|entry| entry.chord != "v" && !entry.chord.starts_with("v /")),
"bare `v` must not be advertised — composer typing owns it"
);
}
#[test]
fn every_advertised_key_names_exactly_one_canonical_action() {
struct Use {
chord: &'static str,
alternative: String,
description_id: crate::localization::MessageId,
}
let mut uses_by_key: std::collections::BTreeMap<String, Vec<Use>> =
std::collections::BTreeMap::new();
for entry in KEYBINDINGS {
for alternative in entry.chord.split(" / ") {
let alternative = alternative.trim();
let base = alternative
.split_once(" (")
.map(|(head, _)| head)
.unwrap_or(alternative)
.trim()
.to_string();
uses_by_key.entry(base).or_default().push(Use {
chord: entry.chord,
alternative: alternative.to_string(),
description_id: entry.description_id,
});
}
}
for (key, uses) in &uses_by_key {
if uses.len() == 1 {
continue;
}
let single_action = uses
.iter()
.all(|entry| entry.description_id == uses[0].description_id);
if single_action {
continue;
}
let unqualified: Vec<&str> = uses
.iter()
.filter(|entry| !entry.alternative.contains('('))
.map(|entry| entry.chord)
.collect();
assert!(
unqualified.len() <= 1,
"{key} is advertised for more than one action without naming the \
context that selects between them: {unqualified:?}"
);
}
}
#[test]
fn stash_chords_advertise_stashing_and_nothing_else() {
let stash_entries: Vec<&KeybindingEntry> = KEYBINDINGS
.iter()
.filter(|entry| {
entry
.chord
.split(" / ")
.map(str::trim)
.any(|chord| matches!(chord, "Ctrl+G" | "Ctrl+S"))
})
.collect();
assert_eq!(
stash_entries.len(),
1,
"Ctrl+G / Ctrl+S must be documented exactly once, together"
);
assert_eq!(stash_entries[0].chord, "Ctrl+G / Ctrl+S");
assert_eq!(
stash_entries[0].description_id,
crate::localization::MessageId::KbStashDraft
);
let copy = crate::localization::tr(
crate::localization::Locale::En,
crate::localization::MessageId::KbStashDraft,
)
.to_ascii_lowercase();
for forbidden in ["send", "queue", "steer", "submit", "save"] {
assert!(
!copy.contains(forbidden),
"stash copy must not read as {forbidden:?}: {copy:?}"
);
}
assert!(
copy.contains("stash"),
"stash copy must name the action it performs: {copy:?}"
);
}
#[test]
fn running_turn_verbs_belong_to_one_chord_each() {
let entry_for = |id| {
KEYBINDINGS
.iter()
.find(|entry| entry.description_id == id)
.expect("running-turn binding should be documented")
};
assert_eq!(
entry_for(crate::localization::MessageId::KbSendDraft).chord,
"Enter"
);
assert!(
KEYBINDINGS
.iter()
.all(|entry| !entry.chord.contains("Ctrl+Enter")
&& !entry.chord.contains("Cmd+Enter"))
);
assert_eq!(
entry_for(crate::localization::MessageId::KbInsertNewline).chord,
"Ctrl+J / Alt+Enter / Shift+Enter"
);
let newline_copy = crate::localization::tr(
crate::localization::Locale::En,
crate::localization::MessageId::KbInsertNewline,
)
.to_ascii_lowercase();
for forbidden in ["send", "steer", "queue"] {
assert!(
!newline_copy.contains(forbidden),
"newline chords must not read as {forbidden:?}: {newline_copy:?}"
);
}
}
#[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");
}
const KEYBINDINGS_DOC: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../docs/KEYBINDINGS.md"
));
const DOC_CHORD_ALLOWLIST: &[&str] = &[
"ctrlenter",
"ctrlclick",
"ctrln",
];
fn normalize_chord(raw: &str) -> String {
raw.to_lowercase()
.replace("option", "alt")
.replace("cmd", "ctrl")
.chars()
.filter(|c| !matches!(c, '-' | '+' | ' ' | '`'))
.collect()
}
fn expand_digit_family(lowered: &str) -> Option<Vec<String>> {
let (head, tail) = lowered.rsplit_once('-')?;
let end: u32 = tail.parse().ok()?;
let start: u32 = head.chars().next_back()?.to_digit(10)?;
let prefix = &head[..head.len() - 1];
if !matches!(prefix, "alt+" | "ctrl+" | "shift+") || start > end {
return None;
}
Some(
(start..=end)
.map(|digit| normalize_chord(&format!("{prefix}{digit}")))
.collect(),
)
}
fn segment_atoms(segment: &str, out: &mut Vec<String>) {
let lowered = segment
.trim()
.to_lowercase()
.replace("option", "alt")
.replace("cmd", "ctrl");
if lowered.is_empty() {
return;
}
if let Some(family) = expand_digit_family(&lowered) {
out.extend(family);
return;
}
let mut prefix = String::new();
let mut rest = lowered.as_str();
loop {
let mut consumed = false;
for modifier in ["ctrl+", "alt+", "shift+", "ctrl-", "alt-", "shift-"] {
if let Some(stripped) = rest.strip_prefix(modifier) {
prefix.push_str(&modifier[..modifier.len() - 1]);
prefix.push('+');
rest = stripped;
consumed = true;
break;
}
}
if !consumed {
break;
}
}
let parts: Vec<&str> = rest.split('/').collect();
if !prefix.is_empty() && parts.len() > 1 && parts.iter().all(|p| !p.is_empty()) {
for part in parts {
out.push(normalize_chord(&format!("{prefix}{part}")));
}
} else {
out.push(normalize_chord(&lowered));
}
}
fn catalog_chord_atoms() -> Vec<String> {
let mut out = Vec::new();
for entry in KEYBINDINGS {
let chord = entry.chord.split(" (").next().unwrap_or(entry.chord);
for segment in chord.split(" / ") {
let segment = segment.trim();
if segment.starts_with('/') || segment.starts_with('@') || segment.contains("click")
{
continue;
}
segment_atoms(segment, &mut out);
}
}
out
}
fn doc_table_chord_atoms() -> Vec<String> {
const NAMED: &[&str] = &[
"tab",
"esc",
"enter",
"backspace",
"delete",
"home",
"end",
"pgup",
"pgdn",
"↑",
"↓",
"←",
"→",
];
let mut out = Vec::new();
for line in KEYBINDINGS_DOC.lines() {
if !line.trim_start().starts_with('|') {
continue;
}
for token in line.split('`').skip(1).step_by(2) {
let lowered = token.to_lowercase();
if lowered.starts_with('/') || lowered.starts_with('@') || lowered.starts_with('!')
{
continue;
}
let has_modifier = ["ctrl", "alt", "shift", "option", "cmd"]
.iter()
.any(|m| lowered.contains(m));
let is_named = NAMED.contains(&lowered.as_str())
|| (lowered.len() == 2
&& lowered.starts_with('f')
&& lowered[1..].parse::<u8>().is_ok());
let is_named_combo = lowered.contains(' ')
&& lowered.split(' ').all(|part| {
let part = part.trim();
NAMED.contains(&part)
|| ["ctrl", "alt", "shift", "option", "cmd"]
.iter()
.any(|m| part.contains(m))
});
if has_modifier || is_named || is_named_combo {
segment_atoms(token, &mut out);
}
}
}
out
}
#[test]
fn keybindings_md_and_help_catalog_do_not_drift() {
use std::collections::BTreeSet;
let catalog: BTreeSet<String> = catalog_chord_atoms().into_iter().collect();
let doc_atoms: BTreeSet<String> = doc_table_chord_atoms().into_iter().collect();
let allowlist: BTreeSet<&str> = DOC_CHORD_ALLOWLIST.iter().copied().collect();
let undocumented: Vec<&String> = doc_atoms
.iter()
.filter(|atom| !catalog.contains(*atom) && !allowlist.contains(atom.as_str()))
.collect();
assert!(
undocumented.is_empty(),
"docs/KEYBINDINGS.md advertises chords missing from the KEYBINDINGS \
catalog — add the binding, fix the docs, or justify an allowlist entry: \
{undocumented:?}"
);
let normalized_doc = normalize_chord(KEYBINDINGS_DOC);
let undocumented: Vec<&String> = catalog
.iter()
.filter(|atom| !doc_atoms.contains(*atom) && !normalized_doc.contains(atom.as_str()))
.collect();
assert!(
undocumented.is_empty(),
"KEYBINDINGS catalog advertises chords absent from docs/KEYBINDINGS.md \
— document the chord or remove it from the catalog: {undocumented:?}"
);
}
}