use crate::{
event::{AppEvent, Event, EventHandler},
message::Message,
model::{AppState, Model},
update,
};
use ratatui::{crossterm::event::KeyCode, DefaultTerminal};
pub struct App {
pub model: Model,
pub events: EventHandler,
}
impl App {
pub fn new() -> Self {
Self {
model: Model::new(),
events: EventHandler::new(),
}
}
pub async fn run(mut self, mut terminal: DefaultTerminal) -> color_eyre::Result<()> {
while !self.model.should_quit() {
terminal.draw(|frame| {
crate::view::render(&mut self.model, frame.area(), frame.buffer_mut())
})?;
let event = self.events.next().await?;
let message = self.event_to_message(event)?;
if let Some(msg) = message {
update::update(&mut self.model, msg).await;
}
}
Ok(())
}
fn event_to_message(&self, event: Event) -> color_eyre::Result<Option<Message>> {
match event {
Event::Tick => Ok(Some(Message::Tick)),
Event::Crossterm(event) => {
if let crossterm::event::Event::Key(key_event) = event {
Ok(self.key_to_message(key_event.code))
} else {
Ok(None)
}
}
Event::App(app_event) => Ok(Some(match app_event {
AppEvent::MenuUp => Message::MenuUp,
AppEvent::MenuDown => Message::MenuDown,
AppEvent::Execute => {
let command = self.model.get_selected_command();
Message::ExecuteCommand(command)
}
AppEvent::EnterChild => Message::EnterChild,
AppEvent::ExitChild => Message::ExitChild,
AppEvent::Quit => Message::Quit,
})),
}
}
fn key_to_message(&self, key: KeyCode) -> Option<Message> {
if self.model.theme_selector.open {
return match key {
KeyCode::Esc | KeyCode::Char('q') => Some(Message::ToggleThemeSelector),
KeyCode::Up | KeyCode::Char('k') => Some(Message::ThemePrev),
KeyCode::Down | KeyCode::Char('j') => Some(Message::ThemeNext),
KeyCode::Char('t') => Some(Message::ThemeNext),
KeyCode::Enter => Some(Message::ThemeApply),
_ => None,
};
}
if key == KeyCode::Char('T') {
return Some(Message::ToggleThemeSelector);
}
match self.model.state {
AppState::Startup => Some(Message::SkipStartup),
AppState::Menu => match key {
KeyCode::Esc | KeyCode::Char('q') => Some(Message::Quit),
KeyCode::Up | KeyCode::Char('k') => Some(Message::MenuUp),
KeyCode::Down | KeyCode::Char('j') => Some(Message::MenuDown),
KeyCode::Tab => Some(Message::SectionNext),
KeyCode::BackTab => Some(Message::SectionPrev),
KeyCode::Char('t') => Some(Message::ThemeNext),
KeyCode::Char('r') => Some(Message::RefreshDeviceInfo),
KeyCode::Char('d') => Some(Message::NextDevice),
KeyCode::Char('L') => Some(Message::OpenLogcat),
KeyCode::Char('D') => Some(Message::OpenDevMode),
KeyCode::Enter => {
let command = self.model.get_selected_command();
Some(Message::ExecuteCommand(command))
}
_ => None,
},
AppState::Loading => match key {
KeyCode::Esc | KeyCode::Char('q') => Some(Message::ReturnToMenu),
_ => None,
},
AppState::ShowResult => match key {
KeyCode::Up | KeyCode::Char('k') => Some(Message::ScrollUp),
KeyCode::Down | KeyCode::Char('j') => Some(Message::ScrollDown),
KeyCode::PageUp => Some(Message::ScrollPageUp),
KeyCode::PageDown => Some(Message::ScrollPageDown),
KeyCode::Home => Some(Message::ScrollToTop),
KeyCode::End => Some(Message::ScrollToBottom),
KeyCode::Esc | KeyCode::Char('q') | KeyCode::Enter | KeyCode::Backspace => {
Some(Message::ReturnToMenu)
}
_ => Some(Message::ReturnToMenu),
},
AppState::DevMode => {
use crate::devtools::DevFocus;
if self.model.devtools.editor_picker_open {
return match key {
KeyCode::Up | KeyCode::Char('k') => Some(Message::DevEditorUp),
KeyCode::Down | KeyCode::Char('j') => Some(Message::DevEditorDown),
KeyCode::Enter => Some(Message::DevEditorConfirm),
KeyCode::Esc => Some(Message::DevToggleEditorPicker),
_ => None,
};
}
if self.model.devtools.variant_picker_open {
return match key {
KeyCode::Up | KeyCode::Char('k') => Some(Message::DevPrevVariant),
KeyCode::Down | KeyCode::Char('j') => Some(Message::DevNextVariant),
KeyCode::Enter => Some(Message::DevToggleVariantPicker),
KeyCode::Esc => Some(Message::DevToggleVariantPicker),
_ => None,
};
}
match key {
KeyCode::Esc | KeyCode::Char('q') => Some(Message::CloseDevMode),
KeyCode::Char('t') => Some(Message::ThemeNext),
KeyCode::Char('b') => Some(Message::DevBuild),
KeyCode::Char('R') => Some(Message::DevRun),
KeyCode::Char('E') => Some(Message::DevToggleEditorPicker),
KeyCode::Char('v') => Some(Message::DevToggleVariantPicker),
KeyCode::Tab => Some(Message::DevCycleFocus),
KeyCode::Char('L') => Some(Message::OpenLogcat),
KeyCode::Char('e') => Some(Message::DevOpenFile),
_ => {
match self.model.devtools.focus {
DevFocus::FileBrowser => {
let key_event = crossterm::event::KeyEvent::new(
key,
crossterm::event::KeyModifiers::NONE,
);
Some(Message::DevFileExplorerKey(key_event))
}
DevFocus::BuildOutput => {
match key {
KeyCode::Up | KeyCode::Char('k') => None, KeyCode::Down | KeyCode::Char('j') => None,
_ => None,
}
}
DevFocus::Toolbar => match key {
KeyCode::Left => Some(Message::DevPrevVariant),
KeyCode::Right => Some(Message::DevNextVariant),
_ => None,
},
}
}
}
}
AppState::Logcat => {
use crate::logcat::FilterField;
use crate::model::LogcatSaveMode;
if self.model.logcat_save_active {
if self.model.logcat_save_mode == LogcatSaveMode::FileBrowser {
let key_event = crossterm::event::KeyEvent::new(
key,
crossterm::event::KeyModifiers::NONE,
);
return Some(Message::LogcatFileExplorerKey(key_event));
}
return match key {
KeyCode::Esc => Some(Message::LogcatCancelSave),
KeyCode::Enter => {
let path = self.model.logcat_save_path.clone();
if path.trim().is_empty() {
Some(Message::LogcatCancelSave)
} else {
Some(Message::LogcatFileSaved(path))
}
}
KeyCode::Char('S') => Some(Message::LogcatSaveAs),
KeyCode::Backspace => Some(Message::LogcatSearchBackspace),
KeyCode::Left => Some(Message::LogcatCursorLeft),
KeyCode::Right => Some(Message::LogcatCursorRight),
KeyCode::Tab => Some(Message::LogcatSaveFilteredOnly),
KeyCode::Char(c) => Some(Message::LogcatSearchInput(c)),
_ => None,
};
}
if self.model.logcat.detail_open {
return match key {
KeyCode::Esc | KeyCode::Enter | KeyCode::Char('q') => {
Some(Message::LogcatToggleDetail)
}
KeyCode::Up | KeyCode::Char('k') => Some(Message::LogcatSelectUp),
KeyCode::Down | KeyCode::Char('j') => Some(Message::LogcatSelectDown),
KeyCode::Char('y') => Some(Message::LogcatCopyLine),
KeyCode::Char('m') => Some(Message::LogcatBookmarkToggle),
_ => None,
};
}
let editing = self.model.logcat.filter.active_field != FilterField::None;
if editing {
match key {
KeyCode::Esc => Some(Message::LogcatExitFilter),
KeyCode::Enter => Some(Message::LogcatExitFilter),
KeyCode::Backspace => Some(Message::LogcatSearchBackspace),
KeyCode::Delete => Some(Message::LogcatSearchDelete),
KeyCode::Left => Some(Message::LogcatCursorLeft),
KeyCode::Right => Some(Message::LogcatCursorRight),
KeyCode::Char(c) => Some(Message::LogcatSearchInput(c)),
_ => None,
}
} else {
match key {
KeyCode::Esc | KeyCode::Char('q') => Some(Message::CloseLogcat),
KeyCode::Up | KeyCode::Char('k') => Some(Message::LogcatScrollUp),
KeyCode::Down | KeyCode::Char('j') => Some(Message::LogcatScrollDown),
KeyCode::PageUp => Some(Message::LogcatScrollPageUp),
KeyCode::PageDown => Some(Message::LogcatScrollPageDown),
KeyCode::Home => Some(Message::LogcatScrollToTop),
KeyCode::End => Some(Message::LogcatScrollToBottom),
KeyCode::Char('G') => Some(Message::LogcatScrollToBottom),
KeyCode::Char('g') => Some(Message::LogcatScrollToTop),
KeyCode::Char(' ') => Some(Message::LogcatTogglePause),
KeyCode::Char('c') => Some(Message::LogcatClear),
KeyCode::Char('l') => Some(Message::LogcatCycleLevel),
KeyCode::Char('w') => Some(Message::LogcatToggleWordWrap),
KeyCode::Char('f') => Some(Message::LogcatToggleSearch),
KeyCode::Char('t') => Some(Message::LogcatToggleTagFilter),
KeyCode::Char('p') => Some(Message::LogcatTogglePackageFilter),
KeyCode::Char('s') => Some(Message::LogcatSave),
KeyCode::Char('S') => Some(Message::LogcatSaveAs),
KeyCode::Char('r') => Some(Message::LogcatToggleRegex),
KeyCode::Char('e') => Some(Message::LogcatToggleExclude),
KeyCode::Char('x') => Some(Message::LogcatToggleCompact),
KeyCode::Enter => Some(Message::LogcatToggleDetail),
KeyCode::Char('m') => Some(Message::LogcatBookmarkToggle),
KeyCode::Char('[') => Some(Message::LogcatBookmarkPrev),
KeyCode::Char(']') => Some(Message::LogcatBookmarkNext),
KeyCode::Left => Some(Message::LogcatHScrollLeft),
KeyCode::Right => Some(Message::LogcatHScrollRight),
KeyCode::Char('0') => Some(Message::LogcatHScrollReset),
KeyCode::Char('y') => Some(Message::LogcatCopyLine),
KeyCode::Char('F') => Some(Message::LogcatToggleFold),
_ => None,
}
}
}
}
}
}
impl Default for App {
fn default() -> Self {
Self::new()
}
}