1use anyhow::Context;
6use dialoguer::{Confirm, Input, Select};
7use std::{ffi::OsString, fs, process::exit};
8
9use crate::{
10 args::TauriVersion,
11 category::Category,
12 deps::print_missing_deps,
13 package_manager::PackageManager,
14 utils::{colors::*, dialoguer_theme::ColorfulTheme},
15};
16
17mod args;
18mod category;
19mod deps;
20mod manifest;
21mod package_manager;
22mod template;
23mod utils;
24
25pub mod internal {
26 pub mod package_manager {
33 pub use crate::package_manager::*;
34 }
35
36 pub mod template {
37 pub use crate::template::*;
38 }
39}
40
41pub fn run<I, A>(args: I, bin_name: Option<String>, detected_manager: Option<String>)
42where
43 I: IntoIterator<Item = A>,
44 A: Into<OsString> + Clone,
45{
46 let _ = ctrlc::set_handler(move || {
47 eprint!("\x1b[?25h");
48 exit(0);
49 });
50 if let Err(e) = try_run(args, bin_name, detected_manager) {
51 eprintln!("{BOLD}{RED}error{RESET}: {e:#}");
52 exit(1);
53 }
54}
55
56fn try_run<I, A>(
57 args: I,
58 bin_name: Option<String>,
59 detected_manager: Option<String>,
60) -> anyhow::Result<()>
61where
62 I: IntoIterator<Item = A>,
63 A: Into<OsString> + Clone,
64{
65 let detected_manager = detected_manager.and_then(|p| p.parse::<PackageManager>().ok());
66 let args = args::parse(args.into_iter().map(Into::into).collect(), bin_name)?;
67 let defaults = args::Args::default();
68 let args::Args {
69 skip,
70 tauri_version,
71 manager,
72 project_name,
73 template,
74 force,
75 identifier,
76 } = args;
77 let cwd = std::env::current_dir()?;
78
79 let project_name = match project_name {
82 Some(name) => name,
83 None => {
84 let default = defaults
85 .project_name
86 .context("default project_name not set")?;
87 if skip {
88 default
89 } else {
90 Input::<String>::with_theme(&ColorfulTheme::default())
91 .with_prompt("Project name")
92 .default(default)
93 .interact_text()?
94 .trim()
95 .into()
96 }
97 }
98 };
99
100 let target_dir = cwd.join(&project_name);
101
102 let package_name = if utils::is_valid_pkg_name(&project_name) {
104 project_name.clone()
105 } else {
106 let valid_name = utils::to_valid_pkg_name(&project_name);
107 if skip {
108 valid_name
109 } else {
110 Input::<String>::with_theme(&ColorfulTheme::default())
111 .with_prompt("Package name")
112 .default(valid_name.clone())
113 .with_initial_text(valid_name)
114 .validate_with(|input: &String| {
115 if utils::is_valid_pkg_name(input) {
116 Ok(())
117 } else {
118 Err("Package name should only include lowercase alphanumeric character and hyphens \"-\" and doesn't start with numbers")
119 }
120 })
121 .interact_text()?
122 .trim().to_string()
123 }
124 };
125
126 let identifier = match identifier {
127 Some(name) => name,
128 None => {
129 let valid_user_name = utils::to_valid_pkg_name(&whoami::username());
130 let default = format!("com.{valid_user_name}.{package_name}");
131 if skip {
132 default
133 } else {
134 Input::<String>::with_theme(&ColorfulTheme::default())
135 .with_prompt("Identifier")
136 .default(default)
137 .interact_text()?
138 .trim()
139 .into()
140 }
141 }
142 };
143
144 if target_dir.exists() && target_dir.read_dir()?.next().is_some() {
146 let overwrite = if force {
147 true
148 } else if skip {
149 false
150 } else {
151 Confirm::with_theme(&ColorfulTheme::default())
152 .with_prompt(format!(
153 "{} directory is not empty, do you want to overwrite?",
154 if target_dir == cwd {
155 "Current".to_string()
156 } else {
157 target_dir
158 .file_name()
159 .unwrap()
160 .to_string_lossy()
161 .to_string()
162 }
163 ))
164 .default(false)
165 .interact()?
166 };
167 if !overwrite {
168 eprintln!("{BOLD}{RED}✘{RESET} Directory is not empty, Operation Cancelled");
169 exit(1);
170 }
171 };
172
173 let category = if manager.is_none() {
175 let managers = PackageManager::ALL.to_vec();
177 let managers = template
178 .map(|t| {
179 managers
180 .iter()
181 .copied()
182 .filter(|p| p.templates_no_flavors().contains(&t.without_flavor()))
183 .collect::<Vec<_>>()
184 })
185 .unwrap_or(managers);
186
187 let categories = Category::ALL.to_vec();
189 let mut categories = categories
190 .into_iter()
191 .filter(|c| c.package_managers().iter().any(|p| managers.contains(p)))
192 .collect::<Vec<_>>();
193
194 categories.sort_by(|a, b| {
197 detected_manager
198 .map(|p| b.package_managers().contains(&p))
199 .unwrap_or(false)
200 .cmp(
201 &detected_manager
202 .map(|p| a.package_managers().contains(&p))
203 .unwrap_or(false),
204 )
205 });
206
207 if categories.len() == 1 || skip {
209 Some(categories[0])
210 } else {
211 let index = Select::with_theme(&ColorfulTheme::default())
212 .with_prompt("Choose which language to use for your frontend")
213 .items(&categories)
214 .default(0)
215 .interact()?;
216 Some(categories[index])
217 }
218 } else {
219 None
220 };
221
222 let pkg_manager = match manager {
225 Some(manager) => manager,
226 None => {
227 if let Some(category) = category {
228 let mut managers = category.package_managers().to_owned();
229 managers.sort_by(|a, b| {
231 detected_manager
232 .map(|p| p == *b)
233 .unwrap_or(false)
234 .cmp(&detected_manager.map(|p| p == *a).unwrap_or(false))
235 });
236 if managers.len() == 1 || skip {
238 managers[0]
239 } else {
240 let index = Select::with_theme(&ColorfulTheme::default())
241 .with_prompt("Choose your package manager")
242 .items(&managers)
243 .default(0)
244 .interact()?;
245 managers[index]
246 }
247 } else {
248 defaults.manager.context("default manager not set")?
249 }
250 }
251 };
252
253 let templates_no_flavors = pkg_manager.templates_no_flavors();
254
255 let template = match template {
257 Some(template) => template,
258 None => {
259 if skip {
260 defaults.template.context("default template not set")?
261 } else {
262 let index = Select::with_theme(&ColorfulTheme::default())
263 .with_prompt("Choose your UI template")
264 .items(
265 &templates_no_flavors
266 .iter()
267 .map(|t| t.select_text())
268 .collect::<Vec<_>>(),
269 )
270 .default(0)
271 .interact()?;
272
273 let template = templates_no_flavors[index];
274
275 let flavors = template.flavors(pkg_manager);
277 if let Some(flavors) = flavors {
278 let index = Select::with_theme(&ColorfulTheme::default())
279 .with_prompt("Choose your UI flavor")
280 .items(flavors)
281 .default(0)
282 .interact()?;
283 template.from_flavor(flavors[index])
284 } else {
285 template
286 }
287 }
288 }
289 };
290
291 if !pkg_manager.templates().contains(&template) {
295 eprintln!(
296 "{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: [{}]" ,
297 templates_no_flavors.iter().map(|e|format!("{GREEN}{e}{RESET}")).collect::<Vec<_>>().join(", "),
298 template.possible_package_managers().iter().map(|e|format!("{GREEN}{e}{RESET}")).collect::<Vec<_>>().join(", "),
299 );
300 exit(1);
301 }
302
303 if target_dir.exists() {
306 #[inline(always)]
307 fn clean_dir(dir: &std::path::PathBuf) -> anyhow::Result<()> {
308 for entry in fs::read_dir(dir)?.flatten() {
309 let path = entry.path();
310 if entry.file_type()?.is_dir() {
311 if entry.file_name() != ".git" {
312 clean_dir(&path)?;
313 std::fs::remove_dir(path)?;
314 }
315 } else {
316 fs::remove_file(path)?;
317 }
318 }
319 Ok(())
320 }
321 clean_dir(&target_dir)?;
322 } else {
323 let _ = fs::create_dir_all(&target_dir);
324 }
325
326 template.render(
328 &target_dir,
329 pkg_manager,
330 &project_name,
331 &package_name,
332 &identifier,
333 tauri_version,
334 )?;
335
336 println!();
338 print!("Template created!");
339 let has_missing = print_missing_deps(pkg_manager, template, tauri_version);
340 if has_missing {
341 let prereqs_url = match tauri_version {
342 TauriVersion::V1 => "https://v1.tauri.app/v1/guides/getting-started/prerequisites/",
343 TauriVersion::V2 => "https://tauri.app/start/prerequisites/",
344 };
345
346 println!("Make sure you have installed the prerequisites for your OS: {BLUE}{BOLD}{prereqs_url}{RESET}, then run:");
347 } else {
348 println!(" To get started run:")
349 }
350 if target_dir != cwd {
351 println!(
352 " cd {}",
353 if project_name.contains(' ') {
354 format!("\"{project_name}\"")
355 } else {
356 project_name
357 }
358 );
359 }
360 if let Some(cmd) = pkg_manager.install_cmd() {
361 println!(" {cmd}");
362 }
363 if matches!(tauri_version, TauriVersion::V1) {
364 println!(" {} tauri dev", pkg_manager.run_cmd());
365 } else {
366 println!(" {} tauri android init", pkg_manager.run_cmd());
367 #[cfg(target_os = "macos")]
368 println!(" {} tauri ios init", pkg_manager.run_cmd());
369
370 println!();
371 println!("For Desktop development, run:");
372 println!(" {} tauri dev", pkg_manager.run_cmd());
373 println!();
374 println!("For Android development, run:");
375 println!(" {} tauri android dev", pkg_manager.run_cmd());
376 #[cfg(target_os = "macos")]
377 {
378 println!();
379 println!("For iOS development, run:");
380 println!(" {} tauri ios dev", pkg_manager.run_cmd());
381 }
382 }
383 println!();
384 Ok(())
385}