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 #16 from suri-codes/todo

major leaps in todo

authored by

Surendra Jammishetti and committed by
GitHub
62059178 228a5065

+1369 -424
+30 -19
.config/config.ron
··· 1 1 ( 2 2 directory: "/Users/suri/dev/projects/filaments/ZettelKasten", 3 3 global_key_binds: { 4 - "ctrl-z": Suspend, 5 4 "down": MoveDown, 6 - "up": MoveUp, 7 5 "ctrl-c": Quit, 6 + "up": MoveUp, 7 + "ctrl-z": Suspend, 8 8 }, 9 9 zk: ( 10 10 keybinds: { 11 + "enter": OpenZettel, 11 12 "tab": SwitchTo( 12 13 page: Todo(Explorer), 13 14 ), 14 15 "ctrl-n": NewZettel, 15 - "enter": OpenZettel, 16 16 }, 17 17 ), 18 18 todo: ( 19 19 explorer: ( 20 20 keybinds: { 21 + "j": MoveDown, 22 + "1": SwitchTo( 23 + page: Todo(Explorer), 24 + ), 25 + "t": NewTask, 21 26 "2": SwitchTo( 22 27 page: Todo(Inspector), 23 28 ), 24 - "k": MoveUp, 25 - "1": SwitchTo( 26 - page: Todo(Explorer), 29 + "shift-g": NewGroup, 30 + "g": NewSubGroup, 31 + "enter": SwitchTo( 32 + page: Todo(Inspector), 27 33 ), 28 34 "tab": SwitchTo( 29 35 page: Zk, ··· 31 37 "3": SwitchTo( 32 38 page: Todo(TaskList), 33 39 ), 34 - "j": MoveDown, 40 + "k": MoveUp, 35 41 }, 36 42 ), 37 43 inspector: ( 38 44 keybinds: { 45 + "tab": SwitchTo( 46 + page: Zk, 47 + ), 48 + "p": EditPriority, 49 + "n": EditName, 39 50 "3": SwitchTo( 40 51 page: Todo(TaskList), 41 52 ), 42 - "1": SwitchTo( 43 - page: Todo(Explorer), 44 - ), 45 - "tab": SwitchTo( 46 - page: Zk, 47 - ), 48 53 "2": SwitchTo( 49 54 page: Todo(Inspector), 50 55 ), 56 + "1": SwitchTo( 57 + page: Todo(Explorer), 58 + ), 51 59 }, 52 60 ), 53 61 tasklist: ( 54 62 keybinds: { 55 - "1": SwitchTo( 56 - page: Todo(Explorer), 57 - ), 58 63 "tab": SwitchTo( 59 64 page: Zk, 60 65 ), 66 + "3": SwitchTo( 67 + page: Todo(TaskList), 68 + ), 61 69 "2": SwitchTo( 62 70 page: Todo(Inspector), 63 71 ), 64 - "3": SwitchTo( 65 - page: Todo(TaskList), 72 + "enter": SwitchTo( 73 + page: Todo(Inspector), 66 74 ), 67 - "j": MoveDown, 75 + "1": SwitchTo( 76 + page: Todo(Explorer), 77 + ), 68 78 "k": MoveUp, 79 + "j": MoveDown, 69 80 }, 70 81 ), 71 82 ),
+7
.config/default_config.ron
··· 22 22 "3": SwitchTo(page: Todo(TaskList)), 23 23 "j": MoveDown, 24 24 "k": MoveUp, 25 + "shift-g": NewGroup, 26 + "g": NewSubGroup, 27 + "t": NewTask, 28 + "enter": SwitchTo(page: Todo(Inspector)) 25 29 }, 26 30 ), 27 31 inspector: ( ··· 30 34 "1": SwitchTo(page: Todo(Explorer)), 31 35 "2": SwitchTo(page: Todo(Inspector)), 32 36 "3": SwitchTo(page: Todo(TaskList)), 37 + "n": EditName, 38 + "p": EditPriority 33 39 }, 34 40 ), 35 41 tasklist: ( ··· 40 46 "3": SwitchTo(page: Todo(TaskList)), 41 47 "j": MoveDown, 42 48 "k": MoveUp, 49 + "enter": SwitchTo(page: Todo(Inspector)) 43 50 }, 44 51 ), 45 52 ),
+2 -3
crates/dto/src/entity/tag.rs
··· 40 40 self.color = Set(Color::default()); 41 41 } 42 42 43 - if let Set(ref mut name) = self.name 44 - && insert 45 - { 43 + if let Set(ref mut name) = self.name { 46 44 *name = name.replace(' ', "_"); 47 45 } 46 + 48 47 Box::pin(ready(Ok(self))) 49 48 } 50 49 }
+1 -1
crates/dto/src/entity/task.rs
··· 2 2 3 3 use migration::prelude::Local; 4 4 use migration::types::*; 5 - use sea_orm::entity::prelude::*; 6 5 use sea_orm::ActiveValue::Set; 6 + use sea_orm::entity::prelude::*; 7 7 use std::future::ready; 8 8 use std::pin::Pin; 9 9
+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::{prelude::Local, types::*}; 4 - use sea_orm::entity::prelude::*; 5 4 use sea_orm::ActiveValue::Set; 5 + use sea_orm::entity::prelude::*; 6 6 use std::{future::ready, pin::Pin}; 7 7 8 8 #[sea_orm::model]
+6 -6
crates/tree/src/iterators.rs
··· 14 14 15 15 impl<'a, T> Children<'a, T> { 16 16 // we actually want to 17 - #[allow(clippy::use_self)] 17 + #[expect(clippy::use_self)] 18 18 pub(crate) fn new(tree: &'a Tree<T>, node_id: &NodeId) -> Children<'a, T> { 19 19 Children { 20 20 tree, ··· 60 60 } 61 61 62 62 impl<'a> ChildrenIds<'a> { 63 - #[allow(clippy::use_self)] 63 + #[expect(clippy::use_self)] 64 64 pub(crate) fn new<T>(tree: &'a Tree<T>, node_id: &NodeId) -> ChildrenIds<'a> { 65 65 ChildrenIds { 66 66 child_ids: tree ··· 95 95 } 96 96 97 97 impl<'a, T> Ancestors<'a, T> { 98 - #[allow(clippy::use_self)] 98 + #[expect(clippy::use_self)] 99 99 pub(crate) const fn new(tree: &'a Tree<T>, node_id: NodeId) -> Ancestors<'a, T> { 100 100 Ancestors { 101 101 tree, ··· 137 137 } 138 138 139 139 impl<'a, T> AncestorsIds<'a, T> { 140 - #[allow(clippy::use_self)] 140 + #[expect(clippy::use_self)] 141 141 pub(crate) const fn new(tree: &'a Tree<T>, node_id: NodeId) -> AncestorsIds<'a, T> { 142 142 AncestorsIds { 143 143 tree, ··· 179 179 } 180 180 181 181 impl<'a, T> PreOrderTraversal<'a, T> { 182 - #[allow(clippy::use_self)] 182 + #[expect(clippy::use_self)] 183 183 pub(crate) fn new(tree: &'a Tree<T>, node_id: NodeId) -> PreOrderTraversal<'a, T> { 184 184 let mut data = VecDeque::with_capacity(tree.capacity()); 185 185 data.push_front(node_id); ··· 223 223 } 224 224 225 225 impl<'a, T> PreOrderTraversalIds<'a, T> { 226 - #[allow(clippy::use_self)] 226 + #[expect(clippy::use_self)] 227 227 pub(crate) fn new(tree: &'a Tree<T>, node_id: NodeId) -> PreOrderTraversalIds<'a, T> { 228 228 // Over allocating, but all at once instead of resizing and reallocating as we go. 229 229 let mut data = VecDeque::with_capacity(tree.capacity());
+1 -1
crates/tree/src/lib.rs
··· 39 39 40 40 impl NodeId { 41 41 // This is okay since we are practically never reaching 2^32. 42 - #[allow(clippy::cast_possible_truncation)] 42 + #[expect(clippy::cast_possible_truncation)] 43 43 pub(crate) const fn new(index: usize) -> Self { 44 44 Self { 45 45 index: index as u32,
+11 -1
crates/tree/src/node.rs
··· 1 + use std::cmp::Ordering; 2 + 1 3 use serde::{Deserialize, Serialize}; 2 4 3 5 use crate::NodeId; ··· 29 31 /// let _one: Node<i32> = Node::new(1); 30 32 /// ``` 31 33 /// 32 - #[allow(clippy::use_self)] 34 + #[expect(clippy::use_self)] 33 35 pub const fn new(data: T) -> Node<T> { 34 36 Self { 35 37 parent: None, ··· 108 110 /// ``` 109 111 pub const fn children(&self) -> &Vec<NodeId> { 110 112 &self.children 113 + } 114 + 115 + /// Are able to sort the children of this `Node` 116 + pub fn sort_children_by<F>(&mut self, mut compare: F) 117 + where 118 + F: FnMut(&NodeId, &NodeId) -> Ordering, 119 + { 120 + self.children.sort_by(|a, b| compare(a, b)); 111 121 } 112 122 113 123 pub(crate) const fn children_mut(&mut self) -> &mut Vec<NodeId> {
+11 -11
crates/tree/src/tree.rs
··· 30 30 /// let _tree_builder: TreeBuilder<i32> = TreeBuilder::new(); 31 31 /// 32 32 /// ``` 33 - #[allow(clippy::use_self)] 33 + #[expect(clippy::use_self)] 34 34 #[must_use] 35 35 pub const fn new() -> TreeBuilder<T> { 36 36 TreeBuilder { ··· 49 49 /// let _tree_builder: TreeBuilder<i32> = TreeBuilder::new().with_root(Node::new(1)); 50 50 /// ``` 51 51 #[must_use] 52 - #[allow(clippy::use_self)] 52 + #[expect(clippy::use_self)] 53 53 pub fn with_root(mut self, root: Node<T>) -> TreeBuilder<T> { 54 54 self.root = Some(root); 55 55 self ··· 71 71 /// 72 72 /// ``` 73 73 #[must_use] 74 - #[allow(clippy::use_self)] 74 + #[expect(clippy::use_self)] 75 75 pub const fn with_node_capacity(mut self, node_capacity: usize) -> TreeBuilder<T> { 76 76 self.node_capacity = node_capacity; 77 77 self ··· 82 82 /// `Tree`'s attempt to save time by reusing storage space 83 83 /// when `Node`'s are removed (instead of shuffling `Node`'s around internally). 84 84 /// To do this, the `Tree` must store information about the space left behind when a `Node` 85 - /// is removed. Using this setting allows the `Tree` to pre-allocate this storage 85 + /// is removed. Using this setting expects the `Tree` to pre-allocate this storage 86 86 /// space instead of doing so as `Node`'s are removed from the `Tree`. 87 87 /// 88 88 /// _Use of this setting is recommended if you know the **maximum "net number ··· 110 110 /// 111 111 /// ``` 112 112 #[must_use] 113 - #[allow(clippy::use_self)] 113 + #[expect(clippy::use_self)] 114 114 pub const fn with_swap_capacity(mut self, swap_capacity: usize) -> TreeBuilder<T> { 115 115 self.swap_capacity = swap_capacity; 116 116 self ··· 217 217 /// let _tree: Tree<i32> = Tree::new(); 218 218 /// ``` 219 219 #[must_use] 220 - #[allow(clippy::use_self)] 220 + #[expect(clippy::use_self)] 221 221 pub fn new() -> Tree<T> { 222 222 TreeBuilder::new().build() 223 223 } ··· 380 380 /// 381 381 /// tree.insert(child_node, UnderNode(&root_id)).unwrap(); 382 382 /// ``` 383 - #[allow(clippy::needless_pass_by_value)] 383 + #[expect(clippy::needless_pass_by_value)] 384 384 pub fn insert( 385 385 &mut self, 386 386 node: Node<T>, ··· 425 425 /// # assert_eq!(child.children().len(), 0); 426 426 /// # assert_eq!(child.parent(), None); 427 427 /// ``` 428 - #[allow(clippy::needless_pass_by_value)] 428 + #[expect(clippy::needless_pass_by_value)] 429 429 pub fn remove_node( 430 430 &mut self, 431 431 node_id: NodeId, ··· 494 494 /// Can panic if the `NodeId` does not exist in the `Tree`, but this would 495 495 /// be a bug in `tree` 496 496 /// 497 - #[allow(clippy::needless_pass_by_value)] 497 + #[expect(clippy::needless_pass_by_value)] 498 498 pub fn move_node( 499 499 &mut self, 500 500 node_id: &NodeId, ··· 985 985 } 986 986 987 987 // We want to have the node_id be consumed by this remove function. 988 - #[allow(clippy::needless_pass_by_value)] 988 + #[expect(clippy::needless_pass_by_value)] 989 989 fn remove_node_internal(&mut self, node_id: NodeId) -> Node<T> { 990 990 if let Some(root_id) = &self.root 991 991 && node_id == *root_id ··· 1213 1213 } 1214 1214 1215 1215 #[cfg(test)] 1216 - #[allow(clippy::similar_names)] 1216 + #[expect(clippy::similar_names)] 1217 1217 mod tree_tests { 1218 1218 use crate::InsertBehavior; 1219 1219 use crate::MoveBehavior;
+14 -14
flake.lock
··· 2 2 "nodes": { 3 3 "crane": { 4 4 "locked": { 5 - "lastModified": 1775839657, 6 - "narHash": "sha256-SPm9ck7jh3Un9nwPuMGbRU04UroFmOHjLP56T10MOeM=", 5 + "lastModified": 1776533550, 6 + "narHash": "sha256-8mTHsQ8cB0jGlXE4WWKqpQFQPM/VotDnr2uzfrOGNKI=", 7 7 "owner": "ipetkov", 8 8 "repo": "crane", 9 - "rev": "7cf72d978629469c4bd4206b95c402514c1f6000", 9 + "rev": "e24d86e91348e3d44014974fa24c9a22cfd663b5", 10 10 "type": "github" 11 11 }, 12 12 "original": { ··· 23 23 "rust-analyzer-src": "rust-analyzer-src" 24 24 }, 25 25 "locked": { 26 - "lastModified": 1776025235, 27 - "narHash": "sha256-zZIrW6G6GiFkpfFrOVjxYAVcXoKBMHv06kaUTOiqOq0=", 26 + "lastModified": 1776497206, 27 + "narHash": "sha256-Em+RSdFnwyyKPGUBFtQYtVjm+1UvIc9gOR91Y22zlzg=", 28 28 "owner": "nix-community", 29 29 "repo": "fenix", 30 - "rev": "dfe3f197bf8f3e95d705b7323b46e3a087c88972", 30 + "rev": "df2295365fb081fe0745449762a771290782c22d", 31 31 "type": "github" 32 32 }, 33 33 "original": { ··· 38 38 }, 39 39 "nixpkgs": { 40 40 "locked": { 41 - "lastModified": 1775710090, 42 - "narHash": "sha256-ar3rofg+awPB8QXDaFJhJ2jJhu+KqN/PRCXeyuXR76E=", 43 - "rev": "4c1018dae018162ec878d42fec712642d214fdfa", 44 - "revCount": 977467, 41 + "lastModified": 1776169885, 42 + "narHash": "sha256-l/iNYDZ4bGOAFQY2q8y5OAfBBtrDAaPuRQqWaFHVRXM=", 43 + "rev": "4bd9165a9165d7b5e33ae57f3eecbcb28fb231c9", 44 + "revCount": 980183, 45 45 "type": "tarball", 46 - "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.977467%2Brev-4c1018dae018162ec878d42fec712642d214fdfa/019d75d9-1186-76c1-baa5-93baafa37a8d/source.tar.gz" 46 + "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.980183%2Brev-4bd9165a9165d7b5e33ae57f3eecbcb28fb231c9/019d927d-72eb-7ff0-8888-fde211b3ffbd/source.tar.gz" 47 47 }, 48 48 "original": { 49 49 "type": "tarball", ··· 60 60 "rust-analyzer-src": { 61 61 "flake": false, 62 62 "locked": { 63 - "lastModified": 1775939146, 64 - "narHash": "sha256-YI8Hkuc2PSEJNwC8Qj3/DlMt1JfRv0MqzrnH/Mh9i5s=", 63 + "lastModified": 1776441750, 64 + "narHash": "sha256-1rVfG+mj8R4ze+lSYCa4iAv7FzrB03Cprtxmd1MfZak=", 65 65 "owner": "rust-lang", 66 66 "repo": "rust-analyzer", 67 - "rev": "09dc7aca674f53032f9d819f6fea01046e3d473f", 67 + "rev": "251df518d73abb5c5d573c4d5d266a3edae9ca5a", 68 68 "type": "github" 69 69 }, 70 70 "original": {
+5 -1
justfile
··· 19 19 # Run all tests 20 20 test: 21 21 cargo nextest r {{_cargo_flags}} 22 - 22 + reset: 23 + rm -rf ZettleKasten 24 + rm -rf ./.data 25 + cargo run -- init 26 + cargo run 23 27 24 28 # Only used to build / generate entities 25 29 dev-db := justfile_directory() + "/target/dev.db"
+6 -109
src/cli/process.rs
··· 4 4 io::Write, 5 5 }; 6 6 7 - use color_eyre::eyre::{Context, Result, eyre}; 8 - use dto::{ 9 - Date, DateTime, GroupActiveModel, GroupEntity, HasOne, IntoActiveModel, TagActiveModel, 10 - TagEntity, TaskActiveModel, TaskEntity, Time, ZettelEntity, 11 - }; 7 + use color_eyre::eyre::{Context, Result}; 12 8 use tower_lsp::{LspService, Server}; 13 9 14 10 use crate::{ 15 11 cli::{Commands, ZettelSubcommand}, 16 12 config::{Config, get_config_dir}, 17 13 lsp::Backend, 18 - types::{Group, Kasten, Priority, Tag, Task, Zettel}, 14 + types::{Group, Kasten, Priority, Task, Zettel}, 19 15 }; 20 16 21 17 impl Commands { 22 - #[expect(clippy::too_many_lines)] 23 18 pub async fn process(self) -> Result<()> { 24 19 match self { 25 20 Self::Init { name } => { ··· 82 77 match command { 83 78 super::TodoSubcommand::Group { name, parent_id } => { 84 79 // lets create a tag for this first group first 85 - let tag: Tag = TagActiveModel::builder() 86 - .set_name(name.clone()) 87 - .insert(&kt.db) 88 - .await? 89 - .into(); 90 - 91 - let tag_id = tag.id.clone(); 92 - 93 - // then create the zettel for the group 94 - let zettel = Zettel::new(name.clone(), &mut kt, vec![tag]).await?; 95 - 96 - // then insert that shi 97 - let inserted = GroupActiveModel::builder() 98 - .set_name(name) 99 - .set_parent_group_id(parent_id) 100 - .set_tag( 101 - TagEntity::load() 102 - .filter_by_nano_id(tag_id) 103 - .one(&kt.db) 104 - .await? 105 - .expect("Tag must exist since we just created it") 106 - .into_active_model(), 107 - ) 108 - .set_zettel( 109 - ZettelEntity::load() 110 - .filter_by_nano_id(zettel.id) 111 - .one(&kt.db) 112 - .await? 113 - .expect("Zettel must exist since we just created it") 114 - .into_active_model(), 115 - ) 116 - .set_priority(Priority::default()) 117 - .insert(&kt.db) 118 - .await?; 119 - 120 - // group should also have the accompanying tag for it. 121 - let group: Group = GroupEntity::load() 122 - .with(TagEntity) 123 - .with((ZettelEntity, TagEntity)) 124 - .filter_by_nano_id(inserted.nano_id) 125 - .one(&kt.db) 126 - .await? 127 - .expect("We just inserted it") 128 - .into(); 129 - 80 + let group = Group::new(name, parent_id, &mut kt).await?; 130 81 println!("created group {group:#?}"); 131 82 } 132 83 super::TodoSubcommand::Task { name, parent_id } => { 133 - // need to create the task 134 - let parent = GroupEntity::load() 135 - .with(TagEntity) 136 - .filter_by_nano_id(parent_id) 137 - .one(&kt.db) 138 - .await 139 - .with_context(|| "failed to communicate with db")? 140 - .ok_or_else(|| eyre!("could not find the group"))?; 141 - 142 - let HasOne::Loaded(tag) = parent.tag else { 143 - panic!("this has to be loaded since we just loaded it right above") 144 - }; 145 - 146 - let zettel = 147 - Zettel::new(name.clone(), &mut kt, vec![(*tag).into()]).await?; 148 - 149 - let inserted = TaskActiveModel::builder() 150 - .set_name(name) 151 - .set_group_id(parent.nano_id.clone()) 152 - .set_priority(Priority::default()) 153 - .set_zettel( 154 - ZettelEntity::load() 155 - .filter_by_nano_id(zettel.id) 156 - .one(&kt.db) 157 - .await? 158 - .expect("Zettel must exist since we just created it") 159 - .into_active_model(), 160 - ) 161 - .set_due(Some(DateTime::new( 162 - Date::from_ymd_opt(2026, 1, 31).unwrap(), 163 - Time::from_hms_opt(10, 10, 10).unwrap(), 164 - ))) 165 - .insert(&kt.db) 166 - .await?; 167 - 168 - let group = GroupEntity::load() 169 - .with(TagEntity) 170 - .with((ZettelEntity, TagEntity)) 171 - .filter_by_nano_id(parent.nano_id) 172 - .one(&kt.db) 173 - .await? 174 - .expect("We just inserted it"); 175 - 176 - let mut task = TaskEntity::load() 177 - .with((ZettelEntity, TagEntity)) 178 - .filter_by_nano_id(inserted.nano_id) 179 - .one(&kt.db) 180 - .await? 181 - .expect("We just inserted it"); 182 - 183 - task.group = HasOne::Loaded(Box::new(group)); 184 - 185 - println!("task: {task:#?}"); 186 - 187 - let task: Task = task.into(); 188 - 189 - println!("created task: {task:#?}"); 84 + let task = 85 + Task::new(name, parent_id, &mut kt, None, Priority::default()).await?; 86 + println!("created task {task:#?}"); 190 87 } 191 88 } 192 89 }
+6 -2
src/logging.rs
··· 1 1 use std::{ 2 - fs::{File, create_dir_all}, 2 + fs::{OpenOptions, create_dir_all}, 3 3 sync::LazyLock, 4 4 }; 5 5 ··· 29 29 create_dir_all(&directory)?; 30 30 31 31 let log_path = directory.join(LOG_FILE.clone()); 32 - let log_file = File::create(log_path)?; 32 + 33 + let log_file = OpenOptions::new() 34 + .append(true) 35 + .create(true) 36 + .open(log_path)?; 33 37 34 38 let env_filter = EnvFilter::builder().with_default_directive(Level::INFO.into()); 35 39
+1 -1
src/main.rs
··· 64 64 move || -> color_eyre::Result<()> { 65 65 // block the tui on the same runtime as above 66 66 tui_rt.block_on(async { 67 - let mut tui = TuiApp::new(args.tick_rate, args.frame_rate, kh, signal_tx).await?; 67 + let mut tui = TuiApp::new(args.tick_rate, args.frame_rate, kh, signal_tx)?; 68 68 tui.run().await?; 69 69 // just close everything as soon as the tui is done running 70 70 process::exit(0);
+19 -9
src/tui/app.rs
··· 25 25 components: Vec<Box<dyn Component>>, 26 26 should_quit: bool, 27 27 should_suspend: bool, 28 - #[allow(dead_code)] 29 28 page: Page, 30 29 last_tick_key_events: Vec<KeyEvent>, 31 30 kh: KastenHandle, 32 31 signal_tx: UnboundedSender<Signal>, 33 32 signal_rx: UnboundedReceiver<Signal>, 34 33 viz_signal_tx: UnboundedSender<Signal>, 34 + raw_text: bool, 35 35 } 36 36 37 37 /// The different regions of the application that the user can 38 38 /// be interacting with. Think of these kind of like the highest class of 39 39 /// components. 40 - #[derive( 41 - Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, EnumIter, Display, 42 - )] 40 + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, EnumIter, Display)] 43 41 pub enum Page { 44 - #[default] 45 42 Zk, 46 43 Todo(TodoRegion), 47 44 } 48 45 46 + impl Default for Page { 47 + fn default() -> Self { 48 + Self::Todo(TodoRegion::Explorer) 49 + } 50 + } 51 + 49 52 impl App { 50 53 /// Construct a new `App` instance. 51 - pub async fn new( 54 + pub fn new( 52 55 tick_rate: f64, 53 56 frame_rate: f64, 54 57 kh: KastenHandle, ··· 60 63 tick_rate, 61 64 frame_rate, 62 65 // components: vec![Box::new(Zk::new(kh.clone()).await?)], 63 - components: vec![Box::new(Viewport::new(kh.clone()).await?)], 66 + components: vec![Box::new(Viewport::new(kh.clone()))], 64 67 should_quit: false, 65 68 should_suspend: false, 66 69 config: Config::parse()?, ··· 70 73 signal_tx, 71 74 signal_rx, 72 75 viz_signal_tx, 76 + raw_text: false, 73 77 }) 74 78 } 75 79 ··· 148 152 fn handle_key_event(&mut self, key: KeyEvent) -> Result<()> { 149 153 debug!("key received: {key:#?}"); 150 154 155 + if self.raw_text { 156 + debug!("Raw text enabled, refusing to interpret as Signal"); 157 + return Ok(()); 158 + } 159 + 151 160 let signal_tx = self.signal_tx.clone(); 152 161 153 162 let Some(page_keymap) = self.config.keymap.get(&self.page) else { 154 163 return Ok(()); 155 164 }; 156 - 157 - info!("page: {:#?}, page_keymap: {page_keymap:#?}", self.page); 158 165 159 166 if let Some(signal) = page_keymap.get(&vec![key]) { 160 167 signal_tx.send(signal.clone())?; ··· 243 250 Signal::ClearScreen => tui.terminal.clear()?, 244 251 Signal::Resize(x, y) => self.handle_resize(tui, x, y)?, 245 252 Signal::Render => self.render(tui)?, 253 + 254 + Signal::EnterRawText => self.raw_text = true, 255 + Signal::ExitRawText => self.raw_text = false, 246 256 _ => {} 247 257 } 248 258
+35 -6
src/tui/components/todo/explorer.rs
··· 3 3 text::{Line, Span, Text}, 4 4 widgets::{Block, BorderType, Borders, List, ListState}, 5 5 }; 6 - use tracing::info; 6 + use tracing::debug; 7 7 use tree::NodeId; 8 8 9 - use crate::types::{TodoNode, TodoNodeKind, TodoTree}; 9 + use crate::types::{Group, TodoNode, TodoNodeKind, TodoTree}; 10 10 11 + #[derive(Debug)] 11 12 pub struct Explorer<'text> { 12 13 pub render_list: ratatui::widgets::List<'text>, 13 - #[allow(dead_code)] 14 14 pub id_list: Vec<NodeId>, 15 15 pub state: ListState, 16 - #[allow(dead_code)] 16 + #[expect(dead_code)] 17 17 pub width: u16, 18 18 } 19 19 ··· 58 58 } 59 59 60 60 pub fn set_active(&mut self) { 61 + debug!("Explorer set active!"); 61 62 self.render_list = self.render_list.clone().block( 62 63 Block::new() 63 64 .title("[1]") ··· 78 79 .border_type(BorderType::Rounded), 79 80 ); 80 81 } 82 + 83 + /// Returns the parent `Group` of the current selection in the `Explorer` 84 + pub fn group_of_current_selection<'tree>(&self, tree: &'tree TodoTree) -> Option<&'tree Group> { 85 + let selected = self.id_list.get(self.state.selected()?)?; 86 + 87 + if let TodoNodeKind::Group(group) = &tree 88 + .tree 89 + .get(selected) 90 + .expect("Invaraint Broken! This must be a valid id") 91 + .data() 92 + .kind 93 + { 94 + return Some(group); 95 + } 96 + 97 + let mut ancestors = tree.tree.ancestors(selected).expect("Must be a valid id"); 98 + 99 + ancestors 100 + .next() 101 + .and_then(|parent| match &parent.data().kind { 102 + TodoNodeKind::Root => None, 103 + 104 + TodoNodeKind::Task(_) => { 105 + panic!("Invariant broken! how is a task a parent?!") 106 + } 107 + 108 + TodoNodeKind::Group(group) => Some(group), 109 + }) 110 + .map(|g| &**g) 111 + } 81 112 } 82 113 83 114 pub struct ExplorerListItem<'text> { ··· 123 154 Style::default().bg(color), 124 155 )); 125 156 } 126 - 127 - info!("{spans:#?}"); 128 157 129 158 Line::from(spans).into() 130 159 }
+24 -10
src/tui/components/todo/inspector/groupview.rs
··· 1 1 use ratatui::{ 2 2 layout::{Constraint, Layout}, 3 - widgets::{Paragraph, Widget}, 3 + style::Style, 4 + widgets::{Block, Paragraph, Widget}, 4 5 }; 6 + use ratatui_textarea::TextArea; 5 7 6 8 use crate::types::Group; 7 9 8 10 #[derive(Debug, Clone)] 9 11 pub struct GroupView<'text> { 10 - name: Paragraph<'text>, 11 - priority: Paragraph<'text>, 12 + pub name: TextArea<'text>, 13 + pub priority: TextArea<'text>, 12 14 created_at: Paragraph<'text>, 13 15 layouts: Layouts, 14 16 } ··· 23 25 fn default() -> Self { 24 26 Self { 25 27 left_content: Layout::horizontal(vec![ 26 - Constraint::Percentage(50), 28 + Constraint::Percentage(30), 27 29 Constraint::Fill(100), 28 30 ]), 29 31 name_priority_created_at: Layout::vertical(vec![ 30 - Constraint::Percentage(33), 31 - Constraint::Percentage(33), 32 - Constraint::Percentage(33), 32 + Constraint::Min(3), 33 + Constraint::Min(3), 34 + Constraint::Min(3), 35 + Constraint::Min(3), 33 36 ]), 34 37 } 35 38 } ··· 37 40 38 41 impl From<&Group> for GroupView<'_> { 39 42 fn from(value: &Group) -> Self { 43 + let mut name = TextArea::new(vec![value.name.clone()]); 44 + name.set_block(Block::bordered().title("[N]ame")); 45 + name.set_cursor_style(Style::reset()); 46 + name.set_cursor_line_style(Style::reset()); 47 + 48 + let mut priority = TextArea::new(vec![value.priority.to_string()]); 49 + priority.set_block(Block::bordered().title("[P]riority")); 50 + priority.set_cursor_style(Style::reset()); 51 + priority.set_cursor_line_style(Style::reset()); 52 + 40 53 Self { 41 - name: Paragraph::new(value.name.clone()), 42 - priority: Paragraph::new(value.priority.to_string()), 43 - created_at: Paragraph::new(value.created_at()), 54 + name, 55 + priority, 56 + created_at: Paragraph::new(value.created_at()) 57 + .block(Block::bordered().title("Created At")), 44 58 layouts: Layouts::default(), 45 59 } 46 60 }
+247 -48
src/tui/components/todo/inspector/mod.rs
··· 1 + use async_trait::async_trait; 2 + use crossterm::event::{KeyCode, KeyEvent}; 3 + use dto::NanoId; 1 4 use ratatui::{ 2 - layout::{Constraint, Direction, Layout}, 5 + Frame, 6 + layout::{Constraint, Direction, Layout, Rect}, 3 7 style::{Color, Style}, 4 - widgets::{Block, BorderType, Borders, Widget}, 8 + widgets::{Block, BorderType, Borders}, 5 9 }; 10 + use ratatui_textarea::CursorMove; 11 + use tokio::sync::mpsc::UnboundedSender; 6 12 7 - use crate::types::{TodoNode, TodoNodeKind}; 13 + use crate::{ 14 + tui::{ 15 + Signal, 16 + components::{Component, DEFAULT_NAME}, 17 + }, 18 + types::{Group, KastenHandle, Task, TodoNode, TodoNodeKind}, 19 + }; 8 20 9 21 mod rootview; 10 22 use rootview::RootView; ··· 16 28 use groupview::GroupView; 17 29 18 30 pub struct Inspector<'text> { 19 - render_data: RenderData<'text>, 31 + pub render_data: RenderData<'text>, 20 32 margins: Layout, 21 33 block: Block<'text>, 34 + kh: KastenHandle, 35 + signal_tx: Option<UnboundedSender<Signal>>, 36 + is_active: bool, 37 + editing: Option<Edit>, 38 + // this is the `NanoId` of the thing we are actually inspecting 39 + inspecting: Option<NanoId>, 40 + } 41 + 42 + enum Edit { 43 + Name, 44 + Priority, 22 45 } 23 46 24 47 impl Inspector<'_> { 48 + pub fn new(kh: KastenHandle, node: &TodoNode) -> Self { 49 + let margins = Layout::new(Direction::Horizontal, [Constraint::Percentage(100)]) 50 + .horizontal_margin(3) 51 + .vertical_margin(2); 52 + 53 + let block = Block::new() 54 + .title("[2]") 55 + .title("Inspector") 56 + .borders(Borders::LEFT | Borders::TOP | Borders::BOTTOM) 57 + .border_style(Style::new().fg(Color::Gray)) 58 + .border_type(BorderType::Rounded); 59 + 60 + let mut nanoid = None; 61 + 62 + let render_data = match node.kind { 63 + TodoNodeKind::Root => RenderData::Root { 64 + widget: Box::new(RootView::default()), 65 + }, 66 + TodoNodeKind::Group(ref group) => { 67 + nanoid = Some(group.id.clone()); 68 + 69 + RenderData::Group { 70 + widget: Box::new(GroupView::from(&**group)), 71 + } 72 + } 73 + TodoNodeKind::Task(ref task) => { 74 + nanoid = Some(task.id.clone()); 75 + 76 + RenderData::Task { 77 + widget: Box::new(TaskView::from(&**task)), 78 + } 79 + } 80 + }; 81 + 82 + Self { 83 + render_data, 84 + margins, 85 + block, 86 + kh, 87 + is_active: false, 88 + editing: None, 89 + inspecting: nanoid, 90 + signal_tx: None, 91 + } 92 + } 93 + 25 94 pub fn set_active(&mut self) { 95 + self.is_active = true; 96 + 26 97 self.block = Block::new() 27 98 .title("[2]") 28 99 .title("Inspector") ··· 32 103 } 33 104 34 105 pub fn set_inactive(&mut self) { 106 + self.is_active = false; 35 107 self.block = Block::new() 36 108 .title("[2]") 37 109 .title("Inspector") ··· 39 111 .border_style(Style::new().fg(Color::Gray)) 40 112 .border_type(BorderType::Rounded); 41 113 } 114 + 115 + pub fn inspect(&mut self, node: &TodoNode) { 116 + self.render_data = match node.kind { 117 + TodoNodeKind::Root => { 118 + self.inspecting = None; 119 + RenderData::Root { 120 + widget: Box::new(RootView::default()), 121 + } 122 + } 123 + TodoNodeKind::Group(ref group) => { 124 + self.inspecting = Some(group.id.clone()); 125 + RenderData::Group { 126 + widget: Box::new(GroupView::from(&**group)), 127 + } 128 + } 129 + TodoNodeKind::Task(ref task) => { 130 + self.inspecting = Some(task.id.clone()); 131 + RenderData::Task { 132 + widget: Box::new(TaskView::from(&**task)), 133 + } 134 + } 135 + } 136 + } 42 137 } 43 138 44 - enum RenderData<'text> { 139 + pub enum RenderData<'text> { 45 140 Root { widget: Box<RootView<'text>> }, 46 141 Task { widget: Box<TaskView<'text>> }, 47 142 Group { widget: Box<GroupView<'text>> }, 48 143 } 49 144 50 - impl From<&TodoNode> for Inspector<'_> { 51 - fn from(value: &TodoNode) -> Self { 52 - let margins = Layout::new(Direction::Horizontal, [Constraint::Percentage(100)]) 53 - .horizontal_margin(3) 54 - .vertical_margin(2); 145 + #[async_trait] 146 + impl Component for Inspector<'_> { 147 + fn register_signal_handler(&mut self, tx: UnboundedSender<Signal>) -> color_eyre::Result<()> { 148 + self.signal_tx = Some(tx); 149 + Ok(()) 150 + } 151 + 152 + async fn update(&mut self, signal: Signal) -> color_eyre::Result<Option<Signal>> { 153 + match signal { 154 + Signal::EditName => { 155 + let name = match &mut self.render_data { 156 + RenderData::Root { widget: _ } => return Ok(None), 157 + RenderData::Task { widget } => &mut widget.name, 158 + RenderData::Group { widget } => &mut widget.name, 159 + }; 160 + 161 + name.set_block( 162 + name.block() 163 + .cloned() 164 + .expect("All of them should have blocks") 165 + .border_style(Style::default().fg(Color::Green)), 166 + ); 167 + 168 + if name.lines()[0].as_str().contains(DEFAULT_NAME) { 169 + name.delete_line_by_end(); 170 + } else { 171 + name.move_cursor(CursorMove::End); 172 + } 173 + 174 + name.set_cursor_style(Style::default().reversed()); 175 + name.set_cursor_line_style(Style::default().underlined()); 176 + 177 + self.editing = Some(Edit::Name); 178 + return Ok(Some(Signal::EnterRawText)); 179 + } 180 + Signal::EditPriority => { 181 + let priority = match &mut self.render_data { 182 + RenderData::Root { widget: _ } => return Ok(None), 183 + RenderData::Task { widget } => &mut widget.priority, 184 + RenderData::Group { widget } => &mut widget.priority, 185 + }; 186 + 187 + priority.set_block( 188 + priority 189 + .block() 190 + .cloned() 191 + .expect("All of them should have blocks") 192 + .border_style(Style::default().fg(Color::Green)), 193 + ); 194 + 195 + priority.set_cursor_style(Style::default().reversed()); 196 + priority.set_cursor_line_style(Style::default().underlined()); 197 + 198 + self.editing = Some(Edit::Priority); 199 + return Ok(Some(Signal::EnterRawText)); 200 + } 201 + 202 + _ => {} 203 + } 204 + Ok(None) 205 + } 206 + 207 + async fn handle_key_event(&mut self, key: KeyEvent) -> color_eyre::Result<Option<Signal>> { 208 + let signal_tx = self 209 + .signal_tx 210 + .as_mut() 211 + .expect("Invariant Broken, signal_tx must be initialized"); 212 + 213 + match self.editing { 214 + Some(Edit::Name) => { 215 + let name = match &mut self.render_data { 216 + RenderData::Root { widget: _ } => return Ok(None), 217 + RenderData::Task { widget } => &mut widget.name, 218 + RenderData::Group { widget } => &mut widget.name, 219 + }; 220 + 221 + if key.code == KeyCode::Enter { 222 + name.set_cursor_style(Style::reset()); 223 + name.set_cursor_line_style(Style::reset()); 224 + name.set_block( 225 + name.block() 226 + .cloned() 227 + .expect("All of them should have blocks") 228 + .border_style(Style::default().fg(Color::Reset)), 229 + ); 230 + self.editing = None; 231 + signal_tx.send(Signal::ExitRawText)?; 232 + 233 + let new_name = name.lines()[0].clone(); 234 + let id = self 235 + .inspecting 236 + .clone() 237 + .expect("Invariant Broken, this must be some id"); 238 + 239 + let mut kt = self.kh.write().await; 240 + match &self.render_data { 241 + RenderData::Task { .. } => { 242 + Task::alter_name(id.clone(), new_name, &mut kt).await?; 243 + } 244 + RenderData::Group { .. } => { 245 + Group::alter_name(id.clone(), new_name, &mut kt).await?; 246 + } 247 + RenderData::Root { .. } => unreachable!("Already returned above"), 248 + } 249 + 250 + drop(kt); 251 + 252 + Ok(Some(Signal::Refresh)) 253 + } else { 254 + name.input_without_shortcuts(key); 255 + Ok(None) 256 + } 257 + } 258 + Some(Edit::Priority) => { 259 + let priority = match &mut self.render_data { 260 + RenderData::Root { widget: _ } => return Ok(None), 261 + RenderData::Task { widget } => &mut widget.priority, 262 + RenderData::Group { widget } => &mut widget.priority, 263 + }; 264 + 265 + if key.code == KeyCode::Enter { 266 + priority.set_cursor_style(Style::reset()); 267 + priority.set_cursor_line_style(Style::reset()); 268 + 269 + priority.set_block( 270 + priority 271 + .block() 272 + .cloned() 273 + .expect("All of them should have blocks") 274 + .border_style(Style::default().fg(Color::Reset)), 275 + ); 276 + 277 + self.editing = None; 278 + Ok(Some(Signal::ExitRawText)) 279 + } else { 280 + priority.input_without_shortcuts(key); 281 + Ok(None) 282 + } 283 + } 55 284 56 - let block = Block::new() 57 - .title("[2]") 58 - .title("Inspector") 59 - .borders(Borders::LEFT | Borders::TOP | Borders::BOTTOM) 60 - .border_style(Style::new().fg(Color::Gray)) 61 - .border_type(BorderType::Rounded); 62 - match value.kind { 63 - TodoNodeKind::Root => Self { 64 - render_data: RenderData::Root { 65 - widget: Box::new(RootView::default()), 66 - }, 67 - margins, 68 - block, 69 - }, 70 - TodoNodeKind::Group(ref group) => Self { 71 - render_data: RenderData::Group { 72 - widget: Box::new(GroupView::from(&**group)), 73 - }, 74 - margins, 75 - block, 76 - }, 77 - TodoNodeKind::Task(ref task) => Self { 78 - render_data: RenderData::Task { 79 - widget: Box::new(TaskView::from(&**task)), 80 - }, 81 - margins, 82 - block, 83 - }, 285 + None => return Ok(None), 84 286 } 85 287 } 86 - } 87 288 88 - impl Widget for &Inspector<'_> { 89 - fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) 90 - where 91 - Self: Sized, 92 - { 93 - self.block.clone().render(area, buf); 289 + fn draw(&mut self, frame: &mut Frame, area: Rect) -> color_eyre::Result<()> { 290 + frame.render_widget(self.block.clone(), area); 94 291 95 292 let area = self.margins.split(area)[0]; 96 293 97 294 match &self.render_data { 98 - RenderData::Root { widget } => widget.clone().render(area, buf), 99 - RenderData::Task { widget } => widget.clone().render(area, buf), 100 - RenderData::Group { widget } => widget.clone().render(area, buf), 295 + RenderData::Root { widget } => frame.render_widget(*widget.clone(), area), 296 + RenderData::Task { widget } => frame.render_widget(*widget.clone(), area), 297 + RenderData::Group { widget } => frame.render_widget(*widget.clone(), area), 101 298 } 299 + 300 + Ok(()) 102 301 } 103 302 }
+45 -24
src/tui/components/todo/inspector/taskview.rs
··· 1 1 use ratatui::{ 2 2 layout::{Constraint, Layout}, 3 - widgets::{Paragraph, Widget}, 3 + style::Style, 4 + widgets::{Block, Paragraph, Widget}, 4 5 }; 6 + use ratatui_textarea::TextArea; 5 7 6 8 use crate::types::Task; 7 9 8 10 #[derive(Debug, Clone)] 9 11 pub struct TaskView<'text> { 10 - name: Paragraph<'text>, 11 - priority: Paragraph<'text>, 12 - #[expect(dead_code)] 13 - created_at: Paragraph<'text>, 14 - #[expect(dead_code)] 12 + pub name: TextArea<'text>, 13 + pub priority: TextArea<'text>, 15 14 parent_group: Paragraph<'text>, 16 - 17 - due: Paragraph<'text>, 18 - 15 + due_finished_at: Paragraph<'text>, 19 16 layouts: Layouts, 20 17 } 21 18 22 19 #[derive(Debug, Clone)] 23 20 struct Layouts { 24 21 left_content: Layout, 25 - name_priority_due: Layout, 22 + name_priority_due_group: Layout, 26 23 } 27 24 28 25 impl Default for Layouts { 29 26 fn default() -> Self { 30 27 Self { 31 28 left_content: Layout::horizontal(vec![ 32 - Constraint::Percentage(50), 29 + Constraint::Percentage(30), 33 30 Constraint::Fill(100), 34 31 ]), 35 - name_priority_due: Layout::vertical(vec![ 36 - Constraint::Percentage(33), 37 - Constraint::Percentage(33), 38 - Constraint::Percentage(33), 32 + name_priority_due_group: Layout::vertical(vec![ 33 + Constraint::Min(3), 34 + Constraint::Min(3), 35 + Constraint::Min(3), 36 + Constraint::Min(3), 39 37 ]), 40 38 } 41 39 } ··· 43 41 44 42 impl From<&Task> for TaskView<'_> { 45 43 fn from(value: &Task) -> Self { 44 + let mut name = TextArea::new(vec![value.name.clone()]); 45 + name.set_block(Block::bordered().title("[N]ame")); 46 + name.set_cursor_style(Style::reset()); 47 + name.set_cursor_line_style(Style::reset()); 48 + 49 + let mut priority = TextArea::new(vec![value.priority.to_string()]); 50 + priority.set_block(Block::bordered().title("[P]riority")); 51 + priority.set_cursor_style(Style::reset()); 52 + priority.set_cursor_line_style(Style::reset()); 53 + 54 + let due_finished_at = { 55 + value.finished_at().map_or_else( 56 + || { 57 + value.due().map_or_else( 58 + || Paragraph::new("None").block(Block::bordered().title("Due")), 59 + |due| Paragraph::new(due).block(Block::bordered().title("Due")), 60 + ) 61 + }, 62 + |finished| Paragraph::new(finished).block(Block::bordered().title("Finished At")), 63 + ) 64 + }; 65 + 46 66 Self { 47 - name: Paragraph::new(value.name.clone()), 48 - priority: Paragraph::new(value.priority.to_string()), 49 - created_at: Paragraph::new(value.created_at()), 50 - parent_group: Paragraph::new(value.group.name.clone()), 51 - due: Paragraph::new(value.due().unwrap_or_else(|| "None".to_owned())), 67 + name, 68 + priority, 69 + due_finished_at, 70 + parent_group: Paragraph::new(value.group.name.clone()) 71 + .block(Block::bordered().title("Group")), 52 72 layouts: Layouts::default(), 53 73 } 54 74 } ··· 59 79 where 60 80 Self: Sized, 61 81 { 62 - let (name_rect, priority_rect, due_rect, _content_rect) = { 82 + let (name_rect, priority_rect, due_rect, group_rect, _content_rect) = { 63 83 let rects = self.layouts.left_content.split(area); 64 - let l_rects = self.layouts.name_priority_due.split(rects[0]); 84 + let l_rects = self.layouts.name_priority_due_group.split(rects[0]); 65 85 66 - (l_rects[0], l_rects[1], l_rects[2], rects[1]) 86 + (l_rects[0], l_rects[1], l_rects[2], l_rects[3], rects[1]) 67 87 }; 68 88 69 89 self.name.render(name_rect, buf); 70 90 self.priority.render(priority_rect, buf); 71 - self.due.render(due_rect, buf); 91 + self.due_finished_at.render(due_rect, buf); 92 + self.parent_group.render(group_rect, buf); 72 93 } 73 94 }
+204 -14
src/tui/components/todo/mod.rs
··· 1 1 use async_trait::async_trait; 2 + use color_eyre::eyre::Result; 3 + use crossterm::event::KeyEvent; 2 4 use ratatui::{ 3 5 Frame, 4 6 layout::{Constraint, Layout, Rect, Size}, ··· 7 9 use serde::{Deserialize, Serialize}; 8 10 use strum::{Display, EnumIter}; 9 11 use tokio::sync::mpsc::UnboundedSender; 12 + use tracing::debug; 10 13 11 14 use crate::{ 12 15 tui::{Page, Signal, components::Component}, 13 - types::KastenHandle, 16 + types::{Group, KastenHandle, Priority, Task, TodoTree}, 14 17 }; 15 18 16 19 mod explorer; 17 20 use explorer::Explorer; 18 - 19 21 mod tasklist; 20 22 use tasklist::TaskList; 21 23 ··· 23 25 use inspector::Inspector; 24 26 25 27 pub struct Todo<'text> { 26 - #[expect(dead_code)] 27 28 signal_tx: Option<UnboundedSender<Signal>>, 28 29 kh: KastenHandle, 29 30 layouts: Layouts, 31 + 30 32 explorer: Option<Explorer<'text>>, 31 33 task_list: Option<TaskList<'text>>, 32 34 inspector: Option<Inspector<'text>>, 33 35 36 + area: Size, 34 37 active: TodoRegion, 35 38 } 39 + 40 + pub const DEFAULT_NAME: &str = "Rename Me!"; 36 41 37 42 /// The different regions inside the `Todo` component 38 43 #[derive( ··· 54 59 explorer: None, 55 60 task_list: None, 56 61 inspector: None, 62 + area: Size::default(), 57 63 active: TodoRegion::default(), 58 64 } 59 65 } 60 66 67 + pub async fn refresh(&mut self) { 68 + let explorer = self 69 + .explorer 70 + .as_mut() 71 + .expect("This should have already been init.ialized"); 72 + let task_list = self 73 + .task_list 74 + .as_mut() 75 + .expect("This should have already been initialized"); 76 + 77 + let explorer_selection = explorer 78 + .state 79 + .selected() 80 + .and_then(|idx| explorer.id_list.get(idx)); 81 + let task_list_selection = task_list 82 + .state 83 + .selected() 84 + .and_then(|idx| task_list.id_list.get(idx)); 85 + 86 + let mut kt = self.kh.write().await; 87 + 88 + // fuck it we just fully rebuild the tree, how computationally expensive could it even be 89 + kt.todo_tree = TodoTree::construct(&kt.db).await.expect("Must not error"); 90 + 91 + let tree = &kt.todo_tree; 92 + 93 + debug!("tree after refresh {tree:#?}"); 94 + let splits = self 95 + .layouts 96 + .split(Rect::new(0, 0, self.area.width, self.area.height)); 97 + 98 + let l_state = ListState::default(); 99 + 100 + //TODO: instead of tree.root_id this probably should be scope. 101 + let mut explorer = Explorer::new(tree, &tree.root_id, l_state, splits.explorer.width); 102 + let mut task_list = TaskList::new(tree, &tree.root_id, l_state, splits.task_list.width); 103 + 104 + debug!("explorer constructed after refresh {explorer:#?}"); 105 + 106 + drop(kt); 107 + 108 + let explorer_selection_idx = 109 + explorer_selection.and_then(|id| explorer.id_list.iter().position(|e| id == e)); 110 + 111 + let task_list_selection_idx = 112 + task_list_selection.and_then(|id| task_list.id_list.iter().position(|e| id == e)); 113 + 114 + explorer.state.select(explorer_selection_idx); 115 + task_list.state.select(task_list_selection_idx); 116 + 117 + match self.active { 118 + TodoRegion::Inspector => { 119 + explorer.set_inactive(); 120 + task_list.set_inactive(); 121 + } 122 + TodoRegion::TaskList => { 123 + explorer.set_inactive(); 124 + task_list.set_active(); 125 + } 126 + TodoRegion::Explorer => { 127 + explorer.set_active(); 128 + task_list.set_inactive(); 129 + } 130 + } 131 + 132 + self.explorer = Some(explorer); 133 + self.task_list = Some(task_list); 134 + self.update_inspector_from_selection().await; 135 + } 136 + 61 137 async fn update_inspector_from_selection(&mut self) { 62 138 let explorer = self 63 139 .explorer ··· 71 147 .inspector 72 148 .as_mut() 73 149 .expect("This should have already been initialized"); 150 + 74 151 let selected_node_id = match self.active { 75 152 TodoRegion::TaskList => { 76 153 let Some(idx) = task_list.state.selected() else { ··· 92 169 }; 93 170 let tree = &self.kh.read().await.todo_tree.tree; 94 171 95 - *inspector = tree 96 - .get(selected_node_id) 97 - .expect("Nodeid must be valid") 98 - .data() 99 - .into(); 172 + inspector.inspect( 173 + tree.get(selected_node_id) 174 + .expect("Nodeid must be valid") 175 + .data(), 176 + ); 100 177 } 101 178 } 102 179 ··· 112 189 Constraint::Percentage(40), 113 190 Constraint::Fill(100), 114 191 ]), 115 - inspector_task_list: Layout::vertical(vec![ 116 - Constraint::Percentage(30), 117 - Constraint::Fill(100), 118 - ]), 192 + inspector_task_list: Layout::vertical(vec![Constraint::Min(16), Constraint::Fill(100)]), 119 193 } 120 194 } 121 195 } ··· 142 216 #[async_trait] 143 217 impl Component for Todo<'_> { 144 218 async fn init(&mut self, area: Size) -> color_eyre::Result<()> { 219 + self.area = area; 145 220 let tree = &self.kh.read().await.todo_tree; 146 221 let splits = self.layouts.split(Rect::new(0, 0, area.width, area.height)); 147 222 ··· 162 237 ) 163 238 .expect("Node id must be valid"); 164 239 165 - let mut inspector: Inspector<'_> = first.data().into(); 240 + let mut inspector: Inspector<'_> = Inspector::new(self.kh.clone(), first.data()); 166 241 167 242 explorer.set_inactive(); 168 243 inspector.set_inactive(); 169 244 task_list.set_inactive(); 170 245 246 + inspector.register_signal_handler( 247 + self.signal_tx 248 + .clone() 249 + .expect("Must be initialized by this point"), 250 + )?; 251 + 171 252 self.explorer = Some(explorer); 172 253 self.task_list = Some(task_list); 173 254 self.inspector = Some(inspector); ··· 175 256 Ok(()) 176 257 } 177 258 259 + fn register_signal_handler(&mut self, tx: UnboundedSender<Signal>) -> Result<()> { 260 + self.signal_tx = Some(tx); 261 + Ok(()) 262 + } 263 + 264 + #[expect(clippy::too_many_lines)] 178 265 async fn update(&mut self, signal: Signal) -> color_eyre::Result<Option<Signal>> { 179 266 let explorer = self 180 267 .explorer ··· 190 277 .as_mut() 191 278 .expect("This should have already been initialized"); 192 279 280 + let signal_tx = self 281 + .signal_tx 282 + .as_mut() 283 + .expect("Invariant broken, must exist"); 284 + 285 + if let Ok(Some(signal)) = inspector.update(signal.clone()).await { 286 + signal_tx.send(signal)?; 287 + } 288 + 193 289 match signal { 290 + Signal::Select { nanoid } => { 291 + let node_id = self 292 + .kh 293 + .read() 294 + .await 295 + .todo_tree 296 + .nanoid_to_nodeid 297 + .get(&nanoid) 298 + .expect("Invariant broken, why would we ever ask for this") 299 + .clone(); 300 + 301 + let Some(pos) = explorer.id_list.iter().position(|id| node_id == *id) else { 302 + return Ok(None); 303 + }; 304 + explorer.state.select(Some(pos)); 305 + self.update_inspector_from_selection().await; 306 + } 307 + 194 308 Signal::SwitchTo { 195 309 page: Page::Todo(region), 196 310 } => { ··· 215 329 216 330 self.update_inspector_from_selection().await; 217 331 } 332 + 218 333 Signal::MoveDown => { 219 334 match self.active { 220 335 TodoRegion::TaskList => { ··· 244 359 245 360 self.update_inspector_from_selection().await; 246 361 } 362 + 363 + Signal::NewTask => { 364 + if self.active != TodoRegion::Explorer { 365 + return Ok(None); 366 + } 367 + 368 + let mut kt = self.kh.write().await; 369 + let Some(parent) = explorer 370 + .group_of_current_selection(&kt.todo_tree) 371 + // .cloned() 372 + .map(|parent| parent.id.clone()) 373 + else { 374 + return Ok(None); 375 + }; 376 + let task = 377 + Task::new(DEFAULT_NAME, parent, &mut kt, None, Priority::default()).await?; 378 + 379 + drop(kt); 380 + debug!("created task: {task:#?}"); 381 + 382 + signal_tx.send(Signal::Refresh)?; 383 + signal_tx.send(Signal::Select { nanoid: task.id })?; 384 + return Ok(Some(Signal::SwitchTo { 385 + page: Page::Todo(TodoRegion::Inspector), 386 + })); 387 + } 388 + 389 + Signal::NewSubGroup => { 390 + if self.active != TodoRegion::Explorer { 391 + return Ok(None); 392 + } 393 + let mut kt = self.kh.write().await; 394 + let parent = explorer 395 + .group_of_current_selection(&kt.todo_tree) 396 + .map(|parent| parent.id.clone()); 397 + let group = Group::new(DEFAULT_NAME, parent, &mut kt).await?; 398 + drop(kt); 399 + debug!("Created group: {group:#?}"); 400 + 401 + signal_tx.send(Signal::Refresh)?; 402 + signal_tx.send(Signal::Select { nanoid: group.id })?; 403 + return Ok(Some(Signal::SwitchTo { 404 + page: Page::Todo(TodoRegion::Inspector), 405 + })); 406 + } 407 + 408 + Signal::NewGroup => { 409 + if self.active != TodoRegion::Explorer { 410 + return Ok(None); 411 + } 412 + debug!("Creating Group!"); 413 + let mut kt = self.kh.write().await; 414 + let group = Group::new(DEFAULT_NAME, None, &mut kt).await?; 415 + drop(kt); 416 + debug!("Created group: {group:#?}"); 417 + 418 + signal_tx.send(Signal::Refresh)?; 419 + signal_tx.send(Signal::Select { nanoid: group.id })?; 420 + return Ok(Some(Signal::SwitchTo { 421 + page: Page::Todo(TodoRegion::Inspector), 422 + })); 423 + } 424 + 425 + Signal::Refresh => { 426 + self.refresh().await; 427 + } 428 + 247 429 _ => {} 248 430 } 249 431 Ok(None) 432 + } 433 + 434 + async fn handle_key_event(&mut self, key: KeyEvent) -> color_eyre::Result<Option<Signal>> { 435 + self.inspector.as_mut().unwrap().handle_key_event(key).await 250 436 } 251 437 252 438 fn draw(&mut self, frame: &mut Frame, area: Rect) -> color_eyre::Result<()> { 253 439 let explorer = self.explorer.as_mut().unwrap(); 254 440 let task_list = self.task_list.as_mut().unwrap(); 255 441 442 + let inspector = self.inspector.as_mut().unwrap(); 443 + 256 444 let splits = self.layouts.split(area); 257 445 258 446 frame.render_stateful_widget(&explorer.render_list, splits.explorer, &mut explorer.state); ··· 261 449 splits.task_list, 262 450 &mut task_list.state, 263 451 ); 264 - frame.render_widget(self.inspector.as_ref().unwrap(), splits.inspector); 452 + 453 + inspector.draw(frame, splits.inspector)?; 454 + 265 455 Ok(()) 266 456 } 267 457 }
+1 -1
src/tui/components/todo/tasklist.rs
··· 22 22 let render_list = List::new( 23 23 tree.tree 24 24 .traverse_pre_order(scope) 25 - .expect("nthis should not panic as the node id should exist inside") 25 + .expect("This should not panic as the node id should exist inside") 26 26 .zip( 27 27 tree.tree 28 28 .traverse_pre_order_ids(scope)
+12 -7
src/tui/components/viewport/mod.rs
··· 32 32 use switcher::Switcher; 33 33 34 34 impl Viewport<'_> { 35 - pub async fn new(kh: KastenHandle) -> Result<Self> { 35 + pub fn new(kh: KastenHandle) -> Self { 36 36 let mut switcher = Switcher::default(); 37 37 switcher.select_region(Page::default()); 38 38 39 - Ok(Self { 39 + Self { 40 40 signal_tx: None, 41 41 _layouts: Layouts::default(), 42 42 switcher, 43 - zk: Zk::new(kh.clone()).await?, 43 + zk: Zk::new(kh.clone()), 44 44 todo: Todo::new(kh.clone()), 45 45 active_page: Page::default(), 46 46 kh, 47 - }) 47 + } 48 48 } 49 49 } 50 50 ··· 65 65 async fn init(&mut self, area: Size) -> color_eyre::Result<()> { 66 66 self.zk.init(area).await?; 67 67 self.todo.init(area).await?; 68 + 69 + self.signal_tx.as_mut().unwrap().send(Signal::SwitchTo { 70 + page: Page::default(), 71 + })?; 72 + 68 73 Ok(()) 69 74 } 70 75 ··· 83 88 debug!("active page switched to : {page}"); 84 89 } 85 90 86 - match self.active_page { 87 - Page::Zk => self.zk.update(signal).await, 88 - Page::Todo(_) => self.todo.update(signal).await, 91 + if let Some(signal) = self.zk.update(signal.clone()).await? { 92 + self.signal_tx.as_mut().unwrap().send(signal)?; 89 93 } 94 + self.todo.update(signal).await 90 95 } 91 96 92 97 async fn handle_key_event(&mut self, key: KeyEvent) -> color_eyre::Result<Option<Signal>> {
+204 -95
src/tui/components/zk/mod.rs
··· 7 7 use tracing::info; 8 8 9 9 use crate::{ 10 - tui::{Signal, components::Component}, 10 + tui::{Page, Signal, components::Component}, 11 11 types::{KastenHandle, Zettel}, 12 12 }; 13 13 ··· 30 30 signal_tx: Option<UnboundedSender<Signal>>, 31 31 kh: KastenHandle, 32 32 layouts: Layouts, 33 - search: Search<'text>, 34 - zettel_list: ZettelList<'text>, 35 - zettel_view: ZettelView<'text>, 36 - preview: Preview<'text>, 33 + area: Size, 34 + 35 + search: Option<Search<'text>>, 36 + zettel_list: Option<ZettelList<'text>>, 37 + zettel_view: Option<ZettelView<'text>>, 38 + preview: Option<Preview<'text>>, 39 + 40 + active: bool, 37 41 } 38 42 39 43 struct Layouts { ··· 42 46 z_preview: Layout, 43 47 } 44 48 49 + impl Layouts { 50 + fn split(&self, area: Rect) -> LayoutSplit { 51 + let rects = self.left_right.split(area); 52 + 53 + let (left, right) = (rects[0], rects[2]); 54 + 55 + let l_rects = self.search_zl.split(left); 56 + 57 + let r_rects = self.z_preview.split(right); 58 + 59 + LayoutSplit { 60 + search: l_rects[0], 61 + zettel_list: l_rects[1], 62 + zettel_view: r_rects[0], 63 + preview: r_rects[1], 64 + } 65 + } 66 + } 67 + 68 + struct LayoutSplit { 69 + search: Rect, 70 + zettel_list: Rect, 71 + zettel_view: Rect, 72 + preview: Rect, 73 + } 74 + 45 75 impl Default for Layouts { 46 76 fn default() -> Self { 47 77 Self { ··· 57 87 } 58 88 59 89 impl Zk<'_> { 60 - pub async fn new(kh: KastenHandle) -> Result<Self> { 90 + pub fn new(kh: KastenHandle) -> Self { 91 + Self { 92 + signal_tx: None, 93 + kh, 94 + 95 + layouts: Layouts::default(), 96 + area: Size::default(), 97 + 98 + search: None, 99 + zettel_list: None, 100 + zettel_view: None, 101 + preview: None, 102 + 103 + active: false, 104 + } 105 + } 106 + 107 + async fn refresh(&mut self) -> Result<()> { 108 + let zettel_list = self.zettel_list.as_mut().expect("Must be initialized"); 109 + 110 + let selected = zettel_list 111 + .state 112 + .selected() 113 + .and_then(|idx| zettel_list.id_list.get(idx)); 114 + 115 + let splits = self 116 + .layouts 117 + .split(Rect::new(0, 0, self.area.width, self.area.height)); 118 + 119 + let kt = self.kh.read().await; 120 + let db = kt.db.clone(); 121 + 122 + // ideally we just keep the same selection as we had originally 123 + 61 124 let fetch_all = async || -> Result<Vec<Zettel>> { 62 125 Ok(ZettelEntity::load() 63 126 .with(TagEntity) 64 127 .order_by_desc(ZettelColumns::ModifiedAt) 65 - .all(&kh.read().await.db) 128 + .all(&db) 66 129 .await? 67 130 .into_iter() 68 131 .map(Into::into) 69 132 .collect()) 70 133 }; 71 134 72 - let mut zettels: Vec<Zettel> = fetch_all().await?; 135 + let zettels: Vec<Zettel> = fetch_all().await?; 73 136 74 - if zettels.is_empty() { 75 - let _ = Zettel::new("Welcome!", &mut *kh.write().await, vec![]).await?; 76 - zettels = fetch_all().await?; 77 - } 78 - 79 - // in theory this is wasted compute, we should be initializing all our 80 - // stuff inside the init function 81 - let mut l_state = ListState::default(); 82 - l_state.select_first(); 83 - let zettel_list = ZettelList::new(zettels.clone(), l_state, 0); 137 + let mut zettel_list = ZettelList::new( 138 + zettels.clone(), 139 + ListState::default(), 140 + splits.zettel_list.width, 141 + ); 84 142 85 - let selected_zettel = zettel_list 86 - .id_list 87 - .get( 88 - zettel_list 89 - .state 90 - .selected() 91 - .expect("We explicitly select the first item"), 92 - ) 93 - // so technically this might not exist 94 - .expect("There must always be one atleast one zettel"); 143 + let selected_zettel_idx = 144 + selected.and_then(|desired| zettel_list.id_list.iter().position(|id| id == desired)); 95 145 96 - let kt = kh.read().await; 146 + zettel_list.state.select(selected_zettel_idx); 97 147 98 148 let zettel = zettels 99 149 .iter() 100 - .find(|&z| &z.id == selected_zettel) 150 + //TODO: expect probably should not look like this 151 + .find(|&z| &z.id == selected.expect("Something should be selected")) 101 152 .expect("we selected it out of the list so it must exist"); 102 153 103 154 let preview = Preview::from(zettel.content(&kt.index).clone()); 104 155 105 156 drop(kt); 106 157 107 - Ok(Self { 108 - signal_tx: None, 109 - search: Search::new(kh.clone()), 110 - kh, 111 - layouts: Layouts::default(), 112 - zettel_list, 113 - zettel_view: zettel.into(), 114 - preview, 115 - }) 158 + let search = Search::new(self.kh.clone()); 159 + let zettel_view = zettel.into(); 160 + 161 + self.search = Some(search); 162 + self.zettel_view = Some(zettel_view); 163 + self.preview = Some(preview); 164 + self.zettel_list = Some(zettel_list); 165 + 166 + Ok(()) 116 167 } 117 168 118 169 async fn update_views_from_zettel_list_selection(&mut self) -> Result<()> { 119 - let selection_idx = self 120 - .zettel_list 170 + let zettel_list = self.zettel_list.as_mut().expect("Must be initialzied"); 171 + 172 + let selection_idx = zettel_list 121 173 .state 122 174 .selected() 123 175 .expect("i have no idea what to do if this doesnt exist"); 124 176 125 177 // sometimes the selection we get is over the length of the thing, so its 126 178 // actually fine if this is none, just means we reached the end of the list 127 - let Some(zid) = self.zettel_list.id_list.get(selection_idx) else { 179 + let Some(zid) = zettel_list.id_list.get(selection_idx) else { 128 180 return Ok(()); 129 181 }; 130 182 ··· 134 186 .await? 135 187 .context("Unknown Behaviour, A selected zettel got deleted somehow.")?; 136 188 137 - self.preview = zettel.content(&kh.index).clone().into(); 189 + self.preview = Some(zettel.content(&kh.index).clone().into()); 138 190 drop(kh); 139 191 140 - self.zettel_view = zettel.into(); 192 + self.zettel_view = Some(zettel.into()); 141 193 142 194 Ok(()) 143 195 } ··· 161 213 } 162 214 163 215 pub async fn update_with_respect_to_query(&mut self) -> Result<()> { 164 - let zettels = self 216 + let curr_zettels = self.get_zettels_by_current_query().await?; 217 + 218 + let search = self 165 219 .search 166 - .rank(self.get_zettels_by_current_query().await?) 167 - .await; 220 + .as_mut() 221 + .expect("Must be initalized by this point"); 222 + 223 + let zettel_list = self 224 + .zettel_list 225 + .as_mut() 226 + .expect("Must be initialized by this point"); 168 227 169 - self.zettel_list = ZettelList::new(zettels, self.zettel_list.state, self.zettel_list.width); 228 + let zettels = search.rank(curr_zettels).await; 229 + 230 + *zettel_list = ZettelList::new(zettels, zettel_list.state, zettel_list.width); 231 + 170 232 info!("we are moving selection to first"); 171 - self.zettel_list.state.select_first(); 233 + 234 + zettel_list.state.select_first(); 172 235 self.update_views_from_zettel_list_selection().await?; 173 236 174 237 Ok(()) ··· 179 242 impl Component for Zk<'_> { 180 243 /// this tells us how big the space we have for this is 181 244 async fn init(&mut self, area: Size) -> color_eyre::Result<()> { 182 - let total_width = area.width; 245 + self.area = area; 246 + let splits = self.layouts.split(Rect::new(0, 0, area.width, area.height)); 247 + let mut kt = self.kh.write().await; 248 + let db = kt.db.clone(); 249 + 250 + let fetch_all = async || -> Result<Vec<Zettel>> { 251 + Ok(ZettelEntity::load() 252 + .with(TagEntity) 253 + .order_by_desc(ZettelColumns::ModifiedAt) 254 + .all(&db) 255 + .await? 256 + .into_iter() 257 + .map(Into::into) 258 + .collect()) 259 + }; 260 + 261 + let mut zettels: Vec<Zettel> = fetch_all().await?; 262 + 263 + if zettels.is_empty() { 264 + let _ = Zettel::new("Welcome!", &mut kt, vec![]).await?; 265 + zettels = fetch_all().await?; 266 + } 183 267 184 268 // in theory this is wasted compute, we should be initializing all our 269 + // stuff inside the init function 185 270 let mut l_state = ListState::default(); 186 271 l_state.select_first(); 272 + let zettel_list = ZettelList::new(zettels.clone(), l_state, splits.zettel_list.width); 187 273 188 - let zettel_list = ZettelList::new( 189 - self.get_zettels_by_current_query().await?, 190 - l_state, 191 - total_width / 2, 192 - ); 274 + let selected_zettel = zettel_list 275 + .id_list 276 + .get( 277 + zettel_list 278 + .state 279 + .selected() 280 + .expect("We explicitly select the first item"), 281 + ) 282 + // so technically this might not exist 283 + .expect("There must always be one atleast one zettel"); 284 + 285 + let zettel = zettels 286 + .iter() 287 + .find(|&z| &z.id == selected_zettel) 288 + .expect("we selected it out of the list so it must exist"); 289 + 290 + let preview = Preview::from(zettel.content(&kt.index).clone()); 291 + 292 + drop(kt); 293 + 294 + let search = Search::new(self.kh.clone()); 295 + let zettel_view = zettel.into(); 193 296 194 - self.zettel_list = zettel_list; 297 + self.search = Some(search); 298 + self.zettel_view = Some(zettel_view); 299 + self.preview = Some(preview); 300 + self.zettel_list = Some(zettel_list); 195 301 196 302 Ok(()) 197 303 } ··· 202 308 } 203 309 204 310 async fn update(&mut self, signal: Signal) -> Result<Option<crate::tui::Signal>> { 311 + let zettel_list = self.zettel_list.as_mut().expect("Must be initialized"); 312 + let search = self.search.as_mut().expect("Must be initialized"); 205 313 match signal { 206 - Signal::MoveDown => { 207 - self.zettel_list.state.select_next(); 314 + Signal::SwitchTo { page } => { 315 + self.active = page == Page::Zk; 316 + } 317 + 318 + Signal::Refresh => { 319 + self.refresh().await?; 320 + } 321 + 322 + Signal::MoveDown if self.active => { 323 + zettel_list.state.select_next(); 208 324 self.update_views_from_zettel_list_selection().await?; 209 325 } 210 - Signal::MoveUp => { 211 - self.zettel_list.state.select_previous(); 326 + Signal::MoveUp if self.active => { 327 + zettel_list.state.select_previous(); 212 328 self.update_views_from_zettel_list_selection().await?; 213 329 } 214 330 215 331 Signal::OpenZettel => { 216 - let Some(selcted) = self.zettel_list.state.selected() else { 332 + let Some(selcted) = zettel_list.state.selected() else { 217 333 return Ok(None); 218 334 }; 219 335 220 - let Some(zid) = self.zettel_list.id_list.get(selcted) else { 336 + let Some(zid) = zettel_list.id_list.get(selcted) else { 221 337 return Ok(None); 222 338 }; 223 339 ··· 236 352 let mut kt = self.kh.write().await; 237 353 238 354 // we create the zettel with the query as the 239 - let z = Zettel::new(self.search.query(), &mut kt, vec![]) 355 + let z = Zettel::new(search.query(), &mut kt, vec![]) 240 356 .await 241 357 .with_context(|| "Failed to create a new Zettel!")?; 242 358 ··· 263 379 } 264 380 265 381 Signal::ClosedZettel { zid } => { 266 - // regenerate a fresh zettel list 267 - self.zettel_list = ZettelList::new( 268 - self.get_zettels_by_current_query().await?, 269 - self.zettel_list.state, 270 - self.zettel_list.width, 271 - ); 382 + let curr_zettels = self.get_zettels_by_current_query().await?; // regenerate a fresh zettel list 383 + 384 + let zettel_list = self.zettel_list.as_mut().expect("Must be initialized"); 385 + let zettel_view = self.zettel_view.as_mut().expect("Must be initialized"); 386 + let preview = self.preview.as_mut().expect("Must be initialized"); 387 + let search = self.search.as_mut().expect("Must be initialized"); 388 + 389 + *zettel_list = ZettelList::new(curr_zettels, zettel_list.state, zettel_list.width); 272 390 273 391 let kt = self.kh.read().await; 274 392 ··· 276 394 .await? 277 395 .expect("invariant broken, we just closed this zettel"); 278 396 279 - let idx = self 280 - .zettel_list 281 - .id_list 282 - .iter() 283 - .position(|id| *id == zettel.id); 397 + let idx = zettel_list.id_list.iter().position(|id| *id == zettel.id); 284 398 285 399 // reset the state of the component 286 - self.search.clear_query(); 287 - self.zettel_list.state.select(idx); 400 + search.clear_query(); 401 + zettel_list.state.select(idx); 288 402 289 - self.zettel_view = ZettelView::from(&zettel); 290 - self.preview = Preview::from(zettel.content(&kt.index).clone()); 403 + *zettel_view = ZettelView::from(&zettel); 404 + *preview = Preview::from(zettel.content(&kt.index).clone()); 291 405 drop(kt); 292 406 } 293 407 ··· 300 414 // NOTE: this is hardcoded for now, but I honestly think people should not 301 415 // be able to change these binds, opinionated software or something... 302 416 if !(key.code.is_up() || key.code.is_down() || key.code.is_enter() || key.code.is_tab()) { 303 - self.search.query.input(key); 417 + self.search.as_mut().unwrap().query.input(key); 304 418 self.update_with_respect_to_query().await?; 305 419 } 306 420 ··· 312 426 frame: &mut ratatui::Frame, 313 427 area: ratatui::prelude::Rect, 314 428 ) -> color_eyre::Result<()> { 315 - let (search_layout, zettel_list_layout, zettel_layout, preview_layout) = { 316 - let rects = self.layouts.left_right.split(area); 429 + let zettel_list = self.zettel_list.as_mut().expect("Must be initialized"); 430 + let zettel_view = self.zettel_view.as_mut().expect("Must be initialized"); 431 + let preview = self.preview.as_mut().expect("Must be initialized"); 432 + let search = self.search.as_mut().expect("Must be initialized"); 317 433 318 - let (left, right) = (rects[0], rects[2]); 434 + let splits = self.layouts.split(area); 319 435 320 - let l_rects = self.layouts.search_zl.split(left); 321 - 322 - let r_rects = self.layouts.z_preview.split(right); 323 - 324 - (l_rects[0], l_rects[1], r_rects[0], r_rects[1]) 325 - }; 326 - 327 - frame.render_widget(self.search.clone(), search_layout); 436 + frame.render_widget(search.clone(), splits.search); 328 437 329 438 frame.render_stateful_widget( 330 - &self.zettel_list.render_list, 331 - zettel_list_layout, 332 - &mut self.zettel_list.state, 439 + &zettel_list.render_list, 440 + splits.zettel_list, 441 + &mut zettel_list.state, 333 442 ); 334 443 335 - frame.render_widget(self.zettel_view.clone(), zettel_layout); 336 - frame.render_widget(self.preview.clone(), preview_layout); 444 + frame.render_widget(zettel_view.clone(), splits.zettel_view); 445 + frame.render_widget(preview.clone(), splits.preview); 337 446 338 447 Ok(()) 339 448 }
+1 -1
src/tui/components/zk/search.rs
··· 107 107 }) 108 108 .collect(); 109 109 110 - results.sort_by(|a, b| b.1.cmp(&a.1)); 110 + results.sort_by_key(|b| std::cmp::Reverse(b.1)); 111 111 112 112 results.into_iter().map(|(i, _)| i).collect() 113 113 }
+34 -22
src/tui/signal.rs
··· 1 - use std::{path::PathBuf, str::FromStr}; 1 + use std::path::PathBuf; 2 2 3 - use color_eyre::eyre::eyre; 3 + use dto::NanoId; 4 4 use strum::Display; 5 5 6 6 use serde::{Deserialize, Serialize}; ··· 23 23 Error(String), 24 24 Help, 25 25 26 + /// Request a refresh of the components being displayed due 27 + /// to an update to the `Kasten` 28 + Refresh, 29 + 26 30 SwitchTo { 27 31 page: Page, 28 32 }, ··· 53 57 zid: ZettelId, 54 58 }, 55 59 60 + /// Create a new `Group` inside the currently selected group 61 + NewSubGroup, 62 + 63 + /// Create a new `Group` in the current scope 64 + NewGroup, 65 + 66 + /// Create a new `Task` 67 + NewTask, 68 + 69 + /// Edit the name of a `Task` or a `Group`. 70 + /// Only works with the inspector 71 + EditName, 72 + 73 + /// Edit the `Priority` of a `Task` or a `Group`. 74 + /// Only works with the inspector 75 + EditPriority, 76 + 77 + /// Internal Signal that tells the app to resume interpreting keys 78 + ExitRawText, 79 + 80 + /// Internal Signal that tells the app to stop interpreting keys 81 + /// as signals 82 + EnterRawText, 83 + 56 84 /// this is fucking temporary 57 85 Helix { 58 86 path: PathBuf, 59 87 }, 60 - } 61 88 62 - impl FromStr for Signal { 63 - type Err = color_eyre::Report; 64 - 65 - fn from_str(s: &str) -> Result<Self, Self::Err> { 66 - Ok(match s.to_lowercase().as_str() { 67 - "suspend" => Self::Suspend, 68 - "resume" => Self::Resume, 69 - "quit" => Self::Quit, 70 - "movedown" => Self::MoveDown, 71 - "moveup" => Self::MoveUp, 72 - "openzettel" => Self::OpenZettel, 73 - "newzettel" => Self::NewZettel, 74 - _ => { 75 - return Err(eyre!(format!( 76 - "Attempt to construct a non-user Signal from str: {s}" 77 - ))); 78 - } 79 - }) 80 - } 89 + /// Requests the `Explorer` to select the following `NanoId`. 90 + Select { 91 + nanoid: NanoId, 92 + }, 81 93 }
+30
src/types/frontmatter.rs
··· 117 117 118 118 Ok((Self::new(title, created_at, tag_strings), remaining)) 119 119 } 120 + 121 + pub fn flush_to_file(&self, path: impl AsRef<Path>) -> Result<()> { 122 + let path = path.as_ref(); 123 + let string = std::fs::read_to_string(path)?; 124 + 125 + let lines: Vec<_> = string.lines().collect(); 126 + 127 + let closing_delim = lines 128 + .iter() 129 + .enumerate() 130 + .skip(1) 131 + .find(|(_, line)| line.trim() == "---") 132 + .map(|(i, _)| i) 133 + .ok_or_else(|| eyre!("Could not find closing front matter delimiter"))?; 134 + 135 + let body = lines[closing_delim + 1..].join("\n"); 136 + 137 + std::fs::write(path, format!("{self}{body}"))?; 138 + Ok(()) 139 + } 140 + } 141 + 142 + impl From<Zettel> for FrontMatter { 143 + fn from(value: Zettel) -> Self { 144 + Self { 145 + title: value.title, 146 + created_at: value.created_at, 147 + tag_strings: value.tags.into_iter().map(|t| t.name).collect(), 148 + } 149 + } 120 150 } 121 151 122 152 impl Display for FrontMatter {
+93 -2
src/types/group.rs
··· 1 - use dto::{DateTime, GroupModelEx, NanoId}; 1 + use color_eyre::eyre::Result; 2 + use dto::{ 3 + DateTime, GroupActiveModel, GroupEntity, GroupModelEx, IntoActiveModel as _, NanoId, 4 + TagActiveModel, TagEntity, ZettelEntity, 5 + }; 2 6 3 - use crate::types::{Priority, Tag, Zettel, frontmatter}; 7 + use crate::types::{Kasten, Priority, Tag, Zettel, frontmatter}; 4 8 5 9 /// A `Group` which contains tasks! 6 10 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] ··· 33 37 self.modified_at 34 38 .format(frontmatter::DATE_FMT_STR) 35 39 .to_string() 40 + } 41 + 42 + pub async fn alter_name( 43 + id: NanoId, 44 + new_name: impl Into<String>, 45 + kt: &mut Kasten, 46 + ) -> Result<()> { 47 + let new_name = new_name.into(); 48 + 49 + let g = GroupEntity::load() 50 + .filter_by_nano_id(id.clone()) 51 + .with(TagEntity) 52 + .with((ZettelEntity, TagEntity)) 53 + .one(&kt.db) 54 + .await? 55 + .expect("Invariant Broken: Must exist"); 56 + 57 + let tag_id = g.tag.as_ref().expect("Must be loaded").nano_id.clone(); 58 + 59 + let zettel_id = g.zettel_id.clone(); 60 + 61 + let _ = g 62 + .into_active_model() 63 + .set_name(new_name.as_str()) 64 + .update(&kt.db) 65 + .await?; 66 + 67 + Tag::alter_name(tag_id, &new_name, kt).await?; 68 + 69 + Zettel::alter_name(zettel_id.into(), &new_name, kt).await?; 70 + 71 + Ok(()) 72 + } 73 + 74 + pub async fn new( 75 + name: impl Into<String>, 76 + parent_id: Option<NanoId>, 77 + kt: &mut Kasten, 78 + ) -> Result<Self> { 79 + let name = name.into(); 80 + let tag: Tag = TagActiveModel::builder() 81 + .set_name(name.clone()) 82 + .insert(&kt.db) 83 + .await? 84 + .into(); 85 + 86 + let tag_id = tag.id.clone(); 87 + 88 + // then create the zettel for the group 89 + let zettel = Zettel::new(name.clone(), kt, vec![tag]).await?; 90 + 91 + // then insert that shi 92 + let inserted = GroupActiveModel::builder() 93 + .set_name(name) 94 + .set_parent_group_id(parent_id) 95 + .set_tag( 96 + TagEntity::load() 97 + .filter_by_nano_id(tag_id) 98 + .one(&kt.db) 99 + .await? 100 + .expect("Tag must exist since we just created it") 101 + .into_active_model(), 102 + ) 103 + .set_zettel( 104 + ZettelEntity::load() 105 + .filter_by_nano_id(zettel.id) 106 + .one(&kt.db) 107 + .await? 108 + .expect("Zettel must exist since we just created it") 109 + .into_active_model(), 110 + ) 111 + .set_priority(Priority::default()) 112 + .insert(&kt.db) 113 + .await?; 114 + 115 + // group should also have the accompanying tag for it. 116 + let group: Self = GroupEntity::load() 117 + .with(TagEntity) 118 + .with((ZettelEntity, TagEntity)) 119 + .filter_by_nano_id(inserted.nano_id) 120 + .one(&kt.db) 121 + .await? 122 + .expect("We just inserted it") 123 + .into(); 124 + 125 + kt.todo_tree.insert_group(&group); 126 + Ok(group) 36 127 } 37 128 } 38 129
+1 -1
src/types/kasten/index/mod.rs
··· 41 41 .par_bridge() 42 42 .flatten() 43 43 .filter(|entry| { 44 - entry.file_type().map(|ft| ft.is_file()).unwrap_or(false) 44 + entry.file_type().is_ok_and(|ft| ft.is_file()) 45 45 && entry 46 46 .path() 47 47 .extension()
+116 -1
src/types/kasten/todo_tree.rs
··· 1 - use std::collections::HashMap; 1 + use std::{cmp::Ordering, collections::HashMap}; 2 2 3 3 use color_eyre::eyre::{Context, Result}; 4 4 use dto::{ ··· 136 136 } 137 137 138 138 Ok(()) 139 + } 140 + 141 + pub fn insert_group(&mut self, group: &Group) { 142 + let parent_node_id = group 143 + .parent_id 144 + .clone() 145 + .and_then(|id| self.nanoid_to_nodeid.get(&id)) 146 + .unwrap_or(&self.root_id) 147 + .clone(); 148 + 149 + let my_depth = if parent_node_id == self.root_id { 150 + 0 151 + } else { 152 + self.tree 153 + .get(&parent_node_id) 154 + .expect("Must exist inside tree") 155 + .data() 156 + .depth 157 + + 1 158 + }; 159 + 160 + let inserted_node_id = self 161 + .tree 162 + .insert( 163 + Node::new(TodoNode::new( 164 + super::TodoNodeKind::Group(Box::new(group.clone())), 165 + my_depth, 166 + )), 167 + tree::InsertBehavior::UnderNode(&parent_node_id), 168 + ) 169 + .expect("Insertion of group should not error!"); 170 + 171 + self.reorder_chidren(&parent_node_id); 172 + 173 + self.nanoid_to_nodeid 174 + .insert(group.id.clone(), inserted_node_id); 175 + } 176 + 177 + pub fn insert_task(&mut self, task: &Task) { 178 + let parent_node_id = self 179 + .nanoid_to_nodeid 180 + .get(&task.group_id) 181 + .expect("The group must already be in the lookup hashmap") 182 + .clone(); 183 + 184 + let my_depth = self 185 + .tree 186 + .get(&parent_node_id) 187 + .expect("Must exist inside tree") 188 + .data() 189 + .depth 190 + + 1; 191 + 192 + let inserted_node_id = self 193 + .tree 194 + .insert( 195 + Node::new(TodoNode::new( 196 + super::TodoNodeKind::Task(Box::new(task.clone())), 197 + my_depth, 198 + )), 199 + tree::InsertBehavior::UnderNode(&parent_node_id), 200 + ) 201 + .expect("Insertion of Task should not error!"); 202 + 203 + self.reorder_chidren(&parent_node_id); 204 + 205 + self.nanoid_to_nodeid 206 + .insert(task.id.clone(), inserted_node_id); 207 + } 208 + 209 + fn reorder_chidren(&mut self, parent_node_id: &NodeId) { 210 + let children = self 211 + .tree 212 + .children(parent_node_id) 213 + .expect("Must be valid") 214 + .zip( 215 + self.tree 216 + .children_ids(parent_node_id) 217 + .expect("Must be valid"), 218 + ) 219 + .map(|(a, b)| (b.clone(), matches!(a.data().kind, TodoNodeKind::Task(_)))) 220 + .collect::<HashMap<_, _>>(); 221 + 222 + let parent = self 223 + .tree 224 + .get_mut(parent_node_id) 225 + .expect("parent must exist"); 226 + 227 + parent.sort_children_by(|a, _| { 228 + let a = children.get(a).expect("must exist"); 229 + 230 + if *a { 231 + return Ordering::Less; 232 + } 233 + 234 + Ordering::Equal 235 + }); 236 + } 237 + 238 + pub fn get_node_by_nano_id(&self, nano_id: &NanoId) -> &Node<TodoNode> { 239 + let node_id = self 240 + .nanoid_to_nodeid 241 + .get(nano_id) 242 + .expect("invariant broken!"); 243 + 244 + self.tree.get(node_id).expect("Invariant Broken!") 245 + } 246 + 247 + pub fn get_node_mut_by_nano_id(&mut self, nano_id: &NanoId) -> &mut Node<TodoNode> { 248 + let node_id = self 249 + .nanoid_to_nodeid 250 + .get(nano_id) 251 + .expect("invariant broken!"); 252 + 253 + self.tree.get_mut(node_id).expect("Invariant Broken!") 139 254 } 140 255 }
+42 -2
src/types/tag.rs
··· 1 - use dto::{NanoId, TagModel, TagModelEx}; 1 + use color_eyre::eyre::Result; 2 + use dto::{IntoActiveModel as _, NanoId, TagEntity, TagModel, TagModelEx, ZettelEntity}; 2 3 3 - use crate::types::Color; 4 + use crate::types::{Color, Kasten, Zettel}; 4 5 5 6 /// Represents a `Tag` in a `ZettelKasten` note taking method. 6 7 /// Easy way to link multiple notes under one simple word. ··· 15 16 pub name: String, 16 17 /// Color of the tag 17 18 pub color: Color, 19 + } 20 + 21 + impl Tag { 22 + pub async fn alter_name( 23 + id: NanoId, 24 + new_name: impl Into<String>, 25 + kt: &mut Kasten, 26 + ) -> Result<()> { 27 + let new_name = new_name.into(); 28 + 29 + TagEntity::load() 30 + .filter_by_nano_id(id.clone()) 31 + .one(&kt.db) 32 + .await? 33 + .expect("Invariant Broken: Must exist") 34 + .into_active_model() 35 + .set_name(new_name.as_str()) 36 + .update(&kt.db) 37 + .await?; 38 + 39 + // fetch all the zettels for this tag 40 + let tag = TagEntity::load() 41 + .filter_by_nano_id(id) 42 + .with(ZettelEntity) 43 + .one(&kt.db) 44 + .await? 45 + .expect("We just saved it"); 46 + 47 + assert!( 48 + tag.zettels.is_loaded(), 49 + "We expect the zettels to be loaded" 50 + ); 51 + 52 + for zettel in tag.zettels { 53 + Zettel::write_tags_from_db(zettel.nano_id.into(), kt).await?; 54 + } 55 + 56 + Ok(()) 57 + } 18 58 } 19 59 20 60 impl From<TagModel> for Tag {
+106 -9
src/types/task.rs
··· 1 - use dto::{ DateTime, NanoId, TaskModelEx}; 1 + use color_eyre::eyre::{Context, Result, eyre}; 2 + use dto::{ 3 + DateTime, GroupEntity, HasOne, IntoActiveModel as _, NanoId, TagEntity, TaskActiveModel, 4 + TaskEntity, TaskModelEx, ZettelEntity, 5 + }; 2 6 3 - use crate::types::{Group, Priority, Zettel, frontmatter}; 7 + use crate::types::{Group, Kasten, Priority, Zettel, frontmatter}; 4 8 5 9 /// a `Task` that you have to complete! 6 10 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] 7 11 pub struct Task { 8 12 /// Should only be constructed from models. 9 - _private:(), 13 + _private: (), 10 14 11 15 pub id: NanoId, 12 16 pub name: String, ··· 22 26 } 23 27 24 28 impl Task { 29 + pub async fn new( 30 + name: impl Into<String>, 31 + parent_id: NanoId, 32 + kt: &mut Kasten, 33 + due: Option<DateTime>, 34 + priority: Priority, 35 + ) -> Result<Self> { 36 + let name = name.into(); 37 + 38 + let parent = GroupEntity::load() 39 + .with(TagEntity) 40 + .filter_by_nano_id(parent_id) 41 + .one(&kt.db) 42 + .await 43 + .with_context(|| "failed to communicate with db")? 44 + .ok_or_else(|| eyre!("could not find the group"))?; 45 + 46 + let HasOne::Loaded(tag) = parent.tag else { 47 + panic!("this has to be loaded since we just loaded it right above") 48 + }; 49 + 50 + let zettel = Zettel::new(name.clone(), kt, vec![(*tag).into()]).await?; 51 + 52 + let inserted = TaskActiveModel::builder() 53 + .set_name(name) 54 + .set_group_id(parent.nano_id.clone()) 55 + .set_priority(priority) 56 + .set_zettel( 57 + ZettelEntity::load() 58 + .filter_by_nano_id(zettel.id) 59 + .one(&kt.db) 60 + .await? 61 + .expect("Zettel must exist since we just created it") 62 + .into_active_model(), 63 + ) 64 + .set_due(due) 65 + .insert(&kt.db) 66 + .await?; 67 + 68 + let group = GroupEntity::load() 69 + .with(TagEntity) 70 + .with((ZettelEntity, TagEntity)) 71 + .filter_by_nano_id(parent.nano_id) 72 + .one(&kt.db) 73 + .await? 74 + .expect("We just inserted it"); 75 + 76 + let mut task_am = TaskEntity::load() 77 + .with((ZettelEntity, TagEntity)) 78 + .filter_by_nano_id(inserted.nano_id) 79 + .one(&kt.db) 80 + .await? 81 + .expect("We just inserted it"); 82 + 83 + task_am.group = HasOne::Loaded(Box::new(group)); 84 + 85 + let task: Self = task_am.into(); 86 + 87 + kt.todo_tree.insert_task(&task); 88 + 89 + Ok(task) 90 + } 91 + 92 + pub async fn alter_name( 93 + id: NanoId, 94 + new_name: impl Into<String>, 95 + kt: &mut Kasten, 96 + ) -> Result<()> { 97 + let new_name = new_name.into(); 98 + 99 + let task = TaskEntity::load() 100 + .filter_by_nano_id(id.clone()) 101 + .one(&kt.db) 102 + .await? 103 + .expect("Invariant Broken: Must exist"); 104 + 105 + let zettel_id = task.zettel_id.clone(); 106 + 107 + let _ = task 108 + .into_active_model() 109 + .set_name(new_name.as_str()) 110 + .save(&kt.db) 111 + .await?; 112 + 113 + Zettel::alter_name(zettel_id.into(), new_name, kt).await?; 114 + 115 + Ok(()) 116 + } 117 + 25 118 pub fn due(&self) -> Option<String> { 26 - self.due.map(|due|due.format(frontmatter::DATE_FMT_STR).to_string()) 119 + self.due 120 + .map(|due| due.format(frontmatter::DATE_FMT_STR).to_string()) 27 121 } 28 122 pub fn finished_at(&self) -> Option<String> { 29 - self.finished_at. 30 - map(|finished_at|finished_at.format(frontmatter::DATE_FMT_STR).to_string()) 123 + self.finished_at 124 + .map(|finished_at| finished_at.format(frontmatter::DATE_FMT_STR).to_string()) 31 125 } 32 126 pub fn created_at(&self) -> String { 33 - self.created_at.format(frontmatter::DATE_FMT_STR).to_string() 127 + self.created_at 128 + .format(frontmatter::DATE_FMT_STR) 129 + .to_string() 34 130 } 35 131 pub fn modified_at(&self) -> String { 36 - self.modified_at.format(frontmatter::DATE_FMT_STR).to_string() 132 + self.modified_at 133 + .format(frontmatter::DATE_FMT_STR) 134 + .to_string() 37 135 } 38 136 } 39 137 ··· 63 161 "When fetching a Task from the database, we expect to always have the Group loaded!!", 64 162 ) 65 163 .into(), 66 - 67 164 } 68 165 } 69 166 }
+46 -1
src/types/zettel/mod.rs
··· 1 1 use std::path::Path; 2 2 3 3 use dto::{ 4 - DatabaseConnection, DateTime, TagEntity, ZettelActiveModel, ZettelEntity, ZettelModelEx, 4 + DatabaseConnection, DateTime, IntoActiveModel, TagEntity, ZettelActiveModel, ZettelEntity, 5 + ZettelModelEx, 5 6 }; 6 7 7 8 use color_eyre::eyre::{Context, Result}; ··· 40 41 .one(db) 41 42 .await? 42 43 .map(Into::into)) 44 + } 45 + 46 + pub async fn alter_name( 47 + id: ZettelId, 48 + new_name: impl Into<String>, 49 + kt: &mut Kasten, 50 + ) -> Result<()> { 51 + let new_name = new_name.into(); 52 + 53 + // ok we need to change it on the actual zettel and then change 54 + // it in the frontmatter 55 + let _ = ZettelEntity::load() 56 + .filter_by_nano_id(id.clone()) 57 + .one(&kt.db) 58 + .await? 59 + .expect("Must exist") 60 + .into_active_model() 61 + .set_title(new_name) 62 + .save(&kt.db) 63 + .await?; 64 + 65 + let zettel = Self::fetch_from_db(&id, &kt.db) 66 + .await? 67 + .expect("We just saved it"); 68 + 69 + let file_path = zettel.absolute_path(&kt.index); 70 + let new_fm = FrontMatter::from(zettel); 71 + 72 + new_fm.flush_to_file(file_path)?; 73 + kt.index.process_zid(&id)?; 74 + 75 + Ok(()) 76 + } 77 + 78 + /// Gets the tags from the database for this `Zettel` and write it to the file. 79 + pub async fn write_tags_from_db(id: ZettelId, kt: &mut Kasten) -> Result<()> { 80 + let zettel = Self::fetch_from_db(&id, &kt.db).await?.expect("must exist"); 81 + 82 + let file_path = zettel.absolute_path(&kt.index); 83 + let new_fm = FrontMatter::from(zettel); 84 + 85 + new_fm.flush_to_file(file_path)?; 86 + kt.index.process_zid(&id)?; 87 + Ok(()) 43 88 } 44 89 45 90 pub async fn new(title: impl Into<String>, kt: &mut Kasten, tags: Vec<Tag>) -> Result<Self> {
+7 -1
src/viz/mod.rs
··· 91 91 if let Ok(signal) = self.signal_rx.try_recv() { 92 92 debug!("received signal in filaments: {signal}"); 93 93 94 - #[allow(clippy::single_match)] 95 94 match signal { 96 95 Signal::CreatedZettel { zid } => { 97 96 block_on(async { ··· 104 103 self.filaments.set_links_for_zid(&zid, links); 105 104 } 106 105 106 + // this will refresh the entire screen, esentially spawning 107 + // in a new graph, maybe there is a better way to do this? 108 + // might be clean with conditional name-showing... 109 + Signal::Refresh => block_on(async { 110 + let index = &self.kh.read().await.index; 111 + self.filaments = Filaments::from(index); 112 + }), 107 113 _ => {} 108 114 } 109 115 }