Skip to main content

oracle_lib/ui/app/
mod.rs

1//! Main Oracle TUI application — composed from blocks (header, list, status, overlays, right_panel).
2
3mod header;
4mod layout;
5mod list;
6mod overlays;
7mod right_panel;
8mod status;
9mod types;
10
11pub use layout::tabs_rect_for_area;
12pub use types::{Focus, Tab};
13
14use crate::analyzer::AnalyzedItem;
15use crate::analyzer::CrateInfo;
16use crate::crates_io::CrateDocInfo;
17use crate::ui::animation::AnimationState;
18use crate::ui::components::TabBar;
19use crate::ui::search::{CompletionCandidate, SearchBar, SearchCompletion};
20use crate::ui::theme::Theme;
21
22use ratatui::{
23    buffer::Buffer,
24    layout::{Constraint, Direction, Layout, Rect},
25    style::Style,
26    widgets::{block::BorderType, Block, Borders, Widget},
27};
28
29/// Main Oracle UI widget — data and builder; rendering is delegated to block modules.
30pub struct OracleUi<'a> {
31    // Data
32    pub(super) items: &'a [AnalyzedItem],
33    pub(super) all_items_impl_lookup: Option<&'a [AnalyzedItem]>,
34    pub(super) filtered_items: &'a [&'a AnalyzedItem],
35    pub(super) candidates: &'a [CompletionCandidate],
36    pub(super) crate_info: Option<&'a CrateInfo>,
37    pub(super) dependency_tree: &'a [(String, usize)],
38    pub(super) filtered_dependency_indices: &'a [usize],
39    pub(super) crate_doc: Option<&'a CrateDocInfo>,
40    pub(super) crate_doc_loading: bool,
41    pub(super) crate_doc_failed: bool,
42    pub(super) selected_installed_crate: Option<&'a crate::analyzer::InstalledCrate>,
43    pub(super) installed_crate_items: &'a [&'a AnalyzedItem],
44    pub(super) target_size_bytes: Option<u64>,
45    // UI state
46    pub(super) search_input: &'a str,
47    pub(super) current_tab: Tab,
48    pub(super) focus: Focus,
49    pub(super) list_selected: Option<usize>,
50    pub(super) selected_item: Option<&'a AnalyzedItem>,
51    pub(super) completion_selected: usize,
52    pub(super) show_completion: bool,
53    pub(super) show_help: bool,
54    pub(super) show_settings: bool,
55    pub(super) status_message: &'a str,
56    pub(super) inspector_scroll: usize,
57    pub(super) animation: Option<&'a AnimationState>,
58    pub(super) theme: &'a Theme,
59    // Copilot in-TUI chat
60    pub(super) show_copilot_chat: bool,
61    pub(super) copilot_chat_messages: &'a [(String, String)],
62    pub(super) copilot_chat_input: &'a str,
63    pub(super) copilot_chat_loading: bool,
64    pub(super) copilot_chat_scroll: usize,
65}
66
67impl<'a> OracleUi<'a> {
68    pub fn new(theme: &'a Theme) -> Self {
69        Self {
70            items: &[],
71            all_items_impl_lookup: None,
72            filtered_items: &[],
73            candidates: &[],
74            crate_info: None,
75            dependency_tree: &[],
76            filtered_dependency_indices: &[],
77            crate_doc: None,
78            crate_doc_loading: false,
79            crate_doc_failed: false,
80            selected_installed_crate: None,
81            installed_crate_items: &[],
82            target_size_bytes: None,
83            search_input: "",
84            current_tab: Tab::default(),
85            focus: Focus::default(),
86            list_selected: None,
87            selected_item: None,
88            completion_selected: 0,
89            show_completion: false,
90            show_help: false,
91            show_settings: false,
92            status_message: "",
93            inspector_scroll: 0,
94            animation: None,
95            theme,
96            show_copilot_chat: false,
97            copilot_chat_messages: &[],
98            copilot_chat_input: "",
99            copilot_chat_loading: false,
100            copilot_chat_scroll: 0,
101        }
102    }
103
104    #[must_use]
105    pub fn items(mut self, items: &'a [AnalyzedItem]) -> Self {
106        self.items = items;
107        self
108    }
109    #[must_use]
110    pub fn all_items_impl_lookup(mut self, items: Option<&'a [AnalyzedItem]>) -> Self {
111        self.all_items_impl_lookup = items;
112        self
113    }
114    #[must_use]
115    pub fn filtered_items(mut self, items: &'a [&'a AnalyzedItem]) -> Self {
116        self.filtered_items = items;
117        self
118    }
119    #[must_use]
120    pub fn selected_installed_crate(
121        mut self,
122        crate_info: Option<&'a crate::analyzer::InstalledCrate>,
123    ) -> Self {
124        self.selected_installed_crate = crate_info;
125        self
126    }
127    #[must_use]
128    pub fn installed_crate_items(mut self, items: &'a [&'a AnalyzedItem]) -> Self {
129        self.installed_crate_items = items;
130        self
131    }
132    #[must_use]
133    pub fn target_size_bytes(mut self, bytes: Option<u64>) -> Self {
134        self.target_size_bytes = bytes;
135        self
136    }
137    #[must_use]
138    pub fn list_selected(mut self, selected: Option<usize>) -> Self {
139        self.list_selected = selected;
140        self
141    }
142    #[must_use]
143    pub fn candidates(mut self, candidates: &'a [CompletionCandidate]) -> Self {
144        self.candidates = candidates;
145        self
146    }
147    #[must_use]
148    pub fn crate_info(mut self, info: Option<&'a CrateInfo>) -> Self {
149        self.crate_info = info;
150        self
151    }
152    #[must_use]
153    pub fn dependency_tree(mut self, tree: &'a [(String, usize)]) -> Self {
154        self.dependency_tree = tree;
155        self
156    }
157    #[must_use]
158    pub fn filtered_dependency_indices(mut self, indices: &'a [usize]) -> Self {
159        self.filtered_dependency_indices = indices;
160        self
161    }
162    #[must_use]
163    pub fn crate_doc(mut self, doc: Option<&'a CrateDocInfo>) -> Self {
164        self.crate_doc = doc;
165        self
166    }
167    #[must_use]
168    pub fn crate_doc_loading(mut self, loading: bool) -> Self {
169        self.crate_doc_loading = loading;
170        self
171    }
172    #[must_use]
173    pub fn crate_doc_failed(mut self, failed: bool) -> Self {
174        self.crate_doc_failed = failed;
175        self
176    }
177    #[must_use]
178    pub fn search_input(mut self, input: &'a str) -> Self {
179        self.search_input = input;
180        self
181    }
182    #[must_use]
183    pub fn current_tab(mut self, tab: Tab) -> Self {
184        self.current_tab = tab;
185        self
186    }
187    #[must_use]
188    pub fn focus(mut self, focus: Focus) -> Self {
189        self.focus = focus;
190        self
191    }
192    #[must_use]
193    pub fn selected_item(mut self, item: Option<&'a AnalyzedItem>) -> Self {
194        self.selected_item = item;
195        self
196    }
197    #[must_use]
198    pub fn completion_selected(mut self, index: usize) -> Self {
199        self.completion_selected = index;
200        self
201    }
202    #[must_use]
203    pub fn show_completion(mut self, show: bool) -> Self {
204        self.show_completion = show;
205        self
206    }
207    #[must_use]
208    pub fn show_help(mut self, show: bool) -> Self {
209        self.show_help = show;
210        self
211    }
212    #[must_use]
213    pub fn show_settings(mut self, show: bool) -> Self {
214        self.show_settings = show;
215        self
216    }
217    #[must_use]
218    pub fn status_message(mut self, msg: &'a str) -> Self {
219        self.status_message = msg;
220        self
221    }
222    #[must_use]
223    pub fn inspector_scroll(mut self, scroll: usize) -> Self {
224        self.inspector_scroll = scroll;
225        self
226    }
227    #[must_use]
228    pub fn animation_state(mut self, animation: &'a AnimationState) -> Self {
229        self.animation = Some(animation);
230        self
231    }
232    #[must_use]
233    pub fn show_copilot_chat(mut self, show: bool) -> Self {
234        self.show_copilot_chat = show;
235        self
236    }
237    #[must_use]
238    pub fn copilot_chat_messages(mut self, messages: &'a [(String, String)]) -> Self {
239        self.copilot_chat_messages = messages;
240        self
241    }
242    #[must_use]
243    pub fn copilot_chat_input(mut self, input: &'a str) -> Self {
244        self.copilot_chat_input = input;
245        self
246    }
247    #[must_use]
248    pub fn copilot_chat_loading(mut self, loading: bool) -> Self {
249        self.copilot_chat_loading = loading;
250        self
251    }
252    #[must_use]
253    pub fn copilot_chat_scroll(mut self, scroll: usize) -> Self {
254        self.copilot_chat_scroll = scroll;
255        self
256    }
257
258    fn render_search(&self, area: Rect, buf: &mut Buffer) {
259        let placeholder = match self.current_tab {
260            Tab::Types => "Search types... (struct, enum, type)",
261            Tab::Functions => "Search functions...",
262            Tab::Modules => "Search modules...",
263            Tab::Crates => {
264                if self.selected_installed_crate.is_some() {
265                    "Filter items... (e.g., de::Deserialize)"
266                } else {
267                    "Search crates... (filter by name)"
268                }
269            }
270        };
271        let search = SearchBar::new(self.search_input, self.theme)
272            .focused(self.focus == Focus::Search)
273            .placeholder(placeholder);
274        search.render(area, buf);
275    }
276
277    fn render_completion(&self, search_area: Rect, buf: &mut Buffer) {
278        if !self.show_completion || self.candidates.is_empty() {
279            return;
280        }
281        let max_height = 12.min(self.candidates.len() as u16 + 2);
282        let dropdown_area = Rect {
283            x: search_area.x + 2,
284            y: search_area.y + search_area.height,
285            width: search_area.width.saturating_sub(4).min(60),
286            height: max_height,
287        };
288        let completion = SearchCompletion::new(self.candidates, self.theme)
289            .selected(self.completion_selected)
290            .filter(self.search_input)
291            .max_visible(10);
292        completion.render(dropdown_area, buf);
293    }
294
295    fn render_tabs(&self, area: Rect, buf: &mut Buffer) {
296        let titles: Vec<&str> = Tab::all().iter().map(|t| t.title()).collect();
297        let tab_bar = TabBar::new(titles, self.theme)
298            .select(self.current_tab.index())
299            .focused(self.focus == Focus::Inspector);
300        tab_bar.render(area, buf);
301    }
302}
303
304impl Widget for OracleUi<'_> {
305    fn render(self, area: Rect, buf: &mut Buffer) {
306        use layout::{BODY_MARGIN, HEADER_HEIGHT, STATUS_HEIGHT};
307
308        let outer = Block::default()
309            .borders(Borders::ALL)
310            .border_type(BorderType::Rounded)
311            .border_style(self.theme.style_border_glow())
312            .style(Style::default().bg(self.theme.bg));
313        let content_area = outer.inner(area);
314        outer.render(area, buf);
315
316        let padded = Rect {
317            x: content_area.x + BODY_MARGIN,
318            y: content_area.y + BODY_MARGIN,
319            width: content_area.width.saturating_sub(2 * BODY_MARGIN),
320            height: content_area.height.saturating_sub(2 * BODY_MARGIN),
321        };
322        let chunks = Layout::default()
323            .direction(Direction::Vertical)
324            .constraints([
325                Constraint::Length(HEADER_HEIGHT),
326                Constraint::Min(12),
327                Constraint::Length(STATUS_HEIGHT),
328            ])
329            .split(padded);
330
331        self.render_header(chunks[0], buf);
332
333        let body = chunks[1];
334        let left_div_right = Layout::default()
335            .direction(Direction::Horizontal)
336            .constraints([
337                Constraint::Ratio(1, 3),
338                Constraint::Length(1),
339                Constraint::Ratio(2, 3),
340            ])
341            .split(body);
342        let left_column = left_div_right[0];
343        let div_rect = left_div_right[1];
344        let right_column = left_div_right[2];
345
346        let left_split = Layout::default()
347            .direction(Direction::Vertical)
348            .constraints([Constraint::Length(3), Constraint::Min(6)])
349            .split(left_column);
350        let search_rect = left_split[0];
351        let list_rect = left_split[1];
352
353        let right_split = Layout::default()
354            .direction(Direction::Vertical)
355            .constraints([Constraint::Length(3), Constraint::Min(6)])
356            .split(right_column);
357        let tabs_rect = right_split[0];
358        let right_content = right_split[1];
359
360        let (inspector_rect, chat_rect) = if self.show_copilot_chat {
361            let horz = Layout::default()
362                .direction(Direction::Horizontal)
363                .constraints([Constraint::Percentage(55), Constraint::Percentage(45)])
364                .split(right_content);
365            (horz[0], horz[1])
366        } else {
367            (right_content, right_content) // chat_rect unused
368        };
369
370        self.render_search(search_rect, buf);
371        self.render_list(list_rect, buf);
372        self.render_vertical_divider(div_rect, buf);
373        self.render_tabs(tabs_rect, buf);
374        self.render_inspector(inspector_rect, buf);
375        if self.show_copilot_chat {
376            self.render_copilot_chat(chat_rect, buf);
377        }
378        self.render_status(chunks[2], buf);
379        self.render_completion(search_rect, buf);
380        self.render_settings_overlay(area, buf);
381        self.render_help_overlay(area, buf);
382    }
383}