tui_theme_builder_derive/
lib.rs1use core::panic;
2use proc_macro::TokenStream;
3use proc_macro2::{Punct, Spacing, TokenStream as TokenStream2, TokenTree};
4use quote::quote;
5use syn::{parse::ParseStream, parse_macro_input, Attribute, Data, DeriveInput, Fields, Ident};
6
7#[allow(clippy::too_many_lines)]
11#[proc_macro_derive(ThemeBuilder, attributes(context, builder, style))]
12pub fn derive_theme_builder(input: TokenStream) -> TokenStream {
13 let input = parse_macro_input!(input as DeriveInput);
14
15 let struct_name = &input.ident;
16
17 let Data::Struct(data) = &input.data else {
18 panic!("derive must be attached to a struct");
19 };
20
21 let builder_attr = extract_builder_attribute(&input.attrs);
22 let Some(builder_attr) = builder_attr else {
23 panic!("no `context` attribute found on struct");
24 };
25 let context_name = process_builder_struct_attribute(builder_attr);
26 let Some(context_name) = context_name else {
27 panic!("no `context` field found in builder annotation");
28 };
29
30 let Fields::Named(fields) = &data.fields else {
31 panic!("expected named fields, got {:?}", &data.fields)
32 };
33
34 let mut field_constructors: Vec<TokenStream2> = Vec::new();
35
36 for field in &fields.named {
37 let field_name = field.ident.as_ref().unwrap();
38 let field_type = &field.ty;
39
40 let mut field_constructor = quote! {};
41
42 let attr = extract_style_attribute(&field.attrs);
44 if let Some(attr) = attr {
45 let style_values = process_style_attribute(attr);
46
47 field_constructor.extend(quote! {
48 #field_name: ratatui::style::Style::default()
49 });
50
51 if let Some(foreground_color) = style_values.foreground {
52 field_constructor.extend(quote! {
53 .fg(context.#foreground_color.clone().into())
54 });
55 }
56
57 if let Some(background_color) = style_values.background {
58 field_constructor.extend(quote! {
59 .bg(context.#background_color.clone().into())
60 });
61 }
62
63 if style_values.bold.is_some() {
64 field_constructor.extend(quote! {
65 .add_modifier(ratatui::style::Modifier::BOLD)
66 });
67 }
68
69 if style_values.dim.is_some() {
70 field_constructor.extend(quote! {
71 .add_modifier(ratatui::style::Modifier::DIM)
72 });
73 }
74
75 if style_values.italic.is_some() {
76 field_constructor.extend(quote! {
77 .add_modifier(ratatui::style::Modifier::ITALIC)
78 });
79 }
80
81 if style_values.underlined.is_some() {
82 field_constructor.extend(quote! {
83 .add_modifier(ratatui::style::Modifier::UNDERLINED)
84 });
85 }
86
87 if style_values.slow_blink.is_some() {
88 field_constructor.extend(quote! {
89 .add_modifier(ratatui::style::Modifier::SLOW_BLINK)
90 });
91 }
92
93 if style_values.rapid_blink.is_some() {
94 field_constructor.extend(quote! {
95 .add_modifier(ratatui::style::Modifier::RAPID_BLINK)
96 });
97 }
98
99 if style_values.reversed.is_some() {
100 field_constructor.extend(quote! {
101 .add_modifier(ratatui::style::Modifier::REVERSED)
102 });
103 }
104
105 if style_values.hidden.is_some() {
106 field_constructor.extend(quote! {
107 .add_modifier(ratatui::style::Modifier::HIDDEN)
108 });
109 }
110
111 if style_values.crossed_out.is_some() {
112 field_constructor.extend(quote! {
113 .add_modifier(ratatui::style::Modifier::CROSSED_OUT)
114 });
115 }
116
117 field_constructors.push(field_constructor);
118 continue;
119 }
120
121 let attr = extract_builder_attribute(&field.attrs);
123 if let Some(attr) = attr {
124 let value = process_builder_field_attribute(attr);
125 let Some(value) = value else {
126 panic!("missing value in `builder` on field `{:?}`", field_name);
127 };
128
129 match value {
130 BuilderFieldAttribute::Value(value) => {
131 field_constructor.extend(quote! {
132 #field_name: context.#value.clone()
133 });
134 }
135 BuilderFieldAttribute::Default => {
136 field_constructor.extend(quote! {
137 #field_name: <#field_type>::default()
138 });
139 }
140 }
141
142 field_constructors.push(field_constructor);
143 continue;
144 }
145
146 field_constructor.extend(quote! {
148 #field_name: #field_type::build(context)
149 });
150
151 field_constructors.push(field_constructor);
152 }
153
154 let implementation = quote! {
155 impl tui_theme_builder::ThemeBuilder for #struct_name {
156 type Context = #context_name;
157 fn build(context: &#context_name) -> Self {
158 Self {
159 #(#field_constructors),*
160 }
161 }
162 }
163 };
164
165 TokenStream::from(implementation)
166}
167
168fn extract_builder_attribute(attrs: &[Attribute]) -> Option<&Attribute> {
170 attrs.iter().find(|attr| attr.path().is_ident("builder"))
171}
172
173fn process_builder_field_attribute(attr: &Attribute) -> Option<BuilderFieldAttribute> {
175 let mut attribute: Option<BuilderFieldAttribute> = None;
176
177 let _ = attr.parse_nested_meta(|meta| {
178 if meta.path.is_ident("value") {
179 let value = meta.value()?;
180 let value = extract_metadata_stream(value)?;
181 if value.to_string() == "default" {
182 attribute = Some(BuilderFieldAttribute::Default);
183 } else {
184 attribute = Some(BuilderFieldAttribute::Value(value));
185 }
186 Ok(())
187 } else {
188 Err(meta.error("unsupported attribute"))
189 }
190 });
191
192 attribute
193}
194
195enum BuilderFieldAttribute {
196 Value(TokenStream2),
197 Default,
198}
199
200fn process_builder_struct_attribute(attr: &Attribute) -> Option<Ident> {
203 let mut context: Option<Ident> = None;
204
205 let _ = attr.parse_nested_meta(|meta| {
206 if meta.path.is_ident("context") {
207 let value = meta.value()?;
208 let ident: syn::Ident = value.parse()?;
209 context = Some(ident);
210 Ok(())
211 } else {
212 Err(meta.error("unsupported attribute"))
213 }
214 });
215
216 context
217}
218
219fn extract_style_attribute(attrs: &[Attribute]) -> Option<&Attribute> {
221 attrs.iter().find(|attr| attr.path().is_ident("style"))
222}
223
224fn process_style_attribute(attr: &Attribute) -> StyleValues {
226 let mut foreground: Option<TokenStream2> = None;
227 let mut background: Option<TokenStream2> = None;
228 let mut bold: Option<bool> = None;
229 let mut dim: Option<bool> = None;
230 let mut italic: Option<bool> = None;
231 let mut underlined: Option<bool> = None;
232 let mut slow_blink: Option<bool> = None;
233 let mut rapid_blink: Option<bool> = None;
234 let mut reversed: Option<bool> = None;
235 let mut hidden: Option<bool> = None;
236 let mut crossed_out: Option<bool> = None;
237
238 let _ = attr.parse_nested_meta(|meta| {
239 if let Some(ident) = meta.path.get_ident() {
240 match ident.to_string().as_str() {
241 "bold" => bold = Some(true),
242 "dim" => dim = Some(true),
243 "italic" => italic = Some(true),
244 "underlined" => underlined = Some(true),
245 "slow_blink" => slow_blink = Some(true),
246 "rapid_blink" => rapid_blink = Some(true),
247 "reversed" => reversed = Some(true),
248 "hidden" => hidden = Some(true),
249 "crossed_out" => crossed_out = Some(true),
250 "fg" | "foreground" => {
251 let value = meta.value()?;
252 let ident = extract_metadata_stream(value).unwrap();
253 foreground = Some(ident);
254 }
255 "bg" | "background" => {
256 let value = meta.value()?;
257 let ident = extract_metadata_stream(value)?;
258 background = Some(ident);
259 }
260 _ => {}
261 }
262 }
263
264 Ok(())
265 });
266
267 StyleValues {
268 foreground,
269 background,
270 bold,
271 dim,
272 italic,
273 underlined,
274 slow_blink,
275 rapid_blink,
276 reversed,
277 hidden,
278 crossed_out,
279 }
280}
281
282struct StyleValues {
283 foreground: Option<TokenStream2>,
284 background: Option<TokenStream2>,
285 bold: Option<bool>,
286 dim: Option<bool>,
287 italic: Option<bool>,
288 underlined: Option<bool>,
289 slow_blink: Option<bool>,
290 rapid_blink: Option<bool>,
291 reversed: Option<bool>,
292 hidden: Option<bool>,
293 crossed_out: Option<bool>,
294}
295
296fn extract_metadata_stream(input: ParseStream) -> Result<TokenStream2, syn::Error> {
299 let mut tokens = TokenStream2::new();
300 while !input.is_empty() {
301 if input.peek(Ident) {
302 let ident: Ident = input.parse()?;
303 tokens.extend(Some(TokenTree::Ident(ident)));
304 } else if input.peek(syn::Token![.]) {
305 let _dot: syn::Token![.] = input.parse()?;
306 tokens.extend(Some(TokenTree::Punct(Punct::new('.', Spacing::Alone))));
307 } else if input.peek(syn::Token![,]) {
308 break;
309 } else {
310 return Err(input.error(format!(
311 "expected an identifier or a dot, but got {input:?}",
312 )));
313 }
314 }
315
316 Ok(tokens)
317}