use crate::element::{Component, Element};
use crate::style::{Color, Modifier, Style};
#[derive(Debug, Clone)]
pub struct KeyHint {
pub key: String,
pub action: String,
}
impl KeyHint {
pub fn new(key: impl Into<String>, action: impl Into<String>) -> Self {
Self {
key: key.into(),
action: action.into(),
}
}
}
impl<K: Into<String>, A: Into<String>> From<(K, A)> for KeyHint {
fn from((key, action): (K, A)) -> Self {
KeyHint::new(key, action)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum KeyHintSeparator {
#[default]
Bullet,
Pipe,
Slash,
Space,
DoubleSpace,
}
impl KeyHintSeparator {
pub fn as_str(&self) -> &'static str {
match self {
KeyHintSeparator::Bullet => " • ",
KeyHintSeparator::Pipe => " | ",
KeyHintSeparator::Slash => " / ",
KeyHintSeparator::Space => " ",
KeyHintSeparator::DoubleSpace => " ",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum KeyHintStyle {
#[default]
Compact,
Bracketed,
Colon,
ActionFirst,
}
#[derive(Debug, Clone)]
pub struct KeyHintsProps {
pub hints: Vec<KeyHint>,
pub separator: KeyHintSeparator,
pub style: KeyHintStyle,
pub key_color: Option<Color>,
pub action_color: Option<Color>,
pub separator_color: Option<Color>,
pub bold_keys: bool,
pub dim_actions: bool,
}
impl Default for KeyHintsProps {
fn default() -> Self {
Self {
hints: Vec::new(),
separator: KeyHintSeparator::Bullet,
style: KeyHintStyle::Compact,
key_color: None,
action_color: None,
separator_color: Some(Color::DarkGray),
bold_keys: true,
dim_actions: true,
}
}
}
impl KeyHintsProps {
pub fn new<I, T>(hints: I) -> Self
where
I: IntoIterator<Item = T>,
T: Into<KeyHint>,
{
Self {
hints: hints.into_iter().map(Into::into).collect(),
..Default::default()
}
}
#[must_use]
pub fn separator(mut self, separator: KeyHintSeparator) -> Self {
self.separator = separator;
self
}
#[must_use]
pub fn style(mut self, style: KeyHintStyle) -> Self {
self.style = style;
self
}
#[must_use]
pub fn key_color(mut self, color: Color) -> Self {
self.key_color = Some(color);
self
}
#[must_use]
pub fn action_color(mut self, color: Color) -> Self {
self.action_color = Some(color);
self
}
#[must_use]
pub fn separator_color(mut self, color: Color) -> Self {
self.separator_color = Some(color);
self
}
#[must_use]
pub fn bold_keys(mut self, bold: bool) -> Self {
self.bold_keys = bold;
self
}
#[must_use]
pub fn dim_actions(mut self, dim: bool) -> Self {
self.dim_actions = dim;
self
}
#[must_use]
pub fn hint(mut self, key: impl Into<String>, action: impl Into<String>) -> Self {
self.hints.push(KeyHint::new(key, action));
self
}
fn format_hint(&self, hint: &KeyHint) -> String {
match self.style {
KeyHintStyle::Compact => format!("{} {}", hint.key, hint.action),
KeyHintStyle::Bracketed => format!("[{}] {}", hint.key, hint.action),
KeyHintStyle::Colon => format!("{}: {}", hint.key, hint.action),
KeyHintStyle::ActionFirst => format!("{} {}", hint.action, hint.key),
}
}
pub fn render_string(&self) -> String {
if self.hints.is_empty() {
return String::new();
}
let separator = self.separator.as_str();
self.hints
.iter()
.map(|h| self.format_hint(h))
.collect::<Vec<_>>()
.join(separator)
}
}
pub struct KeyHints;
impl Component for KeyHints {
type Props = KeyHintsProps;
fn render(props: &Self::Props) -> Element {
let content = props.render_string();
let mut style = Style::new();
if props.dim_actions {
style = style.add_modifier(Modifier::DIM);
}
if let Some(color) = props.key_color {
style = style.fg(color);
}
Element::styled_text(&content, style)
}
}
pub fn key_hints<I, T>(hints: I) -> String
where
I: IntoIterator<Item = T>,
T: Into<KeyHint>,
{
KeyHintsProps::new(hints).render_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_keyhint_new() {
let hint = KeyHint::new("^C", "exit");
assert_eq!(hint.key, "^C");
assert_eq!(hint.action, "exit");
}
#[test]
fn test_keyhint_from_tuple() {
let hint: KeyHint = ("Enter", "select").into();
assert_eq!(hint.key, "Enter");
assert_eq!(hint.action, "select");
}
#[test]
fn test_keyhints_props_new() {
let props = KeyHintsProps::new([("a", "action1"), ("b", "action2")]);
assert_eq!(props.hints.len(), 2);
}
#[test]
fn test_keyhints_props_builder() {
let props = KeyHintsProps::new(Vec::<KeyHint>::new())
.hint("^C", "exit")
.hint("q", "quit")
.separator(KeyHintSeparator::Pipe)
.key_color(Color::Cyan);
assert_eq!(props.hints.len(), 2);
assert_eq!(props.separator, KeyHintSeparator::Pipe);
assert_eq!(props.key_color, Some(Color::Cyan));
}
#[test]
fn test_keyhints_render_empty() {
let props = KeyHintsProps::new(Vec::<KeyHint>::new());
assert_eq!(props.render_string(), "");
}
#[test]
fn test_keyhints_render_compact() {
let props =
KeyHintsProps::new([("^C", "exit"), ("q", "quit")]).style(KeyHintStyle::Compact);
assert_eq!(props.render_string(), "^C exit • q quit");
}
#[test]
fn test_keyhints_render_bracketed() {
let props = KeyHintsProps::new([("^C", "exit")]).style(KeyHintStyle::Bracketed);
assert_eq!(props.render_string(), "[^C] exit");
}
#[test]
fn test_keyhints_render_colon() {
let props = KeyHintsProps::new([("^C", "exit")]).style(KeyHintStyle::Colon);
assert_eq!(props.render_string(), "^C: exit");
}
#[test]
fn test_keyhints_render_action_first() {
let props = KeyHintsProps::new([("^C", "exit")]).style(KeyHintStyle::ActionFirst);
assert_eq!(props.render_string(), "exit ^C");
}
#[test]
fn test_keyhints_separator_pipe() {
let props =
KeyHintsProps::new([("a", "one"), ("b", "two")]).separator(KeyHintSeparator::Pipe);
assert_eq!(props.render_string(), "a one | b two");
}
#[test]
fn test_keyhints_separator_slash() {
let props =
KeyHintsProps::new([("a", "one"), ("b", "two")]).separator(KeyHintSeparator::Slash);
assert_eq!(props.render_string(), "a one / b two");
}
#[test]
fn test_keyhints_helper() {
let result = key_hints([("^C", "exit"), ("q", "quit")]);
assert!(result.contains("^C exit"));
assert!(result.contains("q quit"));
}
#[test]
fn test_keyhints_component_render() {
let props = KeyHintsProps::new([("^C", "exit")]);
let elem = KeyHints::render(&props);
assert!(elem.is_text());
}
}