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;
8use app_input::{AppInput, preview_const_ident};
9
10#[proc_macro]
16pub fn t(input: TokenStream) -> TokenStream {
17 match syn::parse::<t_macro::TInput>(input) {
18 Ok(parsed) => t_macro::expand(parsed).into(),
19 Err(e) => e.to_compile_error().into(),
20 }
21}
22
23#[proc_macro]
24pub fn app(input: TokenStream) -> TokenStream {
25 let AppInput {
26 theme_type,
27 setup,
28 config,
29 app_expr,
30 } = match syn::parse::<AppInput>(input) {
31 Ok(v) => v,
32 Err(e) => return e.to_compile_error().into(),
33 };
34
35 let theme_type_str = theme_type
37 .to_token_stream()
38 .to_string()
39 .replace(" :: ", "::");
40
41 let TranspileOutput {
42 include_stmts,
43 rerun_stmts,
44 preview_const_idents,
45 } = match transpile_project(Some(theme_type_str.as_str())) {
46 Ok(o) => o,
47 Err(err) => return err.into(),
48 };
49
50 let preview_fn = quote! {
51 pub fn telar_all_preview_entries() -> ::std::vec::Vec<::telar::PreviewEntry> {
52 let mut entries = ::std::vec::Vec::new();
53 #( entries.extend_from_slice(#preview_const_idents); )*
54 entries
55 }
56 };
57
58 let is_hot_reload = std::env::var("TELAR_HOT_RELOAD_BUILD").is_ok();
60 let is_preview = std::env::var("TELAR_PREVIEW_BUILD").is_ok();
61
62 let run_tail = quote! {
63 #setup
64 if ::std::env::var("TELAR_PREVIEW_LIST").is_ok() {
65 for entry in telar_all_preview_entries() {
66 ::std::println!("{}\t{}", entry.component_name, entry.preview_name);
67 }
68 ::std::process::exit(0);
69 }
70 if ::std::env::var("TELAR_TEST").is_ok() {
71 ::telar::try_run_test(telar_all_preview_entries(), ::telar::AppConfig::from(#config));
72 }
73 if ::std::env::var("TELAR_PREVIEW").is_ok() {
74 if ::telar::try_run_preview(telar_all_preview_entries(), ::telar::AppConfig::from(#config)) {
75 return;
76 }
77 }
78 ::telar::run_app_with_name(
79 ::telar::AppConfig::from(#config),
80 #app_expr,
81 env!("CARGO_PKG_NAME"),
82 )
83 };
84
85 let hot_reload_prefix = if is_hot_reload {
86 quote! {
87 if let (::std::result::Result::Ok(lib_path), ::std::result::Result::Ok(hot_port)) = (
88 ::std::env::var("TELAR_HOT_LIB"),
89 ::std::env::var("TELAR_HOT_PORT"),
90 ) {
91 #setup
92 ::telar::run_hot_reload_host(
93 &lib_path,
94 &hot_port,
95 ::telar::AppConfig::from(#config),
96 env!("CARGO_PKG_NAME"),
97 );
98 return;
99 }
100 }
101 } else {
102 quote! {}
103 };
104
105 let desktop_run = quote! {
106 #[cfg(not(target_os = "android"))]
107 pub fn run() {
108 #hot_reload_prefix
109 #run_tail
110 }
111 };
112
113 let hot_export = if is_hot_reload {
115 let body: TokenStream2 = if is_preview {
116 quote! {
117 return ::telar::make_hot_preview_app(telar_all_preview_entries());
118 }
119 } else {
120 quote! {
121 return ::std::boxed::Box::new(#app_expr);
122 }
123 };
124 quote! {
125 #[unsafe(no_mangle)]
126 pub unsafe extern "Rust" fn _rsx_hot_create_app() -> ::std::boxed::Box<dyn ::telar::App> {
127 #setup
128 #body
129 }
130 }
131 } else {
132 quote! {}
133 };
134
135 let hot_cleanup = if is_hot_reload {
137 quote! {
138 #[unsafe(no_mangle)]
139 pub unsafe extern "Rust" fn _rsx_hot_cleanup() {
140 ::telar::motion::reset();
142 ::telar::reset_runtime();
143 }
144 }
145 } else {
146 quote! {}
147 };
148
149 let hot_state_symbols = if is_hot_reload {
151 quote! {
152 #[unsafe(no_mangle)]
153 pub unsafe extern "Rust" fn _rsx_hot_snapshot() -> ::std::string::String {
154 ::telar::hot_snapshot_json()
155 }
156 #[unsafe(no_mangle)]
157 pub unsafe extern "Rust" fn _rsx_hot_restore(blob: &str) {
158 ::telar::hot_restore_json(blob);
159 }
160 }
161 } else {
162 quote! {}
163 };
164
165 let hot_tree_symbols = if is_hot_reload {
169 quote! {
170 #[unsafe(no_mangle)]
171 pub unsafe extern "Rust" fn _rsx_hot_tree_mount(
172 app: &dyn ::telar::App,
173 ) -> *mut ::telar::HotTree {
174 ::telar::HotTree::mount(app)
175 }
176 #[unsafe(no_mangle)]
177 pub unsafe extern "Rust" fn _rsx_hot_tree_release(tree: *mut ::telar::HotTree) {
178 unsafe { ::telar::HotTree::release(tree) }
179 }
180 #[unsafe(no_mangle)]
181 pub unsafe extern "Rust" fn _rsx_hot_tree_on_event(
182 tree: *mut ::telar::HotTree,
183 event: &::telar::Event,
184 ) -> bool {
185 unsafe { ::telar::HotTree::on_event(tree, event) }
186 }
187 #[unsafe(no_mangle)]
188 pub unsafe extern "Rust" fn _rsx_hot_tree_paint(
189 tree: *mut ::telar::HotTree,
190 ) -> ::std::vec::Vec<::telar::DrawCommand> {
191 unsafe { ::telar::HotTree::paint(tree) }
192 }
193 #[unsafe(no_mangle)]
194 pub unsafe extern "Rust" fn _rsx_hot_tree_dirty(tree: *mut ::telar::HotTree) -> bool {
195 unsafe { ::telar::HotTree::is_dirty(tree) }
196 }
197 #[unsafe(no_mangle)]
198 pub unsafe extern "Rust" fn _rsx_hot_tree_generation(
199 tree: *mut ::telar::HotTree,
200 ) -> u64 {
201 unsafe { ::telar::HotTree::generation(tree) }
202 }
203 #[unsafe(no_mangle)]
204 pub unsafe extern "Rust" fn _rsx_hot_tree_walk(
205 tree: *mut ::telar::HotTree,
206 ) -> ::std::vec::Vec<::telar::SegmentNodeInfo> {
207 unsafe { ::telar::HotTree::walk(tree) }
208 }
209 }
210 } else {
211 quote! {}
212 };
213
214 let hot_motion_symbols = if is_hot_reload {
216 quote! {
217 #[unsafe(no_mangle)]
218 pub unsafe extern "Rust" fn _rsx_hot_motion_tick(now: ::std::time::Instant) {
219 ::telar::motion::tick(now);
220 }
221 #[unsafe(no_mangle)]
222 pub unsafe extern "Rust" fn _rsx_hot_motion_active() -> bool {
223 ::telar::motion::has_active()
224 }
225 #[unsafe(no_mangle)]
227 pub unsafe extern "Rust" fn _rsx_hot_begin_batch() {
228 ::telar::begin_batch();
229 }
230 #[unsafe(no_mangle)]
231 pub unsafe extern "Rust" fn _rsx_hot_end_batch() {
232 ::telar::end_batch();
233 }
234 #[unsafe(no_mangle)]
238 pub unsafe extern "Rust" fn _rsx_hot_relayout() {
239 ::telar::relayout_if_dirty();
240 }
241 #[unsafe(no_mangle)]
245 pub unsafe extern "Rust" fn _rsx_hot_dispatch_overlays(event: &::telar::Event) -> bool {
246 ::telar::dispatch_overlays(event)
247 }
248 #[unsafe(no_mangle)]
251 pub unsafe extern "Rust" fn _rsx_hot_set_system_dark(dark: bool) {
252 ::telar::set_system_dark(dark);
253 }
254 #[unsafe(no_mangle)]
258 pub unsafe extern "Rust" fn _rsx_hot_drain_window_commands()
259 -> ::std::vec::Vec<::telar::WindowCommand> {
260 ::telar::take_window_commands()
261 }
262 }
263 } else {
264 quote! {}
265 };
266
267 let android_run = quote! {
268 #[cfg(target_os = "android")]
269 #[unsafe(no_mangle)]
270 fn android_main(android_app: ::telar::AndroidApp) {
271 #setup
272 ::telar::run_android_app_with_name(
273 ::telar::AppConfig::from(#config),
274 #app_expr,
275 env!("CARGO_PKG_NAME"),
276 android_app,
277 );
278 }
279 };
280
281 quote! {
282 #rerun_stmts
283 #include_stmts
284 #preview_fn
285 #desktop_run
286 #android_run
287 #hot_export
288 #hot_cleanup
289 #hot_state_symbols
290 #hot_tree_symbols
291 #hot_motion_symbols
292 }
293 .into()
294}
295
296struct TranspileOutput {
297 include_stmts: TokenStream2,
298 rerun_stmts: TokenStream2,
299 preview_const_idents: Vec<Ident>,
300}
301
302fn transpile_project(theme_type_str: Option<&str>) -> Result<TranspileOutput, TokenStream2> {
308 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")
309 .map(PathBuf::from)
310 .map_err(|_| quote! { compile_error!("CARGO_MANIFEST_DIR not set") })?;
311
312 let generated_dir = manifest_dir.join(".telar").join("build");
313 if let Err(e) = std::fs::create_dir_all(&generated_dir) {
314 let msg = format!("Failed to create .telar/build/: {e}");
315 return Err(quote! { compile_error!(#msg) });
316 }
317
318 let src_dir = manifest_dir.join("src");
319 let rsx_files = telar_transpiler::find_rsx_files(&src_dir);
320 let assets_root = telar_transpiler::assets_root(&manifest_dir);
323
324 let mut registry = telar_transpiler::ComponentRegistry::new();
328 for (name, sig) in telar_transpiler::external_component_sigs() {
330 registry.insert(name.to_string(), sig);
331 }
332 for rsx_file in &rsx_files {
333 let Ok(source) = std::fs::read_to_string(rsx_file) else {
334 continue;
335 };
336 let sig = telar_transpiler::scan_component_sig(&source);
337 let stem = telar_transpiler::relative_stem(rsx_file, &src_dir);
338 registry.insert(telar_transpiler::naming::to_snake_case(&stem), sig.clone());
339 if let Some(base) = rsx_file.file_stem().and_then(|s| s.to_str()) {
340 registry
341 .entry(telar_transpiler::naming::to_snake_case(base))
342 .or_insert(sig);
343 }
344 }
345
346 let mut include_stmts = TokenStream2::new();
347 let mut rerun_stmts = TokenStream2::new();
348 let mut preview_const_idents: Vec<Ident> = Vec::new();
349
350 for rsx_file in &rsx_files {
351 let source = match std::fs::read_to_string(rsx_file) {
352 Ok(s) => s,
353 Err(e) => {
354 let msg = format!("Failed to read {}: {e}", rsx_file.display());
355 return Err(quote! { compile_error!(#msg) });
356 }
357 };
358
359 let stem = telar_transpiler::relative_stem(rsx_file, &src_dir);
360
361 let result = match telar_transpiler::transpile_source_full(
362 &source,
363 &stem,
364 theme_type_str,
365 Some(assets_root.as_path()),
366 Some(®istry),
367 ) {
368 Ok(r) => r,
369 Err(telar_transpiler::TranspileError::Parse(ref pe)) => {
370 let msg = format!("{}:{}: {}", rsx_file.display(), pe.line, pe.message);
371 return Err(quote! { compile_error!(#msg) });
372 }
373 Err(e) => {
374 let msg = format!("Failed to transpile {}: {e}", rsx_file.display());
375 return Err(quote! { compile_error!(#msg) });
376 }
377 };
378
379 let Some(rel_out) = telar_transpiler::relative_output_path(rsx_file, &src_dir) else {
381 continue;
382 };
383 let out_path = generated_dir.join(rel_out);
384 if let Some(parent) = out_path.parent() {
385 if let Err(e) = std::fs::create_dir_all(parent) {
386 let msg = format!("Failed to create {}: {e}", parent.display());
387 return Err(quote! { compile_error!(#msg) });
388 }
389 }
390
391 let needs_write = std::fs::read_to_string(&out_path)
393 .map(|existing| existing != result.rust_code)
394 .unwrap_or(true);
395 if needs_write {
396 if let Err(e) = std::fs::write(&out_path, &result.rust_code) {
397 let msg = format!("Failed to write {}: {e}", out_path.display());
398 return Err(quote! { compile_error!(#msg) });
399 }
400 }
401
402 let map_path = out_path.with_extension("rs.map");
405 let map_json = telar_transpiler::source_map_to_json(&result.source_map);
406 let map_stale = std::fs::read_to_string(&map_path)
407 .map(|existing| existing != map_json)
408 .unwrap_or(true);
409 if map_stale {
410 let _ = std::fs::write(&map_path, &map_json);
411 }
412
413 let out_path_str = out_path.to_string_lossy().to_string();
417 let mod_ident = Ident::new(
418 &format!("__rsx_mod_{}", telar_transpiler::naming::to_snake_case(&stem)),
419 Span::call_site(),
420 );
421 include_stmts.extend(quote! {
422 #[path = #out_path_str]
423 mod #mod_ident;
424 #[allow(unused_imports)]
425 pub use #mod_ident::*;
426 });
427
428 let base_name = rsx_file
432 .file_stem()
433 .map(|s| s.to_string_lossy().to_string())
434 .unwrap_or_default();
435 let base_fn = telar_transpiler::naming::to_snake_case(&base_name);
436 let full_fn = telar_transpiler::naming::to_snake_case(&stem);
437 if !base_fn.is_empty() && base_fn != full_fn {
438 let full_fn_ident = Ident::new(&full_fn, Span::call_site());
439 let base_fn_ident = Ident::new(&base_fn, Span::call_site());
440 include_stmts.extend(quote! {
441 #[allow(unused_imports)]
442 pub use #mod_ident::#full_fn_ident as #base_fn_ident;
443 });
444 if result.has_props {
445 let full_props = Ident::new(
446 &(telar_transpiler::naming::to_pascal_case(&full_fn) + "Props"),
447 Span::call_site(),
448 );
449 let base_props = Ident::new(
450 &(telar_transpiler::naming::to_pascal_case(&base_fn) + "Props"),
451 Span::call_site(),
452 );
453 include_stmts.extend(quote! {
454 #[allow(unused_imports)]
455 pub use #mod_ident::#full_props as #base_props;
456 });
457 }
458 }
459
460 let rsx_path_str = rsx_file.to_string_lossy().to_string();
461 rerun_stmts.extend(quote! { const _: &str = include_str!(#rsx_path_str); });
462
463 if !result.preview_names.is_empty() {
464 preview_const_idents.push(preview_const_ident(&stem));
465 }
466 }
467
468 let telar_toml = manifest_dir.join("telar.toml");
471 if telar_toml.exists() {
472 let telar_toml_str = telar_toml.to_string_lossy().to_string();
474 rerun_stmts.extend(quote! { const _: &str = include_str!(#telar_toml_str); });
475 }
476 if telar_transpiler::auto_modules_enabled(&manifest_dir) {
477 let modtree_dir = generated_dir.join("__modules");
480 if let Err(e) = std::fs::create_dir_all(&modtree_dir) {
481 let msg = format!("Failed to create .telar/build/__modules/: {e}");
482 return Err(quote! { compile_error!(#msg) });
483 }
484 let modules_src = match telar_transpiler::discover_rust_modules(&src_dir, &modtree_dir) {
485 Ok(s) => s,
486 Err(e) => {
487 let msg = format!("Failed to write the auto-discovered module tree: {e}");
488 return Err(quote! { compile_error!(#msg) });
489 }
490 };
491 match modules_src.parse::<TokenStream2>() {
492 Ok(tokens) => include_stmts.extend(tokens),
493 Err(e) => {
494 let msg = format!("Failed to emit auto-discovered modules: {e}");
495 return Err(quote! { compile_error!(#msg) });
496 }
497 }
498 }
499
500 match telar_transpiler::parse_catalog(&manifest_dir) {
504 Ok(Some(catalog)) => {
505 let src = telar_transpiler::bake_catalog_to_source(&catalog);
506 let out_path = generated_dir.join("__i18n.rs");
507 let needs_write = std::fs::read_to_string(&out_path)
508 .map(|existing| existing != src)
509 .unwrap_or(true);
510 if needs_write && let Err(e) = std::fs::write(&out_path, &src) {
511 let msg = format!("Failed to write {}: {e}", out_path.display());
512 return Err(quote! { compile_error!(#msg) });
513 }
514 let out_path_str = out_path.to_string_lossy().to_string();
515 let mod_ident = Ident::new(telar_transpiler::I18N_MODULE, Span::call_site());
516 include_stmts.extend(quote! {
517 #[path = #out_path_str]
518 #[allow(dead_code)]
519 pub mod #mod_ident;
520 });
521 for file in telar_transpiler::catalog_files(&manifest_dir) {
523 let path_str = file.to_string_lossy().to_string();
524 rerun_stmts.extend(quote! { const _: &str = include_str!(#path_str); });
525 }
526 }
527 Ok(None) => {}
528 Err(msg) => return Err(quote! { compile_error!(#msg) }),
529 }
530
531 Ok(TranspileOutput {
532 include_stmts,
533 rerun_stmts,
534 preview_const_idents,
535 })
536}
537
538#[proc_macro]
545pub fn rsx_modules(input: TokenStream) -> TokenStream {
546 let theme_type_str = if input.is_empty() {
547 None
548 } else {
549 match syn::parse::<syn::Path>(input) {
550 Ok(path) => Some(path.to_token_stream().to_string().replace(" :: ", "::")),
551 Err(e) => return e.to_compile_error().into(),
552 }
553 };
554 let TranspileOutput {
555 include_stmts,
556 rerun_stmts,
557 preview_const_idents,
558 } = match transpile_project(theme_type_str.as_deref()) {
559 Ok(o) => o,
560 Err(err) => return err.into(),
561 };
562 let preview_fn = quote! {
563 pub fn telar_all_preview_entries() -> ::std::vec::Vec<::telar::PreviewEntry> {
564 let mut entries = ::std::vec::Vec::new();
565 #( entries.extend_from_slice(#preview_const_idents); )*
566 entries
567 }
568 };
569 quote! {
570 #rerun_stmts
571 #include_stmts
572 #preview_fn
573 }
574 .into()
575}