1use itertools::Itertools;
10use ratatui::layout::Flex;
11use ratatui::prelude::*;
12use ratatui::widgets::WidgetRef;
13use thiserror::Error;
14
15#[derive(Error, Debug)]
17pub enum StatusBarError {
18 #[error("Index out of bounds: {0}")]
20 IndexOutOfBounds(usize),
21}
22
23#[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 #[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 #[must_use]
50 pub fn content(mut self, content: impl Into<Line<'a>>) -> Self {
51 self.content = content.into();
52 self
53 }
54
55 #[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#[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 #[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 #[must_use]
124 pub fn flex(mut self, flex: Flex) -> Self {
125 self.flex = flex;
126 self
127 }
128
129 #[must_use]
131 pub fn spacing(mut self, spacing: impl Into<u16>) -> Self {
132 self.spacing = spacing.into();
133 self
134 }
135
136 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 §ion.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}