crepuscularity-cli 0.10.5

crepus CLI — scaffolding and builds for Crepuscularity (UNSTABLE; in active development).
use std::path::PathBuf;

use console::style;
use crepuscularity_core::context::{TemplateContext, TemplateValue};
use crepuscularity_embedded::{write_ppm, Ui};
use crepuscularity_lvgl::{
    render_component_file_to_lvgl_xml, render_template_to_lvgl_xml_with_options, LvglOptions,
    LvglRoot,
};
use crepuscularity_native::{
    render_component_file_to_ir, render_template_to_ir, to_json, to_json_pretty,
};

use crate::build_options::BuildOptions;
use crate::crepus_toml::ResolvedTarget;
use crate::ui;

pub(crate) trait TargetBuilder {
    fn build(&self, options: &BuildOptions) -> Result<(), String>;
    fn output_path(&self, options: &BuildOptions) -> PathBuf;
}

pub(crate) struct WebBuilder<'a> {
    target: &'a ResolvedTarget,
}

pub(crate) struct WebextBuilder<'a> {
    target: &'a ResolvedTarget,
}

pub(crate) struct LvglBuilder<'a> {
    target: &'a ResolvedTarget,
}

pub(crate) struct NativeBuilder<'a> {
    target: &'a ResolvedTarget,
}

pub(crate) struct EmbeddedBuilder<'a> {
    target: &'a ResolvedTarget,
}

pub(crate) fn builder_for(target: &ResolvedTarget) -> Box<dyn TargetBuilder + '_> {
    match target.target_type.as_str() {
        "web" => Box::new(WebBuilder { target }),
        "webext" => Box::new(WebextBuilder { target }),
        "lvgl" => Box::new(LvglBuilder { target }),
        "native" | "ir" => Box::new(NativeBuilder { target }),
        "embedded" => Box::new(EmbeddedBuilder { target }),
        other => panic!("unsupported target type {other:?}"),
    }
}

impl TargetBuilder for WebBuilder<'_> {
    fn build(&self, options: &BuildOptions) -> Result<(), String> {
        crate::web::build_site_wasm(&crate::web::WebBuildArgs {
            site_dir: Some(self.target.dir.clone()),
            out_dir: self.target.out.clone(),
            entry: self.target.entry.clone(),
            target_id: None,
            manifest: None,
            meta: Some(self.target.web.clone()),
            options: *options,
        });
        Ok(())
    }

    fn output_path(&self, _options: &BuildOptions) -> PathBuf {
        self.target
            .out
            .clone()
            .unwrap_or_else(|| self.target.dir.join("dist"))
    }
}

impl TargetBuilder for WebextBuilder<'_> {
    fn build(&self, options: &BuildOptions) -> Result<(), String> {
        if let Some(manifest) = &self.target.webext {
            crate::webext::build_app_target(
                &self.target.dir,
                manifest,
                &self.target.webext_config,
                *options,
            );
        } else {
            crate::webext::build_app_path(&self.target.dir, *options);
        }
        Ok(())
    }

    fn output_path(&self, _options: &BuildOptions) -> PathBuf {
        self.target.dir.join("dist").join("unpacked")
    }
}

impl TargetBuilder for LvglBuilder<'_> {
    fn build(&self, options: &BuildOptions) -> Result<(), String> {
        let template_path = resolve_template_path(self.target);
        let template = std::fs::read_to_string(&template_path)
            .map_err(|e| format!("read {}: {e}", template_path.display()))?;
        let ctx = target_context(self.target, template_path.parent().map(PathBuf::from));
        let name = self
            .target
            .name
            .clone()
            .unwrap_or_else(|| self.target.id.clone());
        let root = match self.target.root.as_deref().unwrap_or("component") {
            "screen" => LvglRoot::Screen,
            "component" => LvglRoot::Component,
            other => {
                return Err(format!(
                    "lvgl root must be component or screen, got {other:?}"
                ))
            }
        };
        let xml = if let Some(component) = &self.target.component {
            render_component_file_to_lvgl_xml(&template, component, &ctx)
        } else {
            render_template_to_lvgl_xml_with_options(&template, &ctx, &LvglOptions { name, root })
        }
        .map_err(|e| format!("render lvgl target {:?}: {e}", self.target.id))?;
        let out = self.output_path(options);
        write_output(&out, xml.as_bytes());
        eprintln!(
            "{} {} {}",
            ui::ok(),
            style(&self.target.id).cyan().bold(),
            style(out.display().to_string()).dim()
        );
        Ok(())
    }

    fn output_path(&self, _options: &BuildOptions) -> PathBuf {
        self.target.out.clone().unwrap_or_else(|| {
            self.target
                .dir
                .join("dist")
                .join(format!("{}.xml", self.target.id))
        })
    }
}

impl TargetBuilder for NativeBuilder<'_> {
    fn build(&self, options: &BuildOptions) -> Result<(), String> {
        let template_path = resolve_template_path(self.target);
        let template = std::fs::read_to_string(&template_path)
            .map_err(|e| format!("read {}: {e}", template_path.display()))?;
        let ctx = target_context(self.target, template_path.parent().map(PathBuf::from));
        let ir = if let Some(component) = &self.target.component {
            render_component_file_to_ir(&template, component, &ctx)
        } else {
            render_template_to_ir(&template, &ctx)
        }
        .map_err(|e| format!("render native target {:?}: {e}", self.target.id))?;
        let json = if self.target.root.as_deref() == Some("pretty") {
            to_json_pretty(&ir)
        } else {
            to_json(&ir)
        }
        .map_err(|e| format!("serialize native target {:?}: {e}", self.target.id))?;
        let out = self.output_path(options);
        write_output(&out, json.as_bytes());
        eprintln!(
            "{} {} {}",
            ui::ok(),
            style(&self.target.id).cyan().bold(),
            style(out.display().to_string()).dim()
        );
        Ok(())
    }

    fn output_path(&self, _options: &BuildOptions) -> PathBuf {
        self.target.out.clone().unwrap_or_else(|| {
            self.target
                .dir
                .join("dist")
                .join(format!("{}.json", self.target.id))
        })
    }
}

impl TargetBuilder for EmbeddedBuilder<'_> {
    fn build(&self, options: &BuildOptions) -> Result<(), String> {
        let width = self
            .target
            .width
            .ok_or_else(|| "embedded target needs width".to_string())?;
        let height = self
            .target
            .height
            .ok_or_else(|| "embedded target needs height".to_string())?;
        let template_path = resolve_template_path(self.target);
        let template = std::fs::read_to_string(&template_path)
            .map_err(|e| format!("read {}: {e}", template_path.display()))?;
        let ctx = target_context(self.target, template_path.parent().map(PathBuf::from));
        let mut ui_view = Ui::new(width, height, &template);
        if let Some(component) = &self.target.component {
            ui_view.set_component(component.clone());
        }
        for (key, value) in ctx.vars {
            ui_view.set(key, value);
        }
        let screen = ui_view.screen();
        let mut ppm =
            crepuscularity_embedded::Rgb888Buffer::new(screen, crepuscularity_embedded::DEFAULT_BG);
        ui_view
            .render_into(&mut ppm)
            .map_err(|e| format!("render embedded target {:?}: {e}", self.target.id))?;
        let out = self.output_path(options);
        if let Some(parent) = out.parent() {
            std::fs::create_dir_all(parent)
                .map_err(|e| format!("create {}: {e}", parent.display()))?;
        }
        write_ppm(&out, &ppm).map_err(|e| format!("write {}: {e}", out.display()))?;
        eprintln!(
            "{} {} {}",
            ui::ok(),
            style(&self.target.id).cyan().bold(),
            style(out.display().to_string()).dim()
        );
        Ok(())
    }

    fn output_path(&self, _options: &BuildOptions) -> PathBuf {
        self.target.out.clone().unwrap_or_else(|| {
            self.target
                .dir
                .join("dist")
                .join(format!("{}.ppm", self.target.id))
        })
    }
}

fn resolve_template_path(target: &ResolvedTarget) -> PathBuf {
    target
        .template
        .clone()
        .or_else(|| target.entry.as_ref().map(|entry| target.dir.join(entry)))
        .unwrap_or_else(|| target.dir.join("ui.crepus"))
}

fn target_context(target: &ResolvedTarget, base_dir: Option<PathBuf>) -> TemplateContext {
    let mut ctx = TemplateContext::new();
    ctx.base_dir = base_dir;
    if let Some(path) = &target.ctx {
        load_ctx_file(path, &mut ctx);
    }
    for (key, value) in &target.vars {
        ctx.set(key, toml_to_template_value(value));
    }
    ctx
}

fn load_ctx_file(path: &PathBuf, ctx: &mut TemplateContext) {
    let raw = std::fs::read_to_string(path)
        .unwrap_or_else(|e| ui::error(&format!("read {}: {e}", path.display())));
    let table = raw
        .parse::<toml::Table>()
        .unwrap_or_else(|e| ui::error(&format!("parse {}: {e}", path.display())));
    for (key, value) in table {
        ctx.set(key, toml_to_template_value(&value));
    }
}

fn toml_to_template_value(value: &toml::Value) -> TemplateValue {
    match value {
        toml::Value::String(s) => TemplateValue::Str(s.clone()),
        toml::Value::Integer(n) => TemplateValue::Int(*n),
        toml::Value::Float(n) => TemplateValue::Float(*n),
        toml::Value::Boolean(b) => TemplateValue::Bool(*b),
        toml::Value::Array(items) => TemplateValue::List(
            items
                .iter()
                .map(|item| {
                    let mut row = TemplateContext::new();
                    row.set("value", toml_to_template_value(item));
                    row
                })
                .collect(),
        ),
        toml::Value::Table(table) => {
            let mut ctx = TemplateContext::new();
            for (key, value) in table {
                ctx.set(key, toml_to_template_value(value));
            }
            TemplateValue::Scope(ctx)
        }
        toml::Value::Datetime(dt) => TemplateValue::Str(dt.to_string()),
    }
}

fn write_output(path: &PathBuf, bytes: &[u8]) {
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)
            .unwrap_or_else(|e| ui::error(&format!("create {}: {e}", parent.display())));
    }
    std::fs::write(path, bytes)
        .unwrap_or_else(|e| ui::error(&format!("write {}: {e}", path.display())));
}