/* * Copyright (c) 2023 Marceline Cramer * Copyright (c) 2023 Emma Tebibyte * SPDX-License-Identifier: AGPL-3.0-or-later * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more * details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ use std::path::{Path, PathBuf}; use std::sync::Arc; use crossbeam_channel::{unbounded, Receiver, RecvError, Sender}; use notify::{Event, EventKind, RecommendedWatcher, Watcher}; use parking_lot::Mutex; use crate::keybinds::Keybinds; use crate::theme::{StyleStore, Theme}; pub struct Config { /// Whether or not to wrap lines when moving the cursor left and right. pub move_linewrap: bool, /// Key bindings. pub keybinds: Keybinds, } impl Default for Config { fn default() -> Self { Self { move_linewrap: true, keybinds: Default::default(), } } } /// Gets the configuration directory. Panics if unavailable. pub fn get_config_dir() -> PathBuf { std::env::var_os("XDG_CONFIG_HOME") .map(|p| PathBuf::try_from(p).expect("$XDG_CONFIG_HOME is not a valid path.")) .unwrap_or_else(|| { std::env::var_os("HOME") .map(PathBuf::try_from) .expect("$HOME is not a valid path.") .expect("User has no $HOME or $XDG_CONFIG_HOME.") .join(".config") }) .join("breed") } pub fn get_data_dir() -> PathBuf { std::env::var_os("XDG_DATA_HOME") .map(|p| PathBuf::try_from(p).expect("$XDG_DATA_HOME is not a valid path.")) .unwrap_or_else(|| { std::env::var_os("HOME") .map(PathBuf::try_from) .expect("$HOME is not a valid path.") .expect("User has no $HOME or $XDG_DATA_HOME.") .join(".local/share") }) .join("breed") } /// Watches a theme file and automatically reloads it into a [StyleStore]. pub struct ThemeWatcher { styles: Arc>, watcher: RecommendedWatcher, current_path: PathBuf, fs_rx: Receiver>, command_rx: Receiver, quit: bool, } impl ThemeWatcher { pub fn spawn(styles: Arc>) -> Sender { let themes_dir = get_data_dir().join("themes"); let default_path = themes_dir.join("default.toml"); let (fs_tx, fs_rx) = unbounded(); let (command_tx, command_rx) = unbounded(); let watcher = notify::recommended_watcher(fs_tx).unwrap(); let mut watcher = Self { styles, current_path: PathBuf::new(), watcher, fs_rx, command_rx, quit: false, }; watcher.watch_theme(&default_path).unwrap(); std::thread::spawn(move || watcher.run()); command_tx } fn load_theme(&mut self, path: &Path) -> Result<(), String> { let name = path.file_stem().unwrap_or_default().to_string_lossy(); let contents = std::fs::read_to_string(path) .map_err(|err| format!("Failed to read theme file: {:?}", err))?; let value = toml::from_str(&contents).map_err(|err| format!("Failed to parse theme: {:?}", err))?; let theme = Theme::from_value(&name, value)?; self.styles.lock().apply(theme); Ok(()) } fn watch_theme(&mut self, path: &Path) -> notify::Result<()> { let old = self.current_path.clone(); let mode = notify::RecursiveMode::NonRecursive; self.watcher.watch(&path, mode)?; self.current_path = path.to_path_buf(); if old != PathBuf::new() { self.watcher.unwatch(&old)?; } self.load_theme(path).map_err(|err| notify::Error { kind: notify::ErrorKind::Generic(err), paths: vec![path.to_owned()], }) } fn on_fs(&mut self, event: Result, crossbeam_channel::RecvError>) { match event { Err(RecvError) => self.quit = true, Ok(Ok(event)) => match event.kind { EventKind::Modify(_) => { for path in event.paths { self.load_theme(&path).unwrap(); } } _ => {} }, _ => {} // TODO display errors? } } fn on_command(&mut self, path: Result) { match path { Err(RecvError) => self.quit = true, Ok(path) => { self.watch_theme(&path).unwrap(); } } } fn run(mut self) { while !self.quit { crossbeam_channel::select! { recv(self.fs_rx) -> event => self.on_fs(event), recv(self.command_rx) -> path => self.on_command(path), } } } }