breed/src/config.rs

167 lines
5.3 KiB
Rust

/*
* Copyright (c) 2023 Marceline Cramer
* Copyright (c) 2023 Emma Tebibyte <emma@tebibyte.media>
* 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<Mutex<StyleStore>>,
watcher: RecommendedWatcher,
current_path: PathBuf,
fs_rx: Receiver<notify::Result<Event>>,
command_rx: Receiver<PathBuf>,
quit: bool,
}
impl ThemeWatcher {
pub fn spawn(styles: Arc<Mutex<StyleStore>>) -> Sender<PathBuf> {
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<notify::Result<Event>, 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<PathBuf, RecvError>) {
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),
}
}
}
}