feat(app): keyboard event handling with panel switching and CRUD

- App struct with full state (lists, tasks, focus, popups, DB)
- Tab cycles focus: Tabs -> TaskList -> Detail -> Tabs
- Left/Right arrows switch lists when focus on Tabs
- Up/Down navigate tasks (TaskList) or scroll (Detail)
- Alt+Up/Down reorder tasks with position persistence
- n: create new list or task, d: delete, e: edit title
- Enter on Detail opens DatePicker popup
- InputPopup with full text editing (navigation, insert, delete)
- ConfirmDelete popup before destructive actions
- DatePicker adjusts draft_date with Up/Down
- main.rs: terminal setup, event loop, raw mode, alternate screen
This commit is contained in:
Ruben Rosario
2026-06-20 19:38:12 +01:00
parent efc3c1c84c
commit 3b6726a726
2 changed files with 468 additions and 3 deletions
+59 -3
View File
@@ -1,7 +1,63 @@
mod app;
mod domain;
mod ui;
mod infrastructure;
mod ui;
fn main() {
println!("Task App - Google Tasks TUI");
use std::io;
use crossterm::event::{self, Event};
use crossterm::terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen};
use crossterm::ExecutableCommand;
use ratatui::backend::CrosstermBackend;
use ratatui::Terminal;
use crate::app::App;
use crate::infrastructure::db::Db;
use crate::ui::{draw, AppView};
fn main() -> io::Result<()> {
let db_path = dirs::data_dir()
.unwrap_or_else(|| std::path::PathBuf::from("."))
.join("task_app")
.join("tasks.db");
std::fs::create_dir_all(db_path.parent().unwrap()).ok();
let db = Db::new(db_path.to_str().unwrap()).expect("Failed to open database");
enable_raw_mode()?;
let mut stdout = io::stdout();
stdout.execute(EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let mut app = App::new(db);
while !app.should_quit {
terminal.draw(|frame| {
let view = AppView {
lists: &app.lists,
tasks: &app.tasks,
selected_list: app.selected_list,
selected_task: app.selected_task,
focus: app.focus.clone(),
show_popup: app.show_popup.as_ref(),
popup_input: &app.popup_input,
popup_cursor: app.popup_cursor,
draft_date: app.draft_date,
network_status: &app.network_status,
task_list_scroll: app.task_list_scroll,
detail_scroll: app.detail_scroll,
};
draw(frame, view);
})?;
if let Event::Key(key) = event::read()? {
app.handle_key(key);
}
}
disable_raw_mode()?;
io::stdout().execute(LeaveAlternateScreen)?;
Ok(())
}