etty_macros/
lib.rs

1use quote::quote;
2use syn::parse_macro_input;
3use syn::Token;
4
5#[proc_macro]
6#[doc(hidden)]
7pub fn gen_csi(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
8    parse_macro_input!(input as GenCsi).tts.into()
9}
10
11struct GenCsi {
12    tts: proc_macro2::TokenStream,
13}
14
15impl syn::parse::Parse for GenCsi {
16    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
17        let csis = syn::punctuated::Punctuated::<CsiParse, Token![;]>::parse_terminated(input)?;
18        let tts = csis
19            .iter()
20            .map(|csi| {
21                let tts = &csi.tts;
22                quote!(#tts)
23            })
24            .collect::<proc_macro2::TokenStream>();
25        Ok(GenCsi { tts })
26    }
27}
28
29struct CsiParse {
30    tts: proc_macro2::TokenStream,
31}
32
33impl syn::parse::Parse for CsiParse {
34    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
35        let visi = input.parse::<syn::Visibility>()?;
36        let nm = input.parse::<syn::Ident>()?;
37        let _fat_arrow = input.parse::<Token![=>]>()?;
38        let CsiFmtParse { doc, fmt, nms_ord } = input.parse::<CsiFmtParse>()?;
39
40        let args = {
41            #[derive(Clone)]
42            struct Arg {
43                nm: proc_macro2::Ident,
44                ty: proc_macro2::Ident,
45            }
46            let mut args = Vec::<Arg>::with_capacity(nms_ord.len());
47            for _ in 0..nms_ord.len() {
48                let _comma = input.parse::<Token![,]>()?;
49                let CsiArgParse { nm, ty } = input.parse::<CsiArgParse>()?;
50                args.push(Arg { nm, ty });
51            }
52            if nms_ord.len() != args.len() {
53                return Err(syn::Error::new(
54                    proc_macro2::Span::call_site(),
55                    "unmatch args' count",
56                ));
57            }
58            args
59        };
60
61        let arg_nms = {
62            let mut nms = Vec::<proc_macro2::Ident>::with_capacity(args.len());
63            let mut args = args.clone(); // clone `args` and reorder it according to `nms_ord`
64            for ref nm_ord in nms_ord {
65                let idx = args
66                    .iter()
67                    .enumerate()
68                    .find_map(|(i, arg)| (nm_ord == &arg.nm.to_string()).then_some(i));
69                let Some(idx) = idx else {
70                    return Err(syn::Error::new(proc_macro2::Span::call_site(), "unmatch args' name"));
71                };
72                let arg = args.remove(idx);
73                nms.push(arg.nm);
74            }
75            nms
76        };
77
78        let tts = {
79            let doc = format!("`\\x1b[{}`", doc);
80            let arg_exprs = args.iter().map(|arg| {
81                let nm = &arg.nm;
82                let ty = &arg.ty;
83                quote! { #nm: #ty }
84            });
85            let ret = {
86                if args.is_empty() {
87                    quote! { Csi(std::borrow::Cow::from(#fmt)) }
88                } else {
89                    quote! { Csi(std::borrow::Cow::from(std::format!(#fmt, #(#arg_nms,)*))) }
90                }
91            };
92            quote! {
93                #[doc = #doc]
94                #visi fn #nm (#(#arg_exprs,)*) ->  Csi<'static> {
95                    #ret
96                }
97            }
98        };
99        Ok(CsiParse { tts })
100    }
101}
102
103struct CsiArgParse {
104    nm: proc_macro2::Ident,
105    ty: proc_macro2::Ident,
106}
107
108impl syn::parse::Parse for CsiArgParse {
109    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
110        static INT_TYS: [&str; 6] = ["u8", "u16", "u32", "u64", "u128", "usize"];
111        let nm = input.parse::<proc_macro2::Ident>()?;
112        let Ok(_colon) = input.parse::<Token![:]>() else {
113            let ty = proc_macro2::Ident::new("u16", proc_macro2::Span::call_site());
114            return Ok(CsiArgParse { nm, ty })
115        };
116        let ty = input.parse::<proc_macro2::Ident>()?;
117        if !INT_TYS.contains(&ty.to_string().as_str()) {
118            let msg = format!("expect {:?}", INT_TYS);
119            return Err(syn::Error::new_spanned(ty, msg));
120        }
121        Ok(CsiArgParse { nm, ty })
122    }
123}
124
125struct CsiFmtParse {
126    doc: String,
127    fmt: proc_macro2::Literal,
128    nms_ord: Vec<String>,
129}
130
131impl syn::parse::Parse for CsiFmtParse {
132    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
133        let litstr = input.parse::<syn::LitStr>()?;
134        let s = litstr.value();
135        let mut bytes = s.bytes().peekable();
136        let mut nms_ord = Vec::<String>::new();
137        let mut docbuf = Vec::<u8>::with_capacity(s.len() * 2);
138        let mut fmtbuf = {
139            let mut v = Vec::<u8>::with_capacity(s.len() + 2);
140            v.push(b'\x1b');
141            v.push(b'[');
142            v
143        };
144        let mut nmbuf = Vec::<u8>::new();
145
146        while let Some(byte) = bytes.next() {
147            match byte {
148                b @ b'{' => {
149                    docbuf.push(b);
150                    fmtbuf.push(b);
151                    'cb: loop {
152                        match bytes.next() {
153                            None => {
154                                return Err(syn::Error::new_spanned(
155                                    litstr,
156                                    "expect a closing brace `}`",
157                                ));
158                            }
159                            Some(b @ b'{') => {
160                                fmtbuf.push(b);
161                                break 'cb;
162                            }
163                            Some(b @ b'}') => {
164                                docbuf.push(b);
165                                fmtbuf.push(b);
166                                let nm = if nmbuf.is_empty() {
167                                    return Err(syn::Error::new_spanned(
168                                        litstr,
169                                        "expect arg name inside the `{}`",
170                                    ));
171                                } else {
172                                    let bytes = nmbuf.drain(..).collect::<Vec<u8>>();
173                                    String::from_utf8(bytes).unwrap()
174                                };
175                                nms_ord.push(nm);
176                                break 'cb;
177                            }
178                            Some(b) => {
179                                docbuf.push(b);
180                                nmbuf.push(b);
181                                continue;
182                            }
183                        }
184                    }
185                }
186                b @ b'}' => {
187                    let Some(b'}') = bytes.next() else{
188                        return Err(syn::Error::new_spanned(
189                            litstr,
190                            "expect a `{` before a `}`",
191                        ));
192                    };
193                    docbuf.push(b);
194                    fmtbuf.push(b);
195                    fmtbuf.push(b);
196                }
197                b => {
198                    docbuf.push(b);
199                    fmtbuf.push(b);
200                    continue;
201                }
202            };
203        }
204        let doc = String::from_utf8(docbuf).unwrap();
205        let fmt = {
206            let s = String::from_utf8(fmtbuf).unwrap();
207            proc_macro2::Literal::string(&s)
208        };
209        Ok(CsiFmtParse { doc, fmt, nms_ord })
210    }
211}
212
213// fn snake_to_pascal(s: &str) -> Option<String> {
214//     let mut buf = Vec::<u8>::with_capacity(s.len());
215//     let mut bytes = s.bytes();
216//     let b = bytes
217//         .by_ref()
218//         .skip_while(|b| *b == b'_')
219//         .find(|b| b.is_ascii_alphabetic())?;
220//     buf.push(b.to_ascii_uppercase());
221//     let mut flag = false;
222//     for b in bytes {
223//         match b {
224//             b'_' => flag = true,
225//             b if b.is_ascii_alphanumeric() => {
226//                 let b = match flag && b.is_ascii_alphabetic() {
227//                     true => b.to_ascii_uppercase(),
228//                     false => b.to_ascii_lowercase(),
229//                 };
230//                 buf.push(b);
231//                 flag = false;
232//             }
233//             _ => return None,
234//         }
235//     }
236//     String::from_utf8(buf).ok()
237// }
238
239// =============================================================
240
241#[doc(hidden)]
242#[proc_macro]
243pub fn gen_clr_const(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
244    parse_macro_input!(input as GenClrConst).tts.into()
245}
246
247struct GenClrConst {
248    tts: proc_macro2::TokenStream,
249}
250
251impl syn::parse::Parse for GenClrConst {
252    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
253        let mut val = {
254            let lit = input.parse::<syn::LitInt>()?;
255            lit.base10_parse::<u8>()?
256        };
257        let _fat_arrow = input.parse::<Token![=>]>()?;
258        let tts = syn::punctuated::Punctuated::<syn::Expr, Token![,]>::parse_terminated(input)?
259            .into_iter()
260            .map(|expr| {
261                let ident = {
262                    let mut tts = (quote!(#expr)).into_iter();
263                    match (tts.next(), tts.next()) {
264                        (Some(ident), None) => ident,
265                        (None, Some(_)) => unreachable!(),
266                        _ => {
267                            return syn::Error::new_spanned(expr, "expect ident")
268                                .into_compile_error();
269                        }
270                    }
271                };
272                let (fg, bg) = {
273                    let s = ident.to_string();
274                    let fg = proc_macro2::Ident::new(&format!("FG_{}", s), ident.span());
275                    let bg = proc_macro2::Ident::new(&format!("BG_{}", s), ident.span());
276                    (fg, bg)
277                };
278                let (fgval, bgval) = {
279                    let fg = proc_macro2::Literal::u8_unsuffixed(val);
280                    let bg = proc_macro2::Literal::u8_unsuffixed(val + 10);
281                    val += 1;
282                    (fg, bg)
283                };
284                quote! {
285                    pub const #fg: u8 = #fgval;
286                    pub const #bg: u8 = #bgval;
287                }
288            })
289            .collect::<proc_macro2::TokenStream>();
290        Ok(GenClrConst { tts })
291    }
292}
293
294// =============================================================
295
296#[doc(hidden)]
297#[proc_macro]
298pub fn gen_sty_const(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
299    parse_macro_input!(input as GenStyConst).tts.into()
300}
301
302struct GenStyConst {
303    tts: proc_macro2::TokenStream,
304}
305
306impl syn::parse::Parse for GenStyConst {
307    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
308        let mut val = {
309            let lit = input.parse::<syn::LitInt>()?;
310            lit.base10_parse::<u8>()?
311        };
312        let tts = syn::punctuated::Punctuated::<syn::Expr, Token![,]>::parse_terminated(input)?
313            .into_iter()
314            .map(|expr| {
315                let ident = {
316                    let mut tts = (quote!(#expr)).into_iter();
317                    match (tts.next(), tts.next()) {
318                        (Some(ident), None) => ident,
319                        (None, Some(_)) => unreachable!(),
320                        _ => {
321                            return syn::Error::new_spanned(expr, "expect ident")
322                                .into_compile_error();
323                        }
324                    }
325                };
326                let (set, unset) = {
327                    let s = ident.to_string();
328                    let set = proc_macro2::Ident::new(&format!("STY_{}_SET", s), ident.span());
329                    let unset = proc_macro2::Ident::new(&format!("STY_{}_RST", s), ident.span());
330                    (set, unset)
331                };
332                let (setval, unsetval) = {
333                    let set = proc_macro2::Literal::u8_unsuffixed(val);
334                    let unset = proc_macro2::Literal::u8_unsuffixed(val + 10);
335                    val += 1;
336                    (set, unset)
337                };
338                quote! {
339                    pub const #set: u8 = #setval;
340                    pub const #unset: u8 = #unsetval;
341                }
342            })
343            .collect::<proc_macro2::TokenStream>();
344        Ok(GenStyConst { tts })
345    }
346}
347
348// =============================================================
349
350/// A macro for building [SGR][wiki-sgr].
351///
352/// `sgr!` creates [`etty::csi::Csi`](etty::csi::Csi) type.
353/// It is expected to be used in conjunction with [`etty::sgr_const`][mod-sgr-const].
354///
355/// ```rust
356/// etty::sgr!(etty::STY_BOLD_SET, etty::FG_BLU, etty::BG_RED).out();
357/// etty::out!("I'm bold and blue, my background is red");
358/// etty::sgr_rst().out();
359/// ```
360///
361/// If multiple SGR parameters to be displayed consecutively, use this macro for a shorter sequence.
362///
363/// ``` rust
364/// let sgr = etty::sgr!(etty::STY_BOLD_SET, etty::FG_BLU, etty::BG_RED).to_string();
365/// assert_eq!(sgr, "\x1b[1;34;41m");
366///
367/// let sgr = format!("{}{}{}", etty::sty_bold_set(), etty::fg_blu(), etty::bg_red());
368/// assert_eq!(sgr, "\x1b[1m\x1b[34m\x1b[41m");
369/// ````
370///
371/// [wiki-sgr]: https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters
372/// [mod-sgr-const]: etty::sgr_const
373#[proc_macro]
374pub fn sgr(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
375    parse_macro_input!(input as Sgr).tts.into()
376}
377
378struct Sgr {
379    tts: proc_macro2::TokenStream,
380}
381
382impl syn::parse::Parse for Sgr {
383    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
384        let (fmt, exprs) = {
385            let exprs =
386                syn::punctuated::Punctuated::<syn::Expr, Token![,]>::parse_terminated(input)?;
387            if exprs.is_empty() {
388                let err = syn::parse::Error::new_spanned(exprs, "expect at least one expression");
389                return Err(err);
390            };
391            let mut buf = Vec::<&str>::with_capacity(2 + (exprs.len() * 2));
392            buf.push("\x1b[");
393            for i in 0..exprs.len() {
394                buf.push("{}");
395                if i == exprs.len() - 1 {
396                    buf.push("m");
397                } else {
398                    buf.push(";");
399                }
400            }
401            (buf.concat(), exprs.into_iter())
402        };
403        let tts = quote! { etty::csi::Csi(std::borrow::Cow::from(std::format!(#fmt, #(#exprs as u8,)*))) };
404        Ok(Sgr { tts })
405    }
406}
407
408// =============================================================
409
410/// A convenience macro for writing into [`std::io::Stdout`](std::io::Stdout).
411///
412/// ```rust
413/// etty::out!("{}{}hello world! {}", etty::ers_all(), etty::cus_home(), "你好世界!👋");
414/// etty::out!(etty::cus_next_ln(1));
415/// etty::out!(42);
416/// etty::out!('\x20');
417/// etty::out!('A');
418/// etty::flush();
419/// ```
420///
421/// Please be noted that we don't need string literal as the first argument if formatting isn't necessary.
422///
423/// ```rust
424/// etty::out!(42);        // do this
425/// etty::out!("{}", 42);  // instead of this
426/// ```
427///
428#[proc_macro]
429pub fn out(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
430    struct Out(proc_macro2::TokenStream);
431    impl syn::parse::Parse for Out {
432        fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
433            let fmtargs = input.parse::<FmtArgsExprs>()?.0;
434            let tts = quote! {{
435                use std::io::Write;
436                std::write!(std::io::stdout(), #fmtargs).unwrap();
437            }};
438            Ok(Out(tts))
439        }
440    }
441    parse_macro_input!(input as Out).0.into()
442}
443
444/// Same with [`etty::macros::out!`](etty::macros::out!) but with newline.
445#[proc_macro]
446pub fn outln(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
447    struct Outln(proc_macro2::TokenStream);
448    impl syn::parse::Parse for Outln {
449        fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
450            let fmtargs = input.parse::<FmtArgsExprs>()?.0;
451            let tts = quote! {{
452                use std::io::Write;
453                std::writeln!(std::io::stdout(), #fmtargs).unwrap();
454            }};
455            Ok(Outln(tts))
456        }
457    }
458    parse_macro_input!(input as Outln).0.into()
459}
460
461/// Same with [`etty::macros::out!`](etty::macros::out!) but perform [`std::io::Stdout::flush`](std::io::Stdout::flush) immediately.
462#[proc_macro]
463pub fn outf(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
464    struct Outf(proc_macro2::TokenStream);
465    impl syn::parse::Parse for Outf {
466        fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
467            let fmtargs = input.parse::<FmtArgsExprs>()?.0;
468            let tts = quote! {{
469                use std::io::Write;
470                std::write!(std::io::stdout(), #fmtargs).unwrap();
471                std::io::stdout().flush().unwrap();
472            }};
473            Ok(Outf(tts))
474        }
475    }
476    parse_macro_input!(input as Outf).0.into()
477}
478
479struct FmtArgsExprs(proc_macro2::TokenStream);
480
481impl syn::parse::Parse for FmtArgsExprs {
482    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
483        let lead = input.parse::<syn::Expr>()?;
484        let tts = if let syn::Expr::Lit(syn::ExprLit {
485            lit: syn::Lit::Str(litstr),
486            ..
487        }) = lead
488        {
489            if !input.is_empty() {
490                let _comma = input.parse::<Token![,]>()?;
491            }
492            let exprs =
493                syn::punctuated::Punctuated::<syn::Expr, Token![,]>::parse_terminated(input)?
494                    .into_iter();
495            quote! { #litstr, #(#exprs,)* }
496        } else {
497            quote! { "{}", #lead }
498        };
499        Ok(FmtArgsExprs(tts))
500    }
501}