use clap::Parser;
use std::fs;
use std::io::IsTerminal;
use std::path::{Path, PathBuf};
#[derive(Parser)]
pub struct Args {
name: String,
#[arg(short = 't', long = "template")]
template: Option<String>,
}
pub fn run(args: &Args) {
let name: &str = args.name.as_str();
if !crate::path::is_installed() {
eprintln!("❌ SGDK not installed. Please run `sgdkx install` first.");
std::process::exit(1);
}
let sgdk_path = crate::path::sgdk_dir();
let dest_path = Path::new(name);
if dest_path.exists() {
eprintln!("❌ '{}' already exists.", name);
std::process::exit(1);
}
let template_path = select_template(&sgdk_path, args.template.as_deref());
println!("📁 Creating project from SGDK template: '{}'", name);
let mut opts = fs_extra::dir::CopyOptions::new();
opts.copy_inside = true;
fs_extra::dir::copy(&template_path, dest_path, &opts).expect("Template copy failed");
println!("✅ Project '{}' created!", name);
create_clangd_config(dest_path);
create_vscode_config(dest_path);
create_vscode_debug_config(dest_path);
create_gitignore(dest_path);
create_makefile(dest_path);
generate_compile_commands(dest_path);
}
fn collect_templates(sgdk_path: &Path) -> Vec<(String, PathBuf)> {
fn walk(base: &Path, rel: String, out: &mut Vec<(String, PathBuf)>) {
if base.join("src").exists() {
out.push((rel.clone(), base.to_path_buf()));
}
if let Ok(entries) = std::fs::read_dir(base) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
let name = entry.file_name().to_string_lossy().to_string();
let new_rel = if rel.is_empty() { name } else { format!("{rel}/{name}") };
walk(&path, new_rel, out);
}
}
}
}
let mut templates = Vec::new();
walk(&sgdk_path.join("sample"), String::new(), &mut templates);
templates.sort_by(|a, b| a.0.cmp(&b.0));
templates
}
fn select_template(sgdk_path: &Path, explicit: Option<&str>) -> PathBuf {
let templates = collect_templates(sgdk_path);
if templates.is_empty() {
eprintln!("❌ No templates found in {}", sgdk_path.join("sample").display());
std::process::exit(1);
}
let list_available = || {
eprintln!("Available templates:");
for (rel, _) in &templates {
eprintln!(" {rel}");
}
};
if let Some(name) = explicit {
return match templates.iter().find(|(rel, _)| rel == name) {
Some((rel, path)) => {
println!("Using template: {rel}");
path.clone()
}
None => {
eprintln!("❌ template '{name}' not found.");
list_available();
std::process::exit(1);
}
};
}
if !std::io::stdin().is_terminal() {
eprintln!("❌ no template selected. Re-run with --template <name> (required when non-interactive).");
list_available();
std::process::exit(1);
}
use dialoguer::{Select, theme::ColorfulTheme};
let items: Vec<&str> = templates.iter().map(|(rel, _)| rel.as_str()).collect();
let selection = Select::with_theme(&ColorfulTheme::default())
.with_prompt("Select a project template from SGDK/sample (Esc to cancel)")
.items(&items)
.default(0)
.interact_opt()
.unwrap();
match selection {
Some(idx) => {
println!("Selected template: {}", templates[idx].0);
templates[idx].1.clone()
}
None => {
println!("Cancelled.");
std::process::exit(0);
}
}
}
pub fn generate_compile_commands(project_path: &Path) {
println!("🔧 Generating compile_commands.json...");
let output = match crate::commands::make::make_command(&["-nwB"])
.current_dir(project_path)
.output()
{
Ok(o) => o,
Err(e) => {
eprintln!("⚠️ could not run make for compile_commands.json: {}", e);
return;
}
};
let stdout = String::from_utf8_lossy(&output.stdout);
let dir = project_path
.canonicalize()
.unwrap_or_else(|_| project_path.to_path_buf());
let dir_str = dir.to_string_lossy().replace(r"\\?\", "");
let mut entries: Vec<serde_json::Value> = Vec::new();
for line in stdout.lines() {
let line = line.trim();
if !(line.contains("gcc") && line.contains(" -c ") && !line.contains(" -E")) {
continue;
}
let file = match line
.split_once(" -c ")
.and_then(|(_, rest)| rest.split_once(" -o "))
.map(|(f, _)| f.trim())
{
Some(f) if f.ends_with(".c") => f,
_ => continue,
};
entries.push(serde_json::json!({
"directory": dir_str,
"command": line,
"file": file,
}));
}
if entries.is_empty() {
eprintln!("⚠️ no compile commands captured; compile_commands.json not written");
return;
}
let json = serde_json::to_string_pretty(&entries).unwrap();
match fs::write(project_path.join("compile_commands.json"), json) {
Ok(_) => println!("✅ compile_commands.json generated ({} entries)", entries.len()),
Err(e) => eprintln!("⚠️ failed to write compile_commands.json: {}", e),
}
}
pub fn create_clangd_config(project_path: &Path) {
println!("📄 Creating .clangd configuration file...");
let clangd_content = r#"# Configuration for using clangd with SGDK projects in Zed Editor (adjustments for GCC-based code)
CompileFlags:
Add:
- '-DSGDK_GCC'
- '-include'
- 'types.h'
- '-std=gnu17'
Remove:
- '-ffat-lto-objects'
- '-externally_visible'
- '-f*'
- '-m68000'
Diagnostics:
Suppress:
- 'main_arg_wrong'
- '-Wunknown-attributes'
"#;
let clangd_path = project_path.join(".clangd");
fs::write(clangd_path, clangd_content).expect("Failed to create .clangd file");
println!("✅ .clangd configuration file created");
}
pub fn create_vscode_config(project_path: &Path) {
println!("📄 Creating .vscode/c_cpp_properties.json...");
let vscode_dir = project_path.join(".vscode");
if !vscode_dir.exists() {
fs::create_dir_all(&vscode_dir).expect("Failed to create .vscode directory");
}
let cpp_properties_content = r#"{
"configurations": [
{
"name": "sgdk",
"cStandard": "gnu17",
"intelliSenseMode": "gcc-x86",
"compileCommands": "${workspaceFolder}/compile_commands.json"
}
],
"version": 4
}
"#;
let cpp_properties_path = vscode_dir.join("c_cpp_properties.json");
fs::write(cpp_properties_path, cpp_properties_content)
.expect("Failed to create c_cpp_properties.json");
println!("✅ VS Code C++ configuration file created");
}
pub fn create_vscode_debug_config(project_path: &Path) {
println!("📄 Creating .vscode/launch.json + tasks.json (gdb debugging)...");
let vscode_dir = project_path.join(".vscode");
if !vscode_dir.exists() {
fs::create_dir_all(&vscode_dir).expect("Failed to create .vscode directory");
}
let launch_json = r#"{
// Source-level debugging of the ROM in (patched) BlastEm via m68k-elf-gdb.
// Set breakpoints in src/*.c, press F5, then Continue (▶) to reach them.
// Drive with breakpoints + Continue; only "Step Into" your OWN functions, and
// don't "Step Over" the SYS_doVBlankProcess() line (frame-sync; use Continue).
"version": "0.2.0",
"configurations": [
{
"name": "Debug ROM (BlastEm) — cpptools",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/out/debug/rom.out",
"cwd": "${workspaceFolder}",
"MIMode": "gdb",
"miDebuggerPath": "${userHome}/.sgdkx/data/m68k-elf-gdb/bin/m68k-elf-gdb",
"miDebuggerServerAddress": "localhost:1234",
"stopAtConnect": false,
"externalConsole": false,
"preLaunchTask": "blastem-gdb",
"sourceFileMap": {
"/Users/runner/work/sgdk-native-builds/sgdk-native-builds/SGDK": "${userHome}/.sgdkx/data/SGDK"
},
"setupCommands": [
{ "description": "break at main", "text": "-break-insert main", "ignoreFailures": true }
]
},
{
"name": "Debug ROM (BlastEm) — Native Debug",
"type": "gdb",
"request": "attach",
"executable": "${workspaceFolder}/out/debug/rom.out",
"target": "localhost:1234",
"remote": true,
"cwd": "${workspaceFolder}",
"gdbpath": "${userHome}/.sgdkx/data/m68k-elf-gdb/bin/m68k-elf-gdb",
"valuesFormatting": "parseText",
"stopAtConnect": false,
"preLaunchTask": "blastem-gdb",
"autorun": [
"set substitute-path /Users/runner/work/sgdk-native-builds/sgdk-native-builds/SGDK ${userHome}/.sgdkx/data/SGDK",
"break main"
]
}
]
}
"#;
let tasks_json = r#"{
// build-debug : -O0 debug ROM with DWARF (clean stepping); see the Makefile OPT note.
// blastem-gdb : runs the patched BlastEm as a gdb server on TCP localhost:1234.
// It blocks until the debugger connects; SDL_AUDIODRIVER=dummy + BLASTEM_NO_GUI
// keep headless launches from stalling on a CoreAudio error dialog.
"version": "2.0.0",
"tasks": [
{
"label": "build-debug",
"type": "shell",
"command": "sgdkx make clean-debug && sgdkx make debug OPT=-O0",
"options": { "cwd": "${workspaceFolder}" },
"group": "build",
"problemMatcher": ["$gcc"]
},
{
"label": "blastem-gdb",
"type": "shell",
"command": "sgdkx",
"args": ["blastem", "${workspaceFolder}/out/debug/rom.bin", "-D"],
"options": {
"env": {
"BLASTEM_GDB_PORT": "1234",
"BLASTEM_NO_GUI": "1",
"SDL_AUDIODRIVER": "dummy"
}
},
"dependsOn": "build-debug",
"isBackground": true,
"problemMatcher": {
"pattern": { "regexp": "^____no_problems____$" },
"background": {
"activeOnStart": true,
"beginsPattern": ".",
"endsPattern": "Waiting for GDB connection"
}
}
}
]
}
"#;
fs::write(vscode_dir.join("launch.json"), launch_json).expect("Failed to create launch.json");
fs::write(vscode_dir.join("tasks.json"), tasks_json).expect("Failed to create tasks.json");
println!("✅ VS Code debug configuration created (gdb via patched BlastEm)");
}
pub fn create_gitignore(project_path: &Path) {
println!("📄 Creating .gitignore file...");
let gitignore_content = r#"/compile_commands.json
/.cache
/out
/res/**/*.h
/res/**/*.rs
"#;
let gitignore_path = project_path.join(".gitignore");
fs::write(gitignore_path, gitignore_content).expect("Failed to create .gitignore file");
println!("✅ .gitignore file created");
}
pub fn create_makefile(project_path: &Path) {
println!("📄 Creating Makefile...");
let makefile_content = r#"# SGDK project Makefile — generated by sgdkx.
#
# sgdkx make # build (release)
# sgdkx make debug # debug build: -O0 + symbols, lean libmd.a
# sgdkx make debug OPT=-Og # debug build, lighter optimization
# sgdkx make debug SGDK_DEBUG=1 # also step into SGDK source (needs SGDK >= 2.10)
# sgdkx make clean # remove build artifacts
GDK ?= $(HOME)/.sgdkx/data/SGDK
include $(GDK)/makefile.gen
# Tune `make debug` for source-level debugging (SGDK's debug build is -O1 +
# libmd_debug.a): -O0 keeps stepping/locals reliable; the lean libmd.a debugs your
# code only and keeps the ROM small (SGDK_DEBUG=1 to step into SGDK instead).
# Switching OPT/SGDK_DEBUG needs `make clean-debug` (make keys on timestamps).
ifeq ($(BUILD_TYPE),debug)
OPT ?= -O0
override CFLAGS := $(filter-out -O1,$(CFLAGS)) $(OPT)
ifndef SGDK_DEBUG
override LIBMD := $(LIB)/libmd.a
endif
endif
"#;
let makefile_path = project_path.join("Makefile");
fs::write(makefile_path, makefile_content).expect("Failed to create Makefile");
println!("✅ Makefile created");
}