|
| 1 | +mod key; |
| 2 | +mod key_str; |
| 3 | +#[cfg(test)] |
| 4 | +mod test; |
| 5 | + |
| 6 | +use core::fmt; |
| 7 | +use std::{collections::BTreeMap, fs::read_to_string, result}; |
| 8 | + |
| 9 | +use crossterm::event::{KeyCode, KeyModifiers}; |
| 10 | +use etcetera::{choose_base_strategy, home_dir, BaseStrategy}; |
| 11 | +use key::Key; |
| 12 | +use serde::Deserialize; |
| 13 | + |
| 14 | +use crate::app::{Action, ScrollAmount}; |
| 15 | + |
| 16 | +#[derive(Clone, Debug, PartialEq, Deserialize)] |
| 17 | +pub struct KeyBinding { |
| 18 | + pub key: Key, |
| 19 | + pub command: Command, |
| 20 | +} |
| 21 | + |
| 22 | +impl fmt::Display for KeyBinding { |
| 23 | + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
| 24 | + write!(f, "{}", self.key) |
| 25 | + } |
| 26 | +} |
| 27 | + |
| 28 | +pub const CTRL_C: KeyBinding = KeyBinding { |
| 29 | + key: Key { |
| 30 | + modifiers: KeyModifiers::CONTROL, |
| 31 | + code: KeyCode::Char('c'), |
| 32 | + }, |
| 33 | + command: Command::Quit, |
| 34 | +}; |
| 35 | + |
| 36 | +#[derive(Clone, Debug, PartialEq, Deserialize)] |
| 37 | +#[serde(rename_all = "snake_case")] |
| 38 | +pub enum Command { |
| 39 | + ScrollUp, |
| 40 | + ScrollDown, |
| 41 | + PageUp, |
| 42 | + PageDown, |
| 43 | + Next, |
| 44 | + Previous, |
| 45 | + Quit, |
| 46 | + Select, |
| 47 | + ToggleHelp, |
| 48 | + ToggleMode, |
| 49 | + ToggleVaultSelector, |
| 50 | +} |
| 51 | + |
| 52 | +impl From<Command> for Action { |
| 53 | + fn from(value: Command) -> Self { |
| 54 | + match value { |
| 55 | + Command::ScrollUp => Action::ScrollUp(ScrollAmount::One), |
| 56 | + Command::ScrollDown => Action::ScrollDown(ScrollAmount::One), |
| 57 | + Command::PageUp => Action::ScrollUp(ScrollAmount::HalfPage), |
| 58 | + Command::PageDown => Action::ScrollDown(ScrollAmount::HalfPage), |
| 59 | + Command::Next => Action::Next, |
| 60 | + Command::Previous => Action::Prev, |
| 61 | + Command::Quit => Action::Quit, |
| 62 | + Command::Select => Action::Select, |
| 63 | + Command::ToggleHelp => Action::ToggleHelp, |
| 64 | + Command::ToggleMode => Action::ToggleMode, |
| 65 | + Command::ToggleVaultSelector => Action::ToggleVaultSelector, |
| 66 | + } |
| 67 | + } |
| 68 | +} |
| 69 | + |
| 70 | +impl From<&KeyBinding> for Action { |
| 71 | + fn from(value: &KeyBinding) -> Self { |
| 72 | + value.command.clone().into() |
| 73 | + } |
| 74 | +} |
| 75 | + |
| 76 | +#[derive(Clone, Debug, Default, PartialEq)] |
| 77 | +pub struct Config { |
| 78 | + pub key_bindings: BTreeMap<String, KeyBinding>, |
| 79 | +} |
| 80 | + |
| 81 | +impl Config { |
| 82 | + /// Takes self and another config and merges the `key_bindings` together overwriting the |
| 83 | + /// existing entries with the value from another config. |
| 84 | + pub(crate) fn merge(&self, config: Config) -> Config { |
| 85 | + config |
| 86 | + .key_bindings |
| 87 | + .into_iter() |
| 88 | + .fold(self.key_bindings.clone(), |mut acc, (key, value)| { |
| 89 | + acc.entry(key) |
| 90 | + .and_modify(|v| *v = value.clone()) |
| 91 | + .or_insert(value); |
| 92 | + acc |
| 93 | + }) |
| 94 | + .into() |
| 95 | + } |
| 96 | + |
| 97 | + pub fn get_key_binding(&self, key: Key) -> Option<&KeyBinding> { |
| 98 | + self.key_bindings.get(&key.to_string()) |
| 99 | + } |
| 100 | +} |
| 101 | + |
| 102 | +impl<const N: usize> From<[KeyBinding; N]> for Config { |
| 103 | + fn from(value: [KeyBinding; N]) -> Self { |
| 104 | + Self { |
| 105 | + key_bindings: BTreeMap::from( |
| 106 | + value.map(|key_binding| (key_binding.to_string(), key_binding)), |
| 107 | + ), |
| 108 | + } |
| 109 | + } |
| 110 | +} |
| 111 | + |
| 112 | +impl From<BTreeMap<String, KeyBinding>> for Config { |
| 113 | + fn from(value: BTreeMap<String, KeyBinding>) -> Self { |
| 114 | + Self { |
| 115 | + key_bindings: value, |
| 116 | + } |
| 117 | + } |
| 118 | +} |
| 119 | + |
| 120 | +impl From<TomlConfig> for Config { |
| 121 | + fn from(value: TomlConfig) -> Self { |
| 122 | + Self { |
| 123 | + key_bindings: value |
| 124 | + .key_bindings |
| 125 | + .into_iter() |
| 126 | + .map(|key_binding| (key_binding.key.to_string(), key_binding)) |
| 127 | + .collect(), |
| 128 | + } |
| 129 | + } |
| 130 | +} |
| 131 | + |
| 132 | +#[derive(Clone, Debug, PartialEq, Deserialize, Default)] |
| 133 | +struct TomlConfig { |
| 134 | + #[serde(default)] |
| 135 | + key_bindings: Vec<KeyBinding>, |
| 136 | +} |
| 137 | + |
| 138 | +/// Finds and reads the user configuration file in order of priority. |
| 139 | +/// |
| 140 | +/// The function checks two standard locations: |
| 141 | +/// |
| 142 | +/// 1. Directly under the user's home directory: `$HOME/.basalt.toml` |
| 143 | +/// 2. Under the user's config directory: `$HOME/.config/basalt/config.toml` |
| 144 | +/// |
| 145 | +/// It first attempts to find the config file in the home directory. If not found, it then checks |
| 146 | +/// the config directory. |
| 147 | +fn read_user_config() -> Option<TomlConfig> { |
| 148 | + let config_path = home_dir() |
| 149 | + .map(|home_dir| home_dir.join(".basalt.toml")) |
| 150 | + .or_else(|_| { |
| 151 | + choose_base_strategy().map(|strategy| strategy.config_dir().join("basalt/config.toml")) |
| 152 | + }) |
| 153 | + .ok()?; |
| 154 | + |
| 155 | + // TODO: Parsing errors related to the configuration file should ideally be surfaced as warnings. |
| 156 | + // This is pending a solution for toast notifications and proper warning/error logging. |
| 157 | + toml::from_str::<TomlConfig>(read_to_string(config_path).unwrap_or_default().as_str()).ok() |
| 158 | +} |
| 159 | + |
| 160 | +pub fn read_config() -> Result<Config> { |
| 161 | + let default_config: Config = |
| 162 | + toml::from_str::<TomlConfig>(include_str!("../../config.toml"))?.into(); |
| 163 | + |
| 164 | + let constant_config: Config = BTreeMap::from([(CTRL_C.to_string(), CTRL_C)]).into(); |
| 165 | + |
| 166 | + Ok(default_config |
| 167 | + .merge(read_user_config().unwrap_or_default().into()) |
| 168 | + .merge(constant_config)) |
| 169 | +} |
| 170 | + |
| 171 | +/// A [`std::result::Result`] type for fallible operations in [`crate::config`]. |
| 172 | +/// |
| 173 | +/// For convenience of use and to avoid writing [`Error`] directly. All fallible operations return |
| 174 | +/// [`Error`] as the error variant. |
| 175 | +pub type Result<T> = result::Result<T, ConfigError>; |
| 176 | + |
| 177 | +/// Error type for fallible operations in this [`crate`]. |
| 178 | +/// |
| 179 | +/// Implements [`std::error::Error`] via [thiserror](https://docs.rs/thiserror). |
| 180 | +#[derive(thiserror::Error, Debug)] |
| 181 | +pub enum ConfigError { |
| 182 | + /// TOML (De)serialization error, from [`toml::de::Error`]. |
| 183 | + #[error("Toml (de)serialization error: {0}")] |
| 184 | + Toml(#[from] toml::de::Error), |
| 185 | + |
| 186 | + #[error("Invalid key binding: {0}")] |
| 187 | + InvalidKeybinding(String), |
| 188 | + |
| 189 | + #[error("Invalid key code: {0}")] |
| 190 | + InvalidKeyCode(String), |
| 191 | +} |
| 192 | + |
| 193 | +// let mut config = toml::toml! { |
| 194 | +// key_bindings = [ |
| 195 | +// { key = "q", command = "quit" }, |
| 196 | +// { key = "?", command = "toggle_help" }, |
| 197 | +// { key = " ", command = "toggle_vault_selector" }, |
| 198 | +// { key = "t", command = "toggle_mode" }, |
| 199 | +// { key = "up", command = "scroll_up" }, |
| 200 | +// { key = "down", command = "scroll_down" }, |
| 201 | +// { key = "ctrl+u", command = "page_up" }, |
| 202 | +// { key = "ctrl+d", command = "page_down" }, |
| 203 | +// { key = "k", command = "previous" }, |
| 204 | +// { key = "j", command = "next" }, |
| 205 | +// { key = "enter", command = "select" } |
| 206 | +// ] |
| 207 | +// }; |
0 commit comments