ratatui_statusbar/
lib.rs

1//! # `ratatui-statusbar` Crate
2//!
3//! This crate provides components for creating status bars within Ratatui applications.
4//!
5//! ## Features
6//! - Define status bar layouts with any number of sections
7//! - Customizable flex layout and spacing between sections
8
9use itertools::Itertools;
10use ratatui::layout::Flex;
11use ratatui::prelude::*;
12use ratatui::widgets::WidgetRef;
13use thiserror::Error;
14
15/// An enumeration of potential errors that can impact the [`StatusBar`] operations.
16#[derive(Error, Debug)]
17pub enum StatusBarError {
18    /// The requested index does not exist.
19    #[error("Index out of bounds: {0}")]
20    IndexOutOfBounds(usize),
21}
22
23/// A representation of a single section in a [`StatusBar`]
24/// including optional decorators (pre/post separators) around the content.
25///
26/// # Examples
27/// ```
28/// let section = StatusBarSection::default()
29///     .pre_separator(" | ")
30///     .content("Section Content")
31///     .post_separator(" | ");
32/// ```
33#[derive(Debug, Default, Clone)]
34pub struct StatusBarSection<'a> {
35    pre_separator: Option<Span<'a>>,
36    content: Line<'a>,
37    post_separator: Option<Span<'a>>,
38}
39
40impl<'a> StatusBarSection<'a> {
41    /// Associates a pre-separator with the section.
42    #[must_use]
43    pub fn pre_separator(mut self, separator: impl Into<Span<'a>>) -> Self {
44        self.pre_separator = Some(separator.into());
45        self
46    }
47
48    /// Sets the main content of the section.
49    #[must_use]
50    pub fn content(mut self, content: impl Into<Line<'a>>) -> Self {
51        self.content = content.into();
52        self
53    }
54
55    /// Associates a post-separator with the section.
56    #[must_use]
57    pub fn post_separator(mut self, separator: impl Into<Span<'a>>) -> Self {
58        self.post_separator = Some(separator.into());
59        self
60    }
61}
62
63impl<'a> From<Line<'a>> for StatusBarSection<'a> {
64    fn from(line: Line<'a>) -> Self {
65        StatusBarSection {
66            pre_separator: None,
67            content: line,
68            post_separator: None,
69        }
70    }
71}
72
73impl<'a> From<Span<'a>> for StatusBarSection<'a> {
74    fn from(span: Span<'a>) -> Self {
75        StatusBarSection {
76            pre_separator: None,
77            content: span.into(),
78            post_separator: None,
79        }
80    }
81}
82
83impl<'a> From<&'a str> for StatusBarSection<'a> {
84    fn from(s: &'a str) -> Self {
85        StatusBarSection {
86            pre_separator: None,
87            content: s.into(),
88            post_separator: None,
89        }
90    }
91}
92
93/// A customizable [`StatusBar`] that can contain multiple sections.
94///
95/// # Examples
96/// ```
97/// let status_bar = StatusBar::new(3)
98///     .flex(Flex::Center)
99///     .spacing(2)
100///     .section(0, "Left Section")?
101///     .section(1, "Center Section")?
102///     .section(2, "Right Section")?;
103/// ```
104#[derive(Debug, Default)]
105pub struct StatusBar<'a> {
106    sections: Vec<StatusBarSection<'a>>,
107    flex: Flex,
108    spacing: u16,
109}
110
111impl<'a> StatusBar<'a> {
112    /// Initializes a new [`StatusBar`] with a specified number of sections, all set to default.
113    #[must_use]
114    pub fn new(nsections: usize) -> Self {
115        Self {
116            sections: vec![StatusBarSection::default(); nsections],
117            flex: Flex::default(),
118            spacing: 1,
119        }
120    }
121
122    /// Configures the flex layout mode of the sections in the [`StatusBar`].
123    #[must_use]
124    pub fn flex(mut self, flex: Flex) -> Self {
125        self.flex = flex;
126        self
127    }
128
129    /// Sets the spacing between [`StatusBar`] sections.
130    #[must_use]
131    pub fn spacing(mut self, spacing: impl Into<u16>) -> Self {
132        self.spacing = spacing.into();
133        self
134    }
135
136    /// Modifies a specific section within the [`StatusBar`] based on its index.
137    ///
138    /// # Errors
139    ///
140    /// This function will return an error if the index is out of bounds, using the [`StatusBarError`] enum.
141    pub fn section(
142        mut self,
143        index: usize,
144        section: impl Into<StatusBarSection<'a>>,
145    ) -> Result<Self, StatusBarError> {
146        if let Some(s) = self.sections.get_mut(index) {
147            *s = section.into();
148            Ok(self)
149        } else {
150            Err(StatusBarError::IndexOutOfBounds(index))
151        }
152    }
153}
154
155impl Widget for StatusBar<'_> {
156    fn render(self, area: Rect, buf: &mut Buffer) {
157        self.render_ref(area, buf);
158    }
159}
160
161impl WidgetRef for StatusBar<'_> {
162    fn render_ref(&self, area: Rect, buf: &mut Buffer) {
163        if area.is_empty() {
164            return;
165        }
166
167        let layout = Layout::horizontal(
168            self.sections
169                .iter()
170                .map(|s| Constraint::Length(u16::try_from(s.content.width()).unwrap())),
171        )
172        .flex(self.flex)
173        .spacing(self.spacing);
174
175        let areas = layout.split(area);
176        let areas = areas.iter().collect_vec();
177
178        for (section, rect) in self.sections.iter().zip(areas) {
179            buf.set_line(
180                rect.left(),
181                rect.top(),
182                &section.content,
183                u16::try_from(section.content.width()).unwrap(),
184            );
185        }
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use ratatui::backend::TestBackend;
192
193    use super::*;
194
195    #[test]
196    fn test_print_statusbar() -> color_eyre::Result<()> {
197        let mut buf = Vec::new();
198        let backend = CrosstermBackend::new(&mut buf);
199        let mut terminal = Terminal::with_options(
200            backend,
201            TerminalOptions {
202                viewport: Viewport::Inline(1),
203            },
204        )?;
205        let status_bar = StatusBar::new(2).section(0, "hello")?.section(1, "world")?;
206        terminal
207            .draw(|f| f.render_widget(status_bar, f.size()))
208            .unwrap();
209        drop(terminal);
210        let view = String::from_utf8(buf).unwrap();
211        println!("{view}");
212        Ok(())
213    }
214
215    #[test]
216    fn render_default() -> color_eyre::Result<()> {
217        let area = Rect::new(0, 0, 15, 1);
218        let backend = TestBackend::new(area.width, area.height);
219        let status_bar = StatusBar::new(2).section(0, "hello")?.section(1, "world")?;
220        let mut terminal = Terminal::new(backend)?;
221        terminal.draw(|f| f.render_widget(status_bar, f.size()))?;
222        let expected = Buffer::with_lines(vec!["hello world    "]);
223        terminal.backend().assert_buffer(&expected);
224        Ok(())
225    }
226}