Browse Source

Replace serde's derive with custom proc macro

This replaces the existing `Deserialize` derive from serde with a
`ConfigDeserialize` derive. The goal of this new proc macro is to allow
a more error-friendly deserialization for the Alacritty configuration
file without having to manage a lot of boilerplate code inside the
configuration modules.

The first part of the derive macro is for struct deserialization. This
takes structs which have `Default` implemented and will only replace
fields which can be successfully deserialized. Otherwise the `log` crate
is used for printing errors. Since this deserialization takes the
default value from the struct instead of the value, it removes the
necessity for creating new types just to implement `Default` on them for
deserialization.

Additionally, the struct deserialization also checks for `Option` values
and makes sure that explicitly specifying `none` as text literal is
allowed for all options.

The other part of the derive macro is responsible for deserializing
enums. While only enums with Unit variants are supported, it will
automatically implement a deserializer for these enums which accepts any
form of capitalization.

Since this custom derive prevents us from using serde's attributes on
fields, some of the attributes have been reimplemented for
`ConfigDeserialize`. These include `#[config(flatten)]`,
`#[config(skip)]` and `#[config(alias = "alias)]`. The flatten attribute
is currently limited to at most one per struct.

Additionally the `#[config(deprecated = "optional message")]` attribute
allows easily defining uniform deprecation messages for fields on
structs.
Christian Duerr 4 months ago
parent
commit
6e1b9d8b25
38 changed files with 1037 additions and 1048 deletions
  1. 3 0
      CHANGELOG.md
  2. 19 4
      Cargo.lock
  3. 1 0
      Cargo.toml
  4. 1 1
      alacritty.yml
  5. 5 1
      alacritty/Cargo.toml
  6. 14 16
      alacritty/src/cli.rs
  7. 10 8
      alacritty/src/config/bindings.rs
  8. 5 36
      alacritty/src/config/debug.rs
  9. 40 113
      alacritty/src/config/font.rs
  10. 2 54
      alacritty/src/config/mod.rs
  11. 10 61
      alacritty/src/config/mouse.rs
  12. 53 76
      alacritty/src/config/ui_config.rs
  13. 91 107
      alacritty/src/config/window.rs
  14. 2 2
      alacritty/src/cursor.rs
  15. 7 9
      alacritty/src/display.rs
  16. 10 11
      alacritty/src/event.rs
  17. 11 30
      alacritty/src/input.rs
  18. 1 1
      alacritty/src/logging.rs
  19. 1 1
      alacritty/src/main.rs
  20. 12 12
      alacritty/src/renderer/mod.rs
  21. 3 3
      alacritty/src/wayland_theme.rs
  22. 5 4
      alacritty/src/window.rs
  23. 21 0
      alacritty_config_derive/Cargo.toml
  24. 1 0
      alacritty_config_derive/LICENSE-APACHE
  25. 23 0
      alacritty_config_derive/LICENSE-MIT
  26. 66 0
      alacritty_config_derive/src/de_enum.rs
  27. 226 0
      alacritty_config_derive/src/de_struct.rs
  28. 27 0
      alacritty_config_derive/src/lib.rs
  29. 155 0
      alacritty_config_derive/tests/config.rs
  30. 4 0
      alacritty_terminal/Cargo.toml
  31. 11 61
      alacritty_terminal/src/config/bell.rs
  32. 79 158
      alacritty_terminal/src/config/colors.rs
  33. 49 175
      alacritty_terminal/src/config/mod.rs
  34. 23 55
      alacritty_terminal/src/config/scrolling.rs
  35. 1 1
      alacritty_terminal/src/event_loop.rs
  36. 7 7
      alacritty_terminal/src/selection.rs
  37. 26 26
      alacritty_terminal/src/term/color.rs
  38. 12 15
      alacritty_terminal/src/term/mod.rs

+ 3 - 0
CHANGELOG.md

@@ -21,6 +21,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 ### Changed
 
 - Nonexistent config imports are ignored instead of raising an error
+- Value for disabling logging with `config.log_level` is `Off` instead of `None`
 
 ### Fixed
 
@@ -51,6 +52,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
     * `--dimensions`
     * `--position`
 - `live-shader-reload` feature
+- Config option `dynamic_title`, you should use `window.dynamic_title` instead
+- Config option `scrolling.faux_multiplier`, which was replaced by escape `CSI ? 1007 h/l`
 
 ## 0.6.0
 

+ 19 - 4
Cargo.lock

@@ -25,6 +25,7 @@ dependencies = [
 name = "alacritty"
 version = "0.7.0-dev"
 dependencies = [
+ "alacritty_config_derive",
  "alacritty_terminal",
  "bitflags",
  "clap",
@@ -53,10 +54,23 @@ dependencies = [
  "xdg",
 ]
 
+[[package]]
+name = "alacritty_config_derive"
+version = "0.1.0"
+dependencies = [
+ "log",
+ "proc-macro2",
+ "quote",
+ "serde",
+ "serde_yaml",
+ "syn",
+]
+
 [[package]]
 name = "alacritty_terminal"
 version = "0.11.1-dev"
 dependencies = [
+ "alacritty_config_derive",
  "base64",
  "bitflags",
  "libc",
@@ -1077,6 +1091,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b"
 dependencies = [
  "cfg-if",
+ "serde",
 ]
 
 [[package]]
@@ -1794,9 +1809,9 @@ dependencies = [
 
 [[package]]
 name = "serde_yaml"
-version = "0.8.13"
+version = "0.8.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae3e2dd40a7cdc18ca80db804b7f461a39bb721160a85c9a1fa30134bf3c02a5"
+checksum = "f7baae0a99f1a324984bcdc5f0718384c1f69775f1c7eec8b859b71b443e3fd7"
 dependencies = [
  "dtoa",
  "linked-hash-map",
@@ -1942,9 +1957,9 @@ checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c"
 
 [[package]]
 name = "syn"
-version = "1.0.46"
+version = "1.0.53"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5ad5de3220ea04da322618ded2c42233d02baca219d6f160a3e9c87cda16c942"
+checksum = "8833e20724c24de12bbaba5ad230ea61c3eafb05b881c7c9d3cfe8638b187e68"
 dependencies = [
  "proc-macro2",
  "quote",

+ 1 - 0
Cargo.toml

@@ -2,6 +2,7 @@
 members = [
     "alacritty",
     "alacritty_terminal",
+    "alacritty_config_derive",
 ]
 
 [profile.release]

+ 1 - 1
alacritty.yml

@@ -799,7 +799,7 @@
   # Log level
   #
   # Values for `log_level`:
-  #   - None
+  #   - Off
   #   - Error
   #   - Warn
   #   - Info

+ 5 - 1
alacritty/Cargo.toml

@@ -13,9 +13,13 @@ path = "../alacritty_terminal"
 version = "0.11.1-dev"
 default-features = false
 
+[dependencies.alacritty_config_derive]
+path = "../alacritty_config_derive"
+version = "0.1.0"
+
 [dependencies]
 clap = "2"
-log = { version = "0.4", features = ["std"] }
+log = { version = "0.4", features = ["std", "serde"] }
 time = "0.1.40"
 fnv = "1"
 serde = { version = "1", features = ["derive"] }

+ 14 - 16
alacritty/src/cli.rs

@@ -230,13 +230,17 @@ impl Options {
 
         config.hold = self.hold;
 
-        let dynamic_title = config.ui_config.dynamic_title() && self.title.is_none();
-        config.ui_config.set_dynamic_title(dynamic_title);
-
-        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());
+        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);
@@ -249,12 +253,6 @@ impl Options {
     }
 }
 
-fn replace_if_some<T>(option: &mut T, value: Option<T>) {
-    if let Some(value) = value {
-        *option = value;
-    }
-}
-
 /// 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());
@@ -289,11 +287,11 @@ mod tests {
     #[test]
     fn dynamic_title_ignoring_options_by_default() {
         let mut config = Config::default();
-        let old_dynamic_title = config.ui_config.dynamic_title();
+        let old_dynamic_title = config.ui_config.window.dynamic_title;
 
         Options::default().override_config(&mut config);
 
-        assert_eq!(old_dynamic_title, config.ui_config.dynamic_title());
+        assert_eq!(old_dynamic_title, config.ui_config.window.dynamic_title);
     }
 
     #[test]
@@ -304,7 +302,7 @@ mod tests {
         options.title = Some("foo".to_owned());
         options.override_config(&mut config);
 
-        assert!(!config.ui_config.dynamic_title());
+        assert!(!config.ui_config.window.dynamic_title);
     }
 
     #[test]
@@ -314,7 +312,7 @@ mod tests {
         config.ui_config.window.title = "foo".to_owned();
         Options::default().override_config(&mut config);
 
-        assert!(config.ui_config.dynamic_title());
+        assert!(config.ui_config.window.dynamic_title);
     }
 
     #[test]

+ 10 - 8
alacritty/src/config/bindings.rs

@@ -10,6 +10,8 @@ use serde::de::{self, MapAccess, Unexpected, Visitor};
 use serde::{Deserialize, Deserializer};
 use serde_yaml::Value as SerdeValue;
 
+use alacritty_config_derive::ConfigDeserialize;
+
 use alacritty_terminal::config::Program;
 use alacritty_terminal::term::TermMode;
 use alacritty_terminal::vi_mode::ViMotion;
@@ -79,26 +81,26 @@ impl<T: Eq> Binding<T> {
     }
 }
 
-#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
+#[derive(ConfigDeserialize, Debug, Clone, PartialEq, Eq)]
 pub enum Action {
     /// Write an escape sequence.
-    #[serde(skip)]
+    #[config(skip)]
     Esc(String),
 
     /// Run given command.
-    #[serde(skip)]
+    #[config(skip)]
     Command(Program),
 
     /// Move vi mode cursor.
-    #[serde(skip)]
+    #[config(skip)]
     ViMotion(ViMotion),
 
     /// Perform vi mode action.
-    #[serde(skip)]
+    #[config(skip)]
     ViAction(ViAction),
 
     /// Perform search mode action.
-    #[serde(skip)]
+    #[config(skip)]
     SearchAction(SearchAction),
 
     /// Paste contents of system clipboard.
@@ -227,7 +229,7 @@ impl Display for Action {
 }
 
 /// Vi mode specific actions.
-#[derive(Deserialize, Debug, Copy, Clone, PartialEq, Eq)]
+#[derive(ConfigDeserialize, Debug, Copy, Clone, PartialEq, Eq)]
 pub enum ViAction {
     /// Toggle normal vi selection.
     ToggleNormalSelection,
@@ -912,7 +914,7 @@ impl<'a> Deserialize<'a> for RawBinding {
                     where
                         E: de::Error,
                     {
-                        match value {
+                        match value.to_ascii_lowercase().as_str() {
                             "key" => Ok(Field::Key),
                             "mods" => Ok(Field::Mods),
                             "mode" => Ok(Field::Mode),

+ 5 - 36
alacritty/src/config/debug.rs

@@ -1,35 +1,29 @@
-use log::{error, LevelFilter};
-use serde::{Deserialize, Deserializer};
+use log::LevelFilter;
 
-use alacritty_terminal::config::{failure_default, LOG_TARGET_CONFIG};
+use alacritty_config_derive::ConfigDeserialize;
 
 /// Debugging options.
-#[serde(default)]
-#[derive(Deserialize, Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
+#[derive(ConfigDeserialize, Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
 pub struct Debug {
-    #[serde(default = "default_log_level", deserialize_with = "deserialize_log_level")]
     pub log_level: LevelFilter,
 
-    #[serde(deserialize_with = "failure_default")]
     pub print_events: bool,
 
     /// Keep the log file after quitting.
-    #[serde(deserialize_with = "failure_default")]
     pub persistent_logging: bool,
 
     /// Should show render timer.
-    #[serde(deserialize_with = "failure_default")]
     pub render_timer: bool,
 
     /// Record ref test.
-    #[serde(skip)]
+    #[config(skip)]
     pub ref_test: bool,
 }
 
 impl Default for Debug {
     fn default() -> Self {
         Self {
-            log_level: default_log_level(),
+            log_level: LevelFilter::Warn,
             print_events: Default::default(),
             persistent_logging: Default::default(),
             render_timer: Default::default(),
@@ -37,28 +31,3 @@ impl Default for Debug {
         }
     }
 }
-
-fn default_log_level() -> LevelFilter {
-    LevelFilter::Warn
-}
-
-fn deserialize_log_level<'a, D>(deserializer: D) -> Result<LevelFilter, D::Error>
-where
-    D: Deserializer<'a>,
-{
-    Ok(match failure_default::<D, String>(deserializer)?.to_lowercase().as_str() {
-        "off" | "none" => LevelFilter::Off,
-        "error" => LevelFilter::Error,
-        "warn" => LevelFilter::Warn,
-        "info" => LevelFilter::Info,
-        "debug" => LevelFilter::Debug,
-        "trace" => LevelFilter::Trace,
-        level => {
-            error!(
-                target: LOG_TARGET_CONFIG,
-                "Problem with config: invalid log level {}; using level Warn", level
-            );
-            default_log_level()
-        },
-    })
-}

+ 40 - 113
alacritty/src/config/font.rs

@@ -1,14 +1,11 @@
 use std::fmt;
 
-use crossfont::Size;
-use log::error;
-use serde::de::Visitor;
+use crossfont::Size as FontSize;
+use serde::de::{self, Visitor};
 use serde::{Deserialize, Deserializer};
 
-use alacritty_terminal::config::{failure_default, LOG_TARGET_CONFIG};
+use alacritty_config_derive::ConfigDeserialize;
 
-#[cfg(target_os = "macos")]
-use crate::config::ui_config::DefaultTrueBool;
 use crate::config::ui_config::Delta;
 
 /// Font config.
@@ -17,62 +14,41 @@ use crate::config::ui_config::Delta;
 /// field in this struct. It might be nice in the future to have defaults for
 /// each value independently. Alternatively, maybe erroring when the user
 /// doesn't provide complete config is Ok.
-#[serde(default)]
-#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
+#[derive(ConfigDeserialize, Default, Debug, Clone, PartialEq, Eq)]
 pub struct Font {
+    /// Extra spacing per character.
+    pub offset: Delta<i8>,
+
+    /// Glyph offset within character cell.
+    pub glyph_offset: Delta<i8>,
+
+    pub use_thin_strokes: bool,
+
     /// Normal font face.
-    #[serde(deserialize_with = "failure_default")]
     normal: FontDescription,
 
     /// Bold font face.
-    #[serde(deserialize_with = "failure_default")]
     bold: SecondaryFontDescription,
 
     /// Italic font face.
-    #[serde(deserialize_with = "failure_default")]
     italic: SecondaryFontDescription,
 
     /// Bold italic font face.
-    #[serde(deserialize_with = "failure_default")]
     bold_italic: SecondaryFontDescription,
 
     /// Font size in points.
-    #[serde(deserialize_with = "DeserializeSize::deserialize")]
-    pub size: Size,
-
-    /// Extra spacing per character.
-    #[serde(deserialize_with = "failure_default")]
-    pub offset: Delta<i8>,
-
-    /// Glyph offset within character cell.
-    #[serde(deserialize_with = "failure_default")]
-    pub glyph_offset: Delta<i8>,
-
-    #[cfg(target_os = "macos")]
-    #[serde(deserialize_with = "failure_default")]
-    use_thin_strokes: DefaultTrueBool,
-}
-
-impl Default for Font {
-    fn default() -> Font {
-        Font {
-            size: default_font_size(),
-            normal: Default::default(),
-            bold: Default::default(),
-            italic: Default::default(),
-            bold_italic: Default::default(),
-            glyph_offset: Default::default(),
-            offset: Default::default(),
-            #[cfg(target_os = "macos")]
-            use_thin_strokes: Default::default(),
-        }
-    }
+    size: Size,
 }
 
 impl Font {
     /// Get a font clone with a size modification.
-    pub fn with_size(self, size: Size) -> Font {
-        Font { size, ..self }
+    pub fn with_size(self, size: FontSize) -> Font {
+        Font { size: Size(size), ..self }
+    }
+
+    #[inline]
+    pub fn size(&self) -> FontSize {
+        self.size.0
     }
 
     /// Get normal font description.
@@ -94,29 +70,12 @@ impl Font {
     pub fn bold_italic(&self) -> FontDescription {
         self.bold_italic.desc(&self.normal)
     }
-
-    #[cfg(target_os = "macos")]
-    pub fn use_thin_strokes(&self) -> bool {
-        self.use_thin_strokes.0
-    }
-
-    #[cfg(not(target_os = "macos"))]
-    pub fn use_thin_strokes(&self) -> bool {
-        false
-    }
-}
-
-fn default_font_size() -> Size {
-    Size::new(11.)
 }
 
 /// Description of the normal font.
-#[serde(default)]
-#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
+#[derive(ConfigDeserialize, Debug, Clone, PartialEq, Eq)]
 pub struct FontDescription {
-    #[serde(deserialize_with = "failure_default")]
     pub family: String,
-    #[serde(deserialize_with = "failure_default")]
     pub style: Option<String>,
 }
 
@@ -135,12 +94,9 @@ impl Default for FontDescription {
 }
 
 /// Description of the italic and bold font.
-#[serde(default)]
-#[derive(Debug, Default, Deserialize, Clone, PartialEq, Eq)]
+#[derive(ConfigDeserialize, Debug, Default, Clone, PartialEq, Eq)]
 pub struct SecondaryFontDescription {
-    #[serde(deserialize_with = "failure_default")]
     family: Option<String>,
-    #[serde(deserialize_with = "failure_default")]
     style: Option<String>,
 }
 
@@ -153,66 +109,37 @@ impl SecondaryFontDescription {
     }
 }
 
-trait DeserializeSize: Sized {
-    fn deserialize<'a, D>(_: D) -> ::std::result::Result<Self, D::Error>
-    where
-        D: serde::de::Deserializer<'a>;
+#[derive(Debug, Clone, PartialEq, Eq)]
+struct Size(FontSize);
+
+impl Default for Size {
+    fn default() -> Self {
+        Self(FontSize::new(11.))
+    }
 }
 
-impl DeserializeSize for Size {
-    fn deserialize<'a, D>(deserializer: D) -> ::std::result::Result<Self, D::Error>
+impl<'de> Deserialize<'de> for Size {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
     where
-        D: serde::de::Deserializer<'a>,
+        D: Deserializer<'de>,
     {
-        use std::marker::PhantomData;
-
-        struct NumVisitor<__D> {
-            _marker: PhantomData<__D>,
-        }
-
-        impl<'a, __D> Visitor<'a> for NumVisitor<__D>
-        where
-            __D: serde::de::Deserializer<'a>,
-        {
-            type Value = f64;
+        struct NumVisitor;
+        impl<'v> Visitor<'v> for NumVisitor {
+            type Value = Size;
 
             fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
                 f.write_str("f64 or u64")
             }
 
-            fn visit_f64<E>(self, value: f64) -> ::std::result::Result<Self::Value, E>
-            where
-                E: ::serde::de::Error,
-            {
-                Ok(value)
+            fn visit_f64<E: de::Error>(self, value: f64) -> Result<Self::Value, E> {
+                Ok(Size(FontSize::new(value as f32)))
             }
 
-            fn visit_u64<E>(self, value: u64) -> ::std::result::Result<Self::Value, E>
-            where
-                E: ::serde::de::Error,
-            {
-                Ok(value as f64)
+            fn visit_u64<E: de::Error>(self, value: u64) -> Result<Self::Value, E> {
+                Ok(Size(FontSize::new(value as f32)))
             }
         }
 
-        let value = serde_yaml::Value::deserialize(deserializer)?;
-        let size = value
-            .deserialize_any(NumVisitor::<D> { _marker: PhantomData })
-            .map(|v| Size::new(v as _));
-
-        // Use default font size as fallback.
-        match size {
-            Ok(size) => Ok(size),
-            Err(err) => {
-                let size = default_font_size();
-                error!(
-                    target: LOG_TARGET_CONFIG,
-                    "Problem with config: {}; using size {}",
-                    err,
-                    size.as_f32_pts()
-                );
-                Ok(size)
-            },
-        }
+        deserializer.deserialize_any(NumVisitor)
     }
 }

+ 2 - 54
alacritty/src/config/mod.rs

@@ -2,7 +2,7 @@ use std::fmt::{self, Display, Formatter};
 use std::path::PathBuf;
 use std::{env, fs, io};
 
-use log::{error, info, warn};
+use log::{error, info};
 use serde::Deserialize;
 use serde_yaml::mapping::Mapping;
 use serde_yaml::Value;
@@ -68,7 +68,7 @@ impl Display for Error {
                 write!(f, "Unable to read $HOME environment variable: {}", err)
             },
             Error::Io(err) => write!(f, "Error reading config file: {}", err),
-            Error::Yaml(err) => write!(f, "Problem with config: {}", err),
+            Error::Yaml(err) => write!(f, "Config error: {}", err),
         }
     }
 }
@@ -157,8 +157,6 @@ fn read_config(path: &PathBuf, cli_config: Value) -> Result<Config> {
     let mut config = Config::deserialize(config_value)?;
     config.ui_config.config_paths = config_paths;
 
-    print_deprecation_warnings(&config);
-
     Ok(config)
 }
 
@@ -287,56 +285,6 @@ 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!(
-            target: LOG_TARGET_CONFIG,
-            "Config scrolling.faux_multiplier is deprecated; the alternate scroll escape can now \
-             be used to disable it and `scrolling.multiplier` controls the number of scrolled \
-             lines"
-        );
-    }
-
-    if config.scrolling.auto_scroll.is_some() {
-        warn!(
-            target: LOG_TARGET_CONFIG,
-            "Config scrolling.auto_scroll has been removed and is now always disabled, it can be \
-             safely removed from the config"
-        );
-    }
-
-    if config.tabspaces.is_some() {
-        warn!(
-            target: LOG_TARGET_CONFIG,
-            "Config tabspaces has been removed and is now always 8, it can be safely removed from \
-             the config"
-        );
-    }
-
-    if config.visual_bell.is_some() {
-        warn!(
-            target: LOG_TARGET_CONFIG,
-            "Config visual_bell has been deprecated; please use bell instead"
-        )
-    }
-
-    if config.ui_config.dynamic_title.is_some() {
-        warn!(
-            target: LOG_TARGET_CONFIG,
-            "Config dynamic_title is deprecated; please use window.dynamic_title instead",
-        )
-    }
-
-    #[cfg(all(windows, not(feature = "winpty")))]
-    if config.winpty_backend {
-        warn!(
-            target: LOG_TARGET_CONFIG,
-            "Config winpty_backend is deprecated and requires a compilation flag; it should be \
-             removed from the config",
-        )
-    }
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;

+ 10 - 61
alacritty/src/config/mouse.rs

@@ -1,35 +1,26 @@
 use std::time::Duration;
 
 use glutin::event::ModifiersState;
-use log::error;
-use serde::{Deserialize, Deserializer};
 
-use alacritty_terminal::config::{failure_default, Program, LOG_TARGET_CONFIG};
+use alacritty_config_derive::ConfigDeserialize;
+use alacritty_terminal::config::Program;
 
 use crate::config::bindings::ModsWrapper;
 
-#[serde(default)]
-#[derive(Default, Clone, Debug, Deserialize, PartialEq, Eq)]
+#[derive(ConfigDeserialize, Default, Clone, Debug, PartialEq, Eq)]
 pub struct Mouse {
-    #[serde(deserialize_with = "failure_default")]
     pub double_click: ClickHandler,
-    #[serde(deserialize_with = "failure_default")]
     pub triple_click: ClickHandler,
-    #[serde(deserialize_with = "failure_default")]
     pub hide_when_typing: bool,
-    #[serde(deserialize_with = "failure_default")]
     pub url: Url,
 }
 
-#[serde(default)]
-#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
+#[derive(ConfigDeserialize, Clone, Debug, PartialEq, Eq)]
 pub struct Url {
     /// Program for opening links.
-    #[serde(deserialize_with = "deserialize_launcher")]
     pub launcher: Option<Program>,
 
     /// Modifier used to open links.
-    #[serde(deserialize_with = "failure_default")]
     modifiers: ModsWrapper,
 }
 
@@ -39,34 +30,6 @@ impl Url {
     }
 }
 
-fn deserialize_launcher<'a, D>(deserializer: D) -> std::result::Result<Option<Program>, D::Error>
-where
-    D: Deserializer<'a>,
-{
-    let default = Url::default().launcher;
-
-    // Deserialize to generic value.
-    let val = serde_yaml::Value::deserialize(deserializer)?;
-
-    // Accept `None` to disable the launcher.
-    if val.as_str().filter(|v| v.to_lowercase() == "none").is_some() {
-        return Ok(None);
-    }
-
-    match <Option<Program>>::deserialize(val) {
-        Ok(launcher) => Ok(launcher),
-        Err(err) => {
-            error!(
-                target: LOG_TARGET_CONFIG,
-                "Problem with config: {}; using {}",
-                err,
-                default.clone().unwrap().program()
-            );
-            Ok(default)
-        },
-    }
-}
-
 impl Default for Url {
     fn default() -> Url {
         Url {
@@ -81,33 +44,19 @@ impl Default for Url {
     }
 }
 
-#[serde(default)]
-#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
+#[derive(ConfigDeserialize, Clone, Debug, PartialEq, Eq)]
 pub struct ClickHandler {
-    #[serde(deserialize_with = "deserialize_duration_ms")]
-    pub threshold: Duration,
+    threshold: u16,
 }
 
 impl Default for ClickHandler {
     fn default() -> Self {
-        ClickHandler { threshold: default_threshold_ms() }
+        Self { threshold: 300 }
     }
 }
 
-fn default_threshold_ms() -> Duration {
-    Duration::from_millis(300)
-}
-
-fn deserialize_duration_ms<'a, D>(deserializer: D) -> ::std::result::Result<Duration, D::Error>
-where
-    D: Deserializer<'a>,
-{
-    let value = serde_yaml::Value::deserialize(deserializer)?;
-    match u64::deserialize(value) {
-        Ok(threshold_ms) => Ok(Duration::from_millis(threshold_ms)),
-        Err(err) => {
-            error!(target: LOG_TARGET_CONFIG, "Problem with config: {}; using default value", err);
-            Ok(default_threshold_ms())
-        },
+impl ClickHandler {
+    pub fn threshold(&self) -> Duration {
+        Duration::from_millis(self.threshold as u64)
     }
 }

+ 53 - 76
alacritty/src/config/ui_config.rs

@@ -3,7 +3,8 @@ use std::path::PathBuf;
 use log::error;
 use serde::{Deserialize, Deserializer};
 
-use alacritty_terminal::config::{failure_default, Percentage, LOG_TARGET_CONFIG};
+use alacritty_config_derive::ConfigDeserialize;
+use alacritty_terminal::config::{Percentage, LOG_TARGET_CONFIG};
 
 use crate::config::bindings::{self, Binding, KeyBinding, MouseBinding};
 use crate::config::debug::Debug;
@@ -11,66 +12,52 @@ use crate::config::font::Font;
 use crate::config::mouse::Mouse;
 use crate::config::window::WindowConfig;
 
-#[derive(Debug, PartialEq, Deserialize)]
+#[derive(ConfigDeserialize, Debug, PartialEq)]
 pub struct UIConfig {
     /// Font configuration.
-    #[serde(default, deserialize_with = "failure_default")]
     pub font: Font,
 
     /// Window configuration.
-    #[serde(default, deserialize_with = "failure_default")]
     pub window: WindowConfig,
 
-    #[serde(default, deserialize_with = "failure_default")]
     pub mouse: Mouse,
 
-    /// Keybindings.
-    #[serde(default = "default_key_bindings", deserialize_with = "deserialize_key_bindings")]
-    pub key_bindings: Vec<KeyBinding>,
-
-    /// Bindings for the mouse.
-    #[serde(default = "default_mouse_bindings", deserialize_with = "deserialize_mouse_bindings")]
-    pub mouse_bindings: Vec<MouseBinding>,
-
     /// Debug options.
-    #[serde(default, deserialize_with = "failure_default")]
     pub debug: Debug,
 
     /// Send escape sequences using the alt key.
-    #[serde(default, deserialize_with = "failure_default")]
-    alt_send_esc: DefaultTrueBool,
+    pub alt_send_esc: bool,
 
     /// Live config reload.
-    #[serde(default, deserialize_with = "failure_default")]
-    live_config_reload: DefaultTrueBool,
-
-    /// Background opacity from 0.0 to 1.0.
-    #[serde(default, deserialize_with = "failure_default")]
-    background_opacity: Percentage,
+    pub live_config_reload: bool,
 
     /// Path where config was loaded from.
-    #[serde(skip)]
+    #[config(skip)]
     pub config_paths: Vec<PathBuf>,
 
-    // TODO: DEPRECATED
-    #[serde(default, deserialize_with = "failure_default")]
-    pub dynamic_title: Option<bool>,
+    /// Keybindings.
+    key_bindings: KeyBindings,
+
+    /// Bindings for the mouse.
+    mouse_bindings: MouseBindings,
+
+    /// Background opacity from 0.0 to 1.0.
+    background_opacity: Percentage,
 }
 
 impl Default for UIConfig {
     fn default() -> Self {
-        UIConfig {
+        Self {
+            alt_send_esc: true,
+            live_config_reload: true,
             font: Default::default(),
             window: Default::default(),
             mouse: Default::default(),
-            key_bindings: default_key_bindings(),
-            mouse_bindings: default_mouse_bindings(),
             debug: Default::default(),
-            alt_send_esc: Default::default(),
-            background_opacity: Default::default(),
-            live_config_reload: Default::default(),
-            dynamic_title: Default::default(),
             config_paths: Default::default(),
+            key_bindings: Default::default(),
+            mouse_bindings: Default::default(),
+            background_opacity: Default::default(),
         }
     }
 }
@@ -82,48 +69,50 @@ impl UIConfig {
     }
 
     #[inline]
-    pub fn dynamic_title(&self) -> bool {
-        self.dynamic_title.unwrap_or_else(|| self.window.dynamic_title())
+    pub fn key_bindings(&self) -> &[KeyBinding] {
+        &self.key_bindings.0.as_slice()
     }
 
     #[inline]
-    pub fn set_dynamic_title(&mut self, dynamic_title: bool) {
-        self.window.set_dynamic_title(dynamic_title);
+    pub fn mouse_bindings(&self) -> &[MouseBinding] {
+        self.mouse_bindings.0.as_slice()
     }
+}
 
-    /// Live config reload.
-    #[inline]
-    pub fn live_config_reload(&self) -> bool {
-        self.live_config_reload.0
-    }
+#[derive(Debug, PartialEq)]
+struct KeyBindings(Vec<KeyBinding>);
 
-    /// Send escape sequences using the alt key.
-    #[inline]
-    pub fn alt_send_esc(&self) -> bool {
-        self.alt_send_esc.0
+impl Default for KeyBindings {
+    fn default() -> Self {
+        Self(bindings::default_key_bindings())
     }
 }
 
-fn default_key_bindings() -> Vec<KeyBinding> {
-    bindings::default_key_bindings()
+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)?))
+    }
 }
 
-fn default_mouse_bindings() -> Vec<MouseBinding> {
-    bindings::default_mouse_bindings()
-}
+#[derive(Debug, PartialEq)]
+struct MouseBindings(Vec<MouseBinding>);
 
-fn deserialize_key_bindings<'a, D>(deserializer: D) -> Result<Vec<KeyBinding>, D::Error>
-where
-    D: Deserializer<'a>,
-{
-    deserialize_bindings(deserializer, bindings::default_key_bindings())
+impl Default for MouseBindings {
+    fn default() -> Self {
+        Self(bindings::default_mouse_bindings())
+    }
 }
 
-fn deserialize_mouse_bindings<'a, D>(deserializer: D) -> Result<Vec<MouseBinding>, D::Error>
-where
-    D: Deserializer<'a>,
-{
-    deserialize_bindings(deserializer, 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(deserialize_bindings(deserializer, Self::default().0)?))
+    }
 }
 
 fn deserialize_bindings<'a, D, T>(
@@ -143,7 +132,7 @@ where
         match Binding::<T>::deserialize(value) {
             Ok(binding) => bindings.push(binding),
             Err(err) => {
-                error!(target: LOG_TARGET_CONFIG, "Problem with config: {}; ignoring binding", err);
+                error!(target: LOG_TARGET_CONFIG, "Config error: {}; ignoring binding", err);
             },
         }
     }
@@ -158,23 +147,11 @@ where
     Ok(bindings)
 }
 
-#[derive(Deserialize, Copy, Clone, Debug, PartialEq, Eq)]
-pub struct DefaultTrueBool(pub bool);
-
-impl Default for DefaultTrueBool {
-    fn default() -> Self {
-        DefaultTrueBool(true)
-    }
-}
-
 /// A delta for a point in a 2 dimensional plane.
-#[serde(default, bound(deserialize = "T: Deserialize<'de> + Default"))]
-#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)]
-pub struct Delta<T: Default + PartialEq + Eq> {
+#[derive(ConfigDeserialize, Clone, Copy, Debug, Default, PartialEq, Eq)]
+pub struct Delta<T: Default> {
     /// Horizontal change.
-    #[serde(deserialize_with = "failure_default")]
     pub x: T,
     /// Vertical change.
-    #[serde(deserialize_with = "failure_default")]
     pub y: T,
 }

+ 91 - 107
alacritty/src/config/window.rs

@@ -1,81 +1,76 @@
+use std::fmt::{self, Formatter};
 use std::os::raw::c_ulong;
 
 use glutin::window::Fullscreen;
 use log::error;
+use serde::de::{self, MapAccess, Visitor};
 use serde::{Deserialize, Deserializer};
-use serde_yaml::Value;
 
-use alacritty_terminal::config::{failure_default, option_explicit_none, LOG_TARGET_CONFIG};
+use alacritty_config_derive::ConfigDeserialize;
+use alacritty_terminal::config::LOG_TARGET_CONFIG;
 use alacritty_terminal::index::{Column, Line};
 
-use crate::config::ui_config::{DefaultTrueBool, Delta};
+use crate::config::ui_config::Delta;
 
 /// Default Alacritty name, used for window title and class.
 pub const DEFAULT_NAME: &str = "Alacritty";
 
-#[serde(default)]
-#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
+#[derive(ConfigDeserialize, Debug, Clone, PartialEq, Eq)]
 pub struct WindowConfig {
     /// Initial position.
-    #[serde(deserialize_with = "failure_default")]
     pub position: Option<Delta<i32>>,
 
     /// Draw the window with title bar / borders.
-    #[serde(deserialize_with = "failure_default")]
     pub decorations: Decorations,
 
     /// Startup mode.
-    #[serde(deserialize_with = "failure_default")]
     pub startup_mode: StartupMode,
 
-    /// Window title.
-    #[serde(default = "default_title")]
-    pub title: String,
-
-    /// Window class.
-    #[serde(deserialize_with = "deserialize_class")]
-    pub class: Class,
-
     /// XEmbed parent.
-    #[serde(skip)]
+    #[config(skip)]
     pub embed: Option<c_ulong>,
 
     /// GTK theme variant.
-    #[serde(deserialize_with = "option_explicit_none")]
     pub gtk_theme_variant: Option<String>,
 
     /// Spread out additional padding evenly.
-    #[serde(deserialize_with = "failure_default")]
     pub dynamic_padding: bool,
 
+    /// Use dynamic title.
+    pub dynamic_title: bool,
+
+    /// Window title.
+    pub title: String,
+
+    /// Window class.
+    pub class: Class,
+
     /// Pixel padding.
-    #[serde(deserialize_with = "failure_default")]
     padding: Delta<u8>,
 
-    /// Use dynamic title.
-    #[serde(default, deserialize_with = "failure_default")]
-    dynamic_title: DefaultTrueBool,
-
     /// Initial dimensions.
-    #[serde(deserialize_with = "failure_default")]
     dimensions: Dimensions,
 }
 
-pub fn default_title() -> String {
-    DEFAULT_NAME.to_string()
+impl Default for WindowConfig {
+    fn default() -> Self {
+        Self {
+            dynamic_title: true,
+            title: DEFAULT_NAME.into(),
+            position: Default::default(),
+            decorations: Default::default(),
+            startup_mode: Default::default(),
+            embed: Default::default(),
+            gtk_theme_variant: Default::default(),
+            dynamic_padding: Default::default(),
+            class: Default::default(),
+            padding: Default::default(),
+            dimensions: Default::default(),
+        }
+    }
 }
 
 impl WindowConfig {
-    #[inline]
-    pub fn dynamic_title(&self) -> bool {
-        self.dynamic_title.0
-    }
-
-    #[inline]
-    pub fn set_dynamic_title(&mut self, dynamic_title: bool) {
-        self.dynamic_title.0 = dynamic_title;
-    }
-
     #[inline]
     pub fn dimensions(&self) -> Option<Dimensions> {
         if self.dimensions.columns.0 != 0
@@ -110,25 +105,7 @@ impl WindowConfig {
     }
 }
 
-impl Default for WindowConfig {
-    fn default() -> WindowConfig {
-        WindowConfig {
-            dimensions: Default::default(),
-            position: Default::default(),
-            padding: Default::default(),
-            decorations: Default::default(),
-            dynamic_padding: Default::default(),
-            startup_mode: Default::default(),
-            class: Default::default(),
-            embed: Default::default(),
-            gtk_theme_variant: Default::default(),
-            title: default_title(),
-            dynamic_title: Default::default(),
-        }
-    }
-}
-
-#[derive(Debug, Deserialize, Copy, Clone, PartialEq, Eq)]
+#[derive(ConfigDeserialize, Debug, Copy, Clone, PartialEq, Eq)]
 pub enum StartupMode {
     Windowed,
     Maximized,
@@ -143,17 +120,13 @@ impl Default for StartupMode {
     }
 }
 
-#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)]
+#[derive(ConfigDeserialize, Debug, Copy, Clone, PartialEq, Eq)]
 pub enum Decorations {
-    #[serde(rename = "full")]
     Full,
     #[cfg(target_os = "macos")]
-    #[serde(rename = "transparent")]
     Transparent,
     #[cfg(target_os = "macos")]
-    #[serde(rename = "buttonless")]
     Buttonless,
-    #[serde(rename = "none")]
     None,
 }
 
@@ -166,71 +139,82 @@ impl Default for Decorations {
 /// Window Dimensions.
 ///
 /// Newtype to avoid passing values incorrectly.
-#[serde(default)]
-#[derive(Default, Debug, Copy, Clone, Deserialize, PartialEq, Eq)]
+#[derive(ConfigDeserialize, Default, Debug, Copy, Clone, PartialEq, Eq)]
 pub struct Dimensions {
     /// Window width in character columns.
-    #[serde(deserialize_with = "failure_default")]
     pub columns: Column,
 
     /// Window Height in character lines.
-    #[serde(deserialize_with = "failure_default")]
     pub lines: Line,
 }
 
 /// Window class hint.
-#[serde(default)]
-#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
+#[derive(Debug, Clone, PartialEq, Eq)]
 pub struct Class {
-    #[serde(deserialize_with = "deserialize_class_resource")]
     pub instance: String,
-
-    #[serde(deserialize_with = "deserialize_class_resource")]
     pub general: String,
 }
 
 impl Default for Class {
     fn default() -> Self {
-        Class { instance: DEFAULT_NAME.into(), general: DEFAULT_NAME.into() }
-    }
-}
-
-fn deserialize_class_resource<'a, D>(deserializer: D) -> Result<String, D::Error>
-where
-    D: Deserializer<'a>,
-{
-    let value = Value::deserialize(deserializer)?;
-    match String::deserialize(value) {
-        Ok(value) => Ok(value),
-        Err(err) => {
-            error!(
-                target: LOG_TARGET_CONFIG,
-                "Problem with config: {}, using default value {}", err, DEFAULT_NAME,
-            );
-
-            Ok(DEFAULT_NAME.into())
-        },
+        Self { instance: DEFAULT_NAME.into(), general: DEFAULT_NAME.into() }
     }
 }
 
-fn deserialize_class<'a, D>(deserializer: D) -> Result<Class, D::Error>
-where
-    D: Deserializer<'a>,
-{
-    let value = Value::deserialize(deserializer)?;
-
-    if let Value::String(instance) = value {
-        return Ok(Class { instance, general: DEFAULT_NAME.into() });
-    }
+impl<'de> Deserialize<'de> for Class {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        struct ClassVisitor;
+        impl<'a> Visitor<'a> for ClassVisitor {
+            type Value = Class;
+
+            fn expecting(&self, f: &mut Formatter<'_>) -> fmt::Result {
+                f.write_str("a mapping")
+            }
+
+            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
+            where
+                E: de::Error,
+            {
+                Ok(Self::Value { instance: value.into(), ..Self::Value::default() })
+            }
+
+            fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
+            where
+                M: MapAccess<'a>,
+            {
+                let mut class = Self::Value::default();
+
+                while let Some((key, value)) = map.next_entry::<String, serde_yaml::Value>()? {
+                    match key.as_str() {
+                        "instance" => match String::deserialize(value) {
+                            Ok(instance) => class.instance = instance,
+                            Err(err) => {
+                                error!(
+                                    target: LOG_TARGET_CONFIG,
+                                    "Config error: class.instance: {}", err
+                                );
+                            },
+                        },
+                        "general" => match String::deserialize(value) {
+                            Ok(general) => class.general = general,
+                            Err(err) => {
+                                error!(
+                                    target: LOG_TARGET_CONFIG,
+                                    "Config error: class.instance: {}", err
+                                );
+                            },
+                        },
+                        _ => (),
+                    }
+                }
+
+                Ok(class)
+            }
+        }
 
-    match Class::deserialize(value) {
-        Ok(value) => Ok(value),
-        Err(err) => {
-            error!(
-                target: LOG_TARGET_CONFIG,
-                "Problem with config: {}; using class {}", err, DEFAULT_NAME
-            );
-            Ok(Class::default())
-        },
+        deserializer.deserialize_any(ClassVisitor)
     }
 }

+ 2 - 2
alacritty/src/cursor.rs

@@ -10,7 +10,7 @@ pub fn get_cursor_glyph(
     offset_x: i8,
     offset_y: i8,
     is_wide: bool,
-    cursor_thickness: f64,
+    cursor_thickness: f32,
 ) -> RasterizedGlyph {
     // Calculate the cell metrics.
     //
@@ -18,7 +18,7 @@ pub fn get_cursor_glyph(
     // https://github.com/rust-lang/rust/commit/14d608f1d8a0b84da5f3bccecb3efb3d35f980dc
     let height = (metrics.line_height + f64::from(offset_y)).max(1.) as usize;
     let mut width = (metrics.average_advance + f64::from(offset_x)).max(1.) as usize;
-    let line_width = (cursor_thickness * width as f64).round().max(1.) as usize;
+    let line_width = (cursor_thickness * width as f32).round().max(1.) as usize;
 
     // Double the cursor width if it's above a double-width glyph.
     if is_wide {

+ 7 - 9
alacritty/src/display.rs

@@ -20,8 +20,6 @@ use unicode_width::UnicodeWidthChar;
 #[cfg(all(feature = "wayland", not(any(target_os = "macos", windows))))]
 use wayland_client::{Display as WaylandDisplay, EventQueue};
 
-#[cfg(target_os = "macos")]
-use crossfont::set_font_smoothing;
 use crossfont::{self, Rasterize, Rasterizer};
 
 use alacritty_terminal::event::{EventListener, OnResize};
@@ -254,7 +252,7 @@ impl Display {
 
         // Set subpixel anti-aliasing.
         #[cfg(target_os = "macos")]
-        set_font_smoothing(config.ui_config.font.use_thin_strokes());
+        crossfont::set_font_smoothing(config.ui_config.font.use_thin_strokes);
 
         #[cfg(all(feature = "x11", not(any(target_os = "macos", windows))))]
         let is_x11 = event_loop.is_x11();
@@ -313,7 +311,7 @@ impl Display {
         config: &Config,
     ) -> Result<(GlyphCache, f32, f32), Error> {
         let font = config.ui_config.font.clone();
-        let rasterizer = Rasterizer::new(dpr as f32, config.ui_config.font.use_thin_strokes())?;
+        let rasterizer = Rasterizer::new(dpr as f32, config.ui_config.font.use_thin_strokes)?;
 
         // Initialize glyph cache.
         let glyph_cache = {
@@ -491,8 +489,8 @@ impl Display {
                             .map_or(false, |viewport_match| viewport_match.contains(&cell_point))
                     {
                         let colors = config.colors.search.focused_match;
-                        let match_fg = colors.foreground().color(cell.fg, cell.bg);
-                        cell.bg = colors.background().color(cell.fg, cell.bg);
+                        let match_fg = colors.foreground.color(cell.fg, cell.bg);
+                        cell.bg = colors.background.color(cell.fg, cell.bg);
                         cell.fg = match_fg;
                         cell.bg_alpha = 1.0;
                     }
@@ -558,8 +556,8 @@ impl Display {
             let y = size_info.cell_height().mul_add(start_line.0 as f32, size_info.padding_y());
 
             let color = match message.ty() {
-                MessageType::Error => config.colors.normal().red,
-                MessageType::Warning => config.colors.normal().yellow,
+                MessageType::Error => config.colors.normal.red,
+                MessageType::Warning => config.colors.normal.yellow,
             };
 
             let message_bar_rect =
@@ -680,7 +678,7 @@ impl Display {
 
         let timing = format!("{:.3} usec", self.meter.average());
         let fg = config.colors.primary.background;
-        let bg = config.colors.normal().red;
+        let bg = config.colors.normal.red;
 
         self.renderer.with_api(&config.ui_config, config.cursor, &size_info, |mut api| {
             api.render_string(glyph_cache, size_info.screen_lines() - 2, &timing[..], fg, Some(bg));

+ 10 - 11
alacritty/src/event.rs

@@ -4,6 +4,7 @@ use std::borrow::Cow;
 use std::cmp::{max, min};
 use std::collections::VecDeque;
 use std::env;
+use std::f32;
 use std::fmt::Debug;
 #[cfg(not(any(target_os = "macos", windows)))]
 use std::fs;
@@ -26,8 +27,6 @@ use glutin::platform::unix::EventLoopWindowTargetExtUnix;
 use log::info;
 use serde_json as json;
 
-#[cfg(target_os = "macos")]
-use crossfont::set_font_smoothing;
 use crossfont::{self, Size};
 
 use alacritty_terminal::config::LOG_TARGET_CONFIG;
@@ -396,7 +395,7 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext<T> for ActionCon
     }
 
     fn reset_font_size(&mut self) {
-        *self.font_size = self.config.ui_config.font.size;
+        *self.font_size = self.config.ui_config.font.size();
         self.display_update_pending.set_font(self.config.ui_config.font.clone());
         self.terminal.dirty = true;
     }
@@ -874,7 +873,7 @@ impl<N: Notify + OnResize> Processor<N> {
             received_count: 0,
             suppress_chars: false,
             modifiers: Default::default(),
-            font_size: config.ui_config.font.size,
+            font_size: config.ui_config.font.size(),
             config,
             message_buffer,
             display,
@@ -1080,13 +1079,13 @@ impl<N: Notify + OnResize> Processor<N> {
                 Event::TerminalEvent(event) => match event {
                     TerminalEvent::Title(title) => {
                         let ui_config = &processor.ctx.config.ui_config;
-                        if ui_config.dynamic_title() {
+                        if ui_config.window.dynamic_title {
                             processor.ctx.window.set_title(&title);
                         }
                     },
                     TerminalEvent::ResetTitle => {
                         let ui_config = &processor.ctx.config.ui_config;
-                        if ui_config.dynamic_title() {
+                        if ui_config.window.dynamic_title {
                             processor.ctx.window.set_title(&ui_config.window.title);
                         }
                     },
@@ -1241,15 +1240,15 @@ impl<N: Notify + OnResize> Processor<N> {
 
         // Reload cursor if its thickness has changed.
         if (processor.ctx.config.cursor.thickness() - config.cursor.thickness()).abs()
-            > std::f64::EPSILON
+            > f32::EPSILON
         {
             processor.ctx.display_update_pending.set_cursor_dirty();
         }
 
         if processor.ctx.config.ui_config.font != config.ui_config.font {
             // Do not update font size if it has been changed at runtime.
-            if *processor.ctx.font_size == processor.ctx.config.ui_config.font.size {
-                *processor.ctx.font_size = config.ui_config.font.size;
+            if *processor.ctx.font_size == processor.ctx.config.ui_config.font.size() {
+                *processor.ctx.font_size = config.ui_config.font.size();
             }
 
             let font = config.ui_config.font.clone().with_size(*processor.ctx.font_size);
@@ -1265,7 +1264,7 @@ impl<N: Notify + OnResize> Processor<N> {
         }
 
         // Live title reload.
-        if !config.ui_config.dynamic_title()
+        if !config.ui_config.window.dynamic_title
             || processor.ctx.config.ui_config.window.title != config.ui_config.window.title
         {
             processor.ctx.window.set_title(&config.ui_config.window.title);
@@ -1278,7 +1277,7 @@ impl<N: Notify + OnResize> Processor<N> {
 
         // Set subpixel anti-aliasing.
         #[cfg(target_os = "macos")]
-        set_font_smoothing(config.ui_config.font.use_thin_strokes());
+        crossfont::set_font_smoothing(config.ui_config.font.use_thin_strokes);
 
         *processor.ctx.config = config;
 

+ 11 - 30
alacritty/src/input.rs

@@ -561,10 +561,10 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
                     self.ctx.mouse_mut().last_click_button = button;
                     ClickState::Click
                 },
-                ClickState::Click if elapsed < mouse_config.double_click.threshold => {
+                ClickState::Click if elapsed < mouse_config.double_click.threshold() => {
                     ClickState::DoubleClick
                 },
-                ClickState::DoubleClick if elapsed < mouse_config.triple_click.threshold => {
+                ClickState::DoubleClick if elapsed < mouse_config.triple_click.threshold() => {
                     ClickState::TripleClick
                 },
                 _ => ClickState::Click,
@@ -714,13 +714,7 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
             .contains(TermMode::ALT_SCREEN | TermMode::ALTERNATE_SCROLL)
             && !self.ctx.modifiers().shift()
         {
-            let multiplier = f64::from(
-                self.ctx
-                    .config()
-                    .scrolling
-                    .faux_multiplier()
-                    .unwrap_or_else(|| self.ctx.config().scrolling.multiplier()),
-            );
+            let multiplier = f64::from(self.ctx.config().scrolling.multiplier);
             self.ctx.mouse_mut().scroll_px += new_scroll_px * multiplier;
 
             let cmd = if new_scroll_px > 0. { b'A' } else { b'B' };
@@ -734,7 +728,7 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
             }
             self.ctx.write_to_pty(content);
         } else {
-            let multiplier = f64::from(self.ctx.config().scrolling.multiplier());
+            let multiplier = f64::from(self.ctx.config().scrolling.multiplier);
             self.ctx.mouse_mut().scroll_px += new_scroll_px * multiplier;
 
             let lines = self.ctx.mouse().scroll_px / height;
@@ -878,7 +872,7 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
             c.encode_utf8(&mut bytes[..]);
         }
 
-        if self.ctx.config().ui_config.alt_send_esc()
+        if self.ctx.config().ui_config.alt_send_esc
             && *self.ctx.received_count() == 0
             && self.ctx.modifiers().alt()
             && utf8_len == 1
@@ -900,8 +894,8 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
         let mods = *self.ctx.modifiers();
         let mut suppress_chars = None;
 
-        for i in 0..self.ctx.config().ui_config.key_bindings.len() {
-            let binding = &self.ctx.config().ui_config.key_bindings[i];
+        for i in 0..self.ctx.config().ui_config.key_bindings().len() {
+            let binding = &self.ctx.config().ui_config.key_bindings()[i];
 
             let key = match (binding.trigger, input.virtual_keycode) {
                 (Key::Scancode(_), _) => Key::Scancode(input.scancode),
@@ -932,8 +926,8 @@ impl<'a, T: EventListener, A: ActionContext<T>> Processor<'a, T, A> {
         let mouse_mode = self.ctx.mouse_mode();
         let mods = *self.ctx.modifiers();
 
-        for i in 0..self.ctx.config().ui_config.mouse_bindings.len() {
-            let mut binding = self.ctx.config().ui_config.mouse_bindings[i].clone();
+        for i in 0..self.ctx.config().ui_config.mouse_bindings().len() {
+            let mut binding = self.ctx.config().ui_config.mouse_bindings()[i].clone();
 
             // Require shift for all modifiers when mouse mode is active.
             if mouse_mode {
@@ -1072,7 +1066,6 @@ mod tests {
     use alacritty_terminal::event::Event as TerminalEvent;
     use alacritty_terminal::selection::Selection;
 
-    use crate::config::ClickHandler;
     use crate::message_bar::MessageBuffer;
 
     const KEY: VirtualKeyCode = VirtualKeyCode::Key0;
@@ -1251,18 +1244,8 @@ mod tests {
         } => {
             #[test]
             fn $name() {
-                let mut cfg = Config::default();
-                cfg.ui_config.mouse = crate::config::Mouse {
-                    double_click: ClickHandler {
-                        threshold: Duration::from_millis(1000),
-                    },
-                    triple_click: ClickHandler {
-                        threshold: Duration::from_millis(1000),
-                    },
-                    hide_when_typing: false,
-                    url: Default::default(),
-                };
-
+                let mut clipboard = Clipboard::new_nop();
+                let cfg = Config::default();
                 let size = SizeInfo::new(
                     21.0,
                     51.0,
@@ -1273,8 +1256,6 @@ mod tests {
                     false,
                 );
 
-                let mut clipboard = Clipboard::new_nop();
-
                 let mut terminal = Term::new(&cfg, size, MockEventProxy);
 
                 let mut mouse = Mouse::default();

+ 1 - 1
alacritty/src/logging.rs

@@ -23,7 +23,7 @@ use crate::message_bar::{Message, MessageType};
 const ALACRITTY_LOG_ENV: &str = "ALACRITTY_LOG";
 /// List of targets which will be logged by Alacritty.
 const ALLOWED_TARGETS: [&str; 4] =
-    ["alacritty_terminal", "alacritty_config", "alacritty", "crossfont"];
+    ["alacritty_terminal", "alacritty_config_derive", "alacritty", "crossfont"];
 
 pub fn initialize(
     options: &Options,

+ 1 - 1
alacritty/src/main.rs

@@ -186,7 +186,7 @@ fn run(
     //
     // The monitor watches the config file for changes and reloads it. Pending
     // config changes are processed in the main loop.
-    if config.ui_config.live_config_reload() {
+    if config.ui_config.live_config_reload {
         monitor::watch(config.ui_config.config_paths.clone(), event_proxy);
     }
 

+ 12 - 12
alacritty/src/renderer/mod.rs

@@ -156,15 +156,15 @@ impl GlyphCache {
         // Need to load at least one glyph for the face before calling metrics.
         // The glyph requested here ('m' at the time of writing) has no special
         // meaning.
-        rasterizer.get_glyph(GlyphKey { font_key: regular, c: 'm', size: font.size })?;
+        rasterizer.get_glyph(GlyphKey { font_key: regular, c: 'm', size: font.size() })?;
 
-        let metrics = rasterizer.metrics(regular, font.size)?;
+        let metrics = rasterizer.metrics(regular, font.size())?;
 
         let mut cache = Self {
             cache: HashMap::default(),
             cursor_cache: HashMap::default(),
             rasterizer,
-            font_size: font.size,
+            font_size: font.size(),
             font_key: regular,
             bold_key: bold,
             italic_key: italic,
@@ -190,7 +190,7 @@ impl GlyphCache {
         font: &Font,
         rasterizer: &mut Rasterizer,
     ) -> Result<(FontKey, FontKey, FontKey, FontKey), crossfont::Error> {
-        let size = font.size;
+        let size = font.size();
 
         // Load regular font.
         let regular_desc = Self::make_desc(&font.normal(), Slant::Normal, Weight::Normal);
@@ -291,12 +291,12 @@ impl GlyphCache {
         let (regular, bold, italic, bold_italic) =
             Self::compute_font_keys(font, &mut self.rasterizer)?;
 
-        self.rasterizer.get_glyph(GlyphKey { font_key: regular, c: 'm', size: font.size })?;
-        let metrics = self.rasterizer.metrics(regular, font.size)?;
+        self.rasterizer.get_glyph(GlyphKey { font_key: regular, c: 'm', size: font.size() })?;
+        let metrics = self.rasterizer.metrics(regular, font.size())?;
 
-        info!("Font size changed to {:?} with DPR of {}", font.size, dpr);
+        info!("Font size changed to {:?} with DPR of {}", font.size(), dpr);
 
-        self.font_size = font.size;
+        self.font_size = font.size();
         self.font_key = regular;
         self.bold_key = bold;
         self.italic_key = italic;
@@ -322,12 +322,12 @@ impl GlyphCache {
 
     /// Calculate font metrics without access to a glyph cache.
     pub fn static_metrics(font: Font, dpr: f64) -> Result<crossfont::Metrics, crossfont::Error> {
-        let mut rasterizer = crossfont::Rasterizer::new(dpr as f32, font.use_thin_strokes())?;
+        let mut rasterizer = crossfont::Rasterizer::new(dpr as f32, font.use_thin_strokes)?;
         let regular_desc = GlyphCache::make_desc(&font.normal(), Slant::Normal, Weight::Normal);
-        let regular = Self::load_regular_font(&mut rasterizer, &regular_desc, font.size)?;
-        rasterizer.get_glyph(GlyphKey { font_key: regular, c: 'm', size: font.size })?;
+        let regular = Self::load_regular_font(&mut rasterizer, &regular_desc, font.size())?;
+        rasterizer.get_glyph(GlyphKey { font_key: regular, c: 'm', size: font.size() })?;
 
-        rasterizer.metrics(regular, font.size)
+        rasterizer.metrics(regular, font.size())
     }
 }
 

+ 3 - 3
alacritty/src/wayland_theme.rs

@@ -17,9 +17,9 @@ pub struct AlacrittyWaylandTheme {
 
 impl AlacrittyWaylandTheme {
     pub fn new(colors: &Colors) -> Self {
-        let hovered_close_icon = colors.normal().red.into_rgba();
-        let hovered_maximize_icon = colors.normal().green.into_rgba();
-        let hovered_minimize_icon = colors.normal().yellow.into_rgba();
+        let hovered_close_icon = colors.normal.red.into_rgba();
+        let hovered_maximize_icon = colors.normal.green.into_rgba();
+        let hovered_minimize_icon = colors.normal.yellow.into_rgba();
         let foreground = colors.search_bar_foreground().into_rgba();
         let background = colors.search_bar_background().into_rgba();
 

+ 5 - 4
alacritty/src/window.rs

@@ -271,8 +271,6 @@ impl Window {
             Icon::from_rgba(buf, info.width, info.height)
         };
 
-        let class = &window_config.class;
-
         let builder = WindowBuilder::new()
             .with_title(title)
             .with_visible(false)
@@ -285,10 +283,13 @@ impl Window {
         let builder = builder.with_window_icon(icon.ok());
 
         #[cfg(feature = "wayland")]
-        let builder = builder.with_app_id(class.instance.clone());
+        let builder = builder.with_app_id(window_config.class.instance.to_owned());
 
         #[cfg(feature = "x11")]
-        let builder = builder.with_class(class.instance.clone(), class.general.clone());
+        let builder = builder.with_class(
+            window_config.class.instance.to_owned(),
+            window_config.class.general.to_owned(),
+        );
 
         #[cfg(feature = "x11")]
         let builder = match &window_config.gtk_theme_variant {

+ 21 - 0
alacritty_config_derive/Cargo.toml

@@ -0,0 +1,21 @@
+[package]
+name = "alacritty_config_derive"
+version = "0.1.0"
+authors = ["Christian Duerr <contact@christianduerr.com>"]
+license = "MIT/Apache-2.0"
+description = "Failure resistant deserialization derive"
+homepage = "https://github.com/alacritty/alacritty"
+edition = "2018"
+
+[lib]
+proc-macro = true
+
+[dependencies]
+syn = { version = "1.0.53", features = ["derive", "parsing", "proc-macro", "printing"], default-features = false }
+proc-macro2 = "1.0.24"
+quote = "1.0.7"
+
+[dev-dependencies]
+serde_yaml = "0.8.14"
+serde = "1.0.117"
+log = "0.4.11"

+ 1 - 0
alacritty_config_derive/LICENSE-APACHE

@@ -0,0 +1 @@
+../LICENSE-APACHE

+ 23 - 0
alacritty_config_derive/LICENSE-MIT

@@ -0,0 +1,23 @@
+Permission is hereby granted, free of charge, to any
+person obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the
+Software without restriction, including without
+limitation the rights to use, copy, modify, merge,
+publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software
+is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice
+shall be included in all copies or substantial portions
+of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.

+ 66 - 0
alacritty_config_derive/src/de_enum.rs

@@ -0,0 +1,66 @@
+use proc_macro::TokenStream;
+use proc_macro2::TokenStream as TokenStream2;
+use quote::{format_ident, quote};
+use syn::{DataEnum, Ident};
+
+pub fn derive_deserialize(ident: Ident, data_enum: DataEnum) -> TokenStream {
+    let visitor = format_ident!("{}Visitor", ident);
+
+    // Create match arm streams and get a list with all available values.
+    let mut match_arms_stream = TokenStream2::new();
+    let mut available_values = String::from("one of ");
+    for variant in data_enum.variants.iter().filter(|variant| {
+        // Skip deserialization for `#[config(skip)]` fields.
+        variant.attrs.iter().all(|attr| {
+            !crate::path_ends_with(&attr.path, "config") || attr.tokens.to_string() != "(skip)"
+        })
+    }) {
+        let variant_ident = &variant.ident;
+        let variant_str = variant_ident.to_string();
+        available_values = format!("{}`{}`, ", available_values, variant_str);
+
+        let literal = variant_str.to_lowercase();
+
+        match_arms_stream.extend(quote! {
+            #literal => Ok(#ident :: #variant_ident),
+        });
+    }
+
+    // Remove trailing `, ` from the last enum variant.
+    available_values.truncate(available_values.len().saturating_sub(2));
+
+    // Generate deserialization impl.
+    let tokens = quote! {
+        struct #visitor;
+        impl<'de> serde::de::Visitor<'de> for #visitor {
+            type Value = #ident;
+
+            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
+                formatter.write_str(#available_values)
+            }
+
+            fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
+            where
+                E: serde::de::Error,
+            {
+                match s.to_lowercase().as_str() {
+                    #match_arms_stream
+                    _ => Err(E::custom(
+                            &format!("unknown variant `{}`, expected {}", s, #available_values)
+                    )),
+                }
+            }
+        }
+
+        impl<'de> serde::Deserialize<'de> for #ident {
+            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+            where
+                D: serde::Deserializer<'de>,
+            {
+                deserializer.deserialize_str(#visitor)
+            }
+        }
+    };
+
+    tokens.into()
+}

+ 226 - 0
alacritty_config_derive/src/de_struct.rs

@@ -0,0 +1,226 @@
+use proc_macro::TokenStream;
+use proc_macro2::TokenStream as TokenStream2;
+use quote::{format_ident, quote};
+use syn::parse::{self, Parse, ParseStream};
+use syn::punctuated::Punctuated;
+use syn::spanned::Spanned;
+use syn::{Error, Field, GenericParam, Generics, Ident, LitStr, Token, Type, TypeParam};
+
+/// Error message when attempting to flatten multiple fields.
+const MULTIPLE_FLATTEN_ERROR: &str = "At most one instance of #[config(flatten)] is supported";
+/// Use this crate's name as log target.
+const LOG_TARGET: &str = env!("CARGO_PKG_NAME");
+
+pub fn derive_deserialize<T>(
+    ident: Ident,
+    generics: Generics,
+    fields: Punctuated<Field, T>,
+) -> TokenStream {
+    // Create all necessary tokens for the implementation.
+    let GenericsStreams { unconstrained, constrained, phantoms } =
+        generics_streams(generics.params);
+    let FieldStreams { flatten, match_assignments } = fields_deserializer(&fields);
+    let visitor = format_ident!("{}Visitor", ident);
+
+    // Generate deserialization impl.
+    let tokens = quote! {
+        #[derive(Default)]
+        #[allow(non_snake_case)]
+        struct #visitor < #unconstrained > {
+            #phantoms
+        }
+
+        impl<'de, #constrained> serde::de::Visitor<'de> for #visitor < #unconstrained > {
+            type Value = #ident < #unconstrained >;
+
+            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
+                formatter.write_str("a mapping")
+            }
+
+            fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
+            where
+                M: serde::de::MapAccess<'de>,
+            {
+                let mut config = Self::Value::default();
+
+                // NOTE: This could be used to print unused keys.
+                let mut unused = serde_yaml::Mapping::new();
+
+                while let Some((key, value)) = map.next_entry::<String, serde_yaml::Value>()? {
+                    match key.as_str() {
+                        #match_assignments
+                        _ => {
+                            unused.insert(serde_yaml::Value::String(key), value);
+                        },
+                    }
+                }
+
+                #flatten
+
+                Ok(config)
+            }
+        }
+
+        impl<'de, #constrained> serde::Deserialize<'de> for #ident < #unconstrained > {
+            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+            where
+                D: serde::Deserializer<'de>,
+            {
+                deserializer.deserialize_map(#visitor :: default())
+            }
+        }
+    };
+
+    tokens.into()
+}
+
+// Token streams created from the fields in the struct.
+#[derive(Default)]
+struct FieldStreams {
+    match_assignments: TokenStream2,
+    flatten: TokenStream2,
+}
+
+/// Create the deserializers for match arms and flattened fields.
+fn fields_deserializer<T>(fields: &Punctuated<Field, T>) -> FieldStreams {
+    let mut field_streams = FieldStreams::default();
+
+    // Create the deserialization stream for each field.
+    for field in fields.iter() {
+        if let Err(err) = field_deserializer(&mut field_streams, field) {
+            field_streams.flatten = err.to_compile_error();
+            return field_streams;
+        }
+    }
+
+    field_streams
+}
+
+/// Append a single field deserializer to the stream.
+fn field_deserializer(field_streams: &mut FieldStreams, field: &Field) -> Result<(), Error> {
+    let ident = field.ident.as_ref().expect("unreachable tuple struct");
+    let literal = ident.to_string();
+    let mut literals = vec![literal.clone()];
+
+    // Create default stream for deserializing fields.
+    let mut match_assignment_stream = quote! {
+        match serde::Deserialize::deserialize(value) {
+            Ok(value) => config.#ident = value,
+            Err(err) => {
+                log::error!(target: #LOG_TARGET, "Config error: {}: {}", #literal, err);
+            },
+        }
+    };
+
+    // Iterate over all #[config(...)] attributes.
+    for attr in field.attrs.iter().filter(|attr| crate::path_ends_with(&attr.path, "config")) {
+        let parsed = match attr.parse_args::<Attr>() {
+            Ok(parsed) => parsed,
+            Err(_) => continue,
+        };
+
+        match parsed.ident.as_str() {
+            // Skip deserialization for `#[config(skip)]` fields.
+            "skip" => return Ok(()),
+            "flatten" => {
+                // NOTE: Currently only a single instance of flatten is supported per struct
+                // for complexity reasons.
+                if !field_streams.flatten.is_empty() {
+                    return Err(Error::new(attr.span(), MULTIPLE_FLATTEN_ERROR));
+                }
+
+                // Create the tokens to deserialize the flattened struct from the unused fields.
+                field_streams.flatten.extend(quote! {
+                    let unused = serde_yaml::Value::Mapping(unused);
+                    config.#ident = serde::Deserialize::deserialize(unused).unwrap_or_default();
+                });
+            },
+            "deprecated" => {
+                // Construct deprecation message and append optional attribute override.
+                let mut message = format!("Config warning: {} is deprecated", literal);
+                if let Some(warning) = parsed.param {
+                    message = format!("{}; {}", message, warning.value());
+                }
+
+                // Append stream to log deprecation warning.
+                match_assignment_stream.extend(quote! {
+                    log::warn!(target: #LOG_TARGET, #message);
+                });
+            },
+            // Add aliases to match pattern.
+            "alias" => {
+                if let Some(alias) = parsed.param {
+                    literals.push(alias.value());
+                }
+            },
+            _ => (),
+        }
+    }
+
+    // Create token stream for deserializing "none" string into `Option<T>`.
+    if let Type::Path(type_path) = &field.ty {
+        if crate::path_ends_with(&type_path.path, "Option") {
+            match_assignment_stream = quote! {
+                if value.as_str().map_or(false, |s| s.eq_ignore_ascii_case("none")) {
+                    config.#ident = None;
+                    continue;
+                }
+                #match_assignment_stream
+            };
+        }
+    }
+
+    // Create the token stream for deserialization and error handling.
+    field_streams.match_assignments.extend(quote! {
+        #(#literals)|* => { #match_assignment_stream },
+    });
+
+    Ok(())
+}
+
+/// Field attribute.
+struct Attr {
+    ident: String,
+    param: Option<LitStr>,
+}
+
+impl Parse for Attr {
+    fn parse(input: ParseStream) -> parse::Result<Self> {
+        let ident = input.parse::<Ident>()?.to_string();
+        let param = input.parse::<Token![=]>().and_then(|_| input.parse()).ok();
+        Ok(Self { ident, param })
+    }
+}
+
+/// Storage for all necessary generics information.
+#[derive(Default)]
+struct GenericsStreams {
+    unconstrained: TokenStream2,
+    constrained: TokenStream2,
+    phantoms: TokenStream2,
+}
+
+/// Create the necessary generics annotations.
+///
+/// This will create three different token streams, which might look like this:
+///  - unconstrained: `T`
+///  - constrained: `T: Default + Deserialize<'de>`
+///  - phantoms: `T: PhantomData<T>,`
+fn generics_streams<T>(params: Punctuated<GenericParam, T>) -> GenericsStreams {
+    let mut generics = GenericsStreams::default();
+
+    for generic in params {
+        // NOTE: Lifetimes and const params are not supported.
+        if let GenericParam::Type(TypeParam { ident, .. }) = generic {
+            generics.unconstrained.extend(quote!( #ident , ));
+            generics.constrained.extend(quote! {
+                #ident : Default + serde::Deserialize<'de> ,
+            });
+            generics.phantoms.extend(quote! {
+                #ident : std::marker::PhantomData < #ident >,
+            });
+        }
+    }
+
+    generics
+}

+ 27 - 0
alacritty_config_derive/src/lib.rs

@@ -0,0 +1,27 @@
+use proc_macro::TokenStream;
+use syn::{parse_macro_input, Data, DataStruct, DeriveInput, Error, Fields, Path};
+
+mod de_enum;
+mod de_struct;
+
+/// Error if the derive was used on an unsupported type.
+const UNSUPPORTED_ERROR: &str = "ConfigDeserialize must be used on a struct with fields";
+
+#[proc_macro_derive(ConfigDeserialize, attributes(config))]
+pub fn derive_config_deserialize(input: TokenStream) -> TokenStream {
+    let input = parse_macro_input!(input as DeriveInput);
+
+    match input.data {
+        Data::Struct(DataStruct { fields: Fields::Named(fields), .. }) => {
+            de_struct::derive_deserialize(input.ident, input.generics, fields.named)
+        },
+        Data::Enum(data_enum) => de_enum::derive_deserialize(input.ident, data_enum),
+        _ => Error::new(input.ident.span(), UNSUPPORTED_ERROR).to_compile_error().into(),
+    }
+}
+
+/// Verify that a token path ends with a specific segment.
+pub(crate) fn path_ends_with(path: &Path, segment: &str) -> bool {
+    let segments = path.segments.iter();
+    segments.last().map_or(false, |s| s.ident == segment)
+}

+ 155 - 0
alacritty_config_derive/tests/config.rs

@@ -0,0 +1,155 @@
+use std::sync::{Arc, Mutex};
+
+use log::{Level, Log, Metadata, Record};
+
+use alacritty_config_derive::ConfigDeserialize;
+
+#[derive(ConfigDeserialize, Debug, PartialEq, Eq)]
+enum TestEnum {
+    One,
+    Two,
+    Three,
+    #[config(skip)]
+    Nine(String),
+}
+
+impl Default for TestEnum {
+    fn default() -> Self {
+        Self::Nine(String::from("nine"))
+    }
+}
+
+#[derive(ConfigDeserialize)]
+struct Test {
+    #[config(alias = "noalias")]
+    #[config(deprecated = "use field2 instead")]
+    field1: usize,
+    #[config(deprecated = "shouldn't be hit")]
+    field2: String,
+    field3: Option<u8>,
+    #[doc("aaa")]
+    nesting: Test2<usize>,
+    #[config(flatten)]
+    flatten: Test3,
+    enom_small: TestEnum,
+    enom_big: TestEnum,
+    #[config(deprecated)]
+    enom_error: TestEnum,
+}
+
+impl Default for Test {
+    fn default() -> Self {
+        Self {
+            field1: 13,
+            field2: String::from("field2"),
+            field3: Some(23),
+            nesting: Test2::default(),
+            flatten: Test3::default(),
+            enom_small: TestEnum::default(),
+            enom_big: TestEnum::default(),
+            enom_error: TestEnum::default(),
+        }
+    }
+}
+
+#[derive(ConfigDeserialize, Default)]
+struct Test2<T: Default> {
+    field1: T,
+    field2: Option<usize>,
+    #[config(skip)]
+    field3: usize,
+    #[config(alias = "aliased")]
+    field4: u8,
+}
+
+#[derive(ConfigDeserialize, Default)]
+struct Test3 {
+    flatty: usize,
+}
+
+#[test]
+fn config_deserialize() {
+    let logger = unsafe {
+        LOGGER = Some(Logger::default());
+        LOGGER.as_mut().unwrap()
+    };
+
+    log::set_logger(logger).unwrap();
+    log::set_max_level(log::LevelFilter::Warn);
+
+    let test: Test = serde_yaml::from_str(
+        r#"
+        field1: 3
+        field3: 32
+        nesting:
+          field1: "testing"
+          field2: None
+          field3: 99
+          aliased: 8
+        flatty: 123
+        enom_small: "one"
+        enom_big: "THREE"
+        enom_error: "HugaBuga"
+    "#,
+    )
+    .unwrap();
+
+    // Verify fields were deserialized correctly.
+    assert_eq!(test.field1, 3);
+    assert_eq!(test.field2, Test::default().field2);
+    assert_eq!(test.field3, Some(32));
+    assert_eq!(test.enom_small, TestEnum::One);
+    assert_eq!(test.enom_big, TestEnum::Three);
+    assert_eq!(test.enom_error, Test::default().enom_error);
+    assert_eq!(test.nesting.field1, Test::default().nesting.field1);
+    assert_eq!(test.nesting.field2, None);
+    assert_eq!(test.nesting.field3, Test::default().nesting.field3);
+    assert_eq!(test.nesting.field4, 8);
+    assert_eq!(test.flatten.flatty, 123);
+
+    // Verify all log messages are correct.
+    let error_logs = logger.error_logs.lock().unwrap();
+    assert_eq!(error_logs.as_slice(), [
+        "Config error: field1: invalid type: string \"testing\", expected usize",
+        "Config error: enom_error: unknown variant `HugaBuga`, expected one of `One`, `Two`, \
+         `Three`",
+    ]);
+    let warn_logs = logger.warn_logs.lock().unwrap();
+    assert_eq!(warn_logs.as_slice(), [
+        "Config warning: field1 is deprecated; use field2 instead",
+        "Config warning: enom_error is deprecated",
+    ]);
+}
+
+static mut LOGGER: Option<Logger> = None;
+
+/// Logger storing all messages for later validation.
+#[derive(Default)]
+struct Logger {
+    error_logs: Arc<Mutex<Vec<String>>>,
+    warn_logs: Arc<Mutex<Vec<String>>>,
+}
+
+impl Log for Logger {
+    fn log(&self, record: &Record) {
+        assert_eq!(record.target(), env!("CARGO_PKG_NAME"));
+
+        match record.level() {
+            Level::Error => {
+                let mut error_logs = self.error_logs.lock().unwrap();
+                error_logs.push(record.args().to_string());
+            },
+            Level::Warn => {
+                let mut warn_logs = self.warn_logs.lock().unwrap();
+                warn_logs.push(record.args().to_string());
+            },
+            _ => unreachable!(),
+        }
+    }
+
+    fn enabled(&self, _metadata: &Metadata) -> bool {
+        true
+    }
+
+    fn flush(&self) {}
+}

+ 4 - 0
alacritty_terminal/Cargo.toml

@@ -8,6 +8,10 @@ readme = "../README.md"
 homepage = "https://github.com/alacritty/alacritty"
 edition = "2018"
 
+[dependencies.alacritty_config_derive]
+path = "../alacritty_config_derive"
+version = "0.1.0"
+
 [dependencies]
 libc = "0.2"
 bitflags = "1"

+ 11 - 61
alacritty_terminal/src/config/bell.rs

@@ -1,95 +1,45 @@
 use std::time::Duration;
 
-use log::error;
-use serde::{Deserialize, Deserializer};
-use serde_yaml::Value;
+use alacritty_config_derive::ConfigDeserialize;
 
-use crate::config::{failure_default, Program, LOG_TARGET_CONFIG};
+use crate::config::Program;
 use crate::term::color::Rgb;
 
-const DEFAULT_BELL_COLOR: Rgb = Rgb { r: 255, g: 255, b: 255 };
-
-#[serde(default)]
-#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
+#[derive(ConfigDeserialize, Clone, Debug, PartialEq, Eq)]
 pub struct BellConfig {
     /// Visual bell animation function.
-    #[serde(deserialize_with = "failure_default")]
     pub animation: BellAnimation,
 
-    /// Visual bell duration in milliseconds.
-    #[serde(deserialize_with = "failure_default")]
-    duration: u16,
+    /// Command to run on bell.
+    pub command: Option<Program>,
 
     /// Visual bell flash color.
-    #[serde(deserialize_with = "deserialize_bell_color")]
     pub color: Rgb,
 
-    /// Command to run on bell.
-    #[serde(deserialize_with = "deserialize_bell_command")]
-    pub command: Option<Program>,
+    /// Visual bell duration in milliseconds.
+    duration: u16,
 }
 
 impl Default for BellConfig {
     fn default() -> Self {
         Self {
+            color: Rgb { r: 255, g: 255, b: 255 },
             animation: Default::default(),
-            duration: Default::default(),
             command: Default::default(),
-            color: DEFAULT_BELL_COLOR,
+            duration: Default::default(),
         }
     }
 }
 
 impl BellConfig {
-    /// Visual bell duration in milliseconds.
-    #[inline]
     pub fn duration(&self) -> Duration {
-        Duration::from_millis(u64::from(self.duration))
-    }
-}
-
-fn deserialize_bell_color<'a, D>(deserializer: D) -> Result<Rgb, D::Error>
-where
-    D: Deserializer<'a>,
-{
-    let value = Value::deserialize(deserializer)?;
-    match Rgb::deserialize(value) {
-        Ok(value) => Ok(value),
-        Err(err) => {
-            error!(
-                target: LOG_TARGET_CONFIG,
-                "Problem with config: {}, using default color value {}", err, DEFAULT_BELL_COLOR
-            );
-
-            Ok(DEFAULT_BELL_COLOR)
-        },
-    }
-}
-
-fn deserialize_bell_command<'a, D>(deserializer: D) -> Result<Option<Program>, D::Error>
-where
-    D: Deserializer<'a>,
-{
-    // Deserialize to generic value.
-    let val = Value::deserialize(deserializer)?;
-
-    // Accept `None` to disable the bell command.
-    if val.as_str().filter(|v| v.to_lowercase() == "none").is_some() {
-        return Ok(None);
-    }
-
-    match Program::deserialize(val) {
-        Ok(command) => Ok(Some(command)),
-        Err(err) => {
-            error!(target: LOG_TARGET_CONFIG, "Problem with config: {}; ignoring field", err);
-            Ok(None)
-        },
+        Duration::from_millis(self.duration as u64)
     }
 }
 
 /// `VisualBellAnimations` are modeled after a subset of CSS transitions and Robert
 /// Penner's Easing Functions.
-#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
+#[derive(ConfigDeserialize, Clone, Copy, Debug, PartialEq, Eq)]
 pub enum BellAnimation {
     // CSS animation.
     Ease,

+ 79 - 158
alacritty_terminal/src/config/colors.rs

@@ -1,42 +1,24 @@
-use log::error;
+use serde::de::Error as SerdeError;
 use serde::{Deserialize, Deserializer};
-use serde_yaml::Value;
 
-use crate::config::{failure_default, LOG_TARGET_CONFIG};
+use alacritty_config_derive::ConfigDeserialize;
+
 use crate::term::color::{CellRgb, Rgb};
 
-#[serde(default)]
-#[derive(Deserialize, Clone, Debug, Default, PartialEq, Eq)]
+#[derive(ConfigDeserialize, Clone, Debug, Default, PartialEq, Eq)]
 pub struct Colors {
-    #[serde(deserialize_with = "failure_default")]
     pub primary: PrimaryColors,
-    #[serde(deserialize_with = "failure_default")]
-    pub cursor: CursorColors,
-    #[serde(deserialize_with = "failure_default")]
-    pub vi_mode_cursor: CursorColors,
-    #[serde(deserialize_with = "failure_default")]
+    pub cursor: InvertedCellColors,
+    pub vi_mode_cursor: InvertedCellColors,
     pub selection: InvertedCellColors,
-    #[serde(deserialize_with = "failure_default")]
-    normal: NormalColors,
-    #[serde(deserialize_with = "failure_default")]
-    bright: BrightColors,
-    #[serde(deserialize_with = "failure_default")]
-    pub dim: Option<AnsiColors>,
-    #[serde(deserialize_with = "failure_default")]
+    pub normal: NormalColors,
+    pub bright: BrightColors,
+    pub dim: Option<DimColors>,
     pub indexed_colors: Vec<IndexedColor>,
-    #[serde(deserialize_with = "failure_default")]
     pub search: SearchColors,
 }
 
 impl Colors {
-    pub fn normal(&self) -> &AnsiColors {
-        &self.normal.0
-    }
-
-    pub fn bright(&self) -> &AnsiColors {
-        &self.bright.0
-    }
-
     pub fn search_bar_foreground(&self) -> Rgb {
         self.search.bar.foreground.unwrap_or(self.primary.background)
     }
@@ -46,158 +28,88 @@ impl Colors {
     }
 }
 
-#[derive(Deserialize, Copy, Clone, Debug, PartialEq, Eq)]
-struct DefaultForegroundCellRgb(CellRgb);
-
-impl Default for DefaultForegroundCellRgb {
-    fn default() -> Self {
-        Self(CellRgb::CellForeground)
-    }
-}
-
-#[derive(Deserialize, Copy, Clone, Debug, PartialEq, Eq)]
-struct DefaultBackgroundCellRgb(CellRgb);
-
-impl Default for DefaultBackgroundCellRgb {
-    fn default() -> Self {
-        Self(CellRgb::CellBackground)
-    }
-}
-
-#[serde(default)]
-#[derive(Deserialize, Clone, Default, Debug, PartialEq, Eq)]
+#[derive(Deserialize, Copy, Clone, Default, Debug, PartialEq, Eq)]
 pub struct IndexedColor {
-    #[serde(deserialize_with = "deserialize_color_index")]
-    pub index: u8,
-    #[serde(deserialize_with = "failure_default")]
     pub color: Rgb,
-}
 
-fn deserialize_color_index<'a, D>(deserializer: D) -> Result<u8, D::Error>
-where
-    D: Deserializer<'a>,
-{
-    let value = Value::deserialize(deserializer)?;
-    match u8::deserialize(value) {
-        Ok(index) => {
-            if index < 16 {
-                error!(
-                    target: LOG_TARGET_CONFIG,
-                    "Problem with config: indexed_color's index is {}, but a value bigger than 15 \
-                     was expected; ignoring setting",
-                    index
-                );
-
-                // Return value out of range to ignore this color.
-                Ok(0)
-            } else {
-                Ok(index)
-            }
-        },
-        Err(err) => {
-            error!(target: LOG_TARGET_CONFIG, "Problem with config: {}; ignoring setting", err);
+    index: ColorIndex,
+}
 
-            // Return value out of range to ignore this color.
-            Ok(0)
-        },
+impl IndexedColor {
+    #[inline]
+    pub fn index(&self) -> u8 {
+        self.index.0
     }
 }
 
-#[serde(default)]
-#[derive(Deserialize, Debug, Copy, Clone, Default, PartialEq, Eq)]
-pub struct CursorColors {
-    #[serde(deserialize_with = "failure_default")]
-    text: DefaultBackgroundCellRgb,
-    #[serde(deserialize_with = "failure_default")]
-    cursor: DefaultForegroundCellRgb,
-}
+#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)]
+struct ColorIndex(u8);
 
-impl CursorColors {
-    pub fn text(self) -> CellRgb {
-        self.text.0
-    }
+impl<'de> Deserialize<'de> for ColorIndex {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        let index = u8::deserialize(deserializer)?;
 
-    pub fn cursor(self) -> CellRgb {
-        self.cursor.0
+        if index < 16 {
+            Err(SerdeError::custom(
+                "Config error: indexed_color's index is {}, but a value bigger than 15 was \
+                 expected; ignoring setting",
+            ))
+        } else {
+            Ok(Self(index))
+        }
     }
 }
 
-#[serde(default)]
-#[derive(Deserialize, Debug, Copy, Clone, Default, PartialEq, Eq)]
+#[derive(ConfigDeserialize, Debug, Copy, Clone, PartialEq, Eq)]
 pub struct InvertedCellColors {
-    #[serde(deserialize_with = "failure_default", alias = "text")]
-    foreground: DefaultBackgroundCellRgb,
-    #[serde(deserialize_with = "failure_default")]
-    background: DefaultForegroundCellRgb,
+    #[config(alias = "text")]
+    pub foreground: CellRgb,
+    #[config(alias = "cursor")]
+    pub background: CellRgb,
 }
 
-impl InvertedCellColors {
-    pub fn foreground(self) -> CellRgb {
-        self.foreground.0
-    }
-
-    pub fn background(self) -> CellRgb {
-        self.background.0
+impl Default for InvertedCellColors {
+    fn default() -> Self {
+        Self { foreground: CellRgb::CellBackground, background: CellRgb::CellForeground }
     }
 }
 
-#[serde(default)]
-#[derive(Deserialize, Debug, Copy, Clone, Default, PartialEq, Eq)]
+#[derive(ConfigDeserialize, Debug, Copy, Clone, Default, PartialEq, Eq)]
 pub struct SearchColors {
-    #[serde(deserialize_with = "failure_default")]
-    pub matches: MatchColors,
-    #[serde(deserialize_with = "failure_default")]
     pub focused_match: InvertedCellColors,
-    #[serde(deserialize_with = "failure_default")]
+    pub matches: MatchColors,
     bar: BarColors,
 }
 
-#[serde(default)]
-#[derive(Deserialize, Debug, Copy, Clone, PartialEq, Eq)]
+#[derive(ConfigDeserialize, Debug, Copy, Clone, PartialEq, Eq)]
 pub struct MatchColors {
-    #[serde(deserialize_with = "failure_default")]
     pub foreground: CellRgb,
-    #[serde(deserialize_with = "deserialize_match_background")]
     pub background: CellRgb,
 }
 
 impl Default for MatchColors {
     fn default() -> Self {
-        Self { foreground: CellRgb::default(), background: default_match_background() }
+        Self {
+            background: CellRgb::Rgb(Rgb { r: 0xff, g: 0xff, b: 0xff }),
+            foreground: CellRgb::Rgb(Rgb { r: 0x00, g: 0x00, b: 0x00 }),
+        }
     }
 }
 
-fn deserialize_match_background<'a, D>(deserializer: D) -> Result<CellRgb, D::Error>
-where
-    D: Deserializer<'a>,
-{
-    let value = Value::deserialize(deserializer)?;
-    Ok(CellRgb::deserialize(value).unwrap_or_else(|_| default_match_background()))
-}
-
-fn default_match_background() -> CellRgb {
-    CellRgb::Rgb(Rgb { r: 0xff, g: 0xff, b: 0xff })
-}
-
-#[serde(default)]
-#[derive(Deserialize, Debug, Copy, Clone, Default, PartialEq, Eq)]
+#[derive(ConfigDeserialize, Debug, Copy, Clone, Default, PartialEq, Eq)]
 pub struct BarColors {
-    #[serde(deserialize_with = "failure_default")]
     foreground: Option<Rgb>,
-    #[serde(deserialize_with = "failure_default")]
     background: Option<Rgb>,
 }
 
-#[serde(default)]
-#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
+#[derive(ConfigDeserialize, Clone, Debug, PartialEq, Eq)]
 pub struct PrimaryColors {
-    #[serde(deserialize_with = "failure_default")]
-    pub background: Rgb,
-    #[serde(deserialize_with = "failure_default")]
     pub foreground: Rgb,
-    #[serde(deserialize_with = "failure_default")]
+    pub background: Rgb,
     pub bright_foreground: Option<Rgb>,
-    #[serde(deserialize_with = "failure_default")]
     pub dim_foreground: Option<Rgb>,
 }
 
@@ -212,33 +124,21 @@ impl Default for PrimaryColors {
     }
 }
 
-/// The 8-colors sections of config.
-#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
-pub struct AnsiColors {
-    #[serde(deserialize_with = "failure_default")]
+#[derive(ConfigDeserialize, Clone, Debug, PartialEq, Eq)]
+pub struct NormalColors {
     pub black: Rgb,
-    #[serde(deserialize_with = "failure_default")]
     pub red: Rgb,
-    #[serde(deserialize_with = "failure_default")]
     pub green: Rgb,
-    #[serde(deserialize_with = "failure_default")]
     pub yellow: Rgb,
-    #[serde(deserialize_with = "failure_default")]
     pub blue: Rgb,
-    #[serde(deserialize_with = "failure_default")]
     pub magenta: Rgb,
-    #[serde(deserialize_with = "failure_default")]
     pub cyan: Rgb,
-    #[serde(deserialize_with = "failure_default")]
     pub white: Rgb,
 }
 
-#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
-struct NormalColors(AnsiColors);
-
 impl Default for NormalColors {
     fn default() -> Self {
-        NormalColors(AnsiColors {
+        NormalColors {
             black: Rgb { r: 0x1d, g: 0x1f, b: 0x21 },
             red: Rgb { r: 0xcc, g: 0x66, b: 0x66 },
             green: Rgb { r: 0xb5, g: 0xbd, b: 0x68 },
@@ -247,16 +147,25 @@ impl Default for NormalColors {
             magenta: Rgb { r: 0xb2, g: 0x94, b: 0xbb },
             cyan: Rgb { r: 0x8a, g: 0xbe, b: 0xb7 },
             white: Rgb { r: 0xc5, g: 0xc8, b: 0xc6 },
-        })
+        }
     }
 }
 
-#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
-struct BrightColors(AnsiColors);
+#[derive(ConfigDeserialize, Clone, Debug, PartialEq, Eq)]
+pub struct BrightColors {
+    pub black: Rgb,
+    pub red: Rgb,
+    pub green: Rgb,
+    pub yellow: Rgb,
+    pub blue: Rgb,
+    pub magenta: Rgb,
+    pub cyan: Rgb,
+    pub white: Rgb,
+}
 
 impl Default for BrightColors {
     fn default() -> Self {
-        BrightColors(AnsiColors {
+        BrightColors {
             black: Rgb { r: 0x66, g: 0x66, b: 0x66 },
             red: Rgb { r: 0xd5, g: 0x4e, b: 0x53 },
             green: Rgb { r: 0xb9, g: 0xca, b: 0x4a },
@@ -265,6 +174,18 @@ impl Default for BrightColors {
             magenta: Rgb { r: 0xc3, g: 0x97, b: 0xd8 },
             cyan: Rgb { r: 0x70, g: 0xc0, b: 0xb1 },
             white: Rgb { r: 0xea, g: 0xea, b: 0xea },
-        })
+        }
     }
 }
+
+#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
+pub struct DimColors {
+    pub black: Rgb,
+    pub red: Rgb,
+    pub green: Rgb,
+    pub yellow: Rgb,
+    pub blue: Rgb,
+    pub magenta: Rgb,
+    pub cyan: Rgb,
+    pub white: Rgb,
+}

+ 49 - 175
alacritty_terminal/src/config/mod.rs

@@ -1,11 +1,10 @@
 use std::cmp::max;
 use std::collections::HashMap;
-use std::fmt::Display;
 use std::path::PathBuf;
 
-use log::error;
-use serde::{Deserialize, Deserializer};
-use serde_yaml::Value;
+use serde::Deserialize;
+
+use alacritty_config_derive::ConfigDeserialize;
 
 mod bell;
 mod colors;
@@ -17,132 +16,103 @@ pub use crate::config::bell::{BellAnimation, BellConfig};
 pub use crate::config::colors::Colors;
 pub use crate::config::scrolling::Scrolling;
 
-pub const LOG_TARGET_CONFIG: &str = "alacritty_config";
-const DEFAULT_CURSOR_THICKNESS: f32 = 0.15;
-const MAX_SCROLLBACK_LINES: u32 = 100_000;
+pub const LOG_TARGET_CONFIG: &str = "alacritty_config_derive";
 const MIN_BLINK_INTERVAL: u64 = 10;
 
 pub type MockConfig = Config<HashMap<String, serde_yaml::Value>>;
 
 /// Top-level config type.
-#[derive(Debug, PartialEq, Default, Deserialize)]
+#[derive(ConfigDeserialize, Debug, PartialEq, Default)]
 pub struct Config<T> {
     /// TERM env variable.
-    #[serde(default, deserialize_with = "failure_default")]
     pub env: HashMap<String, String>,
 
     /// Should draw bold text with brighter colors instead of bold font.
-    #[serde(default, deserialize_with = "failure_default")]
-    draw_bold_text_with_bright_colors: bool,
+    pub draw_bold_text_with_bright_colors: bool,
 
-    #[serde(default, deserialize_with = "failure_default")]
     pub colors: Colors,
 
-    #[serde(default, deserialize_with = "failure_default")]
     pub selection: Selection,
 
     /// Path to a shell program to run on startup.
-    #[serde(default, deserialize_with = "failure_default")]
     pub shell: Option<Program>,
 
-    /// Bell configuration.
-    #[serde(default, deserialize_with = "failure_default")]
-    bell: BellConfig,
-
     /// How much scrolling history to keep.
-    #[serde(default, deserialize_with = "failure_default")]
     pub scrolling: Scrolling,
 
     /// Cursor configuration.
-    #[serde(default, deserialize_with = "failure_default")]
     pub cursor: Cursor,
 
     /// Shell startup directory.
-    #[serde(default, deserialize_with = "option_explicit_none")]
     pub working_directory: Option<PathBuf>,
 
     /// Additional configuration options not directly required by the terminal.