aboutsummaryrefslogtreecommitdiff
path: root/alacritty_config_derive
diff options
context:
space:
mode:
authorChristian Duerr <contact@christianduerr.com>2022-08-31 22:48:38 +0000
committerGitHub <noreply@github.com>2022-09-01 01:48:38 +0300
commit4ddb608563d985060d69594d1004550a680ae3bd (patch)
tree0b02a330b3e59300cff80a147f3c1bdab7f9ea57 /alacritty_config_derive
parent18f9c2793924aec91c80a69ccb45f529adaffae5 (diff)
downloadalacritty-4ddb608563d985060d69594d1004550a680ae3bd.tar.gz
alacritty-4ddb608563d985060d69594d1004550a680ae3bd.zip
Add IPC config subcommand
This patch adds a new mechanism for changing configuration options without editing the configuration file, by sending options to running instances through `alacritty msg`. Each window will load Alacritty's configuration file by default and then accept IPC messages for config updates using the `alacritty msg config` subcommand. By default all windows will be updated, individual windows can be addressed using `alacritty msg config --window-id "$ALACRITTY_WINDOW_ID"`. Each option will replace the config's current value and cannot be reset until Alacritty is restarted or the option is overwritten with a new value. Configuration options are passed in the format `field.subfield=value`, where `value` is interpreted as yaml. Closes #472.
Diffstat (limited to 'alacritty_config_derive')
-rw-r--r--alacritty_config_derive/Cargo.toml6
-rw-r--r--alacritty_config_derive/src/config_deserialize/de_enum.rs (renamed from alacritty_config_derive/src/de_enum.rs)11
-rw-r--r--alacritty_config_derive/src/config_deserialize/de_struct.rs (renamed from alacritty_config_derive/src/de_struct.rs)69
-rw-r--r--alacritty_config_derive/src/config_deserialize/mod.rs22
-rw-r--r--alacritty_config_derive/src/lib.rs77
-rw-r--r--alacritty_config_derive/src/serde_replace.rs113
-rw-r--r--alacritty_config_derive/tests/config.rs38
7 files changed, 260 insertions, 76 deletions
diff --git a/alacritty_config_derive/Cargo.toml b/alacritty_config_derive/Cargo.toml
index 8584d0d2..c901815e 100644
--- a/alacritty_config_derive/Cargo.toml
+++ b/alacritty_config_derive/Cargo.toml
@@ -16,7 +16,11 @@ syn = { version = "1.0.53", features = ["derive", "parsing", "proc-macro", "prin
proc-macro2 = "1.0.24"
quote = "1.0.7"
+[dev-dependencies.alacritty_config]
+path = "../alacritty_config"
+version = "0.1.0"
+
[dev-dependencies]
+serde = { version = "1.0.117", features = ["derive"] }
serde_yaml = "0.8.14"
-serde = "1.0.117"
log = "0.4.11"
diff --git a/alacritty_config_derive/src/de_enum.rs b/alacritty_config_derive/src/config_deserialize/de_enum.rs
index 98247c0c..73634e73 100644
--- a/alacritty_config_derive/src/de_enum.rs
+++ b/alacritty_config_derive/src/config_deserialize/de_enum.rs
@@ -1,9 +1,11 @@
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
-use syn::{DataEnum, Ident};
+use syn::{DataEnum, Generics, Ident};
-pub fn derive_deserialize(ident: Ident, data_enum: DataEnum) -> TokenStream {
+use crate::serde_replace;
+
+pub fn derive_deserialize(ident: Ident, generics: Generics, data_enum: DataEnum) -> TokenStream {
let visitor = format_ident!("{}Visitor", ident);
// Create match arm streams and get a list with all available values.
@@ -30,7 +32,7 @@ pub fn derive_deserialize(ident: Ident, data_enum: DataEnum) -> TokenStream {
available_values.truncate(available_values.len().saturating_sub(2));
// Generate deserialization impl.
- let tokens = quote! {
+ let mut tokens = quote! {
struct #visitor;
impl<'de> serde::de::Visitor<'de> for #visitor {
type Value = #ident;
@@ -62,5 +64,8 @@ pub fn derive_deserialize(ident: Ident, data_enum: DataEnum) -> TokenStream {
}
};
+ // Automatically implement [`alacritty_config::SerdeReplace`].
+ tokens.extend(serde_replace::derive_direct(ident, generics));
+
tokens.into()
}
diff --git a/alacritty_config_derive/src/de_struct.rs b/alacritty_config_derive/src/config_deserialize/de_struct.rs
index cf7ea141..4245764f 100644
--- a/alacritty_config_derive/src/de_struct.rs
+++ b/alacritty_config_derive/src/config_deserialize/de_struct.rs
@@ -1,13 +1,12 @@
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};
+use syn::{Error, Field, Generics, Ident, Type};
+
+use crate::{serde_replace, Attr, GenericsStreams, MULTIPLE_FLATTEN_ERROR};
-/// 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");
@@ -18,20 +17,20 @@ pub fn derive_deserialize<T>(
) -> TokenStream {
// Create all necessary tokens for the implementation.
let GenericsStreams { unconstrained, constrained, phantoms } =
- generics_streams(generics.params);
+ crate::generics_streams(&generics.params);
let FieldStreams { flatten, match_assignments } = fields_deserializer(&fields);
let visitor = format_ident!("{}Visitor", ident);
// Generate deserialization impl.
- let tokens = quote! {
+ let mut tokens = quote! {
#[derive(Default)]
#[allow(non_snake_case)]
- struct #visitor < #unconstrained > {
+ struct #visitor <#unconstrained> {
#phantoms
}
- impl<'de, #constrained> serde::de::Visitor<'de> for #visitor < #unconstrained > {
- type Value = #ident < #unconstrained >;
+ 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")
@@ -61,7 +60,7 @@ pub fn derive_deserialize<T>(
}
}
- impl<'de, #constrained> serde::Deserialize<'de> for #ident < #unconstrained > {
+ impl <'de, #constrained> serde::Deserialize<'de> for #ident <#unconstrained> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
@@ -71,6 +70,9 @@ pub fn derive_deserialize<T>(
}
};
+ // Automatically implement [`alacritty_config::SerdeReplace`].
+ tokens.extend(serde_replace::derive_recursive(ident, generics, fields));
+
tokens.into()
}
@@ -177,50 +179,3 @@ fn field_deserializer(field_streams: &mut FieldStreams, field: &Field) -> Result
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
-}
diff --git a/alacritty_config_derive/src/config_deserialize/mod.rs b/alacritty_config_derive/src/config_deserialize/mod.rs
new file mode 100644
index 00000000..b1923377
--- /dev/null
+++ b/alacritty_config_derive/src/config_deserialize/mod.rs
@@ -0,0 +1,22 @@
+use proc_macro::TokenStream;
+use syn::{parse_macro_input, Data, DataStruct, DeriveInput, Error, Fields};
+
+/// Error if the derive was used on an unsupported type.
+const UNSUPPORTED_ERROR: &str = "ConfigDeserialize must be used on an enum or struct with fields";
+
+mod de_enum;
+mod de_struct;
+
+pub fn derive(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, input.generics, data_enum)
+ },
+ _ => Error::new(input.ident.span(), UNSUPPORTED_ERROR).to_compile_error().into(),
+ }
+}
diff --git a/alacritty_config_derive/src/lib.rs b/alacritty_config_derive/src/lib.rs
index af8f2e7f..116d4828 100644
--- a/alacritty_config_derive/src/lib.rs
+++ b/alacritty_config_derive/src/lib.rs
@@ -2,25 +2,27 @@
#![cfg_attr(feature = "cargo-clippy", deny(warnings))]
use proc_macro::TokenStream;
-use syn::{parse_macro_input, Data, DataStruct, DeriveInput, Error, Fields, Path};
+use proc_macro2::TokenStream as TokenStream2;
+use quote::quote;
+use syn::parse::{self, Parse, ParseStream};
+use syn::punctuated::Punctuated;
+use syn::{GenericParam, Ident, LitStr, Path, Token, TypeParam};
-mod de_enum;
-mod de_struct;
+mod config_deserialize;
+mod serde_replace;
-/// Error if the derive was used on an unsupported type.
-const UNSUPPORTED_ERROR: &str = "ConfigDeserialize must be used on a struct with fields";
+/// Error message when attempting to flatten multiple fields.
+pub(crate) const MULTIPLE_FLATTEN_ERROR: &str =
+ "At most one instance of #[config(flatten)] is supported";
#[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(),
- }
+ config_deserialize::derive(input)
+}
+
+#[proc_macro_derive(SerdeReplace)]
+pub fn derive_serde_replace(input: TokenStream) -> TokenStream {
+ serde_replace::derive(input)
}
/// Verify that a token path ends with a specific segment.
@@ -28,3 +30,50 @@ 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)
}
+
+/// 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>,`
+pub(crate) 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> + alacritty_config::SerdeReplace,
+ });
+ generics.phantoms.extend(quote! {
+ #ident : std::marker::PhantomData < #ident >,
+ });
+ }
+ }
+
+ generics
+}
+
+/// Field attribute.
+pub(crate) 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 })
+ }
+}
diff --git a/alacritty_config_derive/src/serde_replace.rs b/alacritty_config_derive/src/serde_replace.rs
new file mode 100644
index 00000000..4a0a6a99
--- /dev/null
+++ b/alacritty_config_derive/src/serde_replace.rs
@@ -0,0 +1,113 @@
+use proc_macro::TokenStream;
+use proc_macro2::TokenStream as TokenStream2;
+use quote::quote;
+use syn::punctuated::Punctuated;
+use syn::{
+ parse_macro_input, Data, DataStruct, DeriveInput, Error, Field, Fields, Generics, Ident,
+};
+
+use crate::{Attr, GenericsStreams, MULTIPLE_FLATTEN_ERROR};
+
+/// Error if the derive was used on an unsupported type.
+const UNSUPPORTED_ERROR: &str = "SerdeReplace must be used on a tuple struct";
+
+pub fn derive(input: TokenStream) -> TokenStream {
+ let input = parse_macro_input!(input as DeriveInput);
+
+ match input.data {
+ Data::Struct(DataStruct { fields: Fields::Unnamed(_), .. }) | Data::Enum(_) => {
+ derive_direct(input.ident, input.generics).into()
+ },
+ Data::Struct(DataStruct { fields: Fields::Named(fields), .. }) => {
+ derive_recursive(input.ident, input.generics, fields.named).into()
+ },
+ _ => Error::new(input.ident.span(), UNSUPPORTED_ERROR).to_compile_error().into(),
+ }
+}
+
+pub fn derive_direct(ident: Ident, generics: Generics) -> TokenStream2 {
+ quote! {
+ impl <#generics> alacritty_config::SerdeReplace for #ident <#generics> {
+ fn replace(&mut self, key: &str, value: serde_yaml::Value) -> Result<(), Box<dyn std::error::Error>> {
+ if !key.is_empty() {
+ let error = format!("Fields \"{}\" do not exist", key);
+ return Err(error.into());
+ }
+ *self = serde::Deserialize::deserialize(value)?;
+
+ Ok(())
+ }
+ }
+ }
+}
+
+pub fn derive_recursive<T>(
+ ident: Ident,
+ generics: Generics,
+ fields: Punctuated<Field, T>,
+) -> TokenStream2 {
+ let GenericsStreams { unconstrained, constrained, .. } =
+ crate::generics_streams(&generics.params);
+ let replace_arms = match_arms(&fields);
+
+ quote! {
+ #[allow(clippy::extra_unused_lifetimes)]
+ impl <'de, #constrained> alacritty_config::SerdeReplace for #ident <#unconstrained> {
+ fn replace(&mut self, key: &str, value: serde_yaml::Value) -> Result<(), Box<dyn std::error::Error>> {
+ if key.is_empty() {
+ *self = serde::Deserialize::deserialize(value)?;
+ return Ok(());
+ }
+
+ let (field, next_key) = key.split_once('.').unwrap_or((key, ""));
+ match field {
+ #replace_arms
+ _ => {
+ let error = format!("Field \"{}\" does not exist", field);
+ return Err(error.into());
+ },
+ }
+
+ Ok(())
+ }
+ }
+ }
+}
+
+/// Create SerdeReplace recursive match arms.
+fn match_arms<T>(fields: &Punctuated<Field, T>) -> TokenStream2 {
+ let mut stream = TokenStream2::default();
+ let mut flattened_arm = None;
+
+ // Create arm for each field.
+ for field in fields {
+ let ident = field.ident.as_ref().expect("unreachable tuple struct");
+ let literal = ident.to_string();
+
+ // Check if #[config(flattened)] attribute is present.
+ let flatten = field
+ .attrs
+ .iter()
+ .filter_map(|attr| attr.parse_args::<Attr>().ok())
+ .any(|parsed| parsed.ident.as_str() == "flatten");
+
+ if flatten && flattened_arm.is_some() {
+ return Error::new(ident.span(), MULTIPLE_FLATTEN_ERROR).to_compile_error();
+ } else if flatten {
+ flattened_arm = Some(quote! {
+ _ => alacritty_config::SerdeReplace::replace(&mut self.#ident, key, value)?,
+ });
+ } else {
+ stream.extend(quote! {
+ #literal => alacritty_config::SerdeReplace::replace(&mut self.#ident, next_key, value)?,
+ });
+ }
+ }
+
+ // Add the flattened catch-all as last match arm.
+ if let Some(flattened_arm) = flattened_arm.take() {
+ stream.extend(flattened_arm);
+ }
+
+ stream
+}
diff --git a/alacritty_config_derive/tests/config.rs b/alacritty_config_derive/tests/config.rs
index 4828b822..bd449ff8 100644
--- a/alacritty_config_derive/tests/config.rs
+++ b/alacritty_config_derive/tests/config.rs
@@ -1,8 +1,10 @@
use std::sync::{Arc, Mutex};
use log::{Level, Log, Metadata, Record};
+use serde::Deserialize;
-use alacritty_config_derive::ConfigDeserialize;
+use alacritty_config::SerdeReplace as _;
+use alacritty_config_derive::{ConfigDeserialize, SerdeReplace};
#[derive(ConfigDeserialize, Debug, PartialEq, Eq)]
enum TestEnum {
@@ -63,6 +65,7 @@ struct Test2<T: Default> {
field3: usize,
#[config(alias = "aliased")]
field4: u8,
+ newtype: NewType,
}
#[derive(ConfigDeserialize, Default)]
@@ -70,6 +73,9 @@ struct Test3 {
flatty: usize,
}
+#[derive(SerdeReplace, Deserialize, Default, PartialEq, Eq, Debug)]
+struct NewType(usize);
+
#[test]
fn config_deserialize() {
let logger = unsafe {
@@ -159,3 +165,33 @@ impl Log for Logger {
fn flush(&self) {}
}
+
+#[test]
+fn field_replacement() {
+ let mut test = Test::default();
+
+ let value = serde_yaml::to_value(13).unwrap();
+ test.replace("nesting.field2", value).unwrap();
+
+ assert_eq!(test.nesting.field2, Some(13));
+}
+
+#[test]
+fn replace_derive() {
+ let mut test = Test::default();
+
+ let value = serde_yaml::to_value(9).unwrap();
+ test.replace("nesting.newtype", value).unwrap();
+
+ assert_eq!(test.nesting.newtype, NewType(9));
+}
+
+#[test]
+fn replace_flatten() {
+ let mut test = Test::default();
+
+ let value = serde_yaml::to_value(7).unwrap();
+ test.replace("flatty", value).unwrap();
+
+ assert_eq!(test.flatten.flatty, 7);
+}