1#![allow(clippy::print_stdout)]
16
17use std::fs;
18use std::path::{Path, PathBuf};
19
20#[derive(Debug, clap::Args)]
26pub struct NewArgs {
27 pub name: String,
29}
30
31pub fn execute(args: &NewArgs) -> Result<(), String> {
58 let project_name = &args.name;
59
60 validate_project_name(project_name)?;
62
63 let project_path = PathBuf::from(project_name);
65
66 if project_path.exists() {
68 return Err(format!("Directory '{}' already exists", project_name));
69 }
70
71 match create_project(project_name, &project_path) {
73 Ok(()) => {
74 println!("Created new Dampen project: {}", project_name);
75 println!();
76 println!("Next steps:");
77 println!(" cd {}", project_name);
78 println!(" dampen run");
79 Ok(())
80 }
81 Err(e) => {
82 cleanup_on_error(&project_path);
84 Err(e)
85 }
86 }
87}
88
89fn validate_project_name(name: &str) -> Result<(), String> {
97 if name.is_empty() {
99 return Err("Project name cannot be empty".to_string());
100 }
101
102 if let Some(first) = name.chars().next() {
104 if !first.is_alphabetic() && first != '_' {
105 return Err("Project name must start with a letter or underscore".to_string());
106 }
107 }
108
109 if !name
111 .chars()
112 .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
113 {
114 return Err(
115 "Project name can only contain letters, numbers, hyphens, and underscores".to_string(),
116 );
117 }
118
119 const RESERVED: &[&str] = &["test", "doc", "build", "target", "src"];
121 if RESERVED.contains(&name) {
122 return Err(format!("'{}' is a reserved name", name));
123 }
124
125 Ok(())
126}
127
128fn create_project(project_name: &str, project_path: &Path) -> Result<(), String> {
130 create_project_structure(project_path)?;
132
133 generate_cargo_toml(project_path, project_name)?;
135 generate_build_rs(project_path, project_name)?;
136 generate_main_rs(project_path, project_name)?;
137 generate_ui_mod_rs(project_path, project_name)?;
138 generate_ui_window_rs(project_path, project_name)?;
139 generate_window_dampen(project_path, project_name)?;
140 generate_integration_test(project_path, project_name)?;
141 generate_readme(project_path, project_name)?;
142
143 Ok(())
144}
145
146fn create_project_structure(project_path: &Path) -> Result<(), String> {
148 fs::create_dir(project_path).map_err(|e| {
150 format!(
151 "Failed to create directory '{}': {}",
152 project_path.display(),
153 e
154 )
155 })?;
156
157 let src_dir = project_path.join("src");
159 fs::create_dir(&src_dir)
160 .map_err(|e| format!("Failed to create directory '{}': {}", src_dir.display(), e))?;
161
162 let ui_dir = src_dir.join("ui");
164 fs::create_dir(&ui_dir)
165 .map_err(|e| format!("Failed to create directory '{}': {}", ui_dir.display(), e))?;
166
167 let tests_dir = project_path.join("tests");
169 fs::create_dir(&tests_dir).map_err(|e| {
170 format!(
171 "Failed to create directory '{}': {}",
172 tests_dir.display(),
173 e
174 )
175 })?;
176
177 Ok(())
178}
179
180fn generate_cargo_toml(project_path: &Path, project_name: &str) -> Result<(), String> {
182 let template = include_str!("../../templates/new/Cargo.toml.template");
183
184 let dampen_version = env!("CARGO_PKG_VERSION");
186 let iced_version = env!("ICED_VERSION");
187 let serde_version = env!("SERDE_VERSION");
188 let serde_json_version = env!("SERDE_JSON_VERSION");
189
190 let content = template
191 .replace("{{PROJECT_NAME}}", project_name)
192 .replace("{{DAMPEN_VERSION}}", dampen_version)
193 .replace("{{ICED_VERSION}}", iced_version)
194 .replace("{{SERDE_VERSION}}", serde_version)
195 .replace("{{SERDE_JSON_VERSION}}", serde_json_version);
196
197 let file_path = project_path.join("Cargo.toml");
198 fs::write(&file_path, content)
199 .map_err(|e| format!("Failed to write '{}': {}", file_path.display(), e))?;
200
201 Ok(())
202}
203
204fn generate_build_rs(project_path: &Path, _project_name: &str) -> Result<(), String> {
206 let template = include_str!("../../templates/build.rs.template");
207 let file_path = project_path.join("build.rs");
210 fs::write(&file_path, template)
211 .map_err(|e| format!("Failed to write '{}': {}", file_path.display(), e))?;
212
213 Ok(())
214}
215
216fn generate_main_rs(project_path: &Path, project_name: &str) -> Result<(), String> {
218 let template = include_str!("../../templates/new/main.rs.template");
219 let content = template.replace("{{PROJECT_NAME}}", project_name);
220
221 let file_path = project_path.join("src/main.rs");
222 fs::write(&file_path, content)
223 .map_err(|e| format!("Failed to write '{}': {}", file_path.display(), e))?;
224
225 Ok(())
226}
227
228fn generate_ui_mod_rs(project_path: &Path, project_name: &str) -> Result<(), String> {
230 let template = include_str!("../../templates/new/src/ui/mod.rs.template");
231 let content = template.replace("{{PROJECT_NAME}}", project_name);
232
233 let file_path = project_path.join("src/ui/mod.rs");
234 fs::write(&file_path, content)
235 .map_err(|e| format!("Failed to write '{}': {}", file_path.display(), e))?;
236
237 Ok(())
238}
239
240fn generate_ui_window_rs(project_path: &Path, project_name: &str) -> Result<(), String> {
242 let template = include_str!("../../templates/new/src/ui/window.rs.template");
243 let content = template.replace("{{PROJECT_NAME}}", project_name);
244
245 let file_path = project_path.join("src/ui/window.rs");
246 fs::write(&file_path, content)
247 .map_err(|e| format!("Failed to write '{}': {}", file_path.display(), e))?;
248
249 Ok(())
250}
251
252fn generate_window_dampen(project_path: &Path, project_name: &str) -> Result<(), String> {
254 let template = include_str!("../../templates/new/window.dampen.template");
255 let content = template.replace("{{PROJECT_NAME}}", project_name);
256
257 let file_path = project_path.join("src/ui/window.dampen");
258 fs::write(&file_path, content)
259 .map_err(|e| format!("Failed to write '{}': {}", file_path.display(), e))?;
260
261 Ok(())
262}
263
264fn generate_integration_test(project_path: &Path, project_name: &str) -> Result<(), String> {
266 let template = include_str!("../../templates/new/tests/integration.rs.template");
267
268 let sanitized_name = project_name.replace('-', "_");
270
271 let content = template.replace("{{PROJECT_NAME}}", &sanitized_name);
272
273 let file_path = project_path.join("tests/integration.rs");
274 fs::write(&file_path, content)
275 .map_err(|e| format!("Failed to write '{}': {}", file_path.display(), e))?;
276
277 Ok(())
278}
279
280fn generate_readme(project_path: &Path, project_name: &str) -> Result<(), String> {
282 let template = include_str!("../../templates/new/README.md.template");
283 let content = template.replace("{{PROJECT_NAME}}", project_name);
284
285 let file_path = project_path.join("README.md");
286 fs::write(&file_path, content)
287 .map_err(|e| format!("Failed to write '{}': {}", file_path.display(), e))?;
288
289 Ok(())
290}
291
292fn cleanup_on_error(project_path: &Path) {
294 if project_path.exists() {
295 let _ = fs::remove_dir_all(project_path);
296 }
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302
303 #[test]
304 fn test_validate_project_name_valid() {
305 assert!(validate_project_name("my-app").is_ok());
306 assert!(validate_project_name("my_app").is_ok());
307 assert!(validate_project_name("myapp").is_ok());
308 assert!(validate_project_name("MyApp").is_ok());
309 assert!(validate_project_name("my-app-123").is_ok());
310 assert!(validate_project_name("_private").is_ok());
311 }
312
313 #[test]
314 fn test_validate_project_name_invalid() {
315 assert!(validate_project_name("").is_err());
316 assert!(validate_project_name("123").is_err());
317 assert!(validate_project_name("-invalid").is_err());
318 assert!(validate_project_name("my app").is_err());
319 assert!(validate_project_name("my/app").is_err());
320 assert!(validate_project_name("test").is_err());
321 assert!(validate_project_name("build").is_err());
322 }
323}