cargo-auto 2026.306.1319

Automation tasks coded in Rust language for the workflow of Rust projects
Documentation
// template_new_pwa_wasm_mod.rs

//! Template for new_pwa_wasm.
//!
//! The template is downloaded from codeberg:  
//! <https://codeberg.org/automation-tasks-rs/cargo_auto_template_new_pwa_wasm/releases/vx.x.x/download/template.tar.gz>

use crate::{pos, utils_mod::OptionLogNone, ResultLogError, GREEN, RED, RESET, YELLOW};

/// Creates a new Rust project from template.
pub fn new_pwa_wasm(remote_repo: Option<String>, web_server_user_at_domain: Option<String>) -> anyhow::Result<()> {
    /// internal function: favicon.ico with 16 and 32 icons.
    fn encode_to_favicon_ico(img: &image::DynamicImage, rust_project_name: &str) -> anyhow::Result<()> {
        /// internal function: favicon
        fn favicon_add_entry(img: &image::DynamicImage, size: u32, icon_dir: &mut ico::IconDir) -> anyhow::Result<()> {
            // icons need smaller images 48, 32 and 16
            let img_rgba_vec = img
                .resize(size, size, image::imageops::FilterType::Lanczos3)
                .into_rgba8()
                .into_raw();
            // create an IconImage from raw RGBA pixel data from another image library
            let icon_image = ico::IconImage::from_rgba_data(size, size, img_rgba_vec);
            icon_dir.add_entry(ico::IconDirEntry::encode(&icon_image)?);
            Ok(())
        }
        // Create a new, empty icon collection:
        let mut icon_dir = ico::IconDir::new(ico::ResourceType::Icon);
        favicon_add_entry(img, 16, &mut icon_dir).log(pos!())?;
        favicon_add_entry(img, 32, &mut icon_dir).log(pos!())?;
        favicon_add_entry(img, 48, &mut icon_dir).log(pos!())?;
        let file_name = format!("{rust_project_name}/web_server_folder/{rust_project_name}/favicon.ico");
        let buffer = std::fs::File::create(file_name).log(pos!())?;
        icon_dir.write(buffer).log(pos!())?;
        Ok(())
    }

    /// Internal function: decode png.
    fn decode_png(vec: Vec<u8>) -> anyhow::Result<image::DynamicImage> {
        let img = image::ImageReader::new(std::io::Cursor::new(vec));
        let img = img.with_guessed_format().log(pos!())?;
        // return img
        Ok(img.decode()?)
    }

    /// Internal function: resize img.
    fn resize_image(img: &image::DynamicImage, img_size: u32, file_name: &str, rust_project_name: &str) -> anyhow::Result<()> {
        /// internal function: encode to png
        fn encode_to_png(new_img: image::DynamicImage) -> anyhow::Result<Vec<u8>> {
            let vec_u8: Vec<u8> = Vec::new();
            let mut cursor_1 = std::io::Cursor::new(vec_u8);
            new_img.write_to(&mut cursor_1, image::ImageFormat::Png).log(pos!())?;
            // return
            Ok(cursor_1.get_ref().to_owned())
        }
        let new_img = img.resize(img_size, img_size, image::imageops::FilterType::Lanczos3);
        let vec_u8 = encode_to_png(new_img).log(pos!())?;

        let file_name = format!("{rust_project_name}/web_server_folder/{rust_project_name}/icons/{file_name}");
        std::fs::write(file_name, vec_u8).log(pos!())?;
        Ok(())
    }

    let Some(remote_repo) = remote_repo else {
        println!("{RED}Error: Argument remote_repo is missing: `cargo auto new_pwa_wasm remote_repo web_server_user_at_domain`{RESET}");
        println!("{RED}Example: `cargo auto new_pwa_wasm codeberg.org/bestia-dev-work-in-progress/hello_world luciano@bestia.dev`{RESET}");
        return Ok(());
    };
    let Some(web_server_user_at_domain) = web_server_user_at_domain else {
        println!("{RED}Error: Argument web_server_user_at_domain is missing: `cargo auto new_pwa_wasm remote_repo web_server_user_at_domain`{RESET}");
        println!("{RED}Example: `cargo auto new_pwa_wasm codeberg.org/bestia-dev-work-in-progress/hello_world luciano@bestia.dev`{RESET}");
        return Ok(());
    };

    if !std::path::Path::new("icon512x512.png").exists() {
        println!(
            r#"
{RED}The mandatory file `icon512x512.png` is not found in this folder.{RESET}
  {YELLOW}If you don't have your icon, you can download and use this default:{RESET}
{GREEN}curl -L https://codeberg.org/automation-tasks-rs/cargo_auto_template_new_pwa_wasm/raw/main/icon512x512.png --output icon512x512.png{RESET}
"#
        );
        return Ok(());
    }

    let mut splitted = remote_repo.split("/");
    let repo_domain = splitted.next().log_msg(pos!(), "repo_domain missing")?;
    let owner_or_organization = splitted.next().log_msg(pos!(), "owner_or_organization missing")?;
    let rust_project_name = splitted.next().log_msg(pos!(), "rust_project_name missing")?;

    let mut splitted = web_server_user_at_domain.split("@");
    let web_server_username = splitted.next().log_msg(pos!(), "server_username missing")?;
    let web_server_domain = splitted.next().log_msg(pos!(), "web_server_domain missing")?;

    // the icon exist, already checked
    copy_to_files(
        repo_domain,
        owner_or_organization,
        rust_project_name,
        web_server_username,
        web_server_domain,
    )
    .log(pos!())?;

    // region: png with various sizes for: favicon png, pwa Android and pwa iOS
    // 32, 72, 96, 120, 128, 144, 152, 167, 180, 192, 196, 512
    let img = std::fs::read("icon512x512.png").log(pos!())?;
    let img = decode_png(img).log(pos!())?;

    resize_image(&img, 32, "icon-032.png", rust_project_name).log(pos!())?;
    resize_image(&img, 72, "icon-072.png", rust_project_name).log(pos!())?;
    resize_image(&img, 96, "icon-096.png", rust_project_name).log(pos!())?;
    resize_image(&img, 120, "icon-120.png", rust_project_name).log(pos!())?;
    resize_image(&img, 128, "icon-128.png", rust_project_name).log(pos!())?;
    resize_image(&img, 144, "icon-144.png", rust_project_name).log(pos!())?;
    resize_image(&img, 152, "icon-152.png", rust_project_name).log(pos!())?;
    resize_image(&img, 167, "icon-167.png", rust_project_name).log(pos!())?;
    resize_image(&img, 180, "icon-180.png", rust_project_name).log(pos!())?;
    resize_image(&img, 192, "icon-192.png", rust_project_name).log(pos!())?;
    resize_image(&img, 196, "icon-196.png", rust_project_name).log(pos!())?;
    // overwrite the default with the new
    resize_image(&img, 512, "icon-512.png", rust_project_name).log(pos!())?;

    // maskable icon 192
    resize_image(&img, 192, "icon-maskable.png", rust_project_name).log(pos!())?;

    // favicon.ico with 16, 32 and 48 icons
    encode_to_favicon_ico(&img, rust_project_name).log(pos!())?;

    // endregion
    println!(
        r#"
  {YELLOW}You can open this new Rust project in VSCode:{RESET}
{GREEN}code {package_name}{RESET}
  {YELLOW}Then build it with:{RESET}
{GREEN}cargo auto build{RESET}
  {YELLOW}Then follow the detailed instructions.{RESET}
"#,
        package_name = rust_project_name
    );
    Ok(())
}

/// Copy the Rust project into a compressed file.  
fn copy_to_files(
    repo_domain: &str,
    owner_or_organization: &str,
    rust_project_name: &str,
    web_server_username: &str,
    web_server_domain: &str,
) -> anyhow::Result<()> {
    let folder_path = std::path::Path::new(rust_project_name);
    if folder_path.exists() {
        anyhow::bail!("{RED}Error: Folder {rust_project_name} already exists! {RESET}");
    }
    std::fs::create_dir_all(folder_path).log(pos!())?;

    // download latest template.tar.gz
    println!("  {YELLOW}Downloading template.tar.gz...{RESET}");
    let file_to_download = "template.tar.gz";
    let path = "./template.tar.gz";

    // I need to get the latest version from codeberg.org.
    // When requesting for the latest release it responds with a redirect with the actual version number.
    let url = "https://codeberg.org/automation-tasks-rs/cargo_auto_template_new_pwa_wasm/releases/latest";
    let custom_policy = reqwest::redirect::Policy::custom(|attempt| attempt.stop());
    let reqwest_client = reqwest::blocking::ClientBuilder::new()
        .redirect(custom_policy)
        .build()
        .log(pos!())?;
    let http_response = reqwest_client.get(url).send().log(pos!())?;
    let location = http_response
        .headers()
        .get(reqwest::header::LOCATION)
        .log_msg(pos!(), "header Location None")?;
    let location = location.to_str().log(pos!())?;
    // /automation-tasks-rs/cargo_auto_template_new_pwa_wasm/releases/tag/v0.0.23
    // use the redirect location to create the url for download the latest release file.
    // https://codeberg.org/automation-tasks-rs/cargo_auto_template_new_pwa_wasm/releases/tag/v0.0.23
    let location = location.replace("/tag/", "/download/");
    let url = format!("https://codeberg.org{location}/{file_to_download}");
    // https://codeberg.org/automation-tasks-rs/cargo_auto_template_new_pwa_wasm/releases/download/v0.0.23/template.tar.gz

    let reqwest_client = reqwest::blocking::Client::new();
    let http_response = reqwest_client.get(url).send();

    if let Ok(body) = http_response {
        let body = body.bytes().log(pos!())?;
        // Get the content of the response
        std::fs::write(path, &body)
            .or_else(|err| anyhow::bail!("Download failed for {file_to_download} {err}"))
            .log(pos!())?;
    } else {
        anyhow::bail!("Error while retrieving data: {:#?}", http_response.err());
    }

    // decompress into folder_path
    let tar_gz = std::fs::File::open(path).log(pos!())?;
    let tar = flate2::read::GzDecoder::new(tar_gz);
    let mut archive = tar::Archive::new(tar);
    archive.unpack(folder_path).log(pos!())?;
    std::fs::remove_file(path).log(pos!())?;

    // replace placeholders inside text files
    for entry in walkdir::WalkDir::new(folder_path).into_iter().filter_map(Result::ok) {
        if entry.file_type().is_file() {
            if entry.file_name().to_string_lossy().ends_with(".ico")
                || entry.file_name().to_string_lossy().ends_with(".png")
                || entry.file_name().to_string_lossy().ends_with(".woff2")
            {
                // cannot replace text in binary files
            } else {
                // template has only valid utf8 files
                println!("replace: {}", entry.path().to_string_lossy());
                let mut content = std::fs::read_to_string(entry.path()).log(pos!())?;
                content = content.replace(
                    "codeberg.org/automation-tasks-rs/cargo_auto_template_new_pwa_wasm",
                    &format!("{repo_domain}/{owner_or_organization}/{rust_project_name}"),
                );
                content = content.replace(
                    "automation-tasks-rs/cargo_auto_template_new_pwa_wasm",
                    &format!("{owner_or_organization}/{rust_project_name}"),
                );
                content = content.replace("cargo_auto_template_new_pwa_wasm", rust_project_name);
                content = content.replace("CARGO_AUTO_TEMPLATE_NEW_PWA_WASM", rust_project_name.to_uppercase().as_str());

                let content = content.replace("web_server_domain", web_server_domain);
                let content = content.replace("web_server_username", web_server_username);
                std::fs::write(entry.path(), content).log(pos!())?;
            }
        }
    }
    // renaming files is tricky and must be traverse in reverse.
    let mut traverse_reverse: Vec<walkdir::DirEntry> = walkdir::WalkDir::new(folder_path).into_iter().filter_map(Result::ok).collect();
    traverse_reverse.reverse();
    for entry in traverse_reverse.iter() {
        if entry.file_name().to_string_lossy().contains("cargo_auto_template_new_pwa_wasm") {
            println!("rename: {}", entry.path().to_string_lossy());
            std::fs::rename(
                entry.path(),
                entry
                    .path()
                    .to_string_lossy()
                    .replace("cargo_auto_template_new_pwa_wasm", rust_project_name),
            )
            .log(pos!())?;
        }
    }
    Ok(())
}