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