Skip to main content

xos/
lib.rs

1// --- Optional Python Bindings ---
2// Using rustpython-vm instead of pyo3
3
4use 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
24/// True if `path` looks like the root of the xos repository (not just any Rust project).
25pub 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
51/// If `exe` is `.../target/release/xos(.exe)` or `.../target/debug/xos(.exe)`, returns the repo
52/// root that contains that `target/` directoryβ€”the tree this binary was built from.
53fn 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
76/// Locate the xos repo: `XOS_PROJECT_ROOT`, then the repo containing a `target/release|debug`
77/// `xos` binary (if that is what is running), else walk parents of the executable, then
78/// compile-time [`CARGO_MANIFEST_DIR`] (for `cargo install` copies), then walk up from
79/// [`std::env::current_dir`].
80pub 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(&current) {
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    // Python application wrapper - TODO: Reimplement with proper rustpython API
140    // This is a placeholder for now since the API migration is complex
141    use crate::engine::{Application, EngineState};
142    
143    pub struct PyApplicationWrapper {
144        // Placeholder - will be reimplemented
145    }
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            // No-op
160        }
161        
162        fn on_mouse_down(&mut self, _state: &mut EngineState) {
163            // No-op
164        }
165        
166        fn on_mouse_up(&mut self, _state: &mut EngineState) {
167            // No-op
168        }
169        
170        fn on_mouse_move(&mut self, _state: &mut EngineState) {
171            // No-op
172        }
173    }
174}
175
176// --- Native startup ---
177#[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// --- WASM startup ---
191#[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
204// --- Tooling helpers ---
205fn 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            // always use the XOS root index.html
250            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                // fallback to index.html so SPA still loads
262                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
297// --- Main logic ---
298pub 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        // println!("πŸ–₯️  Launching '{game}' in native mode...");
311
312        #[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
329// Python bindings are now handled via rustpython-vm in py_engine module
330// No extension module needed - we embed the Python interpreter instead
331
332/// Print a message (works on all platforms)
333/// On iOS, forwards to Swift's console; otherwise uses standard println!
334/// Also logs to the coder terminal if enabled
335pub fn print(message: &str) {
336    // Log to coder terminal first (if enabled)
337    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
350// XOS namespace module for standardized APIs (external use)
351pub mod xos {
352    pub use crate::print;
353}
354
355pub fn launch_ios_app(app_name: &str) {
356    #[cfg(target_os = "ios")]
357    {
358        // iOS app launching is handled by the iOS build system
359        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        // Pass the app name via environment variable - this is used by the build system
387        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/// Internal CLI flags for `xos::run()` used by third-party apps
403#[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            // println!("πŸ–₯️  Launching app in native mode...");
439            engine::start_native(Box::new(app)).unwrap();
440        }
441    }
442}
443