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 && !first.is_alphabetic()
105 && first != '_'
106 {
107 return Err("Project name must start with a letter or underscore".to_string());
108 }
109
110 if !name
112 .chars()
113 .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
114 {
115 return Err(
116 "Project name can only contain letters, numbers, hyphens, and underscores".to_string(),
117 );
118 }
119
120 const RESERVED: &[&str] = &["test", "doc", "build", "target", "src"];
122 if RESERVED.contains(&name) {
123 return Err(format!("'{}' is a reserved name", name));
124 }
125
126 Ok(())
127}
128
129fn create_project(project_name: &str, project_path: &Path) -> Result<(), String> {
131 create_project_structure(project_path)?;
133
134 generate_cargo_toml(project_path, project_name)?;
136 generate_build_rs(project_path, project_name)?;
137 generate_main_rs(project_path, project_name)?;
138 generate_ui_mod_rs(project_path, project_name)?;
139 generate_ui_window_rs(project_path, project_name)?;
140 generate_window_dampen(project_path, project_name)?;
141 generate_theme_dampen(project_path, project_name)?;
142 generate_integration_tests(project_path, project_name)?;
143 generate_readme(project_path, project_name)?;
144
145 Ok(())
146}
147
148fn create_project_structure(project_path: &Path) -> Result<(), String> {
150 fs::create_dir(project_path).map_err(|e| {
152 format!(
153 "Failed to create directory '{}': {}",
154 project_path.display(),
155 e
156 )
157 })?;
158
159 let src_dir = project_path.join("src");
161 fs::create_dir(&src_dir)
162 .map_err(|e| format!("Failed to create directory '{}': {}", src_dir.display(), e))?;
163
164 let ui_dir = src_dir.join("ui");
166 fs::create_dir(&ui_dir)
167 .map_err(|e| format!("Failed to create directory '{}': {}", ui_dir.display(), e))?;
168
169 let theme_dir = ui_dir.join("theme");
171 fs::create_dir(&theme_dir).map_err(|e| {
172 format!(
173 "Failed to create directory '{}': {}",
174 theme_dir.display(),
175 e
176 )
177 })?;
178
179 let tests_dir = project_path.join("tests");
181 fs::create_dir(&tests_dir).map_err(|e| {
182 format!(
183 "Failed to create directory '{}': {}",
184 tests_dir.display(),
185 e
186 )
187 })?;
188
189 Ok(())
190}
191
192fn generate_cargo_toml(project_path: &Path, project_name: &str) -> Result<(), String> {
194 let template = include_str!("../../templates/new/Cargo.toml.template");
195
196 let dampen_version = env!("CARGO_PKG_VERSION");
198 let iced_version = env!("ICED_VERSION");
199 let serde_version = env!("SERDE_VERSION");
200 let serde_json_version = env!("SERDE_JSON_VERSION");
201
202 let content = template
203 .replace("{{PROJECT_NAME}}", project_name)
204 .replace("{{DAMPEN_VERSION}}", dampen_version)
205 .replace("{{ICED_VERSION}}", iced_version)
206 .replace("{{SERDE_VERSION}}", serde_version)
207 .replace("{{SERDE_JSON_VERSION}}", serde_json_version);
208
209 let file_path = project_path.join("Cargo.toml");
210 fs::write(&file_path, content)
211 .map_err(|e| format!("Failed to write '{}': {}", file_path.display(), e))?;
212
213 Ok(())
214}
215
216fn generate_build_rs(project_path: &Path, _project_name: &str) -> Result<(), String> {
218 let template = include_str!("../../templates/build.rs.template");
219 let file_path = project_path.join("build.rs");
222 fs::write(&file_path, template)
223 .map_err(|e| format!("Failed to write '{}': {}", file_path.display(), e))?;
224
225 Ok(())
226}
227
228fn generate_main_rs(project_path: &Path, project_name: &str) -> Result<(), String> {
230 let template = include_str!("../../templates/new/src/main.rs.template");
231 let content = template.replace("{{PROJECT_NAME}}", project_name);
232
233 let file_path = project_path.join("src/main.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_mod_rs(project_path: &Path, project_name: &str) -> Result<(), String> {
242 let template = include_str!("../../templates/new/src/ui/mod.rs.template");
243 let content = template.replace("{{PROJECT_NAME}}", project_name);
244
245 let file_path = project_path.join("src/ui/mod.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_ui_window_rs(project_path: &Path, project_name: &str) -> Result<(), String> {
254 let template = include_str!("../../templates/new/src/ui/window.rs.template");
255 let content = template.replace("{{PROJECT_NAME}}", project_name);
256
257 let file_path = project_path.join("src/ui/window.rs");
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_window_dampen(project_path: &Path, project_name: &str) -> Result<(), String> {
266 let template = include_str!("../../templates/new/src/ui/window.dampen.template");
267 let content = template.replace("{{PROJECT_NAME}}", project_name);
268
269 let file_path = project_path.join("src/ui/window.dampen");
270 fs::write(&file_path, content)
271 .map_err(|e| format!("Failed to write '{}': {}", file_path.display(), e))?;
272
273 Ok(())
274}
275
276fn generate_theme_dampen(project_path: &Path, project_name: &str) -> Result<(), String> {
278 let template = include_str!("../../templates/new/src/ui/theme/theme.dampen.template");
279 let content = template.replace("{{PROJECT_NAME}}", project_name);
280
281 let file_path = project_path.join("src/ui/theme/theme.dampen");
282 fs::write(&file_path, content)
283 .map_err(|e| format!("Failed to write '{}': {}", file_path.display(), e))?;
284
285 Ok(())
286}
287
288fn generate_integration_tests(project_path: &Path, project_name: &str) -> Result<(), String> {
290 let template = include_str!("../../templates/new/tests/integration.rs.template");
291 let content = template.replace("{{PROJECT_NAME}}", project_name);
292
293 let file_path = project_path.join("tests/integration.rs");
294 fs::write(&file_path, content)
295 .map_err(|e| format!("Failed to write '{}': {}", file_path.display(), e))?;
296
297 Ok(())
298}
299
300fn generate_readme(project_path: &Path, project_name: &str) -> Result<(), String> {
302 let template = include_str!("../../templates/new/README.md.template");
303 let content = template.replace("{{PROJECT_NAME}}", project_name);
304
305 let file_path = project_path.join("README.md");
306 fs::write(&file_path, content)
307 .map_err(|e| format!("Failed to write '{}': {}", file_path.display(), e))?;
308
309 Ok(())
310}
311
312fn cleanup_on_error(project_path: &Path) {
314 if project_path.exists() {
315 let _ = fs::remove_dir_all(project_path);
316 }
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322
323 #[test]
324 fn test_validate_project_name_valid() {
325 assert!(validate_project_name("my-app").is_ok());
326 assert!(validate_project_name("my_app").is_ok());
327 assert!(validate_project_name("myapp").is_ok());
328 assert!(validate_project_name("MyApp").is_ok());
329 assert!(validate_project_name("my-app-123").is_ok());
330 assert!(validate_project_name("_private").is_ok());
331 }
332
333 #[test]
334 fn test_validate_project_name_invalid() {
335 assert!(validate_project_name("").is_err());
336 assert!(validate_project_name("123").is_err());
337 assert!(validate_project_name("-invalid").is_err());
338 assert!(validate_project_name("my app").is_err());
339 assert!(validate_project_name("my/app").is_err());
340 assert!(validate_project_name("test").is_err());
341 assert!(validate_project_name("build").is_err());
342 }
343}