use std::time::Duration;
use ratatui::crossterm::event::{
DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers, MouseButton,
MouseEventKind,
};
use ratatui::crossterm::terminal::size;
use crate::app::App;
use crate::app::Screen;
use crate::domain::TodoService;
use crate::error::AppError;
use crate::screens;
use crate::screens::editor::EditorMode;
use super::handler;
use super::keymap;
pub async fn run(
app: &mut App,
service: &TodoService,
exe_dir: &std::path::Path,
) -> Result<(), AppError> {
let mut terminal = ratatui::init();
ratatui::crossterm::execute!(std::io::stderr(), EnableMouseCapture)
.map_err(|e| AppError::Init(format!("failed to enable mouse: {}", e)))?;
loop {
terminal
.draw(|f| match app.screen {
Screen::List => screens::list::render_list(app, f),
Screen::Detail => screens::detail::render_detail(app, f),
Screen::NoteEditor => {
if let Some(ref ta_cell) = app.note_editor {
screens::editor::render_editor(
f,
EditorMode::Edit,
&ta_cell.borrow(),
"[Enter] Newline | [Ctrl+S] Save | [Esc] Cancel",
);
}
}
Screen::CreateTodo => {
if let Some(ref ta_cell) = app.create_todo_editor {
screens::editor::render_editor(
f,
EditorMode::Create,
&ta_cell.borrow(),
"[Enter] Newline | [Ctrl+S] Save | [Esc] Cancel",
);
}
}
Screen::DeleteConfirm => {
match app.previous_screen.as_ref() {
Some(Screen::Detail) => screens::detail::render_detail(app, f),
_ => screens::list::render_list(app, f),
}
let selected_title = app
.selected_id
.as_ref()
.and_then(|id| app.todos.iter().find(|t| t.id == *id))
.map(|t| t.title.as_str());
screens::delete_confirm::render_delete_confirm(f, selected_title);
}
})
.map_err(|e| AppError::Init(format!("draw failed: {}", e)))?;
if app.should_quit {
break;
}
if let Some(event) = read_event().await? {
match event {
Event::Key(key) => handle_key_event(app, service, exe_dir, key).await?,
Event::Mouse(mouse) => handle_mouse_event(app, service, exe_dir, mouse).await?,
_ => {}
}
}
}
let _ = ratatui::crossterm::execute!(std::io::stderr(), DisableMouseCapture);
ratatui::restore();
Ok(())
}
async fn read_event() -> Result<Option<Event>, AppError> {
match ratatui::crossterm::event::poll(Duration::from_millis(16)) {
Ok(true) => {
let event =
ratatui::crossterm::event::read().map_err(|e| AppError::Init(e.to_string()))?;
Ok(Some(event))
}
Ok(false) => return Ok(None),
Err(_) => return Ok(None),
}
}
async fn handle_key_event(
app: &mut App,
service: &TodoService,
exe_dir: &std::path::Path,
key: ratatui::crossterm::event::KeyEvent,
) -> Result<(), AppError> {
if key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL) {
app.should_quit = true;
return Ok(());
}
if let Some(action) = keymap::handle_screen_key(app, &key) {
handler::execute(app, action, service, exe_dir).await?;
} else if let Some(action) = keymap::translate_key(app, &key) {
handler::execute(app, action, service, exe_dir).await?;
}
Ok(())
}
async fn handle_mouse_event(
app: &mut App,
service: &TodoService,
exe_dir: &std::path::Path,
mouse: ratatui::crossterm::event::MouseEvent,
) -> Result<(), AppError> {
let Some(terminal_area) = terminal_area() else {
return Ok(());
};
match mouse.kind {
MouseEventKind::Down(MouseButton::Left) => match app.screen {
Screen::List => {
if let Some(action) =
screens::list::mouse_action(app, terminal_area, mouse.row, mouse.column)
{
handler::execute(app, action, service, exe_dir).await?;
}
}
Screen::DeleteConfirm => {
if let Some(action) = screens::delete_confirm::click_to_action(
mouse.row,
mouse.column,
terminal_area,
app.selected_id.as_deref(),
) {
handler::execute(app, action, service, exe_dir).await?;
}
}
Screen::Detail => {
if let Some(action) = screens::detail::footer_click_action(
terminal_area,
mouse.row,
mouse.column,
app.selected_id.as_deref(),
) {
handler::execute(app, action, service, exe_dir).await?;
}
}
_ => {}
},
MouseEventKind::ScrollUp => {
if matches!(app.screen, Screen::List) {
handler::execute(
app,
crate::actions::AppAction::SelectPrevious,
service,
exe_dir,
)
.await?;
}
}
MouseEventKind::ScrollDown => {
if matches!(app.screen, Screen::List) {
handler::execute(
app,
crate::actions::AppAction::SelectNext,
service,
exe_dir,
)
.await?;
}
}
_ => {}
}
Ok(())
}
fn terminal_area() -> Option<ratatui::layout::Rect> {
match size() {
Ok((cols, rows)) => Some(ratatui::layout::Rect::new(0, 0, cols, rows)),
Err(err) => {
eprintln!("Warning: failed to read terminal size for mouse event: {}", err);
None
}
}
}