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