Skip to content
Merged
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
21 changes: 20 additions & 1 deletion toki-tui/src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 => {
Expand Down Expand Up @@ -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);
}

Expand Down
3 changes: 3 additions & 0 deletions toki-tui/src/runtime/action_queue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ pub(super) enum Action {
saved_selected_project: Option<Project>,
saved_selected_activity: Option<Activity>,
},
OpenEditActivityPicker {
project_id: String,
},
StartTimer,
SaveTimer,
SyncRunningTimerNote {
Expand Down
162 changes: 98 additions & 64 deletions toki-tui/src/runtime/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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?;
}
Expand Down Expand Up @@ -140,26 +159,12 @@ async fn handle_project_selection_enter(
saved_selected_project: Option<types::Project>,
saved_selected_activity: Option<types::Activity>,
) {
// 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 {
Expand All @@ -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,
Expand Down Expand Up @@ -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(&note);
// Open description editor
app.navigate_to(app::View::EditDescription);
}
}
}
Expand Down
5 changes: 4 additions & 1 deletion toki-tui/src/runtime/event_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion toki-tui/src/runtime/views/history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
Expand Down
10 changes: 9 additions & 1 deletion toki-tui/src/runtime/views/timer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
}
}
}
Expand Down
Loading