romm_cli/tui/screens/
browse.rs1use 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#[derive(Debug, Clone)]
10pub struct Section {
11 pub name: String,
12 pub endpoint_indices: Vec<usize>,
13}
14
15pub 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}