| 1 | //! Full viewport ownership uses the existing modal stack. Hidden composer, |
| 2 | //! history, selection and active Engine state are untouched. |
| 3 | use super::Control; |
| 4 | use crate::tui::{ |
| 5 | shell_key_routing::{self, Focus, ShellBindingId as Id}, |
| 6 | views::{ModalKind, ModalView, ViewAction}, |
| 7 | }; |
| 8 | use crossterm::event::{KeyEvent, MouseEvent}; |
| 9 | use ratatui::{buffer::Buffer, layout::Rect}; |
| 10 | use std::sync::{Arc, Mutex}; |
| 11 | pub struct Habitat { |
| 12 | controls: Arc<Mutex<Vec<Control>>>, |
| 13 | } |
| 14 | impl Habitat { |
| 15 | pub fn new(controls: Arc<Mutex<Vec<Control>>>) -> Self { |
| 16 | Self { controls } |
| 17 | } |
| 18 | } |
| 19 | impl ModalView for Habitat { |
| 20 | fn kind(&self) -> ModalKind { |
| 21 | ModalKind::PetHabitat |
| 22 | } |
| 23 | fn handle_key(&mut self, key: KeyEvent) -> ViewAction { |
| 24 | let action = match shell_key_routing::route(Focus::Modal(self.kind()), &key) { |
| 25 | Some(Id::PetResultUp) => Some(Control::Scroll(-1)), |
| 26 | Some(Id::PetResultDown) => Some(Control::Scroll(1)), |
| 27 | Some(Id::PetResultPageUp) => Some(Control::Scroll(-10)), |
| 28 | Some(Id::PetResultPageDown) => Some(Control::Scroll(10)), |
| 29 | Some(Id::PetBack) => return ViewAction::Close, |
| 30 | Some(Id::PetSound) => Some(Control::Sound), |
| 31 | Some(Id::PetBrowser) => Some(Control::Browser), |
| 32 | Some(Id::PetWindow) => Some(Control::Window), |
| 33 | _ => None, |
| 34 | }; |
| 35 | if let Some(action) = action |
| 36 | && let Ok(mut queue) = self.controls.lock() |
| 37 | && queue.len() < 16 |
| 38 | { |
| 39 | queue.push(action); |
| 40 | } |
| 41 | ViewAction::None |
| 42 | } |
| 43 | fn handle_mouse(&mut self, _mouse: MouseEvent) -> ViewAction { |
| 44 | ViewAction::None |
| 45 | } |
| 46 | fn render(&self, _area: Rect, _buf: &mut Buffer) {} |
| 47 | fn as_any_mut(&mut self) -> &mut dyn std::any::Any { |
| 48 | self |
| 49 | } |
| 50 | } |
| 51 | pub fn hints(locale: codewhale_localization::Locale) -> String { |
| 52 | use codewhale_localization::{MessageId, tr}; |
| 53 | let mut text = tr(locale, MessageId::PetHabitatHints).into_owned(); |
| 54 | for (name, id) in [ |
| 55 | ("back", Id::PetBack), |
| 56 | ("sound", Id::PetSound), |
| 57 | ("browser", Id::PetBrowser), |
| 58 | ("window", Id::PetWindow), |
| 59 | ] { |
| 60 | text = text.replace( |
| 61 | &format!("{{{name}}}"), |
| 62 | shell_key_routing::binding(id).footer_chord, |
| 63 | ); |
| 64 | } |
| 65 | text |
| 66 | } |
| 67 | |
| 68 | #[cfg(test)] |
| 69 | mod tests { |
| 70 | use super::*; |
| 71 | use crossterm::event::{KeyCode, KeyModifiers}; |
| 72 | #[test] |
| 73 | fn pet_habitat_full_view_preserves_composer_history_and_session_on_escape() { |
| 74 | let mut app = |
| 75 | crate::test_support::test_app_with_options(crate::test_support::test_tui_options(".")); |
| 76 | app.input = "unfinished composer".into(); |
| 77 | app.add_message(crate::tui::history::HistoryCell::System { |
| 78 | content: "retained transcript".into(), |
| 79 | }); |
| 80 | let history = app.history.len(); |
| 81 | let session = app.current_session_id.clone(); |
| 82 | // Push the actual focus owner without starting a network client. |
| 83 | let controls = Arc::new(Mutex::new(Vec::new())); |
| 84 | app.view_stack.push(Habitat::new(controls.clone())); |
| 85 | assert_eq!(app.focus(), Focus::Modal(ModalKind::PetHabitat)); |
| 86 | assert_eq!( |
| 87 | shell_key_routing::route( |
| 88 | Focus::Composer, |
| 89 | &KeyEvent::new(KeyCode::F(5), KeyModifiers::NONE) |
| 90 | ), |
| 91 | None |
| 92 | ); |
| 93 | app.view_stack |
| 94 | .handle_key(KeyEvent::new(KeyCode::F(6), KeyModifiers::NONE)); |
| 95 | assert!(matches!( |
| 96 | controls.lock().unwrap().as_slice(), |
| 97 | [Control::Sound] |
| 98 | )); |
| 99 | app.view_stack |
| 100 | .handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE)); |
| 101 | app.view_stack |
| 102 | .handle_key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); |
| 103 | assert!(app.view_stack.is_empty()); |
| 104 | assert_eq!(app.input, "unfinished composer"); |
| 105 | assert_eq!(app.history.len(), history); |
| 106 | assert_eq!(app.current_session_id, session); |
| 107 | } |
| 108 | } |
| 109 |