use super::keys::{KeyHelpMode, KeyMap};
use super::text_flow::{wrap_fields, wrap_words};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SelectItem {
pub hotkey: String,
pub label: String,
pub description: Option<String>,
pub aliases: Vec<String>,
}
impl SelectItem {
pub fn new(hotkey: impl Into<String>, label: impl Into<String>) -> Self {
Self {
hotkey: hotkey.into(),
label: label.into(),
description: None,
aliases: Vec::new(),
}
}
pub fn button(label: impl Into<String>) -> Self {
Self {
hotkey: String::new(),
label: label.into(),
description: None,
aliases: Vec::new(),
}
}
pub fn with_optional_hotkey(hotkey: Option<impl Into<String>>, label: impl Into<String>) -> Self {
Self {
hotkey: hotkey.map(Into::into).unwrap_or_default(),
label: label.into(),
description: None,
aliases: Vec::new(),
}
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn with_aliases(mut self, aliases: &[&str]) -> Self {
self.aliases = aliases
.iter()
.map(|s| s.to_string())
.filter(|s| {
if s.len() == 1
&& s.chars()
.next()
.map(|c| c.is_ascii_digit())
.unwrap_or(false)
&& s != &self.hotkey
{
return false;
}
if s.eq_ignore_ascii_case("q") && !self.hotkey.eq_ignore_ascii_case("q") {
return false;
}
true
})
.collect();
self
}
}
pub type MenuEntry = SelectItem;
#[derive(Debug, Clone)]
pub enum ModalSection {
Title(TitleSection),
Fields(FieldSection),
Text(TextSection),
Menu(MenuSection),
Custom(Vec<String>),
Divider { dimmed: bool },
Footer(Box<FooterSection>),
}
#[derive(Debug, Clone)]
pub struct TitleSection {
pub title: String,
pub breadcrumbs: Option<String>,
pub show_breadcrumbs: bool,
pub is_error: bool,
}
impl TitleSection {
pub fn new(title: impl Into<String>) -> Self {
Self {
title: title.into(),
breadcrumbs: None,
show_breadcrumbs: true,
is_error: false,
}
}
pub fn with_breadcrumbs(mut self, breadcrumbs: impl Into<String>) -> Self {
self.breadcrumbs = Some(breadcrumbs.into());
self.show_breadcrumbs = true;
self
}
pub fn without_breadcrumbs(mut self) -> Self {
self.show_breadcrumbs = false;
self
}
pub fn with_error(mut self, is_error: bool) -> Self {
self.is_error = is_error;
self
}
}
#[derive(Debug, Clone)]
pub struct FieldSection {
pub items: Vec<String>,
pub separator: String,
}
impl Default for FieldSection {
fn default() -> Self {
Self::new()
}
}
impl FieldSection {
pub fn new() -> Self {
Self {
items: Vec::new(),
separator: " | ".to_string(),
}
}
pub fn with_items(items: Vec<String>, separator: impl Into<String>) -> Self {
Self {
items,
separator: separator.into(),
}
}
pub fn from_raw(raw: impl Into<String>, separator: impl Into<String>) -> Self {
let sep = separator.into();
let r = raw.into();
let parts: Vec<String> = r
.split(&sep)
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
Self {
items: parts,
separator: sep,
}
}
pub fn with_separator(mut self, separator: impl Into<String>) -> Self {
self.separator = separator.into();
self
}
pub fn add_field(mut self, item: impl Into<String>) -> Self {
self.items.push(item.into());
self
}
pub fn add_item(&mut self, item: impl Into<String>) -> &mut Self {
self.items.push(item.into());
self
}
pub fn render(&self, available_width: usize) -> Vec<String> {
wrap_fields(&self.items, &self.separator, available_width)
}
}
#[derive(Debug, Clone)]
pub struct TextSection {
pub text: String,
}
impl TextSection {
pub fn new(text: impl Into<String>) -> Self {
Self { text: text.into() }
}
pub fn render(&self, available_width: usize) -> Vec<String> {
wrap_words(&self.text, available_width)
}
}
#[derive(Debug, Clone)]
pub struct MenuSection {
pub entries: Vec<SelectItem>,
pub allow_toggle: bool,
}
impl MenuSection {
pub fn new(entries: Vec<SelectItem>) -> Self {
Self {
entries,
allow_toggle: false,
}
}
pub fn with_allow_toggle(mut self, allow: bool) -> Self {
self.allow_toggle = allow;
self
}
}
#[derive(Debug, Clone)]
pub struct FooterSection {
pub keymap: KeyMap,
pub mode: KeyHelpMode,
pub custom_help: Option<String>,
}
impl FooterSection {
pub fn new(mode: KeyHelpMode) -> Self {
Self {
keymap: KeyMap::default(),
mode,
custom_help: None,
}
}
pub fn with_keymap(mut self, keymap: KeyMap) -> Self {
self.keymap = keymap;
self
}
pub fn with_custom_help(mut self, help: impl Into<String>) -> Self {
self.custom_help = Some(help.into());
self
}
pub fn render(&self) -> String {
if let Some(ref custom) = self.custom_help {
custom.clone()
} else {
self.keymap.footer_help_text(self.mode)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_field_section_from_raw() {
let sec = FieldSection::from_raw("Host: Ubuntu | RAM: 16GB | Daemon: [ONLINE]", " | ");
assert_eq!(sec.items.len(), 3);
assert_eq!(sec.items[0], "Host: Ubuntu");
assert_eq!(sec.items[1], "RAM: 16GB");
assert_eq!(sec.items[2], "Daemon: [ONLINE]");
let rendered = sec.render(80);
assert_eq!(rendered.len(), 1);
assert_eq!(rendered[0], "Host: Ubuntu | RAM: 16GB | Daemon: [ONLINE]");
let narrow_rendered = sec.render(20);
assert_eq!(narrow_rendered.len(), 3);
assert_eq!(narrow_rendered[0], "Host: Ubuntu");
assert_eq!(narrow_rendered[1], "RAM: 16GB");
assert_eq!(narrow_rendered[2], "Daemon: [ONLINE]");
}
#[test]
fn test_title_section_builder() {
let title = TitleSection::new("DASHBOARD")
.with_breadcrumbs("Dashboard › Servers")
.with_error(false);
assert_eq!(title.title, "DASHBOARD");
assert_eq!(title.breadcrumbs.as_deref(), Some("Dashboard › Servers"));
assert!(!title.is_error);
}
}