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}