use crate::components::{Box as TinkBox, Text};
use crate::core::{Color, Element, FlexDirection};
#[derive(Debug, Clone)]
pub struct KeyBinding {
pub key: String,
pub description: String,
}
impl KeyBinding {
pub fn new(key: impl Into<String>, description: impl Into<String>) -> Self {
Self {
key: key.into(),
description: description.into(),
}
}
}
#[derive(Debug, Clone)]
pub struct HelpStyle {
pub key_color: Option<Color>,
pub description_color: Option<Color>,
pub separator: String,
pub binding_separator: String,
pub key_bold: bool,
pub description_dim: bool,
}
impl Default for HelpStyle {
fn default() -> Self {
Self {
key_color: Some(Color::Cyan),
description_color: Some(Color::BrightBlack),
separator: " ".to_string(),
binding_separator: " • ".to_string(),
key_bold: true,
description_dim: true,
}
}
}
impl HelpStyle {
pub fn new() -> Self {
Self::default()
}
pub fn key_color(mut self, color: Color) -> Self {
self.key_color = Some(color);
self
}
pub fn description_color(mut self, color: Color) -> Self {
self.description_color = Some(color);
self
}
pub fn separator(mut self, sep: impl Into<String>) -> Self {
self.separator = sep.into();
self
}
pub fn binding_separator(mut self, sep: impl Into<String>) -> Self {
self.binding_separator = sep.into();
self
}
pub fn key_bold(mut self, bold: bool) -> Self {
self.key_bold = bold;
self
}
pub fn description_dim(mut self, dim: bool) -> Self {
self.description_dim = dim;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum HelpMode {
#[default]
SingleLine,
MultiLine,
TwoColumn,
}
#[derive(Clone)]
pub struct Help {
bindings: Vec<KeyBinding>,
mode: HelpMode,
style: HelpStyle,
max_width: Option<usize>,
visible: bool,
}
impl Help {
pub fn new(bindings: Vec<KeyBinding>) -> Self {
Self {
bindings,
mode: HelpMode::default(),
style: HelpStyle::default(),
max_width: None,
visible: true,
}
}
pub fn from_tuples<I, K, D>(iter: I) -> Self
where
I: IntoIterator<Item = (K, D)>,
K: Into<String>,
D: Into<String>,
{
let bindings = iter
.into_iter()
.map(|(k, d)| KeyBinding::new(k, d))
.collect();
Self::new(bindings)
}
pub fn binding(mut self, key: impl Into<String>, description: impl Into<String>) -> Self {
self.bindings.push(KeyBinding::new(key, description));
self
}
pub fn mode(mut self, mode: HelpMode) -> Self {
self.mode = mode;
self
}
pub fn single_line(mut self) -> Self {
self.mode = HelpMode::SingleLine;
self
}
pub fn multi_line(mut self) -> Self {
self.mode = HelpMode::MultiLine;
self
}
pub fn two_column(mut self) -> Self {
self.mode = HelpMode::TwoColumn;
self
}
pub fn style(mut self, style: HelpStyle) -> Self {
self.style = style;
self
}
pub fn max_width(mut self, width: usize) -> Self {
self.max_width = Some(width);
self
}
pub fn visible(mut self, visible: bool) -> Self {
self.visible = visible;
self
}
pub fn toggle(&mut self) {
self.visible = !self.visible;
}
pub fn is_visible(&self) -> bool {
self.visible
}
pub fn len(&self) -> usize {
self.bindings.len()
}
pub fn is_empty(&self) -> bool {
self.bindings.is_empty()
}
pub fn into_element(self) -> Element {
if !self.visible || self.bindings.is_empty() {
return TinkBox::new().into_element();
}
match self.mode {
HelpMode::SingleLine => self.render_single_line(),
HelpMode::MultiLine => self.render_multi_line(),
HelpMode::TwoColumn => self.render_two_column(),
}
}
fn render_single_line(self) -> Element {
let mut parts = Vec::new();
for (i, binding) in self.bindings.iter().enumerate() {
if i > 0 {
parts.push(self.style.binding_separator.clone());
}
parts.push(binding.key.clone());
parts.push(self.style.separator.clone());
parts.push(binding.description.clone());
}
let text = parts.join("");
let mut text_elem = Text::new(&text);
if self.style.description_dim {
text_elem = text_elem.dim();
}
text_elem.into_element()
}
fn render_multi_line(self) -> Element {
let mut container = TinkBox::new().flex_direction(FlexDirection::Column);
for binding in &self.bindings {
let line = format!(
"{}{}{}",
binding.key, self.style.separator, binding.description
);
let mut text = Text::new(&line);
if self.style.description_dim {
text = text.dim();
}
container = container.child(text.into_element());
}
container.into_element()
}
fn render_two_column(self) -> Element {
let mut container = TinkBox::new().flex_direction(FlexDirection::Column);
let max_key_width = self
.bindings
.iter()
.map(|b| b.key.chars().count())
.max()
.unwrap_or(0);
for binding in &self.bindings {
let padded_key = format!("{:width$}", binding.key, width = max_key_width);
let line = format!(
"{}{}{}",
padded_key, self.style.separator, binding.description
);
let mut text = Text::new(&line);
if self.style.description_dim {
text = text.dim();
}
container = container.child(text.into_element());
}
container.into_element()
}
}
impl Default for Help {
fn default() -> Self {
Self::new(Vec::new())
}
}
pub fn navigation_help() -> Vec<KeyBinding> {
vec![
KeyBinding::new("↑/↓", "Navigate"),
KeyBinding::new("Enter", "Select"),
KeyBinding::new("Esc", "Cancel"),
]
}
pub fn vim_navigation_help() -> Vec<KeyBinding> {
vec![
KeyBinding::new("j/k", "Navigate"),
KeyBinding::new("Enter", "Select"),
KeyBinding::new("q", "Quit"),
]
}
pub fn editor_help() -> Vec<KeyBinding> {
vec![
KeyBinding::new("Ctrl+S", "Save"),
KeyBinding::new("Ctrl+Q", "Quit"),
KeyBinding::new("Ctrl+Z", "Undo"),
KeyBinding::new("Ctrl+Y", "Redo"),
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_key_binding_creation() {
let binding = KeyBinding::new("Ctrl+C", "Copy");
assert_eq!(binding.key, "Ctrl+C");
assert_eq!(binding.description, "Copy");
}
#[test]
fn test_help_creation() {
let bindings = vec![
KeyBinding::new("↑/↓", "Navigate"),
KeyBinding::new("Enter", "Select"),
];
let help = Help::new(bindings);
assert_eq!(help.len(), 2);
assert!(!help.is_empty());
}
#[test]
fn test_help_from_tuples() {
let help = Help::from_tuples([("↑/↓", "Navigate"), ("Enter", "Select"), ("q", "Quit")]);
assert_eq!(help.len(), 3);
}
#[test]
fn test_help_builder() {
let help = Help::new(vec![])
.binding("↑/↓", "Navigate")
.binding("Enter", "Select")
.binding("q", "Quit");
assert_eq!(help.len(), 3);
}
#[test]
fn test_help_mode() {
let help = Help::new(vec![]).single_line();
assert_eq!(help.mode, HelpMode::SingleLine);
let help = Help::new(vec![]).multi_line();
assert_eq!(help.mode, HelpMode::MultiLine);
let help = Help::new(vec![]).two_column();
assert_eq!(help.mode, HelpMode::TwoColumn);
}
#[test]
fn test_help_visibility() {
let mut help = Help::new(vec![]);
assert!(help.is_visible());
help = help.visible(false);
assert!(!help.is_visible());
help.toggle();
assert!(help.is_visible());
}
#[test]
fn test_help_style() {
let style = HelpStyle::new()
.key_color(Color::Green)
.description_color(Color::White)
.separator(": ")
.key_bold(false);
assert_eq!(style.key_color, Some(Color::Green));
assert_eq!(style.separator, ": ");
assert!(!style.key_bold);
}
#[test]
fn test_navigation_help() {
let bindings = navigation_help();
assert_eq!(bindings.len(), 3);
}
#[test]
fn test_vim_navigation_help() {
let bindings = vim_navigation_help();
assert_eq!(bindings.len(), 3);
}
#[test]
fn test_editor_help() {
let bindings = editor_help();
assert_eq!(bindings.len(), 4);
}
#[test]
fn test_help_empty() {
let help = Help::new(vec![]);
assert!(help.is_empty());
assert_eq!(help.len(), 0);
}
}