From 1b318431494cf0355cc0e5a2ed669e33cbdede8a Mon Sep 17 00:00:00 2001 From: Simon Cardona Date: Sun, 25 Jan 2026 15:46:10 +0100 Subject: [PATCH 01/15] feat(db/schema): create spaces --- src/db/schema/spaces.sql | 9 +++++++++ src/db/schema/tasks.sql | 6 ++++-- 2 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 src/db/schema/spaces.sql diff --git a/src/db/schema/spaces.sql b/src/db/schema/spaces.sql new file mode 100644 index 0000000..fdcba04 --- /dev/null +++ b/src/db/schema/spaces.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS spaces ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + archived INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL, + updated_at TEXT, + archived_at TEXT +); + diff --git a/src/db/schema/tasks.sql b/src/db/schema/tasks.sql index 2eceafe..96dc73b 100644 --- a/src/db/schema/tasks.sql +++ b/src/db/schema/tasks.sql @@ -7,6 +7,8 @@ CREATE TABLE IF NOT EXISTS tasks ( priority TEXT, created_at TEXT NOT NULL, updated_at TEXT, - archived_at TEXT -); + archived_at TEXT, + space_id TEXT, + FOREIGN KEY(space_id) REFERENCES spaces(id) +); From c793ceadaba0c225aec5907cd02d782cf4f76a17 Mon Sep 17 00:00:00 2001 From: Simon Cardona Date: Sun, 25 Jan 2026 15:46:24 +0100 Subject: [PATCH 02/15] feat(db/repositories): add SpaceRepository --- src/db/repositories/mod.rs | 2 ++ src/db/repositories/space.rs | 56 ++++++++++++++++++++++++++++++++++++ src/db/repositories/task.rs | 2 ++ 3 files changed, 60 insertions(+) create mode 100644 src/db/repositories/space.rs diff --git a/src/db/repositories/mod.rs b/src/db/repositories/mod.rs index 9c16939..a3879da 100644 --- a/src/db/repositories/mod.rs +++ b/src/db/repositories/mod.rs @@ -1,3 +1,5 @@ +pub mod space; pub mod task; +pub use space::SpaceRepository; pub use task::TaskRepository; diff --git a/src/db/repositories/space.rs b/src/db/repositories/space.rs new file mode 100644 index 0000000..e49db21 --- /dev/null +++ b/src/db/repositories/space.rs @@ -0,0 +1,56 @@ +use std::error::Error; + +use chrono::{DateTime, Utc}; +use rusqlite::{Connection, Row}; +use uuid::Uuid; + +use crate::models::Space; + +pub struct SpaceRepository; + +impl SpaceRepository { + pub fn create(connection: &Connection, space: &Space) -> Result<(), rusqlite::Error> { + connection.execute( + "INSERT INTO spaces (id, title, created_at) VALUES (?1, ?2, ?3)", + ( + space.id.to_string(), + &space.title, + space.created_at.to_rfc3339(), + ), + )?; + + Ok(()) + } + + pub fn get_all(connection: &Connection) -> Result, Box> { + let mut stmt = connection.prepare("SELECT * from spaces")?; + let mut rows = stmt.query([])?; + + let mut spaces: Vec = Vec::new(); + while let Some(row) = rows.next()? { + spaces.push(Self::parse_row(row)?); + } + + Ok(spaces) + } + + fn parse_row(row: &Row) -> Result> { + let id: String = row.get("id")?; + let created_at: String = row.get("created_at")?; + let updated_at: Option = row.get("updated_at")?; + let archived_at: Option = row.get("archived_at")?; + + Ok(Space { + id: Uuid::parse_str(&id)?, + title: row.get("title")?, + archived: row.get::<_, i32>("archived")? != 0, + created_at: DateTime::parse_from_rfc3339(&created_at)?.with_timezone(&Utc), + updated_at: updated_at + .map(|s| DateTime::parse_from_rfc3339(&s).map(|d| d.with_timezone(&Utc))) + .transpose()?, + archived_at: archived_at + .map(|s| DateTime::parse_from_rfc3339(&s).map(|d| d.with_timezone(&Utc))) + .transpose()?, + }) + } +} diff --git a/src/db/repositories/task.rs b/src/db/repositories/task.rs index 98d2ff8..fcf4d46 100644 --- a/src/db/repositories/task.rs +++ b/src/db/repositories/task.rs @@ -74,6 +74,7 @@ impl TaskRepository { fn parse_row(row: &Row) -> Result> { let id: String = row.get("id")?; + let space_id: Option = row.get("space_id")?; let priority: Option = row.get("priority")?; let created_at: String = row.get("created_at")?; let updated_at: Option = row.get("updated_at")?; @@ -93,6 +94,7 @@ impl TaskRepository { archived_at: archived_at .map(|s| DateTime::parse_from_rfc3339(&s).map(|d| d.with_timezone(&Utc))) .transpose()?, + space_id: space_id.map(|s| Uuid::parse_str(&s)).transpose()?, }) } } From b0869d21b511f7cfbb616da76b4546a673a1f929 Mon Sep 17 00:00:00 2001 From: Simon Cardona Date: Sun, 25 Jan 2026 15:46:43 +0100 Subject: [PATCH 03/15] feat(db/connection): add lazytasks.db and load spaces --- src/db/connection.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/db/connection.rs b/src/db/connection.rs index b0fb5a0..cb1351b 100644 --- a/src/db/connection.rs +++ b/src/db/connection.rs @@ -16,7 +16,7 @@ impl Db { fs::create_dir(&data_dir).expect("Couldn't create your data directory") } - let db_path = data_dir.join("tasks.db"); + let db_path = data_dir.join("lazytasks.db"); let connection = Connection::open(&db_path).expect("Couldn't open database"); @@ -28,8 +28,12 @@ impl Db { } fn init_schema(&self) -> Result<(), rusqlite::Error> { - let schema = include_str!("schema/tasks.sql"); + let spaces_schema = include_str!("schema/spaces.sql"); + self.connection.execute_batch(spaces_schema)?; - self.connection.execute_batch(schema) + let tasks_schema = include_str!("schema/tasks.sql"); + self.connection.execute_batch(tasks_schema)?; + + Ok(()) } } From fe4b82ba885af16901ab5b2d825c16ad4319ff8c Mon Sep 17 00:00:00 2001 From: Simon Cardona Date: Sun, 25 Jan 2026 15:47:06 +0100 Subject: [PATCH 04/15] feat(models): create space model --- src/models/mod.rs | 2 ++ src/models/space.rs | 26 ++++++++++++++++++++++++++ src/models/task.rs | 2 ++ 3 files changed, 30 insertions(+) create mode 100644 src/models/space.rs diff --git a/src/models/mod.rs b/src/models/mod.rs index 9eb572f..6fab2a6 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,5 +1,7 @@ pub mod priority; +pub mod space; pub mod task; pub use priority::Priority; +pub use space::Space; pub use task::Task; diff --git a/src/models/space.rs b/src/models/space.rs new file mode 100644 index 0000000..da08572 --- /dev/null +++ b/src/models/space.rs @@ -0,0 +1,26 @@ +use chrono::{DateTime, Utc}; +use uuid::Uuid; + +pub struct Space { + pub id: Uuid, + pub title: String, + pub archived: bool, + pub created_at: DateTime, + pub updated_at: Option>, + pub archived_at: Option>, +} + +impl Space { + pub fn new(title: impl Into) -> Self { + Space { + id: Uuid::new_v4(), + title: title.into(), + archived: false, + created_at: Utc::now(), + updated_at: None, + archived_at: None, + } + } + + pub fn get_all() {} +} diff --git a/src/models/task.rs b/src/models/task.rs index 6072da8..8340c92 100644 --- a/src/models/task.rs +++ b/src/models/task.rs @@ -14,6 +14,7 @@ pub struct Task { pub created_at: DateTime, pub updated_at: Option>, pub archived_at: Option>, + pub space_id: Option, } impl Task { @@ -28,6 +29,7 @@ impl Task { created_at: Utc::now(), updated_at: None, archived_at: None, + space_id: None, } } From 4945e2f0dd3ae5920c31ec0e36802ce1d088de3e Mon Sep 17 00:00:00 2001 From: Simon Cardona Date: Sun, 25 Jan 2026 15:47:24 +0100 Subject: [PATCH 05/15] feat(actions): add space creation --- src/actions/create_space.rs | 16 ++++++++++++++++ src/actions/mod.rs | 8 ++++++-- src/actions/open_create_space_modal.rs | 7 +++++++ ...ate_modal.rs => open_create_task_modal.rs} | 2 +- src/app.rs | 19 ++++++++++++++++--- src/keybindings.rs | 17 ++++++++++++++++- src/state.rs | 9 +++++++++ 7 files changed, 71 insertions(+), 7 deletions(-) create mode 100644 src/actions/create_space.rs create mode 100644 src/actions/open_create_space_modal.rs rename src/actions/{open_create_modal.rs => open_create_task_modal.rs} (75%) diff --git a/src/actions/create_space.rs b/src/actions/create_space.rs new file mode 100644 index 0000000..c2b66bc --- /dev/null +++ b/src/actions/create_space.rs @@ -0,0 +1,16 @@ +use crate::{app::App, db::repositories::SpaceRepository, models}; + +pub fn create_space(app: &mut App, title: String) { + let new_space = models::Space::new(title); + + if let Err(e) = SpaceRepository::create(&app.db.connection, &new_space) { + app.error = Some(e.to_string()); + + return; + }; + + app.spaces.push(new_space); + + let new_index = app.active_tasks().len() - 1; + app.state.active_tasks_state.select(Some(new_index)); +} diff --git a/src/actions/mod.rs b/src/actions/mod.rs index 71f3868..8f85dca 100644 --- a/src/actions/mod.rs +++ b/src/actions/mod.rs @@ -1,12 +1,14 @@ pub mod clean_err_msg; pub mod close_modal; +pub mod create_space; pub mod create_task; pub mod delete_task; pub mod edit_priority; pub mod edit_task; pub mod edit_title; pub mod open_archive_modal; -pub mod open_create_modal; +pub mod open_create_space_modal; +pub mod open_create_task_modal; pub mod open_delete_modal; pub mod open_edit_title_modal; pub mod open_priority_modal; @@ -20,13 +22,15 @@ pub mod toggle_task_selection; pub use clean_err_msg::clean_err_msg; pub use close_modal::close_modal; +pub use create_space::create_space; pub use create_task::create_task; pub use delete_task::delete_task; pub use edit_priority::edit_priority; pub use edit_task::edit_task; pub use edit_title::edit_title; pub use open_archive_modal::open_archive_modal; -pub use open_create_modal::open_create_modal; +pub use open_create_space_modal::open_create_space_modal; +pub use open_create_task_modal::open_create_task_modal; pub use open_delete_modal::open_delete_modal; pub use open_edit_title_modal::open_edit_title_modal; pub use open_priority_modal::open_priority_modal; diff --git a/src/actions/open_create_space_modal.rs b/src/actions/open_create_space_modal.rs new file mode 100644 index 0000000..24e03e6 --- /dev/null +++ b/src/actions/open_create_space_modal.rs @@ -0,0 +1,7 @@ +use crate::{app::App, state::PanelState}; + +pub fn open_create_space_modal(app: &mut App) { + if app.state.active_panel == PanelState::ActiveTasks { + app.state.open_create_space() + } +} diff --git a/src/actions/open_create_modal.rs b/src/actions/open_create_task_modal.rs similarity index 75% rename from src/actions/open_create_modal.rs rename to src/actions/open_create_task_modal.rs index 26d4210..26c0ad9 100644 --- a/src/actions/open_create_modal.rs +++ b/src/actions/open_create_task_modal.rs @@ -1,6 +1,6 @@ use crate::{app::App, state::PanelState}; -pub fn open_create_modal(app: &mut App) { +pub fn open_create_task_modal(app: &mut App) { if app.state.active_panel == PanelState::ActiveTasks { app.state.open_create_task() } diff --git a/src/app.rs b/src/app.rs index 1c4d82b..cd9cc92 100644 --- a/src/app.rs +++ b/src/app.rs @@ -8,7 +8,10 @@ use uuid::Uuid; use crate::{ components, - db::{Db, repositories::TaskRepository}, + db::{ + Db, + repositories::{SpaceRepository, TaskRepository}, + }, keybindings::handle_key_event, models::Task, state, @@ -17,6 +20,7 @@ use crate::{models, state::ModalState}; pub struct App { pub exit: bool, + pub spaces: Vec, pub tasks: Vec, pub selected_tasks: Vec, pub state: state::AppState, @@ -29,18 +33,24 @@ impl App { let state = state::AppState::new(); let db = Db::new(); - let (tasks, error) = match TaskRepository::get_all(&db.connection) { + let (tasks, _) = match TaskRepository::get_all(&db.connection) { Ok(tasks) => (tasks, None), Err(err) => (vec![], Some(err.to_string())), }; + let (spaces, space_err) = match SpaceRepository::get_all(&db.connection) { + Ok(spaces) => (spaces, None), + Err(err) => (vec![], Some(err.to_string())), + }; + App { exit: false, selected_tasks: Vec::new(), state, db, tasks, - error, + spaces, + error: space_err, } } @@ -89,6 +99,9 @@ impl App { }) => { components::modals::priority_task::render(frame, selected_option); } + Some(ModalState::CreateSpace { input }) => { + components::modals::create_task::render(frame, input); + } None => {} } } diff --git a/src/keybindings.rs b/src/keybindings.rs index 62cb7e0..87123f3 100644 --- a/src/keybindings.rs +++ b/src/keybindings.rs @@ -97,9 +97,24 @@ pub fn handle_key_event(app: &mut App, event: &Event, terminal: &mut DefaultTerm } _ => {} }, + Some(ModalState::CreateSpace { input }) => match key.code { + crossterm::event::KeyCode::Esc => actions::close_modal(app), + crossterm::event::KeyCode::Enter => { + let title = input.value().trim().to_owned(); + + if !title.is_empty() { + actions::create_space(app, title); + } + actions::close_modal(app); + } + _ => { + input.handle_event(event); + } + }, None => match key.code { crossterm::event::KeyCode::Char('a') => actions::open_archive_modal(app), - crossterm::event::KeyCode::Char('c') => actions::open_create_modal(app), + crossterm::event::KeyCode::Char('c') => actions::open_create_task_modal(app), + crossterm::event::KeyCode::Char('s') => actions::open_create_space_modal(app), crossterm::event::KeyCode::Char('e') => actions::open_edit_title_modal(app), crossterm::event::KeyCode::Char('p') => actions::open_priority_modal(app), crossterm::event::KeyCode::Char('E') => actions::edit_task(app, terminal), diff --git a/src/state.rs b/src/state.rs index 2396ee7..ebb4782 100644 --- a/src/state.rs +++ b/src/state.rs @@ -45,6 +45,9 @@ pub enum ModalState { task_ids: Vec, selected_option: ListState, }, + CreateSpace { + input: Input, + }, } impl AppState { @@ -146,6 +149,12 @@ impl AppState { } } + pub fn open_create_space(&mut self) { + self.active_modal = Some(ModalState::CreateSpace { + input: Input::default(), + }) + } + pub fn close_modal(&mut self) { self.active_modal = None } From 18b5ac6472b8b88623748b6da0064ae4016c9ce3 Mon Sep 17 00:00:00 2001 From: Simon Cardona Date: Sun, 25 Jan 2026 15:47:45 +0100 Subject: [PATCH 06/15] feat(dependencies): add tui-tree-widget --- Cargo.lock | 189 +++++++++++++++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 1 + 2 files changed, 183 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d67ef9b..028bde7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,6 +93,20 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "compact_str" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + [[package]] name = "convert_case" version = "0.10.0" @@ -186,6 +200,15 @@ dependencies = [ "syn", ] +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", +] + [[package]] name = "derive_more" version = "2.1.0" @@ -336,6 +359,8 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ + "allocator-api2", + "equivalent", "foldhash 0.2.0", ] @@ -415,6 +440,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -431,6 +465,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kasuari" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fe90c1150662e858c7d5f945089b7517b0a80d8bf7ba4b1b5ffc984e7230a5b" +dependencies = [ + "hashbrown 0.16.1", + "thiserror", +] + [[package]] name = "lazytasks" version = "0.1.0" @@ -441,6 +485,7 @@ dependencies = [ "ratatui", "rusqlite", "tui-input", + "tui-tree-widget", "uuid", ] @@ -471,6 +516,15 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "line-clipping" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4de44e98ddbf09375cbf4d17714d18f39195f4f4894e8524501726fd9a8a4a" +dependencies = [ + "bitflags", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -513,6 +567,15 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "lru" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" +dependencies = [ + "hashbrown 0.16.1", +] + [[package]] name = "mio" version = "1.1.1" @@ -525,6 +588,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + [[package]] name = "num-traits" version = "0.2.19" @@ -581,6 +650,12 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "proc-macro2" version = "1.0.103" @@ -613,16 +688,55 @@ checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" dependencies = [ "bitflags", "cassowary", - "compact_str", + "compact_str 0.8.1", "crossterm 0.28.1", "indoc", "instability", - "itertools", - "lru", + "itertools 0.13.0", + "lru 0.12.5", "paste", - "strum", + "strum 0.26.3", + "unicode-segmentation", + "unicode-truncate 1.1.0", + "unicode-width 0.2.0", +] + +[[package]] +name = "ratatui-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293" +dependencies = [ + "bitflags", + "compact_str 0.9.0", + "hashbrown 0.16.1", + "indoc", + "itertools 0.14.0", + "kasuari", + "lru 0.16.3", + "strum 0.27.2", + "thiserror", + "unicode-segmentation", + "unicode-truncate 2.0.1", + "unicode-width 0.2.0", +] + +[[package]] +name = "ratatui-widgets" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7dbfa023cd4e604c2553483820c5fe8aa9d71a42eea5aa77c6e7f35756612db" +dependencies = [ + "bitflags", + "hashbrown 0.16.1", + "indoc", + "instability", + "itertools 0.14.0", + "line-clipping", + "ratatui-core", + "strum 0.27.2", + "time", "unicode-segmentation", - "unicode-truncate", "unicode-width 0.2.0", ] @@ -793,7 +907,16 @@ version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" dependencies = [ - "strum_macros", + "strum_macros 0.26.4", +] + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros 0.27.2", ] [[package]] @@ -809,6 +932,18 @@ dependencies = [ "syn", ] +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "syn" version = "2.0.111" @@ -840,6 +975,24 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.3.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9da98b7d9b7dad93488a84b8248efc35352b0b2657397d4167e7ad67e5d535e5" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + [[package]] name = "tui-input" version = "0.14.0" @@ -850,6 +1003,17 @@ dependencies = [ "unicode-width 0.2.0", ] +[[package]] +name = "tui-tree-widget" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deca119555009eee2e0cfb9c020f39f632444dc4579918d5fc009d51d75dff92" +dependencies = [ + "ratatui-core", + "ratatui-widgets", + "unicode-width 0.2.0", +] + [[package]] name = "unicode-ident" version = "1.0.22" @@ -868,11 +1032,22 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" dependencies = [ - "itertools", + "itertools 0.13.0", "unicode-segmentation", "unicode-width 0.1.14", ] +[[package]] +name = "unicode-truncate" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" +dependencies = [ + "itertools 0.14.0", + "unicode-segmentation", + "unicode-width 0.2.0", +] + [[package]] name = "unicode-width" version = "0.1.14" diff --git a/Cargo.toml b/Cargo.toml index a1a01ba..ddeb0f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,3 +11,4 @@ ratatui = "0.29.0" tui-input = "0.14.0" uuid = { version = "1.19.0", features = ["v4"] } rusqlite = { version = "0.38.0", features = ["bundled"] } +tui-tree-widget = "0.24.0" From 1d0661c1e32b7f1f3b62cfa090eee96b5621ef67 Mon Sep 17 00:00:00 2001 From: Simon Cardona Date: Sat, 31 Jan 2026 00:27:12 +0100 Subject: [PATCH 07/15] feat: add delete space --- src/actions/create_space.rs | 8 ++- src/actions/create_task.rs | 4 +- src/actions/delete_space.rs | 19 +++++ src/actions/mod.rs | 4 ++ src/actions/open_create_task_modal.rs | 3 +- src/actions/open_delete_modal.rs | 32 +++++++-- src/actions/open_delete_space_modal.rs | 22 ++++++ src/app.rs | 8 ++- src/components/modals/delete_space.rs | 22 ++++++ src/components/modals/mod.rs | 1 + .../workspace/sidebar/active_spaces.rs | 71 +++++++++++++++++++ src/components/workspace/sidebar/mod.rs | 4 +- src/db/repositories/space.rs | 14 ++++ src/db/repositories/task.rs | 8 ++- src/keybindings.rs | 66 +++++++++++++++-- src/models/task.rs | 4 +- src/state.rs | 25 ++++++- 17 files changed, 290 insertions(+), 25 deletions(-) create mode 100644 src/actions/delete_space.rs create mode 100644 src/actions/open_delete_space_modal.rs create mode 100644 src/components/modals/delete_space.rs create mode 100644 src/components/workspace/sidebar/active_spaces.rs diff --git a/src/actions/create_space.rs b/src/actions/create_space.rs index c2b66bc..4d2bcc0 100644 --- a/src/actions/create_space.rs +++ b/src/actions/create_space.rs @@ -2,15 +2,17 @@ use crate::{app::App, db::repositories::SpaceRepository, models}; pub fn create_space(app: &mut App, title: String) { let new_space = models::Space::new(title); + let space_id = new_space.id.to_string(); if let Err(e) = SpaceRepository::create(&app.db.connection, &new_space) { app.error = Some(e.to_string()); - return; }; + let is_first_space = app.spaces.is_empty(); app.spaces.push(new_space); - let new_index = app.active_tasks().len() - 1; - app.state.active_tasks_state.select(Some(new_index)); + if is_first_space { + app.state.spaces_tree_state.select(vec![space_id]); + } } diff --git a/src/actions/create_task.rs b/src/actions/create_task.rs index 616d521..2089f3e 100644 --- a/src/actions/create_task.rs +++ b/src/actions/create_task.rs @@ -1,7 +1,7 @@ use crate::{app::App, db::repositories::TaskRepository, models}; -pub fn create_task(app: &mut App, title: String) { - let new_task = models::Task::new(title); +pub fn create_task(app: &mut App, title: String, space_id: String) { + let new_task = models::Task::new(title, space_id); if let Err(e) = TaskRepository::create(&app.db.connection, &new_task) { app.error = Some(e.to_string()); diff --git a/src/actions/delete_space.rs b/src/actions/delete_space.rs new file mode 100644 index 0000000..bfef5fb --- /dev/null +++ b/src/actions/delete_space.rs @@ -0,0 +1,19 @@ +use uuid::Uuid; + +use crate::{app::App, db::repositories::SpaceRepository}; + +pub fn delete_space(app: &mut App, option_idx: Option, space_id: Uuid) { + if option_idx != Some(0) { + return; + } + + if let Err(e) = SpaceRepository::delete(&app.db.connection, &space_id) { + app.error = Some(e.to_string()); + return; + } + + app.tasks.retain(|t| t.space_id != Some(space_id)); + app.spaces.retain(|s| s.id != space_id); + app.selected_tasks.clear(); + app.state.spaces_tree_state.select_first(); +} diff --git a/src/actions/mod.rs b/src/actions/mod.rs index 8f85dca..6de176d 100644 --- a/src/actions/mod.rs +++ b/src/actions/mod.rs @@ -2,6 +2,7 @@ pub mod clean_err_msg; pub mod close_modal; pub mod create_space; pub mod create_task; +pub mod delete_space; pub mod delete_task; pub mod edit_priority; pub mod edit_task; @@ -10,6 +11,7 @@ pub mod open_archive_modal; pub mod open_create_space_modal; pub mod open_create_task_modal; pub mod open_delete_modal; +pub mod open_delete_space_modal; pub mod open_edit_title_modal; pub mod open_priority_modal; pub mod quit; @@ -24,6 +26,7 @@ pub use clean_err_msg::clean_err_msg; pub use close_modal::close_modal; pub use create_space::create_space; pub use create_task::create_task; +pub use delete_space::delete_space; pub use delete_task::delete_task; pub use edit_priority::edit_priority; pub use edit_task::edit_task; @@ -32,6 +35,7 @@ pub use open_archive_modal::open_archive_modal; pub use open_create_space_modal::open_create_space_modal; pub use open_create_task_modal::open_create_task_modal; pub use open_delete_modal::open_delete_modal; +pub use open_delete_space_modal::open_delete_space_modal; pub use open_edit_title_modal::open_edit_title_modal; pub use open_priority_modal::open_priority_modal; pub use quit::quit; diff --git a/src/actions/open_create_task_modal.rs b/src/actions/open_create_task_modal.rs index 26c0ad9..f0aea2e 100644 --- a/src/actions/open_create_task_modal.rs +++ b/src/actions/open_create_task_modal.rs @@ -2,6 +2,7 @@ use crate::{app::App, state::PanelState}; pub fn open_create_task_modal(app: &mut App) { if app.state.active_panel == PanelState::ActiveTasks { - app.state.open_create_task() + let space_id = app.state.spaces_tree_state.selected()[0].clone(); + app.state.open_create_task(space_id) } } diff --git a/src/actions/open_delete_modal.rs b/src/actions/open_delete_modal.rs index d38da86..81955d7 100644 --- a/src/actions/open_delete_modal.rs +++ b/src/actions/open_delete_modal.rs @@ -1,12 +1,30 @@ -use crate::app::App; +use uuid::Uuid; + +use crate::{app::App, state::PanelState}; pub fn open_delete_modal(app: &mut App) { - if app.selected_tasks.is_empty() { - if let Some(task_index) = app.state.get_selected_panel_state().and_then(|s| s.selected()) { - let task_id = app.get_current_tasks()[task_index].id; - app.state.open_delete_task(vec![task_id]); - } - } else { + if !app.selected_tasks.is_empty() { app.state.open_delete_task(app.selected_tasks.clone()); + return; + } + + if app.state.active_panel == PanelState::ActiveTasks { + let selected = app.state.spaces_tree_state.selected(); + if selected.is_empty() { + return; + } + + let selected_id = selected.last().unwrap(); + if let Ok(uuid) = Uuid::parse_str(selected_id) { + if app.tasks.iter().any(|t| t.id == uuid) { + app.state.open_delete_task(vec![uuid]); + } + } + } else if let Some(task_index) = app.state.get_selected_panel_state().and_then(|s| s.selected()) + { + let tasks = app.get_current_tasks(); + if task_index < tasks.len() { + app.state.open_delete_task(vec![tasks[task_index].id]); + } } } diff --git a/src/actions/open_delete_space_modal.rs b/src/actions/open_delete_space_modal.rs new file mode 100644 index 0000000..7ed8c69 --- /dev/null +++ b/src/actions/open_delete_space_modal.rs @@ -0,0 +1,22 @@ +use uuid::Uuid; + +use crate::{app::App, state::PanelState}; + +pub fn open_delete_space_modal(app: &mut App) { + if app.state.active_panel != PanelState::ActiveTasks { + return; + } + + let selected = app.state.spaces_tree_state.selected(); + if selected.len() != 1 { + return; + } + + let selected_id = &selected[0]; + + if let Ok(uuid) = Uuid::parse_str(selected_id) { + if app.spaces.iter().any(|s| s.id == uuid) { + app.state.open_delete_space(uuid); + } + } +} diff --git a/src/app.rs b/src/app.rs index cd9cc92..f1afa81 100644 --- a/src/app.rs +++ b/src/app.rs @@ -74,7 +74,7 @@ impl App { components::bottom_bar::render(frame, layout[1], self); match &mut self.state.active_modal { - Some(ModalState::CreateTask { input }) => { + Some(ModalState::CreateTask { input, space_id: _ }) => { components::modals::create_task::render(frame, input); } Some(ModalState::EditTask { task_id: _, input }) => { @@ -102,6 +102,12 @@ impl App { Some(ModalState::CreateSpace { input }) => { components::modals::create_task::render(frame, input); } + Some(ModalState::DeleteSpace { + space_id: _, + selected_option, + }) => { + components::modals::delete_space::render(frame, selected_option); + } None => {} } } diff --git a/src/components/modals/delete_space.rs b/src/components/modals/delete_space.rs new file mode 100644 index 0000000..8d7f454 --- /dev/null +++ b/src/components/modals/delete_space.rs @@ -0,0 +1,22 @@ +use ratatui::{ + Frame, + style::{Color, Modifier, Style}, + widgets::{List, ListItem, ListState}, +}; + +use crate::components::shared; + +pub fn render(frame: &mut Frame, selected_option: &mut ListState) { + let area = shared::modal::Modal::new("Delete space") + .height(4) + .render(frame); + let list_items: Vec = vec![ListItem::from("Delete"), ListItem::from("Cancel")]; + let delete_options = List::new(list_items).highlight_style( + Style::default() + .bg(Color::Blue) + .fg(Color::White) + .add_modifier(Modifier::BOLD), + ); + + frame.render_stateful_widget(delete_options, area, selected_option); +} diff --git a/src/components/modals/mod.rs b/src/components/modals/mod.rs index 5c6ebdd..4c0c763 100644 --- a/src/components/modals/mod.rs +++ b/src/components/modals/mod.rs @@ -1,5 +1,6 @@ pub mod archive_task; pub mod create_task; +pub mod delete_space; pub mod delete_task; pub mod edit_task; pub mod priority_task; diff --git a/src/components/workspace/sidebar/active_spaces.rs b/src/components/workspace/sidebar/active_spaces.rs new file mode 100644 index 0000000..84e4925 --- /dev/null +++ b/src/components/workspace/sidebar/active_spaces.rs @@ -0,0 +1,71 @@ +use ratatui::{ + Frame, + layout::Rect, + style::{Color, Modifier, Style}, + widgets::{Block, BorderType, Borders}, +}; +use tui_tree_widget::{Tree, TreeItem}; + +use crate::{app::App, components::shared, state::PanelState}; + +pub fn render(frame: &mut Frame, area: Rect, app: &mut App) { + let is_active = + app.state.active_modal.is_none() && app.state.active_panel == PanelState::ActiveTasks; + let mut items: Vec> = Vec::new(); + + let border_color = if is_active { + Color::Green + } else { + Color::White + }; + + for space in &app.spaces { + let space_tasks: Vec> = app + .tasks + .iter() + .filter(|t| t.space_id == Some(space.id) && !t.archived) + .map(|task| TreeItem::new_leaf(task.id.to_string(), task.title.clone())) + .collect(); + + let space_item = TreeItem::new( + space.id.to_string(), + format!("{} ({})", space.title.clone(), space_tasks.len()), + space_tasks, + ) + .unwrap(); + + items.push(space_item); + } + + for task in &app.tasks { + if task.space_id.is_none() && !task.archived { + items.push(TreeItem::new_leaf(task.id.to_string(), task.title.clone())); + } + } + + let tree = Tree::new(&items) + .expect("identifiers are unique") + .block( + Block::new() + .title(" Spaces ") + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(border_color), + ) + .highlight_style( + Style::default() + .bg(Color::Blue) + .add_modifier(Modifier::BOLD), + ) + .node_closed_symbol("▶ ") + .node_open_symbol("▼ ") + .node_no_children_symbol(" "); + + frame.render_stateful_widget(tree, area, &mut app.state.spaces_tree_state); + shared::scrollbar::render( + frame, + area, + app.spaces.len(), + app.state.active_tasks_state.offset(), + ); +} diff --git a/src/components/workspace/sidebar/mod.rs b/src/components/workspace/sidebar/mod.rs index 2665ac1..0c51990 100644 --- a/src/components/workspace/sidebar/mod.rs +++ b/src/components/workspace/sidebar/mod.rs @@ -1,4 +1,5 @@ pub mod about; +pub mod active_spaces; pub mod active_tasks; pub mod archived_tasks; @@ -32,6 +33,7 @@ pub fn render(frame: &mut Frame, area: Rect, app: &mut App) { Task::sort_by_archived_date(&mut archived_tasks); sidebar::about::render(frame, sidebar[0], app); - sidebar::active_tasks::render(frame, sidebar[1], app, active_tasks); + // sidebar::active_tasks::render(frame, sidebar[1], app, active_tasks); + sidebar::active_spaces::render(frame, sidebar[1], app); sidebar::archived_tasks::render(frame, sidebar[2], app, archived_tasks); } diff --git a/src/db/repositories/space.rs b/src/db/repositories/space.rs index e49db21..7741077 100644 --- a/src/db/repositories/space.rs +++ b/src/db/repositories/space.rs @@ -53,4 +53,18 @@ impl SpaceRepository { .transpose()?, }) } + + pub fn delete(connection: &Connection, space_id: &Uuid) -> Result<(), rusqlite::Error> { + let tx = connection.unchecked_transaction()?; + + tx.execute( + "DELETE FROM tasks WHERE space_id = ?1", + [space_id.to_string()], + )?; + tx.execute("DELETE FROM spaces WHERE id = ?1", [space_id.to_string()])?; + + tx.commit()?; + + Ok(()) + } } diff --git a/src/db/repositories/task.rs b/src/db/repositories/task.rs index fcf4d46..d39273e 100644 --- a/src/db/repositories/task.rs +++ b/src/db/repositories/task.rs @@ -10,12 +10,18 @@ pub struct TaskRepository; impl TaskRepository { pub fn create(connection: &Connection, task: &Task) -> Result<(), rusqlite::Error> { + let space_id = match &task.space_id { + Some(v) => Some(Uuid::to_string(v)), + None => None, + }; + connection.execute( - "INSERT INTO tasks (id, title, created_at) VALUES (?1, ?2, ?3) ", + "INSERT INTO tasks (id, title, created_at, space_id) VALUES (?1, ?2, ?3, ?4) ", ( task.id.to_string(), &task.title, task.created_at.to_rfc3339(), + space_id, ), )?; diff --git a/src/keybindings.rs b/src/keybindings.rs index 87123f3..bb9101f 100644 --- a/src/keybindings.rs +++ b/src/keybindings.rs @@ -2,20 +2,23 @@ use ratatui::crossterm::{self, event::Event}; use ratatui::DefaultTerminal; use tui_input::backend::crossterm::EventHandler; +use tui_tree_widget::TreeState; use crate::actions; +use crate::state::PanelState; use crate::{app::App, state::ModalState}; pub fn handle_key_event(app: &mut App, event: &Event, terminal: &mut DefaultTerminal) { if let crossterm::event::Event::Key(key) = event { match &mut app.state.active_modal { - Some(ModalState::CreateTask { input }) => match key.code { + Some(ModalState::CreateTask { input, space_id }) => match key.code { crossterm::event::KeyCode::Esc => actions::close_modal(app), crossterm::event::KeyCode::Enter => { let title = input.value().trim().to_owned(); + let space_id = space_id.clone(); if !title.is_empty() { - actions::create_task(app, title); + actions::create_task(app, title, space_id); } actions::close_modal(app); } @@ -111,6 +114,26 @@ pub fn handle_key_event(app: &mut App, event: &Event, terminal: &mut DefaultTerm input.handle_event(event); } }, + Some(ModalState::DeleteSpace { + space_id, + selected_option, + }) => match key.code { + crossterm::event::KeyCode::Esc => actions::close_modal(app), + crossterm::event::KeyCode::Enter => { + let option_idx = selected_option.selected(); + let space_id = *space_id; + + actions::delete_space(app, option_idx, space_id); + actions::close_modal(app); + } + crossterm::event::KeyCode::Char('j') => { + selected_option.select_next(); + } + crossterm::event::KeyCode::Char('k') => { + selected_option.select_previous(); + } + _ => {} + }, None => match key.code { crossterm::event::KeyCode::Char('a') => actions::open_archive_modal(app), crossterm::event::KeyCode::Char('c') => actions::open_create_task_modal(app), @@ -120,12 +143,43 @@ pub fn handle_key_event(app: &mut App, event: &Event, terminal: &mut DefaultTerm crossterm::event::KeyCode::Char('E') => actions::edit_task(app, terminal), crossterm::event::KeyCode::Char('y') => actions::toggle_task_completion(app), crossterm::event::KeyCode::Char('q') => actions::quit(app), - crossterm::event::KeyCode::Char('d') => actions::open_delete_modal(app), - crossterm::event::KeyCode::Char('j') => actions::select_next_task(app), - crossterm::event::KeyCode::Char('k') => actions::select_previous_task(app), + crossterm::event::KeyCode::Char('d') => { + if app.state.active_panel == PanelState::ActiveTasks { + actions::open_delete_space_modal(app); + } + if app.state.active_modal.is_none() { + actions::open_delete_modal(app); + } + } + crossterm::event::KeyCode::Char('j') => { + match app.state.active_panel { + PanelState::ActiveTasks => { + TreeState::key_down(&mut app.state.spaces_tree_state); + } + _ => actions::select_next_task(app), + }; + } + crossterm::event::KeyCode::Char('k') => { + match app.state.active_panel { + PanelState::ActiveTasks => { + TreeState::key_up(&mut app.state.spaces_tree_state); + } + _ => actions::select_previous_task(app), + }; + } crossterm::event::KeyCode::Char(' ') => actions::toggle_task_selection(app), crossterm::event::KeyCode::Tab => actions::switch_panel(app), - crossterm::event::KeyCode::Enter => actions::clean_err_msg(app), + crossterm::event::KeyCode::Enter => match app.state.active_panel { + PanelState::ActiveTasks => { + if app.error.is_some() { + actions::clean_err_msg(app); + } else { + let selected = app.state.spaces_tree_state.selected().to_vec(); + TreeState::toggle(&mut app.state.spaces_tree_state, selected); + } + } + _ => actions::clean_err_msg(app), + }, _ => {} }, } diff --git a/src/models/task.rs b/src/models/task.rs index 8340c92..3152c8e 100644 --- a/src/models/task.rs +++ b/src/models/task.rs @@ -18,7 +18,7 @@ pub struct Task { } impl Task { - pub fn new(title: impl Into) -> Self { + pub fn new(title: impl Into, space_id: String) -> Self { Task { id: Uuid::new_v4(), title: title.into(), @@ -29,7 +29,7 @@ impl Task { created_at: Utc::now(), updated_at: None, archived_at: None, - space_id: None, + space_id: Some(Uuid::parse_str(&space_id).unwrap()), } } diff --git a/src/state.rs b/src/state.rs index ebb4782..dc20369 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,5 +1,6 @@ use ratatui::widgets::ListState; use tui_input::Input; +use tui_tree_widget::TreeState; use uuid::Uuid; /// The application global state @@ -13,6 +14,9 @@ pub struct AppState { /// State of the focus pane pub active_panel: PanelState, + /// State of the spaces tree (identifier = UUID as String) + pub spaces_tree_state: TreeState, + /// State of the current active modal (CreateTask, EditTask, ArchivedTask, DeleteTask) pub active_modal: Option, } @@ -27,6 +31,7 @@ pub enum PanelState { pub enum ModalState { CreateTask { input: Input, + space_id: String, }, EditTask { task_id: Uuid, @@ -48,6 +53,10 @@ pub enum ModalState { CreateSpace { input: Input, }, + DeleteSpace { + space_id: Uuid, + selected_option: ListState, + }, } impl AppState { @@ -58,11 +67,15 @@ impl AppState { let mut archived_tasks_state = ListState::default(); archived_tasks_state.select(Some(0)); + let mut spaces_tree_state = TreeState::default(); + spaces_tree_state.select_first(); + AppState { active_tasks_state, archived_tasks_state, active_panel: PanelState::ActiveTasks, active_modal: None, + spaces_tree_state, } } @@ -100,9 +113,10 @@ impl AppState { } } - pub fn open_create_task(&mut self) { + pub fn open_create_task(&mut self, space_id: String) { self.active_modal = Some(ModalState::CreateTask { input: Input::default(), + space_id, }) } @@ -158,4 +172,13 @@ impl AppState { pub fn close_modal(&mut self) { self.active_modal = None } + + pub fn open_delete_space(&mut self, space_id: Uuid) { + let mut option_list_state = ListState::default(); + option_list_state.select(Some(0)); + self.active_modal = Some(ModalState::DeleteSpace { + space_id, + selected_option: option_list_state, + }) + } } From 73e5be883b793a0dfdcd0317df697f14dd247978 Mon Sep 17 00:00:00 2001 From: Simon Cardona Date: Sat, 31 Jan 2026 00:33:58 +0100 Subject: [PATCH 08/15] feat: add archive space --- src/actions/archive_space.rs | 47 +++++++++++++++++++ src/actions/mod.rs | 4 ++ src/actions/open_archive_modal.rs | 31 +++++++++--- src/actions/open_archive_space_modal.rs | 22 +++++++++ src/app.rs | 7 +++ src/components/modals/archive_space.rs | 23 +++++++++ src/components/modals/mod.rs | 1 + .../workspace/sidebar/active_spaces.rs | 2 +- src/db/repositories/space.rs | 20 ++++++++ src/keybindings.rs | 30 +++++++++++- src/state.rs | 15 ++++++ 11 files changed, 194 insertions(+), 8 deletions(-) create mode 100644 src/actions/archive_space.rs create mode 100644 src/actions/open_archive_space_modal.rs create mode 100644 src/components/modals/archive_space.rs diff --git a/src/actions/archive_space.rs b/src/actions/archive_space.rs new file mode 100644 index 0000000..263d8d6 --- /dev/null +++ b/src/actions/archive_space.rs @@ -0,0 +1,47 @@ +use chrono::Utc; +use uuid::Uuid; + +use crate::{ + app::App, + db::repositories::{SpaceRepository, TaskRepository}, +}; + +pub fn archive_space(app: &mut App, option_idx: Option, space_id: Uuid) { + if option_idx != Some(0) { + return; + } + + let space = match app.spaces.iter_mut().find(|s| s.id == space_id) { + Some(s) => s, + None => return, + }; + + space.archived = !space.archived; + space.archived_at = if space.archived { + Some(Utc::now()) + } else { + None + }; + space.updated_at = Some(Utc::now()); + + if let Err(e) = SpaceRepository::update(&app.db.connection, space) { + app.error = Some(e.to_string()); + return; + } + + let is_archived = space.archived; + + for task in app.tasks.iter_mut().filter(|t| t.space_id == Some(space_id)) { + task.archived = is_archived; + task.archived_at = if is_archived { Some(Utc::now()) } else { None }; + task.updated_at = Some(Utc::now()); + + if let Err(e) = TaskRepository::update(&app.db.connection, task) { + app.error = Some(e.to_string()); + return; + } + } + + app.selected_tasks.clear(); + app.state.spaces_tree_state.select_first(); +} diff --git a/src/actions/mod.rs b/src/actions/mod.rs index 6de176d..012c6b6 100644 --- a/src/actions/mod.rs +++ b/src/actions/mod.rs @@ -1,3 +1,4 @@ +pub mod archive_space; pub mod clean_err_msg; pub mod close_modal; pub mod create_space; @@ -8,6 +9,7 @@ pub mod edit_priority; pub mod edit_task; pub mod edit_title; pub mod open_archive_modal; +pub mod open_archive_space_modal; pub mod open_create_space_modal; pub mod open_create_task_modal; pub mod open_delete_modal; @@ -22,6 +24,7 @@ pub mod toggle_archive_task; pub mod toggle_task_completion; pub mod toggle_task_selection; +pub use archive_space::archive_space; pub use clean_err_msg::clean_err_msg; pub use close_modal::close_modal; pub use create_space::create_space; @@ -32,6 +35,7 @@ pub use edit_priority::edit_priority; pub use edit_task::edit_task; pub use edit_title::edit_title; pub use open_archive_modal::open_archive_modal; +pub use open_archive_space_modal::open_archive_space_modal; pub use open_create_space_modal::open_create_space_modal; pub use open_create_task_modal::open_create_task_modal; pub use open_delete_modal::open_delete_modal; diff --git a/src/actions/open_archive_modal.rs b/src/actions/open_archive_modal.rs index 464b944..d7ab62c 100644 --- a/src/actions/open_archive_modal.rs +++ b/src/actions/open_archive_modal.rs @@ -1,15 +1,34 @@ +use uuid::Uuid; + use crate::{app::App, state::PanelState}; pub fn open_archive_modal(app: &mut App) { let is_archived = app.state.active_panel == PanelState::ArchivedTasks; - if app.selected_tasks.is_empty() { - if let Some(task_index) = app.state.get_selected_panel_state().and_then(|s| s.selected()) { - let task_id = app.get_current_tasks()[task_index].id; - app.state.open_archived_task(vec![task_id], is_archived) - } - } else { + if !app.selected_tasks.is_empty() { app.state .open_archived_task(app.selected_tasks.clone(), is_archived); + return; + } + + if app.state.active_panel == PanelState::ActiveTasks { + let selected = app.state.spaces_tree_state.selected(); + if selected.is_empty() { + return; + } + + let selected_id = selected.last().unwrap(); + if let Ok(uuid) = Uuid::parse_str(selected_id) { + if app.tasks.iter().any(|t| t.id == uuid) { + app.state.open_archived_task(vec![uuid], is_archived); + } + } + } else if let Some(task_index) = app.state.get_selected_panel_state().and_then(|s| s.selected()) + { + let tasks = app.get_current_tasks(); + if task_index < tasks.len() { + app.state + .open_archived_task(vec![tasks[task_index].id], is_archived); + } } } diff --git a/src/actions/open_archive_space_modal.rs b/src/actions/open_archive_space_modal.rs new file mode 100644 index 0000000..ba24bbe --- /dev/null +++ b/src/actions/open_archive_space_modal.rs @@ -0,0 +1,22 @@ +use uuid::Uuid; + +use crate::{app::App, state::PanelState}; + +pub fn open_archive_space_modal(app: &mut App) { + if app.state.active_panel != PanelState::ActiveTasks { + return; + } + + let selected = app.state.spaces_tree_state.selected(); + if selected.len() != 1 { + return; + } + + let selected_id = &selected[0]; + + if let Ok(uuid) = Uuid::parse_str(selected_id) { + if let Some(space) = app.spaces.iter().find(|s| s.id == uuid) { + app.state.open_archive_space(uuid, space.archived); + } + } +} diff --git a/src/app.rs b/src/app.rs index f1afa81..1aaf55d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -108,6 +108,13 @@ impl App { }) => { components::modals::delete_space::render(frame, selected_option); } + Some(ModalState::ArchiveSpace { + space_id: _, + selected_option, + is_archived, + }) => { + components::modals::archive_space::render(frame, selected_option, *is_archived); + } None => {} } } diff --git a/src/components/modals/archive_space.rs b/src/components/modals/archive_space.rs new file mode 100644 index 0000000..6c9a6f6 --- /dev/null +++ b/src/components/modals/archive_space.rs @@ -0,0 +1,23 @@ +use ratatui::{ + Frame, + style::{Color, Modifier, Style}, + widgets::{List, ListItem, ListState}, +}; + +use crate::components::shared; + +pub fn render(frame: &mut Frame, selected_option: &mut ListState, is_archived: bool) { + let action_name = if is_archived { "Unarchive" } else { "Archive" }; + let area = shared::modal::Modal::new(format!("{} space", action_name)) + .height(4) + .render(frame); + let list_items: Vec = vec![ListItem::from(action_name), ListItem::from("Cancel")]; + let options = List::new(list_items).highlight_style( + Style::default() + .bg(Color::Blue) + .fg(Color::White) + .add_modifier(Modifier::BOLD), + ); + + frame.render_stateful_widget(options, area, selected_option); +} diff --git a/src/components/modals/mod.rs b/src/components/modals/mod.rs index 4c0c763..2eb53c8 100644 --- a/src/components/modals/mod.rs +++ b/src/components/modals/mod.rs @@ -1,3 +1,4 @@ +pub mod archive_space; pub mod archive_task; pub mod create_task; pub mod delete_space; diff --git a/src/components/workspace/sidebar/active_spaces.rs b/src/components/workspace/sidebar/active_spaces.rs index 84e4925..452d0a9 100644 --- a/src/components/workspace/sidebar/active_spaces.rs +++ b/src/components/workspace/sidebar/active_spaces.rs @@ -19,7 +19,7 @@ pub fn render(frame: &mut Frame, area: Rect, app: &mut App) { Color::White }; - for space in &app.spaces { + for space in app.spaces.iter().filter(|s| !s.archived) { let space_tasks: Vec> = app .tasks .iter() diff --git a/src/db/repositories/space.rs b/src/db/repositories/space.rs index 7741077..45982b6 100644 --- a/src/db/repositories/space.rs +++ b/src/db/repositories/space.rs @@ -54,6 +54,26 @@ impl SpaceRepository { }) } + pub fn update(connection: &Connection, space: &Space) -> Result<(), rusqlite::Error> { + connection.execute( + "UPDATE spaces SET + title = ?2, + archived = ?3, + updated_at = ?4, + archived_at = ?5 + WHERE id = ?1", + ( + space.id.to_string(), + &space.title, + space.archived as u32, + space.updated_at.map(|d| d.to_rfc3339()), + space.archived_at.map(|d| d.to_rfc3339()), + ), + )?; + + Ok(()) + } + pub fn delete(connection: &Connection, space_id: &Uuid) -> Result<(), rusqlite::Error> { let tx = connection.unchecked_transaction()?; diff --git a/src/keybindings.rs b/src/keybindings.rs index bb9101f..11c28ce 100644 --- a/src/keybindings.rs +++ b/src/keybindings.rs @@ -134,8 +134,36 @@ pub fn handle_key_event(app: &mut App, event: &Event, terminal: &mut DefaultTerm } _ => {} }, + Some(ModalState::ArchiveSpace { + space_id, + selected_option, + is_archived: _, + }) => match key.code { + crossterm::event::KeyCode::Esc => actions::close_modal(app), + crossterm::event::KeyCode::Enter => { + let option_idx = selected_option.selected(); + let space_id = *space_id; + + actions::archive_space(app, option_idx, space_id); + actions::close_modal(app); + } + crossterm::event::KeyCode::Char('j') => { + selected_option.select_next(); + } + crossterm::event::KeyCode::Char('k') => { + selected_option.select_previous(); + } + _ => {} + }, None => match key.code { - crossterm::event::KeyCode::Char('a') => actions::open_archive_modal(app), + crossterm::event::KeyCode::Char('a') => { + if app.state.active_panel == PanelState::ActiveTasks { + actions::open_archive_space_modal(app); + } + if app.state.active_modal.is_none() { + actions::open_archive_modal(app); + } + } crossterm::event::KeyCode::Char('c') => actions::open_create_task_modal(app), crossterm::event::KeyCode::Char('s') => actions::open_create_space_modal(app), crossterm::event::KeyCode::Char('e') => actions::open_edit_title_modal(app), diff --git a/src/state.rs b/src/state.rs index dc20369..478ba2b 100644 --- a/src/state.rs +++ b/src/state.rs @@ -57,6 +57,11 @@ pub enum ModalState { space_id: Uuid, selected_option: ListState, }, + ArchiveSpace { + space_id: Uuid, + selected_option: ListState, + is_archived: bool, + }, } impl AppState { @@ -181,4 +186,14 @@ impl AppState { selected_option: option_list_state, }) } + + pub fn open_archive_space(&mut self, space_id: Uuid, is_archived: bool) { + let mut option_list_state = ListState::default(); + option_list_state.select(Some(0)); + self.active_modal = Some(ModalState::ArchiveSpace { + space_id, + selected_option: option_list_state, + is_archived, + }) + } } From 1e6d369bfaa36c94ecc63f95ca8f96d6a7b24105 Mon Sep 17 00:00:00 2001 From: Simon Cardona Date: Sat, 31 Jan 2026 00:47:33 +0100 Subject: [PATCH 09/15] feat: add move task between space --- src/actions/mod.rs | 4 +++ src/actions/move_task.rs | 29 +++++++++++++++++++ src/actions/open_move_task_modal.rs | 22 ++++++++++++++ src/app.rs | 14 +++++++-- src/components/modals/mod.rs | 1 + src/components/modals/move_task.rs | 29 +++++++++++++++++++ .../workspace/sidebar/active_spaces.rs | 8 +++-- src/keybindings.rs | 22 ++++++++++++++ src/models/space.rs | 1 + src/state.rs | 13 +++++++++ 10 files changed, 138 insertions(+), 5 deletions(-) create mode 100644 src/actions/move_task.rs create mode 100644 src/actions/open_move_task_modal.rs create mode 100644 src/components/modals/move_task.rs diff --git a/src/actions/mod.rs b/src/actions/mod.rs index 012c6b6..ec66d8f 100644 --- a/src/actions/mod.rs +++ b/src/actions/mod.rs @@ -8,6 +8,7 @@ pub mod delete_task; pub mod edit_priority; pub mod edit_task; pub mod edit_title; +pub mod move_task; pub mod open_archive_modal; pub mod open_archive_space_modal; pub mod open_create_space_modal; @@ -15,6 +16,7 @@ pub mod open_create_task_modal; pub mod open_delete_modal; pub mod open_delete_space_modal; pub mod open_edit_title_modal; +pub mod open_move_task_modal; pub mod open_priority_modal; pub mod quit; pub mod select_next_task; @@ -34,6 +36,7 @@ pub use delete_task::delete_task; pub use edit_priority::edit_priority; pub use edit_task::edit_task; pub use edit_title::edit_title; +pub use move_task::move_task; pub use open_archive_modal::open_archive_modal; pub use open_archive_space_modal::open_archive_space_modal; pub use open_create_space_modal::open_create_space_modal; @@ -41,6 +44,7 @@ pub use open_create_task_modal::open_create_task_modal; pub use open_delete_modal::open_delete_modal; pub use open_delete_space_modal::open_delete_space_modal; pub use open_edit_title_modal::open_edit_title_modal; +pub use open_move_task_modal::open_move_task_modal; pub use open_priority_modal::open_priority_modal; pub use quit::quit; pub use select_next_task::select_next_task; diff --git a/src/actions/move_task.rs b/src/actions/move_task.rs new file mode 100644 index 0000000..51e8fd5 --- /dev/null +++ b/src/actions/move_task.rs @@ -0,0 +1,29 @@ +use chrono::Utc; +use uuid::Uuid; + +use crate::{app::App, db::repositories::TaskRepository, models::Space}; + +pub fn move_task(app: &mut App, option_idx: Option, task_id: Uuid, spaces: &[Space]) { + let idx = match option_idx { + Some(i) => i, + None => return, + }; + + let new_space_id = if idx >= spaces.len() { + None + } else { + spaces.get(idx).map(|s| s.id) + }; + + let task = match app.tasks.iter_mut().find(|t| t.id == task_id) { + Some(t) => t, + None => return, + }; + + task.space_id = new_space_id; + task.updated_at = Some(Utc::now()); + + if let Err(e) = TaskRepository::update(&app.db.connection, task) { + app.error = Some(e.to_string()); + } +} diff --git a/src/actions/open_move_task_modal.rs b/src/actions/open_move_task_modal.rs new file mode 100644 index 0000000..0e27b5a --- /dev/null +++ b/src/actions/open_move_task_modal.rs @@ -0,0 +1,22 @@ +use uuid::Uuid; + +use crate::{app::App, state::PanelState}; + +pub fn open_move_task_modal(app: &mut App) { + if app.state.active_panel != PanelState::ActiveTasks { + return; + } + + let selected = app.state.spaces_tree_state.selected(); + if selected.is_empty() { + return; + } + + let selected_id = selected.last().unwrap(); + + if let Ok(uuid) = Uuid::parse_str(selected_id) { + if app.tasks.iter().any(|t| t.id == uuid && !t.archived) { + app.state.open_move_task(uuid); + } + } +} diff --git a/src/app.rs b/src/app.rs index 1aaf55d..3827279 100644 --- a/src/app.rs +++ b/src/app.rs @@ -43,7 +43,7 @@ impl App { Err(err) => (vec![], Some(err.to_string())), }; - App { + let mut app = App { exit: false, selected_tasks: Vec::new(), state, @@ -51,7 +51,10 @@ impl App { tasks, spaces, error: space_err, - } + }; + + app.state.spaces_tree_state.select_first(); + app } pub fn run(&mut self, terminal: &mut DefaultTerminal) -> Result<()> { @@ -115,6 +118,13 @@ impl App { }) => { components::modals::archive_space::render(frame, selected_option, *is_archived); } + Some(ModalState::MoveTask { + task_id: _, + selected_option, + }) => { + let spaces: Vec<_> = self.spaces.iter().filter(|s| !s.archived).cloned().collect(); + components::modals::move_task::render(frame, selected_option, &spaces); + } None => {} } } diff --git a/src/components/modals/mod.rs b/src/components/modals/mod.rs index 2eb53c8..cfa5d2e 100644 --- a/src/components/modals/mod.rs +++ b/src/components/modals/mod.rs @@ -4,4 +4,5 @@ pub mod create_task; pub mod delete_space; pub mod delete_task; pub mod edit_task; +pub mod move_task; pub mod priority_task; diff --git a/src/components/modals/move_task.rs b/src/components/modals/move_task.rs new file mode 100644 index 0000000..a923e44 --- /dev/null +++ b/src/components/modals/move_task.rs @@ -0,0 +1,29 @@ +use ratatui::{ + Frame, + style::{Color, Modifier, Style}, + widgets::{List, ListItem, ListState}, +}; + +use crate::{components::shared, models::Space}; + +pub fn render(frame: &mut Frame, selected_option: &mut ListState, spaces: &[Space]) { + let height = (spaces.len() + 3).max(3).min(10) as u16; + let area = shared::modal::Modal::new("Move to space") + .height(height) + .render(frame); + + let mut list_items: Vec = spaces + .iter() + .map(|s| ListItem::from(s.title.clone())) + .collect(); + list_items.push(ListItem::from("No space")); + + let options = List::new(list_items).highlight_style( + Style::default() + .bg(Color::Blue) + .fg(Color::White) + .add_modifier(Modifier::BOLD), + ); + + frame.render_stateful_widget(options, area, selected_option); +} diff --git a/src/components/workspace/sidebar/active_spaces.rs b/src/components/workspace/sidebar/active_spaces.rs index 452d0a9..2af12bd 100644 --- a/src/components/workspace/sidebar/active_spaces.rs +++ b/src/components/workspace/sidebar/active_spaces.rs @@ -52,11 +52,13 @@ pub fn render(frame: &mut Frame, area: Rect, app: &mut App) { .border_type(BorderType::Rounded) .border_style(border_color), ) - .highlight_style( + .highlight_style(if is_active { Style::default() .bg(Color::Blue) - .add_modifier(Modifier::BOLD), - ) + .add_modifier(Modifier::BOLD) + } else { + Style::default() + }) .node_closed_symbol("▶ ") .node_open_symbol("▼ ") .node_no_children_symbol(" "); diff --git a/src/keybindings.rs b/src/keybindings.rs index 11c28ce..df7ed36 100644 --- a/src/keybindings.rs +++ b/src/keybindings.rs @@ -155,6 +155,27 @@ pub fn handle_key_event(app: &mut App, event: &Event, terminal: &mut DefaultTerm } _ => {} }, + Some(ModalState::MoveTask { + task_id, + selected_option, + }) => match key.code { + crossterm::event::KeyCode::Esc => actions::close_modal(app), + crossterm::event::KeyCode::Enter => { + let option_idx = selected_option.selected(); + let task_id = *task_id; + let spaces: Vec<_> = app.spaces.iter().filter(|s| !s.archived).cloned().collect(); + + actions::move_task(app, option_idx, task_id, &spaces); + actions::close_modal(app); + } + crossterm::event::KeyCode::Char('j') => { + selected_option.select_next(); + } + crossterm::event::KeyCode::Char('k') => { + selected_option.select_previous(); + } + _ => {} + }, None => match key.code { crossterm::event::KeyCode::Char('a') => { if app.state.active_panel == PanelState::ActiveTasks { @@ -171,6 +192,7 @@ pub fn handle_key_event(app: &mut App, event: &Event, terminal: &mut DefaultTerm crossterm::event::KeyCode::Char('E') => actions::edit_task(app, terminal), crossterm::event::KeyCode::Char('y') => actions::toggle_task_completion(app), crossterm::event::KeyCode::Char('q') => actions::quit(app), + crossterm::event::KeyCode::Char('m') => actions::open_move_task_modal(app), crossterm::event::KeyCode::Char('d') => { if app.state.active_panel == PanelState::ActiveTasks { actions::open_delete_space_modal(app); diff --git a/src/models/space.rs b/src/models/space.rs index da08572..d437b0c 100644 --- a/src/models/space.rs +++ b/src/models/space.rs @@ -1,6 +1,7 @@ use chrono::{DateTime, Utc}; use uuid::Uuid; +#[derive(Clone)] pub struct Space { pub id: Uuid, pub title: String, diff --git a/src/state.rs b/src/state.rs index 478ba2b..eab0a73 100644 --- a/src/state.rs +++ b/src/state.rs @@ -62,6 +62,10 @@ pub enum ModalState { selected_option: ListState, is_archived: bool, }, + MoveTask { + task_id: Uuid, + selected_option: ListState, + }, } impl AppState { @@ -196,4 +200,13 @@ impl AppState { is_archived, }) } + + pub fn open_move_task(&mut self, task_id: Uuid) { + let mut option_list_state = ListState::default(); + option_list_state.select(Some(0)); + self.active_modal = Some(ModalState::MoveTask { + task_id, + selected_option: option_list_state, + }) + } } From 0b96addf87df26b66fe7cfe302726ee26a356e04 Mon Sep 17 00:00:00 2001 From: Simon Cardona Date: Sat, 31 Jan 2026 01:10:02 +0100 Subject: [PATCH 10/15] fix: rename space for workspace --- Cargo.lock | 889 +++++++++++++++--- Cargo.toml | 6 +- ...{archive_space.rs => archive_workspace.rs} | 20 +- src/actions/create_space.rs | 18 - src/actions/create_task.rs | 4 +- src/actions/create_workspace.rs | 18 + src/actions/delete_space.rs | 19 - src/actions/delete_workspace.rs | 19 + src/actions/mod.rs | 24 +- src/actions/move_task.rs | 10 +- src/actions/open_archive_modal.rs | 2 +- ...dal.rs => open_archive_workspace_modal.rs} | 8 +- src/actions/open_create_task_modal.rs | 4 +- ...odal.rs => open_create_workspace_modal.rs} | 4 +- src/actions/open_delete_modal.rs | 2 +- ...odal.rs => open_delete_workspace_modal.rs} | 8 +- src/actions/open_move_task_modal.rs | 2 +- src/app.rs | 39 +- ...{archive_space.rs => archive_workspace.rs} | 2 +- .../{delete_space.rs => delete_workspace.rs} | 2 +- src/components/modals/mod.rs | 4 +- src/components/modals/move_task.rs | 14 +- ...{active_spaces.rs => active_workspaces.rs} | 24 +- src/components/workspace/sidebar/mod.rs | 4 +- src/db/connection.rs | 4 +- src/db/repositories/mod.rs | 4 +- src/db/repositories/space.rs | 90 -- src/db/repositories/task.rs | 10 +- src/db/repositories/workspace.rs | 93 ++ src/db/schema/tasks.sql | 4 +- src/db/schema/{spaces.sql => workspaces.sql} | 3 +- src/keybindings.rs | 44 +- src/models/mod.rs | 4 +- src/models/task.rs | 6 +- src/models/{space.rs => workspace.rs} | 8 +- src/state.rs | 42 +- 36 files changed, 1026 insertions(+), 432 deletions(-) rename src/actions/{archive_space.rs => archive_workspace.rs} (50%) delete mode 100644 src/actions/create_space.rs create mode 100644 src/actions/create_workspace.rs delete mode 100644 src/actions/delete_space.rs create mode 100644 src/actions/delete_workspace.rs rename src/actions/{open_archive_space_modal.rs => open_archive_workspace_modal.rs} (52%) rename src/actions/{open_create_space_modal.rs => open_create_workspace_modal.rs} (53%) rename src/actions/{open_delete_space_modal.rs => open_delete_workspace_modal.rs} (57%) rename src/components/modals/{archive_space.rs => archive_workspace.rs} (89%) rename src/components/modals/{delete_space.rs => delete_workspace.rs} (91%) rename src/components/workspace/sidebar/{active_spaces.rs => active_workspaces.rs} (71%) delete mode 100644 src/db/repositories/space.rs create mode 100644 src/db/repositories/workspace.rs rename src/db/schema/{spaces.sql => workspaces.sql} (80%) rename src/models/{space.rs => workspace.rs} (87%) diff --git a/Cargo.lock b/Cargo.lock index 028bde7..872879a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -17,18 +26,69 @@ dependencies = [ "libc", ] +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "atomic" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" +dependencies = [ + "bytemuck", +] + [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.19.1" @@ -36,10 +96,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] -name = "cassowary" -version = "0.3.0" +name = "bytemuck" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" [[package]] name = "castaway" @@ -66,6 +126,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.42" @@ -79,20 +145,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "compact_str" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" -dependencies = [ - "castaway", - "cfg-if", - "itoa", - "rustversion", - "ryu", - "static_assertions", -] - [[package]] name = "compact_str" version = "0.9.0" @@ -122,13 +174,22 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "crossterm" version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags", + "bitflags 2.10.0", "crossterm_winapi", "mio", "parking_lot", @@ -144,7 +205,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags", + "bitflags 2.10.0", "crossterm_winapi", "derive_more", "document-features", @@ -165,6 +226,26 @@ dependencies = [ "winapi", ] +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "csscolorparser" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf" +dependencies = [ + "lab", + "phf", +] + [[package]] name = "darling" version = "0.20.11" @@ -186,7 +267,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.111", ] [[package]] @@ -197,9 +278,15 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn", + "syn 2.0.111", ] +[[package]] +name = "deltae" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4" + [[package]] name = "deranged" version = "0.5.5" @@ -228,7 +315,17 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn", + "syn 2.0.111", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", ] [[package]] @@ -283,6 +380,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "euclid" +version = "0.22.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df61bf483e837f88d5c2291dcf55c67be7e676b3a51acc48db3a7b163b91ed63" +dependencies = [ + "num-traits", +] + [[package]] name = "fallible-iterator" version = "0.3.0" @@ -295,12 +401,45 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set", + "regex", +] + +[[package]] +name = "filedescriptor" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + [[package]] name = "find-msvc-tools" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +[[package]] +name = "finl_unicode" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9844ddc3a6e533d62bba727eb6c28b5d360921d5175e9ff0f1e621a5c590a4d5" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "fnv" version = "1.0.7" @@ -309,15 +448,19 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" -version = "0.1.5" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] -name = "foldhash" -version = "0.2.0" +name = "generic-array" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] [[package]] name = "getrandom" @@ -342,17 +485,6 @@ dependencies = [ "wasip2", ] -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.1.5", -] - [[package]] name = "hashbrown" version = "0.16.1" @@ -361,7 +493,7 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", - "foldhash 0.2.0", + "foldhash", ] [[package]] @@ -370,7 +502,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230" dependencies = [ - "hashbrown 0.16.1", + "hashbrown", ] [[package]] @@ -379,6 +511,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "iana-time-zone" version = "0.1.64" @@ -428,16 +566,7 @@ dependencies = [ "indoc", "proc-macro2", "quote", - "syn", -] - -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", + "syn 2.0.111", ] [[package]] @@ -471,16 +600,29 @@ version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fe90c1150662e858c7d5f945089b7517b0a80d8bf7ba4b1b5ffc984e7230a5b" dependencies = [ - "hashbrown 0.16.1", - "thiserror", + "hashbrown", + "portable-atomic", + "thiserror 2.0.17", ] +[[package]] +name = "lab" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "lazytasks" version = "0.1.0" dependencies = [ "chrono", - "crossterm 0.29.0", + "crossterm 0.28.1", "dirs", "ratatui", "rusqlite", @@ -501,7 +643,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df15f6eac291ed1cf25865b1ee60399f57e7c227e7f51bdbd4c5270396a9ed50" dependencies = [ - "bitflags", + "bitflags 2.10.0", "libc", ] @@ -522,7 +664,7 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f4de44e98ddbf09375cbf4d17714d18f39195f4f4894e8524501726fd9a8a4a" dependencies = [ - "bitflags", + "bitflags 2.10.0", ] [[package]] @@ -560,22 +702,50 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lru" -version = "0.12.5" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" dependencies = [ - "hashbrown 0.15.5", + "hashbrown", ] [[package]] -name = "lru" -version = "0.16.3" +name = "mac_address" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" +checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" dependencies = [ - "hashbrown 0.16.1", + "nix", + "winapi", ] +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "memmem" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "mio" version = "1.1.1" @@ -588,12 +758,46 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "num-conv" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -603,6 +807,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -615,6 +828,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + [[package]] name = "parking_lot" version = "0.12.5" @@ -639,10 +861,99 @@ dependencies = [ ] [[package]] -name = "paste" -version = "1.0.15" +name = "pest" +version = "2.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +checksum = "2c9eb05c21a464ea704b53158d358a31e6425db2f63a1a7312268b05fe2b75f7" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f9dbced329c441fa79d80472764b1a2c7e57123553b8519b36663a2fb234ed" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bb96d5051a78f44f43c8f712d8e810adb0ebf923fc9ed2655a7f66f63ba8ee5" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "pest_meta" +version = "2.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "602113b5b5e8621770cfd490cfd90b9f84ab29bd2b0e49ad83eb6d186cef2365" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] [[package]] name = "pkg-config" @@ -650,6 +961,12 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "portable-atomic" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" + [[package]] name = "powerfmt" version = "0.2.0" @@ -680,25 +997,33 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + [[package]] name = "ratatui" -version = "0.29.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +checksum = "d1ce67fb8ba4446454d1c8dbaeda0557ff5e94d39d5e5ed7f10a65eb4c8266bc" dependencies = [ - "bitflags", - "cassowary", - "compact_str 0.8.1", - "crossterm 0.28.1", - "indoc", "instability", - "itertools 0.13.0", - "lru 0.12.5", - "paste", - "strum 0.26.3", - "unicode-segmentation", - "unicode-truncate 1.1.0", - "unicode-width 0.2.0", + "ratatui-core", + "ratatui-crossterm", + "ratatui-macros", + "ratatui-termwiz", + "ratatui-widgets", ] [[package]] @@ -707,18 +1032,50 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293" dependencies = [ - "bitflags", - "compact_str 0.9.0", - "hashbrown 0.16.1", + "bitflags 2.10.0", + "compact_str", + "hashbrown", "indoc", - "itertools 0.14.0", + "itertools", "kasuari", - "lru 0.16.3", - "strum 0.27.2", - "thiserror", + "lru", + "strum", + "thiserror 2.0.17", "unicode-segmentation", - "unicode-truncate 2.0.1", - "unicode-width 0.2.0", + "unicode-truncate", + "unicode-width", +] + +[[package]] +name = "ratatui-crossterm" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "577c9b9f652b4c121fb25c6a391dd06406d3b092ba68827e6d2f09550edc54b3" +dependencies = [ + "cfg-if", + "crossterm 0.29.0", + "instability", + "ratatui-core", +] + +[[package]] +name = "ratatui-macros" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7f1342a13e83e4bb9d0b793d0ea762be633f9582048c892ae9041ef39c936f4" +dependencies = [ + "ratatui-core", + "ratatui-widgets", +] + +[[package]] +name = "ratatui-termwiz" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f76fe0bd0ed4295f0321b1676732e2454024c15a35d01904ddb315afd3d545c" +dependencies = [ + "ratatui-core", + "termwiz", ] [[package]] @@ -727,17 +1084,17 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7dbfa023cd4e604c2553483820c5fe8aa9d71a42eea5aa77c6e7f35756612db" dependencies = [ - "bitflags", - "hashbrown 0.16.1", + "bitflags 2.10.0", + "hashbrown", "indoc", "instability", - "itertools 0.14.0", + "itertools", "line-clipping", "ratatui-core", - "strum 0.27.2", + "strum", "time", "unicode-segmentation", - "unicode-width 0.2.0", + "unicode-width", ] [[package]] @@ -746,7 +1103,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.10.0", ] [[package]] @@ -757,16 +1114,45 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.16", "libredox", - "thiserror", + "thiserror 2.0.17", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", ] +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + [[package]] name = "rusqlite" version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1c93dd1c9683b438c392c492109cb702b8090b2bfc8fed6f6e4eb4523f17af3" dependencies = [ - "bitflags", + "bitflags 2.10.0", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -790,7 +1176,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -803,7 +1189,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys 0.11.0", @@ -834,6 +1220,47 @@ version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" @@ -870,6 +1297,12 @@ dependencies = [ "libc", ] +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "smallvec" version = "1.15.1" @@ -883,9 +1316,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05e98301bf8b0540c7de45ecd760539b9c62f5772aed172f08efba597c11cd5d" dependencies = [ "cc", - "hashbrown 0.16.1", + "hashbrown", "js-sys", - "thiserror", + "thiserror 2.0.17", "wasm-bindgen", ] @@ -901,47 +1334,36 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" -[[package]] -name = "strum" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" -dependencies = [ - "strum_macros 0.26.4", -] - [[package]] name = "strum" version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" dependencies = [ - "strum_macros 0.27.2", + "strum_macros", ] [[package]] name = "strum_macros" -version = "0.26.4" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" dependencies = [ "heck", "proc-macro2", "quote", - "rustversion", - "syn", + "syn 2.0.111", ] [[package]] -name = "strum_macros" -version = "0.27.2" +name = "syn" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ - "heck", "proc-macro2", "quote", - "syn", + "unicode-ident", ] [[package]] @@ -955,13 +1377,96 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "terminfo" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662" +dependencies = [ + "fnv", + "nom", + "phf", + "phf_codegen", +] + +[[package]] +name = "termios" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" +dependencies = [ + "libc", +] + +[[package]] +name = "termwiz" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" +dependencies = [ + "anyhow", + "base64", + "bitflags 2.10.0", + "fancy-regex", + "filedescriptor", + "finl_unicode", + "fixedbitset", + "hex", + "lazy_static", + "libc", + "log", + "memmem", + "nix", + "num-derive", + "num-traits", + "ordered-float", + "pest", + "pest_derive", + "phf", + "sha2", + "signal-hook", + "siphasher", + "terminfo", + "termios", + "thiserror 1.0.69", + "ucd-trie", + "unicode-segmentation", + "vtparse", + "wezterm-bidi", + "wezterm-blob-leases", + "wezterm-color-types", + "wezterm-dynamic", + "wezterm-input-types", + "winapi", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", ] [[package]] @@ -972,7 +1477,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -982,8 +1487,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9da98b7d9b7dad93488a84b8248efc35352b0b2657397d4167e7ad67e5d535e5" dependencies = [ "deranged", + "libc", "num-conv", + "num_threads", "powerfmt", + "serde_core", "time-core", ] @@ -995,12 +1503,12 @@ checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "tui-input" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "911e93158bf80bbc94bad533b2b16e3d711e1132d69a6a6980c3920a63422c19" +checksum = "79c1ee964298f136020f5f69e0e601f4d3a1f610a7baf1af9fcb96152e8a2c45" dependencies = [ "ratatui", - "unicode-width 0.2.0", + "unicode-width", ] [[package]] @@ -1011,9 +1519,21 @@ checksum = "deca119555009eee2e0cfb9c020f39f632444dc4579918d5fc009d51d75dff92" dependencies = [ "ratatui-core", "ratatui-widgets", - "unicode-width 0.2.0", + "unicode-width", ] +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "unicode-ident" version = "1.0.22" @@ -1026,39 +1546,28 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" -[[package]] -name = "unicode-truncate" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" -dependencies = [ - "itertools 0.13.0", - "unicode-segmentation", - "unicode-width 0.1.14", -] - [[package]] name = "unicode-truncate" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" dependencies = [ - "itertools 0.14.0", + "itertools", "unicode-segmentation", - "unicode-width 0.2.0", + "unicode-width", ] [[package]] name = "unicode-width" -version = "0.1.14" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] -name = "unicode-width" -version = "0.2.0" +name = "utf8parse" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" @@ -1066,6 +1575,7 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ + "atomic", "getrandom 0.3.4", "js-sys", "wasm-bindgen", @@ -1077,6 +1587,21 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vtparse" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0" +dependencies = [ + "utf8parse", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -1124,7 +1649,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.111", "wasm-bindgen-shared", ] @@ -1137,6 +1662,78 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wezterm-bidi" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0a6e355560527dd2d1cf7890652f4f09bb3433b6aadade4c9b5ed76de5f3ec" +dependencies = [ + "log", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-blob-leases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7" +dependencies = [ + "getrandom 0.3.4", + "mac_address", + "sha2", + "thiserror 1.0.69", + "uuid", +] + +[[package]] +name = "wezterm-color-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296" +dependencies = [ + "csscolorparser", + "deltae", + "lazy_static", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-dynamic" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac" +dependencies = [ + "log", + "ordered-float", + "strsim", + "thiserror 1.0.69", + "wezterm-dynamic-derive", +] + +[[package]] +name = "wezterm-dynamic-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c0cf2d539c645b448eaffec9ec494b8b19bd5077d9e58cb1ae7efece8d575b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "wezterm-input-types" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7012add459f951456ec9d6c7e6fc340b1ce15d6fc9629f8c42853412c029e57e" +dependencies = [ + "bitflags 1.3.2", + "euclid", + "lazy_static", + "serde", + "wezterm-dynamic", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1180,7 +1777,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -1191,7 +1788,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index ddeb0f2..5245b64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,10 +5,10 @@ edition = "2024" [dependencies] chrono = "0.4.42" -crossterm = "0.29.0" +crossterm = "0.28.1" dirs = "6.0.0" -ratatui = "0.29.0" -tui-input = "0.14.0" +ratatui = "0.30.0" +tui-input = "0.15.0" uuid = { version = "1.19.0", features = ["v4"] } rusqlite = { version = "0.38.0", features = ["bundled"] } tui-tree-widget = "0.24.0" diff --git a/src/actions/archive_space.rs b/src/actions/archive_workspace.rs similarity index 50% rename from src/actions/archive_space.rs rename to src/actions/archive_workspace.rs index 263d8d6..3555b59 100644 --- a/src/actions/archive_space.rs +++ b/src/actions/archive_workspace.rs @@ -3,35 +3,35 @@ use uuid::Uuid; use crate::{ app::App, - db::repositories::{SpaceRepository, TaskRepository}, + db::repositories::{WorkspaceRepository, TaskRepository}, }; -pub fn archive_space(app: &mut App, option_idx: Option, space_id: Uuid) { +pub fn archive_workspace(app: &mut App, option_idx: Option, workspace_id: Uuid) { if option_idx != Some(0) { return; } - let space = match app.spaces.iter_mut().find(|s| s.id == space_id) { + let workspace = match app.workspaces.iter_mut().find(|s| s.id == workspace_id) { Some(s) => s, None => return, }; - space.archived = !space.archived; - space.archived_at = if space.archived { + workspace.archived = !workspace.archived; + workspace.archived_at = if workspace.archived { Some(Utc::now()) } else { None }; - space.updated_at = Some(Utc::now()); + workspace.updated_at = Some(Utc::now()); - if let Err(e) = SpaceRepository::update(&app.db.connection, space) { + if let Err(e) = WorkspaceRepository::update(&app.db.connection, workspace) { app.error = Some(e.to_string()); return; } - let is_archived = space.archived; + let is_archived = workspace.archived; - for task in app.tasks.iter_mut().filter(|t| t.space_id == Some(space_id)) { + for task in app.tasks.iter_mut().filter(|t| t.workspace_id == Some(workspace_id)) { task.archived = is_archived; task.archived_at = if is_archived { Some(Utc::now()) } else { None }; task.updated_at = Some(Utc::now()); @@ -43,5 +43,5 @@ pub fn archive_space(app: &mut App, option_idx: Option, space_id: Uuid) { } app.selected_tasks.clear(); - app.state.spaces_tree_state.select_first(); + app.state.workspaces_tree_state.select_first(); } diff --git a/src/actions/create_space.rs b/src/actions/create_space.rs deleted file mode 100644 index 4d2bcc0..0000000 --- a/src/actions/create_space.rs +++ /dev/null @@ -1,18 +0,0 @@ -use crate::{app::App, db::repositories::SpaceRepository, models}; - -pub fn create_space(app: &mut App, title: String) { - let new_space = models::Space::new(title); - let space_id = new_space.id.to_string(); - - if let Err(e) = SpaceRepository::create(&app.db.connection, &new_space) { - app.error = Some(e.to_string()); - return; - }; - - let is_first_space = app.spaces.is_empty(); - app.spaces.push(new_space); - - if is_first_space { - app.state.spaces_tree_state.select(vec![space_id]); - } -} diff --git a/src/actions/create_task.rs b/src/actions/create_task.rs index 2089f3e..dec0e93 100644 --- a/src/actions/create_task.rs +++ b/src/actions/create_task.rs @@ -1,7 +1,7 @@ use crate::{app::App, db::repositories::TaskRepository, models}; -pub fn create_task(app: &mut App, title: String, space_id: String) { - let new_task = models::Task::new(title, space_id); +pub fn create_task(app: &mut App, title: String, workspace_id: String) { + let new_task = models::Task::new(title, workspace_id); if let Err(e) = TaskRepository::create(&app.db.connection, &new_task) { app.error = Some(e.to_string()); diff --git a/src/actions/create_workspace.rs b/src/actions/create_workspace.rs new file mode 100644 index 0000000..b47948b --- /dev/null +++ b/src/actions/create_workspace.rs @@ -0,0 +1,18 @@ +use crate::{app::App, db::repositories::WorkspaceRepository, models}; + +pub fn create_workspace(app: &mut App, title: String) { + let new_workspace = models::Workspace::new(title); + let workspace_id = new_workspace.id.to_string(); + + if let Err(e) = WorkspaceRepository::create(&app.db.connection, &new_workspace) { + app.error = Some(e.to_string()); + return; + }; + + let is_first_workspace = app.workspaces.is_empty(); + app.workspaces.push(new_workspace); + + if is_first_workspace { + app.state.workspaces_tree_state.select(vec![workspace_id]); + } +} diff --git a/src/actions/delete_space.rs b/src/actions/delete_space.rs deleted file mode 100644 index bfef5fb..0000000 --- a/src/actions/delete_space.rs +++ /dev/null @@ -1,19 +0,0 @@ -use uuid::Uuid; - -use crate::{app::App, db::repositories::SpaceRepository}; - -pub fn delete_space(app: &mut App, option_idx: Option, space_id: Uuid) { - if option_idx != Some(0) { - return; - } - - if let Err(e) = SpaceRepository::delete(&app.db.connection, &space_id) { - app.error = Some(e.to_string()); - return; - } - - app.tasks.retain(|t| t.space_id != Some(space_id)); - app.spaces.retain(|s| s.id != space_id); - app.selected_tasks.clear(); - app.state.spaces_tree_state.select_first(); -} diff --git a/src/actions/delete_workspace.rs b/src/actions/delete_workspace.rs new file mode 100644 index 0000000..423c076 --- /dev/null +++ b/src/actions/delete_workspace.rs @@ -0,0 +1,19 @@ +use uuid::Uuid; + +use crate::{app::App, db::repositories::WorkspaceRepository}; + +pub fn delete_workspace(app: &mut App, option_idx: Option, workspace_id: Uuid) { + if option_idx != Some(0) { + return; + } + + if let Err(e) = WorkspaceRepository::delete(&app.db.connection, &workspace_id) { + app.error = Some(e.to_string()); + return; + } + + app.tasks.retain(|t| t.workspace_id != Some(workspace_id)); + app.workspaces.retain(|s| s.id != workspace_id); + app.selected_tasks.clear(); + app.state.workspaces_tree_state.select_first(); +} diff --git a/src/actions/mod.rs b/src/actions/mod.rs index ec66d8f..2faf081 100644 --- a/src/actions/mod.rs +++ b/src/actions/mod.rs @@ -1,20 +1,20 @@ -pub mod archive_space; +pub mod archive_workspace; pub mod clean_err_msg; pub mod close_modal; -pub mod create_space; pub mod create_task; -pub mod delete_space; +pub mod create_workspace; pub mod delete_task; +pub mod delete_workspace; pub mod edit_priority; pub mod edit_task; pub mod edit_title; pub mod move_task; pub mod open_archive_modal; -pub mod open_archive_space_modal; -pub mod open_create_space_modal; +pub mod open_archive_workspace_modal; pub mod open_create_task_modal; +pub mod open_create_workspace_modal; pub mod open_delete_modal; -pub mod open_delete_space_modal; +pub mod open_delete_workspace_modal; pub mod open_edit_title_modal; pub mod open_move_task_modal; pub mod open_priority_modal; @@ -26,23 +26,23 @@ pub mod toggle_archive_task; pub mod toggle_task_completion; pub mod toggle_task_selection; -pub use archive_space::archive_space; +pub use archive_workspace::archive_workspace; pub use clean_err_msg::clean_err_msg; pub use close_modal::close_modal; -pub use create_space::create_space; pub use create_task::create_task; -pub use delete_space::delete_space; +pub use create_workspace::create_workspace; pub use delete_task::delete_task; +pub use delete_workspace::delete_workspace; pub use edit_priority::edit_priority; pub use edit_task::edit_task; pub use edit_title::edit_title; pub use move_task::move_task; pub use open_archive_modal::open_archive_modal; -pub use open_archive_space_modal::open_archive_space_modal; -pub use open_create_space_modal::open_create_space_modal; +pub use open_archive_workspace_modal::open_archive_workspace_modal; pub use open_create_task_modal::open_create_task_modal; +pub use open_create_workspace_modal::open_create_workspace_modal; pub use open_delete_modal::open_delete_modal; -pub use open_delete_space_modal::open_delete_space_modal; +pub use open_delete_workspace_modal::open_delete_workspace_modal; pub use open_edit_title_modal::open_edit_title_modal; pub use open_move_task_modal::open_move_task_modal; pub use open_priority_modal::open_priority_modal; diff --git a/src/actions/move_task.rs b/src/actions/move_task.rs index 51e8fd5..46f1fa7 100644 --- a/src/actions/move_task.rs +++ b/src/actions/move_task.rs @@ -1,18 +1,18 @@ use chrono::Utc; use uuid::Uuid; -use crate::{app::App, db::repositories::TaskRepository, models::Space}; +use crate::{app::App, db::repositories::TaskRepository, models::Workspace}; -pub fn move_task(app: &mut App, option_idx: Option, task_id: Uuid, spaces: &[Space]) { +pub fn move_task(app: &mut App, option_idx: Option, task_id: Uuid, workspaces: &[Workspace]) { let idx = match option_idx { Some(i) => i, None => return, }; - let new_space_id = if idx >= spaces.len() { + let new_workspace_id = if idx >= workspaces.len() { None } else { - spaces.get(idx).map(|s| s.id) + workspaces.get(idx).map(|w| w.id) }; let task = match app.tasks.iter_mut().find(|t| t.id == task_id) { @@ -20,7 +20,7 @@ pub fn move_task(app: &mut App, option_idx: Option, task_id: Uuid, spaces None => return, }; - task.space_id = new_space_id; + task.workspace_id = new_workspace_id; task.updated_at = Some(Utc::now()); if let Err(e) = TaskRepository::update(&app.db.connection, task) { diff --git a/src/actions/open_archive_modal.rs b/src/actions/open_archive_modal.rs index d7ab62c..4b23406 100644 --- a/src/actions/open_archive_modal.rs +++ b/src/actions/open_archive_modal.rs @@ -12,7 +12,7 @@ pub fn open_archive_modal(app: &mut App) { } if app.state.active_panel == PanelState::ActiveTasks { - let selected = app.state.spaces_tree_state.selected(); + let selected = app.state.workspaces_tree_state.selected(); if selected.is_empty() { return; } diff --git a/src/actions/open_archive_space_modal.rs b/src/actions/open_archive_workspace_modal.rs similarity index 52% rename from src/actions/open_archive_space_modal.rs rename to src/actions/open_archive_workspace_modal.rs index ba24bbe..1869c7a 100644 --- a/src/actions/open_archive_space_modal.rs +++ b/src/actions/open_archive_workspace_modal.rs @@ -2,12 +2,12 @@ use uuid::Uuid; use crate::{app::App, state::PanelState}; -pub fn open_archive_space_modal(app: &mut App) { +pub fn open_archive_workspace_modal(app: &mut App) { if app.state.active_panel != PanelState::ActiveTasks { return; } - let selected = app.state.spaces_tree_state.selected(); + let selected = app.state.workspaces_tree_state.selected(); if selected.len() != 1 { return; } @@ -15,8 +15,8 @@ pub fn open_archive_space_modal(app: &mut App) { let selected_id = &selected[0]; if let Ok(uuid) = Uuid::parse_str(selected_id) { - if let Some(space) = app.spaces.iter().find(|s| s.id == uuid) { - app.state.open_archive_space(uuid, space.archived); + if let Some(workspace) = app.workspaces.iter().find(|s| s.id == uuid) { + app.state.open_archive_workspace(uuid, workspace.archived); } } } diff --git a/src/actions/open_create_task_modal.rs b/src/actions/open_create_task_modal.rs index f0aea2e..f44163f 100644 --- a/src/actions/open_create_task_modal.rs +++ b/src/actions/open_create_task_modal.rs @@ -2,7 +2,7 @@ use crate::{app::App, state::PanelState}; pub fn open_create_task_modal(app: &mut App) { if app.state.active_panel == PanelState::ActiveTasks { - let space_id = app.state.spaces_tree_state.selected()[0].clone(); - app.state.open_create_task(space_id) + let workspace_id = app.state.workspaces_tree_state.selected()[0].clone(); + app.state.open_create_task(workspace_id) } } diff --git a/src/actions/open_create_space_modal.rs b/src/actions/open_create_workspace_modal.rs similarity index 53% rename from src/actions/open_create_space_modal.rs rename to src/actions/open_create_workspace_modal.rs index 24e03e6..6531ac3 100644 --- a/src/actions/open_create_space_modal.rs +++ b/src/actions/open_create_workspace_modal.rs @@ -1,7 +1,7 @@ use crate::{app::App, state::PanelState}; -pub fn open_create_space_modal(app: &mut App) { +pub fn open_create_workspace_modal(app: &mut App) { if app.state.active_panel == PanelState::ActiveTasks { - app.state.open_create_space() + app.state.open_create_workspace() } } diff --git a/src/actions/open_delete_modal.rs b/src/actions/open_delete_modal.rs index 81955d7..19af2ab 100644 --- a/src/actions/open_delete_modal.rs +++ b/src/actions/open_delete_modal.rs @@ -9,7 +9,7 @@ pub fn open_delete_modal(app: &mut App) { } if app.state.active_panel == PanelState::ActiveTasks { - let selected = app.state.spaces_tree_state.selected(); + let selected = app.state.workspaces_tree_state.selected(); if selected.is_empty() { return; } diff --git a/src/actions/open_delete_space_modal.rs b/src/actions/open_delete_workspace_modal.rs similarity index 57% rename from src/actions/open_delete_space_modal.rs rename to src/actions/open_delete_workspace_modal.rs index 7ed8c69..afb0cd6 100644 --- a/src/actions/open_delete_space_modal.rs +++ b/src/actions/open_delete_workspace_modal.rs @@ -2,12 +2,12 @@ use uuid::Uuid; use crate::{app::App, state::PanelState}; -pub fn open_delete_space_modal(app: &mut App) { +pub fn open_delete_workspace_modal(app: &mut App) { if app.state.active_panel != PanelState::ActiveTasks { return; } - let selected = app.state.spaces_tree_state.selected(); + let selected = app.state.workspaces_tree_state.selected(); if selected.len() != 1 { return; } @@ -15,8 +15,8 @@ pub fn open_delete_space_modal(app: &mut App) { let selected_id = &selected[0]; if let Ok(uuid) = Uuid::parse_str(selected_id) { - if app.spaces.iter().any(|s| s.id == uuid) { - app.state.open_delete_space(uuid); + if app.workspaces.iter().any(|s| s.id == uuid) { + app.state.open_delete_workspace(uuid); } } } diff --git a/src/actions/open_move_task_modal.rs b/src/actions/open_move_task_modal.rs index 0e27b5a..a040071 100644 --- a/src/actions/open_move_task_modal.rs +++ b/src/actions/open_move_task_modal.rs @@ -7,7 +7,7 @@ pub fn open_move_task_modal(app: &mut App) { return; } - let selected = app.state.spaces_tree_state.selected(); + let selected = app.state.workspaces_tree_state.selected(); if selected.is_empty() { return; } diff --git a/src/app.rs b/src/app.rs index 3827279..c18432b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -10,7 +10,7 @@ use crate::{ components, db::{ Db, - repositories::{SpaceRepository, TaskRepository}, + repositories::{WorkspaceRepository, TaskRepository}, }, keybindings::handle_key_event, models::Task, @@ -20,7 +20,7 @@ use crate::{models, state::ModalState}; pub struct App { pub exit: bool, - pub spaces: Vec, + pub workspaces: Vec, pub tasks: Vec, pub selected_tasks: Vec, pub state: state::AppState, @@ -38,23 +38,20 @@ impl App { Err(err) => (vec![], Some(err.to_string())), }; - let (spaces, space_err) = match SpaceRepository::get_all(&db.connection) { - Ok(spaces) => (spaces, None), + let (workspaces, workspace_err) = match WorkspaceRepository::get_all(&db.connection) { + Ok(workspaces) => (workspaces, None), Err(err) => (vec![], Some(err.to_string())), }; - let mut app = App { + App { exit: false, selected_tasks: Vec::new(), state, db, tasks, - spaces, - error: space_err, - }; - - app.state.spaces_tree_state.select_first(); - app + workspaces, + error: workspace_err, + } } pub fn run(&mut self, terminal: &mut DefaultTerminal) -> Result<()> { @@ -77,7 +74,7 @@ impl App { components::bottom_bar::render(frame, layout[1], self); match &mut self.state.active_modal { - Some(ModalState::CreateTask { input, space_id: _ }) => { + Some(ModalState::CreateTask { input, workspace_id: _ }) => { components::modals::create_task::render(frame, input); } Some(ModalState::EditTask { task_id: _, input }) => { @@ -102,28 +99,28 @@ impl App { }) => { components::modals::priority_task::render(frame, selected_option); } - Some(ModalState::CreateSpace { input }) => { + Some(ModalState::CreateWorkspace { input }) => { components::modals::create_task::render(frame, input); } - Some(ModalState::DeleteSpace { - space_id: _, + Some(ModalState::DeleteWorkspace { + workspace_id: _, selected_option, }) => { - components::modals::delete_space::render(frame, selected_option); + components::modals::delete_workspace::render(frame, selected_option); } - Some(ModalState::ArchiveSpace { - space_id: _, + Some(ModalState::ArchiveWorkspace { + workspace_id: _, selected_option, is_archived, }) => { - components::modals::archive_space::render(frame, selected_option, *is_archived); + components::modals::archive_workspace::render(frame, selected_option, *is_archived); } Some(ModalState::MoveTask { task_id: _, selected_option, }) => { - let spaces: Vec<_> = self.spaces.iter().filter(|s| !s.archived).cloned().collect(); - components::modals::move_task::render(frame, selected_option, &spaces); + let workspaces: Vec<_> = self.workspaces.iter().filter(|s| !s.archived).cloned().collect(); + components::modals::move_task::render(frame, selected_option, &workspaces); } None => {} } diff --git a/src/components/modals/archive_space.rs b/src/components/modals/archive_workspace.rs similarity index 89% rename from src/components/modals/archive_space.rs rename to src/components/modals/archive_workspace.rs index 6c9a6f6..fc9ec76 100644 --- a/src/components/modals/archive_space.rs +++ b/src/components/modals/archive_workspace.rs @@ -8,7 +8,7 @@ use crate::components::shared; pub fn render(frame: &mut Frame, selected_option: &mut ListState, is_archived: bool) { let action_name = if is_archived { "Unarchive" } else { "Archive" }; - let area = shared::modal::Modal::new(format!("{} space", action_name)) + let area = shared::modal::Modal::new(format!("{} workspace", action_name)) .height(4) .render(frame); let list_items: Vec = vec![ListItem::from(action_name), ListItem::from("Cancel")]; diff --git a/src/components/modals/delete_space.rs b/src/components/modals/delete_workspace.rs similarity index 91% rename from src/components/modals/delete_space.rs rename to src/components/modals/delete_workspace.rs index 8d7f454..48b2344 100644 --- a/src/components/modals/delete_space.rs +++ b/src/components/modals/delete_workspace.rs @@ -7,7 +7,7 @@ use ratatui::{ use crate::components::shared; pub fn render(frame: &mut Frame, selected_option: &mut ListState) { - let area = shared::modal::Modal::new("Delete space") + let area = shared::modal::Modal::new("Delete workspace") .height(4) .render(frame); let list_items: Vec = vec![ListItem::from("Delete"), ListItem::from("Cancel")]; diff --git a/src/components/modals/mod.rs b/src/components/modals/mod.rs index cfa5d2e..048a563 100644 --- a/src/components/modals/mod.rs +++ b/src/components/modals/mod.rs @@ -1,8 +1,8 @@ -pub mod archive_space; pub mod archive_task; +pub mod archive_workspace; pub mod create_task; -pub mod delete_space; pub mod delete_task; +pub mod delete_workspace; pub mod edit_task; pub mod move_task; pub mod priority_task; diff --git a/src/components/modals/move_task.rs b/src/components/modals/move_task.rs index a923e44..d082c90 100644 --- a/src/components/modals/move_task.rs +++ b/src/components/modals/move_task.rs @@ -4,19 +4,19 @@ use ratatui::{ widgets::{List, ListItem, ListState}, }; -use crate::{components::shared, models::Space}; +use crate::{components::shared, models::Workspace}; -pub fn render(frame: &mut Frame, selected_option: &mut ListState, spaces: &[Space]) { - let height = (spaces.len() + 3).max(3).min(10) as u16; - let area = shared::modal::Modal::new("Move to space") +pub fn render(frame: &mut Frame, selected_option: &mut ListState, workspaces: &[Workspace]) { + let height = (workspaces.len() + 3).max(3).min(10) as u16; + let area = shared::modal::Modal::new("Move to workspace") .height(height) .render(frame); - let mut list_items: Vec = spaces + let mut list_items: Vec = workspaces .iter() - .map(|s| ListItem::from(s.title.clone())) + .map(|w| ListItem::from(w.title.clone())) .collect(); - list_items.push(ListItem::from("No space")); + list_items.push(ListItem::from("No workspace")); let options = List::new(list_items).highlight_style( Style::default() diff --git a/src/components/workspace/sidebar/active_spaces.rs b/src/components/workspace/sidebar/active_workspaces.rs similarity index 71% rename from src/components/workspace/sidebar/active_spaces.rs rename to src/components/workspace/sidebar/active_workspaces.rs index 2af12bd..44a68d4 100644 --- a/src/components/workspace/sidebar/active_spaces.rs +++ b/src/components/workspace/sidebar/active_workspaces.rs @@ -19,26 +19,26 @@ pub fn render(frame: &mut Frame, area: Rect, app: &mut App) { Color::White }; - for space in app.spaces.iter().filter(|s| !s.archived) { - let space_tasks: Vec> = app + for workspace in app.workspaces.iter().filter(|w| !w.archived) { + let workspace_tasks: Vec> = app .tasks .iter() - .filter(|t| t.space_id == Some(space.id) && !t.archived) + .filter(|t| t.workspace_id == Some(workspace.id) && !t.archived) .map(|task| TreeItem::new_leaf(task.id.to_string(), task.title.clone())) .collect(); - let space_item = TreeItem::new( - space.id.to_string(), - format!("{} ({})", space.title.clone(), space_tasks.len()), - space_tasks, + let workspace_item = TreeItem::new( + workspace.id.to_string(), + format!("{} ({})", workspace.title.clone(), workspace_tasks.len()), + workspace_tasks, ) .unwrap(); - items.push(space_item); + items.push(workspace_item); } for task in &app.tasks { - if task.space_id.is_none() && !task.archived { + if task.workspace_id.is_none() && !task.archived { items.push(TreeItem::new_leaf(task.id.to_string(), task.title.clone())); } } @@ -47,7 +47,7 @@ pub fn render(frame: &mut Frame, area: Rect, app: &mut App) { .expect("identifiers are unique") .block( Block::new() - .title(" Spaces ") + .title(" Workspaces ") .borders(Borders::ALL) .border_type(BorderType::Rounded) .border_style(border_color), @@ -63,11 +63,11 @@ pub fn render(frame: &mut Frame, area: Rect, app: &mut App) { .node_open_symbol("▼ ") .node_no_children_symbol(" "); - frame.render_stateful_widget(tree, area, &mut app.state.spaces_tree_state); + frame.render_stateful_widget(tree, area, &mut app.state.workspaces_tree_state); shared::scrollbar::render( frame, area, - app.spaces.len(), + app.workspaces.len(), app.state.active_tasks_state.offset(), ); } diff --git a/src/components/workspace/sidebar/mod.rs b/src/components/workspace/sidebar/mod.rs index 0c51990..cc37d04 100644 --- a/src/components/workspace/sidebar/mod.rs +++ b/src/components/workspace/sidebar/mod.rs @@ -1,5 +1,5 @@ pub mod about; -pub mod active_spaces; +pub mod active_workspaces; pub mod active_tasks; pub mod archived_tasks; @@ -34,6 +34,6 @@ pub fn render(frame: &mut Frame, area: Rect, app: &mut App) { sidebar::about::render(frame, sidebar[0], app); // sidebar::active_tasks::render(frame, sidebar[1], app, active_tasks); - sidebar::active_spaces::render(frame, sidebar[1], app); + sidebar::active_workspaces::render(frame, sidebar[1], app); sidebar::archived_tasks::render(frame, sidebar[2], app, archived_tasks); } diff --git a/src/db/connection.rs b/src/db/connection.rs index cb1351b..ed60491 100644 --- a/src/db/connection.rs +++ b/src/db/connection.rs @@ -28,8 +28,8 @@ impl Db { } fn init_schema(&self) -> Result<(), rusqlite::Error> { - let spaces_schema = include_str!("schema/spaces.sql"); - self.connection.execute_batch(spaces_schema)?; + let workspaces_schema = include_str!("schema/workspaces.sql"); + self.connection.execute_batch(workspaces_schema)?; let tasks_schema = include_str!("schema/tasks.sql"); self.connection.execute_batch(tasks_schema)?; diff --git a/src/db/repositories/mod.rs b/src/db/repositories/mod.rs index a3879da..a2c8534 100644 --- a/src/db/repositories/mod.rs +++ b/src/db/repositories/mod.rs @@ -1,5 +1,5 @@ -pub mod space; pub mod task; +pub mod workspace; -pub use space::SpaceRepository; pub use task::TaskRepository; +pub use workspace::WorkspaceRepository; diff --git a/src/db/repositories/space.rs b/src/db/repositories/space.rs deleted file mode 100644 index 45982b6..0000000 --- a/src/db/repositories/space.rs +++ /dev/null @@ -1,90 +0,0 @@ -use std::error::Error; - -use chrono::{DateTime, Utc}; -use rusqlite::{Connection, Row}; -use uuid::Uuid; - -use crate::models::Space; - -pub struct SpaceRepository; - -impl SpaceRepository { - pub fn create(connection: &Connection, space: &Space) -> Result<(), rusqlite::Error> { - connection.execute( - "INSERT INTO spaces (id, title, created_at) VALUES (?1, ?2, ?3)", - ( - space.id.to_string(), - &space.title, - space.created_at.to_rfc3339(), - ), - )?; - - Ok(()) - } - - pub fn get_all(connection: &Connection) -> Result, Box> { - let mut stmt = connection.prepare("SELECT * from spaces")?; - let mut rows = stmt.query([])?; - - let mut spaces: Vec = Vec::new(); - while let Some(row) = rows.next()? { - spaces.push(Self::parse_row(row)?); - } - - Ok(spaces) - } - - fn parse_row(row: &Row) -> Result> { - let id: String = row.get("id")?; - let created_at: String = row.get("created_at")?; - let updated_at: Option = row.get("updated_at")?; - let archived_at: Option = row.get("archived_at")?; - - Ok(Space { - id: Uuid::parse_str(&id)?, - title: row.get("title")?, - archived: row.get::<_, i32>("archived")? != 0, - created_at: DateTime::parse_from_rfc3339(&created_at)?.with_timezone(&Utc), - updated_at: updated_at - .map(|s| DateTime::parse_from_rfc3339(&s).map(|d| d.with_timezone(&Utc))) - .transpose()?, - archived_at: archived_at - .map(|s| DateTime::parse_from_rfc3339(&s).map(|d| d.with_timezone(&Utc))) - .transpose()?, - }) - } - - pub fn update(connection: &Connection, space: &Space) -> Result<(), rusqlite::Error> { - connection.execute( - "UPDATE spaces SET - title = ?2, - archived = ?3, - updated_at = ?4, - archived_at = ?5 - WHERE id = ?1", - ( - space.id.to_string(), - &space.title, - space.archived as u32, - space.updated_at.map(|d| d.to_rfc3339()), - space.archived_at.map(|d| d.to_rfc3339()), - ), - )?; - - Ok(()) - } - - pub fn delete(connection: &Connection, space_id: &Uuid) -> Result<(), rusqlite::Error> { - let tx = connection.unchecked_transaction()?; - - tx.execute( - "DELETE FROM tasks WHERE space_id = ?1", - [space_id.to_string()], - )?; - tx.execute("DELETE FROM spaces WHERE id = ?1", [space_id.to_string()])?; - - tx.commit()?; - - Ok(()) - } -} diff --git a/src/db/repositories/task.rs b/src/db/repositories/task.rs index d39273e..35e00ea 100644 --- a/src/db/repositories/task.rs +++ b/src/db/repositories/task.rs @@ -10,18 +10,18 @@ pub struct TaskRepository; impl TaskRepository { pub fn create(connection: &Connection, task: &Task) -> Result<(), rusqlite::Error> { - let space_id = match &task.space_id { + let workspace_id = match &task.workspace_id { Some(v) => Some(Uuid::to_string(v)), None => None, }; connection.execute( - "INSERT INTO tasks (id, title, created_at, space_id) VALUES (?1, ?2, ?3, ?4) ", + "INSERT INTO tasks (id, title, created_at, workspace_id) VALUES (?1, ?2, ?3, ?4) ", ( task.id.to_string(), &task.title, task.created_at.to_rfc3339(), - space_id, + workspace_id, ), )?; @@ -80,7 +80,7 @@ impl TaskRepository { fn parse_row(row: &Row) -> Result> { let id: String = row.get("id")?; - let space_id: Option = row.get("space_id")?; + let workspace_id: Option = row.get("workspace_id")?; let priority: Option = row.get("priority")?; let created_at: String = row.get("created_at")?; let updated_at: Option = row.get("updated_at")?; @@ -100,7 +100,7 @@ impl TaskRepository { archived_at: archived_at .map(|s| DateTime::parse_from_rfc3339(&s).map(|d| d.with_timezone(&Utc))) .transpose()?, - space_id: space_id.map(|s| Uuid::parse_str(&s)).transpose()?, + workspace_id: workspace_id.map(|s| Uuid::parse_str(&s)).transpose()?, }) } } diff --git a/src/db/repositories/workspace.rs b/src/db/repositories/workspace.rs new file mode 100644 index 0000000..499aa71 --- /dev/null +++ b/src/db/repositories/workspace.rs @@ -0,0 +1,93 @@ +use std::error::Error; + +use chrono::{DateTime, Utc}; +use rusqlite::{Connection, Row}; +use uuid::Uuid; + +use crate::models::Workspace; + +pub struct WorkspaceRepository; + +impl WorkspaceRepository { + pub fn create(connection: &Connection, workspace: &Workspace) -> Result<(), rusqlite::Error> { + connection.execute( + "INSERT INTO workspaces (id, title, created_at) VALUES (?1, ?2, ?3)", + ( + workspace.id.to_string(), + &workspace.title, + workspace.created_at.to_rfc3339(), + ), + )?; + + Ok(()) + } + + pub fn get_all(connection: &Connection) -> Result, Box> { + let mut stmt = connection.prepare("SELECT * from workspaces")?; + let mut rows = stmt.query([])?; + + let mut workspaces: Vec = Vec::new(); + while let Some(row) = rows.next()? { + workspaces.push(Self::parse_row(row)?); + } + + Ok(workspaces) + } + + fn parse_row(row: &Row) -> Result> { + let id: String = row.get("id")?; + let created_at: String = row.get("created_at")?; + let updated_at: Option = row.get("updated_at")?; + let archived_at: Option = row.get("archived_at")?; + + Ok(Workspace { + id: Uuid::parse_str(&id)?, + title: row.get("title")?, + archived: row.get::<_, i32>("archived")? != 0, + created_at: DateTime::parse_from_rfc3339(&created_at)?.with_timezone(&Utc), + updated_at: updated_at + .map(|s| DateTime::parse_from_rfc3339(&s).map(|d| d.with_timezone(&Utc))) + .transpose()?, + archived_at: archived_at + .map(|s| DateTime::parse_from_rfc3339(&s).map(|d| d.with_timezone(&Utc))) + .transpose()?, + }) + } + + pub fn update(connection: &Connection, workspace: &Workspace) -> Result<(), rusqlite::Error> { + connection.execute( + "UPDATE workspaces SET + title = ?2, + archived = ?3, + updated_at = ?4, + archived_at = ?5 + WHERE id = ?1", + ( + workspace.id.to_string(), + &workspace.title, + workspace.archived as u32, + workspace.updated_at.map(|d| d.to_rfc3339()), + workspace.archived_at.map(|d| d.to_rfc3339()), + ), + )?; + + Ok(()) + } + + pub fn delete(connection: &Connection, workspace_id: &Uuid) -> Result<(), rusqlite::Error> { + let tx = connection.unchecked_transaction()?; + + tx.execute( + "DELETE FROM tasks WHERE workspace_id = ?1", + [workspace_id.to_string()], + )?; + tx.execute( + "DELETE FROM workspaces WHERE id = ?1", + [workspace_id.to_string()], + )?; + + tx.commit()?; + + Ok(()) + } +} diff --git a/src/db/schema/tasks.sql b/src/db/schema/tasks.sql index 96dc73b..f61d1d1 100644 --- a/src/db/schema/tasks.sql +++ b/src/db/schema/tasks.sql @@ -8,7 +8,7 @@ CREATE TABLE IF NOT EXISTS tasks ( created_at TEXT NOT NULL, updated_at TEXT, archived_at TEXT, - space_id TEXT, + workspace_id TEXT, - FOREIGN KEY(space_id) REFERENCES spaces(id) + FOREIGN KEY(workspace_id) REFERENCES workspaces(id) ); diff --git a/src/db/schema/spaces.sql b/src/db/schema/workspaces.sql similarity index 80% rename from src/db/schema/spaces.sql rename to src/db/schema/workspaces.sql index fdcba04..6a4ea94 100644 --- a/src/db/schema/spaces.sql +++ b/src/db/schema/workspaces.sql @@ -1,4 +1,4 @@ -CREATE TABLE IF NOT EXISTS spaces ( +CREATE TABLE IF NOT EXISTS workspaces ( id TEXT PRIMARY KEY, title TEXT NOT NULL, archived INTEGER NOT NULL DEFAULT 0, @@ -6,4 +6,3 @@ CREATE TABLE IF NOT EXISTS spaces ( updated_at TEXT, archived_at TEXT ); - diff --git a/src/keybindings.rs b/src/keybindings.rs index df7ed36..9c141db 100644 --- a/src/keybindings.rs +++ b/src/keybindings.rs @@ -11,14 +11,14 @@ use crate::{app::App, state::ModalState}; pub fn handle_key_event(app: &mut App, event: &Event, terminal: &mut DefaultTerminal) { if let crossterm::event::Event::Key(key) = event { match &mut app.state.active_modal { - Some(ModalState::CreateTask { input, space_id }) => match key.code { + Some(ModalState::CreateTask { input, workspace_id }) => match key.code { crossterm::event::KeyCode::Esc => actions::close_modal(app), crossterm::event::KeyCode::Enter => { let title = input.value().trim().to_owned(); - let space_id = space_id.clone(); + let workspace_id = workspace_id.clone(); if !title.is_empty() { - actions::create_task(app, title, space_id); + actions::create_task(app, title, workspace_id); } actions::close_modal(app); } @@ -100,13 +100,13 @@ pub fn handle_key_event(app: &mut App, event: &Event, terminal: &mut DefaultTerm } _ => {} }, - Some(ModalState::CreateSpace { input }) => match key.code { + Some(ModalState::CreateWorkspace { input }) => match key.code { crossterm::event::KeyCode::Esc => actions::close_modal(app), crossterm::event::KeyCode::Enter => { let title = input.value().trim().to_owned(); if !title.is_empty() { - actions::create_space(app, title); + actions::create_workspace(app, title); } actions::close_modal(app); } @@ -114,16 +114,16 @@ pub fn handle_key_event(app: &mut App, event: &Event, terminal: &mut DefaultTerm input.handle_event(event); } }, - Some(ModalState::DeleteSpace { - space_id, + Some(ModalState::DeleteWorkspace { + workspace_id, selected_option, }) => match key.code { crossterm::event::KeyCode::Esc => actions::close_modal(app), crossterm::event::KeyCode::Enter => { let option_idx = selected_option.selected(); - let space_id = *space_id; + let workspace_id = *workspace_id; - actions::delete_space(app, option_idx, space_id); + actions::delete_workspace(app, option_idx, workspace_id); actions::close_modal(app); } crossterm::event::KeyCode::Char('j') => { @@ -134,17 +134,17 @@ pub fn handle_key_event(app: &mut App, event: &Event, terminal: &mut DefaultTerm } _ => {} }, - Some(ModalState::ArchiveSpace { - space_id, + Some(ModalState::ArchiveWorkspace { + workspace_id, selected_option, is_archived: _, }) => match key.code { crossterm::event::KeyCode::Esc => actions::close_modal(app), crossterm::event::KeyCode::Enter => { let option_idx = selected_option.selected(); - let space_id = *space_id; + let workspace_id = *workspace_id; - actions::archive_space(app, option_idx, space_id); + actions::archive_workspace(app, option_idx, workspace_id); actions::close_modal(app); } crossterm::event::KeyCode::Char('j') => { @@ -163,9 +163,9 @@ pub fn handle_key_event(app: &mut App, event: &Event, terminal: &mut DefaultTerm crossterm::event::KeyCode::Enter => { let option_idx = selected_option.selected(); let task_id = *task_id; - let spaces: Vec<_> = app.spaces.iter().filter(|s| !s.archived).cloned().collect(); + let workspaces: Vec<_> = app.workspaces.iter().filter(|s| !s.archived).cloned().collect(); - actions::move_task(app, option_idx, task_id, &spaces); + actions::move_task(app, option_idx, task_id, &workspaces); actions::close_modal(app); } crossterm::event::KeyCode::Char('j') => { @@ -179,14 +179,14 @@ pub fn handle_key_event(app: &mut App, event: &Event, terminal: &mut DefaultTerm None => match key.code { crossterm::event::KeyCode::Char('a') => { if app.state.active_panel == PanelState::ActiveTasks { - actions::open_archive_space_modal(app); + actions::open_archive_workspace_modal(app); } if app.state.active_modal.is_none() { actions::open_archive_modal(app); } } crossterm::event::KeyCode::Char('c') => actions::open_create_task_modal(app), - crossterm::event::KeyCode::Char('s') => actions::open_create_space_modal(app), + crossterm::event::KeyCode::Char('s') => actions::open_create_workspace_modal(app), crossterm::event::KeyCode::Char('e') => actions::open_edit_title_modal(app), crossterm::event::KeyCode::Char('p') => actions::open_priority_modal(app), crossterm::event::KeyCode::Char('E') => actions::edit_task(app, terminal), @@ -195,7 +195,7 @@ pub fn handle_key_event(app: &mut App, event: &Event, terminal: &mut DefaultTerm crossterm::event::KeyCode::Char('m') => actions::open_move_task_modal(app), crossterm::event::KeyCode::Char('d') => { if app.state.active_panel == PanelState::ActiveTasks { - actions::open_delete_space_modal(app); + actions::open_delete_workspace_modal(app); } if app.state.active_modal.is_none() { actions::open_delete_modal(app); @@ -204,7 +204,7 @@ pub fn handle_key_event(app: &mut App, event: &Event, terminal: &mut DefaultTerm crossterm::event::KeyCode::Char('j') => { match app.state.active_panel { PanelState::ActiveTasks => { - TreeState::key_down(&mut app.state.spaces_tree_state); + TreeState::key_down(&mut app.state.workspaces_tree_state); } _ => actions::select_next_task(app), }; @@ -212,7 +212,7 @@ pub fn handle_key_event(app: &mut App, event: &Event, terminal: &mut DefaultTerm crossterm::event::KeyCode::Char('k') => { match app.state.active_panel { PanelState::ActiveTasks => { - TreeState::key_up(&mut app.state.spaces_tree_state); + TreeState::key_up(&mut app.state.workspaces_tree_state); } _ => actions::select_previous_task(app), }; @@ -224,8 +224,8 @@ pub fn handle_key_event(app: &mut App, event: &Event, terminal: &mut DefaultTerm if app.error.is_some() { actions::clean_err_msg(app); } else { - let selected = app.state.spaces_tree_state.selected().to_vec(); - TreeState::toggle(&mut app.state.spaces_tree_state, selected); + let selected = app.state.workspaces_tree_state.selected().to_vec(); + TreeState::toggle(&mut app.state.workspaces_tree_state, selected); } } _ => actions::clean_err_msg(app), diff --git a/src/models/mod.rs b/src/models/mod.rs index 6fab2a6..5343479 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,7 +1,7 @@ pub mod priority; -pub mod space; pub mod task; +pub mod workspace; pub use priority::Priority; -pub use space::Space; pub use task::Task; +pub use workspace::Workspace; diff --git a/src/models/task.rs b/src/models/task.rs index 3152c8e..c9f9376 100644 --- a/src/models/task.rs +++ b/src/models/task.rs @@ -14,11 +14,11 @@ pub struct Task { pub created_at: DateTime, pub updated_at: Option>, pub archived_at: Option>, - pub space_id: Option, + pub workspace_id: Option, } impl Task { - pub fn new(title: impl Into, space_id: String) -> Self { + pub fn new(title: impl Into, workspace_id: String) -> Self { Task { id: Uuid::new_v4(), title: title.into(), @@ -29,7 +29,7 @@ impl Task { created_at: Utc::now(), updated_at: None, archived_at: None, - space_id: Some(Uuid::parse_str(&space_id).unwrap()), + workspace_id: Some(Uuid::parse_str(&workspace_id).unwrap()), } } diff --git a/src/models/space.rs b/src/models/workspace.rs similarity index 87% rename from src/models/space.rs rename to src/models/workspace.rs index d437b0c..d3064d5 100644 --- a/src/models/space.rs +++ b/src/models/workspace.rs @@ -2,7 +2,7 @@ use chrono::{DateTime, Utc}; use uuid::Uuid; #[derive(Clone)] -pub struct Space { +pub struct Workspace { pub id: Uuid, pub title: String, pub archived: bool, @@ -11,9 +11,9 @@ pub struct Space { pub archived_at: Option>, } -impl Space { +impl Workspace { pub fn new(title: impl Into) -> Self { - Space { + Workspace { id: Uuid::new_v4(), title: title.into(), archived: false, @@ -22,6 +22,4 @@ impl Space { archived_at: None, } } - - pub fn get_all() {} } diff --git a/src/state.rs b/src/state.rs index eab0a73..0ce3353 100644 --- a/src/state.rs +++ b/src/state.rs @@ -14,8 +14,8 @@ pub struct AppState { /// State of the focus pane pub active_panel: PanelState, - /// State of the spaces tree (identifier = UUID as String) - pub spaces_tree_state: TreeState, + /// State of the workspaces tree (identifier = UUID as String) + pub workspaces_tree_state: TreeState, /// State of the current active modal (CreateTask, EditTask, ArchivedTask, DeleteTask) pub active_modal: Option, @@ -31,7 +31,7 @@ pub enum PanelState { pub enum ModalState { CreateTask { input: Input, - space_id: String, + workspace_id: String, }, EditTask { task_id: Uuid, @@ -50,15 +50,15 @@ pub enum ModalState { task_ids: Vec, selected_option: ListState, }, - CreateSpace { + CreateWorkspace { input: Input, }, - DeleteSpace { - space_id: Uuid, + DeleteWorkspace { + workspace_id: Uuid, selected_option: ListState, }, - ArchiveSpace { - space_id: Uuid, + ArchiveWorkspace { + workspace_id: Uuid, selected_option: ListState, is_archived: bool, }, @@ -76,15 +76,15 @@ impl AppState { let mut archived_tasks_state = ListState::default(); archived_tasks_state.select(Some(0)); - let mut spaces_tree_state = TreeState::default(); - spaces_tree_state.select_first(); + let mut workspaces_tree_state = TreeState::default(); + workspaces_tree_state.select_first(); AppState { active_tasks_state, archived_tasks_state, active_panel: PanelState::ActiveTasks, active_modal: None, - spaces_tree_state, + workspaces_tree_state, } } @@ -122,10 +122,10 @@ impl AppState { } } - pub fn open_create_task(&mut self, space_id: String) { + pub fn open_create_task(&mut self, workspace_id: String) { self.active_modal = Some(ModalState::CreateTask { input: Input::default(), - space_id, + workspace_id, }) } @@ -172,8 +172,8 @@ impl AppState { } } - pub fn open_create_space(&mut self) { - self.active_modal = Some(ModalState::CreateSpace { + pub fn open_create_workspace(&mut self) { + self.active_modal = Some(ModalState::CreateWorkspace { input: Input::default(), }) } @@ -182,20 +182,20 @@ impl AppState { self.active_modal = None } - pub fn open_delete_space(&mut self, space_id: Uuid) { + pub fn open_delete_workspace(&mut self, workspace_id: Uuid) { let mut option_list_state = ListState::default(); option_list_state.select(Some(0)); - self.active_modal = Some(ModalState::DeleteSpace { - space_id, + self.active_modal = Some(ModalState::DeleteWorkspace { + workspace_id, selected_option: option_list_state, }) } - pub fn open_archive_space(&mut self, space_id: Uuid, is_archived: bool) { + pub fn open_archive_workspace(&mut self, workspace_id: Uuid, is_archived: bool) { let mut option_list_state = ListState::default(); option_list_state.select(Some(0)); - self.active_modal = Some(ModalState::ArchiveSpace { - space_id, + self.active_modal = Some(ModalState::ArchiveWorkspace { + workspace_id, selected_option: option_list_state, is_archived, }) From 1fa00ddc21bad449a02cbe4da83f1a067c938944 Mon Sep 17 00:00:00 2001 From: Simon Cardona Date: Sat, 31 Jan 2026 01:28:19 +0100 Subject: [PATCH 11/15] feat: edit priority --- src/actions/edit_task.rs | 58 ++++++++++--------- src/actions/open_edit_title_modal.rs | 19 ++++-- src/actions/open_priority_modal.rs | 32 +++++----- src/actions/toggle_task_completion.rs | 32 +++++----- src/actions/toggle_task_selection.rs | 31 ++++++---- .../workspace/sidebar/active_workspaces.rs | 43 +++++++++++--- 6 files changed, 139 insertions(+), 76 deletions(-) diff --git a/src/actions/edit_task.rs b/src/actions/edit_task.rs index d7c9955..498a57c 100644 --- a/src/actions/edit_task.rs +++ b/src/actions/edit_task.rs @@ -1,35 +1,41 @@ use chrono::Utc; use ratatui::DefaultTerminal; +use uuid::Uuid; use crate::{app::App, db::repositories::TaskRepository, editor, state::PanelState}; pub fn edit_task(app: &mut App, terminal: &mut DefaultTerminal) { - if app.state.active_panel == PanelState::ActiveTasks { - let task_id = app - .state - .get_selected_panel_state() - .and_then(|s| s.selected()) - .and_then(|idx| app.get_current_tasks().get(idx).map(|t| t.id)); - - if let Some(task_id) = task_id - && let Some(task_ref) = app.tasks.iter().find(|t| t.id == task_id) - { - let update = editor::open_in_editor(task_ref, terminal); - - // Only apply changes if title is not empty - if !update.title.is_empty() - && let Some(task) = app.tasks.iter_mut().find(|t| t.id == task_id) - { - task.title = update.title; - task.description = Some(update.description); - task.updated_at = Some(Utc::now()); - - if let Err(e) = TaskRepository::update(&app.db.connection, task) { - app.error = Some(e.to_string()); - - return; - }; - } + if app.state.active_panel != PanelState::ActiveTasks { + return; + } + + let selected = app.state.workspaces_tree_state.selected(); + if selected.is_empty() { + return; + } + + let selected_id = selected.last().unwrap(); + let task_id = match Uuid::parse_str(selected_id) { + Ok(uuid) => uuid, + Err(_) => return, + }; + + let task_ref = match app.tasks.iter().find(|t| t.id == task_id) { + Some(t) => t, + None => return, + }; + + let update = editor::open_in_editor(task_ref, terminal); + + if !update.title.is_empty() { + if let Some(task) = app.tasks.iter_mut().find(|t| t.id == task_id) { + task.title = update.title; + task.description = Some(update.description); + task.updated_at = Some(Utc::now()); + + if let Err(e) = TaskRepository::update(&app.db.connection, task) { + app.error = Some(e.to_string()); + }; } } } diff --git a/src/actions/open_edit_title_modal.rs b/src/actions/open_edit_title_modal.rs index da29aed..4535f37 100644 --- a/src/actions/open_edit_title_modal.rs +++ b/src/actions/open_edit_title_modal.rs @@ -1,10 +1,19 @@ +use uuid::Uuid; + use crate::{app::App, state::PanelState}; pub fn open_edit_title_modal(app: &mut App) { - if let Some(task_index) = app.state.get_selected_panel_state().and_then(|s| s.selected()) - && app.state.active_panel == PanelState::ActiveTasks - { - let task = &app.get_current_tasks()[task_index]; - app.state.open_edit_task(task.id, task.title.clone()); + if app.state.active_panel == PanelState::ActiveTasks { + let selected = app.state.workspaces_tree_state.selected(); + if selected.is_empty() { + return; + } + + let selected_id = selected.last().unwrap(); + if let Ok(uuid) = Uuid::parse_str(selected_id) { + if let Some(task) = app.tasks.iter().find(|t| t.id == uuid) { + app.state.open_edit_task(task.id, task.title.clone()); + } + } } } diff --git a/src/actions/open_priority_modal.rs b/src/actions/open_priority_modal.rs index 606498b..4be374b 100644 --- a/src/actions/open_priority_modal.rs +++ b/src/actions/open_priority_modal.rs @@ -1,20 +1,26 @@ +use uuid::Uuid; + use crate::{app::App, state::PanelState}; pub fn open_priority_modal(app: &mut App) { - let is_active_tasks = app.state.active_panel == PanelState::ActiveTasks; + if app.state.active_panel != PanelState::ActiveTasks { + return; + } + + if !app.selected_tasks.is_empty() { + app.state.open_priority_task(app.selected_tasks.clone()); + return; + } + + let selected = app.state.workspaces_tree_state.selected(); + if selected.is_empty() { + return; + } - if is_active_tasks { - if app.selected_tasks.is_empty() { - if let Some(task_index) = app - .state - .get_selected_panel_state() - .and_then(|s| s.selected()) - { - let task_id = app.get_current_tasks()[task_index].id; - app.state.open_priority_task(vec![task_id]); - } - } else { - app.state.open_priority_task(app.selected_tasks.clone()); + let selected_id = selected.last().unwrap(); + if let Ok(uuid) = Uuid::parse_str(selected_id) { + if app.tasks.iter().any(|t| t.id == uuid) { + app.state.open_priority_task(vec![uuid]); } } } diff --git a/src/actions/toggle_task_completion.rs b/src/actions/toggle_task_completion.rs index 3d74ac2..0544396 100644 --- a/src/actions/toggle_task_completion.rs +++ b/src/actions/toggle_task_completion.rs @@ -1,16 +1,12 @@ use chrono::Utc; +use uuid::Uuid; -use crate::{app::App, db::repositories::TaskRepository}; +use crate::{app::App, db::repositories::TaskRepository, state::PanelState}; pub fn toggle_task_completion(app: &mut App) { - if app.selected_tasks.is_empty() { - if let Some(task_index) = app - .state - .get_selected_panel_state() - .and_then(|s| s.selected()) - { - let task = app.get_current_tasks()[task_index].clone(); - if let Some(task) = app.tasks.iter_mut().find(|t| t.id == task.id) { + if !app.selected_tasks.is_empty() { + for task in app.tasks.iter_mut() { + if app.selected_tasks.contains(&task.id) { task.completed = !task.completed; task.updated_at = Some(Utc::now()); @@ -20,18 +16,26 @@ pub fn toggle_task_completion(app: &mut App) { }; } } - } else { - for task in app.tasks.iter_mut() { - if app.selected_tasks.contains(&task.id) { + app.selected_tasks.clear(); + return; + } + + if app.state.active_panel == PanelState::ActiveTasks { + let selected = app.state.workspaces_tree_state.selected(); + if selected.is_empty() { + return; + } + + let selected_id = selected.last().unwrap(); + if let Ok(uuid) = Uuid::parse_str(selected_id) { + if let Some(task) = app.tasks.iter_mut().find(|t| t.id == uuid) { task.completed = !task.completed; task.updated_at = Some(Utc::now()); if let Err(e) = TaskRepository::update(&app.db.connection, task) { app.error = Some(e.to_string()); - return; }; } } - app.selected_tasks.clear(); } } diff --git a/src/actions/toggle_task_selection.rs b/src/actions/toggle_task_selection.rs index 2810ffd..b0bf8a0 100644 --- a/src/actions/toggle_task_selection.rs +++ b/src/actions/toggle_task_selection.rs @@ -1,16 +1,25 @@ -use crate::app::App; +use uuid::Uuid; + +use crate::{app::App, state::PanelState}; pub fn toggle_task_selection(app: &mut App) { - if let Some(task_index) = app - .state - .get_selected_panel_state() - .and_then(|s| s.selected()) - { - let task_id = app.get_current_tasks()[task_index].id; - if app.selected_tasks.contains(&task_id) { - app.selected_tasks.retain(|id| *id != task_id); - } else { - app.selected_tasks.push(task_id); + if app.state.active_panel != PanelState::ActiveTasks { + return; + } + + let selected = app.state.workspaces_tree_state.selected(); + if selected.is_empty() { + return; + } + + let selected_id = selected.last().unwrap(); + if let Ok(uuid) = Uuid::parse_str(selected_id) { + if app.tasks.iter().any(|t| t.id == uuid) { + if app.selected_tasks.contains(&uuid) { + app.selected_tasks.retain(|id| *id != uuid); + } else { + app.selected_tasks.push(uuid); + } } } } diff --git a/src/components/workspace/sidebar/active_workspaces.rs b/src/components/workspace/sidebar/active_workspaces.rs index 44a68d4..f690863 100644 --- a/src/components/workspace/sidebar/active_workspaces.rs +++ b/src/components/workspace/sidebar/active_workspaces.rs @@ -2,11 +2,12 @@ use ratatui::{ Frame, layout::Rect, style::{Color, Modifier, Style}, + text::{Line, Span}, widgets::{Block, BorderType, Borders}, }; use tui_tree_widget::{Tree, TreeItem}; -use crate::{app::App, components::shared, state::PanelState}; +use crate::{app::App, components::shared, models::Task, state::PanelState}; pub fn render(frame: &mut Frame, area: Rect, app: &mut App) { let is_active = @@ -20,11 +21,26 @@ pub fn render(frame: &mut Frame, area: Rect, app: &mut App) { }; for workspace in app.workspaces.iter().filter(|w| !w.archived) { - let workspace_tasks: Vec> = app + let mut tasks_in_workspace: Vec = app .tasks .iter() .filter(|t| t.workspace_id == Some(workspace.id) && !t.archived) - .map(|task| TreeItem::new_leaf(task.id.to_string(), task.title.clone())) + .cloned() + .collect(); + Task::sort_by_priority(&mut tasks_in_workspace); + + let workspace_tasks: Vec> = tasks_in_workspace + .iter() + .map(|task| { + let line = match &task.priority { + Some(p) => Line::from(vec![ + Span::styled(format!("{} ", p.label()), Style::default().fg(p.color())), + Span::raw(task.title.clone()), + ]), + None => Line::from(task.title.clone()), + }; + TreeItem::new_leaf(task.id.to_string(), line) + }) .collect(); let workspace_item = TreeItem::new( @@ -37,10 +53,23 @@ pub fn render(frame: &mut Frame, area: Rect, app: &mut App) { items.push(workspace_item); } - for task in &app.tasks { - if task.workspace_id.is_none() && !task.archived { - items.push(TreeItem::new_leaf(task.id.to_string(), task.title.clone())); - } + let mut orphan_tasks: Vec = app + .tasks + .iter() + .filter(|t| t.workspace_id.is_none() && !t.archived) + .cloned() + .collect(); + Task::sort_by_priority(&mut orphan_tasks); + + for task in &orphan_tasks { + let line = match &task.priority { + Some(p) => Line::from(vec![ + Span::styled(format!("{} ", p.label()), Style::default().fg(p.color())), + Span::raw(task.title.clone()), + ]), + None => Line::from(task.title.clone()), + }; + items.push(TreeItem::new_leaf(task.id.to_string(), line)); } let tree = Tree::new(&items) From 721c180ba28aced669ae929cf38c565777bc9e48 Mon Sep 17 00:00:00 2001 From: Simon Cardona Date: Sat, 31 Jan 2026 11:09:20 +0100 Subject: [PATCH 12/15] feat: support create orphelin task --- src/actions/create_task.rs | 4 +- src/actions/open_create_task_modal.rs | 19 +++++++-- src/actions/toggle_task_selection.rs | 25 +++++++++++ src/app.rs | 4 +- src/components/bottom_bar/key_hints.rs | 2 +- src/components/modals/create_task.rs | 4 +- .../workspace/sidebar/active_workspaces.rs | 42 ++++++++++++++----- src/keybindings.rs | 2 +- src/models/task.rs | 4 +- src/state.rs | 4 +- 10 files changed, 86 insertions(+), 24 deletions(-) diff --git a/src/actions/create_task.rs b/src/actions/create_task.rs index dec0e93..80dedfa 100644 --- a/src/actions/create_task.rs +++ b/src/actions/create_task.rs @@ -1,6 +1,8 @@ +use uuid::Uuid; + use crate::{app::App, db::repositories::TaskRepository, models}; -pub fn create_task(app: &mut App, title: String, workspace_id: String) { +pub fn create_task(app: &mut App, title: String, workspace_id: Option) { let new_task = models::Task::new(title, workspace_id); if let Err(e) = TaskRepository::create(&app.db.connection, &new_task) { diff --git a/src/actions/open_create_task_modal.rs b/src/actions/open_create_task_modal.rs index f44163f..404c52a 100644 --- a/src/actions/open_create_task_modal.rs +++ b/src/actions/open_create_task_modal.rs @@ -1,8 +1,21 @@ +use uuid::Uuid; + use crate::{app::App, state::PanelState}; pub fn open_create_task_modal(app: &mut App) { - if app.state.active_panel == PanelState::ActiveTasks { - let workspace_id = app.state.workspaces_tree_state.selected()[0].clone(); - app.state.open_create_task(workspace_id) + if app.state.active_panel != PanelState::ActiveTasks { + return; } + + let selected = app.state.workspaces_tree_state.selected(); + + let workspace_id = if selected.is_empty() { + None + } else { + Uuid::parse_str(&selected[0]).ok().filter(|uuid| { + app.workspaces.iter().any(|w| w.id == *uuid) + }) + }; + + app.state.open_create_task(workspace_id) } diff --git a/src/actions/toggle_task_selection.rs b/src/actions/toggle_task_selection.rs index b0bf8a0..4d05895 100644 --- a/src/actions/toggle_task_selection.rs +++ b/src/actions/toggle_task_selection.rs @@ -20,6 +20,31 @@ pub fn toggle_task_selection(app: &mut App) { } else { app.selected_tasks.push(uuid); } + return; + } + + if app.workspaces.iter().any(|w| w.id == uuid) { + let workspace_task_ids: Vec = app + .tasks + .iter() + .filter(|t| t.workspace_id == Some(uuid) && !t.archived) + .map(|t| t.id) + .collect(); + + let all_selected = workspace_task_ids + .iter() + .all(|id| app.selected_tasks.contains(id)); + + if all_selected { + app.selected_tasks + .retain(|id| !workspace_task_ids.contains(id)); + } else { + for task_id in workspace_task_ids { + if !app.selected_tasks.contains(&task_id) { + app.selected_tasks.push(task_id); + } + } + } } } } diff --git a/src/app.rs b/src/app.rs index c18432b..19a906b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -75,7 +75,7 @@ impl App { match &mut self.state.active_modal { Some(ModalState::CreateTask { input, workspace_id: _ }) => { - components::modals::create_task::render(frame, input); + components::modals::create_task::render(frame, input, "Create task"); } Some(ModalState::EditTask { task_id: _, input }) => { components::modals::edit_task::render(frame, input); @@ -100,7 +100,7 @@ impl App { components::modals::priority_task::render(frame, selected_option); } Some(ModalState::CreateWorkspace { input }) => { - components::modals::create_task::render(frame, input); + components::modals::create_task::render(frame, input, "Create workspace"); } Some(ModalState::DeleteWorkspace { workspace_id: _, diff --git a/src/components/bottom_bar/key_hints.rs b/src/components/bottom_bar/key_hints.rs index 5caaa0c..a5321f7 100644 --- a/src/components/bottom_bar/key_hints.rs +++ b/src/components/bottom_bar/key_hints.rs @@ -10,7 +10,7 @@ use crate::{app::App, state::PanelState}; pub fn render(frame: &mut Frame, area: Rect, app: &mut App) { let help_text = match app.state.active_panel { PanelState::ActiveTasks => { - "Create: c | Edit (Title): e | Edit: E | Delete: d | Complete: y | Archive: a | Select: | Keybindings: ?" + "Create: c | Workspace: w | Edit (Title): e | Edit: E | Delete: d | Complete: y | Archive: a | Select: | Keybindings: ?" } PanelState::ArchivedTasks => { "Unarchive: a | Complete: y | Delete: d | Select: | Keybindings: ?" diff --git a/src/components/modals/create_task.rs b/src/components/modals/create_task.rs index dfacf5b..aef912c 100644 --- a/src/components/modals/create_task.rs +++ b/src/components/modals/create_task.rs @@ -3,8 +3,8 @@ use tui_input::Input; use crate::components::shared; -pub fn render(frame: &mut Frame, input_state: &Input) { - let area = shared::modal::Modal::new("Create task") +pub fn render(frame: &mut Frame, input_state: &Input, title: &str) { + let area = shared::modal::Modal::new(title) .height(3) .render(frame); let width = area.width.saturating_sub(2) as usize; diff --git a/src/components/workspace/sidebar/active_workspaces.rs b/src/components/workspace/sidebar/active_workspaces.rs index f690863..f9d5f7b 100644 --- a/src/components/workspace/sidebar/active_workspaces.rs +++ b/src/components/workspace/sidebar/active_workspaces.rs @@ -32,23 +32,38 @@ pub fn render(frame: &mut Frame, area: Rect, app: &mut App) { let workspace_tasks: Vec> = tasks_in_workspace .iter() .map(|task| { + let is_selected = app.selected_tasks.contains(&task.id); + let title_style = if is_selected { + Style::default().fg(Color::LightGreen) + } else { + Style::default() + }; + let line = match &task.priority { Some(p) => Line::from(vec![ Span::styled(format!("{} ", p.label()), Style::default().fg(p.color())), - Span::raw(task.title.clone()), + Span::styled(task.title.clone(), title_style), ]), - None => Line::from(task.title.clone()), + None => Line::from(Span::styled(task.title.clone(), title_style)), }; TreeItem::new_leaf(task.id.to_string(), line) }) .collect(); - let workspace_item = TreeItem::new( - workspace.id.to_string(), - format!("{} ({})", workspace.title.clone(), workspace_tasks.len()), - workspace_tasks, - ) - .unwrap(); + let workspace_task_ids: Vec<_> = tasks_in_workspace.iter().map(|t| t.id).collect(); + let all_selected = + !workspace_task_ids.is_empty() && workspace_task_ids.iter().all(|id| app.selected_tasks.contains(id)); + + let workspace_title = if all_selected { + Line::from(Span::styled( + format!("{} ({})", workspace.title.clone(), workspace_tasks.len()), + Style::default().fg(Color::LightGreen), + )) + } else { + Line::from(format!("{} ({})", workspace.title.clone(), workspace_tasks.len())) + }; + + let workspace_item = TreeItem::new(workspace.id.to_string(), workspace_title, workspace_tasks).unwrap(); items.push(workspace_item); } @@ -62,12 +77,19 @@ pub fn render(frame: &mut Frame, area: Rect, app: &mut App) { Task::sort_by_priority(&mut orphan_tasks); for task in &orphan_tasks { + let is_selected = app.selected_tasks.contains(&task.id); + let title_style = if is_selected { + Style::default().fg(Color::LightGreen) + } else { + Style::default() + }; + let line = match &task.priority { Some(p) => Line::from(vec![ Span::styled(format!("{} ", p.label()), Style::default().fg(p.color())), - Span::raw(task.title.clone()), + Span::styled(task.title.clone(), title_style), ]), - None => Line::from(task.title.clone()), + None => Line::from(Span::styled(task.title.clone(), title_style)), }; items.push(TreeItem::new_leaf(task.id.to_string(), line)); } diff --git a/src/keybindings.rs b/src/keybindings.rs index 9c141db..7887ab8 100644 --- a/src/keybindings.rs +++ b/src/keybindings.rs @@ -186,7 +186,7 @@ pub fn handle_key_event(app: &mut App, event: &Event, terminal: &mut DefaultTerm } } crossterm::event::KeyCode::Char('c') => actions::open_create_task_modal(app), - crossterm::event::KeyCode::Char('s') => actions::open_create_workspace_modal(app), + crossterm::event::KeyCode::Char('w') => actions::open_create_workspace_modal(app), crossterm::event::KeyCode::Char('e') => actions::open_edit_title_modal(app), crossterm::event::KeyCode::Char('p') => actions::open_priority_modal(app), crossterm::event::KeyCode::Char('E') => actions::edit_task(app, terminal), diff --git a/src/models/task.rs b/src/models/task.rs index c9f9376..0cdf25c 100644 --- a/src/models/task.rs +++ b/src/models/task.rs @@ -18,7 +18,7 @@ pub struct Task { } impl Task { - pub fn new(title: impl Into, workspace_id: String) -> Self { + pub fn new(title: impl Into, workspace_id: Option) -> Self { Task { id: Uuid::new_v4(), title: title.into(), @@ -29,7 +29,7 @@ impl Task { created_at: Utc::now(), updated_at: None, archived_at: None, - workspace_id: Some(Uuid::parse_str(&workspace_id).unwrap()), + workspace_id, } } diff --git a/src/state.rs b/src/state.rs index 0ce3353..c3bbdbd 100644 --- a/src/state.rs +++ b/src/state.rs @@ -31,7 +31,7 @@ pub enum PanelState { pub enum ModalState { CreateTask { input: Input, - workspace_id: String, + workspace_id: Option, }, EditTask { task_id: Uuid, @@ -122,7 +122,7 @@ impl AppState { } } - pub fn open_create_task(&mut self, workspace_id: String) { + pub fn open_create_task(&mut self, workspace_id: Option) { self.active_modal = Some(ModalState::CreateTask { input: Input::default(), workspace_id, From 9a6d0c67b200a65f14e2f6579da2fb65cf4d0d18 Mon Sep 17 00:00:00 2001 From: Simon Cardona Date: Sat, 31 Jan 2026 11:11:37 +0100 Subject: [PATCH 13/15] fix: key_hints copy --- src/components/bottom_bar/key_hints.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/bottom_bar/key_hints.rs b/src/components/bottom_bar/key_hints.rs index a5321f7..791918b 100644 --- a/src/components/bottom_bar/key_hints.rs +++ b/src/components/bottom_bar/key_hints.rs @@ -10,7 +10,7 @@ use crate::{app::App, state::PanelState}; pub fn render(frame: &mut Frame, area: Rect, app: &mut App) { let help_text = match app.state.active_panel { PanelState::ActiveTasks => { - "Create: c | Workspace: w | Edit (Title): e | Edit: E | Delete: d | Complete: y | Archive: a | Select: | Keybindings: ?" + "Create task: c | Create workspace: w | Edit title: e | Edit: E | Delete: d | Complete: y | Archive: a | Select: | Keybindings: ?" } PanelState::ArchivedTasks => { "Unarchive: a | Complete: y | Delete: d | Select: | Keybindings: ?" From 879efbf93271bb936777402463e32360f15f9705 Mon Sep 17 00:00:00 2001 From: Simon Cardona Date: Sat, 31 Jan 2026 11:44:22 +0100 Subject: [PATCH 14/15] feat: add table on workspace --- src/components/workspace/context_view/mod.rs | 27 +++- .../workspace/context_view/task_view.rs | 147 ++++++++++-------- .../workspace/context_view/workspace_view.rs | 76 +++++++++ .../workspace/sidebar/active_workspaces.rs | 12 +- 4 files changed, 194 insertions(+), 68 deletions(-) create mode 100644 src/components/workspace/context_view/workspace_view.rs diff --git a/src/components/workspace/context_view/mod.rs b/src/components/workspace/context_view/mod.rs index ee84edc..62caae8 100644 --- a/src/components/workspace/context_view/mod.rs +++ b/src/components/workspace/context_view/mod.rs @@ -1,17 +1,36 @@ pub mod about; pub mod task_view; +pub mod workspace_view; use ratatui::{Frame, layout::Rect}; +use uuid::Uuid; use crate::{app::App, components::workspace::context_view, state::PanelState}; pub fn render(frame: &mut Frame, area: Rect, app: &mut App) { match app.state.active_panel { PanelState::ActiveTasks => { - let title = String::from(" Task details "); - let tasks = app.active_tasks(); - let current_list = &app.state.active_tasks_state; - context_view::task_view::render(frame, area, title, current_list, tasks); + let selected = app.state.workspaces_tree_state.selected(); + + if selected.is_empty() { + context_view::task_view::render_empty(frame, area); + return; + } + + let selected_id = selected.last().unwrap(); + if let Ok(uuid) = Uuid::parse_str(selected_id) { + if let Some(workspace) = app.workspaces.iter().find(|w| w.id == uuid) { + context_view::workspace_view::render(frame, area, workspace, &app.tasks); + return; + } + + if let Some(task) = app.tasks.iter().find(|t| t.id == uuid) { + context_view::task_view::render_task(frame, area, task); + return; + } + } + + context_view::task_view::render_empty(frame, area); } PanelState::ArchivedTasks => { let title = String::from(" Task details "); diff --git a/src/components/workspace/context_view/task_view.rs b/src/components/workspace/context_view/task_view.rs index 7e10684..6b30ea5 100644 --- a/src/components/workspace/context_view/task_view.rs +++ b/src/components/workspace/context_view/task_view.rs @@ -20,68 +20,7 @@ pub fn render( let text = if let Some(selected_idx) = current_list.selected() { if let Some(task) = tasks.get(selected_idx) { - let updated_at = task - .updated_at - .map(|d| d.with_timezone(&Local).format("%d/%m/%Y %H:%M").to_string()) - .unwrap_or_else(|| "-".to_string()); - let archived_at = task - .archived_at - .map(|d| d.with_timezone(&Local).format("%d/%m/%Y %H:%M").to_string()) - .unwrap_or_else(|| "-".to_string()); - - let mut lines = vec![ - Line::from(Span::styled("---", dim_style)), - Line::from(Span::styled( - format!("ID : {}", task.id), - dim_style, - )), - Line::from(Span::styled( - format!( - "Created_at : {}", - task.created_at - .with_timezone(&Local) - .format("%d/%m/%Y %H:%M") - ), - dim_style, - )), - Line::from(Span::styled( - format!("Updated_at : {}", updated_at), - dim_style, - )), - Line::from(Span::styled( - format!("Archived_at : {}", archived_at), - dim_style, - )), - Line::from(Span::styled( - format!("Completed : {}", task.completed), - dim_style, - )), - Line::from(Span::styled( - format!( - "Priority : {}", - match &task.priority { - Some(p) => Priority::label(p), - None => "x", - } - ), - dim_style, - )), - Line::from(Span::styled("---", dim_style)), - Line::from(""), - Line::from(Span::styled( - format!("# {}", task.title), - Style::default().add_modifier(Modifier::BOLD), - )), - Line::from(""), - ]; - - if let Some(description) = &task.description { - for desc_line in description.lines() { - lines.push(Line::from(format!(" {}", desc_line))); - } - } - - lines + build_task_lines(task) } else { vec![Line::from("Task not found")] } @@ -99,3 +38,87 @@ pub fn render( .wrap(Wrap { trim: true }); frame.render_widget(main_view, area); } + +pub fn render_task(frame: &mut Frame, area: Rect, task: &Task) { + let text = build_task_lines(task); + + let main_view = Paragraph::new(text) + .block( + Block::new() + .title(" Task details ") + .borders(Borders::ALL) + .border_type(BorderType::Rounded), + ) + .wrap(Wrap { trim: true }); + frame.render_widget(main_view, area); +} + +pub fn render_empty(frame: &mut Frame, area: Rect) { + let dim_style = Style::default().fg(Color::DarkGray); + let text = vec![Line::from(Span::styled("No task selected", dim_style))]; + + let main_view = Paragraph::new(text) + .block( + Block::new() + .title(" Task details ") + .borders(Borders::ALL) + .border_type(BorderType::Rounded), + ) + .wrap(Wrap { trim: true }); + frame.render_widget(main_view, area); +} + +fn build_task_lines(task: &Task) -> Vec> { + let dim_style = Style::default().fg(Color::DarkGray); + + let updated_at = task + .updated_at + .map(|d| d.with_timezone(&Local).format("%d/%m/%Y %H:%M").to_string()) + .unwrap_or_else(|| "-".to_string()); + let archived_at = task + .archived_at + .map(|d| d.with_timezone(&Local).format("%d/%m/%Y %H:%M").to_string()) + .unwrap_or_else(|| "-".to_string()); + + let mut lines = vec![ + Line::from(Span::styled("---", dim_style)), + Line::from(Span::styled(format!("ID : {}", task.id), dim_style)), + Line::from(Span::styled( + format!( + "Created_at : {}", + task.created_at + .with_timezone(&Local) + .format("%d/%m/%Y %H:%M") + ), + dim_style, + )), + Line::from(Span::styled(format!("Updated_at : {}", updated_at), dim_style)), + Line::from(Span::styled(format!("Archived_at : {}", archived_at), dim_style)), + Line::from(Span::styled(format!("Completed : {}", task.completed), dim_style)), + Line::from(Span::styled( + format!( + "Priority : {}", + match &task.priority { + Some(p) => Priority::label(p), + None => "x", + } + ), + dim_style, + )), + Line::from(Span::styled("---", dim_style)), + Line::from(""), + Line::from(Span::styled( + format!("# {}", task.title), + Style::default().add_modifier(Modifier::BOLD), + )), + Line::from(""), + ]; + + if let Some(description) = &task.description { + for desc_line in description.lines() { + lines.push(Line::from(format!(" {}", desc_line))); + } + } + + lines +} diff --git a/src/components/workspace/context_view/workspace_view.rs b/src/components/workspace/context_view/workspace_view.rs new file mode 100644 index 0000000..09e5cb5 --- /dev/null +++ b/src/components/workspace/context_view/workspace_view.rs @@ -0,0 +1,76 @@ +use chrono::Local; +use ratatui::{ + Frame, + layout::{Alignment, Constraint, Rect}, + style::{Color, Modifier, Style}, + text::Line, + widgets::{Block, BorderType, Borders, Cell, Row, Table}, +}; + +use crate::models::{Task, Workspace}; + +pub fn render(frame: &mut Frame, area: Rect, workspace: &Workspace, tasks: &[Task]) { + let mut workspace_tasks: Vec = tasks + .iter() + .filter(|t| t.workspace_id == Some(workspace.id) && !t.archived) + .cloned() + .collect(); + Task::sort_by_priority(&mut workspace_tasks); + + let header = Row::new(vec![ + Cell::from(Line::from("Priority").alignment(Alignment::Center)), + Cell::from("Title"), + Cell::from(Line::from("Created").alignment(Alignment::Center)), + Cell::from(Line::from("Completed").alignment(Alignment::Center)), + ]) + .style(Style::default().add_modifier(Modifier::BOLD)) + .height(1); + + let rows: Vec = workspace_tasks + .iter() + .map(|task| { + let priority_cell = match &task.priority { + Some(p) => Cell::from(Line::from(p.label()).alignment(Alignment::Center)) + .style(Style::default().fg(p.color())), + None => Cell::from(Line::from("-").alignment(Alignment::Center)), + }; + + let created_at = task + .created_at + .with_timezone(&Local) + .format("%d/%m/%Y") + .to_string(); + + let completed_cell = if task.completed { + Cell::from(Line::from("✓").alignment(Alignment::Center)) + .style(Style::default().fg(Color::Green)) + } else { + Cell::from(Line::from("✗").alignment(Alignment::Center)) + .style(Style::default().fg(Color::DarkGray)) + }; + + Row::new(vec![ + priority_cell, + Cell::from(task.title.clone()), + Cell::from(Line::from(created_at).alignment(Alignment::Center)), + completed_cell, + ]) + }) + .collect(); + + let table = Table::new(rows, [ + Constraint::Length(8), + Constraint::Fill(1), + Constraint::Length(12), + Constraint::Length(10), + ]) + .header(header) + .block( + Block::new() + .title(format!(" {} ({} tasks) ", workspace.title, workspace_tasks.len())) + .borders(Borders::ALL) + .border_type(BorderType::Rounded), + ); + + frame.render_widget(table, area); +} diff --git a/src/components/workspace/sidebar/active_workspaces.rs b/src/components/workspace/sidebar/active_workspaces.rs index f9d5f7b..987b34b 100644 --- a/src/components/workspace/sidebar/active_workspaces.rs +++ b/src/components/workspace/sidebar/active_workspaces.rs @@ -33,12 +33,16 @@ pub fn render(frame: &mut Frame, area: Rect, app: &mut App) { .iter() .map(|task| { let is_selected = app.selected_tasks.contains(&task.id); - let title_style = if is_selected { + let mut title_style = if is_selected { Style::default().fg(Color::LightGreen) } else { Style::default() }; + if task.completed { + title_style = title_style.add_modifier(Modifier::CROSSED_OUT); + } + let line = match &task.priority { Some(p) => Line::from(vec![ Span::styled(format!("{} ", p.label()), Style::default().fg(p.color())), @@ -78,12 +82,16 @@ pub fn render(frame: &mut Frame, area: Rect, app: &mut App) { for task in &orphan_tasks { let is_selected = app.selected_tasks.contains(&task.id); - let title_style = if is_selected { + let mut title_style = if is_selected { Style::default().fg(Color::LightGreen) } else { Style::default() }; + if task.completed { + title_style = title_style.add_modifier(Modifier::CROSSED_OUT); + } + let line = match &task.priority { Some(p) => Line::from(vec![ Span::styled(format!("{} ", p.label()), Style::default().fg(p.color())), From 311b6f70fd11193afa7e46cfc28a7f9992025d0a Mon Sep 17 00:00:00 2001 From: Simon Cardona Date: Mon, 2 Feb 2026 12:55:33 +0100 Subject: [PATCH 15/15] feat: add create oprhean task --- src/keybindings.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/keybindings.rs b/src/keybindings.rs index 7887ab8..3b5aeff 100644 --- a/src/keybindings.rs +++ b/src/keybindings.rs @@ -186,6 +186,7 @@ pub fn handle_key_event(app: &mut App, event: &Event, terminal: &mut DefaultTerm } } crossterm::event::KeyCode::Char('c') => actions::open_create_task_modal(app), + crossterm::event::KeyCode::Char('C') => app.state.open_create_task(None), crossterm::event::KeyCode::Char('w') => actions::open_create_workspace_modal(app), crossterm::event::KeyCode::Char('e') => actions::open_edit_title_modal(app), crossterm::event::KeyCode::Char('p') => actions::open_priority_modal(app),