summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChristian Duerr <contact@christianduerr.com>2020-08-22 20:55:27 +0000
committerGitHub <noreply@github.com>2020-08-22 20:55:27 +0000
commit6cfcd7c25930d0478bc35731543e37a2e0c8f13f (patch)
treec4ded7fbb33774d915d6a25048d8a523cfd57c34
parent92a3d482d076eebbd12681444ae8f4dfdf08da69 (diff)
Add CLI parameter to override config options
This uses the facilities added in 3c3e6870dedad56b270f5b65ea57d5a6e46b1de6 to allow overriding individual configuration file options dynamically from the CLI using the --options/-o parameter. Fixes #1258.
-rw-r--r--CHANGELOG.md1
-rw-r--r--alacritty/src/cli.rs150
-rw-r--r--alacritty/src/config/mod.rs126
-rw-r--r--alacritty/src/config/serde_utils.rs5
-rw-r--r--alacritty/src/event.rs12
-rw-r--r--alacritty/src/main.rs24
-rw-r--r--extra/alacritty.man9
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"