167 lines
5.3 KiB
Rust
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),
|
|
}
|
|
}
|
|
}
|
|
}
|