use std::borrow::Cow;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use crate::tui::key_shortcuts;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ShellBindingId {
ToolDetails,
ContextInspector,
Help,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ShellBinding {
pub id: ShellBindingId,
pub catalog_chord: &'static str,
pub footer_chord: &'static str,
}
pub const SHELL_BINDINGS: &[ShellBinding] = &[
ShellBinding {
id: ShellBindingId::ToolDetails,
catalog_chord: "Alt+V",
footer_chord: "Alt+V",
},
ShellBinding {
id: ShellBindingId::ContextInspector,
catalog_chord: "/context",
footer_chord: "/context",
},
ShellBinding {
id: ShellBindingId::Help,
catalog_chord: "F1 / Ctrl+/",
footer_chord: "F1",
},
];
#[must_use]
pub fn binding(id: ShellBindingId) -> &'static ShellBinding {
SHELL_BINDINGS
.iter()
.find(|binding| binding.id == id)
.expect("shell binding catalog is exhaustive")
}
#[must_use]
pub fn display_chord(chord: &'static str) -> Cow<'static, str> {
display_chord_for_platform(chord, cfg!(target_os = "macos"))
}
#[must_use]
pub fn display_chord_for_platform(chord: &'static str, is_macos: bool) -> Cow<'static, str> {
if !is_macos {
return Cow::Borrowed(chord);
}
let rendered = chord.replace("Alt+", "⌥").replace("F1", "fn+F1");
if rendered == chord {
Cow::Borrowed(chord)
} else {
Cow::Owned(rendered)
}
}
#[must_use]
pub fn footer_action_hints(include_context: bool) -> String {
footer_action_hints_for_platform(include_context, cfg!(target_os = "macos"))
}
#[must_use]
pub fn footer_action_hints_for_platform(include_context: bool, is_macos: bool) -> String {
let details =
display_chord_for_platform(binding(ShellBindingId::ToolDetails).footer_chord, is_macos);
let help = display_chord_for_platform(binding(ShellBindingId::Help).footer_chord, is_macos);
if include_context {
format!(
"{details}:{{output}} · {}:{{context}} · {help}:{{keys}}",
binding(ShellBindingId::ContextInspector).footer_chord
)
} else {
format!("{details}:{{output}} · {help}:{{keys}}")
}
}
#[must_use]
pub fn is_tool_details_shortcut(key: &KeyEvent) -> bool {
if key_shortcuts::is_macos_option_v_legacy_key(key) {
return true;
}
matches!(key.code, KeyCode::Char('v') | KeyCode::Char('V'))
&& key_shortcuts::alt_nav_modifiers(key.modifiers)
}
#[must_use]
pub fn is_context_inspector_shortcut(key: &KeyEvent) -> bool {
matches!(key.code, KeyCode::Char('c') | KeyCode::Char('C'))
&& key_shortcuts::alt_nav_modifiers(key.modifiers)
}
#[must_use]
pub fn is_help_shortcut(key: &KeyEvent) -> bool {
if matches!(key.code, KeyCode::F(1)) {
return true;
}
if matches!(key.code, KeyCode::Char('/')) && key.modifiers.contains(KeyModifiers::CONTROL) {
return true;
}
if matches!(key.code, KeyCode::Char('7') | KeyCode::Char('_'))
&& key.modifiers.contains(KeyModifiers::CONTROL)
{
return true;
}
matches!(key.code, KeyCode::Char('?')) && key_shortcuts::alt_nav_modifiers(key.modifiers)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bare_v_is_never_a_shortcut_in_any_state() {
let plain_v = KeyEvent::new(KeyCode::Char('v'), KeyModifiers::NONE);
assert!(!is_tool_details_shortcut(&plain_v));
let plain_upper_v = KeyEvent::new(KeyCode::Char('V'), KeyModifiers::SHIFT);
assert!(!is_tool_details_shortcut(&plain_upper_v));
}
#[test]
fn alt_v_and_macos_option_v_open_details() {
let alt_v = KeyEvent::new(KeyCode::Char('v'), KeyModifiers::ALT);
assert!(is_tool_details_shortcut(&alt_v));
let alt_upper_v = KeyEvent::new(KeyCode::Char('V'), KeyModifiers::ALT);
assert!(is_tool_details_shortcut(&alt_upper_v));
}
#[test]
fn details_label_is_option_glyph_on_macos_and_alt_elsewhere() {
assert_eq!(display_chord_for_platform("Alt+V", true), "⌥V");
assert_eq!(display_chord_for_platform("Alt+V", false), "Alt+V");
let macos = footer_action_hints_for_platform(true, true);
assert!(macos.starts_with("⌥V:"), "{macos}");
assert!(!macos.contains("Alt"), "{macos}");
assert!(!macos.contains("Cmd"), "{macos}");
let other = footer_action_hints_for_platform(true, false);
assert!(other.starts_with("Alt+V:"), "{other}");
}
#[test]
fn footer_hints_never_advertise_bare_v_alt_question_or_alt_c() {
for is_macos in [true, false] {
for include_context in [true, false] {
let hints = footer_action_hints_for_platform(include_context, is_macos);
assert!(!hints.starts_with("v:"), "{hints}");
assert!(!hints.contains(" v:"), "{hints}");
assert!(!hints.contains("Alt+?"), "{hints}");
assert!(!hints.contains("Alt+C"), "{hints}");
assert!(hints.contains("F1:"), "{hints}");
if is_macos {
assert!(hints.contains("fn+F1:"), "{hints}");
}
if include_context {
assert!(hints.contains("/context:"), "{hints}");
}
}
}
}
#[test]
fn help_accepts_f1_ctrl_slash_and_unadvertised_fallbacks() {
assert!(is_help_shortcut(&KeyEvent::new(
KeyCode::F(1),
KeyModifiers::NONE
)));
assert!(is_help_shortcut(&KeyEvent::new(
KeyCode::Char('/'),
KeyModifiers::CONTROL
)));
assert!(is_help_shortcut(&KeyEvent::new(
KeyCode::Char('7'),
KeyModifiers::CONTROL
)));
assert!(is_help_shortcut(&KeyEvent::new(
KeyCode::Char('_'),
KeyModifiers::CONTROL
)));
assert!(is_help_shortcut(&KeyEvent::new(
KeyCode::Char('?'),
KeyModifiers::ALT
)));
let inverted_question = KeyEvent::new(KeyCode::Char('\u{00bf}'), KeyModifiers::NONE);
assert!(!is_help_shortcut(&inverted_question));
}
#[test]
fn context_accepts_explicit_alt_c_without_stealing_layout_characters() {
let alt_c = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::ALT);
assert!(is_context_inspector_shortcut(&alt_c));
let cedilla = KeyEvent::new(KeyCode::Char('\u{00e7}'), KeyModifiers::NONE);
assert!(!is_context_inspector_shortcut(¸la));
}
#[test]
fn catalog_chords_match_final_contract() {
assert_eq!(binding(ShellBindingId::Help).catalog_chord, "F1 / Ctrl+/");
assert_eq!(
binding(ShellBindingId::ContextInspector).catalog_chord,
"/context"
);
assert_eq!(binding(ShellBindingId::ToolDetails).catalog_chord, "Alt+V");
for binding in SHELL_BINDINGS {
assert!(!binding.catalog_chord.contains("Alt+?"));
assert_ne!(binding.catalog_chord, "v");
assert!(!binding.catalog_chord.starts_with("v /"));
assert!(!binding.footer_chord.contains("Alt+?"));
assert_ne!(binding.footer_chord, "v");
}
}
}