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 #4 from suri-codes/types

Types

authored by

Surendra Jammishetti and committed by
GitHub
e3deba4b 666da3f1

+395 -148
+4
crates/dto/migration/src/m20260319_002245_task_table.rs
··· 31 31 .default(Priority::default().to_string()), 32 32 ) 33 33 .col(timestamp(Task::Due).null()) 34 + .col(timestamp(Task::FinishedAt).null()) 34 35 .col(timestamp(Task::CreatedAt).default(Expr::current_timestamp())) 35 36 .col(timestamp(Task::ModifiedAt).default(Expr::current_timestamp())) 36 37 .col(string(Task::ZettelId).not_null().unique_key()) ··· 131 132 132 133 /// The duedate for this task 133 134 Due, 135 + 136 + /// Completion time 137 + FinishedAt, 134 138 135 139 /// Creation time 136 140 CreatedAt,
-85
crates/dto/src/db.rs
··· 1 - use std::{ops::Deref, path::PathBuf}; 2 - 3 - use migration::{Migrator, MigratorTrait as _}; 4 - use sea_orm::{Database, DatabaseConnection}; 5 - use tracing::debug; 6 - 7 - use thiserror::Error; 8 - 9 - /// Database struct 10 - #[derive(Debug)] 11 - pub struct Db { 12 - conn: DatabaseConnection, 13 - } 14 - 15 - pub type DbResult<T> = Result<T, DbError>; 16 - 17 - #[derive(Debug, Error)] 18 - pub enum DbError { 19 - #[error("database file not found, tried looking at {not_found_at}")] 20 - NotFound { not_found_at: String }, 21 - #[error("Seaorm Error")] 22 - SeaOrm(#[from] sea_orm::error::DbErr), 23 - } 24 - 25 - impl AsRef<DatabaseConnection> for Db { 26 - fn as_ref(&self) -> &DatabaseConnection { 27 - &self.conn 28 - } 29 - } 30 - 31 - impl Deref for Db { 32 - type Target = DatabaseConnection; 33 - fn deref(&self) -> &Self::Target { 34 - &self.conn 35 - } 36 - } 37 - 38 - impl Db { 39 - /// Connects to the database to the database at `path`. 40 - /// 41 - /// # Errors 42 - /// Will error if `path` is not a valid file. 43 - pub async fn connect(path: impl Into<PathBuf>) -> DbResult<Self> { 44 - let path = path.into(); 45 - 46 - let connection_string = format!( 47 - "sqlite://{}", 48 - path.canonicalize() 49 - .map_err(|_| DbError::NotFound { 50 - not_found_at: path.to_string_lossy().to_string() 51 - })? 52 - .to_string_lossy() 53 - ); 54 - 55 - debug!("connecting to {connection_string}"); 56 - 57 - let conn = Database::connect(connection_string).await?; 58 - 59 - // run all migrations on connection 60 - Migrator::up(&conn, None).await?; 61 - 62 - Ok(Self { conn }) 63 - } 64 - } 65 - 66 - #[cfg(test)] 67 - mod tests { 68 - use std::{ 69 - fs::{File, create_dir_all}, 70 - path::PathBuf, 71 - }; 72 - 73 - use crate::Db; 74 - 75 - #[tokio::test] 76 - async fn test_connect() { 77 - let path = PathBuf::new(); 78 - let _ = Db::connect(&path).await.expect_err("not found"); 79 - 80 - let path = PathBuf::from("/tmp/filaments/test_db.sqlite"); 81 - create_dir_all(path.parent().unwrap()).unwrap(); 82 - let _ = File::create(&path).unwrap(); 83 - let _db = Db::connect(&path).await.unwrap(); 84 - } 85 - }
+1 -1
crates/dto/src/entity/tag.rs
··· 1 1 //! `SeaORM` Entity, @generated by sea-orm-codegen 2.0 2 2 3 3 use migration::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]
+1
crates/dto/src/entity/task.rs
··· 17 17 pub name: String, 18 18 pub priority: Priority, 19 19 pub due: Option<DateTimeUtc>, 20 + pub finished_at: Option<DateTimeUtc>, 20 21 pub created_at: DateTimeUtc, 21 22 pub modified_at: DateTimeUtc, 22 23 #[sea_orm(unique)]
+1 -1
crates/dto/src/entity/zettel.rs
··· 1 1 //! `SeaORM` Entity, @generated by sea-orm-codegen 2.0 2 2 3 3 use migration::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]
+5 -3
crates/dto/src/lib.rs
··· 1 1 //! The DTO's (Data Transfer Objects) used to interact with 2 2 //! the Database. There is also a simple database struct in here. 3 3 4 - /// Database and its Errors 5 - mod db; 6 - pub use db::*; 4 + /// For database stuff 5 + pub use migration::{Migrator, MigratorTrait}; 6 + pub use sea_orm::{Database, DatabaseConnection}; 7 7 8 8 /// exported traits for the database 9 9 pub use sea_orm::ActiveModelTrait; ··· 14 14 /// Exporting this as DTO so we can newtype this in a later crate 15 15 /// and add additional functionality to it. 16 16 pub use migration::types::Priority as PriorityDTO; 17 + 18 + pub use sea_orm::entity::prelude::DateTimeUtc; 17 19 18 20 /// Color type, exporting as DTO because I might 19 21 /// want to newtype wrap this, might not have to, depending
+10 -4
crates/dto/tests/common/mod.rs
··· 3 3 path::PathBuf, 4 4 }; 5 5 6 - use dto::Db; 7 6 use rand::RngExt; 7 + use sea_orm::{Database, DatabaseConnection}; 8 8 9 - pub async fn fresh_test_db() -> Db { 9 + pub async fn fresh_test_db() -> DatabaseConnection { 10 10 let rand_id = { 11 11 let mut rng = rand::rng(); 12 12 let mut rand_id = [0_u8; 4]; ··· 15 15 String::from_utf8(rand_id.to_vec()).unwrap() 16 16 }; 17 17 18 - let path = PathBuf::from(format!("/tmp/filaments/test_db_{rand_id}")); 18 + let path = PathBuf::from(format!("/tmp/filaments/test_db_{rand_id}.db")); 19 19 20 20 create_dir_all(path.parent().unwrap()).unwrap(); 21 21 22 22 let _ = File::create(&path).unwrap(); 23 - Db::connect(&path).await.unwrap() 23 + 24 + let db_conn_string = format!( 25 + "sqlite://{}", 26 + path.clone().canonicalize().unwrap().to_string_lossy() 27 + ); 28 + 29 + Database::connect(db_conn_string).await.unwrap() 24 30 }
+6 -6
crates/dto/tests/task.rs
··· 16 16 file_path: Set("/voo/doo".to_owned()), 17 17 ..Default::default() 18 18 } 19 - .insert(db.as_ref()) 19 + .insert(&db) 20 20 .await 21 21 .unwrap(); 22 22 ··· 26 26 zettel_id: Set(group_zettel.nano_id.clone()), 27 27 ..Default::default() 28 28 } 29 - .insert(db.as_ref()) 29 + .insert(&db) 30 30 .await 31 31 .unwrap(); 32 32 ··· 36 36 file_path: Set("/voo/doo".to_owned()), 37 37 ..Default::default() 38 38 } 39 - .insert(db.as_ref()) 39 + .insert(&db) 40 40 .await 41 41 .unwrap(); 42 42 ··· 46 46 zettel_id: Set(task_zettel.nano_id.clone()), 47 47 ..Default::default() 48 48 } 49 - .insert(db.as_ref()) 49 + .insert(&db) 50 50 .await 51 51 .unwrap(); 52 52 ··· 54 54 .filter_by_nano_id(task.nano_id.clone()) 55 55 .with(GroupEntity) 56 56 .with(ZettelEntity) 57 - .one(db.as_ref()) 57 + .one(&db) 58 58 .await 59 59 .unwrap() 60 60 .unwrap(); ··· 63 63 .filter_by_nano_id(group.nano_id.clone()) 64 64 .with(TaskEntity) 65 65 .with(ZettelEntity) 66 - .one(db.as_ref()) 66 + .one(&db) 67 67 .await 68 68 .unwrap() 69 69 .unwrap();
+5 -5
crates/dto/tests/zettel.rs
··· 15 15 color: Set(ColorDTO::new(255, 0, 0)), 16 16 ..Default::default() 17 17 } 18 - .insert(&*db) 18 + .insert(&db) 19 19 .await 20 20 .unwrap(); 21 21 ··· 25 25 file_path: Set("/voo/doo".to_owned()), 26 26 ..Default::default() 27 27 } 28 - .insert(db.as_ref()) 28 + .insert(&db) 29 29 .await 30 30 .unwrap(); 31 31 ··· 38 38 // .set_color("some color"), 39 39 // ) 40 40 .add_tag(tag.clone().into_active_model()) 41 - .insert(&*db) 41 + .insert(&db) 42 42 .await 43 43 .unwrap(); 44 44 ··· 49 49 file_path: Set("/voo/doo".to_owned()), 50 50 ..Default::default() 51 51 } 52 - .insert(db.as_ref()) 52 + .insert(&db) 53 53 .await 54 54 .unwrap(); 55 55 56 56 let zettels_for_tag = TagEntity::load() 57 57 .filter_by_nano_id(tag.nano_id.clone()) 58 58 .with(ZettelEntity) 59 - .all(db.as_ref()) 59 + .all(&db) 60 60 .await 61 61 .unwrap(); 62 62
+6 -6
flake.lock
··· 8 8 "rust-analyzer-src": "rust-analyzer-src" 9 9 }, 10 10 "locked": { 11 - "lastModified": 1774555695, 12 - "narHash": "sha256-JpTx7Rn8sILuXAH9a0K0UCZST5KY9ZTMzrZ61KcsNno=", 11 + "lastModified": 1774682177, 12 + "narHash": "sha256-OVbuJnJLlbHE28eRMudjtA6NXz/ifuXSho79gvh6GHY=", 13 13 "owner": "nix-community", 14 14 "repo": "fenix", 15 - "rev": "d76ca95395662ed18b02b894e487eb41fd0e7d99", 15 + "rev": "e0f515387df77b9fdbaaf81e7f866f0365474c18", 16 16 "type": "github" 17 17 }, 18 18 "original": { ··· 44 44 "rust-analyzer-src": { 45 45 "flake": false, 46 46 "locked": { 47 - "lastModified": 1774454876, 48 - "narHash": "sha256-bwkM8HseUs/22x+hy6FWvJMP6q/2CKBrm4sYxz9rMY8=", 47 + "lastModified": 1774569884, 48 + "narHash": "sha256-E8iWEPzg7OnE0XXXjo75CX7xFauqzJuGZ5wSO9KS8Ek=", 49 49 "owner": "rust-lang", 50 50 "repo": "rust-analyzer", 51 - "rev": "9253d39eab8b9c9da3c1412fc94764e01d55a02b", 51 + "rev": "443ddcddd0c73b07b799d052f5ef3b448c2f3508", 52 52 "type": "github" 53 53 }, 54 54 "original": {
+3 -9
src/cli/process.rs
··· 9 9 use crate::{ 10 10 cli::Commands, 11 11 config::{Config, get_config_dir}, 12 + types::Workspace, 12 13 }; 13 14 14 15 impl Commands { 15 - pub fn process(self) -> Result<()> { 16 + pub async fn process(self) -> Result<()> { 16 17 match self { 17 18 Self::Init { name } => { 18 19 // create the directory ··· 20 21 .context("Failed to get current directory")? 21 22 .join(&name); 22 23 23 - // create the .filaments folder 24 - let filaments_dir = dir.join(".filaments"); 25 - 26 - create_dir_all(&filaments_dir) 27 - .context("Failed to create the filaments directory!")?; 28 - 29 - // create the database inside there 30 - File::create(filaments_dir.join("filaments.db"))?; 24 + Workspace::initialize(dir.clone()).await?; 31 25 32 26 // write config that sets the filaments directory to current dir! 33 27 let config_kdl = dbg! {Config::generate(&dir)};
+23 -17
src/gui/mod.rs
··· 1 - use std::{ 2 - process, 3 - sync::{ 4 - Arc, 5 - atomic::{AtomicBool, Ordering}, 6 - }, 7 - }; 8 - 9 1 use eframe::egui; 10 2 11 3 /// The `Filaments Visualizer`, which is an instance of `eframe`, which uses `egui` 12 4 #[derive(Default)] 13 5 pub struct FilViz { 14 - shutdown_signal: Arc<AtomicBool>, 15 6 /// example for now 16 7 text: String, 17 8 } 18 9 19 10 impl FilViz { 20 11 /// Create a new instance of the `FiLViz` 21 - const fn new(_cc: &eframe::CreationContext<'_>, shutdown_signal: Arc<AtomicBool>) -> Self { 12 + const fn new(_cc: &eframe::CreationContext<'_>) -> Self { 22 13 // Customize egui here with cc.egui_ctx.set_fonts and cc.egui_ctx.set_global_style. 23 14 // Restore app state using cc.storage (requires the "persistence" feature). 24 15 // Use the cc.gl (a glow::Context) to create graphics shaders and buffers that you can use 25 16 // for e.g. egui::PaintCallback. 26 17 Self { 27 - shutdown_signal, 28 18 text: String::new(), 29 19 } 30 20 } 31 21 32 22 /// Create and run the `FilViz`. 33 - pub fn run(shutdown_signal: Arc<AtomicBool>) -> color_eyre::Result<()> { 23 + pub fn run() -> color_eyre::Result<()> { 34 24 let native_options = eframe::NativeOptions::default(); 35 25 eframe::run_native( 36 26 "Filaments Visualizer", 37 27 native_options, 38 - Box::new(|cc| Ok(Box::new(Self::new(cc, shutdown_signal)))), 28 + Box::new(|cc| Ok(Box::new(Self::new(cc)))), 39 29 )?; 40 30 41 31 Ok(()) ··· 44 34 45 35 impl eframe::App for FilViz { 46 36 fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) { 47 - if self.shutdown_signal.load(Ordering::Relaxed) { 48 - process::exit(0) 49 - } 50 - 51 37 egui::CentralPanel::default().show_inside(ui, |ui| { 52 38 ui.heading("Hello World!"); 53 39 ui.text_edit_singleline(&mut self.text); 40 + 41 + // credits! 42 + ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { 43 + powered_by_egui_and_eframe(ui); 44 + egui::warn_if_debug_build(ui); 45 + }); 54 46 }); 55 47 } 56 48 } 49 + 50 + fn powered_by_egui_and_eframe(ui: &mut egui::Ui) { 51 + ui.horizontal(|ui| { 52 + ui.spacing_mut().item_spacing.x = 0.0; 53 + ui.label("Powered by "); 54 + ui.hyperlink_to("egui", "https://github.com/emilk/egui"); 55 + ui.label(" and "); 56 + ui.hyperlink_to( 57 + "eframe", 58 + "https://github.com/emilk/egui/tree/master/crates/eframe", 59 + ); 60 + ui.label("."); 61 + }); 62 + }
+9 -11
src/main.rs
··· 2 2 //! My (suri.codes) personal-knowledge-system, with deeply integrated task tracking and long term goal planning capabilities. 3 3 //! 4 4 5 - use std::sync::{ 6 - Arc, 7 - atomic::{AtomicBool, Ordering}, 8 - }; 5 + use std::{process, sync::Arc}; 9 6 10 7 use crate::{cli::Cli, gui::FilViz, tui::TuiApp}; 11 8 use clap::Parser; ··· 13 10 mod cli; 14 11 mod gui; 15 12 mod tui; 13 + mod types; 16 14 17 15 mod config; 18 16 mod errors; ··· 29 27 30 28 // if there are any commands, run those and exit 31 29 if let Some(command) = args.command { 32 - return rt.block_on(async { command.process() }); 30 + return rt.block_on(async { command.process().await }); 33 31 } 34 - 35 - let shutdown_signal = Arc::new(AtomicBool::new(false)); 36 32 37 33 // then we spawn the tui on its own thread 38 34 let tui_handle = std::thread::spawn({ 35 + // arc stuff 39 36 let tui_rt = rt.clone(); 40 - let shutdown = shutdown_signal.clone(); 37 + 38 + // closure to run the tui 41 39 move || -> color_eyre::Result<()> { 42 40 // block the tui on the same runtime as above 43 41 tui_rt.block_on(async { 44 42 let mut tui = TuiApp::new(args.tick_rate, args.frame_rate)?; 45 43 tui.run().await?; 46 - shutdown.store(true, Ordering::Relaxed); 47 - Ok(()) 44 + // just close everything as soon as the tui is done running 45 + process::exit(0); 48 46 }) 49 47 } 50 48 }); ··· 53 51 if args.visualizer { 54 52 // enter the guard so egui_async works properly 55 53 let _rt_guard = rt.enter(); 56 - FilViz::run(shutdown_signal)?; 54 + FilViz::run()?; 57 55 } 58 56 59 57 // join on the tui
+13
src/types/color.rs
··· 1 + use dto::ColorDTO; 2 + 3 + /// Agnostic Color type, 4 + /// internally represented as rgb 5 + #[expect(dead_code)] 6 + #[derive(Debug, Clone)] 7 + pub struct Color(ColorDTO); 8 + 9 + impl From<ColorDTO> for Color { 10 + fn from(value: ColorDTO) -> Self { 11 + Self(value) 12 + } 13 + }
+43
src/types/group.rs
··· 1 + use dto::{DateTimeUtc, GroupModelEx, NanoId}; 2 + 3 + use crate::types::{Color, Priority, Zettel}; 4 + 5 + /// A `Group` which contains tasks! 6 + #[expect(dead_code)] 7 + #[derive(Debug, Clone)] 8 + pub struct Group { 9 + /// Should only be constructed from models. 10 + _private: (), 11 + 12 + pub id: NanoId, 13 + pub name: String, 14 + pub color: Color, 15 + pub priority: Priority, 16 + pub created_at: DateTimeUtc, 17 + pub modified_at: DateTimeUtc, 18 + /// The `Zettel` that is related to this `Group`. 19 + /// Can store notes regarding this group in 20 + /// the `Zettel` 21 + pub zettel: Zettel, 22 + } 23 + 24 + impl From<GroupModelEx> for Group { 25 + fn from(value: GroupModelEx) -> Self { 26 + Self { 27 + _private: (), 28 + id: value.nano_id, 29 + name: value.name, 30 + color: value.color.into(), 31 + priority: value.priority.into(), 32 + created_at: value.created_at, 33 + modified_at: value.modified_at, 34 + zettel: value 35 + .zettel 36 + .into_option() 37 + .expect( 38 + "When fetching a Group from the database, we expect to always have the Zettel loaded!!", 39 + ) 40 + .into(), 41 + } 42 + } 43 + }
+21
src/types/mod.rs
··· 1 + mod color; 2 + pub use color::Color; 3 + 4 + mod tag; 5 + pub use tag::Tag; 6 + 7 + mod priority; 8 + pub use priority::Priority; 9 + 10 + mod zettel; 11 + pub use zettel::Zettel; 12 + 13 + mod group; 14 + pub use group::Group; 15 + 16 + mod task; 17 + #[expect(unused_imports)] 18 + pub use task::Task; 19 + 20 + mod workspace; 21 + pub use workspace::Workspace;
+13
src/types/priority.rs
··· 1 + use dto::PriorityDTO; 2 + 3 + /// An Enum for the various `Priority` levels 4 + /// for `Task`s and `Group`s 5 + #[expect(dead_code)] 6 + #[derive(Debug, Clone)] 7 + pub struct Priority(PriorityDTO); 8 + 9 + impl From<PriorityDTO> for Priority { 10 + fn from(value: PriorityDTO) -> Self { 11 + Self(value) 12 + } 13 + }
+41
src/types/tag.rs
··· 1 + use dto::{NanoId, TagModel, TagModelEx}; 2 + 3 + use crate::types::Color; 4 + 5 + /// Represents a `Tag` in a `ZettelKasten` note taking method. 6 + /// Easy way to link multiple notes under one simple word. 7 + #[expect(dead_code)] 8 + #[derive(Debug, Clone)] 9 + pub struct Tag { 10 + /// Should only be constructed from models. 11 + _private: (), 12 + 13 + /// A unique `NanoId` 14 + pub id: NanoId, 15 + /// Name of the tag 16 + pub name: String, 17 + /// Color of the tag 18 + pub color: Color, 19 + } 20 + 21 + impl From<TagModel> for Tag { 22 + fn from(value: TagModel) -> Self { 23 + Self { 24 + _private: (), 25 + id: value.nano_id, 26 + name: value.name, 27 + color: value.color.into(), 28 + } 29 + } 30 + } 31 + 32 + impl From<TagModelEx> for Tag { 33 + fn from(value: TagModelEx) -> Self { 34 + Self { 35 + _private: (), 36 + id: value.nano_id, 37 + name: value.name, 38 + color: value.color.into(), 39 + } 40 + } 41 + }
+52
src/types/task.rs
··· 1 + use dto::{DateTimeUtc, NanoId, TaskModelEx}; 2 + 3 + use crate::types::{Group, Priority, Zettel}; 4 + 5 + /// a `Task` that you have to complete! 6 + #[expect(dead_code)] 7 + #[derive(Debug, Clone)] 8 + pub struct Task { 9 + /// Should only be constructed from models. 10 + _private:(), 11 + 12 + pub id: NanoId, 13 + pub name: String, 14 + pub priority: Priority, 15 + pub due: Option<DateTimeUtc>, 16 + pub finished_at: Option<DateTimeUtc>, 17 + pub created_at: DateTimeUtc, 18 + pub modified_at: DateTimeUtc, 19 + /// Each task has its own related `Zettel`. 20 + pub zettel: Zettel, 21 + pub group: Group, 22 + } 23 + 24 + impl From<TaskModelEx> for Task { 25 + fn from(value: TaskModelEx) -> Self { 26 + Self { 27 + _private: (), 28 + id: value.nano_id, 29 + name: value.name, 30 + priority: value.priority.into(), 31 + due: value.due, 32 + finished_at: value.finished_at, 33 + created_at: value.created_at, 34 + modified_at: value.modified_at, 35 + zettel: value 36 + .zettel 37 + .into_option() 38 + .expect( 39 + "When fetching a Task from the database, we expect to always have the Zettel loaded!!", 40 + ) 41 + .into(), 42 + group: value 43 + .group 44 + .into_option() 45 + .expect( 46 + "When fetching a Task from the database, we expect to always have the Group loaded!!", 47 + ) 48 + .into(), 49 + 50 + } 51 + } 52 + }
+99
src/types/workspace.rs
··· 1 + use std::path::PathBuf; 2 + 3 + use color_eyre::eyre::{Context, Result}; 4 + use dto::{Database, DatabaseConnection}; 5 + use tokio::fs::{File, create_dir_all}; 6 + use tracing::debug; 7 + 8 + /// The `Workspace` in which the filaments exist. 9 + #[expect(dead_code)] 10 + #[derive(Debug, Clone)] 11 + pub struct Workspace { 12 + /// Private field so it can only be instantiated from a `Path` 13 + _private: (), 14 + /// Connection to the sqlite database inside the `Workspace` 15 + pub db: DatabaseConnection, 16 + /// The path to the root of this workspace 17 + pub root: PathBuf, 18 + } 19 + 20 + impl Workspace { 21 + /// Given a path, try to construct a `Workspace` based on its contents. 22 + /// 23 + /// Note: this means that there should already exist a valid `Workspace` 24 + /// at that path. 25 + pub async fn instansiate(path: impl Into<PathBuf>) -> Result<Self> { 26 + let path = path.into(); 27 + 28 + let db_conn_string = format!( 29 + "sqlite://{}", 30 + path.clone() 31 + .join(".filaments/filaments.db") 32 + .canonicalize() 33 + .context("Invalid Filaments workspace!!")? 34 + .to_string_lossy() 35 + ); 36 + 37 + debug!("connecting to {db_conn_string}"); 38 + 39 + let conn = Database::connect(db_conn_string) 40 + .await 41 + .context("Failed to connect to the database in the filaments workspace!")?; 42 + 43 + Ok(Self { 44 + _private: (), 45 + db: conn, 46 + root: path, 47 + }) 48 + } 49 + 50 + pub async fn initialize(path: impl Into<PathBuf>) -> Result<Self> { 51 + let path = path.into(); 52 + 53 + // create the .filaments folder 54 + let filaments_dir = path.join(".filaments"); 55 + 56 + create_dir_all(&filaments_dir) 57 + .await 58 + .context("Failed to create the filaments directory!")?; 59 + 60 + // create the database inside there 61 + File::create(filaments_dir.join("filaments.db")).await?; 62 + 63 + Ok(Self::instansiate(path).await.expect( 64 + "Invariant broken. This instantiation call must always work 65 + since we just initialized the workspace.", 66 + )) 67 + } 68 + } 69 + 70 + #[cfg(test)] 71 + mod tests { 72 + use std::{ 73 + fs::{File, create_dir_all}, 74 + path::PathBuf, 75 + }; 76 + 77 + use dto::NanoId; 78 + 79 + use crate::types::Workspace; 80 + 81 + #[tokio::test] 82 + async fn test_instantiation() { 83 + let path = PathBuf::from("/tmp/filaments/.filaments/filaments.db"); 84 + create_dir_all(path.parent().unwrap()).unwrap(); 85 + let _ = File::create(&path).unwrap(); 86 + let _ws = Workspace::instansiate(dbg!(&path.parent().unwrap().parent().unwrap())) 87 + .await 88 + .unwrap(); 89 + } 90 + 91 + #[tokio::test] 92 + async fn test_initialization() { 93 + let path = PathBuf::from(format!("/tmp/filaments/{}", NanoId::default())); 94 + 95 + Workspace::initialize(path) 96 + .await 97 + .expect("Should initialize just fine"); 98 + } 99 + }
+39
src/types/zettel.rs
··· 1 + use std::path::PathBuf; 2 + 3 + use dto::{NanoId, ZettelModelEx}; 4 + 5 + use crate::types::Tag; 6 + 7 + /// A `Zettel` is a note about a single idea. 8 + /// It can have many `Tag`s, just meaning it can fall under many 9 + /// categories. 10 + #[expect(dead_code)] 11 + #[derive(Debug, Clone)] 12 + pub struct Zettel { 13 + /// Should only be constructed from models. 14 + _private: (), 15 + 16 + pub id: NanoId, 17 + pub title: String, 18 + /// a workspace-local file path, needs to be canonicalized before usage 19 + pub file_path: PathBuf, 20 + pub tags: Vec<Tag>, 21 + } 22 + 23 + impl From<ZettelModelEx> for Zettel { 24 + fn from(value: ZettelModelEx) -> Self { 25 + assert!( 26 + !value.tags.is_unloaded(), 27 + "When fetching a Zettel from the database, we expect 28 + to always have the tags loaded!!" 29 + ); 30 + 31 + Self { 32 + _private: (), 33 + id: value.nano_id, 34 + title: value.title, 35 + file_path: value.file_path.into(), 36 + tags: value.tags.into_iter().map(Into::into).collect(), 37 + } 38 + } 39 + }