Merge pull request #13 from SteelDynamite/rename-to-onyx
rename-to-onyx
This commit is contained in:
commit
6959b1f44f
16
CLAUDE.md
16
CLAUDE.md
|
|
@ -4,16 +4,16 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
Bevy Tasks is a local-first, cross-platform task management app built in Rust. Tasks are stored as markdown files with YAML frontmatter in user-selected folders. The GUI uses Tauri v2 (Svelte 5 + Tailwind CSS 4) in `apps/tauri/`.
|
Onyx is a local-first, cross-platform task management app built in Rust. Tasks are stored as markdown files with YAML frontmatter in user-selected folders. The GUI uses Tauri v2 (Svelte 5 + Tailwind CSS 4) in `apps/tauri/`.
|
||||||
|
|
||||||
## Build & Test Commands
|
## Build & Test Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo build # Build all crates
|
cargo build # Build all crates
|
||||||
cargo build -p bevy-tasks-cli # Build CLI only
|
cargo build -p onyx-cli # Build CLI only
|
||||||
cargo test # Run all tests
|
cargo test # Run all tests
|
||||||
cargo test -p bevy-tasks-core # Run core library tests only
|
cargo test -p onyx-core # Run core library tests only
|
||||||
cargo run -p bevy-tasks-cli -- <args> # Run CLI with arguments
|
cargo run -p onyx-cli -- <args> # Run CLI with arguments
|
||||||
|
|
||||||
# Tauri GUI
|
# Tauri GUI
|
||||||
cd apps/tauri && npm install # Install frontend dependencies
|
cd apps/tauri && npm install # Install frontend dependencies
|
||||||
|
|
@ -21,7 +21,7 @@ WEBKIT_DISABLE_DMABUF_RENDERER=1 npm run tauri dev # Run Tauri in dev mode (Way
|
||||||
npm run tauri build # Build for production
|
npm run tauri build # Build for production
|
||||||
```
|
```
|
||||||
|
|
||||||
The CLI binary is named `bevy-tasks` (from the `bevy-tasks-cli` crate).
|
The CLI binary is named `onyx` (from the `onyx-cli` crate).
|
||||||
|
|
||||||
The Tauri dev server runs on port 1422 (`vite.config.ts` and `tauri.conf.json`).
|
The Tauri dev server runs on port 1422 (`vite.config.ts` and `tauri.conf.json`).
|
||||||
|
|
||||||
|
|
@ -29,9 +29,9 @@ The Tauri dev server runs on port 1422 (`vite.config.ts` and `tauri.conf.json`).
|
||||||
|
|
||||||
Two-crate workspace (`resolver = "2"`, edition 2021) plus a Tauri app:
|
Two-crate workspace (`resolver = "2"`, edition 2021) plus a Tauri app:
|
||||||
|
|
||||||
- **bevy-tasks-core** — Pure Rust library. Storage trait with `FileSystemStorage` implementation, `TaskRepository` (main API), data models, config, error types. No CLI/UI dependencies.
|
- **onyx-core** — Pure Rust library. Storage trait with `FileSystemStorage` implementation, `TaskRepository` (main API), data models, config, error types. No CLI/UI dependencies.
|
||||||
- **bevy-tasks-cli** — CLI frontend using clap. Commands are in `src/commands/` (init, workspace, list, task, group). Output formatting in `src/output.rs`.
|
- **onyx-cli** — CLI frontend using clap. Commands are in `src/commands/` (init, workspace, list, task, group). Output formatting in `src/output.rs`.
|
||||||
- **apps/tauri/** — Tauri v2 GUI. Svelte 5 frontend in `src/`, Rust backend in `src-tauri/` with Tauri commands that call into `bevy-tasks-core`.
|
- **apps/tauri/** — Tauri v2 GUI. Svelte 5 frontend in `src/`, Rust backend in `src-tauri/` with Tauri commands that call into `onyx-core`.
|
||||||
|
|
||||||
### Key patterns
|
### Key patterns
|
||||||
|
|
||||||
|
|
|
||||||
6
Cargo.lock
generated
6
Cargo.lock
generated
|
|
@ -105,11 +105,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bevy-tasks-cli"
|
name = "onyx-cli"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bevy-tasks-core",
|
"onyx-core",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"colored",
|
"colored",
|
||||||
|
|
@ -120,7 +120,7 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bevy-tasks-core"
|
name = "onyx-core"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"crates/bevy-tasks-core",
|
"crates/onyx-core",
|
||||||
"crates/bevy-tasks-cli",
|
"crates/onyx-cli",
|
||||||
]
|
]
|
||||||
exclude = [
|
exclude = [
|
||||||
"apps/tauri/src-tauri",
|
"apps/tauri/src-tauri",
|
||||||
|
|
|
||||||
120
PLAN.md
120
PLAN.md
|
|
@ -1,4 +1,4 @@
|
||||||
# Bevy Tasks - Project Plan
|
# Onyx - Project Plan
|
||||||
|
|
||||||
## Vision
|
## Vision
|
||||||
|
|
||||||
|
|
@ -41,15 +41,15 @@ A **local-first, cross-platform tasks application** inspired by Google Tasks. Bu
|
||||||
|
|
||||||
#### Cargo Workspace Structure
|
#### Cargo Workspace Structure
|
||||||
```
|
```
|
||||||
bevy-tasks/
|
onyx/
|
||||||
├── Cargo.toml # Workspace definition
|
├── Cargo.toml # Workspace definition
|
||||||
├── PLAN.md
|
├── PLAN.md
|
||||||
├── README.md
|
├── README.md
|
||||||
├── apps/
|
├── apps/
|
||||||
│ └── tauri/ # Tauri GUI (Svelte + Tailwind)
|
│ └── tauri/ # Tauri GUI (Svelte + Tailwind)
|
||||||
├── crates/
|
├── crates/
|
||||||
│ ├── bevy-tasks-core/ # Core library (backend)
|
│ ├── onyx-core/ # Core library (backend)
|
||||||
│ └── bevy-tasks-cli/ # CLI frontend
|
│ └── onyx-cli/ # CLI frontend
|
||||||
└── docs/
|
└── docs/
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -162,9 +162,9 @@ WorkspaceConfig {
|
||||||
- Tasks without due dates appear at the end when grouping is enabled
|
- Tasks without due dates appear at the end when grouping is enabled
|
||||||
|
|
||||||
**App Configuration** (separate from task data, supports multiple workspaces):
|
**App Configuration** (separate from task data, supports multiple workspaces):
|
||||||
- Windows: `%APPDATA%/bevy-tasks/config.json`
|
- Windows: `%APPDATA%/onyx/config.json`
|
||||||
- Linux: `~/.config/bevy-tasks/config.json`
|
- Linux: `~/.config/onyx/config.json`
|
||||||
- macOS: `~/Library/Application Support/bevy-tasks/config.json`
|
- macOS: `~/Library/Application Support/onyx/config.json`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
|
@ -226,8 +226,8 @@ pub trait Storage {
|
||||||
```toml
|
```toml
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"crates/bevy-tasks-core",
|
"crates/onyx-core",
|
||||||
"crates/bevy-tasks-cli",
|
"crates/onyx-cli",
|
||||||
]
|
]
|
||||||
exclude = [
|
exclude = [
|
||||||
"apps/tauri/src-tauri",
|
"apps/tauri/src-tauri",
|
||||||
|
|
@ -242,10 +242,10 @@ anyhow = "1.0"
|
||||||
tokio = { version = "1.40", features = ["full"] }
|
tokio = { version = "1.40", features = ["full"] }
|
||||||
```
|
```
|
||||||
|
|
||||||
**bevy-tasks-core/Cargo.toml**:
|
**onyx-core/Cargo.toml**:
|
||||||
```toml
|
```toml
|
||||||
[package]
|
[package]
|
||||||
name = "bevy-tasks-core"
|
name = "onyx-core"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
|
|
@ -261,19 +261,19 @@ directories = "5.0"
|
||||||
tempfile = "3.0"
|
tempfile = "3.0"
|
||||||
```
|
```
|
||||||
|
|
||||||
**bevy-tasks-cli/Cargo.toml**:
|
**onyx-cli/Cargo.toml**:
|
||||||
```toml
|
```toml
|
||||||
[package]
|
[package]
|
||||||
name = "bevy-tasks-cli"
|
name = "onyx-cli"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "bevy-tasks"
|
name = "onyx"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bevy-tasks-core = { path = "../bevy-tasks-core" }
|
onyx-core = { path = "../onyx-core" }
|
||||||
clap = { version = "4.5", features = ["derive", "env"] }
|
clap = { version = "4.5", features = ["derive", "env"] }
|
||||||
colored = "2.0"
|
colored = "2.0"
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
|
|
@ -310,46 +310,46 @@ fs_extra = "1.3"
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# First run: initialize a workspace (creates named workspace)
|
# First run: initialize a workspace (creates named workspace)
|
||||||
$ bevy-tasks init ~/Documents/Tasks --name personal
|
$ onyx init ~/Documents/Tasks --name personal
|
||||||
✓ Initialized workspace "personal" at ~/Documents/Tasks
|
✓ Initialized workspace "personal" at ~/Documents/Tasks
|
||||||
✓ Created default list "My Tasks"
|
✓ Created default list "My Tasks"
|
||||||
✓ Set "personal" as current workspace
|
✓ Set "personal" as current workspace
|
||||||
|
|
||||||
# Add more workspaces (e.g., for shared/collaborative tasks)
|
# Add more workspaces (e.g., for shared/collaborative tasks)
|
||||||
$ bevy-tasks workspace add shared ~/Dropbox/TeamTasks
|
$ onyx workspace add shared ~/Dropbox/TeamTasks
|
||||||
✓ Added workspace "shared" at ~/Dropbox/TeamTasks
|
✓ Added workspace "shared" at ~/Dropbox/TeamTasks
|
||||||
✓ Created default list "My Tasks"
|
✓ Created default list "My Tasks"
|
||||||
|
|
||||||
# List all workspaces
|
# List all workspaces
|
||||||
$ bevy-tasks workspace list
|
$ onyx workspace list
|
||||||
personal: ~/Documents/Tasks (current)
|
personal: ~/Documents/Tasks (current)
|
||||||
shared: ~/Dropbox/TeamTasks
|
shared: ~/Dropbox/TeamTasks
|
||||||
|
|
||||||
# Switch between workspaces
|
# Switch between workspaces
|
||||||
$ bevy-tasks workspace switch shared
|
$ onyx workspace switch shared
|
||||||
✓ Switched to workspace "shared"
|
✓ Switched to workspace "shared"
|
||||||
|
|
||||||
# Create a new task list
|
# Create a new task list
|
||||||
$ bevy-tasks list create "Work"
|
$ onyx list create "Work"
|
||||||
✓ Created list "Work"
|
✓ Created list "Work"
|
||||||
|
|
||||||
$ bevy-tasks list create "Personal Projects"
|
$ onyx list create "Personal Projects"
|
||||||
✓ Created list "Personal Projects"
|
✓ Created list "Personal Projects"
|
||||||
|
|
||||||
# Add tasks (uses current workspace by default)
|
# Add tasks (uses current workspace by default)
|
||||||
$ bevy-tasks add "Buy groceries"
|
$ onyx add "Buy groceries"
|
||||||
✓ Created task "Buy groceries" (550e8400-e29b-41d4-a716-446655440000)
|
✓ Created task "Buy groceries" (550e8400-e29b-41d4-a716-446655440000)
|
||||||
|
|
||||||
$ bevy-tasks add "Review PR #123" --list "Work" --due "2026-11-15"
|
$ onyx add "Review PR #123" --list "Work" --due "2026-11-15"
|
||||||
✓ Created task "Review PR #123" (7f3a9c21-b8d2-4e5f-9a1c-3d8e7f6a2b1c)
|
✓ Created task "Review PR #123" (7f3a9c21-b8d2-4e5f-9a1c-3d8e7f6a2b1c)
|
||||||
Due: 2026-11-15
|
Due: 2026-11-15
|
||||||
|
|
||||||
# Or specify workspace explicitly
|
# Or specify workspace explicitly
|
||||||
$ bevy-tasks add "Team meeting" --workspace shared
|
$ onyx add "Team meeting" --workspace shared
|
||||||
✓ Created task "Team meeting" in workspace "shared"
|
✓ Created task "Team meeting" in workspace "shared"
|
||||||
|
|
||||||
# List all tasks (from current workspace)
|
# List all tasks (from current workspace)
|
||||||
$ bevy-tasks list show
|
$ onyx list show
|
||||||
My Tasks (3 tasks)
|
My Tasks (3 tasks)
|
||||||
[ ] Buy groceries
|
[ ] Buy groceries
|
||||||
[ ] Call dentist
|
[ ] Call dentist
|
||||||
|
|
@ -360,37 +360,37 @@ Work (2 tasks)
|
||||||
[ ] Team meeting prep
|
[ ] Team meeting prep
|
||||||
|
|
||||||
# List tasks from specific workspace
|
# List tasks from specific workspace
|
||||||
$ bevy-tasks list show --workspace shared
|
$ onyx list show --workspace shared
|
||||||
Shared Tasks (2 tasks)
|
Shared Tasks (2 tasks)
|
||||||
[ ] Team meeting
|
[ ] Team meeting
|
||||||
[ ] Quarterly planning
|
[ ] Quarterly planning
|
||||||
|
|
||||||
# List tasks in specific list
|
# List tasks in specific list
|
||||||
$ bevy-tasks list show --list "Work"
|
$ onyx list show --list "Work"
|
||||||
Work (2 tasks)
|
Work (2 tasks)
|
||||||
[ ] Review PR #123 (due: 2026-11-15)
|
[ ] Review PR #123 (due: 2026-11-15)
|
||||||
[ ] Team meeting prep
|
[ ] Team meeting prep
|
||||||
|
|
||||||
# Complete a task
|
# Complete a task
|
||||||
$ bevy-tasks complete 550e8400-e29b-41d4-a716-446655440000
|
$ onyx complete 550e8400-e29b-41d4-a716-446655440000
|
||||||
✓ Completed task "Buy groceries"
|
✓ Completed task "Buy groceries"
|
||||||
|
|
||||||
# Edit a task (CLI-only: creates temp file, opens $EDITOR, blocks until editor exits, then parses)
|
# Edit a task (CLI-only: creates temp file, opens $EDITOR, blocks until editor exits, then parses)
|
||||||
$ bevy-tasks edit 7f3a9c21-b8d2-4e5f-9a1c-3d8e7f6a2b1c
|
$ onyx edit 7f3a9c21-b8d2-4e5f-9a1c-3d8e7f6a2b1c
|
||||||
# Opens editor with task markdown file
|
# Opens editor with task markdown file
|
||||||
# User edits and saves, then exits editor
|
# User edits and saves, then exits editor
|
||||||
✓ Updated task "Review PR #123"
|
✓ Updated task "Review PR #123"
|
||||||
|
|
||||||
# Delete a task
|
# Delete a task
|
||||||
$ bevy-tasks delete 550e8400-e29b-41d4-a716-446655440000
|
$ onyx delete 550e8400-e29b-41d4-a716-446655440000
|
||||||
✓ Deleted task "Buy groceries"
|
✓ Deleted task "Buy groceries"
|
||||||
|
|
||||||
# Retarget workspace (files already at new location, just update config)
|
# Retarget workspace (files already at new location, just update config)
|
||||||
$ bevy-tasks workspace retarget personal ~/new/path/to/Tasks
|
$ onyx workspace retarget personal ~/new/path/to/Tasks
|
||||||
✓ Workspace "personal" now points to ~/new/path/to/Tasks
|
✓ Workspace "personal" now points to ~/new/path/to/Tasks
|
||||||
|
|
||||||
# Migrate workspace (move files to new location)
|
# Migrate workspace (move files to new location)
|
||||||
$ bevy-tasks workspace migrate personal ~/Dropbox/Tasks
|
$ onyx workspace migrate personal ~/Dropbox/Tasks
|
||||||
⚠ This will move all files from ~/Documents/Tasks to ~/Dropbox/Tasks
|
⚠ This will move all files from ~/Documents/Tasks to ~/Dropbox/Tasks
|
||||||
Continue? (y/n): y
|
Continue? (y/n): y
|
||||||
Moving files...
|
Moving files...
|
||||||
|
|
@ -401,22 +401,22 @@ Moving files...
|
||||||
✓ Workspace "personal" now points to ~/Dropbox/Tasks
|
✓ Workspace "personal" now points to ~/Dropbox/Tasks
|
||||||
|
|
||||||
# Remove a workspace
|
# Remove a workspace
|
||||||
$ bevy-tasks workspace remove shared
|
$ onyx workspace remove shared
|
||||||
⚠ Warning: This will delete workspace config (files remain on disk)
|
⚠ Warning: This will delete workspace config (files remain on disk)
|
||||||
Continue? (y/n): y
|
Continue? (y/n): y
|
||||||
✓ Removed workspace "shared"
|
✓ Removed workspace "shared"
|
||||||
|
|
||||||
# Toggle grouping by due date (tasks always use manual task_order within groups)
|
# Toggle grouping by due date (tasks always use manual task_order within groups)
|
||||||
$ bevy-tasks group enable --list "Work"
|
$ onyx group enable --list "Work"
|
||||||
✓ Enabled group-by-due-date for list "Work"
|
✓ Enabled group-by-due-date for list "Work"
|
||||||
|
|
||||||
$ bevy-tasks group disable --list "Personal"
|
$ onyx group disable --list "Personal"
|
||||||
✓ Disabled group-by-due-date for list "Personal"
|
✓ Disabled group-by-due-date for list "Personal"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Deliverables
|
### Deliverables
|
||||||
|
|
||||||
- [x] `bevy-tasks-core` library with stable API
|
- [x] `onyx-core` library with stable API
|
||||||
- [x] Functional CLI that can manage tasks
|
- [x] Functional CLI that can manage tasks
|
||||||
- [x] Data persists as Obsidian-compatible .md files
|
- [x] Data persists as Obsidian-compatible .md files
|
||||||
- [x] Well-tested backend (>80% coverage)
|
- [x] Well-tested backend (>80% coverage)
|
||||||
|
|
@ -427,17 +427,17 @@ $ bevy-tasks group disable --list "Personal"
|
||||||
```bash
|
```bash
|
||||||
# Clone and build
|
# Clone and build
|
||||||
git clone <repository-url>
|
git clone <repository-url>
|
||||||
cd bevy-tasks
|
cd onyx
|
||||||
cargo build
|
cargo build
|
||||||
|
|
||||||
# Run tests
|
# Run tests
|
||||||
cargo test -p bevy-tasks-core
|
cargo test -p onyx-core
|
||||||
|
|
||||||
# Run CLI
|
# Run CLI
|
||||||
cargo run -p bevy-tasks-cli -- init ~/test-tasks --name test
|
cargo run -p onyx-cli -- init ~/test-tasks --name test
|
||||||
cargo run -p bevy-tasks-cli -- add "Test task"
|
cargo run -p onyx-cli -- add "Test task"
|
||||||
cargo run -p bevy-tasks-cli -- list
|
cargo run -p onyx-cli -- list
|
||||||
cargo run -p bevy-tasks-cli -- workspace list
|
cargo run -p onyx-cli -- workspace list
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -450,7 +450,7 @@ cargo run -p bevy-tasks-cli -- workspace list
|
||||||
|
|
||||||
#### WebDAV Integration
|
#### WebDAV Integration
|
||||||
|
|
||||||
Add WebDAV support to `bevy-tasks-core`:
|
Add WebDAV support to `onyx-core`:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// Update WorkspaceConfig to include WebDAV
|
// Update WorkspaceConfig to include WebDAV
|
||||||
|
|
@ -466,7 +466,7 @@ AppConfig {
|
||||||
current_workspace: Option<String>,
|
current_workspace: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync functions in bevy_tasks_core::sync module (standalone, not on TaskRepository)
|
// Sync functions in onyx_core::sync module (standalone, not on TaskRepository)
|
||||||
pub async fn sync_workspace(
|
pub async fn sync_workspace(
|
||||||
workspace_path: &Path,
|
workspace_path: &Path,
|
||||||
webdav_url: &str,
|
webdav_url: &str,
|
||||||
|
|
@ -477,7 +477,7 @@ pub async fn sync_workspace(
|
||||||
|
|
||||||
pub fn get_sync_status(workspace_path: &Path) -> Result<SyncStatusInfo>;
|
pub fn get_sync_status(workspace_path: &Path) -> Result<SyncStatusInfo>;
|
||||||
|
|
||||||
// Credential functions in bevy_tasks_core::webdav module
|
// Credential functions in onyx_core::webdav module
|
||||||
pub fn store_credentials(domain: &str, username: &str, password: &str) -> Result<()>;
|
pub fn store_credentials(domain: &str, username: &str, password: &str) -> Result<()>;
|
||||||
pub fn load_credentials(domain: &str) -> Result<(String, String)>;
|
pub fn load_credentials(domain: &str) -> Result<(String, String)>;
|
||||||
pub fn delete_credentials(domain: &str) -> Result<()>;
|
pub fn delete_credentials(domain: &str) -> Result<()>;
|
||||||
|
|
@ -492,14 +492,14 @@ pub fn delete_credentials(domain: &str) -> Result<()>;
|
||||||
|
|
||||||
**Primary**: Platform Keychain via `keyring` crate
|
**Primary**: Platform Keychain via `keyring` crate
|
||||||
- Store WebDAV username + password in system keychain
|
- Store WebDAV username + password in system keychain
|
||||||
- Key format: `com.bevy-tasks.webdav.{server-domain}`
|
- Key format: `com.onyx.webdav.{server-domain}`
|
||||||
- Works on: Windows (Credential Manager), macOS (Keychain), Linux (Secret Service), iOS/Android (Keystore)
|
- Works on: Windows (Credential Manager), macOS (Keychain), Linux (Secret Service), iOS/Android (Keystore)
|
||||||
|
|
||||||
**Fallback**: Encrypted local storage if keychain unavailable
|
**Fallback**: Encrypted local storage if keychain unavailable
|
||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
|
|
||||||
Add to `bevy-tasks-core/Cargo.toml`:
|
Add to `onyx-core/Cargo.toml`:
|
||||||
```toml
|
```toml
|
||||||
reqwest = { version = "0.12", features = ["json", "rustls-tls"] }
|
reqwest = { version = "0.12", features = ["json", "rustls-tls"] }
|
||||||
keyring = "3.0"
|
keyring = "3.0"
|
||||||
|
|
@ -523,7 +523,7 @@ keyring = "3.0"
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Setup WebDAV for current workspace
|
# Setup WebDAV for current workspace
|
||||||
$ bevy-tasks sync --setup
|
$ onyx sync --setup
|
||||||
WebDAV URL: https://nextcloud.example.com/remote.php/dav/files/username/Tasks
|
WebDAV URL: https://nextcloud.example.com/remote.php/dav/files/username/Tasks
|
||||||
Username: myuser
|
Username: myuser
|
||||||
Password: ********
|
Password: ********
|
||||||
|
|
@ -531,7 +531,7 @@ Password: ********
|
||||||
✓ Connection verified for workspace "personal"
|
✓ Connection verified for workspace "personal"
|
||||||
|
|
||||||
# Setup WebDAV for specific workspace
|
# Setup WebDAV for specific workspace
|
||||||
$ bevy-tasks sync --setup --workspace shared
|
$ onyx sync --setup --workspace shared
|
||||||
WebDAV URL: https://nextcloud.example.com/remote.php/dav/files/username/SharedTasks
|
WebDAV URL: https://nextcloud.example.com/remote.php/dav/files/username/SharedTasks
|
||||||
Username: myuser
|
Username: myuser
|
||||||
Password: ********
|
Password: ********
|
||||||
|
|
@ -539,7 +539,7 @@ Password: ********
|
||||||
✓ Connection verified for workspace "shared"
|
✓ Connection verified for workspace "shared"
|
||||||
|
|
||||||
# Push local changes to WebDAV server (current workspace)
|
# Push local changes to WebDAV server (current workspace)
|
||||||
$ bevy-tasks sync --push
|
$ onyx sync --push
|
||||||
Syncing workspace "personal" to https://nextcloud.example.com/...
|
Syncing workspace "personal" to https://nextcloud.example.com/...
|
||||||
Uploading My Tasks/.listdata.json
|
Uploading My Tasks/.listdata.json
|
||||||
Uploading My Tasks/Buy groceries.md
|
Uploading My Tasks/Buy groceries.md
|
||||||
|
|
@ -547,14 +547,14 @@ Syncing workspace "personal" to https://nextcloud.example.com/...
|
||||||
✓ Pushed 3 files to WebDAV server
|
✓ Pushed 3 files to WebDAV server
|
||||||
|
|
||||||
# Pull changes from WebDAV server
|
# Pull changes from WebDAV server
|
||||||
$ bevy-tasks sync --pull
|
$ onyx sync --pull
|
||||||
Syncing workspace "personal" from https://nextcloud.example.com/...
|
Syncing workspace "personal" from https://nextcloud.example.com/...
|
||||||
Downloading Work/Team meeting notes.md
|
Downloading Work/Team meeting notes.md
|
||||||
Downloading Personal/Call mom.md
|
Downloading Personal/Call mom.md
|
||||||
✓ Pulled 2 files from WebDAV server
|
✓ Pulled 2 files from WebDAV server
|
||||||
|
|
||||||
# Automatic two-way sync
|
# Automatic two-way sync
|
||||||
$ bevy-tasks sync
|
$ onyx sync
|
||||||
Syncing workspace "personal" with https://nextcloud.example.com/...
|
Syncing workspace "personal" with https://nextcloud.example.com/...
|
||||||
↑ Uploading My Tasks/New task.md
|
↑ Uploading My Tasks/New task.md
|
||||||
↓ Downloading Work/Updated task.md
|
↓ Downloading Work/Updated task.md
|
||||||
|
|
@ -562,12 +562,12 @@ Syncing workspace "personal" with https://nextcloud.example.com/...
|
||||||
✓ Sync complete
|
✓ Sync complete
|
||||||
|
|
||||||
# Sync specific workspace
|
# Sync specific workspace
|
||||||
$ bevy-tasks sync --workspace shared
|
$ onyx sync --workspace shared
|
||||||
Syncing workspace "shared" with https://nextcloud.example.com/...
|
Syncing workspace "shared" with https://nextcloud.example.com/...
|
||||||
✓ Sync complete (no changes)
|
✓ Sync complete (no changes)
|
||||||
|
|
||||||
# Check sync status for current workspace
|
# Check sync status for current workspace
|
||||||
$ bevy-tasks sync --status
|
$ onyx sync --status
|
||||||
Workspace: personal
|
Workspace: personal
|
||||||
WebDAV Server: https://nextcloud.example.com/remote.php/dav/files/username/Tasks
|
WebDAV Server: https://nextcloud.example.com/remote.php/dav/files/username/Tasks
|
||||||
Status: Connected
|
Status: Connected
|
||||||
|
|
@ -576,7 +576,7 @@ Local changes: 2 files modified
|
||||||
Remote changes: 0 files modified
|
Remote changes: 0 files modified
|
||||||
|
|
||||||
# Check sync status for all workspaces
|
# Check sync status for all workspaces
|
||||||
$ bevy-tasks sync --status --all
|
$ onyx sync --status --all
|
||||||
Workspace: personal
|
Workspace: personal
|
||||||
WebDAV: https://nextcloud.example.com/.../Tasks
|
WebDAV: https://nextcloud.example.com/.../Tasks
|
||||||
Status: Connected
|
Status: Connected
|
||||||
|
|
@ -608,7 +608,7 @@ Workspace: shared
|
||||||
**Decision**: Use Tauri v2 with Svelte and Tailwind for the GUI
|
**Decision**: Use Tauri v2 with Svelte and Tailwind for the GUI
|
||||||
|
|
||||||
**Why Tauri?**
|
**Why Tauri?**
|
||||||
- Native Rust backend — direct integration with `bevy-tasks-core`
|
- Native Rust backend — direct integration with `onyx-core`
|
||||||
- Svelte 5 for reactive, performant UI with minimal boilerplate
|
- Svelte 5 for reactive, performant UI with minimal boilerplate
|
||||||
- Tailwind CSS 4 for rapid, consistent styling
|
- Tailwind CSS 4 for rapid, consistent styling
|
||||||
- Small binary size (~5-10MB)
|
- Small binary size (~5-10MB)
|
||||||
|
|
@ -715,11 +715,11 @@ WorkspaceConfig {
|
||||||
- [x] Settings popup overlay (WebDAV config, dark mode toggle)
|
- [x] Settings popup overlay (WebDAV config, dark mode toggle)
|
||||||
- [x] Dark mode (GNOME-style neutral theme, cyan-blue accent)
|
- [x] Dark mode (GNOME-style neutral theme, cyan-blue accent)
|
||||||
- [x] Animated completed section show/hide
|
- [x] Animated completed section show/hide
|
||||||
- [ ] Move task between lists (needs `move_task(from_list, to_list, task_id)` added to bevy-tasks-core + Tauri command, then wire into task detail kebab menu)
|
- [ ] Move task between lists (needs `move_task(from_list, to_list, task_id)` added to onyx-core + Tauri command, then wire into task detail kebab menu)
|
||||||
- [ ] Optional time on due dates (backend `due_date` is `DateTime<Utc>` — needs a separate `due_time` field or a nullable time component so date-only tasks don't default to midnight; currently the GUI uses `hours == 0 && minutes == 0` as a heuristic for "no time set" which breaks for actual midnight times)
|
- [ ] Optional time on due dates (backend `due_date` is `DateTime<Utc>` — needs a separate `due_time` field or a nullable time component so date-only tasks don't default to midnight; currently the GUI uses `hours == 0 && minutes == 0` as a heuristic for "no time set" which breaks for actual midnight times)
|
||||||
- [ ] Due date picker/editor (backend supports it, needs date input in new task toast + inline editing)
|
- [ ] Due date picker/editor (backend supports it, needs date input in new task toast + inline editing)
|
||||||
- [ ] WebDAV setup flow with credentials (settings panel has fields, triggerSync needs to pull creds from config)
|
- [ ] WebDAV setup flow with credentials (settings panel has fields, triggerSync needs to pull creds from config)
|
||||||
- [ ] List/workspace rename (needs `rename_list` added to bevy-tasks-core first)
|
- [ ] List/workspace rename (needs `rename_list` added to onyx-core first)
|
||||||
- [ ] Keyboard shortcuts (Escape to close drawers/menus, tab navigation, Enter behaviors)
|
- [ ] Keyboard shortcuts (Escape to close drawers/menus, tab navigation, Enter behaviors)
|
||||||
- [ ] Sync status indicators (per workspace)
|
- [ ] Sync status indicators (per workspace)
|
||||||
- [ ] Push/pull sync mode selection
|
- [ ] Push/pull sync mode selection
|
||||||
|
|
@ -765,7 +765,7 @@ Tauri v2 supports iOS and Android natively. The same Svelte frontend and Rust ba
|
||||||
|
|
||||||
**iOS**:
|
**iOS**:
|
||||||
- Tauri generates Xcode project
|
- Tauri generates Xcode project
|
||||||
- Bundle identifier: `com.bevytasks.app`
|
- Bundle identifier: `com.onyx.app`
|
||||||
- Target: `aarch64-apple-ios`
|
- Target: `aarch64-apple-ios`
|
||||||
|
|
||||||
**Android**:
|
**Android**:
|
||||||
|
|
@ -952,7 +952,7 @@ If you want game-like polish after Phase 7:
|
||||||
- Migrate GUI from Tauri/Svelte to Bevy
|
- Migrate GUI from Tauri/Svelte to Bevy
|
||||||
- Full control over animations and rendering
|
- Full control over animations and rendering
|
||||||
- Unique, polished look beyond standard apps
|
- Unique, polished look beyond standard apps
|
||||||
- Backend (`bevy-tasks-core`) stays identical
|
- Backend (`onyx-core`) stays identical
|
||||||
- Only rewrite the GUI layer
|
- Only rewrite the GUI layer
|
||||||
|
|
||||||
### Deliverables
|
### Deliverables
|
||||||
|
|
|
||||||
50
README.md
50
README.md
|
|
@ -1,4 +1,4 @@
|
||||||
# Bevy Tasks
|
# Onyx
|
||||||
|
|
||||||
A **local-first, cross-platform tasks application** built with Rust. Inspired by Google Tasks, designed for speed and flexibility.
|
A **local-first, cross-platform tasks application** built with Rust. Inspired by Google Tasks, designed for speed and flexibility.
|
||||||
|
|
||||||
|
|
@ -12,13 +12,13 @@ A **local-first, cross-platform tasks application** built with Rust. Inspired by
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
bevy-tasks/
|
onyx/
|
||||||
├── Cargo.toml # Workspace definition
|
├── Cargo.toml # Workspace definition
|
||||||
├── PLAN.md # Detailed project plan
|
├── PLAN.md # Detailed project plan
|
||||||
├── README.md # This file
|
├── README.md # This file
|
||||||
├── crates/
|
├── crates/
|
||||||
│ ├── bevy-tasks-core/ # Core library (backend)
|
│ ├── onyx-core/ # Core library (backend)
|
||||||
│ └── bevy-tasks-cli/ # CLI frontend
|
│ └── onyx-cli/ # CLI frontend
|
||||||
├── apps/
|
├── apps/
|
||||||
│ └── tauri/ # Tauri v2 GUI (Svelte 5 + Tailwind CSS 4)
|
│ └── tauri/ # Tauri v2 GUI (Svelte 5 + Tailwind CSS 4)
|
||||||
└── docs/
|
└── docs/
|
||||||
|
|
@ -30,7 +30,7 @@ bevy-tasks/
|
||||||
- **Phase 2** (WebDAV Sync): Backend and CLI complete, GUI partially wired
|
- **Phase 2** (WebDAV Sync): Backend and CLI complete, GUI partially wired
|
||||||
- **Phase 3** (GUI MVP): In progress — core task CRUD working, UI polished
|
- **Phase 3** (GUI MVP): In progress — core task CRUD working, UI polished
|
||||||
|
|
||||||
### Core Library (`bevy-tasks-core`)
|
### Core Library (`onyx-core`)
|
||||||
- Data models (Task, TaskList, AppConfig, WorkspaceConfig)
|
- Data models (Task, TaskList, AppConfig, WorkspaceConfig)
|
||||||
- Markdown file I/O with YAML frontmatter
|
- Markdown file I/O with YAML frontmatter
|
||||||
- Local storage with repository pattern
|
- Local storage with repository pattern
|
||||||
|
|
@ -39,7 +39,7 @@ bevy-tasks/
|
||||||
- WebDAV sync with three-way diff and offline queue
|
- WebDAV sync with three-way diff and offline queue
|
||||||
- Platform keychain credential storage
|
- Platform keychain credential storage
|
||||||
|
|
||||||
### CLI (`bevy-tasks-cli`)
|
### CLI (`onyx-cli`)
|
||||||
- Workspace management (init, add, list, switch, remove, retarget, migrate)
|
- Workspace management (init, add, list, switch, remove, retarget, migrate)
|
||||||
- Task list management (create, show, delete)
|
- Task list management (create, show, delete)
|
||||||
- Task operations (add, complete, delete, edit)
|
- Task operations (add, complete, delete, edit)
|
||||||
|
|
@ -66,15 +66,15 @@ bevy-tasks/
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Clone and build
|
# Clone and build
|
||||||
git clone https://github.com/SteelDynamite/bevy-tasks.git
|
git clone https://github.com/SteelDynamite/onyx.git
|
||||||
cd bevy-tasks
|
cd onyx
|
||||||
cargo build
|
cargo build
|
||||||
|
|
||||||
# Run tests
|
# Run tests
|
||||||
cargo test -p bevy-tasks-core
|
cargo test -p onyx-core
|
||||||
|
|
||||||
# Run CLI
|
# Run CLI
|
||||||
cargo run -p bevy-tasks-cli -- --help
|
cargo run -p onyx-cli -- --help
|
||||||
|
|
||||||
# Run Tauri GUI
|
# Run Tauri GUI
|
||||||
cd apps/tauri && npm install
|
cd apps/tauri && npm install
|
||||||
|
|
@ -87,7 +87,7 @@ npm run tauri dev
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Initialize a new workspace
|
# Initialize a new workspace
|
||||||
cargo run -p bevy-tasks-cli -- init ~/Documents/Tasks --name personal
|
cargo run -p onyx-cli -- init ~/Documents/Tasks --name personal
|
||||||
|
|
||||||
# This creates:
|
# This creates:
|
||||||
# - A workspace named "personal" at ~/Documents/Tasks
|
# - A workspace named "personal" at ~/Documents/Tasks
|
||||||
|
|
@ -99,51 +99,51 @@ cargo run -p bevy-tasks-cli -- init ~/Documents/Tasks --name personal
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Add a task
|
# Add a task
|
||||||
cargo run -p bevy-tasks-cli -- add "Buy groceries"
|
cargo run -p onyx-cli -- add "Buy groceries"
|
||||||
|
|
||||||
# Add a task with due date
|
# Add a task with due date
|
||||||
cargo run -p bevy-tasks-cli -- add "Review PR #123" --list "Work" --due "2026-11-15"
|
cargo run -p onyx-cli -- add "Review PR #123" --list "Work" --due "2026-11-15"
|
||||||
|
|
||||||
# List all tasks
|
# List all tasks
|
||||||
cargo run -p bevy-tasks-cli -- list show
|
cargo run -p onyx-cli -- list show
|
||||||
|
|
||||||
# Complete a task
|
# Complete a task
|
||||||
cargo run -p bevy-tasks-cli -- complete <task-id>
|
cargo run -p onyx-cli -- complete <task-id>
|
||||||
|
|
||||||
# Edit a task (opens in $EDITOR)
|
# Edit a task (opens in $EDITOR)
|
||||||
cargo run -p bevy-tasks-cli -- edit <task-id>
|
cargo run -p onyx-cli -- edit <task-id>
|
||||||
|
|
||||||
# Delete a task
|
# Delete a task
|
||||||
cargo run -p bevy-tasks-cli -- delete <task-id>
|
cargo run -p onyx-cli -- delete <task-id>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Manage workspaces
|
### Manage workspaces
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Add another workspace
|
# Add another workspace
|
||||||
cargo run -p bevy-tasks-cli -- workspace add shared ~/Dropbox/TeamTasks
|
cargo run -p onyx-cli -- workspace add shared ~/Dropbox/TeamTasks
|
||||||
|
|
||||||
# List workspaces
|
# List workspaces
|
||||||
cargo run -p bevy-tasks-cli -- workspace list
|
cargo run -p onyx-cli -- workspace list
|
||||||
|
|
||||||
# Switch workspace
|
# Switch workspace
|
||||||
cargo run -p bevy-tasks-cli -- workspace switch shared
|
cargo run -p onyx-cli -- workspace switch shared
|
||||||
|
|
||||||
# Use specific workspace for a command
|
# Use specific workspace for a command
|
||||||
cargo run -p bevy-tasks-cli -- add "Team meeting" --workspace shared
|
cargo run -p onyx-cli -- add "Team meeting" --workspace shared
|
||||||
```
|
```
|
||||||
|
|
||||||
### Manage task lists
|
### Manage task lists
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Create a new list
|
# Create a new list
|
||||||
cargo run -p bevy-tasks-cli -- list create "Work"
|
cargo run -p onyx-cli -- list create "Work"
|
||||||
|
|
||||||
# Show tasks in a specific list
|
# Show tasks in a specific list
|
||||||
cargo run -p bevy-tasks-cli -- list show --list "Work"
|
cargo run -p onyx-cli -- list show --list "Work"
|
||||||
|
|
||||||
# Delete a list
|
# Delete a list
|
||||||
cargo run -p bevy-tasks-cli -- list delete "Work"
|
cargo run -p onyx-cli -- list delete "Work"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Data Format
|
## Data Format
|
||||||
|
|
@ -190,7 +190,7 @@ Run the test suite:
|
||||||
cargo test
|
cargo test
|
||||||
|
|
||||||
# Run tests for specific crate
|
# Run tests for specific crate
|
||||||
cargo test -p bevy-tasks-core
|
cargo test -p onyx-core
|
||||||
|
|
||||||
# Run tests with output
|
# Run tests with output
|
||||||
cargo test -- --nocapture
|
cargo test -- --nocapture
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# bevy_tasks
|
# onyx
|
||||||
|
|
||||||
A new Flutter project.
|
A new Flutter project.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,19 +32,19 @@ Future<void> main() async {
|
||||||
runApp(
|
runApp(
|
||||||
ChangeNotifierProvider(
|
ChangeNotifierProvider(
|
||||||
create: (_) => AppState()..loadConfig(),
|
create: (_) => AppState()..loadConfig(),
|
||||||
child: const BevyTasksApp(),
|
child: const OnyxApp(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class BevyTasksApp extends StatelessWidget {
|
class OnyxApp extends StatelessWidget {
|
||||||
const BevyTasksApp({super.key});
|
const OnyxApp({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final state = context.watch<AppState>();
|
final state = context.watch<AppState>();
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: 'Bevy Tasks',
|
title: 'Onyx',
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: AppTheme.light(),
|
theme: AppTheme.light(),
|
||||||
darkTheme: AppTheme.dark(),
|
darkTheme: AppTheme.dark(),
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
|
||||||
|
|
||||||
static const kDefaultExternalLibraryLoaderConfig =
|
static const kDefaultExternalLibraryLoaderConfig =
|
||||||
ExternalLibraryLoaderConfig(
|
ExternalLibraryLoaderConfig(
|
||||||
stem: 'bevy_tasks_flutter',
|
stem: 'onyx_flutter',
|
||||||
ioDirectory: 'rust/target/release/',
|
ioDirectory: 'rust/target/release/',
|
||||||
webPrefix: 'pkg/',
|
webPrefix: 'pkg/',
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ class _SetupScreenState extends State<SetupScreen> {
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('Bevy Tasks',
|
Text('Onyx',
|
||||||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700,
|
style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700,
|
||||||
color: isDark ? AppTheme.textDark : AppTheme.textLight)),
|
color: isDark ? AppTheme.textDark : AppTheme.textLight)),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@ project(runner LANGUAGES CXX)
|
||||||
|
|
||||||
# The name of the executable created for the application. Change this to change
|
# The name of the executable created for the application. Change this to change
|
||||||
# the on-disk name of your application.
|
# the on-disk name of your application.
|
||||||
set(BINARY_NAME "bevy_tasks")
|
set(BINARY_NAME "onyx")
|
||||||
# The unique GTK application identifier for this application. See:
|
# The unique GTK application identifier for this application. See:
|
||||||
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
|
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
|
||||||
set(APPLICATION_ID "com.bevytasks.bevy_tasks")
|
set(APPLICATION_ID "com.onyx.onyx")
|
||||||
|
|
||||||
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
||||||
# versions of CMake.
|
# versions of CMake.
|
||||||
|
|
@ -59,7 +59,7 @@ add_subdirectory("runner")
|
||||||
|
|
||||||
# Build the Rust FFI library for flutter_rust_bridge
|
# Build the Rust FFI library for flutter_rust_bridge
|
||||||
set(RUST_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../rust")
|
set(RUST_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../rust")
|
||||||
set(RUST_LIB_NAME "libbevy_tasks_flutter.so")
|
set(RUST_LIB_NAME "libonyx_flutter.so")
|
||||||
if(CMAKE_BUILD_TYPE MATCHES "Debug")
|
if(CMAKE_BUILD_TYPE MATCHES "Debug")
|
||||||
set(RUST_TARGET_DIR "${RUST_DIR}/target/debug")
|
set(RUST_TARGET_DIR "${RUST_DIR}/target/debug")
|
||||||
add_custom_command(
|
add_custom_command(
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ static void my_application_activate(GApplication* application) {
|
||||||
|
|
||||||
// Frameless transparent window
|
// Frameless transparent window
|
||||||
gtk_window_set_decorated(window, FALSE);
|
gtk_window_set_decorated(window, FALSE);
|
||||||
gtk_window_set_title(window, "bevy_tasks");
|
gtk_window_set_title(window, "onyx");
|
||||||
gtk_window_set_default_size(window, 400, 700);
|
gtk_window_set_default_size(window, 400, 700);
|
||||||
|
|
||||||
// Enable transparency
|
// Enable transparency
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
name: bevy_tasks
|
name: onyx
|
||||||
description: "Bevy Tasks - local-first task management"
|
description: "Onyx - local-first task management"
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 1.0.0+1
|
version: 1.0.0+1
|
||||||
|
|
||||||
|
|
|
||||||
6
apps/flutter/rust/Cargo.lock
generated
6
apps/flutter/rust/Cargo.lock
generated
|
|
@ -109,7 +109,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bevy-tasks-core"
|
name = "onyx-core"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|
@ -126,10 +126,10 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bevy-tasks-flutter"
|
name = "onyx-flutter"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bevy-tasks-core",
|
"onyx-core",
|
||||||
"chrono",
|
"chrono",
|
||||||
"flutter_rust_bridge",
|
"flutter_rust_bridge",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
[package]
|
[package]
|
||||||
name = "bevy-tasks-flutter"
|
name = "onyx-flutter"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
|
|
@ -8,7 +8,7 @@ crate-type = ["cdylib", "staticlib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
flutter_rust_bridge = "=2.11.1"
|
flutter_rust_bridge = "=2.11.1"
|
||||||
bevy-tasks-core = { path = "../../../crates/bevy-tasks-core" }
|
onyx-core = { path = "../../../crates/onyx-core" }
|
||||||
uuid = { version = "1", features = ["serde", "v4"] }
|
uuid = { version = "1", features = ["serde", "v4"] }
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
once_cell = "1"
|
once_cell = "1"
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ use std::sync::Mutex;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use bevy_tasks_core::{
|
use onyx_core::{
|
||||||
config::{AppConfig, WorkspaceConfig},
|
config::{AppConfig, WorkspaceConfig},
|
||||||
models::{Task, TaskList, TaskStatus},
|
models::{Task, TaskList, TaskStatus},
|
||||||
repository::TaskRepository,
|
repository::TaskRepository,
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
import 'package:bevy_tasks/main.dart';
|
import 'package:onyx/main.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
# Project-level configuration.
|
# Project-level configuration.
|
||||||
cmake_minimum_required(VERSION 3.14)
|
cmake_minimum_required(VERSION 3.14)
|
||||||
project(bevy_tasks LANGUAGES CXX)
|
project(onyx LANGUAGES CXX)
|
||||||
|
|
||||||
# The name of the executable created for the application. Change this to change
|
# The name of the executable created for the application. Change this to change
|
||||||
# the on-disk name of your application.
|
# the on-disk name of your application.
|
||||||
set(BINARY_NAME "bevy_tasks")
|
set(BINARY_NAME "onyx")
|
||||||
|
|
||||||
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
||||||
# versions of CMake.
|
# versions of CMake.
|
||||||
|
|
|
||||||
|
|
@ -90,12 +90,12 @@ BEGIN
|
||||||
BLOCK "040904e4"
|
BLOCK "040904e4"
|
||||||
BEGIN
|
BEGIN
|
||||||
VALUE "CompanyName", "com.example" "\0"
|
VALUE "CompanyName", "com.example" "\0"
|
||||||
VALUE "FileDescription", "bevy_tasks" "\0"
|
VALUE "FileDescription", "onyx" "\0"
|
||||||
VALUE "FileVersion", VERSION_AS_STRING "\0"
|
VALUE "FileVersion", VERSION_AS_STRING "\0"
|
||||||
VALUE "InternalName", "bevy_tasks" "\0"
|
VALUE "InternalName", "onyx" "\0"
|
||||||
VALUE "LegalCopyright", "Copyright (C) 2026 com.example. All rights reserved." "\0"
|
VALUE "LegalCopyright", "Copyright (C) 2026 com.example. All rights reserved." "\0"
|
||||||
VALUE "OriginalFilename", "bevy_tasks.exe" "\0"
|
VALUE "OriginalFilename", "onyx.exe" "\0"
|
||||||
VALUE "ProductName", "bevy_tasks" "\0"
|
VALUE "ProductName", "onyx" "\0"
|
||||||
VALUE "ProductVersion", VERSION_AS_STRING "\0"
|
VALUE "ProductVersion", VERSION_AS_STRING "\0"
|
||||||
END
|
END
|
||||||
END
|
END
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
||||||
FlutterWindow window(project);
|
FlutterWindow window(project);
|
||||||
Win32Window::Point origin(10, 10);
|
Win32Window::Point origin(10, 10);
|
||||||
Win32Window::Size size(1280, 720);
|
Win32Window::Size size(1280, 720);
|
||||||
if (!window.Create(L"bevy_tasks", origin, size)) {
|
if (!window.Create(L"onyx", origin, size)) {
|
||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
window.SetQuitOnClose(true);
|
window.SetQuitOnClose(true);
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Bevy Tasks</title>
|
<title>Onyx</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
|
||||||
4
apps/tauri/package-lock.json
generated
4
apps/tauri/package-lock.json
generated
|
|
@ -1,11 +1,11 @@
|
||||||
{
|
{
|
||||||
"name": "bevy-tasks",
|
"name": "onyx",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "bevy-tasks",
|
"name": "onyx",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2.0.0",
|
"@tauri-apps/api": "^2.0.0",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "bevy-tasks",
|
"name": "onyx",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
|
||||||
6
apps/tauri/src-tauri/Cargo.lock
generated
6
apps/tauri/src-tauri/Cargo.lock
generated
|
|
@ -95,7 +95,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bevy-tasks-core"
|
name = "onyx-core"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|
@ -112,10 +112,10 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bevy-tasks-tauri"
|
name = "onyx-tauri"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bevy-tasks-core",
|
"onyx-core",
|
||||||
"chrono",
|
"chrono",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
[package]
|
[package]
|
||||||
name = "bevy-tasks-tauri"
|
name = "onyx-tauri"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Tauri v2 desktop GUI for Bevy Tasks"
|
description = "Tauri v2 desktop GUI for Onyx"
|
||||||
license = "GPL-3.0-or-later"
|
license = "GPL-3.0-or-later"
|
||||||
repository = "https://github.com/SteelDynamite/bevy-tasks"
|
repository = "https://github.com/SteelDynamite/onyx"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "bevy_tasks_tauri_lib"
|
name = "onyx_tauri_lib"
|
||||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
|
|
@ -19,7 +19,7 @@ tauri-plugin-dialog = "2"
|
||||||
tauri-plugin-os = "2"
|
tauri-plugin-os = "2"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
bevy-tasks-core = { path = "../../../crates/bevy-tasks-core" }
|
onyx-core = { path = "../../../crates/onyx-core" }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
uuid = { version = "1", features = ["serde", "v4"] }
|
uuid = { version = "1", features = ["serde", "v4"] }
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"identifier": "default",
|
"identifier": "default",
|
||||||
"description": "Default capabilities for Bevy Tasks",
|
"description": "Default capabilities for Onyx",
|
||||||
"windows": ["main"],
|
"windows": ["main"],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default",
|
"core:default",
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use bevy_tasks_core::{
|
use onyx_core::{
|
||||||
config::{AppConfig, WorkspaceConfig},
|
config::{AppConfig, WorkspaceConfig},
|
||||||
models::{Task, TaskList, TaskStatus},
|
models::{Task, TaskList, TaskStatus},
|
||||||
repository::TaskRepository,
|
repository::TaskRepository,
|
||||||
|
|
@ -321,7 +321,7 @@ async fn test_webdav_connection(
|
||||||
username: String,
|
username: String,
|
||||||
password: String,
|
password: String,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let client = bevy_tasks_core::webdav::WebDavClient::new(&url, &username, &password);
|
let client = onyx_core::webdav::WebDavClient::new(&url, &username, &password);
|
||||||
client
|
client
|
||||||
.test_connection()
|
.test_connection()
|
||||||
.await
|
.await
|
||||||
|
|
|
||||||
|
|
@ -2,5 +2,5 @@
|
||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
bevy_tasks_tauri_lib::run()
|
onyx_tauri_lib::run()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://raw.githubusercontent.com/nicegui-org/nicegui/v2/tauri-conf-schema.json",
|
"$schema": "https://raw.githubusercontent.com/nicegui-org/nicegui/v2/tauri-conf-schema.json",
|
||||||
"productName": "Bevy Tasks",
|
"productName": "Onyx",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"identifier": "com.bevytasks.app",
|
"identifier": "com.onyx.app",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../dist",
|
"frontendDist": "../dist",
|
||||||
"devUrl": "http://localhost:1422",
|
"devUrl": "http://localhost:1422",
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
"withGlobalTauri": false,
|
"withGlobalTauri": false,
|
||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"title": "Bevy Tasks",
|
"title": "Onyx",
|
||||||
"width": 400,
|
"width": 400,
|
||||||
"height": 700,
|
"height": 700,
|
||||||
"minWidth": 320,
|
"minWidth": 320,
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
<div
|
<div
|
||||||
class="w-full max-w-sm rounded-2xl bg-card-light p-8 shadow-lg dark:bg-card-dark"
|
class="w-full max-w-sm rounded-2xl bg-card-light p-8 shadow-lg dark:bg-card-dark"
|
||||||
>
|
>
|
||||||
<h1 class="mb-1 text-2xl font-bold">Bevy Tasks</h1>
|
<h1 class="mb-1 text-2xl font-bold">Onyx</h1>
|
||||||
<p class="mb-6 text-sm text-text-secondary-light dark:text-text-secondary-dark">
|
<p class="mb-6 text-sm text-text-secondary-light dark:text-text-secondary-dark">
|
||||||
Create or open a workspace to get started.
|
Create or open a workspace to get started.
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
22
crates/onyx-cli/Cargo.toml
Normal file
22
crates/onyx-cli/Cargo.toml
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
[package]
|
||||||
|
name = "onyx-cli"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "CLI frontend for Onyx, a local-first task management app"
|
||||||
|
license = "GPL-3.0-or-later"
|
||||||
|
repository = "https://github.com/SteelDynamite/onyx"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "onyx"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
onyx-core = { path = "../onyx-core" }
|
||||||
|
clap = { version = "4.5", features = ["derive", "env"] }
|
||||||
|
colored = "2.0"
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
chrono = { workspace = true }
|
||||||
|
uuid = { workspace = true }
|
||||||
|
fs_extra = "1.3"
|
||||||
|
tokio = { workspace = true }
|
||||||
|
rpassword = "5.0"
|
||||||
39
crates/onyx-cli/src/commands/group.rs
Normal file
39
crates/onyx-cli/src/commands/group.rs
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use crate::output;
|
||||||
|
use crate::commands::get_repository;
|
||||||
|
|
||||||
|
pub fn enable(list_name: String, workspace: Option<String>) -> Result<()> {
|
||||||
|
let (mut repo, _workspace_name) = get_repository(workspace)?;
|
||||||
|
|
||||||
|
let lists = repo.get_lists()
|
||||||
|
.context("Failed to get lists")?;
|
||||||
|
|
||||||
|
let list = lists.iter()
|
||||||
|
.find(|l| l.title == list_name)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("List '{}' not found", list_name))?;
|
||||||
|
|
||||||
|
repo.set_group_by_due_date(list.id, true)
|
||||||
|
.context("Failed to enable grouping")?;
|
||||||
|
|
||||||
|
output::success(&format!("Enabled group-by-due-date for list \"{}\"", list_name));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn disable(list_name: String, workspace: Option<String>) -> Result<()> {
|
||||||
|
let (mut repo, _workspace_name) = get_repository(workspace)?;
|
||||||
|
|
||||||
|
let lists = repo.get_lists()
|
||||||
|
.context("Failed to get lists")?;
|
||||||
|
|
||||||
|
let list = lists.iter()
|
||||||
|
.find(|l| l.title == list_name)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("List '{}' not found", list_name))?;
|
||||||
|
|
||||||
|
repo.set_group_by_due_date(list.id, false)
|
||||||
|
.context("Failed to disable grouping")?;
|
||||||
|
|
||||||
|
output::success(&format!("Disabled group-by-due-date for list \"{}\"", list_name));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
43
crates/onyx-cli/src/commands/init.rs
Normal file
43
crates/onyx-cli/src/commands/init.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use onyx_core::{AppConfig, TaskRepository, WorkspaceConfig};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use crate::output;
|
||||||
|
|
||||||
|
pub fn execute(path: String, name: String) -> Result<()> {
|
||||||
|
let path_buf = PathBuf::from(path);
|
||||||
|
let path_buf = if path_buf.is_relative() {
|
||||||
|
std::env::current_dir()?.join(path_buf)
|
||||||
|
} else {
|
||||||
|
path_buf
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize the repository
|
||||||
|
let mut repo = TaskRepository::init(path_buf.clone())
|
||||||
|
.context("Failed to initialize tasks folder")?;
|
||||||
|
|
||||||
|
// Create default list if it doesn't exist
|
||||||
|
let lists = repo.get_lists().context("Failed to get lists")?;
|
||||||
|
if !lists.iter().any(|l| l.title == "My Tasks") {
|
||||||
|
repo.create_list("My Tasks".to_string())
|
||||||
|
.context("Failed to create default list")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load or create config
|
||||||
|
let config_path = AppConfig::get_config_path();
|
||||||
|
let mut config = AppConfig::load_from_file(&config_path)
|
||||||
|
.unwrap_or_else(|_| AppConfig::new());
|
||||||
|
|
||||||
|
// Add workspace
|
||||||
|
config.add_workspace(name.clone(), WorkspaceConfig::new(path_buf.clone()));
|
||||||
|
config.set_current_workspace(name.clone())?;
|
||||||
|
|
||||||
|
// Save config
|
||||||
|
config.save_to_file(&config_path)
|
||||||
|
.context("Failed to save config")?;
|
||||||
|
|
||||||
|
output::success(&format!("Initialized workspace \"{}\" at {}", name, path_buf.display()));
|
||||||
|
output::success("Created default list \"My Tasks\"");
|
||||||
|
output::success(&format!("Set \"{}\" as current workspace", name));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
91
crates/onyx-cli/src/commands/list.rs
Normal file
91
crates/onyx-cli/src/commands/list.rs
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use colored::*;
|
||||||
|
use onyx_core::{Task, TaskStatus};
|
||||||
|
use crate::output;
|
||||||
|
use crate::commands::get_repository;
|
||||||
|
|
||||||
|
fn print_tasks(tasks: &[Task]) {
|
||||||
|
if tasks.is_empty() {
|
||||||
|
output::item("No tasks");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for task in tasks {
|
||||||
|
let checkbox = if task.status == TaskStatus::Completed { "[✓]".green() } else { "[ ]".normal() };
|
||||||
|
let due_str = task.due_date.map(|d| format!(" (due: {})", d.format("%Y-%m-%d")).yellow().to_string()).unwrap_or_default();
|
||||||
|
output::item(&format!("{} {}{} {}", checkbox, task.title, due_str, task.id.to_string().dimmed()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create(name: String, workspace: Option<String>) -> Result<()> {
|
||||||
|
let (mut repo, _workspace_name) = get_repository(workspace)?;
|
||||||
|
|
||||||
|
repo.create_list(name.clone())
|
||||||
|
.context("Failed to create list")?;
|
||||||
|
|
||||||
|
output::success(&format!("Created list \"{}\"", name));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show(list_name: Option<String>, workspace: Option<String>) -> Result<()> {
|
||||||
|
let (repo, _workspace_name) = get_repository(workspace)?;
|
||||||
|
|
||||||
|
let lists = repo.get_lists()
|
||||||
|
.context("Failed to get lists")?;
|
||||||
|
|
||||||
|
if lists.is_empty() {
|
||||||
|
output::info("No lists found. Create one with 'onyx list create <name>'");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a specific list is requested, show only that one
|
||||||
|
if let Some(name) = list_name {
|
||||||
|
let list = lists.iter()
|
||||||
|
.find(|l| l.title == name)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("List '{}' not found", name))?;
|
||||||
|
|
||||||
|
output::header(&format!("{} ({})", list.title, format!("{} tasks", list.tasks.len()).dimmed()));
|
||||||
|
print_tasks(&list.tasks);
|
||||||
|
} else {
|
||||||
|
// Show all lists
|
||||||
|
for list in &lists {
|
||||||
|
output::header(&format!("{} ({})", list.title, format!("{} tasks", list.tasks.len()).dimmed()));
|
||||||
|
print_tasks(&list.tasks);
|
||||||
|
output::blank();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete(name: String, workspace: Option<String>) -> Result<()> {
|
||||||
|
let (mut repo, _workspace_name) = get_repository(workspace)?;
|
||||||
|
|
||||||
|
let lists = repo.get_lists()
|
||||||
|
.context("Failed to get lists")?;
|
||||||
|
|
||||||
|
let list = lists.iter()
|
||||||
|
.find(|l| l.title == name)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("List '{}' not found", name))?;
|
||||||
|
|
||||||
|
// Confirm
|
||||||
|
output::warning(&format!("This will delete list \"{}\" and all its tasks", name));
|
||||||
|
print!("Continue? (y/n): ");
|
||||||
|
use std::io::{self, Write};
|
||||||
|
io::stdout().flush()?;
|
||||||
|
|
||||||
|
let mut input = String::new();
|
||||||
|
io::stdin().read_line(&mut input)?;
|
||||||
|
|
||||||
|
if input.trim().to_lowercase() != "y" {
|
||||||
|
output::info("Cancelled");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
repo.delete_list(list.id)
|
||||||
|
.context("Failed to delete list")?;
|
||||||
|
|
||||||
|
output::success(&format!("Deleted list \"{}\"", name));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
43
crates/onyx-cli/src/commands/mod.rs
Normal file
43
crates/onyx-cli/src/commands/mod.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
pub mod init;
|
||||||
|
pub mod workspace;
|
||||||
|
pub mod list;
|
||||||
|
pub mod task;
|
||||||
|
pub mod group;
|
||||||
|
pub mod sync;
|
||||||
|
|
||||||
|
use onyx_core::{AppConfig, TaskRepository};
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
pub fn get_config_path() -> PathBuf {
|
||||||
|
AppConfig::get_config_path()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_config() -> Result<AppConfig> {
|
||||||
|
let path = get_config_path();
|
||||||
|
AppConfig::load_from_file(&path).context("Failed to load config")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_config(config: &AppConfig) -> Result<()> {
|
||||||
|
let path = get_config_path();
|
||||||
|
config.save_to_file(&path).context("Failed to save config")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_repository(workspace_name: Option<String>) -> Result<(TaskRepository, String)> {
|
||||||
|
let config = load_config()?;
|
||||||
|
|
||||||
|
let (name, workspace_config) = if let Some(name) = workspace_name {
|
||||||
|
let workspace_config = config.get_workspace(&name)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Workspace '{}' not found", name))?;
|
||||||
|
(name, workspace_config.clone())
|
||||||
|
} else {
|
||||||
|
let (name, workspace_config) = config.get_current_workspace()
|
||||||
|
.context("No workspace set. Use 'onyx init' to create one.")?;
|
||||||
|
(name.clone(), workspace_config.clone())
|
||||||
|
};
|
||||||
|
|
||||||
|
let repo = TaskRepository::new(workspace_config.path.clone())
|
||||||
|
.context(format!("Failed to open workspace '{}'", name))?;
|
||||||
|
|
||||||
|
Ok((repo, name))
|
||||||
|
}
|
||||||
229
crates/onyx-cli/src/commands/sync.rs
Normal file
229
crates/onyx-cli/src/commands/sync.rs
Normal file
|
|
@ -0,0 +1,229 @@
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use colored::Colorize;
|
||||||
|
use onyx_core::sync::{SyncMode, sync_workspace, get_sync_status};
|
||||||
|
use onyx_core::webdav::{WebDavClient, store_credentials, load_credentials};
|
||||||
|
use crate::output;
|
||||||
|
use super::{load_config, save_config};
|
||||||
|
|
||||||
|
/// Run sync setup: prompt for URL, username, password, test connection, store credentials.
|
||||||
|
pub fn setup(workspace_name: Option<String>) -> Result<()> {
|
||||||
|
let mut config = load_config()?;
|
||||||
|
|
||||||
|
let (name, workspace) = if let Some(name) = workspace_name {
|
||||||
|
let ws = config.get_workspace(&name)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Workspace '{}' not found", name))?
|
||||||
|
.clone();
|
||||||
|
(name, ws)
|
||||||
|
} else {
|
||||||
|
let (n, ws) = config.get_current_workspace()
|
||||||
|
.context("No workspace set. Use 'onyx init' to create one.")?;
|
||||||
|
(n.clone(), ws.clone())
|
||||||
|
};
|
||||||
|
|
||||||
|
// Prompt for WebDAV URL
|
||||||
|
output::header(&format!("WebDAV sync setup for workspace \"{}\"", name.green()));
|
||||||
|
output::blank();
|
||||||
|
|
||||||
|
let url = prompt("WebDAV URL: ")?;
|
||||||
|
if url.is_empty() {
|
||||||
|
output::error("URL cannot be empty");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let username = prompt("Username: ")?;
|
||||||
|
let password = rpassword::read_password_from_tty(Some("Password: "))
|
||||||
|
.context("Failed to read password")?;
|
||||||
|
|
||||||
|
// Test connection
|
||||||
|
output::blank();
|
||||||
|
output::info("Testing connection...");
|
||||||
|
|
||||||
|
let rt = tokio::runtime::Runtime::new().context("Failed to create async runtime")?;
|
||||||
|
let client = WebDavClient::new(&url, &username, &password);
|
||||||
|
|
||||||
|
match rt.block_on(client.test_connection()) {
|
||||||
|
Ok(()) => {
|
||||||
|
output::success("Connection successful!");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
output::error(&format!("Connection failed: {}", e));
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store credentials in keychain
|
||||||
|
let domain = extract_domain(&url);
|
||||||
|
match store_credentials(&domain, &username, &password) {
|
||||||
|
Ok(()) => output::info("Credentials stored in system keychain"),
|
||||||
|
Err(e) => {
|
||||||
|
output::warning(&format!(
|
||||||
|
"Could not store in keychain ({}). Set ONYX_WEBDAV_USER and ONYX_WEBDAV_PASS env vars instead.",
|
||||||
|
e
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update workspace config with WebDAV URL
|
||||||
|
let mut ws = workspace;
|
||||||
|
ws.webdav_url = Some(url);
|
||||||
|
config.add_workspace(name, ws);
|
||||||
|
save_config(&config)?;
|
||||||
|
|
||||||
|
output::success("Sync setup complete. Run 'onyx sync' to sync.");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute a sync operation.
|
||||||
|
pub fn execute(mode: SyncMode, workspace_name: Option<String>) -> Result<()> {
|
||||||
|
let config = load_config()?;
|
||||||
|
|
||||||
|
let (name, workspace) = if let Some(name) = workspace_name {
|
||||||
|
let ws = config.get_workspace(&name)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Workspace '{}' not found", name))?
|
||||||
|
.clone();
|
||||||
|
(name, ws)
|
||||||
|
} else {
|
||||||
|
let (n, ws) = config.get_current_workspace()
|
||||||
|
.context("No workspace set. Use 'onyx init' to create one.")?;
|
||||||
|
(n.clone(), ws.clone())
|
||||||
|
};
|
||||||
|
|
||||||
|
let url = workspace.webdav_url.as_ref()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!(
|
||||||
|
"No WebDAV URL configured for workspace '{}'. Run 'onyx sync --setup' first.", name
|
||||||
|
))?;
|
||||||
|
|
||||||
|
let domain = extract_domain(url);
|
||||||
|
let (username, password) = load_credentials(&domain)
|
||||||
|
.context("Failed to load credentials")?;
|
||||||
|
|
||||||
|
let mode_str = match mode {
|
||||||
|
SyncMode::Full => "Syncing",
|
||||||
|
SyncMode::Push => "Pushing",
|
||||||
|
SyncMode::Pull => "Pulling",
|
||||||
|
};
|
||||||
|
output::info(&format!("{} workspace \"{}\"...", mode_str, name.green()));
|
||||||
|
|
||||||
|
let rt = tokio::runtime::Runtime::new().context("Failed to create async runtime")?;
|
||||||
|
let result = rt.block_on(sync_workspace(
|
||||||
|
&workspace.path,
|
||||||
|
url,
|
||||||
|
&username,
|
||||||
|
&password,
|
||||||
|
mode,
|
||||||
|
Some(Box::new(|msg: &str| { println!("{}", msg); })),
|
||||||
|
)).context("Sync failed")?;
|
||||||
|
|
||||||
|
// Print summary
|
||||||
|
let mut parts = Vec::new();
|
||||||
|
if result.uploaded > 0 { parts.push(format!("{} uploaded", result.uploaded)); }
|
||||||
|
if result.downloaded > 0 { parts.push(format!("{} downloaded", result.downloaded)); }
|
||||||
|
if result.deleted_local > 0 { parts.push(format!("{} deleted locally", result.deleted_local)); }
|
||||||
|
if result.deleted_remote > 0 { parts.push(format!("{} deleted remotely", result.deleted_remote)); }
|
||||||
|
if result.conflicts > 0 { parts.push(format!("{} conflicts", result.conflicts)); }
|
||||||
|
|
||||||
|
if parts.is_empty() {
|
||||||
|
output::success("Already in sync, nothing to do.");
|
||||||
|
} else {
|
||||||
|
let summary = parts.join(", ");
|
||||||
|
if result.errors.is_empty() {
|
||||||
|
output::success(&format!("Sync complete: {}", summary));
|
||||||
|
} else {
|
||||||
|
output::warning(&format!("Sync complete with errors: {}", summary));
|
||||||
|
for err in &result.errors {
|
||||||
|
output::error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show sync status for a workspace.
|
||||||
|
pub fn status(workspace_name: Option<String>, all: bool) -> Result<()> {
|
||||||
|
let config = load_config()?;
|
||||||
|
|
||||||
|
if all {
|
||||||
|
// Show status for all workspaces that have sync configured
|
||||||
|
let mut found_any = false;
|
||||||
|
let mut names: Vec<_> = config.workspaces.keys().cloned().collect();
|
||||||
|
names.sort();
|
||||||
|
for name in names {
|
||||||
|
let ws = config.get_workspace(&name).unwrap();
|
||||||
|
if ws.webdav_url.is_some() {
|
||||||
|
found_any = true;
|
||||||
|
print_workspace_status(&name, &ws.path, ws.webdav_url.as_deref())?;
|
||||||
|
output::blank();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found_any {
|
||||||
|
output::info("No workspaces have sync configured. Run 'onyx sync --setup' to set up.");
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let (name, workspace) = if let Some(name) = workspace_name {
|
||||||
|
let ws = config.get_workspace(&name)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Workspace '{}' not found", name))?
|
||||||
|
.clone();
|
||||||
|
(name, ws)
|
||||||
|
} else {
|
||||||
|
let (n, ws) = config.get_current_workspace()
|
||||||
|
.context("No workspace set.")?;
|
||||||
|
(n.clone(), ws.clone())
|
||||||
|
};
|
||||||
|
|
||||||
|
print_workspace_status(&name, &workspace.path, workspace.webdav_url.as_deref())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_workspace_status(name: &str, path: &std::path::Path, webdav_url: Option<&str>) -> Result<()> {
|
||||||
|
output::header(&format!("Workspace: {}", name.green()));
|
||||||
|
|
||||||
|
if let Some(url) = webdav_url {
|
||||||
|
output::detail("WebDAV URL", url);
|
||||||
|
} else {
|
||||||
|
output::detail("WebDAV", &"not configured".dimmed().to_string());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let info = get_sync_status(path)?;
|
||||||
|
|
||||||
|
if let Some(last) = info.last_sync {
|
||||||
|
output::detail("Last sync", &last.format("%Y-%m-%d %H:%M:%S UTC").to_string());
|
||||||
|
} else {
|
||||||
|
output::detail("Last sync", &"never".dimmed().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
output::detail("Tracked files", &info.tracked_files.to_string());
|
||||||
|
output::detail("Pending changes", &info.pending_changes.to_string());
|
||||||
|
if info.queued_operations > 0 {
|
||||||
|
output::detail("Queued operations", &format!("{}", info.queued_operations).yellow().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract domain from a URL for credential storage.
|
||||||
|
fn extract_domain(url: &str) -> String {
|
||||||
|
url.split("://")
|
||||||
|
.nth(1)
|
||||||
|
.unwrap_or(url)
|
||||||
|
.split('/')
|
||||||
|
.next()
|
||||||
|
.unwrap_or(url)
|
||||||
|
.split(':')
|
||||||
|
.next()
|
||||||
|
.unwrap_or(url)
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prompt the user for text input.
|
||||||
|
fn prompt(message: &str) -> Result<String> {
|
||||||
|
use std::io::Write;
|
||||||
|
print!("{}", message);
|
||||||
|
std::io::stdout().flush()?;
|
||||||
|
let mut input = String::new();
|
||||||
|
std::io::stdin().read_line(&mut input)?;
|
||||||
|
Ok(input.trim().to_string())
|
||||||
|
}
|
||||||
222
crates/onyx-cli/src/commands/task.rs
Normal file
222
crates/onyx-cli/src/commands/task.rs
Normal file
|
|
@ -0,0 +1,222 @@
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use onyx_core::Task;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use uuid::Uuid;
|
||||||
|
use crate::output;
|
||||||
|
use crate::commands::get_repository;
|
||||||
|
|
||||||
|
pub fn add(title: String, list_name: Option<String>, due_str: Option<String>, workspace: Option<String>) -> Result<()> {
|
||||||
|
let (mut repo, _workspace_name) = get_repository(workspace)?;
|
||||||
|
|
||||||
|
// Get lists
|
||||||
|
let lists = repo.get_lists()
|
||||||
|
.context("Failed to get lists")?;
|
||||||
|
|
||||||
|
if lists.is_empty() {
|
||||||
|
anyhow::bail!("No lists found. Create one with 'onyx list create <name>'");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the target list
|
||||||
|
let list = if let Some(name) = list_name {
|
||||||
|
lists.iter()
|
||||||
|
.find(|l| l.title == name)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("List '{}' not found", name))?
|
||||||
|
} else {
|
||||||
|
// Use the first list
|
||||||
|
&lists[0]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create task
|
||||||
|
let mut task = Task::new(title.clone());
|
||||||
|
|
||||||
|
// Parse due date if provided
|
||||||
|
if let Some(due_str) = due_str {
|
||||||
|
let due_date = parse_due_date(&due_str)?;
|
||||||
|
task.due_date = Some(due_date);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save task
|
||||||
|
repo.create_task(list.id, task.clone())
|
||||||
|
.context("Failed to create task")?;
|
||||||
|
|
||||||
|
let due_info = if let Some(due) = task.due_date {
|
||||||
|
format!("\n Due: {}", due.format("%Y-%m-%d"))
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
output::success(&format!("Created task \"{}\" ({}){}", title, task.id, due_info));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn complete(task_id_str: String, workspace: Option<String>) -> Result<()> {
|
||||||
|
let (mut repo, _workspace_name) = get_repository(workspace)?;
|
||||||
|
|
||||||
|
let task_id = Uuid::parse_str(&task_id_str)
|
||||||
|
.context("Invalid task ID")?;
|
||||||
|
|
||||||
|
// Find the task across all lists
|
||||||
|
let lists = repo.get_lists()?;
|
||||||
|
let mut found = false;
|
||||||
|
|
||||||
|
for list in lists {
|
||||||
|
if let Some(mut task) = list.tasks.iter().find(|t| t.id == task_id).cloned() {
|
||||||
|
task.complete();
|
||||||
|
repo.update_task(list.id, task.clone())
|
||||||
|
.context("Failed to update task")?;
|
||||||
|
|
||||||
|
output::success(&format!("Completed task \"{}\"", task.title));
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
anyhow::bail!("Task not found: {}", task_id_str);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete(task_id_str: String, workspace: Option<String>) -> Result<()> {
|
||||||
|
let (mut repo, _workspace_name) = get_repository(workspace)?;
|
||||||
|
|
||||||
|
let task_id = Uuid::parse_str(&task_id_str)
|
||||||
|
.context("Invalid task ID")?;
|
||||||
|
|
||||||
|
// Find the task across all lists
|
||||||
|
let lists = repo.get_lists()?;
|
||||||
|
let mut found = false;
|
||||||
|
|
||||||
|
for list in lists {
|
||||||
|
if let Some(task) = list.tasks.iter().find(|t| t.id == task_id) {
|
||||||
|
let title = task.title.clone();
|
||||||
|
|
||||||
|
output::warning(&format!("This will delete task \"{}\"", title));
|
||||||
|
print!("Continue? (y/n): ");
|
||||||
|
use std::io::{self, Write};
|
||||||
|
io::stdout().flush()?;
|
||||||
|
let mut input = String::new();
|
||||||
|
io::stdin().read_line(&mut input)?;
|
||||||
|
if input.trim().to_lowercase() != "y" {
|
||||||
|
output::info("Cancelled");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
repo.delete_task(list.id, task_id)
|
||||||
|
.context("Failed to delete task")?;
|
||||||
|
|
||||||
|
output::success(&format!("Deleted task \"{}\"", title));
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
anyhow::bail!("Task not found: {}", task_id_str);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn edit(task_id_str: String, workspace: Option<String>) -> Result<()> {
|
||||||
|
let (mut repo, _workspace_name) = get_repository(workspace)?;
|
||||||
|
|
||||||
|
let task_id = Uuid::parse_str(&task_id_str)
|
||||||
|
.context("Invalid task ID")?;
|
||||||
|
|
||||||
|
// Find the task across all lists
|
||||||
|
let lists = repo.get_lists()?;
|
||||||
|
let mut task_list_id = None;
|
||||||
|
let mut task_to_edit = None;
|
||||||
|
|
||||||
|
for list in lists {
|
||||||
|
if let Some(task) = list.tasks.iter().find(|t| t.id == task_id).cloned() {
|
||||||
|
task_list_id = Some(list.id);
|
||||||
|
task_to_edit = Some(task);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (list_id, task) = match (task_list_id, task_to_edit) {
|
||||||
|
(Some(lid), Some(t)) => (lid, t),
|
||||||
|
_ => anyhow::bail!("Task not found: {}", task_id_str),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create temporary file with task content
|
||||||
|
let temp_dir = std::env::temp_dir();
|
||||||
|
let temp_file = temp_dir.join(format!("onyx-{}.md", task.id));
|
||||||
|
|
||||||
|
// Write current task content to temp file
|
||||||
|
let content = format!("# {}\n\n{}", task.title, task.description);
|
||||||
|
std::fs::write(&temp_file, content)?;
|
||||||
|
|
||||||
|
// Get editor from environment
|
||||||
|
let editor = std::env::var("EDITOR").unwrap_or_else(|_| {
|
||||||
|
if cfg!(windows) {
|
||||||
|
"notepad".to_string()
|
||||||
|
} else {
|
||||||
|
"nano".to_string()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Open editor
|
||||||
|
let status = std::process::Command::new(&editor)
|
||||||
|
.arg(&temp_file)
|
||||||
|
.status()
|
||||||
|
.context(format!("Failed to open editor: {}", editor))?;
|
||||||
|
|
||||||
|
if !status.success() {
|
||||||
|
anyhow::bail!("Editor exited with non-zero status");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read updated content
|
||||||
|
let updated_content = std::fs::read_to_string(&temp_file)?;
|
||||||
|
|
||||||
|
// Parse the content
|
||||||
|
let lines: Vec<&str> = updated_content.lines().collect();
|
||||||
|
let (title, description) = if !lines.is_empty() && lines[0].starts_with("# ") {
|
||||||
|
let title = lines[0].trim_start_matches("# ").trim().to_string();
|
||||||
|
let description = if lines.len() > 2 {
|
||||||
|
lines[2..].join("\n").trim().to_string()
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
(title, description)
|
||||||
|
} else {
|
||||||
|
(task.title.clone(), updated_content.trim().to_string())
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update task
|
||||||
|
let mut updated_task = task.clone();
|
||||||
|
updated_task.title = title;
|
||||||
|
updated_task.description = description;
|
||||||
|
updated_task.updated_at = Utc::now();
|
||||||
|
|
||||||
|
repo.update_task(list_id, updated_task.clone())
|
||||||
|
.context("Failed to update task")?;
|
||||||
|
|
||||||
|
// Clean up temp file
|
||||||
|
std::fs::remove_file(&temp_file).ok();
|
||||||
|
|
||||||
|
output::success(&format!("Updated task \"{}\"", updated_task.title));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_due_date(s: &str) -> Result<DateTime<Utc>> {
|
||||||
|
// Try parsing as date only (YYYY-MM-DD)
|
||||||
|
if let Ok(naive_date) = chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d") {
|
||||||
|
let naive_datetime = naive_date.and_hms_opt(0, 0, 0)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Invalid date"))?;
|
||||||
|
return Ok(DateTime::from_naive_utc_and_offset(naive_datetime, Utc));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try parsing as full datetime (ISO 8601)
|
||||||
|
if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
|
||||||
|
return Ok(dt.with_timezone(&Utc));
|
||||||
|
}
|
||||||
|
|
||||||
|
anyhow::bail!("Invalid date format. Use YYYY-MM-DD or ISO 8601 format (YYYY-MM-DDTHH:MM:SS)")
|
||||||
|
}
|
||||||
209
crates/onyx-cli/src/commands/workspace.rs
Normal file
209
crates/onyx-cli/src/commands/workspace.rs
Normal file
|
|
@ -0,0 +1,209 @@
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use onyx_core::{TaskRepository, WorkspaceConfig};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use colored::*;
|
||||||
|
use crate::output;
|
||||||
|
use crate::commands::{load_config, save_config};
|
||||||
|
|
||||||
|
pub fn add(name: String, path: String) -> Result<()> {
|
||||||
|
let path_buf = PathBuf::from(path);
|
||||||
|
let path_buf = if path_buf.is_relative() {
|
||||||
|
std::env::current_dir()?.join(path_buf)
|
||||||
|
} else {
|
||||||
|
path_buf
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize the repository
|
||||||
|
let mut repo = TaskRepository::init(path_buf.clone())
|
||||||
|
.context("Failed to initialize tasks folder")?;
|
||||||
|
|
||||||
|
// Create default list if it doesn't exist
|
||||||
|
let lists = repo.get_lists().context("Failed to get lists")?;
|
||||||
|
if !lists.iter().any(|l| l.title == "My Tasks") {
|
||||||
|
repo.create_list("My Tasks".to_string())
|
||||||
|
.context("Failed to create default list")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load config
|
||||||
|
let mut config = load_config()?;
|
||||||
|
|
||||||
|
// Check if workspace already exists
|
||||||
|
if config.get_workspace(&name).is_some() {
|
||||||
|
anyhow::bail!("Workspace '{}' already exists", name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add workspace
|
||||||
|
config.add_workspace(name.clone(), WorkspaceConfig::new(path_buf.clone()));
|
||||||
|
|
||||||
|
// Save config
|
||||||
|
save_config(&config)?;
|
||||||
|
|
||||||
|
output::success(&format!("Added workspace \"{}\" at {}", name, path_buf.display()));
|
||||||
|
output::success("Created default list \"My Tasks\"");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list() -> Result<()> {
|
||||||
|
let config = load_config()?;
|
||||||
|
|
||||||
|
if config.workspaces.is_empty() {
|
||||||
|
output::info("No workspaces configured. Use 'onyx init' to create one.");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let current = config.current_workspace.as_deref();
|
||||||
|
|
||||||
|
let mut workspaces: Vec<_> = config.workspaces.iter().collect();
|
||||||
|
workspaces.sort_by(|a, b| a.0.cmp(b.0));
|
||||||
|
|
||||||
|
for (name, workspace_config) in workspaces {
|
||||||
|
let marker = if Some(name.as_str()) == current {
|
||||||
|
" (current)".green()
|
||||||
|
} else {
|
||||||
|
"".normal()
|
||||||
|
};
|
||||||
|
output::item(&format!("{}: {}{}", name, workspace_config.path.display(), marker));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn switch(name: String) -> Result<()> {
|
||||||
|
let mut config = load_config()?;
|
||||||
|
|
||||||
|
// Verify workspace exists
|
||||||
|
if config.get_workspace(&name).is_none() {
|
||||||
|
anyhow::bail!("Workspace '{}' not found", name);
|
||||||
|
}
|
||||||
|
|
||||||
|
config.set_current_workspace(name.clone())?;
|
||||||
|
save_config(&config)?;
|
||||||
|
|
||||||
|
output::success(&format!("Switched to workspace \"{}\"", name));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove(name: String) -> Result<()> {
|
||||||
|
let mut config = load_config()?;
|
||||||
|
|
||||||
|
// Verify workspace exists
|
||||||
|
if config.get_workspace(&name).is_none() {
|
||||||
|
anyhow::bail!("Workspace '{}' not found", name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm
|
||||||
|
output::warning("This will delete workspace config (files remain on disk)");
|
||||||
|
print!("Continue? (y/n): ");
|
||||||
|
use std::io::{self, Write};
|
||||||
|
io::stdout().flush()?;
|
||||||
|
|
||||||
|
let mut input = String::new();
|
||||||
|
io::stdin().read_line(&mut input)?;
|
||||||
|
|
||||||
|
if input.trim().to_lowercase() != "y" {
|
||||||
|
output::info("Cancelled");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
config.remove_workspace(&name);
|
||||||
|
save_config(&config)?;
|
||||||
|
|
||||||
|
output::success(&format!("Removed workspace \"{}\"", name));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn retarget(name: String, path: String) -> Result<()> {
|
||||||
|
let path_buf = PathBuf::from(path);
|
||||||
|
let path_buf = if path_buf.is_relative() {
|
||||||
|
std::env::current_dir()?.join(path_buf)
|
||||||
|
} else {
|
||||||
|
path_buf
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut config = load_config()?;
|
||||||
|
|
||||||
|
// Verify workspace exists
|
||||||
|
if config.get_workspace(&name).is_none() {
|
||||||
|
anyhow::bail!("Workspace '{}' not found", name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update path
|
||||||
|
config.add_workspace(name.clone(), WorkspaceConfig::new(path_buf.clone()));
|
||||||
|
save_config(&config)?;
|
||||||
|
|
||||||
|
output::success(&format!("Workspace \"{}\" now points to {}", name, path_buf.display()));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn migrate(name: String, new_path: String) -> Result<()> {
|
||||||
|
let new_path_buf = PathBuf::from(new_path);
|
||||||
|
let new_path_buf = if new_path_buf.is_relative() {
|
||||||
|
std::env::current_dir()?.join(new_path_buf)
|
||||||
|
} else {
|
||||||
|
new_path_buf
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut config = load_config()?;
|
||||||
|
|
||||||
|
// Get current workspace config
|
||||||
|
let old_path = config.get_workspace(&name)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Workspace '{}' not found", name))?
|
||||||
|
.path.clone();
|
||||||
|
|
||||||
|
// Confirm
|
||||||
|
output::warning(&format!("This will move all files from {} to {}", old_path.display(), new_path_buf.display()));
|
||||||
|
print!("Continue? (y/n): ");
|
||||||
|
use std::io::{self, Write};
|
||||||
|
io::stdout().flush()?;
|
||||||
|
|
||||||
|
let mut input = String::new();
|
||||||
|
io::stdin().read_line(&mut input)?;
|
||||||
|
|
||||||
|
if input.trim().to_lowercase() != "y" {
|
||||||
|
output::info("Cancelled");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create destination directory
|
||||||
|
std::fs::create_dir_all(&new_path_buf)?;
|
||||||
|
|
||||||
|
// Move files
|
||||||
|
output::info("Moving files...");
|
||||||
|
let entries = std::fs::read_dir(&old_path)?;
|
||||||
|
let mut count = 0;
|
||||||
|
|
||||||
|
for entry in entries {
|
||||||
|
let entry = entry?;
|
||||||
|
let file_name = entry.file_name();
|
||||||
|
let dest = new_path_buf.join(&file_name);
|
||||||
|
|
||||||
|
if entry.path().is_dir() {
|
||||||
|
let mut options = fs_extra::dir::CopyOptions::new();
|
||||||
|
options.copy_inside = true;
|
||||||
|
fs_extra::dir::move_dir(entry.path(), &new_path_buf, &options)?;
|
||||||
|
output::item(&format!("Moved {}/", file_name.to_string_lossy()));
|
||||||
|
} else {
|
||||||
|
std::fs::rename(entry.path(), dest)?;
|
||||||
|
output::item(&format!("Moved {}", file_name.to_string_lossy()));
|
||||||
|
}
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove old directory if empty
|
||||||
|
if old_path.read_dir()?.next().is_none() {
|
||||||
|
std::fs::remove_dir(&old_path)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update config
|
||||||
|
config.add_workspace(name.clone(), WorkspaceConfig::new(new_path_buf.clone()));
|
||||||
|
save_config(&config)?;
|
||||||
|
|
||||||
|
output::success(&format!("Migrated {} items to {}", count, new_path_buf.display()));
|
||||||
|
output::success(&format!("Workspace \"{}\" now points to {}", name, new_path_buf.display()));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
277
crates/onyx-cli/src/main.rs
Normal file
277
crates/onyx-cli/src/main.rs
Normal file
|
|
@ -0,0 +1,277 @@
|
||||||
|
mod commands;
|
||||||
|
mod output;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
use commands::*;
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(name = "onyx")]
|
||||||
|
#[command(about = "A local-first, cross-platform tasks application", long_about = None)]
|
||||||
|
struct Cli {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: Commands,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
enum Commands {
|
||||||
|
/// Initialize a new workspace
|
||||||
|
Init {
|
||||||
|
/// Path to store tasks
|
||||||
|
path: String,
|
||||||
|
/// Name of the workspace
|
||||||
|
#[arg(short, long)]
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Manage workspaces
|
||||||
|
#[command(subcommand)]
|
||||||
|
Workspace(WorkspaceCommands),
|
||||||
|
|
||||||
|
/// Manage task lists
|
||||||
|
#[command(subcommand)]
|
||||||
|
List(ListCommands),
|
||||||
|
|
||||||
|
/// Add a new task
|
||||||
|
Add {
|
||||||
|
/// Task title
|
||||||
|
title: String,
|
||||||
|
/// List to add task to
|
||||||
|
#[arg(short, long)]
|
||||||
|
list: Option<String>,
|
||||||
|
/// Due date (ISO 8601 format: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS)
|
||||||
|
#[arg(short, long)]
|
||||||
|
due: Option<String>,
|
||||||
|
/// Workspace to use
|
||||||
|
#[arg(short, long)]
|
||||||
|
workspace: Option<String>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Mark a task as complete
|
||||||
|
Complete {
|
||||||
|
/// Task ID
|
||||||
|
task_id: String,
|
||||||
|
/// Workspace to use
|
||||||
|
#[arg(short, long)]
|
||||||
|
workspace: Option<String>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Delete a task
|
||||||
|
Delete {
|
||||||
|
/// Task ID
|
||||||
|
task_id: String,
|
||||||
|
/// Workspace to use
|
||||||
|
#[arg(short, long)]
|
||||||
|
workspace: Option<String>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Edit a task
|
||||||
|
Edit {
|
||||||
|
/// Task ID
|
||||||
|
task_id: String,
|
||||||
|
/// Workspace to use
|
||||||
|
#[arg(short, long)]
|
||||||
|
workspace: Option<String>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Toggle group-by-due-date for a list
|
||||||
|
#[command(subcommand)]
|
||||||
|
Group(GroupCommands),
|
||||||
|
|
||||||
|
/// Sync workspace with WebDAV server
|
||||||
|
Sync {
|
||||||
|
/// Run initial setup (URL, credentials)
|
||||||
|
#[arg(long)]
|
||||||
|
setup: bool,
|
||||||
|
/// Push-only sync (upload local changes)
|
||||||
|
#[arg(long, conflicts_with_all = ["pull", "setup", "status"])]
|
||||||
|
push: bool,
|
||||||
|
/// Pull-only sync (download remote changes)
|
||||||
|
#[arg(long, conflicts_with_all = ["push", "setup", "status"])]
|
||||||
|
pull: bool,
|
||||||
|
/// Show sync status
|
||||||
|
#[arg(long, conflicts_with_all = ["push", "pull", "setup"])]
|
||||||
|
status: bool,
|
||||||
|
/// Show status for all workspaces (with --status)
|
||||||
|
#[arg(long, requires = "status")]
|
||||||
|
all: bool,
|
||||||
|
/// Workspace to use
|
||||||
|
#[arg(short, long)]
|
||||||
|
workspace: Option<String>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
enum WorkspaceCommands {
|
||||||
|
/// Add a new workspace
|
||||||
|
Add {
|
||||||
|
/// Name of the workspace
|
||||||
|
name: String,
|
||||||
|
/// Path to store tasks
|
||||||
|
path: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// List all workspaces
|
||||||
|
List,
|
||||||
|
|
||||||
|
/// Switch to a different workspace
|
||||||
|
Switch {
|
||||||
|
/// Name of the workspace
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Remove a workspace
|
||||||
|
Remove {
|
||||||
|
/// Name of the workspace
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Update workspace path without moving files
|
||||||
|
Retarget {
|
||||||
|
/// Name of the workspace
|
||||||
|
name: String,
|
||||||
|
/// New path
|
||||||
|
path: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Move workspace files to a new location
|
||||||
|
Migrate {
|
||||||
|
/// Name of the workspace
|
||||||
|
name: String,
|
||||||
|
/// New path
|
||||||
|
path: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
enum ListCommands {
|
||||||
|
/// Create a new task list
|
||||||
|
Create {
|
||||||
|
/// Name of the list
|
||||||
|
name: String,
|
||||||
|
/// Workspace to use
|
||||||
|
#[arg(short, long)]
|
||||||
|
workspace: Option<String>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Show all tasks (or tasks in a specific list)
|
||||||
|
Show {
|
||||||
|
/// Name of the list to show
|
||||||
|
#[arg(short, long)]
|
||||||
|
list: Option<String>,
|
||||||
|
/// Workspace to use
|
||||||
|
#[arg(short, long)]
|
||||||
|
workspace: Option<String>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Delete a task list
|
||||||
|
Delete {
|
||||||
|
/// Name of the list to delete
|
||||||
|
name: String,
|
||||||
|
/// Workspace to use
|
||||||
|
#[arg(short, long)]
|
||||||
|
workspace: Option<String>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
enum GroupCommands {
|
||||||
|
/// Enable group-by-due-date for a list
|
||||||
|
Enable {
|
||||||
|
/// Name of the list
|
||||||
|
#[arg(short, long)]
|
||||||
|
list: String,
|
||||||
|
/// Workspace to use
|
||||||
|
#[arg(short, long)]
|
||||||
|
workspace: Option<String>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Disable group-by-due-date for a list
|
||||||
|
Disable {
|
||||||
|
/// Name of the list
|
||||||
|
#[arg(short, long)]
|
||||||
|
list: String,
|
||||||
|
/// Workspace to use
|
||||||
|
#[arg(short, long)]
|
||||||
|
workspace: Option<String>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
match cli.command {
|
||||||
|
Commands::Init { path, name } => {
|
||||||
|
init::execute(path, name)?;
|
||||||
|
}
|
||||||
|
Commands::Workspace(cmd) => match cmd {
|
||||||
|
WorkspaceCommands::Add { name, path } => {
|
||||||
|
workspace::add(name, path)?;
|
||||||
|
}
|
||||||
|
WorkspaceCommands::List => {
|
||||||
|
workspace::list()?;
|
||||||
|
}
|
||||||
|
WorkspaceCommands::Switch { name } => {
|
||||||
|
workspace::switch(name)?;
|
||||||
|
}
|
||||||
|
WorkspaceCommands::Remove { name } => {
|
||||||
|
workspace::remove(name)?;
|
||||||
|
}
|
||||||
|
WorkspaceCommands::Retarget { name, path } => {
|
||||||
|
workspace::retarget(name, path)?;
|
||||||
|
}
|
||||||
|
WorkspaceCommands::Migrate { name, path } => {
|
||||||
|
workspace::migrate(name, path)?;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Commands::List(cmd) => match cmd {
|
||||||
|
ListCommands::Create { name, workspace } => {
|
||||||
|
list::create(name, workspace)?;
|
||||||
|
}
|
||||||
|
ListCommands::Show { list, workspace } => {
|
||||||
|
list::show(list, workspace)?;
|
||||||
|
}
|
||||||
|
ListCommands::Delete { name, workspace } => {
|
||||||
|
list::delete(name, workspace)?;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Commands::Add { title, list, due, workspace } => {
|
||||||
|
task::add(title, list, due, workspace)?;
|
||||||
|
}
|
||||||
|
Commands::Complete { task_id, workspace } => {
|
||||||
|
task::complete(task_id, workspace)?;
|
||||||
|
}
|
||||||
|
Commands::Delete { task_id, workspace } => {
|
||||||
|
task::delete(task_id, workspace)?;
|
||||||
|
}
|
||||||
|
Commands::Edit { task_id, workspace } => {
|
||||||
|
task::edit(task_id, workspace)?;
|
||||||
|
}
|
||||||
|
Commands::Group(cmd) => match cmd {
|
||||||
|
GroupCommands::Enable { list, workspace } => {
|
||||||
|
group::enable(list, workspace)?;
|
||||||
|
}
|
||||||
|
GroupCommands::Disable { list, workspace } => {
|
||||||
|
group::disable(list, workspace)?;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Commands::Sync { setup, push, pull, status, all, workspace } => {
|
||||||
|
if setup {
|
||||||
|
sync::setup(workspace)?;
|
||||||
|
} else if status {
|
||||||
|
sync::status(workspace, all)?;
|
||||||
|
} else {
|
||||||
|
let mode = if push {
|
||||||
|
onyx_core::sync::SyncMode::Push
|
||||||
|
} else if pull {
|
||||||
|
onyx_core::sync::SyncMode::Pull
|
||||||
|
} else {
|
||||||
|
onyx_core::sync::SyncMode::Full
|
||||||
|
};
|
||||||
|
sync::execute(mode, workspace)?;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
33
crates/onyx-cli/src/output.rs
Normal file
33
crates/onyx-cli/src/output.rs
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
use colored::*;
|
||||||
|
|
||||||
|
pub fn success(message: &str) {
|
||||||
|
println!("{} {}", "✓".green(), message);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn error(message: &str) {
|
||||||
|
eprintln!("{} {}", "✗".red(), message);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn warning(message: &str) {
|
||||||
|
println!("{} {}", "⚠".yellow(), message);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn info(message: &str) {
|
||||||
|
println!("{} {}", "ℹ".blue(), message);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn header(message: &str) {
|
||||||
|
println!("{}", message.bold());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn detail(label: &str, value: &str) {
|
||||||
|
println!(" {}: {}", label, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn item(message: &str) {
|
||||||
|
println!(" {}", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn blank() {
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
[package]
|
[package]
|
||||||
name = "bevy-tasks-core"
|
name = "onyx-core"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Core library for local-first task management with markdown storage and WebDAV sync"
|
description = "Core library for local-first task management with markdown storage and WebDAV sync"
|
||||||
license = "GPL-3.0-or-later"
|
license = "GPL-3.0-or-later"
|
||||||
repository = "https://github.com/SteelDynamite/bevy-tasks"
|
repository = "https://github.com/SteelDynamite/onyx"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
|
|
@ -82,7 +82,7 @@ impl AppConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_config_path() -> PathBuf {
|
pub fn get_config_path() -> PathBuf {
|
||||||
let config_dir = directories::ProjectDirs::from("", "", "bevy-tasks")
|
let config_dir = directories::ProjectDirs::from("", "", "onyx")
|
||||||
.expect("Failed to determine config directory");
|
.expect("Failed to determine config directory");
|
||||||
config_dir.config_dir().join("config.json")
|
config_dir.config_dir().join("config.json")
|
||||||
}
|
}
|
||||||
|
|
@ -381,7 +381,7 @@ fn extract_relative_path(href: &str, base_url: &str, request_path: &str) -> Stri
|
||||||
|
|
||||||
/// Store WebDAV credentials in the platform keychain.
|
/// Store WebDAV credentials in the platform keychain.
|
||||||
pub fn store_credentials(domain: &str, username: &str, password: &str) -> Result<()> {
|
pub fn store_credentials(domain: &str, username: &str, password: &str) -> Result<()> {
|
||||||
let service = format!("com.bevy-tasks.webdav.{}", domain);
|
let service = format!("com.onyx.webdav.{}", domain);
|
||||||
|
|
||||||
let user_entry = keyring::Entry::new(&service, "username")
|
let user_entry = keyring::Entry::new(&service, "username")
|
||||||
.map_err(|e| Error::Credential(format!("Failed to create keyring entry: {}", e)))?;
|
.map_err(|e| Error::Credential(format!("Failed to create keyring entry: {}", e)))?;
|
||||||
|
|
@ -398,7 +398,7 @@ pub fn store_credentials(domain: &str, username: &str, password: &str) -> Result
|
||||||
|
|
||||||
/// Load WebDAV credentials from the platform keychain, falling back to env vars.
|
/// Load WebDAV credentials from the platform keychain, falling back to env vars.
|
||||||
pub fn load_credentials(domain: &str) -> Result<(String, String)> {
|
pub fn load_credentials(domain: &str) -> Result<(String, String)> {
|
||||||
let service = format!("com.bevy-tasks.webdav.{}", domain);
|
let service = format!("com.onyx.webdav.{}", domain);
|
||||||
|
|
||||||
let user_entry = keyring::Entry::new(&service, "username")
|
let user_entry = keyring::Entry::new(&service, "username")
|
||||||
.map_err(|e| Error::Credential(format!("Failed to create keyring entry: {}", e)))?;
|
.map_err(|e| Error::Credential(format!("Failed to create keyring entry: {}", e)))?;
|
||||||
|
|
@ -411,21 +411,21 @@ pub fn load_credentials(domain: &str) -> Result<(String, String)> {
|
||||||
|
|
||||||
// Fallback to env vars for headless/CI environments
|
// Fallback to env vars for headless/CI environments
|
||||||
if let (Ok(user), Ok(pass)) = (
|
if let (Ok(user), Ok(pass)) = (
|
||||||
std::env::var("BEVY_TASKS_WEBDAV_USER"),
|
std::env::var("ONYX_WEBDAV_USER"),
|
||||||
std::env::var("BEVY_TASKS_WEBDAV_PASS"),
|
std::env::var("ONYX_WEBDAV_PASS"),
|
||||||
) {
|
) {
|
||||||
return Ok((user, pass));
|
return Ok((user, pass));
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(Error::Credential(format!(
|
Err(Error::Credential(format!(
|
||||||
"No credentials found for '{}'. Run 'bevy-tasks sync --setup' or set BEVY_TASKS_WEBDAV_USER and BEVY_TASKS_WEBDAV_PASS.",
|
"No credentials found for '{}'. Run 'onyx sync --setup' or set ONYX_WEBDAV_USER and ONYX_WEBDAV_PASS.",
|
||||||
domain
|
domain
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete WebDAV credentials from the platform keychain.
|
/// Delete WebDAV credentials from the platform keychain.
|
||||||
pub fn delete_credentials(domain: &str) -> Result<()> {
|
pub fn delete_credentials(domain: &str) -> Result<()> {
|
||||||
let service = format!("com.bevy-tasks.webdav.{}", domain);
|
let service = format!("com.onyx.webdav.{}", domain);
|
||||||
|
|
||||||
if let Ok(entry) = keyring::Entry::new(&service, "username") {
|
if let Ok(entry) = keyring::Entry::new(&service, "username") {
|
||||||
let _ = entry.delete_credential();
|
let _ = entry.delete_credential();
|
||||||
30
docs/API.md
30
docs/API.md
|
|
@ -1,8 +1,8 @@
|
||||||
# Bevy Tasks Core - API Documentation
|
# Onyx Core - API Documentation
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
The `bevy-tasks-core` library provides a complete backend for managing tasks in a local-first manner. Tasks are stored as markdown files with YAML frontmatter, compatible with Obsidian and other markdown editors.
|
The `onyx-core` library provides a complete backend for managing tasks in a local-first manner. Tasks are stored as markdown files with YAML frontmatter, compatible with Obsidian and other markdown editors.
|
||||||
|
|
||||||
## Core Concepts
|
## Core Concepts
|
||||||
|
|
||||||
|
|
@ -33,7 +33,7 @@ pub enum TaskStatus {
|
||||||
**Creating a Task:**
|
**Creating a Task:**
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use bevy_tasks_core::Task;
|
use onyx_core::Task;
|
||||||
|
|
||||||
// Simple task
|
// Simple task
|
||||||
let task = Task::new("Buy groceries".to_string());
|
let task = Task::new("Buy groceries".to_string());
|
||||||
|
|
@ -73,14 +73,14 @@ pub struct AppConfig {
|
||||||
```
|
```
|
||||||
|
|
||||||
**Location:**
|
**Location:**
|
||||||
- Windows: `%APPDATA%/bevy-tasks/config.json`
|
- Windows: `%APPDATA%/onyx/config.json`
|
||||||
- Linux: `~/.config/bevy-tasks/config.json`
|
- Linux: `~/.config/onyx/config.json`
|
||||||
- macOS: `~/Library/Application Support/bevy-tasks/config.json`
|
- macOS: `~/Library/Application Support/onyx/config.json`
|
||||||
|
|
||||||
**Usage:**
|
**Usage:**
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use bevy_tasks_core::AppConfig;
|
use onyx_core::AppConfig;
|
||||||
|
|
||||||
// Load config
|
// Load config
|
||||||
let config_path = AppConfig::get_config_path();
|
let config_path = AppConfig::get_config_path();
|
||||||
|
|
@ -116,7 +116,7 @@ The main interface for interacting with tasks and lists.
|
||||||
### Initialization
|
### Initialization
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use bevy_tasks_core::TaskRepository;
|
use onyx_core::TaskRepository;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
// Open existing repository
|
// Open existing repository
|
||||||
|
|
@ -280,12 +280,12 @@ The sync module provides bi-directional WebDAV synchronization with three-way di
|
||||||
|
|
||||||
### Sync Functions
|
### Sync Functions
|
||||||
|
|
||||||
Sync functions live in the `bevy_tasks_core::sync` module as standalone functions (not on `TaskRepository`).
|
Sync functions live in the `onyx_core::sync` module as standalone functions (not on `TaskRepository`).
|
||||||
|
|
||||||
#### Sync a Workspace
|
#### Sync a Workspace
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use bevy_tasks_core::sync::{sync_workspace, SyncMode};
|
use onyx_core::sync::{sync_workspace, SyncMode};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
// Full bi-directional sync
|
// Full bi-directional sync
|
||||||
|
|
@ -305,7 +305,7 @@ sync_workspace(path, url, user, pass, SyncMode::PullOnly).await?;
|
||||||
#### Check Sync Status
|
#### Check Sync Status
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use bevy_tasks_core::sync::get_sync_status;
|
use onyx_core::sync::get_sync_status;
|
||||||
|
|
||||||
let status = get_sync_status(Path::new("/home/user/tasks"))?;
|
let status = get_sync_status(Path::new("/home/user/tasks"))?;
|
||||||
// Returns SyncStatusInfo with last sync time, pending changes, etc.
|
// Returns SyncStatusInfo with last sync time, pending changes, etc.
|
||||||
|
|
@ -316,7 +316,7 @@ let status = get_sync_status(Path::new("/home/user/tasks"))?;
|
||||||
Credentials are stored in the platform keychain (Windows Credential Manager, macOS Keychain, Linux Secret Service).
|
Credentials are stored in the platform keychain (Windows Credential Manager, macOS Keychain, Linux Secret Service).
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use bevy_tasks_core::webdav::{store_credentials, load_credentials, delete_credentials};
|
use onyx_core::webdav::{store_credentials, load_credentials, delete_credentials};
|
||||||
|
|
||||||
// Store credentials
|
// Store credentials
|
||||||
store_credentials("nextcloud.example.com", "username", "password")?;
|
store_credentials("nextcloud.example.com", "username", "password")?;
|
||||||
|
|
@ -331,7 +331,7 @@ delete_credentials("nextcloud.example.com")?;
|
||||||
### WebDAV Client
|
### WebDAV Client
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use bevy_tasks_core::webdav::WebDavClient;
|
use onyx_core::webdav::WebDavClient;
|
||||||
|
|
||||||
let client = WebDavClient::new(
|
let client = WebDavClient::new(
|
||||||
"https://nextcloud.example.com/remote.php/dav/files/user/Tasks",
|
"https://nextcloud.example.com/remote.php/dav/files/user/Tasks",
|
||||||
|
|
@ -380,7 +380,7 @@ pub enum Error {
|
||||||
## Example: Complete Workflow
|
## Example: Complete Workflow
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use bevy_tasks_core::{TaskRepository, Task, AppConfig, WorkspaceConfig};
|
use onyx_core::{TaskRepository, Task, AppConfig, WorkspaceConfig};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
|
@ -428,7 +428,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
The core library includes comprehensive tests. Run them with:
|
The core library includes comprehensive tests. Run them with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo test -p bevy-tasks-core
|
cargo test -p onyx-core
|
||||||
```
|
```
|
||||||
|
|
||||||
Key test areas:
|
Key test areas:
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Clone the repository
|
# Clone the repository
|
||||||
git clone https://github.com/SteelDynamite/bevy-tasks.git
|
git clone https://github.com/SteelDynamite/onyx.git
|
||||||
cd bevy-tasks
|
cd onyx
|
||||||
|
|
||||||
# Build the project
|
# Build the project
|
||||||
cargo build
|
cargo build
|
||||||
|
|
@ -23,7 +23,7 @@ cargo build
|
||||||
cargo test
|
cargo test
|
||||||
|
|
||||||
# Run the CLI
|
# Run the CLI
|
||||||
cargo run -p bevy-tasks-cli -- --help
|
cargo run -p onyx-cli -- --help
|
||||||
|
|
||||||
# Run the Tauri GUI
|
# Run the Tauri GUI
|
||||||
cd apps/tauri && npm install
|
cd apps/tauri && npm install
|
||||||
|
|
@ -33,10 +33,10 @@ npm run tauri dev
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
bevy-tasks/
|
onyx/
|
||||||
├── Cargo.toml # Workspace manifest
|
├── Cargo.toml # Workspace manifest
|
||||||
├── crates/
|
├── crates/
|
||||||
│ ├── bevy-tasks-core/ # Core library
|
│ ├── onyx-core/ # Core library
|
||||||
│ │ ├── src/
|
│ │ ├── src/
|
||||||
│ │ │ ├── lib.rs # Library entry point
|
│ │ │ ├── lib.rs # Library entry point
|
||||||
│ │ │ ├── models.rs # Data models (Task, TaskList, etc.)
|
│ │ │ ├── models.rs # Data models (Task, TaskList, etc.)
|
||||||
|
|
@ -47,7 +47,7 @@ bevy-tasks/
|
||||||
│ │ │ ├── sync.rs # Three-way sync engine with offline queue
|
│ │ │ ├── sync.rs # Three-way sync engine with offline queue
|
||||||
│ │ │ └── webdav.rs # WebDAV client and credential storage
|
│ │ │ └── webdav.rs # WebDAV client and credential storage
|
||||||
│ │ └── Cargo.toml
|
│ │ └── Cargo.toml
|
||||||
│ ├── bevy-tasks-cli/ # CLI application
|
│ ├── onyx-cli/ # CLI application
|
||||||
│ │ ├── src/
|
│ │ ├── src/
|
||||||
│ │ │ ├── main.rs # CLI entry point and command parsing
|
│ │ │ ├── main.rs # CLI entry point and command parsing
|
||||||
│ │ │ ├── output.rs # Output formatting utilities
|
│ │ │ ├── output.rs # Output formatting utilities
|
||||||
|
|
@ -95,10 +95,10 @@ bevy-tasks/
|
||||||
cargo test
|
cargo test
|
||||||
|
|
||||||
# Run tests for a specific crate
|
# Run tests for a specific crate
|
||||||
cargo test -p bevy-tasks-core
|
cargo test -p onyx-core
|
||||||
|
|
||||||
# Run a specific test
|
# Run a specific test
|
||||||
cargo test -p bevy-tasks-core test_create_and_list_tasks
|
cargo test -p onyx-core test_create_and_list_tasks
|
||||||
|
|
||||||
# Run tests with output
|
# Run tests with output
|
||||||
cargo test -- --nocapture
|
cargo test -- --nocapture
|
||||||
|
|
@ -114,17 +114,17 @@ cargo build
|
||||||
cargo build --release
|
cargo build --release
|
||||||
|
|
||||||
# Build specific crate
|
# Build specific crate
|
||||||
cargo build -p bevy-tasks-cli
|
cargo build -p onyx-cli
|
||||||
```
|
```
|
||||||
|
|
||||||
### Running the CLI in Development
|
### Running the CLI in Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run with cargo (recommended for development)
|
# Run with cargo (recommended for development)
|
||||||
cargo run -p bevy-tasks-cli -- init ~/test-tasks --name test
|
cargo run -p onyx-cli -- init ~/test-tasks --name test
|
||||||
|
|
||||||
# Run the compiled binary
|
# Run the compiled binary
|
||||||
./target/debug/bevy-tasks init ~/test-tasks --name test
|
./target/debug/onyx init ~/test-tasks --name test
|
||||||
```
|
```
|
||||||
|
|
||||||
## Code Style
|
## Code Style
|
||||||
|
|
@ -155,7 +155,7 @@ cargo clippy -- -W clippy::all
|
||||||
|
|
||||||
## Architecture Guidelines
|
## Architecture Guidelines
|
||||||
|
|
||||||
### Core Library (`bevy-tasks-core`)
|
### Core Library (`onyx-core`)
|
||||||
|
|
||||||
**Principles:**
|
**Principles:**
|
||||||
- Pure Rust, no CLI dependencies
|
- Pure Rust, no CLI dependencies
|
||||||
|
|
@ -171,7 +171,7 @@ cargo clippy -- -W clippy::all
|
||||||
4. Write tests
|
4. Write tests
|
||||||
5. Update API documentation
|
5. Update API documentation
|
||||||
|
|
||||||
### CLI (`bevy-tasks-cli`)
|
### CLI (`onyx-cli`)
|
||||||
|
|
||||||
**Principles:**
|
**Principles:**
|
||||||
- Thin layer over core library
|
- Thin layer over core library
|
||||||
|
|
@ -210,8 +210,8 @@ mod tests {
|
||||||
Located in `tests/` directories within each crate:
|
Located in `tests/` directories within each crate:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// crates/bevy-tasks-core/tests/integration_test.rs
|
// crates/onyx-core/tests/integration_test.rs
|
||||||
use bevy_tasks_core::*;
|
use onyx_core::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_full_workflow() {
|
fn test_full_workflow() {
|
||||||
|
|
@ -321,13 +321,13 @@ ls -la ~/test-tasks
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Verify workspace configuration
|
# Verify workspace configuration
|
||||||
cat ~/.config/bevy-tasks/config.json | jq
|
cat ~/.config/onyx/config.json | jq
|
||||||
|
|
||||||
# Check current workspace
|
# Check current workspace
|
||||||
cargo run -p bevy-tasks-cli -- workspace list
|
cargo run -p onyx-cli -- workspace list
|
||||||
|
|
||||||
# Initialize if needed
|
# Initialize if needed
|
||||||
cargo run -p bevy-tasks-cli -- init ~/test-tasks --name test
|
cargo run -p onyx-cli -- init ~/test-tasks --name test
|
||||||
```
|
```
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue