This commit is contained in:
Stephen Hellings 2026-03-09 18:07:07 -04:00
parent d28e50fcf9
commit 83cdaffd05
9 changed files with 415 additions and 78 deletions

View file

@ -1156,6 +1156,29 @@ function connectToCompositor(): Promise<void> {
continue; continue;
} }
if (magic === MAGIC.ENV_UPDATE) {
// magic(4) + json_len(4) + json(N) = 8 + N bytes
if (compositorRxBuffer.length < 8) break;
const jsonLen = compositorRxBuffer.readUInt32LE(4);
if (compositorRxBuffer.length < 8 + jsonLen) break;
const jsonStr = compositorRxBuffer.slice(8, 8 + jsonLen).toString('utf8');
compositorRxBuffer = compositorRxBuffer.slice(8 + jsonLen);
try {
const vars = JSON.parse(jsonStr) as Record<string, string>;
for (const [key, value] of Object.entries(vars)) {
process.env[key] = value;
debugLog(`[Wo] env update: ${key}=${value}`);
}
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('wo:env-update', vars);
}
} catch (err) {
debugLog('[Wo] ENV_UPDATE parse error:', err);
}
continue;
}
// Unknown message magic; resync by advancing one byte. // Unknown message magic; resync by advancing one byte.
debugLog('[Wo] UNKNOWN magic 0x' + magic.toString(16).padStart(8, '0') + ' bufLen=' + compositorRxBuffer.length + ' — stream may be corrupted'); debugLog('[Wo] UNKNOWN magic 0x' + magic.toString(16).padStart(8, '0') + ' bufLen=' + compositorRxBuffer.length + ' — stream may be corrupted');
compositorRxBuffer = compositorRxBuffer.slice(1); compositorRxBuffer = compositorRxBuffer.slice(1);

View file

@ -91,6 +91,14 @@ const compositorAPI: CompositorAPI = {
return () => ipcRenderer.removeListener('wo:pointer-lock-request', handler); return () => ipcRenderer.removeListener('wo:pointer-lock-request', handler);
}, },
// Environment variable updates from compositor (e.g. DISPLAY after XWayland ready)
onEnvUpdate: (callback: (vars: Record<string, string>) => void) => {
const handler = (_event: Electron.IpcRendererEvent, vars: Record<string, string>) =>
callback(vars);
ipcRenderer.on('wo:env-update', handler);
return () => ipcRenderer.removeListener('wo:env-update', handler);
},
// Generic portal request event for client-provided popup/approval handlers // Generic portal request event for client-provided popup/approval handlers
onPortalRequest: (callback: (data: { onPortalRequest: (callback: (data: {
requestId: string; requestId: string;

View file

@ -30,6 +30,7 @@ export const MAGIC = {
FORWARD_KEYBOARD: 0x574F4B45, // "WOKE" — forwarded keyboard from web UI canvas FORWARD_KEYBOARD: 0x574F4B45, // "WOKE" — forwarded keyboard from web UI canvas
FORWARD_RELATIVE_POINTER: 0x574F5245, // "WORE" — forwarded relative pointer (dx/dy) FORWARD_RELATIVE_POINTER: 0x574F5245, // "WORE" — forwarded relative pointer (dx/dy)
POINTER_LOCK_REQUEST: 0x574F504C, // "WOPL" — server-to-client pointer lock request POINTER_LOCK_REQUEST: 0x574F504C, // "WOPL" — server-to-client pointer lock request
ENV_UPDATE: 0x574F4555, // "WOEU" — environment variable update broadcast
} as const; } as const;
/** /**
@ -90,6 +91,11 @@ export interface PointerLockRequestEvent {
lock: boolean; lock: boolean;
} }
export interface EnvUpdateEvent {
type: 'envUpdate';
vars: Record<string, string>;
}
export type InputEvent = export type InputEvent =
| MouseMoveEvent | MouseMoveEvent
| MouseButtonEvent | MouseButtonEvent
@ -97,7 +103,8 @@ export type InputEvent =
| ScrollEvent | ScrollEvent
| FocusChangeEvent | FocusChangeEvent
| WindowMetadataEvent | WindowMetadataEvent
| PointerLockRequestEvent; | PointerLockRequestEvent
| EnvUpdateEvent;
/** /**
* Compositor Actions from Client → Compositor * Compositor Actions from Client → Compositor

View file

@ -278,6 +278,12 @@ export interface Compositor {
*/ */
onPointerLockRequest(callback: (data: { window: string; lock: boolean }) => void): Unsubscribe; onPointerLockRequest(callback: (data: { window: string; lock: boolean }) => void): Unsubscribe;
/**
* Subscribe to environment variable updates from the compositor
* (e.g. DISPLAY becoming available after XWayland starts).
*/
onEnvUpdate(callback: (vars: Record<string, string>) => void): Unsubscribe;
/** /**
* Subscribe to generic portal requests so clients can provide custom UI. * Subscribe to generic portal requests so clients can provide custom UI.
*/ */

View file

@ -48,6 +48,7 @@ pub const MAGIC_FORWARD_RELATIVE_POINTER: u32 = 0x574F5245; // "WORE"
pub const MAGIC_FORWARD_POINTER_BUTTON: u32 = 0x574F5042; // "WOPB" pub const MAGIC_FORWARD_POINTER_BUTTON: u32 = 0x574F5042; // "WOPB"
pub const MAGIC_FORWARD_POINTER_SCROLL: u32 = 0x574F5053; // "WOPS" pub const MAGIC_FORWARD_POINTER_SCROLL: u32 = 0x574F5053; // "WOPS"
pub const MAGIC_POINTER_LOCK_REQUEST: u32 = 0x574F504C; // "WOPL" — server-to-client pointer lock request pub const MAGIC_POINTER_LOCK_REQUEST: u32 = 0x574F504C; // "WOPL" — server-to-client pointer lock request
pub const MAGIC_ENV_UPDATE: u32 = 0x574F4555; // "WOEU" — environment variable update broadcast
#[repr(C, packed)] #[repr(C, packed)]
@ -143,6 +144,8 @@ pub enum ElectronInputEvent {
FocusChange { window_name: String, focused: bool }, FocusChange { window_name: String, focused: bool },
WindowMetadata { metadata: String }, // JSON payload WindowMetadata { metadata: String }, // JSON payload
PointerLockRequest { window_name: String, lock: bool }, PointerLockRequest { window_name: String, lock: bool },
/// Environment variable update (e.g. DISPLAY after XWayland is ready).
EnvUpdate { vars: String },
} }
/// A bidirectional connection to an Electron client. /// A bidirectional connection to an Electron client.
@ -435,6 +438,14 @@ impl ElectronClientConnection {
buf[12..].copy_from_slice(name_bytes); buf[12..].copy_from_slice(name_bytes);
self.write_bytes(&buf)?; self.write_bytes(&buf)?;
}, },
ElectronInputEvent::EnvUpdate { vars } => {
let var_bytes = vars.as_bytes();
let mut buf = vec![0u8; 8 + var_bytes.len()];
buf[0..4].copy_from_slice(&MAGIC_ENV_UPDATE.to_le_bytes());
buf[4..8].copy_from_slice(&(var_bytes.len() as u32).to_le_bytes());
buf[8..].copy_from_slice(var_bytes);
self.write_bytes(&buf)?;
},
} }
Ok(()) Ok(())
} }
@ -502,6 +513,14 @@ impl ElectronClientConnection {
buf[12..].copy_from_slice(name_bytes); buf[12..].copy_from_slice(name_bytes);
self.write_bytes_nonblocking(&buf) self.write_bytes_nonblocking(&buf)
} }
ElectronInputEvent::EnvUpdate { vars } => {
let var_bytes = vars.as_bytes();
let mut buf = vec![0u8; 8 + var_bytes.len()];
buf[0..4].copy_from_slice(&MAGIC_ENV_UPDATE.to_le_bytes());
buf[4..8].copy_from_slice(&(var_bytes.len() as u32).to_le_bytes());
buf[8..].copy_from_slice(var_bytes);
self.write_bytes_nonblocking(&buf)
}
} }
} }
} }
@ -667,6 +686,24 @@ impl ElectronIpc {
Ok(()) Ok(())
} }
/// Broadcast environment variable updates to all connected Electron clients.
/// `vars` is a JSON object like `{"DISPLAY":":0"}`.
pub fn broadcast_env_update(&self, vars: &str) -> Result<()> {
let snapshot: Vec<_> = {
let clients = self.clients.lock().unwrap();
clients.values().cloned().collect()
};
let event = ElectronInputEvent::EnvUpdate {
vars: vars.to_string(),
};
for client in &snapshot {
if let Err(e) = client.try_send_input_event(&event) {
warn!("env update broadcast to '{}' failed: {e:#}", client.name);
}
}
Ok(())
}
/// Send syscall response to a window (non-blocking). /// Send syscall response to a window (non-blocking).
/// If the socket buffer is full, the response is dropped and a warning is /// If the socket buffer is full, the response is dropped and a warning is
/// logged. This is preferable to blocking the compositor event loop — the /// logged. This is preferable to blocking the compositor event loop — the

View file

@ -228,33 +228,7 @@ impl WoState {
return; return;
} }
// In nested mode, pointer events are routed through Electron/comraw. self.dispatch_pointer_to_targets(event.time_msec());
if self.electron_ipc.is_some() {
let under_window =
self.window_under((self.pointer_location.x, self.pointer_location.y));
if self.pointer_window_focus != under_window {
self.pointer_window_focus = under_window.clone();
}
if let Some(ref win_name) = under_window {
if let Some(ref ipc) = self.electron_ipc {
if let Some(local) = self.local_window_coords(
win_name,
(self.pointer_location.x, self.pointer_location.y),
) {
if let Some(client) = ipc.clients.lock().unwrap().get(win_name).cloned() {
// Non-blocking send.
let _ = client.try_send_input_event(&ElectronInputEvent::MouseMove {
x: local.0,
y: local.1,
});
}
}
}
}
return;
}
} }
fn on_pointer_motion_abs<B: InputBackend>(&mut self, event: B::PointerMotionAbsoluteEvent) { fn on_pointer_motion_abs<B: InputBackend>(&mut self, event: B::PointerMotionAbsoluteEvent) {
@ -329,23 +303,30 @@ impl WoState {
return; return;
} }
// In nested mode, pointer events are routed through Electron/comraw. self.dispatch_pointer_to_targets(event.time_msec());
if self.electron_ipc.is_some() {
let under_window =
self.window_under((self.pointer_location.x, self.pointer_location.y));
if self.pointer_window_focus != under_window {
self.pointer_window_focus = under_window.clone();
} }
if let Some(ref win_name) = under_window { /// Route the current pointer location to Electron windows (IPC) and/or
/// native Wayland/X11 windows (Smithay pointer.motion). Called from both
/// relative and absolute pointer motion handlers.
fn dispatch_pointer_to_targets(&mut self, time: u32) {
use smithay::{
desktop::WindowSurfaceType,
input::pointer::MotionEvent,
utils::{Logical, Point},
};
let pos = (self.pointer_location.x, self.pointer_location.y);
// Route to Electron-managed window if cursor is over one
let under_electron = self.window_under(pos);
if self.pointer_window_focus != under_electron {
self.pointer_window_focus = under_electron.clone();
}
if let Some(ref win_name) = under_electron {
if let Some(ref ipc) = self.electron_ipc { if let Some(ref ipc) = self.electron_ipc {
if let Some(local) = self.local_window_coords( if let Some(local) = self.local_window_coords(win_name, pos) {
win_name, if let Some(client) = ipc.clients.lock().unwrap().get(win_name).cloned() {
(self.pointer_location.x, self.pointer_location.y),
) {
let client = ipc.clients.lock().unwrap().get(win_name).cloned();
if let Some(client) = client {
let _ = client.try_send_input_event(&ElectronInputEvent::MouseMove { let _ = client.try_send_input_event(&ElectronInputEvent::MouseMove {
x: local.0, x: local.0,
y: local.1, y: local.1,
@ -354,8 +335,58 @@ impl WoState {
} }
} }
} }
let pointer = match self.seat.get_pointer() {
Some(p) => p,
None => return,
};
let serial = SERIAL_COUNTER.next_serial();
// When the cursor is over an Electron-managed window, the compositor
// must NOT touch Smithay's pointer state at all. Electron/comraw owns
// the cursor: it renders the cursor sprite, tracks position inside its
// canvas, and — when the cursor is over an embedded native Wayland/X11
// window — sends a ForwardedPointer IPC back to the compositor. That
// forwarded path (handle_forwarded_pointer_event) is the ONLY code that
// should call pointer.motion() for native windows.
//
// Previously this block called pointer.motion(None) to "clear" native
// focus, but Smithay interprets that as a real motion event and sends
// wl_pointer.leave to any focused client. When Electron's forwarded
// event arrives a moment later and calls pointer.motion(Some(focus)),
// Smithay sends wl_pointer.enter — so the client sees
// leave→enter→leave→enter every frame, causing cursor rubberbanding.
if under_electron.is_some() && self.electron_ipc.is_some() {
return; return;
} }
// Not over an Electron window — dispatch directly to native windows.
let pos_logical: Point<f64, Logical> = self.pointer_location;
let under_native = self.space.element_under(pos_logical);
let focus = under_native.and_then(|(window, _loc)| {
let win_loc = self.space.element_location(&window)?;
let local_x = self.pointer_location.x - win_loc.x as f64;
let local_y = self.pointer_location.y - win_loc.y as f64;
window
.surface_under(Point::<f64, Logical>::from((local_x, local_y)), WindowSurfaceType::ALL)
.map(|(s, offset)| {
let surface_global: Point<f64, Logical> = (
win_loc.x as f64 + offset.x as f64,
win_loc.y as f64 + offset.y as f64,
).into();
(s, surface_global)
})
});
pointer.motion(
self,
focus,
&MotionEvent {
location: self.pointer_location,
serial,
time,
},
);
pointer.frame(self);
} }
fn on_pointer_button<B: InputBackend>(&mut self, event: B::PointerButtonEvent) { fn on_pointer_button<B: InputBackend>(&mut self, event: B::PointerButtonEvent) {
@ -488,10 +519,13 @@ impl WoState {
Some(w) => w, Some(w) => w,
None => return, None => return,
}; };
let win_loc = self.space.element_location(&window).unwrap_or_default();
let global_x = win_loc.x as f64 + x; // Do NOT overwrite self.pointer_location here. The hardware input
let global_y = win_loc.y as f64 + y; // handler (on_pointer_motion / on_pointer_motion_abs) already set it
self.pointer_location = (global_x, global_y).into(); // to the authoritative screen position. Recomputing it from
// element_location + geo offsets uses a different coordinate system
// and causes the position to ping-pong between the hardware value
// and the recomputed value, producing visible rubberbanding.
let pointer = match self.seat.get_pointer() { let pointer = match self.seat.get_pointer() {
Some(p) => p, Some(p) => p,

View file

@ -648,6 +648,11 @@ fn render_frame(data: &mut EventLoopData) {
); );
} }
// Wayland window surfaces are exported to Electron clients via
// DMABUF/SHM passthrough (below), not rendered directly to the DRM
// framebuffer. Only render them directly when there is no Electron IPC
// (i.e. pure compositor mode without a shell).
if data.state.electron_ipc.is_none() {
for element in wayland_elements { for element in wayland_elements {
let src = element.src(); let src = element.src();
let dst = element.geometry(scale); let dst = element.geometry(scale);
@ -660,6 +665,7 @@ fn render_frame(data: &mut EventLoopData) {
trace!("Element rendering failed: {e}"); trace!("Element rendering failed: {e}");
} }
} }
}
// Render cursor on top of everything // Render cursor on top of everything
if !cursor_elements.is_empty() { if !cursor_elements.is_empty() {
@ -702,9 +708,9 @@ fn render_frame(data: &mut EventLoopData) {
} }
let dirty_surfaces: Vec<_> = data.state.dirty_surfaces.drain().collect(); let dirty_surfaces: Vec<_> = data.state.dirty_surfaces.drain().collect();
for surface in dirty_surfaces { for surface in &dirty_surfaces {
smithay::desktop::utils::send_frames_surface_tree( smithay::desktop::utils::send_frames_surface_tree(
&surface, surface,
&data.state.backend.output, &data.state.backend.output,
presented_at, presented_at,
Some(data.frame_time), Some(data.frame_time),
@ -712,6 +718,101 @@ fn render_frame(data: &mut EventLoopData) {
); );
} }
// Export dirty Wayland surfaces to Electron clients via DMABUF/SHM
if data.state.electron_ipc.is_some() {
use smithay::backend::renderer::utils::RendererSurfaceStateUserData;
use smithay::reexports::wayland_server::Resource;
use smithay::wayland::compositor::with_states;
use smithay::wayland::shm::with_buffer_contents;
use tracing::debug;
let mut exported_pixels: Vec<(String, u32, u32, u32, Vec<u8>)> = Vec::new();
for surface in &dirty_surfaces {
let surface_id = surface.id();
let (window_name, window_geo) = if let Some(n) =
data.state.wayland_window_names.get(&surface_id).cloned()
{
let sid = surface_id.clone();
let geo = data.state.space.elements().find_map(|w| {
let wl_id = w.toplevel()
.map(|t| t.wl_surface().id())
.or_else(|| w.x11_surface().and_then(|x| x.wl_surface().map(|s| s.id())));
if wl_id == Some(sid.clone()) {
data.state.space.element_geometry(w)
} else {
None
}
});
(n, geo)
} else {
let sid = surface_id.clone();
let x11_result = data.state.space.elements().find_map(|w| {
let x11 = w.x11_surface()?;
let wl = x11.wl_surface()?;
if wl.id() == sid.clone() {
let name = data.state.x11_window_names.get(&x11.window_id()).cloned()?;
let geo = data.state.space.element_geometry(w);
Some((name, geo))
} else {
None
}
});
match x11_result {
Some((n, geo)) => (n, geo),
None => continue,
}
};
let Some(_geo) = window_geo else { continue };
// Try SHM buffer first (software-rendered clients)
let pixels_opt: Option<(u32, u32, u32, Vec<u8>)> =
with_states(surface, |states| {
let rss_data = states.data_map.get::<RendererSurfaceStateUserData>()?;
let rss = rss_data.lock().ok()?;
let buffer = rss.buffer()?;
let mut result: Option<(u32, u32, u32, Vec<u8>)> = None;
let _ = with_buffer_contents(buffer, |ptr, len, spec| {
let pixel_bytes =
(spec.stride.max(0) * spec.height.max(0)) as usize;
if pixel_bytes > 0 && pixel_bytes <= len {
let slice =
unsafe { std::slice::from_raw_parts(ptr, pixel_bytes) };
result = Some((
spec.width as u32,
spec.height as u32,
spec.stride as u32,
slice.to_vec(),
));
}
});
result
});
if let Some((bw, bh, stride, pixels)) = pixels_opt {
exported_pixels.push((window_name, bw, bh, stride, pixels));
} else if let Some(ref ipc) = data.state.electron_ipc {
// Try DMABUF (hardware-rendered clients / games)
let dmabuf_opt = with_states(surface, |states| {
let rss_data = states.data_map.get::<RendererSurfaceStateUserData>()?;
let rss = rss_data.lock().ok()?;
let buffer = rss.buffer()?;
smithay::wayland::dmabuf::get_dmabuf(buffer).ok().cloned()
});
if let Some(dmabuf) = dmabuf_opt {
if let Err(e) = ipc.broadcast_dmabuf_frame(&window_name, &dmabuf) {
debug!("DRM dmabuf broadcast failed for {}: {}", window_name, e);
}
}
}
}
data.state.pending_shm_exports = exported_pixels;
}
// Send frame callbacks for the cursor surface so the client keeps // Send frame callbacks for the cursor surface so the client keeps
// animating it (e.g. spinning/loading cursors). // animating it (e.g. spinning/loading cursors).
if let smithay::input::pointer::CursorImageStatus::Surface(ref cursor_surface) = data.state.cursor_status { if let smithay::input::pointer::CursorImageStatus::Surface(ref cursor_surface) = data.state.cursor_status {
@ -766,6 +867,51 @@ fn render_frame(data: &mut EventLoopData) {
data.state.damaged_windows.clear(); data.state.damaged_windows.clear();
data.state.dirty_surfaces.clear(); data.state.dirty_surfaces.clear();
// Write exported SHM pixel buffers to memfd and broadcast to Electron
{
use std::os::unix::io::AsRawFd;
use nix::sys::memfd::MFdFlags;
use tracing::debug;
let exports = std::mem::take(&mut data.state.pending_shm_exports);
for (window_name, w, h, stride, pixels) in exports {
let memfd_fd = data.state
.window_shm_buffers
.entry(window_name.clone())
.or_insert_with(|| {
nix::sys::memfd::memfd_create(
std::ffi::CStr::from_bytes_with_nul(b"wayland_window\0").unwrap(),
MFdFlags::MFD_CLOEXEC,
)
.expect("creating memfd")
});
let size = pixels.len() as i64;
let stat = nix::sys::stat::fstat(&*memfd_fd).unwrap();
if stat.st_size != size {
nix::unistd::ftruncate(&*memfd_fd, size).expect("truncating memfd");
}
nix::unistd::lseek(&*memfd_fd, 0, nix::unistd::Whence::SeekSet)
.expect("seeking memfd");
nix::unistd::write(&*memfd_fd, &pixels).expect("writing to memfd");
if let Some(ref ipc) = data.state.electron_ipc {
let pid = std::process::id();
if let Err(e) = ipc.broadcast_shm_buffer(
&window_name,
w,
h,
stride,
pid,
memfd_fd.as_raw_fd() as i32,
) {
debug!("DRM shm buffer broadcast failed for {}: {}", window_name, e);
}
}
}
}
// Get or create the DRM framebuffer for this slot. // Get or create the DRM framebuffer for this slot.
// Caching in the slot's userdata avoids a create/destroy cycle every // Caching in the slot's userdata avoids a create/destroy cycle every
// frame and — critically — keeps the GbmFramebuffer alive through // frame and — critically — keeps the GbmFramebuffer alive through
@ -1288,7 +1434,7 @@ fn run_drm(mut config: Config) -> Result<(), anyhow::Error> {
std::iter::empty::<(String, String)>(), std::iter::empty::<(String, String)>(),
true, true,
std::process::Stdio::null(), std::process::Stdio::null(),
std::process::Stdio::null(), std::process::Stdio::inherit(),
|_| {}, |_| {},
) { ) {
Ok((xwayland, xw_client)) => { Ok((xwayland, xw_client)) => {
@ -1306,6 +1452,14 @@ fn run_drm(mut config: Config) -> Result<(), anyhow::Error> {
} => { } => {
info!("XWayland ready on DISPLAY :{display_number}"); info!("XWayland ready on DISPLAY :{display_number}");
std::env::set_var("DISPLAY", format!(":{display_number}")); std::env::set_var("DISPLAY", format!(":{display_number}"));
crate::state::propagate_environment(&["DISPLAY", "WAYLAND_DISPLAY"]);
// Broadcast DISPLAY to already-running Electron clients
if let Some(ref ipc) = data.state.electron_ipc {
let vars = format!("{{\"DISPLAY\":\":{display_number}\"}}");
if let Err(e) = ipc.broadcast_env_update(&vars) {
warn!("Failed to broadcast DISPLAY to Electron: {e:#}");
}
}
if let Some(client) = xw_client_cb.borrow_mut().take() { if let Some(client) = xw_client_cb.borrow_mut().take() {
match X11Wm::start_wm(wm_handle.clone(), x11_socket, client) { match X11Wm::start_wm(wm_handle.clone(), x11_socket, client) {
Ok(wm) => { Ok(wm) => {
@ -1333,6 +1487,8 @@ fn run_drm(mut config: Config) -> Result<(), anyhow::Error> {
} }
std::env::set_var("WAYLAND_DISPLAY", &config.compositor.socket_name); std::env::set_var("WAYLAND_DISPLAY", &config.compositor.socket_name);
// Propagate now; DISPLAY will be re-propagated once XWayland is ready.
crate::state::propagate_environment(&["WAYLAND_DISPLAY"]);
let app_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("electron"); let app_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("electron");
let electron_render_node = render_node_for_card(&gpu_path); let electron_render_node = render_node_for_card(&gpu_path);
@ -2057,11 +2213,22 @@ fn run_drm(mut config: Config) -> Result<(), anyhow::Error> {
// Execute any due autostart tasks (scheduled at startup) // Execute any due autostart tasks (scheduled at startup)
if !data.autostart_tasks.is_empty() { if !data.autostart_tasks.is_empty() {
let now = std::time::Instant::now(); let now = std::time::Instant::now();
let display_ready = std::env::var("DISPLAY").is_ok();
let mut pending: Vec<(crate::config::AutostartConfig, std::time::Instant)> = let mut pending: Vec<(crate::config::AutostartConfig, std::time::Instant)> =
Vec::new(); Vec::new();
for (task, when) in data.autostart_tasks.drain(..) { for (task, when) in data.autostart_tasks.drain(..) {
if when <= now { if when <= now {
let cmd = task.command.clone(); let cmd = task.command.clone();
// Defer non-builtin commands until DISPLAY is set
// (XWayland ready). X11 apps like Steam need it.
if !display_ready
&& !cmd.starts_with("wo://")
{
pending.push((task, now + std::time::Duration::from_millis(200)));
continue;
}
// Handle simple wo://window/<name> built-in action // Handle simple wo://window/<name> built-in action
if let Some(name) = cmd.strip_prefix("wo://window/") { if let Some(name) = cmd.strip_prefix("wo://window/") {
if let Some(win_cfg) = if let Some(win_cfg) =

View file

@ -179,7 +179,7 @@ pub fn run_nested(config: Config) -> Result<()> {
std::iter::empty::<(String, String)>(), std::iter::empty::<(String, String)>(),
true, // open abstract socket for X11 compatibility true, // open abstract socket for X11 compatibility
std::process::Stdio::null(), std::process::Stdio::null(),
std::process::Stdio::null(), std::process::Stdio::inherit(),
|_| {}, // no extra user data |_| {}, // no extra user data
) { ) {
Ok((xwayland, xw_client)) => { Ok((xwayland, xw_client)) => {
@ -196,6 +196,14 @@ pub fn run_nested(config: Config) -> Result<()> {
} => { } => {
info!("XWayland ready on DISPLAY :{display_number}"); info!("XWayland ready on DISPLAY :{display_number}");
std::env::set_var("DISPLAY", format!(":{display_number}")); std::env::set_var("DISPLAY", format!(":{display_number}"));
crate::state::propagate_environment(&["DISPLAY", "WAYLAND_DISPLAY"]);
// Broadcast DISPLAY to already-running Electron clients
if let Some(ref ipc) = state.electron_ipc {
let vars = format!("{{\"DISPLAY\":\":{display_number}\"}}");
if let Err(e) = ipc.broadcast_env_update(&vars) {
warn!("Failed to broadcast DISPLAY to Electron: {e:#}");
}
}
if let Some(client) = xw_client_cb.borrow_mut().take() { if let Some(client) = xw_client_cb.borrow_mut().take() {
match X11Wm::start_wm(wm_handle.clone(), x11_socket, client) { match X11Wm::start_wm(wm_handle.clone(), x11_socket, client) {
Ok(wm) => { Ok(wm) => {
@ -224,6 +232,7 @@ pub fn run_nested(config: Config) -> Result<()> {
std::env::set_var("WAYLAND_DISPLAY", &config.compositor.socket_name); std::env::set_var("WAYLAND_DISPLAY", &config.compositor.socket_name);
info!("Set WAYLAND_DISPLAY={}", config.compositor.socket_name); info!("Set WAYLAND_DISPLAY={}", config.compositor.socket_name);
crate::state::propagate_environment(&["WAYLAND_DISPLAY"]);
let app_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("electron"); let app_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("electron");
// Nested mode has no DRM card; use the first available render node. // Nested mode has no DRM card; use the first available render node.
@ -1351,12 +1360,10 @@ fn process_electron_message(
let surface_x = px; let surface_x = px;
let surface_y = py; let surface_y = py;
let global: Point<f64, Logical> = ( // Do NOT overwrite state.pointer_location.
loc.x as f64 - geo.loc.x.max(0) as f64 + surface_x, // The hardware input handler already set it
loc.y as f64 - geo.loc.y.max(0) as f64 + surface_y, // to the authoritative position; recomputing
) // from element_location + geo causes ping-pong.
.into();
state.pointer_location = global;
let under = window.surface_under( let under = window.surface_under(
(surface_x, surface_y), (surface_x, surface_y),
smithay::desktop::WindowSurfaceType::ALL, smithay::desktop::WindowSurfaceType::ALL,
@ -1376,7 +1383,7 @@ fn process_electron_message(
state, state,
focus, focus,
&smithay::input::pointer::MotionEvent { &smithay::input::pointer::MotionEvent {
location: global, location: state.pointer_location,
serial, serial,
time, time,
}, },
@ -1415,12 +1422,8 @@ fn process_electron_message(
let surface_x = px; let surface_x = px;
let surface_y = py; let surface_y = py;
let global: Point<f64, Logical> = ( // Do NOT overwrite state.pointer_location
loc.x as f64 - geo.loc.x.max(0) as f64 + surface_x, // (same rationale as pointer_motion above).
loc.y as f64 - geo.loc.y.max(0) as f64 + surface_y,
)
.into();
state.pointer_location = global;
if pressed { if pressed {
let surface = window let surface = window
.toplevel() .toplevel()

View file

@ -650,3 +650,55 @@ impl KeyboardShortcutsInhibitHandler for WoState {
} }
smithay::delegate_keyboard_shortcuts_inhibit!(WoState); smithay::delegate_keyboard_shortcuts_inhibit!(WoState);
/// Propagate environment variables to the DBus session and systemd user
/// manager so that applications launched via desktop entries, D-Bus
/// activation, or Steam inherit DISPLAY and WAYLAND_DISPLAY.
pub fn propagate_environment(vars: &[&str]) {
use tracing::{info, warn};
let var_args: Vec<String> = vars
.iter()
.filter_map(|&name| {
std::env::var(name).ok().map(|val| format!("{name}={val}"))
})
.collect();
if var_args.is_empty() {
return;
}
let names: Vec<&str> = vars
.iter()
.filter(|&&name| std::env::var(name).is_ok())
.copied()
.collect();
// dbus-update-activation-environment sets variables for D-Bus-activated services.
let mut dbus_cmd = std::process::Command::new("dbus-update-activation-environment");
dbus_cmd.arg("--systemd");
for arg in &var_args {
dbus_cmd.arg(arg);
}
match dbus_cmd.stdout(std::process::Stdio::null()).stderr(std::process::Stdio::null()).spawn() {
Ok(mut child) => {
let _ = child.wait();
info!("Propagated {:?} to DBus activation environment", names);
}
Err(e) => warn!("dbus-update-activation-environment failed: {e}"),
}
// systemctl --user import-environment sets variables for systemd user services.
let mut systemctl_cmd = std::process::Command::new("systemctl");
systemctl_cmd.arg("--user").arg("import-environment");
for name in &names {
systemctl_cmd.arg(name);
}
match systemctl_cmd.stdout(std::process::Stdio::null()).stderr(std::process::Stdio::null()).spawn() {
Ok(mut child) => {
let _ = child.wait();
info!("Propagated {:?} to systemd user environment", names);
}
Err(e) => warn!("systemctl --user import-environment failed: {e}"),
}
}