diff --git a/.config/config.ron b/.config/config.ron index b7545e3..f56c87e 100644 --- a/.config/config.ron +++ b/.config/config.ron @@ -2,26 +2,26 @@ directory: "/Users/suri/dev/projects/filaments/ZettelKasten", global_key_binds: { "up": MoveUp, + "ctrl-z": Suspend, "ctrl-c": Quit, "down": MoveDown, - "ctrl-z": Suspend, }, zk: ( keybinds: { - "": NewZettel, - "enter": OpenZettel, "tab": SwitchTo( region: Todo, ), + "": NewZettel, + "enter": OpenZettel, }, ), todo: ( keybinds: { + "j": MoveDown, "tab": SwitchTo( region: Zk, ), "k": MoveUp, - "j": MoveDown, }, ), ) \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index adc099d..ee12099 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2315,6 +2315,7 @@ name = "filaments" version = "0.1.0" dependencies = [ "anyhow", + "async-recursion", "async-trait", "better-panic", "clap", @@ -2344,6 +2345,7 @@ dependencies = [ "tracing", "tracing-error", "tracing-subscriber", + "tree", "vergen-gix", ] diff --git a/Cargo.toml b/Cargo.toml index 5b8831a..b9a59f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ snafu = "0.9.0" serde = "1.0.228" tracing = "0.1.44" tokio = { version = "1.51.1", features = ["full"] } +tree = {path = "crates/tree"} [package] name = "filaments" @@ -83,6 +84,8 @@ nucleo-matcher = "0.3.1" ron = "0.12.1" tower-lsp = "0.20.0" notify = "8.2.0" +tree = {workspace = true} +async-recursion = "1.1.1" [build-dependencies] anyhow = "1.0.102" diff --git a/crates/dto/src/entity/group.rs b/crates/dto/src/entity/group.rs index d7d4d96..fd2c5bb 100644 --- a/crates/dto/src/entity/group.rs +++ b/crates/dto/src/entity/group.rs @@ -2,8 +2,8 @@ use migration::prelude::Local; use migration::types::*; -use sea_orm::entity::prelude::*; use sea_orm::ActiveValue::Set; +use sea_orm::entity::prelude::*; use std::future::ready; use std::pin::Pin; diff --git a/crates/dto/src/lib.rs b/crates/dto/src/lib.rs index dc6cb99..ad88025 100644 --- a/crates/dto/src/lib.rs +++ b/crates/dto/src/lib.rs @@ -13,6 +13,8 @@ pub use sea_orm::EntityTrait; pub use sea_orm::IntoActiveModel; pub use sea_orm::QueryFilter; pub use sea_orm::QueryOrder; +pub use sea_orm::entity::compound::HasMany; +pub use sea_orm::entity::compound::HasOne; /// Exporting this as a generic NanoId. pub use migration::types::NanoId; diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 6835ed6..6b2b194 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -59,6 +59,7 @@ pub enum Commands { // default values if they arent present / aren't able to be // parsed properly // Import(ImportArgs), + Test, } #[derive(Subcommand, Debug)] @@ -90,6 +91,16 @@ pub enum TodoSubcommand { #[arg(short, long)] parent_id: Option, }, + + Task { + /// Name of the task + #[arg(short, long)] + name: String, + + /// Parent group of the task + #[arg(short, long)] + parent_id: NanoId, + }, } // #[derive(Subcommand, Debug)] diff --git a/src/cli/process.rs b/src/cli/process.rs index fb4f78f..441a04c 100644 --- a/src/cli/process.rs +++ b/src/cli/process.rs @@ -4,9 +4,10 @@ use std::{ io::Write, }; -use color_eyre::eyre::{Context, Result}; +use color_eyre::eyre::{Context, Result, eyre}; use dto::{ - GroupActiveModel, GroupEntity, IntoActiveModel, TagActiveModel, TagEntity, ZettelEntity, + GroupActiveModel, GroupEntity, HasOne, IntoActiveModel, TagActiveModel, TagEntity, + TaskActiveModel, TaskEntity, ZettelEntity, }; use tower_lsp::{LspService, Server}; @@ -14,10 +15,11 @@ use crate::{ cli::{Commands, ZettelSubcommand}, config::{Config, get_config_dir}, lsp::Backend, - types::{Group, Kasten, Priority, Tag, Zettel}, + types::{Group, Kasten, Priority, Tag, Task, Zettel}, }; impl Commands { + #[expect(clippy::too_many_lines)] pub async fn process(self) -> Result<()> { match self { Self::Init { name } => { @@ -80,19 +82,18 @@ impl Commands { match command { super::TodoSubcommand::Group { name, parent_id } => { // lets create a tag for this first group first - let tag: Tag = TagActiveModel::builder() .set_name(name.clone()) .insert(&kt.db) .await? .into(); - //TODO: this zettel would need to be created with the parent of all - // of its groups? let tag_id = tag.id.clone(); + // then create the zettel for the group let zettel = Zettel::new(name.clone(), &mut kt, vec![tag]).await?; + // then insert that shi let inserted = GroupActiveModel::builder() .set_name(name) .set_parent_group_id(parent_id) @@ -106,7 +107,6 @@ impl Commands { ) .set_zettel( ZettelEntity::load() - // .with(TagEntity) .filter_by_nano_id(zettel.id) .one(&kt.db) .await? @@ -129,8 +129,68 @@ impl Commands { println!("created group {group:#?}"); } + super::TodoSubcommand::Task { name, parent_id } => { + // need to create the task + let parent = GroupEntity::load() + .with(TagEntity) + .filter_by_nano_id(parent_id) + .one(&kt.db) + .await + .with_context(|| "failed to communicate with db")? + .ok_or_else(|| eyre!("could not find the group"))?; + + let HasOne::Loaded(tag) = parent.tag else { + panic!("this has to be loaded since we just loaded it right above") + }; + + let zettel = + Zettel::new(name.clone(), &mut kt, vec![(*tag).into()]).await?; + + let inserted = TaskActiveModel::builder() + .set_name(name) + .set_group_id(parent.nano_id.clone()) + .set_priority(Priority::default()) + .set_zettel( + ZettelEntity::load() + .filter_by_nano_id(zettel.id) + .one(&kt.db) + .await? + .expect("Zettel must exist since we just created it") + .into_active_model(), + ) + .insert(&kt.db) + .await?; + + let group = GroupEntity::load() + .with(TagEntity) + .with((ZettelEntity, TagEntity)) + .filter_by_nano_id(parent.nano_id) + .one(&kt.db) + .await? + .expect("We just inserted it"); + + let mut task = TaskEntity::load() + .with((ZettelEntity, TagEntity)) + .filter_by_nano_id(inserted.nano_id) + .one(&kt.db) + .await? + .expect("We just inserted it"); + + task.group = HasOne::Loaded(Box::new(group)); + + println!("task: {task:#?}"); + + let task: Task = task.into(); + + println!("created task: {task:#?}"); + } } } + Self::Test => { + let conf = Config::parse()?; + let kt = Kasten::instansiate(conf.fil_dir).await?; + println!("kt: {kt:#?}"); + } } Ok(()) diff --git a/src/tui/components/todo/explorer.rs b/src/tui/components/todo/explorer.rs new file mode 100644 index 0000000..6971712 --- /dev/null +++ b/src/tui/components/todo/explorer.rs @@ -0,0 +1,28 @@ +#![allow(dead_code)] + +use dto::NanoId; +use ratatui::{text::Span, widgets::ListState}; + +pub struct Explorer<'text> { + pub render_list: ratatui::widgets::List<'text>, + pub id_list: Vec, + pub state: ListState, + pub width: u16, +} + +pub struct ExplorerListItem<'text> { + name: Span<'text>, +} + +// impl From<&Task> for ExplorerListItem { +// fn from(value: &Task) -> Self { +// Self { +// name: Span { style: (), content: () } +// } +// } +// } + +// impl Explorer { +// pub async fn + +// } diff --git a/src/tui/components/todo/mod.rs b/src/tui/components/todo/mod.rs index 4037656..dc9da63 100644 --- a/src/tui/components/todo/mod.rs +++ b/src/tui/components/todo/mod.rs @@ -1,7 +1,12 @@ use async_trait::async_trait; +use color_eyre::eyre::Result; +use dto::{ + ColumnTrait as _, GroupColumns, GroupEntity, QueryFilter as _, TagEntity, TaskEntity, + ZettelEntity, +}; use ratatui::{ Frame, - layout::{Constraint, Layout, Rect}, + layout::{Constraint, Layout, Rect, Size}, style::{Color, Stylize}, widgets::Block, }; @@ -12,6 +17,8 @@ use crate::{ types::KastenHandle, }; +mod explorer; + #[expect(dead_code)] pub struct Todo { signal_tx: Option>, @@ -20,12 +27,24 @@ pub struct Todo { } impl Todo { - pub fn new(kh: KastenHandle) -> Self { - Self { + pub async fn new(kh: KastenHandle) -> Result { + let kt = kh.read().await; + + let _roots = GroupEntity::load() + .with(TagEntity) + .with(TaskEntity) + .with((ZettelEntity, TagEntity)) + .filter(GroupColumns::ParentGroupId.is_null()) + .all(&kt.db) + .await?; + + drop(kt); + + Ok(Self { kh, layouts: Layouts::default(), signal_tx: None, - } + }) } } @@ -44,6 +63,12 @@ impl Default for Layouts { #[async_trait] impl Component for Todo { + async fn init(&mut self, area: Size) -> color_eyre::Result<()> { + let _ = area; // to appease clippy + + Ok(()) + } + async fn update(&mut self, _signal: Signal) -> color_eyre::Result> { Ok(None) } diff --git a/src/tui/components/viewport/mod.rs b/src/tui/components/viewport/mod.rs index 01aee4b..575f684 100644 --- a/src/tui/components/viewport/mod.rs +++ b/src/tui/components/viewport/mod.rs @@ -41,7 +41,7 @@ impl Viewport<'_> { _layouts: Layouts::default(), switcher, zk: Zk::new(kh.clone()).await?, - todo: Todo::new(kh.clone()), + todo: Todo::new(kh.clone()).await?, active_region: Region::default(), kh, }) diff --git a/src/types/filaments.rs b/src/types/filaments.rs index 9e82c8b..747b17f 100644 --- a/src/types/filaments.rs +++ b/src/types/filaments.rs @@ -6,7 +6,7 @@ use egui_graphs::{ petgraph::{Directed, graph::NodeIndex, prelude::StableGraph}, }; -use crate::types::{Index, Link, ZettelId, index::ZettelOnDisk}; +use crate::types::{Index, Link, ZettelId, kasten::ZettelOnDisk}; pub type ZkGraph = Graph; diff --git a/src/types/group.rs b/src/types/group.rs index b2f252d..3b2043d 100644 --- a/src/types/group.rs +++ b/src/types/group.rs @@ -12,6 +12,7 @@ pub struct Group { pub id: NanoId, pub name: String, pub priority: Priority, + pub parent_id: Option, pub created_at: DateTime, pub modified_at: DateTime, /// The `Zettel` that is related to this `Group`. @@ -30,6 +31,7 @@ impl From for Group { id: value.nano_id, name: value.name, priority: value.priority.into(), + parent_id: value.parent_group_id, created_at: value.created_at, modified_at: value.modified_at, zettel: value diff --git a/src/types/index.rs b/src/types/kasten/index/mod.rs similarity index 98% rename from src/types/index.rs rename to src/types/kasten/index/mod.rs index 5f6cef3..086bc7c 100644 --- a/src/types/index.rs +++ b/src/types/kasten/index/mod.rs @@ -18,7 +18,7 @@ use crate::types::{FrontMatter, Link, ZettelId, frontmatter::Body}; #[derive(Debug, Clone)] pub struct Index { pub(super) zods: HashMap, - pub(super) outgoing_links: HashMap>, + pub outgoing_links: HashMap>, // pub(super) incoming_links: HashMap>, } @@ -230,25 +230,25 @@ impl Index { Ok(()) } + pub fn get_links(&self, zid: &ZettelId) -> &Vec { + self.outgoing_links + .get(zid) + .expect("Invariant broken. Any zid we look up exist inside this map") + } + + pub fn sync_with_db(&self, _db: &DatabaseConnection) { + todo!() + } + pub fn get_zod(&self, zid: &ZettelId) -> &ZettelOnDisk { self.zods.get(zid).expect("Invariant broken. Any zid we lookup must exist in the index, otherwise the db is corrupt or not sync'd.") } - fn get_zod_mut(&mut self, zid: &ZettelId) -> &mut ZettelOnDisk { + pub fn get_zod_mut(&mut self, zid: &ZettelId) -> &mut ZettelOnDisk { self.zods.get_mut(zid).expect("Invariant broken. Any zid we lookup must exist in the index, otherwise the db is corrupt or not sync'd.") } - pub fn get_links(&self, zid: &ZettelId) -> &Vec { - self.outgoing_links - .get(zid) - .expect("Invariant broken. Any zid we look up exist inside this map") - } - pub const fn zods(&self) -> &HashMap { &self.zods } - - pub fn sync_with_db(&self, _db: &DatabaseConnection) { - todo!() - } } diff --git a/src/types/kasten/index/zod.rs b/src/types/kasten/index/zod.rs new file mode 100644 index 0000000..8920caa --- /dev/null +++ b/src/types/kasten/index/zod.rs @@ -0,0 +1,6 @@ +#[derive(Debug, Clone)] +pub struct ZettelOnDisk { + pub fm: FrontMatter, + pub body: Body, + pub path: PathBuf, +} diff --git a/src/types/kasten.rs b/src/types/kasten/mod.rs similarity index 92% rename from src/types/kasten.rs rename to src/types/kasten/mod.rs index f819425..391b79e 100644 --- a/src/types/kasten.rs +++ b/src/types/kasten/mod.rs @@ -11,9 +11,15 @@ use tokio::{ }; use tracing::debug; -use crate::types::{FrontMatter, Index, ZettelId, index::ZettelOnDisk}; +use crate::types::{FrontMatter, ZettelId}; -#[derive(Debug, Clone)] +mod index; +pub use index::Index; +pub use index::ZettelOnDisk; +mod todo_tree; +pub use todo_tree::{TodoNode, TodoTree}; + +#[derive(Debug)] pub struct Kasten { /// Private field so it can only be instantiated from a `Path` _private: (), @@ -22,6 +28,8 @@ pub struct Kasten { pub index: Index, + pub todo_tree: TodoTree, + pub db: DatabaseConnection, } @@ -54,11 +62,14 @@ impl Kasten { // run da migrations every time we connect, just in case Migrator::up(&conn, None).await?; + let todo_tree = TodoTree::construct(&conn).await?; + Ok(Self { _private: (), db: conn, root, index, + todo_tree, }) } diff --git a/src/types/kasten/todo_tree.rs b/src/types/kasten/todo_tree.rs new file mode 100644 index 0000000..c144dfc --- /dev/null +++ b/src/types/kasten/todo_tree.rs @@ -0,0 +1,126 @@ +use std::collections::HashMap; + +use color_eyre::eyre::{Context, Result}; +use dto::{ + ColumnTrait as _, DatabaseConnection, GroupColumns, GroupEntity, NanoId, QueryFilter as _, + TagEntity, TaskColumns, TaskEntity, ZettelEntity, +}; +use tree::{InsertBehavior, Node, NodeId, Tree}; + +use crate::types::{Group, Task}; + +#[expect(dead_code)] +#[derive(Debug, Clone)] +pub enum TodoNode { + Root, + Group(Box), + Task(Box), +} + +#[derive(Debug)] +pub struct TodoTree { + tree: Tree, + nanoid_to_nodeid: HashMap, + #[expect(dead_code)] + root_id: NodeId, +} + +impl TodoTree { + pub async fn construct(db: &DatabaseConnection) -> Result { + let mut tree = Tree::::new(); + let root_id = tree + .insert(Node::new(TodoNode::Root), InsertBehavior::AsRoot) + .with_context(|| "Could not create root node.")?; + + let root_groups: Vec = GroupEntity::load() + .with(TagEntity) + .with(TaskEntity) + .with((ZettelEntity, TagEntity)) + .filter(GroupColumns::ParentGroupId.is_null()) + .all(db) + .await? + .into_iter() + .map(Into::into) + .collect(); + + let mut todo_tree = Self { + tree, + nanoid_to_nodeid: HashMap::new(), + root_id: root_id.clone(), + }; + + for group in root_groups { + todo_tree + .add_group_to_tree(db, &root_id, Box::new(group)) + .await?; + } + + Ok(todo_tree) + } + + #[async_recursion::async_recursion] + async fn add_group_to_tree( + &mut self, + db: &DatabaseConnection, + parent_node_id: &NodeId, + group: Box, + ) -> Result<()> { + let group_id = group.id.clone(); + + let group_node_id = self.tree.insert( + Node::new(TodoNode::Group(group)), + InsertBehavior::UnderNode(parent_node_id), + )?; + + self.nanoid_to_nodeid + .insert(group_id.clone(), group_node_id.clone()); + + let group_model = GroupEntity::load() + .with(TagEntity) + .with((ZettelEntity, TagEntity)) + .filter_by_nano_id(group_id.clone()) + .one(db) + .await? + .expect("We just inserted it"); + + let tasks: Vec = TaskEntity::load() + .with((ZettelEntity, TagEntity)) + .filter(TaskColumns::GroupId.eq(group_id.clone())) + .all(db) + .await? + .into_iter() + .map(|mut am| { + am.group = dto::HasOne::Loaded(Box::new(group_model.clone())); + am.into() + }) + .collect(); + + for task in tasks { + let task_id = task.id.clone(); + let task_node_id = self.tree.insert( + Node::new(TodoNode::Task(Box::new(task))), + InsertBehavior::UnderNode(&group_node_id), + )?; + + self.nanoid_to_nodeid.insert(task_id, task_node_id); + } + + let children_groups: Vec = GroupEntity::load() + .with(TagEntity) + .with(TaskEntity) + .with((ZettelEntity, TagEntity)) + .filter(GroupColumns::ParentGroupId.eq(group_id)) + .all(db) + .await? + .into_iter() + .map(Into::into) + .collect(); + + for group in children_groups { + self.add_group_to_tree(db, &group_node_id, Box::new(group)) + .await?; + } + + Ok(()) + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs index 54f191f..ee73e81 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -15,7 +15,6 @@ mod group; pub use group::Group; mod task; -#[expect(unused_imports)] pub use task::Task; mod link; @@ -24,12 +23,15 @@ pub use link::Link; mod filaments; pub use filaments::Filaments; -mod index; -pub use index::Index; - mod kasten; + +pub use kasten::Index; pub use kasten::Kasten; pub use kasten::KastenHandle; +#[expect(unused_imports)] +pub use kasten::TodoNode; +#[expect(unused_imports)] +pub use kasten::TodoTree; mod frontmatter; pub use frontmatter::FrontMatter; diff --git a/src/types/task.rs b/src/types/task.rs index 7f9d2f5..2c2743e 100644 --- a/src/types/task.rs +++ b/src/types/task.rs @@ -13,6 +13,7 @@ pub struct Task { pub name: String, pub priority: Priority, pub due: Option, + pub group_id: NanoId, pub finished_at: Option, pub created_at: DateTime, pub modified_at: DateTime, @@ -29,6 +30,7 @@ impl From for Task { name: value.name, priority: value.priority.into(), due: value.due, + group_id: value.group_id, finished_at: value.finished_at, created_at: value.created_at, modified_at: value.modified_at,