1use hac_core::{collection::Collection, command::Command};
2
3use crate::event_pool::Event;
4use crate::pages::collection_dashboard::CollectionDashboard;
5use crate::pages::collection_viewer::collection_store::CollectionStore;
6use crate::pages::collection_viewer::CollectionViewer;
7use crate::pages::terminal_too_small::TerminalTooSmall;
8use crate::pages::{Eventful, Renderable};
9
10use std::{cell::RefCell, rc::Rc};
11
12use ratatui::{layout::Rect, Frame};
13use tokio::sync::mpsc::UnboundedSender;
14
15#[derive(Debug, Clone, PartialEq)]
16pub enum Screens {
17 CollectionDashboard,
18 CollectionViewer,
19 TerminalTooSmall,
20}
21
22pub struct ScreenManager<'sm> {
25 terminal_too_small: TerminalTooSmall<'sm>,
26 collection_list: CollectionDashboard<'sm>,
27 collection_viewer: Option<CollectionViewer<'sm>>,
30
31 curr_screen: Screens,
32 prev_screen: Screens,
35
36 size: Rect,
37 colors: &'sm hac_colors::Colors,
38 config: &'sm hac_config::Config,
39 dry_run: bool,
40
41 collection_store: Rc<RefCell<CollectionStore>>,
42
43 sender: Option<UnboundedSender<Command>>,
46}
47
48impl<'sm> ScreenManager<'sm> {
49 pub fn new(
50 size: Rect,
51 colors: &'sm hac_colors::Colors,
52 collections: Vec<Collection>,
53 config: &'sm hac_config::Config,
54 dry_run: bool,
55 ) -> anyhow::Result<Self> {
56 Ok(Self {
57 curr_screen: Screens::CollectionDashboard,
58 prev_screen: Screens::CollectionDashboard,
59 collection_viewer: None,
60 terminal_too_small: TerminalTooSmall::new(colors),
61 collection_list: CollectionDashboard::new(size, colors, collections, dry_run)?,
62 collection_store: Rc::new(RefCell::new(CollectionStore::default())),
63 size,
64 colors,
65 config,
66 sender: None,
67 dry_run,
68 })
69 }
70
71 fn restore_screen(&mut self) {
72 std::mem::swap(&mut self.curr_screen, &mut self.prev_screen);
73 }
74
75 fn switch_screen(&mut self, screen: Screens) {
76 if self.curr_screen == screen {
77 return;
78 }
79 std::mem::swap(&mut self.curr_screen, &mut self.prev_screen);
80 self.curr_screen = screen;
81 }
82
83 pub fn handle_command(&mut self, command: Command) {
87 match command {
88 Command::SelectCollection(collection) | Command::CreateCollection(collection) => {
89 tracing::debug!("changing to api explorer: {}", collection.info.name);
90 self.switch_screen(Screens::CollectionViewer);
91 self.collection_store.borrow_mut().set_state(collection);
92 self.collection_viewer = Some(CollectionViewer::new(
93 self.size,
94 self.collection_store.clone(),
95 self.colors,
96 self.config,
97 self.dry_run,
98 ));
99 self.collection_viewer.as_mut().unwrap()
100 .register_command_handler(
101 self.sender
102 .as_ref()
103 .expect("attempted to register the sender on collection_viewer but it was None")
104 .clone(),
105 )
106 .ok();
107 }
108 Command::Error(msg) => {
109 self.collection_list.display_error(msg);
110 }
111 _ => {}
112 }
113 }
114}
115
116impl Renderable for ScreenManager<'_> {
117 fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> {
118 match (size.width < 80, size.height < 22) {
119 (true, _) => self.switch_screen(Screens::TerminalTooSmall),
120 (_, true) => self.switch_screen(Screens::TerminalTooSmall),
121 (false, false) if self.curr_screen.eq(&Screens::TerminalTooSmall) => {
122 self.restore_screen()
123 }
124 _ => {}
125 }
126
127 match &self.curr_screen {
128 Screens::CollectionViewer => self
129 .collection_viewer
130 .as_mut()
131 .expect(
132 "should never be able to switch to editor screen without having a collection",
133 )
134 .draw(frame, frame.size())?,
135 Screens::CollectionDashboard => self.collection_list.draw(frame, frame.size())?,
136 Screens::TerminalTooSmall => self.terminal_too_small.draw(frame, frame.size())?,
137 };
138
139 Ok(())
140 }
141
142 fn register_command_handler(&mut self, sender: UnboundedSender<Command>) -> anyhow::Result<()> {
143 self.sender = Some(sender.clone());
144 self.collection_list
145 .register_command_handler(sender.clone())?;
146 Ok(())
147 }
148
149 fn resize(&mut self, new_size: Rect) {
150 self.size = new_size;
151 self.collection_list.resize(new_size);
152
153 if let Some(e) = self.collection_viewer.as_mut() {
154 e.resize(new_size)
155 }
156 }
157
158 fn handle_tick(&mut self) -> anyhow::Result<()> {
159 if let Screens::CollectionViewer = &self.curr_screen {
162 self.collection_viewer
163 .as_mut()
164 .expect("we are displaying the editor without having one")
165 .handle_tick()?
166 };
167
168 Ok(())
169 }
170}
171
172impl Eventful for ScreenManager<'_> {
173 type Result = Command;
174
175 fn handle_event(&mut self, event: Option<Event>) -> anyhow::Result<Option<Command>> {
176 match self.curr_screen {
177 Screens::CollectionViewer => self
178 .collection_viewer
179 .as_mut()
180 .expect(
181 "should never be able to switch to editor screen without having a collection",
182 )
183 .handle_event(event),
184 Screens::CollectionDashboard => self.collection_list.handle_event(event),
185 Screens::TerminalTooSmall => Ok(None),
186 }
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
194 use hac_core::collection::{self, types::*};
195 use ratatui::{backend::TestBackend, Terminal};
196 use std::{
197 fs::{create_dir, File},
198 io::Write,
199 };
200 use tempfile::{tempdir, TempDir};
201
202 fn setup_temp_collections(amount: usize) -> (TempDir, String) {
203 let tmp_data_dir = tempdir().expect("Failed to create temp data dir");
204
205 let tmp_dir = tmp_data_dir.path().join("collections");
206 create_dir(&tmp_dir).expect("Failed to create collections directory");
207
208 for i in 0..amount {
209 let file_path = tmp_dir.join(format!("test_collection_{}.json", i));
210 let mut tmp_file = File::create(&file_path).expect("Failed to create file");
211
212 write!(
213 tmp_file,
214 r#"{{"info": {{ "name": "test_collection_{}", "description": "test_description_{}" }}}}"#,
215 i, i
216 ).expect("Failed to write to file");
217
218 tmp_file.flush().expect("Failed to flush file");
219 }
220
221 (tmp_data_dir, tmp_dir.to_string_lossy().to_string())
222 }
223
224 #[test]
225 fn test_show_terminal_too_small_screen() {
226 let small_in_width = Rect::new(0, 0, 79, 22);
227 let small_in_height = Rect::new(0, 0, 100, 19);
228 let colors = hac_colors::Colors::default();
229 let (_guard, path) = setup_temp_collections(10);
230 let collections = collection::collection::get_collections(path).unwrap();
231 let config = hac_config::load_config();
232 let mut sm =
233 ScreenManager::new(small_in_width, &colors, collections, &config, false).unwrap();
234 let mut terminal = Terminal::new(TestBackend::new(80, 22)).unwrap();
235
236 sm.draw(&mut terminal.get_frame(), small_in_width).unwrap();
237 assert_eq!(sm.curr_screen, Screens::TerminalTooSmall);
238
239 sm.draw(&mut terminal.get_frame(), small_in_height).unwrap();
240 assert_eq!(sm.curr_screen, Screens::TerminalTooSmall);
241 }
242
243 #[test]
244 fn test_restore_screeen() {
245 let small = Rect::new(0, 0, 79, 22);
246 let enough = Rect::new(0, 0, 80, 22);
247 let colors = hac_colors::Colors::default();
248 let (_guard, path) = setup_temp_collections(10);
249 let collections = collection::collection::get_collections(path).unwrap();
250 let config = hac_config::load_config();
251 let mut sm = ScreenManager::new(small, &colors, collections, &config, false).unwrap();
252 let mut terminal = Terminal::new(TestBackend::new(80, 22)).unwrap();
253
254 terminal.resize(small).unwrap();
255 sm.draw(&mut terminal.get_frame(), small).unwrap();
256 assert_eq!(sm.curr_screen, Screens::TerminalTooSmall);
257 assert_eq!(sm.prev_screen, Screens::CollectionDashboard);
258
259 terminal.resize(enough).unwrap();
260 sm.draw(&mut terminal.get_frame(), enough).unwrap();
261 assert_eq!(sm.curr_screen, Screens::CollectionDashboard);
262 assert_eq!(sm.prev_screen, Screens::TerminalTooSmall);
263 }
264
265 #[test]
266 fn test_resizing() {
267 let initial = Rect::new(0, 0, 80, 22);
268 let expected = Rect::new(0, 0, 100, 22);
269 let colors = hac_colors::Colors::default();
270 let (_guard, path) = setup_temp_collections(10);
271 let collection = collection::collection::get_collections(path).unwrap();
272 let config = hac_config::load_config();
273 let mut sm = ScreenManager::new(initial, &colors, collection, &config, false).unwrap();
274
275 sm.resize(expected);
276
277 assert_eq!(sm.size, expected);
278 }
279
280 #[test]
281 fn test_switch_to_explorer_on_select() {
282 let initial = Rect::new(0, 0, 80, 22);
283 let colors = hac_colors::Colors::default();
284 let collection = Collection {
285 info: Info {
286 name: String::from("any_name"),
287 description: None,
288 },
289 path: "any_path".into(),
290 requests: None,
291 };
292 let command = Command::SelectCollection(collection.clone());
293 let (_guard, path) = setup_temp_collections(10);
294 let collection = collection::collection::get_collections(path).unwrap();
295 let config = hac_config::load_config();
296 let (tx, _) = tokio::sync::mpsc::unbounded_channel::<Command>();
297 let mut sm = ScreenManager::new(initial, &colors, collection, &config, false).unwrap();
298 _ = sm.register_command_handler(tx.clone());
299 assert_eq!(sm.curr_screen, Screens::CollectionDashboard);
300
301 sm.handle_command(command);
302 assert_eq!(sm.curr_screen, Screens::CollectionViewer);
303 }
304
305 #[test]
306 fn test_register_command_sender_for_dashboard() {
307 let initial = Rect::new(0, 0, 80, 22);
308 let colors = hac_colors::Colors::default();
309 let (_guard, path) = setup_temp_collections(10);
310 let collections = collection::collection::get_collections(path).unwrap();
311 let config = hac_config::load_config();
312 let mut sm = ScreenManager::new(initial, &colors, collections, &config, false).unwrap();
313
314 let (tx, _) = tokio::sync::mpsc::unbounded_channel::<Command>();
315
316 sm.register_command_handler(tx.clone()).unwrap();
317
318 assert!(sm.collection_list.command_sender.is_some());
319 }
320
321 #[test]
322 fn test_quit_event() {
323 let initial = Rect::new(0, 0, 80, 22);
324 let colors = hac_colors::Colors::default();
325 let (_guard, path) = setup_temp_collections(10);
326 let collections = collection::collection::get_collections(path).unwrap();
327 let config = hac_config::load_config();
328 let mut sm = ScreenManager::new(initial, &colors, collections, &config, false).unwrap();
329
330 let event = Event::Key(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL));
331
332 let command = sm.handle_event(Some(event)).unwrap();
333
334 assert!(command.is_some());
335 }
336}