diff options
Diffstat (limited to 'alacritty/src')
-rw-r--r-- | alacritty/src/config/mod.rs | 109 | ||||
-rw-r--r-- | alacritty/src/config/monitor.rs | 115 | ||||
-rw-r--r-- | alacritty/src/config/serde_utils.rs | 89 | ||||
-rw-r--r-- | alacritty/src/config/ui_config.rs | 7 | ||||
-rw-r--r-- | alacritty/src/event.rs | 2 | ||||
-rw-r--r-- | alacritty/src/main.rs | 15 |
6 files changed, 260 insertions, 77 deletions
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. |