shulkerscript_cli/subcommands/
watch.rs1use std::{
2 env, io, iter,
3 path::PathBuf,
4 process::{self, ExitStatus},
5 thread,
6 time::Duration,
7};
8
9use clap::Parser;
10use colored::Colorize;
11use notify_debouncer_mini::{new_debouncer, notify::*, DebounceEventResult};
12
13use crate::{
14 cli::Args,
15 error::Result,
16 terminal_output::{print_error, print_info, print_warning},
17 util,
18};
19
20#[derive(Debug, clap::Args, Clone)]
21pub struct WatchArgs {
22 #[arg(default_value = ".")]
24 pub path: PathBuf,
25 #[arg(short, long)]
29 pub no_inital: bool,
30 #[arg(short, long, value_name = "TIME_IN_MS", default_value = "2000")]
32 pub debounce_time: u64,
33 #[arg(short, long, value_name = "PATH")]
38 pub watch: Vec<PathBuf>,
39 #[arg(short = 'x', long, value_name = "COMMAND", default_value = "build .")]
48 pub execute: Vec<String>,
49 #[arg(short = 'X', long)]
51 pub no_execute: bool,
52 #[arg(short, long, value_name = "COMMAND")]
58 pub shell: Vec<String>,
59}
60
61pub fn watch(args: &WatchArgs) -> Result<()> {
62 let path = util::get_project_path(&args.path).unwrap_or(args.path.clone());
63 print_info(format!("Watching project at {}", path.display()));
64 print_info(format!(
65 "Press {} to stop watching",
66 "Ctrl-C".underline().blue()
67 ));
68
69 let commands = args
70 .execute
71 .iter()
72 .map(|cmd| {
73 let split = cmd.split_whitespace();
74 let prog_name = std::env::args()
75 .next()
76 .unwrap_or(env!("CARGO_PKG_NAME").to_string());
77 Args::parse_from(iter::once(prog_name.as_str()).chain(split.clone()))
78 })
79 .collect::<Vec<_>>();
80
81 let current_dir = if args.no_inital {
82 print_info("Skipping initial commands because of cli flag.");
83 None
84 } else {
85 env::current_dir().ok()
86 };
87
88 if !args.no_inital && (current_dir.is_none() || env::set_current_dir(&path).is_err()) {
89 print_warning("Failed to change working directory to project path. Commands may not work.");
90 }
91
92 #[allow(clippy::collapsible_if)]
93 if !args.no_inital {
94 run_cmds(&commands, args.no_execute, &args.shell, true);
95 }
96
97 ctrlc::set_handler(move || {
98 print_info("Stopping watcher...");
99 process::exit(0);
100 })
101 .expect("Error setting Ctrl-C handler");
102
103 let shell_commands = args.shell.clone();
104 let no_execute = args.no_execute;
105
106 let mut debouncer = new_debouncer(
107 Duration::from_millis(args.debounce_time),
108 move |res: DebounceEventResult| {
109 if res.is_ok() {
110 run_cmds(&commands, no_execute, &shell_commands, false)
111 } else {
112 process::exit(1);
113 }
114 },
115 )
116 .expect("Failed to initialize watcher");
117
118 if let Some(prev_cwd) = current_dir {
119 env::set_current_dir(prev_cwd).expect("Failed to change working directory back");
120 }
121
122 let assets_path = super::build::get_pack_config(&path)
123 .ok()
124 .and_then(|(conf, _)| conf.compiler.and_then(|c| c.assets));
125
126 let watcher = debouncer.watcher();
127 watcher
128 .watch(path.join("src").as_path(), RecursiveMode::Recursive)
129 .expect("Failed to watch project src");
130 watcher
131 .watch(path.join("pack.png").as_path(), RecursiveMode::NonRecursive)
132 .expect("Failed to watch project pack.png");
133 watcher
134 .watch(
135 path.join("pack.toml").as_path(),
136 RecursiveMode::NonRecursive,
137 )
138 .expect("Failed to watch project pack.toml");
139 if let Some(assets_path) = assets_path {
140 let full_assets_path = path.join(assets_path);
141 if full_assets_path.exists() {
142 watcher
143 .watch(full_assets_path.as_path(), RecursiveMode::Recursive)
144 .expect("Failed to watch project assets");
145 }
146 }
147
148 for path in args.watch.iter() {
150 if path.exists() {
151 watcher
152 .watch(path, RecursiveMode::Recursive)
153 .expect("Failed to watch custom path");
154 } else {
155 print_warning(format!(
156 "Path {} does not exist. Skipping...",
157 path.display()
158 ));
159 }
160 }
161
162 if env::set_current_dir(path).is_err() {
163 print_warning("Failed to change working directory to project path. Commands may not work.");
164 }
165
166 loop {
167 thread::sleep(Duration::from_secs(60));
168 }
169}
170
171fn run_cmds(cmds: &[Args], no_execute: bool, shell_cmds: &[String], initial: bool) {
172 if initial {
173 print_info("Running commands initially...");
174 } else {
175 print_info("Changes have been detected. Running commands...");
176 }
177 if !no_execute {
178 for (index, args) in cmds.iter().enumerate() {
179 if args.run().is_err() {
180 print_error(format!("Error running command: {}", index + 1));
181 print_error("Not running further commands.");
182 return;
183 }
184 }
185 }
186 for (index, cmd) in shell_cmds.iter().enumerate() {
187 let status = run_shell_cmd(cmd);
188 match status {
189 Ok(status) if !status.success() => {
190 print_error(format!(
191 "Shell command {} exited unsuccessfully with status code {}",
192 index + 1,
193 status.code().unwrap_or(1)
194 ));
195 print_error("Not running further shell commands.");
196 return;
197 }
198 Ok(_) => {}
199 Err(_) => {
200 print_error(format!("Error running shell command: {}", index + 1));
201 print_error("Not running further shell commands.");
202 return;
203 }
204 }
205 }
206}
207
208fn run_shell_cmd(cmd: &str) -> io::Result<ExitStatus> {
209 let mut command = if cfg!(target_os = "windows") {
210 let mut command = process::Command::new("cmd");
211 command.arg("/C");
212 command
213 } else {
214 let mut command = process::Command::new(env::var("SHELL").unwrap_or("sh".to_string()));
215 command.arg("-c");
216 command
217 };
218
219 command.arg(cmd).status()
220}