Skip to main content

fm/config/
oncelock_static.rs

1use std::{
2    fs::File,
3    ops::DerefMut,
4    path::{Path, PathBuf},
5    sync::OnceLock,
6};
7
8use anyhow::{anyhow, Result};
9use nucleo::Matcher;
10use parking_lot::{Mutex, MutexGuard};
11use ratatui::style::Color;
12use serde_yaml_ng::{from_reader, Value};
13use strum::{EnumIter, IntoEnumIterator};
14use syntect::{
15    dumps::{from_binary, from_dump_file},
16    highlighting::{Theme, ThemeSet},
17};
18
19use crate::{
20    app::{build_previewer_plugins, PreviewerPlugin},
21    common::{tilde, CONFIG_FOLDER, CONFIG_PATH, PREVIEWER_PATH, SYNTECT_THEMES_PATH},
22    config::{
23        read_normal_file_colorer, FileStyle, Gradient, MenuStyle, NormalFileColorer,
24        PreferedImager, SyntectTheme, MAX_GRADIENT_NORMAL,
25    },
26    log_info,
27    modes::PreviewerCommand,
28};
29
30/// Starting folder of the application. Read from arguments if any `-P ~/Downloads` else it uses the current folder: `.`.
31pub static START_FOLDER: OnceLock<PathBuf> = OnceLock::new();
32
33/// Store true if logging is enabled else false.
34/// Set by the application itself and read before updating zoxide database.
35pub static IS_LOGGING: OnceLock<bool> = OnceLock::new();
36
37/// Colors read from the config file.
38/// We define a colors for every kind of file except normal files.
39/// Colors for normal files are calculated from their extension and
40/// are greens or blues.
41///
42/// Colors are setup on start and never change afterwards.
43pub static FILE_STYLES: OnceLock<FileStyle> = OnceLock::new();
44
45/// Menu color struct
46pub static MENU_STYLES: OnceLock<MenuStyle> = OnceLock::new();
47
48/// Defines a palette which will color the "normal" files based on their extension.
49/// We try to read a yaml value and pick one of 3 palettes :
50/// "green-red", "blue-green", "blue-red", "red-green", "red-blue", "green-blue" which is the default.
51/// "custom" will create a gradient from start_palette to end_palette. Both values should be "rgb(u8, u8, u8)".
52pub static COLORER: OnceLock<fn(usize) -> Color> = OnceLock::new();
53
54/// Gradient for normal files
55pub static ARRAY_GRADIENT: OnceLock<[Color; MAX_GRADIENT_NORMAL]> = OnceLock::new();
56
57/// Highlighting theme color used to preview code file
58static SYNTECT_THEME: OnceLock<Theme> = OnceLock::new();
59
60static PREVIEWER_PLUGINS: OnceLock<Vec<(String, PreviewerPlugin)>> = OnceLock::new();
61static PREVIEWER_COMMANDS: OnceLock<Vec<PreviewerCommand>> = OnceLock::new();
62
63static PREFERED_IMAGER: OnceLock<PreferedImager> = OnceLock::new();
64
65/// The prefered method to display images set in config file
66pub fn get_prefered_imager() -> Option<&'static PreferedImager> {
67    PREFERED_IMAGER.get()
68}
69
70/// Attach a map of name -> path to the `PLUGINS` static variable.
71pub fn set_previewer_plugins(plugins: Vec<(String, String)>) -> Result<()> {
72    let _ = PREVIEWER_PLUGINS.set(build_previewer_plugins(plugins));
73    Ok(())
74}
75
76/// `PLUGINS` static map. Returns a map of name -> path.
77pub fn get_previewer_plugins() -> Option<&'static Vec<(String, PreviewerPlugin)>> {
78    PREVIEWER_PLUGINS.get()
79}
80
81fn parse_previewer_commands() -> Option<Vec<PreviewerCommand>> {
82    let file = std::fs::File::open(tilde(PREVIEWER_PATH).as_ref()).ok()?;
83    let commands: Vec<PreviewerCommand> = from_reader(file).ok()?;
84    log_info!("Previewer commands: {commands:?}");
85    Some(commands)
86}
87
88/// Attach a map of name -> path to the `PREVIEWER_COMMAND` static variable.
89pub fn set_previewer_command() -> Result<()> {
90    let commands = parse_previewer_commands().unwrap_or_default();
91    let _ = PREVIEWER_COMMANDS.set(commands);
92    Ok(())
93}
94
95/// `command` static map. Returns a map of name -> path.
96pub fn get_previewer_command() -> Option<&'static Vec<PreviewerCommand>> {
97    PREVIEWER_COMMANDS.get()
98}
99
100/// Reads the syntect_theme configuration value and tries to load if from configuration files.
101///
102/// If it doesn't work, it will load the default set from binary file itself: monokai.
103pub fn set_syntect_theme() -> Result<()> {
104    let config_theme = SyntectTheme::from_config(CONFIG_PATH)?;
105    if !set_syntect_theme_from_config(&config_theme.name) {
106        set_syntect_theme_from_source_code()
107    }
108    Ok(())
109}
110
111pub fn set_prefered_imager() -> Result<()> {
112    let prefered_imager = PreferedImager::from_config(CONFIG_PATH)?;
113    let _ = PREFERED_IMAGER.set(prefered_imager);
114    Ok(())
115}
116
117#[derive(EnumIter, Debug)]
118enum SyntectThemeKind {
119    TmTheme,
120    Dump,
121}
122
123impl SyntectThemeKind {
124    fn extension(&self) -> &str {
125        match self {
126            Self::TmTheme => "tmTheme",
127            Self::Dump => "themedump",
128        }
129    }
130
131    fn load(&self, themepath: &Path) -> Result<Theme> {
132        match self {
133            Self::TmTheme => ThemeSet::get_theme(themepath)
134                .map_err(|e| anyhow!("Couldn't load syntect theme {e:}")),
135            Self::Dump => {
136                from_dump_file(themepath).map_err(|e| anyhow!("Couldn't load syntect theme {e:}"))
137            }
138        }
139    }
140}
141
142fn set_syntect_theme_from_config(syntect_theme: &str) -> bool {
143    let syntect_theme_path = PathBuf::from(tilde(SYNTECT_THEMES_PATH).as_ref());
144    for kind in SyntectThemeKind::iter() {
145        if load_syntect(&syntect_theme_path, syntect_theme, &kind) {
146            return true;
147        }
148        log_info!("Couldn't load {syntect_theme} {kind:?}");
149    }
150    false
151}
152
153fn load_syntect(syntect_theme_path: &Path, syntect_theme: &str, kind: &SyntectThemeKind) -> bool {
154    let mut full_path = syntect_theme_path.to_path_buf();
155    full_path.push(syntect_theme);
156    full_path.set_extension(kind.extension());
157    if !full_path.exists() {
158        return false;
159    }
160    let Ok(theme) = kind.load(&full_path) else {
161        crate::log_info!("Syntect couldn't load {fp}", fp = full_path.display());
162        return false;
163    };
164    let name = theme.name.clone();
165    if SYNTECT_THEME.set(theme).is_ok() {
166        log_info!("SYNTECT_THEME set to {name:?}");
167        true
168    } else {
169        crate::log_info!("SYNTECT_THEME was already set!");
170        false
171    }
172}
173
174fn set_syntect_theme_from_source_code() {
175    let _ = SYNTECT_THEME.set(from_binary(include_bytes!(
176        "../../assets/themes/monokai.themedump"
177    )));
178}
179
180/// Reads the syntect theme from memory. It should never be `None`.
181pub fn get_syntect_theme() -> Option<&'static Theme> {
182    SYNTECT_THEME.get()
183}
184
185static ICON: OnceLock<bool> = OnceLock::new();
186static ICON_WITH_METADATA: OnceLock<bool> = OnceLock::new();
187
188/// Does the user wants nerdfont icons ? Default: false.
189pub fn with_icon() -> bool {
190    *ICON.get().unwrap_or(&false)
191}
192
193/// Does the user wants nerdfont icons even if metadata are shown ? Default: false.
194pub fn with_icon_metadata() -> bool {
195    *ICON_WITH_METADATA.get().unwrap_or(&false)
196}
197
198fn set_start_folder(start_folder: &str) -> Result<()> {
199    START_FOLDER
200        .set(std::fs::canonicalize(tilde(start_folder).as_ref()).unwrap_or_default())
201        .map_err(|_| anyhow!("Start folder shouldn't be set"))?;
202    Ok(())
203}
204
205fn set_file_styles(yaml: &Option<Value>) -> Result<()> {
206    FILE_STYLES
207        .set(FileStyle::from_config(yaml))
208        .map_err(|_| anyhow!("File colors shouldn't be set"))?;
209    Ok(())
210}
211
212fn set_menu_styles(yaml: &Option<Value>) -> Result<()> {
213    MENU_STYLES
214        .set(MenuStyle::default().update(yaml))
215        .map_err(|_| anyhow!("Menu colors shouldn't be set"))?;
216    Ok(())
217}
218
219fn set_normal_file_colorer(yaml: &Option<Value>) -> Result<()> {
220    let (start_color, stop_color) = read_normal_file_colorer(yaml);
221    ARRAY_GRADIENT
222        .set(Gradient::new(start_color, stop_color, MAX_GRADIENT_NORMAL).as_array()?)
223        .map_err(|_| anyhow!("Gradient shouldn't be set"))?;
224    COLORER
225        .set(NormalFileColorer::colorer as fn(usize) -> Color)
226        .map_err(|_| anyhow!("Colorer shouldn't be set"))?;
227
228    Ok(())
229}
230
231fn read_yaml_bool(yaml: &Value, key: &str) -> Option<bool> {
232    yaml[key].as_bool()
233}
234
235fn read_icon_icon_with_metadata() -> (bool, bool) {
236    let Ok(file) = File::open(Path::new(&tilde(CONFIG_PATH).to_string())) else {
237        crate::log_info!("Couldn't read config file at {CONFIG_PATH}");
238        return (false, false);
239    };
240    let Ok(yaml) = from_reader::<File, Value>(file) else {
241        return (false, false);
242    };
243    let mut icon: bool = false;
244    let mut icon_with_metadata: bool = false;
245    if let Some(i) = read_yaml_bool(&yaml, "icon") {
246        icon = i;
247    }
248    if !icon {
249        icon_with_metadata = false;
250    } else if let Some(icon_with) = read_yaml_bool(&yaml, "icon_with_metadata") {
251        icon_with_metadata = icon_with;
252    }
253    (icon, icon_with_metadata)
254}
255
256/// Read `icon` and `icon_with_metadata` from the config file and store them in static values.
257///
258/// `icon_with_metadata` can't be true if `icon` is false, even if the user set it to true.
259/// If the user hasn't installed nerdfont, the icons can't be shown properly and `icon` shouldn't be shown.
260/// It leads to a quite complex parsing:
261/// - If the file can't be read (should never happen, the application should have quit already), both icon & icon_with_metadata are false,
262/// - If the values aren't in the yaml file, both are false,
263/// - If icon is false, icon_with_metadata is false,
264/// - Otherwise, we use the values from the file.
265pub fn set_icon_icon_with_metadata() -> Result<()> {
266    let (icon, icon_with_metadata) = read_icon_icon_with_metadata();
267    ICON.set(icon)
268        .map_err(|_| anyhow!("ICON shouldn't be set"))?;
269    ICON_WITH_METADATA
270        .set(icon_with_metadata)
271        .map_err(|_| anyhow!("ICON_WITH_METADATA shouldn't be set"))?;
272    Ok(())
273}
274
275/// Set all the values which could be configured from config file or arguments staticly.
276/// It allows us to read those values globally without having to pass them through to every function.
277/// All values use a [`std::sync::OnceLock`] internally.
278pub fn set_configurable_static(
279    start_folder: &str,
280    plugins: Vec<(String, String)>,
281    theme: String,
282) -> Result<()> {
283    let theme_yaml = read_theme(theme);
284    set_start_folder(start_folder)?;
285    set_menu_styles(&theme_yaml)?;
286    set_file_styles(&theme_yaml)?;
287    set_normal_file_colorer(&theme_yaml)?;
288    set_icon_icon_with_metadata()?;
289    set_syntect_theme()?;
290    set_prefered_imager()?;
291    set_previewer_plugins(plugins)?;
292    set_previewer_command()?;
293    Ok(())
294}
295
296fn read_theme(theme: String) -> Option<Value> {
297    read_yaml_value(&build_theme_path(theme))
298}
299
300fn build_theme_path(theme: String) -> PathBuf {
301    let config_folder = tilde(CONFIG_FOLDER);
302    let mut theme_path = PathBuf::from(config_folder.as_ref());
303    theme_path.push("themes");
304    theme_path.push(theme);
305    theme_path.set_extension("yaml");
306    theme_path
307}
308
309fn read_yaml_value(path: &Path) -> Option<Value> {
310    let Ok(file) = File::open(path) else {
311        return None;
312    };
313    let Ok(yaml) = from_reader::<File, Value>(file) else {
314        return None;
315    };
316    Some(yaml)
317}
318
319/// Copied from [Helix](https://github.com/helix-editor/helix/blob/master/helix-core/src/fuzzy.rs)
320///
321/// A mutex which is instancied lazylly.
322/// The mutex is created with `None` as value and, once locked, is instancied if necessary.
323pub struct LazyMutex<T> {
324    inner: Mutex<Option<T>>,
325    init: fn() -> T,
326}
327
328impl<T> LazyMutex<T> {
329    /// Instanciate a new `LazyMutex` with `None` as value.
330    pub const fn new(init: fn() -> T) -> Self {
331        Self {
332            inner: Mutex::new(None),
333            init,
334        }
335    }
336
337    /// Lock the mutex.
338    /// At the first call, the value is created with the `init` function passed to `new`.
339    /// Other calls won't have to do it. We just get the already created value.
340    pub fn lock(&self) -> impl DerefMut<Target = T> + '_ {
341        MutexGuard::map(self.inner.lock(), |val| val.get_or_insert_with(self.init))
342    }
343}
344
345/// A nucleo matcher behind a lazy mutex.
346/// Instanciated once and lazylly.
347pub static MATCHER: LazyMutex<Matcher> = LazyMutex::new(Matcher::default);