mirror of
https://github.com/wez/wezterm.git
synced 2025-01-01 01:59:49 +03:00
config: derive some metadata about the config
This isn't used by anything yet, but will enable some runtime config editing functions that I have planeed for the future.
This commit is contained in:
parent
694ffdbed2
commit
8508860136
10
Cargo.lock
generated
10
Cargo.lock
generated
@ -723,6 +723,7 @@ dependencies = [
|
||||
"toml",
|
||||
"umask",
|
||||
"wezterm-bidi",
|
||||
"wezterm-config-derive",
|
||||
"wezterm-dynamic",
|
||||
"wezterm-input-types",
|
||||
"wezterm-term",
|
||||
@ -5444,6 +5445,15 @@ dependencies = [
|
||||
"wezterm-dynamic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wezterm-config-derive"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wezterm-dynamic"
|
||||
version = "0.1.0"
|
||||
|
@ -39,6 +39,7 @@ smol = "1.2"
|
||||
termwiz = { path = "../termwiz", features=["use_serde"] }
|
||||
toml = "0.5"
|
||||
umask = { path = "../umask" }
|
||||
wezterm-config-derive = { version="0.1", path="derive" }
|
||||
wezterm-dynamic = { path = "../wezterm-dynamic" }
|
||||
wezterm-bidi = { path = "../bidi" }
|
||||
wezterm-input-types = { path = "../wezterm-input-types" }
|
||||
|
15
config/derive/Cargo.toml
Normal file
15
config/derive/Cargo.toml
Normal file
@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "wezterm-config-derive"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/wez/wezterm"
|
||||
description = "derive config metadata"
|
||||
license = "MIT"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = "1.0"
|
||||
quote = "1.0.2"
|
||||
syn = "1.0"
|
21
config/derive/LICENSE.md
Normal file
21
config/derive/LICENSE.md
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Wez Furlong
|
||||
|
||||
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.
|
279
config/derive/src/attr.rs
Normal file
279
config/derive/src/attr.rs
Normal file
@ -0,0 +1,279 @@
|
||||
use proc_macro2::{Span, TokenStream};
|
||||
use quote::quote;
|
||||
use syn::{
|
||||
Attribute, Error, Field, GenericArgument, Ident, Lit, Meta, NestedMeta, Path, PathArguments,
|
||||
Result, Type,
|
||||
};
|
||||
|
||||
pub struct ContainerInfo {
|
||||
pub into: Option<Path>,
|
||||
pub try_from: Option<Path>,
|
||||
pub debug: bool,
|
||||
}
|
||||
|
||||
pub fn container_info(attrs: &[Attribute]) -> Result<ContainerInfo> {
|
||||
let mut into = None;
|
||||
let mut try_from = None;
|
||||
let mut debug = false;
|
||||
|
||||
for attr in attrs {
|
||||
if !attr.path.is_ident("dynamic") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let list = match attr.parse_meta()? {
|
||||
Meta::List(list) => list,
|
||||
other => return Err(Error::new_spanned(other, "unsupported attribute")),
|
||||
};
|
||||
|
||||
for meta in &list.nested {
|
||||
match meta {
|
||||
NestedMeta::Meta(Meta::Path(path)) => {
|
||||
if path.is_ident("debug") {
|
||||
debug = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
NestedMeta::Meta(Meta::NameValue(value)) => {
|
||||
if value.path.is_ident("into") {
|
||||
if let Lit::Str(s) = &value.lit {
|
||||
into = Some(s.parse()?);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if value.path.is_ident("try_from") {
|
||||
if let Lit::Str(s) = &value.lit {
|
||||
try_from = Some(s.parse()?);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
return Err(Error::new_spanned(meta, "unsupported attribute"));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ContainerInfo {
|
||||
into,
|
||||
try_from,
|
||||
debug,
|
||||
})
|
||||
}
|
||||
|
||||
pub enum DefValue {
|
||||
None,
|
||||
Default,
|
||||
Path(Path),
|
||||
}
|
||||
|
||||
pub struct FieldInfo<'a> {
|
||||
pub field: &'a Field,
|
||||
pub type_name: String,
|
||||
pub name: String,
|
||||
pub skip: bool,
|
||||
pub flatten: bool,
|
||||
pub allow_default: DefValue,
|
||||
pub into: Option<Path>,
|
||||
pub try_from: Option<Path>,
|
||||
pub deprecated: Option<String>,
|
||||
pub validate: Option<Path>,
|
||||
pub doc: String,
|
||||
pub container_type: ContainerType,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ContainerType {
|
||||
None,
|
||||
Option,
|
||||
Vec,
|
||||
Map,
|
||||
}
|
||||
|
||||
impl<'a> FieldInfo<'a> {
|
||||
pub fn to_option(&self) -> TokenStream {
|
||||
let name = &self.name;
|
||||
let doc = &self.doc;
|
||||
let type_name = &self.type_name;
|
||||
let container_type = Ident::new(&format!("{:?}", self.container_type), Span::call_site());
|
||||
let get_default = match self.compute_default() {
|
||||
Some(def) => quote!(Some(|| #def.to_dynamic())),
|
||||
None => quote!(None),
|
||||
};
|
||||
quote!(
|
||||
crate::meta::ConfigOption {
|
||||
name: #name,
|
||||
doc: #doc,
|
||||
tags: &[],
|
||||
container: crate::meta::ConfigContainer::#container_type,
|
||||
type_name: #type_name,
|
||||
default_value: #get_default,
|
||||
possible_values: &[],
|
||||
fields: &[],
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fn compute_default(&self) -> Option<TokenStream> {
|
||||
let ty = &self.field.ty;
|
||||
match &self.allow_default {
|
||||
DefValue::Default => Some(quote!(
|
||||
<#ty>::default()
|
||||
)),
|
||||
DefValue::Path(default) => Some(quote!(
|
||||
#default()
|
||||
)),
|
||||
DefValue::None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn field_info(field: &Field) -> Result<FieldInfo> {
|
||||
let mut name = field.ident.as_ref().unwrap().to_string();
|
||||
let mut skip = false;
|
||||
let mut flatten = false;
|
||||
let mut allow_default = DefValue::None;
|
||||
let mut try_from = None;
|
||||
let mut validate = None;
|
||||
let mut into = None;
|
||||
let mut deprecated = None;
|
||||
let mut doc = String::new();
|
||||
let mut container_type = ContainerType::None;
|
||||
|
||||
let type_name = match &field.ty {
|
||||
Type::Path(p) => {
|
||||
let last_seg = p.path.segments.last().unwrap();
|
||||
match &last_seg.arguments {
|
||||
PathArguments::None => last_seg.ident.to_string(),
|
||||
PathArguments::AngleBracketed(args) if args.args.len() == 1 => {
|
||||
let arg = args.args.first().unwrap();
|
||||
match arg {
|
||||
GenericArgument::Type(Type::Path(t)) => {
|
||||
container_type = match last_seg.ident.to_string().as_str() {
|
||||
"Option" => ContainerType::Option,
|
||||
"Vec" => ContainerType::Vec,
|
||||
_ => panic!("unhandled type for {name}: {:#?}", field.ty),
|
||||
};
|
||||
t.path.segments.last().unwrap().ident.to_string()
|
||||
}
|
||||
_ => panic!("unhandled type for {name}: {:#?}", field.ty),
|
||||
}
|
||||
}
|
||||
PathArguments::AngleBracketed(args) if args.args.len() == 2 => {
|
||||
let arg = args.args.last().unwrap();
|
||||
match arg {
|
||||
GenericArgument::Type(Type::Path(t)) => {
|
||||
container_type = match last_seg.ident.to_string().as_str() {
|
||||
"HashMap" => ContainerType::Map,
|
||||
_ => panic!("unhandled type for {name}: {:#?}", field.ty),
|
||||
};
|
||||
t.path.segments.last().unwrap().ident.to_string()
|
||||
}
|
||||
_ => panic!("unhandled type for {name}: {:#?}", field.ty),
|
||||
}
|
||||
}
|
||||
_ => panic!("unhandled type for {name}: {:#?}", field.ty),
|
||||
}
|
||||
}
|
||||
_ => panic!("unhandled type for {name}: {:#?}", field.ty),
|
||||
};
|
||||
|
||||
for attr in &field.attrs {
|
||||
if !attr.path.is_ident("dynamic") && !attr.path.is_ident("doc") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let list = match attr.parse_meta()? {
|
||||
Meta::List(list) => list,
|
||||
Meta::NameValue(value) if value.path.is_ident("doc") => {
|
||||
if let Lit::Str(s) = &value.lit {
|
||||
if !doc.is_empty() {
|
||||
doc.push('\n');
|
||||
}
|
||||
doc.push_str(&s.value());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
other => {
|
||||
return Err(Error::new_spanned(
|
||||
other.clone(),
|
||||
format!("unsupported attribute {other:?}"),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
for meta in &list.nested {
|
||||
match meta {
|
||||
NestedMeta::Meta(Meta::NameValue(value)) => {
|
||||
if value.path.is_ident("rename") {
|
||||
if let Lit::Str(s) = &value.lit {
|
||||
name = s.value();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if value.path.is_ident("default") {
|
||||
if let Lit::Str(s) = &value.lit {
|
||||
allow_default = DefValue::Path(s.parse()?);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if value.path.is_ident("deprecated") {
|
||||
if let Lit::Str(s) = &value.lit {
|
||||
deprecated.replace(s.value());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if value.path.is_ident("into") {
|
||||
if let Lit::Str(s) = &value.lit {
|
||||
into = Some(s.parse()?);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if value.path.is_ident("try_from") {
|
||||
if let Lit::Str(s) = &value.lit {
|
||||
try_from = Some(s.parse()?);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if value.path.is_ident("validate") {
|
||||
if let Lit::Str(s) = &value.lit {
|
||||
validate = Some(s.parse()?);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
NestedMeta::Meta(Meta::Path(path)) => {
|
||||
if path.is_ident("skip") {
|
||||
skip = true;
|
||||
continue;
|
||||
}
|
||||
if path.is_ident("flatten") {
|
||||
flatten = true;
|
||||
continue;
|
||||
}
|
||||
if path.is_ident("default") {
|
||||
allow_default = DefValue::Default;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
return Err(Error::new_spanned(meta, "unsupported attribute"));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(FieldInfo {
|
||||
type_name,
|
||||
field,
|
||||
name,
|
||||
skip,
|
||||
flatten,
|
||||
allow_default,
|
||||
try_from,
|
||||
into,
|
||||
deprecated,
|
||||
validate,
|
||||
doc,
|
||||
container_type,
|
||||
})
|
||||
}
|
16
config/derive/src/bound.rs
Normal file
16
config/derive/src/bound.rs
Normal file
@ -0,0 +1,16 @@
|
||||
use proc_macro2::TokenStream;
|
||||
use syn::{parse_quote, Generics, WhereClause, WherePredicate};
|
||||
|
||||
pub fn where_clause_with_bound(generics: &Generics, bound: TokenStream) -> WhereClause {
|
||||
let new_predicates = generics.type_params().map::<WherePredicate, _>(|param| {
|
||||
let param = ¶m.ident;
|
||||
parse_quote!(#param : #bound)
|
||||
});
|
||||
|
||||
let mut generics = generics.clone();
|
||||
generics
|
||||
.make_where_clause()
|
||||
.predicates
|
||||
.extend(new_predicates);
|
||||
generics.where_clause.unwrap()
|
||||
}
|
64
config/derive/src/configmeta.rs
Normal file
64
config/derive/src/configmeta.rs
Normal file
@ -0,0 +1,64 @@
|
||||
use crate::{attr, bound};
|
||||
use proc_macro2::{Span, TokenStream};
|
||||
use quote::quote;
|
||||
use syn::{parse_quote, Data, DataStruct, DeriveInput, Error, Fields, FieldsNamed, Ident, Result};
|
||||
|
||||
pub fn derive(input: DeriveInput) -> Result<TokenStream> {
|
||||
match &input.data {
|
||||
Data::Struct(DataStruct {
|
||||
fields: Fields::Named(fields),
|
||||
..
|
||||
}) => derive_struct(&input, fields),
|
||||
Data::Struct(_) => Err(Error::new(
|
||||
Span::call_site(),
|
||||
"currently only structs with named fields are supported",
|
||||
)),
|
||||
_ => Err(Error::new(
|
||||
Span::call_site(),
|
||||
"currently only structs and enums are supported by this derive",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn derive_struct(input: &DeriveInput, fields: &FieldsNamed) -> Result<TokenStream> {
|
||||
let info = attr::container_info(&input.attrs)?;
|
||||
let ident = &input.ident;
|
||||
let (impl_generics, ty_generics, _where_clause) = input.generics.split_for_impl();
|
||||
let dummy = Ident::new(
|
||||
&format!("_IMPL_CONFIGMETA_FOR_{}", ident),
|
||||
Span::call_site(),
|
||||
);
|
||||
|
||||
let options = fields
|
||||
.named
|
||||
.iter()
|
||||
.map(attr::field_info)
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
let options = options
|
||||
.into_iter()
|
||||
.filter_map(|f| if f.skip { None } else { Some(f.to_option()) })
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let bound = parse_quote!(crate::ConfigMeta);
|
||||
let bounded_where_clause = bound::where_clause_with_bound(&input.generics, bound);
|
||||
|
||||
let tokens = quote! {
|
||||
#[allow(non_upper_case_globals)]
|
||||
const #dummy: () = {
|
||||
impl #impl_generics crate::meta::ConfigMeta for #ident #ty_generics #bounded_where_clause {
|
||||
fn get_config_options(&self) -> &'static [crate::meta::ConfigOption]
|
||||
{
|
||||
&[
|
||||
#( #options, )*
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
if info.debug {
|
||||
eprintln!("{}", tokens);
|
||||
}
|
||||
Ok(tokens)
|
||||
}
|
13
config/derive/src/lib.rs
Normal file
13
config/derive/src/lib.rs
Normal file
@ -0,0 +1,13 @@
|
||||
use proc_macro::TokenStream;
|
||||
use syn::{parse_macro_input, DeriveInput};
|
||||
|
||||
mod attr;
|
||||
mod bound;
|
||||
mod configmeta;
|
||||
|
||||
#[proc_macro_derive(ConfigMeta, attributes(config))]
|
||||
pub fn derive_config(input: TokenStream) -> TokenStream {
|
||||
configmeta::derive(parse_macro_input!(input as DeriveInput))
|
||||
.unwrap_or_else(|err| err.to_compile_error())
|
||||
.into()
|
||||
}
|
@ -38,11 +38,12 @@ use std::time::Duration;
|
||||
use termwiz::hyperlink;
|
||||
use termwiz::surface::CursorShape;
|
||||
use wezterm_bidi::ParagraphDirectionHint;
|
||||
use wezterm_config_derive::ConfigMeta;
|
||||
use wezterm_dynamic::{FromDynamic, ToDynamic};
|
||||
use wezterm_input_types::{Modifiers, WindowDecorations};
|
||||
use wezterm_term::TerminalSize;
|
||||
|
||||
#[derive(Debug, Clone, FromDynamic, ToDynamic)]
|
||||
#[derive(Debug, Clone, FromDynamic, ToDynamic, ConfigMeta)]
|
||||
pub struct Config {
|
||||
/// The font size, measured in points
|
||||
#[dynamic(default = "default_font_size")]
|
||||
|
@ -30,6 +30,7 @@ mod frontend;
|
||||
pub mod keyassignment;
|
||||
mod keys;
|
||||
pub mod lua;
|
||||
pub mod meta;
|
||||
mod scheme_data;
|
||||
mod ssh;
|
||||
mod terminal;
|
||||
|
33
config/src/meta.rs
Normal file
33
config/src/meta.rs
Normal file
@ -0,0 +1,33 @@
|
||||
use wezterm_dynamic::Value;
|
||||
|
||||
/// Trait for returning metadata about config options
|
||||
pub trait ConfigMeta {
|
||||
fn get_config_options(&self) -> &'static [ConfigOption];
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ConfigContainer {
|
||||
None,
|
||||
Option,
|
||||
Vec,
|
||||
Map,
|
||||
}
|
||||
|
||||
/// Metadata about a config option
|
||||
pub struct ConfigOption {
|
||||
/// The field name
|
||||
pub name: &'static str,
|
||||
/// Brief documentation
|
||||
pub doc: &'static str,
|
||||
/// TODO: tags to categorize the option
|
||||
pub tags: &'static [&'static str],
|
||||
pub container: ConfigContainer,
|
||||
/// The type of the field
|
||||
pub type_name: &'static str,
|
||||
/// call this to get the default value
|
||||
pub default_value: Option<fn() -> Value>,
|
||||
/// TODO: For enum types, the set of possible values
|
||||
pub possible_values: &'static [&'static Value],
|
||||
/// TODO: For struct types, the fields in the child struct
|
||||
pub fields: &'static [ConfigOption],
|
||||
}
|
Loading…
Reference in New Issue
Block a user