From e1c15ea463608dfb1cdab56b82dbd325177ab761 Mon Sep 17 00:00:00 2001 From: Marco Allegretti Date: Wed, 11 Mar 2026 11:23:46 +0100 Subject: [PATCH] feat(appd): add QueryInstalledApps IPC request; wire launcher in system UI --- crates/weft-appd/Cargo.toml | 1 + crates/weft-appd/src/ipc.rs | 10 ++++ crates/weft-appd/src/main.rs | 91 +++++++++++++++++++++++++++++++++++- infra/shell/system-ui.html | 74 ++++++++++++++++++++++++++++- 4 files changed, 173 insertions(+), 3 deletions(-) diff --git a/crates/weft-appd/Cargo.toml b/crates/weft-appd/Cargo.toml index 7a71dd0..dd6d3db 100644 --- a/crates/weft-appd/Cargo.toml +++ b/crates/weft-appd/Cargo.toml @@ -11,6 +11,7 @@ path = "src/main.rs" [dependencies] anyhow = "1.0" sd-notify = "0.4" +toml = "0.8" tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "io-util", "signal", "sync", "process", "time"] } serde = { version = "1", features = ["derive"] } rmp-serde = "1" diff --git a/crates/weft-appd/src/ipc.rs b/crates/weft-appd/src/ipc.rs index 8b268a3..6a6830c 100644 --- a/crates/weft-appd/src/ipc.rs +++ b/crates/weft-appd/src/ipc.rs @@ -8,6 +8,7 @@ pub enum Request { TerminateApp { session_id: u64 }, QueryRunning, QueryAppState { session_id: u64 }, + QueryInstalledApps, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -16,6 +17,12 @@ pub struct SessionInfo { pub app_id: String, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AppInfo { + pub app_id: String, + pub name: String, +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "SCREAMING_SNAKE_CASE")] pub enum Response { @@ -34,6 +41,9 @@ pub enum Response { session_id: u64, state: AppStateKind, }, + InstalledApps { + apps: Vec, + }, Error { code: u32, message: String, diff --git a/crates/weft-appd/src/main.rs b/crates/weft-appd/src/main.rs index 8bb603b..416ed30 100644 --- a/crates/weft-appd/src/main.rs +++ b/crates/weft-appd/src/main.rs @@ -9,7 +9,7 @@ mod ipc; mod runtime; mod ws; -use ipc::{AppStateKind, Request, Response, SessionInfo}; +use ipc::{AppInfo, AppStateKind, Request, Response, SessionInfo}; pub(crate) type Registry = Arc>; @@ -254,9 +254,68 @@ pub(crate) async fn dispatch(req: Request, registry: &Registry) -> Response { let state = registry.lock().await.state(session_id); Response::AppState { session_id, state } } + Request::QueryInstalledApps => { + let apps = scan_installed_apps(); + Response::InstalledApps { apps } + } } } +fn app_store_roots() -> Vec { + if let Ok(explicit) = std::env::var("WEFT_APP_STORE") { + return vec![std::path::PathBuf::from(explicit)]; + } + let mut roots = Vec::new(); + if let Ok(home) = std::env::var("HOME") { + roots.push( + std::path::PathBuf::from(home) + .join(".local") + .join("share") + .join("weft") + .join("apps"), + ); + } + roots.push(std::path::PathBuf::from("/usr/share/weft/apps")); + roots +} + +#[derive(serde::Deserialize)] +struct WappPackage { + id: String, + name: String, +} + +#[derive(serde::Deserialize)] +struct WappManifest { + package: WappPackage, +} + +fn scan_installed_apps() -> Vec { + let mut seen = std::collections::HashSet::new(); + let mut apps = Vec::new(); + for root in app_store_roots() { + let Ok(entries) = std::fs::read_dir(&root) else { + continue; + }; + for entry in entries.flatten() { + let manifest_path = entry.path().join("wapp.toml"); + let Ok(contents) = std::fs::read_to_string(&manifest_path) else { + continue; + }; + let Ok(m) = toml::from_str::(&contents) else { + continue; + }; + if seen.insert(m.package.id.clone()) { + apps.push(AppInfo { + app_id: m.package.id, + name: m.package.name, + }); + } + } + } + apps +} + fn appd_socket_path() -> anyhow::Result { if let Ok(p) = std::env::var("WEFT_APPD_SOCKET") { return Ok(PathBuf::from(p)); @@ -432,6 +491,36 @@ mod tests { assert!(matches!(reg.state(42), AppStateKind::NotFound)); } + #[test] + fn scan_installed_apps_finds_valid_packages() { + use std::fs; + let store = std::env::temp_dir().join(format!("weft_appd_scan_{}", std::process::id())); + let app_dir = store.join("com.example.scanner"); + fs::create_dir_all(&app_dir).unwrap(); + fs::write( + app_dir.join("wapp.toml"), + "[package]\nid = \"com.example.scanner\"\nname = \"Scanner\"\nversion = \"1.0.0\"\n\ + [runtime]\nmodule = \"app.wasm\"\n[ui]\nentry = \"ui/index.html\"\n", + ) + .unwrap(); + + let prior = std::env::var("WEFT_APP_STORE").ok(); + unsafe { std::env::set_var("WEFT_APP_STORE", &store) }; + + let apps = scan_installed_apps(); + assert_eq!(apps.len(), 1); + assert_eq!(apps[0].app_id, "com.example.scanner"); + assert_eq!(apps[0].name, "Scanner"); + + unsafe { + match prior { + Some(v) => std::env::set_var("WEFT_APP_STORE", v), + None => std::env::remove_var("WEFT_APP_STORE"), + } + } + let _ = fs::remove_dir_all(&store); + } + #[cfg(unix)] #[tokio::test(flavor = "current_thread")] async fn supervisor_transitions_through_ready_to_stopped() { diff --git a/infra/shell/system-ui.html b/infra/shell/system-ui.html index 9d9971e..cce3c81 100644 --- a/infra/shell/system-ui.html +++ b/infra/shell/system-ui.html @@ -94,12 +94,45 @@ backdrop-filter: blur(32px); -webkit-backdrop-filter: blur(32px); z-index: 100; + padding: 24px; + overflow-y: auto; } weft-launcher:not([hidden]) { display: block; } + weft-app-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); + gap: 16px; + max-width: 640px; + margin: 0 auto; + } + + weft-app-icon { + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + padding: 12px 8px; + border-radius: 12px; + cursor: pointer; + color: var(--text-primary); + font-size: 12px; + text-align: center; + word-break: break-word; + transition: background 0.15s; + } + + weft-app-icon:hover { + background: var(--surface-border); + } + + weft-app-icon svg { + flex-shrink: 0; + } + weft-notification-center { position: fixed; top: 8px; @@ -138,7 +171,9 @@ - +