Repository: alacritty/alacritty (Rust)

Issue:
CLI -o config overrides should accept aliases as documented
### Expected behavior
As indicated by `man alacritty`, the `-o` option can be used to override configuration from the `alacritty.toml` config file with the properties specified by `man 5 alacritty`. One would therefore expect that
```alacritty -o 'colors.cursor.cursor="CellBackground"'```
leads to an invisible cursor.

### Actual behavior
Instead, it gives the error
```Unable to override option 'colors.cursor.cursor="CellBackground"': Field 'cursor' does not exist```
and no change to the background color is effected.

### Workaround
Inspecting the source code, this appears to stem from the fact that the `colors.cursor` configuration option is an `InvertedCellColors` struct, for which `cursor` is an alias for a property named `background`. Indeed,
```alacritty -o 'colors.cursor.background="CellBackground"'```
yields the desired result. The `-o` overrides should resolve those aliases since only the aliases are used in the documentation.

### Possible cause
Digging a bit deeper, I think the relevant part of the code is the function `derive_deserialize` in `alacritty/alacritty_config_derive/src/config_deserialize/de_struct.rs` which implements `SerdeReplace` without using the result of `fields_deserializer` which includes the substitution of aliases.

### System

OS: Linux 6.7.4-arch1-1
Version: alacritty 0.13.1 (fe2a3c56)
Compositor: sway


Discussion:
The problem is [here](https://github.com/alacritty/alacritty/blob/master/alacritty_config_derive/src/serde_replace.rs#L51).
This is what's generated when I look with `cargo expand`:
```rust
    #[allow(clippy::extra_unused_lifetimes)]
    impl<'de> alacritty_config::SerdeReplace for InvertedCellColors {
        fn replace(
            &mut self,
            value: toml::Value,
        ) -> Result<(), Box<dyn std::error::Error>> {
            match value.as_table() {
                Some(table) => {
                    for (field, next_value) in table {
                        let next_value = next_value.clone();
                        let value = value.clone();
                        match field.as_str() {
                            "foreground" => {
                                alacritty_config::SerdeReplace::replace(
                                    &mut self.foreground,
                                    next_value,
                                )?
                            }
                            "background" => {
                                alacritty_config::SerdeReplace::replace(
                                    &mut self.background,
                                    next_value,
                                )?
                            }
                            _ => {
                                let error = {
                                    let res = ::alloc::fmt::format(
                                        format_args!("Field \"{0}\" does not exist", field),
                                    );
                                    res
                                };
                                return Err(error.into());
                            }
                        }
                    }
                }
                None => *self = serde::Deserialize::deserialize(value)?,
            }
            Ok(())
        }
    }
```

The `alias=cursor` is not used in the replace



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e28eadf0162..58ce9764ff7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -28,6 +28,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Autokey no longer working with alacritty on X11
 - Freeze when moving window between monitors on Xfwm
 - Mouse cursor not changing on Wayland when cursor theme uses legacy cursor icon names
+- Config keys are available under proper names
 
 ### Changed
 
diff --git a/alacritty_config_derive/src/serde_replace.rs b/alacritty_config_derive/src/serde_replace.rs
index 0a703b4c5a5..ddd0cf75767 100644
--- a/alacritty_config_derive/src/serde_replace.rs
+++ b/alacritty_config_derive/src/serde_replace.rs
@@ -44,7 +44,10 @@ pub fn derive_recursive<T>(
 ) -> TokenStream2 {
     let GenericsStreams { unconstrained, constrained, .. } =
         crate::generics_streams(&generics.params);
-    let replace_arms = match_arms(&fields);
+    let replace_arms = match match_arms(&fields) {
+        Err(e) => return e.to_compile_error(),
+        Ok(replace_arms) => replace_arms,
+    };
 
     quote! {
         #[allow(clippy::extra_unused_lifetimes)]
@@ -75,7 +78,7 @@ pub fn derive_recursive<T>(
 }
 
 /// Create SerdeReplace recursive match arms.
-fn match_arms<T>(fields: &Punctuated<Field, T>) -> TokenStream2 {
+fn match_arms<T>(fields: &Punctuated<Field, T>) -> Result<TokenStream2, syn::Error> {
     let mut stream = TokenStream2::default();
     let mut flattened_arm = None;
 
@@ -88,19 +91,42 @@ fn match_arms<T>(fields: &Punctuated<Field, T>) -> TokenStream2 {
         let flatten = field
             .attrs
             .iter()
+            .filter(|attr| (*attr).path().is_ident("config"))
             .filter_map(|attr| attr.parse_args::<Attr>().ok())
             .any(|parsed| parsed.ident.as_str() == "flatten");
 
         if flatten && flattened_arm.is_some() {
-            return Error::new(ident.span(), MULTIPLE_FLATTEN_ERROR).to_compile_error();
+            return Err(Error::new(ident.span(), MULTIPLE_FLATTEN_ERROR));
         } else if flatten {
             flattened_arm = Some(quote! {
                 _ => alacritty_config::SerdeReplace::replace(&mut self.#ident, value)?,
             });
         } else {
+            // Extract all `#[config(alias = "...")]` attribute values.
+            let aliases = field
+                .attrs
+                .iter()
+                .filter(|attr| (*attr).path().is_ident("config"))
+                .filter_map(|attr| attr.parse_args::<Attr>().ok())
+                .filter(|parsed| parsed.ident.as_str() == "alias")
+                .map(|parsed| {
+                    let value = parsed
+                        .param
+                        .ok_or_else(|| format!("Field \"{}\" has no alias value", ident))?
+                        .value();
+
+                    if value.trim().is_empty() {
+                        return Err(format!("Field \"{}\" has an empty alias value", ident));
+                    }
+
+                    Ok(value)
+                })
+                .collect::<Result<Vec<String>, String>>()
+                .map_err(|msg| Error::new(ident.span(), msg))?;
+
             stream.extend(quote! {
-                #literal => alacritty_config::SerdeReplace::replace(&mut self.#ident, next_value)?,
-            });
+                    #(#aliases)|* | #literal => alacritty_config::SerdeReplace::replace(&mut
+            self.#ident, next_value)?,         });
         }
     }
 
@@ -109,5 +135,5 @@ fn match_arms<T>(fields: &Punctuated<Field, T>) -> TokenStream2 {
         stream.extend(flattened_arm);
     }
 
-    stream
+    Ok(stream)
 }


Test patch:
diff --git a/alacritty_config_derive/tests/config.rs b/alacritty_config_derive/tests/config.rs
index 3130bcdae68..27a968ede7b 100644
--- a/alacritty_config_derive/tests/config.rs
+++ b/alacritty_config_derive/tests/config.rs
@@ -23,7 +23,7 @@ impl Default for TestEnum {
 
 #[derive(ConfigDeserialize)]
 struct Test {
-    #[config(alias = "noalias")]
+    #[config(alias = "field1_alias")]
     #[config(deprecated = "use field2 instead")]
     field1: usize,
     #[config(deprecated = "shouldn't be hit")]
@@ -39,6 +39,9 @@ struct Test {
     enom_error: TestEnum,
     #[config(removed = "it's gone")]
     gone: bool,
+    #[config(alias = "multiple_alias1")]
+    #[config(alias = "multiple_alias2")]
+    multiple_alias_field: usize,
 }
 
 impl Default for Test {
@@ -53,6 +56,7 @@ impl Default for Test {
             enom_big: TestEnum::default(),
             enom_error: TestEnum::default(),
             gone: false,
+            multiple_alias_field: 0,
         }
     }
 }
@@ -70,6 +74,7 @@ struct Test2<T: Default> {
 
 #[derive(ConfigDeserialize, Default)]
 struct Test3 {
+    #[config(alias = "flatty_alias")]
     flatty: usize,
 }
 
@@ -189,6 +194,33 @@ fn replace_derive() {
     assert_eq!(test.nesting.newtype, NewType(9));
 }
 
+#[test]
+fn replace_derive_using_alias() {
+    let mut test = Test::default();
+
+    assert_ne!(test.field1, 9);
+
+    let value = toml::from_str("field1_alias=9").unwrap();
+    test.replace(value).unwrap();
+
+    assert_eq!(test.field1, 9);
+}
+
+#[test]
+fn replace_derive_using_multiple_aliases() {
+    let mut test = Test::default();
+
+    let toml_value = toml::from_str("multiple_alias1=6").unwrap();
+    test.replace(toml_value).unwrap();
+
+    assert_eq!(test.multiple_alias_field, 6);
+
+    let toml_value = toml::from_str("multiple_alias1=7").unwrap();
+    test.replace(toml_value).unwrap();
+
+    assert_eq!(test.multiple_alias_field, 7);
+}
+
 #[test]
 fn replace_flatten() {
     let mut test = Test::default();
@@ -198,3 +230,15 @@ fn replace_flatten() {
 
     assert_eq!(test.flatten.flatty, 7);
 }
+
+#[test]
+fn replace_flatten_using_alias() {
+    let mut test = Test::default();
+
+    assert_ne!(test.flatten.flatty, 7);
+
+    let value = toml::from_str("flatty_alias=7").unwrap();
+    test.replace(value).unwrap();
+
+    assert_eq!(test.flatten.flatty, 7);
+}