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::app::{build_plugins, PreviewerPlugin};
20use crate::config::{
21    read_normal_file_colorer, FileStyle, Gradient, MenuStyle, NormalFileColorer, PreferedImager,
22    SyntectTheme, MAX_GRADIENT_NORMAL,
23};
24
25use crate::{
26    common::{tilde, CONFIG_PATH, SYNTECT_THEMES_PATH},
27    log_info,
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 PLUGINS: OnceLock<Vec<(String, PreviewerPlugin)>> = OnceLock::new();
61
62static PREFERED_IMAGER: OnceLock<PreferedImager> = OnceLock::new();
63
64pub fn get_prefered_imager() -> Option<&'static PreferedImager> {
65    PREFERED_IMAGER.get()
66}
67
68/// Attach a map of name -> path to the `PLUGINS` static variable.
69pub fn set_previewer_plugins(plugins: Vec<(String, String)>) -> Result<()> {
70    let _ = PLUGINS.set(build_plugins(plugins));
71    Ok(())
72}
73
74/// `PLUGINS` static map. Returns a map of name -> path.
75pub fn get_previewer_plugins() -> Option<&'static Vec<(String, PreviewerPlugin)>> {
76    PLUGINS.get()
77}
78
79/// Reads the syntect_theme configuration value and tries to load if from configuration files.
80///
81/// If it doesn't work, it will load the default set from binary file itself: monokai.
82pub fn set_syntect_theme() -> Result<()> {
83    let config_theme = SyntectTheme::from_config(CONFIG_PATH)?;
84    if !set_syntect_theme_from_config(&config_theme.name) {
85        set_syntect_theme_from_source_code()
86    }
87    Ok(())
88}
89
90pub fn set_prefered_imager() -> Result<()> {
91    let prefered_imager = PreferedImager::from_config(CONFIG_PATH)?;
92    let _ = PREFERED_IMAGER.set(prefered_imager);
93    Ok(())
94}
95
96#[derive(EnumIter, Debug)]
97enum SyntectThemeKind {
98    TmTheme,
99    Dump,
100}
101
102impl SyntectThemeKind {
103    fn extension(&self) -> &str {
104        match self {
105            Self::TmTheme => "tmTheme",
106            Self::Dump => "themedump",
107        }
108    }
109
110    fn load(&self, themepath: &Path) -> Result<Theme> {
111        match self {
112            Self::TmTheme => ThemeSet::get_theme(themepath)
113                .map_err(|e| anyhow!("Couldn't load syntect theme {e:}")),
114            Self::Dump => {
115                from_dump_file(themepath).map_err(|e| anyhow!("Couldn't load syntect theme {e:}"))
116            }
117        }
118    }
119}
120
121fn set_syntect_theme_from_config(syntect_theme: &str) -> bool {
122    let syntect_theme_path = PathBuf::from(tilde(SYNTECT_THEMES_PATH).as_ref());
123    for kind in SyntectThemeKind::iter() {
124        if load_syntect(&syntect_theme_path, syntect_theme, &kind) {
125            return true;
126        }
127        log_info!("Couldn't load {syntect_theme} {kind:?}");
128    }
129    false
130}
131
132fn load_syntect(syntect_theme_path: &Path, syntect_theme: &str, kind: &SyntectThemeKind) -> bool {
133    let mut full_path = syntect_theme_path.to_path_buf();
134    full_path.push(syntect_theme);
135    full_path.set_extension(kind.extension());
136    if !full_path.exists() {
137        return false;
138    }
139    let Ok(theme) = kind.load(&full_path) else {
140        crate::log_info!("Syntect couldn't load {fp}", fp = full_path.display());
141        return false;
142    };
143    let name = theme.name.clone();
144    if SYNTECT_THEME.set(theme).is_ok() {
145        log_info!("SYNTECT_THEME set to {name:?}");
146        true
147    } else {
148        crate::log_info!("SYNTECT_THEME was already set!");
149        false
150    }
151}
152
153fn set_syntect_theme_from_source_code() {
154    let _ = SYNTECT_THEME.set(from_binary(include_bytes!(
155        "../../assets/themes/monokai.themedump"
156    )));
157}
158
159/// Reads the syntect theme from memory. It should never be `None`.
160pub fn get_syntect_theme() -> Option<&'static Theme> {
161    SYNTECT_THEME.get()
162}
163
164static ICON: OnceLock<bool> = OnceLock::new();
165static ICON_WITH_METADATA: OnceLock<bool> = OnceLock::new();
166
167/// Does the user wants nerdfont icons ? Default: false.
168pub fn with_icon() -> bool {
169    *ICON.get().unwrap_or(&false)
170}
171
172/// Does the user wants nerdfont icons even if metadata are shown ? Default: false.
173pub fn with_icon_metadata() -> bool {
174    *ICON_WITH_METADATA.get().unwrap_or(&false)
175}
176
177fn set_start_folder(start_folder: &str) -> Result<()> {
178    START_FOLDER
179        .set(std::fs::canonicalize(tilde(start_folder).as_ref()).unwrap_or_default())
180        .map_err(|_| anyhow!("Start folder shouldn't be set"))?;
181    Ok(())
182}
183
184fn set_file_styles() -> Result<()> {
185    FILE_STYLES
186        .set(FileStyle::from_config())
187        .map_err(|_| anyhow!("File colors shouldn't be set"))?;
188    Ok(())
189}
190
191fn set_menu_styles() -> Result<()> {
192    MENU_STYLES
193        .set(MenuStyle::default().update())
194        .map_err(|_| anyhow!("Menu colors shouldn't be set"))?;
195    Ok(())
196}
197
198fn set_normal_file_colorer() -> Result<()> {
199    let (start_color, stop_color) = read_normal_file_colorer();
200    ARRAY_GRADIENT
201        .set(Gradient::new(start_color, stop_color, MAX_GRADIENT_NORMAL).as_array()?)
202        .map_err(|_| anyhow!("Gradient shouldn't be set"))?;
203    COLORER
204        .set(NormalFileColorer::colorer as fn(usize) -> Color)
205        .map_err(|_| anyhow!("Colorer shouldn't be set"))?;
206
207    Ok(())
208}
209
210fn read_yaml_bool(yaml: &Value, key: &str) -> Option<bool> {
211    yaml[key].as_bool()
212}
213
214fn read_icon_icon_with_metadata() -> (bool, bool) {
215    let Ok(file) = File::open(Path::new(&tilde(CONFIG_PATH).to_string())) else {
216        crate::log_info!("Couldn't read config file at {CONFIG_PATH}");
217        return (false, false);
218    };
219    let Ok(yaml) = from_reader::<File, Value>(file) else {
220        return (false, false);
221    };
222    let mut icon: bool = false;
223    let mut icon_with_metadata: bool = false;
224    if let Some(i) = read_yaml_bool(&yaml, "icon") {
225        icon = i;
226    }
227    if !icon {
228        icon_with_metadata = false;
229    } else if let Some(icon_with) = read_yaml_bool(&yaml, "icon_with_metadata") {
230        icon_with_metadata = icon_with;
231    }
232    (icon, icon_with_metadata)
233}
234
235/// Read `icon` and `icon_with_metadata` from the config file and store them in static values.
236///
237/// `icon_with_metadata` can't be true if `icon` is false, even if the user set it to true.
238/// If the user hasn't installed nerdfont, the icons can't be shown properly and `icon` shouldn't be shown.
239/// It leads to a quite complex parsing:
240/// - If the file can't be read (should never happen, the application should have quit already), both icon & icon_with_metadata are false,
241/// - If the values aren't in the yaml file, both are false,
242/// - If icon is false, icon_with_metadata is false,
243/// - Otherwise, we use the values from the file.
244pub fn set_icon_icon_with_metadata() -> Result<()> {
245    let (icon, icon_with_metadata) = read_icon_icon_with_metadata();
246    ICON.set(icon)
247        .map_err(|_| anyhow!("ICON shouldn't be set"))?;
248    ICON_WITH_METADATA
249        .set(icon_with_metadata)
250        .map_err(|_| anyhow!("ICON_WITH_METADATA shouldn't be set"))?;
251    Ok(())
252}
253
254/// Set all the values which could be configured from config file or arguments staticly.
255/// It allows us to read those values globally without having to pass them through to every function.
256/// All values use a [`std::sync::OnceLock`] internally.
257pub fn set_configurable_static(start_folder: &str, plugins: Vec<(String, String)>) -> Result<()> {
258    set_start_folder(start_folder)?;
259    set_menu_styles()?;
260    set_file_styles()?;
261    set_normal_file_colorer()?;
262    set_icon_icon_with_metadata()?;
263    set_syntect_theme()?;
264    set_prefered_imager()?;
265    set_previewer_plugins(plugins)
266}
267
268/// Copied from [Helix](https://github.com/helix-editor/helix/blob/master/helix-core/src/fuzzy.rs)
269///
270/// A mutex which is instancied lazylly.
271/// The mutex is created with `None` as value and, once locked, is instancied if necessary.
272pub struct LazyMutex<T> {
273    inner: Mutex<Option<T>>,
274    init: fn() -> T,
275}
276
277impl<T> LazyMutex<T> {
278    /// Instanciate a new `LazyMutex` with `None` as value.
279    pub const fn new(init: fn() -> T) -> Self {
280        Self {
281            inner: Mutex::new(None),
282            init,
283        }
284    }
285
286    /// Lock the mutex.
287    /// At the first call, the value is created with the `init` function passed to `new`.
288    /// Other calls won't have to do it. We just get the already created value.
289    pub fn lock(&self) -> impl DerefMut<Target = T> + '_ {
290        MutexGuard::map(self.inner.lock(), |val| val.get_or_insert_with(self.init))
291    }
292}
293
294/// A nucleo matcher behind a lazy mutex.
295/// Instanciated once and lazylly.
296pub static MATCHER: LazyMutex<Matcher> = LazyMutex::new(Matcher::default);