diff --git a/Cargo.toml b/Cargo.toml index 11ca95a..ae33e0e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,9 +17,10 @@ name = "shulkerscript" path = "src/main.rs" [features] -default = ["zip", "lua"] +default = ["zip", "lua", "watch"] lang-debug = [] lua = ["shulkerscript/lua"] +watch = ["dep:notify-debouncer-mini", "dep:ctrlc"] zip = ["shulkerbox/zip"] [dependencies] @@ -34,4 +35,5 @@ git2 = { version = "0.18.3", default-features = false } path-absolutize = "3.1.1" color-eyre = "0.6.3" dotenvy = "0.15.7" - +notify-debouncer-mini = { version = "0.4.1", default-features = false, optional = true } +ctrlc = { version = "3.4.4", optional = true } diff --git a/README.md b/README.md index 9d51794..10858b0 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,16 @@ Options: Environment variables: - `DATAPACK_DIR` The output directory [default: `./dist`] +### Watch for changes +```bash +shulkerscript watch [OPTIONS] [SUBCOMMAND] +``` +Where [SUBCOMMAND] is either `build` or `package` [default: `build`] + +Options: +- `--no-initial` Do not run the command initially +- `--debounce-time ` The time to wait in ms after the last change before running the command [default: `2000`] + ## Contributing Pull requests are welcome. For major changes, please open an issue first diff --git a/src/cli.rs b/src/cli.rs index 29062ad..d156b86 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -24,6 +24,9 @@ pub enum Command { #[cfg(feature = "zip")] /// Build and package the project. Package(subcommands::PackageArgs), + #[cfg(feature = "watch")] + /// Watch for changes and execute command. + Watch(subcommands::WatchArgs), #[cfg(feature = "lang-debug")] /// Build the project and dump the intermediate state. LangDebug(subcommands::LangDebugArgs), @@ -31,12 +34,20 @@ pub enum Command { impl Args { pub fn run(&self) -> Result<()> { - match &self.cmd { - Command::Init(args) => subcommands::init(self.verbose, args)?, - Command::Build(args) => subcommands::build(self.verbose, args)?, - Command::Clean(args) => subcommands::clean(self.verbose, args)?, + self.cmd.run(self.verbose) + } +} + +impl Command { + pub fn run(&self, verbose: bool) -> Result<()> { + match self { + Command::Init(args) => subcommands::init(verbose, args)?, + Command::Build(args) => subcommands::build(verbose, args)?, + Command::Clean(args) => subcommands::clean(verbose, args)?, #[cfg(feature = "zip")] - Command::Package(args) => subcommands::package(self.verbose, args)?, + Command::Package(args) => subcommands::package(verbose, args)?, + #[cfg(feature = "watch")] + Command::Watch(args) => subcommands::watch(verbose, args)?, #[cfg(feature = "lang-debug")] Command::LangDebug(args) => subcommands::lang_debug(args)?, } diff --git a/src/subcommands/init.rs b/src/subcommands/init.rs index 98b21e7..8f4a206 100644 --- a/src/subcommands/init.rs +++ b/src/subcommands/init.rs @@ -20,26 +20,26 @@ use crate::{ pub struct InitArgs { /// The path of the folder to initialize in. #[clap(default_value = ".")] - path: PathBuf, + pub path: PathBuf, /// The name of the project. #[clap(short, long)] - name: Option, + pub name: Option, /// The description of the project. #[clap(short, long)] - description: Option, + pub description: Option, /// The pack format version. #[clap(short, long)] - pack_format: Option, + pub pack_format: Option, /// Force initialization even if the directory is not empty. #[clap(short, long)] - force: bool, + pub force: bool, /// The version control system to initialize. #[clap(long, default_value = "git")] - vcs: VersionControlSystem, + pub vcs: VersionControlSystem, } #[derive(Debug, Clone, Copy, Default, ValueEnum)] -enum VersionControlSystem { +pub enum VersionControlSystem { #[default] Git, None, diff --git a/src/subcommands/lang_debug.rs b/src/subcommands/lang_debug.rs index da68912..0cc4a7c 100644 --- a/src/subcommands/lang_debug.rs +++ b/src/subcommands/lang_debug.rs @@ -7,13 +7,13 @@ use std::path::PathBuf; pub struct LangDebugArgs { /// The path of the project to compile. #[clap(default_value = ".")] - path: PathBuf, + pub path: PathBuf, /// The state to dump. #[clap(short, long, default_value = "ast")] - dump: DumpState, + pub dump: DumpState, /// Pretty-print the output. #[clap(short, long)] - pretty: bool, + pub pretty: bool, } #[derive(ValueEnum, Debug, Clone, Copy, Default)] diff --git a/src/subcommands/mod.rs b/src/subcommands/mod.rs index f45cbdd..7b784be 100644 --- a/src/subcommands/mod.rs +++ b/src/subcommands/mod.rs @@ -12,6 +12,11 @@ mod package; #[cfg(feature = "zip")] pub use package::{package, PackageArgs}; +#[cfg(feature = "watch")] +mod watch; +#[cfg(feature = "watch")] +pub use watch::{watch, WatchArgs}; + #[cfg(feature = "lang-debug")] mod lang_debug; #[cfg(feature = "lang-debug")] diff --git a/src/subcommands/package.rs b/src/subcommands/package.rs index b9ba460..4ee95d3 100644 --- a/src/subcommands/package.rs +++ b/src/subcommands/package.rs @@ -14,7 +14,7 @@ use super::BuildArgs; #[derive(Debug, clap::Args, Clone)] pub struct PackageArgs { #[clap(flatten)] - build_args: BuildArgs, + pub build_args: BuildArgs, } pub fn package(_verbose: bool, args: &PackageArgs) -> Result<()> { diff --git a/src/subcommands/watch.rs b/src/subcommands/watch.rs new file mode 100644 index 0000000..7a4fbb6 --- /dev/null +++ b/src/subcommands/watch.rs @@ -0,0 +1,112 @@ +use std::{ + path::{Path, PathBuf}, + thread, + time::Duration, +}; + +use clap::Subcommand; +use notify_debouncer_mini::{new_debouncer, notify::*, DebounceEventResult}; + +use super::BuildArgs; +use crate::{ + cli::Command, + error::Result, + terminal_output::{print_error, print_info}, +}; + +#[derive(Debug, clap::Args, Clone)] +pub struct WatchArgs { + /// Do not run the command when starting, only after changes are detected. + #[clap(short, long)] + no_inital: bool, + /// The time to wait in ms before running the command after changes are detected. + #[clap(short, long, default_value = "2000")] + debounce_time: u64, + /// The command to run when changes are detected. + #[command(subcommand)] + cmd: Option, +} + +#[derive(Debug, Clone, Subcommand)] +pub enum WatchSubcommand { + /// Build the project. + Build(BuildArgs), + #[cfg(feature = "zip")] + /// Build and package the project. + Package(super::PackageArgs), +} + +impl From for Command { + fn from(value: WatchSubcommand) -> Self { + match value { + WatchSubcommand::Build(args) => Command::Build(args), + #[cfg(feature = "zip")] + WatchSubcommand::Package(args) => Command::Package(args), + } + } +} + +pub fn watch(verbose: bool, args: &WatchArgs) -> Result<()> { + let cmd = Command::from(args.cmd.to_owned().unwrap_or_else(|| { + WatchSubcommand::Build(BuildArgs { + path: PathBuf::from("."), + output: None, + assets: None, + }) + })); + + let project_path = match &args.cmd { + Some(WatchSubcommand::Build(args)) => args.path.as_path(), + #[cfg(feature = "zip")] + Some(WatchSubcommand::Package(args)) => args.build_args.path.as_path(), + None => Path::new("."), + }; + + #[allow(clippy::collapsible_if)] + if !args.no_inital { + if cmd.run(verbose).is_err() { + print_error("Command failed to run initially"); + } + } + + ctrlc::set_handler(move || { + print_info("Stopping watcher..."); + std::process::exit(0); + }) + .expect("Error setting Ctrl-C handler"); + + let mut debouncer = new_debouncer( + Duration::from_millis(args.debounce_time), + move |res: DebounceEventResult| { + if res.is_ok() { + if cmd.run(verbose).is_err() { + print_error("Command failed to run"); + } + } else { + std::process::exit(1); + } + }, + ) + .expect("Failed to initialize watcher"); + + let watcher = debouncer.watcher(); + watcher + .watch(project_path.join("src").as_path(), RecursiveMode::Recursive) + .expect("Failed to watch project src"); + watcher + .watch( + project_path.join("pack.png").as_path(), + RecursiveMode::NonRecursive, + ) + .expect("Failed to watch project pack.png"); + watcher + .watch( + project_path.join("pack.toml").as_path(), + RecursiveMode::NonRecursive, + ) + .expect("Failed to watch project pack.toml"); + + loop { + thread::sleep(Duration::from_secs(60)); + } +}