Cry
This commit is contained in:
parent
d28e50fcf9
commit
83cdaffd05
9 changed files with 415 additions and 78 deletions
|
|
@ -1156,6 +1156,29 @@ function connectToCompositor(): Promise<void> {
|
|||
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.
|
||||
debugLog('[Wo] UNKNOWN magic 0x' + magic.toString(16).padStart(8, '0') + ' bufLen=' + compositorRxBuffer.length + ' — stream may be corrupted');
|
||||
compositorRxBuffer = compositorRxBuffer.slice(1);
|
||||
|
|
|
|||
|
|
@ -91,6 +91,14 @@ const compositorAPI: CompositorAPI = {
|
|||
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
|
||||
onPortalRequest: (callback: (data: {
|
||||
requestId: string;
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export const MAGIC = {
|
|||
FORWARD_KEYBOARD: 0x574F4B45, // "WOKE" — forwarded keyboard from web UI canvas
|
||||
FORWARD_RELATIVE_POINTER: 0x574F5245, // "WORE" — forwarded relative pointer (dx/dy)
|
||||
POINTER_LOCK_REQUEST: 0x574F504C, // "WOPL" — server-to-client pointer lock request
|
||||
ENV_UPDATE: 0x574F4555, // "WOEU" — environment variable update broadcast
|
||||
} as const;
|
||||
|
||||
/**
|
||||
|
|
@ -90,6 +91,11 @@ export interface PointerLockRequestEvent {
|
|||
lock: boolean;
|
||||
}
|
||||
|
||||
export interface EnvUpdateEvent {
|
||||
type: 'envUpdate';
|
||||
vars: Record<string, string>;
|
||||
}
|
||||
|
||||
export type InputEvent =
|
||||
| MouseMoveEvent
|
||||
| MouseButtonEvent
|
||||
|
|
@ -97,7 +103,8 @@ export type InputEvent =
|
|||
| ScrollEvent
|
||||
| FocusChangeEvent
|
||||
| WindowMetadataEvent
|
||||
| PointerLockRequestEvent;
|
||||
| PointerLockRequestEvent
|
||||
| EnvUpdateEvent;
|
||||
|
||||
/**
|
||||
* Compositor Actions from Client → Compositor
|
||||
|
|
|
|||
6
electron/types/types.d.ts
vendored
6
electron/types/types.d.ts
vendored
|
|
@ -278,6 +278,12 @@ export interface Compositor {
|
|||
*/
|
||||
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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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_SCROLL: u32 = 0x574F5053; // "WOPS"
|
||||
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)]
|
||||
|
|
@ -143,6 +144,8 @@ pub enum ElectronInputEvent {
|
|||
FocusChange { window_name: String, focused: bool },
|
||||
WindowMetadata { metadata: String }, // JSON payload
|
||||
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.
|
||||
|
|
@ -435,6 +438,14 @@ impl ElectronClientConnection {
|
|||
buf[12..].copy_from_slice(name_bytes);
|
||||
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(())
|
||||
}
|
||||
|
|
@ -502,6 +513,14 @@ impl ElectronClientConnection {
|
|||
buf[12..].copy_from_slice(name_bytes);
|
||||
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(())
|
||||
}
|
||||
|
||||
/// 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).
|
||||
/// 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
|
||||
|
|
|
|||
|
|
@ -228,33 +228,7 @@ impl WoState {
|
|||
return;
|
||||
}
|
||||
|
||||
// In nested mode, pointer events are routed through Electron/comraw.
|
||||
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;
|
||||
}
|
||||
self.dispatch_pointer_to_targets(event.time_msec());
|
||||
}
|
||||
|
||||
fn on_pointer_motion_abs<B: InputBackend>(&mut self, event: B::PointerMotionAbsoluteEvent) {
|
||||
|
|
@ -329,33 +303,90 @@ impl WoState {
|
|||
return;
|
||||
}
|
||||
|
||||
// In nested mode, pointer events are routed through Electron/comraw.
|
||||
if self.electron_ipc.is_some() {
|
||||
let under_window =
|
||||
self.window_under((self.pointer_location.x, self.pointer_location.y));
|
||||
self.dispatch_pointer_to_targets(event.time_msec());
|
||||
}
|
||||
|
||||
if self.pointer_window_focus != under_window {
|
||||
self.pointer_window_focus = under_window.clone();
|
||||
}
|
||||
/// 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},
|
||||
};
|
||||
|
||||
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),
|
||||
) {
|
||||
let client = ipc.clients.lock().unwrap().get(win_name).cloned();
|
||||
if let Some(client) = client {
|
||||
let _ = client.try_send_input_event(&ElectronInputEvent::MouseMove {
|
||||
x: local.0,
|
||||
y: local.1,
|
||||
});
|
||||
}
|
||||
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(local) = self.local_window_coords(win_name, pos) {
|
||||
if let Some(client) = ipc.clients.lock().unwrap().get(win_name).cloned() {
|
||||
let _ = client.try_send_input_event(&ElectronInputEvent::MouseMove {
|
||||
x: local.0,
|
||||
y: local.1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
|
@ -488,10 +519,13 @@ impl WoState {
|
|||
Some(w) => w,
|
||||
None => return,
|
||||
};
|
||||
let win_loc = self.space.element_location(&window).unwrap_or_default();
|
||||
let global_x = win_loc.x as f64 + x;
|
||||
let global_y = win_loc.y as f64 + y;
|
||||
self.pointer_location = (global_x, global_y).into();
|
||||
|
||||
// Do NOT overwrite self.pointer_location here. The hardware input
|
||||
// handler (on_pointer_motion / on_pointer_motion_abs) already set it
|
||||
// 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() {
|
||||
Some(p) => p,
|
||||
|
|
|
|||
191
src/main.rs
191
src/main.rs
|
|
@ -648,16 +648,22 @@ fn render_frame(data: &mut EventLoopData) {
|
|||
);
|
||||
}
|
||||
|
||||
for element in wayland_elements {
|
||||
let src = element.src();
|
||||
let dst = element.geometry(scale);
|
||||
if let Err(e) = element.draw(&mut frame, src, dst, &[data.output_rect], &[]) {
|
||||
if is_egl_context_or_surface_lost(&e.to_string()) {
|
||||
error!("EGL context lost during space rendering");
|
||||
context_lost = true;
|
||||
return false;
|
||||
// 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 {
|
||||
let src = element.src();
|
||||
let dst = element.geometry(scale);
|
||||
if let Err(e) = element.draw(&mut frame, src, dst, &[data.output_rect], &[]) {
|
||||
if is_egl_context_or_surface_lost(&e.to_string()) {
|
||||
error!("EGL context lost during space rendering");
|
||||
context_lost = true;
|
||||
return false;
|
||||
}
|
||||
trace!("Element rendering failed: {e}");
|
||||
}
|
||||
trace!("Element rendering failed: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -702,9 +708,9 @@ fn render_frame(data: &mut EventLoopData) {
|
|||
}
|
||||
|
||||
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(
|
||||
&surface,
|
||||
surface,
|
||||
&data.state.backend.output,
|
||||
presented_at,
|
||||
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
|
||||
// animating it (e.g. spinning/loading cursors).
|
||||
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.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.
|
||||
// Caching in the slot's userdata avoids a create/destroy cycle every
|
||||
// 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)>(),
|
||||
true,
|
||||
std::process::Stdio::null(),
|
||||
std::process::Stdio::null(),
|
||||
std::process::Stdio::inherit(),
|
||||
|_| {},
|
||||
) {
|
||||
Ok((xwayland, xw_client)) => {
|
||||
|
|
@ -1306,6 +1452,14 @@ fn run_drm(mut config: Config) -> Result<(), anyhow::Error> {
|
|||
} => {
|
||||
info!("XWayland ready on DISPLAY :{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() {
|
||||
match X11Wm::start_wm(wm_handle.clone(), x11_socket, client) {
|
||||
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);
|
||||
// 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 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)
|
||||
if !data.autostart_tasks.is_empty() {
|
||||
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)> =
|
||||
Vec::new();
|
||||
for (task, when) in data.autostart_tasks.drain(..) {
|
||||
if when <= now {
|
||||
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
|
||||
if let Some(name) = cmd.strip_prefix("wo://window/") {
|
||||
if let Some(win_cfg) =
|
||||
|
|
|
|||
|
|
@ -179,7 +179,7 @@ pub fn run_nested(config: Config) -> Result<()> {
|
|||
std::iter::empty::<(String, String)>(),
|
||||
true, // open abstract socket for X11 compatibility
|
||||
std::process::Stdio::null(),
|
||||
std::process::Stdio::null(),
|
||||
std::process::Stdio::inherit(),
|
||||
|_| {}, // no extra user data
|
||||
) {
|
||||
Ok((xwayland, xw_client)) => {
|
||||
|
|
@ -196,6 +196,14 @@ pub fn run_nested(config: Config) -> Result<()> {
|
|||
} => {
|
||||
info!("XWayland ready on DISPLAY :{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() {
|
||||
match X11Wm::start_wm(wm_handle.clone(), x11_socket, client) {
|
||||
Ok(wm) => {
|
||||
|
|
@ -224,6 +232,7 @@ pub fn run_nested(config: Config) -> Result<()> {
|
|||
|
||||
std::env::set_var("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");
|
||||
// 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_y = py;
|
||||
|
||||
let global: Point<f64, Logical> = (
|
||||
loc.x as f64 - geo.loc.x.max(0) as f64 + surface_x,
|
||||
loc.y as f64 - geo.loc.y.max(0) as f64 + surface_y,
|
||||
)
|
||||
.into();
|
||||
state.pointer_location = global;
|
||||
// Do NOT overwrite state.pointer_location.
|
||||
// The hardware input handler already set it
|
||||
// to the authoritative position; recomputing
|
||||
// from element_location + geo causes ping-pong.
|
||||
let under = window.surface_under(
|
||||
(surface_x, surface_y),
|
||||
smithay::desktop::WindowSurfaceType::ALL,
|
||||
|
|
@ -1376,7 +1383,7 @@ fn process_electron_message(
|
|||
state,
|
||||
focus,
|
||||
&smithay::input::pointer::MotionEvent {
|
||||
location: global,
|
||||
location: state.pointer_location,
|
||||
serial,
|
||||
time,
|
||||
},
|
||||
|
|
@ -1415,12 +1422,8 @@ fn process_electron_message(
|
|||
let surface_x = px;
|
||||
let surface_y = py;
|
||||
|
||||
let global: Point<f64, Logical> = (
|
||||
loc.x as f64 - geo.loc.x.max(0) as f64 + surface_x,
|
||||
loc.y as f64 - geo.loc.y.max(0) as f64 + surface_y,
|
||||
)
|
||||
.into();
|
||||
state.pointer_location = global;
|
||||
// Do NOT overwrite state.pointer_location
|
||||
// (same rationale as pointer_motion above).
|
||||
if pressed {
|
||||
let surface = window
|
||||
.toplevel()
|
||||
|
|
|
|||
52
src/state.rs
52
src/state.rs
|
|
@ -650,3 +650,55 @@ impl KeyboardShortcutsInhibitHandler for 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}"),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue