use std::fs;
use std::io::{self, Write};
use std::path::Path;
use anyhow::{anyhow, bail, Context, Result};
use clap::Args;
use crate::template::Kind;
#[derive(Args)]
pub struct NewCmd {
name: String,
#[arg(long)]
lib: bool,
}
#[derive(Args)]
pub struct InitCmd {
#[arg(long)]
name: Option<String>,
#[arg(long)]
lib: bool,
}
pub fn new(cmd: NewCmd) -> Result<()> {
nova_pm::check_name(&cmd.name).map_err(|message| anyhow!(message))?;
let kind = kind(&cmd.name, cmd.lib)?;
let dir = Path::new(&cmd.name);
if dir.exists() && !is_empty_dir(dir)? {
bail!(
"`{}` already exists and is not an empty directory",
cmd.name
);
}
for sub in dirs(kind) {
fs::create_dir_all(dir.join(sub)).with_context(|| format!("creating {}", dir.display()))?;
}
let mut wrote = Vec::new();
for (path, text) in crate::template::files(&cmd.name, kind) {
if write_new(&dir.join(&path), &text)? {
wrote.push(path);
}
}
println!("created `{}`: {}", cmd.name, wrote.join(", "));
Ok(())
}
pub fn init(cmd: InitCmd) -> Result<()> {
let cwd = std::env::current_dir().context("reading the current directory")?;
if cwd.join(nova_pm::MANIFEST).exists() {
if cmd.lib && !cwd.join("src").join("lib.nova").exists() {
bail!(
"this directory already has a nova.toml; to add a library to its package, \
write src/lib.nova"
);
}
bail!("this directory already has a nova.toml");
}
let name = match cmd.name {
Some(name) => {
nova_pm::check_name(&name).map_err(|message| anyhow!(message))?;
name
}
None => {
let name = cwd
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_default();
nova_pm::check_name(&name).map_err(|message| {
anyhow!("{message}; name the project with `nova init --name <name>`")
})?;
name
}
};
let kind = kind(&name, cmd.lib)?;
for sub in dirs(kind) {
fs::create_dir_all(cwd.join(sub)).with_context(|| format!("creating {sub}"))?;
}
let mut wrote = Vec::new();
let mut kept = Vec::new();
for (path, text) in crate::template::files(&name, kind) {
if write_new(&cwd.join(&path), &text)? {
wrote.push(path);
} else {
kept.push(path);
}
}
let wrote = if wrote.is_empty() {
"nothing".to_string()
} else {
wrote.join(", ")
};
println!("wrote: {wrote}");
if !kept.is_empty() {
println!("kept: {}", kept.join(", "));
}
Ok(())
}
fn is_empty_dir(dir: &Path) -> Result<bool> {
if !dir.is_dir() {
return Ok(false);
}
let mut entries = fs::read_dir(dir).with_context(|| format!("reading {}", dir.display()))?;
Ok(entries.next().is_none())
}
fn kind(name: &str, lib: bool) -> Result<Kind> {
if !lib {
return Ok(Kind::Program);
}
let import = nova_pm::import_name(name);
if nova_lexer::KEYWORDS.contains(&import.as_str()) {
bail!(
"a library called `{name}` would be imported as `{import}`, which is a keyword; \
choose another name"
);
}
Ok(Kind::Library)
}
fn dirs(kind: Kind) -> &'static [&'static str] {
match kind {
Kind::Program => &["src"],
Kind::Library => &["src", "tests"],
}
}
fn write_new(path: &Path, text: &str) -> Result<bool> {
match fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(path)
{
Ok(mut file) => {
file.write_all(text.as_bytes())
.with_context(|| format!("writing {}", path.display()))?;
Ok(true)
}
Err(error) if error.kind() == io::ErrorKind::AlreadyExists => Ok(false),
Err(error) => Err(error).with_context(|| format!("creating {}", path.display())),
}
}