Skip to main content

romm_cli/tui/screens/
browse.rs

1use ratatui::layout::{Constraint, Layout, Rect};
2use ratatui::widgets::{Block, Borders, List, ListItem, ListState};
3use ratatui::Frame;
4
5use crate::tui::openapi::{ApiEndpoint, EndpointRegistry};
6use std::collections::HashMap;
7
8/// Logical grouping of API endpoints (by tag or path prefix).
9#[derive(Debug, Clone)]
10pub struct Section {
11    pub name: String,
12    pub endpoint_indices: Vec<usize>,
13}
14
15/// API browser screen for exploring ROMM endpoints.
16pub struct BrowseScreen {
17    pub registry: EndpointRegistry,
18    pub sections: Vec<Section>,
19    pub selected_section: usize,
20    pub selected_endpoint: usize,
21    pub view_mode: ViewMode,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq)]
25pub enum ViewMode {
26    Sections,
27    Endpoints,
28}
29
30impl BrowseScreen {
31    pub fn new(registry: EndpointRegistry) -> Self {
32        let mut sections_map: HashMap<String, Vec<usize>> = HashMap::new();
33
34        for (idx, endpoint) in registry.endpoints.iter().enumerate() {
35            let section_name = if !endpoint.tags.is_empty() {
36                endpoint.tags[0].clone()
37            } else {
38                let path_parts: Vec<&str> =
39                    endpoint.path.split('/').filter(|s| !s.is_empty()).collect();
40                if path_parts.len() >= 2 {
41                    path_parts[1].to_string()
42                } else {
43                    "Other".to_string()
44                }
45            };
46
47            sections_map.entry(section_name).or_default().push(idx);
48        }
49
50        let mut sections: Vec<Section> = sections_map
51            .into_iter()
52            .map(|(name, endpoint_indices)| Section {
53                name,
54                endpoint_indices,
55            })
56            .collect();
57
58        sections.sort_by(|a, b| a.name.cmp(&b.name));
59
60        Self {
61            registry,
62            sections,
63            selected_section: 0,
64            selected_endpoint: 0,
65            view_mode: ViewMode::Sections,
66        }
67    }
68
69    pub fn next(&mut self) {
70        match self.view_mode {
71            ViewMode::Sections => {
72                if !self.sections.is_empty() {
73                    self.selected_section = (self.selected_section + 1) % self.sections.len();
74                    self.selected_endpoint = 0;
75                }
76            }
77            ViewMode::Endpoints => {
78                if let Some(section) = self.sections.get(self.selected_section) {
79                    if !section.endpoint_indices.is_empty() {
80                        self.selected_endpoint =
81                            (self.selected_endpoint + 1) % section.endpoint_indices.len();
82                    }
83                }
84            }
85        }
86    }
87
88    pub fn previous(&mut self) {
89        match self.view_mode {
90            ViewMode::Sections => {
91                if !self.sections.is_empty() {
92                    self.selected_section = if self.selected_section == 0 {
93                        self.sections.len() - 1
94                    } else {
95                        self.selected_section - 1
96                    };
97                    self.selected_endpoint = 0;
98                }
99            }
100            ViewMode::Endpoints => {
101                if let Some(section) = self.sections.get(self.selected_section) {
102                    if !section.endpoint_indices.is_empty() {
103                        self.selected_endpoint = if self.selected_endpoint == 0 {
104                            section.endpoint_indices.len() - 1
105                        } else {
106                            self.selected_endpoint - 1
107                        };
108                    }
109                }
110            }
111        }
112    }
113
114    pub fn switch_view(&mut self) {
115        self.view_mode = match self.view_mode {
116            ViewMode::Sections => ViewMode::Endpoints,
117            ViewMode::Endpoints => ViewMode::Sections,
118        };
119        self.selected_endpoint = 0;
120    }
121
122    pub fn get_selected_endpoint(&self) -> Option<&ApiEndpoint> {
123        self.sections
124            .get(self.selected_section)
125            .and_then(|section| section.endpoint_indices.get(self.selected_endpoint))
126            .and_then(|&idx| self.registry.endpoints.get(idx))
127    }
128
129    pub fn render(&self, f: &mut Frame, area: ratatui::layout::Rect) {
130        let chunks = Layout::default()
131            .constraints([Constraint::Percentage(30), Constraint::Percentage(70)])
132            .direction(ratatui::layout::Direction::Horizontal)
133            .split(area);
134
135        self.render_sections(f, chunks[0]);
136
137        let endpoint_chunks = Layout::default()
138            .constraints([Constraint::Min(5), Constraint::Length(3)])
139            .direction(ratatui::layout::Direction::Vertical)
140            .split(chunks[1]);
141
142        self.render_endpoints(f, endpoint_chunks[0]);
143        self.render_help(f, endpoint_chunks[1]);
144    }
145
146    fn render_help(&self, f: &mut Frame, area: Rect) {
147        let help_text = match self.view_mode {
148            ViewMode::Sections => "Tab/→: View endpoints | ↑↓: Navigate sections | Esc: Back",
149            ViewMode::Endpoints => {
150                "Tab/←: View sections | ↑↓: Navigate | Enter: Execute | Esc: Back"
151            }
152        };
153        let help = ratatui::widgets::Paragraph::new(help_text)
154            .block(ratatui::widgets::Block::default().borders(ratatui::widgets::Borders::ALL));
155        f.render_widget(help, area);
156    }
157
158    fn render_sections(&self, f: &mut Frame, area: Rect) {
159        let items: Vec<ListItem> = self
160            .sections
161            .iter()
162            .enumerate()
163            .map(|(idx, section)| {
164                let count = section.endpoint_indices.len();
165                let name = if idx == self.selected_section && self.view_mode == ViewMode::Sections {
166                    format!("▶ {} ({})", section.name, count)
167                } else {
168                    format!("  {} ({})", section.name, count)
169                };
170                ListItem::new(name)
171            })
172            .collect();
173
174        let list = List::new(items)
175            .block(Block::default().title("Sections").borders(Borders::ALL))
176            .highlight_symbol(if self.view_mode == ViewMode::Sections {
177                ">> "
178            } else {
179                "   "
180            });
181
182        let mut state = ListState::default();
183        if self.view_mode == ViewMode::Sections {
184            state.select(Some(self.selected_section));
185        }
186
187        f.render_stateful_widget(list, area, &mut state);
188    }
189
190    fn render_endpoints(&self, f: &mut Frame, area: Rect) {
191        let (items, count): (Vec<ListItem>, usize) =
192            if let Some(section) = self.sections.get(self.selected_section) {
193                let items: Vec<ListItem> = section
194                    .endpoint_indices
195                    .iter()
196                    .map(|&endpoint_idx| {
197                        let ep = &self.registry.endpoints[endpoint_idx];
198                        let method_color = match ep.method.as_str() {
199                            "GET" => ratatui::style::Color::Green,
200                            "POST" => ratatui::style::Color::Blue,
201                            "PUT" => ratatui::style::Color::Yellow,
202                            "DELETE" => ratatui::style::Color::Red,
203                            _ => ratatui::style::Color::White,
204                        };
205
206                        let summary = ep
207                            .summary
208                            .as_ref()
209                            .map(|s| format!(" - {}", s))
210                            .unwrap_or_default();
211
212                        ListItem::new(format!("{} {}{}", ep.method, ep.path, summary))
213                            .style(ratatui::style::Style::default().fg(method_color))
214                    })
215                    .collect();
216                let count = items.len();
217                (items, count)
218            } else {
219                (vec![], 0)
220            };
221
222        let section_name = self
223            .sections
224            .get(self.selected_section)
225            .map(|s| s.name.clone())
226            .unwrap_or_else(|| "No Section".to_string());
227
228        let list = List::new(items)
229            .block(
230                Block::default()
231                    .title(format!("Endpoints: {} ({})", section_name, count))
232                    .borders(Borders::ALL),
233            )
234            .highlight_symbol(if self.view_mode == ViewMode::Endpoints {
235                ">> "
236            } else {
237                "   "
238            });
239
240        let mut state = ListState::default();
241        if self.view_mode == ViewMode::Endpoints {
242            state.select(Some(self.selected_endpoint));
243        }
244
245        f.render_stateful_widget(list, area, &mut state);
246    }
247}