mirror of
https://github.com/marcoallegretti/WEFT_OS.git
synced 2026-03-26 17:03:09 +00:00
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
This commit is contained in:
parent
a401510b88
commit
4d0089a107
27 changed files with 1127 additions and 73 deletions
31
.github/workflows/ci.yml
vendored
31
.github/workflows/ci.yml
vendored
|
|
@ -25,9 +25,18 @@ jobs:
|
|||
- name: cargo fmt
|
||||
run: cargo fmt --all --check
|
||||
- name: cargo clippy (cross-platform crates)
|
||||
run: cargo clippy --workspace --exclude weft-compositor --all-targets -- -D warnings
|
||||
run: >
|
||||
cargo clippy --workspace
|
||||
--exclude weft-compositor
|
||||
--exclude weft-servo-shell
|
||||
--exclude weft-app-shell
|
||||
--all-targets -- -D warnings
|
||||
- name: cargo test (cross-platform crates)
|
||||
run: cargo test --workspace --exclude weft-compositor
|
||||
run: >
|
||||
cargo test --workspace
|
||||
--exclude weft-compositor
|
||||
--exclude weft-servo-shell
|
||||
--exclude weft-app-shell
|
||||
|
||||
# Wayland compositor and other Linux-only system crates.
|
||||
# These require libwayland-server and other Linux kernel interfaces.
|
||||
|
|
@ -52,8 +61,18 @@ jobs:
|
|||
libinput-dev \
|
||||
libseat-dev \
|
||||
libudev-dev \
|
||||
libsystemd-dev \
|
||||
pkg-config
|
||||
- name: cargo clippy (weft-compositor)
|
||||
run: cargo clippy -p weft-compositor --all-targets -- -D warnings
|
||||
- name: cargo test (weft-compositor)
|
||||
run: cargo test -p weft-compositor
|
||||
- name: cargo clippy (linux-only crates)
|
||||
run: >
|
||||
cargo clippy
|
||||
-p weft-compositor
|
||||
-p weft-servo-shell
|
||||
-p weft-app-shell
|
||||
--all-targets -- -D warnings
|
||||
- name: cargo test (linux-only crates)
|
||||
run: >
|
||||
cargo test
|
||||
-p weft-compositor
|
||||
-p weft-servo-shell
|
||||
-p weft-app-shell
|
||||
|
|
|
|||
9
.vscode/settings.json
vendored
Normal file
9
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"python-envs.pythonProjects": [
|
||||
{
|
||||
"path": "docu_dev/old",
|
||||
"envManager": "ms-python.python:venv",
|
||||
"packageManager": "ms-python.python:pip"
|
||||
}
|
||||
]
|
||||
}
|
||||
15
Cargo.lock
generated
15
Cargo.lock
generated
|
|
@ -3973,6 +3973,21 @@ dependencies = [
|
|||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "weft-app-shell"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.11.0",
|
||||
"serde",
|
||||
"toml 0.8.23",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "weft-appd"
|
||||
version = "0.1.0"
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ path = "src/main.rs"
|
|||
# weft-servo-shell/SERVO_PIN.md to this file before building; they are not
|
||||
# included here to avoid pulling the Servo monorepo (~1 GB) into every
|
||||
# `cargo check` cycle.
|
||||
servo-embed = []
|
||||
servo-embed = ["dep:serde", "dep:toml"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
|
|
@ -23,3 +23,5 @@ wayland-client = "0.31"
|
|||
wayland-backend = "0.3"
|
||||
wayland-scanner = "0.31"
|
||||
bitflags = "2"
|
||||
serde = { version = "1", features = ["derive"], optional = true }
|
||||
toml = { version = "0.8", optional = true }
|
||||
|
|
|
|||
|
|
@ -169,6 +169,9 @@ impl ApplicationHandler<ServoWake> for App {
|
|||
let rendering_context = build_rendering_ctx(event_loop, &window, size);
|
||||
|
||||
let ucm = Rc::new(UserContentManager::new(&servo));
|
||||
if let Some(kit_js) = load_ui_kit_script() {
|
||||
ucm.add_script(Rc::new(UserScript::new(kit_js, None)));
|
||||
}
|
||||
let bridge_js = format!(
|
||||
r#"(function(){{var ws=new WebSocket('ws://127.0.0.1:{p}');var sid={sid};var q=[];var r=false;ws.onopen=function(){{r=true;q.forEach(function(m){{ws.send(JSON.stringify(m))}});q.length=0}};window.weftSessionId=sid;window.weftIpc={{send:function(m){{if(r)ws.send(JSON.stringify(m));else q.push(m)}},onmessage:null}};ws.onmessage=function(e){{if(window.weftIpc.onmessage)window.weftIpc.onmessage(JSON.parse(e.data))}}}})()"#,
|
||||
p = self.ws_port,
|
||||
|
|
@ -326,14 +329,71 @@ fn build_rendering_ctx(
|
|||
))
|
||||
}
|
||||
|
||||
fn resolve_weft_app_url(app_id: &str) -> Option<ServoUrl> {
|
||||
let url_str = format!("weft-app://{app_id}/index.html");
|
||||
let raw = ServoUrl::parse(&url_str).ok()?;
|
||||
let rel = raw.path().trim_start_matches('/');
|
||||
let file_path = app_store_roots()
|
||||
fn ui_kit_path() -> std::path::PathBuf {
|
||||
if let Ok(p) = std::env::var("WEFT_UI_KIT_JS") {
|
||||
return std::path::PathBuf::from(p);
|
||||
}
|
||||
std::path::PathBuf::from("/usr/share/weft/system/weft-ui-kit.js")
|
||||
}
|
||||
|
||||
fn load_ui_kit_script() -> Option<String> {
|
||||
std::fs::read_to_string(ui_kit_path()).ok()
|
||||
}
|
||||
|
||||
fn resolve_weft_system_url(url: &ServoUrl) -> Option<ServoUrl> {
|
||||
if url.scheme() != "weft-system" { return None; }
|
||||
let host = url.host_str().unwrap_or("");
|
||||
let path = url.path().trim_start_matches('/');
|
||||
let system_root = std::env::var("WEFT_SYSTEM_RESOURCES")
|
||||
.unwrap_or_else(|_| "/usr/share/weft/system".to_owned());
|
||||
let file = std::path::Path::new(&system_root).join(host).join(path);
|
||||
ServoUrl::parse(&format!("file://{}", file.display())).ok()
|
||||
}
|
||||
|
||||
fn read_ui_entry(app_id: &str) -> Option<String> {
|
||||
#[derive(serde::Deserialize)]
|
||||
struct Ui {
|
||||
entry: String,
|
||||
}
|
||||
#[derive(serde::Deserialize)]
|
||||
struct Manifest {
|
||||
ui: Ui,
|
||||
}
|
||||
|
||||
let erofs_manifest = std::path::Path::new("/run/weft/apps")
|
||||
.join(app_id)
|
||||
.join("merged")
|
||||
.join("wapp.toml");
|
||||
let toml_text = std::fs::read_to_string(&erofs_manifest)
|
||||
.ok()
|
||||
.or_else(|| {
|
||||
app_store_roots()
|
||||
.into_iter()
|
||||
.find_map(|r| std::fs::read_to_string(r.join(app_id).join("wapp.toml")).ok())
|
||||
})?;
|
||||
let m: Manifest = toml::from_str(&toml_text).ok()?;
|
||||
Some(m.ui.entry)
|
||||
}
|
||||
|
||||
fn resolve_app_file_path(app_id: &str, rel: &str) -> Option<std::path::PathBuf> {
|
||||
let erofs_root = std::path::Path::new("/run/weft/apps")
|
||||
.join(app_id)
|
||||
.join("merged");
|
||||
if erofs_root.exists() {
|
||||
let p = erofs_root.join(rel);
|
||||
if p.exists() {
|
||||
return Some(p);
|
||||
}
|
||||
}
|
||||
app_store_roots()
|
||||
.into_iter()
|
||||
.map(|r| r.join(app_id).join("ui").join(rel))
|
||||
.find(|p| p.exists())?;
|
||||
.map(|r| r.join(app_id).join(rel))
|
||||
.find(|p| p.exists())
|
||||
}
|
||||
|
||||
fn resolve_weft_app_url(app_id: &str) -> Option<ServoUrl> {
|
||||
let entry = read_ui_entry(app_id).unwrap_or_else(|| "ui/index.html".to_owned());
|
||||
let file_path = resolve_app_file_path(app_id, &entry)?;
|
||||
let s = format!("file://{}", file_path.display());
|
||||
ServoUrl::parse(&s).ok()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ impl Dispatch<wl_registry::WlRegistry, ()> for AppData {
|
|||
} = event
|
||||
&& interface == "zweft_shell_manager_v1"
|
||||
{
|
||||
let mgr = registry.bind::<ZweftShellManagerV1, _, _>(name, version.min(1), qh, ());
|
||||
let mgr = registry.bind::<ZweftShellManagerV1, _, _>(name, version.min(2), qh, ());
|
||||
state.manager = Some(mgr);
|
||||
}
|
||||
}
|
||||
|
|
@ -118,11 +118,11 @@ impl Dispatch<ZweftShellWindowV1, ()> for AppData {
|
|||
} => {
|
||||
tracing::trace!(tv_sec, tv_nsec, refresh, "app shell presentation feedback");
|
||||
}
|
||||
zweft_shell_window_v1::Event::NavigationGesture { .. } => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct ShellClient {
|
||||
event_queue: EventQueue<AppData>,
|
||||
data: AppData,
|
||||
|
|
@ -181,7 +181,6 @@ impl ShellClient {
|
|||
Ok(Self { event_queue, data })
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn dispatch_pending(&mut self) -> anyhow::Result<bool> {
|
||||
self.event_queue
|
||||
.dispatch_pending(&mut self.data)
|
||||
|
|
@ -190,7 +189,6 @@ impl ShellClient {
|
|||
Ok(!self.data.window_state.closed)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn window_state(&self) -> &ShellWindowState {
|
||||
&self.data.window_state
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,3 +21,4 @@ tokio-tungstenite = "0.24"
|
|||
futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
libc = "0.2"
|
||||
|
|
|
|||
|
|
@ -9,6 +9,13 @@ pub enum Request {
|
|||
QueryRunning,
|
||||
QueryAppState { session_id: u64 },
|
||||
QueryInstalledApps,
|
||||
IpcForward { session_id: u64, payload: String },
|
||||
PanelGesture {
|
||||
gesture_type: u32,
|
||||
fingers: u32,
|
||||
dx: f64,
|
||||
dy: f64,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
|
@ -45,6 +52,16 @@ pub enum Response {
|
|||
InstalledApps {
|
||||
apps: Vec<AppInfo>,
|
||||
},
|
||||
IpcMessage {
|
||||
session_id: u64,
|
||||
payload: String,
|
||||
},
|
||||
NavigationGesture {
|
||||
gesture_type: u32,
|
||||
fingers: u32,
|
||||
dx: f64,
|
||||
dy: f64,
|
||||
},
|
||||
Error {
|
||||
code: u32,
|
||||
message: String,
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ struct SessionRegistry {
|
|||
broadcast: tokio::sync::broadcast::Sender<Response>,
|
||||
abort_senders: std::collections::HashMap<u64, tokio::sync::oneshot::Sender<()>>,
|
||||
compositor_tx: Option<compositor_client::CompositorSender>,
|
||||
ipc_socket: Option<PathBuf>,
|
||||
ipc_senders: std::collections::HashMap<u64, tokio::sync::mpsc::Sender<String>>,
|
||||
}
|
||||
|
||||
impl Default for SessionRegistry {
|
||||
|
|
@ -38,7 +38,7 @@ impl Default for SessionRegistry {
|
|||
broadcast,
|
||||
abort_senders: std::collections::HashMap::new(),
|
||||
compositor_tx: None,
|
||||
ipc_socket: None,
|
||||
ipc_senders: std::collections::HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -105,6 +105,25 @@ impl SessionRegistry {
|
|||
self.abort_senders.remove(&session_id);
|
||||
}
|
||||
|
||||
pub(crate) fn register_ipc_sender(
|
||||
&mut self,
|
||||
session_id: u64,
|
||||
tx: tokio::sync::mpsc::Sender<String>,
|
||||
) {
|
||||
self.ipc_senders.insert(session_id, tx);
|
||||
}
|
||||
|
||||
pub(crate) fn ipc_sender_for(
|
||||
&self,
|
||||
session_id: u64,
|
||||
) -> Option<tokio::sync::mpsc::Sender<String>> {
|
||||
self.ipc_senders.get(&session_id).cloned()
|
||||
}
|
||||
|
||||
pub(crate) fn remove_ipc_sender(&mut self, session_id: u64) {
|
||||
self.ipc_senders.remove(&session_id);
|
||||
}
|
||||
|
||||
pub(crate) fn subscribe(&self) -> tokio::sync::broadcast::Receiver<Response> {
|
||||
self.broadcast.subscribe()
|
||||
}
|
||||
|
|
@ -143,8 +162,6 @@ async fn run() -> anyhow::Result<()> {
|
|||
tracing::info!(path = %socket_path.display(), "IPC socket listening");
|
||||
|
||||
let registry: Registry = Arc::new(Mutex::new(SessionRegistry::default()));
|
||||
registry.lock().await.ipc_socket = Some(socket_path.clone());
|
||||
|
||||
if let Some(path) = compositor_client::socket_path() {
|
||||
let tx = compositor_client::spawn(path);
|
||||
registry.lock().await.compositor_tx = Some(tx);
|
||||
|
|
@ -272,6 +289,17 @@ async fn handle_connection(
|
|||
stream: tokio::net::UnixStream,
|
||||
registry: Registry,
|
||||
) -> anyhow::Result<()> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let cred = stream
|
||||
.peer_cred()
|
||||
.context("SO_PEERCRED")?;
|
||||
let our_uid = unsafe { libc::getuid() };
|
||||
if cred.uid() != our_uid {
|
||||
anyhow::bail!("peer UID {} != process UID {}; connection rejected", cred.uid(), our_uid);
|
||||
}
|
||||
}
|
||||
|
||||
let (reader, writer) = tokio::io::split(stream);
|
||||
let mut reader = tokio::io::BufReader::new(reader);
|
||||
let mut writer = tokio::io::BufWriter::new(writer);
|
||||
|
|
@ -285,6 +313,13 @@ async fn handle_connection(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn session_ipc_socket_path(session_id: u64) -> Option<PathBuf> {
|
||||
let runtime_dir = std::env::var("XDG_RUNTIME_DIR").ok()?;
|
||||
let dir = PathBuf::from(runtime_dir).join("weft");
|
||||
std::fs::create_dir_all(&dir).ok()?;
|
||||
Some(dir.join(format!("ipc-{session_id}.sock")))
|
||||
}
|
||||
|
||||
pub(crate) async fn dispatch(req: Request, registry: &Registry) -> Response {
|
||||
match req {
|
||||
Request::LaunchApp {
|
||||
|
|
@ -295,7 +330,15 @@ pub(crate) async fn dispatch(req: Request, registry: &Registry) -> Response {
|
|||
tracing::info!(session_id, %app_id, "launched");
|
||||
let abort_rx = registry.lock().await.register_abort(session_id);
|
||||
let compositor_tx = registry.lock().await.compositor_tx.clone();
|
||||
let ipc_socket = registry.lock().await.ipc_socket.clone();
|
||||
let ipc_socket = session_ipc_socket_path(session_id);
|
||||
let broadcast = registry.lock().await.broadcast().clone();
|
||||
if let Some(ref sock_path) = ipc_socket {
|
||||
if let Some(tx) =
|
||||
runtime::spawn_ipc_relay(session_id, sock_path.clone(), broadcast).await
|
||||
{
|
||||
registry.lock().await.register_ipc_sender(session_id, tx);
|
||||
}
|
||||
}
|
||||
let reg = Arc::clone(registry);
|
||||
let aid = app_id.clone();
|
||||
tokio::spawn(async move {
|
||||
|
|
@ -339,6 +382,36 @@ pub(crate) async fn dispatch(req: Request, registry: &Registry) -> Response {
|
|||
let apps = scan_installed_apps();
|
||||
Response::InstalledApps { apps }
|
||||
}
|
||||
Request::IpcForward {
|
||||
session_id,
|
||||
payload,
|
||||
} => {
|
||||
if let Some(tx) = registry.lock().await.ipc_sender_for(session_id) {
|
||||
if tx.send(payload).await.is_err() {
|
||||
tracing::warn!(session_id, "IPC relay sender closed");
|
||||
registry.lock().await.remove_ipc_sender(session_id);
|
||||
}
|
||||
}
|
||||
Response::AppState {
|
||||
session_id,
|
||||
state: ipc::AppStateKind::Running,
|
||||
}
|
||||
}
|
||||
Request::PanelGesture {
|
||||
gesture_type,
|
||||
fingers,
|
||||
dx,
|
||||
dy,
|
||||
} => {
|
||||
let msg = Response::NavigationGesture {
|
||||
gesture_type,
|
||||
fingers,
|
||||
dx,
|
||||
dy,
|
||||
};
|
||||
let _ = registry.lock().await.broadcast().send(msg.clone());
|
||||
msg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,63 @@
|
|||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter};
|
||||
use weft_ipc_types::AppdToCompositor;
|
||||
|
||||
use crate::Registry;
|
||||
use crate::compositor_client::CompositorSender;
|
||||
use crate::ipc::{AppStateKind, Response};
|
||||
|
||||
pub(crate) async fn spawn_ipc_relay(
|
||||
session_id: u64,
|
||||
socket_path: PathBuf,
|
||||
broadcast: tokio::sync::broadcast::Sender<Response>,
|
||||
) -> Option<tokio::sync::mpsc::Sender<String>> {
|
||||
let _ = std::fs::remove_file(&socket_path);
|
||||
let listener = tokio::net::UnixListener::bind(&socket_path).ok()?;
|
||||
let (html_to_wasm_tx, mut html_to_wasm_rx) = tokio::sync::mpsc::channel::<String>(64);
|
||||
tokio::spawn(async move {
|
||||
let Ok((stream, _)) = listener.accept().await else {
|
||||
tracing::warn!(session_id, "IPC relay: failed to accept connection");
|
||||
let _ = std::fs::remove_file(&socket_path);
|
||||
return;
|
||||
};
|
||||
let (reader, writer) = tokio::io::split(stream);
|
||||
let mut reader = BufReader::new(reader);
|
||||
let mut writer = BufWriter::new(writer);
|
||||
loop {
|
||||
let mut line = String::new();
|
||||
tokio::select! {
|
||||
n = reader.read_line(&mut line) => {
|
||||
match n {
|
||||
Ok(0) | Err(_) => break,
|
||||
Ok(_) => {
|
||||
let payload = line.trim_end().to_owned();
|
||||
let _ = broadcast.send(Response::IpcMessage { session_id, payload });
|
||||
}
|
||||
}
|
||||
}
|
||||
msg = html_to_wasm_rx.recv() => {
|
||||
match msg {
|
||||
Some(payload) => {
|
||||
let mut data = payload;
|
||||
data.push('\n');
|
||||
if writer.write_all(data.as_bytes()).await.is_err()
|
||||
|| writer.flush().await.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = std::fs::remove_file(&socket_path);
|
||||
});
|
||||
Some(html_to_wasm_tx)
|
||||
}
|
||||
|
||||
const READY_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
fn systemd_cgroup_available() -> bool {
|
||||
|
|
|
|||
|
|
@ -539,6 +539,9 @@ fn connector_connected(
|
|||
|
||||
state.space.map_output(&output, (0, 0));
|
||||
tracing::info!(?name, "output connected");
|
||||
let (pw, ph) = (wl_mode.size.w, wl_mode.size.h);
|
||||
state.weft_shell_state.reconfigure_panels(0, 0, pw, ph);
|
||||
state.weft_shell_state.retain_alive_panels();
|
||||
render_output(state, node, crtc);
|
||||
}
|
||||
|
||||
|
|
@ -558,6 +561,15 @@ fn connector_disconnected(
|
|||
{
|
||||
state.space.unmap_output(&surface.output);
|
||||
}
|
||||
let (pw, ph) = state
|
||||
.space
|
||||
.outputs()
|
||||
.next()
|
||||
.and_then(|o| state.space.output_geometry(o))
|
||||
.map(|g| (g.size.w, g.size.h))
|
||||
.unwrap_or((0, 0));
|
||||
state.weft_shell_state.reconfigure_panels(0, 0, pw, ph);
|
||||
state.weft_shell_state.retain_alive_panels();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
|
|
|
|||
|
|
@ -117,6 +117,13 @@ pub fn run() -> anyhow::Result<()> {
|
|||
output.set_preferred(new_mode);
|
||||
state.space.map_output(&output, (0, 0));
|
||||
damage_tracker = OutputDamageTracker::from_output(&output);
|
||||
state.weft_shell_state.reconfigure_panels(
|
||||
0,
|
||||
0,
|
||||
size.w,
|
||||
size.h,
|
||||
);
|
||||
state.weft_shell_state.retain_alive_panels();
|
||||
}
|
||||
WinitEvent::Input(input_event) => {
|
||||
input::process_input_event(state, input_event);
|
||||
|
|
|
|||
|
|
@ -273,36 +273,49 @@ fn handle_touch_cancel<B: InputBackend>(
|
|||
}
|
||||
}
|
||||
|
||||
const NAVIGATION_SWIPE_FINGERS: u32 = 3;
|
||||
const NAVIGATION_SWIPE_THRESHOLD: f64 = 100.0;
|
||||
|
||||
fn handle_gesture_swipe_begin<B: InputBackend>(
|
||||
state: &mut WeftCompositorState,
|
||||
event: B::GestureSwipeBeginEvent,
|
||||
) {
|
||||
let serial = SERIAL_COUNTER.next_serial();
|
||||
let fingers = event.fingers();
|
||||
if let Some(pointer) = state.seat.get_pointer() {
|
||||
pointer.gesture_swipe_begin(
|
||||
state,
|
||||
&smithay::input::pointer::GestureSwipeBeginEvent {
|
||||
serial,
|
||||
time: event.time_msec(),
|
||||
fingers: event.fingers(),
|
||||
fingers,
|
||||
},
|
||||
);
|
||||
}
|
||||
state.gesture_state.in_progress = true;
|
||||
state.gesture_state.fingers = fingers;
|
||||
state.gesture_state.dx = 0.0;
|
||||
state.gesture_state.dy = 0.0;
|
||||
}
|
||||
|
||||
fn handle_gesture_swipe_update<B: InputBackend>(
|
||||
state: &mut WeftCompositorState,
|
||||
event: B::GestureSwipeUpdateEvent,
|
||||
) {
|
||||
let delta = event.delta();
|
||||
if let Some(pointer) = state.seat.get_pointer() {
|
||||
pointer.gesture_swipe_update(
|
||||
state,
|
||||
&smithay::input::pointer::GestureSwipeUpdateEvent {
|
||||
time: event.time_msec(),
|
||||
delta: event.delta(),
|
||||
delta,
|
||||
},
|
||||
);
|
||||
}
|
||||
if state.gesture_state.in_progress {
|
||||
state.gesture_state.dx += delta.x;
|
||||
state.gesture_state.dy += delta.y;
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_gesture_swipe_end<B: InputBackend>(
|
||||
|
|
@ -310,16 +323,28 @@ fn handle_gesture_swipe_end<B: InputBackend>(
|
|||
event: B::GestureSwipeEndEvent,
|
||||
) {
|
||||
let serial = SERIAL_COUNTER.next_serial();
|
||||
let cancelled = event.cancelled();
|
||||
if let Some(pointer) = state.seat.get_pointer() {
|
||||
pointer.gesture_swipe_end(
|
||||
state,
|
||||
&smithay::input::pointer::GestureSwipeEndEvent {
|
||||
serial,
|
||||
time: event.time_msec(),
|
||||
cancelled: event.cancelled(),
|
||||
cancelled,
|
||||
},
|
||||
);
|
||||
}
|
||||
let gs = std::mem::take(&mut state.gesture_state);
|
||||
if !cancelled
|
||||
&& gs.in_progress
|
||||
&& gs.fingers >= NAVIGATION_SWIPE_FINGERS
|
||||
&& (gs.dx.abs() >= NAVIGATION_SWIPE_THRESHOLD
|
||||
|| gs.dy.abs() >= NAVIGATION_SWIPE_THRESHOLD)
|
||||
{
|
||||
state
|
||||
.weft_shell_state
|
||||
.send_navigation_gesture_to_panels(0, gs.fingers, gs.dx, gs.dy);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_gesture_pinch_begin<B: InputBackend>(
|
||||
|
|
|
|||
|
|
@ -17,13 +17,13 @@ pub mod server {
|
|||
pub use server::zweft_shell_manager_v1::ZweftShellManagerV1;
|
||||
pub use server::zweft_shell_window_v1::ZweftShellWindowV1;
|
||||
|
||||
use wayland_server::{DisplayHandle, GlobalDispatch, backend::GlobalId};
|
||||
use wayland_server::{DisplayHandle, GlobalDispatch, Resource, backend::GlobalId};
|
||||
|
||||
pub struct WeftShellState {
|
||||
_global: GlobalId,
|
||||
panels: Vec<ZweftShellWindowV1>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct WeftShellWindowData {
|
||||
pub app_id: String,
|
||||
pub title: String,
|
||||
|
|
@ -38,15 +38,51 @@ impl WeftShellState {
|
|||
D: GlobalDispatch<ZweftShellManagerV1, ()>,
|
||||
D: 'static,
|
||||
{
|
||||
let global = display.create_global::<D, ZweftShellManagerV1, ()>(1, ());
|
||||
Self { _global: global }
|
||||
let global = display.create_global::<D, ZweftShellManagerV1, ()>(2, ());
|
||||
Self {
|
||||
_global: global,
|
||||
panels: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_panel(&mut self, window: ZweftShellWindowV1) {
|
||||
self.panels.push(window);
|
||||
}
|
||||
|
||||
pub fn reconfigure_panels(&self, x: i32, y: i32, width: i32, height: i32) {
|
||||
for panel in &self.panels {
|
||||
if panel.is_alive() {
|
||||
panel.configure(x, y, width, height, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_navigation_gesture_to_panels(
|
||||
&self,
|
||||
gesture_type: u32,
|
||||
fingers: u32,
|
||||
dx: f64,
|
||||
dy: f64,
|
||||
) {
|
||||
for panel in &self.panels {
|
||||
if panel.is_alive() && panel.version() >= 2 {
|
||||
panel.navigation_gesture(gesture_type, fingers, dx, dy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn retain_alive_panels(&mut self) {
|
||||
self.panels.retain(|p| p.is_alive());
|
||||
}
|
||||
|
||||
pub fn panels(&self) -> impl Iterator<Item = &ZweftShellWindowV1> {
|
||||
self.panels.iter()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::atomic::Ordering;
|
||||
use wayland_server::Resource;
|
||||
|
||||
use super::*;
|
||||
|
||||
|
|
@ -83,14 +119,14 @@ mod tests {
|
|||
fn manager_interface_name_and_version() {
|
||||
let iface = ZweftShellManagerV1::interface();
|
||||
assert_eq!(iface.name, "zweft_shell_manager_v1");
|
||||
assert_eq!(iface.version, 1);
|
||||
assert_eq!(iface.version, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_interface_name_and_version() {
|
||||
let iface = ZweftShellWindowV1::interface();
|
||||
assert_eq!(iface.name, "zweft_shell_window_v1");
|
||||
assert_eq!(iface.version, 1);
|
||||
assert_eq!(iface.version, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -60,13 +60,21 @@ impl ClientData for WeftClientState {
|
|||
fn disconnected(&self, _client_id: ClientId, _reason: DisconnectReason) {}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Accumulated state for a multi-touch swipe gesture in progress.
|
||||
#[derive(Default)]
|
||||
pub struct GestureState {
|
||||
pub in_progress: bool,
|
||||
pub fingers: u32,
|
||||
pub dx: f64,
|
||||
pub dy: f64,
|
||||
}
|
||||
|
||||
pub struct WeftCompositorState {
|
||||
pub display_handle: DisplayHandle,
|
||||
pub loop_signal: LoopSignal,
|
||||
pub loop_handle: LoopHandle<'static, WeftCompositorState>,
|
||||
pub gesture_state: GestureState,
|
||||
|
||||
// Wayland protocol globals
|
||||
pub compositor_state: CompositorState,
|
||||
pub xdg_shell_state: XdgShellState,
|
||||
pub layer_shell_state: WlrLayerShellState,
|
||||
|
|
@ -79,26 +87,21 @@ pub struct WeftCompositorState {
|
|||
pub pointer_constraints_state: PointerConstraintsState,
|
||||
pub cursor_shape_state: CursorShapeManagerState,
|
||||
|
||||
// Desktop abstraction layer
|
||||
pub space: Space<Window>,
|
||||
pub popups: PopupManager,
|
||||
|
||||
// Seat and input state
|
||||
pub seat_state: SeatState<Self>,
|
||||
pub seat: Seat<Self>,
|
||||
pub pointer_location: Point<f64, Logical>,
|
||||
pub cursor_image_status: CursorImageStatus,
|
||||
|
||||
// Set by the backend after renderer initialisation when DMA-BUF is supported.
|
||||
#[allow(dead_code)]
|
||||
pub dmabuf_global: Option<DmabufGlobal>,
|
||||
|
||||
// Set to false when the compositor should exit the event loop.
|
||||
pub running: bool,
|
||||
|
||||
// WEFT compositor–shell protocol global.
|
||||
pub weft_shell_state: WeftShellState,
|
||||
|
||||
// IPC channel with weft-appd (compositor is the server, appd is the client).
|
||||
#[cfg(unix)]
|
||||
pub appd_ipc: Option<WeftAppdIpc>,
|
||||
|
||||
|
|
@ -159,6 +162,7 @@ impl WeftCompositorState {
|
|||
cursor_image_status: CursorImageStatus::Hidden,
|
||||
dmabuf_global: None,
|
||||
running: true,
|
||||
gesture_state: GestureState::default(),
|
||||
#[cfg(unix)]
|
||||
appd_ipc: None,
|
||||
#[cfg(target_os = "linux")]
|
||||
|
|
@ -230,10 +234,8 @@ impl XdgShellHandler for WeftCompositorState {
|
|||
}
|
||||
|
||||
fn new_toplevel(&mut self, surface: ToplevelSurface) {
|
||||
// Send initial configure before wrapping — the toplevel needs a configure to map.
|
||||
surface.send_configure();
|
||||
let window = Window::new_wayland_window(surface);
|
||||
// Map at origin; proper placement policy comes with the shell protocol wave.
|
||||
self.space.map_element(window, (0, 0), false);
|
||||
}
|
||||
|
||||
|
|
@ -307,7 +309,21 @@ impl SeatHandler for WeftCompositorState {
|
|||
&mut self.seat_state
|
||||
}
|
||||
|
||||
fn focus_changed(&mut self, _seat: &Seat<Self>, _focused: Option<&WlSurface>) {}
|
||||
fn focus_changed(&mut self, _seat: &Seat<Self>, focused: Option<&WlSurface>) {
|
||||
let focused_id = focused.map(|s| s.id());
|
||||
for panel in self.weft_shell_state.panels() {
|
||||
if !panel.is_alive() {
|
||||
continue;
|
||||
}
|
||||
let data = panel.data::<WeftShellWindowData>();
|
||||
let is_focused = data
|
||||
.and_then(|d| d.surface.as_ref())
|
||||
.map(|s| Some(s.id()) == focused_id)
|
||||
.unwrap_or(false);
|
||||
panel.focus_changed(if is_focused { 1 } else { 0 });
|
||||
}
|
||||
self.weft_shell_state.retain_alive_panels();
|
||||
}
|
||||
|
||||
fn cursor_image(&mut self, _seat: &Seat<Self>, image: CursorImageStatus) {
|
||||
self.cursor_image_status = image;
|
||||
|
|
@ -439,7 +455,7 @@ impl GlobalDispatch<ZweftShellManagerV1, ()> for WeftCompositorState {
|
|||
|
||||
impl Dispatch<ZweftShellManagerV1, ()> for WeftCompositorState {
|
||||
fn request(
|
||||
_state: &mut Self,
|
||||
state: &mut Self,
|
||||
_client: &Client,
|
||||
_resource: &ZweftShellManagerV1,
|
||||
request: zweft_shell_manager_v1::Request,
|
||||
|
|
@ -460,6 +476,7 @@ impl Dispatch<ZweftShellManagerV1, ()> for WeftCompositorState {
|
|||
width,
|
||||
height,
|
||||
} => {
|
||||
let is_panel = role == "panel";
|
||||
let window = data_init.init(
|
||||
id,
|
||||
WeftShellWindowData {
|
||||
|
|
@ -470,7 +487,25 @@ impl Dispatch<ZweftShellManagerV1, ()> for WeftCompositorState {
|
|||
closed: std::sync::atomic::AtomicBool::new(false),
|
||||
},
|
||||
);
|
||||
window.configure(x, y, width, height, 0);
|
||||
if is_panel {
|
||||
let (ox, oy, ow, oh) = state
|
||||
.space
|
||||
.outputs()
|
||||
.next()
|
||||
.and_then(|o| state.space.output_geometry(o))
|
||||
.map(|g| (g.loc.x, g.loc.y, g.size.w, g.size.h))
|
||||
.unwrap_or((x, y, width, height));
|
||||
window.configure(
|
||||
ox,
|
||||
oy,
|
||||
ow,
|
||||
oh,
|
||||
crate::protocols::server::zweft_shell_window_v1::State::Maximized as u32,
|
||||
);
|
||||
state.weft_shell_state.add_panel(window);
|
||||
} else {
|
||||
window.configure(x, y, width, height, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::os::unix::net::{UnixListener, UnixStream};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Context;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
|
@ -38,7 +39,7 @@ fn main() -> anyhow::Result<()> {
|
|||
}
|
||||
|
||||
let socket_path = &args[1];
|
||||
let allowed = parse_allowed(&args[2..]);
|
||||
let allowed = Arc::new(parse_allowed(&args[2..]));
|
||||
|
||||
if Path::new(socket_path).exists() {
|
||||
std::fs::remove_file(socket_path)
|
||||
|
|
@ -50,7 +51,10 @@ fn main() -> anyhow::Result<()> {
|
|||
|
||||
for stream in listener.incoming() {
|
||||
match stream {
|
||||
Ok(s) => handle_connection(s, &allowed),
|
||||
Ok(s) => {
|
||||
let allowed = Arc::clone(&allowed);
|
||||
std::thread::spawn(move || handle_connection(s, &allowed));
|
||||
}
|
||||
Err(e) => eprintln!("accept error: {e}"),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -189,14 +189,8 @@ fn check_package(dir: &Path) -> anyhow::Result<String> {
|
|||
errors.push(format!("ui.entry '{}' not found", ui_path.display()));
|
||||
}
|
||||
|
||||
const KNOWN_CAPS: &[&str] = &[
|
||||
"fs:rw:app-data",
|
||||
"fs:read:app-data",
|
||||
"fs:rw:xdg-documents",
|
||||
"fs:read:xdg-documents",
|
||||
];
|
||||
for cap in m.package.capabilities.iter().flatten() {
|
||||
if !KNOWN_CAPS.contains(&cap.as_str()) {
|
||||
if !is_known_capability(cap) {
|
||||
errors.push(format!("unknown capability '{cap}'"));
|
||||
}
|
||||
}
|
||||
|
|
@ -235,6 +229,28 @@ fn print_info(m: &Manifest) {
|
|||
}
|
||||
}
|
||||
|
||||
fn is_known_capability(cap: &str) -> bool {
|
||||
const EXACT: &[&str] = &[
|
||||
"fs:rw:app-data",
|
||||
"fs:read:app-data",
|
||||
"fs:rw:xdg-documents",
|
||||
"fs:read:xdg-documents",
|
||||
"net:fetch:*",
|
||||
"hw:gpu:compute",
|
||||
"hw:gpu:render",
|
||||
"sys:notifications",
|
||||
"sys:clipboard:read",
|
||||
"sys:clipboard:write",
|
||||
];
|
||||
if EXACT.contains(&cap) {
|
||||
return true;
|
||||
}
|
||||
if let Some(domain) = cap.strip_prefix("net:fetch:") {
|
||||
return !domain.is_empty() && domain != "*";
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn is_valid_app_id(id: &str) -> bool {
|
||||
let parts: Vec<&str> = id.split('.').collect();
|
||||
if parts.len() < 3 {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ path = "src/main.rs"
|
|||
default = []
|
||||
wasmtime-runtime = ["dep:wasmtime", "dep:wasmtime-wasi", "dep:cap-std"]
|
||||
seccomp = ["dep:seccompiler", "dep:libc"]
|
||||
net-fetch = ["dep:ureq"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
|
|
@ -22,3 +23,4 @@ wasmtime-wasi = { version = "30", optional = true }
|
|||
cap-std = { version = "3", optional = true }
|
||||
seccompiler = { version = "0.4", optional = true }
|
||||
libc = { version = "0.2", optional = true }
|
||||
ureq = { version = "2", optional = true }
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ fn run_module(
|
|||
ipc_socket: Option<&str>,
|
||||
) -> anyhow::Result<()> {
|
||||
use cap_std::{ambient_authority, fs::Dir};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use wasmtime::{
|
||||
Config, Engine, Store,
|
||||
component::{Component, Linker},
|
||||
|
|
@ -107,9 +108,64 @@ fn run_module(
|
|||
bindings::sync::Command,
|
||||
};
|
||||
|
||||
struct IpcState {
|
||||
socket: std::os::unix::net::UnixStream,
|
||||
recv_buf: Vec<u8>,
|
||||
}
|
||||
|
||||
impl IpcState {
|
||||
fn connect(path: &str) -> Option<Self> {
|
||||
let socket = std::os::unix::net::UnixStream::connect(path).ok()?;
|
||||
socket.set_nonblocking(true).ok()?;
|
||||
Some(Self {
|
||||
socket,
|
||||
recv_buf: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
fn send(&mut self, payload: &str) -> Result<(), String> {
|
||||
use std::io::Write;
|
||||
let _ = self.socket.set_nonblocking(false);
|
||||
let mut line = payload.to_owned();
|
||||
line.push('\n');
|
||||
let result = self
|
||||
.socket
|
||||
.write_all(line.as_bytes())
|
||||
.map_err(|e| e.to_string());
|
||||
let _ = self.socket.set_nonblocking(true);
|
||||
result
|
||||
}
|
||||
|
||||
fn recv(&mut self) -> Option<String> {
|
||||
use std::io::Read;
|
||||
let mut chunk = [0u8; 4096];
|
||||
loop {
|
||||
match self.socket.read(&mut chunk) {
|
||||
Ok(0) => break,
|
||||
Ok(n) => self.recv_buf.extend_from_slice(&chunk[..n]),
|
||||
Err(e)
|
||||
if e.kind() == std::io::ErrorKind::WouldBlock
|
||||
|| e.kind() == std::io::ErrorKind::TimedOut =>
|
||||
{
|
||||
break;
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
if let Some(pos) = self.recv_buf.iter().position(|&b| b == b'\n') {
|
||||
let raw: Vec<u8> = self.recv_buf.drain(..=pos).collect();
|
||||
return String::from_utf8(raw)
|
||||
.ok()
|
||||
.map(|s| s.trim_end_matches('\n').trim_end_matches('\r').to_owned());
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
struct State {
|
||||
ctx: WasiCtx,
|
||||
table: ResourceTable,
|
||||
ipc: Arc<Mutex<Option<IpcState>>>,
|
||||
}
|
||||
|
||||
impl WasiView for State {
|
||||
|
|
@ -130,20 +186,97 @@ fn run_module(
|
|||
|
||||
let mut linker: Linker<State> = Linker::new(&engine);
|
||||
add_to_linker_sync(&mut linker).context("add WASI to linker")?;
|
||||
let ipc_state: Arc<Mutex<Option<IpcState>>> = Arc::new(Mutex::new(None));
|
||||
|
||||
{
|
||||
let ipc_send = Arc::clone(&ipc_state);
|
||||
let ipc_recv = Arc::clone(&ipc_state);
|
||||
linker
|
||||
.instance("weft:app/notify@0.1.0")
|
||||
.context("define weft:app/notify instance")?
|
||||
.func_wrap("ready", |_: wasmtime::StoreContextMut<'_, State>, ()| {
|
||||
println!("READY");
|
||||
Ok::<(), wasmtime::Error>(())
|
||||
})
|
||||
.context("define weft:app/notify#ready")?;
|
||||
|
||||
let mut ipc_instance = linker
|
||||
.instance("weft:app/ipc@0.1.0")
|
||||
.context("define weft:app/ipc instance")?;
|
||||
|
||||
ipc_instance
|
||||
.func_wrap(
|
||||
"send",
|
||||
move |_: wasmtime::StoreContextMut<'_, State>,
|
||||
(payload,): (String,)|
|
||||
-> wasmtime::Result<(Result<(), String>,)> {
|
||||
let mut guard = ipc_send.lock().unwrap();
|
||||
match guard.as_mut() {
|
||||
Some(ipc) => Ok((ipc.send(&payload),)),
|
||||
None => Ok((Err("IPC not connected".to_owned()),)),
|
||||
}
|
||||
},
|
||||
)
|
||||
.context("define weft:app/ipc#send")?;
|
||||
|
||||
ipc_instance
|
||||
.func_wrap(
|
||||
"recv",
|
||||
move |_: wasmtime::StoreContextMut<'_, State>,
|
||||
()|
|
||||
-> wasmtime::Result<(Option<String>,)> {
|
||||
let mut guard = ipc_recv.lock().unwrap();
|
||||
Ok((guard.as_mut().and_then(|ipc| ipc.recv()),))
|
||||
},
|
||||
)
|
||||
.context("define weft:app/ipc#recv")?;
|
||||
}
|
||||
|
||||
linker
|
||||
.instance("weft:app/notify@0.1.0")
|
||||
.context("define weft:app/notify instance")?
|
||||
.func_wrap("ready", |_: wasmtime::StoreContextMut<'_, State>, ()| {
|
||||
println!("READY");
|
||||
Ok::<(), wasmtime::Error>(())
|
||||
})
|
||||
.context("define weft:app/notify#ready")?;
|
||||
.instance("weft:app/fetch@0.1.0")
|
||||
.context("define weft:app/fetch instance")?
|
||||
.func_wrap(
|
||||
"fetch",
|
||||
|_: wasmtime::StoreContextMut<'_, State>,
|
||||
(url, method, headers, body): (
|
||||
String,
|
||||
String,
|
||||
Vec<(String, String)>,
|
||||
Option<Vec<u8>>,
|
||||
)|
|
||||
-> wasmtime::Result<(
|
||||
Result<(u16, String, Vec<u8>), String>,
|
||||
)> {
|
||||
let result = host_fetch(&url, &method, &headers, body.as_deref());
|
||||
Ok((result,))
|
||||
},
|
||||
)
|
||||
.context("define weft:app/fetch#fetch")?;
|
||||
|
||||
linker
|
||||
.instance("weft:app/notifications@0.1.0")
|
||||
.context("define weft:app/notifications instance")?
|
||||
.func_wrap(
|
||||
"notify",
|
||||
|_: wasmtime::StoreContextMut<'_, State>,
|
||||
(title, body, icon): (String, String, Option<String>)|
|
||||
-> wasmtime::Result<(Result<(), String>,)> {
|
||||
let result = host_notify(&title, &body, icon.as_deref());
|
||||
Ok((result,))
|
||||
},
|
||||
)
|
||||
.context("define weft:app/notifications#notify")?;
|
||||
|
||||
let mut ctx_builder = WasiCtxBuilder::new();
|
||||
ctx_builder.inherit_stdout().inherit_stderr();
|
||||
|
||||
if let Some(socket_path) = ipc_socket {
|
||||
ctx_builder.env("WEFT_IPC_SOCKET", socket_path);
|
||||
if let Some(ipc) = IpcState::connect(socket_path) {
|
||||
*ipc_state.lock().unwrap() = Some(ipc);
|
||||
} else {
|
||||
tracing::warn!("weft:app/ipc: could not connect to IPC socket {socket_path}");
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(portal_socket) = std::env::var("WEFT_FILE_PORTAL_SOCKET") {
|
||||
|
|
@ -162,6 +295,7 @@ fn run_module(
|
|||
State {
|
||||
ctx,
|
||||
table: ResourceTable::new(),
|
||||
ipc: ipc_state,
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -175,6 +309,60 @@ fn run_module(
|
|||
.map_err(|()| anyhow::anyhow!("wasm component run exited with error"))
|
||||
}
|
||||
|
||||
#[cfg(feature = "net-fetch")]
|
||||
fn host_fetch(
|
||||
url: &str,
|
||||
method: &str,
|
||||
headers: &[(String, String)],
|
||||
body: Option<&[u8]>,
|
||||
) -> Result<(u16, String, Vec<u8>), String> {
|
||||
use std::io::Read;
|
||||
let mut req = ureq::request(method, url);
|
||||
for (name, value) in headers {
|
||||
req = req.set(name, value);
|
||||
}
|
||||
let response = match body {
|
||||
Some(b) => req.send_bytes(b),
|
||||
None => req.call(),
|
||||
}
|
||||
.map_err(|e| e.to_string())?;
|
||||
let status = response.status();
|
||||
let content_type = response.content_type().to_owned();
|
||||
let mut body_bytes = Vec::new();
|
||||
response
|
||||
.into_reader()
|
||||
.read_to_end(&mut body_bytes)
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok((status, content_type, body_bytes))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "net-fetch"))]
|
||||
fn host_fetch(
|
||||
_url: &str,
|
||||
_method: &str,
|
||||
_headers: &[(String, String)],
|
||||
_body: Option<&[u8]>,
|
||||
) -> Result<(u16, String, Vec<u8>), String> {
|
||||
Err("net-fetch capability not compiled in".to_owned())
|
||||
}
|
||||
|
||||
fn host_notify(title: &str, body: &str, icon: Option<&str>) -> Result<(), String> {
|
||||
let mut cmd = std::process::Command::new("notify-send");
|
||||
if let Some(i) = icon {
|
||||
cmd.arg("--icon").arg(i);
|
||||
}
|
||||
cmd.arg("--").arg(title).arg(body);
|
||||
cmd.status()
|
||||
.map_err(|e| e.to_string())
|
||||
.and_then(|s| {
|
||||
if s.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("notify-send exited with {s}"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "seccomp")]
|
||||
fn apply_seccomp_filter() -> anyhow::Result<()> {
|
||||
use seccompiler::{BpfProgram, SeccompAction, SeccompFilter, SeccompRule};
|
||||
|
|
|
|||
|
|
@ -8,3 +8,45 @@ interface notify {
|
|||
/// Running state.
|
||||
ready: func();
|
||||
}
|
||||
|
||||
/// Host interface for bidirectional IPC between the Wasm component and the
|
||||
/// HTML front-end served by weft-app-shell. Messages are JSON strings.
|
||||
interface ipc {
|
||||
/// Send a payload to the HTML front-end. Returns an error string if the
|
||||
/// channel is not connected or the write fails.
|
||||
send: func(payload: string) -> result<_, string>;
|
||||
|
||||
/// Return the next pending payload from the HTML front-end, or none if
|
||||
/// no message is currently available. Non-blocking.
|
||||
recv: func() -> option<string>;
|
||||
}
|
||||
|
||||
/// Host interface for outbound HTTP requests. Requires the net:fetch
|
||||
/// capability to be declared in wapp.toml.
|
||||
interface fetch {
|
||||
record response {
|
||||
status: u16,
|
||||
content-type: string,
|
||||
body: list<u8>,
|
||||
}
|
||||
|
||||
/// Perform a synchronous HTTP request. method is GET, POST, etc.
|
||||
/// headers is a list of (name, value) pairs. body is the request body.
|
||||
fetch: func(
|
||||
url: string,
|
||||
method: string,
|
||||
headers: list<tuple<string, string>>,
|
||||
body: option<list<u8>>,
|
||||
) -> result<response, string>;
|
||||
}
|
||||
|
||||
/// Host interface for sending desktop notifications. Requires the
|
||||
/// sys:notifications capability to be declared in wapp.toml.
|
||||
interface notifications {
|
||||
/// Send a desktop notification. icon is an optional XDG icon name.
|
||||
notify: func(
|
||||
title: string,
|
||||
body: string,
|
||||
icon: option<string>,
|
||||
) -> result<_, string>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -173,6 +173,9 @@ impl ApplicationHandler<ServoWake> for App {
|
|||
let rendering_context = build_rendering_ctx(event_loop, &window, size);
|
||||
|
||||
let user_content_manager = Rc::new(UserContentManager::new(&servo));
|
||||
if let Some(kit_js) = load_ui_kit_script() {
|
||||
user_content_manager.add_script(Rc::new(UserScript::new(kit_js, None)));
|
||||
}
|
||||
let bridge_js = format!(
|
||||
r#"(function(){{var ws=new WebSocket('ws://127.0.0.1:{p}');var q=[];var r=false;ws.onopen=function(){{r=true;q.forEach(function(m){{ws.send(JSON.stringify(m))}});q.length=0}};window.weftIpc={{send:function(m){{if(r)ws.send(JSON.stringify(m));else q.push(m)}},onmessage:null}};ws.onmessage=function(e){{if(window.weftIpc.onmessage)window.weftIpc.onmessage(JSON.parse(e.data))}}}})()"#,
|
||||
p = self.ws_port
|
||||
|
|
@ -214,6 +217,13 @@ impl ApplicationHandler<ServoWake> for App {
|
|||
Err(e) => tracing::warn!("shell client dispatch error: {e}"),
|
||||
Ok(true) => {}
|
||||
}
|
||||
let gestures = sc.take_pending_gestures();
|
||||
if !gestures.is_empty() {
|
||||
let ws_port = self.ws_port;
|
||||
std::thread::spawn(move || {
|
||||
forward_gestures_to_appd(ws_port, &gestures);
|
||||
});
|
||||
}
|
||||
}
|
||||
if let Some(servo) = &self.servo {
|
||||
servo.spin_event_loop();
|
||||
|
|
@ -340,6 +350,15 @@ fn resolve_weft_app_url(url: &ServoUrl) -> Option<ServoUrl> {
|
|||
ServoUrl::parse(&s).ok()
|
||||
}
|
||||
|
||||
fn load_ui_kit_script() -> Option<String> {
|
||||
let path = std::env::var("WEFT_UI_KIT_JS")
|
||||
.map(std::path::PathBuf::from)
|
||||
.unwrap_or_else(|_| {
|
||||
std::path::PathBuf::from("/usr/share/weft/system/weft-ui-kit.js")
|
||||
});
|
||||
std::fs::read_to_string(path).ok()
|
||||
}
|
||||
|
||||
fn app_store_roots() -> Vec<PathBuf> {
|
||||
if let Ok(v) = std::env::var("WEFT_APP_STORE") {
|
||||
return vec![PathBuf::from(v)];
|
||||
|
|
@ -381,3 +400,37 @@ pub fn run(
|
|||
.run_app(&mut app)
|
||||
.map_err(|e| anyhow::anyhow!("event loop run: {e}"))
|
||||
}
|
||||
|
||||
fn forward_gestures_to_appd(
|
||||
ws_port: u16,
|
||||
gestures: &[crate::shell_client::PendingGesture],
|
||||
) {
|
||||
use std::net::TcpStream;
|
||||
let addr = format!("127.0.0.1:{ws_port}");
|
||||
let stream = match TcpStream::connect(&addr) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
tracing::warn!("gesture forward: connect to {addr} failed: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let url = format!("ws://{addr}/");
|
||||
let (mut ws, _) = match tungstenite::client::client(url, stream) {
|
||||
Ok(pair) => pair,
|
||||
Err(e) => {
|
||||
tracing::warn!("gesture forward: WebSocket handshake failed: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
for g in gestures {
|
||||
let json = format!(
|
||||
r#"{{"type":"PANEL_GESTURE","gesture_type":{},"fingers":{},"dx":{},"dy":{}}}"#,
|
||||
g.gesture_type, g.fingers, g.dx, g.dy
|
||||
);
|
||||
if let Err(e) = ws.send(tungstenite::Message::Text(json)) {
|
||||
tracing::warn!("gesture forward: send failed: {e}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
let _ = ws.close(None);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,10 +23,18 @@ pub struct ShellWindowState {
|
|||
|
||||
// ── Internal Wayland dispatch state ──────────────────────────────────────────
|
||||
|
||||
pub struct PendingGesture {
|
||||
pub gesture_type: u32,
|
||||
pub fingers: u32,
|
||||
pub dx: f64,
|
||||
pub dy: f64,
|
||||
}
|
||||
|
||||
struct AppData {
|
||||
manager: Option<ZweftShellManagerV1>,
|
||||
window: Option<ZweftShellWindowV1>,
|
||||
window_state: ShellWindowState,
|
||||
pending_gestures: Vec<PendingGesture>,
|
||||
}
|
||||
|
||||
impl AppData {
|
||||
|
|
@ -43,6 +51,7 @@ impl AppData {
|
|||
focused: false,
|
||||
closed: false,
|
||||
},
|
||||
pending_gestures: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -63,7 +72,7 @@ impl Dispatch<wl_registry::WlRegistry, ()> for AppData {
|
|||
} = event
|
||||
&& interface == "zweft_shell_manager_v1"
|
||||
{
|
||||
let mgr = registry.bind::<ZweftShellManagerV1, _, _>(name, version.min(1), qh, ());
|
||||
let mgr = registry.bind::<ZweftShellManagerV1, _, _>(name, version.min(2), qh, ());
|
||||
state.manager = Some(mgr);
|
||||
}
|
||||
}
|
||||
|
|
@ -122,13 +131,26 @@ impl Dispatch<ZweftShellWindowV1, ()> for AppData {
|
|||
} => {
|
||||
tracing::trace!(tv_sec, tv_nsec, refresh, "shell presentation feedback");
|
||||
}
|
||||
zweft_shell_window_v1::Event::NavigationGesture {
|
||||
r#type,
|
||||
fingers,
|
||||
dx,
|
||||
dy,
|
||||
} => {
|
||||
tracing::debug!(r#type, fingers, dx, dy, "navigation gesture from compositor");
|
||||
state.pending_gestures.push(PendingGesture {
|
||||
gesture_type: r#type,
|
||||
fingers,
|
||||
dx,
|
||||
dy,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Public client ─────────────────────────────────────────────────────────────
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct ShellClient {
|
||||
event_queue: EventQueue<AppData>,
|
||||
data: AppData,
|
||||
|
|
@ -185,7 +207,6 @@ impl ShellClient {
|
|||
Ok(Self { event_queue, data })
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn dispatch_pending(&mut self) -> anyhow::Result<bool> {
|
||||
self.event_queue
|
||||
.dispatch_pending(&mut self.data)
|
||||
|
|
@ -194,8 +215,11 @@ impl ShellClient {
|
|||
Ok(!self.data.window_state.closed)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn window_state(&self) -> &ShellWindowState {
|
||||
&self.data.window_state
|
||||
}
|
||||
|
||||
pub fn take_pending_gestures(&mut self) -> Vec<PendingGesture> {
|
||||
std::mem::take(&mut self.data.pending_gestures)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -301,6 +301,15 @@
|
|||
});
|
||||
} 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);
|
||||
}
|
||||
|
|
|
|||
332
infra/shell/weft-ui-kit.js
Normal file
332
infra/shell/weft-ui-kit.js
Normal file
|
|
@ -0,0 +1,332 @@
|
|||
(function () {
|
||||
'use strict';
|
||||
|
||||
var CSS = `
|
||||
:host {
|
||||
box-sizing: border-box;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
:host([hidden]) { display: none !important; }
|
||||
`;
|
||||
|
||||
function sheet(extra) {
|
||||
var s = new CSSStyleSheet();
|
||||
s.replaceSync(CSS + (extra || ''));
|
||||
return s;
|
||||
}
|
||||
|
||||
/* ── weft-button ─────────────────────────────────────────────── */
|
||||
class WeftButton extends HTMLElement {
|
||||
static observedAttributes = ['variant', 'disabled'];
|
||||
constructor() {
|
||||
super();
|
||||
var root = this.attachShadow({ mode: 'open' });
|
||||
root.adoptedStyleSheets = [sheet(`
|
||||
:host { display: inline-block; }
|
||||
button {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
gap: 6px; padding: 8px 16px; border: none; border-radius: 8px;
|
||||
font-size: 14px; font-weight: 500; cursor: pointer;
|
||||
background: rgba(91,138,245,0.9); color: #fff;
|
||||
transition: opacity 0.15s, background 0.15s;
|
||||
width: 100%;
|
||||
}
|
||||
button:hover { background: rgba(91,138,245,1); }
|
||||
button:active { opacity: 0.8; }
|
||||
button:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
:host([variant=secondary]) button {
|
||||
background: rgba(255,255,255,0.1);
|
||||
color: rgba(255,255,255,0.9);
|
||||
}
|
||||
:host([variant=destructive]) button {
|
||||
background: rgba(220,50,50,0.85);
|
||||
}
|
||||
`)];
|
||||
this._btn = document.createElement('button');
|
||||
this._btn.appendChild(document.createElement('slot'));
|
||||
root.appendChild(this._btn);
|
||||
}
|
||||
attributeChangedCallback(name, _old, val) {
|
||||
if (name === 'disabled') this._btn.disabled = val !== null;
|
||||
}
|
||||
connectedCallback() {
|
||||
this._btn.disabled = this.hasAttribute('disabled');
|
||||
}
|
||||
}
|
||||
|
||||
/* ── weft-card ───────────────────────────────────────────────── */
|
||||
class WeftCard extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
var root = this.attachShadow({ mode: 'open' });
|
||||
root.adoptedStyleSheets = [sheet(`
|
||||
:host { display: block; }
|
||||
.card {
|
||||
background: rgba(255,255,255,0.07);
|
||||
border: 1px solid rgba(255,255,255,0.12);
|
||||
border-radius: 12px; padding: 16px;
|
||||
}
|
||||
`)];
|
||||
var d = document.createElement('div');
|
||||
d.className = 'card';
|
||||
d.appendChild(document.createElement('slot'));
|
||||
root.appendChild(d);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── weft-dialog ─────────────────────────────────────────────── */
|
||||
class WeftDialog extends HTMLElement {
|
||||
static observedAttributes = ['open'];
|
||||
constructor() {
|
||||
super();
|
||||
var root = this.attachShadow({ mode: 'open' });
|
||||
root.adoptedStyleSheets = [sheet(`
|
||||
:host { display: none; }
|
||||
:host([open]) { display: flex; align-items: center; justify-content: center;
|
||||
position: fixed; inset: 0; z-index: 9000;
|
||||
background: rgba(0,0,0,0.55); }
|
||||
.dialog {
|
||||
background: #1a1d28; border: 1px solid rgba(255,255,255,0.15);
|
||||
border-radius: 16px; padding: 24px; min-width: 320px;
|
||||
max-width: 90vw; max-height: 80vh; overflow-y: auto;
|
||||
box-shadow: 0 24px 64px rgba(0,0,0,0.6);
|
||||
}
|
||||
`)];
|
||||
var d = document.createElement('div');
|
||||
d.className = 'dialog';
|
||||
d.appendChild(document.createElement('slot'));
|
||||
root.appendChild(d);
|
||||
root.addEventListener('click', function (e) {
|
||||
if (e.target === root.host) root.host.removeAttribute('open');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* ── weft-icon ───────────────────────────────────────────────── */
|
||||
class WeftIcon extends HTMLElement {
|
||||
static observedAttributes = ['name', 'size'];
|
||||
constructor() {
|
||||
super();
|
||||
var root = this.attachShadow({ mode: 'open' });
|
||||
root.adoptedStyleSheets = [sheet(`
|
||||
:host { display: inline-flex; align-items: center; justify-content: center; }
|
||||
svg { width: var(--icon-size, 20px); height: var(--icon-size, 20px);
|
||||
fill: currentColor; }
|
||||
`)];
|
||||
this._root = root;
|
||||
this._render();
|
||||
}
|
||||
attributeChangedCallback() { this._render(); }
|
||||
_render() {
|
||||
var size = this.getAttribute('size') || '20';
|
||||
this._root.host.style.setProperty('--icon-size', size + 'px');
|
||||
}
|
||||
}
|
||||
|
||||
/* ── weft-list / weft-list-item ─────────────────────────────── */
|
||||
class WeftList extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
var root = this.attachShadow({ mode: 'open' });
|
||||
root.adoptedStyleSheets = [sheet(`
|
||||
:host { display: block; }
|
||||
ul { list-style: none; margin: 0; padding: 0; }
|
||||
`)];
|
||||
var ul = document.createElement('ul');
|
||||
ul.appendChild(document.createElement('slot'));
|
||||
root.appendChild(ul);
|
||||
}
|
||||
}
|
||||
|
||||
class WeftListItem extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
var root = this.attachShadow({ mode: 'open' });
|
||||
root.adoptedStyleSheets = [sheet(`
|
||||
:host { display: block; }
|
||||
li {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 10px 12px; border-radius: 8px; cursor: pointer;
|
||||
color: rgba(255,255,255,0.88);
|
||||
transition: background 0.12s;
|
||||
}
|
||||
li:hover { background: rgba(255,255,255,0.08); }
|
||||
`)];
|
||||
var li = document.createElement('li');
|
||||
li.appendChild(document.createElement('slot'));
|
||||
root.appendChild(li);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── weft-menu / weft-menu-item ─────────────────────────────── */
|
||||
class WeftMenu extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
var root = this.attachShadow({ mode: 'open' });
|
||||
root.adoptedStyleSheets = [sheet(`
|
||||
:host { display: block; }
|
||||
.menu {
|
||||
background: rgba(20,22,32,0.95); backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255,255,255,0.12); border-radius: 10px;
|
||||
padding: 4px; min-width: 160px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
|
||||
}
|
||||
`)];
|
||||
var d = document.createElement('div');
|
||||
d.className = 'menu';
|
||||
d.appendChild(document.createElement('slot'));
|
||||
root.appendChild(d);
|
||||
}
|
||||
}
|
||||
|
||||
class WeftMenuItem extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
var root = this.attachShadow({ mode: 'open' });
|
||||
root.adoptedStyleSheets = [sheet(`
|
||||
:host { display: block; }
|
||||
.item {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 8px 12px; border-radius: 6px; cursor: pointer;
|
||||
font-size: 13px; color: rgba(255,255,255,0.88);
|
||||
transition: background 0.1s;
|
||||
}
|
||||
.item:hover { background: rgba(91,138,245,0.25); }
|
||||
:host([destructive]) .item { color: #f87171; }
|
||||
`)];
|
||||
var d = document.createElement('div');
|
||||
d.className = 'item';
|
||||
d.appendChild(document.createElement('slot'));
|
||||
root.appendChild(d);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── weft-progress ───────────────────────────────────────────── */
|
||||
class WeftProgress extends HTMLElement {
|
||||
static observedAttributes = ['value', 'max', 'indeterminate'];
|
||||
constructor() {
|
||||
super();
|
||||
var root = this.attachShadow({ mode: 'open' });
|
||||
root.adoptedStyleSheets = [sheet(`
|
||||
:host { display: block; }
|
||||
.track {
|
||||
height: 6px; background: rgba(255,255,255,0.12);
|
||||
border-radius: 3px; overflow: hidden;
|
||||
}
|
||||
.fill {
|
||||
height: 100%; background: #5b8af5; border-radius: 3px;
|
||||
transition: width 0.2s;
|
||||
}
|
||||
@keyframes indeterminate {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(400%); }
|
||||
}
|
||||
:host([indeterminate]) .fill {
|
||||
width: 25%; animation: indeterminate 1.4s linear infinite;
|
||||
}
|
||||
`)];
|
||||
this._track = document.createElement('div');
|
||||
this._track.className = 'track';
|
||||
this._fill = document.createElement('div');
|
||||
this._fill.className = 'fill';
|
||||
this._track.appendChild(this._fill);
|
||||
root.appendChild(this._track);
|
||||
this._update();
|
||||
}
|
||||
attributeChangedCallback() { this._update(); }
|
||||
_update() {
|
||||
if (this.hasAttribute('indeterminate')) {
|
||||
this._fill.style.width = '25%';
|
||||
} else {
|
||||
var val = parseFloat(this.getAttribute('value') || '0');
|
||||
var max = parseFloat(this.getAttribute('max') || '100');
|
||||
this._fill.style.width = (Math.min(100, (val / max) * 100)) + '%';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ── weft-input ──────────────────────────────────────────────── */
|
||||
class WeftInput extends HTMLElement {
|
||||
static observedAttributes = ['placeholder', 'type', 'value', 'disabled'];
|
||||
constructor() {
|
||||
super();
|
||||
var root = this.attachShadow({ mode: 'open' });
|
||||
root.adoptedStyleSheets = [sheet(`
|
||||
:host { display: block; }
|
||||
input {
|
||||
width: 100%; padding: 9px 12px; border-radius: 8px;
|
||||
border: 1px solid rgba(255,255,255,0.15);
|
||||
background: rgba(255,255,255,0.07);
|
||||
color: rgba(255,255,255,0.92); font-size: 14px;
|
||||
outline: none; transition: border-color 0.15s;
|
||||
}
|
||||
input::placeholder { color: rgba(255,255,255,0.35); }
|
||||
input:focus { border-color: rgba(91,138,245,0.7); }
|
||||
input:disabled { opacity: 0.45; cursor: not-allowed; }
|
||||
`)];
|
||||
this._input = document.createElement('input');
|
||||
root.appendChild(this._input);
|
||||
this._input.addEventListener('input', function (e) {
|
||||
this.dispatchEvent(new CustomEvent('weft:input', { detail: e.target.value, bubbles: true }));
|
||||
}.bind(this));
|
||||
this._sync();
|
||||
}
|
||||
attributeChangedCallback() { this._sync(); }
|
||||
_sync() {
|
||||
var i = this._input;
|
||||
if (!i) return;
|
||||
i.placeholder = this.getAttribute('placeholder') || '';
|
||||
i.type = this.getAttribute('type') || 'text';
|
||||
if (this.hasAttribute('value')) i.value = this.getAttribute('value');
|
||||
i.disabled = this.hasAttribute('disabled');
|
||||
}
|
||||
get value() { return this._input ? this._input.value : ''; }
|
||||
set value(v) { if (this._input) this._input.value = v; }
|
||||
}
|
||||
|
||||
/* ── weft-label ──────────────────────────────────────────────── */
|
||||
class WeftLabel extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
var root = this.attachShadow({ mode: 'open' });
|
||||
root.adoptedStyleSheets = [sheet(`
|
||||
:host { display: inline-block; }
|
||||
.label {
|
||||
display: inline-flex; align-items: center; gap: 4px;
|
||||
padding: 2px 8px; border-radius: 100px; font-size: 11px;
|
||||
font-weight: 600; letter-spacing: 0.02em;
|
||||
background: rgba(91,138,245,0.2); color: #93b4ff;
|
||||
}
|
||||
:host([variant=success]) .label { background: rgba(52,199,89,0.2); color: #6ee09c; }
|
||||
:host([variant=warning]) .label { background: rgba(255,159,10,0.2); color: #ffd060; }
|
||||
:host([variant=error]) .label { background: rgba(255,69,58,0.2); color: #ff8a80; }
|
||||
:host([variant=neutral]) .label { background: rgba(255,255,255,0.1); color: rgba(255,255,255,0.7); }
|
||||
`)];
|
||||
var d = document.createElement('div');
|
||||
d.className = 'label';
|
||||
d.appendChild(document.createElement('slot'));
|
||||
root.appendChild(d);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── registration ────────────────────────────────────────────── */
|
||||
var defs = {
|
||||
'weft-button': WeftButton,
|
||||
'weft-card': WeftCard,
|
||||
'weft-dialog': WeftDialog,
|
||||
'weft-icon': WeftIcon,
|
||||
'weft-list': WeftList,
|
||||
'weft-list-item': WeftListItem,
|
||||
'weft-menu': WeftMenu,
|
||||
'weft-menu-item': WeftMenuItem,
|
||||
'weft-progress': WeftProgress,
|
||||
'weft-input': WeftInput,
|
||||
'weft-label': WeftLabel,
|
||||
};
|
||||
|
||||
Object.keys(defs).forEach(function (name) {
|
||||
if (!customElements.get(name)) {
|
||||
customElements.define(name, defs[name]);
|
||||
}
|
||||
});
|
||||
}());
|
||||
|
|
@ -6,12 +6,12 @@ After=weft-compositor.service
|
|||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/packages/system/servo-shell/active/bin/weft-servo-shell
|
||||
PassEnvironment=WAYLAND_DISPLAY XDG_RUNTIME_DIR DISPLAY
|
||||
Environment=WEFT_APPD_WS_PORT=7410
|
||||
ExecStart=/packages/system/weft-servo-shell/active/bin/weft-servo-shell \
|
||||
/packages/system/weft-servo-shell/active/share/weft/shell/system-ui.html
|
||||
Restart=on-failure
|
||||
RestartSec=2
|
||||
# WAYLAND_DISPLAY is exported by weft-compositor after sd_notify(READY=1).
|
||||
# Downstream services that need the shell ready must declare
|
||||
# After=servo-shell.service and a suitable readiness mechanism.
|
||||
|
||||
[Install]
|
||||
WantedBy=graphical.target
|
||||
|
|
|
|||
|
|
@ -7,9 +7,16 @@ After=weft-compositor.service servo-shell.service
|
|||
[Service]
|
||||
Type=notify
|
||||
Environment=WEFT_RUNTIME_BIN=/packages/system/weft-runtime/active/bin/weft-runtime
|
||||
Environment=WEFT_APP_SHELL_BIN=/packages/system/weft-app-shell/active/bin/weft-app-shell
|
||||
Environment=WEFT_FILE_PORTAL_BIN=/packages/system/weft-file-portal/active/bin/weft-file-portal
|
||||
Environment=WEFT_MOUNT_HELPER=/packages/system/weft-mount-helper/active/bin/weft-mount-helper
|
||||
PassEnvironment=WAYLAND_DISPLAY XDG_RUNTIME_DIR DISPLAY
|
||||
ExecStart=/packages/system/weft-appd/active/bin/weft-appd
|
||||
Restart=on-failure
|
||||
RestartSec=1s
|
||||
ProtectSystem=strict
|
||||
PrivateTmp=true
|
||||
ReadWritePaths=%t
|
||||
|
||||
[Install]
|
||||
WantedBy=graphical.target
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@
|
|||
|
||||
<!-- ───────────────────────────── Manager ───────────────────────────── -->
|
||||
|
||||
<interface name="zweft_shell_manager_v1" version="1">
|
||||
<interface name="zweft_shell_manager_v1" version="2">
|
||||
|
||||
<description summary="shell session manager">
|
||||
Bound once by weft-servo-shell. The manager owns the shell session.
|
||||
|
|
@ -90,7 +90,7 @@
|
|||
|
||||
<!-- ───────────────────────────── Window ────────────────────────────── -->
|
||||
|
||||
<interface name="zweft_shell_window_v1" version="1">
|
||||
<interface name="zweft_shell_window_v1" version="2">
|
||||
|
||||
<description summary="a compositor-managed window slot">
|
||||
Represents one visual window. The compositor controls the effective
|
||||
|
|
@ -193,6 +193,24 @@
|
|||
<arg name="refresh" type="uint" summary="output refresh interval in nanoseconds"/>
|
||||
</event>
|
||||
|
||||
<event name="navigation_gesture" since="2">
|
||||
<description summary="compositor-recognized navigation gesture">
|
||||
Sent to panel windows only when the compositor recognizes a
|
||||
multi-touch gesture as a navigation intent rather than a standard
|
||||
pointer gesture. The shell uses this to present the launcher,
|
||||
switch applications, or perform other system-level transitions.
|
||||
|
||||
type values: 0 = swipe, 1 = pinch, 2 = hold.
|
||||
dx and dy are total displacement in logical pixels at gesture end.
|
||||
The compositor only emits this after the gesture has ended and
|
||||
met the recognition threshold.
|
||||
</description>
|
||||
<arg name="type" type="uint" summary="gesture type: 0=swipe 1=pinch 2=hold"/>
|
||||
<arg name="fingers" type="uint" summary="number of fingers"/>
|
||||
<arg name="dx" type="fixed" summary="horizontal displacement in logical pixels"/>
|
||||
<arg name="dy" type="fixed" summary="vertical displacement in logical pixels"/>
|
||||
</event>
|
||||
|
||||
</interface>
|
||||
|
||||
</protocol>
|
||||
|
|
|
|||
Loading…
Reference in a new issue