diff --git a/CLAUDE.md b/CLAUDE.md index 4445b64..5b915fb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,13 +37,14 @@ Two-crate workspace (`resolver = "2"`, edition 2021) plus a Tauri app: - **Storage trait** (`storage.rs`): Strategy pattern for task persistence. `FileSystemStorage` reads/writes markdown files with YAML frontmatter and JSON metadata files. Atomic writes (temp file + rename, with temp cleanup on failure) for all metadata files. Input validation: task titles max 500 chars, descriptions max 1MB, list names max 255 chars, YAML frontmatter max 64KB. Delete operations update metadata before removing files to prevent orphaned metadata on crash. - **Repository** (`repository.rs`): `TaskRepository` wraps a `Storage` impl and provides the public API for task/list CRUD, ordering, and grouping. Tests live here. -- **Config** (`config.rs`): `AppConfig` manages workspaces keyed by UUID string. `WorkspaceConfig` stores `name`, `path`, `mode` (Local/Webdav), `webdav_url`, `webdav_path` (user-selected remote folder), `theme`, and `sync_interval_secs`. `add_workspace` returns a generated UUID. Stored in platform-specific config dirs via the `directories` crate. Atomic writes (temp file + rename) prevent corruption on crash. +- **Config** (`config.rs`): `AppConfig` manages workspaces keyed by UUID string. `WorkspaceConfig` stores `name`, `path`, `mode` (Local/Webdav/GoogleTasks), `webdav_url`, `webdav_path` (user-selected remote folder), `google_account` (display name/email for GoogleTasks workspaces), `theme`, `sync_interval_secs` (focused polling interval), and `sync_interval_unfocused_secs` (lower-frequency polling when window loses focus, for mobile battery optimization). `add_workspace` returns a generated UUID. Stored in platform-specific config dirs via the `directories` crate. Atomic writes (temp file + rename) prevent corruption on crash. - **Sync** (`sync.rs`): Three-way diff sync with offline queue. File-based `.sync.lock` prevents concurrent sync operations (auto-cleaned after 5 minutes if stale). Checksum-based conflict resolution: downloads remote, compares SHA-256 — identical content is a false conflict (skipped); when different, remote wins and local is recovered as a duplicate with a new UUID and `[RECOVERED FROM CONFLICT]` prefix (duplicate file cleaned up if metadata update fails). Auto-sync lifecycle: periodic polling (configurable interval, default 60s), debounced file-change (5s), window-focus (30s stale threshold). Wrapped in `tokio::time::timeout` (60s) to handle unreachable servers on Windows. Path traversal validation rejects `..` components and backslashes anywhere in sync paths. Atomic writes for sync state and queue files (temp cleanup on failure). - **WebDAV** (`webdav.rs`): reqwest client with rustls-tls, 30s request timeout, 10s connect timeout. Rejects non-HTTPS URLs. `Zeroizing` for credential fields. `move_resource` method for WebDAV MOVE (workspace rename). 10MB cap on both PROPFIND responses and file downloads. Desktop credentials via `keyring` crate (feature-gated); Tauri GUI uses `tauri-plugin-credentials` for cross-platform support (Android Keystore + desktop keychain). +- **Google Tasks** (`google_tasks.rs`): Read-only Google Tasks API client using reqwest with Bearer auth. `gt_id_to_uuid()` converts Google Task IDs to stable UUID v5 values for consistent cross-sync identity. Fetches all task lists and tasks from the Google Tasks REST API and writes them locally via `FileSystemStorage`. Remote always wins (read-only workspace mode). OAuth flow is partially implemented — client ID/secret are placeholders pending real credentials. ### On-disk format -Workspaces are plain folders. Each task list is a subfolder containing `.listdata.json` (metadata/ordering) and one `.md` file per task. The workspace root has `.onyx-workspace.json` for list ordering and workspace detection. Sync only processes files at expected depths: `.onyx-workspace.json` at root, `.listdata.json` and `*.md` inside list directories. Task frontmatter contains `id`, `status`, `version` (u64, increments via `saturating_add` on every write, defaults to 1 for legacy files), and optionally `due`, `has_time`, `parent` (omitted when false/null). `list_tasks` auto-deduplicates by UUID, keeping the highest-version file and deleting stale copies; when versions are equal, filesystem modification time is used as a tiebreaker. +Workspaces are plain folders. Each task list is a subfolder containing `.listdata.json` (metadata/ordering) and one `.md` file per task. The workspace root has `.onyx-workspace.json` for list ordering and workspace detection. Sync only processes files at expected depths: `.onyx-workspace.json` at root, `.listdata.json` and `*.md` inside list directories. Task frontmatter contains `id`, `status`, `version` (u64, increments via `saturating_add` on every write, defaults to 1 for legacy files), and optionally `date`, `has_time`, `parent` (omitted when false/null). `list_tasks` auto-deduplicates by UUID, keeping the highest-version file and deleting stale copies; when versions are equal, filesystem modification time is used as a tiebreaker. ### Tauri GUI structure @@ -63,7 +64,7 @@ The GUI uses Svelte 5 runes mode (`$state`, `$derived`, `$effect`, `$props()`). Pre-alpha. No users, no released builds, no data to migrate. Breaking changes to on-disk formats, config structure, or sync conventions are free — do not add migration logic. -### Current state (2026-04-06) +### Current state (2026-04-15) - **Phase 1** (Core + CLI): Complete - **Phase 2** (WebDAV sync): Complete — remote folder browsing, checksum-based conflict resolution, auto-sync lifecycle, per-workspace sync interval @@ -79,7 +80,7 @@ Pre-alpha. No users, no released builds, no data to migrate. Breaking changes to - Sliding lists drawer with checkmark selection - Settings popup overlay - Workspace switcher drop-up with add/remove -- Per-workspace theme system (System default, Light, Dark, Nord, Dracula, Solarized Dark) via CSS `data-theme` attribute +- Per-workspace theme system (System default, Light, Dark, Nord, Dracula, Solarized Dark, Ink) via CSS `data-theme` attribute - Completed tasks section with animated show/hide - Date picker/editor (DateTimePicker in new task + task detail); `has_time: bool` field tracks whether time is set - Move task between lists (inline list in kebab menu, no submenu) @@ -96,7 +97,8 @@ Pre-alpha. No users, no released builds, no data to migrate. Breaking changes to - Upload/download counts in drawer footer (hidden when zero) - Transient sync error suppression (connectivity issues update status dot only, no error banner) - File watcher (notify crate, 500ms debounce, auto-reloads on external changes, emits `watcher-error` event to frontend on failures) -- WorkspaceMode enum (local/webdav) with per-workspace config, UUID-keyed workspaces +- WorkspaceMode enum (local/webdav/googletasks) with per-workspace config, UUID-keyed workspaces +- Window decorations selector (Custom/None/System) — persisted to localStorage, toggled via settings; Custom renders a Linux-style rounded border with `data-decorations` attribute - Workspace rename (local folder rename + WebDAV MOVE for remote folders, with confirmation dialog) - Desktop packaging (Linux: AppImage + .deb; Windows: MSI) - Tauri desktop-only deps (notify, keyring) feature-gated for Android compilation diff --git a/PLAN.md b/PLAN.md index 7a1028b..d42f586 100644 --- a/PLAN.md +++ b/PLAN.md @@ -62,7 +62,8 @@ Tasks are stored as individual `.md` files with YAML frontmatter: id: 550e8400-e29b-41d4-a716-446655440000 status: backlog version: 3 -due: 2026-11-15T14:00:00Z +date: 2026-11-15T14:00:00Z +has_time: true parent: 550e8400-e29b-41d4-a716-446655440001 --- @@ -84,8 +85,8 @@ Task { title: String, // Derived from filename (without .md extension) description: String, // Markdown content status: TaskStatus, // Backlog or Completed - due_date: Option, - has_time: bool, // Whether due_date includes a specific time + date: Option, + has_time: bool, // Whether date includes a specific time version: u64, // Increments (saturating) on every write; used for sync dedup parent_id: Option, // For subtasks } @@ -98,10 +99,10 @@ enum TaskStatus { TaskList { id: Uuid, title: String, // Derived from folder name - tasks: Vec, // Ordered by task_order, optionally grouped by due date + tasks: Vec, // Ordered by task_order, optionally grouped by date created_at: DateTime, updated_at: DateTime, - group_by_due_date: bool, // If true, group by due date before applying task_order + group_by_date: bool, // If true, group by date before applying task_order } AppConfig { @@ -109,14 +110,22 @@ AppConfig { current_workspace: Option, // UUID } +WorkspaceMode { + Local, + Webdav, + GoogleTasks, +} + WorkspaceConfig { - name: String, // Display name + name: String, // Display name path: PathBuf, - mode: WorkspaceMode, // Local or Webdav + mode: WorkspaceMode, // Local, Webdav, or GoogleTasks webdav_url: Option, - webdav_path: Option, // User-selected remote folder + webdav_path: Option, // User-selected remote folder + google_account: Option, // Email/display name (GoogleTasks workspaces) theme: Option, - sync_interval_secs: Option, + sync_interval_secs: Option, // Auto-sync polling interval (focused) + sync_interval_unfocused_secs: Option, // Auto-sync interval when unfocused } ``` @@ -152,7 +161,7 @@ WorkspaceConfig { "id": "list-uuid-1", "created_at": "2026-10-26T10:00:00Z", "updated_at": "2026-10-27T14:30:00Z", - "group_by_due_date": false, + "group_by_date": false, "task_order": [ "task-uuid-1", "task-uuid-2", @@ -163,8 +172,8 @@ WorkspaceConfig { **Task Ordering**: - Tasks are always ordered according to the `task_order` array (manual ordering) -- When `group_by_due_date` is `true`, tasks are first grouped by their due date, then sorted within each group by `task_order` -- Tasks without due dates appear at the end when grouping is enabled +- When `group_by_date` is `true`, tasks are first grouped by their date, then sorted within each group by `task_order` +- Tasks without dates appear at the end when grouping is enabled **App Configuration** (separate from task data, supports multiple workspaces): - Windows: `%APPDATA%/onyx/config.json` @@ -220,8 +229,8 @@ impl TaskRepository { pub fn get_task_order(&self, list_id: Uuid) -> Result>; // Grouping preference (modifies .listdata.json) - pub fn set_group_by_due_date(&mut self, list_id: Uuid, enabled: bool) -> Result<()>; - pub fn get_group_by_due_date(&self, list_id: Uuid) -> Result; + pub fn set_group_by_date(&mut self, list_id: Uuid, enabled: bool) -> Result<()>; + pub fn get_group_by_date(&self, list_id: Uuid) -> Result; } pub trait Storage { @@ -351,9 +360,9 @@ $ onyx list create "Personal Projects" $ onyx add "Buy groceries" ✓ Created task "Buy groceries" (550e8400-e29b-41d4-a716-446655440000) -$ onyx add "Review PR #123" --list "Work" --due "2026-11-15" +$ onyx add "Review PR #123" --list "Work" --date "2026-11-15" ✓ Created task "Review PR #123" (7f3a9c21-b8d2-4e5f-9a1c-3d8e7f6a2b1c) - Due: 2026-11-15 + Date: 2026-11-15 # Or specify workspace explicitly $ onyx add "Team meeting" --workspace shared @@ -367,7 +376,7 @@ My Tasks (3 tasks) [✓] Pay bills Work (2 tasks) - [ ] Review PR #123 (due: 2026-11-15) + [ ] Review PR #123 (date: 2026-11-15) [ ] Team meeting prep # List tasks from specific workspace @@ -379,7 +388,7 @@ Shared Tasks (2 tasks) # List tasks in specific list $ onyx list show --list "Work" Work (2 tasks) - [ ] Review PR #123 (due: 2026-11-15) + [ ] Review PR #123 (date: 2026-11-15) [ ] Team meeting prep # Complete a task @@ -417,12 +426,12 @@ $ onyx workspace remove shared Continue? (y/n): y ✓ Removed workspace "shared" -# Toggle grouping by due date (tasks always use manual task_order within groups) +# Toggle grouping by date (tasks always use manual task_order within groups) $ onyx group enable --list "Work" -✓ Enabled group-by-due-date for list "Work" +✓ Enabled group-by-date for list "Work" $ onyx group disable --list "Personal" -✓ Disabled group-by-due-date for list "Personal" +✓ Disabled group-by-date for list "Personal" ``` ### Deliverables @@ -468,12 +477,14 @@ Add WebDAV support to `onyx-core`: WorkspaceConfig { name: String, path: PathBuf, - mode: WorkspaceMode, // Local or Webdav + mode: WorkspaceMode, // Local, Webdav, or GoogleTasks webdav_url: Option, - webdav_path: Option, // User-selected remote folder + webdav_path: Option, // User-selected remote folder + google_account: Option, // Email/display name (GoogleTasks workspaces) last_sync: Option, theme: Option, - sync_interval_secs: Option, + sync_interval_secs: Option, // Auto-sync polling interval (focused) + sync_interval_unfocused_secs: Option, // Auto-sync interval when unfocused } // AppConfig remains the same (workspaces + current_workspace) @@ -688,14 +699,16 @@ AppConfig { } WorkspaceConfig { - name: String, // Display name + name: String, // Display name path: PathBuf, - mode: WorkspaceMode, // Local or Webdav + mode: WorkspaceMode, // Local, Webdav, or GoogleTasks webdav_url: Option, - webdav_path: Option, // User-selected remote folder + webdav_path: Option, // User-selected remote folder + google_account: Option, // Email/display name (GoogleTasks workspaces) last_sync: Option, - theme: Option, // Per-workspace theme - sync_interval_secs: Option, // Auto-sync interval + theme: Option, // Per-workspace theme + sync_interval_secs: Option, // Auto-sync interval (focused) + sync_interval_unfocused_secs: Option, // Auto-sync interval when unfocused } ``` @@ -741,7 +754,7 @@ WorkspaceConfig { - [x] Keyboard shortcuts (Escape closes settings → detail → drawer → menus in priority order) - [x] Sync status indicators (last-sync time + upload/download counts chip in TasksScreen) - [x] Push/pull sync mode selection (session-only sync direction selector in SettingsScreen) -- [x] Group-by-due-date toggle per list (checkmark toggle in list kebab menu) +- [x] Group-by-date toggle per list (checkmark toggle in list kebab menu) - [x] Subtask hierarchy (expand/collapse, inline add, cascade toggle/delete) - [ ] Search/filter tasks - [x] Desktop packaging (Linux: AppImage + .deb; Windows: MSI; macOS not yet verified) @@ -955,10 +968,13 @@ npm run tauri ios build ### Features #### Google Tasks Importer -- [ ] **Import from Google Tasks** (via API or export) -- [ ] Migrate tasks, lists, due dates, notes +- [x] `google_tasks.rs` module in `onyx-core` — client, UUID mapping, read-only sync (remote always wins) +- [x] `GoogleTasks` workspace mode and `google_account` config field +- [x] Tauri commands: `google_tasks_authorize()`, `google_tasks_sync()` +- [ ] Complete OAuth flow (client ID/secret placeholders need real credentials) +- [ ] Migrate tasks, lists, due dates, notes with full UI integration - [ ] Preserve task hierarchy and order -- [ ] Easy onboarding for Google Tasks users +- [ ] Easy onboarding flow for Google Tasks users #### Advanced Task Management - [ ] **Recurring tasks** (tasks that automatically uncomplete and reschedule) @@ -1029,6 +1045,6 @@ This project is free and open-source software licensed under GPL v3. --- -**Last Updated**: 2026-04-05 +**Last Updated**: 2026-04-15 **Document Version**: 4.3 **Status**: Ready to Implement - Milestone-Driven Plan diff --git a/README.md b/README.md index b3e7b8b..a0723a9 100644 --- a/README.md +++ b/README.md @@ -114,8 +114,8 @@ cargo run -p onyx-cli -- init ~/Documents/Tasks --name personal # Add a task cargo run -p onyx-cli -- add "Buy groceries" -# Add a task with due date -cargo run -p onyx-cli -- add "Review PR #123" --list "Work" --due "2026-11-15" +# Add a task with a date +cargo run -p onyx-cli -- add "Review PR #123" --list "Work" --date "2026-11-15" # List all tasks cargo run -p onyx-cli -- list show @@ -168,7 +168,7 @@ Tasks are stored as markdown files with YAML frontmatter (Obsidian-compatible): id: 550e8400-e29b-41d4-a716-446655440000 status: backlog version: 3 -due: 2026-11-15T14:00:00Z +date: 2026-11-15T14:00:00Z --- Task description and notes go here in **markdown** format. diff --git a/docs/API.md b/docs/API.md index 0c09e1d..5f79cdb 100644 --- a/docs/API.md +++ b/docs/API.md @@ -18,8 +18,8 @@ pub struct Task { pub title: String, pub description: String, pub status: TaskStatus, - pub due_date: Option>, - pub has_time: bool, // Whether due_date includes a specific time + pub date: Option>, + pub has_time: bool, // Whether date includes a specific time pub version: u64, // Increments (saturating) on every write; used for sync dedup pub parent_id: Option, } @@ -38,10 +38,10 @@ use onyx_core::Task; // Simple task let task = Task::new("Buy groceries".to_string()); -// Task with description and due date +// Task with description and date let task = Task::new("Review PR #123".to_string()) .with_description("Check the authentication changes".to_string()) - .with_due_date(chrono::Utc::now() + chrono::Duration::days(2)); + .with_date(chrono::Utc::now() + chrono::Duration::days(2)); ``` #### TaskList @@ -55,7 +55,7 @@ pub struct TaskList { pub tasks: Vec, pub created_at: DateTime, pub updated_at: DateTime, - pub group_by_due_date: bool, + pub group_by_date: bool, } ``` @@ -108,15 +108,23 @@ config.save_to_file(&config_path)?; Configuration for a single workspace. ```rust +pub enum WorkspaceMode { + Local, + Webdav, + GoogleTasks, +} + pub struct WorkspaceConfig { - pub name: String, // Display name + pub name: String, // Display name pub path: PathBuf, - pub mode: WorkspaceMode, // Local or Webdav + pub mode: WorkspaceMode, // Local, Webdav, or GoogleTasks pub webdav_url: Option, - pub webdav_path: Option, // User-selected remote folder path + pub webdav_path: Option, // User-selected remote folder path + pub google_account: Option, // Email/display name (GoogleTasks workspaces) pub last_sync: Option>, pub theme: Option, - pub sync_interval_secs: Option, // Auto-sync polling interval + pub sync_interval_secs: Option, // Auto-sync polling interval (focused) + pub sync_interval_unfocused_secs: Option, // Auto-sync interval when unfocused } ``` @@ -217,17 +225,17 @@ let order = repo.get_task_order(list_id)?; ### Grouping -#### Enable/Disable Group by Due Date +#### Enable/Disable Group by Date ```rust // Enable grouping -repo.set_group_by_due_date(list_id, true)?; +repo.set_group_by_date(list_id, true)?; // Disable grouping -repo.set_group_by_due_date(list_id, false)?; +repo.set_group_by_date(list_id, false)?; // Check current setting -let is_grouped = repo.get_group_by_due_date(list_id)?; +let is_grouped = repo.get_group_by_date(list_id)?; ``` ## File Format @@ -241,7 +249,8 @@ Tasks are stored as `.md` files with YAML frontmatter: id: 550e8400-e29b-41d4-a716-446655440000 status: backlog version: 3 -due: 2026-11-15T14:00:00Z +date: 2026-11-15T14:00:00Z +has_time: true parent: 550e8400-e29b-41d4-a716-446655440001 --- @@ -263,7 +272,7 @@ Each list folder contains a `.listdata.json` file: "id": "list-uuid-1", "created_at": "2026-10-26T10:00:00Z", "updated_at": "2026-10-27T14:30:00Z", - "group_by_due_date": false, + "group_by_date": false, "task_order": [ "task-uuid-1", "task-uuid-2", @@ -453,7 +462,7 @@ fn main() -> Result<(), Box> { let task1 = repo.create_task(list.id, task1)?; let task2 = Task::new("Call dentist".to_string()) - .with_due_date(chrono::Utc::now() + chrono::Duration::days(1)); + .with_date(chrono::Utc::now() + chrono::Duration::days(1)); let task2 = repo.create_task(list.id, task2)?; // List all tasks diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 66dcd2a..e837c09 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -45,7 +45,8 @@ onyx/ │ │ │ ├── repository.rs # Repository pattern (TaskRepository) │ │ │ ├── error.rs # Error types │ │ │ ├── sync.rs # Three-way sync engine with offline queue -│ │ │ └── webdav.rs # WebDAV client and credential storage +│ │ │ ├── webdav.rs # WebDAV client and credential storage +│ │ │ └── google_tasks.rs # Google Tasks API client (read-only sync) │ │ └── Cargo.toml │ ├── onyx-cli/ # CLI application │ │ ├── src/