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)
}