From c4c03679ae2bc215582b696539873e4e2b35d3a7 Mon Sep 17 00:00:00 2001 From: Tristan Michael Date: Tue, 31 Mar 2026 13:27:47 -0700 Subject: [PATCH 1/7] feat(core): add move_task and rename_list to onyx-core Add TaskRepository::move_task() to move tasks between lists and rename_list() to rename lists on the filesystem. Adds rename_list to the Storage trait with FileSystemStorage implementation. Includes tests for both operations plus duplicate-name error case. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/onyx-core/src/repository.rs | 59 ++++++++++++++++++++++++++++++ crates/onyx-core/src/storage.rs | 23 ++++++++++++ 2 files changed, 82 insertions(+) diff --git a/crates/onyx-core/src/repository.rs b/crates/onyx-core/src/repository.rs index d8492f3..3ba3ea2 100644 --- a/crates/onyx-core/src/repository.rs +++ b/crates/onyx-core/src/repository.rs @@ -68,6 +68,17 @@ impl TaskRepository { self.storage.delete_list(list_id) } + pub fn rename_list(&mut self, list_id: Uuid, new_name: String) -> Result<()> { + self.storage.rename_list(list_id, new_name) + } + + pub fn move_task(&mut self, from_list_id: Uuid, to_list_id: Uuid, task_id: Uuid) -> Result<()> { + let task = self.storage.read_task(from_list_id, task_id)?; + self.storage.write_task(to_list_id, &task)?; + self.storage.delete_task(from_list_id, task_id)?; + Ok(()) + } + // Task ordering pub fn reorder_task(&mut self, list_id: Uuid, task_id: Uuid, new_position: usize) -> Result<()> { let mut metadata = self.storage.read_list_metadata(list_id)?; @@ -320,6 +331,54 @@ mod tests { assert!(lists.is_empty()); } + #[test] + fn test_move_task_between_lists() { + let temp_dir = TempDir::new().unwrap(); + let mut repo = TaskRepository::init(temp_dir.path().to_path_buf()).unwrap(); + + let list_a = repo.create_list("List A".to_string()).unwrap(); + let list_b = repo.create_list("List B".to_string()).unwrap(); + let task = repo.create_task(list_a.id, Task::new("Movable".to_string())).unwrap(); + + repo.move_task(list_a.id, list_b.id, task.id).unwrap(); + + let tasks_a = repo.list_tasks(list_a.id).unwrap(); + assert_eq!(tasks_a.len(), 0); + + let tasks_b = repo.list_tasks(list_b.id).unwrap(); + assert_eq!(tasks_b.len(), 1); + assert_eq!(tasks_b[0].title, "Movable"); + } + + #[test] + fn test_rename_list() { + let temp_dir = TempDir::new().unwrap(); + let mut repo = TaskRepository::init(temp_dir.path().to_path_buf()).unwrap(); + + let list = repo.create_list("Old Name".to_string()).unwrap(); + repo.rename_list(list.id, "New Name".to_string()).unwrap(); + + let renamed = repo.get_list(list.id).unwrap(); + assert_eq!(renamed.title, "New Name"); + + // Old directory should be gone + assert!(!temp_dir.path().join("Old Name").exists()); + assert!(temp_dir.path().join("New Name").exists()); + } + + #[test] + fn test_rename_list_duplicate_name() { + let temp_dir = TempDir::new().unwrap(); + let mut repo = TaskRepository::init(temp_dir.path().to_path_buf()).unwrap(); + + repo.create_list("A".to_string()).unwrap(); + let list_b = repo.create_list("B".to_string()).unwrap(); + + let result = repo.rename_list(list_b.id, "A".to_string()); + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), Error::InvalidData(_))); + } + #[test] fn test_delete_list_removes_from_root_metadata() { let temp_dir = TempDir::new().unwrap(); diff --git a/crates/onyx-core/src/storage.rs b/crates/onyx-core/src/storage.rs index e6c6a4b..2fda73a 100644 --- a/crates/onyx-core/src/storage.rs +++ b/crates/onyx-core/src/storage.rs @@ -88,6 +88,8 @@ pub trait Storage { fn read_root_metadata(&self) -> Result; fn write_root_metadata(&mut self, metadata: &RootMetadata) -> Result<()>; + fn rename_list(&mut self, list_id: Uuid, new_name: String) -> Result<()>; + fn read_list_metadata(&self, list_id: Uuid) -> Result; fn write_list_metadata(&mut self, metadata: &ListMetadata) -> Result<()>; } @@ -464,6 +466,27 @@ impl Storage for FileSystemStorage { Ok(()) } + fn rename_list(&mut self, list_id: Uuid, new_name: String) -> Result<()> { + let old_dir = self.list_dir_path(list_id)?; + let new_dir = self.list_dir_path_by_name(&new_name); + + if new_dir.exists() { + return Err(Error::InvalidData(format!("A list named '{}' already exists", new_name))); + } + + fs::rename(&old_dir, &new_dir)?; + + // Update metadata timestamp + let metadata_path = new_dir.join(".listdata.json"); + let content = fs::read_to_string(&metadata_path)?; + let mut metadata: ListMetadata = serde_json::from_str(&content)?; + metadata.updated_at = Utc::now(); + let json = serde_json::to_string_pretty(&metadata)?; + fs::write(&metadata_path, json)?; + + Ok(()) + } + fn read_root_metadata(&self) -> Result { self.read_root_metadata_internal() } From 5faf285d28319ede6dc476aabdb4e2a6655da4a6 Mon Sep 17 00:00:00 2001 From: Tristan Michael Date: Tue, 31 Mar 2026 13:27:56 -0700 Subject: [PATCH 2/7] feat(tauri): add move, rename, grouping commands and file watcher Add Tauri commands: move_task, rename_list, set/get_group_by_due_date, watch_workspace. Implement file watcher using notify crate with 500ms debounce and self-change suppression via mute_watcher(). Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/tauri/src-tauri/Cargo.lock | 213 ++++++++++++++++++++++++++------ apps/tauri/src-tauri/Cargo.toml | 2 + apps/tauri/src-tauri/src/lib.rs | 148 +++++++++++++++++++++- 3 files changed, 327 insertions(+), 36 deletions(-) diff --git a/apps/tauri/src-tauri/Cargo.lock b/apps/tauri/src-tauri/Cargo.lock index 2bab9cb..0b0814b 100644 --- a/apps/tauri/src-tauri/Cargo.lock +++ b/apps/tauri/src-tauri/Cargo.lock @@ -94,39 +94,6 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "onyx-core" -version = "0.1.0" -dependencies = [ - "chrono", - "directories", - "keyring", - "quick-xml 0.36.2", - "reqwest 0.12.28", - "serde", - "serde_json", - "serde_yaml", - "sha2", - "tokio", - "uuid", -] - -[[package]] -name = "onyx-tauri" -version = "0.1.0" -dependencies = [ - "onyx-core", - "chrono", - "serde", - "serde_json", - "tauri", - "tauri-build", - "tauri-plugin-dialog", - "tauri-plugin-os", - "tokio", - "uuid", -] - [[package]] name = "bit-set" version = "0.8.0" @@ -842,6 +809,17 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -912,6 +890,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "futf" version = "0.1.5" @@ -1661,6 +1648,35 @@ dependencies = [ "cfb", ] +[[package]] +name = "inotify" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + [[package]] name = "ipnet" version = "2.12.0" @@ -1786,6 +1802,26 @@ dependencies = [ "zeroize", ] +[[package]] +name = "kqueue" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + [[package]] name = "kuchikiki" version = "0.8.8-speedreader" @@ -1859,7 +1895,10 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ + "bitflags 2.11.0", "libc", + "plain", + "redox_syscall 0.7.3", ] [[package]] @@ -1981,6 +2020,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", + "log", "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.61.2", ] @@ -2060,6 +2100,46 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" +[[package]] +name = "notify" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c533b4c39709f9ba5005d8002048266593c1cfaf3c5f0739d5b8ab0c6c504009" +dependencies = [ + "bitflags 2.11.0", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.52.0", +] + +[[package]] +name = "notify-debouncer-mini" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaa5a66d07ed97dce782be94dcf5ab4d1b457f4243f7566c7557f15cabc8c799" +dependencies = [ + "log", + "notify", + "notify-types", + "tempfile", +] + +[[package]] +name = "notify-types" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585d3cb5e12e01aed9e8a1f70d5c6b5e86fe2a6e48fc8cd0b3e0b8df6f6eb174" +dependencies = [ + "instant", +] + [[package]] name = "num-conv" version = "0.2.0" @@ -2299,6 +2379,41 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "onyx-core" +version = "0.1.0" +dependencies = [ + "chrono", + "directories", + "keyring", + "quick-xml 0.36.2", + "reqwest 0.12.28", + "serde", + "serde_json", + "serde_yaml", + "sha2", + "tokio", + "uuid", +] + +[[package]] +name = "onyx-tauri" +version = "0.1.0" +dependencies = [ + "chrono", + "notify", + "notify-debouncer-mini", + "onyx-core", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-dialog", + "tauri-plugin-os", + "tokio", + "uuid", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -2364,7 +2479,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link 0.2.1", ] @@ -2580,6 +2695,12 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "plist" version = "1.8.0" @@ -2933,6 +3054,15 @@ dependencies = [ "bitflags 2.11.0", ] +[[package]] +name = "redox_syscall" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +dependencies = [ + "bitflags 2.11.0", +] + [[package]] name = "redox_users" version = "0.4.6" @@ -3612,7 +3742,7 @@ dependencies = [ "objc2-foundation", "objc2-quartz-core", "raw-window-handle", - "redox_syscall", + "redox_syscall 0.5.18", "tracing", "wasm-bindgen", "web-sys", @@ -4131,6 +4261,19 @@ dependencies = [ "toml 0.9.12+spec-1.1.0", ] +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "tendril" version = "0.4.3" diff --git a/apps/tauri/src-tauri/Cargo.toml b/apps/tauri/src-tauri/Cargo.toml index be4977a..3752fda 100644 --- a/apps/tauri/src-tauri/Cargo.toml +++ b/apps/tauri/src-tauri/Cargo.toml @@ -23,6 +23,8 @@ onyx-core = { path = "../../../crates/onyx-core" } tokio = { version = "1", features = ["full"] } uuid = { version = "1", features = ["serde", "v4"] } chrono = { version = "0.4", features = ["serde"] } +notify = "7" +notify-debouncer-mini = "0.5" [package.metadata.tauri] diff --git a/apps/tauri/src-tauri/src/lib.rs b/apps/tauri/src-tauri/src/lib.rs index 85c95c2..fc7d3da 100644 --- a/apps/tauri/src-tauri/src/lib.rs +++ b/apps/tauri/src-tauri/src/lib.rs @@ -1,8 +1,10 @@ use std::path::PathBuf; use std::sync::Mutex; +use std::time::Instant; +use notify_debouncer_mini::{new_debouncer, DebouncedEventKind}; use serde::{Deserialize, Serialize}; -use tauri::State; +use tauri::{Emitter, Manager, State}; use uuid::Uuid; use onyx_core::{ @@ -13,6 +15,13 @@ use onyx_core::{ webdav, }; +/// Active file watcher stored globally so it lives for the app lifetime. +static WATCHER: Mutex>> = + Mutex::new(None); + +/// Shared mute timestamp — set before writes, checked by the watcher. +static LAST_WRITE: Mutex> = Mutex::new(None); + /// Shared application state behind a mutex. struct AppState { config: AppConfig, @@ -43,6 +52,11 @@ impl From for SyncResult { } } +/// Suppress file watcher events for the next second (call before writes). +fn mute_watcher(_state: &mut AppState) { + *LAST_WRITE.lock().unwrap() = Some(Instant::now()); +} + /// Helper: get or open a TaskRepository for the current workspace. fn ensure_repo(state: &mut AppState) -> Result<(), String> { if state.repo.is_some() { @@ -151,6 +165,7 @@ fn create_list( ) -> Result { let mut s = state.lock().unwrap(); ensure_repo(&mut s)?; + mute_watcher(&mut s); s.repo .as_mut() .unwrap() @@ -165,6 +180,7 @@ fn delete_list( ) -> Result<(), String> { let mut s = state.lock().unwrap(); ensure_repo(&mut s)?; + mute_watcher(&mut s); let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?; s.repo .as_mut() @@ -199,6 +215,7 @@ fn create_task( ) -> Result { let mut s = state.lock().unwrap(); ensure_repo(&mut s)?; + mute_watcher(&mut s); let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?; let mut task = Task::new(title); if let Some(desc) = description.filter(|d| !d.is_empty()) { @@ -219,6 +236,7 @@ fn update_task( ) -> Result<(), String> { let mut s = state.lock().unwrap(); ensure_repo(&mut s)?; + mute_watcher(&mut s); let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?; s.repo .as_mut() @@ -235,6 +253,7 @@ fn delete_task( ) -> Result<(), String> { let mut s = state.lock().unwrap(); ensure_repo(&mut s)?; + mute_watcher(&mut s); let lid = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?; let tid = Uuid::parse_str(&task_id).map_err(|e| e.to_string())?; s.repo @@ -252,6 +271,7 @@ fn toggle_task( ) -> Result { let mut s = state.lock().unwrap(); ensure_repo(&mut s)?; + mute_watcher(&mut s); let lid = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?; let tid = Uuid::parse_str(&task_id).map_err(|e| e.to_string())?; let repo = s.repo.as_mut().unwrap(); @@ -274,6 +294,7 @@ fn reorder_task( ) -> Result<(), String> { let mut s = state.lock().unwrap(); ensure_repo(&mut s)?; + mute_watcher(&mut s); let lid = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?; let tid = Uuid::parse_str(&task_id).map_err(|e| e.to_string())?; s.repo @@ -283,6 +304,77 @@ fn reorder_task( .map_err(|e| e.to_string()) } +// ── Move / rename / grouping ──────────────────────────────────────── + +#[tauri::command] +fn move_task( + from_list_id: String, + to_list_id: String, + task_id: String, + state: State<'_, Mutex>, +) -> Result<(), String> { + let mut s = state.lock().unwrap(); + ensure_repo(&mut s)?; + mute_watcher(&mut s); + let from = Uuid::parse_str(&from_list_id).map_err(|e| e.to_string())?; + let to = Uuid::parse_str(&to_list_id).map_err(|e| e.to_string())?; + let tid = Uuid::parse_str(&task_id).map_err(|e| e.to_string())?; + s.repo + .as_mut() + .unwrap() + .move_task(from, to, tid) + .map_err(|e| e.to_string()) +} + +#[tauri::command] +fn rename_list( + list_id: String, + new_name: String, + state: State<'_, Mutex>, +) -> Result<(), String> { + let mut s = state.lock().unwrap(); + ensure_repo(&mut s)?; + mute_watcher(&mut s); + let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?; + s.repo + .as_mut() + .unwrap() + .rename_list(id, new_name) + .map_err(|e| e.to_string()) +} + +#[tauri::command] +fn set_group_by_due_date( + list_id: String, + enabled: bool, + state: State<'_, Mutex>, +) -> Result<(), String> { + let mut s = state.lock().unwrap(); + ensure_repo(&mut s)?; + mute_watcher(&mut s); + let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?; + s.repo + .as_mut() + .unwrap() + .set_group_by_due_date(id, enabled) + .map_err(|e| e.to_string()) +} + +#[tauri::command] +fn get_group_by_due_date( + list_id: String, + state: State<'_, Mutex>, +) -> Result { + let mut s = state.lock().unwrap(); + ensure_repo(&mut s)?; + let id = Uuid::parse_str(&list_id).map_err(|e| e.to_string())?; + s.repo + .as_ref() + .unwrap() + .get_group_by_due_date(id) + .map_err(|e| e.to_string()) +} + // ── Sync commands ──────────────────────────────────────────────────── #[tauri::command] @@ -348,6 +440,43 @@ async fn sync_workspace( Ok(result.into()) } +// ── File watcher ──────────────────────────────────────────────────── + +fn start_watcher(handle: tauri::AppHandle, path: PathBuf) { + let handle = handle.clone(); + let debouncer = new_debouncer( + std::time::Duration::from_millis(500), + move |events: Result, notify::Error>| { + let Ok(events) = events else { return }; + // Only care about data file changes + let has_data_change = events.iter().any(|e| { + if e.kind != DebouncedEventKind::Any { return false; } + let p = e.path.to_string_lossy(); + p.ends_with(".md") || p.ends_with(".json") + }); + if !has_data_change { return; } + // Skip if we wrote recently (self-change suppression) + if let Some(t) = *LAST_WRITE.lock().unwrap() { + if t.elapsed() < std::time::Duration::from_secs(1) { return; } + } + let _ = handle.emit("fs-changed", ()); + }, + ); + match debouncer { + Ok(mut d) => { + let _ = d.watcher().watch(&path, notify::RecursiveMode::Recursive); + *WATCHER.lock().unwrap() = Some(d); + } + Err(e) => eprintln!("Failed to start file watcher: {e}"), + } +} + +#[tauri::command] +fn watch_workspace(path: String, app_handle: tauri::AppHandle) -> Result<(), String> { + start_watcher(app_handle, PathBuf::from(path)); + Ok(()) +} + // ── App entry ──────────────────────────────────────────────────────── #[cfg_attr(mobile, tauri::mobile_entry_point)] @@ -360,6 +489,18 @@ pub fn run() { .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_os::init()) .manage(Mutex::new(AppState { config, repo: None })) + .setup(|app| { + let handle = app.handle().clone(); + let state: State<'_, Mutex> = app.state(); + let workspace_path = { + let s = state.lock().unwrap(); + s.config.get_current_workspace().ok().map(|(_, ws)| ws.path.clone()) + }; + if let Some(path) = workspace_path { + start_watcher(handle, path); + } + Ok(()) + }) .invoke_handler(tauri::generate_handler![ get_config, save_config, @@ -376,11 +517,16 @@ pub fn run() { delete_task, toggle_task, reorder_task, + move_task, + rename_list, + set_group_by_due_date, + get_group_by_due_date, set_webdav_config, store_credentials, load_credentials, test_webdav_connection, sync_workspace, + watch_workspace, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); From 1a967c7fdd4277b5e0847145379a24392a2e8bf5 Mon Sep 17 00:00:00 2001 From: Tristan Michael Date: Tue, 31 Mar 2026 13:28:06 -0700 Subject: [PATCH 3/7] feat(tauri): move-to-list, rename, group toggle, keyboard shortcuts, WebDAV fix - TaskDetailView: 'Move to...' submenu in kebab menu - TasksScreen: list rename (inline input), group-by-due-date toggle, global Escape key handler for closing overlays - SettingsScreen: auto-populate WebDAV URL/credentials on open - SetupScreen: add window dragging, minimize/close buttons, 'Open Existing Folder' button - Store: moveTask, renameList, setGroupByDueDate methods + fs-changed event listener for file watcher Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/lib/components/TaskDetailView.svelte | 31 ++++ .../src/lib/screens/SettingsScreen.svelte | 16 ++ apps/tauri/src/lib/screens/SetupScreen.svelte | 141 +++++++++++++----- apps/tauri/src/lib/screens/TasksScreen.svelte | 98 ++++++++++-- apps/tauri/src/lib/stores/app.svelte.ts | 49 ++++++ 5 files changed, 285 insertions(+), 50 deletions(-) diff --git a/apps/tauri/src/lib/components/TaskDetailView.svelte b/apps/tauri/src/lib/components/TaskDetailView.svelte index 06863d9..478dd35 100644 --- a/apps/tauri/src/lib/components/TaskDetailView.svelte +++ b/apps/tauri/src/lib/components/TaskDetailView.svelte @@ -14,10 +14,13 @@ let title = $state(task.title); let description = $state(task.description); let showMenu = $state(false); + let showMoveSubmenu = $state(false); let menuEl = $state(null); let showDatePicker = $state(false); let saveTimer: ReturnType; + let otherLists = $derived(app.lists.filter((l) => l.id !== app.activeListId)); + function handleHeaderMouseDown(e: MouseEvent) { if (e.button !== 0) return; if ((e.target as HTMLElement).closest("button")) return; @@ -126,6 +129,34 @@ {isCompleted ? "Restore task" : "Mark as completed"} + {#if otherLists.length > 0} +
+ + {#if showMoveSubmenu} +
+ {#each otherLists as list} + + {/each} +
+ {/if} +
+ {/if} + {/if} + + + {/if} + - +
+
+

Onyx

+

+ Create a new workspace or open an existing one. +

+ + + + + +
+ + +
- - -
- + +
+
+ or +
+
+ +
- -
diff --git a/apps/tauri/src/lib/screens/TasksScreen.svelte b/apps/tauri/src/lib/screens/TasksScreen.svelte index 11c9e68..92793e4 100644 --- a/apps/tauri/src/lib/screens/TasksScreen.svelte +++ b/apps/tauri/src/lib/screens/TasksScreen.svelte @@ -44,6 +44,8 @@ let completedVisible = $state(false); let listMenuId = $state(null); let wsMenuName = $state(null); + let renamingListId = $state(null); + let renameValue = $state(""); let dragId = $state(null); let dragOverId = $state(null); let resizing = $state(false); @@ -77,6 +79,40 @@ await app.deleteList(id); } + function startRenameList(id: string) { + listMenuId = null; + const list = app.lists.find(l => l.id === id); + if (!list) return; + renamingListId = id; + renameValue = list.title; + } + + async function handleRenameList() { + if (!renamingListId || !renameValue.trim()) { renamingListId = null; return; } + const list = app.lists.find(l => l.id === renamingListId); + if (renameValue.trim() !== list?.title) { + await app.renameList(renamingListId, renameValue.trim()); + } + renamingListId = null; + } + + async function handleToggleGroupByDueDate(id: string) { + listMenuId = null; + const list = app.lists.find(l => l.id === id); + if (!list) return; + await app.setGroupByDueDate(id, !list.group_by_due_date); + } + + function handleKeydown(e: KeyboardEvent) { + if (e.key !== "Escape") return; + if (showSettings) { showSettings = false; return; } + if (selectedTaskId) { selectedTaskId = null; return; } + if (showDrawer) { closeDrawer(); return; } + if (listMenuId) { listMenuId = null; return; } + if (wsMenuName) { wsMenuName = null; return; } + if (showWorkspacePicker) { showWorkspacePicker = false; return; } + } + function handleDragStart(e: DragEvent, taskId: string) { dragId = taskId; if (e.dataTransfer) { @@ -148,6 +184,8 @@ let translateX = $derived(showDrawer ? '0' : '-80cqi'); + +
@@ -238,17 +276,30 @@
{#each app.lists as list (list.id)}
- + {#if renamingListId === list.id} +
+ { if (e.key === "Enter") handleRenameList(); if (e.key === "Escape") renamingListId = null; }} + onblur={handleRenameList} + autofocus + /> +
+ {:else} + + {/if}
{#if listMenuId === list.id} -
+
+ +