use clap::Parser;
use newter_compiler::{
compile, format_error, has_state_vars, layout_to_html, layout_to_react,
layout_to_reactive_html, theme_css_vars, Source, DEFAULT_SERVE_PORT, DEFAULT_VIEWPORT_H,
DEFAULT_VIEWPORT_W,
};
use std::path::PathBuf;
use std::process::ExitCode;
fn main() -> ExitCode {
env_logger::init();
let cli = match Cli::try_parse() {
Ok(c) => c,
Err(e) => {
eprintln!("{}", e);
return ExitCode::from(1);
}
};
match cli.command {
None => run_run(cli.file, cli.screen),
Some(Command::Serve { file, port, host, screen }) => {
let path = file.unwrap_or_else(|| {
eprintln!("error: serve requires <file>");
eprintln!("usage: newter-compiler serve <file.newt> [--port PORT] [--host HOST] [--screen NAME]");
std::process::exit(1);
});
if !path.exists() {
eprintln!("error: file not found: {}", path.display());
return ExitCode::from(1);
}
let rt = tokio::runtime::Runtime::new().expect("tokio runtime");
if let Err(e) = rt.block_on(newter_compiler::serve::serve(path, port, host, screen)) {
eprintln!("serve error: {}", e);
return ExitCode::from(1);
}
ExitCode::SUCCESS
}
Some(Command::Build {
file,
html,
json,
react,
output,
screen,
}) => {
let path = file.unwrap_or_else(|| {
eprintln!("error: build requires <file>");
eprintln!("usage: newter-compiler build <file.newt> [--html|--json|--react] [-o OUT] [--screen NAME]");
std::process::exit(1);
});
run_build(path, html, json, react, output, screen)
}
Some(Command::Check { file }) => {
let path = file.unwrap_or_else(|| {
eprintln!("error: check requires <file>");
eprintln!("usage: newter-compiler check <file.newt>");
std::process::exit(1);
});
run_check(path)
}
Some(Command::Watch {
file,
html,
json,
output,
screen,
}) => {
let path = file.unwrap_or_else(|| {
eprintln!("error: watch requires <file>");
std::process::exit(1);
});
run_watch(path, html, json, output, screen)
}
}
}
fn run_check(path: PathBuf) -> ExitCode {
let source = match std::fs::read_to_string(&path) {
Ok(s) => s,
Err(e) => {
eprintln!("error: failed to read file: {}", e);
return ExitCode::from(1);
}
};
let path_str = path.to_str();
let source_obj = Source::new(source.clone(), path_str.map(String::from));
match compile(source.trim(), Some(path.as_path()), None) {
Ok(_) => {
println!("check ok");
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("{}", format_error(&source_obj, &e));
ExitCode::from(1)
}
}
}
fn run_build(
path: PathBuf,
html: bool,
json: bool,
react: bool,
output: Option<PathBuf>,
screen: Option<String>,
) -> ExitCode {
let source = match std::fs::read_to_string(&path) {
Ok(s) => s,
Err(e) => {
eprintln!("error: failed to read file: {}", e);
return ExitCode::from(1);
}
};
let source_obj = Source::new(source.clone(), path.to_str().map(String::from));
let (program, layout) = match compile(source.trim(), Some(path.as_path()), screen.as_deref()) {
Ok(r) => r,
Err(e) => {
eprintln!("{}", format_error(&source_obj, &e));
return ExitCode::from(1);
}
};
let effective_html = html || (!json && !react);
if json {
let out_path = output.unwrap_or_else(|| PathBuf::from("out.json"));
let json_out = match serde_json::to_string_pretty(&layout) {
Ok(s) => s,
Err(e) => {
eprintln!("error: JSON serialization failed: {}", e);
return ExitCode::from(1);
}
};
if let Err(e) = std::fs::write(&out_path, json_out) {
eprintln!("error: write {}: {}", out_path.display(), e);
return ExitCode::from(1);
}
println!("Wrote {}", out_path.display());
return ExitCode::SUCCESS;
}
if react {
let out_path = output.unwrap_or_else(|| PathBuf::from("out.jsx"));
let react_out = newter_compiler::layout_to_react(&program, &layout);
if let Err(e) = std::fs::write(&out_path, react_out) {
eprintln!("error: write {}: {}", out_path.display(), e);
return ExitCode::from(1);
}
println!("Wrote {}", out_path.display());
return ExitCode::SUCCESS;
}
if !effective_html {
eprintln!("build: use --html, --json, or --react to select output format");
return ExitCode::SUCCESS;
}
let out_path = output.unwrap_or_else(|| PathBuf::from("out.html"));
let vars = theme_css_vars(&program);
let vars_ref = if vars.is_empty() {
None
} else {
Some(vars.as_slice())
};
let html_out = if has_state_vars(&program) {
layout_to_reactive_html(
&program,
&layout,
DEFAULT_VIEWPORT_W as u32,
DEFAULT_VIEWPORT_H as u32,
vars_ref,
)
} else {
layout_to_html(
&layout,
DEFAULT_VIEWPORT_W as u32,
DEFAULT_VIEWPORT_H as u32,
vars_ref,
)
};
if let Err(e) = std::fs::write(&out_path, html_out) {
eprintln!("error: write {}: {}", out_path.display(), e);
return ExitCode::from(1);
}
println!("Wrote {}", out_path.display());
ExitCode::SUCCESS
}
fn run_watch(
path: PathBuf,
html: bool,
json: bool,
output: Option<PathBuf>,
screen: Option<String>,
) -> ExitCode {
use notify_debouncer_full::{new_debouncer, notify::RecursiveMode};
use std::sync::mpsc;
use std::time::Duration;
let (tx, rx) = mpsc::channel();
let mut debouncer = match new_debouncer(Duration::from_millis(300), None, move |_| {
let _ = tx.send(());
}) {
Ok(d) => d,
Err(e) => {
eprintln!("watch error: {}", e);
return ExitCode::from(1);
}
};
if debouncer.watch(path.as_path(), RecursiveMode::NonRecursive).is_err() {
eprintln!("error: could not watch {}", path.display());
return ExitCode::from(1);
}
println!("Watching {} (Ctrl+C to stop)", path.display());
loop {
if rx.recv().is_ok() {
if run_build(path.clone(), html, json, false, output.clone(), screen.clone()) == ExitCode::SUCCESS {
println!("Build ok");
}
}
}
}
fn run_run(file: Option<PathBuf>, screen: Option<String>) -> ExitCode {
let (source_path, source_code) = if let Some(path) = file {
match std::fs::read_to_string(&path) {
Ok(code) => (Some(path), code),
Err(e) => {
eprintln!("error: failed to read file: {}", e);
return ExitCode::from(1);
}
}
} else {
let default = r#"
let padding = 24;
let fill = #f0f0f0;
screen Main {
column { gap: 16, padding: padding } {
box { fill: #ffffff, radius: 8, padding: 16 } {
text { content: "Newt Canvas", fontSize: 24 }
}
row { gap: 12 } {
box { fill: #e0e0e0, radius: 4 } { text { content: "One" } }
box { fill: #e0e0e0, radius: 4 } { text { content: "Two" } }
box { fill: #e0e0e0, radius: 4 } { text { content: "Three" } }
}
}
}
"#;
(None, default.to_string())
};
let source = Source::new(
source_code.clone(),
source_path.as_ref().and_then(|p| p.to_str().map(String::from)),
);
if let Err(e) = compile(
&source_code,
source_path.as_ref().map(|p| p.as_path()),
screen.as_deref(),
) {
eprintln!("{}", format_error(&source, &e));
return ExitCode::from(1);
}
let mut app = newter_compiler::App::new(source_path, source_code, screen);
let event_loop = winit::event_loop::EventLoop::new().unwrap();
let _ = event_loop.run_app(&mut app);
ExitCode::SUCCESS
}
#[derive(clap::Parser)]
#[command(name = "newter-compiler")]
#[command(about = "Newt design canvas compiler")]
struct Cli {
file: Option<PathBuf>,
#[arg(long, global = true)]
screen: Option<String>,
#[command(subcommand)]
command: Option<Command>,
}
#[derive(clap::Subcommand)]
enum Command {
Check {
file: Option<PathBuf>,
},
Build {
file: Option<PathBuf>,
#[arg(long)]
html: bool,
#[arg(long)]
json: bool,
#[arg(long)]
react: bool,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(long)]
screen: Option<String>,
},
Serve {
file: Option<PathBuf>,
#[arg(long, default_value_t = DEFAULT_SERVE_PORT)]
port: u16,
#[arg(long, default_value = "0.0.0.0")]
host: String,
#[arg(long)]
screen: Option<String>,
},
Watch {
file: Option<PathBuf>,
#[arg(long)]
html: bool,
#[arg(long)]
json: bool,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(long)]
screen: Option<String>,
},
}