use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Shortcut {
pub keys: String,
pub description: String,
}
impl Shortcut {
pub fn new(keys: impl Into<String>, description: impl Into<String>) -> Self {
Self {
keys: keys.into(),
description: description.into(),
}
}
pub fn dismiss() -> Self {
Self::new("Enter/Esc", "Dismiss")
}
pub fn select() -> Self {
Self::new("Enter/→", "Select")
}
pub fn move_selection() -> Self {
Self::new("↑/↓/j/k", "Move")
}
pub fn move_arrows() -> Self {
Self::new("↑/↓", "Move")
}
pub fn back() -> Self {
Self::new("Esc/←", "Back")
}
pub fn cancel() -> Self {
Self::new("Esc", "Cancel")
}
pub fn confirm() -> Self {
Self::new("Enter", "Confirm")
}
pub fn save() -> Self {
Self::new("Enter", "Save")
}
pub fn toggle() -> Self {
Self::new("Space", "Toggle")
}
pub fn exit() -> Self {
Self::new("q", "Exit")
}
pub fn scroll() -> Self {
Self::new("↑/↓/PgUp/PgDn", "Scroll")
}
pub fn page() -> Self {
Self::new("PgUp/PgDn", "Page")
}
pub fn format(&self) -> String {
format!("[{}] {}", self.keys, self.description)
}
}
impl fmt::Display for Shortcut {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[{}] {}", self.keys, self.description)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Shortcuts {
items: Vec<Shortcut>,
separator: String,
}
impl Default for Shortcuts {
fn default() -> Self {
Self {
items: Vec::new(),
separator: " | ".to_string(),
}
}
}
impl Shortcuts {
pub fn new() -> Self {
Self::default()
}
pub fn empty() -> Self {
Self::default()
}
pub fn with_separator(mut self, separator: impl Into<String>) -> Self {
self.separator = separator.into();
self
}
pub fn add(mut self, keys: impl Into<String>, description: impl Into<String>) -> Self {
self.items.push(Shortcut::new(keys, description));
self
}
pub fn push(&mut self, keys: impl Into<String>, description: impl Into<String>) -> &mut Self {
self.items.push(Shortcut::new(keys, description));
self
}
pub fn add_shortcut(mut self, shortcut: Shortcut) -> Self {
self.items.push(shortcut);
self
}
pub fn push_shortcut(&mut self, shortcut: Shortcut) -> &mut Self {
self.items.push(shortcut);
self
}
pub fn add_if(
mut self,
condition: bool,
keys: impl Into<String>,
description: impl Into<String>,
) -> Self {
if condition {
self.items.push(Shortcut::new(keys, description));
}
self
}
pub fn move_selection(self) -> Self {
self.add_shortcut(Shortcut::move_selection())
}
pub fn move_arrows(self) -> Self {
self.add_shortcut(Shortcut::move_arrows())
}
pub fn select(self) -> Self {
self.add_shortcut(Shortcut::select())
}
pub fn back(self) -> Self {
self.add_shortcut(Shortcut::back())
}
pub fn cancel(self) -> Self {
self.add_shortcut(Shortcut::cancel())
}
pub fn confirm(self) -> Self {
self.add_shortcut(Shortcut::confirm())
}
pub fn dismiss(self) -> Self {
self.add_shortcut(Shortcut::dismiss())
}
pub fn save(self) -> Self {
self.add_shortcut(Shortcut::save())
}
pub fn toggle(self) -> Self {
self.add_shortcut(Shortcut::toggle())
}
pub fn exit(self) -> Self {
self.add_shortcut(Shortcut::exit())
}
pub fn exit_if(self, condition: bool) -> Self {
if condition {
self.exit()
} else {
self
}
}
pub fn scroll(self) -> Self {
self.add_shortcut(Shortcut::scroll())
}
pub fn scroll_if(self, condition: bool) -> Self {
if condition {
self.scroll()
} else {
self
}
}
pub fn page(self) -> Self {
self.add_shortcut(Shortcut::page())
}
pub fn page_if(self, condition: bool) -> Self {
if condition {
self.page()
} else {
self
}
}
pub fn items(&self) -> &[Shortcut] {
&self.items
}
pub fn to_button_items(&self) -> Vec<String> {
self.items.iter().map(|s| s.format()).collect()
}
pub fn to_footer_string(&self) -> String {
self.to_button_items().join(&self.separator)
}
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
pub fn len(&self) -> usize {
self.items.len()
}
}
impl fmt::Display for Shortcuts {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_footer_string())
}
}
impl From<Vec<Shortcut>> for Shortcuts {
fn from(items: Vec<Shortcut>) -> Self {
Self {
items,
separator: " | ".to_string(),
}
}
}
impl FromIterator<Shortcut> for Shortcuts {
fn from_iter<I: IntoIterator<Item = Shortcut>>(iter: I) -> Self {
Self {
items: iter.into_iter().collect(),
separator: " | ".to_string(),
}
}
}
impl From<&str> for Shortcuts {
fn from(s: &str) -> Self {
let mut sc = Shortcuts::new();
for part in s.split('|') {
let trimmed = part.trim();
if trimmed.is_empty() {
continue;
}
if let Some(start) = trimmed.find('[') {
if let Some(end) = trimmed.find(']') {
let keys = &trimmed[start + 1..end];
let desc = trimmed[end + 1..].trim();
sc.items.push(Shortcut::new(keys, desc));
continue;
}
}
sc.items.push(Shortcut::new("", trimmed));
}
sc
}
}
impl From<String> for Shortcuts {
fn from(s: String) -> Self {
Shortcuts::from(s.as_str())
}
}
pub type ShortcutBar = Shortcuts;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_shortcut_format() {
let s = Shortcut::new("Enter/Esc", "Dismiss");
assert_eq!(s.format(), "[Enter/Esc] Dismiss");
assert_eq!(s.to_string(), "[Enter/Esc] Dismiss");
}
#[test]
fn test_shortcuts_builder_and_display() {
let sc = Shortcuts::new().dismiss().exit();
assert_eq!(sc.len(), 2);
assert_eq!(sc.to_footer_string(), "[Enter/Esc] Dismiss | [q] Exit");
assert_eq!(sc.to_string(), "[Enter/Esc] Dismiss | [q] Exit");
assert_eq!(
sc.to_button_items(),
vec!["[Enter/Esc] Dismiss", "[q] Exit"]
);
}
#[test]
fn test_conditional_shortcuts() {
let sc_no_scroll = Shortcuts::new().scroll_if(false).dismiss().exit_if(true);
assert_eq!(
sc_no_scroll.to_footer_string(),
"[Enter/Esc] Dismiss | [q] Exit"
);
let sc_with_scroll = Shortcuts::new().scroll_if(true).dismiss().exit_if(false);
assert_eq!(
sc_with_scroll.to_footer_string(),
"[↑/↓/PgUp/PgDn] Scroll | [Enter/Esc] Dismiss"
);
}
#[test]
fn test_from_str_parsing() {
let raw = "[↑/↓] Move | [Enter] Select | [Esc] Back";
let sc = Shortcuts::from(raw);
assert_eq!(sc.len(), 3);
assert_eq!(sc.items()[0].keys, "↑/↓");
assert_eq!(sc.items()[0].description, "Move");
assert_eq!(sc.items()[1].keys, "Enter");
assert_eq!(sc.items()[1].description, "Select");
assert_eq!(sc.items()[2].keys, "Esc");
assert_eq!(sc.items()[2].description, "Back");
}
}