1use std::path::{Path, PathBuf};
5use std::process::Command;
6use std::{fs, thread};
7use tiny_http::{Server, Response};
8use webbrowser;
9
10pub mod random;
11pub mod tuneable;
12pub mod engine;
13pub mod video;
14pub mod apps;
15pub mod ui;
16pub mod tensor;
17
18#[path = "../py/mod.rs"]
19pub mod python_api;
20
21pub mod clipboard;
22pub mod rasterizer;
23
24pub fn is_xos_project_root(path: &Path) -> bool {
26 let cargo = path.join("Cargo.toml");
27 if !cargo.exists() {
28 return false;
29 }
30 if path
31 .join("src")
32 .join("core")
33 .join("crates")
34 .join("xos-java")
35 .join("Cargo.toml")
36 .exists()
37 {
38 return true;
39 }
40 if path
41 .join("src")
42 .join("ios")
43 .join("build-ios.sh")
44 .exists()
45 {
46 return true;
47 }
48 path.join("src").join("core").join("apps").join("ball.rs").exists()
49}
50
51fn project_root_from_target_executable(exe: &Path) -> Option<PathBuf> {
54 let file_name = exe.file_name()?.to_str()?;
55 if file_name != "xos" && file_name != "xos.exe" {
56 return None;
57 }
58 let profile = exe.parent()?.file_name()?.to_str()?;
59 if profile != "release" && profile != "debug" {
60 return None;
61 }
62 let target_dir = exe.parent()?.parent()?;
63 if target_dir.file_name()?.to_str()? != "target" {
64 return None;
65 }
66 let root = target_dir.parent()?.to_path_buf();
67 if !is_xos_project_root(&root) {
68 return None;
69 }
70 match std::fs::canonicalize(&root) {
71 Ok(c) if is_xos_project_root(&c) => Some(c),
72 Ok(_) | Err(_) => Some(root),
73 }
74}
75
76pub fn find_xos_project_root() -> Result<PathBuf, String> {
81 if let Ok(env) = std::env::var("XOS_PROJECT_ROOT") {
82 let p = PathBuf::from(env.trim());
83 if is_xos_project_root(&p) {
84 return Ok(p);
85 }
86 return Err(format!(
87 "XOS_PROJECT_ROOT is set but does not look like the xos repo: {}",
88 p.display()
89 ));
90 }
91
92 if let Ok(exe) = std::env::current_exe() {
93 if let Some(root) = project_root_from_target_executable(&exe) {
94 return Ok(root);
95 }
96 let mut opt = exe.parent().map(PathBuf::from);
97 for _ in 0..16 {
98 if let Some(ref dir) = opt {
99 if is_xos_project_root(dir) {
100 return Ok(dir.clone());
101 }
102 opt = dir.parent().map(PathBuf::from);
103 } else {
104 break;
105 }
106 }
107 }
108
109 if let Some(dir) = option_env!("CARGO_MANIFEST_DIR") {
110 let p = PathBuf::from(dir);
111 if is_xos_project_root(&p) {
112 return Ok(p);
113 }
114 }
115
116 let mut current =
117 std::env::current_dir().map_err(|e| format!("current_dir: {e}"))?;
118 loop {
119 if is_xos_project_root(¤t) {
120 return Ok(current);
121 }
122 let xos_sub = current.join("xos");
123 if is_xos_project_root(&xos_sub) {
124 return Ok(xos_sub);
125 }
126 match current.parent() {
127 Some(parent) => current = parent.to_path_buf(),
128 None => {
129 return Err(
130 "could not find xos project root (set XOS_PROJECT_ROOT to your clone, or run from inside the repo)"
131 .into(),
132 );
133 }
134 }
135 }
136}
137
138pub mod py_engine {
139 use crate::engine::{Application, EngineState};
142
143 pub struct PyApplicationWrapper {
144 }
146
147 impl PyApplicationWrapper {
148 pub fn new_from_source(_source: &str, _app_class_name: String) -> Result<Self, String> {
149 Err("Python application wrapper not yet implemented with rustpython".to_string())
150 }
151 }
152
153 impl Application for PyApplicationWrapper {
154 fn setup(&mut self, _state: &mut EngineState) -> Result<(), String> {
155 Err("Not implemented".to_string())
156 }
157
158 fn tick(&mut self, _state: &mut EngineState) {
159 }
161
162 fn on_mouse_down(&mut self, _state: &mut EngineState) {
163 }
165
166 fn on_mouse_up(&mut self, _state: &mut EngineState) {
167 }
169
170 fn on_mouse_move(&mut self, _state: &mut EngineState) {
171 }
173 }
174}
175
176#[cfg(not(target_arch = "wasm32"))]
178pub fn start(game: &str) -> Result<(), Box<dyn std::error::Error>> {
179 if let Some(app) = apps::get_app(game) {
180 #[cfg(not(target_os = "ios"))]
181 if game == "overlay" {
182 return engine::start_overlay_native(app);
183 }
184 engine::start_native(app)
185 } else {
186 Err(format!("App '{}' not found", game).into())
187 }
188}
189
190#[cfg(target_arch = "wasm32")]
192use wasm_bindgen::prelude::*;
193#[cfg(target_arch = "wasm32")]
194use wasm_bindgen::JsValue;
195
196#[cfg(target_arch = "wasm32")]
197#[wasm_bindgen(start)]
198pub fn start() -> Result<(), JsValue> {
199 let game = option_env!("GAME_SELECTION").unwrap_or("ball");
200 let app = apps::get_app(game).ok_or(JsValue::from_str("App not found"))?;
201 engine::run_web(app)
202}
203
204fn build_wasm(app_name: &str) {
206 let out_dir = format!("src/core/react-native-embedder/static/pkg/");
207
208 let mut command = Command::new("wasm-pack");
209 command
210 .env("GAME_SELECTION", app_name)
211 .args(["build", "--target", "web", "--out-dir", &out_dir]);
212
213 let status = command.status().expect("Failed to run wasm-pack");
214 if !status.success() {
215 panic!("WASM build failed");
216 }
217
218 println!("β
WASM built to {out_dir} with app: {app_name}");
219}
220
221
222fn launch_browser() {
223 thread::spawn(|| {
224 let _ = webbrowser::open("http://localhost:8080");
225 });
226}
227
228fn mime_type(path: &str) -> &'static str {
229 if path.ends_with(".html") {
230 "text/html"
231 } else if path.ends_with(".js") {
232 "application/javascript"
233 } else if path.ends_with(".wasm") {
234 "application/wasm"
235 } else if path.ends_with(".css") {
236 "text/css"
237 } else {
238 "application/octet-stream"
239 }
240}
241
242fn start_web_server() {
243 let server = Server::http("0.0.0.0:8080").unwrap();
244 println!("π Serving at http://localhost:8080");
245
246 for request in server.incoming_requests() {
247 let url = request.url();
248 let path = if url == "/" {
249 concat!(
251 env!("CARGO_MANIFEST_DIR"),
252 "/src/core/react-native-embedder/static/index.html"
253 )
254 .to_string()
255 } else {
256 let full_path = format!("src/core/react-native-embedder/static{}", url);
257 if std::fs::metadata(&full_path).map_or(false, |m| m.is_file()) {
258 full_path
259 } else {
260 eprintln!("β File not found: {full_path}");
261 concat!(
263 env!("CARGO_MANIFEST_DIR"),
264 "/src/core/react-native-embedder/static/index.html"
265 )
266 .to_string()
267 }
268 };
269
270 match fs::read(&path) {
271 Ok(data) => {
272 let content_type = mime_type(&path);
273 let response = Response::from_data(data)
274 .with_header(tiny_http::Header::from_bytes(&b"Content-Type"[..], content_type).unwrap());
275 let _ = request.respond(response);
276 }
277 Err(e) => {
278 eprintln!("β Failed to read {path}: {e}");
279 let response = Response::from_string("404 Not Found").with_status_code(404);
280 let _ = request.respond(response);
281 }
282 }
283 }
284}
285
286fn launch_expo() {
287 let mut cmd = Command::new("npx");
288 cmd.arg("expo").arg("start").arg("--tunnel");
289 cmd.current_dir("src/core/react-native-embedder");
290
291 let status = cmd.status().expect("Failed to launch Expo. Is it installed?");
292 if !status.success() {
293 panic!("Expo failed to launch.");
294 }
295}
296
297pub fn run_game(game: &str, web: bool, react_native: bool) {
299 if web {
300 println!("π Launching '{game}' in web mode...");
301 build_wasm(game);
302 launch_browser();
303 start_web_server();
304 } else if react_native {
305 println!("π± Launching '{game}' in React Native mode...");
306 build_wasm(game);
307 thread::spawn(start_web_server);
308 launch_expo();
309 } else {
310 #[cfg(not(target_arch = "wasm32"))]
313 {
314 start(game).unwrap();
315 }
316
317 #[cfg(target_arch = "wasm32")]
318 {
319 start().unwrap();
320 }
321 }
322}
323
324
325pub fn version() -> &'static str {
326 env!("CARGO_PKG_VERSION")
327}
328
329pub fn print(message: &str) {
336 crate::apps::coder::logging::log_to_coder(message);
338
339 #[cfg(target_os = "ios")]
340 {
341 crate::engine::ios_ffi::log_to_ios(message);
342 }
343
344 #[cfg(not(target_os = "ios"))]
345 {
346 std::println!("{}", message);
347 }
348}
349
350pub mod xos {
352 pub use crate::print;
353}
354
355pub fn launch_ios_app(app_name: &str) {
356 #[cfg(target_os = "ios")]
357 {
358 crate::print(&format!("Launching iOS app: {}", app_name));
360 }
361 #[cfg(not(target_os = "ios"))]
362 {
363 use std::process::{Command, Stdio};
364
365 let project_root = match find_xos_project_root() {
366 Ok(p) => p,
367 Err(e) => {
368 eprintln!("β {e}");
369 std::process::exit(1);
370 }
371 };
372
373 let launch_script = project_root.join("src").join("ios").join("launch-device.sh");
374
375 if !launch_script.exists() {
376 eprintln!("β launch-device.sh not found at: {}", launch_script.display());
377 eprintln!(" Expected location: src/ios/launch-device.sh");
378 std::process::exit(1);
379 }
380
381 println!("π± Deploying app '{}' to iOS device...", app_name);
382
383 let mut cmd = Command::new("bash");
384 cmd.arg(&launch_script);
385 cmd.current_dir(project_root.join("src").join("ios"));
386 cmd.env("XOS_APP_NAME", app_name);
388 cmd.stdout(Stdio::inherit());
389 cmd.stderr(Stdio::inherit());
390
391 let status = cmd.status().expect("Failed to run launch-device.sh");
392 if !status.success() {
393 eprintln!("β iOS deployment failed");
394 std::process::exit(1);
395 }
396 }
397}
398
399
400use clap::Parser;
401
402#[derive(Parser, Debug)]
404#[command(name = "xos-app")]
405struct XosAppArgs {
406 #[arg(long)]
407 web: bool,
408
409 #[arg(long = "react-native")]
410 react_native: bool,
411}
412
413
414
415pub fn run<T: engine::Application + 'static>(app: T) {
416 let args = XosAppArgs::parse();
417
418 let app_name = env!("CARGO_PKG_NAME");
419
420 #[cfg(target_arch = "wasm32")]
421 {
422 engine::run_web(Box::new(app)).unwrap();
423 }
424
425 #[cfg(not(target_arch = "wasm32"))]
426 {
427 if args.web {
428 println!("π Launching app in web mode...");
429 build_wasm(app_name);
430 launch_browser();
431 start_web_server();
432 } else if args.react_native {
433 println!("π± Launching app in React Native mode...");
434 build_wasm(app_name);
435 thread::spawn(start_web_server);
436 launch_expo();
437 } else {
438 engine::start_native(Box::new(app)).unwrap();
440 }
441 }
442}
443