My personal-knowledge-system, with deeply integrated task tracking and long term goal planning capabilities.
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #6 from suri-codes/kasten

Kasten

authored by

Surendra Jammishetti and committed by
GitHub
75011e20 0bc11e3c

+576 -33
+149 -3
Cargo.lock
··· 24 24 source = "registry+https://github.com/rust-lang/crates.io-index" 25 25 checksum = "5351dcebb14b579ccab05f288596b2ae097005be7ee50a7c3d4ca9d0d5a66f6a" 26 26 dependencies = [ 27 + "enumn", 28 + "serde", 27 29 "uuid", 28 30 ] 29 31 ··· 147 149 "const-random", 148 150 "getrandom 0.3.4", 149 151 "once_cell", 152 + "serde", 150 153 "version_check", 151 154 "zerocopy", 152 155 ] ··· 1508 1511 ] 1509 1512 1510 1513 [[package]] 1514 + name = "crossbeam-deque" 1515 + version = "0.8.6" 1516 + source = "registry+https://github.com/rust-lang/crates.io-index" 1517 + checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 1518 + dependencies = [ 1519 + "crossbeam-epoch", 1520 + "crossbeam-utils", 1521 + ] 1522 + 1523 + [[package]] 1524 + name = "crossbeam-epoch" 1525 + version = "0.9.18" 1526 + source = "registry+https://github.com/rust-lang/crates.io-index" 1527 + checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 1528 + dependencies = [ 1529 + "crossbeam-utils", 1530 + ] 1531 + 1532 + [[package]] 1511 1533 name = "crossbeam-queue" 1512 1534 version = "0.3.12" 1513 1535 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1889 1911 dependencies = [ 1890 1912 "bytemuck", 1891 1913 "emath", 1914 + "serde", 1892 1915 ] 1893 1916 1894 1917 [[package]] ··· 1941 1964 "log", 1942 1965 "nohash-hasher", 1943 1966 "profiling", 1967 + "ron", 1968 + "serde", 1944 1969 "smallvec", 1945 1970 "unicode-segmentation", 1946 1971 ] ··· 2002 2027 "wasm-bindgen", 2003 2028 "web-sys", 2004 2029 "winit", 2030 + ] 2031 + 2032 + [[package]] 2033 + name = "egui_graphs" 2034 + version = "0.30.0" 2035 + source = "git+https://github.com/suri-codes/egui_graphs?rev=3327417007520300782574e3d6bc377bbafa8b5a#3327417007520300782574e3d6bc377bbafa8b5a" 2036 + dependencies = [ 2037 + "egui", 2038 + "getrandom 0.2.17", 2039 + "petgraph", 2040 + "rand 0.9.2", 2041 + "serde", 2042 + "web-time", 2005 2043 ] 2006 2044 2007 2045 [[package]] ··· 2020 2058 checksum = "0a05cd8bdf3b598488c627ca97c7fe8909448ffa26278dd3c7e535cdb554d721" 2021 2059 dependencies = [ 2022 2060 "bytemuck", 2061 + "serde", 2023 2062 ] 2024 2063 2025 2064 [[package]] ··· 2065 2104 ] 2066 2105 2067 2106 [[package]] 2107 + name = "enumn" 2108 + version = "0.1.14" 2109 + source = "registry+https://github.com/rust-lang/crates.io-index" 2110 + checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" 2111 + dependencies = [ 2112 + "proc-macro2", 2113 + "quote", 2114 + "syn 2.0.117", 2115 + ] 2116 + 2117 + [[package]] 2068 2118 name = "epaint" 2069 2119 version = "0.34.1" 2070 2120 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2081 2131 "parking_lot", 2082 2132 "profiling", 2083 2133 "self_cell", 2134 + "serde", 2084 2135 "skrifa", 2085 2136 "smallvec", 2086 2137 "vello_cpu", ··· 2248 2299 "directories", 2249 2300 "dto", 2250 2301 "eframe", 2302 + "egui_graphs", 2251 2303 "futures", 2252 2304 "human-panic", 2253 2305 "kdl", 2306 + "pulldown-cmark", 2307 + "rand 0.10.0", 2254 2308 "ratatui", 2309 + "rayon", 2255 2310 "serde", 2256 2311 "signal-hook 0.4.3", 2257 2312 "strum 0.28.0", ··· 2304 2359 checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" 2305 2360 2306 2361 [[package]] 2362 + name = "fixedbitset" 2363 + version = "0.5.7" 2364 + source = "registry+https://github.com/rust-lang/crates.io-index" 2365 + checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" 2366 + 2367 + [[package]] 2307 2368 name = "flate2" 2308 2369 version = "1.1.9" 2309 2370 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2349 2410 checksum = "73829a7b5c91198af28a99159b7ae4afbb252fb906159ff7f189f3a2ceaa3df2" 2350 2411 dependencies = [ 2351 2412 "bytemuck", 2413 + "serde", 2352 2414 ] 2353 2415 2354 2416 [[package]] ··· 2526 2588 ] 2527 2589 2528 2590 [[package]] 2591 + name = "getopts" 2592 + version = "0.2.24" 2593 + source = "registry+https://github.com/rust-lang/crates.io-index" 2594 + checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" 2595 + dependencies = [ 2596 + "unicode-width 0.2.2", 2597 + ] 2598 + 2599 + [[package]] 2529 2600 name = "getrandom" 2530 2601 version = "0.2.17" 2531 2602 source = "registry+https://github.com/rust-lang/crates.io-index" 2532 2603 checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" 2533 2604 dependencies = [ 2534 2605 "cfg-if", 2606 + "js-sys", 2535 2607 "libc", 2536 2608 "wasi", 2609 + "wasm-bindgen", 2537 2610 ] 2538 2611 2539 2612 [[package]] ··· 3425 3498 "log", 3426 3499 "presser", 3427 3500 "thiserror 2.0.18", 3428 - "windows 0.61.3", 3501 + "windows 0.62.2", 3429 3502 ] 3430 3503 3431 3504 [[package]] ··· 3629 3702 "js-sys", 3630 3703 "log", 3631 3704 "wasm-bindgen", 3632 - "windows-core 0.61.2", 3705 + "windows-core 0.62.2", 3633 3706 ] 3634 3707 3635 3708 [[package]] ··· 4322 4395 "rgb", 4323 4396 "sea-orm", 4324 4397 "sea-orm-migration", 4398 + "serde", 4325 4399 ] 4326 4400 4327 4401 [[package]] ··· 5127 5201 ] 5128 5202 5129 5203 [[package]] 5204 + name = "petgraph" 5205 + version = "0.8.3" 5206 + source = "registry+https://github.com/rust-lang/crates.io-index" 5207 + checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" 5208 + dependencies = [ 5209 + "fixedbitset 0.5.7", 5210 + "hashbrown 0.15.5", 5211 + "indexmap", 5212 + "serde", 5213 + "serde_derive", 5214 + ] 5215 + 5216 + [[package]] 5130 5217 name = "pgvector" 5131 5218 version = "0.4.1" 5132 5219 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 5493 5580 ] 5494 5581 5495 5582 [[package]] 5583 + name = "pulldown-cmark" 5584 + version = "0.13.3" 5585 + source = "registry+https://github.com/rust-lang/crates.io-index" 5586 + checksum = "7c3a14896dfa883796f1cb410461aef38810ea05f2b2c33c5aded3649095fdad" 5587 + dependencies = [ 5588 + "bitflags 2.11.0", 5589 + "getopts", 5590 + "memchr", 5591 + "pulldown-cmark-escape", 5592 + "unicase", 5593 + ] 5594 + 5595 + [[package]] 5596 + name = "pulldown-cmark-escape" 5597 + version = "0.11.0" 5598 + source = "registry+https://github.com/rust-lang/crates.io-index" 5599 + checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" 5600 + 5601 + [[package]] 5496 5602 name = "pxfm" 5497 5603 version = "0.1.28" 5498 5604 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 5736 5842 ] 5737 5843 5738 5844 [[package]] 5845 + name = "rayon" 5846 + version = "1.11.0" 5847 + source = "registry+https://github.com/rust-lang/crates.io-index" 5848 + checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" 5849 + dependencies = [ 5850 + "either", 5851 + "rayon-core", 5852 + ] 5853 + 5854 + [[package]] 5855 + name = "rayon-core" 5856 + version = "1.13.0" 5857 + source = "registry+https://github.com/rust-lang/crates.io-index" 5858 + checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" 5859 + dependencies = [ 5860 + "crossbeam-deque", 5861 + "crossbeam-utils", 5862 + ] 5863 + 5864 + [[package]] 5739 5865 name = "read-fonts" 5740 5866 version = "0.37.0" 5741 5867 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 5863 5989 "proc-macro2", 5864 5990 "quote", 5865 5991 "syn 1.0.109", 5992 + ] 5993 + 5994 + [[package]] 5995 + name = "ron" 5996 + version = "0.12.0" 5997 + source = "registry+https://github.com/rust-lang/crates.io-index" 5998 + checksum = "fd490c5b18261893f14449cbd28cb9c0b637aebf161cd77900bfdedaff21ec32" 5999 + dependencies = [ 6000 + "bitflags 2.11.0", 6001 + "once_cell", 6002 + "serde", 6003 + "serde_derive", 6004 + "typeid", 6005 + "unicode-ident", 5866 6006 ] 5867 6007 5868 6008 [[package]] ··· 6983 7123 "fancy-regex", 6984 7124 "filedescriptor", 6985 7125 "finl_unicode", 6986 - "fixedbitset", 7126 + "fixedbitset 0.4.2", 6987 7127 "hex", 6988 7128 "lazy_static", 6989 7129 "libc", ··· 7372 7512 dependencies = [ 7373 7513 "rustc-hash 2.1.1", 7374 7514 ] 7515 + 7516 + [[package]] 7517 + name = "typeid" 7518 + version = "1.0.3" 7519 + source = "registry+https://github.com/rust-lang/crates.io-index" 7520 + checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" 7375 7521 7376 7522 [[package]] 7377 7523 name = "typenum"
+8
Cargo.toml
··· 75 75 dto = {path="./crates/dto"} 76 76 eframe = "0.34.1" 77 77 async-trait = "0.1.89" 78 + egui_graphs = "0.30.0" 79 + rayon = "1.11.0" 80 + rand = "0.10.0" 81 + pulldown-cmark = { version = "0.13.3", features = ["simd"] } 78 82 79 83 [build-dependencies] 80 84 anyhow = "1.0.102" 81 85 vergen-gix = {version = "9.1.0", features = ["build", "cargo"]} 86 + 87 + # patch egui_graphs to have egui 0.34.1 88 + [patch.crates-io] 89 + egui_graphs = { git = "https://github.com/suri-codes/egui_graphs", rev = "3327417007520300782574e3d6bc377bbafa8b5a" } 82 90 83 91 [profile.release] 84 92 codegen-units = 1
+1
crates/dto/migration/Cargo.toml
··· 13 13 nanoid = "0.4.0" 14 14 sea-orm = "2.0.0-rc" 15 15 rgb = "0.8.53" 16 + serde = {workspace = true} 16 17 17 18 [dependencies.sea-orm-migration] 18 19 version = "2.0.0-rc"
+12 -1
crates/dto/migration/src/types/nano_id.rs
··· 1 + use std::str::FromStr as _; 2 + 1 3 use nanoid::nanoid; 2 4 use sea_orm::DeriveValueType; 5 + use serde::{Deserialize, Serialize}; 3 6 4 - #[derive(Clone, Debug, PartialEq, Eq, DeriveValueType)] 7 + #[derive( 8 + Debug, Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, DeriveValueType, 9 + )] 5 10 #[sea_orm(value_type = "String")] 6 11 pub struct NanoId(pub(crate) String); 7 12 ··· 44 49 Ok(Self(s.to_owned())) 45 50 } 46 51 } 52 + 53 + impl From<&str> for NanoId { 54 + fn from(value: &str) -> Self { 55 + NanoId::from_str(value).unwrap() 56 + } 57 + }
+1 -6
crates/dto/src/entity/group.rs
··· 2 2 3 3 use migration::prelude::Local; 4 4 use migration::types::*; 5 - use sea_orm::ActiveValue::Set; 6 5 use sea_orm::entity::prelude::*; 6 + use sea_orm::ActiveValue::Set; 7 7 use std::future::ready; 8 8 use std::pin::Pin; 9 9 ··· 56 56 'life0: 'async_trait, 57 57 { 58 58 let now = Local::now().naive_local(); 59 - 60 - // set modified at 61 59 self.modified_at = Set(now); 62 - 63 - // we set the timestamp to non-utc 64 60 if insert && self.created_at.is_not_set() { 65 61 self.created_at = Set(now); 66 62 } 67 - 68 63 if insert && self.nano_id.is_not_set() { 69 64 self.nano_id = Set(NanoId::default()); 70 65 }
+1 -6
crates/dto/src/entity/task.rs
··· 2 2 3 3 use migration::prelude::Local; 4 4 use migration::types::*; 5 - use sea_orm::ActiveValue::Set; 6 5 use sea_orm::entity::prelude::*; 6 + use sea_orm::ActiveValue::Set; 7 7 use std::future::ready; 8 8 use std::pin::Pin; 9 9 ··· 54 54 'life0: 'async_trait, 55 55 { 56 56 let now = Local::now().naive_local(); 57 - 58 - // set modified at 59 57 self.modified_at = Set(now); 60 - 61 - // we set the timestamp to non-utc 62 58 if insert && self.created_at.is_not_set() { 63 59 self.created_at = Set(now); 64 60 } 65 - 66 61 if insert && self.nano_id.is_not_set() { 67 62 self.nano_id = Set(NanoId::default()); 68 63 }
+1 -6
crates/dto/src/entity/zettel.rs
··· 1 1 //! `SeaORM` Entity, @generated by sea-orm-codegen 2.0 2 2 3 3 use migration::{prelude::Local, types::*}; 4 - use sea_orm::ActiveValue::Set; 5 4 use sea_orm::entity::prelude::*; 5 + use sea_orm::ActiveValue::Set; 6 6 use std::{future::ready, pin::Pin}; 7 7 8 8 #[sea_orm::model] ··· 38 38 'life0: 'async_trait, 39 39 { 40 40 let now = Local::now().naive_local(); 41 - 42 - // set modified at 43 41 self.modified_at = Set(now); 44 - 45 - // we set the timestamp to non-utc 46 42 if insert && self.created_at.is_not_set() { 47 43 self.created_at = Set(now); 48 44 } 49 - 50 45 if insert && self.nano_id.is_not_set() { 51 46 self.nano_id = Set(NanoId::default()); 52 47 }
+16
crates/dto/src/lib.rs
··· 8 8 /// exported traits for the database 9 9 pub use sea_orm::ActiveModelTrait; 10 10 pub use sea_orm::ActiveValue; 11 + pub use sea_orm::ColumnTrait; 12 + pub use sea_orm::EntityTrait; 13 + pub use sea_orm::IntoActiveModel; 14 + pub use sea_orm::QueryFilter; 11 15 12 16 /// Exporting this as a generic NanoId. 13 17 pub use migration::types::NanoId; ··· 27 31 /// Everything related to groups. 28 32 pub use entity::group::ActiveModel as GroupActiveModel; 29 33 pub use entity::group::ActiveModelEx as GroupActiveModelEx; 34 + pub use entity::group::Column as GroupColumns; 30 35 pub use entity::group::Entity as GroupEntity; 31 36 pub use entity::group::Model as GroupModel; 32 37 pub use entity::group::ModelEx as GroupModelEx; ··· 34 39 /// Everything related to tasks. 35 40 pub use entity::task::ActiveModel as TaskActiveModel; 36 41 pub use entity::task::ActiveModelEx as TaskActiveModelEx; 42 + pub use entity::task::Column as TaskColumns; 37 43 pub use entity::task::Entity as TaskEntity; 38 44 pub use entity::task::Model as TaskModel; 39 45 pub use entity::task::ModelEx as TaskModelEx; ··· 41 47 /// Everything related to zetetl's. 42 48 pub use entity::zettel::ActiveModel as ZettelActiveModel; 43 49 pub use entity::zettel::ActiveModelEx as ZettelActiveModelEx; 50 + pub use entity::zettel::Column as ZettelColumns; 44 51 pub use entity::zettel::Entity as ZettelEntity; 45 52 pub use entity::zettel::Model as ZettelModel; 46 53 pub use entity::zettel::ModelEx as ZettelModelEx; ··· 48 55 /// Everything related to tag's. 49 56 pub use entity::tag::ActiveModel as TagActiveModel; 50 57 pub use entity::tag::ActiveModelEx as TagActiveModelEx; 58 + pub use entity::tag::Column as TagColumns; 51 59 pub use entity::tag::Entity as TagEntity; 52 60 pub use entity::tag::Model as TagModel; 53 61 pub use entity::tag::ModelEx as TagModelEx; 62 + 63 + /// Everything related to the zettel_tag entries. 64 + pub use entity::zettel_tag::ActiveModel as ZettelTagActiveModel; 65 + pub use entity::zettel_tag::ActiveModelEx as ZettelTagActiveModelEx; 66 + pub use entity::zettel_tag::Column as ZettelTagColumns; 67 + pub use entity::zettel_tag::Entity as ZettelTagEntity; 68 + pub use entity::zettel_tag::Model as ZettelTagModel; 69 + pub use entity::zettel_tag::ModelEx as ZettelTagModelEx;
+1 -1
crates/dto/tests/task.rs
··· 70 70 71 71 println!("group: {group:#?}"); 72 72 println!("task: {task:#?}"); 73 - panic!() 73 + // panic!() 74 74 }
+2
flake.nix
··· 94 94 cargo-deny 95 95 cargo-edit 96 96 cargo-watch 97 + cargo-nextest 98 + 97 99 rust-analyzer 98 100 99 101 sea-orm-cli
+1 -1
justfile
··· 18 18 19 19 # Run all tests 20 20 test: 21 - cargo test {{_cargo_flags}} 21 + cargo nextest r {{_cargo_flags}} 22 22 23 23 24 24 # Only used to build / generate entities
+11
src/types/frontmatter.rs
··· 2 2 3 3 // use chrono::format::StrftimeItems; 4 4 use color_eyre::eyre::{Result, eyre}; 5 + use egui_graphs::Node; 5 6 use serde::{Deserialize, Serialize}; 6 7 use tokio::fs; 7 8 8 9 use dto::DateTime; 10 + 11 + use crate::types::{Link, Zettel}; 9 12 10 13 const DATE_FMT_STR: &str = "%Y-%m-%d %I:%M:%S %p"; 11 14 ··· 29 32 created_at, 30 33 tag_strings, 31 34 } 35 + } 36 + 37 + /// Apply the features of `FrontMatter` onto a 38 + /// `Node` 39 + pub fn apply_node_transform(&self, node: &mut Node<Zettel, Link>) { 40 + node.set_label(self.title.clone()); 41 + let disp = node.display_mut(); 42 + disp.radius = 100.0; 32 43 } 33 44 34 45 /// Reads in file and returns the front matter as well as the content after it.
+105
src/types/kasten.rs
··· 1 + use crate::types::{Link, Zettel, ZettelId}; 2 + use color_eyre::eyre::Result; 3 + use eframe::emath; 4 + use egui_graphs::{ 5 + Graph, 6 + petgraph::{Directed, graph::NodeIndex, prelude::StableGraph}, 7 + }; 8 + use rayon::iter::{ParallelBridge as _, ParallelIterator as _}; 9 + use std::{cmp::max, collections::HashMap}; 10 + 11 + use crate::types::Workspace; 12 + 13 + #[derive(Debug, Clone)] 14 + #[expect(dead_code)] 15 + pub struct Kasten { 16 + /// Private field so it can only be instantiated from a `Path` 17 + _private: (), 18 + 19 + /// The workspace this `Kasten` is in 20 + pub ws: Workspace, 21 + 22 + /// the graph of `Zettel`s and the `Links` between them 23 + pub graph: ZkGraph, 24 + 25 + /// simple conversions 26 + zid_to_gid: HashMap<ZettelId, NodeIndex>, 27 + 28 + pub most_recently_edited: Option<NodeIndex>, 29 + } 30 + 31 + pub type ZkGraph = Graph<Zettel, Link, Directed>; 32 + 33 + /// Minimum number of nodes in our graph. 34 + const GRAPH_MIN_NODES: usize = 128; 35 + /// Arbitrarily chosen minimum number of edges 36 + const GRAPH_MIN_EDGES: usize = GRAPH_MIN_NODES * 3; 37 + 38 + impl Kasten { 39 + /// Indexes the `Workspace` and constructs a `Kasten` 40 + #[expect(dead_code)] 41 + pub async fn index(ws: Workspace) -> Result<Self> { 42 + let paths = std::fs::read_dir(&ws.root)? 43 + .par_bridge() 44 + .flatten() 45 + .filter(|entry| { 46 + entry.file_type().map(|ft| ft.is_file()).unwrap_or(false) 47 + && entry 48 + .path() 49 + .extension() 50 + .and_then(|ext| ext.to_str()) 51 + .is_some_and(|ext| ext == "md") 52 + }) 53 + .map(|entry| entry.path()) 54 + .collect::<Vec<_>>(); 55 + 56 + let zettel_tasks = paths 57 + .into_iter() 58 + .map(|path| { 59 + let ws = ws.clone(); 60 + tokio::spawn(async move { Zettel::from_path(path, &ws).await }) 61 + }) 62 + .collect::<Vec<_>>(); 63 + 64 + // await all of them 65 + let zettels = futures::future::join_all(zettel_tasks) 66 + .await 67 + .into_iter() 68 + .filter_map(|result| result.ok()?.ok()) 69 + .collect::<Vec<Zettel>>(); 70 + 71 + // capacity! 72 + let mut graph: ZkGraph = ZkGraph::from(&StableGraph::with_capacity( 73 + max(zettels.len() * 2, GRAPH_MIN_EDGES), 74 + max(zettels.len() * 3, GRAPH_MIN_EDGES), 75 + )); 76 + 77 + let mut zid_to_gid = HashMap::new(); 78 + for zettel in &zettels { 79 + let fm = zettel.front_matter(&ws).await?; 80 + let id = graph.add_node_custom(zettel.clone(), |node| { 81 + fm.apply_node_transform(node); 82 + let x = rand::random_range(0.0..=100.0); 83 + let y = rand::random_range(0.0..=100.0); 84 + node.set_location(emath::Pos2 { x, y }); 85 + }); 86 + zid_to_gid.insert(zettel.id.clone(), id); 87 + } 88 + 89 + for zettel in &zettels { 90 + let src = zid_to_gid.get(&zettel.id).expect("must exist"); 91 + for link in &zettel.links(&ws).await? { 92 + let dst = zid_to_gid.get(&link.dest).expect("must exist"); 93 + graph.add_edge(*src, *dst, link.clone()); 94 + } 95 + } 96 + 97 + Ok(Self { 98 + _private: (), 99 + ws, 100 + graph, 101 + zid_to_gid, 102 + most_recently_edited: None, 103 + }) 104 + } 105 + }
+18
src/types/link.rs
··· 1 + use serde::{Deserialize, Serialize}; 2 + 3 + use crate::types::ZettelId; 4 + 5 + #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord)] 6 + pub struct Link { 7 + pub source: ZettelId, 8 + pub dest: ZettelId, 9 + } 10 + 11 + impl Link { 12 + pub fn new(source: impl Into<ZettelId>, dest: impl Into<ZettelId>) -> Self { 13 + Self { 14 + source: source.into(), 15 + dest: dest.into(), 16 + } 17 + } 18 + }
+8
src/types/mod.rs
··· 9 9 10 10 mod zettel; 11 11 pub use zettel::Zettel; 12 + pub use zettel::ZettelId; 12 13 13 14 mod group; 14 15 pub use group::Group; ··· 19 20 20 21 mod workspace; 21 22 pub use workspace::Workspace; 23 + 24 + mod link; 25 + pub use link::Link; 26 + 27 + mod kasten; 28 + #[expect(unused_imports)] 29 + pub use kasten::Kasten; 22 30 23 31 mod frontmatter; 24 32 pub use frontmatter::FrontMatter;
+1 -1
src/types/workspace.rs
··· 85 85 let path = PathBuf::from("/tmp/filaments/.filaments/filaments.db"); 86 86 create_dir_all(path.parent().unwrap()).unwrap(); 87 87 let _ = File::create(&path).unwrap(); 88 - let _ws = Workspace::instansiate(dbg!(&path.parent().unwrap().parent().unwrap())) 88 + let _ws = Workspace::instansiate(&path.parent().unwrap().parent().unwrap()) 89 89 .await 90 90 .unwrap(); 91 91 }
+240 -8
src/types/zettel.rs
··· 1 - use dto::{DateTime, TagEntity, ZettelActiveModel, ZettelEntity, ZettelModelEx}; 2 - use std::path::PathBuf; 1 + use dto::{ 2 + ActiveModelTrait, ActiveValue, ColumnTrait, DateTime, EntityTrait as _, IntoActiveModel, 3 + QueryFilter, TagActiveModel, TagEntity, ZettelActiveModel, ZettelEntity, ZettelModelEx, 4 + ZettelTagActiveModel, ZettelTagColumns, ZettelTagEntity, 5 + }; 6 + use pulldown_cmark::{Event, Parser, Tag as MkTag}; 7 + use serde::{Deserialize, Serialize}; 8 + use std::{ 9 + fmt::Display, 10 + path::{Path, PathBuf}, 11 + }; 12 + use tracing::{error, info}; 3 13 4 - use color_eyre::eyre::Result; 14 + use color_eyre::eyre::{Error, Result, eyre}; 5 15 use dto::NanoId; 6 16 use tokio::{fs::File, io::AsyncWriteExt}; 7 17 8 - use crate::types::{FrontMatter, Tag, Workspace}; 18 + use crate::types::{FrontMatter, Link, Tag, Workspace}; 9 19 10 20 /// A `Zettel` is a note about a single idea. 11 21 /// It can have many `Tag`s, just meaning it can fall under many ··· 15 25 pub struct Zettel { 16 26 /// Should only be constructed from models. 17 27 _private: (), 18 - pub id: NanoId, 28 + pub id: ZettelId, 19 29 pub title: String, 20 30 /// a workspace-local file path, needs to be canonicalized before usage 21 31 pub file_path: PathBuf, ··· 23 33 pub tags: Vec<Tag>, 24 34 } 25 35 36 + /// A `ZettelId` is essentially a `NanoId`, 37 + /// with some `Zettel` specific helpers written 38 + /// onto it 39 + #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 40 + pub struct ZettelId(NanoId); 41 + 26 42 impl Zettel { 27 43 pub async fn new(title: impl Into<String>, ws: &Workspace) -> Result<Self> { 28 44 // fn new(title: impl Into<String>) -> Result<Self> { ··· 65 81 66 82 /// Returns the most up-to-date `FrontMatter` for this 67 83 /// `Zettel` 68 - #[expect(dead_code)] 69 84 pub async fn front_matter(&self, ws: &Workspace) -> Result<FrontMatter> { 70 85 let path = self.absolute_path(ws); 71 86 let (fm, _) = FrontMatter::extract_from_file(path).await?; ··· 74 89 75 90 /// Returns the content of this `Zettel`, which is everything 76 91 /// but the `FrontMatter` 77 - #[expect(dead_code)] 78 92 pub async fn content(&self, ws: &Workspace) -> Result<String> { 79 93 let path = self.absolute_path(ws); 80 94 let (_, content) = FrontMatter::extract_from_file(path).await?; ··· 90 104 fn absolute_path(&self, ws: &Workspace) -> PathBuf { 91 105 ws.root.clone().join(&self.file_path) 92 106 } 107 + 108 + /// uses the id and root to parse out of the root directory 109 + #[expect(dead_code)] 110 + pub async fn from_id(id: &ZettelId, ws: &Workspace) -> Result<Self> { 111 + let mut path = ws.root.clone(); 112 + path.push(id.0.to_string()); 113 + Self::from_path(path, ws).await 114 + } 115 + 116 + pub async fn from_path(path: impl Into<PathBuf>, ws: &Workspace) -> Result<Self> { 117 + let path: PathBuf = path.into(); 118 + 119 + let id = ZettelId::try_from(path.as_path())?; 120 + 121 + let (front_matter, _) = FrontMatter::extract_from_file(&ws.root.clone().join(path)).await?; 122 + 123 + let mut zettel_tag_strings = front_matter.tag_strings.clone(); 124 + 125 + zettel_tag_strings.sort(); 126 + 127 + // get the zettel from the db 128 + let db_zettel: ZettelModelEx = if let Some(existing_zettel) = ZettelEntity::load() 129 + .with(TagEntity) 130 + .filter_by_nano_id(id.clone()) 131 + .one(&ws.db) 132 + .await? 133 + { 134 + existing_zettel 135 + } else { 136 + // if zettel is missing from db, we just add it here 137 + info!("adding zettel to db"); 138 + let am = ZettelActiveModel { 139 + nano_id: ActiveValue::Set(id.clone().into()), 140 + title: ActiveValue::Set(front_matter.title.clone()), 141 + ..Default::default() 142 + }; 143 + 144 + am.insert(&ws.db).await?; 145 + 146 + ZettelEntity::load() 147 + .with(TagEntity) 148 + .filter_by_nano_id(id.clone()) 149 + .one(&ws.db) 150 + .await? 151 + .expect("we just inserted the zettel") 152 + }; 153 + 154 + // get the tags for it 155 + for db_tag in db_zettel.tags { 156 + if let Ok(idx) = zettel_tag_strings.binary_search(&db_tag.name) { 157 + // we remove tags we have already processed 158 + zettel_tag_strings.remove(idx); 159 + } else { 160 + // the db says the file has tag `x`, but that tag is missing from the 161 + // front matter, we can assume its gone, lets delete that link 162 + let to_remove = ZettelTagEntity::find() 163 + .filter(ZettelTagColumns::ZettelNanoId.eq(id.0.clone())) 164 + .filter(ZettelTagColumns::TagNanoId.eq(db_tag.nano_id)) 165 + .one(&ws.db) 166 + .await? 167 + .expect("this link must exist"); 168 + 169 + to_remove.into_active_model().delete(&ws.db).await?; 170 + } 171 + } 172 + 173 + // now any tags that are left inside zettel_tag_strings, 174 + // we have to put them inside the db 175 + for new_tag in zettel_tag_strings { 176 + // create a new tag 177 + let tag = TagActiveModel { 178 + name: ActiveValue::Set(new_tag), 179 + ..Default::default() 180 + } 181 + .insert(&ws.db) 182 + .await?; 183 + 184 + // this zettel has this tag now 185 + let _ = ZettelTagActiveModel { 186 + zettel_nano_id: ActiveValue::Set(id.to_string()), 187 + tag_nano_id: ActiveValue::Set(tag.nano_id.to_string()), 188 + } 189 + .insert(&ws.db) 190 + .await?; 191 + } 192 + 193 + if front_matter.title != db_zettel.title { 194 + let am = ZettelActiveModel { 195 + id: ActiveValue::Unchanged(db_zettel.id), 196 + title: ActiveValue::Set(front_matter.title.clone()), 197 + ..Default::default() 198 + }; 199 + 200 + am.update(&ws.db).await?; 201 + } 202 + 203 + Ok(ZettelEntity::load() 204 + .with(TagEntity) 205 + .filter_by_nano_id(id.clone()) 206 + .one(&ws.db) 207 + .await? 208 + .expect("We just inserted it right above") 209 + .into()) 210 + } 211 + 212 + /// The `Link`s that are going out of this `Zettel` 213 + pub async fn links(&self, ws: &Workspace) -> Result<Vec<Link>> { 214 + let content = self.content(ws).await?; 215 + let parsed = Parser::new(&content); 216 + 217 + let mut links = vec![]; 218 + 219 + for event in parsed { 220 + if let Event::Start(MkTag::Link { dest_url, .. }) = event { 221 + info!("Found dest_url: {dest_url:#?}"); 222 + 223 + let dest_path = { 224 + // remove leading "./" 225 + let without_prefix = dest_url.strip_prefix("./").unwrap_or(&dest_url); 226 + 227 + // remove "#" and everything after it 228 + let without_anchor = without_prefix.split('#').next().unwrap(); 229 + 230 + // add .md if not present 231 + let normalized = if std::path::Path::new(without_anchor) 232 + .extension() 233 + .is_some_and(|ext| ext.eq_ignore_ascii_case("md")) 234 + { 235 + without_anchor.to_string() 236 + } else { 237 + format!("{without_anchor}.md") 238 + }; 239 + 240 + let mut tmp_root = ws.root.clone(); 241 + tmp_root.push(normalized); 242 + tmp_root 243 + }; 244 + // simplest way to validate that the path exists 245 + let Ok(canon_url) = dest_path.canonicalize() else { 246 + error!("Link not found!: {dest_path:?}"); 247 + continue; 248 + }; 249 + 250 + let dst_id = ZettelId::try_from(canon_url)?; 251 + 252 + let link = Link::new(self.id.clone(), dst_id); 253 + 254 + links.push(link); 255 + } 256 + } 257 + 258 + Ok(links) 259 + } 93 260 } 94 261 95 262 impl From<ZettelModelEx> for Zettel { ··· 102 269 103 270 Self { 104 271 _private: (), 105 - id: value.nano_id, 272 + id: value.nano_id.into(), 106 273 title: value.title, 107 274 file_path: value.file_path.into(), 108 275 created_at: value.created_at, ··· 110 277 } 111 278 } 112 279 } 280 + 281 + impl From<&str> for ZettelId { 282 + fn from(value: &str) -> Self { 283 + Self(NanoId::from(value)) 284 + } 285 + } 286 + 287 + impl From<&NanoId> for ZettelId { 288 + fn from(value: &NanoId) -> Self { 289 + value.clone().into() 290 + } 291 + } 292 + 293 + impl From<NanoId> for ZettelId { 294 + fn from(value: NanoId) -> Self { 295 + Self(value) 296 + } 297 + } 298 + 299 + impl TryFrom<PathBuf> for ZettelId { 300 + type Error = Error; 301 + 302 + fn try_from(value: PathBuf) -> Result<Self, Self::Error> { 303 + let path = value.as_path(); 304 + path.try_into() 305 + } 306 + } 307 + 308 + impl TryFrom<&Path> for ZettelId { 309 + type Error = Error; 310 + 311 + fn try_from(value: &Path) -> Result<Self, Self::Error> { 312 + let extension = value 313 + .extension() 314 + .and_then(|ext| ext.to_str()) 315 + .ok_or_else(|| eyre!("Unable to turn file extension into string".to_owned(),))?; 316 + 317 + if extension != "md" { 318 + return Err(eyre!(format!("Wrong extension: {extension}, expected .md"))); 319 + } 320 + 321 + let id: Self = value 322 + .file_name() 323 + .ok_or_else(|| eyre!("Invalid File Name!".to_owned()))? 324 + .to_str() 325 + .ok_or_else(|| eyre!("File Name cannot be translated into str!".to_owned(),))? 326 + .strip_suffix(".md") 327 + .expect("we statically verify this right above") 328 + .into(); 329 + 330 + Ok(id) 331 + } 332 + } 333 + 334 + impl Display for ZettelId { 335 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 336 + f.write_str(&self.0.to_string()) 337 + } 338 + } 339 + 340 + impl From<ZettelId> for NanoId { 341 + fn from(value: ZettelId) -> Self { 342 + value.0 343 + } 344 + }