use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use walkdir::WalkDir;
use crate::cli::InitArgs;
use crate::theme::{install_theme_source, resolve_demo, resolve_theme};
use crate::utils::resolve_root;
const DIRECTORIES: &[&str] = &["html", "posts", "templates", "skel", "themes", "pages"];
const CONFIG_FILE: &str = "bckt.yaml";
const DEFAULT_THEME_NAME: &str = "bckt3";
const DEFAULT_CONFIG_TEMPLATE: &str = r#"title: "My bckt Site"
description: "A site generated by bckt."
open_graph_image: "/open-graph.png"
base_url: "https://example.com"
homepage_posts: 10
date_format: "[year]-[month]-[day]"
default_timezone: "+00:00"
theme: {theme}
"#;
const SAMPLE_POST: &str = r#"---
title: "Hello From bckt"
slug: "hello-from-bckt"
date: "2024-01-01T00:00:00Z"
tags:
- welcome
abstract: "Kick the tires on the generator."
attached: []
images: []
---
This is the starter post. Edit it or drop in your own content to get going.
"#;
pub fn run_init_command(args: InitArgs) -> Result<()> {
let root = resolve_root(args.root.as_deref())?;
establish_directories(&root)?;
let theme_spec = args.theme.as_deref().unwrap_or(DEFAULT_THEME_NAME);
let theme_name = theme_name_from_spec(theme_spec);
let theme_dir = root.join("themes").join(&theme_name);
let theme_available = ensure_theme(&theme_dir, theme_spec)?;
seed_configuration(&root, &theme_name)?;
if theme_available {
seed_templates(&root, &theme_dir)?;
seed_static_assets(&root, &theme_dir)?;
}
if let Some(demo_name) = args.demo.as_deref() {
match resolve_demo(demo_name) {
Ok(demo_path) => {
apply_demo(&demo_path, &root)?;
println!("Populated project with demo '{demo_name}'");
}
Err(err) => eprintln!("Warning: {err}"),
}
} else {
seed_sample_post(&root)?;
}
if theme_available {
println!("Initialized project with theme '{theme_name}'");
} else {
println!(
"Initialized project. Theme '{theme_name}' was not found; install one with: bckt themes install <path-to-zip>"
);
}
Ok(())
}
fn theme_name_from_spec(spec: &str) -> String {
Path::new(spec)
.file_stem()
.and_then(|stem| stem.to_str())
.unwrap_or(spec)
.to_string()
}
fn establish_directories(root: &Path) -> Result<()> {
for entry in DIRECTORIES {
let path = root.join(entry);
if path.exists() {
continue;
}
fs::create_dir_all(&path)
.with_context(|| format!("failed to create directory {}", path.display()))?;
}
Ok(())
}
fn ensure_theme(theme_dir: &Path, spec: &str) -> Result<bool> {
if theme_dir.exists() {
return Ok(true);
}
match resolve_theme(spec) {
Ok(source) => {
install_theme_source(&source, theme_dir)?;
Ok(true)
}
Err(err) => {
eprintln!("Warning: {err}");
Ok(false)
}
}
}
fn seed_configuration(root: &Path, theme_name: &str) -> Result<()> {
let destination = root.join(CONFIG_FILE);
if destination.exists() {
return Ok(());
}
let contents = DEFAULT_CONFIG_TEMPLATE.replace("{theme}", theme_name);
write_if_missing(&destination, &contents)
.with_context(|| format!("failed to write {}", CONFIG_FILE))
}
fn seed_templates(root: &Path, theme_root: &Path) -> Result<()> {
let source = theme_root.join("templates");
copy_if_missing(&source, &root.join("templates"))?;
let pages = theme_root.join("pages");
copy_if_missing(&pages, &root.join("pages"))
}
fn seed_static_assets(root: &Path, theme_root: &Path) -> Result<()> {
let source = theme_root.join("skel");
copy_if_missing(&source, &root.join("skel"))
}
fn seed_sample_post(root: &Path) -> Result<()> {
let sample_dir = root.join(
["posts", "hello-from-bckt"]
.into_iter()
.collect::<PathBuf>(),
);
if !sample_dir.exists() {
fs::create_dir_all(&sample_dir)
.with_context(|| format!("failed to create {}", sample_dir.display()))?;
}
write_if_missing(&sample_dir.join("post.md"), SAMPLE_POST)
.context("failed to write sample post")
}
fn write_if_missing(path: &Path, contents: &str) -> Result<()> {
write_bytes_if_missing(path, contents.as_bytes())
}
fn write_bytes_if_missing(path: &Path, contents: &[u8]) -> Result<()> {
if path.exists() {
return Ok(());
}
if let Some(parent) = path.parent()
&& !parent.exists()
{
fs::create_dir_all(parent)
.with_context(|| format!("failed to create {}", parent.display()))?;
}
let mut file =
fs::File::create(path).with_context(|| format!("failed to create {}", path.display()))?;
file.write_all(contents)
.with_context(|| format!("failed to write {}", path.display()))?;
file.flush()
.with_context(|| format!("failed to flush {}", path.display()))?;
Ok(())
}
fn apply_demo(demo_path: &Path, project_root: &Path) -> Result<()> {
for component in ["posts", "pages"] {
let src = demo_path.join(component);
if src.exists() {
copy_tree(&src, &project_root.join(component))?;
}
}
let demo_config = demo_path.join("bckt.yaml");
if demo_config.exists() {
fs::copy(&demo_config, project_root.join(CONFIG_FILE)).with_context(|| {
format!("failed to copy demo config from {}", demo_config.display())
})?;
}
Ok(())
}
fn copy_tree(source_root: &Path, destination_root: &Path) -> Result<()> {
for entry in WalkDir::new(source_root).into_iter().filter_map(|e| e.ok()) {
let path = entry.path();
if entry.file_type().is_dir() {
continue;
}
let relative = path
.strip_prefix(source_root)
.with_context(|| format!("failed to strip prefix for {}", path.display()))?;
let destination = destination_root.join(relative);
if let Some(parent) = destination.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("failed to create {}", parent.display()))?;
}
fs::copy(path, &destination).with_context(|| {
format!(
"failed to copy {} to {}",
path.display(),
destination.display()
)
})?;
}
Ok(())
}
fn copy_if_missing(source_root: &Path, destination_root: &Path) -> Result<()> {
if !source_root.exists() {
return Ok(());
}
for entry in WalkDir::new(source_root).into_iter().filter_map(|e| e.ok()) {
let path = entry.path();
if entry.file_type().is_dir() {
continue;
}
let relative = path
.strip_prefix(source_root)
.with_context(|| format!("failed to strip prefix for {}", path.display()))?;
let destination = destination_root.join(relative);
if destination.exists() {
continue;
}
if let Some(parent) = destination.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("failed to create {}", parent.display()))?;
}
fs::copy(path, &destination).with_context(|| {
format!(
"failed to copy {} to {}",
path.display(),
destination.display()
)
})?;
}
Ok(())
}