Skip to content

Commit 49a02cf

Browse files
committed
wip
1 parent 7f01a1d commit 49a02cf

File tree

10 files changed

+437
-364
lines changed

10 files changed

+437
-364
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

basalt/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ toml = "0.8.22"
1919
etcetera = "0.10.0"
2020
pulldown-cmark = "0.13.0"
2121
textwrap = "0.16.2"
22+
thiserror = "2.0.11"
2223

2324
[dev-dependencies]
2425
indoc = "2"

basalt/src/app.rs

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use ratatui::{
1111
use std::{cell::RefCell, io::Result, marker::PhantomData};
1212

1313
use crate::{
14-
config::{Config, Key},
14+
config::{read_config, Config},
1515
help_modal::{HelpModal, HelpModalState},
1616
sidepanel::{SidePanel, SidePanelState},
1717
start::{StartScreen, StartState},
@@ -113,7 +113,6 @@ impl Default for Screen<'_> {
113113

114114
#[derive(Debug, Default, Clone, PartialEq)]
115115
pub struct AppState<'a> {
116-
pub config: Config,
117116
pub help_modal: Option<HelpModalState>,
118117
pub vault_selector_modal: Option<VaultSelectorModalState<'a>>,
119118
pub size: Size,
@@ -124,6 +123,7 @@ pub struct AppState<'a> {
124123

125124
pub struct App<'a> {
126125
pub state: AppState<'a>,
126+
pub config: Config,
127127
terminal: RefCell<DefaultTerminal>,
128128
}
129129

@@ -222,7 +222,6 @@ impl<'a> App<'a> {
222222
let size = terminal.size()?;
223223

224224
let state = AppState {
225-
config: Config::build(),
226225
screen: Screen::Start(Start {
227226
start_state: StartState::new(&version, size, vaults),
228227
}),
@@ -233,6 +232,7 @@ impl<'a> App<'a> {
233232
};
234233

235234
App {
235+
config: read_config().unwrap(),
236236
state: state.clone(),
237237
terminal: RefCell::new(terminal),
238238
}
@@ -573,14 +573,9 @@ impl<'a> App<'a> {
573573
}
574574

575575
fn handle_press_key_event(&self, key_event: &KeyEvent) -> Option<Action> {
576-
let key_binding: Key = key_event.into();
577-
578-
self.state
579-
.config
580-
.keymap
581-
.get(&key_binding)
582-
.cloned()
583-
.or_else(|| key_binding.eq(&Key::CTRLC).then_some(Action::Quit))
576+
self.config
577+
.get_key_binding(key_event.into())
578+
.map(|key_binding| key_binding.into())
584579
}
585580

586581
fn draw(&self, state: &AppState<'a>) -> Result<()> {

basalt/src/config.rs

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
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

Comments
 (0)