Add CLI parameter to override config options

This uses the facilities added in
3c3e6870de to allow overriding individual
configuration file options dynamically from the CLI using the
--options/-o parameter.

Fixes #1258.
This commit is contained in:
Christian Duerr 2020-08-22 20:55:27 +00:00 committed by GitHub
parent 92a3d482d0
commit 6cfcd7c259
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 239 additions and 88 deletions

View file

@ -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

View file

@ -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));
}
}

View file

@ -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());
}

View file

@ -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]

View file

@ -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.

View file

@ -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();

View file

@ -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"