Skip to main content

stynx_code_tui/widgets/
sidebar_info.rs

1use ratatui::{
2    buffer::Buffer,
3    layout::Rect,
4    style::{Color, Modifier, Style},
5    text::{Line, Span},
6    widgets::{Paragraph, Widget},
7};
8
9use crate::theme;
10use crate::widgets::footer::{fmt_elapsed_short, pretty_model, shrink_path};
11use crate::widgets::spinner::FRAMES;
12
13/// The bottom section of the left sidebar: model / session info, stacked
14/// vertically (replaces the bottom powerline lualine).
15pub struct SidebarInfo<'a> {
16    pub cwd: &'a str,
17    pub model: &'a str,
18    pub mode: &'a str,
19    pub cost: f64,
20    pub git_branch: Option<&'a str>,
21    pub is_streaming: bool,
22    pub is_pending: bool,
23    pub is_paused: bool,
24    pub spinner_frame: usize,
25    pub elapsed_secs: u64,
26}
27
28fn row(icon: &str, value: impl Into<String>, color: Color) -> Line<'static> {
29    Line::from(vec![
30        Span::styled(
31            format!(" {icon} "),
32            Style::default().fg(color).add_modifier(Modifier::BOLD),
33        ),
34        Span::styled(value.into(), Style::default().fg(theme::TEXT())),
35    ])
36}
37
38impl<'a> Widget for SidebarInfo<'a> {
39    fn render(self, area: Rect, buf: &mut Buffer) {
40        if area.height == 0 || area.width < 4 {
41            return;
42        }
43        let bg = theme::BACKGROUND();
44        for y in area.y..area.y + area.height {
45            for x in area.x..area.x + area.width {
46                buf[(x, y)].set_style(Style::default().bg(bg));
47            }
48        }
49
50        let w = area.width as usize;
51        let mut lines: Vec<Line<'static>> = Vec::new();
52
53        // Divider from the tools section above.
54        lines.push(Line::from(Span::styled(
55            "\u{2500}".repeat(w.saturating_sub(2)),
56            Style::default().fg(theme::OVERLAY()),
57        )));
58
59        // Live status (only one of these at a time).
60        if self.is_paused {
61            lines.push(row("\u{23F8}", "paused", theme::GOLD()));
62        } else if self.is_pending {
63            let ch = FRAMES[self.spinner_frame % FRAMES.len()];
64            lines.push(row(&ch.to_string(), "connecting…", theme::SUBTLE()));
65        } else if self.is_streaming {
66            let ch = FRAMES[self.spinner_frame % FRAMES.len()];
67            lines.push(row(
68                &ch.to_string(),
69                format!("generating  {}", fmt_elapsed_short(self.elapsed_secs)),
70                theme::FOAM(),
71            ));
72        }
73
74        // Model.
75        lines.push(row("\u{25C7}", pretty_model(self.model), theme::IRIS()));
76
77        // Permission mode (hidden in Normal to stay clean).
78        if self.mode != "Normal" {
79            let (icon, color) = match self.mode {
80                "Auto-accept" => ("\u{26A1}", theme::GOLD()),
81                "Plan" => ("\u{25C6}", theme::IRIS()),
82                "Bypass" => ("\u{26A0}", theme::LOVE()),
83                _ => ("\u{25CF}", theme::FOAM()),
84            };
85            lines.push(row(icon, self.mode.to_string(), color));
86        }
87
88        // Git branch.
89        if let Some(branch) = self.git_branch {
90            lines.push(row("\u{E0A0}", branch.to_string(), theme::FOAM()));
91        }
92
93        // Working directory.
94        lines.push(row(
95            "\u{F07C}",
96            shrink_path(self.cwd, w.saturating_sub(4)),
97            theme::SUBTLE(),
98        ));
99
100        // Cost.
101        lines.push(row("\u{0024}", format!("{:.4}", self.cost), theme::IRIS()));
102
103        Paragraph::new(lines)
104            .style(Style::default().bg(bg))
105            .render(area, buf);
106    }
107}