1use crate::{
2 event::{AppEvent, Event, EventHandler},
3 message::Message,
4 model::{AppState, Model},
5 update,
6};
7use ratatui::{crossterm::event::KeyCode, DefaultTerminal};
8
9pub struct App {
12 pub model: Model,
14
15 pub events: EventHandler,
17}
18
19impl App {
20 pub fn new() -> Self {
22 Self {
23 model: Model::new(),
24 events: EventHandler::new(),
25 }
26 }
27
28 pub async fn run(mut self, mut terminal: DefaultTerminal) -> color_eyre::Result<()> {
34 while !self.model.should_quit() {
35 terminal.draw(|frame| {
37 crate::view::render(&mut self.model, frame.area(), frame.buffer_mut())
38 })?;
39
40 let event = self.events.next().await?;
42
43 let message = self.event_to_message(event)?;
45 if let Some(msg) = message {
46 update::update(&mut self.model, msg).await;
47 }
48 }
49
50 Ok(())
51 }
52
53 fn event_to_message(&self, event: Event) -> color_eyre::Result<Option<Message>> {
55 match event {
56 Event::Tick => Ok(Some(Message::Tick)),
57
58 Event::Crossterm(event) => {
59 if let crossterm::event::Event::Key(key_event) = event {
60 Ok(self.key_to_message(key_event.code))
61 } else {
62 Ok(None)
63 }
64 }
65
66 Event::App(app_event) => Ok(Some(match app_event {
67 AppEvent::MenuUp => Message::MenuUp,
68 AppEvent::MenuDown => Message::MenuDown,
69 AppEvent::Execute => {
70 let command = self.model.get_selected_command();
71 Message::ExecuteCommand(command)
72 }
73 AppEvent::EnterChild => Message::EnterChild,
74 AppEvent::ExitChild => Message::ExitChild,
75 AppEvent::Quit => Message::Quit,
76 })),
77 }
78 }
79
80 fn key_to_message(&self, key: KeyCode) -> Option<Message> {
82 match self.model.state {
83 AppState::Startup => Some(Message::SkipStartup),
84
85 AppState::Menu => match key {
86 KeyCode::Esc | KeyCode::Char('q') => Some(Message::Quit),
87 KeyCode::Up | KeyCode::Char('k') => Some(Message::MenuUp),
88 KeyCode::Down | KeyCode::Char('j') => Some(Message::MenuDown),
89 KeyCode::Tab => Some(Message::SectionNext),
90 KeyCode::BackTab => Some(Message::SectionPrev),
91 KeyCode::Char('r') => Some(Message::RefreshDeviceInfo),
92 KeyCode::Char('d') => Some(Message::NextDevice),
93 KeyCode::Char('L') => Some(Message::OpenLogcat),
94 KeyCode::Enter => {
95 let command = self.model.get_selected_command();
96 Some(Message::ExecuteCommand(command))
97 }
98 _ => None,
99 },
100
101 AppState::Loading => match key {
102 KeyCode::Esc | KeyCode::Char('q') => Some(Message::ReturnToMenu),
103 _ => None,
104 },
105
106 AppState::ShowResult => match key {
107 KeyCode::Up | KeyCode::Char('k') => Some(Message::ScrollUp),
108 KeyCode::Down | KeyCode::Char('j') => Some(Message::ScrollDown),
109 KeyCode::PageUp => Some(Message::ScrollPageUp),
110 KeyCode::PageDown => Some(Message::ScrollPageDown),
111 KeyCode::Home => Some(Message::ScrollToTop),
112 KeyCode::End => Some(Message::ScrollToBottom),
113 KeyCode::Esc | KeyCode::Char('q') | KeyCode::Enter | KeyCode::Backspace => {
114 Some(Message::ReturnToMenu)
115 }
116 _ => Some(Message::ReturnToMenu),
117 },
118
119 AppState::Logcat => {
120 use crate::logcat::FilterField;
121 use crate::model::LogcatSaveMode;
122
123 if self.model.logcat_save_active {
125 if self.model.logcat_save_mode == LogcatSaveMode::FileBrowser {
127 let key_event = crossterm::event::KeyEvent::new(
129 key,
130 crossterm::event::KeyModifiers::NONE,
131 );
132 return Some(Message::LogcatFileExplorerKey(key_event));
133 }
134
135 return match key {
137 KeyCode::Esc => Some(Message::LogcatCancelSave),
138 KeyCode::Enter => {
139 let path = self.model.logcat_save_path.clone();
140 if path.trim().is_empty() {
141 Some(Message::LogcatCancelSave)
142 } else {
143 Some(Message::LogcatFileSaved(path))
144 }
145 }
146 KeyCode::F(2) => Some(Message::LogcatSaveAs),
147 KeyCode::Backspace => Some(Message::LogcatSearchBackspace),
148 KeyCode::Left => Some(Message::LogcatCursorLeft),
149 KeyCode::Right => Some(Message::LogcatCursorRight),
150 KeyCode::Tab => Some(Message::LogcatSaveFilteredOnly),
151 KeyCode::Char(c) => Some(Message::LogcatSearchInput(c)),
152 _ => None,
153 };
154 }
155
156 if self.model.logcat.detail_open {
158 return match key {
159 KeyCode::Esc | KeyCode::Enter | KeyCode::Char('q') => {
160 Some(Message::LogcatToggleDetail)
161 }
162 KeyCode::Up | KeyCode::Char('k') => Some(Message::LogcatSelectUp),
163 KeyCode::Down | KeyCode::Char('j') => Some(Message::LogcatSelectDown),
164 KeyCode::Char('y') => Some(Message::LogcatCopyLine),
165 KeyCode::Char('m') => Some(Message::LogcatBookmarkToggle),
166 _ => None,
167 };
168 }
169
170 let editing = self.model.logcat.filter.active_field != FilterField::None;
172
173 if editing {
174 match key {
175 KeyCode::Esc => Some(Message::LogcatExitFilter),
176 KeyCode::Enter => Some(Message::LogcatExitFilter),
177 KeyCode::Backspace => Some(Message::LogcatSearchBackspace),
178 KeyCode::Delete => Some(Message::LogcatSearchDelete),
179 KeyCode::Left => Some(Message::LogcatCursorLeft),
180 KeyCode::Right => Some(Message::LogcatCursorRight),
181 KeyCode::Char(c) => Some(Message::LogcatSearchInput(c)),
182 _ => None,
183 }
184 } else {
185 match key {
187 KeyCode::Esc | KeyCode::Char('q') => Some(Message::CloseLogcat),
188 KeyCode::Up | KeyCode::Char('k') => Some(Message::LogcatScrollUp),
189 KeyCode::Down | KeyCode::Char('j') => Some(Message::LogcatScrollDown),
190 KeyCode::PageUp => Some(Message::LogcatScrollPageUp),
191 KeyCode::PageDown => Some(Message::LogcatScrollPageDown),
192 KeyCode::Home => Some(Message::LogcatScrollToTop),
193 KeyCode::End => Some(Message::LogcatScrollToBottom),
194 KeyCode::Char('G') => Some(Message::LogcatScrollToBottom),
195 KeyCode::Char('g') => Some(Message::LogcatScrollToTop),
196 KeyCode::Char(' ') => Some(Message::LogcatTogglePause),
197 KeyCode::Char('c') => Some(Message::LogcatClear),
198 KeyCode::Char('l') => Some(Message::LogcatCycleLevel),
199 KeyCode::Char('w') => Some(Message::LogcatToggleWordWrap),
200 KeyCode::Char('/') => Some(Message::LogcatToggleSearch),
201 KeyCode::Char('t') => Some(Message::LogcatToggleTagFilter),
202 KeyCode::Char('p') => Some(Message::LogcatTogglePackageFilter),
203 KeyCode::Char('s') => Some(Message::LogcatSave),
204 KeyCode::Char('S') => Some(Message::LogcatSaveFilteredOnly),
205 KeyCode::Char('r') => Some(Message::LogcatToggleRegex),
207 KeyCode::Char('e') => Some(Message::LogcatToggleExclude),
208 KeyCode::Char('x') => Some(Message::LogcatToggleCompact),
209 KeyCode::Enter => Some(Message::LogcatToggleDetail),
210 KeyCode::Char('m') => Some(Message::LogcatBookmarkToggle),
211 KeyCode::Char('[') => Some(Message::LogcatBookmarkPrev),
212 KeyCode::Char(']') => Some(Message::LogcatBookmarkNext),
213 KeyCode::Left => Some(Message::LogcatHScrollLeft),
214 KeyCode::Right => Some(Message::LogcatHScrollRight),
215 KeyCode::Char('0') => Some(Message::LogcatHScrollReset),
216 KeyCode::Char('y') => Some(Message::LogcatCopyLine),
217 KeyCode::Char('f') => Some(Message::LogcatToggleFold),
218 _ => None,
219 }
220 }
221 }
222 }
223 }
224}
225
226impl Default for App {
227 fn default() -> Self {
228 Self::new()
229 }
230}