Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions toki-tui/src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,20 @@ impl App {
}

/// Clear timer and reset to default state
/// Clear the selected project and activity (timer must be stopped).
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());
}

/// Clear the note/description input (timer must be stopped).
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;
Expand Down Expand Up @@ -373,6 +387,9 @@ 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.filtered_activities = self.activities.clone();
self.filtered_activity_index = 0;
self.selection_list_focused = false;
}
View::EditDescription => {
Expand Down
120 changes: 74 additions & 46 deletions toki-tui/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use anyhow::{Context, Result};
use api_client::ApiClient;
use app::{App, TextInput};
use crossterm::{
event::{self, Event, KeyCode, KeyModifiers},
event::{self, Event, KeyCode, KeyEventKind, KeyModifiers},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
Expand Down Expand Up @@ -237,6 +237,9 @@ async fn run_app(

if event::poll(Duration::from_millis(100))? {
if let Event::Key(key) = event::read()? {
if key.kind != KeyEventKind::Press {
continue;
}
// Milltime re-auth overlay intercepts all keys while it is open
if app.milltime_reauth.is_some() {
match key.code {
Expand Down Expand Up @@ -650,7 +653,7 @@ async fn run_app(
app.entry_edit_next_field();
}
_ => {
handle_entry_edit_enter(app);
handle_entry_edit_enter_async(app, client).await;
}
}
}
Expand Down Expand Up @@ -873,7 +876,7 @@ async fn run_app(
}
_ => {
// In edit mode, Enter on Project/Activity/Note opens modal
handle_entry_edit_enter(app);
handle_entry_edit_enter_async(app, client).await;
}
}
}
Expand Down Expand Up @@ -908,6 +911,14 @@ async fn run_app(
if !on_note {
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 app.focused_box == app::FocusedBox::Today
&& app.focused_this_week_index.is_some_and(|idx| {
!(app.timer_state == app::TimerState::Running && idx == 0)
Expand Down Expand Up @@ -1193,57 +1204,74 @@ async fn handle_save_timer_with_action(app: &mut App, client: &mut ApiClient) ->

// Helper functions for edit mode

/// Handle Enter key in edit mode - open modal for Project/Activity/Note or move to next field
fn handle_entry_edit_enter(app: &mut App) {
// 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();
enum EditEnterAction {
OpenProjectPicker,
OpenActivityPicker { project_id: String },
OpenNoteEditor { note: String },
}

/// Handle Enter key in edit mode - returns the action to perform (caller handles async fetch)
fn handle_entry_edit_enter(app: &mut App) -> Option<EditEnterAction> {
if let Some(state) = app.current_edit_state() {
match state.focused_field {
app::EntryEditField::Project => Some(EditEnterAction::OpenProjectPicker),
app::EntryEditField::Activity => {
if let Some(pid) = state.project_id.clone() {
Some(EditEnterAction::OpenActivityPicker { project_id: pid })
} else {
app.set_status("Please select a project first".to_string());
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);
app::EntryEditField::Note => {
let note = state.note.value.clone();
Some(EditEnterAction::OpenNoteEditor { note })
}
'A' => {
app.navigate_to(app::View::SelectActivity);
app::EntryEditField::StartTime | app::EntryEditField::EndTime => {
app.entry_edit_next_field();
None
}
'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);
}
} else {
None
}
}

/// Async wrapper: determines the edit-mode Enter action and performs it, fetching activities
/// from the API if they are not yet cached for the entry's project.
async fn handle_entry_edit_enter_async(app: &mut App, client: &mut ApiClient) {
let action = handle_entry_edit_enter(app);
match action {
Some(EditEnterAction::OpenProjectPicker) => {
app.navigate_to(app::View::SelectProject);
}
Some(EditEnterAction::OpenActivityPicker { project_id }) => {
// Fetch activities for the entry's project if not already cached.
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));
}
}
// Open description editor
app.navigate_to(app::View::EditDescription);
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;
}
_ => {}
app.navigate_to(app::View::SelectActivity);
}
Some(EditEnterAction::OpenNoteEditor { note }) => {
app.saved_timer_note = Some(app.description_input.value.clone());
app.description_input = TextInput::from_str(&note);
app.navigate_to(app::View::EditDescription);
}
None => {}
}
}

Expand Down