354 lines
12 KiB
Rust
354 lines
12 KiB
Rust
use std::cmp::max;
|
|
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 crate::config::serde_utils;
|
|
use crate::config::window::DEFAULT_NAME;
|
|
use crate::config::Config;
|
|
|
|
#[cfg(not(any(target_os = "macos", windows)))]
|
|
const CONFIG_PATH: &str = "$XDG_CONFIG_HOME/alacritty/alacritty.yml";
|
|
#[cfg(windows)]
|
|
const CONFIG_PATH: &str = "%APPDATA%\\alacritty\\alacritty.yml";
|
|
#[cfg(target_os = "macos")]
|
|
const CONFIG_PATH: &str = "$HOME/.config/alacritty/alacritty.yml";
|
|
|
|
/// Options specified on the command line.
|
|
pub struct Options {
|
|
pub print_events: bool,
|
|
pub ref_test: bool,
|
|
pub title: Option<String>,
|
|
pub class_instance: Option<String>,
|
|
pub class_general: Option<String>,
|
|
pub embed: Option<String>,
|
|
pub log_level: LevelFilter,
|
|
pub command: Option<Program>,
|
|
pub hold: bool,
|
|
pub working_directory: Option<PathBuf>,
|
|
pub config_path: Option<PathBuf>,
|
|
pub config_options: Value,
|
|
}
|
|
|
|
impl Default for Options {
|
|
fn default() -> Options {
|
|
Options {
|
|
print_events: false,
|
|
ref_test: false,
|
|
title: None,
|
|
class_instance: None,
|
|
class_general: None,
|
|
embed: None,
|
|
log_level: LevelFilter::Warn,
|
|
command: None,
|
|
hold: false,
|
|
working_directory: None,
|
|
config_path: None,
|
|
config_options: Value::Null,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Options {
|
|
/// Build `Options` from command line arguments.
|
|
pub fn new() -> Self {
|
|
let mut version = crate_version!().to_owned();
|
|
let commit_hash = env!("GIT_HASH");
|
|
if !commit_hash.is_empty() {
|
|
version = format!("{} ({})", version, commit_hash);
|
|
}
|
|
|
|
let mut options = Options::default();
|
|
|
|
let matches = App::new(crate_name!())
|
|
.version(version.as_str())
|
|
.author(crate_authors!("\n"))
|
|
.about(crate_description!())
|
|
.arg(Arg::with_name("ref-test").long("ref-test").help("Generates ref test"))
|
|
.arg(
|
|
Arg::with_name("print-events")
|
|
.long("print-events")
|
|
.help("Print all events to stdout"),
|
|
)
|
|
.arg(
|
|
Arg::with_name("title")
|
|
.long("title")
|
|
.short("t")
|
|
.takes_value(true)
|
|
.help(&format!("Defines the window title [default: {}]", DEFAULT_NAME)),
|
|
)
|
|
.arg(
|
|
Arg::with_name("class")
|
|
.long("class")
|
|
.value_name("instance> | <instance>,<general")
|
|
.takes_value(true)
|
|
.use_delimiter(true)
|
|
.help(&format!(
|
|
"Defines window class/app_id on X11/Wayland [default: {}]",
|
|
DEFAULT_NAME
|
|
)),
|
|
)
|
|
.arg(
|
|
Arg::with_name("embed").long("embed").takes_value(true).help(
|
|
"Defines the X11 window ID (as a decimal integer) to embed Alacritty within",
|
|
),
|
|
)
|
|
.arg(
|
|
Arg::with_name("q")
|
|
.short("q")
|
|
.multiple(true)
|
|
.conflicts_with("v")
|
|
.help("Reduces the level of verbosity (the min level is -qq)"),
|
|
)
|
|
.arg(
|
|
Arg::with_name("v")
|
|
.short("v")
|
|
.multiple(true)
|
|
.conflicts_with("q")
|
|
.help("Increases the level of verbosity (the max level is -vvv)"),
|
|
)
|
|
.arg(
|
|
Arg::with_name("working-directory")
|
|
.long("working-directory")
|
|
.takes_value(true)
|
|
.help("Start the shell in the specified working directory"),
|
|
)
|
|
.arg(Arg::with_name("config-file").long("config-file").takes_value(true).help(
|
|
&format!("Specify alternative configuration file [default: {}]", CONFIG_PATH),
|
|
))
|
|
.arg(
|
|
Arg::with_name("command")
|
|
.long("command")
|
|
.short("e")
|
|
.multiple(true)
|
|
.takes_value(true)
|
|
.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: cursor.style=Beam]"),
|
|
)
|
|
.get_matches();
|
|
|
|
if matches.is_present("ref-test") {
|
|
options.ref_test = true;
|
|
}
|
|
|
|
if matches.is_present("print-events") {
|
|
options.print_events = true;
|
|
}
|
|
|
|
if let Some(mut class) = matches.values_of("class") {
|
|
options.class_instance = class.next().map(|instance| instance.to_owned());
|
|
options.class_general = class.next().map(|general| general.to_owned());
|
|
}
|
|
|
|
options.title = matches.value_of("title").map(ToOwned::to_owned);
|
|
options.embed = matches.value_of("embed").map(ToOwned::to_owned);
|
|
|
|
match matches.occurrences_of("q") {
|
|
0 => (),
|
|
1 => options.log_level = LevelFilter::Error,
|
|
_ => options.log_level = LevelFilter::Off,
|
|
}
|
|
|
|
match matches.occurrences_of("v") {
|
|
0 if !options.print_events => options.log_level = LevelFilter::Warn,
|
|
0 | 1 => options.log_level = LevelFilter::Info,
|
|
2 => options.log_level = LevelFilter::Debug,
|
|
_ => options.log_level = LevelFilter::Trace,
|
|
}
|
|
|
|
if let Some(dir) = matches.value_of("working-directory") {
|
|
options.working_directory = Some(PathBuf::from(dir.to_string()));
|
|
}
|
|
|
|
if let Some(path) = matches.value_of("config-file") {
|
|
options.config_path = Some(PathBuf::from(path.to_string()));
|
|
}
|
|
|
|
if let Some(mut args) = matches.values_of("command") {
|
|
// The following unwrap is guaranteed to succeed.
|
|
// If `command` exists it must also have a first item since
|
|
// `Arg::min_values(1)` is set.
|
|
let program = String::from(args.next().unwrap());
|
|
let args = args.map(String::from).collect();
|
|
options.command = Some(Program::WithArgs { program, args });
|
|
}
|
|
|
|
if matches.is_present("hold") {
|
|
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_path.clone()
|
|
}
|
|
|
|
/// 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(command) = &self.command {
|
|
config.shell = Some(command.clone());
|
|
}
|
|
|
|
config.hold = self.hold;
|
|
|
|
if let Some(title) = self.title.clone() {
|
|
config.ui_config.window.title = title
|
|
}
|
|
if let Some(class_instance) = self.class_instance.clone() {
|
|
config.ui_config.window.class.instance = class_instance;
|
|
}
|
|
if let Some(class_general) = self.class_general.clone() {
|
|
config.ui_config.window.class.general = class_general;
|
|
}
|
|
|
|
config.ui_config.window.dynamic_title &= self.title.is_none();
|
|
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;
|
|
|
|
if config.ui_config.debug.print_events {
|
|
config.ui_config.debug.log_level =
|
|
max(config.ui_config.debug.log_level, LevelFilter::Info);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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 super::*;
|
|
|
|
use serde_yaml::mapping::Mapping;
|
|
|
|
#[test]
|
|
fn dynamic_title_ignoring_options_by_default() {
|
|
let mut config = Config::default();
|
|
let old_dynamic_title = config.ui_config.window.dynamic_title;
|
|
|
|
Options::default().override_config(&mut config);
|
|
|
|
assert_eq!(old_dynamic_title, config.ui_config.window.dynamic_title);
|
|
}
|
|
|
|
#[test]
|
|
fn dynamic_title_overridden_by_options() {
|
|
let mut config = Config::default();
|
|
|
|
let options = Options { title: Some("foo".to_owned()), ..Options::default() };
|
|
options.override_config(&mut config);
|
|
|
|
assert!(!config.ui_config.window.dynamic_title);
|
|
}
|
|
|
|
#[test]
|
|
fn dynamic_title_not_overridden_by_config() {
|
|
let mut config = Config::default();
|
|
|
|
config.ui_config.window.title = "foo".to_owned();
|
|
Options::default().override_config(&mut config);
|
|
|
|
assert!(config.ui_config.window.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));
|
|
}
|
|
}
|