use crate::config::PlaydateConfig;
use crate::error::{CrankError, Result};
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug)]
pub struct Project {
pub root: PathBuf,
pub config: PlaydateConfig,
}
impl Project {
pub fn find_and_load() -> Result<Self> {
let (config, root) = PlaydateConfig::find_and_load()?;
Ok(Self { root, config })
}
pub fn load_from<P: AsRef<Path>>(path: P) -> Result<Self> {
let root = path.as_ref().to_path_buf();
let config_path = root.join("Playdate.toml");
let config = PlaydateConfig::from_file(config_path)?;
Ok(Self { root, config })
}
pub fn source_dir(&self) -> PathBuf {
self.root.join(&self.config.build.source_dir)
}
pub fn output_dir(&self) -> PathBuf {
self.root.join(&self.config.build.output_dir)
}
pub fn assets_dir(&self) -> PathBuf {
self.root.join(&self.config.build.assets_dir)
}
pub fn tests_dir(&self) -> PathBuf {
self.root.join("tests")
}
pub fn pdx_path(&self) -> PathBuf {
self.output_dir()
.join(format!("{}.pdx", self.config.package.name))
}
pub fn validate(&self) -> Result<()> {
let source_dir = self.source_dir();
if !source_dir.exists() {
return Err(CrankError::InvalidProject(format!(
"Source directory not found: {}",
source_dir.display()
)));
}
let entry_point = match self.config.build.language.as_str() {
"lua" => source_dir.join("main.lua"),
"swift" => source_dir.join("main.swift"),
_ => {
return Err(CrankError::InvalidProject(format!(
"Unsupported language: {}",
self.config.build.language
)))
}
};
if !entry_point.exists() {
return Err(CrankError::InvalidProject(format!(
"Entry point not found: {}",
entry_point.display()
)));
}
Ok(())
}
pub fn ensure_output_dir(&self) -> Result<()> {
let output_dir = self.output_dir();
if !output_dir.exists() {
fs::create_dir_all(&output_dir)?;
}
Ok(())
}
pub fn clean(&self) -> Result<()> {
let output_dir = self.output_dir();
if output_dir.exists() {
fs::remove_dir_all(&output_dir)?;
}
Ok(())
}
pub fn generate_pdxinfo(&self) -> Result<()> {
let pdxinfo_content = format!(
"name={}\nauthor={}\ndescription={}\nbundleID={}\nversion={}\nbuildNumber={}\nimagePath={}\n",
self.config.package.name,
self.config.package.author,
self.config.package.description,
self.config.package.bundle_id,
self.config.package.version,
self.config.playdate.build_number,
self.config.playdate.image_path
);
let pdxinfo_path = self.source_dir().join("pdxinfo");
fs::write(pdxinfo_path, pdxinfo_content)?;
Ok(())
}
}
pub fn create_new_project<P: AsRef<Path>>(name: &str, path: P, template: &str) -> Result<Project> {
let project_path = path.as_ref().join(name);
if !is_valid_project_name(name) {
return Err(CrankError::InvalidProjectName(name.to_string()));
}
if project_path.exists() {
return Err(CrankError::ProjectExists(project_path));
}
fs::create_dir_all(&project_path)?;
fs::create_dir_all(project_path.join("source"))?;
fs::create_dir_all(project_path.join("assets/images"))?;
fs::create_dir_all(project_path.join("assets/sounds"))?;
fs::create_dir_all(project_path.join("assets/fonts"))?;
fs::create_dir_all(project_path.join("tests"))?;
fs::write(project_path.join("assets/images/.gitkeep"), "")?;
fs::write(project_path.join("assets/sounds/.gitkeep"), "")?;
fs::write(project_path.join("assets/fonts/.gitkeep"), "")?;
let bundle_id = format!("com.example.{}", name.replace("-", ""));
let config = PlaydateConfig::new_project(name, &bundle_id);
config.save(project_path.join("Playdate.toml"))?;
match template {
"lua-basic" => create_lua_basic_template(&project_path, &config)?,
_ => return Err(CrankError::TemplateNotFound(template.to_string())),
}
let project = Project {
root: project_path,
config,
};
Ok(project)
}
fn is_valid_project_name(name: &str) -> bool {
!name.is_empty()
&& name
.chars()
.all(|c| c.is_alphanumeric() || c == '-' || c == '_')
&& !name.starts_with('-')
&& !name.ends_with('-')
}
const TEMPLATE_MAIN_LUA: &str = include_str!("../templates/lua-basic/main.lua");
const TEMPLATE_TEST_LUA: &str = include_str!("../templates/lua-basic/test_basic.lua");
const TEMPLATE_LUAUNIT: &str = include_str!("../templates/lua-basic/luaunit.lua");
const TEMPLATE_README: &str = include_str!("../templates/lua-basic/README.md");
const TEMPLATE_LUARC: &str = include_str!("../templates/lua-basic/.luarc.json");
const TEMPLATE_GITIGNORE: &str = include_str!("../templates/lua-basic/.gitignore");
fn create_lua_basic_template(project_path: &Path, _config: &PlaydateConfig) -> Result<()> {
let project_name = project_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("Playdate Game");
fs::write(project_path.join("source/main.lua"), TEMPLATE_MAIN_LUA)?;
fs::write(project_path.join("tests/test_basic.lua"), TEMPLATE_TEST_LUA)?;
fs::write(project_path.join("tests/luaunit.lua"), TEMPLATE_LUAUNIT)?;
let readme = TEMPLATE_README.replace("{{PROJECT_NAME}}", project_name);
fs::write(project_path.join("README.md"), readme)?;
fs::write(project_path.join(".luarc.json"), TEMPLATE_LUARC)?;
fs::write(project_path.join(".gitignore"), TEMPLATE_GITIGNORE)?;
clone_playdate_luacats(project_path);
Ok(())
}
fn clone_playdate_luacats(project_path: &Path) {
use std::process::Command;
let luacats_path = project_path.join("playdate-luacats");
let git_check = Command::new("git").arg("--version").output();
if git_check.is_err() {
return; }
let clone_result = Command::new("git")
.arg("clone")
.arg("--depth")
.arg("1")
.arg("--quiet")
.arg("https://github.com/notpeter/playdate-luacats.git")
.arg(&luacats_path)
.output();
if clone_result.is_ok() {
let git_dir = luacats_path.join(".git");
let _ = std::fs::remove_dir_all(git_dir);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_project_names() {
assert!(is_valid_project_name("my-game"));
assert!(is_valid_project_name("game123"));
assert!(is_valid_project_name("my_game"));
assert!(!is_valid_project_name("-invalid"));
assert!(!is_valid_project_name("invalid-"));
assert!(!is_valid_project_name(""));
}
}