tododo 0.1.2

A minimal terminal todo manager built with Rust and Ratatui
Documentation
mod actions;
mod app;
mod cli;
mod db;
mod domain;
mod entity;
mod error;
mod repository;
mod screens;
mod tui;

use clap::Parser;
use db::{connect, run_migrations};
use domain::TodoService;
use error::AppError;
use repository::TodoRepository;
use std::path::PathBuf;
use tui::handler;

#[derive(serde::Deserialize)]
struct AppConfig {
    sort_mode: Option<ConfigSortMode>,
}

#[derive(serde::Deserialize)]
enum ConfigSortMode {
    #[serde(rename = "priority")]
    Priority,
    #[serde(rename = "created")]
    Created,
}

impl From<ConfigSortMode> for domain::SortMode {
    fn from(value: ConfigSortMode) -> Self {
        match value {
            ConfigSortMode::Priority => domain::SortMode::Priority,
            ConfigSortMode::Created => domain::SortMode::CreatedAt,
        }
    }
}

fn get_db_path() -> PathBuf {
    let exe_path = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("todo"));
    let exe_dir = exe_path
        .parent()
        .unwrap_or_else(|| std::path::Path::new("."));
    exe_dir.join(".todo").join("todo.db")
}

fn ensure_db_dir() -> Result<PathBuf, std::io::Error> {
    let db_path = get_db_path();
    if let Some(parent) = db_path.parent() {
        if !parent.exists() {
            std::fs::create_dir_all(parent)?;
        }
    }
    Ok(db_path)
}

#[tokio::main]
async fn main() -> Result<(), AppError> {
    let cli = cli::Cli::parse();

    if cli.list || cli.json || cli.import {
        if let Err(e) = run_cli_mode(cli).await {
            eprintln!("Error: {}", e);
            std::process::exit(1);
        }
        return Ok(());
    }

    if let Some(cmd) = cli.command {
        if let Err(e) = run_cli_command(cmd).await {
            eprintln!("Error: {}", e);
            std::process::exit(1);
        }
        return Ok(());
    }

    run_tui_mode().await
}

async fn run_cli_mode(cli: cli::Cli) -> Result<(), AppError> {
    let db_path = ensure_db_dir().map_err(|e| AppError::Init(e.to_string()))?;
    let db_url = format!("sqlite://{}/?mode=rwc", db_path.display());
    let db = connect(&db_url).await?;
    run_migrations(&db).await?;
    let service = TodoService::new(TodoRepository::new(db));

    if cli.list {
        cli::run_list(&service).await?;
    } else if cli.json {
        cli::run_json_export(&service).await?;
    } else if cli.import {
        cli::run_json_import(&service).await?;
    }
    Ok(())
}

async fn run_cli_command(cmd: cli::Commands) -> Result<(), AppError> {
    let db_path = ensure_db_dir().map_err(|e| AppError::Init(e.to_string()))?;
    let db_url = format!("sqlite://{}/?mode=rwc", db_path.display());
    let db = connect(&db_url).await?;
    run_migrations(&db).await?;
    let service = TodoService::new(TodoRepository::new(db));

    match cmd {
        cli::Commands::List => cli::run_list(&service).await?,
        cli::Commands::Add {
            title,
            comment,
            priority,
        } => cli::run_add(&service, &title, comment.as_deref(), priority.as_deref()).await?,
        cli::Commands::Done { index } => cli::run_done(&service, index).await?,
        cli::Commands::Json => cli::run_json_export(&service).await?,
        cli::Commands::Delete { index } => cli::run_delete(&service, index).await?,
        cli::Commands::Import => cli::run_json_import(&service).await?,
        cli::Commands::Comment { index, text } => cli::run_comment(&service, index, &text).await?,
        cli::Commands::Export { week } => {
            if week {
                cli::run_export_week(&service).await?
            } else {
                cli::run_json_export(&service).await?
            }
        }
    }
    Ok(())
}

async fn run_tui_mode() -> Result<(), AppError> {
    let db_path = ensure_db_dir().map_err(|e| AppError::Init(e.to_string()))?;

    let exe_path = std::env::current_exe().map_err(|e| AppError::Init(e.to_string()))?;
    let exe_dir = exe_path
        .parent()
        .unwrap_or_else(|| std::path::Path::new("."))
        .to_path_buf();

    let db_url = format!("sqlite://{}?mode=rwc", db_path.display());

    let db = connect(&db_url)
        .await
        .map_err(|e| AppError::Init(format!("failed to connect to db: {}", e)))?;
    run_migrations(&db)
        .await
        .map_err(|e| AppError::Init(format!("failed to run migrations: {}", e)))?;
    let service = TodoService::new(TodoRepository::new(db));
    let mut app = app::App::new();
    app.sort_mode = load_config(&exe_dir);
    handler::reload_todos(&mut app, &service).await;

    tui::event::run(&mut app, &service, exe_dir.as_path())
        .await
        .map_err(|e| AppError::Init(format!("TUI error: {}", e)))?;

    Ok(())
}

fn load_config(exe_dir: &std::path::Path) -> domain::SortMode {
    let config_path = exe_dir.join(".todo").join("config.json");
    if !config_path.exists() {
        return domain::SortMode::Priority;
    }

    let content = match std::fs::read_to_string(&config_path) {
        Ok(content) => content,
        Err(err) => {
            eprintln!(
                "Warning: failed to read config {}: {}",
                config_path.display(),
                err
            );
            return domain::SortMode::Priority;
        }
    };

    let config = match serde_json::from_str::<AppConfig>(&content) {
        Ok(config) => config,
        Err(err) => {
            eprintln!(
                "Warning: failed to parse config {}: {}",
                config_path.display(),
                err
            );
            return domain::SortMode::Priority;
        }
    };

    config
        .sort_mode
        .map(domain::SortMode::from)
        .unwrap_or(domain::SortMode::Priority)
}