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

Todo

authored by

Surendra Jammishetti and committed by
GitHub
54bf667c a6e18d94

+766 -537
-14
.config/config.kdl
··· 1 - 2 - filaments_dir "./ZettelKasten" 3 - 4 - keymap { 5 - 6 - Home { 7 - <Ctrl-c> Quit // Another way to quit 8 - <Ctrl-z> Suspend // Suspend the application 9 - up MoveUp 10 - down MoveDown 11 - enter OpenZettel 12 - <Ctrl-n> NewZettel 13 - } 14 - }
+29
.config/config.ron
··· 1 + ( 2 + directory: "./ZettelKasten", 3 + global_key_binds: { 4 + "ctrl-c": Quit, 5 + "ctrl-z": Suspend, 6 + "up": MoveUp, 7 + "down": MoveDown, 8 + }, 9 + zk: ( 10 + keybinds: { 11 + "<Ctrl-n>": NewZettel, 12 + "enter": OpenZettel, 13 + "tab": SwitchTo ( 14 + region: Todo 15 + ), 16 + 17 + }, 18 + ), 19 + todo: ( 20 + keybinds: { 21 + "j": MoveDown, 22 + "k": MoveUp, 23 + "tab": SwitchTo ( 24 + region: Zk 25 + ), 26 + 27 + }, 28 + ), 29 + )
-50
.config/config_old.kdl
··· 1 - scroll_offset 4 2 - 3 - keybindings { 4 - Explorer { 5 - "<q>" Quit // Quit the application 6 - "<Ctrl-d>" Quit // Another way to quit 7 - "<Ctrl-c>" Quit // Yet another way to quit 8 - "<Ctrl-z>" Suspend // Suspend the application 9 - "<2>" switch-to="TodoList" 10 - "<3>" switch-to="Inspector" 11 - f ToggleShowFinished 12 - x Delete 13 - t NewTask 14 - g NewSubGroup 15 - "<Shift-g>" NewGroup 16 - j MoveDown 17 - k MoveUp 18 - l MoveInto 19 - h MoveOutOf 20 - } 21 - 22 - TodoList { 23 - "<q>" Quit // Quit the application 24 - "<Ctrl-d>" Quit // Another way to quit 25 - "<Ctrl-c>" Quit // Yet another way to quit 26 - "<Ctrl-z>" Suspend // Suspend the application 27 - "<1>" switch-to="Explorer" 28 - "<3>" switch-to="Inspector" 29 - j MoveDown 30 - k MoveUp 31 - } 32 - 33 - Inspector { 34 - "<q>" Quit // Quit the application 35 - "<Ctrl-d>" Quit // Another way to quit 36 - "<Ctrl-c>" Quit // Yet another way to quit 37 - "<Ctrl-z>" Suspend // Suspend the application 38 - "<1>" switch-to="Explorer" 39 - "<2>" switch-to="TodoList" 40 - r RandomColor 41 - n EditName 42 - c EditColor 43 - p EditPriority 44 - u EditDue 45 - d EditDescription 46 - f ToggleFinishTask 47 - t NewTask 48 - g NewSubGroup 49 - } 50 - }
+21 -82
Cargo.lock
··· 1240 1240 "strsim", 1241 1241 "terminal_size", 1242 1242 "unicase", 1243 - "unicode-width 0.2.2", 1243 + "unicode-width", 1244 1244 ] 1245 1245 1246 1246 [[package]] ··· 1287 1287 dependencies = [ 1288 1288 "serde", 1289 1289 "termcolor", 1290 - "unicode-width 0.2.2", 1290 + "unicode-width", 1291 1291 ] 1292 1292 1293 1293 [[package]] ··· 2302 2302 "egui_graphs", 2303 2303 "futures", 2304 2304 "human-panic", 2305 - "kdl", 2306 2305 "nucleo-matcher", 2307 2306 "pulldown-cmark", 2308 2307 "rand 0.10.0", 2309 2308 "ratatui", 2310 2309 "ratatui-textarea", 2311 2310 "rayon", 2311 + "ron", 2312 2312 "serde", 2313 2313 "signal-hook 0.4.3", 2314 2314 "strum 0.28.0", ··· 2595 2595 source = "registry+https://github.com/rust-lang/crates.io-index" 2596 2596 checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" 2597 2597 dependencies = [ 2598 - "unicode-width 0.2.2", 2598 + "unicode-width", 2599 2599 ] 2600 2600 2601 2601 [[package]] ··· 2705 2705 "gix-utils", 2706 2706 "itoa", 2707 2707 "thiserror 2.0.18", 2708 - "winnow 0.7.15", 2708 + "winnow", 2709 2709 ] 2710 2710 2711 2711 [[package]] ··· 2786 2786 "smallvec", 2787 2787 "thiserror 2.0.18", 2788 2788 "unicode-bom", 2789 - "winnow 0.7.15", 2789 + "winnow", 2790 2790 ] 2791 2791 2792 2792 [[package]] ··· 3033 3033 "itoa", 3034 3034 "smallvec", 3035 3035 "thiserror 2.0.18", 3036 - "winnow 0.7.15", 3036 + "winnow", 3037 3037 ] 3038 3038 3039 3039 [[package]] ··· 3130 3130 "gix-utils", 3131 3131 "maybe-async", 3132 3132 "thiserror 2.0.18", 3133 - "winnow 0.7.15", 3133 + "winnow", 3134 3134 ] 3135 3135 3136 3136 [[package]] ··· 3162 3162 "gix-validate", 3163 3163 "memmap2", 3164 3164 "thiserror 2.0.18", 3165 - "winnow 0.7.15", 3165 + "winnow", 3166 3166 ] 3167 3167 3168 3168 [[package]] ··· 4056 4056 ] 4057 4057 4058 4058 [[package]] 4059 - name = "kdl" 4060 - version = "6.5.0" 4061 - source = "registry+https://github.com/rust-lang/crates.io-index" 4062 - checksum = "81a29e7b50079ff44549f68c0becb1c73d7f6de2a4ea952da77966daf3d4761e" 4063 - dependencies = [ 4064 - "miette", 4065 - "num", 4066 - "winnow 0.6.24", 4067 - ] 4068 - 4069 - [[package]] 4070 4059 name = "khronos-egl" 4071 4060 version = "6.0.0" 4072 4061 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4379 4368 ] 4380 4369 4381 4370 [[package]] 4382 - name = "miette" 4383 - version = "7.6.0" 4384 - source = "registry+https://github.com/rust-lang/crates.io-index" 4385 - checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" 4386 - dependencies = [ 4387 - "cfg-if", 4388 - "unicode-width 0.1.14", 4389 - ] 4390 - 4391 - [[package]] 4392 4371 name = "migration" 4393 4372 version = "0.1.0" 4394 4373 dependencies = [ ··· 4562 4541 ] 4563 4542 4564 4543 [[package]] 4565 - name = "num" 4566 - version = "0.4.3" 4567 - source = "registry+https://github.com/rust-lang/crates.io-index" 4568 - checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" 4569 - dependencies = [ 4570 - "num-bigint", 4571 - "num-complex", 4572 - "num-integer", 4573 - "num-iter", 4574 - "num-rational", 4575 - "num-traits", 4576 - ] 4577 - 4578 - [[package]] 4579 4544 name = "num-bigint" 4580 4545 version = "0.4.6" 4581 4546 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4643 4608 checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" 4644 4609 dependencies = [ 4645 4610 "autocfg", 4646 - "num-integer", 4647 - "num-traits", 4648 - ] 4649 - 4650 - [[package]] 4651 - name = "num-rational" 4652 - version = "0.4.2" 4653 - source = "registry+https://github.com/rust-lang/crates.io-index" 4654 - checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" 4655 - dependencies = [ 4656 - "num-bigint", 4657 4611 "num-integer", 4658 4612 "num-traits", 4659 4613 ] ··· 5782 5736 "thiserror 2.0.18", 5783 5737 "unicode-segmentation", 5784 5738 "unicode-truncate", 5785 - "unicode-width 0.2.2", 5739 + "unicode-width", 5786 5740 ] 5787 5741 5788 5742 [[package]] ··· 5826 5780 "ratatui-core", 5827 5781 "ratatui-crossterm", 5828 5782 "ratatui-widgets", 5829 - "unicode-width 0.2.2", 5783 + "unicode-width", 5830 5784 ] 5831 5785 5832 5786 [[package]] ··· 5845 5799 "strum 0.27.2", 5846 5800 "time", 5847 5801 "unicode-segmentation", 5848 - "unicode-width 0.2.2", 5802 + "unicode-width", 5849 5803 ] 5850 5804 5851 5805 [[package]] ··· 6018 5972 6019 5973 [[package]] 6020 5974 name = "ron" 6021 - version = "0.12.0" 5975 + version = "0.12.1" 6022 5976 source = "registry+https://github.com/rust-lang/crates.io-index" 6023 - checksum = "fd490c5b18261893f14449cbd28cb9c0b637aebf161cd77900bfdedaff21ec32" 5977 + checksum = "4147b952f3f819eca0e99527022f7d6a8d05f111aeb0a62960c74eb283bec8fc" 6024 5978 dependencies = [ 6025 5979 "bitflags 2.11.0", 6026 5980 "once_cell", ··· 7424 7378 "indexmap", 7425 7379 "toml_datetime 1.0.0+spec-1.1.0", 7426 7380 "toml_parser", 7427 - "winnow 0.7.15", 7381 + "winnow", 7428 7382 ] 7429 7383 7430 7384 [[package]] ··· 7433 7387 source = "registry+https://github.com/rust-lang/crates.io-index" 7434 7388 checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" 7435 7389 dependencies = [ 7436 - "winnow 0.7.15", 7390 + "winnow", 7437 7391 ] 7438 7392 7439 7393 [[package]] ··· 7620 7574 dependencies = [ 7621 7575 "itertools", 7622 7576 "unicode-segmentation", 7623 - "unicode-width 0.2.2", 7577 + "unicode-width", 7624 7578 ] 7625 - 7626 - [[package]] 7627 - name = "unicode-width" 7628 - version = "0.1.14" 7629 - source = "registry+https://github.com/rust-lang/crates.io-index" 7630 - checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 7631 7579 7632 7580 [[package]] 7633 7581 name = "unicode-width" ··· 8862 8810 8863 8811 [[package]] 8864 8812 name = "winnow" 8865 - version = "0.6.24" 8866 - source = "registry+https://github.com/rust-lang/crates.io-index" 8867 - checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a" 8868 - dependencies = [ 8869 - "memchr", 8870 - ] 8871 - 8872 - [[package]] 8873 - name = "winnow" 8874 8813 version = "0.7.15" 8875 8814 source = "registry+https://github.com/rust-lang/crates.io-index" 8876 8815 checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" ··· 9102 9041 "uds_windows", 9103 9042 "uuid", 9104 9043 "windows-sys 0.61.2", 9105 - "winnow 0.7.15", 9044 + "winnow", 9106 9045 "zbus_macros", 9107 9046 "zbus_names", 9108 9047 "zvariant", ··· 9154 9093 checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" 9155 9094 dependencies = [ 9156 9095 "serde", 9157 - "winnow 0.7.15", 9096 + "winnow", 9158 9097 "zvariant", 9159 9098 ] 9160 9099 ··· 9286 9225 "endi", 9287 9226 "enumflags2", 9288 9227 "serde", 9289 - "winnow 0.7.15", 9228 + "winnow", 9290 9229 "zvariant_derive", 9291 9230 "zvariant_utils", 9292 9231 ] ··· 9314 9253 "quote", 9315 9254 "serde", 9316 9255 "syn 2.0.117", 9317 - "winnow 0.7.15", 9256 + "winnow", 9318 9257 ]
+1 -1
Cargo.toml
··· 71 71 tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } 72 72 tracing-error = "0.2.1" 73 73 clap = { version = "4.5.60", features = ["derive", "cargo", "wrap_help", "unicode", "string", "unstable-styles"] } 74 - kdl = "6.5.0" 75 74 dto = {path="./crates/dto"} 76 75 eframe = "0.34.1" 77 76 async-trait = "0.1.89" ··· 81 80 pulldown-cmark = { version = "0.13.3", features = ["simd"] } 82 81 ratatui-textarea = "0.8.0" 83 82 nucleo-matcher = "0.3.1" 83 + ron = "0.12.1" 84 84 85 85 [build-dependencies] 86 86 anyhow = "1.0.102"
+1 -3
crates/dto/tests/zettel.rs
··· 2 2 ActiveModelTrait, ActiveValue::Set, ColorDTO, TagActiveModel, TagEntity, TagModel, 3 3 ZettelActiveModel, ZettelEntity, ZettelModel, 4 4 }; 5 - use sea_orm::{IntoActiveModel, QueryOrder}; 5 + use sea_orm::IntoActiveModel; 6 6 7 7 mod common; 8 8 ··· 56 56 let zettels_for_tag = TagEntity::load() 57 57 .filter_by_nano_id(tag.nano_id.clone()) 58 58 .with(ZettelEntity) 59 - 60 59 .all(&db) 61 - 62 60 .await 63 61 .unwrap(); 64 62
+6 -6
flake.lock
··· 8 8 "rust-analyzer-src": "rust-analyzer-src" 9 9 }, 10 10 "locked": { 11 - "lastModified": 1775115015, 12 - "narHash": "sha256-XO7jmyFupI82Sr1M2tLfsSxslIJwUOjzhFqeffaWyNw=", 11 + "lastModified": 1775287186, 12 + "narHash": "sha256-hYntDpbh8MuiYRmBf/6uHMpDOP2m7L7bXQXboWPg6WM=", 13 13 "owner": "nix-community", 14 14 "repo": "fenix", 15 - "rev": "45f82ed61800d52e27390b70823426045d982c84", 15 + "rev": "2517c7fb1eafc7259bb631267f1e1f813cf5f3bc", 16 16 "type": "github" 17 17 }, 18 18 "original": { ··· 44 44 "rust-analyzer-src": { 45 45 "flake": false, 46 46 "locked": { 47 - "lastModified": 1775045117, 48 - "narHash": "sha256-PLZYhcg3HUZ+lUMUV+JbXs9ExOAYpZC0PAtOVHCgYss=", 47 + "lastModified": 1775228522, 48 + "narHash": "sha256-+6eTD6EAabjow5gdjWRP6aI2UUwOZJEjzzsvvbVu8f8=", 49 49 "owner": "rust-lang", 50 50 "repo": "rust-analyzer", 51 - "rev": "e599ad4fc8861e0401906e4d730f74bfcc530e07", 51 + "rev": "f4b77dc99d9925667246e2887783b79bdc46a50d", 52 52 "type": "github" 53 53 }, 54 54 "original": {
+3
flake.nix
··· 124 124 # Set any environment variables for your dev shell 125 125 env = { 126 126 RUST_SRC_PATH = "${pkgs.rustToolchain}/lib/rustlib/src/rust/library"; 127 + 128 + # project specific vars 127 129 FIL_CONFIG = "./.config"; 128 130 FIL_DATA = "./.data"; 131 + FIL_LOG_LEVEL = "DEBUG"; 129 132 }; 130 133 131 134 # Add any shell logic you want executed any time the environment is activated
+4 -4
src/cli/process.rs
··· 24 24 Workspace::initialize(dir.clone()).await?; 25 25 26 26 // write config that sets the filaments directory to current dir! 27 - let config_kdl = dbg! {Config::generate(&dir)}; 27 + let config_str = dbg! {Config::generate(&dir)}?; 28 28 29 29 // create the config dir 30 30 let config_dir = get_config_dir(); 31 31 32 32 create_dir_all(config_dir).expect("creating the config dir should not error"); 33 33 34 - let mut config_file = File::create(get_config_dir().join("config.kdl")) 34 + let mut config_file = File::create(get_config_dir().join("config.ron")) 35 35 .context("Failed to create config file")?; 36 36 37 - write!(config_file, "{config_kdl}")?; 37 + write!(config_file, "{config_str}")?; 38 38 39 39 println!("wrote config to {config_file:#?}"); 40 40 ··· 44 44 45 45 Self::Zettel(zettel_sub_command) => { 46 46 let conf = Config::parse()?; 47 - let ws = Workspace::instansiate(conf.app_config.workspace).await?; 47 + let ws = Workspace::instansiate(conf.fil_dir).await?; 48 48 49 49 match zettel_sub_command { 50 50 ZettelSubcommand::New { title } => {
-152
src/config.rs
··· 1 - use color_eyre::eyre::Context; 2 - use directories::ProjectDirs; 3 - use kdl::KdlDocument; 4 - use serde::Deserialize; 5 - use std::{ 6 - env::{self, home_dir}, 7 - fmt::Debug, 8 - fs::File, 9 - io::Read, 10 - path::{Path, PathBuf}, 11 - sync::LazyLock, 12 - }; 13 - 14 - use crate::tui::KeyMap; 15 - 16 - /// Project Name: Filaments 17 - pub static PROJECT_NAME: LazyLock<String> = 18 - LazyLock::new(|| env!("CARGO_CRATE_NAME").to_uppercase()); 19 - 20 - /// The OS-agnostic data directory for the project. 21 - pub static DATA_DIRECTORY: LazyLock<Option<PathBuf>> = LazyLock::new(|| { 22 - env::var(format!("{}_DATA", PROJECT_NAME.clone())) 23 - .ok() 24 - .map(PathBuf::from) 25 - }); 26 - 27 - /// The OS-agnostic config directory for the project. 28 - pub static CONFIG_DIRECTORY: LazyLock<Option<PathBuf>> = LazyLock::new(|| { 29 - env::var(format!("{}_CONFIG", PROJECT_NAME.clone())) 30 - .ok() 31 - .map(PathBuf::from) 32 - }); 33 - 34 - const DEFAULT_CONFIG: &str = include_str!("../.config/config.kdl"); 35 - 36 - /// The App Config and Data locations. 37 - #[derive(Clone, Debug, Deserialize, Default)] 38 - #[expect(dead_code)] 39 - pub struct AppConfig { 40 - /// The directory where the single instance of the filaments exists. 41 - pub workspace: PathBuf, 42 - #[serde(default)] 43 - pub data: PathBuf, 44 - #[serde(default)] 45 - pub config: PathBuf, 46 - } 47 - 48 - /// Configuration for the App 49 - #[derive(Debug, Clone)] 50 - pub struct Config { 51 - pub app_config: AppConfig, 52 - pub keymap: KeyMap, 53 - // pub styles: Styles, 54 - } 55 - 56 - impl Config { 57 - /// generates a new config with the provided `filaments_dir` 58 - pub fn generate(filaments_dir: &Path) -> KdlDocument { 59 - 60 - 61 - let mut default_config: KdlDocument = DEFAULT_CONFIG 62 - .parse() 63 - .expect("Default config should always be a valid KDL document."); 64 - 65 - if let Some(node) = default_config 66 - .nodes_mut() 67 - .iter_mut() 68 - .find(|n| n.name().value() == "filaments_dir") 69 - && let Some(entry) = node.entries_mut().get_mut(0) 70 - { 71 - *entry.value_mut() = kdl::KdlValue::String(filaments_dir.to_string_lossy().to_string()); 72 - entry.clear_format(); 73 - } 74 - 75 - default_config 76 - } 77 - 78 - /// Parse the config from `~/.config/filametns` 79 - /// 80 - /// # Errors 81 - /// 82 - /// Will error if the config doesn't exist or if there 83 - /// is a problem parsing it. 84 - pub fn parse() -> color_eyre::Result<Self> { 85 - let config: KdlDocument = { 86 - let file_path = get_config_dir().join("config.kdl"); 87 - 88 - let mut file = File::open(file_path).context("Failed to find file!")?; 89 - 90 - let mut str = String::new(); 91 - 92 - file.read_to_string(&mut str) 93 - .context("Failed to read file!")?; 94 - 95 - str.parse().context("Expected to be valid kdl")? 96 - }; 97 - 98 - let keymap = KeyMap::try_from( 99 - config 100 - .get("keymap") 101 - .expect("Keymap must exist in the config"), 102 - ) 103 - .context("Keymap is not valid!")?; 104 - 105 - let filaments_dir_str = config 106 - .get("filaments_dir") 107 - .expect("config should always have this specified") 108 - .get(0) 109 - .and_then(|arg| arg.as_string()) 110 - .expect("filaments_dir must be a string"); 111 - 112 - let filaments_dir = PathBuf::from(filaments_dir_str) 113 - .canonicalize() 114 - .context("Filaments directory does not exist!")?; 115 - 116 - Ok(Self { 117 - app_config: AppConfig { 118 - workspace: filaments_dir, 119 - data: get_data_dir(), 120 - config: get_config_dir(), 121 - }, 122 - keymap, 123 - }) 124 - } 125 - } 126 - 127 - /// Returns the path to the OS-agnostic data directory. 128 - pub fn get_data_dir() -> PathBuf { 129 - DATA_DIRECTORY.clone().unwrap_or_else(|| { 130 - project_directory().map_or_else( 131 - || PathBuf::from(".").join(".data"), 132 - |proj_dirs| proj_dirs.data_local_dir().to_path_buf(), 133 - ) 134 - }) 135 - } 136 - 137 - /// Returns the path to the OS-agnostic config directory. 138 - pub fn get_config_dir() -> PathBuf { 139 - CONFIG_DIRECTORY.clone().unwrap_or_else(|| { 140 - home_dir().map_or_else( 141 - || PathBuf::from(".").join(".config"), 142 - |mut path| { 143 - path.push(".config"); 144 - path.push("filaments"); 145 - path 146 - }, 147 - ) 148 - }) 149 - } 150 - fn project_directory() -> Option<ProjectDirs> { 151 - ProjectDirs::from("com", "suri-codes", env!("CARGO_PKG_NAME")) 152 - }
+55
src/config/file.rs
··· 1 + use std::{collections::HashMap, path::PathBuf}; 2 + 3 + use serde::{Deserialize, Serialize}; 4 + 5 + use crate::tui::Signal; 6 + 7 + #[derive(Debug, Deserialize, Serialize)] 8 + pub struct RonConfig { 9 + pub directory: PathBuf, 10 + pub global_key_binds: HashMap<String, Signal>, 11 + pub zk: ZkConfig, 12 + pub todo: TodoConfig, 13 + } 14 + 15 + #[derive(Debug, Deserialize, Serialize)] 16 + pub struct ZkConfig { 17 + pub keybinds: HashMap<String, Signal>, 18 + } 19 + 20 + #[derive(Debug, Deserialize, Serialize)] 21 + pub struct TodoConfig { 22 + pub keybinds: HashMap<String, Signal>, 23 + } 24 + 25 + #[cfg(test)] 26 + mod tests { 27 + 28 + use super::*; 29 + use std::{collections::HashMap, path::PathBuf}; 30 + 31 + #[test] 32 + fn fucking_around_with_ron() { 33 + let x = RonConfig { 34 + directory: PathBuf::from("some/notes/dir"), 35 + global_key_binds: HashMap::from([("<Ctrl-C>".to_string(), Signal::Quit)]), 36 + zk: ZkConfig { 37 + keybinds: HashMap::from([ 38 + ("<Enter>".to_string(), Signal::OpenZettel), 39 + ("<Esc>".to_string(), Signal::MoveDown), 40 + ]), 41 + }, 42 + todo: TodoConfig { 43 + keybinds: HashMap::from([ 44 + ("<Space>".to_string(), Signal::NewZettel), 45 + ("<Esc>".to_string(), Signal::MoveUp), 46 + ]), 47 + }, 48 + }; 49 + 50 + let ron_string = ron::ser::to_string_pretty(&x, ron::ser::PrettyConfig::default()) 51 + .expect("failed to serialize"); 52 + 53 + println!("{ron_string}"); 54 + } 55 + }
+253
src/config/keymap.rs
··· 1 + use color_eyre::eyre::{Result, eyre}; 2 + use crossterm::event::{KeyCode, KeyModifiers}; 3 + use std::{ 4 + collections::HashMap, 5 + ops::{Deref, DerefMut}, 6 + }; 7 + use strum::IntoEnumIterator; 8 + 9 + use crossterm::event::KeyEvent; 10 + 11 + use crate::{ 12 + config::file::RonConfig, 13 + tui::{Region, Signal}, 14 + }; 15 + #[derive(Debug, Clone)] 16 + pub struct KeyMap(pub HashMap<Region, HashMap<Vec<KeyEvent>, Signal>>); 17 + 18 + impl TryFrom<&RonConfig> for KeyMap { 19 + type Error = color_eyre::Report; 20 + 21 + fn try_from(value: &RonConfig) -> Result<Self, Self::Error> { 22 + let mut binds = HashMap::new(); 23 + 24 + for region in Region::iter() { 25 + let mut region_binds = HashMap::new(); 26 + 27 + let mut parse_and_insert = |str: &str, bind: &Signal| -> Result<()> { 28 + let key_seq = parse_key_sequence(str).map_err(|e| { 29 + eyre!(format!( 30 + "Failed to parse the following keybind as valid keybind: {e}" 31 + )) 32 + })?; 33 + 34 + region_binds.insert(key_seq, bind.clone()); 35 + Ok(()) 36 + }; 37 + 38 + // first thing we have to do is insert the global binds for this region 39 + 40 + for (str, bind) in &value.global_key_binds { 41 + parse_and_insert(str, bind)?; 42 + } 43 + 44 + // now we insert the region specific binds 45 + for (str, bind) in match region { 46 + Region::Zk => value.zk.keybinds.iter(), 47 + Region::Todo => value.todo.keybinds.iter(), 48 + } { 49 + parse_and_insert(str, bind)?; 50 + } 51 + 52 + binds.insert(region, region_binds); 53 + } 54 + 55 + Ok(Self(binds)) 56 + } 57 + } 58 + 59 + impl Deref for KeyMap { 60 + type Target = HashMap<Region, HashMap<Vec<KeyEvent>, Signal>>; 61 + 62 + fn deref(&self) -> &Self::Target { 63 + &self.0 64 + } 65 + } 66 + 67 + impl DerefMut for KeyMap { 68 + fn deref_mut(&mut self) -> &mut Self::Target { 69 + &mut self.0 70 + } 71 + } 72 + 73 + pub fn parse_key_sequence(raw: &str) -> color_eyre::Result<Vec<KeyEvent>, String> { 74 + if raw.chars().filter(|c| *c == '>').count() != raw.chars().filter(|c| *c == '<').count() { 75 + return Err(format!("Unable to parse `{raw}`")); 76 + } 77 + let raw = if raw.contains("><") { 78 + raw 79 + } else { 80 + let raw = raw.strip_prefix('<').unwrap_or(raw); 81 + 82 + raw.strip_prefix('>').unwrap_or(raw) 83 + }; 84 + 85 + raw.split("><") 86 + .map(|seq| { 87 + seq.strip_prefix('<') 88 + .unwrap_or_else(|| seq.strip_suffix('>').map_or(seq, |s| s)) 89 + }) 90 + .map(parse_key_event) 91 + .collect() 92 + } 93 + 94 + fn parse_key_event(raw: &str) -> color_eyre::Result<KeyEvent, String> { 95 + let raw_lower = raw.to_ascii_lowercase(); 96 + let (remaining, modifiers) = extract_modifiers(&raw_lower); 97 + parse_key_code_with_modifiers(remaining, modifiers) 98 + } 99 + 100 + fn extract_modifiers(raw: &str) -> (&str, KeyModifiers) { 101 + let mut modifiers = KeyModifiers::empty(); 102 + let mut current = raw; 103 + 104 + loop { 105 + match current { 106 + rest if rest.starts_with("ctrl-") => { 107 + modifiers.insert(KeyModifiers::CONTROL); 108 + current = &rest[5..]; 109 + } 110 + rest if rest.starts_with("alt-") => { 111 + modifiers.insert(KeyModifiers::ALT); 112 + current = &rest[4..]; 113 + } 114 + rest if rest.starts_with("shift-") => { 115 + modifiers.insert(KeyModifiers::SHIFT); 116 + current = &rest[6..]; 117 + } 118 + _ => break, // break out of the loop if no known prefix is detected 119 + } 120 + } 121 + 122 + (current, modifiers) 123 + } 124 + 125 + fn parse_key_code_with_modifiers( 126 + raw: &str, 127 + mut modifiers: KeyModifiers, 128 + ) -> color_eyre::Result<KeyEvent, String> { 129 + let c = match raw { 130 + "esc" => KeyCode::Esc, 131 + "enter" => KeyCode::Enter, 132 + "left" => KeyCode::Left, 133 + "right" => KeyCode::Right, 134 + "up" => KeyCode::Up, 135 + "down" => KeyCode::Down, 136 + "home" => KeyCode::Home, 137 + "end" => KeyCode::End, 138 + "pageup" => KeyCode::PageUp, 139 + "pagedown" => KeyCode::PageDown, 140 + "backtab" => { 141 + modifiers.insert(KeyModifiers::SHIFT); 142 + KeyCode::BackTab 143 + } 144 + "backspace" => KeyCode::Backspace, 145 + "delete" => KeyCode::Delete, 146 + "insert" => KeyCode::Insert, 147 + "f1" => KeyCode::F(1), 148 + "f2" => KeyCode::F(2), 149 + "f3" => KeyCode::F(3), 150 + "f4" => KeyCode::F(4), 151 + "f5" => KeyCode::F(5), 152 + "f6" => KeyCode::F(6), 153 + "f7" => KeyCode::F(7), 154 + "f8" => KeyCode::F(8), 155 + "f9" => KeyCode::F(9), 156 + "f10" => KeyCode::F(10), 157 + "f11" => KeyCode::F(11), 158 + "f12" => KeyCode::F(12), 159 + "space" => KeyCode::Char(' '), 160 + "hyphen" | "minuc" => KeyCode::Char('-'), 161 + "tab" => KeyCode::Tab, 162 + c if c.len() == 1 => { 163 + let mut c = c.chars().next().unwrap(); 164 + if modifiers.contains(KeyModifiers::SHIFT) { 165 + c = c.to_ascii_uppercase(); 166 + } 167 + KeyCode::Char(c) 168 + } 169 + _ => return Err(format!("Unable to parse {raw}")), 170 + }; 171 + Ok(KeyEvent::new(c, modifiers)) 172 + } 173 + 174 + #[cfg(test)] 175 + mod test { 176 + // use crossterm::event::{KeyEvent, KeyModifiers}; 177 + // use kdl::KdlNode; 178 + 179 + // use crate::tui::{KeyMap, Region, Signal}; 180 + 181 + use crossterm::event::{KeyEvent, KeyModifiers}; 182 + 183 + use crate::{ 184 + config::{file::RonConfig, keymap::KeyMap}, 185 + tui::{Region, Signal}, 186 + }; 187 + 188 + #[test] 189 + fn test_quit() { 190 + let conf_str = r#" 191 + ( 192 + directory: "./ZettelKasten", 193 + global_key_binds: { 194 + "ctrl-c": Quit, 195 + "ctrl-z": Suspend, 196 + "up": MoveUp, 197 + "down": MoveDown, 198 + }, 199 + zk: ( 200 + keybinds: { 201 + "<Ctrl-n>": NewZettel, 202 + "enter": OpenZettel, 203 + "tab": SwitchTo ( 204 + region: Todo 205 + ), 206 + 207 + }, 208 + ), 209 + todo: ( 210 + keybinds: { 211 + "j": MoveDown, 212 + "k": MoveUp, 213 + "tab": SwitchTo ( 214 + region: Zk 215 + ), 216 + 217 + }, 218 + ), 219 + ) 220 + "#; 221 + 222 + let config: RonConfig = ron::from_str(conf_str).unwrap(); 223 + let keymap: KeyMap = (&config).try_into().unwrap(); 224 + 225 + let map = keymap 226 + .get(&Region::Todo) 227 + .expect("Home region must exist in keymap"); 228 + 229 + let signal = map 230 + .get(&vec![KeyEvent::new_with_kind( 231 + crossterm::event::KeyCode::Char('c'), 232 + KeyModifiers::CONTROL, 233 + crossterm::event::KeyEventKind::Press, 234 + )]) 235 + .expect("Must resolve to a signal"); 236 + 237 + assert_eq!(*signal, Signal::Quit); 238 + 239 + let map = keymap 240 + .get(&Region::Zk) 241 + .expect("Home region must exist in keymap"); 242 + 243 + let signal = map 244 + .get(&vec![KeyEvent::new_with_kind( 245 + crossterm::event::KeyCode::Char('c'), 246 + KeyModifiers::CONTROL, 247 + crossterm::event::KeyEventKind::Press, 248 + )]) 249 + .expect("Must resolve to a signal"); 250 + 251 + assert_eq!(*signal, Signal::Quit); 252 + } 253 + }
+100
src/config/mod.rs
··· 1 + use std::{ 2 + env::{self, home_dir}, 3 + fs::read_to_string, 4 + path::{Path, PathBuf}, 5 + sync::LazyLock, 6 + }; 7 + 8 + use color_eyre::eyre::Result; 9 + use directories::ProjectDirs; 10 + 11 + use crate::config::{file::RonConfig, keymap::KeyMap}; 12 + 13 + mod file; 14 + mod keymap; 15 + 16 + /// Project Name: Filaments 17 + pub static PROJECT_NAME: LazyLock<String> = 18 + LazyLock::new(|| env!("CARGO_CRATE_NAME").to_uppercase()); 19 + 20 + /// The OS-agnostic data directory for the project. 21 + pub static DATA_DIRECTORY: LazyLock<Option<PathBuf>> = LazyLock::new(|| { 22 + env::var(format!("{}_DATA", PROJECT_NAME.clone())) 23 + .ok() 24 + .map(PathBuf::from) 25 + }); 26 + 27 + /// The OS-agnostic config directory for the project. 28 + pub static CONFIG_DIRECTORY: LazyLock<Option<PathBuf>> = LazyLock::new(|| { 29 + env::var(format!("{}_CONFIG", PROJECT_NAME.clone())) 30 + .ok() 31 + .map(PathBuf::from) 32 + }); 33 + 34 + const DEFAULT_CONFIG: &str = include_str!("../../.config/config.ron"); 35 + 36 + #[derive(Debug, Clone)] 37 + pub struct Config { 38 + pub fil_dir: PathBuf, 39 + pub keymap: KeyMap, 40 + } 41 + 42 + impl Config { 43 + /// generates a new config with the provided `filaments_dir` 44 + pub fn generate(fil_dir: &Path) -> Result<String> { 45 + let mut default_conf: RonConfig = ron::from_str(DEFAULT_CONFIG) 46 + .expect("The default config must always be a valid RonConfig"); 47 + 48 + default_conf.directory = fil_dir.canonicalize()?; 49 + 50 + Ok(ron::to_string(&default_conf)?) 51 + } 52 + /// Parse the config from `~/.config/filaments`, but will prioritize 53 + /// `FIL_CONFIG_DIR`. 54 + /// 55 + /// # Errors 56 + /// 57 + /// Will error if the config doesn't exist or if there 58 + /// is a problem parsing it. 59 + pub fn parse() -> Result<Self> { 60 + let ron: RonConfig = { 61 + let file_path = get_config_dir().join("config.ron"); 62 + ron::from_str(&read_to_string(file_path)?)? 63 + }; 64 + 65 + let keymap = KeyMap::try_from(&ron)?; 66 + 67 + Ok(Self { 68 + fil_dir: ron.directory.canonicalize()?, 69 + keymap, 70 + }) 71 + } 72 + } 73 + 74 + /// Returns the path to the OS-agnostic data directory. 75 + pub fn get_data_dir() -> PathBuf { 76 + DATA_DIRECTORY.clone().unwrap_or_else(|| { 77 + project_directory().map_or_else( 78 + || PathBuf::from(".").join(".data"), 79 + |proj_dirs| proj_dirs.data_local_dir().to_path_buf(), 80 + ) 81 + }) 82 + } 83 + 84 + /// Returns the path to the OS-agnostic config directory. 85 + pub fn get_config_dir() -> PathBuf { 86 + CONFIG_DIRECTORY.clone().unwrap_or_else(|| { 87 + home_dir().map_or_else( 88 + || PathBuf::from(".").join(".config"), 89 + |mut path| { 90 + path.push(".config"); 91 + path.push("filaments"); 92 + path 93 + }, 94 + ) 95 + }) 96 + } 97 + 98 + fn project_directory() -> Option<ProjectDirs> { 99 + ProjectDirs::from("com", "suri-codes", env!("CARGO_PKG_NAME")) 100 + }
+2 -2
src/main.rs
··· 41 41 // create the kasten handle 42 42 let kh: KastenHandle = rt.block_on(async { 43 43 let cfg = Config::parse()?; 44 - let ws = Workspace::instansiate(cfg.app_config.workspace).await?; 44 + let ws = Workspace::instansiate(cfg.fil_dir).await?; 45 45 Ok::<KastenHandle, color_eyre::Report>(Arc::new(RwLock::new(Kasten::index(ws).await?))) 46 46 })?; 47 47 48 - debug!("{kh:#?}"); 48 + debug!("Kasten Handle: {kh:#?}"); 49 49 50 50 // then we spawn the tui on its own thread 51 51 let tui_handle = std::thread::spawn({
+13 -5
src/tui/app.rs
··· 6 6 use serde::{Deserialize, Serialize}; 7 7 use strum::{Display, EnumIter}; 8 8 use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender}; 9 - use tracing::debug; 9 + use tracing::{debug, trace}; 10 10 11 11 use crate::{ 12 12 config::Config, 13 - tui::{Event, Tui, components::Zk}, 13 + tui::{Event, Tui, components::Viewport}, 14 14 types::KastenHandle, 15 15 }; 16 16 ··· 39 39 )] 40 40 pub enum Region { 41 41 #[default] 42 - Home, 43 42 Zk, 43 + Todo, 44 44 } 45 45 46 46 impl App { ··· 51 51 Ok(Self { 52 52 tick_rate, 53 53 frame_rate, 54 - components: vec![Box::new(Zk::new(kh.clone()).await?)], 54 + // components: vec![Box::new(Zk::new(kh.clone()).await?)], 55 + components: vec![Box::new(Viewport::new(kh.clone()).await?)], 55 56 should_quit: false, 56 57 should_suspend: false, 57 58 config: Config::parse()?, ··· 109 110 return Ok(()); 110 111 }; 111 112 112 - debug!("received event: {event:?}"); 113 + match event { 114 + Event::Tick | Event::Render => trace!("received event: {event:?}"), 115 + _ => debug!("received event: {event:?}"), 116 + } 113 117 114 118 let signal_tx = self.signal_tx.clone(); 115 119 ··· 194 198 195 199 tui.terminal.clear()?; 196 200 tui.enter()?; 201 + } 202 + 203 + Signal::SwitchTo { region } => { 204 + self.region = region; 197 205 } 198 206 199 207 Signal::Suspend => self.should_suspend = true,
+8 -1
src/tui/components/mod.rs
··· 13 13 14 14 /// The zk component 15 15 mod zk; 16 + pub use zk::*; 17 + 18 + /// The todo component 19 + mod todo; 20 + pub use todo::*; 16 21 17 - pub use zk::*; 22 + /// The main view into the app 23 + mod viewport; 24 + pub use viewport::*; 18 25 19 26 /// `Component` is a trait that represents a visual and interactive element of the user interface. 20 27 ///
+56
src/tui/components/todo/mod.rs
··· 1 + use async_trait::async_trait; 2 + use ratatui::{ 3 + Frame, 4 + layout::{Constraint, Layout, Rect}, 5 + style::{Color, Stylize}, 6 + widgets::Block, 7 + }; 8 + use tokio::sync::mpsc::UnboundedSender; 9 + 10 + use crate::{ 11 + tui::{Signal, components::Component}, 12 + types::KastenHandle, 13 + }; 14 + 15 + #[expect(dead_code)] 16 + pub struct Todo { 17 + signal_tx: Option<UnboundedSender<Signal>>, 18 + kh: KastenHandle, 19 + layouts: Layouts, 20 + } 21 + 22 + impl Todo { 23 + pub fn new(kh: KastenHandle) -> Self { 24 + Self { 25 + kh, 26 + layouts: Layouts::default(), 27 + signal_tx: None, 28 + } 29 + } 30 + } 31 + 32 + #[expect(dead_code)] 33 + struct Layouts { 34 + main: Layout, 35 + } 36 + 37 + impl Default for Layouts { 38 + fn default() -> Self { 39 + Self { 40 + main: Layout::horizontal(vec![Constraint::Percentage(50), Constraint::Percentage(50)]), 41 + } 42 + } 43 + } 44 + 45 + #[async_trait] 46 + impl Component for Todo { 47 + async fn update(&mut self, _signal: Signal) -> color_eyre::Result<Option<Signal>> { 48 + Ok(None) 49 + } 50 + 51 + fn draw(&mut self, frame: &mut Frame, area: Rect) -> color_eyre::Result<()> { 52 + frame.render_widget(Block::new().bg(Color::Red), area); 53 + 54 + Ok(()) 55 + } 56 + }
+115
src/tui/components/viewport/mod.rs
··· 1 + use async_trait::async_trait; 2 + use color_eyre::eyre::Result; 3 + use crossterm::event::KeyEvent; 4 + use ratatui::{ 5 + Frame, 6 + layout::{Constraint, Layout, Rect, Size}, 7 + }; 8 + use tokio::sync::mpsc::UnboundedSender; 9 + use tracing::debug; 10 + 11 + use crate::{ 12 + tui::{ 13 + Signal, 14 + app::Region, 15 + components::{Component, Todo, Zk}, 16 + }, 17 + types::KastenHandle, 18 + }; 19 + 20 + pub struct Viewport<'text> { 21 + signal_tx: Option<UnboundedSender<Signal>>, 22 + #[expect(dead_code)] 23 + kh: KastenHandle, 24 + _layouts: Layouts, 25 + switcher: Switcher<'text>, 26 + active_region: Region, 27 + zk: Zk<'text>, 28 + todo: Todo, 29 + } 30 + 31 + mod switcher; 32 + use switcher::Switcher; 33 + 34 + impl Viewport<'_> { 35 + pub async fn new(kh: KastenHandle) -> Result<Self> { 36 + let mut switcher = Switcher::default(); 37 + switcher.select_region(Region::default()); 38 + 39 + Ok(Self { 40 + signal_tx: None, 41 + _layouts: Layouts::default(), 42 + switcher, 43 + zk: Zk::new(kh.clone()).await?, 44 + todo: Todo::new(kh.clone()), 45 + active_region: Region::default(), 46 + kh, 47 + }) 48 + } 49 + } 50 + 51 + struct Layouts { 52 + _main_switcher: Layout, 53 + } 54 + 55 + impl Default for Layouts { 56 + fn default() -> Self { 57 + Self { 58 + _main_switcher: Layout::vertical(vec![Constraint::Fill(90), Constraint::Min(1)]), 59 + } 60 + } 61 + } 62 + 63 + #[async_trait] 64 + impl Component for Viewport<'_> { 65 + async fn init(&mut self, area: Size) -> color_eyre::Result<()> { 66 + match self.active_region { 67 + Region::Zk => self.zk.init(area).await, 68 + Region::Todo => self.todo.init(area).await, 69 + } 70 + } 71 + 72 + fn register_signal_handler(&mut self, tx: UnboundedSender<Signal>) -> Result<()> { 73 + self.signal_tx = Some(tx.clone()); 74 + self.zk.register_signal_handler(tx.clone())?; 75 + self.todo.register_signal_handler(tx)?; 76 + Ok(()) 77 + } 78 + 79 + async fn update(&mut self, signal: Signal) -> color_eyre::Result<Option<Signal>> { 80 + // switch active region 81 + if let Signal::SwitchTo { region } = signal { 82 + self.active_region = region; 83 + self.switcher.select_region(region); 84 + debug!("active region switched to : {region}"); 85 + } 86 + 87 + match self.active_region { 88 + Region::Zk => self.zk.update(signal).await, 89 + Region::Todo => self.todo.update(signal).await, 90 + } 91 + } 92 + 93 + async fn handle_key_event(&mut self, key: KeyEvent) -> color_eyre::Result<Option<Signal>> { 94 + match self.active_region { 95 + Region::Zk => self.zk.handle_key_event(key).await, 96 + Region::Todo => self.todo.handle_key_event(key).await, 97 + } 98 + } 99 + 100 + fn draw(&mut self, frame: &mut Frame, area: Rect) -> color_eyre::Result<()> { 101 + // figure out how we are to do this after 102 + // let (main_layout, _switcher_layout) = { 103 + // let rects = self.layouts.main_switcher.split(area); 104 + // (rects[0], rects[1]) 105 + // }; 106 + 107 + match self.active_region { 108 + Region::Zk => self.zk.draw(frame, area), 109 + Region::Todo => self.todo.draw(frame, area), 110 + }?; 111 + 112 + // frame.render_widget(self.switcher.clone(), area); 113 + Ok(()) 114 + } 115 + }
+68
src/tui/components/viewport/switcher.rs
··· 1 + use ratatui::{ 2 + layout::{Constraint, Layout}, 3 + style::{Color, Style}, 4 + text::{Line, Span}, 5 + widgets::Widget, 6 + }; 7 + use strum::IntoEnumIterator; 8 + 9 + use crate::tui::app::Region; 10 + 11 + #[derive(Debug, Clone)] 12 + pub struct Switcher<'text> { 13 + line: Line<'text>, 14 + layouts: Layouts, 15 + } 16 + 17 + impl Switcher<'_> { 18 + pub fn select_region(&mut self, region: Region) { 19 + self.line = Region::iter() 20 + .map(|r| { 21 + Span::from(format!(" {r} ")).style({ 22 + if r == region { 23 + Style::new().black().on_blue() 24 + } else { 25 + Style::new().black().on_gray() 26 + } 27 + }) 28 + }) 29 + .collect::<Line>(); 30 + } 31 + } 32 + 33 + #[derive(Debug, Clone)] 34 + struct Layouts { 35 + overlay: Layout, 36 + } 37 + 38 + impl Default for Layouts { 39 + fn default() -> Self { 40 + Self { 41 + overlay: Layout::vertical(vec![Constraint::Fill(99), Constraint::Min(1)]), 42 + } 43 + } 44 + } 45 + 46 + impl Default for Switcher<'_> { 47 + fn default() -> Self { 48 + let line = Region::iter() 49 + .map(|r| Span::from(format!(" {r} ")).style(Style::default().bg(Color::DarkGray))) 50 + .collect::<Line>(); 51 + 52 + Self { 53 + line, 54 + layouts: Layouts::default(), 55 + } 56 + } 57 + } 58 + 59 + impl Widget for Switcher<'_> { 60 + fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) 61 + where 62 + Self: Sized, 63 + { 64 + let rect = self.layouts.overlay.split(area)[1]; 65 + 66 + self.line.render(rect, buf); 67 + } 68 + }
+5 -2
src/tui/components/zk/mod.rs
··· 100 100 101 101 let kt = kh.read().await; 102 102 103 + info!("{selected_zettel:#?}"); 104 + info!("{kt:#?}"); 105 + 103 106 let zettel = kt 104 107 .get_node_by_zettel_id(selected_zettel) 105 - .expect("") 108 + .expect("kasten should have the selected zettel") 106 109 .payload(); 107 110 108 111 let preview = Preview::from( ··· 315 318 async fn handle_key_event(&mut self, key: KeyEvent) -> color_eyre::Result<Option<Signal>> { 316 319 // NOTE: this is hardcoded for now, but I honestly think people should not 317 320 // be able to change these binds, opinionated software or something... 318 - if !(key.code.is_up() || key.code.is_down() || key.code.is_enter()) { 321 + if !(key.code.is_up() || key.code.is_down() || key.code.is_enter() || key.code.is_tab()) { 319 322 self.search.query.input(key); 320 323 self.update_with_respect_to_query().await?; 321 324 }
+2 -2
src/tui/components/zk/preview.rs
··· 1 - use ratatui::{style::Style, text::Text, widgets::Widget}; 1 + use ratatui::{text::Text, widgets::Widget}; 2 2 3 3 #[derive(Debug, Clone)] 4 4 pub struct Preview<'text> { ··· 18 18 where 19 19 Self: Sized, 20 20 { 21 - self.content.style(Style::new()).render(area, buf); 21 + self.content.render(area, buf); 22 22 } 23 23 }
-208
src/tui/keymap.rs
··· 1 - use std::{ collections::HashMap, 2 - ops::{Deref, DerefMut}, 3 - }; 4 - 5 - use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; 6 - use kdl::KdlNode; 7 - use strum::IntoEnumIterator; 8 - 9 - use crate::tui::{Signal, app::Region}; 10 - 11 - #[derive(Debug, Clone)] 12 - pub struct KeyMap(pub HashMap<Region, HashMap<Vec<KeyEvent>, Signal>>); 13 - 14 - impl TryFrom<&KdlNode> for KeyMap { 15 - type Error = color_eyre::Report; 16 - 17 - fn try_from(value: &KdlNode) -> std::result::Result<Self, Self::Error> { 18 - let mut all_binds = HashMap::new(); 19 - 20 - for region in Region::iter() { 21 - let mut region_binds = HashMap::new(); 22 - let Some(binds) = value 23 - .children() 24 - .expect("Keymap must have children.") 25 - .get(&region.to_string()) 26 - else { 27 - continue; 28 - }; 29 - 30 - // now we iter through the things children 31 - for child in binds.iter_children() { 32 - let key_combo_str = child.name().to_string(); 33 - let key_combo_str = key_combo_str.trim(); 34 - 35 - let signal_str = child 36 - .entries() 37 - .first() 38 - .expect("A bind must map to an entry") 39 - .to_string(); 40 - let signal_str = signal_str.trim(); 41 - 42 - let signal: Signal = signal_str.parse().expect("Must be a \"bindable\" Signal"); 43 - let key_combo = parse_key_sequence(key_combo_str).unwrap(); 44 - 45 - let _ = region_binds.insert(key_combo, signal); 46 - } 47 - 48 - let _ = all_binds.insert(region, region_binds); 49 - } 50 - 51 - Ok(Self(all_binds)) 52 - } 53 - } 54 - 55 - impl Deref for KeyMap { 56 - type Target = HashMap<Region, HashMap<Vec<KeyEvent>, Signal>>; 57 - 58 - fn deref(&self) -> &Self::Target { 59 - &self.0 60 - } 61 - } 62 - 63 - impl DerefMut for KeyMap { 64 - fn deref_mut(&mut self) -> &mut Self::Target { 65 - &mut self.0 66 - } 67 - } 68 - 69 - pub fn parse_key_sequence(raw: &str) -> color_eyre::Result<Vec<KeyEvent>, String> { 70 - if raw.chars().filter(|c| *c == '>').count() != raw.chars().filter(|c| *c == '<').count() { 71 - return Err(format!("Unable to parse `{raw}`")); 72 - } 73 - let raw = if raw.contains("><") { 74 - raw 75 - } else { 76 - let raw = raw.strip_prefix('<').unwrap_or(raw); 77 - 78 - raw.strip_prefix('>').unwrap_or(raw) 79 - }; 80 - 81 - raw.split("><") 82 - .map(|seq| { 83 - seq.strip_prefix('<') 84 - .unwrap_or_else(|| seq.strip_suffix('>').map_or(seq, |s| s)) 85 - }) 86 - .map(parse_key_event) 87 - .collect() 88 - } 89 - 90 - fn parse_key_event(raw: &str) -> color_eyre::Result<KeyEvent, String> { 91 - let raw_lower = raw.to_ascii_lowercase(); 92 - let (remaining, modifiers) = extract_modifiers(&raw_lower); 93 - parse_key_code_with_modifiers(remaining, modifiers) 94 - } 95 - 96 - fn extract_modifiers(raw: &str) -> (&str, KeyModifiers) { 97 - let mut modifiers = KeyModifiers::empty(); 98 - let mut current = raw; 99 - 100 - loop { 101 - match current { 102 - rest if rest.starts_with("ctrl-") => { 103 - modifiers.insert(KeyModifiers::CONTROL); 104 - current = &rest[5..]; 105 - } 106 - rest if rest.starts_with("alt-") => { 107 - modifiers.insert(KeyModifiers::ALT); 108 - current = &rest[4..]; 109 - } 110 - rest if rest.starts_with("shift-") => { 111 - modifiers.insert(KeyModifiers::SHIFT); 112 - current = &rest[6..]; 113 - } 114 - _ => break, // break out of the loop if no known prefix is detected 115 - } 116 - } 117 - 118 - (current, modifiers) 119 - } 120 - 121 - fn parse_key_code_with_modifiers( 122 - raw: &str, 123 - mut modifiers: KeyModifiers, 124 - ) -> color_eyre::Result<KeyEvent, String> { 125 - let c = match raw { 126 - "esc" => KeyCode::Esc, 127 - "enter" => KeyCode::Enter, 128 - "left" => KeyCode::Left, 129 - "right" => KeyCode::Right, 130 - "up" => KeyCode::Up, 131 - "down" => KeyCode::Down, 132 - "home" => KeyCode::Home, 133 - "end" => KeyCode::End, 134 - "pageup" => KeyCode::PageUp, 135 - "pagedown" => KeyCode::PageDown, 136 - "backtab" => { 137 - modifiers.insert(KeyModifiers::SHIFT); 138 - KeyCode::BackTab 139 - } 140 - "backspace" => KeyCode::Backspace, 141 - "delete" => KeyCode::Delete, 142 - "insert" => KeyCode::Insert, 143 - "f1" => KeyCode::F(1), 144 - "f2" => KeyCode::F(2), 145 - "f3" => KeyCode::F(3), 146 - "f4" => KeyCode::F(4), 147 - "f5" => KeyCode::F(5), 148 - "f6" => KeyCode::F(6), 149 - "f7" => KeyCode::F(7), 150 - "f8" => KeyCode::F(8), 151 - "f9" => KeyCode::F(9), 152 - "f10" => KeyCode::F(10), 153 - "f11" => KeyCode::F(11), 154 - "f12" => KeyCode::F(12), 155 - "space" => KeyCode::Char(' '), 156 - "hyphen" | "minuc" => KeyCode::Char('-'), 157 - "tab" => KeyCode::Tab, 158 - c if c.len() == 1 => { 159 - let mut c = c.chars().next().unwrap(); 160 - if modifiers.contains(KeyModifiers::SHIFT) { 161 - c = c.to_ascii_uppercase(); 162 - } 163 - KeyCode::Char(c) 164 - } 165 - _ => return Err(format!("Unable to parse {raw}")), 166 - }; 167 - Ok(KeyEvent::new(c, modifiers)) 168 - } 169 - 170 - #[cfg(test)] 171 - mod test { 172 - use crossterm::event::{KeyEvent, KeyModifiers}; 173 - use kdl::KdlNode; 174 - 175 - use crate::tui::{KeyMap, Signal, app::Region}; 176 - 177 - #[test] 178 - fn test_quit_in_home_region() { 179 - let keymap_str = " 180 - keymap { 181 - Home { 182 - q Quit 183 - <Ctrl-C> Quit 184 - } 185 - } 186 - "; 187 - 188 - let kdl: &KdlNode = &keymap_str 189 - .parse() 190 - .expect("Keymap_str should be a valid KDL document"); 191 - 192 - let keymap: KeyMap = kdl.try_into().expect("Must be a valid keymap"); 193 - 194 - let map = keymap 195 - .get(&Region::Home) 196 - .expect("Home region must exist in keymap"); 197 - 198 - let signal = map 199 - .get(&vec![KeyEvent::new_with_kind( 200 - crossterm::event::KeyCode::Char('q'), 201 - KeyModifiers::empty(), 202 - crossterm::event::KeyEventKind::Press, 203 - )]) 204 - .expect("Must resolve to a signal"); 205 - 206 - assert_eq!(*signal, Signal::Quit); 207 - } 208 - }
+1 -4
src/tui/mod.rs
··· 1 1 /// The tui app 2 2 mod app; 3 3 pub use app::App as TuiApp; 4 + pub use app::Region; 4 5 5 6 /// Tui components 6 7 mod components; ··· 9 10 mod raw_tui; 10 11 pub use raw_tui::Event; 11 12 pub use raw_tui::Tui; 12 - 13 - /// Keymap for mapping keybinds to regions 14 - mod keymap; 15 - pub use keymap::KeyMap; 16 13 17 14 /// Singals for commands needing to be processed 18 15 mod signal;
+6
src/tui/signal.rs
··· 5 5 6 6 use serde::{Deserialize, Serialize}; 7 7 8 + use crate::tui::Region; 9 + 8 10 /// The varying signals that can be emitted. 9 11 #[derive(Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize)] 10 12 pub enum Signal { ··· 17 19 ClearScreen, 18 20 Error(String), 19 21 Help, 22 + 23 + SwitchTo { 24 + region: Region, 25 + }, 20 26 21 27 // movement 22 28 MoveDown,
+17 -1
src/types/kasten.rs
··· 9 9 use rayon::iter::{ParallelBridge as _, ParallelIterator as _}; 10 10 use std::{cmp::max, collections::HashMap, path::Path, sync::Arc}; 11 11 use tokio::sync::RwLock; 12 + use tracing::{debug, error}; 12 13 13 14 use crate::types::Workspace; 14 15 ··· 56 57 .map(|entry| entry.path()) 57 58 .collect::<Vec<_>>(); 58 59 60 + debug!( 61 + "indexing the following paths {paths:#?} at root {:#?}", 62 + ws.root 63 + ); 64 + 59 65 let zettel_tasks = paths 60 66 .into_iter() 61 67 .map(|path| { ··· 68 74 let zettels = futures::future::join_all(zettel_tasks) 69 75 .await 70 76 .into_iter() 71 - .filter_map(|result| result.ok()?.ok()) 77 + .filter_map(|result| { 78 + result 79 + .inspect_err(|e| error!("Failed to join on zettel task parsing: {e:#?}")) 80 + .ok()? 81 + .inspect_err(|e| error!("Failed to parse file into zettel: {e:#?}")) 82 + .ok() 83 + }) 72 84 .collect::<Vec<Zettel>>(); 85 + 86 + debug!("parsed zettels: {zettels:#?}"); 73 87 74 88 // capacity! 75 89 let mut graph: ZkGraph = ZkGraph::from(&StableGraph::with_capacity( ··· 96 110 graph.add_edge(*src, *dst, link.clone()); 97 111 } 98 112 } 113 + 114 + debug!("parsed graph: {graph:#?}"); 99 115 100 116 Ok(Self { 101 117 _private: (),