Skip to main content

ratatui_toolkit/statusline_stacked/
mod.rs

1//! A status-line widget that can stack up indicators
2//! on the left and right end.
3//!
4//! If you use the constants SLANT_TL_BR and SLANT_BL_TR as
5//! separator you can do neo-vim/neovim style statusline.
6//!
7//! This is adapted from rat-widget's StatusLineStacked.
8//!
9//! # Example
10//!
11//! ```rust,no_run
12//! use ratatui::style::{Color, Style};
13//! use ratatui::text::Span;
14//! use ratatui_toolkit::statusline_stacked::{StatusLineStacked, SLANT_BL_TR, SLANT_TL_BR};
15//!
16//! StatusLineStacked::new()
17//!     .start(
18//!         Span::from(" STATUS ").style(Style::new().fg(Color::Black).bg(Color::DarkGray)),
19//!         Span::from(SLANT_TL_BR).style(Style::new().fg(Color::DarkGray).bg(Color::Green)),
20//!     )
21//!     .start(
22//!         Span::from(" OPERATIONAL ").style(Style::new().fg(Color::Black).bg(Color::Green)),
23//!         Span::from(SLANT_TL_BR).style(Style::new().fg(Color::Green)),
24//!     )
25//!     .center("Some status message...")
26//!     .end(
27//!         Span::from(" INFO ").style(Style::new().fg(Color::Black).bg(Color::Cyan)),
28//!         Span::from(SLANT_BL_TR).style(Style::new().fg(Color::Cyan)),
29//!     );
30//! ```
31
32use ratatui::buffer::Buffer;
33use ratatui::layout::Rect;
34use ratatui::style::{Color, Style};
35use ratatui::text::{Line, Span};
36use ratatui::widgets::Widget;
37use std::marker::PhantomData;
38
39/// PowerLine block cut at the diagonal (top-left to bottom-right).
40/// Requires a Nerd Font or PowerLine font.
41pub const SLANT_TL_BR: &str = "\u{e0b8}";
42
43/// PowerLine block cut at the diagonal (bottom-left to top-right).
44/// Requires a Nerd Font or PowerLine font.
45pub const SLANT_BL_TR: &str = "\u{e0ba}";
46
47/// Statusline with stacked indicators on the left and right side.
48///
49/// This widget creates a statusline with a "stacked" appearance using
50/// PowerLine-style diagonal separators. It has three sections:
51/// - Left: Stack indicators from left to right
52/// - Center: Centered status message
53/// - Right: Stack indicators from right to left
54#[derive(Debug, Default, Clone)]
55pub struct StatusLineStacked<'a> {
56    style: Style,
57    left: Vec<(Line<'a>, Line<'a>)>,
58    center_margin: u16,
59    center: Line<'a>,
60    right: Vec<(Line<'a>, Line<'a>)>,
61    phantom: PhantomData<&'a ()>,
62}
63
64impl<'a> StatusLineStacked<'a> {
65    pub fn new() -> Self {
66        Self::default()
67    }
68
69    /// Baseline style for the center area.
70    pub fn style(mut self, style: Style) -> Self {
71        self.style = style;
72        self
73    }
74
75    /// Add to the start group of status flags.
76    /// These stack from left to right.
77    ///
78    /// # Arguments
79    /// * `text` - The text/span to display
80    /// * `gap` - The separator (usually SLANT_TL_BR or SLANT_BL_TR)
81    pub fn start(mut self, text: impl Into<Line<'a>>, gap: impl Into<Line<'a>>) -> Self {
82        self.left.push((text.into(), gap.into()));
83        self
84    }
85
86    /// Add to the start group without a separator.
87    /// Useful for the last item in the start group.
88    pub fn start_bare(mut self, text: impl Into<Line<'a>>) -> Self {
89        self.left.push((text.into(), "".into()));
90        self
91    }
92
93    /// Margin around centered text.
94    pub fn center_margin(mut self, margin: u16) -> Self {
95        self.center_margin = margin;
96        self
97    }
98
99    /// Centered text.
100    pub fn center(mut self, text: impl Into<Line<'a>>) -> Self {
101        self.center = text.into();
102        self
103    }
104
105    /// Add to the end group of status flags.
106    /// These stack from right to left.
107    ///
108    /// # Arguments
109    /// * `text` - The text/span to display
110    /// * `gap` - The separator (usually SLANT_BL_TR)
111    pub fn end(mut self, text: impl Into<Line<'a>>, gap: impl Into<Line<'a>>) -> Self {
112        self.right.push((text.into(), gap.into()));
113        self
114    }
115
116    /// Add to the end group without a separator.
117    /// Useful for the last item in the end group.
118    pub fn end_bare(mut self, text: impl Into<Line<'a>>) -> Self {
119        self.right.push((text.into(), "".into()));
120        self
121    }
122}
123
124impl<'a> Widget for StatusLineStacked<'a> {
125    fn render(self, area: Rect, buf: &mut Buffer) {
126        // Render right side (from right to left)
127        let mut x_end = area.right();
128        for (status, gap) in self.right.iter() {
129            let width = status.width() as u16;
130            status.render(
131                Rect::new(x_end.saturating_sub(width), area.y, width, 1),
132                buf,
133            );
134            x_end = x_end.saturating_sub(width);
135
136            let width = gap.width() as u16;
137            gap.render(
138                Rect::new(x_end.saturating_sub(width), area.y, width, 1),
139                buf,
140            );
141            x_end = x_end.saturating_sub(width);
142        }
143
144        // Render left side (from left to right)
145        let mut x_start = area.x;
146        for (status, gap) in self.left.iter() {
147            let width = status.width() as u16;
148            status.render(Rect::new(x_start, area.y, width, 1), buf);
149            x_start += width;
150
151            let width = gap.width() as u16;
152            gap.render(Rect::new(x_start, area.y, width, 1), buf);
153            x_start += width;
154        }
155
156        // Fill center with base style
157        buf.set_style(
158            Rect::new(x_start, area.y, x_end.saturating_sub(x_start), 1),
159            self.style,
160        );
161
162        // Render centered text
163        let center_width = x_end
164            .saturating_sub(x_start)
165            .saturating_sub(self.center_margin * 2);
166
167        self.center.render(
168            Rect::new(x_start + self.center_margin, area.y, center_width, 1),
169            buf,
170        );
171    }
172}
173
174/// Operational mode for styled statusline
175#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
176pub enum OperationalMode {
177    /// Normal operation (green)
178    #[default]
179    Operational,
180    /// Warning state (yellow)
181    Dire,
182    /// Critical state (red)
183    Evacuate,
184}
185
186/// Builder for creating a styled statusline that looks like rat-salsa's menu_status2.
187///
188/// This provides a pre-configured statusline with the "Westinghouse" reactor-control aesthetic.
189pub struct StyledStatusLine<'a> {
190    mode: OperationalMode,
191    title: &'a str,
192    center_text: String,
193    render_count: usize,
194    event_count: usize,
195    render_time_us: u64,
196    event_time_us: u64,
197    message_count: u32,
198    use_slants: bool,
199}
200
201impl<'a> StyledStatusLine<'a> {
202    /// Create a new styled statusline with default "WESTINGHOUSE[STATUS]2" theme.
203    pub fn new() -> Self {
204        Self {
205            mode: OperationalMode::Operational,
206            title: " WESTINGHOUSE[STATUS]2 ",
207            center_text: String::new(),
208            render_count: 0,
209            event_count: 0,
210            render_time_us: 0,
211            event_time_us: 0,
212            message_count: 0,
213            use_slants: true,
214        }
215    }
216
217    /// Set the operational mode (changes color scheme).
218    pub fn mode(mut self, mode: OperationalMode) -> Self {
219        self.mode = mode;
220        self
221    }
222
223    /// Set custom title (default is "WESTINGHOUSE[STATUS]2").
224    pub fn title(mut self, title: &'a str) -> Self {
225        self.title = title;
226        self
227    }
228
229    /// Set the centered status message.
230    pub fn center_text(mut self, text: impl Into<String>) -> Self {
231        self.center_text = text.into();
232        self
233    }
234
235    /// Set render metrics.
236    pub fn render_metrics(mut self, count: usize, time_us: u64) -> Self {
237        self.render_count = count;
238        self.render_time_us = time_us;
239        self
240    }
241
242    /// Set event metrics.
243    pub fn event_metrics(mut self, count: usize, time_us: u64) -> Self {
244        self.event_count = count;
245        self.event_time_us = time_us;
246        self
247    }
248
249    /// Set message count.
250    pub fn message_count(mut self, count: u32) -> Self {
251        self.message_count = count;
252        self
253    }
254
255    /// Use slanted separators (default: true). Set to false for a simpler look.
256    pub fn use_slants(mut self, use_slants: bool) -> Self {
257        self.use_slants = use_slants;
258        self
259    }
260
261    /// Build the statusline widget.
262    pub fn build(self) -> StatusLineStacked<'a> {
263        // Color scheme from RADIUM palette (rat-salsa's menu_status2)
264        // These match exactly: pal.gray[3], pal.green[3], pal.yellow[3], pal.red[3], pal.cyan[0], pal.cyan[7]
265        let color_title = Color::Rgb(70, 73, 77); // gray[3]
266        let color_mode = match self.mode {
267            OperationalMode::Operational => Color::Rgb(42, 193, 138), // green[3]
268            OperationalMode::Dire => Color::Rgb(255, 210, 88),        // yellow[3]
269            OperationalMode::Evacuate => Color::Rgb(246, 90, 90),     // red[3]
270        };
271        let color_info = Color::Rgb(44, 163, 170); // cyan[0]
272        let color_dark = Color::Rgb(80, 202, 210); // cyan[7]
273        let text_black = Color::Rgb(16, 19, 23); // text_black
274
275        let mode_str = match self.mode {
276            OperationalMode::Operational => " OPERATIONAL ",
277            OperationalMode::Dire => " DIRE ",
278            OperationalMode::Evacuate => " EVACUATE ",
279        };
280
281        if self.use_slants {
282            // Style 1: With slanted separators (like menu_status2 stacked_1)
283            StatusLineStacked::new()
284                .style(Style::new().fg(Color::White).bg(color_dark))
285                .start(
286                    Span::from(self.title).style(Style::new().fg(text_black).bg(color_title)),
287                    Span::from(SLANT_TL_BR).style(Style::new().fg(color_title).bg(color_mode)),
288                )
289                .start(
290                    Span::from(mode_str).style(Style::new().fg(text_black).bg(color_mode)),
291                    Span::from(SLANT_TL_BR).style(Style::new().fg(color_mode)),
292                )
293                .center_margin(1)
294                .center(self.center_text)
295                .end(
296                    Span::from(format!(
297                        "R[{}][{}µs] ",
298                        self.render_count, self.render_time_us
299                    ))
300                    .style(Style::new().fg(text_black).bg(color_info)),
301                    Span::from(SLANT_BL_TR).style(Style::new().fg(color_info).bg(color_dark)),
302                )
303                .end(
304                    "",
305                    Span::from(SLANT_BL_TR).style(Style::new().fg(color_dark).bg(color_info)),
306                )
307                .end(
308                    Span::from(format!(
309                        "E[{}][{}µs] ",
310                        self.event_count, self.event_time_us
311                    ))
312                    .style(Style::new().fg(text_black).bg(color_info)),
313                    Span::from(SLANT_BL_TR).style(Style::new().fg(color_info).bg(color_dark)),
314                )
315                .end(
316                    "",
317                    Span::from(SLANT_BL_TR).style(Style::new().fg(color_dark).bg(color_info)),
318                )
319                .end(
320                    Span::from(format!("MSG[{}] ", self.message_count))
321                        .style(Style::new().fg(text_black).bg(color_info)),
322                    Span::from(SLANT_BL_TR).style(Style::new().fg(color_info)),
323                )
324        } else {
325            // Style 2: Simple style without decorative slants (like menu_status2 stacked_2)
326            StatusLineStacked::new()
327                .style(Style::new().fg(Color::White).bg(color_dark))
328                .start_bare(
329                    Span::from(self.title).style(Style::new().fg(Color::White).bg(color_title)),
330                )
331                .start_bare(Span::from(mode_str).style(Style::new().fg(text_black).bg(color_mode)))
332                .center_margin(1)
333                .center(self.center_text)
334                .end_bare(
335                    Span::from(format!(
336                        "R[{}][{}µs] ",
337                        self.render_count, self.render_time_us
338                    ))
339                    .style(Style::new().fg(text_black).bg(color_info)),
340                )
341                .end_bare(
342                    Span::from(format!(
343                        "E[{}][{}µs] ",
344                        self.event_count, self.event_time_us
345                    ))
346                    .style(Style::new().fg(text_black).bg(color_info)),
347                )
348                .end_bare(
349                    Span::from(format!(" MSG[{}] ", self.message_count))
350                        .style(Style::new().fg(text_black).bg(color_info)),
351                )
352        }
353    }
354}
355
356impl<'a> Default for StyledStatusLine<'a> {
357    fn default() -> Self {
358        Self::new()
359    }
360}