use proc_macro::TokenStream;
use proc_macro2::{Ident, Span, TokenStream as TokenStream2};
use quote::{ToTokens, quote};
use std::path::PathBuf;
mod app_input;
mod t_macro;
use app_input::{AppInput, preview_const_ident};
#[proc_macro]
pub fn t(input: TokenStream) -> TokenStream {
match syn::parse::<t_macro::TInput>(input) {
Ok(parsed) => t_macro::expand(parsed).into(),
Err(e) => e.to_compile_error().into(),
}
}
#[proc_macro]
pub fn app(input: TokenStream) -> TokenStream {
let AppInput {
theme_type,
setup,
config,
app_expr,
} = match syn::parse::<AppInput>(input) {
Ok(v) => v,
Err(e) => return e.to_compile_error().into(),
};
let theme_type_str = theme_type
.to_token_stream()
.to_string()
.replace(" :: ", "::");
let TranspileOutput {
include_stmts,
rerun_stmts,
preview_const_idents,
} = match transpile_project(Some(theme_type_str.as_str())) {
Ok(o) => o,
Err(err) => return err.into(),
};
let preview_fn = quote! {
pub fn telar_all_preview_entries() -> ::std::vec::Vec<::telar::PreviewEntry> {
let mut entries = ::std::vec::Vec::new();
#( entries.extend_from_slice(#preview_const_idents); )*
entries
}
};
let is_hot_reload = std::env::var("TELAR_HOT_RELOAD_BUILD").is_ok();
let is_preview = std::env::var("TELAR_PREVIEW_BUILD").is_ok();
let run_tail = quote! {
#setup
if ::std::env::var("TELAR_PREVIEW_LIST").is_ok() {
for entry in telar_all_preview_entries() {
::std::println!("{}\t{}", entry.component_name, entry.preview_name);
}
::std::process::exit(0);
}
if ::std::env::var("TELAR_TEST").is_ok() {
::telar::try_run_test(telar_all_preview_entries(), ::telar::AppConfig::from(#config));
}
if ::std::env::var("TELAR_PREVIEW").is_ok() {
if ::telar::try_run_preview(telar_all_preview_entries(), ::telar::AppConfig::from(#config)) {
return;
}
}
::telar::run_app_with_name(
::telar::AppConfig::from(#config),
#app_expr,
env!("CARGO_PKG_NAME"),
)
};
let hot_reload_prefix = if is_hot_reload {
quote! {
if let (::std::result::Result::Ok(lib_path), ::std::result::Result::Ok(hot_port)) = (
::std::env::var("TELAR_HOT_LIB"),
::std::env::var("TELAR_HOT_PORT"),
) {
#setup
::telar::run_hot_reload_host(
&lib_path,
&hot_port,
::telar::AppConfig::from(#config),
env!("CARGO_PKG_NAME"),
);
return;
}
}
} else {
quote! {}
};
let desktop_run = quote! {
#[cfg(not(target_os = "android"))]
pub fn run() {
#hot_reload_prefix
#run_tail
}
};
let hot_export = if is_hot_reload {
let body: TokenStream2 = if is_preview {
quote! {
return ::telar::make_hot_preview_app(telar_all_preview_entries());
}
} else {
quote! {
return ::std::boxed::Box::new(#app_expr);
}
};
quote! {
#[unsafe(no_mangle)]
pub unsafe extern "Rust" fn _rsx_hot_create_app() -> ::std::boxed::Box<dyn ::telar::App> {
#setup
#body
}
}
} else {
quote! {}
};
let hot_cleanup = if is_hot_reload {
quote! {
#[unsafe(no_mangle)]
pub unsafe extern "Rust" fn _rsx_hot_cleanup() {
::telar::motion::reset();
::telar::reset_runtime();
}
}
} else {
quote! {}
};
let hot_state_symbols = if is_hot_reload {
quote! {
#[unsafe(no_mangle)]
pub unsafe extern "Rust" fn _rsx_hot_snapshot() -> ::std::string::String {
::telar::hot_snapshot_json()
}
#[unsafe(no_mangle)]
pub unsafe extern "Rust" fn _rsx_hot_restore(blob: &str) {
::telar::hot_restore_json(blob);
}
}
} else {
quote! {}
};
let hot_tree_symbols = if is_hot_reload {
quote! {
#[unsafe(no_mangle)]
pub unsafe extern "Rust" fn _rsx_hot_tree_mount(
app: &dyn ::telar::App,
) -> *mut ::telar::HotTree {
::telar::HotTree::mount(app)
}
#[unsafe(no_mangle)]
pub unsafe extern "Rust" fn _rsx_hot_tree_release(tree: *mut ::telar::HotTree) {
unsafe { ::telar::HotTree::release(tree) }
}
#[unsafe(no_mangle)]
pub unsafe extern "Rust" fn _rsx_hot_tree_on_event(
tree: *mut ::telar::HotTree,
event: &::telar::Event,
) -> bool {
unsafe { ::telar::HotTree::on_event(tree, event) }
}
#[unsafe(no_mangle)]
pub unsafe extern "Rust" fn _rsx_hot_tree_paint(
tree: *mut ::telar::HotTree,
) -> ::std::vec::Vec<::telar::DrawCommand> {
unsafe { ::telar::HotTree::paint(tree) }
}
#[unsafe(no_mangle)]
pub unsafe extern "Rust" fn _rsx_hot_tree_dirty(tree: *mut ::telar::HotTree) -> bool {
unsafe { ::telar::HotTree::is_dirty(tree) }
}
#[unsafe(no_mangle)]
pub unsafe extern "Rust" fn _rsx_hot_tree_generation(
tree: *mut ::telar::HotTree,
) -> u64 {
unsafe { ::telar::HotTree::generation(tree) }
}
#[unsafe(no_mangle)]
pub unsafe extern "Rust" fn _rsx_hot_tree_walk(
tree: *mut ::telar::HotTree,
) -> ::std::vec::Vec<::telar::SegmentNodeInfo> {
unsafe { ::telar::HotTree::walk(tree) }
}
}
} else {
quote! {}
};
let hot_motion_symbols = if is_hot_reload {
quote! {
#[unsafe(no_mangle)]
pub unsafe extern "Rust" fn _rsx_hot_motion_tick(now: ::std::time::Instant) {
::telar::motion::tick(now);
}
#[unsafe(no_mangle)]
pub unsafe extern "Rust" fn _rsx_hot_motion_active() -> bool {
::telar::motion::has_active()
}
#[unsafe(no_mangle)]
pub unsafe extern "Rust" fn _rsx_hot_begin_batch() {
::telar::begin_batch();
}
#[unsafe(no_mangle)]
pub unsafe extern "Rust" fn _rsx_hot_end_batch() {
::telar::end_batch();
}
#[unsafe(no_mangle)]
pub unsafe extern "Rust" fn _rsx_hot_relayout() {
::telar::relayout_if_dirty();
}
#[unsafe(no_mangle)]
pub unsafe extern "Rust" fn _rsx_hot_dispatch_overlays(event: &::telar::Event) -> bool {
::telar::dispatch_overlays(event)
}
#[unsafe(no_mangle)]
pub unsafe extern "Rust" fn _rsx_hot_set_system_dark(dark: bool) {
::telar::set_system_dark(dark);
}
#[unsafe(no_mangle)]
pub unsafe extern "Rust" fn _rsx_hot_drain_window_commands()
-> ::std::vec::Vec<::telar::WindowCommand> {
::telar::take_window_commands()
}
}
} else {
quote! {}
};
let android_run = quote! {
#[cfg(target_os = "android")]
#[unsafe(no_mangle)]
fn android_main(android_app: ::telar::AndroidApp) {
#setup
::telar::run_android_app_with_name(
::telar::AppConfig::from(#config),
#app_expr,
env!("CARGO_PKG_NAME"),
android_app,
);
}
};
quote! {
#rerun_stmts
#include_stmts
#preview_fn
#desktop_run
#android_run
#hot_export
#hot_cleanup
#hot_state_symbols
#hot_tree_symbols
#hot_motion_symbols
}
.into()
}
struct TranspileOutput {
include_stmts: TokenStream2,
rerun_stmts: TokenStream2,
preview_const_idents: Vec<Ident>,
}
fn transpile_project(theme_type_str: Option<&str>) -> Result<TranspileOutput, TokenStream2> {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")
.map(PathBuf::from)
.map_err(|_| quote! { compile_error!("CARGO_MANIFEST_DIR not set") })?;
let generated_dir = manifest_dir.join(".telar").join("build");
if let Err(e) = std::fs::create_dir_all(&generated_dir) {
let msg = format!("Failed to create .telar/build/: {e}");
return Err(quote! { compile_error!(#msg) });
}
let src_dir = manifest_dir.join("src");
let rsx_files = telar_transpiler::find_rsx_files(&src_dir);
let assets_root = telar_transpiler::assets_root(&manifest_dir);
let mut registry = telar_transpiler::ComponentRegistry::new();
for (name, sig) in telar_transpiler::external_component_sigs() {
registry.insert(name.to_string(), sig);
}
for rsx_file in &rsx_files {
let Ok(source) = std::fs::read_to_string(rsx_file) else {
continue;
};
let sig = telar_transpiler::scan_component_sig(&source);
let stem = telar_transpiler::relative_stem(rsx_file, &src_dir);
registry.insert(telar_transpiler::naming::to_snake_case(&stem), sig.clone());
if let Some(base) = rsx_file.file_stem().and_then(|s| s.to_str()) {
registry
.entry(telar_transpiler::naming::to_snake_case(base))
.or_insert(sig);
}
}
let mut include_stmts = TokenStream2::new();
let mut rerun_stmts = TokenStream2::new();
let mut preview_const_idents: Vec<Ident> = Vec::new();
for rsx_file in &rsx_files {
let source = match std::fs::read_to_string(rsx_file) {
Ok(s) => s,
Err(e) => {
let msg = format!("Failed to read {}: {e}", rsx_file.display());
return Err(quote! { compile_error!(#msg) });
}
};
let stem = telar_transpiler::relative_stem(rsx_file, &src_dir);
let result = match telar_transpiler::transpile_source_full(
&source,
&stem,
theme_type_str,
Some(assets_root.as_path()),
Some(®istry),
) {
Ok(r) => r,
Err(telar_transpiler::TranspileError::Parse(ref pe)) => {
let msg = format!("{}:{}: {}", rsx_file.display(), pe.line, pe.message);
return Err(quote! { compile_error!(#msg) });
}
Err(e) => {
let msg = format!("Failed to transpile {}: {e}", rsx_file.display());
return Err(quote! { compile_error!(#msg) });
}
};
let Some(rel_out) = telar_transpiler::relative_output_path(rsx_file, &src_dir) else {
continue;
};
let out_path = generated_dir.join(rel_out);
if let Some(parent) = out_path.parent() {
if let Err(e) = std::fs::create_dir_all(parent) {
let msg = format!("Failed to create {}: {e}", parent.display());
return Err(quote! { compile_error!(#msg) });
}
}
let needs_write = std::fs::read_to_string(&out_path)
.map(|existing| existing != result.rust_code)
.unwrap_or(true);
if needs_write {
if let Err(e) = std::fs::write(&out_path, &result.rust_code) {
let msg = format!("Failed to write {}: {e}", out_path.display());
return Err(quote! { compile_error!(#msg) });
}
}
let map_path = out_path.with_extension("rs.map");
let map_json = telar_transpiler::source_map_to_json(&result.source_map);
let map_stale = std::fs::read_to_string(&map_path)
.map(|existing| existing != map_json)
.unwrap_or(true);
if map_stale {
let _ = std::fs::write(&map_path, &map_json);
}
let out_path_str = out_path.to_string_lossy().to_string();
let mod_ident = Ident::new(
&format!("__rsx_mod_{}", telar_transpiler::naming::to_snake_case(&stem)),
Span::call_site(),
);
include_stmts.extend(quote! {
#[path = #out_path_str]
mod #mod_ident;
#[allow(unused_imports)]
pub use #mod_ident::*;
});
let base_name = rsx_file
.file_stem()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_default();
let base_fn = telar_transpiler::naming::to_snake_case(&base_name);
let full_fn = telar_transpiler::naming::to_snake_case(&stem);
if !base_fn.is_empty() && base_fn != full_fn {
let full_fn_ident = Ident::new(&full_fn, Span::call_site());
let base_fn_ident = Ident::new(&base_fn, Span::call_site());
include_stmts.extend(quote! {
#[allow(unused_imports)]
pub use #mod_ident::#full_fn_ident as #base_fn_ident;
});
if result.has_props {
let full_props = Ident::new(
&(telar_transpiler::naming::to_pascal_case(&full_fn) + "Props"),
Span::call_site(),
);
let base_props = Ident::new(
&(telar_transpiler::naming::to_pascal_case(&base_fn) + "Props"),
Span::call_site(),
);
include_stmts.extend(quote! {
#[allow(unused_imports)]
pub use #mod_ident::#full_props as #base_props;
});
}
}
let rsx_path_str = rsx_file.to_string_lossy().to_string();
rerun_stmts.extend(quote! { const _: &str = include_str!(#rsx_path_str); });
if !result.preview_names.is_empty() {
preview_const_idents.push(preview_const_ident(&stem));
}
}
let telar_toml = manifest_dir.join("telar.toml");
if telar_toml.exists() {
let telar_toml_str = telar_toml.to_string_lossy().to_string();
rerun_stmts.extend(quote! { const _: &str = include_str!(#telar_toml_str); });
}
if telar_transpiler::auto_modules_enabled(&manifest_dir) {
let modtree_dir = generated_dir.join("__modules");
if let Err(e) = std::fs::create_dir_all(&modtree_dir) {
let msg = format!("Failed to create .telar/build/__modules/: {e}");
return Err(quote! { compile_error!(#msg) });
}
let modules_src = match telar_transpiler::discover_rust_modules(&src_dir, &modtree_dir) {
Ok(s) => s,
Err(e) => {
let msg = format!("Failed to write the auto-discovered module tree: {e}");
return Err(quote! { compile_error!(#msg) });
}
};
match modules_src.parse::<TokenStream2>() {
Ok(tokens) => include_stmts.extend(tokens),
Err(e) => {
let msg = format!("Failed to emit auto-discovered modules: {e}");
return Err(quote! { compile_error!(#msg) });
}
}
}
match telar_transpiler::parse_catalog(&manifest_dir) {
Ok(Some(catalog)) => {
let src = telar_transpiler::bake_catalog_to_source(&catalog);
let out_path = generated_dir.join("__i18n.rs");
let needs_write = std::fs::read_to_string(&out_path)
.map(|existing| existing != src)
.unwrap_or(true);
if needs_write && let Err(e) = std::fs::write(&out_path, &src) {
let msg = format!("Failed to write {}: {e}", out_path.display());
return Err(quote! { compile_error!(#msg) });
}
let out_path_str = out_path.to_string_lossy().to_string();
let mod_ident = Ident::new(telar_transpiler::I18N_MODULE, Span::call_site());
include_stmts.extend(quote! {
#[path = #out_path_str]
#[allow(dead_code)]
pub mod #mod_ident;
});
for file in telar_transpiler::catalog_files(&manifest_dir) {
let path_str = file.to_string_lossy().to_string();
rerun_stmts.extend(quote! { const _: &str = include_str!(#path_str); });
}
}
Ok(None) => {}
Err(msg) => return Err(quote! { compile_error!(#msg) }),
}
Ok(TranspileOutput {
include_stmts,
rerun_stmts,
preview_const_idents,
})
}
#[proc_macro]
pub fn rsx_modules(input: TokenStream) -> TokenStream {
let theme_type_str = if input.is_empty() {
None
} else {
match syn::parse::<syn::Path>(input) {
Ok(path) => Some(path.to_token_stream().to_string().replace(" :: ", "::")),
Err(e) => return e.to_compile_error().into(),
}
};
let TranspileOutput {
include_stmts,
rerun_stmts,
preview_const_idents,
} = match transpile_project(theme_type_str.as_deref()) {
Ok(o) => o,
Err(err) => return err.into(),
};
let preview_fn = quote! {
pub fn telar_all_preview_entries() -> ::std::vec::Vec<::telar::PreviewEntry> {
let mut entries = ::std::vec::Vec::new();
#( entries.extend_from_slice(#preview_const_idents); )*
entries
}
};
quote! {
#rerun_stmts
#include_stmts
#preview_fn
}
.into()
}