Repository: alacritty/alacritty (language: rust)

Issue:
Restart config monitor on file reload
Making changes to a configuration file that is imported by the main alacritty.toml file do not trigger an automatic config reload.

In order to get the changes without restarting alacritty I normally just do something like `touch ~/.config/alacritty/alacritty.toml`.

### System

OS: Linux
Version: alacritty 0.13.2 (bb8ea18e)



Discussion / hints:
you need to touch imported files and not the main one to reload them. 
If I modify (or touch) the toml file being imported (say theme.toml), it does not auto reload the config (as I believe it should).  Instead, I need to touch the top level config (alacritty.toml) for it reload the changes I made in theme.toml

Any symlinking involved? It works just fine for me.
My entire `~/.config/alacritty` directory is a symlink to my dotfiles directory:

`lrwxrwxrwx  1 tllilleh tllilleh   39 May 14 10:13  alacritty -> ../dotfiles/alacritty/.config/alacritty/`

But inside the `alacritty` directory there are no symlinks:

```
$ ll -R
.:
total 16
drwxrwxr-x 3 tllilleh tllilleh 4096 May 15 15:19 ./
drwxrwxr-x 3 tllilleh tllilleh 4096 May 14 10:11 ../
-rw-rw-r-- 1 tllilleh tllilleh  576 May 15 17:24 alacritty.toml
drwxrwxr-x 2 tllilleh tllilleh 4096 May 15 17:24 themes/

./themes:
total 28
drwxrwxr-x 2 tllilleh tllilleh 4096 May 15 17:24 ./
drwxrwxr-x 3 tllilleh tllilleh 4096 May 15 15:19 ../
-rw-rw-r-- 1 tllilleh tllilleh  711 May 15 16:39 base16.toml
-rw-rw-r-- 1 tllilleh tllilleh  494 May 15 08:54 everforest_dark.toml
-rw-rw-r-- 1 tllilleh tllilleh  573 May 15 08:56 gruvbox_dark.toml
-rw-rw-r-- 1 tllilleh tllilleh  638 May 15 17:24 kanagawa_dragon.toml
-rw-rw-r-- 1 tllilleh tllilleh  556 May 15 08:54 zenburn.toml
```
~~Does it work without any symlinks?~~

Nvm, I managed to reproduce without symlinks.
Is it accurate to say you added the import after starting Alacritty? Config imports are only monitored if the import is present at startup.
> Is it accurate to say you added the import after starting Alacritty? Config imports are only monitored if the import is present at startup.

Yes, that seems to be the issue.  If I restart alacritty now changes to my theme files are auto reloaded.
I think there's two reasonable options here, both of which should work fine:
 1) Shut down config monitor when the imports change
 2) Notify the config monitor about changes to specific files (add/remove)
My config imports a theme file but live_config_reload doesn't work and doesn't work if I comment out the import??
I've been trying to figure out why my script to flip alacritty themes between light and dark themes only updates some of the terminals. I think it may be this issue.

In the meantime, is there a command I can run to force alacritty to re-read its config file? `touch` doesn't seem to work for some reason. If I edit the config file in vim and `:w` the terminals all update.

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 203624fed39..20df64f0dc1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -36,6 +36,7 @@ Notable changes to the `alacritty_terminal` crate are documented in its
 - Config emitting errors for nonexistent import paths
 - Kitty keyboard protocol reporting shifted key codes
 - Broken search with words broken across line boundary on the first character
+- Config import changes not being live reloaded
 
 ## 0.13.2
 
diff --git a/alacritty/src/config/monitor.rs b/alacritty/src/config/monitor.rs
index 53cff1c90c2..3f73f120101 100644
--- a/alacritty/src/config/monitor.rs
+++ b/alacritty/src/config/monitor.rs
@@ -1,3 +1,5 @@
+use std::collections::hash_map::DefaultHasher;
+use std::hash::{Hash, Hasher};
 use std::path::PathBuf;
 use std::sync::mpsc::{self, RecvTimeoutError, Sender};
 use std::thread::JoinHandle;
@@ -23,6 +25,7 @@ const FALLBACK_POLLING_TIMEOUT: Duration = Duration::from_secs(1);
 pub struct ConfigMonitor {
     thread: JoinHandle<()>,
     shutdown_tx: Sender<Result<NotifyEvent, NotifyError>>,
+    watched_hash: Option<u64>,
 }
 
 impl ConfigMonitor {
@@ -32,6 +35,9 @@ impl ConfigMonitor {
             return None;
         }
 
+        // Calculate the hash for the unmodified list of paths.
+        let watched_hash = Self::hash_paths(&paths);
+
         // Exclude char devices like `/dev/null`, sockets, and so on, by checking that file type is
         // a regular file.
         paths.retain(|path| {
@@ -139,7 +145,7 @@ impl ConfigMonitor {
             }
         });
 
-        Some(Self { thread: join_handle, shutdown_tx: tx })
+        Some(Self { watched_hash, thread: join_handle, shutdown_tx: tx })
     }
 
     /// Synchronously shut down the monitor.
@@ -154,4 +160,33 @@ impl ConfigMonitor {
             warn!("config monitor shutdown failed: {err:?}");
         }
     }
+
+    /// Check if the config monitor needs to be restarted.
+    ///
+    /// This checks the supplied list of files against the monitored files to determine if a
+    /// restart is necessary.
+    pub fn needs_restart(&self, files: &[PathBuf]) -> bool {
+        Self::hash_paths(files).map_or(true, |hash| Some(hash) == self.watched_hash)
+    }
+
+    /// Generate the hash for a list of paths.
+    fn hash_paths(files: &[PathBuf]) -> Option<u64> {
+        // Use file count limit to avoid allocations.
+        const MAX_PATHS: usize = 1024;
+        if files.len() > MAX_PATHS {
+            return None;
+        }
+
+        // Sort files to avoid restart on order change.
+        let mut sorted_files = [None; MAX_PATHS];
+        for (i, file) in files.iter().enumerate() {
+            sorted_files[i] = Some(file);
+        }
+        sorted_files.sort_unstable();
+
+        // Calculate hash for the paths, regardless of order.
+        let mut hasher = DefaultHasher::new();
+        Hash::hash_slice(&sorted_files, &mut hasher);
+        Some(hasher.finish())
+    }
 }
diff --git a/alacritty/src/event.rs b/alacritty/src/event.rs
index 8da758dfb65..72009d88875 100644
--- a/alacritty/src/event.rs
+++ b/alacritty/src/event.rs
@@ -1,5 +1,6 @@
 //! Process window events.
 
+use crate::ConfigMonitor;
 use std::borrow::Cow;
 use std::cmp::min;
 use std::collections::{HashMap, HashSet, VecDeque};
@@ -70,6 +71,8 @@ const TOUCH_ZOOM_FACTOR: f32 = 0.01;
 /// Stores some state from received events and dispatches actions when they are
 /// triggered.
 pub struct Processor {
+    pub config_monitor: Option<ConfigMonitor>,
+
     clipboard: Clipboard,
     scheduler: Scheduler,
     initial_window_options: Option<WindowOptions>,
@@ -101,6 +104,16 @@ impl Processor {
         // which is done in `loop_exiting`.
         let clipboard = unsafe { Clipboard::new(event_loop.display_handle().unwrap().as_raw()) };
 
+        // Create a config monitor.
+        //
+        // The monitor watches the config file for changes and reloads it. Pending
+        // config changes are processed in the main loop.
+        let mut config_monitor = None;
+        if config.live_config_reload {
+            config_monitor =
+                ConfigMonitor::new(config.config_paths.clone(), event_loop.create_proxy());
+        }
+
         Processor {
             initial_window_options,
             initial_window_error: None,
@@ -113,6 +126,7 @@ impl Processor {
             windows: Default::default(),
             #[cfg(unix)]
             global_ipc_options: Default::default(),
+            config_monitor,
         }
     }
 
@@ -160,8 +174,8 @@ impl Processor {
     /// Run the event loop.
     ///
     /// The result is exit code generate from the loop.
-    pub fn run(mut self, event_loop: EventLoop<Event>) -> Result<(), Box<dyn Error>> {
-        let result = event_loop.run_app(&mut self);
+    pub fn run(&mut self, event_loop: EventLoop<Event>) -> Result<(), Box<dyn Error>> {
+        let result = event_loop.run_app(self);
         if let Some(initial_window_error) = self.initial_window_error.take() {
             Err(initial_window_error)
         } else {
@@ -297,6 +311,17 @@ impl ApplicationHandler<Event> for Processor {
                 if let Ok(config) = config::reload(path, &mut self.cli_options) {
                     self.config = Rc::new(config);
 
+                    // Restart config monitor if imports changed.
+                    if let Some(monitor) = self.config_monitor.take() {
+                        let paths = &self.config.config_paths;
+                        self.config_monitor = if monitor.needs_restart(paths) {
+                            monitor.shutdown();
+                            ConfigMonitor::new(paths.clone(), self.proxy.clone())
+                        } else {
+                            Some(monitor)
+                        };
+                    }
+
                     for window_context in self.windows.values_mut() {
                         window_context.update_config(self.config.clone());
                     }
diff --git a/alacritty/src/main.rs b/alacritty/src/main.rs
index 6219dd78e1c..da50c3e412b 100644
--- a/alacritty/src/main.rs
+++ b/alacritty/src/main.rs
@@ -172,16 +172,6 @@ fn alacritty(mut options: Options) -> Result<(), Box<dyn Error>> {
     #[cfg(target_os = "macos")]
     locale::set_locale_environment();
 
-    // Create a config monitor when config was loaded from path.
-    //
-    // The monitor watches the config file for changes and reloads it. Pending
-    // config changes are processed in the main loop.
-    let mut config_monitor = None;
-    if config.live_config_reload {
-        config_monitor =
-            ConfigMonitor::new(config.config_paths.clone(), window_event_loop.create_proxy());
-    }
-
     // Create the IPC socket listener.
     #[cfg(unix)]
     let socket_path = if config.ipc_socket {
@@ -199,7 +189,7 @@ fn alacritty(mut options: Options) -> Result<(), Box<dyn Error>> {
     };
 
     // Event processor.
-    let processor = Processor::new(config, options, &window_event_loop);
+    let mut processor = Processor::new(config, options, &window_event_loop);
 
     // Start event loop and block until shutdown.
     let result = processor.run(window_event_loop);
@@ -219,7 +209,7 @@ fn alacritty(mut options: Options) -> Result<(), Box<dyn Error>> {
     // FIXME: Change PTY API to enforce the correct drop order with the typesystem.
 
     // Terminate the config monitor.
-    if let Some(config_monitor) = config_monitor.take() {
+    if let Some(config_monitor) = processor.config_monitor.take() {
         config_monitor.shutdown();
     }
 


Test patch: