返回 CodeWhale
habitat.rs
根目录 / crates / tui / src / tui / pet_watch / habitat.rs
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
109 lines RUST