···11//! The DTO's (Data Transfer Objects) used to interact with
22//! the Database. There is also a simple database struct in here.
3344-/// Database and its Errors
55-mod db;
66-pub use db::*;
44+/// For database stuff
55+pub use migration::{Migrator, MigratorTrait};
66+pub use sea_orm::{Database, DatabaseConnection};
7788/// exported traits for the database
99pub use sea_orm::ActiveModelTrait;
···1414/// Exporting this as DTO so we can newtype this in a later crate
1515/// and add additional functionality to it.
1616pub use migration::types::Priority as PriorityDTO;
1717+1818+pub use sea_orm::entity::prelude::DateTimeUtc;
17191820/// Color type, exporting as DTO because I might
1921/// want to newtype wrap this, might not have to, depending
+10-4
crates/dto/tests/common/mod.rs
···33 path::PathBuf,
44};
5566-use dto::Db;
76use rand::RngExt;
77+use sea_orm::{Database, DatabaseConnection};
8899-pub async fn fresh_test_db() -> Db {
99+pub async fn fresh_test_db() -> DatabaseConnection {
1010 let rand_id = {
1111 let mut rng = rand::rng();
1212 let mut rand_id = [0_u8; 4];
···1515 String::from_utf8(rand_id.to_vec()).unwrap()
1616 };
17171818- let path = PathBuf::from(format!("/tmp/filaments/test_db_{rand_id}"));
1818+ let path = PathBuf::from(format!("/tmp/filaments/test_db_{rand_id}.db"));
19192020 create_dir_all(path.parent().unwrap()).unwrap();
21212222 let _ = File::create(&path).unwrap();
2323- Db::connect(&path).await.unwrap()
2323+2424+ let db_conn_string = format!(
2525+ "sqlite://{}",
2626+ path.clone().canonicalize().unwrap().to_string_lossy()
2727+ );
2828+2929+ Database::connect(db_conn_string).await.unwrap()
2430}
···99use crate::{
1010 cli::Commands,
1111 config::{Config, get_config_dir},
1212+ types::Workspace,
1213};
13141415impl Commands {
1515- pub fn process(self) -> Result<()> {
1616+ pub async fn process(self) -> Result<()> {
1617 match self {
1718 Self::Init { name } => {
1819 // create the directory
···2021 .context("Failed to get current directory")?
2122 .join(&name);
22232323- // create the .filaments folder
2424- let filaments_dir = dir.join(".filaments");
2525-2626- create_dir_all(&filaments_dir)
2727- .context("Failed to create the filaments directory!")?;
2828-2929- // create the database inside there
3030- File::create(filaments_dir.join("filaments.db"))?;
2424+ Workspace::initialize(dir.clone()).await?;
31253226 // write config that sets the filaments directory to current dir!
3327 let config_kdl = dbg! {Config::generate(&dir)};
+23-17
src/gui/mod.rs
···11-use std::{
22- process,
33- sync::{
44- Arc,
55- atomic::{AtomicBool, Ordering},
66- },
77-};
88-91use eframe::egui;
102113/// The `Filaments Visualizer`, which is an instance of `eframe`, which uses `egui`
124#[derive(Default)]
135pub struct FilViz {
1414- shutdown_signal: Arc<AtomicBool>,
156 /// example for now
167 text: String,
178}
1891910impl FilViz {
2011 /// Create a new instance of the `FiLViz`
2121- const fn new(_cc: &eframe::CreationContext<'_>, shutdown_signal: Arc<AtomicBool>) -> Self {
1212+ const fn new(_cc: &eframe::CreationContext<'_>) -> Self {
2213 // Customize egui here with cc.egui_ctx.set_fonts and cc.egui_ctx.set_global_style.
2314 // Restore app state using cc.storage (requires the "persistence" feature).
2415 // Use the cc.gl (a glow::Context) to create graphics shaders and buffers that you can use
2516 // for e.g. egui::PaintCallback.
2617 Self {
2727- shutdown_signal,
2818 text: String::new(),
2919 }
3020 }
31213222 /// Create and run the `FilViz`.
3333- pub fn run(shutdown_signal: Arc<AtomicBool>) -> color_eyre::Result<()> {
2323+ pub fn run() -> color_eyre::Result<()> {
3424 let native_options = eframe::NativeOptions::default();
3525 eframe::run_native(
3626 "Filaments Visualizer",
3727 native_options,
3838- Box::new(|cc| Ok(Box::new(Self::new(cc, shutdown_signal)))),
2828+ Box::new(|cc| Ok(Box::new(Self::new(cc)))),
3929 )?;
40304131 Ok(())
···44344535impl eframe::App for FilViz {
4636 fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
4747- if self.shutdown_signal.load(Ordering::Relaxed) {
4848- process::exit(0)
4949- }
5050-5137 egui::CentralPanel::default().show_inside(ui, |ui| {
5238 ui.heading("Hello World!");
5339 ui.text_edit_singleline(&mut self.text);
4040+4141+ // credits!
4242+ ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| {
4343+ powered_by_egui_and_eframe(ui);
4444+ egui::warn_if_debug_build(ui);
4545+ });
5446 });
5547 }
5648}
4949+5050+fn powered_by_egui_and_eframe(ui: &mut egui::Ui) {
5151+ ui.horizontal(|ui| {
5252+ ui.spacing_mut().item_spacing.x = 0.0;
5353+ ui.label("Powered by ");
5454+ ui.hyperlink_to("egui", "https://github.com/emilk/egui");
5555+ ui.label(" and ");
5656+ ui.hyperlink_to(
5757+ "eframe",
5858+ "https://github.com/emilk/egui/tree/master/crates/eframe",
5959+ );
6060+ ui.label(".");
6161+ });
6262+}
+9-11
src/main.rs
···22//! My (suri.codes) personal-knowledge-system, with deeply integrated task tracking and long term goal planning capabilities.
33//!
4455-use std::sync::{
66- Arc,
77- atomic::{AtomicBool, Ordering},
88-};
55+use std::{process, sync::Arc};
96107use crate::{cli::Cli, gui::FilViz, tui::TuiApp};
118use clap::Parser;
···1310mod cli;
1411mod gui;
1512mod tui;
1313+mod types;
16141715mod config;
1816mod errors;
···29273028 // if there are any commands, run those and exit
3129 if let Some(command) = args.command {
3232- return rt.block_on(async { command.process() });
3030+ return rt.block_on(async { command.process().await });
3331 }
3434-3535- let shutdown_signal = Arc::new(AtomicBool::new(false));
36323733 // then we spawn the tui on its own thread
3834 let tui_handle = std::thread::spawn({
3535+ // arc stuff
3936 let tui_rt = rt.clone();
4040- let shutdown = shutdown_signal.clone();
3737+3838+ // closure to run the tui
4139 move || -> color_eyre::Result<()> {
4240 // block the tui on the same runtime as above
4341 tui_rt.block_on(async {
4442 let mut tui = TuiApp::new(args.tick_rate, args.frame_rate)?;
4543 tui.run().await?;
4646- shutdown.store(true, Ordering::Relaxed);
4747- Ok(())
4444+ // just close everything as soon as the tui is done running
4545+ process::exit(0);
4846 })
4947 }
5048 });
···5351 if args.visualizer {
5452 // enter the guard so egui_async works properly
5553 let _rt_guard = rt.enter();
5656- FilViz::run(shutdown_signal)?;
5454+ FilViz::run()?;
5755 }
58565957 // join on the tui
+13
src/types/color.rs
···11+use dto::ColorDTO;
22+33+/// Agnostic Color type,
44+/// internally represented as rgb
55+#[expect(dead_code)]
66+#[derive(Debug, Clone)]
77+pub struct Color(ColorDTO);
88+99+impl From<ColorDTO> for Color {
1010+ fn from(value: ColorDTO) -> Self {
1111+ Self(value)
1212+ }
1313+}
+43
src/types/group.rs
···11+use dto::{DateTimeUtc, GroupModelEx, NanoId};
22+33+use crate::types::{Color, Priority, Zettel};
44+55+/// A `Group` which contains tasks!
66+#[expect(dead_code)]
77+#[derive(Debug, Clone)]
88+pub struct Group {
99+ /// Should only be constructed from models.
1010+ _private: (),
1111+1212+ pub id: NanoId,
1313+ pub name: String,
1414+ pub color: Color,
1515+ pub priority: Priority,
1616+ pub created_at: DateTimeUtc,
1717+ pub modified_at: DateTimeUtc,
1818+ /// The `Zettel` that is related to this `Group`.
1919+ /// Can store notes regarding this group in
2020+ /// the `Zettel`
2121+ pub zettel: Zettel,
2222+}
2323+2424+impl From<GroupModelEx> for Group {
2525+ fn from(value: GroupModelEx) -> Self {
2626+ Self {
2727+ _private: (),
2828+ id: value.nano_id,
2929+ name: value.name,
3030+ color: value.color.into(),
3131+ priority: value.priority.into(),
3232+ created_at: value.created_at,
3333+ modified_at: value.modified_at,
3434+ zettel: value
3535+ .zettel
3636+ .into_option()
3737+ .expect(
3838+ "When fetching a Group from the database, we expect to always have the Zettel loaded!!",
3939+ )
4040+ .into(),
4141+ }
4242+ }
4343+}
+21
src/types/mod.rs
···11+mod color;
22+pub use color::Color;
33+44+mod tag;
55+pub use tag::Tag;
66+77+mod priority;
88+pub use priority::Priority;
99+1010+mod zettel;
1111+pub use zettel::Zettel;
1212+1313+mod group;
1414+pub use group::Group;
1515+1616+mod task;
1717+#[expect(unused_imports)]
1818+pub use task::Task;
1919+2020+mod workspace;
2121+pub use workspace::Workspace;
+13
src/types/priority.rs
···11+use dto::PriorityDTO;
22+33+/// An Enum for the various `Priority` levels
44+/// for `Task`s and `Group`s
55+#[expect(dead_code)]
66+#[derive(Debug, Clone)]
77+pub struct Priority(PriorityDTO);
88+99+impl From<PriorityDTO> for Priority {
1010+ fn from(value: PriorityDTO) -> Self {
1111+ Self(value)
1212+ }
1313+}
+41
src/types/tag.rs
···11+use dto::{NanoId, TagModel, TagModelEx};
22+33+use crate::types::Color;
44+55+/// Represents a `Tag` in a `ZettelKasten` note taking method.
66+/// Easy way to link multiple notes under one simple word.
77+#[expect(dead_code)]
88+#[derive(Debug, Clone)]
99+pub struct Tag {
1010+ /// Should only be constructed from models.
1111+ _private: (),
1212+1313+ /// A unique `NanoId`
1414+ pub id: NanoId,
1515+ /// Name of the tag
1616+ pub name: String,
1717+ /// Color of the tag
1818+ pub color: Color,
1919+}
2020+2121+impl From<TagModel> for Tag {
2222+ fn from(value: TagModel) -> Self {
2323+ Self {
2424+ _private: (),
2525+ id: value.nano_id,
2626+ name: value.name,
2727+ color: value.color.into(),
2828+ }
2929+ }
3030+}
3131+3232+impl From<TagModelEx> for Tag {
3333+ fn from(value: TagModelEx) -> Self {
3434+ Self {
3535+ _private: (),
3636+ id: value.nano_id,
3737+ name: value.name,
3838+ color: value.color.into(),
3939+ }
4040+ }
4141+}
+52
src/types/task.rs
···11+use dto::{DateTimeUtc, NanoId, TaskModelEx};
22+33+use crate::types::{Group, Priority, Zettel};
44+55+/// a `Task` that you have to complete!
66+#[expect(dead_code)]
77+#[derive(Debug, Clone)]
88+pub struct Task {
99+ /// Should only be constructed from models.
1010+ _private:(),
1111+1212+ pub id: NanoId,
1313+ pub name: String,
1414+ pub priority: Priority,
1515+ pub due: Option<DateTimeUtc>,
1616+ pub finished_at: Option<DateTimeUtc>,
1717+ pub created_at: DateTimeUtc,
1818+ pub modified_at: DateTimeUtc,
1919+ /// Each task has its own related `Zettel`.
2020+ pub zettel: Zettel,
2121+ pub group: Group,
2222+}
2323+2424+impl From<TaskModelEx> for Task {
2525+ fn from(value: TaskModelEx) -> Self {
2626+ Self {
2727+ _private: (),
2828+ id: value.nano_id,
2929+ name: value.name,
3030+ priority: value.priority.into(),
3131+ due: value.due,
3232+ finished_at: value.finished_at,
3333+ created_at: value.created_at,
3434+ modified_at: value.modified_at,
3535+ zettel: value
3636+ .zettel
3737+ .into_option()
3838+ .expect(
3939+ "When fetching a Task from the database, we expect to always have the Zettel loaded!!",
4040+ )
4141+ .into(),
4242+ group: value
4343+ .group
4444+ .into_option()
4545+ .expect(
4646+ "When fetching a Task from the database, we expect to always have the Group loaded!!",
4747+ )
4848+ .into(),
4949+5050+ }
5151+ }
5252+}
+99
src/types/workspace.rs
···11+use std::path::PathBuf;
22+33+use color_eyre::eyre::{Context, Result};
44+use dto::{Database, DatabaseConnection};
55+use tokio::fs::{File, create_dir_all};
66+use tracing::debug;
77+88+/// The `Workspace` in which the filaments exist.
99+#[expect(dead_code)]
1010+#[derive(Debug, Clone)]
1111+pub struct Workspace {
1212+ /// Private field so it can only be instantiated from a `Path`
1313+ _private: (),
1414+ /// Connection to the sqlite database inside the `Workspace`
1515+ pub db: DatabaseConnection,
1616+ /// The path to the root of this workspace
1717+ pub root: PathBuf,
1818+}
1919+2020+impl Workspace {
2121+ /// Given a path, try to construct a `Workspace` based on its contents.
2222+ ///
2323+ /// Note: this means that there should already exist a valid `Workspace`
2424+ /// at that path.
2525+ pub async fn instansiate(path: impl Into<PathBuf>) -> Result<Self> {
2626+ let path = path.into();
2727+2828+ let db_conn_string = format!(
2929+ "sqlite://{}",
3030+ path.clone()
3131+ .join(".filaments/filaments.db")
3232+ .canonicalize()
3333+ .context("Invalid Filaments workspace!!")?
3434+ .to_string_lossy()
3535+ );
3636+3737+ debug!("connecting to {db_conn_string}");
3838+3939+ let conn = Database::connect(db_conn_string)
4040+ .await
4141+ .context("Failed to connect to the database in the filaments workspace!")?;
4242+4343+ Ok(Self {
4444+ _private: (),
4545+ db: conn,
4646+ root: path,
4747+ })
4848+ }
4949+5050+ pub async fn initialize(path: impl Into<PathBuf>) -> Result<Self> {
5151+ let path = path.into();
5252+5353+ // create the .filaments folder
5454+ let filaments_dir = path.join(".filaments");
5555+5656+ create_dir_all(&filaments_dir)
5757+ .await
5858+ .context("Failed to create the filaments directory!")?;
5959+6060+ // create the database inside there
6161+ File::create(filaments_dir.join("filaments.db")).await?;
6262+6363+ Ok(Self::instansiate(path).await.expect(
6464+ "Invariant broken. This instantiation call must always work
6565+ since we just initialized the workspace.",
6666+ ))
6767+ }
6868+}
6969+7070+#[cfg(test)]
7171+mod tests {
7272+ use std::{
7373+ fs::{File, create_dir_all},
7474+ path::PathBuf,
7575+ };
7676+7777+ use dto::NanoId;
7878+7979+ use crate::types::Workspace;
8080+8181+ #[tokio::test]
8282+ async fn test_instantiation() {
8383+ let path = PathBuf::from("/tmp/filaments/.filaments/filaments.db");
8484+ create_dir_all(path.parent().unwrap()).unwrap();
8585+ let _ = File::create(&path).unwrap();
8686+ let _ws = Workspace::instansiate(dbg!(&path.parent().unwrap().parent().unwrap()))
8787+ .await
8888+ .unwrap();
8989+ }
9090+9191+ #[tokio::test]
9292+ async fn test_initialization() {
9393+ let path = PathBuf::from(format!("/tmp/filaments/{}", NanoId::default()));
9494+9595+ Workspace::initialize(path)
9696+ .await
9797+ .expect("Should initialize just fine");
9898+ }
9999+}
+39
src/types/zettel.rs
···11+use std::path::PathBuf;
22+33+use dto::{NanoId, ZettelModelEx};
44+55+use crate::types::Tag;
66+77+/// A `Zettel` is a note about a single idea.
88+/// It can have many `Tag`s, just meaning it can fall under many
99+/// categories.
1010+#[expect(dead_code)]
1111+#[derive(Debug, Clone)]
1212+pub struct Zettel {
1313+ /// Should only be constructed from models.
1414+ _private: (),
1515+1616+ pub id: NanoId,
1717+ pub title: String,
1818+ /// a workspace-local file path, needs to be canonicalized before usage
1919+ pub file_path: PathBuf,
2020+ pub tags: Vec<Tag>,
2121+}
2222+2323+impl From<ZettelModelEx> for Zettel {
2424+ fn from(value: ZettelModelEx) -> Self {
2525+ assert!(
2626+ !value.tags.is_unloaded(),
2727+ "When fetching a Zettel from the database, we expect
2828+ to always have the tags loaded!!"
2929+ );
3030+3131+ Self {
3232+ _private: (),
3333+ id: value.nano_id,
3434+ title: value.title,
3535+ file_path: value.file_path.into(),
3636+ tags: value.tags.into_iter().map(Into::into).collect(),
3737+ }
3838+ }
3939+}