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: search functionality!

+156 -48
-1
.config/config.kdl
··· 4 4 keymap { 5 5 6 6 Home { 7 - q Quit // Quit the application 8 7 <Ctrl-c> Quit // Another way to quit 9 8 <Ctrl-z> Suspend // Suspend the application 10 9 up MoveUp
+11
Cargo.lock
··· 2303 2303 "futures", 2304 2304 "human-panic", 2305 2305 "kdl", 2306 + "nucleo-matcher", 2306 2307 "pulldown-cmark", 2307 2308 "rand 0.10.0", 2308 2309 "ratatui", ··· 4548 4549 checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" 4549 4550 dependencies = [ 4550 4551 "windows-sys 0.61.2", 4552 + ] 4553 + 4554 + [[package]] 4555 + name = "nucleo-matcher" 4556 + version = "0.3.1" 4557 + source = "registry+https://github.com/rust-lang/crates.io-index" 4558 + checksum = "bf33f538733d1a5a3494b836ba913207f14d9d4a1d3cd67030c5061bdd2cac85" 4559 + dependencies = [ 4560 + "memchr", 4561 + "unicode-segmentation", 4551 4562 ] 4552 4563 4553 4564 [[package]]
+1
Cargo.toml
··· 80 80 rand = "0.10.0" 81 81 pulldown-cmark = { version = "0.13.3", features = ["simd"] } 82 82 ratatui-textarea = "0.8.0" 83 + nucleo-matcher = "0.3.1" 83 84 84 85 [build-dependencies] 85 86 anyhow = "1.0.102"
+2 -12
src/tui/app.rs
··· 11 11 use crate::{ 12 12 config::Config, 13 13 tui::{Event, Tui, components::Zk}, 14 - types::{KastenHandle, Zettel}, 14 + types::KastenHandle, 15 15 }; 16 16 17 17 use super::{components::Component, signal::Signal}; ··· 124 124 } 125 125 126 126 for component in &mut self.components { 127 - if let Some(signal) = component.handle_events(Some(event.clone()))? { 127 + if let Some(signal) = component.handle_events(Some(event.clone())).await? { 128 128 signal_tx.send(signal)?; 129 129 } 130 130 } ··· 166 166 } 167 167 168 168 Signal::Quit => self.should_quit = true, 169 - 170 - Signal::NewZettel => { 171 - // what the fuck am i going to do in here 172 - 173 - let ws = &self.kh.read().await.ws; 174 - let z = Zettel::new("", ws).await?; 175 - let path = z.absolute_path(ws); 176 - 177 - self.signal_tx.send(Signal::Helix { path })?; 178 - } 179 169 180 170 Signal::Helix { path } => { 181 171 tui.exit()?;
+3 -3
src/tui/components/mod.rs
··· 71 71 /// # Returns 72 72 /// 73 73 /// * [`color_eyre::Result<Option<signal>>`] - A signal to be processed or none. 74 - fn handle_events(&mut self, event: Option<Event>) -> color_eyre::Result<Option<Signal>> { 74 + async fn handle_events(&mut self, event: Option<Event>) -> color_eyre::Result<Option<Signal>> { 75 75 let signal = match event { 76 - Some(Event::Key(key_event)) => self.handle_key_event(key_event)?, 76 + Some(Event::Key(key_event)) => self.handle_key_event(key_event).await?, 77 77 Some(Event::Mouse(mouse_event)) => self.handle_mouse_event(mouse_event)?, 78 78 _ => None, 79 79 }; ··· 88 88 /// # Returns 89 89 /// 90 90 /// * [`color_eyre::Result<Option<signal>>`] - A signal to be processed or none. 91 - fn handle_key_event(&mut self, key: KeyEvent) -> color_eyre::Result<Option<Signal>> { 91 + async fn handle_key_event(&mut self, key: KeyEvent) -> color_eyre::Result<Option<Signal>> { 92 92 let _ = key; // to appease clippy 93 93 Ok(None) 94 94 }
+45 -7
src/tui/components/zk/mod.rs
··· 4 4 use dto::{QueryOrder, TagEntity, ZettelColumns, ZettelEntity}; 5 5 use ratatui::{prelude::*, widgets::ListState}; 6 6 use tokio::sync::mpsc::UnboundedSender; 7 + use tracing::info; 7 8 8 9 use crate::{ 9 10 tui::{Signal, components::Component}, ··· 45 46 fn default() -> Self { 46 47 Self { 47 48 left_right: Layout::horizontal(vec![ 48 - Constraint::Percentage(50), 49 - Constraint::Percentage(50), 49 + Constraint::Fill(51), 50 + Constraint::Min(1), 51 + Constraint::Fill(50), 50 52 ]), 51 - search_zl: Layout::vertical(vec![Constraint::Min(6), Constraint::Fill(95)]), 53 + search_zl: Layout::vertical(vec![Constraint::Min(3), Constraint::Fill(95)]), 52 54 z_preview: Layout::vertical(vec![Constraint::Min(6), Constraint::Fill(95)]), 53 55 } 54 56 } ··· 101 103 .expect("must exist, handle case where it doesnt later...") 102 104 .payload() 103 105 .into(); 106 + 107 + let ws = kt.ws.clone(); 104 108 105 109 drop(kt); 106 110 ··· 111 115 zettel_list, 112 116 zettel_view, 113 117 preview, 114 - search: Search::default(), 118 + search: Search::new(ws), 115 119 }) 116 120 } 117 121 ··· 162 166 // for now we are going to just read that shit every time... 163 167 164 168 let zettels: Vec<Zettel> = models.into_iter().map(Into::into).collect(); 169 + 165 170 Ok(zettels) 166 171 } 172 + 173 + pub async fn update_with_respect_to_query(&mut self) -> Result<()> { 174 + let zettels = self 175 + .search 176 + .rank(self.get_zettels_by_current_query().await?) 177 + .await; 178 + 179 + self.zettel_list = ZettelList::new(zettels, self.zettel_list.state, self.zettel_list.width); 180 + info!("we are moving selection to first"); 181 + self.zettel_list.state.select_first(); 182 + self.update_views_from_zettel_list_selection().await?; 183 + 184 + Ok(()) 185 + } 167 186 } 168 187 169 188 #[async_trait] ··· 211 230 let Some(zid) = self.zettel_list.id_list.get(selcted) else { 212 231 return Ok(None); 213 232 }; 233 + self.search.clear_query(); 214 234 215 235 let kh = self.kh.read().await; 216 236 let path = kh ··· 227 247 return Ok(Some(Signal::Helix { path })); 228 248 } 229 249 250 + Signal::NewZettel => { 251 + // what the fuck am i going to do in here 252 + 253 + let ws = &self.kh.read().await.ws; 254 + 255 + // we create the zettel with the query as the 256 + let z = Zettel::new(self.search.query(), ws).await?; 257 + 258 + let path = z.absolute_path(ws); 259 + 260 + return Ok(Some(Signal::Helix { path })); 261 + } 262 + 230 263 Signal::ClosedZettel => { 231 264 let selected = self.zettel_list.state.selected().expect( 232 265 "still have to ··· 267 300 Ok(None) 268 301 } 269 302 270 - fn handle_key_event(&mut self, key: KeyEvent) -> color_eyre::Result<Option<Signal>> { 303 + async fn handle_key_event(&mut self, key: KeyEvent) -> color_eyre::Result<Option<Signal>> { 271 304 // ok so we get the text here too 272 - // self.search.title.input(key); 305 + // 306 + // need to filter like this to disable the history on the search 307 + if !(key.code.is_up() || key.code.is_down() || key.code.is_enter()) { 308 + self.search.query.input(key); 309 + self.update_with_respect_to_query().await?; 310 + } 273 311 274 312 Ok(None) 275 313 } ··· 282 320 let (search_layout, zettel_list_layout, zettel_layout, preview_layout) = { 283 321 let rects = self.layouts.left_right.split(area); 284 322 285 - let (left, right) = (rects[0], rects[1]); 323 + let (left, right) = (rects[0], rects[2]); 286 324 287 325 let l_rects = self.layouts.search_zl.split(left); 288 326
+93 -24
src/tui/components/zk/search.rs
··· 1 + use color_eyre::eyre::Error; 2 + use nucleo_matcher::{ 3 + Matcher, Utf32Str, 4 + pattern::{CaseMatching, Normalization, Pattern}, 5 + }; 1 6 use ratatui::{ 2 7 layout::{Constraint, Layout}, 3 8 style::Style, ··· 5 10 }; 6 11 use ratatui_textarea::TextArea; 7 12 13 + use crate::types::{Workspace, Zettel}; 14 + 8 15 #[derive(Clone)] 9 16 pub struct Search<'text> { 10 - pub title: TextArea<'text>, 11 - pub tag: TextArea<'text>, 17 + pub query: TextArea<'text>, 12 18 layouts: Layouts, 19 + matcher: Matcher, 20 + ws: Workspace, 13 21 } 14 22 15 - impl Default for Search<'_> { 16 - fn default() -> Self { 17 - let mut title = TextArea::default(); 18 - 19 - title.set_style(Style::default()); 20 - title.set_block( 23 + impl Search<'_> { 24 + pub fn new(ws: Workspace) -> Self { 25 + let mut tag = TextArea::default(); 26 + tag.set_style(Style::default()); 27 + tag.set_block( 21 28 Block::new() 22 29 .border_type(BorderType::Plain) 23 30 .borders(Borders::all()) 24 - .title("Search Titles"), 31 + .title("Filter by Tag"), 25 32 ); 26 33 27 - let mut tag = TextArea::default(); 28 - tag.set_style(Style::default()); 29 - tag.set_block( 34 + Self { 35 + matcher: Matcher::default(), 36 + query: Self::new_query(), 37 + ws, 38 + layouts: Layouts::default(), 39 + } 40 + } 41 + 42 + fn new_query<'a>() -> TextArea<'a> { 43 + let mut query = TextArea::default(); 44 + query.set_style(Style::default()); 45 + query.set_block( 30 46 Block::new() 31 47 .border_type(BorderType::Plain) 32 48 .borders(Borders::all()) 33 - .title("Search Tags"), 49 + .title("Search"), 34 50 ); 35 51 36 - Self { 37 - title, 38 - tag, 39 - layouts: Layouts::default(), 52 + query.set_max_histories(0); 53 + query 54 + } 55 + 56 + /// Clears the query 57 + pub fn clear_query(&mut self) { 58 + self.query = Self::new_query(); 59 + } 60 + 61 + pub fn query(&self) -> &str { 62 + self.query.lines()[0].as_str() 63 + } 64 + 65 + /// Sorts the vector of `Zettels` by their relation to the 66 + /// search query. 67 + //TODO: this should really take in some sort of file cache 68 + // so we arent reloading it every single time... 69 + pub async fn rank(&mut self, zettels: Vec<Zettel>) -> Vec<Zettel> { 70 + // if no query, we dont do any ranking 71 + if self.query().is_empty() { 72 + return zettels; 40 73 } 74 + 75 + let read_tasks = zettels 76 + .into_iter() 77 + .map(|z| { 78 + let ws = self.ws.clone(); 79 + tokio::spawn(async move { 80 + let content = z.content(&ws).await?; 81 + let front_matter = z.front_matter(&ws).await?; 82 + Ok::<(Zettel, String), Error>((z, format!("{content}\n{front_matter}"))) 83 + }) 84 + }) 85 + .collect::<Vec<_>>(); 86 + 87 + // await all of them 88 + let documents = futures::future::join_all(read_tasks) 89 + .await 90 + .into_iter() 91 + .filter_map(|result| result.ok()?.ok()) 92 + .collect::<Vec<(Zettel, String)>>(); 93 + 94 + let pattern = Pattern::parse(self.query(), CaseMatching::Ignore, Normalization::Smart); 95 + 96 + let mut results: Vec<(Zettel, u32)> = documents 97 + .into_iter() 98 + .filter_map(|(z, doc)| { 99 + let mut buf = Vec::new(); 100 + let score = pattern 101 + .score(Utf32Str::new(doc.as_str(), &mut buf), &mut self.matcher) 102 + .unwrap_or_default(); 103 + 104 + if score > 0 { Some((z, score)) } else { None } 105 + }) 106 + .collect(); 107 + 108 + results.sort_by(|a, b| b.1.cmp(&a.1)); 109 + 110 + results.into_iter().map(|(i, _)| i).collect() 41 111 } 42 112 } 43 113 44 114 #[derive(Clone)] 45 115 struct Layouts { 46 - title_tag: Layout, 116 + title: Layout, 47 117 } 48 118 49 119 impl Default for Layouts { 50 120 fn default() -> Self { 51 121 Self { 52 - title_tag: Layout::vertical(vec![Constraint::Min(3), Constraint::Min(3)]), 122 + title: Layout::vertical(vec![Constraint::Min(3)]), 53 123 } 54 124 } 55 125 } ··· 59 129 where 60 130 Self: Sized, 61 131 { 62 - let (title_search_rect, tag_search_rect) = { 63 - let rects = self.layouts.title_tag.split(area); 132 + let title_search_rect = { 133 + let rects = self.layouts.title.split(area); 64 134 65 - (rects[0], rects[1]) 135 + rects[0] 66 136 }; 67 137 68 - self.title.render(title_search_rect, buf); 69 - self.tag.render(tag_search_rect, buf); 138 + self.query.render(title_search_rect, buf); 70 139 } 71 140 }
+1 -1
src/tui/components/zk/zettel_list.rs
··· 35 35 .add_modifier(Modifier::DIM) 36 36 }) 37 37 .collect(), 38 - date: Span::from(value.created_at()).style(Style::new().add_modifier(Modifier::DIM)), 38 + date: Span::from(value.modified_at()).style(Style::new().add_modifier(Modifier::DIM)), 39 39 width: 0, 40 40 } 41 41 }