use std::process::Command; use chrono::Utc; use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; use crate::state::{AppState, TimeItem}; use crate::config::Config; pub enum Screen { Main, Help, ConfigHelp, NewEntry, } pub struct App { pub state: AppState, pub config: Config, pub current_screen: Screen, pub needs_redraw: bool, pub new_entry_buffer: String, pub new_entry_project: String, pub new_entry_cursor: usize, pub new_entry_mode: NewEntryMode, // Task or Project } pub enum NewEntryMode { Task, Project, } impl App { pub fn new() -> anyhow::Result { Ok(Self { state: AppState::load()?, config: Config::load()?, current_screen: Screen::Main, needs_redraw: true, new_entry_buffer: String::new(), new_entry_project: String::new(), new_entry_cursor: 0, new_entry_mode: NewEntryMode::Task, }) } pub fn handle_event(&mut self, event: Event) -> anyhow::Result { match self.current_screen { Screen::Main => self.handle_main_event(event), Screen::Help => self.handle_help_event(event), Screen::ConfigHelp => self.handle_config_help_event(event), Screen::NewEntry => self.handle_new_entry_event(event), } } fn handle_main_event(&mut self, event: Event) -> anyhow::Result { match event { Event::Key(KeyEvent { code, modifiers, .. }) => match (code, modifiers) { (KeyCode::Char('q'), _) => return Ok(true), (KeyCode::Char('j'), _) | (KeyCode::Down, _) => self.move_selection(1), (KeyCode::Char('k'), _) | (KeyCode::Up, _) => self.move_selection(-1), (KeyCode::Char('h'), _) | (KeyCode::Left, _) => self.change_pane(-1), (KeyCode::Char('l'), _) | (KeyCode::Right, _) => self.change_pane(1), (KeyCode::Char('n'), KeyModifiers::CONTROL) => self.change_pane(1), (KeyCode::Char('p'), KeyModifiers::CONTROL) => self.change_pane(-1), (KeyCode::Enter, _) => self.toggle_current_item()?, (KeyCode::Char('e'), KeyModifiers::CONTROL) => self.edit_config()?, (KeyCode::Char('?'), _) => self.current_screen = Screen::Help, (KeyCode::Char('c'), _) => self.edit_app_config()?, (KeyCode::Char('n'), _) => self.start_new_entry(), _ => {} }, _ => {} } Ok(false) } fn handle_new_entry_event(&mut self, event: Event) -> anyhow::Result { match event { Event::Key(KeyEvent { code, modifiers, .. }) => { match (code, modifiers) { (KeyCode::Esc, _) => { self.current_screen = Screen::Main; self.new_entry_buffer.clear(); self.new_entry_project.clear(); self.new_entry_mode = NewEntryMode::Task; } (KeyCode::Enter, _) => { match self.new_entry_mode { NewEntryMode::Task => { if !self.new_entry_buffer.is_empty() { self.new_entry_mode = NewEntryMode::Project; } } NewEntryMode::Project => { if self.new_entry_project.is_empty() || self.config.is_valid_project(&self.new_entry_project) { // Create new time item let item = TimeItem { name: self.new_entry_buffer.clone(), tags: if self.new_entry_project.is_empty() { vec![] } else { vec![self.new_entry_project.clone()] }, last_used: Some(Utc::now()), }; // Add to current pane (or recurring if in recent) match self.state.current_pane { 0 => self.state.permanent_items.push(item), // Permanent Items 1 => self.state.recurring_items.push(item), // Recurring Items 2 => self.state.recent_items.push(item), // Ad-Hoc Items _ => unreachable!(), } self.state.save()?; // Clear and return to main screen self.current_screen = Screen::Main; self.new_entry_buffer.clear(); self.new_entry_project.clear(); self.new_entry_mode = NewEntryMode::Task; } } } } (KeyCode::Backspace, _) => { match self.new_entry_mode { NewEntryMode::Task => { if self.new_entry_cursor > 0 { self.new_entry_buffer.remove(self.new_entry_cursor - 1); self.new_entry_cursor -= 1; } } NewEntryMode::Project => { if self.new_entry_cursor > 0 { self.new_entry_project.remove(self.new_entry_cursor - 1); self.new_entry_cursor -= 1; } } } } (KeyCode::Char(c), m) if m.is_empty() => { match self.new_entry_mode { NewEntryMode::Task => { self.new_entry_buffer.insert(self.new_entry_cursor, c); self.new_entry_cursor += 1; } NewEntryMode::Project => { self.new_entry_project.insert(self.new_entry_cursor, c); self.new_entry_cursor += 1; } } } _ => {} } } _ => {} } Ok(false) } fn handle_help_event(&mut self, event: Event) -> anyhow::Result { match event { Event::Key(KeyEvent { code, .. }) => match code { KeyCode::Char('c') => self.current_screen = Screen::ConfigHelp, KeyCode::Esc | KeyCode::Char('q') => self.current_screen = Screen::Main, _ => {} }, _ => {} } Ok(false) } fn handle_config_help_event(&mut self, event: Event) -> anyhow::Result { match event { Event::Key(KeyEvent { code, .. }) => match code { KeyCode::Esc | KeyCode::Char('q') => self.current_screen = Screen::Help, _ => {} }, _ => {} } Ok(false) } fn move_selection(&mut self, delta: i32) { let items_len = match self.state.current_pane { 0 => self.state.permanent_items.len(), 1 => self.state.recurring_items.len(), 2 => self.state.recent_items.len(), _ => return, }; if items_len == 0 { return; } let current = self.state.selected_indices[self.state.current_pane] as i32; let new_index = (current + delta).rem_euclid(items_len as i32) as usize; self.state.selected_indices[self.state.current_pane] = new_index; } fn change_pane(&mut self, delta: i32) { self.state.current_pane = ((self.state.current_pane as i32 + delta).rem_euclid(3)) as usize; } fn get_current_item(&self) -> Option { let items = match self.state.current_pane { 0 => &self.state.permanent_items, 1 => &self.state.recurring_items, 2 => &self.state.recent_items, _ => return None, }; let index = self.state.selected_indices[self.state.current_pane]; items.get(index).cloned() } fn toggle_current_item(&mut self) -> anyhow::Result<()> { if let Some(item) = self.get_current_item() { if self.state.active_timer.as_ref().map(|(active, _)| active.name == item.name).unwrap_or(false) { self.state.stop_timer()?; } else { self.state.start_timer(item)?; } self.state.save()?; } Ok(()) } fn start_new_entry(&mut self) { self.current_screen = Screen::NewEntry; self.new_entry_buffer.clear(); self.new_entry_project.clear(); self.new_entry_cursor = 0; self.new_entry_mode = NewEntryMode::Task; } fn edit_app_config(&mut self) -> anyhow::Result<()> { use crossterm::{ terminal::{disable_raw_mode, enable_raw_mode}, execute, terminal::{LeaveAlternateScreen, EnterAlternateScreen}, }; use std::io::stdout; let editor = std::env::var("EDITOR").unwrap_or_else(|_| "vi".to_string()); let config_path = Config::config_path()?; // Leave TUI mode disable_raw_mode()?; execute!(stdout(), LeaveAlternateScreen)?; // Run editor let status = Command::new(editor) .arg(&config_path) .status()?; if status.success() { // Reload entire application state self.config = Config::load()?; // Signal for complete reload self.current_screen = Screen::Main; self.needs_redraw = true; } // Return to TUI mode execute!(stdout(), EnterAlternateScreen)?; enable_raw_mode()?; Ok(()) } fn edit_config(&mut self) -> anyhow::Result<()> { use crossterm::{ terminal::{disable_raw_mode, enable_raw_mode}, execute, terminal::{LeaveAlternateScreen, EnterAlternateScreen}, }; use std::io::stdout; let editor = std::env::var("EDITOR").unwrap_or_else(|_| "vi".to_string()); let config_path = AppState::config_file()?; if !config_path.exists() { std::fs::write(&config_path, "# WAT Configuration\n# Add your permanent items here\n\npermanent_items:\n - name: Daily Standup\n tags: [daily, meeting]\n")?; } // Leave TUI mode disable_raw_mode()?; execute!(stdout(), LeaveAlternateScreen)?; // Run editor let status = Command::new(editor) .arg(&config_path) .status()?; if status.success() { // Reload entire application state self.state = AppState::load()?; // Signal for complete reload self.current_screen = Screen::Main; self.needs_redraw = true; } // Return to TUI mode execute!(stdout(), EnterAlternateScreen)?; enable_raw_mode()?; Ok(()) } }