diff options
-rw-r--r-- | CHANGELOG.md | 1 | ||||
-rw-r--r-- | alacritty/src/cli.rs | 150 | ||||
-rw-r--r-- | alacritty/src/config/mod.rs | 126 | ||||
-rw-r--r-- | alacritty/src/config/serde_utils.rs | 5 | ||||
-rw-r--r-- | alacritty/src/event.rs | 12 | ||||
-rw-r--r-- | alacritty/src/main.rs | 24 | ||||
-rw-r--r-- | extra/alacritty.man | 9 |
7 files changed, 239 insertions, 88 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index b32a0905..f1cac476 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - 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`) +- CLI parameter `--option`/`-o` to override any configuration field ### Changed diff --git a/alacritty/src/cli.rs b/alacritty/src/cli.rs index 5da6332b..418e3791 100644 --- a/alacritty/src/cli.rs +++ b/alacritty/src/cli.rs @@ -3,10 +3,12 @@ use std::path::PathBuf; use clap::{crate_authors, crate_description, crate_name, crate_version, App, Arg}; use log::{self, error, LevelFilter}; +use serde_yaml::Value; use alacritty_terminal::config::Program; use alacritty_terminal::index::{Column, Line}; +use crate::config::serde_utils; use crate::config::ui_config::Delta; use crate::config::window::{Dimensions, DEFAULT_NAME}; use crate::config::Config; @@ -32,9 +34,10 @@ pub struct Options { pub log_level: LevelFilter, pub command: Option<Program>, pub hold: bool, - pub working_dir: Option<PathBuf>, - pub config: Option<PathBuf>, + pub working_directory: Option<PathBuf>, + pub config_path: Option<PathBuf>, pub persistent_logging: bool, + pub config_options: Value, } impl Default for Options { @@ -52,9 +55,10 @@ impl Default for Options { log_level: LevelFilter::Warn, command: None, hold: false, - working_dir: None, - config: None, + working_directory: None, + config_path: None, persistent_logging: false, + config_options: Value::Null, } } } @@ -168,11 +172,18 @@ impl Options { .short("e") .multiple(true) .takes_value(true) - .min_values(1) .allow_hyphen_values(true) .help("Command and args to execute (must be last argument)"), ) .arg(Arg::with_name("hold").long("hold").help("Remain open after child process exits")) + .arg( + Arg::with_name("option") + .long("option") + .short("o") + .multiple(true) + .takes_value(true) + .help("Override configuration file options [example: window.title=Alacritty]"), + ) .get_matches(); if matches.is_present("ref-test") { @@ -231,11 +242,11 @@ impl Options { } if let Some(dir) = matches.value_of("working-directory") { - options.working_dir = Some(PathBuf::from(dir.to_string())); + options.working_directory = Some(PathBuf::from(dir.to_string())); } if let Some(path) = matches.value_of("config-file") { - options.config = Some(PathBuf::from(path.to_string())); + options.config_path = Some(PathBuf::from(path.to_string())); } if let Some(mut args) = matches.values_of("command") { @@ -251,23 +262,47 @@ impl Options { options.hold = true; } + if let Some(config_options) = matches.values_of("option") { + for option in config_options { + match option_as_value(option) { + Ok(value) => { + options.config_options = serde_utils::merge(options.config_options, value); + }, + Err(_) => eprintln!("Invalid CLI config option: {:?}", option), + } + } + } + options } + /// Configuration file path. pub fn config_path(&self) -> Option<PathBuf> { - self.config.clone() + self.config_path.clone() } - pub fn into_config(self, mut config: Config) -> Config { - match self.working_dir.or_else(|| config.working_directory.take()) { - Some(ref wd) if !wd.is_dir() => error!("Unable to set working directory to {:?}", wd), - wd => config.working_directory = wd, + /// CLI config options as deserializable serde value. + pub fn config_options(&self) -> &Value { + &self.config_options + } + + /// Override configuration file with options from the CLI. + pub fn override_config(&self, config: &mut Config) { + if let Some(working_directory) = &self.working_directory { + if working_directory.is_dir() { + config.working_directory = Some(working_directory.to_owned()); + } else { + error!("Invalid working directory: {:?}", working_directory); + } } if let Some(lcr) = self.live_config_reload { config.ui_config.set_live_config_reload(lcr); } - config.shell = self.command.or(config.shell); + + if let Some(command) = &self.command { + config.shell = Some(command.clone()); + } config.hold = self.hold; @@ -275,12 +310,12 @@ impl Options { config.ui_config.set_dynamic_title(dynamic_title); replace_if_some(&mut config.ui_config.window.dimensions, self.dimensions); - replace_if_some(&mut config.ui_config.window.title, self.title); - config.ui_config.window.position = self.position.or(config.ui_config.window.position); - config.ui_config.window.embed = self.embed.and_then(|embed| embed.parse().ok()); - replace_if_some(&mut config.ui_config.window.class.instance, self.class_instance); - replace_if_some(&mut config.ui_config.window.class.general, self.class_general); + replace_if_some(&mut config.ui_config.window.title, self.title.clone()); + replace_if_some(&mut config.ui_config.window.class.instance, self.class_instance.clone()); + replace_if_some(&mut config.ui_config.window.class.general, self.class_general.clone()); + config.ui_config.window.position = self.position.or(config.ui_config.window.position); + config.ui_config.window.embed = self.embed.as_ref().and_then(|embed| embed.parse().ok()); config.ui_config.debug.print_events |= self.print_events; config.ui_config.debug.log_level = max(config.ui_config.debug.log_level, self.log_level); config.ui_config.debug.ref_test |= self.ref_test; @@ -290,8 +325,6 @@ impl Options { config.ui_config.debug.log_level = max(config.ui_config.debug.log_level, LevelFilter::Info); } - - config } } @@ -301,28 +334,54 @@ fn replace_if_some<T>(option: &mut T, value: Option<T>) { } } +/// Format an option in the format of `parent.field=value` to a serde Value. +fn option_as_value(option: &str) -> Result<Value, serde_yaml::Error> { + let mut yaml_text = String::with_capacity(option.len()); + let mut closing_brackets = String::new(); + + for (i, c) in option.chars().enumerate() { + match c { + '=' => { + yaml_text.push_str(": "); + yaml_text.push_str(&option[i + 1..]); + break; + }, + '.' => { + yaml_text.push_str(": {"); + closing_brackets.push('}'); + }, + _ => yaml_text.push(c), + } + } + + yaml_text += &closing_brackets; + + serde_yaml::from_str(&yaml_text) +} + #[cfg(test)] mod tests { - use crate::cli::Options; - use crate::config::Config; + use super::*; + + use serde_yaml::mapping::Mapping; #[test] fn dynamic_title_ignoring_options_by_default() { - let config = Config::default(); + let mut config = Config::default(); let old_dynamic_title = config.ui_config.dynamic_title(); - let config = Options::default().into_config(config); + Options::default().override_config(&mut config); assert_eq!(old_dynamic_title, config.ui_config.dynamic_title()); } #[test] fn dynamic_title_overridden_by_options() { - let config = Config::default(); + let mut config = Config::default(); let mut options = Options::default(); options.title = Some("foo".to_owned()); - let config = options.into_config(config); + options.override_config(&mut config); assert!(!config.ui_config.dynamic_title()); } @@ -332,8 +391,45 @@ mod tests { let mut config = Config::default(); config.ui_config.window.title = "foo".to_owned(); - let config = Options::default().into_config(config); + Options::default().override_config(&mut config); assert!(config.ui_config.dynamic_title()); } + + #[test] + fn valid_option_as_value() { + // Test with a single field. + let value = option_as_value("field=true").unwrap(); + + let mut mapping = Mapping::new(); + mapping.insert(Value::String(String::from("field")), Value::Bool(true)); + + assert_eq!(value, Value::Mapping(mapping)); + + // Test with nested fields + let value = option_as_value("parent.field=true").unwrap(); + + let mut parent_mapping = Mapping::new(); + parent_mapping.insert(Value::String(String::from("field")), Value::Bool(true)); + let mut mapping = Mapping::new(); + mapping.insert(Value::String(String::from("parent")), Value::Mapping(parent_mapping)); + + assert_eq!(value, Value::Mapping(mapping)); + } + + #[test] + fn invalid_option_as_value() { + let value = option_as_value("}"); + assert!(value.is_err()); + } + + #[test] + fn float_option_as_value() { + let value = option_as_value("float=3.4").unwrap(); + + let mut expected = Mapping::new(); + expected.insert(Value::String(String::from("float")), Value::Number(3.4.into())); + + assert_eq!(value, Value::Mapping(expected)); + } } diff --git a/alacritty/src/config/mod.rs b/alacritty/src/config/mod.rs index 243f1bae..56a12c7c 100644 --- a/alacritty/src/config/mod.rs +++ b/alacritty/src/config/mod.rs @@ -2,7 +2,7 @@ use std::fmt::{self, Display, Formatter}; use std::path::PathBuf; use std::{env, fs, io}; -use log::{error, warn}; +use log::{error, info, warn}; use serde::Deserialize; use serde_yaml::mapping::Mapping; use serde_yaml::Value; @@ -12,13 +12,14 @@ use alacritty_terminal::config::{Config as TermConfig, LOG_TARGET_CONFIG}; pub mod debug; pub mod font; pub mod monitor; +pub mod serde_utils; pub mod ui_config; pub mod window; mod bindings; mod mouse; -mod serde_utils; +use crate::cli::Options; pub use crate::config::bindings::{Action, Binding, Key, ViAction}; #[cfg(test)] pub use crate::config::mouse::{ClickHandler, Mouse}; @@ -94,48 +95,42 @@ impl From<serde_yaml::Error> for Error { } } -/// Get the location of the first found default config file paths -/// according to the following order: -/// -/// 1. $XDG_CONFIG_HOME/alacritty/alacritty.yml -/// 2. $XDG_CONFIG_HOME/alacritty.yml -/// 3. $HOME/.config/alacritty/alacritty.yml -/// 4. $HOME/.alacritty.yml -#[cfg(not(windows))] -pub fn installed_config() -> Option<PathBuf> { - // Try using XDG location by default. - xdg::BaseDirectories::with_prefix("alacritty") - .ok() - .and_then(|xdg| xdg.find_config_file("alacritty.yml")) - .or_else(|| { - xdg::BaseDirectories::new() - .ok() - .and_then(|fallback| fallback.find_config_file("alacritty.yml")) - }) - .or_else(|| { - if let Ok(home) = env::var("HOME") { - // Fallback path: $HOME/.config/alacritty/alacritty.yml. - let fallback = PathBuf::from(&home).join(".config/alacritty/alacritty.yml"); - if fallback.exists() { - return Some(fallback); - } - // Fallback path: $HOME/.alacritty.yml. - let fallback = PathBuf::from(&home).join(".alacritty.yml"); - if fallback.exists() { - return Some(fallback); - } - } - None - }) +/// Load the configuration file. +pub fn load(options: &Options) -> Config { + // Get config path. + let config_path = match options.config_path().or_else(installed_config) { + Some(path) => path, + None => { + info!(target: LOG_TARGET_CONFIG, "No config file found; using default"); + return Config::default(); + }, + }; + + // Load config, falling back to the default on error. + let config_options = options.config_options().clone(); + let mut config = load_from(&config_path, config_options).unwrap_or_default(); + + // Override config with CLI options. + options.override_config(&mut config); + + config } -#[cfg(windows)] -pub fn installed_config() -> Option<PathBuf> { - dirs::config_dir().map(|path| path.join("alacritty\\alacritty.yml")).filter(|new| new.exists()) +/// Attempt to reload the configuration file. +pub fn reload(config_path: &PathBuf, options: &Options) -> Result<Config> { + // Load config, propagating errors. + let config_options = options.config_options().clone(); + let mut config = load_from(&config_path, config_options)?; + + // Override config with CLI options. + options.override_config(&mut config); + + Ok(config) } -pub fn load_from(path: &PathBuf) -> Result<Config> { - match read_config(path) { +/// Load configuration file and log errors. +fn load_from(path: &PathBuf, cli_config: Value) -> Result<Config> { + match read_config(path, cli_config) { Ok(config) => Ok(config), Err(err) => { error!(target: LOG_TARGET_CONFIG, "Unable to load config {:?}: {}", path, err); @@ -144,10 +139,15 @@ pub fn load_from(path: &PathBuf) -> Result<Config> { } } -fn read_config(path: &PathBuf) -> Result<Config> { +/// Deserialize configuration file from path. +fn read_config(path: &PathBuf, cli_config: Value) -> Result<Config> { let mut config_paths = Vec::new(); - let config_value = parse_config(&path, &mut config_paths, IMPORT_RECURSION_LIMIT)?; + let mut config_value = parse_config(&path, &mut config_paths, IMPORT_RECURSION_LIMIT)?; + + // Override config with CLI options. + config_value = serde_utils::merge(config_value, cli_config); + // Deserialize to concrete type. let mut config = Config::deserialize(config_value)?; config.ui_config.config_paths = config_paths; @@ -231,6 +231,46 @@ fn load_imports(config: &Value, config_paths: &mut Vec<PathBuf>, recursion_limit merged } +/// Get the location of the first found default config file paths +/// according to the following order: +/// +/// 1. $XDG_CONFIG_HOME/alacritty/alacritty.yml +/// 2. $XDG_CONFIG_HOME/alacritty.yml +/// 3. $HOME/.config/alacritty/alacritty.yml +/// 4. $HOME/.alacritty.yml +#[cfg(not(windows))] +fn installed_config() -> Option<PathBuf> { + // Try using XDG location by default. + xdg::BaseDirectories::with_prefix("alacritty") + .ok() + .and_then(|xdg| xdg.find_config_file("alacritty.yml")) + .or_else(|| { + xdg::BaseDirectories::new() + .ok() + .and_then(|fallback| fallback.find_config_file("alacritty.yml")) + }) + .or_else(|| { + if let Ok(home) = env::var("HOME") { + // Fallback path: $HOME/.config/alacritty/alacritty.yml. + let fallback = PathBuf::from(&home).join(".config/alacritty/alacritty.yml"); + if fallback.exists() { + return Some(fallback); + } + // Fallback path: $HOME/.alacritty.yml. + let fallback = PathBuf::from(&home).join(".alacritty.yml"); + if fallback.exists() { + return Some(fallback); + } + } + None + }) +} + +#[cfg(windows)] +fn installed_config() -> Option<PathBuf> { + dirs::config_dir().map(|path| path.join("alacritty\\alacritty.yml")).filter(|new| new.exists()) +} + fn print_deprecation_warnings(config: &Config) { if config.scrolling.faux_multiplier().is_some() { warn!( @@ -282,7 +322,7 @@ mod tests { #[test] fn config_read_eof() { let config_path: PathBuf = DEFAULT_ALACRITTY_CONFIG.into(); - let mut config = read_config(&config_path).unwrap(); + let mut config = read_config(&config_path, Value::Null).unwrap(); config.ui_config.config_paths = Vec::new(); assert_eq!(config, Config::default()); } diff --git a/alacritty/src/config/serde_utils.rs b/alacritty/src/config/serde_utils.rs index ecf1c858..beb9c36b 100644 --- a/alacritty/src/config/serde_utils.rs +++ b/alacritty/src/config/serde_utils.rs @@ -16,6 +16,7 @@ pub fn merge(base: Value, replacement: Value) -> Value { (Value::Mapping(base), Value::Mapping(replacement)) => { Value::Mapping(merge_mapping(base, replacement)) }, + (value, Value::Null) => value, (_, value) => value, } } @@ -54,6 +55,10 @@ mod tests { let base = Value::String(String::new()); let replacement = Value::String(String::from("test")); assert_eq!(merge(base, replacement.clone()), replacement); + + let base = Value::Mapping(Mapping::new()); + let replacement = Value::Null; + assert_eq!(merge(base.clone(), replacement), base); } #[test] diff --git a/alacritty/src/event.rs b/alacritty/src/event.rs index d0ce3601..c3e1b6c8 100644 --- a/alacritty/src/event.rs +++ b/alacritty/src/event.rs @@ -39,7 +39,7 @@ use alacritty_terminal::term::{ClipboardType, SizeInfo, Term, TermMode}; #[cfg(not(windows))] use alacritty_terminal::tty; -use crate::cli::Options; +use crate::cli::Options as CLIOptions; use crate::clipboard::Clipboard; use crate::config; use crate::config::Config; @@ -139,6 +139,7 @@ pub struct ActionContext<'a, N, T> { pub urls: &'a Urls, pub scheduler: &'a mut Scheduler, pub search_state: &'a mut SearchState, + cli_options: &'a CLIOptions, font_size: &'a mut Size, } @@ -693,6 +694,7 @@ pub struct Processor<N> { font_size: Size, event_queue: Vec<GlutinEvent<'static, Event>>, search_state: SearchState, + cli_options: CLIOptions, } impl<N: Notify + OnResize> Processor<N> { @@ -704,6 +706,7 @@ impl<N: Notify + OnResize> Processor<N> { message_buffer: MessageBuffer, config: Config, display: Display, + cli_options: CLIOptions, ) -> Processor<N> { #[cfg(not(any(target_os = "macos", windows)))] let clipboard = Clipboard::new(display.window.wayland_display()); @@ -723,6 +726,7 @@ impl<N: Notify + OnResize> Processor<N> { event_queue: Vec::new(), clipboard, search_state: SearchState::new(), + cli_options, } } @@ -826,6 +830,7 @@ impl<N: Notify + OnResize> Processor<N> { urls: &self.display.urls, scheduler: &mut scheduler, search_state: &mut self.search_state, + cli_options: &self.cli_options, event_loop, }; let mut processor = input::Processor::new(context, &self.display.highlighted_url); @@ -1051,14 +1056,11 @@ impl<N: Notify + OnResize> Processor<N> { processor.ctx.display_update_pending.dirty = true; } - let config = match config::load_from(&path) { + let config = match config::reload(&path, &processor.ctx.cli_options) { Ok(config) => config, Err(_) => return, }; - let options = Options::new(); - let config = options.into_config(config); - processor.ctx.terminal.update_config(&config); // Reload cursor if we've changed its thickness. diff --git a/alacritty/src/main.rs b/alacritty/src/main.rs index 21c4804c..838ce4d2 100644 --- a/alacritty/src/main.rs +++ b/alacritty/src/main.rs @@ -83,12 +83,7 @@ fn main() { .expect("Unable to initialize logger"); // Load configuration file. - let config_path = options.config_path().or_else(config::installed_config); - 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); + let config = config::load(&options); // Update the log level from config. log::set_max_level(config.ui_config.debug.log_level); @@ -104,7 +99,7 @@ fn main() { let persistent_logging = config.ui_config.debug.persistent_logging; // Run Alacritty. - if let Err(err) = run(window_event_loop, config) { + if let Err(err) = run(window_event_loop, config, options) { error!("Alacritty encountered an unrecoverable error:\n\n\t{}\n", err); std::process::exit(1); } @@ -121,7 +116,11 @@ fn main() { /// /// Creates a window, the terminal state, PTY, I/O event loop, input processor, /// config change monitor, and runs the main display loop. -fn run(window_event_loop: GlutinEventLoop<Event>, config: Config) -> Result<(), Box<dyn Error>> { +fn run( + window_event_loop: GlutinEventLoop<Event>, + config: Config, + options: Options, +) -> Result<(), Box<dyn Error>> { info!("Welcome to Alacritty"); info!("Configuration files loaded from:"); @@ -189,8 +188,13 @@ fn run(window_event_loop: GlutinEventLoop<Event>, config: Config) -> Result<(), let message_buffer = MessageBuffer::new(); // Event processor. - let mut processor = - Processor::new(event_loop::Notifier(loop_tx.clone()), message_buffer, config, display); + let mut processor = Processor::new( + event_loop::Notifier(loop_tx.clone()), + message_buffer, + config, + display, + options, + ); // Kick off the I/O thread. let io_thread = event_loop.spawn(); diff --git a/extra/alacritty.man b/extra/alacritty.man index 392f8c3b..8d89d2c5 100644 --- a/extra/alacritty.man +++ b/extra/alacritty.man @@ -64,15 +64,18 @@ On Windows, the configuration file is located at %APPDATA%\\alacritty\\alacritty \fB\-d\fR, \fB\-\-dimensions\fR <columns> <lines> Defines the window dimensions. Falls back to size specified by window manager if set to 0x0 [default: 0x0] .TP +\fB\-\-embed\fR <parent> +Defines the X11 window ID (as a decimal integer) to embed Alacritty within +.TP +\fB\-o\fR, \fB\-\-option\fR <option>... +Override configuration file options [example: window.title=Alacritty] +.TP \fB\-\-position\fR <x-pos> <y-pos> Defines the window position. Falls back to position specified by window manager if unset [default: unset] .TP \fB\-t\fR, \fB\-\-title\fR <title> Defines the window title [default: Alacritty] .TP -\fB\-\-embed\fR <parent> -Defines the X11 window ID (as a decimal integer) to embed Alacritty within -.TP \fB\-\-working\-directory\fR <working\-directory> Start the shell in the specified working directory .SH "SEE ALSO" |