aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md1
-rw-r--r--alacritty.yml8
-rw-r--r--alacritty/src/config/mod.rs109
-rw-r--r--alacritty/src/config/monitor.rs115
-rw-r--r--alacritty/src/config/serde_utils.rs89
-rw-r--r--alacritty/src/config/ui_config.rs7
-rw-r--r--alacritty/src/event.rs2
-rw-r--r--alacritty/src/main.rs15
-rw-r--r--alacritty_terminal/src/config/mod.rs4
9 files changed, 269 insertions, 81 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f09c25b7..b32a0905 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Support for colon separated SGR 38/48
- New Ctrl+C binding to cancel search and leave vi mode
- Escapes for double underlines (`CSI 4 : 2 m`) and underline reset (`CSI 4 : 0 m`)
+- Configuration file option for sourcing other files (`import`)
### Changed
diff --git a/alacritty.yml b/alacritty.yml
index e68449cb..ac4477fc 100644
--- a/alacritty.yml
+++ b/alacritty.yml
@@ -1,5 +1,13 @@
# Configuration for Alacritty, the GPU enhanced terminal emulator.
+# Import additional configuration files
+#
+# These configuration files will be loaded in order, replacing values in files
+# loaded earlier with those loaded later in the chain. The file itself will
+# always be loaded last.
+#import:
+# - /path/to/alacritty.yml
+
# Any items in the `env` entry below will be added as
# environment variables. Some entries may override variables
# set by alacritty itself.
diff --git a/alacritty/src/config/mod.rs b/alacritty/src/config/mod.rs
index 226c6775..e5cc8583 100644
--- a/alacritty/src/config/mod.rs
+++ b/alacritty/src/config/mod.rs
@@ -1,26 +1,32 @@
-use std::env;
use std::fmt::{self, Display, Formatter};
-use std::fs;
-use std::io;
use std::path::PathBuf;
+use std::{env, fs, io};
use log::{error, warn};
+use serde::Deserialize;
+use serde_yaml::mapping::Mapping;
+use serde_yaml::Value;
use alacritty_terminal::config::{Config as TermConfig, LOG_TARGET_CONFIG};
-mod bindings;
pub mod debug;
pub mod font;
pub mod monitor;
-mod mouse;
pub mod ui_config;
pub mod window;
+mod bindings;
+mod mouse;
+mod serde_utils;
+
pub use crate::config::bindings::{Action, Binding, Key, ViAction};
#[cfg(test)]
pub use crate::config::mouse::{ClickHandler, Mouse};
use crate::config::ui_config::UIConfig;
+/// Maximum number of depth for the configuration file imports.
+const IMPORT_RECURSION_LIMIT: usize = 5;
+
pub type Config = TermConfig<UIConfig>;
/// Result from config loading.
@@ -128,13 +134,7 @@ pub fn installed_config() -> Option<PathBuf> {
dirs::config_dir().map(|path| path.join("alacritty\\alacritty.yml")).filter(|new| new.exists())
}
-pub fn load_from(path: PathBuf) -> Config {
- let mut config = reload_from(&path).unwrap_or_else(|_| Config::default());
- config.config_path = Some(path);
- config
-}
-
-pub fn reload_from(path: &PathBuf) -> Result<Config> {
+pub fn load_from(path: &PathBuf) -> Result<Config> {
match read_config(path) {
Ok(config) => Ok(config),
Err(err) => {
@@ -145,6 +145,25 @@ pub fn reload_from(path: &PathBuf) -> Result<Config> {
}
fn read_config(path: &PathBuf) -> Result<Config> {
+ let mut config_paths = Vec::new();
+ let config_value = parse_config(&path, &mut config_paths, IMPORT_RECURSION_LIMIT)?;
+
+ let mut config = Config::deserialize(config_value)?;
+ config.ui_config.config_paths = config_paths;
+
+ print_deprecation_warnings(&config);
+
+ Ok(config)
+}
+
+/// Deserialize all configuration files as generic Value.
+fn parse_config(
+ path: &PathBuf,
+ config_paths: &mut Vec<PathBuf>,
+ recursion_limit: usize,
+) -> Result<Value> {
+ config_paths.push(path.to_owned());
+
let mut contents = fs::read_to_string(path)?;
// Remove UTF-8 BOM.
@@ -152,24 +171,57 @@ fn read_config(path: &PathBuf) -> Result<Config> {
contents = contents.split_off(3);
}
- parse_config(&contents)
-}
-
-fn parse_config(contents: &str) -> Result<Config> {
- match serde_yaml::from_str(contents) {
+ // Load configuration file as Value.
+ let config: Value = match serde_yaml::from_str(&contents) {
+ Ok(config) => config,
Err(error) => {
// Prevent parsing error with an empty string and commented out file.
if error.to_string() == "EOF while parsing a value" {
- Ok(Config::default())
+ Value::Mapping(Mapping::new())
} else {
- Err(Error::Yaml(error))
+ return Err(Error::Yaml(error));
}
},
- Ok(config) => {
- print_deprecation_warnings(&config);
- Ok(config)
- },
+ };
+
+ // Merge config with imports.
+ let imports = load_imports(&config, config_paths, recursion_limit);
+ Ok(serde_utils::merge(imports, config))
+}
+
+/// Load all referenced configuration files.
+fn load_imports(config: &Value, config_paths: &mut Vec<PathBuf>, recursion_limit: usize) -> Value {
+ let mut merged = Value::Null;
+
+ let imports = match config.get("import") {
+ Some(Value::Sequence(imports)) => imports,
+ _ => return merged,
+ };
+
+ // Limit recursion to prevent infinite loops.
+ if !imports.is_empty() && recursion_limit == 0 {
+ error!(target: LOG_TARGET_CONFIG, "Exceeded maximum configuration import depth");
+ return merged;
}
+
+ for import in imports {
+ let path = match import {
+ Value::String(path) => PathBuf::from(path),
+ _ => {
+ error!(target: LOG_TARGET_CONFIG, "Encountered invalid configuration file import");
+ continue;
+ },
+ };
+
+ match parse_config(&path, config_paths, recursion_limit - 1) {
+ Ok(config) => merged = serde_utils::merge(merged, config),
+ Err(err) => {
+ error!(target: LOG_TARGET_CONFIG, "Unable to import config {:?}: {}", path, err)
+ },
+ }
+ }
+
+ merged
}
fn print_deprecation_warnings(config: &Config) {
@@ -215,13 +267,16 @@ fn print_deprecation_warnings(config: &Config) {
#[cfg(test)]
mod tests {
- static DEFAULT_ALACRITTY_CONFIG: &str =
- include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../alacritty.yml"));
+ use super::*;
- use super::Config;
+ static DEFAULT_ALACRITTY_CONFIG: &str =
+ concat!(env!("CARGO_MANIFEST_DIR"), "/../alacritty.yml");
#[test]
fn config_read_eof() {
- assert_eq!(super::parse_config(DEFAULT_ALACRITTY_CONFIG).unwrap(), Config::default());
+ let config_path: PathBuf = DEFAULT_ALACRITTY_CONFIG.into();
+ let mut config = read_config(&config_path).unwrap();
+ config.ui_config.config_paths = Vec::new();
+ assert_eq!(config, Config::default());
}
}
diff --git a/alacritty/src/config/monitor.rs b/alacritty/src/config/monitor.rs
index 2ed0c426..5d388182 100644
--- a/alacritty/src/config/monitor.rs
+++ b/alacritty/src/config/monitor.rs
@@ -1,57 +1,86 @@
+use std::fs;
use std::path::PathBuf;
use std::sync::mpsc;
use std::time::Duration;
+use log::{debug, error};
use notify::{watcher, DebouncedEvent, RecursiveMode, Watcher};
use alacritty_terminal::thread;
use crate::event::{Event, EventProxy};
-pub struct Monitor {
- _thread: ::std::thread::JoinHandle<()>,
-}
+pub fn watch(mut paths: Vec<PathBuf>, event_proxy: EventProxy) {
+ // Canonicalize all paths, filtering out the ones that do not exist.
+ paths = paths
+ .drain(..)
+ .filter_map(|path| match fs::canonicalize(&path) {
+ Ok(path) => Some(path),
+ Err(err) => {
+ error!("Unable to canonicalize config path {:?}: {}", path, err);
+ None
+ },
+ })
+ .collect();
+
+ // Don't monitor config if there is no path to watch.
+ if paths.is_empty() {
+ return;
+ }
+
+ // The Duration argument is a debouncing period.
+ let (tx, rx) = mpsc::channel();
+ let mut watcher = match watcher(tx, Duration::from_millis(10)) {
+ Ok(watcher) => watcher,
+ Err(err) => {
+ error!("Unable to watch config file: {}", err);
+ return;
+ },
+ };
+
+ thread::spawn_named("config watcher", move || {
+ // Get all unique parent directories.
+ let mut parents = paths
+ .iter()
+ .map(|path| {
+ let mut path = path.clone();
+ path.pop();
+ path
+ })
+ .collect::<Vec<PathBuf>>();
+ parents.sort_unstable();
+ parents.dedup();
-impl Monitor {
- pub fn new<P>(path: P, event_proxy: EventProxy) -> Monitor
- where
- P: Into<PathBuf>,
- {
- let path = path.into();
-
- Monitor {
- _thread: thread::spawn_named("config watcher", move || {
- let (tx, rx) = mpsc::channel();
- // The Duration argument is a debouncing period.
- let mut watcher =
- watcher(tx, Duration::from_millis(10)).expect("Unable to spawn file watcher");
- let config_path = ::std::fs::canonicalize(path).expect("canonicalize config path");
-
- // Get directory of config.
- let mut parent = config_path.clone();
- parent.pop();
-
- // Watch directory.
- watcher
- .watch(&parent, RecursiveMode::NonRecursive)
- .expect("watch alacritty.yml dir");
-
- loop {
- match rx.recv().expect("watcher event") {
- DebouncedEvent::Rename(..) => continue,
- DebouncedEvent::Write(path)
- | DebouncedEvent::Create(path)
- | DebouncedEvent::Chmod(path) => {
- if path != config_path {
- continue;
- }
-
- event_proxy.send_event(Event::ConfigReload(path));
- },
- _ => {},
+ // Watch all configuration file directories.
+ for parent in &parents {
+ if let Err(err) = watcher.watch(&parent, RecursiveMode::NonRecursive) {
+ debug!("Unable to watch config directory {:?}: {}", parent, err);
+ }
+ }
+
+ loop {
+ let event = match rx.recv() {
+ Ok(event) => event,
+ Err(err) => {
+ debug!("Config watcher channel dropped unexpectedly: {}", err);
+ break;
+ },
+ };
+
+ match event {
+ DebouncedEvent::Rename(..) => continue,
+ DebouncedEvent::Write(path)
+ | DebouncedEvent::Create(path)
+ | DebouncedEvent::Chmod(path) => {
+ if !paths.contains(&path) {
+ continue;
}
- }
- }),
+
+ // Always reload the primary configuration file.
+ event_proxy.send_event(Event::ConfigReload(paths[0].clone()));
+ },
+ _ => {},
+ }
}
- }
+ });
}
diff --git a/alacritty/src/config/serde_utils.rs b/alacritty/src/config/serde_utils.rs
new file mode 100644
index 00000000..ecf1c858
--- /dev/null
+++ b/alacritty/src/config/serde_utils.rs
@@ -0,0 +1,89 @@
+//! Serde helpers.
+
+use serde_yaml::mapping::Mapping;
+use serde_yaml::Value;
+
+/// Merge two serde structures.
+///
+/// This will take all values from `replacement` and use `base` whenever a value isn't present in
+/// `replacement`.
+pub fn merge(base: Value, replacement: Value) -> Value {
+ match (base, replacement) {
+ (Value::Sequence(mut base), Value::Sequence(mut replacement)) => {
+ base.append(&mut replacement);
+ Value::Sequence(base)
+ },
+ (Value::Mapping(base), Value::Mapping(replacement)) => {
+ Value::Mapping(merge_mapping(base, replacement))
+ },
+ (_, value) => value,
+ }
+}
+
+/// Merge two key/value mappings.
+fn merge_mapping(mut base: Mapping, replacement: Mapping) -> Mapping {
+ for (key, value) in replacement {
+ let value = match base.remove(&key) {
+ Some(base_value) => merge(base_value, value),
+ None => value,
+ };
+ base.insert(key, value);
+ }
+
+ base
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn merge_primitive() {
+ let base = Value::Null;
+ let replacement = Value::Bool(true);
+ assert_eq!(merge(base, replacement.clone()), replacement);
+
+ let base = Value::Bool(false);
+ let replacement = Value::Bool(true);
+ assert_eq!(merge(base, replacement.clone()), replacement);
+
+ let base = Value::Number(0.into());
+ let replacement = Value::Number(1.into());
+ assert_eq!(merge(base, replacement.clone()), replacement);
+
+ let base = Value::String(String::new());
+ let replacement = Value::String(String::from("test"));
+ assert_eq!(merge(base, replacement.clone()), replacement);
+ }
+
+ #[test]
+ fn merge_sequence() {
+ let base = Value::Sequence(vec![Value::Null]);
+ let replacement = Value::Sequence(vec![Value::Bool(true)]);
+ let expected = Value::Sequence(vec![Value::Null, Value::Bool(true)]);
+ assert_eq!(merge(base, replacement), expected);
+ }
+
+ #[test]
+ fn merge_mapping() {
+ let mut base_mapping = Mapping::new();
+ base_mapping.insert(Value::String(String::from("a")), Value::Bool(true));
+ base_mapping.insert(Value::String(String::from("b")), Value::Bool(false));
+ let base = Value::Mapping(base_mapping);
+
+ let mut replacement_mapping = Mapping::new();
+ replacement_mapping.insert(Value::String(String::from("a")), Value::Bool(true));
+ replacement_mapping.insert(Value::String(String::from("c")), Value::Bool(false));
+ let replacement = Value::Mapping(replacement_mapping);
+
+ let merged = merge(base, replacement);
+
+ let mut expected_mapping = Mapping::new();
+ expected_mapping.insert(Value::String(String::from("b")), Value::Bool(false));
+ expected_mapping.insert(Value::String(String::from("a")), Value::Bool(true));
+ expected_mapping.insert(Value::String(String::from("c")), Value::Bool(false));
+ let expected = Value::Mapping(expected_mapping);
+
+ assert_eq!(merged, expected);
+ }
+}
diff --git a/alacritty/src/config/ui_config.rs b/alacritty/src/config/ui_config.rs
index a8b1749f..31654c6c 100644
--- a/alacritty/src/config/ui_config.rs
+++ b/alacritty/src/config/ui_config.rs
@@ -1,3 +1,5 @@
+use std::path::PathBuf;
+
use log::error;
use serde::{Deserialize, Deserializer};
@@ -46,6 +48,10 @@ pub struct UIConfig {
#[serde(default, deserialize_with = "failure_default")]
background_opacity: Percentage,
+ /// Path where config was loaded from.
+ #[serde(skip)]
+ pub config_paths: Vec<PathBuf>,
+
// TODO: DEPRECATED
#[serde(default, deserialize_with = "failure_default")]
pub dynamic_title: Option<bool>,
@@ -64,6 +70,7 @@ impl Default for UIConfig {
background_opacity: Default::default(),
live_config_reload: Default::default(),
dynamic_title: Default::default(),
+ config_paths: Default::default(),
}
}
}
diff --git a/alacritty/src/event.rs b/alacritty/src/event.rs
index 9033049f..d0ce3601 100644
--- a/alacritty/src/event.rs
+++ b/alacritty/src/event.rs
@@ -1051,7 +1051,7 @@ impl<N: Notify + OnResize> Processor<N> {
processor.ctx.display_update_pending.dirty = true;
}
- let config = match config::reload_from(&path) {
+ let config = match config::load_from(&path) {
Ok(config) => config,
Err(_) => return,
};
diff --git a/alacritty/src/main.rs b/alacritty/src/main.rs
index e6884204..21c4804c 100644
--- a/alacritty/src/main.rs
+++ b/alacritty/src/main.rs
@@ -54,7 +54,7 @@ mod gl {
}
use crate::cli::Options;
-use crate::config::monitor::Monitor;
+use crate::config::monitor;
use crate::config::Config;
use crate::display::Display;
use crate::event::{Event, EventProxy, Processor};
@@ -84,7 +84,10 @@ fn main() {
// Load configuration file.
let config_path = options.config_path().or_else(config::installed_config);
- let config = config_path.map(config::load_from).unwrap_or_else(Config::default);
+ let config = config_path
+ .as_ref()
+ .and_then(|path| config::load_from(path).ok())
+ .unwrap_or_else(Config::default);
let config = options.into_config(config);
// Update the log level from config.
@@ -121,9 +124,9 @@ fn main() {
fn run(window_event_loop: GlutinEventLoop<Event>, config: Config) -> Result<(), Box<dyn Error>> {
info!("Welcome to Alacritty");
- match &config.config_path {
- Some(config_path) => info!("Configuration loaded from \"{}\"", config_path.display()),
- None => info!("No configuration file found"),
+ info!("Configuration files loaded from:");
+ for path in &config.ui_config.config_paths {
+ info!(" \"{}\"", path.display());
}
// Set environment variables.
@@ -179,7 +182,7 @@ fn run(window_event_loop: GlutinEventLoop<Event>, config: Config) -> Result<(),
// The monitor watches the config file for changes and reloads it. Pending
// config changes are processed in the main loop.
if config.ui_config.live_config_reload() {
- config.config_path.as_ref().map(|path| Monitor::new(path, event_proxy.clone()));
+ monitor::watch(config.ui_config.config_paths.clone(), event_proxy);
}
// Setup storage for message UI.
diff --git a/alacritty_terminal/src/config/mod.rs b/alacritty_terminal/src/config/mod.rs
index 98579a69..add38c59 100644
--- a/alacritty_terminal/src/config/mod.rs
+++ b/alacritty_terminal/src/config/mod.rs
@@ -43,10 +43,6 @@ pub struct Config<T> {
#[serde(default, deserialize_with = "failure_default")]
pub shell: Option<Program>,
- /// Path where config was loaded from.
- #[serde(default, deserialize_with = "failure_default")]
- pub config_path: Option<PathBuf>,
-
/// Bell configuration.
#[serde(default, deserialize_with = "failure_default")]
bell: BellConfig,