Skip to main content

oflow/
lib.rs

1// Copyright (c) 2026
2//
3// This Source Code Form is subject to the terms of the Mozilla Public
4// License, v. 2.0. If a copy of the MPL was not distributed with this
5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
6
7//! # oflow
8//!
9//! A focused todo CLI library.
10//!
11//! `oflow` provides both a command-line interface and a Rust library for managing
12//! todo items backed by SQLite. Use [`TodoManager`] as the high-level API, or
13//! work directly with [`TodoList`] and [`Todo`] for lower-level control.
14//!
15//! ## Quick Start
16//!
17//! ```rust,no_run
18//! use oflow::TodoManager;
19//!
20//! #[tokio::main]
21//! async fn main() -> color_eyre::Result<()> {
22//!     let mut manager = TodoManager::new().await?;
23//!     manager.add("Buy milk").await?;
24//!     manager.finish("Buy milk").await?;
25//!     manager.clean().await?;
26//!
27//!     for todo in manager.list() {
28//!         println!("{}", todo);
29//!     }
30//!     Ok(())
31//! }
32//! ```
33//!
34//! ## Modules
35//!
36//! - [`commands`] — CLI argument parsing and command dispatch (via `clap`).
37//! - [`database`] — Database persistence layer: sync, load, and query helpers
38//!   implemented on [`TodoList`].
39//! - [`models`] — Core data types: [`Todo`] and [`TodoList`].
40
41pub mod commands;
42/// Database persistence operations for [`TodoList`].
43///
44/// Provides `sync_to_db`, `load_from_db`, and `find_by_content` methods
45/// that are attached to [`TodoList`] via `impl` blocks in this module.
46pub mod database;
47pub mod models;
48mod tui;
49
50// Re-export public types
51pub use commands::{Cli, Commands, execute_command};
52pub use models::{Todo, TodoList};
53use sqlx::SqlitePool;
54
55/// A high-level manager for Todo operations.
56///
57/// Provides a simple API to manage todos without dealing with
58/// database connections and migrations manually.
59pub struct TodoManager {
60    /// The underlying todo list
61    pub todolist: TodoList,
62    pool: SqlitePool,
63}
64
65impl TodoManager {
66    /// Create a new TodoManager with automatic database initialization.
67    ///
68    /// This will:
69    /// 1. Create the parent directory if it doesn't exist
70    /// 2. Connect to SQLite database (creates `todos.db` if not exists)
71    /// 3. Run migrations
72    /// 4. Load existing todos from database
73    ///
74    /// # Errors
75    ///
76    /// Returns an error if database connection fails or migrations cannot run.
77    pub async fn new() -> color_eyre::Result<Self> {
78        // Ensure parent directory exists for the database file
79        if let Some(parent) = std::path::Path::new("todos.db").parent()
80            && !parent.as_os_str().is_empty()
81            && !parent.exists()
82        {
83            std::fs::create_dir_all(parent)?;
84        }
85
86        let pool = SqlitePool::connect("sqlite:todos.db?mode=rwc").await?;
87        sqlx::migrate!("./migrations").run(&pool).await?;
88
89        let mut todolist = TodoList::new();
90        todolist.load_from_db(&pool).await?;
91
92        Ok(Self { todolist, pool })
93    }
94
95    /// Create a new TodoManager with custom database path.
96    ///
97    /// # Arguments
98    ///
99    /// * `db_path` - Path to SQLite database file
100    ///
101    /// # Errors
102    ///
103    /// Returns an error if database connection fails or migrations cannot run.
104    pub async fn with_db_path(db_path: &str) -> color_eyre::Result<Self> {
105        // Ensure parent directory exists for the database file
106        if let Some(parent) = std::path::Path::new(db_path).parent()
107            && !parent.as_os_str().is_empty()
108            && !parent.exists()
109        {
110            std::fs::create_dir_all(parent)?;
111        }
112
113        let pool = SqlitePool::connect(&format!("sqlite:{}?mode=rwc", db_path)).await?;
114        sqlx::migrate!("./migrations").run(&pool).await?;
115
116        let mut todolist = TodoList::new();
117        todolist.load_from_db(&pool).await?;
118
119        Ok(Self { todolist, pool })
120    }
121
122    /// Add a new todo item.
123    ///
124    /// # Arguments
125    ///
126    /// * `content` - The todo content
127    ///
128    /// # Errors
129    ///
130    /// Returns an error if database operation fails.
131    pub async fn add(&mut self, content: &str) -> color_eyre::Result<()> {
132        self.todolist
133            .add_todo_db(content.to_string(), &self.pool)
134            .await?;
135        self.todolist.sync_to_db(&self.pool).await?;
136        Ok(())
137    }
138
139    /// Mark a todo as finished.
140    ///
141    /// # Arguments
142    ///
143    /// * `content` - The todo content to mark as finished
144    ///
145    /// # Errors
146    ///
147    /// Returns an error if database operation fails.
148    pub async fn finish(&mut self, content: &str) -> color_eyre::Result<()> {
149        self.todolist
150            .finish_todo_db(content.to_string(), &self.pool)
151            .await?;
152        self.todolist.sync_to_db(&self.pool).await?;
153        Ok(())
154    }
155
156    /// Edit a todo's content.
157    ///
158    /// # Arguments
159    ///
160    /// * `find` - The content to search for
161    /// * `replace` - The new content
162    ///
163    /// # Errors
164    ///
165    /// Returns an error if database operation fails.
166    pub async fn edit(&mut self, find: &str, replace: &str) -> color_eyre::Result<()> {
167        self.todolist
168            .edit_todo_db(find.to_string(), replace.to_string(), &self.pool)
169            .await?;
170        self.todolist.sync_to_db(&self.pool).await?;
171        Ok(())
172    }
173
174    /// Clean all completed/finished todos.
175    ///
176    /// # Errors
177    ///
178    /// Returns an error if database operation fails.
179    pub async fn clean(&mut self) -> color_eyre::Result<()> {
180        self.todolist.clean_todo_db(&self.pool).await?;
181        Ok(())
182    }
183
184    /// Get an iterator over all todos.
185    pub fn list(&self) -> impl Iterator<Item = &Todo> {
186        self.todolist.todos.values()
187    }
188
189    /// Get an iterator over pending (not finished) todos.
190    pub fn list_pending(&self) -> impl Iterator<Item = &Todo> {
191        self.todolist.todos.values().filter(|t| !t.finished)
192    }
193
194    /// Get an iterator over finished todos.
195    pub fn list_finished(&self) -> impl Iterator<Item = &Todo> {
196        self.todolist.todos.values().filter(|t| t.finished)
197    }
198
199    /// Get todo by content.
200    ///
201    /// # Arguments
202    ///
203    /// * `content` - The content to search for
204    ///
205    /// # Returns
206    ///
207    /// Returns the Todo if found, None otherwise.
208    pub async fn get(&self, content: &str) -> color_eyre::Result<Option<Todo>> {
209        // Query from database for the most up-to-date data
210        let result = sqlx::query_as::<_, Todo>("SELECT * FROM todos WHERE content = ? LIMIT 1")
211            .bind(content)
212            .fetch_optional(&self.pool)
213            .await?;
214        Ok(result)
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    #[tokio::test]
223    #[ignore] // Requires migrations directory to exist
224    async fn test_todo_manager_new() {
225        // This test requires the migrations directory to exist
226        // Use #[ignore] since it depends on external resources
227        let result = TodoManager::with_db_path(":memory:").await;
228        assert!(result.is_ok(), "TodoManager should initialize with valid migrations");
229    }
230}