tododo 0.1.2

A minimal terminal todo manager built with Rust and Ratatui
Documentation
use std::path::Path;

use crate::actions::AppAction;
use crate::app::{App, Screen};
use crate::domain::{SortMode, TodoService};
use crate::error::AppError;

pub async fn execute(
    app: &mut App,
    action: AppAction,
    service: &TodoService,
    exe_dir: &Path,
) -> Result<(), AppError> {
    match action {
        AppAction::Quit => {
            app.should_quit = true;
        }
        AppAction::SelectTodo(index) => {
            if index < app.todos.len() {
                app.list_state.select(Some(index));
                app.selected_id = app.todos.get(index).map(|t| t.id.clone());
            }
        }
        AppAction::SelectPrevious => {
            app.select_previous();
        }
        AppAction::SelectNext => {
            app.select_next();
        }
        AppAction::OpenDetail(id) => {
            // Set selected_id BEFORE transitioning to Detail to avoid rendering with stale data
            app.selected_id = Some(id.clone());
            app.transition_to(Screen::Detail);
            if let Ok(todo) = service.get(&id).await {
                let todo_id = todo.id.clone();
                if let Some(idx) = app.todos.iter().position(|t| t.id == todo_id) {
                    app.todos[idx] = todo;
                }
            }
        }
        AppAction::EditTodo(id) => {
            if let Some(todo) = app.todos.iter().find(|t| t.id == id) {
                let mut content = todo.title.clone();
                if !todo.note.is_empty() {
                    content.push('\n');
                    content.push_str(&todo.note);
                }
                app.init_note_editor(content);
                app.transition_to(Screen::NoteEditor);
            }
        }
        AppAction::CreateTodoStart => {
            app.init_create_todo_editor();
            app.transition_to(Screen::CreateTodo);
        }
        AppAction::CreateTodo(title, note) => {
            if let Ok(todo) = service.create(&title, &note).await {
                reload_todos(app, service).await;
                if let Some(idx) = app.todos.iter().position(|t| t.id == todo.id) {
                    app.list_state.select(Some(idx));
                    app.selected_id = Some(todo.id);
                }
                app.screen = Screen::List;
                app.previous_screen = None;
                app.create_todo_editor = None;
            }
        }
        AppAction::ToggleTodo(id) => {
            app.selected_id = Some(id.clone());
            if let Some(idx) = app.todos.iter().position(|t| t.id == id) {
                app.list_state.select(Some(idx));
            }
            if let Err(e) = service.toggle(&id).await {
                app.error_message = Some(e.to_string());
            } else {
                reload_todos(app, service).await;
            }
        }
        AppAction::ShowDeleteConfirm => {
            app.transition_to(Screen::DeleteConfirm);
        }
        AppAction::CloseDeleteConfirm => {
            app.screen = app.previous_screen.take().unwrap_or(Screen::List);
        }
        AppAction::DeleteTodo(id) => {
            if let Err(e) = service.delete(&id).await {
                app.error_message = Some(e.to_string());
            } else {
                reload_todos(app, service).await;
                app.screen = Screen::List;
                app.previous_screen = None;
            }
        }
        AppAction::SaveTodo(id, title, note) => {
            if let Err(e) = service.update(&id, &title, &note).await {
                app.error_message = Some(e.to_string());
            } else {
                reload_todos(app, service).await;
                app.screen = Screen::Detail;
                app.note_editor = None;
            }
        }
        AppAction::SetPriority(id, priority) => {
            app.selected_id = Some(id.clone());
            if let Some(idx) = app.todos.iter().position(|t| t.id == id) {
                app.list_state.select(Some(idx));
            }
            if let Err(e) = service.set_priority(&id, &priority).await {
                app.error_message = Some(e.to_string());
            } else {
                let prev_id = Some(id);
                reload_todos(app, service).await;
                if let Some(ref prev) = prev_id {
                    if let Some(idx) = app.todos.iter().position(|t| t.id == *prev) {
                        app.list_state.select(Some(idx));
                    }
                }
            }
        }
        AppAction::CloseDetail => {
            app.screen = Screen::List;
            app.previous_screen = None;
        }
        AppAction::CloseNoteEditor => {
            app.screen = Screen::Detail;
            app.note_editor = None;
        }
        AppAction::CloseCreateTodo => {
            app.screen = Screen::List;
            app.previous_screen = None;
            app.create_todo_editor = None;
        }
        AppAction::ToggleSortMode => {
            app.sort_mode.toggle();
            save_config(exe_dir, app.sort_mode);
            reload_todos(app, service).await;
        }
    }
    Ok(())
}

pub async fn reload_todos(app: &mut App, service: &TodoService) {
    let prev_id = app.selected_id.clone();
    match service.list_active_sorted(app.sort_mode).await {
        Ok(todos) => {
            app.todos = todos;
            if !app.todos.is_empty() {
                if let Some(ref id) = prev_id {
                    if let Some(idx) = app.todos.iter().position(|t| t.id == *id) {
                        app.list_state.select(Some(idx));
                        app.selected_id = prev_id;
                    } else {
                        app.list_state.select(Some(0));
                        app.selected_id = app.todos.first().map(|t| t.id.clone());
                    }
                } else {
                    app.list_state.select(Some(0));
                    app.selected_id = app.todos.first().map(|t| t.id.clone());
                }
            }
            app.error_message = None;
        }
        Err(e) => {
            app.error_message = Some(e.to_string());
        }
    }
}

pub fn save_config(exe_dir: &Path, sort_mode: SortMode) {
    let config_path = exe_dir.join(".todo").join("config.json");
    if let Some(parent) = config_path.parent() {
        if let Err(err) = std::fs::create_dir_all(parent) {
            eprintln!(
                "Warning: failed to create config directory {}: {}",
                parent.display(),
                err
            );
            return;
        }
    }
    let json = serde_json::json!({
            "sort_mode": sort_mode.label()
    });
    let content = match serde_json::to_string_pretty(&json) {
        Ok(content) => content,
        Err(err) => {
            eprintln!(
                "Warning: failed to serialize config {}: {}",
                config_path.display(),
                err
            );
            return;
        }
    };
    if let Err(err) = std::fs::write(&config_path, content) {
        eprintln!(
            "Warning: failed to write config {}: {}",
            config_path.display(),
            err
        );
    }
}