diff --git a/src/tui/components/zk/mod.rs b/src/tui/components/zk/mod.rs index 947036d..f1fdf07 100644 --- a/src/tui/components/zk/mod.rs +++ b/src/tui/components/zk/mod.rs @@ -95,9 +95,6 @@ impl Zk<'_> { let kt = kh.read().await; - info!("{selected_zettel:#?}"); - info!("{kt:#?}"); - let zettel = zettels .iter() .find(|&z| &z.id == selected_zettel) diff --git a/src/types/index.rs b/src/types/index.rs index b71b94e..350d1f9 100644 --- a/src/types/index.rs +++ b/src/types/index.rs @@ -1,22 +1,25 @@ use std::{ - collections::HashMap, + collections::{HashMap, HashSet}, path::{Path, PathBuf}, }; use color_eyre::eyre::Result; use dto::{ ActiveModelTrait, ActiveValue, ColumnTrait, DatabaseConnection, EntityTrait, IntoActiveModel, - QueryFilter, TagActiveModel, TagEntity, ZettelEntity, ZettelModelEx, ZettelTagActiveModel, - ZettelTagColumns, ZettelTagEntity, + NanoId, QueryFilter, TagActiveModel, TagEntity, ZettelEntity, ZettelModelEx, + ZettelTagActiveModel, ZettelTagColumns, ZettelTagEntity, }; +use pulldown_cmark::{Event, Options, Parser}; use rayon::iter::{ParallelBridge, ParallelIterator}; use tracing::info; -use crate::types::{FrontMatter, ZettelId, frontmatter::Body}; +use crate::types::{FrontMatter, Link, ZettelId, frontmatter::Body}; #[derive(Debug, Clone)] pub struct Index { pub(super) zods: HashMap, + pub(super) outgoing_links: HashMap>, + // pub(super) incoming_links: HashMap>, } #[derive(Debug, Clone)] @@ -32,6 +35,7 @@ impl Index { let root = root.canonicalize()?; let mut zods = HashMap::new(); + let mut possible_outgoing_links = HashMap::new(); std::fs::read_dir(root)? .par_bridge() @@ -44,28 +48,75 @@ impl Index { .and_then(|ext| ext.to_str()) .is_some_and(|ext| ext == "md") }) - .map(|entry| -> Result<(ZettelId, ZettelOnDisk)> { + .map(|entry| -> Result<(ZettelId, ZettelOnDisk, Vec)> { let path = entry.path(); - let id: ZettelId = path.as_path().try_into()?; + let zid: ZettelId = path.as_path().try_into()?; let (fm, body) = FrontMatter::extract_from_file(&path)?; + let outgoing_links = Self::parse_outgoing_links(&zid, &body); + Ok(( - id, + zid, ZettelOnDisk { fm, body, path: path.canonicalize()?, }, + outgoing_links, )) }) .collect::>>()? .into_iter() // .par_bridge() - .for_each(|(id, zod)| { - zods.insert(id, zod); + .for_each(|(id, zod, zettel_outgoing_links)| { + zods.insert(id.clone(), zod); + possible_outgoing_links.insert(id, zettel_outgoing_links); }); - Ok(Self { zods }) + // simple validation step for links + let zid_set = zods.keys().cloned().collect::>(); + + let outgoing_links = possible_outgoing_links + .into_iter() + .map(|(id, links)| { + let valid_links = links + .into_iter() + .filter(|link| zid_set.contains(&link.source) && zid_set.contains(&link.dest)) + .collect::>(); + + (id, valid_links) + }) + .collect::>(); + + Ok(Self { + zods, + outgoing_links, + }) + } + + pub fn parse_outgoing_links(zid: &ZettelId, body: &str) -> Vec { + let parser = Parser::new_ext(body, Options::ENABLE_WIKILINKS); + + // let mut links = vec![]; + let mut links = vec![]; + + for event in parser { + if let Event::Start(pulldown_cmark::Tag::Link { + link_type: pulldown_cmark::LinkType::WikiLink { has_pothole: _ }, + dest_url, + .. + }) = event + { + let nano_id = dest_url.as_ref(); + + // how do we validate that this is a proper link? + // we are just going to trust them for now + + links.push(Link::new(zid.clone(), NanoId::from(nano_id))); + } + } + + links } pub fn update_path_for_zid(&mut self, zid: &ZettelId, new_path: PathBuf) { @@ -86,7 +137,7 @@ impl Index { /// Sync's the curren title of the `Zettel` with the /// provided `zid` with the `DB` - pub async fn sync_title_with_db( + pub async fn sync_zettel_title_with_db( &mut self, zid: &ZettelId, db: &DatabaseConnection, diff --git a/src/types/kasten.rs b/src/types/kasten.rs index 3fe73bc..897b2be 100644 --- a/src/types/kasten.rs +++ b/src/types/kasten.rs @@ -111,7 +111,7 @@ impl Kasten { self.index.process_zid(&zid)?; // and then we sync tags self.index.sync_tags_with_db(&zid, &self.db).await?; - self.index.sync_title_with_db(&zid, &self.db).await?; + self.index.sync_zettel_title_with_db(&zid, &self.db).await?; Ok(()) } -- 2.51.2 From f4d112fe48fd39acb53feb98617a0e5a8f425c81 Mon Sep 17 00:00:00 2001 From: suri-codes Date: Tue, 7 Apr 2026 23:23:09 -0700 Subject: [PATCH 2/3] feat: dirt basic visualizer --- src/gui/mod.rs | 24 +++++++++++++--------- src/main.rs | 4 +++- src/types/filaments.rs | 45 +++++++++++++++++++++++++++++++----------- src/types/mod.rs | 1 - 4 files changed, 51 insertions(+), 23 deletions(-) diff --git a/src/gui/mod.rs b/src/gui/mod.rs index aea2471..79f3c0c 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -1,42 +1,48 @@ use eframe::egui; +use crate::types::{Filaments, Index}; + /// The `Filaments Visualizer`, which is an instance of `eframe`, which uses `egui` -#[derive(Default)] pub struct FilViz { - /// example for now - text: String, + filaments: Filaments, } impl FilViz { /// Create a new instance of the `FiLViz` - const fn new(_cc: &eframe::CreationContext<'_>) -> Self { + fn new(_cc: &eframe::CreationContext<'_>, idx: &Index) -> Self { // Customize egui here with cc.egui_ctx.set_fonts and cc.egui_ctx.set_global_style. // Restore app state using cc.storage (requires the "persistence" feature). // Use the cc.gl (a glow::Context) to create graphics shaders and buffers that you can use // for e.g. egui::PaintCallback. Self { - text: String::new(), + filaments: Filaments::from(idx), } } /// Create and run the `FilViz`. - pub fn run() -> color_eyre::Result<()> { + pub fn run(idx: &Index) -> color_eyre::Result<()> { let native_options = eframe::NativeOptions::default(); eframe::run_native( "Filaments Visualizer", native_options, - Box::new(|cc| Ok(Box::new(Self::new(cc)))), + Box::new(|cc| Ok(Box::new(Self::new(cc, idx)))), )?; Ok(()) } } +type L = egui_graphs::LayoutForceDirected; +type S = egui_graphs::FruchtermanReingoldWithCenterGravityState; + impl eframe::App for FilViz { fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) { egui::CentralPanel::default().show_inside(ui, |ui| { - ui.heading("Hello World!"); - ui.text_edit_singleline(&mut self.text); + let g = &mut self.filaments.graph; + + let mut view = egui_graphs::GraphView::<_, _, _, _, _, _, S, L>::new(g); + + ui.add(&mut view); // credits! ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { diff --git a/src/main.rs b/src/main.rs index 491a4b4..8821878 100644 --- a/src/main.rs +++ b/src/main.rs @@ -52,6 +52,7 @@ fn main() -> color_eyre::Result<()> { let tui_handle = std::thread::spawn({ // arc stuff let tui_rt = rt.clone(); + let kh = kh.clone(); // closure to run the tui move || -> color_eyre::Result<()> { @@ -69,7 +70,8 @@ fn main() -> color_eyre::Result<()> { if args.visualizer { // enter the guard so egui_async works properly let _rt_guard = rt.enter(); - FilViz::run()?; + let index = rt.block_on(async { kh.read().await.index.clone() }); + FilViz::run(&index)?; } // join on the tui diff --git a/src/types/filaments.rs b/src/types/filaments.rs index 6673837..44c6c12 100644 --- a/src/types/filaments.rs +++ b/src/types/filaments.rs @@ -1,6 +1,7 @@ #![expect(dead_code)] use std::{cmp::max, collections::HashMap}; +use eframe::emath; use egui_graphs::{ Graph, petgraph::{Directed, graph::NodeIndex, prelude::StableGraph}, @@ -16,30 +17,50 @@ const GRAPH_MIN_NODES: usize = 128; const GRAPH_MIN_EDGES: usize = GRAPH_MIN_NODES * 3; pub struct Filaments { - graph: ZkGraph, + pub graph: ZkGraph, /// simple conversions zid_to_gid: HashMap, } -// pub type FilamentsHandle = Arc -// - -// impl Filaments { -// pub fn construct() -> Result {} -// } - impl From<&Index> for Filaments { fn from(value: &Index) -> Self { let number_of_zettels = value.zods().len(); - let mut _graph: ZkGraph = ZkGraph::from(&StableGraph::with_capacity( + let mut zid_to_gid = HashMap::new(); + + let mut graph: ZkGraph = ZkGraph::from(&StableGraph::with_capacity( max(number_of_zettels * 2, GRAPH_MIN_EDGES), max(number_of_zettels * 3, GRAPH_MIN_EDGES), )); - #[expect(clippy::for_kv_map)] - for (_id, _zod) in value.zods() {} + for (zid, zod) in value.zods() { + let node_idx = graph.add_node_custom(zid.clone(), |node| { + node.set_label(zod.fm.title.clone()); + let disp = node.display_mut(); + disp.radius = 100.0; + + // randomize position + let x = rand::random_range(0.0..=100.0); + let y = rand::random_range(0.0..=100.0); + node.set_location(emath::Pos2 { x, y }); + }); + + let _ = zid_to_gid.insert(zid.clone(), node_idx); + } + + for (_, links) in value.outgoing_links.clone() { + for link in links { + let start = zid_to_gid + .get(&link.source) + .expect("Invariant broken, must exist in here if its in the index"); + let end = zid_to_gid + .get(&link.dest) + .expect("Invariant broken, must exist in here if its in the index"); + + let _ = graph.add_edge(*start, *end, link); + } + } - todo!() + Self { graph, zid_to_gid } } } diff --git a/src/types/mod.rs b/src/types/mod.rs index 3be8929..ea43a7d 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -22,7 +22,6 @@ mod link; pub use link::Link; mod filaments; -#[expect(unused_imports)] pub use filaments::Filaments; mod index; -- 2.51.2 From 71d22ec62e4037d1c2a259c9d5105f346cbf96b2 Mon Sep 17 00:00:00 2001 From: suri-codes Date: Wed, 8 Apr 2026 10:02:03 -0700 Subject: [PATCH 3/3] feat: show title always --- src/gui/mod.rs | 16 +++++++++++++++- src/types/filaments.rs | 3 ++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 79f3c0c..95c51e8 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -1,4 +1,5 @@ use eframe::egui; +use egui_graphs::{SettingsInteraction, SettingsNavigation}; use crate::types::{Filaments, Index}; @@ -40,7 +41,20 @@ impl eframe::App for FilViz { egui::CentralPanel::default().show_inside(ui, |ui| { let g = &mut self.filaments.graph; - let mut view = egui_graphs::GraphView::<_, _, _, _, _, _, S, L>::new(g); + let mut view = egui_graphs::GraphView::<_, _, _, _, _, _, S, L>::new(g) + .with_interactions( + &SettingsInteraction::new() + .with_hover_enabled(false) + .with_dragging_enabled(false) + .with_node_selection_enabled(false) + .with_node_selection_multi_enabled(false) + .with_edge_selection_enabled(false) + .with_edge_selection_multi_enabled(false), + ) + .with_navigations( + &SettingsNavigation::new().with_fit_to_screen_padding(0.5), // .with_zoom_and_pan_enabled(true) + // .with_fit_to_screen_enabled(false), + ); ui.add(&mut view); diff --git a/src/types/filaments.rs b/src/types/filaments.rs index 44c6c12..64b517a 100644 --- a/src/types/filaments.rs +++ b/src/types/filaments.rs @@ -37,12 +37,13 @@ impl From<&Index> for Filaments { let node_idx = graph.add_node_custom(zid.clone(), |node| { node.set_label(zod.fm.title.clone()); let disp = node.display_mut(); - disp.radius = 100.0; + disp.radius = 50.0; // randomize position let x = rand::random_range(0.0..=100.0); let y = rand::random_range(0.0..=100.0); node.set_location(emath::Pos2 { x, y }); + node.set_hovered(true); }); let _ = zid_to_gid.insert(zid.clone(), node_idx);