fm/config/
configuration.rs

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