1use crate::{
3 buffer::Buffer,
4 editor::{Action, Actions},
5 key::{Arrow, Input},
6 syntax::TK_DEFAULT,
7 term::{Color, Styles},
8 trie::Trie,
9 util::parent_dir_containing,
10};
11use serde::{Deserialize, Deserializer, de};
12use std::{collections::HashMap, env, fs, iter::successors, ops::Deref, path::Path};
13use tracing::error;
14
15mod raw;
16
17use raw::{RawColorScheme, RawConfig};
18
19pub const DEFAULT_CONFIG: &str = include_str!("../../data/config.toml");
20
21pub(crate) fn config_path() -> String {
22 let home = env::var("HOME").unwrap();
23 format!("{home}/.ad/config.toml")
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct Config {
29 pub editor: EditorConfig,
30 pub filesystem: FsysConfig,
31 pub tree_sitter: TsConfig,
32 pub colorscheme: ColorScheme,
33 pub filetypes: HashMap<String, FtypeConfig>,
34 pub keys: KeyBindings,
35}
36
37impl Default for Config {
38 fn default() -> Self {
39 RawConfig::default()
40 .resolve(&config_path(), "")
41 .expect("default config is broken")
42 }
43}
44
45impl Deref for Config {
46 type Target = EditorConfig;
47
48 fn deref(&self) -> &Self::Target {
49 &self.editor
50 }
51}
52
53impl Config {
54 pub fn try_load_from_path(path: &str, home: &str) -> Result<Self, String> {
58 match fs::read_to_string(path) {
59 Ok(s) => Self::try_load_from_str(&s, path, home),
60 Err(e) => Err(format!("unable to load config file: {e}")),
61 }
62 }
63
64 pub fn try_load_from_str(s: &str, path: &str, home: &str) -> Result<Self, String> {
68 let raw: RawConfig = match toml::from_str(s) {
69 Ok(cfg) => cfg,
70 Err(e) => {
71 error!("malformed config file: {e}");
72 return Err(format!("malformed config file: {e}"));
73 }
74 };
75 let res = raw.resolve(path, home);
76 if let Err(err) = res.as_ref() {
77 error!("malformed config file: {err}");
78 }
79
80 res
81 }
82
83 pub fn try_load() -> Result<Self, String> {
90 let home = env::var("HOME").unwrap();
91 let path = config_path();
92
93 if matches!(fs::exists(&path), Ok(false))
94 && fs::create_dir_all(format!("{home}/.ad")).is_ok()
95 && let Err(e) = fs::write(&path, DEFAULT_CONFIG)
96 {
97 error!("unable to write default config file: {e}");
98 }
99
100 Self::try_load_from_path(&path, &home)
101 }
102}
103
104pub fn ftype_config_for_path_and_first_line<'a>(
107 path: &Path,
108 first_line: &str,
109 filetypes: &'a HashMap<String, FtypeConfig>,
110) -> Option<(&'a String, &'a FtypeConfig)> {
111 let fname = path.file_name()?.to_string_lossy();
112 let os_ext = path.extension().unwrap_or_default();
113 let ext = os_ext.to_str().unwrap_or_default();
114
115 filetypes.iter().find(|(_, c)| {
116 c.filenames.iter().any(|f| *f == fname)
117 || c.extensions.iter().any(|e| e == ext)
118 || c.first_lines.iter().any(|l| first_line.starts_with(l))
119 })
120}
121
122#[derive(Debug, Clone, PartialEq, Eq)]
124pub struct EditorConfig {
125 pub show_splash: bool,
126 pub tabstop: usize,
127 pub expand_tab: bool,
128 pub match_indent: bool,
129 pub lsp_autostart: bool,
130 pub status_timeout: u64,
131 pub double_click_ms: u64,
132 pub minibuffer_lines: usize,
133 pub find_command: String,
134}
135
136impl Default for EditorConfig {
137 fn default() -> Self {
138 Self {
139 show_splash: true,
140 tabstop: 4,
141 expand_tab: true,
142 match_indent: true,
143 lsp_autostart: true,
144 status_timeout: 3,
145 double_click_ms: 200,
146 minibuffer_lines: 8,
147 find_command: "fd --hidden".to_string(),
148 }
149 }
150}
151
152#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
154pub struct FsysConfig {
155 pub enabled: bool,
156 pub auto_mount: bool,
157}
158
159impl Default for FsysConfig {
160 fn default() -> Self {
161 Self {
162 enabled: true,
163 auto_mount: false,
164 }
165 }
166}
167
168#[derive(Debug, Clone, PartialEq, Eq)]
173pub struct ColorScheme {
174 pub bg: Color,
175 pub fg: Color,
176 pub bar_bg: Color,
177 pub signcol_fg: Color,
178 pub minibuffer_hl: Color,
179 pub syntax: HashMap<String, Styles>,
180}
181
182impl Default for ColorScheme {
183 fn default() -> Self {
184 RawColorScheme::default().resolve(&mut Vec::new())
185 }
186}
187
188impl ColorScheme {
189 pub fn styles_for(&self, tag: &str) -> &Styles {
200 successors(Some(tag), |s| Some(s.rsplit_once('.')?.0))
201 .find_map(|k| self.syntax.get(k))
202 .or(self.syntax.get(TK_DEFAULT))
203 .expect("to have default styles")
204 }
205}
206
207#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
208pub struct TsConfig {
209 pub parser_dir: String,
210 pub syntax_query_dir: String,
211}
212
213impl Default for TsConfig {
214 fn default() -> Self {
215 let home = env::var("HOME").unwrap();
216
217 TsConfig {
218 parser_dir: format!("{home}/.ad/tree-sitter/parsers"),
219 syntax_query_dir: format!("{home}/.ad/tree-sitter/queries"),
220 }
221 }
222}
223
224#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize)]
225pub struct FtypeConfig {
226 #[serde(default)]
227 pub extensions: Vec<String>,
228 #[serde(default)]
229 pub first_lines: Vec<String>,
230 #[serde(default)]
231 pub filenames: Vec<String>,
232 #[serde(default)]
233 pub re_syntax: Vec<(String, String)>,
234 #[serde(default)]
235 pub lsp: Option<LspConfig>,
236}
237
238#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize)]
240pub struct LspConfig {
241 pub command: String,
243 #[serde(default)]
245 pub args: Vec<String>,
246 pub roots: Vec<String>,
248 #[serde(default)]
250 pub init_opts: Option<serde_json::Value>,
251}
252
253impl LspConfig {
254 pub fn root_for_dir<'a>(&self, d: &'a Path) -> Option<&'a Path> {
255 for root in self.roots.iter() {
256 if let Some(p) = parent_dir_containing(d, root) {
257 return Some(p);
258 }
259 }
260
261 None
262 }
263
264 pub fn root_for_buffer<'a>(&self, b: &'a Buffer) -> Option<&'a Path> {
265 self.root_for_dir(b.dir()?)
266 }
267}
268
269#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
270pub struct KeyBindings {
271 #[serde(default, deserialize_with = "de_serde_trie")]
272 pub normal: Trie<Input, Actions>,
273 #[serde(default, deserialize_with = "de_serde_trie")]
274 pub insert: Trie<Input, Actions>,
275}
276
277impl Default for KeyBindings {
278 fn default() -> Self {
279 KeyBindings {
280 normal: Trie::try_from_iter(Vec::new()).unwrap(),
281 insert: Trie::try_from_iter(Vec::new()).unwrap(),
282 }
283 }
284}
285
286#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
287#[serde(untagged)]
288pub enum KeyAction {
289 Execute { run: String },
290 Keys { send_keys: Inputs },
291}
292
293impl KeyAction {
294 fn into_actions(self) -> Actions {
295 match self {
296 Self::Execute { run } => Actions::Single(Action::ExecuteString { s: run }),
297 Self::Keys { send_keys } => Actions::Single(Action::SendKeys { ks: send_keys.0 }),
298 }
299 }
300}
301
302#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
304#[serde(try_from = "String")]
305pub struct Inputs(pub(crate) Vec<Input>);
306
307impl TryFrom<String> for Inputs {
308 type Error = String;
309
310 fn try_from(value: String) -> Result<Self, Self::Error> {
311 let inputs = value
312 .split_whitespace()
313 .map(try_input_from_str_template)
314 .collect::<Result<Vec<_>, _>>()?;
315
316 if inputs.is_empty() {
317 return Err("empty key inputs string".to_owned());
318 }
319
320 Ok(Self(inputs))
321 }
322}
323
324fn try_input_from_str_template(s: &str) -> Result<Input, String> {
326 if s.len() == 1 {
327 Ok(Input::Char(s.chars().next().unwrap()))
328 } else if let Some(suffix) = s.strip_prefix("C-A-") {
329 if suffix.len() == 1 {
330 Ok(Input::CtrlAlt(suffix.chars().next().unwrap()))
331 } else if suffix == "<space>" {
332 Ok(Input::CtrlAlt(' '))
333 } else {
334 Err(format!("invalid send_key value: C-A-{suffix}"))
335 }
336 } else if let Some(suffix) = s.strip_prefix("C-") {
337 if suffix.len() == 1 {
338 Ok(Input::Ctrl(suffix.chars().next().unwrap()))
339 } else if suffix == "<space>" {
340 Ok(Input::Ctrl(' '))
341 } else {
342 Err(format!("invalid send_key value: C-{suffix}"))
343 }
344 } else if let Some(suffix) = s.strip_prefix("A-") {
345 if suffix.len() == 1 {
346 Ok(Input::Alt(suffix.chars().next().unwrap()))
347 } else if suffix == "<space>" {
348 Ok(Input::Alt(' '))
349 } else {
350 Err(format!("invalid send_key value: A-{suffix}"))
351 }
352 } else {
353 let i = match s {
354 "<backspace>" => Input::Backspace,
355 "<delete>" => Input::Del,
356 "<end>" => Input::End,
357 "<esc>" => Input::Esc,
358 "<home>" => Input::Home,
359 "<page-down>" => Input::PageDown,
360 "<page-up>" => Input::PageUp,
361 "<space>" => Input::Char(' '),
362 "<tab>" => Input::Tab,
363 "<left>" => Input::Arrow(Arrow::Left),
364 "<right>" => Input::Arrow(Arrow::Right),
365 "<up>" => Input::Arrow(Arrow::Up),
366 "<down>" => Input::Arrow(Arrow::Down),
367 _ => return Err(format!("unknown key {s}")),
368 };
369
370 Ok(i)
371 }
372}
373
374fn de_serde_trie<'de, D>(deserializer: D) -> Result<Trie<Input, Actions>, D::Error>
375where
376 D: Deserializer<'de>,
377{
378 let raw_map: HashMap<String, KeyAction> = Deserialize::deserialize(deserializer)?;
379 if raw_map.is_empty() {
380 return Err(de::Error::custom("empty key map"));
381 }
382
383 let raw = raw_map
384 .into_iter()
385 .map(|(k, action)| {
386 Inputs::try_from(k)
387 .map(|Inputs(keys)| (keys, action.into_actions()))
388 .map_err(de::Error::custom)
389 })
390 .collect::<Result<Vec<_>, _>>()?;
391
392 Trie::try_from_iter(raw).map_err(de::Error::custom)
393}
394
395#[cfg(test)]
396mod tests {
397 use super::*;
398 use simple_test_case::test_case;
399
400 #[test]
401 fn default_config_is_valid() {
402 Config::default(); }
404
405 #[test_case("A", &[Input::Char('A')]; "single letter key")]
406 #[test_case("5", &[Input::Char('5')]; "single digit key")]
407 #[test_case("A-y", &[Input::Alt('y')]; "alt letter")]
408 #[test_case("C-y", &[Input::Ctrl('y')]; "control letter")]
409 #[test_case("C-A-y", &[Input::CtrlAlt('y')]; "control alt letter")]
410 #[test_case("A-<space>", &[Input::Alt(' ')]; "alt space")]
411 #[test_case("C-<space>", &[Input::Ctrl(' ')]; "control space")]
412 #[test_case("C-A-<space>", &[Input::CtrlAlt(' ')]; "control alt space")]
413 #[test_case("<backspace>", &[Input::Backspace]; "backspace")]
414 #[test_case("<delete>", &[Input::Del]; "delete")]
415 #[test_case("<end>", &[Input::End]; "end")]
416 #[test_case("<esc>", &[Input::Esc]; "escape")]
417 #[test_case("<home>", &[Input::Home]; "home")]
418 #[test_case("<page-up>", &[Input::PageUp]; "page up")]
419 #[test_case("<page-down>", &[Input::PageDown]; "page down")]
420 #[test_case("<space>", &[Input::Char(' ')]; "space")]
421 #[test_case("<tab>", &[Input::Tab]; "tab")]
422 #[test_case("<left>", &[Input::Arrow(Arrow::Left)]; "left")]
423 #[test_case("<right>", &[Input::Arrow(Arrow::Right)]; "right")]
424 #[test_case("<up>", &[Input::Arrow(Arrow::Up)]; "up")]
425 #[test_case("<down>", &[Input::Arrow(Arrow::Down)]; "down")]
426 #[test_case("A B C", &[Input::Char('A'), Input::Char('B'), Input::Char('C')]; "sequence")]
427 #[test]
428 fn inputs_try_from_string_works(raw: &str, expected: &[Input]) {
429 let inputs = Inputs::try_from(raw.to_owned()).unwrap();
430 assert_eq!(&inputs.0, expected);
431 }
432
433 #[test]
434 fn inputs_try_from_empty_string_errors() {
435 assert_eq!(
436 &Inputs::try_from(String::new()).unwrap_err(),
437 "empty key inputs string"
438 );
439 }
440
441 #[test]
442 fn parsing_an_empty_keymap_errors() {
443 let res: Result<KeyBindings, _> = toml::from_str("[normal]");
444 assert!(res.is_err());
445 }
446}