1use std::ffi::OsString;
8use std::path::{Path, PathBuf};
9use std::process::Command;
10
11use shuyi_platform::WATCH_VAR;
12
13use crate::contract::codes;
14use crate::export::package_and_program;
15use crate::project::{self, Project};
16
17const DEFAULT_ASSETS: &str = "assets";
19
20#[derive(Debug, PartialEq, Eq)]
22struct Plan {
23 args: Vec<String>,
25 assets: PathBuf,
27}
28
29fn plan(root: &Path, assets: Option<&str>, package: &str, bin: &str) -> Result<Plan, String> {
32 let named = assets.unwrap_or(DEFAULT_ASSETS);
33 let assets = root.join(named);
34 if !assets.is_dir() {
35 return Err(format!(
36 "no folder {named} here: `assets` in {} names the folder of the game's files",
37 project::FILE
38 ));
39 }
40 Ok(Plan {
41 args: ["run", "-p", package, "--bin", bin]
42 .map(str::to_owned)
43 .to_vec(),
44 assets: std::path::absolute(&assets).unwrap_or(assets),
45 })
46}
47
48pub fn main(project: &Project, args: &[String]) -> u8 {
52 let command = project.config.command();
53 if args.iter().any(|arg| arg != "--watch") {
54 let error = "--watch takes nothing else: it runs the game in its window";
55 let help = format!("usage: {command} run --watch");
56 eprint!(
57 "{}",
58 codes::said("run", codes::WRONG_ARGUMENTS.0, error, Some(&help))
59 );
60 return 2;
61 }
62 let config = &project.config;
63 let planned = package_and_program(&project.root, config)
64 .map_err(|e| e.message())
65 .and_then(|(package, bin)| plan(&project.root, config.assets.as_deref(), &package, &bin));
66 let plan = match planned {
67 Ok(plan) => plan,
68 Err(error) => {
69 return crate::contract::cli::setup_failed("run", args, codes::PROJECT_FILE, &error);
70 }
71 };
72 run(&plan, &project.root, crate::cargo())
73}
74
75fn run(plan: &Plan, root: &Path, cargo: OsString) -> u8 {
77 println!(
78 "watching {}: a file changed there reloads in the game",
79 plan.assets.display()
80 );
81 let status = Command::new(cargo)
82 .args(&plan.args)
83 .current_dir(root)
84 .env(WATCH_VAR, &plan.assets)
85 .status();
86 match status {
87 Ok(status) if status.success() => 0,
88 Ok(status) => {
89 eprintln!(
90 "run --watch: cargo {} exited with {status}",
91 plan.args.join(" ")
92 );
93 1
94 }
95 Err(e) => {
96 let error = format!("cargo doesn't start: {e}");
97 eprint!(
98 "{}",
99 codes::said(
100 "run",
101 codes::TOOLCHAIN.0,
102 &error,
103 codes::help_of(codes::TOOLCHAIN.0)
104 )
105 );
106 3
107 }
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use std::fs;
114
115 use super::*;
116 use crate::project::Config;
117
118 fn folder(name: &str) -> PathBuf {
120 let dir = std::env::temp_dir().join(format!("shuyi-watch-{name}-{}", std::process::id()));
121 let _ = fs::remove_dir_all(&dir);
122 fs::create_dir_all(&dir).unwrap_or_else(|e| panic!("{e:?}"));
123 dir
124 }
125
126 #[test]
127 fn it_runs_the_program_over_the_folder_the_project_names() {
128 let dir = folder("names");
129 fs::create_dir(dir.as_path().join("files")).unwrap();
130 let planned = plan(dir.as_path(), Some("files"), "game", "game").unwrap();
131 assert_eq!(planned.args, ["run", "-p", "game", "--bin", "game"]);
132 assert!(planned.assets.ends_with("files"), "{planned:?}");
133 assert!(planned.assets.is_absolute(), "{planned:?}");
134 }
135
136 #[test]
137 fn the_folder_is_assets_unless_the_project_says() {
138 let dir = folder("default");
139 let error = plan(dir.as_path(), None, "game", "game").unwrap_err();
140 assert!(error.contains("no folder assets"), "{error}");
141 assert!(error.contains("shuyi.ron"), "{error}");
142 fs::create_dir(dir.as_path().join("assets")).unwrap();
143 let planned = plan(dir.as_path(), None, "game", "game").unwrap();
144 assert!(planned.assets.ends_with("assets"), "{planned:?}");
145 }
146
147 #[test]
148 fn it_takes_no_other_argument_and_needs_a_program() {
149 let dir = folder("args");
150 let project = Project::new(dir.as_path(), Config::default());
151 let args = ["--watch".to_owned(), "--frames".to_owned()];
152 assert_eq!(main(&project, &args), 2);
153 assert_eq!(main(&project, &["--watch".to_owned()]), 3);
154 }
155
156 #[test]
157 fn a_game_that_fails_is_a_failure_and_no_cargo_a_setup_to_fix() {
158 let dir = folder("fails");
159 let planned = Plan {
160 args: vec!["--no-such-flag".to_owned()],
161 assets: dir.as_path().to_path_buf(),
162 };
163 assert_eq!(run(&planned, dir.as_path(), crate::cargo()), 1);
164 let none = OsString::from("no-such-cargo-here");
165 assert_eq!(run(&planned, dir.as_path(), none), 3);
166 }
167}