diff --git a/toki-tui/src/app/mod.rs b/toki-tui/src/app/mod.rs index 01222362..708e5702 100644 --- a/toki-tui/src/app/mod.rs +++ b/toki-tui/src/app/mod.rs @@ -191,6 +191,18 @@ impl App { } /// Clear timer and reset to default state + pub fn clear_project_activity(&mut self) { + self.selected_project = None; + self.selected_activity = None; + self.status_message = Some("Project and activity cleared".to_string()); + } + + pub fn clear_note(&mut self) { + self.description_input = TextInput::new(); + self.description_is_default = true; + self.status_message = Some("Note cleared".to_string()); + } + pub fn clear_timer(&mut self) { self.timer_state = TimerState::Stopped; self.absolute_start = None; @@ -370,6 +382,8 @@ impl App { .iter() .position(|a| self.selected_activity.as_ref().map(|sa| &sa.id) == Some(&a.id)) .unwrap_or(0); + self.activity_search_input.clear(); + self.filter_activities(); self.selection_list_focused = false; } View::EditDescription => { @@ -479,7 +493,12 @@ impl App { /// Cancel current selection and return to timer view pub fn cancel_selection(&mut self) { - self.pending_edit_selection_restore = None; + if let Some((restore_project, restore_activity)) = + self.pending_edit_selection_restore.take() + { + self.selected_project = restore_project; + self.selected_activity = restore_activity; + } self.navigate_to(View::Timer); } diff --git a/toki-tui/src/runtime/action_queue.rs b/toki-tui/src/runtime/action_queue.rs index c52fc62a..329e16a7 100644 --- a/toki-tui/src/runtime/action_queue.rs +++ b/toki-tui/src/runtime/action_queue.rs @@ -14,6 +14,9 @@ pub(super) enum Action { saved_selected_project: Option, saved_selected_activity: Option, }, + OpenEditActivityPicker { + project_id: String, + }, StartTimer, SaveTimer, SyncRunningTimerNote { diff --git a/toki-tui/src/runtime/actions.rs b/toki-tui/src/runtime/actions.rs index daf54be0..bf666b07 100644 --- a/toki-tui/src/runtime/actions.rs +++ b/toki-tui/src/runtime/actions.rs @@ -4,7 +4,7 @@ use crate::types; use anyhow::{Context, Result}; use std::time::{Duration, Instant}; -use super::action_queue::Action; +use super::action_queue::{Action, ActionTx}; /// Apply an active timer fetched from the server into App state. pub(crate) fn restore_active_timer(app: &mut App, timer: crate::types::ActiveTimerState) { @@ -69,6 +69,25 @@ pub(super) async fn run_action( ) .await; } + Action::OpenEditActivityPicker { project_id } => { + app.pending_edit_selection_restore.get_or_insert_with(|| { + (app.selected_project.clone(), app.selected_activity.clone()) + }); + + let project_name = app + .projects + .iter() + .find_map(|project| (project.id == project_id).then(|| project.name.clone())) + .unwrap_or_default(); + app.selected_project = Some(types::Project { + id: project_id.clone(), + name: project_name, + }); + app.selected_activity = None; + + ensure_activities_for_project(app, client, &project_id).await; + app.navigate_to(app::View::SelectActivity); + } Action::StartTimer => { handle_start_timer(app, client).await?; } @@ -140,26 +159,12 @@ async fn handle_project_selection_enter( saved_selected_project: Option, saved_selected_activity: Option, ) { - // Fetch activities for the selected project (lazy, cached). - if let Some(project) = app.selected_project.clone() { - if !app.activity_cache.contains_key(&project.id) { - app.is_loading = true; - match client.get_activities(&project.id).await { - Ok(activities) => { - app.activity_cache.insert(project.id.clone(), activities); - } - Err(e) => { - app.set_status(format!("Failed to load activities: {}", e)); - } - } - app.is_loading = false; - } - - if let Some(cached) = app.activity_cache.get(&project.id) { - app.activities = cached.clone(); - app.filtered_activities = cached.clone(); - app.filtered_activity_index = 0; - } + if let Some(project_id) = app + .selected_project + .as_ref() + .map(|project| project.id.clone()) + { + ensure_activities_for_project(app, client, &project_id).await; } if had_edit_state { @@ -173,6 +178,32 @@ async fn handle_project_selection_enter( app.navigate_to(app::View::SelectActivity); } +async fn ensure_activities_for_project(app: &mut App, client: &mut ApiClient, project_id: &str) { + if !app.activity_cache.contains_key(project_id) { + app.is_loading = true; + let fetch_result = client.get_activities(project_id).await; + app.is_loading = false; + + match fetch_result { + Ok(activities) => { + app.activity_cache + .insert(project_id.to_string(), activities); + } + Err(e) => { + app.set_status(format!("Failed to load activities: {}", e)); + } + } + } + + if let Some(cached) = app.activity_cache.get(project_id) { + app.activities = cached.clone(); + } else { + app.activities.clear(); + } + app.filtered_activities.clear(); + app.filtered_activity_index = 0; +} + async fn handle_activity_selection_enter( app: &mut App, client: &mut ApiClient, @@ -405,56 +436,59 @@ pub(super) async fn handle_save_timer_with_action( // Helper functions for edit mode -/// Handle Enter key in edit mode - open modal for Project/Activity/Note or move to next field -pub(super) fn handle_entry_edit_enter(app: &mut App) { +enum EditEnterAction { + ProjectPicker, + ActivityPicker { project_id: String }, + NoteEditor { note: String }, +} + +/// Handle Enter key in edit mode - open modal for Project/Activity/Note or move to next field. +pub(super) fn handle_entry_edit_enter(app: &mut App, action_tx: &ActionTx) { // Extract the data we need first to avoid borrow conflicts - let action = { - if let Some(state) = app.current_edit_state() { - match state.focused_field { - app::EntryEditField::Project => Some(('P', None)), - app::EntryEditField::Activity => { - if state.project_id.is_some() { - Some(('A', None)) - } else { - app.set_status("Please select a project first".to_string()); - None - } - } - app::EntryEditField::Note => { - let note = state.note.value.clone(); - Some(('N', Some(note))) - } - app::EntryEditField::StartTime | app::EntryEditField::EndTime => { - // Move to next field (like Tab) - app.entry_edit_next_field(); + let action = if let Some(state) = app.current_edit_state() { + match state.focused_field { + app::EntryEditField::Project => Some(EditEnterAction::ProjectPicker), + app::EntryEditField::Activity => { + if let Some(project_id) = state.project_id.clone() { + Some(EditEnterAction::ActivityPicker { project_id }) + } else { + app.set_status("Please select a project first".to_string()); None } } - } else { - None + app::EntryEditField::Note => { + let note = state.note.value.clone(); + Some(EditEnterAction::NoteEditor { note }) + } + app::EntryEditField::StartTime | app::EntryEditField::EndTime => { + // Move to next field (like Tab) + app.entry_edit_next_field(); + None + } } + } else { + None }; - // Now perform actions that don't require the borrow - if let Some((action, note)) = action { - match action { - 'P' => { - app.navigate_to(app::View::SelectProject); - } - 'A' => { - app.navigate_to(app::View::SelectActivity); - } - 'N' => { - // Save running timer's note before overwriting with entry's note - app.saved_timer_note = Some(app.description_input.value.clone()); - // Set description_input from the edit state before navigating - if let Some(n) = note { - app.description_input = TextInput::from_str(&n); - } - // Open description editor - app.navigate_to(app::View::EditDescription); - } - _ => {} + let Some(action) = action else { + return; + }; + + // Now perform actions that don't require the borrow. + match action { + EditEnterAction::ProjectPicker => { + app.navigate_to(app::View::SelectProject); + } + EditEnterAction::ActivityPicker { project_id } => { + let _ = action_tx.send(Action::OpenEditActivityPicker { project_id }); + } + EditEnterAction::NoteEditor { note } => { + // Save running timer's note before overwriting with entry's note + app.saved_timer_note = Some(app.description_input.value.clone()); + // Set description_input from the edit state before navigating + app.description_input = TextInput::from_str(¬e); + // Open description editor + app.navigate_to(app::View::EditDescription); } } } diff --git a/toki-tui/src/runtime/event_loop.rs b/toki-tui/src/runtime/event_loop.rs index 4e86b853..6a373e70 100644 --- a/toki-tui/src/runtime/event_loop.rs +++ b/toki-tui/src/runtime/event_loop.rs @@ -2,7 +2,7 @@ use crate::api::ApiClient; use crate::app::App; use crate::ui; use anyhow::Result; -use crossterm::event::{self, Event}; +use crossterm::event::{self, Event, KeyEventKind}; use ratatui::{backend::CrosstermBackend, Terminal}; use std::io; use std::time::{Duration, Instant}; @@ -38,6 +38,9 @@ pub async fn run_app( if event::poll(Duration::from_millis(100))? { if let Event::Key(key) = event::read()? { + if key.kind != KeyEventKind::Press { + continue; + } if app.milltime_reauth.is_some() { handle_milltime_reauth_key(key, app, &action_tx); } else { diff --git a/toki-tui/src/runtime/views/history.rs b/toki-tui/src/runtime/views/history.rs index ffa0b4a6..40ff7c77 100644 --- a/toki-tui/src/runtime/views/history.rs +++ b/toki-tui/src/runtime/views/history.rs @@ -64,7 +64,7 @@ pub(super) fn handle_history_key(key: KeyEvent, app: &mut App, action_tx: &Actio app.entry_edit_next_field(); } _ => { - handle_entry_edit_enter(app); + handle_entry_edit_enter(app, action_tx); } } } diff --git a/toki-tui/src/runtime/views/timer.rs b/toki-tui/src/runtime/views/timer.rs index 6b0186ae..c78032ea 100644 --- a/toki-tui/src/runtime/views/timer.rs +++ b/toki-tui/src/runtime/views/timer.rs @@ -94,6 +94,14 @@ pub(super) fn handle_timer_key(key: KeyEvent, app: &mut App, action_tx: &ActionT if !is_note_focused_in_this_week_edit(app) { app.entry_edit_backspace(); } + } else if app.timer_state == app::TimerState::Stopped + && app.focused_box == app::FocusedBox::ProjectActivity + { + app.clear_project_activity(); + } else if app.timer_state == app::TimerState::Stopped + && app.focused_box == app::FocusedBox::Description + { + app.clear_note(); } else if is_persisted_today_row_selected(app) { app.enter_delete_confirm(app::DeleteOrigin::Timer); } @@ -156,7 +164,7 @@ fn handle_enter_key(app: &mut App, action_tx: &ActionTx) { app.entry_edit_next_field(); } _ => { - handle_entry_edit_enter(app); + handle_entry_edit_enter(app, action_tx); } } }