mod item;
pub use item::*;
use ratatui::prelude::*;
use ratatui::widgets::Paragraph;
use super::{Component, RenderContext};
use crate::theme::Theme;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Section {
Left,
Center,
Right,
}
#[derive(Clone, Debug, PartialEq)]
pub enum StatusBarMessage {
SetLeftItems(Vec<StatusBarItem>),
SetCenterItems(Vec<StatusBarItem>),
SetRightItems(Vec<StatusBarItem>),
Clear,
ClearLeft,
ClearCenter,
ClearRight,
Tick(u64),
StartTimer {
section: Section,
index: usize,
},
StopTimer {
section: Section,
index: usize,
},
ResetTimer {
section: Section,
index: usize,
},
IncrementCounter {
section: Section,
index: usize,
},
DecrementCounter {
section: Section,
index: usize,
},
SetCounter {
section: Section,
index: usize,
value: u64,
},
ActivateHeartbeat {
section: Section,
index: usize,
},
DeactivateHeartbeat {
section: Section,
index: usize,
},
PulseHeartbeat {
section: Section,
index: usize,
},
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct StatusBarState {
left: Vec<StatusBarItem>,
center: Vec<StatusBarItem>,
right: Vec<StatusBarItem>,
separator: String,
background: Color,
disabled: bool,
}
impl Default for StatusBarState {
fn default() -> Self {
Self {
left: Vec::new(),
center: Vec::new(),
right: Vec::new(),
separator: " | ".to_string(),
background: Color::DarkGray,
disabled: false,
}
}
}
impl StatusBarState {
pub fn new() -> Self {
Self::default()
}
pub fn with_separator(separator: impl Into<String>) -> Self {
Self {
separator: separator.into(),
..Self::default()
}
}
pub fn left(&self) -> &[StatusBarItem] {
&self.left
}
pub fn center(&self) -> &[StatusBarItem] {
&self.center
}
pub fn right(&self) -> &[StatusBarItem] {
&self.right
}
pub fn set_left(&mut self, items: Vec<StatusBarItem>) {
self.left = items;
}
pub fn set_center(&mut self, items: Vec<StatusBarItem>) {
self.center = items;
}
pub fn set_right(&mut self, items: Vec<StatusBarItem>) {
self.right = items;
}
pub fn push_left(&mut self, item: StatusBarItem) {
self.left.push(item);
}
pub fn push_center(&mut self, item: StatusBarItem) {
self.center.push(item);
}
pub fn push_right(&mut self, item: StatusBarItem) {
self.right.push(item);
}
pub fn clear(&mut self) {
self.left.clear();
self.center.clear();
self.right.clear();
}
pub fn separator(&self) -> &str {
&self.separator
}
pub fn set_separator(&mut self, separator: impl Into<String>) {
self.separator = separator.into();
}
pub fn background(&self) -> Color {
self.background
}
pub fn set_background(&mut self, color: Color) {
self.background = color;
}
pub fn is_disabled(&self) -> bool {
self.disabled
}
pub fn set_disabled(&mut self, disabled: bool) {
self.disabled = disabled;
}
pub fn with_disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn is_empty(&self) -> bool {
self.left.is_empty() && self.center.is_empty() && self.right.is_empty()
}
pub fn len(&self) -> usize {
self.left.len() + self.center.len() + self.right.len()
}
pub fn section(&self, section: Section) -> &[StatusBarItem] {
match section {
Section::Left => &self.left,
Section::Center => &self.center,
Section::Right => &self.right,
}
}
pub fn section_mut(&mut self, section: Section) -> &mut Vec<StatusBarItem> {
match section {
Section::Left => &mut self.left,
Section::Center => &mut self.center,
Section::Right => &mut self.right,
}
}
pub fn get_item_mut(&mut self, section: Section, index: usize) -> Option<&mut StatusBarItem> {
self.section_mut(section).get_mut(index)
}
fn tick_all(&mut self, delta_ms: u64) {
for item in &mut self.left {
item.tick(delta_ms);
}
for item in &mut self.center {
item.tick(delta_ms);
}
for item in &mut self.right {
item.tick(delta_ms);
}
}
}
pub struct StatusBar;
impl StatusBar {
fn render_section(
items: &[StatusBarItem],
separator: &str,
theme: &Theme,
) -> Vec<Span<'static>> {
let mut spans = Vec::new();
for (idx, item) in items.iter().enumerate() {
let style = item.style.style(theme);
spans.push(Span::styled(item.text(), style));
if idx < items.len() - 1 && item.has_separator() {
spans.push(Span::styled(separator.to_string(), theme.disabled_style()));
}
}
spans
}
fn truncate_spans(spans: Vec<Span<'static>>, max_width: usize) -> Vec<Span<'static>> {
if max_width == 0 {
return Vec::new();
}
let total: usize = spans.iter().map(|s| s.content.len()).sum();
if total <= max_width {
return spans;
}
let mut result = Vec::new();
let mut remaining = max_width.saturating_sub(1);
for span in spans {
let len = span.content.len();
if remaining == 0 {
break;
}
if len <= remaining {
remaining -= len;
result.push(span);
} else {
let truncated: String = span.content.chars().take(remaining).collect();
result.push(Span::styled(truncated, span.style));
remaining = 0;
}
}
let ellipsis_style = result.last().map(|s| s.style).unwrap_or_default();
result.push(Span::styled("…", ellipsis_style));
result
}
}
impl Component for StatusBar {
type State = StatusBarState;
type Message = StatusBarMessage;
type Output = ();
fn init() -> Self::State {
StatusBarState::default()
}
fn update(state: &mut Self::State, msg: Self::Message) -> Option<Self::Output> {
match msg {
StatusBarMessage::SetLeftItems(items) => {
state.left = items;
}
StatusBarMessage::SetCenterItems(items) => {
state.center = items;
}
StatusBarMessage::SetRightItems(items) => {
state.right = items;
}
StatusBarMessage::Clear => {
state.clear();
}
StatusBarMessage::ClearLeft => {
state.left.clear();
}
StatusBarMessage::ClearCenter => {
state.center.clear();
}
StatusBarMessage::ClearRight => {
state.right.clear();
}
StatusBarMessage::Tick(delta_ms) => {
state.tick_all(delta_ms);
}
StatusBarMessage::StartTimer { section, index } => {
if let Some(item) = state.get_item_mut(section, index) {
if let StatusBarItemContent::ElapsedTime { running, .. } = &mut item.content {
*running = true;
}
}
}
StatusBarMessage::StopTimer { section, index } => {
if let Some(item) = state.get_item_mut(section, index) {
if let StatusBarItemContent::ElapsedTime { running, .. } = &mut item.content {
*running = false;
}
}
}
StatusBarMessage::ResetTimer { section, index } => {
if let Some(item) = state.get_item_mut(section, index) {
if let StatusBarItemContent::ElapsedTime {
elapsed_ms,
running,
..
} = &mut item.content
{
*elapsed_ms = 0;
*running = false;
}
}
}
StatusBarMessage::IncrementCounter { section, index } => {
if let Some(item) = state.get_item_mut(section, index) {
if let StatusBarItemContent::Counter { value, .. } = &mut item.content {
*value = value.saturating_add(1);
}
}
}
StatusBarMessage::DecrementCounter { section, index } => {
if let Some(item) = state.get_item_mut(section, index) {
if let StatusBarItemContent::Counter { value, .. } = &mut item.content {
*value = value.saturating_sub(1);
}
}
}
StatusBarMessage::SetCounter {
section,
index,
value: new_value,
} => {
if let Some(item) = state.get_item_mut(section, index) {
if let StatusBarItemContent::Counter { value, .. } = &mut item.content {
*value = new_value;
}
}
}
StatusBarMessage::ActivateHeartbeat { section, index } => {
if let Some(item) = state.get_item_mut(section, index) {
if let StatusBarItemContent::Heartbeat { active, .. } = &mut item.content {
*active = true;
}
}
}
StatusBarMessage::DeactivateHeartbeat { section, index } => {
if let Some(item) = state.get_item_mut(section, index) {
if let StatusBarItemContent::Heartbeat { active, .. } = &mut item.content {
*active = false;
}
}
}
StatusBarMessage::PulseHeartbeat { section, index } => {
if let Some(item) = state.get_item_mut(section, index) {
if let StatusBarItemContent::Heartbeat { active, frame } = &mut item.content {
*active = true;
*frame = (*frame + 1) % 4;
}
}
}
}
None
}
fn view(state: &Self::State, ctx: &mut RenderContext<'_, '_>) {
let bg_style = Style::default().bg(state.background);
let left_spans = Self::render_section(&state.left, &state.separator, ctx.theme);
let center_spans = Self::render_section(&state.center, &state.separator, ctx.theme);
let right_spans = Self::render_section(&state.right, &state.separator, ctx.theme);
let left_width: usize = left_spans.iter().map(|s| s.content.len()).sum();
let center_width: usize = center_spans.iter().map(|s| s.content.len()).sum();
let right_width: usize = right_spans.iter().map(|s| s.content.len()).sum();
let total_width = ctx.area.width as usize;
let available_for_center = total_width
.saturating_sub(left_width)
.saturating_sub(right_width);
let effective_center_width = center_width.min(available_for_center);
let center_spans = Self::truncate_spans(center_spans, effective_center_width);
let mut line_spans: Vec<Span> = Vec::new();
line_spans.extend(left_spans);
let left_padding = if effective_center_width > 0 {
let center_start = (total_width.saturating_sub(effective_center_width)) / 2;
center_start.saturating_sub(left_width)
} else {
0
};
if left_padding > 0 {
line_spans.push(Span::raw(" ".repeat(left_padding)));
}
line_spans.extend(center_spans);
let current_width = left_width + left_padding + effective_center_width;
let right_padding = total_width.saturating_sub(current_width + right_width);
if right_padding > 0 {
line_spans.push(Span::raw(" ".repeat(right_padding)));
}
line_spans.extend(right_spans);
let line = Line::from(line_spans);
let paragraph = Paragraph::new(line).style(bg_style);
let item_count = state.left.len() + state.center.len() + state.right.len();
let annotation =
crate::annotation::Annotation::new(crate::annotation::WidgetType::StatusBar)
.with_id("status_bar")
.with_meta("item_count", item_count.to_string());
let annotated = crate::annotation::Annotate::new(paragraph, annotation);
ctx.frame.render_widget(annotated, ctx.area);
}
}
#[cfg(test)]
mod snapshot_tests;
#[cfg(test)]
mod tests;