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.

feat: zettel_list + preview!

+311 -52
+2
.config/config.kdl
··· 8 8 <Ctrl-c> Quit // Another way to quit 9 9 <Ctrl-z> Suspend // Suspend the application 10 10 h Helix 11 + j MoveDown 12 + k MoveUp 11 13 } 12 14 }
+11 -11
flake.lock
··· 8 8 "rust-analyzer-src": "rust-analyzer-src" 9 9 }, 10 10 "locked": { 11 - "lastModified": 1774682177, 12 - "narHash": "sha256-OVbuJnJLlbHE28eRMudjtA6NXz/ifuXSho79gvh6GHY=", 11 + "lastModified": 1774887814, 12 + "narHash": "sha256-8bsvr2SgW9t70nFxUdMVQg4RSU+E/y9+Je4urivtOYE=", 13 13 "owner": "nix-community", 14 14 "repo": "fenix", 15 - "rev": "e0f515387df77b9fdbaaf81e7f866f0365474c18", 15 + "rev": "9b6c1cb49eff1346e1f5d2430994a0ba0fa02910", 16 16 "type": "github" 17 17 }, 18 18 "original": { ··· 23 23 }, 24 24 "nixpkgs": { 25 25 "locked": { 26 - "lastModified": 1774386573, 27 - "narHash": "sha256-4hAV26quOxdC6iyG7kYaZcM3VOskcPUrdCQd/nx8obc=", 28 - "rev": "46db2e09e1d3f113a13c0d7b81e2f221c63b8ce9", 29 - "revCount": 969196, 26 + "lastModified": 1774709303, 27 + "narHash": "sha256-D3Q07BbIA2KnTcSXIqqu9P586uWxN74zNoCH3h2ESHg=", 28 + "rev": "8110df5ad7abf5d4c0f6fb0f8f978390e77f9685", 29 + "revCount": 971119, 30 30 "type": "tarball", 31 - "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.969196%2Brev-46db2e09e1d3f113a13c0d7b81e2f221c63b8ce9/019d279e-af65-79ce-92be-5dee7b1e36d4/source.tar.gz" 31 + "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.971119%2Brev-8110df5ad7abf5d4c0f6fb0f8f978390e77f9685/019d3c72-3e5d-7d8e-a4fc-0fe67ed1554b/source.tar.gz" 32 32 }, 33 33 "original": { 34 34 "type": "tarball", ··· 44 44 "rust-analyzer-src": { 45 45 "flake": false, 46 46 "locked": { 47 - "lastModified": 1774569884, 48 - "narHash": "sha256-E8iWEPzg7OnE0XXXjo75CX7xFauqzJuGZ5wSO9KS8Ek=", 47 + "lastModified": 1774787924, 48 + "narHash": "sha256-Cbpmf0+1pqi/zbpub2vkp5lTPx3QdVtDkkagDwQzHHg=", 49 49 "owner": "rust-lang", 50 50 "repo": "rust-analyzer", 51 - "rev": "443ddcddd0c73b07b799d052f5ef3b448c2f3508", 51 + "rev": "f1297b21119565c626320c1ffc248965fffb2527", 52 52 "type": "github" 53 53 }, 54 54 "original": {
+2 -2
src/main.rs
··· 13 13 }; 14 14 use clap::Parser; 15 15 use tokio::sync::RwLock; 16 - use tracing::{debug, info}; 16 + use tracing::debug; 17 17 18 18 mod cli; 19 19 mod gui; ··· 56 56 move || -> color_eyre::Result<()> { 57 57 // block the tui on the same runtime as above 58 58 tui_rt.block_on(async { 59 - let mut tui = TuiApp::new(args.tick_rate, args.frame_rate, kh)?; 59 + let mut tui = TuiApp::new(args.tick_rate, args.frame_rate, kh).await?; 60 60 tui.run().await?; 61 61 // just close everything as soon as the tui is done running 62 62 process::exit(0);
+4 -4
src/tui/app.rs
··· 26 26 #[allow(dead_code)] 27 27 region: Region, 28 28 last_tick_key_events: Vec<KeyEvent>, 29 - kh: KastenHandle, 29 + _kh: KastenHandle, 30 30 signal_tx: UnboundedSender<Signal>, 31 31 signal_rx: UnboundedReceiver<Signal>, 32 32 } ··· 45 45 46 46 impl App { 47 47 /// Construct a new `App` instance. 48 - pub fn new(tick_rate: f64, frame_rate: f64, kh: KastenHandle) -> Result<Self> { 48 + pub async fn new(tick_rate: f64, frame_rate: f64, kh: KastenHandle) -> Result<Self> { 49 49 let (signal_tx, signal_rx) = mpsc::unbounded_channel(); 50 50 51 51 Ok(Self { 52 52 tick_rate, 53 53 frame_rate, 54 - components: vec![Box::new(Zk::new(kh.clone()))], 54 + components: vec![Box::new(Zk::new(kh.clone()).await?)], 55 55 should_quit: false, 56 56 should_suspend: false, 57 57 config: Config::parse()?, 58 58 region: Region::default(), 59 59 last_tick_key_events: Vec::new(), 60 - kh, 60 + _kh: kh, 61 61 signal_tx, 62 62 signal_rx, 63 63 })
+149 -30
src/tui/components/zk/mod.rs
··· 1 1 use async_trait::async_trait; 2 - use color_eyre::{eyre::Result, owo_colors::colors::Red}; 3 - use ratatui::{prelude::*, widgets::Block}; 2 + use color_eyre::eyre::Result; 3 + use ratatui::{ 4 + prelude::*, 5 + widgets::{Block, List, ListState}, 6 + }; 4 7 use tokio::sync::mpsc::UnboundedSender; 5 8 6 9 use crate::{ 7 10 tui::{Signal, components::Component}, 8 - types::KastenHandle, 11 + types::{KastenHandle, ZettelId}, 9 12 }; 10 13 11 - mod zettel; 14 + mod preview; 15 + mod zettel_view; 16 + 17 + use preview::Preview; 18 + use zettel_view::ZettelView; 12 19 13 - pub struct Zk { 20 + pub struct Zk<'text> { 14 21 signal_tx: Option<UnboundedSender<Signal>>, 15 22 kh: KastenHandle, 16 23 layouts: Layouts, 24 + zettel_list: ZettelList<'text>, 25 + zettel_view: ZettelView<'text>, 26 + preview: Preview<'text>, 17 27 } 18 28 19 29 struct Layouts { ··· 22 32 z_preview: Layout, 23 33 } 24 34 35 + struct ZettelList<'text> { 36 + render_list: List<'text>, 37 + id_list: Vec<ZettelId>, 38 + state: ListState, 39 + } 40 + 25 41 impl Default for Layouts { 26 42 fn default() -> Self { 27 43 Self { ··· 30 46 Constraint::Percentage(50), 31 47 ]), 32 48 search_zl: Layout::vertical(vec![ 33 - Constraint::Percentage(15), 34 - Constraint::Percentage(85), 49 + Constraint::Percentage(10), 50 + Constraint::Percentage(90), 35 51 ]), 36 52 z_preview: Layout::vertical(vec![ 37 - Constraint::Percentage(30), 38 - Constraint::Percentage(70), 53 + Constraint::Percentage(20), 54 + Constraint::Percentage(80), 39 55 ]), 40 56 } 41 57 } 42 58 } 43 59 44 - impl Zk { 45 - pub fn new(kh: KastenHandle) -> Self { 46 - Self { 60 + impl Zk<'_> { 61 + pub async fn new(kh: KastenHandle) -> Result<Self> { 62 + let kt = kh.read().await; 63 + 64 + let nodes = kt.graph.nodes_iter().collect::<Vec<_>>(); 65 + 66 + let zettel_list = { 67 + let render_list = List::new(nodes.iter().map(|(_, n)| { 68 + let z = n.payload(); 69 + let title = z.title.clone(); 70 + let _tags = z.tags.clone(); 71 + // let _last_modified = z.modified_at; 72 + Text::from(title) 73 + })) 74 + .style(Color::White) 75 + .highlight_style(Modifier::REVERSED) 76 + .highlight_symbol("> "); 77 + 78 + let id_list = nodes 79 + .iter() 80 + .map(|(_, n)| n.payload().id.clone()) 81 + .collect::<Vec<_>>(); 82 + 83 + let mut state = ListState::default(); 84 + state.select_first(); 85 + 86 + ZettelList { 87 + render_list, 88 + id_list, 89 + state, 90 + } 91 + }; 92 + 93 + let selected_zettel = zettel_list 94 + .id_list 95 + .get( 96 + zettel_list 97 + .state 98 + .selected() 99 + .expect("TODO: must handle the case where there isnt one..."), 100 + ) 101 + .expect("must exist"); 102 + 103 + let zettel = kt 104 + .get_by_zettel_id(selected_zettel) 105 + .expect("must exist, handle case where it doesnt later...") 106 + .payload(); 107 + 108 + let preview = Preview::from( 109 + zettel 110 + .content(&kt.ws) 111 + .await 112 + .expect("This thing cannot be parsed properly..."), 113 + ); 114 + 115 + // okay now that we have the zettel we need to construct the zettel out of this id 116 + let zettel_view: ZettelView = kt 117 + .get_by_zettel_id(selected_zettel) 118 + .expect("must exist, handle case where it doesnt later...") 119 + .payload() 120 + .into(); 121 + 122 + drop(kt); 123 + 124 + Ok(Self { 47 125 signal_tx: None, 48 126 kh, 49 127 layouts: Layouts::default(), 50 - } 128 + zettel_list, 129 + zettel_view, 130 + preview, 131 + }) 132 + } 133 + 134 + async fn update_views_from_zettel_list_selection(&mut self) -> Result<()> { 135 + let selection_idx = self 136 + .zettel_list 137 + .state 138 + .selected() 139 + .expect("i have no idea what to do if this doesnt exist"); 140 + 141 + // sometimes the selection we get is over the length of the thing, so its 142 + // actually fine if this is none, just means we reached the end of the list 143 + let Some(z_id) = self.zettel_list.id_list.get(selection_idx) else { 144 + return Ok(()); 145 + }; 146 + 147 + let kh = self.kh.read().await; 148 + 149 + self.zettel_view = kh 150 + .get_by_zettel_id(z_id) 151 + .expect("this should be valid unless the kasten changed out underneath us") 152 + .payload() 153 + .into(); 154 + 155 + self.preview = kh 156 + .get_by_zettel_id(z_id) 157 + .expect("this should be valid unless the kasten changed out underneath us") 158 + .payload() 159 + .content(&kh.ws) 160 + .await? 161 + .into(); 162 + drop(kh); 163 + 164 + Ok(()) 51 165 } 52 166 } 53 167 54 168 #[async_trait] 55 - impl Component for Zk { 169 + impl Component for Zk<'_> { 56 170 fn register_signal_handler(&mut self, tx: UnboundedSender<Signal>) -> Result<()> { 57 171 self.signal_tx = Some(tx); 58 172 Ok(()) 59 173 } 60 174 61 - async fn update(&mut self, _signal: Signal) -> Result<Option<crate::tui::Signal>> { 62 - // match signal { 63 - // Signal::Tick => todo!(), 64 - // Signal::Render => todo!(), 65 - // Signal::Resize(_, _) => todo!(), 66 - // Signal::Suspend => todo!(), 67 - // Signal::Resume => todo!(), 68 - // Signal::Quit => todo!(), 69 - // Signal::ClearScreen => todo!(), 70 - // Signal::Error(_) => todo!(), 71 - // Signal::Help => todo!(), 72 - // Signal::Helix => todo!(), 73 - // } 175 + async fn update(&mut self, signal: Signal) -> Result<Option<crate::tui::Signal>> { 176 + match signal { 177 + Signal::MoveDown => { 178 + self.zettel_list.state.select_next(); 179 + self.update_views_from_zettel_list_selection().await?; 180 + } 181 + Signal::MoveUp => { 182 + self.zettel_list.state.select_previous(); 183 + self.update_views_from_zettel_list_selection().await?; 184 + } 185 + _ => {} 186 + } 74 187 Ok(None) 75 188 } 76 189 ··· 92 205 }; 93 206 94 207 frame.render_widget(Block::new().bg(Color::Red), search_layout); 95 - frame.render_widget(Block::new().bg(Color::Blue), zettel_list_layout); 96 - frame.render_widget(Block::new().bg(Color::Green), zettel_layout); 97 - frame.render_widget(Block::new().bg(Color::Yellow), preview_layout); 208 + 209 + frame.render_stateful_widget( 210 + &self.zettel_list.render_list, 211 + zettel_list_layout, 212 + &mut self.zettel_list.state, 213 + ); 214 + 215 + frame.render_widget(self.zettel_view.clone(), zettel_layout); 216 + frame.render_widget(self.preview.clone(), preview_layout); 98 217 99 218 Ok(()) 100 219 }
+23
src/tui/components/zk/preview.rs
··· 1 + use ratatui::{text::Text, widgets::Widget}; 2 + 3 + #[derive(Debug, Clone)] 4 + pub struct Preview<'text> { 5 + content: Text<'text>, 6 + } 7 + 8 + impl From<String> for Preview<'_> { 9 + fn from(value: String) -> Self { 10 + Self { 11 + content: Text::from(value), 12 + } 13 + } 14 + } 15 + 16 + impl Widget for Preview<'_> { 17 + fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) 18 + where 19 + Self: Sized, 20 + { 21 + self.content.render(area, buf); 22 + } 23 + }
src/tui/components/zk/zettel.rs

This is a binary file and will not be displayed.

+89
src/tui/components/zk/zettel_view.rs
··· 1 + use ratatui::{ 2 + layout::{Constraint, Layout}, 3 + text::Text, 4 + widgets::Widget, 5 + }; 6 + 7 + use crate::types::Zettel; 8 + 9 + /// A `Widget` that represents a `Zettel` 10 + #[derive(Debug, Clone)] 11 + pub struct ZettelView<'text> { 12 + title: Text<'text>, 13 + tags: Text<'text>, 14 + created_at: Text<'text>, 15 + modified_at: Text<'text>, 16 + id: Text<'text>, 17 + layouts: Layouts, 18 + } 19 + 20 + impl From<&Zettel> for ZettelView<'_> { 21 + fn from(value: &Zettel) -> Self { 22 + Self { 23 + title: Text::from(value.title.clone()), 24 + tags: Text::from( 25 + value 26 + .tags 27 + .iter() 28 + .fold(" ".to_owned(), |acc, t| format!("{acc} {}", t.name)), 29 + ), 30 + created_at: Text::from(value.created_at()), 31 + modified_at: Text::from(value.modified_at()), 32 + id: Text::from(value.id.to_string()), 33 + layouts: Layouts::default(), 34 + } 35 + } 36 + } 37 + 38 + #[derive(Debug, Clone)] 39 + struct Layouts { 40 + left_right: Layout, 41 + title_tags: Layout, 42 + cr_mod_id: Layout, 43 + } 44 + 45 + impl Default for Layouts { 46 + fn default() -> Self { 47 + Self { 48 + left_right: Layout::horizontal(vec![ 49 + Constraint::Percentage(70), 50 + Constraint::Percentage(30), 51 + ]), 52 + 53 + title_tags: Layout::vertical(vec![ 54 + Constraint::Percentage(50), 55 + Constraint::Percentage(50), 56 + ]), 57 + cr_mod_id: Layout::vertical(vec![ 58 + Constraint::Percentage(33), 59 + Constraint::Percentage(33), 60 + Constraint::Percentage(33), 61 + ]), 62 + } 63 + } 64 + } 65 + 66 + impl Widget for ZettelView<'_> { 67 + fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) 68 + where 69 + Self: Sized, 70 + { 71 + let (title_rect, tags_rect, created_rect, modified_rect, id_rect) = { 72 + let rects = self.layouts.left_right.split(area); 73 + 74 + let (left, right) = (rects[0], rects[1]); 75 + 76 + let l_rects = self.layouts.title_tags.split(left); 77 + 78 + let r_rects = self.layouts.cr_mod_id.split(right); 79 + 80 + (l_rects[0], l_rects[1], r_rects[0], r_rects[1], r_rects[2]) 81 + }; 82 + 83 + self.title.render(title_rect, buf); 84 + self.tags.render(tags_rect, buf); 85 + self.created_at.render(created_rect, buf); 86 + self.modified_at.render(modified_rect, buf); 87 + self.id.render(id_rect, buf); 88 + } 89 + }
+5
src/tui/signal.rs
··· 17 17 ClearScreen, 18 18 Error(String), 19 19 Help, 20 + MoveDown, 21 + MoveUp, 22 + 20 23 /// this is fucking temporary 21 24 Helix, 22 25 } ··· 30 33 "resume" => Self::Resume, 31 34 "quit" => Self::Quit, 32 35 "helix" => Self::Helix, 36 + "movedown" => Self::MoveDown, 37 + "moveup" => Self::MoveUp, 33 38 _ => { 34 39 return Err(eyre!(format!( 35 40 "Attempt to construct a non-user Signal from str: {s}"
+1 -1
src/types/frontmatter.rs
··· 10 10 11 11 use crate::types::{Link, Zettel}; 12 12 13 - const DATE_FMT_STR: &str = "%Y-%m-%d %I:%M:%S %p"; 13 + pub(super) const DATE_FMT_STR: &str = "%Y-%m-%d %I:%M:%S %p"; 14 14 15 15 #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Serialize, Deserialize)] 16 16 pub struct FrontMatter {
+10 -2
src/types/kasten.rs
··· 2 2 use color_eyre::eyre::Result; 3 3 use eframe::emath; 4 4 use egui_graphs::{ 5 - Graph, 5 + Graph, Node, 6 6 petgraph::{Directed, graph::NodeIndex, prelude::StableGraph}, 7 7 }; 8 8 use rayon::iter::{ParallelBridge as _, ParallelIterator as _}; ··· 40 40 41 41 impl Kasten { 42 42 /// Indexes the `Workspace` and constructs a `Kasten` 43 - #[expect(dead_code)] 44 43 pub async fn index(ws: Workspace) -> Result<Self> { 45 44 let paths = std::fs::read_dir(&ws.root)? 46 45 .par_bridge() ··· 104 103 zid_to_gid, 105 104 most_recently_edited: None, 106 105 }) 106 + } 107 + pub fn get_by_zettel_id(&self, id: &ZettelId) -> Option<&Node<Zettel, Link>> { 108 + let idx = self.zid_to_gid.get(id)?; 109 + 110 + let node = self.graph.node(*idx).expect( 111 + "invariant broken if internal hashmap is not uptodate with 112 + the state of the graph...", 113 + ); 114 + Some(node) 107 115 } 108 116 }
+15 -2
src/types/zettel.rs
··· 15 15 use dto::NanoId; 16 16 use tokio::{fs::File, io::AsyncWriteExt}; 17 17 18 - use crate::types::{FrontMatter, Link, Tag, Workspace}; 18 + use crate::types::{FrontMatter, Link, Tag, Workspace, frontmatter}; 19 19 20 20 /// A `Zettel` is a note about a single idea. 21 21 /// It can have many `Tag`s, just meaning it can fall under many 22 22 /// categories. 23 - #[expect(dead_code)] 24 23 #[derive(Debug, Clone)] 25 24 pub struct Zettel { 26 25 /// Should only be constructed from models. ··· 30 29 /// a workspace-local file path, needs to be canonicalized before usage 31 30 pub file_path: PathBuf, 32 31 pub created_at: DateTime, 32 + pub modified_at: DateTime, 33 33 pub tags: Vec<Tag>, 34 34 } 35 35 ··· 111 111 let mut path = ws.root.clone(); 112 112 path.push(id.0.to_string()); 113 113 Self::from_path(path, ws).await 114 + } 115 + 116 + pub fn created_at(&self) -> String { 117 + self.created_at 118 + .format(frontmatter::DATE_FMT_STR) 119 + .to_string() 120 + } 121 + 122 + pub fn modified_at(&self) -> String { 123 + self.modified_at 124 + .format(frontmatter::DATE_FMT_STR) 125 + .to_string() 114 126 } 115 127 116 128 pub async fn from_path(path: impl Into<PathBuf>, ws: &Workspace) -> Result<Self> { ··· 273 285 title: value.title, 274 286 file_path: value.file_path.into(), 275 287 created_at: value.created_at, 288 + modified_at: value.modified_at, 276 289 tags: value.tags.into_iter().map(Into::into).collect(), 277 290 } 278 291 }