use crate::element::{Component, Element};
use crate::style::{Color, Modifier, Style};
#[derive(Debug, Clone)]
pub struct StatusSegment {
pub text: String,
pub icon: Option<String>,
pub color: Option<Color>,
pub bg_color: Option<Color>,
pub bold: bool,
pub dim: bool,
}
impl StatusSegment {
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
icon: None,
color: None,
bg_color: None,
bold: false,
dim: false,
}
}
pub fn with_icon(icon: impl Into<String>, text: impl Into<String>) -> Self {
Self {
text: text.into(),
icon: Some(icon.into()),
color: None,
bg_color: None,
bold: false,
dim: false,
}
}
#[must_use]
pub fn color(mut self, color: Color) -> Self {
self.color = Some(color);
self
}
#[must_use]
pub fn bg(mut self, color: Color) -> Self {
self.bg_color = Some(color);
self
}
#[must_use]
pub fn bold(mut self) -> Self {
self.bold = true;
self
}
#[must_use]
pub fn dim(mut self) -> Self {
self.dim = true;
self
}
#[must_use]
pub fn icon(mut self, icon: impl Into<String>) -> Self {
self.icon = Some(icon.into());
self
}
fn render_string(&self) -> String {
match &self.icon {
Some(icon) => format!("{} {}", icon, self.text),
None => self.text.clone(),
}
}
}
impl<S: Into<String>> From<S> for StatusSegment {
fn from(s: S) -> Self {
StatusSegment::new(s)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum StatusSeparator {
#[default]
Space,
DoubleSpace,
Pipe,
Bullet,
Arrow,
Slash,
None,
}
impl StatusSeparator {
pub fn as_str(&self) -> &str {
match self {
StatusSeparator::Space => " ",
StatusSeparator::DoubleSpace => " ",
StatusSeparator::Pipe => " | ",
StatusSeparator::Bullet => " • ",
StatusSeparator::Arrow => " > ",
StatusSeparator::Slash => " / ",
StatusSeparator::None => "",
}
}
}
pub mod icons {
pub const BRANCH: &str = "";
pub const BRANCH_ASCII: &str = "*";
pub const CHECK: &str = "✓";
pub const CROSS: &str = "✗";
pub const WARNING: &str = "⚠";
pub const INFO: &str = "ℹ";
pub const CLOCK: &str = "⏱";
pub const USER: &str = "👤";
pub const FOLDER: &str = "📁";
pub const FILE: &str = "📄";
pub const LOCK: &str = "🔒";
pub const UNLOCK: &str = "🔓";
pub const ARROW_UP: &str = "↑";
pub const ARROW_DOWN: &str = "↓";
pub const SYNC: &str = "⟳";
pub const PLUS: &str = "+";
pub const MINUS: &str = "-";
pub const MODIFIED: &str = "~";
}
#[derive(Debug, Clone)]
pub struct StatusBarProps {
pub segments: Vec<StatusSegment>,
pub separator: StatusSeparator,
pub separator_color: Option<Color>,
pub prefix: Option<String>,
pub suffix: Option<String>,
pub bracket_color: Option<Color>,
}
impl Default for StatusBarProps {
fn default() -> Self {
Self {
segments: Vec::new(),
separator: StatusSeparator::Space,
separator_color: Some(Color::DarkGray),
prefix: None,
suffix: None,
bracket_color: Some(Color::DarkGray),
}
}
}
impl StatusBarProps {
pub fn new<I, T>(segments: I) -> Self
where
I: IntoIterator<Item = T>,
T: Into<StatusSegment>,
{
Self {
segments: segments.into_iter().map(Into::into).collect(),
..Default::default()
}
}
#[must_use]
pub fn segment(mut self, segment: impl Into<StatusSegment>) -> Self {
self.segments.push(segment.into());
self
}
#[must_use]
pub fn text(mut self, text: impl Into<String>, color: Color) -> Self {
self.segments.push(StatusSegment::new(text).color(color));
self
}
#[must_use]
pub fn with_icon(
mut self,
icon: impl Into<String>,
text: impl Into<String>,
color: Color,
) -> Self {
self.segments
.push(StatusSegment::with_icon(icon, text).color(color));
self
}
#[must_use]
pub fn separator(mut self, separator: StatusSeparator) -> Self {
self.separator = separator;
self
}
#[must_use]
pub fn separator_color(mut self, color: Color) -> Self {
self.separator_color = Some(color);
self
}
#[must_use]
pub fn brackets(mut self, prefix: impl Into<String>, suffix: impl Into<String>) -> Self {
self.prefix = Some(prefix.into());
self.suffix = Some(suffix.into());
self
}
#[must_use]
pub fn square_brackets(self) -> Self {
self.brackets("[", "]")
}
#[must_use]
pub fn parens(self) -> Self {
self.brackets("(", ")")
}
#[must_use]
pub fn bracket_color(mut self, color: Color) -> Self {
self.bracket_color = Some(color);
self
}
pub fn render_string(&self) -> String {
if self.segments.is_empty() {
return String::new();
}
let sep = self.separator.as_str();
let content: Vec<String> = self.segments.iter().map(|s| s.render_string()).collect();
let joined = content.join(sep);
match (&self.prefix, &self.suffix) {
(Some(p), Some(s)) => format!("{}{}{}", p, joined, s),
(Some(p), None) => format!("{}{}", p, joined),
(None, Some(s)) => format!("{}{}", joined, s),
(None, None) => joined,
}
}
}
pub struct StatusBar;
impl Component for StatusBar {
type Props = StatusBarProps;
fn render(props: &Self::Props) -> Element {
if props.segments.is_empty() {
return Element::text("");
}
let sep = props.separator.as_str();
let mut children = Vec::new();
if let Some(prefix) = &props.prefix {
let mut style = Style::new();
if let Some(color) = props.bracket_color {
style = style.fg(color);
}
children.push(Element::styled_text(prefix, style));
}
for (i, segment) in props.segments.iter().enumerate() {
if i > 0 && !sep.is_empty() {
let mut sep_style = Style::new();
if let Some(color) = props.separator_color {
sep_style = sep_style.fg(color);
}
children.push(Element::styled_text(sep, sep_style));
}
let mut style = Style::new();
if let Some(color) = segment.color {
style = style.fg(color);
}
if let Some(bg) = segment.bg_color {
style = style.bg(bg);
}
if segment.bold {
style = style.add_modifier(Modifier::BOLD);
}
if segment.dim {
style = style.add_modifier(Modifier::DIM);
}
children.push(Element::styled_text(segment.render_string(), style));
}
if let Some(suffix) = &props.suffix {
let mut style = Style::new();
if let Some(color) = props.bracket_color {
style = style.fg(color);
}
children.push(Element::styled_text(suffix, style));
}
Element::Fragment(children)
}
}
pub fn git_branch(branch: &str, color: Color) -> StatusSegment {
StatusSegment::with_icon(icons::BRANCH, branch).color(color)
}
pub fn status_ok(text: &str) -> StatusSegment {
StatusSegment::with_icon(icons::CHECK, text).color(Color::Green)
}
pub fn status_error(text: &str) -> StatusSegment {
StatusSegment::with_icon(icons::CROSS, text).color(Color::Red)
}
pub fn status_warning(text: &str) -> StatusSegment {
StatusSegment::with_icon(icons::WARNING, text).color(Color::Yellow)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_segment_new() {
let seg = StatusSegment::new("test");
assert_eq!(seg.text, "test");
assert!(seg.icon.is_none());
}
#[test]
fn test_segment_with_icon() {
let seg = StatusSegment::with_icon("*", "main");
assert_eq!(seg.text, "main");
assert_eq!(seg.icon, Some("*".to_string()));
}
#[test]
fn test_segment_builder() {
let seg = StatusSegment::new("test")
.color(Color::Green)
.bold()
.icon("+");
assert_eq!(seg.color, Some(Color::Green));
assert!(seg.bold);
assert_eq!(seg.icon, Some("+".to_string()));
}
#[test]
fn test_segment_render_string() {
let seg = StatusSegment::new("text");
assert_eq!(seg.render_string(), "text");
let seg_icon = StatusSegment::with_icon("*", "branch");
assert_eq!(seg_icon.render_string(), "* branch");
}
#[test]
fn test_segment_from_string() {
let seg: StatusSegment = "hello".into();
assert_eq!(seg.text, "hello");
}
#[test]
fn test_separator_as_str() {
assert_eq!(StatusSeparator::Space.as_str(), " ");
assert_eq!(StatusSeparator::Pipe.as_str(), " | ");
assert_eq!(StatusSeparator::Bullet.as_str(), " • ");
assert_eq!(StatusSeparator::None.as_str(), "");
}
#[test]
fn test_props_new() {
let props = StatusBarProps::new(["a", "b", "c"]);
assert_eq!(props.segments.len(), 3);
}
#[test]
fn test_props_builder() {
let props = StatusBarProps::new(Vec::<&str>::new())
.segment("one")
.segment("two")
.separator(StatusSeparator::Pipe)
.square_brackets();
assert_eq!(props.segments.len(), 2);
assert_eq!(props.separator, StatusSeparator::Pipe);
assert_eq!(props.prefix, Some("[".to_string()));
assert_eq!(props.suffix, Some("]".to_string()));
}
#[test]
fn test_props_text() {
let props = StatusBarProps::new(Vec::<&str>::new())
.text("ok", Color::Green)
.text("warn", Color::Yellow);
assert_eq!(props.segments.len(), 2);
assert_eq!(props.segments[0].color, Some(Color::Green));
}
#[test]
fn test_props_with_icon() {
let props =
StatusBarProps::new(Vec::<&str>::new()).with_icon(icons::CHECK, "done", Color::Green);
assert_eq!(props.segments.len(), 1);
assert_eq!(props.segments[0].icon, Some("✓".to_string()));
}
#[test]
fn test_render_string() {
let props = StatusBarProps::new(["a", "b", "c"]).separator(StatusSeparator::Pipe);
assert_eq!(props.render_string(), "a | b | c");
}
#[test]
fn test_render_string_with_brackets() {
let props = StatusBarProps::new(["x", "y"]).square_brackets();
assert_eq!(props.render_string(), "[x y]");
}
#[test]
fn test_render_string_empty() {
let props = StatusBarProps::new(Vec::<&str>::new());
assert_eq!(props.render_string(), "");
}
#[test]
fn test_component_render() {
let props = StatusBarProps::new(["a", "b"]);
let elem = StatusBar::render(&props);
assert!(elem.is_fragment());
}
#[test]
fn test_component_render_empty() {
let props = StatusBarProps::new(Vec::<&str>::new());
let elem = StatusBar::render(&props);
assert!(elem.is_text());
}
#[test]
fn test_helper_git_branch() {
let seg = git_branch("main", Color::Green);
assert!(seg.render_string().contains("main"));
assert_eq!(seg.color, Some(Color::Green));
}
#[test]
fn test_helper_status_ok() {
let seg = status_ok("done");
assert!(seg.render_string().contains("done"));
assert_eq!(seg.color, Some(Color::Green));
}
#[test]
fn test_helper_status_error() {
let seg = status_error("failed");
assert!(seg.render_string().contains("failed"));
assert_eq!(seg.color, Some(Color::Red));
}
#[test]
fn test_helper_status_warning() {
let seg = status_warning("slow");
assert!(seg.render_string().contains("slow"));
assert_eq!(seg.color, Some(Color::Yellow));
}
}