Skip to main content

hac_client/
screen_manager.rs

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
22/// ScreenManager is responsible for redirecting the user to the screen it should
23/// be seeing at any point by the application, it is the entity behind navigation
24pub struct ScreenManager<'sm> {
25    terminal_too_small: TerminalTooSmall<'sm>,
26    collection_list: CollectionDashboard<'sm>,
27    /// CollectionViewer is a option as we need a selected collection in order to build
28    /// all the components inside
29    collection_viewer: Option<CollectionViewer<'sm>>,
30
31    curr_screen: Screens,
32    /// we keep track of the previous screen, as when the terminal_too_small screen
33    /// is shown, we know where to redirect the user back
34    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    // we hold a copy of the sender so we can pass it to the editor when we first
44    // build one
45    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    // events can generate commands, which are sent back to the top level event loop through this
84    // channel, and goes back down the chain of components as many components may be interested
85    // in such command
86    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        // currently, only the editor cares about the ticks, used to determine
160        // when to sync changes in disk
161        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}