From 3b7abe02bf6b1760be28d27fc2afacff610f6334 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Sat, 7 Jan 2023 21:53:13 +0100 Subject: [PATCH] feat: Implement command to create mod project from template --- CHANGELOG.adoc | 1 + Cargo.lock | 179 +++++++++++++++++++++++++++++++++++++ crates/dtmt/Cargo.toml | 2 + crates/dtmt/src/cmd/new.rs | 179 ++++++++++++++++++++++++++++++++++--- crates/dtmt/src/main.rs | 2 +- 5 files changed, 351 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index be5ddeb..840de14 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -6,6 +6,7 @@ - show status after adding dictionary entries - implement building mod bundles +- implement command to create mod project from template == [v0.2.0] - 2022-12-28 diff --git a/Cargo.lock b/Cargo.lock index 0c9bbf0..53a5013 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,15 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -185,6 +194,17 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "clipboard-win" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4ab1b92798304eedc095b53942963240037c0516452cb11aeba709d420b2219" +dependencies = [ + "error-code", + "str-buf", + "winapi", +] + [[package]] name = "color-eyre" version = "0.6.2" @@ -313,6 +333,16 @@ dependencies = [ "dirs-sys", ] +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + [[package]] name = "dirs-sys" version = "0.3.7" @@ -324,6 +354,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "dtmt" version = "0.2.0" @@ -338,9 +379,11 @@ dependencies = [ "libloading", "nanorand", "pin-project-lite", + "promptly", "sdk", "serde", "serde_sjson", + "string_template", "tempfile", "tokio", "tokio-stream", @@ -350,6 +393,12 @@ dependencies = [ "zip", ] +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + [[package]] name = "errno" version = "0.2.8" @@ -371,6 +420,16 @@ dependencies = [ "libc", ] +[[package]] +name = "error-code" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" +dependencies = [ + "libc", + "str-buf", +] + [[package]] name = "eyre" version = "0.6.8" @@ -390,6 +449,17 @@ dependencies = [ "instant", ] +[[package]] +name = "fd-lock" +version = "3.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb21c69b9fea5e15dbc1049e4b77145dd0ba1c84019c488102de0dc4ea4b0a27" +dependencies = [ + "cfg-if", + "rustix", + "windows-sys", +] + [[package]] name = "flate2" version = "1.0.25" @@ -650,6 +720,15 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -683,6 +762,28 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "nix" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" +dependencies = [ + "bitflags", + "cc", + "cfg-if", + "libc", + "memoffset", +] + [[package]] name = "nom" version = "7.1.2" @@ -837,6 +938,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "promptly" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9acbc6c5a5b029fe58342f58445acb00ccfe24624e538894bc2f04ce112980ba" +dependencies = [ + "rustyline", +] + [[package]] name = "quote" version = "1.0.23" @@ -846,6 +956,16 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + [[package]] name = "rand_core" version = "0.6.4" @@ -878,6 +998,8 @@ version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" dependencies = [ + "aho-corasick", + "memchr", "regex-syntax", ] @@ -925,12 +1047,42 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "rustyline" +version = "9.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db7826789c0e25614b03e5a54a0717a86f9ff6e6e5247f92b369472869320039" +dependencies = [ + "bitflags", + "cfg-if", + "clipboard-win", + "dirs-next", + "fd-lock", + "libc", + "log", + "memchr", + "nix", + "radix_trie", + "scopeguard", + "smallvec", + "unicode-segmentation", + "unicode-width", + "utf8parse", + "winapi", +] + [[package]] name = "ryu" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + [[package]] name = "sdk" version = "0.2.0" @@ -1037,6 +1189,21 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +[[package]] +name = "str-buf" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" + +[[package]] +name = "string_template" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6f2c6b2c3fa950895c9aeb0c3cb9271d7eb580662af9af2b711b593f446320" +dependencies = [ + "regex", +] + [[package]] name = "strsim" version = "0.10.0" @@ -1282,12 +1449,24 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" +[[package]] +name = "unicode-segmentation" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" + [[package]] name = "unicode-width" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +[[package]] +name = "utf8parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372" + [[package]] name = "valuable" version = "0.1.0" diff --git a/crates/dtmt/Cargo.toml b/crates/dtmt/Cargo.toml index d83c2d2..1a0a1e6 100644 --- a/crates/dtmt/Cargo.toml +++ b/crates/dtmt/Cargo.toml @@ -23,6 +23,8 @@ tracing-error = "0.2.0" tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } confy = "0.5.1" zip = "0.6.3" +string_template = "0.2.1" +promptly = "0.3.1" [dev-dependencies] tempfile = "3.3.0" diff --git a/crates/dtmt/src/cmd/new.rs b/crates/dtmt/src/cmd/new.rs index a798f0c..a7a66ca 100644 --- a/crates/dtmt/src/cmd/new.rs +++ b/crates/dtmt/src/cmd/new.rs @@ -1,19 +1,176 @@ -use clap::{Arg, ArgMatches, Command}; -use color_eyre::eyre::Result; +use std::collections::HashMap; +use std::path::PathBuf; -pub(crate) fn _command_definition() -> Command { +use clap::{Arg, ArgMatches, Command}; +use color_eyre::eyre::{self, Context, Result}; +use color_eyre::Help; +use futures::{StreamExt, TryStreamExt}; +use string_template::Template; +use tokio::fs::{self, DirBuilder}; + +const TEMPLATES: [(&str, &str); 6] = [ + ( + "dtmt.cfg", + r#"name = "{{name}}" +description = "An elaborate description of my cool game mod!" +version = "0.1.0" + +packages = [ + "packages/{{name}}" +] + +depends = [ + "dmf" +] +"#, + ), + ( + "{{name}}.mod", + r#"return { + run = function() + fassert(rawget(_G, "new_mod"), "`{{title}}` encountered an error loading the Darktide Mod Framework.") + + new_mod("{{name}}", { + mod_script = "scripts/mods/{{name}}/{{name}}", + mod_data = "scripts/mods/{{name}}/{{name}}_data", + mod_localization = "scripts/mods/{{name}}/{{name}}_localization", + }) + end, + packages = {}, +}"#, + ), + ( + "packages/{{name}}.package", + r#"lua = [ + "scripts/mods/{{name}}/*" +] +"#, + ), + ( + "scripts/mods/{{name}}/{{name}}.lua", + r#"local mod = get_mod("{{name}}") + +-- Your mod code goes here. +-- https://vmf-docs.verminti.de +"#, + ), + ( + "scripts/mods/{{name}}/{{name}}_data.lua", + r#"local mod = get_mod("{{name}}") + +return { + name = "{{title}}", + description = mod:localize("mod_description"), + is_togglable = true, +}"#, + ), + ( + "scripts/mods/{{name}}/{{name}}_localization.lua", + r#"return { + mod_description = { + en = "An elaborate description of my cool game mod!", + }, +}"#, + ), +]; + +pub(crate) fn command_definition() -> Command { Command::new("new") .about("Create a new project") - .arg(Arg::new("name").help( - "The name of the new project. Will default to the name of the project's directory", - )) - .arg(Arg::new("directory").required(true).help( - "The directory where to initialize the new project. This directory must be empty\ - or must not exist. If `.` is given, the current directory will be used.", + .arg( + Arg::new("title") + .long("title") + .help("The display name of the new mod."), + ) + .arg(Arg::new("root").help( + "The directory where to initialize the new project. This directory must be empty \ + or must not exist. If omitted or `.` is given, the current directory \ + will be used.", )) } #[tracing::instrument(skip_all)] -pub(crate) async fn run(_ctx: sdk::Context, _matches: &ArgMatches) -> Result<()> { - unimplemented!() +pub(crate) async fn run(_ctx: sdk::Context, matches: &ArgMatches) -> Result<()> { + let root = if let Some(dir) = matches.get_one::("root") { + if dir == "." { + std::env::current_dir() + .wrap_err("the current working dir is invalid") + .with_suggestion(|| "Change to a different directory.")? + } else { + PathBuf::from(dir) + } + } else { + let prompt = "The mod directory"; + match std::env::current_dir() { + Ok(default) => promptly::prompt_default(prompt, default)?, + Err(_) => promptly::prompt(prompt)?, + } + }; + + let title = if let Some(title) = matches.get_one::("title") { + title.clone() + } else { + promptly::prompt("The mod display name")? + }; + + let name = { + let default = title + .chars() + .map(|c| { + if c.is_ascii_alphanumeric() { + c.to_ascii_lowercase() + } else { + '_' + } + }) + .collect::(); + promptly::prompt_default("The mod identifier name", default)? + }; + + tracing::debug!(root = %root.display()); + tracing::debug!(title, name); + + let mut data = HashMap::new(); + data.insert("name", name.as_str()); + data.insert("title", title.as_str()); + + let templates = TEMPLATES + .iter() + .map(|(path_tmpl, content_tmpl)| { + let path = Template::new(path_tmpl).render(&data); + let content = Template::new(content_tmpl).render(&data); + + (root.join(path), content) + }) + .map(|(path, content)| async move { + let dir = path + .parent() + .ok_or_else(|| eyre::eyre!("invalid root path"))?; + + DirBuilder::new() + .recursive(true) + .create(&dir) + .await + .wrap_err_with(|| format!("failed to create directory {}", dir.display()))?; + + tracing::trace!("Writing file {}", path.display()); + + fs::write(&path, content.as_bytes()) + .await + .wrap_err_with(|| format!("failed to write content to path {}", path.display())) + }); + + futures::stream::iter(templates) + .buffer_unordered(10) + .try_fold((), |_, _| async { Ok(()) }) + .await?; + + tracing::info!( + "Created {} files for mod '{}' in '{}'.", + TEMPLATES.len(), + title, + root.display() + ); + + Ok(()) } diff --git a/crates/dtmt/src/main.rs b/crates/dtmt/src/main.rs index 96c7769..22399dc 100644 --- a/crates/dtmt/src/main.rs +++ b/crates/dtmt/src/main.rs @@ -64,7 +64,7 @@ async fn main() -> Result<()> { .subcommand(cmd::bundle::command_definition()) .subcommand(cmd::dictionary::command_definition()) .subcommand(cmd::murmur::command_definition()) - // .subcommand(cmd::new::command_definition()) + .subcommand(cmd::new::command_definition()) // .subcommand(cmd::watch::command_definition()) .get_matches();