Skip to main content

vtcode_tui/core_tui/widgets/
sidebar.rs

1use ratatui::{
2    buffer::Buffer,
3    layout::{Constraint, Layout, Rect},
4    text::{Line, Span},
5    widgets::{Clear, List, ListItem, Paragraph, Widget, Wrap},
6};
7
8use super::layout_mode::LayoutMode;
9use super::panel::{Panel, PanelStyles};
10use crate::ui::tui::session::styling::SessionStyles;
11
12/// Sidebar section types for organizing content
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub enum SidebarSection {
15    Queue,
16    Context,
17    Tools,
18    Info,
19}
20
21/// Widget for rendering the sidebar in wide mode
22///
23/// The sidebar provides quick access to:
24/// - Queued inputs/tasks
25/// - Context information
26/// - Recent tool calls
27/// - Session info
28///
29/// # Example
30/// ```ignore
31/// SidebarWidget::new(&styles)
32///     .queue_items(&queue)
33///     .context_info("12K tokens | 45% context")
34///     .active_section(SidebarSection::Queue)
35///     .render(sidebar_area, buf);
36/// ```
37pub struct SidebarWidget<'a> {
38    styles: &'a SessionStyles,
39    queue_items: Vec<String>,
40    context_info: Option<&'a str>,
41    recent_tools: Vec<String>,
42    active_section: Option<SidebarSection>,
43    mode: LayoutMode,
44}
45
46impl<'a> SidebarWidget<'a> {
47    /// Create a new sidebar widget
48    pub fn new(styles: &'a SessionStyles) -> Self {
49        Self {
50            styles,
51            queue_items: Vec::new(),
52            context_info: None,
53            recent_tools: Vec::new(),
54            active_section: None,
55            mode: LayoutMode::Wide,
56        }
57    }
58
59    /// Set queued items to display
60    #[must_use]
61    pub fn queue_items(mut self, items: Vec<String>) -> Self {
62        self.queue_items = items;
63        self
64    }
65
66    /// Set context info text
67    #[must_use]
68    pub fn context_info(mut self, info: &'a str) -> Self {
69        self.context_info = Some(info);
70        self
71    }
72
73    /// Set recent tool calls
74    #[must_use]
75    pub fn recent_tools(mut self, tools: Vec<String>) -> Self {
76        self.recent_tools = tools;
77        self
78    }
79
80    /// Set the active/focused section
81    #[must_use]
82    pub fn active_section(mut self, section: SidebarSection) -> Self {
83        self.active_section = Some(section);
84        self
85    }
86
87    /// Set the layout mode
88    #[must_use]
89    pub fn mode(mut self, mode: LayoutMode) -> Self {
90        self.mode = mode;
91        self
92    }
93
94    fn render_queue_section(&self, area: Rect, buf: &mut Buffer) {
95        let is_active = self.active_section == Some(SidebarSection::Queue);
96        let inner = Panel::new(self.styles)
97            .title("Queue")
98            .active(is_active)
99            .mode(self.mode)
100            .render_and_get_inner(area, buf);
101
102        if inner.height == 0 || inner.width == 0 {
103            return;
104        }
105
106        if self.queue_items.is_empty() {
107            let empty_text = Paragraph::new("No queued items").style(self.styles.muted_style());
108            empty_text.render(inner, buf);
109        } else {
110            let items: Vec<ListItem> = self
111                .queue_items
112                .iter()
113                .enumerate()
114                .map(|(i, item)| {
115                    let prefix = format!("{}. ", i + 1);
116                    let line = Line::from(vec![
117                        Span::styled(prefix, self.styles.accent_style()),
118                        Span::styled(
119                            truncate_string(item, inner.width.saturating_sub(4) as usize),
120                            self.styles.default_style(),
121                        ),
122                    ]);
123                    ListItem::new(line)
124                })
125                .collect();
126
127            let list = List::new(items);
128            list.render(inner, buf);
129        }
130    }
131
132    fn render_context_section(&self, area: Rect, buf: &mut Buffer) {
133        let is_active = self.active_section == Some(SidebarSection::Context);
134        let inner = Panel::new(self.styles)
135            .title("Context")
136            .active(is_active)
137            .mode(self.mode)
138            .render_and_get_inner(area, buf);
139
140        if inner.height == 0 || inner.width == 0 {
141            return;
142        }
143
144        let text = self.context_info.unwrap_or("No context info");
145        let paragraph = Paragraph::new(text)
146            .style(self.styles.default_style())
147            .wrap(Wrap { trim: true });
148        paragraph.render(inner, buf);
149    }
150
151    fn render_tools_section(&self, area: Rect, buf: &mut Buffer) {
152        let is_active = self.active_section == Some(SidebarSection::Tools);
153        let inner = Panel::new(self.styles)
154            .title("Recent Tools")
155            .active(is_active)
156            .mode(self.mode)
157            .render_and_get_inner(area, buf);
158
159        if inner.height == 0 || inner.width == 0 {
160            return;
161        }
162
163        if self.recent_tools.is_empty() {
164            let empty_text = Paragraph::new("No recent tools").style(self.styles.muted_style());
165            empty_text.render(inner, buf);
166        } else {
167            let items: Vec<ListItem> = self
168                .recent_tools
169                .iter()
170                .map(|tool| {
171                    let line = Line::from(Span::styled(
172                        format!(
173                            "▸ {}",
174                            truncate_string(tool, inner.width.saturating_sub(3) as usize)
175                        ),
176                        self.styles.default_style(),
177                    ));
178                    ListItem::new(line)
179                })
180                .collect();
181
182            let list = List::new(items);
183            list.render(inner, buf);
184        }
185    }
186}
187
188impl Widget for SidebarWidget<'_> {
189    fn render(self, area: Rect, buf: &mut Buffer) {
190        if area.height == 0 || area.width == 0 {
191            return;
192        }
193
194        if !self.mode.allow_sidebar() {
195            return;
196        }
197
198        Clear.render(area, buf);
199
200        // Split sidebar into sections
201        let has_queue = !self.queue_items.is_empty();
202        let has_tools = !self.recent_tools.is_empty();
203
204        let constraints = match (has_queue, has_tools) {
205            (true, true) => vec![
206                Constraint::Percentage(40),
207                Constraint::Percentage(30),
208                Constraint::Percentage(30),
209            ],
210            (true, false) | (false, true) => {
211                vec![Constraint::Percentage(50), Constraint::Percentage(50)]
212            }
213            (false, false) => vec![Constraint::Percentage(100)],
214        };
215
216        let chunks = Layout::vertical(constraints).split(area);
217
218        let mut chunk_idx = 0;
219
220        if has_queue && chunk_idx < chunks.len() {
221            self.render_queue_section(chunks[chunk_idx], buf);
222            chunk_idx += 1;
223        }
224
225        if chunk_idx < chunks.len() {
226            self.render_context_section(chunks[chunk_idx], buf);
227            chunk_idx += 1;
228        }
229
230        if has_tools && chunk_idx < chunks.len() {
231            self.render_tools_section(chunks[chunk_idx], buf);
232        }
233    }
234}
235
236/// Truncate a string to fit within a given width
237fn truncate_string(s: &str, max_width: usize) -> String {
238    if s.len() <= max_width {
239        s.to_string()
240    } else if max_width <= 3 {
241        s.chars().take(max_width).collect()
242    } else {
243        let target = max_width.saturating_sub(3);
244        let end = s
245            .char_indices()
246            .map(|(i, _)| i)
247            .rfind(|&i| i <= target)
248            .unwrap_or(0);
249        format!("{}...", &s[..end])
250    }
251}