use crate::console::{Console, ConsoleOptions, Renderable};
use crate::segment::Segment;
use crate::style::Style;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Breadcrumbs {
items: Vec<String>,
separator: String,
style: Style,
separator_style: Style,
active_style: Option<Style>,
}
impl Breadcrumbs {
pub fn from_path(path: &str) -> Self {
let items: Vec<String> = path
.split('/')
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.collect();
Self::new(items)
}
pub fn new(items: Vec<String>) -> Self {
Breadcrumbs {
items,
separator: " > ".to_string(),
style: Style::null(),
separator_style: Style::parse("dim").unwrap_or_else(|_| Style::null()),
active_style: None,
}
}
#[must_use]
pub fn separator(mut self, sep: impl Into<String>) -> Self {
self.separator = sep.into();
self
}
#[must_use]
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
#[must_use]
pub fn separator_style(mut self, style: Style) -> Self {
self.separator_style = style;
self
}
#[must_use]
pub fn active_style(mut self, style: Style) -> Self {
self.active_style = Some(style);
self
}
pub fn push(&mut self, item: impl Into<String>) {
self.items.push(item.into());
}
pub fn pop(&mut self) -> Option<String> {
self.items.pop()
}
pub fn slash(items: Vec<String>) -> Self {
Self::new(items).separator(" / ")
}
pub fn arrow(items: Vec<String>) -> Self {
Self::new(items).separator(" → ")
}
pub fn chevron(items: Vec<String>) -> Self {
Self::new(items).separator(" › ")
}
pub fn len(&self) -> usize {
self.items.len()
}
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
pub fn items(&self) -> &[String] {
&self.items
}
pub fn separator_str(&self) -> &str {
&self.separator
}
pub fn base_style(&self) -> &Style {
&self.style
}
pub fn sep_style(&self) -> &Style {
&self.separator_style
}
pub fn active_style_opt(&self) -> Option<&Style> {
self.active_style.as_ref()
}
}
impl Renderable for Breadcrumbs {
fn gilt_console(&self, _console: &Console, _options: &ConsoleOptions) -> Vec<Segment> {
let mut segments = Vec::new();
let item_count = self.items.len();
for (i, item) in self.items.iter().enumerate() {
let is_last = i == item_count.saturating_sub(1);
let item_style = if is_last {
self.active_style
.clone()
.unwrap_or_else(|| self.style.clone())
} else {
self.style.clone()
};
segments.push(Segment::styled(item, item_style));
if !is_last {
segments.push(Segment::styled(
&self.separator,
self.separator_style.clone(),
));
}
}
segments.push(Segment::line());
segments
}
}
impl std::fmt::Display for Breadcrumbs {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut console = Console::builder()
.width(f.width().unwrap_or(80))
.force_terminal(true)
.no_color(true)
.build();
console.begin_capture();
console.print(self);
let output = console.end_capture();
write!(f, "{}", output.trim_end_matches('\n'))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_console(width: usize) -> Console {
Console::builder()
.width(width)
.force_terminal(true)
.no_color(true)
.markup(false)
.build()
}
fn render_breadcrumbs(console: &Console, breadcrumbs: &Breadcrumbs) -> String {
let opts = console.options();
let segments = breadcrumbs.gilt_console(console, &opts);
segments.iter().map(|s| s.text.as_str()).collect()
}
#[test]
fn test_new() {
let crumbs = Breadcrumbs::new(vec!["Home".into(), "Settings".into()]);
assert_eq!(crumbs.len(), 2);
assert_eq!(crumbs.items(), &["Home", "Settings"]);
assert_eq!(crumbs.separator_str(), " > ");
}
#[test]
fn test_from_path() {
let crumbs = Breadcrumbs::from_path("Home/Settings/Profile");
assert_eq!(crumbs.len(), 3);
assert_eq!(crumbs.items(), &["Home", "Settings", "Profile"]);
}
#[test]
fn test_from_path_with_empty_components() {
let crumbs = Breadcrumbs::from_path("/Home//Settings/");
assert_eq!(crumbs.len(), 2);
assert_eq!(crumbs.items(), &["Home", "Settings"]);
}
#[test]
fn test_empty() {
let crumbs = Breadcrumbs::new(vec![]);
assert!(crumbs.is_empty());
assert_eq!(crumbs.len(), 0);
}
#[test]
fn test_slash() {
let crumbs = Breadcrumbs::slash(vec!["usr".into(), "local".into(), "bin".into()]);
assert_eq!(crumbs.separator_str(), " / ");
assert_eq!(crumbs.items(), &["usr", "local", "bin"]);
}
#[test]
fn test_arrow() {
let crumbs = Breadcrumbs::arrow(vec!["A".into(), "B".into()]);
assert_eq!(crumbs.separator_str(), " → ");
}
#[test]
fn test_chevron() {
let crumbs = Breadcrumbs::chevron(vec!["A".into(), "B".into()]);
assert_eq!(crumbs.separator_str(), " › ");
}
#[test]
fn test_builder_separator() {
let crumbs = Breadcrumbs::new(vec!["A".into(), "B".into()]).separator(" | ");
assert_eq!(crumbs.separator_str(), " | ");
}
#[test]
fn test_builder_style() {
let style = Style::parse("blue").unwrap();
let crumbs = Breadcrumbs::new(vec!["A".into()]).style(style.clone());
assert_eq!(crumbs.base_style(), &style);
}
#[test]
fn test_builder_separator_style() {
let style = Style::parse("yellow").unwrap();
let crumbs = Breadcrumbs::new(vec!["A".into()]).separator_style(style.clone());
assert_eq!(crumbs.sep_style(), &style);
}
#[test]
fn test_builder_active_style() {
let style = Style::parse("bold").unwrap();
let crumbs = Breadcrumbs::new(vec!["A".into()]).active_style(style.clone());
assert_eq!(crumbs.active_style_opt(), Some(&style));
}
#[test]
fn test_builder_chain() {
let crumbs = Breadcrumbs::new(vec!["Home".into(), "Profile".into()])
.separator(" / ")
.style(Style::parse("white").unwrap())
.separator_style(Style::parse("dim").unwrap())
.active_style(Style::parse("bold green").unwrap());
assert_eq!(crumbs.separator_str(), " / ");
assert_eq!(crumbs.len(), 2);
}
#[test]
fn test_push() {
let mut crumbs = Breadcrumbs::new(vec!["Home".into()]);
crumbs.push("Settings");
crumbs.push("Profile");
assert_eq!(crumbs.len(), 3);
assert_eq!(crumbs.items(), &["Home", "Settings", "Profile"]);
}
#[test]
fn test_push_string() {
let mut crumbs = Breadcrumbs::new(vec![]);
crumbs.push("test".to_string());
assert_eq!(crumbs.len(), 1);
}
#[test]
fn test_pop() {
let mut crumbs = Breadcrumbs::new(vec!["A".into(), "B".into(), "C".into()]);
let last = crumbs.pop();
assert_eq!(last, Some("C".to_string()));
assert_eq!(crumbs.len(), 2);
}
#[test]
fn test_pop_empty() {
let mut crumbs = Breadcrumbs::new(vec![]);
let last = crumbs.pop();
assert_eq!(last, None);
}
#[test]
fn test_push_pop_sequence() {
let mut crumbs = Breadcrumbs::new(vec!["Home".into()]);
crumbs.push("Settings");
assert_eq!(crumbs.pop(), Some("Settings".to_string()));
assert_eq!(crumbs.pop(), Some("Home".to_string()));
assert_eq!(crumbs.pop(), None);
}
#[test]
fn test_render_single_item() {
let console = make_console(80);
let crumbs = Breadcrumbs::new(vec!["Home".into()]);
let output = render_breadcrumbs(&console, &crumbs);
assert!(output.contains("Home"));
assert!(!output.contains(" > ")); }
#[test]
fn test_render_multiple_items() {
let console = make_console(80);
let crumbs = Breadcrumbs::new(vec!["Home".into(), "Settings".into(), "Profile".into()]);
let output = render_breadcrumbs(&console, &crumbs);
assert!(output.contains("Home"));
assert!(output.contains("Settings"));
assert!(output.contains("Profile"));
assert!(output.contains(" > "));
let separator_count = output.matches(" > ").count();
assert_eq!(separator_count, 2);
}
#[test]
fn test_render_with_custom_separator() {
let console = make_console(80);
let crumbs = Breadcrumbs::new(vec!["A".into(), "B".into(), "C".into()]).separator(" / ");
let output = render_breadcrumbs(&console, &crumbs);
assert!(output.contains("A / B / C"));
}
#[test]
fn test_render_slash() {
let console = make_console(80);
let crumbs = Breadcrumbs::slash(vec!["usr".into(), "local".into(), "bin".into()]);
let output = render_breadcrumbs(&console, &crumbs);
assert!(output.contains("usr / local / bin"));
}
#[test]
fn test_render_arrow() {
let console = make_console(80);
let crumbs = Breadcrumbs::arrow(vec!["A".into(), "B".into()]);
let output = render_breadcrumbs(&console, &crumbs);
assert!(output.contains("A → B"));
}
#[test]
fn test_render_chevron() {
let console = make_console(80);
let crumbs = Breadcrumbs::chevron(vec!["A".into(), "B".into()]);
let output = render_breadcrumbs(&console, &crumbs);
assert!(output.contains("A › B"));
}
#[test]
fn test_render_empty() {
let console = make_console(80);
let crumbs = Breadcrumbs::new(vec![]);
let output = render_breadcrumbs(&console, &crumbs);
assert_eq!(output.trim(), "");
}
#[test]
fn test_display_trait() {
let crumbs = Breadcrumbs::new(vec!["Home".into(), "Settings".into()]);
let s = format!("{}", crumbs);
assert!(s.contains("Home"));
assert!(s.contains("Settings"));
assert!(s.contains(" > "));
}
#[test]
fn test_display_single_item() {
let crumbs = Breadcrumbs::new(vec!["Home".into()]);
let s = format!("{}", crumbs);
assert_eq!(s, "Home");
}
#[test]
fn test_clone() {
let crumbs = Breadcrumbs::new(vec!["A".into(), "B".into()])
.separator(" / ")
.active_style(Style::parse("bold").unwrap());
let cloned = crumbs.clone();
assert_eq!(crumbs.items(), cloned.items());
assert_eq!(crumbs.separator_str(), cloned.separator_str());
}
#[test]
fn test_file_path_navigation() {
let crumbs = Breadcrumbs::slash(vec![
"home".into(),
"user".into(),
"projects".into(),
"myapp".into(),
"src".into(),
]);
let console = make_console(80);
let output = render_breadcrumbs(&console, &crumbs);
assert!(output.contains("home / user / projects / myapp / src"));
}
#[test]
fn test_navigation_flow() {
let crumbs = Breadcrumbs::new(vec![
"Dashboard".into(),
"Users".into(),
"User Details".into(),
])
.active_style(Style::parse("bold").unwrap());
let console = make_console(80);
let output = render_breadcrumbs(&console, &crumbs);
assert!(output.contains("Dashboard"));
assert!(output.contains("Users"));
assert!(output.contains("User Details"));
}
#[test]
fn test_settings_hierarchy() {
let crumbs = Breadcrumbs::chevron(vec![
"Application".into(),
"Preferences".into(),
"Display".into(),
]);
let console = make_console(80);
let output = render_breadcrumbs(&console, &crumbs);
assert!(output.contains("Application › Preferences › Display"));
}
#[test]
fn test_wizard_steps() {
let crumbs = Breadcrumbs::arrow(vec![
"Welcome".into(),
"Configuration".into(),
"Review".into(),
"Complete".into(),
]);
let console = make_console(80);
let output = render_breadcrumbs(&console, &crumbs);
assert!(output.contains("Welcome → Configuration → Review → Complete"));
}
}