mirror of
https://github.com/marcoallegretti/WEFT_OS.git
synced 2026-03-27 01:13:09 +00:00
feat(appd): add QueryInstalledApps IPC request; wire launcher in system UI
This commit is contained in:
parent
d6de84b4c7
commit
e1c15ea463
4 changed files with 173 additions and 3 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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<AppInfo>,
|
||||
},
|
||||
Error {
|
||||
code: u32,
|
||||
message: String,
|
||||
|
|
|
|||
|
|
@ -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<Mutex<SessionRegistry>>;
|
||||
|
||||
|
|
@ -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<std::path::PathBuf> {
|
||||
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<AppInfo> {
|
||||
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::<WappManifest>(&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<PathBuf> {
|
||||
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() {
|
||||
|
|
|
|||
|
|
@ -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 @@
|
|||
</weft-taskbar>
|
||||
</weft-desktop>
|
||||
|
||||
<weft-launcher hidden id="launcher"></weft-launcher>
|
||||
<weft-launcher hidden id="launcher">
|
||||
<weft-app-grid id="app-grid"></weft-app-grid>
|
||||
</weft-launcher>
|
||||
<weft-notification-center id="notifications"></weft-notification-center>
|
||||
|
||||
<script>
|
||||
|
|
@ -177,6 +212,7 @@
|
|||
ws.addEventListener('open', function () {
|
||||
wsReconnectDelay = 1000;
|
||||
ws.send(JSON.stringify({ type: 'QUERY_RUNNING' }));
|
||||
ws.send(JSON.stringify({ type: 'QUERY_INSTALLED_APPS' }));
|
||||
});
|
||||
|
||||
ws.addEventListener('message', function (ev) {
|
||||
|
|
@ -201,7 +237,9 @@
|
|||
}
|
||||
|
||||
function handleAppdMessage(msg) {
|
||||
if (msg.type === 'APP_READY') {
|
||||
if (msg.type === 'INSTALLED_APPS') {
|
||||
renderLauncher(msg.apps || []);
|
||||
} else if (msg.type === 'APP_READY') {
|
||||
var label = msg.app_id ? msg.app_id.split('.').pop() : String(msg.session_id);
|
||||
showNotification(label + ' is ready');
|
||||
} else if (msg.type === 'APP_STATE') {
|
||||
|
|
@ -217,6 +255,38 @@
|
|||
}
|
||||
}
|
||||
|
||||
function renderLauncher(apps) {
|
||||
var grid = document.getElementById('app-grid');
|
||||
grid.innerHTML = '';
|
||||
if (apps.length === 0) {
|
||||
var empty = document.createElement('p');
|
||||
empty.textContent = 'No apps installed';
|
||||
empty.style.cssText = 'color:var(--text-secondary);font-size:13px;';
|
||||
grid.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
apps.forEach(function (app) {
|
||||
var icon = document.createElement('weft-app-icon');
|
||||
icon.dataset.appId = app.app_id;
|
||||
icon.title = app.app_id;
|
||||
icon.innerHTML =
|
||||
'<svg width="40" height="40" viewBox="0 0 40 40" fill="none"' +
|
||||
' xmlns="http://www.w3.org/2000/svg" aria-hidden="true">' +
|
||||
'<rect width="40" height="40" rx="10" fill="rgba(91,138,245,0.25)"/>' +
|
||||
'<text x="20" y="27" text-anchor="middle" font-size="18" fill="rgba(255,255,255,0.8)">' +
|
||||
(app.name ? app.name.charAt(0).toUpperCase() : '?') +
|
||||
'</text></svg>' +
|
||||
'<span>' + (app.name || app.app_id) + '</span>';
|
||||
icon.addEventListener('click', function () {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: 'LAUNCH_APP', app_id: app.app_id, surface_id: 0 }));
|
||||
}
|
||||
document.getElementById('launcher').setAttribute('hidden', '');
|
||||
});
|
||||
grid.appendChild(icon);
|
||||
});
|
||||
}
|
||||
|
||||
function ensureTaskbarEntry(sessionId, appId) {
|
||||
var id = 'task-' + sessionId;
|
||||
if (document.getElementById(id)) { return; }
|
||||
|
|
|
|||
Loading…
Reference in a new issue