| use std::cmp::max; |
| use std::path::PathBuf; |
| |
| use log::{self, error, LevelFilter}; |
| use serde::{Deserialize, Serialize}; |
| use serde_yaml::Value; |
| use structopt::StructOpt; |
| |
| use alacritty_terminal::config::{Program, PtyConfig}; |
| |
| use crate::config::window::{Class, Identity, DEFAULT_NAME}; |
| use crate::config::{serde_utils, UiConfig}; |
| |
| /// CLI options for the main Alacritty executable. |
| #[derive(StructOpt, Default, Debug)] |
| #[structopt(author, about, version = env!("VERSION"))] |
| pub struct Options { |
| /// Print all events to stdout. |
| #[structopt(long)] |
| pub print_events: bool, |
| |
| /// Generates ref test. |
| #[structopt(long)] |
| pub ref_test: bool, |
| |
| /// Defines the X11 window ID (as a decimal integer) to embed Alacritty within. |
| #[structopt(long)] |
| pub embed: Option<String>, |
| |
| /// Specify alternative configuration file [default: $XDG_CONFIG_HOME/alacritty/alacritty.yml]. |
| #[cfg(not(any(target_os = "macos", windows)))] |
| #[structopt(long)] |
| pub config_file: Option<PathBuf>, |
| |
| /// Specify alternative configuration file [default: %APPDATA%\alacritty\alacritty.yml]. |
| #[cfg(windows)] |
| #[structopt(long)] |
| pub config_file: Option<PathBuf>, |
| |
| /// Specify alternative configuration file [default: $HOME/.config/alacritty/alacritty.yml]. |
| #[cfg(target_os = "macos")] |
| #[structopt(long)] |
| pub config_file: Option<PathBuf>, |
| |
| /// Path for IPC socket creation. |
| #[cfg(unix)] |
| #[structopt(long)] |
| pub socket: Option<PathBuf>, |
| |
| /// Reduces the level of verbosity (the min level is -qq). |
| #[structopt(short, conflicts_with("verbose"), parse(from_occurrences))] |
| quiet: u8, |
| |
| /// Increases the level of verbosity (the max level is -vvv). |
| #[structopt(short, conflicts_with("quiet"), parse(from_occurrences))] |
| verbose: u8, |
| |
| /// Override configuration file options [example: cursor.style=Beam]. |
| #[structopt(short = "o", long)] |
| option: Vec<String>, |
| |
| /// CLI options for config overrides. |
| #[structopt(skip)] |
| pub config_options: Value, |
| |
| /// Options which could be passed via IPC. |
| #[structopt(flatten)] |
| pub window_options: WindowOptions, |
| |
| /// Subcommand passed to the CLI. |
| #[cfg(unix)] |
| #[structopt(subcommand)] |
| pub subcommands: Option<Subcommands>, |
| } |
| |
| impl Options { |
| pub fn new() -> Self { |
| let mut options = Self::from_args(); |
| |
| // Convert `--option` flags into serde `Value`. |
| for option in &options.option { |
| 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 |
| } |
| |
| /// Override configuration file with options from the CLI. |
| pub fn override_config(&self, config: &mut UiConfig) { |
| #[cfg(unix)] |
| { |
| config.ipc_socket |= self.socket.is_some(); |
| } |
| |
| config.window.dynamic_title &= self.window_options.window_identity.title.is_none(); |
| config.window.embed = self.embed.as_ref().and_then(|embed| embed.parse().ok()); |
| config.debug.print_events |= self.print_events; |
| config.debug.log_level = max(config.debug.log_level, self.log_level()); |
| config.debug.ref_test |= self.ref_test; |
| |
| if config.debug.print_events { |
| config.debug.log_level = max(config.debug.log_level, LevelFilter::Info); |
| } |
| } |
| |
| /// Logging filter level. |
| pub fn log_level(&self) -> LevelFilter { |
| match (self.quiet, self.verbose) { |
| // Force at least `Info` level for `--print-events`. |
| (_, 0) if self.print_events => LevelFilter::Info, |
| |
| // Default. |
| (0, 0) => LevelFilter::Warn, |
| |
| // Verbose. |
| (_, 1) => LevelFilter::Info, |
| (_, 2) => LevelFilter::Debug, |
| (0, _) => LevelFilter::Trace, |
| |
| // Quiet. |
| (1, _) => LevelFilter::Error, |
| (..) => LevelFilter::Off, |
| } |
| } |
| } |
| |
| /// 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) |
| } |
| |
| /// Parse the class CLI parameter. |
| fn parse_class(input: &str) -> Result<Class, String> { |
| match input.find(',') { |
| Some(position) => { |
| let general = input[position + 1..].to_owned(); |
| |
| // Warn the user if they've passed too many values. |
| if general.contains(',') { |
| return Err(String::from("Too many parameters")); |
| } |
| |
| Ok(Class { instance: input[..position].into(), general }) |
| }, |
| None => Ok(Class { instance: input.into(), general: DEFAULT_NAME.into() }), |
| } |
| } |
| |
| /// Terminal specific cli options which can be passed to new windows via IPC. |
| #[derive(Serialize, Deserialize, StructOpt, Default, Debug, Clone, PartialEq)] |
| pub struct TerminalOptions { |
| /// Start the shell in the specified working directory. |
| #[structopt(long)] |
| pub working_directory: Option<PathBuf>, |
| |
| /// Remain open after child process exit. |
| #[structopt(long)] |
| pub hold: bool, |
| |
| /// Command and args to execute (must be last argument). |
| #[structopt(short = "e", long, allow_hyphen_values = true)] |
| command: Vec<String>, |
| } |
| |
| impl TerminalOptions { |
| /// Shell override passed through the CLI. |
| pub fn command(&self) -> Option<Program> { |
| let (program, args) = self.command.split_first()?; |
| Some(Program::WithArgs { program: program.clone(), args: args.to_vec() }) |
| } |
| |
| /// Override the [`PtyConfig`]'s fields with the [`TerminalOptions`]. |
| pub fn override_pty_config(&self, pty_config: &mut PtyConfig) { |
| if let Some(working_directory) = &self.working_directory { |
| if working_directory.is_dir() { |
| pty_config.working_directory = Some(working_directory.to_owned()); |
| } else { |
| error!("Invalid working directory: {:?}", working_directory); |
| } |
| } |
| |
| if let Some(command) = self.command() { |
| pty_config.shell = Some(command); |
| } |
| |
| pty_config.hold |= self.hold; |
| } |
| } |
| |
| impl From<TerminalOptions> for PtyConfig { |
| fn from(mut options: TerminalOptions) -> Self { |
| PtyConfig { |
| working_directory: options.working_directory.take(), |
| shell: options.command(), |
| hold: options.hold, |
| } |
| } |
| } |
| |
| /// Window specific cli options which can be passed to new windows via IPC. |
| #[derive(Serialize, Deserialize, StructOpt, Default, Debug, Clone, PartialEq)] |
| pub struct WindowIdentity { |
| /// Defines the window title [default: Alacritty]. |
| #[structopt(short, long)] |
| pub title: Option<String>, |
| |
| /// Defines window class/app_id on X11/Wayland [default: Alacritty]. |
| #[structopt(long, value_name = "instance> | <instance>,<general", parse(try_from_str = parse_class))] |
| pub class: Option<Class>, |
| } |
| |
| impl WindowIdentity { |
| /// Override the [`WindowIdentityConfig`]'s fields with the [`WindowOptions`]. |
| pub fn override_identity_config(&self, identity: &mut Identity) { |
| if let Some(title) = &self.title { |
| identity.title = title.clone(); |
| } |
| if let Some(class) = &self.class { |
| identity.class = class.clone(); |
| } |
| } |
| } |
| |
| /// Available CLI subcommands. |
| #[cfg(unix)] |
| #[derive(StructOpt, Debug)] |
| pub enum Subcommands { |
| Msg(MessageOptions), |
| } |
| |
| /// Send a message to the Alacritty socket. |
| #[cfg(unix)] |
| #[derive(StructOpt, Debug)] |
| pub struct MessageOptions { |
| /// IPC socket connection path override. |
| #[structopt(long, short)] |
| pub socket: Option<PathBuf>, |
| |
| /// Message which should be sent. |
| #[structopt(subcommand)] |
| pub message: SocketMessage, |
| } |
| |
| /// Available socket messages. |
| #[cfg(unix)] |
| #[derive(StructOpt, Serialize, Deserialize, Debug, Clone, PartialEq)] |
| pub enum SocketMessage { |
| /// Create a new window in the same Alacritty process. |
| CreateWindow(WindowOptions), |
| } |
| |
| /// Subset of options that we pass to a 'create-window' subcommand. |
| #[derive(StructOpt, Serialize, Deserialize, Default, Clone, Debug, PartialEq)] |
| pub struct WindowOptions { |
| /// Terminal options which can be passed via IPC. |
| #[structopt(flatten)] |
| pub terminal_options: TerminalOptions, |
| |
| #[structopt(flatten)] |
| /// Window options which could be passed via IPC. |
| pub window_identity: WindowIdentity, |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| |
| #[cfg(target_os = "linux")] |
| use std::fs::File; |
| #[cfg(target_os = "linux")] |
| use std::io::Read; |
| |
| #[cfg(target_os = "linux")] |
| use clap::Shell; |
| use serde_yaml::mapping::Mapping; |
| |
| #[test] |
| fn dynamic_title_ignoring_options_by_default() { |
| let mut config = UiConfig::default(); |
| let old_dynamic_title = config.window.dynamic_title; |
| |
| Options::default().override_config(&mut config); |
| |
| assert_eq!(old_dynamic_title, config.window.dynamic_title); |
| } |
| |
| #[test] |
| fn dynamic_title_overridden_by_options() { |
| let mut config = UiConfig::default(); |
| |
| let title = Some(String::from("foo")); |
| let window_identity = WindowIdentity { title, ..WindowIdentity::default() }; |
| let new_window_options = WindowOptions { window_identity, ..WindowOptions::default() }; |
| let options = Options { window_options: new_window_options, ..Options::default() }; |
| options.override_config(&mut config); |
| |
| assert!(!config.window.dynamic_title); |
| } |
| |
| #[test] |
| fn dynamic_title_not_overridden_by_config() { |
| let mut config = UiConfig::default(); |
| |
| config.window.identity.title = "foo".to_owned(); |
| Options::default().override_config(&mut config); |
| |
| assert!(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)); |
| } |
| |
| #[test] |
| fn parse_instance_class() { |
| let class = parse_class("one").unwrap(); |
| assert_eq!(class.instance, "one"); |
| assert_eq!(class.general, DEFAULT_NAME); |
| } |
| |
| #[test] |
| fn parse_general_class() { |
| let class = parse_class("one,two").unwrap(); |
| assert_eq!(class.instance, "one"); |
| assert_eq!(class.general, "two"); |
| } |
| |
| #[test] |
| fn parse_invalid_class() { |
| let class = parse_class("one,two,three"); |
| assert!(class.is_err()); |
| } |
| |
| #[cfg(target_os = "linux")] |
| #[test] |
| fn completions() { |
| let mut clap = Options::clap(); |
| |
| for (shell, file) in &[ |
| (Shell::Bash, "alacritty.bash"), |
| (Shell::Fish, "alacritty.fish"), |
| (Shell::Zsh, "_alacritty"), |
| ] { |
| let mut generated = Vec::new(); |
| clap.gen_completions_to("alacritty", *shell, &mut generated); |
| let generated = String::from_utf8_lossy(&generated); |
| |
| let mut completion = String::new(); |
| let mut file = File::open(format!("../extra/completions/{}", file)).unwrap(); |
| file.read_to_string(&mut completion).unwrap(); |
| |
| assert_eq!(generated, completion); |
| } |
| |
| // NOTE: Use this to generate new completions. |
| // |
| // clap.gen_completions("alacritty", Shell::Bash, "../extra/completions/"); |
| // clap.gen_completions("alacritty", Shell::Fish, "../extra/completions/"); |
| // clap.gen_completions("alacritty", Shell::Zsh, "../extra/completions/"); |
| } |
| } |