chloe_todo_tui 0.1.0

A terminal-based todo application with TUI
Documentation
use std::sync::Arc;

use anyhow::{Context, Result};
use chrono::Utc;
use diesel::prelude::*;
use diesel::sqlite::SqliteConnection;
use diesel_async::{RunQueryDsl, sync_connection_wrapper::SyncConnectionWrapper};
use tokio::sync::Mutex;

use crate::database::models::todos::{NewTodo, Todo};

#[derive(Clone)]
pub struct TodoRepository {
    conn: Arc<Mutex<SyncConnectionWrapper<SqliteConnection>>>,
}

impl TodoRepository {
    pub fn new(conn: SyncConnectionWrapper<SqliteConnection>) -> Self {
        Self {
            conn: Arc::new(Mutex::new(conn)),
        }
    }

    pub async fn list_all(&self) -> Result<Vec<Todo>> {
        use crate::schema::todos::dsl::*;

        let mut conn = self.conn.lock().await;
        let records = todos
            .order(created_at.desc())
            .select(Todo::as_select())
            .load(&mut *conn)
            .await
            .context("failed to load todos")?;

        Ok(records)
    }

    pub async fn insert(&self, draft: &NewTodoDraft) -> Result<Todo> {
        use crate::schema::todos::dsl::*;

        let mut conn = self.conn.lock().await;
        let inserted = diesel::insert_into(todos)
            .values(draft.as_insertable())
            .returning(Todo::as_returning())
            .get_result(&mut *conn)
            .await
            .context("failed to insert todo")?;

        Ok(inserted)
    }

    pub async fn set_completed(&self, todo_id: i32, completed_state: bool) -> Result<()> {
        use crate::schema::todos::dsl::*;

        let mut conn = self.conn.lock().await;
        let timestamp = Utc::now().naive_utc();
        diesel::update(todos.filter(id.eq(todo_id)))
            .set((completed.eq(completed_state), updated_at.eq(timestamp)))
            .execute(&mut *conn)
            .await
            .with_context(|| format!("failed to update todo #{todo_id}"))?;

        Ok(())
    }

    pub async fn delete(&self, todo_id: i32) -> Result<()> {
        use crate::schema::todos::dsl::*;

        let mut conn = self.conn.lock().await;
        diesel::delete(todos.filter(id.eq(todo_id)))
            .execute(&mut *conn)
            .await
            .with_context(|| format!("failed to delete todo #{todo_id}"))?;

        Ok(())
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Priority {
    Low,
    Medium,
    High,
}

impl Priority {
    pub const fn as_str(self) -> &'static str {
        match self {
            Priority::Low => "low",
            Priority::Medium => "medium",
            Priority::High => "high",
        }
    }

    pub const fn label(self) -> &'static str {
        match self {
            Priority::Low => "Low",
            Priority::Medium => "Medium",
            Priority::High => "High",
        }
    }

    pub const fn increase(self) -> Self {
        match self {
            Priority::Low => Priority::Medium,
            Priority::Medium => Priority::High,
            Priority::High => Priority::High,
        }
    }

    pub const fn decrease(self) -> Self {
        match self {
            Priority::Low => Priority::Low,
            Priority::Medium => Priority::Low,
            Priority::High => Priority::Medium,
        }
    }

    pub const fn cycle(self) -> Self {
        match self {
            Priority::Low => Priority::Medium,
            Priority::Medium => Priority::High,
            Priority::High => Priority::Low,
        }
    }
}

impl Default for Priority {
    fn default() -> Self {
        Priority::Medium
    }
}

#[derive(Clone, Debug, Default)]
pub struct NewTodoDraft {
    pub title: String,
    pub description: String,
    pub priority: Priority,
}

impl NewTodoDraft {
    pub fn is_valid(&self) -> bool {
        !self.title.trim().is_empty()
    }

    pub fn clear(&mut self) {
        self.title.clear();
        self.description.clear();
        self.priority = Priority::Medium;
    }

    pub fn as_insertable(&self) -> NewTodo<'_> {
        NewTodo {
            title: self.title.trim(),
            description: self.description_field(),
            priority: self.priority.as_str(),
        }
    }

    fn description_field(&self) -> Option<&str> {
        let desc = self.description.trim();
        if desc.is_empty() { None } else { Some(desc) }
    }
}