1use proc_macro::TokenStream;
2use proc_macro2::{Ident, Span, TokenStream as TokenStream2};
3use quote::{ToTokens, quote};
4use std::path::PathBuf;
5
6mod app_input;
7mod t_macro;
8mod theme_tokens;
9use app_input::{AppInput, preview_const_ident};
10
11#[proc_macro_derive(ThemeTokens, attributes(token, theme))]
22pub fn derive_theme_tokens(input: TokenStream) -> TokenStream {
23 let parsed = match syn::parse::<syn::DeriveInput>(input) {
24 Ok(parsed) => parsed,
25 Err(e) => return e.to_compile_error().into(),
26 };
27 match theme_tokens::expand(parsed) {
28 Ok(tokens) => tokens.into(),
29 Err(e) => e.to_compile_error().into(),
30 }
31}
32
33#[proc_macro]
39pub fn t(input: TokenStream) -> TokenStream {
40 match syn::parse::<t_macro::TInput>(input) {
41 Ok(parsed) => t_macro::expand(parsed).into(),
42 Err(e) => e.to_compile_error().into(),
43 }
44}
45
46#[proc_macro]
47pub fn app(input: TokenStream) -> TokenStream {
48 let AppInput {
49 theme_type,
50 setup,
51 config,
52 app_expr,
53 } = match syn::parse::<AppInput>(input) {
54 Ok(v) => v,
55 Err(e) => return e.to_compile_error().into(),
56 };
57
58 let theme_type_str = theme_type
60 .to_token_stream()
61 .to_string()
62 .replace(" :: ", "::");
63
64 let TranspileOutput {
65 include_stmts,
66 rerun_stmts,
67 preview_const_idents,
68 } = match transpile_project(Some(theme_type_str.as_str())) {
69 Ok(o) => o,
70 Err(err) => return err.into(),
71 };
72
73 let preview_fn = quote! {
74 pub fn telar_all_preview_entries() -> ::std::vec::Vec<::telar::PreviewEntry> {
75 let mut entries = ::std::vec::Vec::new();
76 #( entries.extend_from_slice(#preview_const_idents); )*
77 entries
78 }
79 };
80
81 let is_hot_reload = hot_reload_build();
83 let is_preview = std::env::var("TELAR_PREVIEW_BUILD").is_ok();
84
85 let run_tail = quote! {
87 if ::telar::dev_entry(
88 telar_all_preview_entries,
89 ::telar::AppConfig::from(#config),
90 || #setup,
91 ) {
92 return;
93 }
94 #setup
95 ::telar::run_app_with_name(
96 ::telar::AppConfig::from(#config),
97 #app_expr,
98 env!("CARGO_PKG_NAME"),
99 )
100 };
101
102 let hot_reload_prefix = if is_hot_reload {
103 quote! {
104 if let (::std::result::Result::Ok(lib_path), ::std::result::Result::Ok(hot_port)) = (
105 ::std::env::var("TELAR_HOT_LIB"),
106 ::std::env::var("TELAR_HOT_PORT"),
107 ) {
108 #setup
109 ::telar::run_hot_reload_host(
110 &lib_path,
111 &hot_port,
112 ::telar::AppConfig::from(#config),
113 env!("CARGO_PKG_NAME"),
114 );
115 return;
116 }
117 }
118 } else {
119 quote! {}
120 };
121
122 let desktop_run = quote! {
123 #[cfg(not(target_os = "android"))]
124 pub fn run() {
125 #hot_reload_prefix
126 #run_tail
127 }
128 };
129
130 let hot_export = if is_hot_reload {
132 let body: TokenStream2 = if is_preview {
133 quote! {
134 return ::std::boxed::Box::new(::telar::PreviewApp {
135 entries: telar_all_preview_entries(),
136 });
137 }
138 } else {
139 quote! {
140 return ::std::boxed::Box::new(#app_expr);
141 }
142 };
143 quote! {
144 #[unsafe(no_mangle)]
145 pub unsafe extern "Rust" fn _rsx_hot_create_app() -> ::std::boxed::Box<dyn ::telar::App> {
146 #setup
147 #body
148 }
149 }
150 } else {
151 quote! {}
152 };
153
154 let hot_cleanup = if is_hot_reload {
156 quote! {
157 #[unsafe(no_mangle)]
158 pub unsafe extern "Rust" fn _rsx_hot_cleanup() {
159 ::telar::motion::reset();
161 ::telar::reset_tasks();
163 ::telar::reset_runtime();
164 }
165 }
166 } else {
167 quote! {}
168 };
169
170 let hot_state_symbols = if is_hot_reload {
172 quote! {
173 #[unsafe(no_mangle)]
174 pub unsafe extern "Rust" fn _rsx_hot_snapshot() -> ::std::string::String {
175 ::telar::hot_snapshot_json()
176 }
177 #[unsafe(no_mangle)]
178 pub unsafe extern "Rust" fn _rsx_hot_restore(blob: &str) {
179 ::telar::hot_restore_json(blob);
180 }
181 }
182 } else {
183 quote! {}
184 };
185
186 let hot_tree_symbols = if is_hot_reload {
190 quote! {
191 #[unsafe(no_mangle)]
192 pub unsafe extern "Rust" fn _rsx_hot_tree_mount(
193 app: &dyn ::telar::App,
194 ) -> *mut ::telar::HotTree {
195 ::telar::HotTree::mount(app)
196 }
197 #[unsafe(no_mangle)]
198 pub unsafe extern "Rust" fn _rsx_hot_tree_release(tree: *mut ::telar::HotTree) {
199 unsafe { ::telar::HotTree::release(tree) }
200 }
201 #[unsafe(no_mangle)]
202 pub unsafe extern "Rust" fn _rsx_hot_tree_on_event(
203 tree: *mut ::telar::HotTree,
204 event: &::telar::Event,
205 ) -> bool {
206 unsafe { ::telar::HotTree::on_event(tree, event) }
207 }
208 #[unsafe(no_mangle)]
209 pub unsafe extern "Rust" fn _rsx_hot_tree_end_frame(tree: *mut ::telar::HotTree) {
210 unsafe { ::telar::HotTree::end_frame(tree) }
211 }
212 #[unsafe(no_mangle)]
213 pub unsafe extern "Rust" fn _rsx_hot_tree_paint(
214 tree: *mut ::telar::HotTree,
215 ) -> ::std::vec::Vec<::telar::DrawCommand> {
216 unsafe { ::telar::HotTree::paint(tree) }
217 }
218 #[unsafe(no_mangle)]
219 pub unsafe extern "Rust" fn _rsx_hot_tree_dirty(tree: *mut ::telar::HotTree) -> bool {
220 unsafe { ::telar::HotTree::is_dirty(tree) }
221 }
222 #[unsafe(no_mangle)]
223 pub unsafe extern "Rust" fn _rsx_hot_tree_generation(
224 tree: *mut ::telar::HotTree,
225 ) -> u64 {
226 unsafe { ::telar::HotTree::generation(tree) }
227 }
228 #[unsafe(no_mangle)]
229 pub unsafe extern "Rust" fn _rsx_hot_tree_walk(
230 tree: *mut ::telar::HotTree,
231 ) -> ::std::vec::Vec<::telar::SegmentNodeInfo> {
232 unsafe { ::telar::HotTree::walk(tree) }
233 }
234 }
235 } else {
236 quote! {}
237 };
238
239 let hot_motion_symbols = if is_hot_reload {
241 quote! {
242 #[unsafe(no_mangle)]
243 pub unsafe extern "Rust" fn _rsx_hot_motion_tick(now: ::std::time::Instant) {
244 ::telar::motion::tick(now);
245 }
246 #[unsafe(no_mangle)]
247 pub unsafe extern "Rust" fn _rsx_hot_motion_active() -> bool {
248 ::telar::motion::has_active()
249 }
250 #[unsafe(no_mangle)]
251 pub unsafe extern "Rust" fn _rsx_hot_motion_continuous() -> bool {
252 ::telar::motion::has_continuous()
253 }
254 #[unsafe(no_mangle)]
256 pub unsafe extern "Rust" fn _rsx_hot_begin_batch() {
257 ::telar::begin_batch();
258 }
259 #[unsafe(no_mangle)]
260 pub unsafe extern "Rust" fn _rsx_hot_end_batch() {
261 ::telar::end_batch();
262 }
263 #[unsafe(no_mangle)]
267 pub unsafe extern "Rust" fn _rsx_hot_relayout() {
268 ::telar::relayout_if_dirty();
269 }
270 #[unsafe(no_mangle)]
274 pub unsafe extern "Rust" fn _rsx_hot_dispatch_overlays(event: &::telar::Event) -> bool {
275 ::telar::dispatch_overlays(event)
276 }
277 #[unsafe(no_mangle)]
280 pub unsafe extern "Rust" fn _rsx_hot_set_system_dark(dark: bool) {
281 ::telar::set_system_dark(dark);
282 }
283 #[unsafe(no_mangle)]
287 pub unsafe extern "Rust" fn _rsx_hot_drain_window_commands()
288 -> ::std::vec::Vec<::telar::WindowCommand> {
289 ::telar::take_window_commands()
290 }
291 #[unsafe(no_mangle)]
295 pub unsafe extern "Rust" fn _rsx_hot_drain_tasks() {
296 ::telar::drain_tasks();
297 }
298 #[unsafe(no_mangle)]
301 pub unsafe extern "Rust" fn _rsx_hot_install_task_waker(waker: ::telar::RedrawWaker) {
302 ::telar::set_task_waker(move || waker.wake());
303 }
304 }
305 } else {
306 quote! {}
307 };
308
309 let android_run = quote! {
310 #[cfg(target_os = "android")]
311 #[unsafe(no_mangle)]
312 fn android_main(android_app: ::telar::AndroidApp) {
313 #setup
314 ::telar::run_android_app_with_name(
315 ::telar::AppConfig::from(#config),
316 #app_expr,
317 env!("CARGO_PKG_NAME"),
318 android_app,
319 );
320 }
321 };
322
323 quote! {
324 #rerun_stmts
325 #include_stmts
326 #preview_fn
327 #desktop_run
328 #android_run
329 #hot_export
330 #hot_cleanup
331 #hot_state_symbols
332 #hot_tree_symbols
333 #hot_motion_symbols
334 }
335 .into()
336}
337
338fn hot_reload_build() -> bool {
340 std::env::var("TELAR_HOT_RELOAD_BUILD").is_ok()
341}
342
343struct TranspileOutput {
344 include_stmts: TokenStream2,
345 rerun_stmts: TokenStream2,
346 preview_const_idents: Vec<Ident>,
347}
348
349fn transpile_project(theme_type_str: Option<&str>) -> Result<TranspileOutput, TokenStream2> {
355 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")
356 .map(PathBuf::from)
357 .map_err(|_| quote! { compile_error!("CARGO_MANIFEST_DIR not set") })?;
358
359 let flavour = if hot_reload_build() {
361 "build-hot"
362 } else {
363 "build"
364 };
365 let generated_dir = manifest_dir.join(".telar").join(flavour);
366 if let Err(e) = std::fs::create_dir_all(&generated_dir) {
367 let msg = format!("Failed to create {}: {e}", generated_dir.display());
368 return Err(quote! { compile_error!(#msg) });
369 }
370
371 let src_dir = manifest_dir.join("src");
372 let rsx_files = telar_transpiler::find_rsx_files(&src_dir);
373 let assets_root = telar_transpiler::assets_root(&manifest_dir);
376
377 let telar_transpiler::ProjectComponents {
381 registry,
382 borrowed: borrowed_files,
383 collision,
384 } = telar_transpiler::build_component_registry(
385 &src_dir,
386 &telar_transpiler::component_paths(&manifest_dir),
387 &[],
388 );
389 if let Some(msg) = collision {
390 return Err(quote! { compile_error!(#msg) });
391 }
392
393 let mut include_stmts = TokenStream2::new();
394 let mut rerun_stmts = TokenStream2::new();
395 let mut preview_const_idents: Vec<Ident> = Vec::new();
396 let mut written_files: std::collections::HashSet<PathBuf> = std::collections::HashSet::new();
398
399 for rsx_file in &rsx_files {
400 let source = match std::fs::read_to_string(rsx_file) {
401 Ok(s) => s,
402 Err(e) => {
403 let msg = format!("Failed to read {}: {e}", rsx_file.display());
404 return Err(quote! { compile_error!(#msg) });
405 }
406 };
407
408 let stem = telar_transpiler::relative_stem(rsx_file, &src_dir);
409
410 let result = match telar_transpiler::transpile_source(
411 &source,
412 &stem,
413 theme_type_str,
414 Some(assets_root.as_path()),
415 Some(®istry),
416 ) {
417 Ok(r) => r,
418 Err(telar_transpiler::TranspileError::Parse(ref pe)) => {
419 let msg = format!("{}:{}: {}", rsx_file.display(), pe.line, pe.message);
420 return Err(quote! { compile_error!(#msg) });
421 }
422 Err(e) => {
423 let msg = format!("Failed to transpile {}: {e}", rsx_file.display());
424 return Err(quote! { compile_error!(#msg) });
425 }
426 };
427
428 let Some(rel_out) = telar_transpiler::relative_output_path(rsx_file, &src_dir) else {
430 continue;
431 };
432 let out_path = generated_dir.join(rel_out);
433 written_files.insert(out_path.clone());
434 if let Some(parent) = out_path.parent() {
435 if let Err(e) = std::fs::create_dir_all(parent) {
436 let msg = format!("Failed to create {}: {e}", parent.display());
437 return Err(quote! { compile_error!(#msg) });
438 }
439 }
440
441 let needs_write = std::fs::read_to_string(&out_path)
443 .map(|existing| existing != result.rust_code)
444 .unwrap_or(true);
445 if needs_write {
446 if let Err(e) = std::fs::write(&out_path, &result.rust_code) {
447 let msg = format!("Failed to write {}: {e}", out_path.display());
448 return Err(quote! { compile_error!(#msg) });
449 }
450 }
451
452 let map_path = out_path.with_extension("rs.map");
456 let map_json =
457 telar_transpiler::SourceMap::new(result.source_map.clone(), result.expr_spans.clone())
458 .to_json();
459 let map_stale = std::fs::read_to_string(&map_path)
460 .map(|existing| existing != map_json)
461 .unwrap_or(true);
462 if map_stale {
463 let _ = std::fs::write(&map_path, &map_json);
464 }
465
466 let out_path_str = out_path.to_string_lossy().to_string();
470 let mod_ident = Ident::new(
471 &format!(
472 "__rsx_mod_{}",
473 telar_transpiler::naming::to_snake_case(&stem)
474 ),
475 Span::call_site(),
476 );
477 include_stmts.extend(quote! {
478 #[path = #out_path_str]
479 mod #mod_ident;
480 #[allow(unused_imports)]
481 pub use #mod_ident::*;
482 });
483
484 let base_name = rsx_file
488 .file_stem()
489 .map(|s| s.to_string_lossy().to_string())
490 .unwrap_or_default();
491 let base_fn = telar_transpiler::naming::to_snake_case(&base_name);
492 let full_fn = telar_transpiler::naming::to_snake_case(&stem);
493 if !base_fn.is_empty() && base_fn != full_fn {
494 let full_fn_ident = Ident::new(&full_fn, Span::call_site());
495 let base_fn_ident = Ident::new(&base_fn, Span::call_site());
496 include_stmts.extend(quote! {
497 #[allow(unused_imports)]
498 pub use #mod_ident::#full_fn_ident as #base_fn_ident;
499 });
500 if result.has_props {
501 let full_props = Ident::new(
502 &(telar_transpiler::naming::to_pascal_case(&full_fn) + "Props"),
503 Span::call_site(),
504 );
505 let base_props = Ident::new(
506 &(telar_transpiler::naming::to_pascal_case(&base_fn) + "Props"),
507 Span::call_site(),
508 );
509 include_stmts.extend(quote! {
510 #[allow(unused_imports)]
511 pub use #mod_ident::#full_props as #base_props;
512 });
513 }
514 }
515
516 let rsx_path_str = rsx_file.to_string_lossy().to_string();
517 rerun_stmts.extend(quote! { const _: &str = include_str!(#rsx_path_str); });
518
519 if !result.preview_names.is_empty() {
520 preview_const_idents.push(preview_const_ident(&stem));
521 }
522 }
523
524 for rsx_file in &borrowed_files {
528 let borrowed_str = rsx_file.to_string_lossy().to_string();
529 rerun_stmts.extend(quote! { const _: &str = include_str!(#borrowed_str); });
530 }
531
532 let telar_toml = manifest_dir.join("telar.toml");
533 if telar_toml.exists() {
534 let telar_toml_str = telar_toml.to_string_lossy().to_string();
536 rerun_stmts.extend(quote! { const _: &str = include_str!(#telar_toml_str); });
537 }
538 if telar_transpiler::auto_modules_enabled(&manifest_dir) {
539 let modtree_dir = generated_dir.join("__modules");
542 if let Err(e) = std::fs::create_dir_all(&modtree_dir) {
543 let msg = format!("Failed to create {}: {e}", modtree_dir.display());
544 return Err(quote! { compile_error!(#msg) });
545 }
546 let (modules_src, modtree_written) =
547 match telar_transpiler::discover_rust_modules(&src_dir, &modtree_dir) {
548 Ok(s) => s,
549 Err(e) => {
550 let msg = format!("Failed to write the auto-discovered module tree: {e}");
551 return Err(quote! { compile_error!(#msg) });
552 }
553 };
554 written_files.extend(modtree_written);
555 match modules_src.parse::<TokenStream2>() {
556 Ok(tokens) => include_stmts.extend(tokens),
557 Err(e) => {
558 let msg = format!("Failed to emit auto-discovered modules: {e}");
559 return Err(quote! { compile_error!(#msg) });
560 }
561 }
562 }
563
564 match telar_transpiler::parse_catalog(&manifest_dir) {
568 Ok(Some(catalog)) => {
569 let src = telar_transpiler::bake_catalog_to_source(&catalog);
570 let out_path = generated_dir.join("__i18n.rs");
571 written_files.insert(out_path.clone());
572 let needs_write = std::fs::read_to_string(&out_path)
573 .map(|existing| existing != src)
574 .unwrap_or(true);
575 if needs_write && let Err(e) = std::fs::write(&out_path, &src) {
576 let msg = format!("Failed to write {}: {e}", out_path.display());
577 return Err(quote! { compile_error!(#msg) });
578 }
579 let out_path_str = out_path.to_string_lossy().to_string();
580 let mod_ident = Ident::new(telar_transpiler::I18N_MODULE, Span::call_site());
581 include_stmts.extend(quote! {
582 #[path = #out_path_str]
583 #[allow(dead_code)]
584 pub mod #mod_ident;
585 });
586 for file in telar_transpiler::catalog_files(&manifest_dir) {
588 let path_str = file.to_string_lossy().to_string();
589 rerun_stmts.extend(quote! { const _: &str = include_str!(#path_str); });
590 }
591 }
592 Ok(None) => {}
593 Err(msg) => return Err(quote! { compile_error!(#msg) }),
594 }
595
596 telar_transpiler::prune_stale_generated(&generated_dir, &written_files);
598
599 Ok(TranspileOutput {
600 include_stmts,
601 rerun_stmts,
602 preview_const_idents,
603 })
604}
605
606#[proc_macro]
613pub fn rsx_modules(input: TokenStream) -> TokenStream {
614 let theme_type_str = if input.is_empty() {
615 None
616 } else {
617 match syn::parse::<syn::Path>(input) {
618 Ok(path) => Some(path.to_token_stream().to_string().replace(" :: ", "::")),
619 Err(e) => return e.to_compile_error().into(),
620 }
621 };
622 let TranspileOutput {
623 include_stmts,
624 rerun_stmts,
625 preview_const_idents,
626 } = match transpile_project(theme_type_str.as_deref()) {
627 Ok(o) => o,
628 Err(err) => return err.into(),
629 };
630 let preview_fn = quote! {
631 pub fn telar_all_preview_entries() -> ::std::vec::Vec<::telar::PreviewEntry> {
632 let mut entries = ::std::vec::Vec::new();
633 #( entries.extend_from_slice(#preview_const_idents); )*
634 entries
635 }
636 };
637 quote! {
638 #rerun_stmts
639 #include_stmts
640 #preview_fn
641 }
642 .into()
643}