#![allow(clippy::print_stdout)]
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, clap::Args)]
pub struct NewArgs {
pub name: String,
}
pub fn execute(args: &NewArgs) -> Result<(), String> {
let project_name = &args.name;
validate_project_name(project_name)?;
let project_path = PathBuf::from(project_name);
if project_path.exists() {
return Err(format!("Directory '{}' already exists", project_name));
}
match create_project(project_name, &project_path) {
Ok(()) => {
println!("Created new Dampen project: {}", project_name);
println!();
println!("Next steps:");
println!(" cd {}", project_name);
println!(" dampen run");
Ok(())
}
Err(e) => {
cleanup_on_error(&project_path);
Err(e)
}
}
}
fn validate_project_name(name: &str) -> Result<(), String> {
if name.is_empty() {
return Err("Project name cannot be empty".to_string());
}
if let Some(first) = name.chars().next()
&& !first.is_alphabetic()
&& first != '_'
{
return Err("Project name must start with a letter or underscore".to_string());
}
if !name
.chars()
.all(|c| c.is_alphanumeric() || c == '-' || c == '_')
{
return Err(
"Project name can only contain letters, numbers, hyphens, and underscores".to_string(),
);
}
const RESERVED: &[&str] = &["test", "doc", "build", "target", "src"];
if RESERVED.contains(&name) {
return Err(format!("'{}' is a reserved name", name));
}
Ok(())
}
fn create_project(project_name: &str, project_path: &Path) -> Result<(), String> {
create_project_structure(project_path)?;
generate_cargo_toml(project_path, project_name)?;
generate_build_rs(project_path, project_name)?;
generate_main_rs(project_path, project_name)?;
generate_ui_mod_rs(project_path, project_name)?;
generate_ui_window_rs(project_path, project_name)?;
generate_window_dampen(project_path, project_name)?;
generate_theme_dampen(project_path, project_name)?;
generate_integration_tests(project_path, project_name)?;
generate_readme(project_path, project_name)?;
Ok(())
}
fn create_project_structure(project_path: &Path) -> Result<(), String> {
fs::create_dir(project_path).map_err(|e| {
format!(
"Failed to create directory '{}': {}",
project_path.display(),
e
)
})?;
let src_dir = project_path.join("src");
fs::create_dir(&src_dir)
.map_err(|e| format!("Failed to create directory '{}': {}", src_dir.display(), e))?;
let ui_dir = src_dir.join("ui");
fs::create_dir(&ui_dir)
.map_err(|e| format!("Failed to create directory '{}': {}", ui_dir.display(), e))?;
let theme_dir = ui_dir.join("theme");
fs::create_dir(&theme_dir).map_err(|e| {
format!(
"Failed to create directory '{}': {}",
theme_dir.display(),
e
)
})?;
let tests_dir = project_path.join("tests");
fs::create_dir(&tests_dir).map_err(|e| {
format!(
"Failed to create directory '{}': {}",
tests_dir.display(),
e
)
})?;
Ok(())
}
fn generate_cargo_toml(project_path: &Path, project_name: &str) -> Result<(), String> {
let template = include_str!("../../templates/new/Cargo.toml.template");
let dampen_version = env!("CARGO_PKG_VERSION");
let iced_version = env!("ICED_VERSION");
let serde_version = env!("SERDE_VERSION");
let serde_json_version = env!("SERDE_JSON_VERSION");
let content = template
.replace("{{PROJECT_NAME}}", project_name)
.replace("{{DAMPEN_VERSION}}", dampen_version)
.replace("{{ICED_VERSION}}", iced_version)
.replace("{{SERDE_VERSION}}", serde_version)
.replace("{{SERDE_JSON_VERSION}}", serde_json_version);
let file_path = project_path.join("Cargo.toml");
fs::write(&file_path, content)
.map_err(|e| format!("Failed to write '{}': {}", file_path.display(), e))?;
Ok(())
}
fn generate_build_rs(project_path: &Path, _project_name: &str) -> Result<(), String> {
let template = include_str!("../../templates/build.rs.template");
let file_path = project_path.join("build.rs");
fs::write(&file_path, template)
.map_err(|e| format!("Failed to write '{}': {}", file_path.display(), e))?;
Ok(())
}
fn generate_main_rs(project_path: &Path, project_name: &str) -> Result<(), String> {
let template = include_str!("../../templates/new/src/main.rs.template");
let content = template.replace("{{PROJECT_NAME}}", project_name);
let file_path = project_path.join("src/main.rs");
fs::write(&file_path, content)
.map_err(|e| format!("Failed to write '{}': {}", file_path.display(), e))?;
Ok(())
}
fn generate_ui_mod_rs(project_path: &Path, project_name: &str) -> Result<(), String> {
let template = include_str!("../../templates/new/src/ui/mod.rs.template");
let content = template.replace("{{PROJECT_NAME}}", project_name);
let file_path = project_path.join("src/ui/mod.rs");
fs::write(&file_path, content)
.map_err(|e| format!("Failed to write '{}': {}", file_path.display(), e))?;
Ok(())
}
fn generate_ui_window_rs(project_path: &Path, project_name: &str) -> Result<(), String> {
let template = include_str!("../../templates/new/src/ui/window.rs.template");
let content = template.replace("{{PROJECT_NAME}}", project_name);
let file_path = project_path.join("src/ui/window.rs");
fs::write(&file_path, content)
.map_err(|e| format!("Failed to write '{}': {}", file_path.display(), e))?;
Ok(())
}
fn generate_window_dampen(project_path: &Path, project_name: &str) -> Result<(), String> {
let template = include_str!("../../templates/new/src/ui/window.dampen.template");
let content = template.replace("{{PROJECT_NAME}}", project_name);
let file_path = project_path.join("src/ui/window.dampen");
fs::write(&file_path, content)
.map_err(|e| format!("Failed to write '{}': {}", file_path.display(), e))?;
Ok(())
}
fn generate_theme_dampen(project_path: &Path, project_name: &str) -> Result<(), String> {
let template = include_str!("../../templates/new/src/ui/theme/theme.dampen.template");
let content = template.replace("{{PROJECT_NAME}}", project_name);
let file_path = project_path.join("src/ui/theme/theme.dampen");
fs::write(&file_path, content)
.map_err(|e| format!("Failed to write '{}': {}", file_path.display(), e))?;
Ok(())
}
fn generate_integration_tests(project_path: &Path, project_name: &str) -> Result<(), String> {
let template = include_str!("../../templates/new/tests/integration.rs.template");
let content = template.replace("{{PROJECT_NAME}}", project_name);
let file_path = project_path.join("tests/integration.rs");
fs::write(&file_path, content)
.map_err(|e| format!("Failed to write '{}': {}", file_path.display(), e))?;
Ok(())
}
fn generate_readme(project_path: &Path, project_name: &str) -> Result<(), String> {
let template = include_str!("../../templates/new/README.md.template");
let content = template.replace("{{PROJECT_NAME}}", project_name);
let file_path = project_path.join("README.md");
fs::write(&file_path, content)
.map_err(|e| format!("Failed to write '{}': {}", file_path.display(), e))?;
Ok(())
}
fn cleanup_on_error(project_path: &Path) {
if project_path.exists() {
let _ = fs::remove_dir_all(project_path);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_project_name_valid() {
assert!(validate_project_name("my-app").is_ok());
assert!(validate_project_name("my_app").is_ok());
assert!(validate_project_name("myapp").is_ok());
assert!(validate_project_name("MyApp").is_ok());
assert!(validate_project_name("my-app-123").is_ok());
assert!(validate_project_name("_private").is_ok());
}
#[test]
fn test_validate_project_name_invalid() {
assert!(validate_project_name("").is_err());
assert!(validate_project_name("123").is_err());
assert!(validate_project_name("-invalid").is_err());
assert!(validate_project_name("my app").is_err());
assert!(validate_project_name("my/app").is_err());
assert!(validate_project_name("test").is_err());
assert!(validate_project_name("build").is_err());
}
}