commit 25890fc39271fb9b684b41dcbe05cd6cb9995002 Author: Moritz Hölting <87192362+moritz-hoelting@users.noreply.github.com> Date: Wed Mar 27 13:58:09 2024 +0100 Add init command diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..26f04f4 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "shulkerscript" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +clap = { version = "4.5.4", features = ["derive"] } +colored = "2.1.0" +serde = { version = "1.0.197", features = ["derive"] } +thiserror = "1.0.58" +toml = "0.8.12" diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..1f2c3cd --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,59 @@ +use std::path::PathBuf; + +use crate::{error::Result, subcommands}; +use clap::{Parser, Subcommand}; + +#[derive(Debug, Parser)] +#[command(version, about, long_about = None)] +pub struct Args { + #[command(subcommand)] + cmd: Command, + /// Enable verbose output. + #[clap(short, long)] + verbose: bool, +} + +#[derive(Debug, Clone, Subcommand)] +pub enum Command { + /// Initialize a new project in the current directory. + Init { + /// The path of the folder to initialize in. + #[clap(default_value = ".")] + path: PathBuf, + /// The name of the project. + #[clap(short, long)] + name: Option, + /// The description of the project. + #[clap(short, long)] + description: Option, + /// The pack format version. + #[clap(short, long)] + pack_format: Option, + /// Force initialization even if the directory is not empty. + #[clap(short, long)] + force: bool, + }, +} + +impl Args { + pub fn run(&self) -> Result<()> { + match &self.cmd { + Command::Init { + path, + name, + description, + pack_format, + force, + } => subcommands::init( + self.verbose, + path, + name.as_deref(), + description.as_deref(), + *pack_format, + *force, + )?, + } + + Ok(()) + } +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..cd5e49f --- /dev/null +++ b/src/config.rs @@ -0,0 +1,49 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct ProjectConfig { + pub pack: PackConfig, +} +impl ProjectConfig { + pub fn new(pack: PackConfig) -> Self { + Self { pack } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PackConfig { + pub name: String, + pub description: String, + #[serde( + rename = "format", + alias = "pack_format", + default = "default_pack_format" + )] + pub pack_format: u8, + pub version: String, +} + +impl PackConfig { + pub fn new(name: &str, description: &str, pack_format: u8) -> Self { + Self { + name: name.to_string(), + description: description.to_string(), + pack_format, + version: "0.1.0".to_string(), + } + } +} +impl Default for PackConfig { + fn default() -> Self { + Self { + name: "shulkerscript-pack".to_string(), + description: "A Minecraft datapack created with shulkerscript".to_string(), + pack_format: 26, + version: "0.1.0".to_string(), + } + } +} + +fn default_pack_format() -> u8 { + 26 +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..8ea1c37 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,19 @@ +use std::path::PathBuf; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("An error occurred while parsing command-line arguments.")] + IoError(#[from] std::io::Error), + #[error("An error occured while serializing to TOML.")] + TomlSerializeError(#[from] toml::ser::Error), + #[error("An error occured while deserializing from TOML.")] + TomlDeserializeError(#[from] toml::de::Error), + #[error("No file/directory found at path {0}.")] + PathNotFoundError(PathBuf), + #[error("An error occured because the directory {0} is not empty.")] + NonEmptyDirectoryError(PathBuf), + #[error("An error occured because the path {0} is not a directory.")] + NotDirectoryError(PathBuf), +} + +pub type Result = std::result::Result; diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..add9d0f --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,6 @@ +pub mod cli; +pub mod config; +pub mod error; +pub mod subcommands; +pub mod terminal_output; +pub mod util; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..f32b514 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,13 @@ +use std::process::ExitCode; + +use clap::Parser; +use shulkerscript::cli::Args; + +fn main() -> ExitCode { + let args = Args::parse(); + + match args.run() { + Ok(_) => ExitCode::SUCCESS, + Err(_) => ExitCode::FAILURE, + } +} diff --git a/src/subcommands/init.rs b/src/subcommands/init.rs new file mode 100644 index 0000000..a25fb6a --- /dev/null +++ b/src/subcommands/init.rs @@ -0,0 +1,103 @@ +use std::{fs, path::Path}; + +use crate::{ + config::ProjectConfig, + error::{Error, Result}, + terminal_output::{print_error, print_info, print_success}, + util::to_absolute_path, +}; + +pub fn init( + verbose: bool, + path: &Path, + name: Option<&str>, + description: Option<&str>, + pack_format: Option, + force: bool, +) -> Result<()> { + if !path.exists() { + print_error("The specified path does not exist."); + Err(Error::PathNotFoundError(path.to_path_buf())) + } else if !path.is_dir() { + print_error("The specified path is not a directory."); + Err(Error::NotDirectoryError(path.to_path_buf())) + } else if !force && path.read_dir()?.next().is_some() { + print_error("The specified directory is not empty."); + Err(Error::NonEmptyDirectoryError(path.to_path_buf())) + } else { + print_info("Initializing a new Shulkerscript project..."); + + // Create the pack.toml file + create_pack_config(verbose, path, name, description, pack_format)?; + + // Create the .gitignore file + create_gitignore(path, verbose)?; + + // Create the src directory + let src_path = path.join("src"); + create_dir(&src_path, verbose)?; + + print_success("Project initialized successfully."); + + Ok(()) + } +} + +fn create_pack_config( + verbose: bool, + base_path: &Path, + name: Option<&str>, + description: Option<&str>, + pack_format: Option, +) -> Result<()> { + let pack_name = name.or_else(|| base_path.file_name().and_then(|os| os.to_str())); + + let path = base_path.join("pack.toml"); + + // Load the default config + let mut content = ProjectConfig::default(); + // Override the default values with the provided ones + if let Some(name) = pack_name { + content.pack.name = name.to_string(); + } + if let Some(description) = description { + content.pack.description = description.to_string(); + } + if let Some(pack_format) = pack_format { + content.pack.pack_format = pack_format; + } + + fs::write(&path, toml::to_string_pretty(&content)?)?; + if verbose { + print_info(&format!( + "Created pack.toml file at {}.", + to_absolute_path(&path)? + )); + } + Ok(()) +} + +fn create_dir(path: &Path, verbose: bool) -> std::io::Result<()> { + if !path.exists() { + fs::create_dir(path)?; + if verbose { + print_info(&format!( + "Created directory at {}.", + to_absolute_path(path)? + )); + } + } + Ok(()) +} + +fn create_gitignore(path: &Path, verbose: bool) -> std::io::Result<()> { + let gitignore = path.join(".gitignore"); + fs::write(&gitignore, "/dist\n")?; + if verbose { + print_info(&format!( + "Created .gitignore file at {}.", + to_absolute_path(&gitignore)? + )); + } + Ok(()) +} diff --git a/src/subcommands/mod.rs b/src/subcommands/mod.rs new file mode 100644 index 0000000..f771f37 --- /dev/null +++ b/src/subcommands/mod.rs @@ -0,0 +1,2 @@ +mod init; +pub use init::init; diff --git a/src/terminal_output.rs b/src/terminal_output.rs new file mode 100644 index 0000000..039619a --- /dev/null +++ b/src/terminal_output.rs @@ -0,0 +1,12 @@ +use colored::Colorize; + +pub fn print_info(msg: &str) { + println!("[{}] {msg}", "INFO".blue()) +} + +pub fn print_success(msg: &str) { + println!("[{}] {msg}", "SUCCESS".green()) +} +pub fn print_error(msg: &str) { + println!("[{}] {msg}", "ERROR".red()) +} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..6c330b6 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,9 @@ +use std::{io, path::Path}; + +pub fn to_absolute_path(path: &Path) -> io::Result { + Ok(std::fs::canonicalize(path)? + .display() + .to_string() + .trim_start_matches(r"\\?\") + .to_string()) +}