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_theme_dampen(project_path, project_name)?;
141 generate_integration_test(project_path, project_name)?;
142 generate_readme(project_path, project_name)?;
143
144 Ok(())
145}
146
147fn create_project_structure(project_path: &Path) -> Result<(), String> {
149 fs::create_dir(project_path).map_err(|e| {
151 format!(
152 "Failed to create directory '{}': {}",
153 project_path.display(),
154 e
155 )
156 })?;
157
158 let src_dir = project_path.join("src");
160 fs::create_dir(&src_dir)
161 .map_err(|e| format!("Failed to create directory '{}': {}", src_dir.display(), e))?;
162
163 let ui_dir = src_dir.join("ui");
165 fs::create_dir(&ui_dir)
166 .map_err(|e| format!("Failed to create directory '{}': {}", ui_dir.display(), e))?;
167
168 let theme_dir = ui_dir.join("theme");
170 fs::create_dir(&theme_dir).map_err(|e| {
171 format!(
172 "Failed to create directory '{}': {}",
173 theme_dir.display(),
174 e
175 )
176 })?;
177
178 let tests_dir = project_path.join("tests");
180 fs::create_dir(&tests_dir).map_err(|e| {
181 format!(
182 "Failed to create directory '{}': {}",
183 tests_dir.display(),
184 e
185 )
186 })?;
187
188 Ok(())
189}
190
191fn generate_cargo_toml(project_path: &Path, project_name: &str) -> Result<(), String> {
193 let template = include_str!("../../templates/new/Cargo.toml.template");
194
195 let dampen_version = env!("CARGO_PKG_VERSION");
197 let iced_version = env!("ICED_VERSION");
198 let serde_version = env!("SERDE_VERSION");
199 let serde_json_version = env!("SERDE_JSON_VERSION");
200
201 let content = template
202 .replace("{{PROJECT_NAME}}", project_name)
203 .replace("{{DAMPEN_VERSION}}", dampen_version)
204 .replace("{{ICED_VERSION}}", iced_version)
205 .replace("{{SERDE_VERSION}}", serde_version)
206 .replace("{{SERDE_JSON_VERSION}}", serde_json_version);
207
208 let file_path = project_path.join("Cargo.toml");
209 fs::write(&file_path, content)
210 .map_err(|e| format!("Failed to write '{}': {}", file_path.display(), e))?;
211
212 Ok(())
213}
214
215fn generate_build_rs(project_path: &Path, _project_name: &str) -> Result<(), String> {
217 let template = include_str!("../../templates/build.rs.template");
218 let file_path = project_path.join("build.rs");
221 fs::write(&file_path, template)
222 .map_err(|e| format!("Failed to write '{}': {}", file_path.display(), e))?;
223
224 Ok(())
225}
226
227fn generate_main_rs(project_path: &Path, project_name: &str) -> Result<(), String> {
229 let template = include_str!("../../templates/new/src/main.rs.template");
230 let content = template.replace("{{PROJECT_NAME}}", project_name);
231
232 let file_path = project_path.join("src/main.rs");
233 fs::write(&file_path, content)
234 .map_err(|e| format!("Failed to write '{}': {}", file_path.display(), e))?;
235
236 Ok(())
237}
238
239fn generate_ui_mod_rs(project_path: &Path, project_name: &str) -> Result<(), String> {
241 let template = include_str!("../../templates/new/src/ui/mod.rs.template");
242 let content = template.replace("{{PROJECT_NAME}}", project_name);
243
244 let file_path = project_path.join("src/ui/mod.rs");
245 fs::write(&file_path, content)
246 .map_err(|e| format!("Failed to write '{}': {}", file_path.display(), e))?;
247
248 Ok(())
249}
250
251fn generate_ui_window_rs(project_path: &Path, project_name: &str) -> Result<(), String> {
253 let template = include_str!("../../templates/new/src/ui/window.rs.template");
254 let content = template.replace("{{PROJECT_NAME}}", project_name);
255
256 let file_path = project_path.join("src/ui/window.rs");
257 fs::write(&file_path, content)
258 .map_err(|e| format!("Failed to write '{}': {}", file_path.display(), e))?;
259
260 Ok(())
261}
262
263fn generate_window_dampen(project_path: &Path, project_name: &str) -> Result<(), String> {
265 let template = include_str!("../../templates/new/src/ui/window.dampen.template");
266 let content = template.replace("{{PROJECT_NAME}}", project_name);
267
268 let file_path = project_path.join("src/ui/window.dampen");
269 fs::write(&file_path, content)
270 .map_err(|e| format!("Failed to write '{}': {}", file_path.display(), e))?;
271
272 Ok(())
273}
274
275fn generate_theme_dampen(project_path: &Path, project_name: &str) -> Result<(), String> {
277 let template = include_str!("../../templates/new/src/ui/theme/theme.dampen.template");
278 let content = template.replace("{{PROJECT_NAME}}", project_name);
279
280 let file_path = project_path.join("src/ui/theme/theme.dampen");
281 fs::write(&file_path, content)
282 .map_err(|e| format!("Failed to write '{}': {}", file_path.display(), e))?;
283
284 Ok(())
285}
286
287fn generate_integration_test(project_path: &Path, project_name: &str) -> Result<(), String> {
289 let template = include_str!("../../templates/new/tests/integration.rs.template");
290
291 let sanitized_name = project_name.replace('-', "_");
293
294 let content = template.replace("{{PROJECT_NAME}}", &sanitized_name);
295
296 let file_path = project_path.join("tests/integration.rs");
297 fs::write(&file_path, content)
298 .map_err(|e| format!("Failed to write '{}': {}", file_path.display(), e))?;
299
300 Ok(())
301}
302
303fn generate_readme(project_path: &Path, project_name: &str) -> Result<(), String> {
305 let template = include_str!("../../templates/new/README.md.template");
306 let content = template.replace("{{PROJECT_NAME}}", project_name);
307
308 let file_path = project_path.join("README.md");
309 fs::write(&file_path, content)
310 .map_err(|e| format!("Failed to write '{}': {}", file_path.display(), e))?;
311
312 Ok(())
313}
314
315fn cleanup_on_error(project_path: &Path) {
317 if project_path.exists() {
318 let _ = fs::remove_dir_all(project_path);
319 }
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325
326 #[test]
327 fn test_validate_project_name_valid() {
328 assert!(validate_project_name("my-app").is_ok());
329 assert!(validate_project_name("my_app").is_ok());
330 assert!(validate_project_name("myapp").is_ok());
331 assert!(validate_project_name("MyApp").is_ok());
332 assert!(validate_project_name("my-app-123").is_ok());
333 assert!(validate_project_name("_private").is_ok());
334 }
335
336 #[test]
337 fn test_validate_project_name_invalid() {
338 assert!(validate_project_name("").is_err());
339 assert!(validate_project_name("123").is_err());
340 assert!(validate_project_name("-invalid").is_err());
341 assert!(validate_project_name("my app").is_err());
342 assert!(validate_project_name("my/app").is_err());
343 assert!(validate_project_name("test").is_err());
344 assert!(validate_project_name("build").is_err());
345 }
346}