1use crate::{key::Input, mode::normal_mode, term::Color, trie::Trie};
3use std::{env, fs, io};
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct Config {
8 pub(crate) tabstop: usize,
9 pub(crate) expand_tab: bool,
10 pub(crate) auto_mount: bool,
11 pub(crate) match_indent: bool,
12 pub(crate) status_timeout: u64,
13 pub(crate) double_click_ms: u128,
14 pub(crate) minibuffer_lines: usize,
15 pub(crate) find_command: String,
16 pub(crate) colorscheme: ColorScheme,
17 pub(crate) bindings: Trie<Input, String>,
18}
19
20impl Default for Config {
21 fn default() -> Self {
22 Self {
23 tabstop: 4,
24 expand_tab: true,
25 auto_mount: false,
26 match_indent: true,
27 status_timeout: 3,
28 double_click_ms: 200,
29 minibuffer_lines: 8,
30 find_command: "fd -t f".to_string(),
31 colorscheme: ColorScheme::default(),
32 bindings: Trie::from_pairs(Vec::new()),
33 }
34 }
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub struct ColorScheme {
40 pub(crate) bg: Color,
42 pub(crate) fg: Color,
43 pub(crate) dot_bg: Color,
44 pub(crate) load_bg: Color,
45 pub(crate) exec_bg: Color,
46 pub(crate) bar_bg: Color,
47 pub(crate) signcol_fg: Color,
48 pub(crate) minibuffer_hl: Color,
49 pub(crate) comment: Color,
51 pub(crate) keyword: Color,
52 pub(crate) control_flow: Color,
53 pub(crate) definition: Color,
54 pub(crate) punctuation: Color,
55 pub(crate) string: Color,
56}
57
58impl Default for ColorScheme {
59 fn default() -> Self {
60 Self {
61 bg: "#1B1720".try_into().unwrap(),
63 fg: "#E6D29E".try_into().unwrap(),
64 dot_bg: "#336677".try_into().unwrap(),
65 load_bg: "#957FB8".try_into().unwrap(),
66 exec_bg: "#Bf616A".try_into().unwrap(),
67 bar_bg: "#4E415C".try_into().unwrap(),
68 signcol_fg: "#544863".try_into().unwrap(),
69 minibuffer_hl: "#3E3549".try_into().unwrap(),
70 comment: "#624354".try_into().unwrap(),
72 keyword: "#Bf616A".try_into().unwrap(),
73 control_flow: "#7E9CD8".try_into().unwrap(),
74 definition: "#957FB8".try_into().unwrap(),
75 punctuation: "#DCA561".try_into().unwrap(),
76 string: "#61DCA5".try_into().unwrap(),
77 }
78 }
79}
80
81impl Config {
82 pub fn try_load() -> Result<Self, String> {
84 let home = env::var("HOME").unwrap();
85
86 let s = match fs::read_to_string(format!("{home}/.ad/init.conf")) {
87 Ok(s) => s,
88 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(Config::default()),
89 Err(e) => return Err(format!("Unable to load config file: {e}")),
90 };
91
92 match Config::parse(&s) {
93 Ok(cfg) => Ok(cfg),
94 Err(e) => Err(format!("Invalid config file: {e}")),
95 }
96 }
97
98 pub fn parse(contents: &str) -> Result<Self, String> {
101 let mut cfg = Config::default();
102 cfg.update_from(contents)?;
103
104 Ok(cfg)
105 }
106
107 pub(crate) fn update_from(&mut self, input: &str) -> Result<(), String> {
108 let mut raw_bindings = Vec::new();
109
110 for line in input.lines() {
111 let line = line.trim_end();
112 if line.starts_with('#') || line.is_empty() {
113 continue;
114 }
115
116 match line.strip_prefix("set ") {
117 Some(line) => self.try_set_prop(line)?,
118 None => match line.strip_prefix("map ") {
119 Some(line) => raw_bindings.push(try_parse_binding(line)?),
120 None => {
121 return Err(format!(
122 "'{line}' should be 'set prop=val' or 'map ... => prog'"
123 ))
124 }
125 },
126 }
127 }
128
129 if !raw_bindings.is_empty() {
130 let nm = normal_mode();
133 for (keys, _) in raw_bindings.iter() {
134 if nm.keymap.contains_key_or_prefix(keys) {
135 let mut s = String::new();
136 for k in keys {
137 if let Input::Char(c) = k {
138 s.push(*c);
139 }
140 }
141
142 return Err(format!("mapping '{s}' collides with a Normal mode mapping"));
143 }
144 }
145 }
146
147 self.bindings = Trie::from_pairs(raw_bindings);
148
149 Ok(())
150 }
151
152 pub(crate) fn try_set_prop(&mut self, input: &str) -> Result<(), String> {
153 let (prop, val) = input
154 .split_once('=')
155 .ok_or_else(|| format!("'{input}' is not a 'set prop=val' statement"))?;
156
157 match prop {
158 "find-command" => self.find_command = val.trim().to_string(),
160
161 "tabstop" => self.tabstop = parse_usize(prop, val)?,
163 "minibuffer-lines" => self.minibuffer_lines = parse_usize(prop, val)?,
164 "status-timeout" => self.status_timeout = parse_usize(prop, val)? as u64,
165 "double-click-ms" => self.double_click_ms = parse_usize(prop, val)? as u128,
166
167 "expand-tab" => self.expand_tab = parse_bool(prop, val)?,
169 "auto-mount" => self.auto_mount = parse_bool(prop, val)?,
170 "match-indent" => self.match_indent = parse_bool(prop, val)?,
171
172 "bg-color" => self.colorscheme.bg = parse_color(prop, val)?,
174 "fg-color" => self.colorscheme.fg = parse_color(prop, val)?,
175 "dot-bg-color" => self.colorscheme.dot_bg = parse_color(prop, val)?,
176 "load-bg-color" => self.colorscheme.load_bg = parse_color(prop, val)?,
177 "exec-bg-color" => self.colorscheme.exec_bg = parse_color(prop, val)?,
178 "bar-bg-color" => self.colorscheme.bar_bg = parse_color(prop, val)?,
179 "signcol-fg-color" => self.colorscheme.signcol_fg = parse_color(prop, val)?,
180 "minibuffer-hl-color" => self.colorscheme.minibuffer_hl = parse_color(prop, val)?,
181 "comment-color" => self.colorscheme.comment = parse_color(prop, val)?,
182 "keyword-color" => self.colorscheme.keyword = parse_color(prop, val)?,
183 "control-flow-color" => self.colorscheme.control_flow = parse_color(prop, val)?,
184 "definition-color" => self.colorscheme.definition = parse_color(prop, val)?,
185 "punctuation-color" => self.colorscheme.punctuation = parse_color(prop, val)?,
186 "string-color" => self.colorscheme.string = parse_color(prop, val)?,
187
188 _ => return Err(format!("'{prop}' is not a known config property")),
189 }
190
191 Ok(())
192 }
193}
194
195fn parse_usize(prop: &str, val: &str) -> Result<usize, String> {
196 match val.parse() {
197 Ok(num) => Ok(num),
198 Err(_) => Err(format!("expected number for '{prop}' but found '{val}'")),
199 }
200}
201
202fn parse_bool(prop: &str, val: &str) -> Result<bool, String> {
203 match val {
204 "true" => Ok(true),
205 "false" => Ok(false),
206 _ => Err(format!(
207 "expected true/false for '{prop}' but found '{val}'"
208 )),
209 }
210}
211
212fn parse_color(prop: &str, val: &str) -> Result<Color, String> {
213 Color::try_from(val)
214 .map_err(|_| format!("expected #RRGGBB string for '{prop}' but found '{val}'"))
215}
216
217fn try_parse_binding(input: &str) -> Result<(Vec<Input>, String), String> {
218 let (keys, prog) = input
219 .split_once("=>")
220 .ok_or_else(|| format!("'{input}' is not a 'map ... => prog' statement"))?;
221
222 let keys: Vec<Input> = keys
223 .split_whitespace()
224 .filter_map(|s| {
225 if s.len() == 1 {
226 let c = s.chars().next().unwrap();
227 if c.is_whitespace() {
228 None
229 } else {
230 Some(Input::Char(c))
231 }
232 } else {
233 match s {
234 "<space>" => Some(Input::Char(' ')),
235 _ => None,
236 }
237 }
238 })
239 .collect();
240
241 Ok((keys, prog.trim().to_string()))
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247
248 const EXAMPLE_CONFIG: &str = include_str!("../data/init.conf");
249 const CUSTOM_CONFIG: &str = "
250# This is a comment
251
252
253# Blank lines should be skipped
254set tabstop=7
255
256set expand-tab=false
257set match-indent=false
258
259map G G => my-prog
260";
261
262 #[test]
265 fn parse_of_example_config_works() {
266 let cfg = Config::parse(EXAMPLE_CONFIG).unwrap();
267 let bindings = Trie::from_pairs(vec![
268 (vec![Input::Char(' '), Input::Char('F')], "fmt".to_string()),
269 (vec![Input::Char('>')], "indent".to_string()),
270 (vec![Input::Char('<')], "unindent".to_string()),
271 ]);
272
273 let expected = Config {
274 bindings,
275 ..Default::default()
276 };
277
278 assert_eq!(cfg, expected);
279 }
280
281 #[test]
282 fn custom_vals_work() {
283 let cfg = Config::parse(CUSTOM_CONFIG).unwrap();
284
285 let expected = Config {
286 tabstop: 7,
287 expand_tab: false,
288 match_indent: false,
289 bindings: Trie::from_pairs(vec![(
290 vec![Input::Char('G'), Input::Char('G')],
291 "my-prog".to_string(),
292 )]),
293 ..Default::default()
294 };
295
296 assert_eq!(cfg, expected);
297 }
298}