//! llimphi-wasm-open — abre una app WASM Tier 3 como una ventana de escritorio. //! //! Cierra el lazo de la distribución: el stack Tier 3 ya sabía resolver una app //! por hash, verificar su integridad y su concesión Ed25519, y materializar su //! `View` en una ventana Llimphi. Lo que faltaba era el **ejecutable** que //! ata todo desde la línea de comandos del escritorio — el gemelo host del //! ejecutor de apps de wawa, y el consumidor real de `app_bus::Launch::Wasm` //! (que hasta ahora devolvía `Unsupported` porque "lo resuelve el chasis": este //! binario *es* ese chasis). //! //! Dos modos: //! //! ```text //! # 1. Un .wasm local — app de sólo-UI, sin permisos (camino rápido). //! llimphi-wasm-open app.wasm //! //! # 2. Por hash desde un CAS local — se verifica integridad y, si hay //! # concesión, se valida contra el anillo de confianza y se corre con //! # los permisos efectivos (los bits gatean qué host imports se enlazan). //! llimphi-wasm-open --hash --store ~/.cache/llimphi/blobs \ //! [--grant ] [--ring claves.txt] [--name "Mi App"] //! ``` //! //! El modo hash es exactamente lo que un launcher de la UI (dock, spotlight, //! mirada) invocaría para un `Launch::Wasm{bytecode_hex, grant_hex}`: spawnear //! este binario con `--hash`/`--grant`/`--store`, manteniendo a `app-bus` //! liviano (sólo transporta hex, no toca el runner ni la GPU). use std::process::ExitCode; use std::sync::OnceLock; use format::{ConcesionCapacidad, Permisos}; use llimphi_ui::{App, Handle, KeyEvent, View}; use llimphi_wasm_dist::{ hash_from_hex, hash_to_hex, resolve_from_catalog, AppManifest, Catalog, CatalogEntry, DiskStore, RunnerMsg, TrustRing, VerifiedAppExt, WasmGuest, }; /// Lo resuelto en `main` y consumido por `Host::init` en el hilo de la UI. /// Cargamos el `WasmGuest` recién en `init` (no antes) porque el `Store` de /// wasmi vive atado a su hilo; aquí viajan sólo datos (`Send`). #[cfg_attr(test, derive(Debug))] struct LaunchSpec { wasm: Vec, permisos: Permisos, title: String, } static SPEC: OnceLock = OnceLock::new(); /// El modelo del host: el guest vivo + el título que mostramos en la barra. struct HostModel { guest: WasmGuest, title: String, } struct Host; impl App for Host { type Model = HostModel; type Msg = RunnerMsg; fn title() -> &'static str { "llimphi · wasm" } fn init(_: &Handle) -> Self::Model { let spec = SPEC.get().expect("LaunchSpec no inicializado"); let guest = WasmGuest::load(&spec.wasm, spec.permisos) .unwrap_or_else(|e| panic!("cargar app WASM: {e}")); HostModel { guest, title: spec.title.clone(), } } fn update(mut model: Self::Model, msg: Self::Msg, _: &Handle) -> Self::Model { if let Err(e) = model.guest.apply(&msg) { eprintln!("wasm dispatch: {e}"); } model } fn view(model: &Self::Model) -> View { model.guest.render() } fn window_title(model: &Self::Model) -> Option { Some(format!("llimphi · {}", model.title)) } fn on_key(model: &Self::Model, event: &KeyEvent) -> Option { model.guest.key_to_msg(event) } fn on_focus(_model: &Self::Model, id: Option) -> Option { Some(WasmGuest::focus_msg(id)) } } fn main() -> ExitCode { let argv: Vec = std::env::args().skip(1).collect(); let tiene = |f: &str| argv.iter().any(|a| a == f); // Modo registro: conectar a un registro de apps (REST en RON o directorio // local) — listar/buscar y, con --ingest, descargar al CAS + catálogo. No // abre ventana. if tiene("--registry") { return match registro(argv.iter().cloned()) { Ok(msg) => { println!("{msg}"); ExitCode::SUCCESS } Err(e) => { eprintln!("llimphi-wasm-open --registry: {e}"); uso(); ExitCode::FAILURE } }; } // Modo catálogo (búsqueda): listar/buscar apps por texto. No abre ventana. if tiene("--list") || tiene("--search") { return match buscar(argv.iter().cloned()) { Ok(msg) => { println!("{msg}"); ExitCode::SUCCESS } Err(e) => { eprintln!("llimphi-wasm-open: {e}"); uso(); ExitCode::FAILURE } }; } // Modo productor: publicar una app (blob al CAS + manifiesto + entrada de // catálogo si se pide). No abre ventana. if tiene("--install") { return match instalar(argv.into_iter()) { Ok(msg) => { println!("{msg}"); ExitCode::SUCCESS } Err(e) => { eprintln!("llimphi-wasm-open --install: {e}"); uso(); ExitCode::FAILURE } }; } // Modo consumidor: resolver la app (por id del catálogo o por hash/archivo) // y correrla según su clase. let here = tiene("--here"); let spec = if tiene("--run") { spec_desde_catalogo(argv.iter().cloned()) } else { resolver_spec(argv.iter().cloned()) }; match spec { Ok(spec) => { // Una app WASI de consola muestra su salida en **shuma** (la // terminal de la suite), no en una ventana de consola propia: si hay // display, shuma está disponible y no nos pidieron `--here`, // delegamos. `--here` (o sin display, o sin shuma) ⇒ corre local. let es_wasi = llimphi_wasm_wasi::detect_kind(&spec.wasm) == llimphi_wasm_wasi::WasmKind::WasiConsole; if es_wasi && !here && hay_display() && delegar_a_shuma(&argv) { return ExitCode::SUCCESS; } correr(spec) } Err(e) => { eprintln!("llimphi-wasm-open: {e}"); uso(); ExitCode::FAILURE } } } /// ¿Hay un servidor gráfico (Wayland/X11)? Sin él no tiene sentido abrir shuma. fn hay_display() -> bool { let set = |k: &str| std::env::var(k).map(|v| !v.is_empty()).unwrap_or(false); set("WAYLAND_DISPLAY") || set("DISPLAY") } /// Argumentos para abrir shuma corriendo esta misma invocación en modo local: /// `-e --here`. shuma une todo tras `-e` en una línea y /// la corre como primer bloque; `--here` evita que el re-lanzamiento vuelva a /// delegar (corre la app WASI e imprime, y shuma captura esa salida). fn comando_para_shuma(exe: &str, argv: &[String]) -> Vec { let mut v = Vec::with_capacity(argv.len() + 3); v.push("-e".to_string()); v.push(exe.to_string()); v.extend(argv.iter().cloned()); v.push("--here".to_string()); v } /// Lanza shuma para correr esta invocación adentro. `true` si se spawneó; en /// error (shuma no instalado, etc.) `false` para caer a ejecución local. fn delegar_a_shuma(argv: &[String]) -> bool { let exe = match std::env::current_exe() { Ok(p) => p.to_string_lossy().to_string(), Err(_) => return false, }; std::process::Command::new("shuma-shell-llimphi") .args(comando_para_shuma(&exe, argv)) .spawn() .is_ok() } /// Corre la app ya resuelta según su clase: una app Tier 3 (UI) abre una /// ventana Llimphi; un programa WASI de consola se ejecuta y vuelca su salida /// al terminal, propagando el código de salida. Así el catálogo no es trampa: /// el WASM de consola (el grueso del ecosistema) también corre. fn correr(spec: LaunchSpec) -> ExitCode { use llimphi_wasm_wasi::{detect_kind, run_console, WasmKind}; match detect_kind(&spec.wasm) { WasmKind::WasiConsole => match run_console(&spec.wasm, &[], &[], &[]) { Ok(out) => { print!("{}", out.stdout_text()); eprint!("{}", out.stderr_text()); ExitCode::from(out.exit_code.clamp(0, 255) as u8) } Err(e) => { eprintln!("llimphi-wasm-open: WASI: {e}"); ExitCode::FAILURE } }, // Tier 3 (o desconocido, que intentamos como UI): ventana Llimphi. _ => { let _ = SPEC.set(spec); llimphi_ui::run::(); ExitCode::SUCCESS } } } fn uso() { eprintln!(); eprintln!("uso:"); eprintln!(" llimphi-wasm-open "); eprintln!(" llimphi-wasm-open --hash [--store ] \\"); eprintln!(" [--grant ] [--ring ] [--name ]"); eprintln!(" llimphi-wasm-open --list [--catalog ]"); eprintln!(" llimphi-wasm-open --search [--catalog ]"); eprintln!(" llimphi-wasm-open --run [--catalog ] \\"); eprintln!(" [--store ] [--ring ]"); eprintln!(" llimphi-wasm-open --install --id \\"); eprintln!(" [--name