use anyhow::Context; use dialoguer::{Confirm, Input, Select}; use std::{ffi::OsString, fs, process::exit}; use crate::{ category::Category, colors::*, deps::print_missing_deps, package_manager::PackageManager, theme::ColorfulTheme, }; mod category; mod cli; mod colors; mod deps; mod lte; mod manifest; mod package_manager; mod template; mod theme; pub mod internal { //! Re-export of create-tauri-app internals //! //! ## Warning //! //! This is meant to be used internally only so use at your own risk //! and expect APIs to break without a prior notice. pub mod package_manager { pub use crate::package_manager::*; } pub mod template { pub use crate::template::*; } } pub fn run(args: I, bin_name: Option, detected_manager: Option) where I: IntoIterator, A: Into + Clone, { let _ = ctrlc::set_handler(move || { eprint!("\x1b[?25h"); exit(0); }); if let Err(e) = try_run(args, bin_name, detected_manager) { eprintln!("{BOLD}{RED}error{RESET}: {e:#}"); exit(1); } } fn try_run( args: I, bin_name: Option, detected_manager: Option, ) -> anyhow::Result<()> where I: IntoIterator, A: Into + Clone, { let detected_manager = detected_manager.and_then(|p| p.parse::().ok()); let args = cli::parse(args.into_iter().map(Into::into).collect(), bin_name)?; let defaults = cli::Args::default(); let cli::Args { skip, mobile, alpha, manager, project_name, template, } = args; let cwd = std::env::current_dir()?; // Allow `--mobile` to only be used with `--alpha` for now // TODO: remove this limitation once tauri@v2 is stable if let Some(mobile) = mobile { if mobile && !alpha { eprintln!( "{BOLD}{RED}error{RESET}: `{GREEN}--mobile{RESET}` option is only available if `{GREEN}--alpha{RESET}` option is also used" ); exit(1); } } // Project name used for the project directory name and productName in tauri.conf.json // and if valid, it will also be used in Cargo.toml, Package.json ...etc let project_name = match project_name { Some(name) => name, None => { if skip { defaults .project_name .context("default project_name not set")? } else { Input::::with_theme(&ColorfulTheme::default()) .with_prompt("Project name") .default("tauri-app".into()) .interact_text()? .trim() .into() } } }; let target_dir = cwd.join(&project_name); // Package name used in Cargo.toml, Package.json ...etc let package_name = if is_valid_pkg_name(&project_name) { project_name.clone() } else { let valid_name = to_valid_pkg_name(&project_name); if skip { valid_name } else { Input::::with_theme(&ColorfulTheme::default()) .with_prompt("Package name") .default(valid_name.clone()) .with_initial_text(valid_name) .validate_with(|input: &String| { if is_valid_pkg_name(input) { Ok(()) } else { Err("Package name should only include lowercase alphanumeric character and hyphens \"-\" and doesn't start with numbers") } }) .interact_text()? .trim().to_string() } }; // Confirm deleting the target project directory if not empty if target_dir.exists() && target_dir.read_dir()?.next().is_some() { let overrwite = if skip { false } else { Confirm::with_theme(&ColorfulTheme::default()) .with_prompt(format!( "{} directory is not empty, do you want to overwrite?", if target_dir == cwd { "Current directory".to_string() } else { target_dir .file_name() .unwrap() .to_string_lossy() .to_string() } )) .default(false) .interact()? }; if !overrwite { eprintln!("{BOLD}{RED}✘{RESET} Operation Cancelled"); exit(1); } }; // Prompt for category if a package manger is not passed on the command line let category = if manager.is_none() && !skip { // Filter managers if a template is passed on the command line let managers = PackageManager::ALL.to_vec(); let managers = args .template .map(|t| { managers .iter() .copied() .filter(|p| p.templates_no_flavors().contains(&t.without_flavor())) .collect::>() }) .unwrap_or(managers); // Filter categories based on the detected package mangers let categories = Category::ALL.to_vec(); let mut categories = categories .into_iter() .filter(|c| c.package_managers().iter().any(|p| managers.contains(p))) .collect::>(); // sort categories so the most relevant category // based on the auto-detected package manager is selected first categories.sort_by(|a, b| { detected_manager .map(|p| b.package_managers().contains(&p)) .unwrap_or(false) .cmp( &detected_manager .map(|p| a.package_managers().contains(&p)) .unwrap_or(false), ) }); // If only one category is detected, skip prompt if categories.len() == 1 { Some(categories[0]) } else { let index = Select::with_theme(&ColorfulTheme::default()) .with_prompt("Choose which language to use for your frontend") .items(&categories) .default(0) .interact()?; Some(categories[index]) } } else { None }; // Package manager which will be used for rendering the template // and the after-render instructions let pkg_manager = match manager { Some(manager) => manager, None => { if skip { defaults.manager.context("default manager not set")? } else { let category = category.context("category shouldn't be None at this point")?; let mut managers = category.package_managers().to_owned(); // sort managers so the auto-detected package manager is selected first managers.sort_by(|a, b| { detected_manager .map(|p| p == *b) .unwrap_or(false) .cmp(&detected_manager.map(|p| p == *a).unwrap_or(false)) }); // If only one package manager is detected, skip prompt if managers.len() == 1 { managers[0] } else { let index = Select::with_theme(&ColorfulTheme::default()) .with_prompt("Choose your package manager") .items(&managers) .default(0) .interact()?; managers[index] } } } }; let templates_no_flavors = pkg_manager.templates_no_flavors(); // Template to render let template = match template { Some(template) => template, None => { if skip { defaults.template.context("default template not set")? } else { let index = Select::with_theme(&ColorfulTheme::default()) .with_prompt("Choose your UI template") .items( &templates_no_flavors .iter() .map(|t| t.select_text()) .collect::>(), ) .default(0) .interact()?; let template = templates_no_flavors[index]; // Prompt for flavors if the template has more than one flavor let flavors = template.flavors(pkg_manager); if let Some(flavors) = flavors { let index = Select::with_theme(&ColorfulTheme::default()) .with_prompt("Choose your UI flavor") .items(flavors) .default(0) .interact()?; template.from_flavor(flavors[index]) } else { template } } } }; // Prompt for wether to bootstrap a mobile-friendly tauri project // This should only be prompted if `--alpha` is used on the command line and `--mobile` wasn't. // TODO: remove this limitation once tauri@v2 is stable let mobile = match mobile { Some(mobile) => mobile, None => { Confirm::with_theme(&ColorfulTheme::default()) .with_prompt("Would you like to setup the project for mobile as well?") .default(false) .interact()? // if skip || !alpha { // defaults.mobile.context("default mobile not set")? // } else { // Confirm::with_theme(&ColorfulTheme::default()) // .with_prompt("Would you like to setup the project for mobile as well?") // .default(false) // .interact()? // } } }; // If the package manager and the template are specified on the command line // then almost all prompts are skipped so we need to make sure that the combination // is valid, otherwise, we error and exit if !pkg_manager.templates().contains(&template) { eprintln!( "{BOLD}{RED}error{RESET}: the {GREEN}{template}{RESET} template is not suppported for the {GREEN}{pkg_manager}{RESET} package manager\n possible templates for {GREEN}{pkg_manager}{RESET} are: [{}]\n or maybe you meant to use another package manager\n possible package managers for {GREEN}{template}{RESET} are: [{}]" , templates_no_flavors.iter().map(|e|format!("{GREEN}{e}{RESET}")).collect::>().join(", "), template.possible_package_managers().iter().map(|e|format!("{GREEN}{e}{RESET}")).collect::>().join(", "), ); exit(1); } // Remove the target dir contents before rendering the template // SAFETY: Upon reaching this line, the user already accepted to overwrite if target_dir.exists() { #[inline(always)] fn clean_dir(dir: &std::path::PathBuf) -> anyhow::Result<()> { for entry in fs::read_dir(dir)?.flatten() { let path = entry.path(); if entry.file_type()?.is_dir() { if entry.file_name() != ".git" { clean_dir(&path)?; std::fs::remove_dir(path)?; } } else { fs::remove_file(path)?; } } Ok(()) } clean_dir(&target_dir)?; } else { let _ = fs::create_dir_all(&target_dir); } // Render the template template.render( &target_dir, pkg_manager, &project_name, &package_name, alpha, mobile, )?; // Print post-render instructions println!(); print!("Template created!"); print_missing_deps(pkg_manager, template, alpha); if target_dir != cwd { println!( " cd {}", if project_name.contains(' ') { format!("\"{project_name}\"") } else { project_name.clone() } ); } if let Some(cmd) = pkg_manager.install_cmd() { println!(" {cmd}"); } if !mobile { println!(" {} run", pkg_manager.run_cmd()); println!(); println!("For Release, run:"); println!(" {} run --release", pkg_manager.run_cmd()); println!(); println!("For Small Release, run:"); println!(" {} run --profile=small", pkg_manager.run_cmd()); } else { println!(" {} makepad android --abi=all install-toolchain", pkg_manager.run_cmd()); #[cfg(target_os = "macos")] println!(" {} makepad apple ios install-toolchain", pkg_manager.run_cmd()); println!(); println!("For Desktop development, run:"); println!(" {} run", pkg_manager.run_cmd()); println!(); println!("For Release, run:"); println!(" {} run --release", pkg_manager.run_cmd()); println!(); println!("For Small Release, run:"); println!(" {} run --profile=small", pkg_manager.run_cmd()); println!(); println!("For Android development, run:"); println!(" {} makepad android run", pkg_manager.run_cmd()); #[cfg(target_os = "macos")] { println!(); println!("For iOS Simulator development, run:"); println!(" {} makepad --org-id=123456 --org={}.com --app={} run-sim --release", pkg_manager.run_cmd(), project_name, project_name); println!(); println!("For iOS Device development, run:"); println!(" {} makepad --org-id=123456 --org={}.com --app={} run-device {}", pkg_manager.run_cmd(), project_name, project_name, project_name); } } println!(); Ok(()) } fn is_valid_pkg_name(project_name: &str) -> bool { let mut chars = project_name.chars().peekable(); !project_name.is_empty() && !chars.peek().map(|c| c.is_ascii_digit()).unwrap_or_default() && !chars.any(|ch| !(ch.is_alphanumeric() || ch == '-' || ch == '_') || ch.is_uppercase()) } fn to_valid_pkg_name(project_name: &str) -> String { let ret = project_name .trim() .to_lowercase() .replace([':', ';', ' ', '~'], "-") .replace(['.', '\\', '/'], ""); let ret = ret .chars() .skip_while(|ch| ch.is_ascii_digit() || *ch == '-') .collect::(); if ret.is_empty() { "tauri-app".to_string() } else { ret } }