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]
|
[dependencies]
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
sd-notify = "0.4"
|
sd-notify = "0.4"
|
||||||
|
toml = "0.8"
|
||||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "io-util", "signal", "sync", "process", "time"] }
|
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "io-util", "signal", "sync", "process", "time"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
rmp-serde = "1"
|
rmp-serde = "1"
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ pub enum Request {
|
||||||
TerminateApp { session_id: u64 },
|
TerminateApp { session_id: u64 },
|
||||||
QueryRunning,
|
QueryRunning,
|
||||||
QueryAppState { session_id: u64 },
|
QueryAppState { session_id: u64 },
|
||||||
|
QueryInstalledApps,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|
@ -16,6 +17,12 @@ pub struct SessionInfo {
|
||||||
pub app_id: String,
|
pub app_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct AppInfo {
|
||||||
|
pub app_id: String,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(tag = "type", rename_all = "SCREAMING_SNAKE_CASE")]
|
#[serde(tag = "type", rename_all = "SCREAMING_SNAKE_CASE")]
|
||||||
pub enum Response {
|
pub enum Response {
|
||||||
|
|
@ -34,6 +41,9 @@ pub enum Response {
|
||||||
session_id: u64,
|
session_id: u64,
|
||||||
state: AppStateKind,
|
state: AppStateKind,
|
||||||
},
|
},
|
||||||
|
InstalledApps {
|
||||||
|
apps: Vec<AppInfo>,
|
||||||
|
},
|
||||||
Error {
|
Error {
|
||||||
code: u32,
|
code: u32,
|
||||||
message: String,
|
message: String,
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ mod ipc;
|
||||||
mod runtime;
|
mod runtime;
|
||||||
mod ws;
|
mod ws;
|
||||||
|
|
||||||
use ipc::{AppStateKind, Request, Response, SessionInfo};
|
use ipc::{AppInfo, AppStateKind, Request, Response, SessionInfo};
|
||||||
|
|
||||||
pub(crate) type Registry = Arc<Mutex<SessionRegistry>>;
|
pub(crate) type Registry = Arc<Mutex<SessionRegistry>>;
|
||||||
|
|
||||||
|
|
@ -254,8 +254,67 @@ pub(crate) async fn dispatch(req: Request, registry: &Registry) -> Response {
|
||||||
let state = registry.lock().await.state(session_id);
|
let state = registry.lock().await.state(session_id);
|
||||||
Response::AppState { session_id, state }
|
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> {
|
fn appd_socket_path() -> anyhow::Result<PathBuf> {
|
||||||
if let Ok(p) = std::env::var("WEFT_APPD_SOCKET") {
|
if let Ok(p) = std::env::var("WEFT_APPD_SOCKET") {
|
||||||
|
|
@ -432,6 +491,36 @@ mod tests {
|
||||||
assert!(matches!(reg.state(42), AppStateKind::NotFound));
|
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)]
|
#[cfg(unix)]
|
||||||
#[tokio::test(flavor = "current_thread")]
|
#[tokio::test(flavor = "current_thread")]
|
||||||
async fn supervisor_transitions_through_ready_to_stopped() {
|
async fn supervisor_transitions_through_ready_to_stopped() {
|
||||||
|
|
|
||||||
|
|
@ -94,12 +94,45 @@
|
||||||
backdrop-filter: blur(32px);
|
backdrop-filter: blur(32px);
|
||||||
-webkit-backdrop-filter: blur(32px);
|
-webkit-backdrop-filter: blur(32px);
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
padding: 24px;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
weft-launcher:not([hidden]) {
|
weft-launcher:not([hidden]) {
|
||||||
display: block;
|
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 {
|
weft-notification-center {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 8px;
|
top: 8px;
|
||||||
|
|
@ -138,7 +171,9 @@
|
||||||
</weft-taskbar>
|
</weft-taskbar>
|
||||||
</weft-desktop>
|
</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>
|
<weft-notification-center id="notifications"></weft-notification-center>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
@ -177,6 +212,7 @@
|
||||||
ws.addEventListener('open', function () {
|
ws.addEventListener('open', function () {
|
||||||
wsReconnectDelay = 1000;
|
wsReconnectDelay = 1000;
|
||||||
ws.send(JSON.stringify({ type: 'QUERY_RUNNING' }));
|
ws.send(JSON.stringify({ type: 'QUERY_RUNNING' }));
|
||||||
|
ws.send(JSON.stringify({ type: 'QUERY_INSTALLED_APPS' }));
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.addEventListener('message', function (ev) {
|
ws.addEventListener('message', function (ev) {
|
||||||
|
|
@ -201,7 +237,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleAppdMessage(msg) {
|
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);
|
var label = msg.app_id ? msg.app_id.split('.').pop() : String(msg.session_id);
|
||||||
showNotification(label + ' is ready');
|
showNotification(label + ' is ready');
|
||||||
} else if (msg.type === 'APP_STATE') {
|
} 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) {
|
function ensureTaskbarEntry(sessionId, appId) {
|
||||||
var id = 'task-' + sessionId;
|
var id = 'task-' + sessionId;
|
||||||
if (document.getElementById(id)) { return; }
|
if (document.getElementById(id)) { return; }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue