use std::{env, fs, sync::mpsc, time::Duration};
use notify_debouncer_full::{new_debouncer, notify::RecursiveMode};
use serde::Deserialize;
use winit::event_loop::EventLoopProxy;
use crate::{
cmd_line::{GeometryArgs, MouseCursorIcon},
error_msg,
frame::Frame,
renderer::box_drawing::BoxDrawingSettings,
window::{EventPayload, UserEvent},
};
use std::path::{Path, PathBuf};
use super::font::FontSettings;
const CONFIG_FILE: &str = "config.toml";
#[cfg(unix)]
fn neovide_config_dir() -> PathBuf {
let xdg_dirs = xdg::BaseDirectories::with_prefix("neovide");
xdg_dirs.get_config_home().unwrap()
}
#[cfg(windows)]
fn neovide_config_dir() -> PathBuf {
let mut path = dirs::config_dir().unwrap();
path.push("neovide");
path
}
pub fn config_path() -> PathBuf {
env::var("NEOVIDE_CONFIG")
.ok()
.map(PathBuf::from)
.filter(|path| path.exists() && path.is_file())
.unwrap_or_else(|| {
let mut path = neovide_config_dir();
path.push(CONFIG_FILE);
path
})
}
#[derive(Debug, Deserialize, Default, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct Config {
pub font: Option<FontSettings>,
pub box_drawing: Option<BoxDrawingSettings>,
pub server: Option<String>,
pub fork: Option<bool>,
pub frame: Option<Frame>,
pub size: Option<String>,
pub grid: Option<String>,
pub idle: Option<bool>,
pub maximized: Option<bool>,
pub neovim_bin: Option<PathBuf>,
pub no_multigrid: Option<bool>,
pub srgb: Option<bool>,
pub tabs: Option<bool>,
pub system_native_tabs: Option<bool>,
pub mouse_cursor_icon: Option<String>,
pub title_hidden: Option<bool>,
pub vsync: Option<bool>,
pub wsl: Option<bool>,
pub backtraces_path: Option<PathBuf>,
pub system_pinned_hotkey: Option<String>,
pub system_switcher_hotkey: Option<String>,
pub system_tab_prev_hotkey: Option<String>,
pub system_tab_next_hotkey: Option<String>,
pub icon: Option<String>,
pub chdir: Option<PathBuf>,
pub opengl: Option<bool>,
pub wayland_app_id: Option<String>,
pub x11_wm_class: Option<String>,
pub x11_wm_class_instance: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
#[allow(clippy::large_enum_variant)]
pub enum HotReloadConfigs {
App(AppHotReloadConfigs),
Renderer(RendererHotReloadConfigs),
Window(WindowHotReloadConfigs),
}
#[derive(Debug, Clone, PartialEq)]
pub enum AppHotReloadConfigs {
Idle(bool),
}
#[derive(Debug, Clone, PartialEq)]
pub enum RendererHotReloadConfigs {
Font(Box<Option<FontSettings>>),
BoxDrawing(Option<BoxDrawingSettings>),
}
#[derive(Debug, Clone, PartialEq)]
pub enum WindowHotReloadConfigs {
TitleHidden(Option<bool>),
MouseCursorIcon(MouseCursorIcon),
Geometry(GeometryArgs),
}
impl Config {
pub fn init() -> Config {
let config = Config::load_from_path(&config_path());
match &config {
Ok(config) => config.write_to_env(),
Err(Some(err)) => eprintln!("{err}"),
Err(None) => {}
};
config.unwrap_or_default()
}
pub fn watch_config_file(init_config: Config, event_loop_proxy: EventLoopProxy<EventPayload>) {
std::thread::spawn(move || watcher_thread(init_config, event_loop_proxy));
}
fn write_to_env(&self) {
if let Some(server) = &self.server {
env::set_var("NEOVIDE_SERVER", server);
}
if let Some(wsl) = self.wsl {
env::set_var("NEOVIDE_WSL", wsl.to_string());
}
if let Some(no_multigrid) = self.no_multigrid {
env::set_var("NEOVIDE_NO_MULTIGRID", no_multigrid.to_string());
}
if let Some(maximized) = self.maximized {
env::set_var("NEOVIDE_MAXIMIZED", maximized.to_string());
}
if let Some(vsync) = self.vsync {
env::set_var("NEOVIDE_VSYNC", vsync.to_string());
}
if let Some(srgb) = self.srgb {
env::set_var("NEOVIDE_SRGB", srgb.to_string());
}
if let Some(fork) = self.fork {
env::set_var("NEOVIDE_FORK", fork.to_string());
}
if let Some(opengl) = self.opengl {
env::set_var("NEOVIDE_OPENGL", opengl.to_string());
}
if let Some(idle) = self.idle {
env::set_var("NEOVIDE_IDLE", idle.to_string());
}
if let Some(frame) = self.frame {
env::set_var("NEOVIDE_FRAME", frame.to_string());
}
if let Some(size) = &self.size {
env::set_var("NEOVIDE_SIZE", size);
}
if let Some(grid) = &self.grid {
env::set_var("NEOVIDE_GRID", grid);
}
if let Some(neovim_bin) = &self.neovim_bin {
env::set_var("NEOVIM_BIN", neovim_bin.to_string_lossy().to_string());
}
if let Some(mouse_cursor_icon) = &self.mouse_cursor_icon {
env::set_var("NEOVIDE_MOUSE_CURSOR_ICON", mouse_cursor_icon);
}
if let Some(title_hidden) = &self.title_hidden {
env::set_var("NEOVIDE_TITLE_HIDDEN", title_hidden.to_string());
}
if let Some(tabs) = &self.tabs {
env::set_var("NEOVIDE_TABS", tabs.to_string());
}
if let Some(system_native_tabs) = &self.system_native_tabs {
env::set_var("NEOVIDE_SYSTEM_NATIVE_TABS", system_native_tabs.to_string());
}
if let Some(pinned_hotkey) = &self.system_pinned_hotkey {
env::set_var("NEOVIDE_SYSTEM_PINNED_HOTKEY", pinned_hotkey);
}
if let Some(switcher_hotkey) = &self.system_switcher_hotkey {
env::set_var("NEOVIDE_SYSTEM_SWITCHER_HOTKEY", switcher_hotkey);
}
if let Some(tab_prev_hotkey) = &self.system_tab_prev_hotkey {
env::set_var("NEOVIDE_SYSTEM_TAB_PREV_HOTKEY", tab_prev_hotkey);
}
if let Some(tab_next_hotkey) = &self.system_tab_next_hotkey {
env::set_var("NEOVIDE_SYSTEM_TAB_NEXT_HOTKEY", tab_next_hotkey);
}
if let Some(icon) = &self.icon {
env::set_var("NEOVIDE_ICON", icon);
}
if let Some(wayland_app_id) = &self.wayland_app_id {
env::set_var("NEOVIDE_APP_ID", wayland_app_id);
}
if let Some(x11_wm_class) = &self.x11_wm_class {
env::set_var("NEOVIDE_WM_CLASS", x11_wm_class);
}
if let Some(x11_wm_class_instance) = &self.x11_wm_class_instance {
env::set_var("NEOVIDE_WM_CLASS_INSTANCE", x11_wm_class_instance);
}
if let Some(chdir) = &self.chdir {
env::set_var("NEOVIDE_CHDIR", chdir.to_string_lossy().to_string());
}
}
fn load_from_path(path: &Path) -> Result<Self, Option<String>> {
if !path.exists() {
return Err(None);
}
let toml = fs::read_to_string(path).map_err(|e| {
format!(
"Error while trying to open config file {}:\n{}\nContinuing with default config.",
path.to_string_lossy(),
e
)
})?;
let config = toml::from_str(&toml).map_err(|e| {
format!(
"Error while parsing config file {}:\n{}\nContinuing with default config.",
path.to_string_lossy(),
e
)
})?;
Ok(config)
}
}
fn watcher_thread(init_config: Config, event_loop_proxy: EventLoopProxy<EventPayload>) {
let config_path = config_path();
let parent_path = match config_path.parent() {
Some(dir) => dir,
None => return,
};
let (tx, rx) = mpsc::channel();
let mut debouncer = new_debouncer(Duration::from_millis(500), None, tx).unwrap();
if let Err(e) = debouncer.watch(
parent_path,
RecursiveMode::NonRecursive,
) {
log::warn!("Error while trying to watch config file parent directory for changes: {e}");
return;
}
let mut previous_config = init_config;
loop {
if let Err(e) = rx.recv() {
eprintln!("Error while watching config file: {e}");
continue;
}
let config = match Config::load_from_path(&config_path) {
Ok(config) => config,
Err(maybe_err) => {
if let Some(err) = maybe_err {
error_msg!("While reloading config file: {err}");
}
continue;
}
};
if config.font != previous_config.font {
event_loop_proxy
.send_event(EventPayload::all(UserEvent::ConfigsChanged(Box::new(
HotReloadConfigs::Renderer(RendererHotReloadConfigs::Font(Box::new(
config.font.clone(),
))),
))))
.unwrap();
}
if config.box_drawing != previous_config.box_drawing {
event_loop_proxy
.send_event(EventPayload::all(UserEvent::ConfigsChanged(Box::new(
HotReloadConfigs::Renderer(RendererHotReloadConfigs::BoxDrawing(
config.box_drawing.clone(),
)),
))))
.unwrap();
}
if config.idle != previous_config.idle {
event_loop_proxy
.send_event(EventPayload::all(UserEvent::ConfigsChanged(Box::new(
HotReloadConfigs::App(AppHotReloadConfigs::Idle(config.idle.unwrap_or(true))),
))))
.unwrap();
}
if config.title_hidden != previous_config.title_hidden {
event_loop_proxy
.send_event(EventPayload::all(UserEvent::ConfigsChanged(Box::new(
HotReloadConfigs::Window(WindowHotReloadConfigs::TitleHidden(
config.title_hidden,
)),
))))
.unwrap();
}
if config.mouse_cursor_icon != previous_config.mouse_cursor_icon {
match MouseCursorIcon::from_config(config.mouse_cursor_icon.as_deref()) {
Ok(mouse_cursor_icon) => {
event_loop_proxy
.send_event(EventPayload::all(UserEvent::ConfigsChanged(Box::new(
HotReloadConfigs::Window(WindowHotReloadConfigs::MouseCursorIcon(
mouse_cursor_icon,
)),
))))
.unwrap();
}
Err(err) => {
error_msg!("While reloading config file: invalid mouse-cursor-icon: {err}");
}
}
}
if config.size != previous_config.size
|| config.grid != previous_config.grid
|| config.maximized != previous_config.maximized
{
match GeometryArgs::from_config(
config.size.as_deref(),
config.grid.as_deref(),
config.maximized,
) {
Ok(geometry) => {
event_loop_proxy
.send_event(EventPayload::all(UserEvent::ConfigsChanged(Box::new(
HotReloadConfigs::Window(WindowHotReloadConfigs::Geometry(geometry)),
))))
.unwrap();
}
Err(err) => {
error_msg!("While reloading config file: invalid geometry: {err}");
}
}
}
previous_config = config;
}
}