Overview
Timo is a command-line application built with Rust that allows users to capture and manage thoughts directly from the terminal. The application follows a clean, layered architecture with clear separation of concerns.Project Structure
The codebase is organized into the following modules:Core Components
Application Entry (main.rs)
The application entry point at src/main.rs:1 orchestrates the initialization:
- Parses command-line arguments using the
Clistruct - Creates the
Appinstance with dependency injection - Delegates command execution to the
Executor
CLI Layer (cli.rs, commands.rs)
The CLI layer uses the clap crate with the derive API:
Cli(src/cli.rs:5): Top-level CLI struct that holds the subcommandCommands(src/commands.rs:3): Enum defining all available commands:Add: Add a new thought with optional labelList: Display all thoughts with optional filteringSearch: Search thoughts by keyword with optional label filterRemove: Delete specific thoughts by IDClear: Remove all thoughts with confirmation
Application Container (app.rs)
The App struct (src/app.rs:4) serves as a dependency injection container:
- Initializes the storage layer (currently
SqliteStorage) - Loads environment variables via
dotenv - Provides storage access to other components
Execution Layer (executor.rs)
The Executor (src/executor.rs:4) is responsible for:
- Bootstrapping the application
- Creating the
TaskServiceinstance - Matching CLI commands to service methods
- Handling command dispatch
Service Layer (task_service.rs)
The TaskService (src/task_service.rs:5) implements business logic:
- Processes raw command arguments
- Coordinates between repository and printer
- Handles data transformation (e.g., joining text arrays)
- Delegates data operations to the repository
Repository Layer (task_repository.rs)
The TaskRepository (src/task_repository.rs:3) provides data access abstraction:
- Wraps the
Storagetrait implementation - Performs data processing (e.g., case-insensitive search)
- Isolates business logic from storage details
Storage Layer (storage.rs, sqlite/)
Storage Trait (src/storage.rs:3)
Defines the storage interface with methods:
add(): Insert a new tasksearch(): Find tasks by content and optional labellist(): Retrieve all tasks with optional label filterdelete(): Remove a specific task by IDclear(): Delete all tasks
src/sqlite/sqlite_storage.rs:35)
The SqliteStorage struct implements the Storage trait:
- Uses rusqlite for database operations
- Stores database in local data directory
- Supports custom database names via
DB_NAMEenvironment variable - Implements parameterized queries for security
Presentation Layer (task_printer.rs)
The TaskPrinter (src/task_printer.rs:4) handles output formatting:
- Uses the colored crate for terminal styling
- Conditionally displays labels with visual highlighting
- Formats task output with ID and content
Data Model
TheTask struct (src/task.rs:1) represents a thought:
Database Schema
Timo uses SQLite for persistent storage with the following schema:Tasks Table
- V1 (
src/sql_migrations/V1__create_tasks_table.sql): Creates the tasks table - V2 (
src/sql_migrations/V2__add_label_column_in_tasks_table.sql): Adds label column
Migration System
Database migrations are handled by the refinery crate:- Migrations are embedded at compile time from
src/sql_migrations/ - Automatically run on application startup
- Ensure database schema is up-to-date
Data Flow
The typical data flow for a command follows this pattern:Example: Adding a Thought
- User runs:
timo add "Buy groceries" -l errands Cli::parse()parses the command intoCommands::AddExecutor::run()matches the command and callsTaskService::add_task()TaskServicejoins the text array and callsTaskRepository::add()TaskRepositorydelegates toStorage::add()SqliteStorage::add()executes an INSERT query- Task is persisted to the SQLite database
Example: Searching Thoughts
- User runs:
timo search meeting -l work -s - Command is parsed into
Commands::Search ExecutorcallsTaskService::search_task()TaskServicejoins the search key and callsTaskRepository::search()TaskRepositoryconverts the key to lowercase and queries storageSqliteStorage::search()executes a SELECT with LIKE and label filter- Results are returned as
Vec<Task> TaskServiceiterates and usesTaskPrinterto display each task
Design Patterns
Trait-Based Abstraction
TheStorage trait provides abstraction over the persistence layer, allowing:
- Easy testing with mock implementations
- Potential for multiple storage backends (PostgreSQL, files, etc.)
- Decoupling of business logic from storage details
Dependency Injection
TheApp struct serves as a simple DI container:
- Creates and manages the storage instance
- Passed to components that need storage access
- Enables better testability and modularity
Repository Pattern
TheTaskRepository implements the repository pattern:
- Provides a collection-like interface to the data layer
- Encapsulates query logic and data access
- Allows business logic to work with domain objects
Service Layer Pattern
TheTaskService implements business logic:
- Orchestrates between repository and presentation
- Handles command-specific processing
- Keeps executor thin and focused on dispatch
Dependencies
Key dependencies fromCargo.toml:
- clap (v4.5.6): Command-line argument parsing with derive macros
- rusqlite (v0.31.0): SQLite database interface with bundled SQLite
- refinery (v0.8.14): Database migration framework
- colored (v2.1.0): Terminal color and styling
- dirs (v5.0.1): Platform-specific directory paths
- dotenv (v0.15.0): Environment variable loading
Configuration
Environment Variables
DB_NAME: Custom database filename (default:.timo.db)- Used during development to avoid conflicts
- Production uses the default value
Database Location
The SQLite database is stored in the platform-specific local data directory:- Linux:
~/.local/share/.timo.db - macOS:
~/Library/Application Support/.timo.db - Windows:
%LOCALAPPDATA%\.timo.db
dirs::data_local_dir() function in src/sqlite/sqlite_storage.rs:14.
Extension Points
The architecture supports several extension possibilities:New Commands
- Add variant to
Commandsenum insrc/commands.rs - Add match arm in
Executor::run()insrc/executor.rs - Implement method in
TaskService - Add storage method if needed in
Storagetrait
Alternative Storage Backends
- Implement the
Storagetrait for new backend - Update
App::new()to instantiate the new storage - All other layers remain unchanged
Enhanced Task Model
- Add fields to
Taskstruct insrc/task.rs - Update database schema with new migration
- Modify storage implementation to handle new fields
- Update printer for new display requirements