diff options
Diffstat (limited to 'alacritty/src')
-rw-r--r-- | alacritty/src/cli.rs | 116 | ||||
-rw-r--r-- | alacritty/src/config/bindings.rs | 38 | ||||
-rw-r--r-- | alacritty/src/config/color.rs | 7 | ||||
-rw-r--r-- | alacritty/src/config/mod.rs | 202 | ||||
-rw-r--r-- | alacritty/src/config/mouse.rs | 28 | ||||
-rw-r--r-- | alacritty/src/config/serde_utils.rs | 66 | ||||
-rw-r--r-- | alacritty/src/config/ui_config.rs | 109 | ||||
-rw-r--r-- | alacritty/src/config/window.rs | 4 | ||||
-rw-r--r-- | alacritty/src/display/content.rs | 4 | ||||
-rw-r--r-- | alacritty/src/main.rs | 15 | ||||
-rw-r--r-- | alacritty/src/migrate.rs | 249 | ||||
-rw-r--r-- | alacritty/src/window_context.rs | 6 |
12 files changed, 593 insertions, 251 deletions
diff --git a/alacritty/src/cli.rs b/alacritty/src/cli.rs index 7f39e527..7eb281d9 100644 --- a/alacritty/src/cli.rs +++ b/alacritty/src/cli.rs @@ -2,12 +2,10 @@ use std::cmp::max; use std::os::raw::c_ulong; use std::path::PathBuf; -#[cfg(unix)] -use clap::Subcommand; -use clap::{ArgAction, Args, Parser, ValueHint}; +use clap::{ArgAction, Args, Parser, Subcommand, ValueHint}; use log::{self, error, LevelFilter}; use serde::{Deserialize, Serialize}; -use serde_yaml::Value; +use toml::{Table, Value}; use alacritty_terminal::config::{Program, PtyConfig}; @@ -30,17 +28,18 @@ pub struct Options { #[clap(long)] pub embed: Option<String>, - /// Specify alternative configuration file [default: $XDG_CONFIG_HOME/alacritty/alacritty.yml]. + /// Specify alternative configuration file [default: + /// $XDG_CONFIG_HOME/alacritty/alacritty.toml]. #[cfg(not(any(target_os = "macos", windows)))] #[clap(long, value_hint = ValueHint::FilePath)] pub config_file: Option<PathBuf>, - /// Specify alternative configuration file [default: %APPDATA%\alacritty\alacritty.yml]. + /// Specify alternative configuration file [default: %APPDATA%\alacritty\alacritty.toml]. #[cfg(windows)] #[clap(long, value_hint = ValueHint::FilePath)] pub config_file: Option<PathBuf>, - /// Specify alternative configuration file [default: $HOME/.config/alacritty/alacritty.yml]. + /// Specify alternative configuration file [default: $HOME/.config/alacritty/alacritty.toml]. #[cfg(target_os = "macos")] #[clap(long, value_hint = ValueHint::FilePath)] pub config_file: Option<PathBuf>, @@ -64,14 +63,13 @@ pub struct Options { /// CLI options for config overrides. #[clap(skip)] - pub config_options: Value, + pub config_options: TomlValue, /// Options which can be passed via IPC. #[clap(flatten)] pub window_options: WindowOptions, /// Subcommand passed to the CLI. - #[cfg(unix)] #[clap(subcommand)] pub subcommands: Option<Subcommands>, } @@ -81,7 +79,7 @@ impl Options { let mut options = Self::parse(); // Convert `--option` flags into serde `Value`. - options.config_options = options_as_value(&options.option); + options.config_options = TomlValue(options_as_value(&options.option)); options } @@ -125,9 +123,9 @@ impl Options { } } -/// Combine multiple options into a [`serde_yaml::Value`]. +/// Combine multiple options into a [`toml::Value`]. pub fn options_as_value(options: &[String]) -> Value { - options.iter().fold(Value::default(), |value, option| match option_as_value(option) { + options.iter().fold(Value::Table(Table::new()), |value, option| match toml::from_str(option) { Ok(new_value) => serde_utils::merge(value, new_value), Err(_) => { eprintln!("Ignoring invalid option: {:?}", option); @@ -136,31 +134,6 @@ pub fn options_as_value(options: &[String]) -> Value { }) } -/// Parse an option in the format of `parent.field=value` as 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> { let (general, instance) = match input.split_once(',') { @@ -259,10 +232,11 @@ impl WindowIdentity { } /// Available CLI subcommands. -#[cfg(unix)] #[derive(Subcommand, Debug)] pub enum Subcommands { + #[cfg(unix)] Msg(MessageOptions), + Migrate(MigrateOptions), } /// Send a message to the Alacritty socket. @@ -289,6 +263,30 @@ pub enum SocketMessage { Config(IpcConfig), } +/// Migrate the configuration file. +#[derive(Args, Clone, Debug)] +pub struct MigrateOptions { + /// Path to the configuration file. + #[clap(short, long, value_hint = ValueHint::FilePath)] + pub config_file: Option<PathBuf>, + + /// Only output TOML config to stdout. + #[clap(short, long)] + pub dry_run: bool, + + /// Do not recurse over imports. + #[clap(short = 'i', long)] + pub skip_imports: bool, + + /// Do not move renamed fields to their new location. + #[clap(long)] + pub skip_renames: bool, + + #[clap(short, long)] + /// Do not output to STDOUT. + pub silent: bool, +} + /// Subset of options that we pass to 'create-window' IPC subcommand. #[derive(Serialize, Deserialize, Args, Default, Clone, Debug, PartialEq, Eq)] pub struct WindowOptions { @@ -320,6 +318,16 @@ pub struct IpcConfig { pub reset: bool, } +/// Toml value with default implementation. +#[derive(Debug)] +pub struct TomlValue(pub Value); + +impl Default for TomlValue { + fn default() -> Self { + Self(Value::Table(Table::new())) + } +} + #[cfg(test)] mod tests { use super::*; @@ -333,7 +341,7 @@ mod tests { use clap::CommandFactory; #[cfg(target_os = "linux")] use clap_complete::Shell; - use serde_yaml::mapping::Mapping; + use toml::Table; #[test] fn dynamic_title_ignoring_options_by_default() { @@ -371,38 +379,38 @@ mod tests { #[test] fn valid_option_as_value() { // Test with a single field. - let value = option_as_value("field=true").unwrap(); + let value: Value = toml::from_str("field=true").unwrap(); - let mut mapping = Mapping::new(); - mapping.insert(Value::String(String::from("field")), Value::Bool(true)); + let mut table = Table::new(); + table.insert(String::from("field"), Value::Boolean(true)); - assert_eq!(value, Value::Mapping(mapping)); + assert_eq!(value, Value::Table(table)); // Test with nested fields - let value = option_as_value("parent.field=true").unwrap(); + let value: Value = toml::from_str("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)); + let mut parent_table = Table::new(); + parent_table.insert(String::from("field"), Value::Boolean(true)); + let mut table = Table::new(); + table.insert(String::from("parent"), Value::Table(parent_table)); - assert_eq!(value, Value::Mapping(mapping)); + assert_eq!(value, Value::Table(table)); } #[test] fn invalid_option_as_value() { - let value = option_as_value("}"); + let value = toml::from_str::<Value>("}"); assert!(value.is_err()); } #[test] fn float_option_as_value() { - let value = option_as_value("float=3.4").unwrap(); + let value: Value = toml::from_str("float=3.4").unwrap(); - let mut expected = Mapping::new(); - expected.insert(Value::String(String::from("float")), Value::Number(3.4.into())); + let mut expected = Table::new(); + expected.insert(String::from("float"), Value::Float(3.4)); - assert_eq!(value, Value::Mapping(expected)); + assert_eq!(value, Value::Table(expected)); } #[test] diff --git a/alacritty/src/config/bindings.rs b/alacritty/src/config/bindings.rs index 692cf7e9..8fd16361 100644 --- a/alacritty/src/config/bindings.rs +++ b/alacritty/src/config/bindings.rs @@ -5,7 +5,7 @@ use std::fmt::{self, Debug, Display}; use bitflags::bitflags; use serde::de::{self, Error as SerdeError, MapAccess, Unexpected, Visitor}; use serde::{Deserialize, Deserializer}; -use serde_yaml::Value as SerdeValue; +use toml::Value as SerdeValue; use winit::event::VirtualKeyCode::*; use winit::event::{ModifiersState, MouseButton, VirtualKeyCode}; @@ -1011,19 +1011,20 @@ impl<'a> Deserialize<'a> for RawBinding { return Err(<V::Error as Error>::duplicate_field("key")); } - let val = map.next_value::<SerdeValue>()?; - if val.is_u64() { - let scancode = val.as_u64().unwrap(); - if scancode > u64::from(u32::MAX) { - return Err(<V::Error as Error>::custom(format!( - "Invalid key binding, scancode too big: {}", - scancode - ))); - } - key = Some(Key::Scancode(scancode as u32)); - } else { - let k = Key::deserialize(val).map_err(V::Error::custom)?; - key = Some(k); + let value = map.next_value::<SerdeValue>()?; + match value.as_integer() { + Some(scancode) => match u32::try_from(scancode) { + Ok(scancode) => key = Some(Key::Scancode(scancode)), + Err(_) => { + return Err(<V::Error as Error>::custom(format!( + "Invalid key binding, scancode is too big: {}", + scancode + ))); + }, + }, + None => { + key = Some(Key::deserialize(value).map_err(V::Error::custom)?); + }, } }, Field::Mods => { @@ -1066,15 +1067,6 @@ impl<'a> Deserialize<'a> for RawBinding { Err(err) => { let value = match value { SerdeValue::String(string) => string, - SerdeValue::Mapping(map) if map.len() == 1 => { - match map.into_iter().next() { - Some(( - SerdeValue::String(string), - SerdeValue::Null, - )) => string, - _ => return Err(err), - } - }, _ => return Err(err), }; return Err(V::Error::custom(format!( diff --git a/alacritty/src/config/color.rs b/alacritty/src/config/color.rs index 23c18e50..e08b08b4 100644 --- a/alacritty/src/config/color.rs +++ b/alacritty/src/config/color.rs @@ -18,16 +18,17 @@ pub struct Colors { pub line_indicator: LineIndicatorColors, pub hints: HintColors, pub transparent_background_colors: bool, + pub draw_bold_text_with_bright_colors: bool, footer_bar: BarColors, } impl Colors { pub fn footer_bar_foreground(&self) -> Rgb { - self.search.bar.foreground.or(self.footer_bar.foreground).unwrap_or(self.primary.background) + self.footer_bar.foreground.unwrap_or(self.primary.background) } pub fn footer_bar_background(&self) -> Rgb { - self.search.bar.background.or(self.footer_bar.background).unwrap_or(self.primary.foreground) + self.footer_bar.background.unwrap_or(self.primary.foreground) } } @@ -126,8 +127,6 @@ impl Default for InvertedCellColors { pub struct SearchColors { pub focused_match: FocusedMatchColors, pub matches: MatchColors, - #[config(deprecated = "use `colors.footer_bar` instead")] - bar: BarColors, } #[derive(ConfigDeserialize, Debug, Copy, Clone, PartialEq, Eq)] diff --git a/alacritty/src/config/mod.rs b/alacritty/src/config/mod.rs index 2f230b06..821e9b6b 100644 --- a/alacritty/src/config/mod.rs +++ b/alacritty/src/config/mod.rs @@ -1,11 +1,14 @@ use std::fmt::{self, Display, Formatter}; use std::path::{Path, PathBuf}; +use std::result::Result as StdResult; use std::{env, fs, io}; -use log::{debug, error, info}; +use log::{debug, error, info, warn}; use serde::Deserialize; -use serde_yaml::mapping::Mapping; -use serde_yaml::Value; +use serde_yaml::Error as YamlError; +use toml::de::Error as TomlError; +use toml::ser::Error as TomlSeError; +use toml::{Table, Value}; use alacritty_terminal::config::LOG_TARGET_CONFIG; @@ -30,7 +33,7 @@ pub use crate::config::mouse::{ClickHandler, Mouse}; pub use crate::config::ui_config::UiConfig; /// Maximum number of depth for the configuration file imports. -const IMPORT_RECURSION_LIMIT: usize = 5; +pub const IMPORT_RECURSION_LIMIT: usize = 5; /// Result from config loading. pub type Result<T> = std::result::Result<T, Error>; @@ -47,8 +50,14 @@ pub enum Error { /// io error reading file. Io(io::Error), - /// Not valid yaml or missing parameters. - Yaml(serde_yaml::Error), + /// Invalid toml. + Toml(TomlError), + + /// Failed toml serialization. + TomlSe(TomlSeError), + + /// Invalid yaml. + Yaml(YamlError), } impl std::error::Error for Error { @@ -57,6 +66,8 @@ impl std::error::Error for Error { Error::NotFound => None, Error::ReadingEnvHome(err) => err.source(), Error::Io(err) => err.source(), + Error::Toml(err) => err.source(), + Error::TomlSe(err) => err.source(), Error::Yaml(err) => err.source(), } } @@ -70,6 +81,8 @@ impl Display for Error { write!(f, "Unable to read $HOME environment variable: {}", err) }, Error::Io(err) => write!(f, "Error reading config file: {}", err), + Error::Toml(err) => write!(f, "Config error: {}", err), + Error::TomlSe(err) => write!(f, "Yaml conversion error: {}", err), Error::Yaml(err) => write!(f, "Config error: {}", err), } } @@ -91,16 +104,32 @@ impl From<io::Error> for Error { } } -impl From<serde_yaml::Error> for Error { - fn from(val: serde_yaml::Error) -> Self { +impl From<TomlError> for Error { + fn from(val: TomlError) -> Self { + Error::Toml(val) + } +} + +impl From<TomlSeError> for Error { + fn from(val: TomlSeError) -> Self { + Error::TomlSe(val) + } +} + +impl From<YamlError> for Error { + fn from(val: YamlError) -> Self { Error::Yaml(val) } } /// Load the configuration file. pub fn load(options: &Options) -> UiConfig { - let config_options = options.config_options.clone(); - let config_path = options.config_file.clone().or_else(installed_config); + let config_options = options.config_options.0.clone(); + let config_path = options + .config_file + .clone() + .or_else(|| installed_config("toml")) + .or_else(|| installed_config("yml")); // Load the config using the following fallback behavior: // - Config path + CLI overrides @@ -128,7 +157,7 @@ pub fn reload(config_path: &Path, options: &Options) -> Result<UiConfig> { debug!("Reloading configuration file: {:?}", config_path); // Load config, propagating errors. - let config_options = options.config_options.clone(); + let config_options = options.config_options.0.clone(); let mut config = load_from(config_path, config_options)?; after_loading(&mut config, options); @@ -179,6 +208,16 @@ fn parse_config( ) -> Result<Value> { config_paths.push(path.to_owned()); + // Deserialize the configuration file. + let config = deserialize_config(path)?; + + // Merge config with imports. + let imports = load_imports(&config, config_paths, recursion_limit); + Ok(serde_utils::merge(imports, config)) +} + +/// Deserialize a configuration file. +pub fn deserialize_config(path: &Path) -> Result<Value> { let mut contents = fs::read_to_string(path)?; // Remove UTF-8 BOM. @@ -186,51 +225,84 @@ fn parse_config( contents = contents.split_off(3); } + // Convert YAML to TOML as a transitionary fallback mechanism. + let extension = path.extension().unwrap_or_default(); + if (extension == "yaml" || extension == "yml") && !contents.trim().is_empty() { + warn!("YAML config {path:?} is deprecated, please migrate to TOML"); + + let value: serde_yaml::Value = serde_yaml::from_str(&contents)?; + contents = toml::to_string(&value)?; + } + // Load configuration file as Value. - let config: Value = match serde_yaml::from_str(&contents) { - Ok(config) => config, - Err(error) => { - // Prevent parsing error with an empty string and commented out file. - if error.to_string() == "EOF while parsing a value" { - Value::Mapping(Mapping::new()) - } else { - return Err(Error::Yaml(error)); - } - }, - }; + let config: Value = toml::from_str(&contents)?; - // Merge config with imports. - let imports = load_imports(&config, config_paths, recursion_limit); - Ok(serde_utils::merge(imports, config)) + Ok(config) } /// Load all referenced configuration files. fn load_imports(config: &Value, config_paths: &mut Vec<PathBuf>, recursion_limit: usize) -> Value { - let imports = match config.get("import") { - Some(Value::Sequence(imports)) => imports, - Some(_) => { - error!(target: LOG_TARGET_CONFIG, "Invalid import type: expected a sequence"); - return Value::Null; + // Get paths for all imports. + let import_paths = match imports(config, recursion_limit) { + Ok(import_paths) => import_paths, + Err(err) => { + error!(target: LOG_TARGET_CONFIG, "{err}"); + return Value::Table(Table::new()); }, - None => return Value::Null, + }; + + // Parse configs for all imports recursively. + let mut merged = Value::Table(Table::new()); + for import_path in import_paths { + let path = match import_path { + Ok(path) => path, + Err(err) => { + error!(target: LOG_TARGET_CONFIG, "{err}"); + continue; + }, + }; + + if !path.exists() { + info!(target: LOG_TARGET_CONFIG, "Config import not found:\n {:?}", path.display()); + continue; + } + + match parse_config(&path, config_paths, recursion_limit - 1) { + Ok(config) => merged = serde_utils::merge(merged, config), + Err(err) => { + error!(target: LOG_TARGET_CONFIG, "Unable to import config {:?}: {}", path, err) + }, + } + } + + merged +} + +// TODO: Merge back with `load_imports` once `alacritty migrate` is dropped. +// +/// Get all import paths for a configuration. +pub fn imports( + config: &Value, + recursion_limit: usize, +) -> StdResult<Vec<StdResult<PathBuf, String>>, String> { + let imports = match config.get("import") { + Some(Value::Array(imports)) => imports, + Some(_) => return Err("Invalid import type: expected a sequence".into()), + None => return Ok(Vec::new()), }; // Limit recursion to prevent infinite loops. if !imports.is_empty() && recursion_limit == 0 { - error!(target: LOG_TARGET_CONFIG, "Exceeded maximum configuration import depth"); - return Value::Null; + return Err("Exceeded maximum configuration import depth".into()); } - let mut merged = Value::Null; + let mut import_paths = Vec::new(); for import in imports { let mut path = match import { Value::String(path) => PathBuf::from(path), _ => { - error!( - target: LOG_TARGET_CONFIG, - "Invalid import element type: expected path string" - ); + import_paths.push(Err("Invalid import element type: expected path string".into())); continue; }, }; @@ -240,49 +312,42 @@ fn load_imports(config: &Value, config_paths: &mut Vec<PathBuf>, recursion_limit path = home_dir.join(stripped); } - if !path.exists() { - info!(target: LOG_TARGET_CONFIG, "Config import not found:\n {:?}", path.display()); - continue; - } - - match parse_config(&path, config_paths, recursion_limit - 1) { - Ok(config) => merged = serde_utils::merge(merged, config), - Err(err) => { - error!(target: LOG_TARGET_CONFIG, "Unable to import config {:?}: {}", path, err) - }, - } + import_paths.push(Ok(path)); } - merged + Ok(import_paths) } /// 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 +/// 1. $XDG_CONFIG_HOME/alacritty/alacritty.toml +/// 2. $XDG_CONFIG_HOME/alacritty.toml +/// 3. $HOME/.config/alacritty/alacritty.toml +/// 4. $HOME/.alacritty.toml #[cfg(not(windows))] -fn installed_config() -> Option<PathBuf> { +pub fn installed_config(suffix: &str) -> Option<PathBuf> { + let file_name = format!("alacritty.{suffix}"); + // Try using XDG location by default. xdg::BaseDirectories::with_prefix("alacritty") .ok() - .and_then(|xdg| xdg.find_config_file("alacritty.yml")) + .and_then(|xdg| xdg.find_config_file(&file_name)) .or_else(|| { xdg::BaseDirectories::new() .ok() - .and_then(|fallback| fallback.find_config_file("alacritty.yml")) + .and_then(|fallback| fallback.find_config_file(&file_name)) }) .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"); + // Fallback path: $HOME/.config/alacritty/alacritty.toml. + let fallback = PathBuf::from(&home).join(".config/alacritty").join(&file_name); if fallback.exists() { return Some(fallback); } - // Fallback path: $HOME/.alacritty.yml. - let fallback = PathBuf::from(&home).join(".alacritty.yml"); + // Fallback path: $HOME/.alacritty.toml. + let hidden_name = format!(".{file_name}"); + let fallback = PathBuf::from(&home).join(hidden_name); if fallback.exists() { return Some(fallback); } @@ -292,22 +357,17 @@ fn installed_config() -> Option<PathBuf> { } #[cfg(windows)] -fn installed_config() -> Option<PathBuf> { - dirs::config_dir().map(|path| path.join("alacritty\\alacritty.yml")).filter(|new| new.exists()) +pub fn installed_config(suffix: &str) -> Option<PathBuf> { + let file_name = format!("alacritty.{suffix}"); + dirs::config_dir().map(|path| path.join("alacritty").join(file_name)).filter(|new| new.exists()) } #[cfg(test)] mod tests { use super::*; - static DEFAULT_ALACRITTY_CONFIG: &str = - concat!(env!("CARGO_MANIFEST_DIR"), "/../alacritty.yml"); - #[test] - fn config_read_eof() { - let config_path: PathBuf = DEFAULT_ALACRITTY_CONFIG.into(); - let mut config = read_config(&config_path, Value::Null).unwrap(); - config.config_paths = Vec::new(); - assert_eq!(config, UiConfig::default()); + fn empty_config() { + toml::from_str::<UiConfig>("").unwrap(); } } diff --git a/alacritty/src/config/mouse.rs b/alacritty/src/config/mouse.rs index 291e4c61..b6556a2c 100644 --- a/alacritty/src/config/mouse.rs +++ b/alacritty/src/config/mouse.rs @@ -1,14 +1,18 @@ use std::time::Duration; -use alacritty_config_derive::ConfigDeserialize; +use serde::{Deserialize, Deserializer}; + +use alacritty_config_derive::{ConfigDeserialize, SerdeReplace}; + +use crate::config::bindings::{self, MouseBinding}; +use crate::config::ui_config; #[derive(ConfigDeserialize, Default, Clone, Debug, PartialEq, Eq)] pub struct Mouse { pub double_click: ClickHandler, pub triple_click: ClickHandler, pub hide_when_typing: bool, - #[config(deprecated = "use `hints` section instead")] - pub url: Option<serde_yaml::Value>, + pub bindings: MouseBindings, } #[derive(ConfigDeserialize, Clone, Debug, PartialEq, Eq)] @@ -27,3 +31,21 @@ impl ClickHandler { Duration::from_millis(self.threshold as u64) } } + +#[derive(SerdeReplace, Clone, Debug, PartialEq, Eq)] +pub struct MouseBindings(pub Vec<MouseBinding>); + +impl Default for MouseBindings { + fn default() -> Self { + Self(bindings::default_mouse_bindings()) + } +} + +impl<'de> Deserialize<'de> for MouseBindings { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + Ok(Self(ui_config::deserialize_bindings(deserializer, Self::default().0)?)) + } +} diff --git a/alacritty/src/config/serde_utils.rs b/alacritty/src/config/serde_utils.rs index beb9c36b..476133e0 100644 --- a/alacritty/src/config/serde_utils.rs +++ b/alacritty/src/config/serde_utils.rs @@ -1,7 +1,6 @@ //! Serde helpers. -use serde_yaml::mapping::Mapping; -use serde_yaml::Value; +use toml::{Table, Value}; /// Merge two serde structures. /// @@ -9,20 +8,19 @@ use serde_yaml::Value; /// `replacement`. pub fn merge(base: Value, replacement: Value) -> Value { match (base, replacement) { - (Value::Sequence(mut base), Value::Sequence(mut replacement)) => { + (Value::Array(mut base), Value::Array(mut replacement)) => { base.append(&mut replacement); - Value::Sequence(base) + Value::Array(base) }, - (Value::Mapping(base), Value::Mapping(replacement)) => { - Value::Mapping(merge_mapping(base, replacement)) + (Value::Table(base), Value::Table(replacement)) => { + Value::Table(merge_tables(base, replacement)) }, - (value, Value::Null) => value, (_, value) => value, } } -/// Merge two key/value mappings. -fn merge_mapping(mut base: Mapping, replacement: Mapping) -> Mapping { +/// Merge two key/value tables. +fn merge_tables(mut base: Table, replacement: Table) -> Table { for (key, value) in replacement { let value = match base.remove(&key) { Some(base_value) => merge(base_value, value), @@ -40,54 +38,54 @@ mod tests { #[test] fn merge_primitive() { - let base = Value::Null; - let replacement = Value::Bool(true); + let base = Value::Table(Table::new()); + let replacement = Value::Boolean(true); assert_eq!(merge(base, replacement.clone()), replacement); - let base = Value::Bool(false); - let replacement = Value::Bool(true); + let base = Value::Boolean(false); + let replacement = Value::Boolean(true); assert_eq!(merge(base, replacement.clone()), replacement); - let base = Value::Number(0.into()); - let replacement = Value::Number(1.into()); + let base = Value::Integer(0.into()); + let replacement = Value::Integer(1.into()); assert_eq!(merge(base, replacement.clone()), replacement); 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; + let base = Value::Table(Table::new()); + let replacement = Value::Table(Table::new()); assert_eq!(merge(base.clone(), replacement), base); } #[test] fn merge_sequence() { - let base = Value::Sequence(vec![Value::Null]); - let replacement = Value::Sequence(vec![Value::Bool(true)]); - let expected = Value::Sequence(vec![Value::Null, Value::Bool(true)]); + let base = Value::Array(vec![Value::Table(Table::new())]); + let replacement = Value::Array(vec![Value::Boolean(true)]); + let expected = Value::Array(vec![Value::Table(Table::new()), Value::Boolean(true)]); assert_eq!(merge(base, replacement), expected); } #[test] - fn merge_mapping() { - let mut base_mapping = Mapping::new(); - base_mapping.insert(Value::String(String::from("a")), Value::Bool(true)); - base_mapping.insert(Value::String(String::from("b")), Value::Bool(false)); - let base = Value::Mapping(base_mapping); + fn merge_tables() { + let mut base_table = Table::new(); + base_table.insert(String::from("a"), Value::Boolean(true)); + base_table.insert(String::from("b"), Value::Boolean(false)); + let base = Value::Table(base_table); - let mut replacement_mapping = Mapping::new(); - replacement_mapping.insert(Value::String(String::from("a")), Value::Bool(true)); - replacement_mapping.insert(Value::String(String::from("c")), Value::Bool(false)); - let replacement = Value::Mapping(replacement_mapping); + let mut replacement_table = Table::new(); + replacement_table.insert(String::from("a"), Value::Boolean(true)); + replacement_table.insert(String::from("c"), Value::Boolean(false)); + let replacement = Value::Table(replacement_table); let merged = merge(base, replacement); - let mut expected_mapping = Mapping::new(); - expected_mapping.insert(Value::String(String::from("b")), Value::Bool(false)); - expected_mapping.insert(Value::String(String::from("a")), Value::Bool(true)); - expected_mapping.insert(Value::String(String::from("c")), Value::Bool(false)); - let expected = Value::Mapping(expected_mapping); + let mut expected_table = Table::new(); + expected_table.insert(String::from("b"), Value::Boolean(false)); + expected_table.insert(String::from("a"), Value::Boolean(true)); + expected_table.insert(String::from("c"), Value::Boolean(false)); + let expected = Value::Table(expected_table); assert_eq!(merged, expected); } diff --git a/alacritty/src/config/ui_config.rs b/alacritty/src/config/ui_config.rs index 29ff2c4c..0933ea42 100644 --- a/alacritty/src/config/ui_config.rs +++ b/alacritty/src/config/ui_config.rs @@ -3,16 +3,14 @@ use std::fmt::{self, Formatter}; use std::path::PathBuf; use std::rc::Rc; -use log::error; +use log::{error, warn}; use serde::de::{Error as SerdeError, MapAccess, Visitor}; use serde::{self, Deserialize, Deserializer}; use unicode_width::UnicodeWidthChar; use winit::event::{ModifiersState, VirtualKeyCode}; use alacritty_config_derive::{ConfigDeserialize, SerdeReplace}; -use alacritty_terminal::config::{ - Config as TerminalConfig, Percentage, Program, LOG_TARGET_CONFIG, -}; +use alacritty_terminal::config::{Config as TerminalConfig, Program, LOG_TARGET_CONFIG}; use alacritty_terminal::term::search::RegexSearch; use crate::config::bell::BellConfig; @@ -22,7 +20,7 @@ use crate::config::bindings::{ use crate::config::color::Colors; use crate::config::debug::Debug; use crate::config::font::Font; -use crate::config::mouse::Mouse; +use crate::config::mouse::{Mouse, MouseBindings}; use crate::config::window::WindowConfig; /// Regex used for the default URL hint. @@ -38,6 +36,7 @@ pub struct UiConfig { /// Window configuration. pub window: WindowConfig, + /// Mouse configuration. pub mouse: Mouse, /// Debug options. @@ -57,9 +56,6 @@ pub struct UiConfig { /// RGB values for colors. pub colors: Colors, - /// Should draw bold text with brighter colors instead of bold font. - pub draw_bold_text_with_bright_colors: bool, - /// Path where config was loaded from. #[config(skip)] pub config_paths: Vec<PathBuf>, @@ -75,37 +71,42 @@ pub struct UiConfig { #[config(flatten)] pub terminal_config: TerminalConfig, + /// Keyboard configuration. + keyboard: Keyboard, + + /// Should draw bold text with brighter colors instead of bold font. + #[config(deprecated = "use colors.draw_bold_text_with_bright_colors instead")] + draw_bold_text_with_bright_colors: bool, + /// Keybindings. + #[config(deprecated = "use keyboard.bindings instead")] key_bindings: KeyBindings, /// Bindings for the mouse. + #[config(deprecated = "use mouse.bindings instead")] mouse_bindings: MouseBindings, - - /// Background opacity from 0.0 to 1.0. - #[config(deprecated = "use window.opacity instead")] - background_opacity: Option<Percentage>, } impl Default for UiConfig { fn default() -> Self { Self { live_config_reload: true, - alt_send_esc: Default::default(), #[cfg(unix)] ipc_socket: true, - font: Default::default(), - window: Default::default(), - mouse: Default::default(), - debug: Default::default(), + draw_bold_text_with_bright_colors: Default::default(), + terminal_config: Default::default(), + mouse_bindings: Default::default(), config_paths: Default::default(), key_bindings: Default::default(), - mouse_bindings: Default::default(), - terminal_config: Default::default(), - background_opacity: Default::default(), - bell: Default::default(), + alt_send_esc: Default::default(), + keyboard: Default::default(), + window: Default::default(), colors: Default::default(), - draw_bold_text_with_bright_colors: Default::default(), + mouse: Default::default(), + debug: Default::default(), hints: Default::default(), + font: Default::default(), + bell: Default::default(), } } } @@ -113,6 +114,15 @@ impl Default for UiConfig { impl UiConfig { /// Generate key bindings for all keyboard hints. pub fn generate_hint_bindings(&mut self) { + // Check which key bindings is most likely to be the user's configuration. + // + // Both will be non-empty due to the presence of the default keybindings. + let key_bindings = if self.keyboard.bindings.0.len() >= self.key_bindings.0.len() { + &mut self.keyboard.bindings.0 + } else { + &mut self.key_bindings.0 + }; + for hint in &self.hints.enabled { let binding = match hint.binding { Some(binding) => binding, @@ -127,54 +137,56 @@ impl UiConfig { action: Action::Hint(hint.clone()), }; - self.key_bindings.0.push(binding); + key_bindings.push(binding); } } #[inline] pub fn window_opacity(&self) -> f32 { - self.background_opacity.unwrap_or(self.window.opacity).as_f32() + self.window.opacity.as_f32() } #[inline] pub fn key_bindings(&self) -> &[KeyBinding] { - self.key_bindings.0.as_slice() + if self.keyboard.bindings.0.len() >= self.key_bindings.0.len() { + self.keyboard.bindings.0.as_slice() + } else { + self.key_bindings.0.as_slice() + } } #[inline] pub fn mouse_bindings(&self) -> &[MouseBinding] { - self.mouse_bindings.0.as_slice() + if self.mouse.bindings.0.len() >= self.mouse_bindings.0.len() { + self.mouse.bindings.0.as_slice() + } else { + self.mouse_bindings.0.as_slice() + } } -} - -#[derive(SerdeReplace, Clone, Debug, PartialEq, Eq)] -struct KeyBindings(Vec<KeyBinding>); -impl Default for KeyBindings { - fn default() -> Self { - Self(bindings::default_key_bindings()) + #[inline] + pub fn draw_bold_text_with_bright_colors(&self) -> bool { + self.colors.draw_bold_text_with_bright_colors || self.draw_bold_text_with_bright_colors } } -impl<'de> Deserialize<'de> for KeyBindings { - fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> - where - D: Deserializer<'de>, - { - Ok(Self(deserialize_bindings(deserializer, Self::default().0)?)) - } +/// Keyboard configuration. +#[derive(ConfigDeserialize, Default, Clone, Debug, PartialEq)] +struct Keyboard { + /// Keybindings. + bindings: KeyBindings, } #[derive(SerdeReplace, Clone, Debug, PartialEq, Eq)] -struct MouseBindings(Vec<MouseBinding>); +struct KeyBindings(Vec<KeyBinding>); -impl Default for MouseBindings { +impl Default for KeyBindings { fn default() -> Self { - Self(bindings::default_mouse_bindings()) + Self(bindings::default_key_bindings()) } } -impl<'de> Deserialize<'de> for MouseBindings { +impl<'de> Deserialize<'de> for KeyBindings { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de>, @@ -183,7 +195,7 @@ impl<'de> Deserialize<'de> for MouseBindings { } } -fn deserialize_bindings<'a, D, T>( +pub fn deserialize_bindings<'a, D, T>( deserializer: D, mut default: Vec<Binding<T>>, ) -> Result<Vec<Binding<T>>, D::Error> @@ -192,7 +204,7 @@ where T: Copy + Eq, Binding<T>: Deserialize<'a>, { - let values = Vec::<serde_yaml::Value>::deserialize(deserializer)?; + let values = Vec::<toml::Value>::deserialize(deserializer)?; // Skip all invalid values. let mut bindings = Vec::with_capacity(values.len()); @@ -388,7 +400,7 @@ impl<'de> Deserialize<'de> for HintContent { { let mut content = Self::Value::default(); - while let Some((key, value)) = map.next_entry::<String, serde_yaml::Value>()? { + while let Some((key, value)) = map.next_entry::<String, toml::Value>()? { match key.as_str() { "regex" => match Option::<LazyRegex>::deserialize(value) { Ok(regex) => content.regex = regex, @@ -408,7 +420,8 @@ impl<'de> Deserialize<'de> for HintContent { ); }, }, - _ => (), + "command" | "action" => (), + key => warn!(target: LOG_TARGET_CONFIG, "Unrecognized hint field: {key}"), } } diff --git a/alacritty/src/config/window.rs b/alacritty/src/config/window.rs index db29fd85..e4236b99 100644 --- a/alacritty/src/config/window.rs +++ b/alacritty/src/config/window.rs @@ -232,7 +232,7 @@ impl<'de> Deserialize<'de> for Class { { let mut class = Self::Value::default(); - while let Some((key, value)) = map.next_entry::<String, serde_yaml::Value>()? { + while let Some((key, value)) = map.next_entry::<String, toml::Value>()? { match key.as_str() { "instance" => match String::deserialize(value) { Ok(instance) => class.instance = instance, @@ -252,7 +252,7 @@ impl<'de> Deserialize<'de> for Class { ); }, }, - _ => (), + key => warn!(target: LOG_TARGET_CONFIG, "Unrecognized class field: {key}"), } } diff --git a/alacritty/src/display/content.rs b/alacritty/src/display/content.rs index da211094..72f71e9b 100644 --- a/alacritty/src/display/content.rs +++ b/alacritty/src/display/content.rs @@ -314,7 +314,7 @@ impl RenderableCell { _ => rgb.into(), }, Color::Named(ansi) => { - match (config.draw_bold_text_with_bright_colors, flags & Flags::DIM_BOLD) { + match (config.draw_bold_text_with_bright_colors(), flags & Flags::DIM_BOLD) { // If no bright foreground is set, treat it like the BOLD flag doesn't exist. (_, Flags::DIM_BOLD) if ansi == NamedColor::Foreground @@ -334,7 +334,7 @@ impl RenderableCell { }, Color::Indexed(idx) => { let idx = match ( - config.draw_bold_text_with_bright_colors, + config.draw_bold_text_with_bright_colors(), flags & Flags::DIM_BOLD, idx, ) { diff --git a/alacritty/src/main.rs b/alacritty/src/main.rs index 29120b35..55f81502 100644 --- a/alacritty/src/main.rs +++ b/alacritty/src/main.rs @@ -42,6 +42,7 @@ mod logging; #[cfg(target_os = "macos")] mod macos; mod message_bar; +mod migrate; #[cfg(windows)] mod panic; mod renderer; @@ -54,9 +55,9 @@ mod gl { include!(concat!(env!("OUT_DIR"), "/gl_bindings.rs")); } -use crate::cli::Options; #[cfg(unix)] -use crate::cli::{MessageOptions, Subcommands}; +use crate::cli::MessageOptions; +use crate::cli::{Options, Subcommands}; use crate::config::{monitor, UiConfig}; use crate::event::{Event, Processor}; #[cfg(target_os = "macos")] @@ -77,14 +78,14 @@ fn main() -> Result<(), Box<dyn Error>> { // Load command line options. let options = Options::new(); - #[cfg(unix)] match options.subcommands { - Some(Subcommands::Msg(options)) => msg(options), - None => alacritty(options), + #[cfg(unix)] + Some(Subcommands::Msg(options)) => msg(options)?, + Some(Subcommands::Migrate(options)) => migrate::migrate(options), + None => alacritty(options)?, } - #[cfg(not(unix))] - alacritty(options) + Ok(()) } /// `msg` subcommand entrypoint. diff --git a/alacritty/src/migrate.rs b/alacritty/src/migrate.rs new file mode 100644 index 00000000..07a10ba4 --- /dev/null +++ b/alacritty/src/migrate.rs @@ -0,0 +1,249 @@ +//! Configuration file migration. + +use std::fs; +use std::path::Path; + +use toml::map::Entry; +use toml::{Table, Value}; + +use crate::cli::MigrateOptions; +use crate::config; + +/// Handle migration. +pub fn migrate(options: MigrateOptions) { + // Find configuration file path. + let config_path = options + .config_file + .clone() + .or_else(|| config::installed_config("toml")) + .or_else(|| config::installed_config("yml")); + + // Abort if system has no installed configuration. + let config_path = match config_path { + Some(config_path) => config_path, + None => { + eprintln!("No configuration file found"); + std::process::exit(1); + }, + }; + + // If we're doing a wet run, perform a dry run first for safety. + if !options.dry_run { + let mut options = options.clone(); + options.silent = true; + options.dry_run = true; + if let Err(err) = migrate_config(&options, &config_path, config::IMPORT_RECURSION_LIMIT) { + eprintln!("Configuration file migration failed:"); + eprintln!(" {config_path:?}: {err}"); + std::process::exit(1); + } + } + + // Migrate the root config. + match migrate_config(&options, &config_path, config::IMPORT_RECURSION_LIMIT) { + Ok(new_path) => { + if !options.silent { + println!("Successfully migrated {config_path:?} to {new_path:?}"); + } + }, + Err(err) => { + eprintln!("Configuration file migration failed:"); + eprintln!(" {config_path:?}: {err}"); + std::process::exit(1); + }, + } +} + +/// Migrate a specific configuration file. +fn migrate_config( + options: &MigrateOptions, + path: &Path, + recursion_limit: usize, +) -> Result<String, String> { + // Ensure configuration file has an extension. + let path_str = path.to_string_lossy(); + let (prefix, suffix) = match path_str.rsplit_once('.') { + Some((prefix, suffix)) => (prefix, suffix), + None => return Err("missing file extension".to_string()), + }; + + // Abort if config is already toml. + if suffix == "toml" { + return Err("already in TOML format".to_string()); + } + + // Try to parse the configuration file. + let mut config = match config::deserialize_config(path) { + Ok(config) => config, + Err(err) => return Err(format!("parsing error: {err}")), + }; + + // Migrate config imports. + if !options.skip_imports { + migrate_imports(options, &mut config, recursion_limit)?; + } + + // Migrate deprecated field names to their new location. + if !options.skip_renames { + migrate_renames(&mut config)?; + } + + // Convert to TOML format. + let toml = toml::to_string(&config).map_err(|err| format!("conversion error: {err}"))?; + let new_path = format!("{prefix}.toml"); + + if options.dry_run && !options.silent { + // Output new content to STDOUT. + println!( + "\nv-----Start TOML for {path:?}-----v\n\n{toml}\n^-----End TOML for {path:?}-----^" + ); + } else if !options.dry_run { + // Write the new toml configuration. + fs::write(&new_path, toml).map_err(|err| format!("filesystem error: {err}"))?; + } + + Ok(new_path) +} + +/// Migrate the imports of a config. +fn migrate_imports( + options: &MigrateOptions, + config: &mut Value, + recursion_limit: usize, +) -> Result<(), String> { + let imports = match config::imports(config, recursion_limit) { + Ok(imports) => imports, + Err(err) => return Err(format!("import error: {err}")), + }; + + // Migrate the individual imports. + let mut new_imports = Vec::new(); + for import in imports { + let import = match import { + Ok(import) => import, + Err(err) => return Err(format!("import error: {err}")), + }; + let new_path = migrate_config(options, &import, recursion_limit - 1)?; + new_imports.push(Value::String(new_path)); + } + + // Update the imports field. + if let Some(import) = config.get_mut("import") { + *import = Value::Array(new_imports); + } + + Ok(()) +} + +/// Migrate deprecated fields. +fn migrate_renames(config: &mut Value) -> Result<(), String> { + let config_table = match config.as_table_mut() { + Some(config_table) => config_table, + None => return Ok(()), + }; + + // draw_bold_text_with_bright_colors -> colors.draw_bold_text_with_bright_colors + move_value(config_table, &["draw_bold_text_with_bright_colors"], &[ + "colors", + "draw_bold_text_with_bright_colors", + ])?; + + // key_bindings -> keyboard.bindings + move_value(config_table, &["key_bindings"], &["keyboard", "bindings"])?; + + // mouse_bindings -> keyboard.bindings + move_value(config_table, &["mouse_bindings"], &["mouse", "bindings"])?; + + Ok(()) +} + +/// Move a toml value from one map to another. +fn move_value(config_table: &mut Table, origin: &[&str], target: &[&str]) -> Result<(), String> { + if let Some(value) = remove_node(config_table, origin)? { + if !insert_node_if_empty(config_table, target, value)? { + return Err(format!( + "conflict: both `{}` and `{}` are set", + origin.join("."), + target.join(".") + )); + } + } + + Ok(()) +} + +/// Remove a node from a tree of tables. +fn remove_node(table: &mut Table, path: &[&str]) -> Result<Option<Value>, String> { + if path.len() == 1 { + Ok(table.remove(path[0])) + } else { + let next_table_value = match table.get_mut(path[0]) { + Some(next_table_value) => next_table_value, + None => return Ok(None), + }; + + let next_table = match next_table_value.as_table_mut() { + Some(next_table) => next_table, + None => return Err(format!("invalid `{}` table", path[0])), + }; + + remove_node(next_table, &path[1..]) + } +} + +/// Try to insert a node into a tree of tables. +/// +/// Returns `false` if the node already exists. +fn insert_node_if_empty(table: &mut Table, path: &[&str], node: Value) -> Result<bool, String> { + if path.len() == 1 { + match table.entry(path[0]) { + Entry::Vacant(vacant_entry) => { + vacant_entry.insert(node); + Ok(true) + }, + Entry::Occupied(_) => Ok(false), + } + } else { + let next_table_value = table.entry(path[0]).or_insert_with(|| Value::Table(Table::new())); + + let next_table = match next_table_value.as_table_mut() { + Some(next_table) => next_table, + None => return Err(format!("invalid `{}` table", path[0])), + }; + + insert_node_if_empty(next_table, &path[1..], node) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn move_values() { + let input = r#" +root_value = 3 + +[table] +table_value = 5 + +[preexisting] +not_moved = 9 + "#; + + let mut value: Value = toml::from_str(input).unwrap(); + let table = value.as_table_mut().unwrap(); + + move_value(table, &["root_value"], &["new_table", "root_value"]).unwrap(); + move_value(table, &["table", "table_value"], &["preexisting", "subtable", "new_name"]) + .unwrap(); + + let output = toml::to_string(table).unwrap(); + + assert_eq!( + output, + "[new_table]\nroot_value = 3\n\n[preexisting]\nnot_moved = \ + 9\n\n[preexisting.subtable]\nnew_name = 5\n\n[table]\n" + ); + } +} diff --git a/alacritty/src/window_context.rs b/alacritty/src/window_context.rs index 885d71a4..a450c77d 100644 --- a/alacritty/src/window_context.rs +++ b/alacritty/src/window_context.rs @@ -70,7 +70,7 @@ pub struct WindowContext { master_fd: RawFd, #[cfg(not(windows))] shell_pid: u32, - ipc_config: Vec<(String, serde_yaml::Value)>, + ipc_config: Vec<(String, toml::Value)>, config: Rc<UiConfig>, } @@ -379,8 +379,8 @@ impl WindowContext { }, }; - // Try and parse value as yaml. - match serde_yaml::from_str(value) { + // Try and parse value as toml. + match toml::from_str(value) { Ok(value) => self.ipc_config.push((key.to_owned(), value)), Err(err) => error!( target: LOG_TARGET_IPC_CONFIG, |