From 9e204ef818ec6af83b703385d6eb7d3a7314fc49 Mon Sep 17 00:00:00 2001 From: Tristan Michael Date: Tue, 31 Mar 2026 09:46:55 -0700 Subject: [PATCH 1/5] rename onyx-core crate (formerly bevy-tasks-core) --- crates/{bevy-tasks-core => onyx-core}/Cargo.toml | 4 ++-- crates/{bevy-tasks-core => onyx-core}/src/config.rs | 2 +- crates/{bevy-tasks-core => onyx-core}/src/error.rs | 0 crates/{bevy-tasks-core => onyx-core}/src/lib.rs | 0 crates/{bevy-tasks-core => onyx-core}/src/models.rs | 0 .../{bevy-tasks-core => onyx-core}/src/repository.rs | 0 crates/{bevy-tasks-core => onyx-core}/src/storage.rs | 0 crates/{bevy-tasks-core => onyx-core}/src/sync.rs | 0 crates/{bevy-tasks-core => onyx-core}/src/webdav.rs | 12 ++++++------ 9 files changed, 9 insertions(+), 9 deletions(-) rename crates/{bevy-tasks-core => onyx-core}/Cargo.toml (88%) rename crates/{bevy-tasks-core => onyx-core}/src/config.rs (99%) rename crates/{bevy-tasks-core => onyx-core}/src/error.rs (100%) rename crates/{bevy-tasks-core => onyx-core}/src/lib.rs (100%) rename crates/{bevy-tasks-core => onyx-core}/src/models.rs (100%) rename crates/{bevy-tasks-core => onyx-core}/src/repository.rs (100%) rename crates/{bevy-tasks-core => onyx-core}/src/storage.rs (100%) rename crates/{bevy-tasks-core => onyx-core}/src/sync.rs (100%) rename crates/{bevy-tasks-core => onyx-core}/src/webdav.rs (98%) diff --git a/crates/bevy-tasks-core/Cargo.toml b/crates/onyx-core/Cargo.toml similarity index 88% rename from crates/bevy-tasks-core/Cargo.toml rename to crates/onyx-core/Cargo.toml index a158b1e..0100d0a 100644 --- a/crates/bevy-tasks-core/Cargo.toml +++ b/crates/onyx-core/Cargo.toml @@ -1,10 +1,10 @@ [package] -name = "bevy-tasks-core" +name = "onyx-core" version = "0.1.0" edition = "2021" description = "Core library for local-first task management with markdown storage and WebDAV sync" license = "GPL-3.0-or-later" -repository = "https://github.com/SteelDynamite/bevy-tasks" +repository = "https://github.com/SteelDynamite/onyx" [dependencies] serde = { workspace = true } diff --git a/crates/bevy-tasks-core/src/config.rs b/crates/onyx-core/src/config.rs similarity index 99% rename from crates/bevy-tasks-core/src/config.rs rename to crates/onyx-core/src/config.rs index 4741204..9667fe3 100644 --- a/crates/bevy-tasks-core/src/config.rs +++ b/crates/onyx-core/src/config.rs @@ -82,7 +82,7 @@ impl AppConfig { } 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"); config_dir.config_dir().join("config.json") } diff --git a/crates/bevy-tasks-core/src/error.rs b/crates/onyx-core/src/error.rs similarity index 100% rename from crates/bevy-tasks-core/src/error.rs rename to crates/onyx-core/src/error.rs diff --git a/crates/bevy-tasks-core/src/lib.rs b/crates/onyx-core/src/lib.rs similarity index 100% rename from crates/bevy-tasks-core/src/lib.rs rename to crates/onyx-core/src/lib.rs diff --git a/crates/bevy-tasks-core/src/models.rs b/crates/onyx-core/src/models.rs similarity index 100% rename from crates/bevy-tasks-core/src/models.rs rename to crates/onyx-core/src/models.rs diff --git a/crates/bevy-tasks-core/src/repository.rs b/crates/onyx-core/src/repository.rs similarity index 100% rename from crates/bevy-tasks-core/src/repository.rs rename to crates/onyx-core/src/repository.rs diff --git a/crates/bevy-tasks-core/src/storage.rs b/crates/onyx-core/src/storage.rs similarity index 100% rename from crates/bevy-tasks-core/src/storage.rs rename to crates/onyx-core/src/storage.rs diff --git a/crates/bevy-tasks-core/src/sync.rs b/crates/onyx-core/src/sync.rs similarity index 100% rename from crates/bevy-tasks-core/src/sync.rs rename to crates/onyx-core/src/sync.rs diff --git a/crates/bevy-tasks-core/src/webdav.rs b/crates/onyx-core/src/webdav.rs similarity index 98% rename from crates/bevy-tasks-core/src/webdav.rs rename to crates/onyx-core/src/webdav.rs index 87c54b7..fc852e5 100644 --- a/crates/bevy-tasks-core/src/webdav.rs +++ b/crates/onyx-core/src/webdav.rs @@ -381,7 +381,7 @@ fn extract_relative_path(href: &str, base_url: &str, request_path: &str) -> Stri /// Store WebDAV credentials in the platform keychain. 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") .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. 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") .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 if let (Ok(user), Ok(pass)) = ( - std::env::var("BEVY_TASKS_WEBDAV_USER"), - std::env::var("BEVY_TASKS_WEBDAV_PASS"), + std::env::var("ONYX_WEBDAV_USER"), + std::env::var("ONYX_WEBDAV_PASS"), ) { return Ok((user, pass)); } 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 ))) } /// Delete WebDAV credentials from the platform keychain. 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") { let _ = entry.delete_credential(); From 27363c8424f48308ae248af7588fbd3a9285e10d Mon Sep 17 00:00:00 2001 From: Tristan Michael Date: Tue, 31 Mar 2026 09:47:02 -0700 Subject: [PATCH 2/5] rename onyx-cli crate (formerly bevy-tasks-cli) --- crates/onyx-cli/Cargo.toml | 22 ++ crates/onyx-cli/src/commands/group.rs | 39 +++ crates/onyx-cli/src/commands/init.rs | 43 ++++ crates/onyx-cli/src/commands/list.rs | 91 +++++++ crates/onyx-cli/src/commands/mod.rs | 43 ++++ crates/onyx-cli/src/commands/sync.rs | 229 ++++++++++++++++++ crates/onyx-cli/src/commands/task.rs | 222 +++++++++++++++++ crates/onyx-cli/src/commands/workspace.rs | 209 ++++++++++++++++ crates/onyx-cli/src/main.rs | 277 ++++++++++++++++++++++ crates/onyx-cli/src/output.rs | 33 +++ 10 files changed, 1208 insertions(+) create mode 100644 crates/onyx-cli/Cargo.toml create mode 100644 crates/onyx-cli/src/commands/group.rs create mode 100644 crates/onyx-cli/src/commands/init.rs create mode 100644 crates/onyx-cli/src/commands/list.rs create mode 100644 crates/onyx-cli/src/commands/mod.rs create mode 100644 crates/onyx-cli/src/commands/sync.rs create mode 100644 crates/onyx-cli/src/commands/task.rs create mode 100644 crates/onyx-cli/src/commands/workspace.rs create mode 100644 crates/onyx-cli/src/main.rs create mode 100644 crates/onyx-cli/src/output.rs diff --git a/crates/onyx-cli/Cargo.toml b/crates/onyx-cli/Cargo.toml new file mode 100644 index 0000000..620b42b --- /dev/null +++ b/crates/onyx-cli/Cargo.toml @@ -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" diff --git a/crates/onyx-cli/src/commands/group.rs b/crates/onyx-cli/src/commands/group.rs new file mode 100644 index 0000000..23cf69f --- /dev/null +++ b/crates/onyx-cli/src/commands/group.rs @@ -0,0 +1,39 @@ +use anyhow::{Context, Result}; +use crate::output; +use crate::commands::get_repository; + +pub fn enable(list_name: String, workspace: Option) -> 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) -> 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(()) +} diff --git a/crates/onyx-cli/src/commands/init.rs b/crates/onyx-cli/src/commands/init.rs new file mode 100644 index 0000000..807258d --- /dev/null +++ b/crates/onyx-cli/src/commands/init.rs @@ -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(()) +} diff --git a/crates/onyx-cli/src/commands/list.rs b/crates/onyx-cli/src/commands/list.rs new file mode 100644 index 0000000..ee56160 --- /dev/null +++ b/crates/onyx-cli/src/commands/list.rs @@ -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) -> 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, workspace: Option) -> 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 '"); + 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) -> 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(()) +} diff --git a/crates/onyx-cli/src/commands/mod.rs b/crates/onyx-cli/src/commands/mod.rs new file mode 100644 index 0000000..c681294 --- /dev/null +++ b/crates/onyx-cli/src/commands/mod.rs @@ -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 { + 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) -> 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)) +} diff --git a/crates/onyx-cli/src/commands/sync.rs b/crates/onyx-cli/src/commands/sync.rs new file mode 100644 index 0000000..234062d --- /dev/null +++ b/crates/onyx-cli/src/commands/sync.rs @@ -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) -> 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) -> 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, 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 { + 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()) +} diff --git a/crates/onyx-cli/src/commands/task.rs b/crates/onyx-cli/src/commands/task.rs new file mode 100644 index 0000000..16ad54d --- /dev/null +++ b/crates/onyx-cli/src/commands/task.rs @@ -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, due_str: Option, workspace: Option) -> 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 '"); + } + + // 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) -> 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) -> 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) -> 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> { + // 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)") +} diff --git a/crates/onyx-cli/src/commands/workspace.rs b/crates/onyx-cli/src/commands/workspace.rs new file mode 100644 index 0000000..4467752 --- /dev/null +++ b/crates/onyx-cli/src/commands/workspace.rs @@ -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(()) +} diff --git a/crates/onyx-cli/src/main.rs b/crates/onyx-cli/src/main.rs new file mode 100644 index 0000000..7b1e3a8 --- /dev/null +++ b/crates/onyx-cli/src/main.rs @@ -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, + /// Due date (ISO 8601 format: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS) + #[arg(short, long)] + due: Option, + /// Workspace to use + #[arg(short, long)] + workspace: Option, + }, + + /// Mark a task as complete + Complete { + /// Task ID + task_id: String, + /// Workspace to use + #[arg(short, long)] + workspace: Option, + }, + + /// Delete a task + Delete { + /// Task ID + task_id: String, + /// Workspace to use + #[arg(short, long)] + workspace: Option, + }, + + /// Edit a task + Edit { + /// Task ID + task_id: String, + /// Workspace to use + #[arg(short, long)] + workspace: Option, + }, + + /// 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, + }, +} + +#[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, + }, + + /// Show all tasks (or tasks in a specific list) + Show { + /// Name of the list to show + #[arg(short, long)] + list: Option, + /// Workspace to use + #[arg(short, long)] + workspace: Option, + }, + + /// Delete a task list + Delete { + /// Name of the list to delete + name: String, + /// Workspace to use + #[arg(short, long)] + workspace: Option, + }, +} + +#[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, + }, + + /// 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, + }, +} + +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(()) +} diff --git a/crates/onyx-cli/src/output.rs b/crates/onyx-cli/src/output.rs new file mode 100644 index 0000000..dd49031 --- /dev/null +++ b/crates/onyx-cli/src/output.rs @@ -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!(); +} From c536250072c5bdfd469984a2a8f656474e6856eb Mon Sep 17 00:00:00 2001 From: Tristan Michael Date: Tue, 31 Mar 2026 09:47:07 -0700 Subject: [PATCH 3/5] rename Tauri app from bevy-tasks to onyx --- apps/tauri/index.html | 2 +- apps/tauri/package-lock.json | 4 ++-- apps/tauri/package.json | 2 +- apps/tauri/src-tauri/Cargo.lock | 6 +++--- apps/tauri/src-tauri/Cargo.toml | 10 +++++----- apps/tauri/src-tauri/capabilities/default.json | 2 +- apps/tauri/src-tauri/src/lib.rs | 4 ++-- apps/tauri/src-tauri/src/main.rs | 2 +- apps/tauri/src-tauri/tauri.conf.json | 6 +++--- apps/tauri/src/lib/screens/SetupScreen.svelte | 2 +- 10 files changed, 20 insertions(+), 20 deletions(-) diff --git a/apps/tauri/index.html b/apps/tauri/index.html index 53b3460..72ab8a2 100644 --- a/apps/tauri/index.html +++ b/apps/tauri/index.html @@ -3,7 +3,7 @@ - Bevy Tasks + Onyx
diff --git a/apps/tauri/package-lock.json b/apps/tauri/package-lock.json index 27a6f67..34449bf 100644 --- a/apps/tauri/package-lock.json +++ b/apps/tauri/package-lock.json @@ -1,11 +1,11 @@ { - "name": "bevy-tasks", + "name": "onyx", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "bevy-tasks", + "name": "onyx", "version": "0.1.0", "dependencies": { "@tauri-apps/api": "^2.0.0", diff --git a/apps/tauri/package.json b/apps/tauri/package.json index 7229dab..c812985 100644 --- a/apps/tauri/package.json +++ b/apps/tauri/package.json @@ -1,5 +1,5 @@ { - "name": "bevy-tasks", + "name": "onyx", "private": true, "version": "0.1.0", "type": "module", diff --git a/apps/tauri/src-tauri/Cargo.lock b/apps/tauri/src-tauri/Cargo.lock index 8b570c1..2bab9cb 100644 --- a/apps/tauri/src-tauri/Cargo.lock +++ b/apps/tauri/src-tauri/Cargo.lock @@ -95,7 +95,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] -name = "bevy-tasks-core" +name = "onyx-core" version = "0.1.0" dependencies = [ "chrono", @@ -112,10 +112,10 @@ dependencies = [ ] [[package]] -name = "bevy-tasks-tauri" +name = "onyx-tauri" version = "0.1.0" dependencies = [ - "bevy-tasks-core", + "onyx-core", "chrono", "serde", "serde_json", diff --git a/apps/tauri/src-tauri/Cargo.toml b/apps/tauri/src-tauri/Cargo.toml index e4073e9..be4977a 100644 --- a/apps/tauri/src-tauri/Cargo.toml +++ b/apps/tauri/src-tauri/Cargo.toml @@ -1,13 +1,13 @@ [package] -name = "bevy-tasks-tauri" +name = "onyx-tauri" version = "0.1.0" edition = "2021" -description = "Tauri v2 desktop GUI for Bevy Tasks" +description = "Tauri v2 desktop GUI for Onyx" license = "GPL-3.0-or-later" -repository = "https://github.com/SteelDynamite/bevy-tasks" +repository = "https://github.com/SteelDynamite/onyx" [lib] -name = "bevy_tasks_tauri_lib" +name = "onyx_tauri_lib" crate-type = ["staticlib", "cdylib", "rlib"] [build-dependencies] @@ -19,7 +19,7 @@ tauri-plugin-dialog = "2" tauri-plugin-os = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" -bevy-tasks-core = { path = "../../../crates/bevy-tasks-core" } +onyx-core = { path = "../../../crates/onyx-core" } tokio = { version = "1", features = ["full"] } uuid = { version = "1", features = ["serde", "v4"] } chrono = { version = "0.4", features = ["serde"] } diff --git a/apps/tauri/src-tauri/capabilities/default.json b/apps/tauri/src-tauri/capabilities/default.json index e08700a..b21cc4f 100644 --- a/apps/tauri/src-tauri/capabilities/default.json +++ b/apps/tauri/src-tauri/capabilities/default.json @@ -1,6 +1,6 @@ { "identifier": "default", - "description": "Default capabilities for Bevy Tasks", + "description": "Default capabilities for Onyx", "windows": ["main"], "permissions": [ "core:default", diff --git a/apps/tauri/src-tauri/src/lib.rs b/apps/tauri/src-tauri/src/lib.rs index 9c2ed51..85c95c2 100644 --- a/apps/tauri/src-tauri/src/lib.rs +++ b/apps/tauri/src-tauri/src/lib.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; use tauri::State; use uuid::Uuid; -use bevy_tasks_core::{ +use onyx_core::{ config::{AppConfig, WorkspaceConfig}, models::{Task, TaskList, TaskStatus}, repository::TaskRepository, @@ -321,7 +321,7 @@ async fn test_webdav_connection( username: String, password: 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 .test_connection() .await diff --git a/apps/tauri/src-tauri/src/main.rs b/apps/tauri/src-tauri/src/main.rs index 27e0a06..486465c 100644 --- a/apps/tauri/src-tauri/src/main.rs +++ b/apps/tauri/src-tauri/src/main.rs @@ -2,5 +2,5 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] fn main() { - bevy_tasks_tauri_lib::run() + onyx_tauri_lib::run() } diff --git a/apps/tauri/src-tauri/tauri.conf.json b/apps/tauri/src-tauri/tauri.conf.json index ad02de7..90e4c75 100644 --- a/apps/tauri/src-tauri/tauri.conf.json +++ b/apps/tauri/src-tauri/tauri.conf.json @@ -1,8 +1,8 @@ { "$schema": "https://raw.githubusercontent.com/nicegui-org/nicegui/v2/tauri-conf-schema.json", - "productName": "Bevy Tasks", + "productName": "Onyx", "version": "0.1.0", - "identifier": "com.bevytasks.app", + "identifier": "com.onyx.app", "build": { "frontendDist": "../dist", "devUrl": "http://localhost:1422", @@ -13,7 +13,7 @@ "withGlobalTauri": false, "windows": [ { - "title": "Bevy Tasks", + "title": "Onyx", "width": 400, "height": 700, "minWidth": 320, diff --git a/apps/tauri/src/lib/screens/SetupScreen.svelte b/apps/tauri/src/lib/screens/SetupScreen.svelte index 4e4f148..7bf8df7 100644 --- a/apps/tauri/src/lib/screens/SetupScreen.svelte +++ b/apps/tauri/src/lib/screens/SetupScreen.svelte @@ -20,7 +20,7 @@
-

Bevy Tasks

+

Onyx

Create or open a workspace to get started.

From aca444a2745e50f7ae292aa4f4b432d48c7b7bdb Mon Sep 17 00:00:00 2001 From: Tristan Michael Date: Tue, 31 Mar 2026 09:47:13 -0700 Subject: [PATCH 4/5] rename Flutter app from bevy_tasks to onyx --- apps/flutter/README.md | 2 +- apps/flutter/lib/main.dart | 8 ++++---- apps/flutter/lib/src/rust/frb_generated.dart | 2 +- apps/flutter/lib/src/screens/setup_screen.dart | 2 +- apps/flutter/linux/CMakeLists.txt | 6 +++--- apps/flutter/linux/runner/my_application.cc | 2 +- apps/flutter/pubspec.yaml | 4 ++-- apps/flutter/rust/Cargo.lock | 6 +++--- apps/flutter/rust/Cargo.toml | 4 ++-- apps/flutter/rust/src/api.rs | 2 +- apps/flutter/test/widget_test.dart | 2 +- apps/flutter/windows/CMakeLists.txt | 4 ++-- apps/flutter/windows/runner/Runner.rc | 8 ++++---- apps/flutter/windows/runner/main.cpp | 2 +- 14 files changed, 27 insertions(+), 27 deletions(-) diff --git a/apps/flutter/README.md b/apps/flutter/README.md index d72b258..075c78a 100644 --- a/apps/flutter/README.md +++ b/apps/flutter/README.md @@ -1,4 +1,4 @@ -# bevy_tasks +# onyx A new Flutter project. diff --git a/apps/flutter/lib/main.dart b/apps/flutter/lib/main.dart index 86d663e..a9a5683 100644 --- a/apps/flutter/lib/main.dart +++ b/apps/flutter/lib/main.dart @@ -32,19 +32,19 @@ Future main() async { runApp( ChangeNotifierProvider( create: (_) => AppState()..loadConfig(), - child: const BevyTasksApp(), + child: const OnyxApp(), ), ); } -class BevyTasksApp extends StatelessWidget { - const BevyTasksApp({super.key}); +class OnyxApp extends StatelessWidget { + const OnyxApp({super.key}); @override Widget build(BuildContext context) { final state = context.watch(); return MaterialApp( - title: 'Bevy Tasks', + title: 'Onyx', debugShowCheckedModeBanner: false, theme: AppTheme.light(), darkTheme: AppTheme.dark(), diff --git a/apps/flutter/lib/src/rust/frb_generated.dart b/apps/flutter/lib/src/rust/frb_generated.dart index 94a1a28..372955b 100644 --- a/apps/flutter/lib/src/rust/frb_generated.dart +++ b/apps/flutter/lib/src/rust/frb_generated.dart @@ -68,7 +68,7 @@ class RustLib extends BaseEntrypoint { static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( - stem: 'bevy_tasks_flutter', + stem: 'onyx_flutter', ioDirectory: 'rust/target/release/', webPrefix: 'pkg/', ); diff --git a/apps/flutter/lib/src/screens/setup_screen.dart b/apps/flutter/lib/src/screens/setup_screen.dart index 9da106b..ada62d7 100644 --- a/apps/flutter/lib/src/screens/setup_screen.dart +++ b/apps/flutter/lib/src/screens/setup_screen.dart @@ -52,7 +52,7 @@ class _SetupScreenState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Bevy Tasks', + Text('Onyx', style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700, color: isDark ? AppTheme.textDark : AppTheme.textLight)), const SizedBox(height: 4), diff --git a/apps/flutter/linux/CMakeLists.txt b/apps/flutter/linux/CMakeLists.txt index 69d23ed..685dbb5 100644 --- a/apps/flutter/linux/CMakeLists.txt +++ b/apps/flutter/linux/CMakeLists.txt @@ -4,10 +4,10 @@ project(runner LANGUAGES CXX) # The name of the executable created for the application. Change this to change # 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: # 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 # versions of CMake. @@ -59,7 +59,7 @@ add_subdirectory("runner") # Build the Rust FFI library for flutter_rust_bridge 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") set(RUST_TARGET_DIR "${RUST_DIR}/target/debug") add_custom_command( diff --git a/apps/flutter/linux/runner/my_application.cc b/apps/flutter/linux/runner/my_application.cc index 4aed40e..e908bd6 100644 --- a/apps/flutter/linux/runner/my_application.cc +++ b/apps/flutter/linux/runner/my_application.cc @@ -27,7 +27,7 @@ static void my_application_activate(GApplication* application) { // Frameless transparent window 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); // Enable transparency diff --git a/apps/flutter/pubspec.yaml b/apps/flutter/pubspec.yaml index 5f95f96..b24d432 100644 --- a/apps/flutter/pubspec.yaml +++ b/apps/flutter/pubspec.yaml @@ -1,5 +1,5 @@ -name: bevy_tasks -description: "Bevy Tasks - local-first task management" +name: onyx +description: "Onyx - local-first task management" publish_to: 'none' version: 1.0.0+1 diff --git a/apps/flutter/rust/Cargo.lock b/apps/flutter/rust/Cargo.lock index cd7a59c..c8f4b3c 100644 --- a/apps/flutter/rust/Cargo.lock +++ b/apps/flutter/rust/Cargo.lock @@ -109,7 +109,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] -name = "bevy-tasks-core" +name = "onyx-core" version = "0.1.0" dependencies = [ "chrono", @@ -126,10 +126,10 @@ dependencies = [ ] [[package]] -name = "bevy-tasks-flutter" +name = "onyx-flutter" version = "0.1.0" dependencies = [ - "bevy-tasks-core", + "onyx-core", "chrono", "flutter_rust_bridge", "once_cell", diff --git a/apps/flutter/rust/Cargo.toml b/apps/flutter/rust/Cargo.toml index 4b60646..6f2f7d3 100644 --- a/apps/flutter/rust/Cargo.toml +++ b/apps/flutter/rust/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "bevy-tasks-flutter" +name = "onyx-flutter" version = "0.1.0" edition = "2021" @@ -8,7 +8,7 @@ crate-type = ["cdylib", "staticlib"] [dependencies] 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"] } chrono = { version = "0.4", features = ["serde"] } once_cell = "1" diff --git a/apps/flutter/rust/src/api.rs b/apps/flutter/rust/src/api.rs index ea22a54..20cda7e 100644 --- a/apps/flutter/rust/src/api.rs +++ b/apps/flutter/rust/src/api.rs @@ -4,7 +4,7 @@ use std::sync::Mutex; use once_cell::sync::Lazy; use uuid::Uuid; -use bevy_tasks_core::{ +use onyx_core::{ config::{AppConfig, WorkspaceConfig}, models::{Task, TaskList, TaskStatus}, repository::TaskRepository, diff --git a/apps/flutter/test/widget_test.dart b/apps/flutter/test/widget_test.dart index 470d712..0c63a1a 100644 --- a/apps/flutter/test/widget_test.dart +++ b/apps/flutter/test/widget_test.dart @@ -8,7 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:bevy_tasks/main.dart'; +import 'package:onyx/main.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { diff --git a/apps/flutter/windows/CMakeLists.txt b/apps/flutter/windows/CMakeLists.txt index 86a0586..94b4c4c 100644 --- a/apps/flutter/windows/CMakeLists.txt +++ b/apps/flutter/windows/CMakeLists.txt @@ -1,10 +1,10 @@ # Project-level configuration. 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 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 # versions of CMake. diff --git a/apps/flutter/windows/runner/Runner.rc b/apps/flutter/windows/runner/Runner.rc index b31e86e..59c146e 100644 --- a/apps/flutter/windows/runner/Runner.rc +++ b/apps/flutter/windows/runner/Runner.rc @@ -90,12 +90,12 @@ BEGIN BLOCK "040904e4" BEGIN VALUE "CompanyName", "com.example" "\0" - VALUE "FileDescription", "bevy_tasks" "\0" + VALUE "FileDescription", "onyx" "\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 "OriginalFilename", "bevy_tasks.exe" "\0" - VALUE "ProductName", "bevy_tasks" "\0" + VALUE "OriginalFilename", "onyx.exe" "\0" + VALUE "ProductName", "onyx" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0" END END diff --git a/apps/flutter/windows/runner/main.cpp b/apps/flutter/windows/runner/main.cpp index c0613b9..8789bfb 100644 --- a/apps/flutter/windows/runner/main.cpp +++ b/apps/flutter/windows/runner/main.cpp @@ -27,7 +27,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, FlutterWindow window(project); Win32Window::Point origin(10, 10); Win32Window::Size size(1280, 720); - if (!window.Create(L"bevy_tasks", origin, size)) { + if (!window.Create(L"onyx", origin, size)) { return EXIT_FAILURE; } window.SetQuitOnClose(true); From 13da81fae6375fe4656d6d009ac3a3001e178902 Mon Sep 17 00:00:00 2001 From: Tristan Michael Date: Tue, 31 Mar 2026 09:47:19 -0700 Subject: [PATCH 5/5] update project docs, workspace config, and lockfile for onyx rename --- CLAUDE.md | 16 +++--- Cargo.lock | 6 +-- Cargo.toml | 4 +- PLAN.md | 120 ++++++++++++++++++++++---------------------- README.md | 50 +++++++++--------- docs/API.md | 30 +++++------ docs/DEVELOPMENT.md | 36 ++++++------- 7 files changed, 131 insertions(+), 131 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index cd2cda7..88e4cd0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,16 +4,16 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## 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 ```bash 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 -p bevy-tasks-core # Run core library tests only -cargo run -p bevy-tasks-cli -- # Run CLI with arguments +cargo test -p onyx-core # Run core library tests only +cargo run -p onyx-cli -- # Run CLI with arguments # Tauri GUI 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 ``` -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`). @@ -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: -- **bevy-tasks-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`. -- **apps/tauri/** — Tauri v2 GUI. Svelte 5 frontend in `src/`, Rust backend in `src-tauri/` with Tauri commands that call into `bevy-tasks-core`. +- **onyx-core** — Pure Rust library. Storage trait with `FileSystemStorage` implementation, `TaskRepository` (main API), data models, config, error types. No CLI/UI dependencies. +- **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 `onyx-core`. ### Key patterns diff --git a/Cargo.lock b/Cargo.lock index f7268ff..d88b146 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -105,11 +105,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] -name = "bevy-tasks-cli" +name = "onyx-cli" version = "0.1.0" dependencies = [ "anyhow", - "bevy-tasks-core", + "onyx-core", "chrono", "clap", "colored", @@ -120,7 +120,7 @@ dependencies = [ ] [[package]] -name = "bevy-tasks-core" +name = "onyx-core" version = "0.1.0" dependencies = [ "chrono", diff --git a/Cargo.toml b/Cargo.toml index 068f1fa..3eda0db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [workspace] members = [ - "crates/bevy-tasks-core", - "crates/bevy-tasks-cli", + "crates/onyx-core", + "crates/onyx-cli", ] exclude = [ "apps/tauri/src-tauri", diff --git a/PLAN.md b/PLAN.md index 1241ee4..ed76fcc 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1,4 +1,4 @@ -# Bevy Tasks - Project Plan +# Onyx - Project Plan ## Vision @@ -41,15 +41,15 @@ A **local-first, cross-platform tasks application** inspired by Google Tasks. Bu #### Cargo Workspace Structure ``` -bevy-tasks/ +onyx/ ├── Cargo.toml # Workspace definition ├── PLAN.md ├── README.md ├── apps/ │ └── tauri/ # Tauri GUI (Svelte + Tailwind) ├── crates/ -│ ├── bevy-tasks-core/ # Core library (backend) -│ └── bevy-tasks-cli/ # CLI frontend +│ ├── onyx-core/ # Core library (backend) +│ └── onyx-cli/ # CLI frontend └── docs/ ``` @@ -162,9 +162,9 @@ WorkspaceConfig { - Tasks without due dates appear at the end when grouping is enabled **App Configuration** (separate from task data, supports multiple workspaces): -- Windows: `%APPDATA%/bevy-tasks/config.json` -- Linux: `~/.config/bevy-tasks/config.json` -- macOS: `~/Library/Application Support/bevy-tasks/config.json` +- Windows: `%APPDATA%/onyx/config.json` +- Linux: `~/.config/onyx/config.json` +- macOS: `~/Library/Application Support/onyx/config.json` ```json { @@ -226,8 +226,8 @@ pub trait Storage { ```toml [workspace] members = [ - "crates/bevy-tasks-core", - "crates/bevy-tasks-cli", + "crates/onyx-core", + "crates/onyx-cli", ] exclude = [ "apps/tauri/src-tauri", @@ -242,10 +242,10 @@ anyhow = "1.0" tokio = { version = "1.40", features = ["full"] } ``` -**bevy-tasks-core/Cargo.toml**: +**onyx-core/Cargo.toml**: ```toml [package] -name = "bevy-tasks-core" +name = "onyx-core" version = "0.1.0" edition = "2021" @@ -261,19 +261,19 @@ directories = "5.0" tempfile = "3.0" ``` -**bevy-tasks-cli/Cargo.toml**: +**onyx-cli/Cargo.toml**: ```toml [package] -name = "bevy-tasks-cli" +name = "onyx-cli" version = "0.1.0" edition = "2021" [[bin]] -name = "bevy-tasks" +name = "onyx" path = "src/main.rs" [dependencies] -bevy-tasks-core = { path = "../bevy-tasks-core" } +onyx-core = { path = "../onyx-core" } clap = { version = "4.5", features = ["derive", "env"] } colored = "2.0" anyhow = { workspace = true } @@ -310,46 +310,46 @@ fs_extra = "1.3" ```bash # 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 ✓ Created default list "My Tasks" ✓ Set "personal" as current workspace # 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 ✓ Created default list "My Tasks" # List all workspaces -$ bevy-tasks workspace list +$ onyx workspace list personal: ~/Documents/Tasks (current) shared: ~/Dropbox/TeamTasks # Switch between workspaces -$ bevy-tasks workspace switch shared +$ onyx workspace switch shared ✓ Switched to workspace "shared" # Create a new task list -$ bevy-tasks list create "Work" +$ onyx list create "Work" ✓ Created list "Work" -$ bevy-tasks list create "Personal Projects" +$ onyx list create "Personal Projects" ✓ Created list "Personal Projects" # 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) -$ 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) Due: 2026-11-15 # Or specify workspace explicitly -$ bevy-tasks add "Team meeting" --workspace shared +$ onyx add "Team meeting" --workspace shared ✓ Created task "Team meeting" in workspace "shared" # List all tasks (from current workspace) -$ bevy-tasks list show +$ onyx list show My Tasks (3 tasks) [ ] Buy groceries [ ] Call dentist @@ -360,37 +360,37 @@ Work (2 tasks) [ ] Team meeting prep # List tasks from specific workspace -$ bevy-tasks list show --workspace shared +$ onyx list show --workspace shared Shared Tasks (2 tasks) [ ] Team meeting [ ] Quarterly planning # List tasks in specific list -$ bevy-tasks list show --list "Work" +$ onyx list show --list "Work" Work (2 tasks) [ ] Review PR #123 (due: 2026-11-15) [ ] Team meeting prep # Complete a task -$ bevy-tasks complete 550e8400-e29b-41d4-a716-446655440000 +$ onyx complete 550e8400-e29b-41d4-a716-446655440000 ✓ Completed task "Buy groceries" # 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 # User edits and saves, then exits editor ✓ Updated task "Review PR #123" # Delete a task -$ bevy-tasks delete 550e8400-e29b-41d4-a716-446655440000 +$ onyx delete 550e8400-e29b-41d4-a716-446655440000 ✓ Deleted task "Buy groceries" # 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 # 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 Continue? (y/n): y Moving files... @@ -401,22 +401,22 @@ Moving files... ✓ Workspace "personal" now points to ~/Dropbox/Tasks # Remove a workspace -$ bevy-tasks workspace remove shared +$ onyx workspace remove shared ⚠ Warning: This will delete workspace config (files remain on disk) Continue? (y/n): y ✓ Removed workspace "shared" # 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" -$ bevy-tasks group disable --list "Personal" +$ onyx group disable --list "Personal" ✓ Disabled group-by-due-date for list "Personal" ``` ### Deliverables -- [x] `bevy-tasks-core` library with stable API +- [x] `onyx-core` library with stable API - [x] Functional CLI that can manage tasks - [x] Data persists as Obsidian-compatible .md files - [x] Well-tested backend (>80% coverage) @@ -427,17 +427,17 @@ $ bevy-tasks group disable --list "Personal" ```bash # Clone and build git clone -cd bevy-tasks +cd onyx cargo build # Run tests -cargo test -p bevy-tasks-core +cargo test -p onyx-core # Run CLI -cargo run -p bevy-tasks-cli -- init ~/test-tasks --name test -cargo run -p bevy-tasks-cli -- add "Test task" -cargo run -p bevy-tasks-cli -- list -cargo run -p bevy-tasks-cli -- workspace list +cargo run -p onyx-cli -- init ~/test-tasks --name test +cargo run -p onyx-cli -- add "Test task" +cargo run -p onyx-cli -- list +cargo run -p onyx-cli -- workspace list ``` --- @@ -450,7 +450,7 @@ cargo run -p bevy-tasks-cli -- workspace list #### WebDAV Integration -Add WebDAV support to `bevy-tasks-core`: +Add WebDAV support to `onyx-core`: ```rust // Update WorkspaceConfig to include WebDAV @@ -466,7 +466,7 @@ AppConfig { current_workspace: Option, } -// 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( workspace_path: &Path, webdav_url: &str, @@ -477,7 +477,7 @@ pub async fn sync_workspace( pub fn get_sync_status(workspace_path: &Path) -> Result; -// 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 load_credentials(domain: &str) -> Result<(String, String)>; pub fn delete_credentials(domain: &str) -> Result<()>; @@ -492,14 +492,14 @@ pub fn delete_credentials(domain: &str) -> Result<()>; **Primary**: Platform Keychain via `keyring` crate - 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) **Fallback**: Encrypted local storage if keychain unavailable ### Dependencies -Add to `bevy-tasks-core/Cargo.toml`: +Add to `onyx-core/Cargo.toml`: ```toml reqwest = { version = "0.12", features = ["json", "rustls-tls"] } keyring = "3.0" @@ -523,7 +523,7 @@ keyring = "3.0" ```bash # Setup WebDAV for current workspace -$ bevy-tasks sync --setup +$ onyx sync --setup WebDAV URL: https://nextcloud.example.com/remote.php/dav/files/username/Tasks Username: myuser Password: ******** @@ -531,7 +531,7 @@ Password: ******** ✓ Connection verified for workspace "personal" # 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 Username: myuser Password: ******** @@ -539,7 +539,7 @@ Password: ******** ✓ Connection verified for workspace "shared" # Push local changes to WebDAV server (current workspace) -$ bevy-tasks sync --push +$ onyx sync --push Syncing workspace "personal" to https://nextcloud.example.com/... Uploading My Tasks/.listdata.json Uploading My Tasks/Buy groceries.md @@ -547,14 +547,14 @@ Syncing workspace "personal" to https://nextcloud.example.com/... ✓ Pushed 3 files to WebDAV server # Pull changes from WebDAV server -$ bevy-tasks sync --pull +$ onyx sync --pull Syncing workspace "personal" from https://nextcloud.example.com/... Downloading Work/Team meeting notes.md Downloading Personal/Call mom.md ✓ Pulled 2 files from WebDAV server # Automatic two-way sync -$ bevy-tasks sync +$ onyx sync Syncing workspace "personal" with https://nextcloud.example.com/... ↑ Uploading My Tasks/New task.md ↓ Downloading Work/Updated task.md @@ -562,12 +562,12 @@ Syncing workspace "personal" with https://nextcloud.example.com/... ✓ Sync complete # Sync specific workspace -$ bevy-tasks sync --workspace shared +$ onyx sync --workspace shared Syncing workspace "shared" with https://nextcloud.example.com/... ✓ Sync complete (no changes) # Check sync status for current workspace -$ bevy-tasks sync --status +$ onyx sync --status Workspace: personal WebDAV Server: https://nextcloud.example.com/remote.php/dav/files/username/Tasks Status: Connected @@ -576,7 +576,7 @@ Local changes: 2 files modified Remote changes: 0 files modified # Check sync status for all workspaces -$ bevy-tasks sync --status --all +$ onyx sync --status --all Workspace: personal WebDAV: https://nextcloud.example.com/.../Tasks Status: Connected @@ -608,7 +608,7 @@ Workspace: shared **Decision**: Use Tauri v2 with Svelte and Tailwind for the GUI **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 - Tailwind CSS 4 for rapid, consistent styling - Small binary size (~5-10MB) @@ -715,11 +715,11 @@ WorkspaceConfig { - [x] Settings popup overlay (WebDAV config, dark mode toggle) - [x] Dark mode (GNOME-style neutral theme, cyan-blue accent) - [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` — 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) - [ ] 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) - [ ] Sync status indicators (per workspace) - [ ] Push/pull sync mode selection @@ -765,7 +765,7 @@ Tauri v2 supports iOS and Android natively. The same Svelte frontend and Rust ba **iOS**: - Tauri generates Xcode project -- Bundle identifier: `com.bevytasks.app` +- Bundle identifier: `com.onyx.app` - Target: `aarch64-apple-ios` **Android**: @@ -952,7 +952,7 @@ If you want game-like polish after Phase 7: - Migrate GUI from Tauri/Svelte to Bevy - Full control over animations and rendering - Unique, polished look beyond standard apps -- Backend (`bevy-tasks-core`) stays identical +- Backend (`onyx-core`) stays identical - Only rewrite the GUI layer ### Deliverables diff --git a/README.md b/README.md index 6071552..8e9a52d 100644 --- a/README.md +++ b/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. @@ -12,13 +12,13 @@ A **local-first, cross-platform tasks application** built with Rust. Inspired by ## Project Structure ``` -bevy-tasks/ +onyx/ ├── Cargo.toml # Workspace definition ├── PLAN.md # Detailed project plan ├── README.md # This file ├── crates/ -│ ├── bevy-tasks-core/ # Core library (backend) -│ └── bevy-tasks-cli/ # CLI frontend +│ ├── onyx-core/ # Core library (backend) +│ └── onyx-cli/ # CLI frontend ├── apps/ │ └── tauri/ # Tauri v2 GUI (Svelte 5 + Tailwind CSS 4) └── docs/ @@ -30,7 +30,7 @@ bevy-tasks/ - **Phase 2** (WebDAV Sync): Backend and CLI complete, GUI partially wired - **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) - Markdown file I/O with YAML frontmatter - Local storage with repository pattern @@ -39,7 +39,7 @@ bevy-tasks/ - WebDAV sync with three-way diff and offline queue - Platform keychain credential storage -### CLI (`bevy-tasks-cli`) +### CLI (`onyx-cli`) - Workspace management (init, add, list, switch, remove, retarget, migrate) - Task list management (create, show, delete) - Task operations (add, complete, delete, edit) @@ -66,15 +66,15 @@ bevy-tasks/ ```bash # Clone and build -git clone https://github.com/SteelDynamite/bevy-tasks.git -cd bevy-tasks +git clone https://github.com/SteelDynamite/onyx.git +cd onyx cargo build # Run tests -cargo test -p bevy-tasks-core +cargo test -p onyx-core # Run CLI -cargo run -p bevy-tasks-cli -- --help +cargo run -p onyx-cli -- --help # Run Tauri GUI cd apps/tauri && npm install @@ -87,7 +87,7 @@ npm run tauri dev ```bash # 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: # - A workspace named "personal" at ~/Documents/Tasks @@ -99,51 +99,51 @@ cargo run -p bevy-tasks-cli -- init ~/Documents/Tasks --name personal ```bash # 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 -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 -cargo run -p bevy-tasks-cli -- list show +cargo run -p onyx-cli -- list show # Complete a task -cargo run -p bevy-tasks-cli -- complete +cargo run -p onyx-cli -- complete # Edit a task (opens in $EDITOR) -cargo run -p bevy-tasks-cli -- edit +cargo run -p onyx-cli -- edit # Delete a task -cargo run -p bevy-tasks-cli -- delete +cargo run -p onyx-cli -- delete ``` ### Manage workspaces ```bash # 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 -cargo run -p bevy-tasks-cli -- workspace list +cargo run -p onyx-cli -- workspace list # 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 -cargo run -p bevy-tasks-cli -- add "Team meeting" --workspace shared +cargo run -p onyx-cli -- add "Team meeting" --workspace shared ``` ### Manage task lists ```bash # 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 -cargo run -p bevy-tasks-cli -- list show --list "Work" +cargo run -p onyx-cli -- list show --list "Work" # Delete a list -cargo run -p bevy-tasks-cli -- list delete "Work" +cargo run -p onyx-cli -- list delete "Work" ``` ## Data Format @@ -190,7 +190,7 @@ Run the test suite: cargo test # Run tests for specific crate -cargo test -p bevy-tasks-core +cargo test -p onyx-core # Run tests with output cargo test -- --nocapture diff --git a/docs/API.md b/docs/API.md index 3da9156..f3aa738 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1,8 +1,8 @@ -# Bevy Tasks Core - API Documentation +# Onyx Core - API Documentation ## 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 @@ -33,7 +33,7 @@ pub enum TaskStatus { **Creating a Task:** ```rust -use bevy_tasks_core::Task; +use onyx_core::Task; // Simple task let task = Task::new("Buy groceries".to_string()); @@ -73,14 +73,14 @@ pub struct AppConfig { ``` **Location:** -- Windows: `%APPDATA%/bevy-tasks/config.json` -- Linux: `~/.config/bevy-tasks/config.json` -- macOS: `~/Library/Application Support/bevy-tasks/config.json` +- Windows: `%APPDATA%/onyx/config.json` +- Linux: `~/.config/onyx/config.json` +- macOS: `~/Library/Application Support/onyx/config.json` **Usage:** ```rust -use bevy_tasks_core::AppConfig; +use onyx_core::AppConfig; // Load config let config_path = AppConfig::get_config_path(); @@ -116,7 +116,7 @@ The main interface for interacting with tasks and lists. ### Initialization ```rust -use bevy_tasks_core::TaskRepository; +use onyx_core::TaskRepository; use std::path::PathBuf; // Open existing repository @@ -280,12 +280,12 @@ The sync module provides bi-directional WebDAV synchronization with three-way di ### 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 ```rust -use bevy_tasks_core::sync::{sync_workspace, SyncMode}; +use onyx_core::sync::{sync_workspace, SyncMode}; use std::path::Path; // Full bi-directional sync @@ -305,7 +305,7 @@ sync_workspace(path, url, user, pass, SyncMode::PullOnly).await?; #### Check Sync Status ```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"))?; // 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). ```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("nextcloud.example.com", "username", "password")?; @@ -331,7 +331,7 @@ delete_credentials("nextcloud.example.com")?; ### WebDAV Client ```rust -use bevy_tasks_core::webdav::WebDavClient; +use onyx_core::webdav::WebDavClient; let client = WebDavClient::new( "https://nextcloud.example.com/remote.php/dav/files/user/Tasks", @@ -380,7 +380,7 @@ pub enum Error { ## Example: Complete Workflow ```rust -use bevy_tasks_core::{TaskRepository, Task, AppConfig, WorkspaceConfig}; +use onyx_core::{TaskRepository, Task, AppConfig, WorkspaceConfig}; use std::path::PathBuf; fn main() -> Result<(), Box> { @@ -428,7 +428,7 @@ fn main() -> Result<(), Box> { The core library includes comprehensive tests. Run them with: ```bash -cargo test -p bevy-tasks-core +cargo test -p onyx-core ``` Key test areas: diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 293e46d..d66428d 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -13,8 +13,8 @@ ```bash # Clone the repository -git clone https://github.com/SteelDynamite/bevy-tasks.git -cd bevy-tasks +git clone https://github.com/SteelDynamite/onyx.git +cd onyx # Build the project cargo build @@ -23,7 +23,7 @@ cargo build cargo test # Run the CLI -cargo run -p bevy-tasks-cli -- --help +cargo run -p onyx-cli -- --help # Run the Tauri GUI cd apps/tauri && npm install @@ -33,10 +33,10 @@ npm run tauri dev ## Project Structure ``` -bevy-tasks/ +onyx/ ├── Cargo.toml # Workspace manifest ├── crates/ -│ ├── bevy-tasks-core/ # Core library +│ ├── onyx-core/ # Core library │ │ ├── src/ │ │ │ ├── lib.rs # Library entry point │ │ │ ├── models.rs # Data models (Task, TaskList, etc.) @@ -47,7 +47,7 @@ bevy-tasks/ │ │ │ ├── sync.rs # Three-way sync engine with offline queue │ │ │ └── webdav.rs # WebDAV client and credential storage │ │ └── Cargo.toml -│ ├── bevy-tasks-cli/ # CLI application +│ ├── onyx-cli/ # CLI application │ │ ├── src/ │ │ │ ├── main.rs # CLI entry point and command parsing │ │ │ ├── output.rs # Output formatting utilities @@ -95,10 +95,10 @@ bevy-tasks/ cargo test # Run tests for a specific crate -cargo test -p bevy-tasks-core +cargo test -p onyx-core # 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 cargo test -- --nocapture @@ -114,17 +114,17 @@ cargo build cargo build --release # Build specific crate -cargo build -p bevy-tasks-cli +cargo build -p onyx-cli ``` ### Running the CLI in Development ```bash # 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 -./target/debug/bevy-tasks init ~/test-tasks --name test +./target/debug/onyx init ~/test-tasks --name test ``` ## Code Style @@ -155,7 +155,7 @@ cargo clippy -- -W clippy::all ## Architecture Guidelines -### Core Library (`bevy-tasks-core`) +### Core Library (`onyx-core`) **Principles:** - Pure Rust, no CLI dependencies @@ -171,7 +171,7 @@ cargo clippy -- -W clippy::all 4. Write tests 5. Update API documentation -### CLI (`bevy-tasks-cli`) +### CLI (`onyx-cli`) **Principles:** - Thin layer over core library @@ -210,8 +210,8 @@ mod tests { Located in `tests/` directories within each crate: ```rust -// crates/bevy-tasks-core/tests/integration_test.rs -use bevy_tasks_core::*; +// crates/onyx-core/tests/integration_test.rs +use onyx_core::*; #[test] fn test_full_workflow() { @@ -321,13 +321,13 @@ ls -la ~/test-tasks ```bash # Verify workspace configuration -cat ~/.config/bevy-tasks/config.json | jq +cat ~/.config/onyx/config.json | jq # Check current workspace -cargo run -p bevy-tasks-cli -- workspace list +cargo run -p onyx-cli -- workspace list # 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