Skip to main content

fm/config/
configuration.rs

1use std::{fs::File, path};
2
3use anyhow::Result;
4use clap::Parser;
5use ratatui::style::{Color, Style};
6use serde_yaml_ng::{from_reader, Value};
7
8use crate::common::{tilde, SYNTECT_DEFAULT_THEME};
9use crate::config::{make_default_config_files, Bindings, ColorG};
10use crate::io::Args;
11use crate::log_info;
12
13/// Holds every configurable aspect of the application.
14/// All styles are hardcoded then updated from optional values
15/// of the config file.
16/// The config file is a YAML file in `~/.config/fm/config.yaml`
17#[derive(Debug, Clone, Default)]
18pub struct Config {
19    /// Configurable keybindings.
20    pub binds: Bindings,
21    pub plugins: Vec<(String, String)>,
22    pub theme: String,
23}
24
25impl Config {
26    /// Updates the config from a yaml value read in the configuration file.
27    fn update_from_config(&mut self, yaml: &Value) -> Result<()> {
28        self.binds.update_normal(&yaml["keys"]);
29        self.binds.update_custom(&yaml["custom"]);
30        self.update_plugins(&yaml["plugins"]["previewer"]);
31        self.update_theme(&yaml["theme"]);
32        Ok(())
33    }
34
35    fn update_theme(&mut self, yaml: &Value) {
36        if let Some(theme) = yaml.as_str() {
37            self.theme = theme.to_string();
38            log_info!("Found theme in config: {theme}");
39        } else {
40            log_info!("Couldn't find a theme in config.");
41        }
42    }
43
44    fn update_plugins(&mut self, yaml: &Value) {
45        let Some(mappings) = yaml.as_mapping() else {
46            return;
47        };
48        for (plugin_name, plugin_path) in mappings.iter() {
49            let Some(plugin_name) = plugin_name.as_str() else {
50                continue;
51            };
52            let Some(plugin_path) = plugin_path.as_str() else {
53                continue;
54            };
55            if path::Path::new(plugin_path).exists() {
56                self.plugins
57                    .push((plugin_name.to_owned(), plugin_path.to_owned()));
58            } else {
59                log_info!("{plugin_path} is specified in config file but doesn't exists.");
60            }
61        }
62        log_info!("found plugins: {plugins:#?}", plugins = self.plugins);
63    }
64}
65
66fn ensure_config_files_exists(path: &str) -> Result<()> {
67    let expanded_path = tilde(path);
68    let expanded_config_path = path::Path::new(expanded_path.as_ref());
69    if !expanded_config_path.exists() {
70        make_default_config_files()?;
71        log_info!("Created default config files.");
72    }
73    Ok(())
74}
75
76/// Returns a config with values from :
77///
78/// 1. hardcoded values
79///
80/// 2. configured values from `~/.config/fm/config_file_name.yaml` if those files exists.
81///
82/// If the config file is poorly formated its simply ignored.
83pub fn load_config(path: &str) -> Result<Config> {
84    ensure_config_files_exists(path)?;
85    let mut config = Config::default();
86    let Ok(file) = File::open(&*tilde(path)) else {
87        crate::log_info!("Couldn't read config file at {path}");
88        return Ok(config);
89    };
90    let Ok(yaml) = from_reader(file) else {
91        return Ok(config);
92    };
93    let _ = config.update_from_config(&yaml);
94    Ok(config)
95}
96
97/// Reads the config file and parse the "palette" values.
98/// The palette format looks like this (with different accepted format)
99/// ```yaml
100/// colors:
101///   normal_start: yellow, #ffff00, rgb(255, 255, 0)
102///   normal_stop:  #ff00ff
103/// ```
104/// Recognized formats are : ansi names (yellow, light_red etc.), rgb like rgb(255, 55, 132) and hexadecimal like #ff3388.
105/// The ANSI names are recognized but we can't get the user settings for all kinds of terminal
106/// so we'll have to use default values.
107///
108/// If we can't read those values, we'll return green and blue.
109pub fn read_normal_file_colorer(yaml: &Option<Value>) -> (ColorG, ColorG) {
110    let default_pair = (ColorG::new(0, 255, 0), ColorG::new(0, 0, 255));
111    let Some(yaml) = yaml else {
112        return default_pair;
113    };
114    let Some(start) = yaml["normal_start"].as_str() else {
115        return default_pair;
116    };
117    let Some(stop) = yaml["normal_stop"].as_str() else {
118        return default_pair;
119    };
120    let Some(start_color) = ColorG::parse_any_color(start) else {
121        return default_pair;
122    };
123    let Some(stop_color) = ColorG::parse_any_color(stop) else {
124        return default_pair;
125    };
126    (start_color, stop_color)
127}
128macro_rules! update_style {
129    ($self_style:expr, $yaml:ident, $key:expr) => {
130        if let Some(color) = read_yaml_string($yaml, $key) {
131            $self_style = crate::config::str_to_ratatui(color).into();
132        }
133    };
134}
135
136fn read_yaml_string(yaml: &Value, key: &str) -> Option<String> {
137    yaml[key].as_str().map(|s| s.to_string())
138}
139
140/// Holds configurable colors for every kind of file.
141/// "Normal" files are displayed with a different color by extension.
142#[derive(Debug, Clone)]
143pub struct FileStyle {
144    /// Color for `directory` files.
145    pub directory: Style,
146    /// Style for `block` files.
147    pub block: Style,
148    /// Style for `char` files.
149    pub char: Style,
150    /// Style for `fifo` files.
151    pub fifo: Style,
152    /// Style for `socket` files.
153    pub socket: Style,
154    /// Style for `symlink` files.
155    pub symlink: Style,
156    /// Style for broken `symlink` files.
157    pub broken: Style,
158}
159
160impl FileStyle {
161    fn new() -> Self {
162        Self {
163            directory: Color::Red.into(),
164            block: Color::Yellow.into(),
165            char: Color::Green.into(),
166            fifo: Color::Blue.into(),
167            socket: Color::Cyan.into(),
168            symlink: Color::Magenta.into(),
169            broken: Color::White.into(),
170        }
171    }
172
173    /// Update every color from a yaml value (read from the config file).
174    fn update_values(&mut self, yaml: &Value) {
175        update_style!(self.directory, yaml, "directory");
176        update_style!(self.block, yaml, "block");
177        update_style!(self.char, yaml, "char");
178        update_style!(self.fifo, yaml, "fifo");
179        update_style!(self.socket, yaml, "socket");
180        update_style!(self.symlink, yaml, "symlink");
181        update_style!(self.broken, yaml, "broken");
182    }
183
184    fn update_from_config(&mut self, yaml: &Option<Value>) {
185        let Some(yaml) = yaml else {
186            return;
187        };
188        self.update_values(yaml);
189    }
190
191    pub fn from_config(yaml: &Option<Value>) -> Self {
192        let mut style = Self::default();
193        style.update_from_config(yaml);
194        style
195    }
196}
197
198impl Default for FileStyle {
199    fn default() -> Self {
200        Self::new()
201    }
202}
203
204/// Different styles for decorating the menus.
205pub struct MenuStyle {
206    pub first: Style,
207    pub second: Style,
208    pub selected_border: Style,
209    pub inert_border: Style,
210    pub palette_1: Style,
211    pub palette_2: Style,
212    pub palette_3: Style,
213    pub palette_4: Style,
214}
215
216impl Default for MenuStyle {
217    fn default() -> Self {
218        Self {
219            first: Color::Rgb(45, 250, 209).into(),
220            second: Color::Rgb(230, 189, 87).into(),
221            selected_border: Color::Rgb(45, 250, 209).into(),
222            inert_border: Color::Rgb(248, 248, 248).into(),
223            palette_1: Color::Rgb(45, 250, 209).into(),
224            palette_2: Color::Rgb(230, 189, 87).into(),
225            palette_3: Color::Rgb(230, 167, 255).into(),
226            palette_4: Color::Rgb(59, 204, 255).into(),
227        }
228    }
229}
230
231impl MenuStyle {
232    pub fn update(mut self, yaml: &Option<Value>) -> Self {
233        if let Some(menu_colors) = yaml {
234            update_style!(self.first, menu_colors, "header_first");
235            update_style!(self.second, menu_colors, "header_second");
236            update_style!(self.selected_border, menu_colors, "selected_border");
237            update_style!(self.inert_border, menu_colors, "inert_border");
238            update_style!(self.palette_1, menu_colors, "palette_1");
239            update_style!(self.palette_2, menu_colors, "palette_2");
240            update_style!(self.palette_3, menu_colors, "palette_3");
241            update_style!(self.palette_4, menu_colors, "palette_4");
242        }
243
244        self
245    }
246
247    #[inline]
248    pub const fn palette(&self) -> [Style; 4] {
249        [
250            self.palette_1,
251            self.palette_2,
252            self.palette_3,
253            self.palette_4,
254        ]
255    }
256
257    #[inline]
258    pub const fn palette_size(&self) -> usize {
259        self.palette().len()
260    }
261}
262
263/// Name of the syntect theme used.
264#[derive(Debug)]
265pub struct SyntectTheme {
266    pub name: String,
267}
268
269impl Default for SyntectTheme {
270    fn default() -> Self {
271        Self {
272            name: SYNTECT_DEFAULT_THEME.to_owned(),
273        }
274    }
275}
276
277impl SyntectTheme {
278    pub fn from_config(path: &str) -> Result<Self> {
279        let Ok(file) = File::open(path::Path::new(&tilde(path).to_string())) else {
280            crate::log_info!("Couldn't read config file at {path}");
281            return Ok(Self::default());
282        };
283        let Ok(yaml) = from_reader::<File, Value>(file) else {
284            return Ok(Self::default());
285        };
286        let Some(name) = yaml["syntect_theme"].as_str() else {
287            return Ok(Self::default());
288        };
289        crate::log_info!("Config: found syntect theme: {name}");
290
291        Ok(Self {
292            name: name.to_string(),
293        })
294    }
295}
296
297#[derive(Default, Debug)]
298pub enum Imagers {
299    #[default]
300    Disabled,
301    Ueberzug,
302    Inline,
303}
304
305/// Name of the syntect theme used.
306#[derive(Debug, Default)]
307pub struct PreferedImager {
308    pub imager: Imagers,
309}
310
311impl PreferedImager {
312    pub fn from_config(path: &str) -> Result<Self> {
313        if Args::parse().disable_images {
314            return Ok(Self::default());
315        }
316        let Ok(file) = File::open(path::Path::new(&tilde(path).to_string())) else {
317            crate::log_info!("Couldn't read config file at {path}");
318            return Ok(Self::default());
319        };
320        let Ok(yaml) = from_reader::<File, Value>(file) else {
321            return Ok(Self::default());
322        };
323        let Some(imager) = yaml["imager"].as_str() else {
324            return Ok(Self::default());
325        };
326        crate::log_info!("Config: found imager : {imager}");
327        let imager = match imager {
328            "Ueberzug" => Imagers::Ueberzug,
329            "Inline" => Imagers::Inline,
330            _ => Imagers::Disabled,
331        };
332
333        Ok(Self { imager })
334    }
335}