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