WEFT_OS/infra/shell/system-ui.html
Marco Allegretti 4d0089a107 feat: appd IPC relay, WIT interfaces, UI kit, gesture routing, and CI hardening
- weft-appd: per-session IPC socket paths; bidirectional Wasm-HTML JSON relay
  via spawn_ipc_relay; SO_PEERCRED UID check on Unix socket connections;
  PanelGesture request and NavigationGesture broadcast for compositor gestures
- weft-runtime: weft:app/ipc, weft:app/fetch, weft:app/notifications WIT
  interfaces; IpcState non-blocking Unix socket host functions; ureq-backed
  net:fetch host function (net-fetch feature); notify-send notifications host
- weft-file-portal: spawn a thread per accepted connection for concurrent access
- weft-app-shell: weft-system:// URL translation; WEFT UI Kit UserScript
  injection; resolve_weft_system_url helper
- weft-servo-shell: forward compositor navigation gestures to weft-appd
  WebSocket as PanelGesture; WEFT UI Kit UserScript injection
- infra/shell: weft-ui-kit.js with 11 custom elements (weft-button, weft-card,
  weft-dialog, weft-icon, weft-list, weft-list-item, weft-menu, weft-menu-item,
  weft-progress, weft-input, weft-label); system-ui.html handles
  NAVIGATION_GESTURE messages and dispatches weft:navigation-gesture CustomEvent
- infra/systemd: add missing env vars to weft-appd.service; correct
  servo-shell.service binary path and system-ui.html argument
- .github/workflows/ci.yml: exclude weft-servo-shell and weft-app-shell from
  cross-platform job; add them to linux-only job with libsystemd-dev dependency
2026-03-12 12:49:45 +01:00

405 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>WEFT Desktop</title>
<style>
:root {
color-scheme: dark;
--surface-bg: rgba(255, 255, 255, 0.07);
--surface-border: rgba(255, 255, 255, 0.12);
--taskbar-height: 48px;
--text-primary: rgba(255, 255, 255, 0.92);
--text-secondary: rgba(255, 255, 255, 0.55);
--accent: #5b8af5;
}
*, *::before, *::after {
box-sizing: border-box;
}
body {
margin: 0;
overflow: hidden;
background: #0f1117;
font-family: system-ui, -apple-system, sans-serif;
color: var(--text-primary);
height: 100dvh;
width: 100dvw;
}
weft-desktop {
display: grid;
grid-template-rows: 1fr var(--taskbar-height);
height: 100dvh;
width: 100dvw;
contain: layout;
}
weft-wallpaper {
display: block;
grid-row: 1;
background: linear-gradient(
145deg,
#0f1117 0%,
#161b2e 40%,
#0d2040 70%,
#0a1a30 100%
);
}
weft-taskbar {
display: flex;
align-items: center;
grid-row: 2;
height: var(--taskbar-height);
padding: 0 12px;
gap: 8px;
background: var(--surface-bg);
border-top: 1px solid var(--surface-border);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
}
weft-taskbar-launcher {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 8px;
cursor: pointer;
background: transparent;
transition: background 0.15s;
}
weft-taskbar-launcher:hover {
background: var(--surface-border);
}
weft-taskbar-clock {
margin-left: auto;
font-size: 13px;
color: var(--text-secondary);
font-variant-numeric: tabular-nums;
}
weft-launcher {
display: none;
position: fixed;
inset: 0;
bottom: var(--taskbar-height);
background: rgba(10, 12, 20, 0.85);
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-taskbar-app {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: var(--text-secondary);
padding: 0 4px 0 6px;
cursor: pointer;
border-radius: 6px;
transition: background 0.15s;
}
weft-taskbar-app:hover {
background: var(--surface-border);
}
weft-taskbar-app .task-close {
opacity: 0;
font-size: 11px;
line-height: 1;
padding: 1px 3px;
border-radius: 3px;
transition: opacity 0.1s, background 0.1s;
}
weft-taskbar-app:hover .task-close {
opacity: 1;
}
weft-taskbar-app .task-close:hover {
background: rgba(255,80,80,0.3);
}
weft-notification-center {
position: fixed;
top: 8px;
right: 8px;
width: 360px;
display: flex;
flex-direction: column;
gap: 8px;
z-index: 200;
pointer-events: none;
}
weft-window {
display: none;
}
</style>
</head>
<body>
<weft-desktop>
<weft-wallpaper></weft-wallpaper>
<weft-taskbar>
<weft-taskbar-launcher id="launcher-btn" title="Apps">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none"
xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<rect x="1" y="1" width="6" height="6" rx="1.5"
fill="rgba(255,255,255,0.7)" />
<rect x="11" y="1" width="6" height="6" rx="1.5"
fill="rgba(255,255,255,0.7)" />
<rect x="1" y="11" width="6" height="6" rx="1.5"
fill="rgba(255,255,255,0.7)" />
<rect x="11" y="11" width="6" height="6" rx="1.5"
fill="rgba(255,255,255,0.7)" />
</svg>
</weft-taskbar-launcher>
<weft-taskbar-clock id="clock"></weft-taskbar-clock>
</weft-taskbar>
</weft-desktop>
<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>
(function () {
var clockEl = document.getElementById('clock');
function updateClock() {
clockEl.textContent = new Date().toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
});
}
updateClock();
setInterval(updateClock, 10000);
document.getElementById('launcher-btn').addEventListener('click', function () {
var launcher = document.getElementById('launcher');
if (launcher.hasAttribute('hidden')) {
launcher.removeAttribute('hidden');
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'QUERY_INSTALLED_APPS' }));
}
} else {
launcher.setAttribute('hidden', '');
}
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') {
document.getElementById('launcher').setAttribute('hidden', '');
}
});
document.getElementById('launcher').addEventListener('click', function (e) {
if (e.target === this) {
this.setAttribute('hidden', '');
}
});
var APPD_WS_PORT = window.WEFT_APPD_WS_PORT || 7410;
var ws = null;
var wsReconnectDelay = 1000;
function appdConnect() {
try {
ws = new WebSocket('ws://127.0.0.1:' + APPD_WS_PORT + '/appd');
} catch (e) {
scheduleReconnect();
return;
}
ws.addEventListener('open', function () {
wsReconnectDelay = 1000;
document.querySelectorAll('weft-taskbar-app').forEach(function (el) { el.remove(); });
ws.send(JSON.stringify({ type: 'QUERY_RUNNING' }));
ws.send(JSON.stringify({ type: 'QUERY_INSTALLED_APPS' }));
});
ws.addEventListener('message', function (ev) {
var msg;
try { msg = JSON.parse(ev.data); } catch (e) { return; }
handleAppdMessage(msg);
});
ws.addEventListener('close', function () {
ws = null;
scheduleReconnect();
});
ws.addEventListener('error', function () {
ws = null;
});
}
function scheduleReconnect() {
setTimeout(appdConnect, wsReconnectDelay);
wsReconnectDelay = Math.min(wsReconnectDelay * 2, 16000);
}
function handleAppdMessage(msg) {
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') {
if (msg.state === 'stopped') {
removeTaskbarEntry(msg.session_id);
}
} else if (msg.type === 'RUNNING_APPS') {
msg.sessions.forEach(function (s) {
ensureTaskbarEntry(s.session_id, s.app_id);
});
} else if (msg.type === 'LAUNCH_ACK') {
ensureTaskbarEntry(msg.session_id, msg.app_id || null);
} else if (msg.type === 'NAVIGATION_GESTURE') {
document.dispatchEvent(new CustomEvent('weft:navigation-gesture', {
detail: {
gesture_type: msg.gesture_type,
fingers: msg.fingers,
dx: msg.dx,
dy: msg.dy,
},
}));
} else if (msg.type === 'ERROR') {
console.warn('appd error', msg.code, msg.message);
}
}
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 + (app.version ? ' v' + app.version : '');
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; }
var label = appId ? appId.split('.').pop() : String(sessionId);
var el = document.createElement('weft-taskbar-app');
el.id = id;
el.dataset.sessionId = sessionId;
if (appId) { el.dataset.appId = appId; }
el.title = appId ? appId + ' (session ' + sessionId + ')' : 'Session ' + sessionId;
var span = document.createElement('span');
span.textContent = '● ' + label;
var close = document.createElement('span');
close.className = 'task-close';
close.textContent = '×';
close.title = 'Terminate';
close.addEventListener('click', function (e) {
e.stopPropagation();
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'TERMINATE_APP', session_id: sessionId }));
}
});
el.appendChild(span);
el.appendChild(close);
var taskbar = document.querySelector('weft-taskbar');
var clock = document.getElementById('clock');
taskbar.insertBefore(el, clock);
}
function removeTaskbarEntry(sessionId) {
var el = document.getElementById('task-' + sessionId);
if (el) { el.remove(); }
}
function showNotification(text) {
var center = document.getElementById('notifications');
var note = document.createElement('div');
note.textContent = text;
note.style.cssText = [
'background:var(--surface-bg)',
'border:1px solid var(--surface-border)',
'border-radius:8px',
'padding:10px 14px',
'font-size:13px',
'color:var(--text-primary)',
'pointer-events:auto',
'backdrop-filter:blur(20px)',
].join(';');
center.appendChild(note);
setTimeout(function () { note.remove(); }, 4000);
}
appdConnect();
}());
</script>
</body>
</html>