Remove garbage GPTed readme
This commit is contained in:
parent
104261d74c
commit
0bede114a6
15 changed files with 499 additions and 189 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -3209,6 +3209,7 @@ dependencies = [
|
||||||
"udev",
|
"udev",
|
||||||
"wayland-backend",
|
"wayland-backend",
|
||||||
"wayland-protocols",
|
"wayland-protocols",
|
||||||
|
"wayland-protocols-wlr",
|
||||||
"wayland-server",
|
"wayland-server",
|
||||||
"xcursor",
|
"xcursor",
|
||||||
"xkbcommon 0.9.0",
|
"xkbcommon 0.9.0",
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ gl = "0.14"
|
||||||
# Wayland
|
# Wayland
|
||||||
wayland-server = "0.31"
|
wayland-server = "0.31"
|
||||||
wayland-protocols = { version = "0.32", features = ["server", "staging", "unstable"] }
|
wayland-protocols = { version = "0.32", features = ["server", "staging", "unstable"] }
|
||||||
|
wayland-protocols-wlr = { version = "0.3", features = ["server"] }
|
||||||
wayland-backend = { version = "0.3", features = ["server_system"] }
|
wayland-backend = { version = "0.3", features = ["server_system"] }
|
||||||
|
|
||||||
# Async / IPC
|
# Async / IPC
|
||||||
|
|
|
||||||
14
electron/dist/main.js
vendored
14
electron/dist/main.js
vendored
|
|
@ -46,7 +46,8 @@ var MAGIC = {
|
||||||
FORWARD_KEYBOARD: 1464814405,
|
FORWARD_KEYBOARD: 1464814405,
|
||||||
FORWARD_RELATIVE_POINTER: 1464816197,
|
FORWARD_RELATIVE_POINTER: 1464816197,
|
||||||
POINTER_LOCK_REQUEST: 1464815692,
|
POINTER_LOCK_REQUEST: 1464815692,
|
||||||
ENV_UPDATE: 1464812885
|
ENV_UPDATE: 1464812885,
|
||||||
|
SCREENCOPY_EVENT: 1464816453
|
||||||
};
|
};
|
||||||
function stringToBuffer(str) {
|
function stringToBuffer(str) {
|
||||||
return Buffer.from(str, "utf-8");
|
return Buffer.from(str, "utf-8");
|
||||||
|
|
@ -1319,6 +1320,17 @@ function connectToCompositor() {
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (magic === MAGIC.SCREENCOPY_EVENT) {
|
||||||
|
if (compositorRxBuffer.length < 12)
|
||||||
|
break;
|
||||||
|
const active = compositorRxBuffer.readUInt32LE(4) !== 0;
|
||||||
|
const clientCount = compositorRxBuffer.readUInt32LE(8);
|
||||||
|
compositorRxBuffer = compositorRxBuffer.slice(12);
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send("wo:screencopy-event", { active, clientCount });
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1219,6 +1219,19 @@ function connectToCompositor(): Promise<void> {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (magic === MAGIC.SCREENCOPY_EVENT) {
|
||||||
|
// magic(4) + active(4) + client_count(4) = 12 bytes
|
||||||
|
if (compositorRxBuffer.length < 12) break;
|
||||||
|
const active = compositorRxBuffer.readUInt32LE(4) !== 0;
|
||||||
|
const clientCount = compositorRxBuffer.readUInt32LE(8);
|
||||||
|
compositorRxBuffer = compositorRxBuffer.slice(12);
|
||||||
|
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send('wo:screencopy-event', { active, clientCount });
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,14 @@ const compositorAPI: CompositorAPI = {
|
||||||
ipcRenderer.on('wo:portal-request', handler);
|
ipcRenderer.on('wo:portal-request', handler);
|
||||||
return () => ipcRenderer.removeListener('wo:portal-request', handler);
|
return () => ipcRenderer.removeListener('wo:portal-request', handler);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Screencopy capture event notifications
|
||||||
|
onScreencopyEvent: (callback: (data: { active: boolean; clientCount: number }) => void) => {
|
||||||
|
const handler = (_event: Electron.IpcRendererEvent, data: { active: boolean; clientCount: number }) =>
|
||||||
|
callback(data);
|
||||||
|
ipcRenderer.on('wo:screencopy-event', handler);
|
||||||
|
return () => ipcRenderer.removeListener('wo:screencopy-event', handler);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const woClientAPI: WoClientAPI = {
|
const woClientAPI: WoClientAPI = {
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ export const MAGIC = {
|
||||||
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
|
ENV_UPDATE: 0x574F4555, // "WOEU" — environment variable update broadcast
|
||||||
|
SCREENCOPY_EVENT: 0x574F5345, // "WOSE" — screencopy capture event notification
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -96,6 +97,12 @@ export interface EnvUpdateEvent {
|
||||||
vars: Record<string, string>;
|
vars: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ScreencopyEvent {
|
||||||
|
type: 'screencopyEvent';
|
||||||
|
active: boolean;
|
||||||
|
clientCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
export type InputEvent =
|
export type InputEvent =
|
||||||
| MouseMoveEvent
|
| MouseMoveEvent
|
||||||
| MouseButtonEvent
|
| MouseButtonEvent
|
||||||
|
|
@ -104,7 +111,8 @@ export type InputEvent =
|
||||||
| FocusChangeEvent
|
| FocusChangeEvent
|
||||||
| WindowMetadataEvent
|
| WindowMetadataEvent
|
||||||
| PointerLockRequestEvent
|
| PointerLockRequestEvent
|
||||||
| EnvUpdateEvent;
|
| EnvUpdateEvent
|
||||||
|
| ScreencopyEvent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compositor Actions from Client → Compositor
|
* Compositor Actions from Client → Compositor
|
||||||
|
|
|
||||||
|
|
@ -1,186 +0,0 @@
|
||||||
# wo-types
|
|
||||||
|
|
||||||
Type definitions for the Wo Wayland compositor Electron integration.
|
|
||||||
|
|
||||||
This package provides fully-typed interfaces for the compositor APIs exposed to Electron windows running within the Wo compositor.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bun install wo-types
|
|
||||||
# or
|
|
||||||
npm install wo-types
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### In Preload Scripts
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import type { CompositorAPI, WoClientAPI } from 'wo-types';
|
|
||||||
|
|
||||||
// These are already exposed as window.compositor and window.woClient
|
|
||||||
const compositorAPI: CompositorAPI = window.compositor;
|
|
||||||
const clientAPI: WoClientAPI = window.woClient;
|
|
||||||
```
|
|
||||||
|
|
||||||
### In React Components
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import type { WoWindow, WindowMetadataEvent } from 'wo-types';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
function MyComponent() {
|
|
||||||
const [windows, setWindows] = useState<WoWindow[]>([]);
|
|
||||||
const [metadata, setMetadata] = useState<WindowMetadataEvent | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Subscribe to window updates
|
|
||||||
const unsubWindows = window.woClient?.onWindows((windowList) => {
|
|
||||||
setWindows(windowList);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Subscribe to metadata updates
|
|
||||||
const unsubMetadata = window.compositor?.onWindowMetadata((meta) => {
|
|
||||||
setMetadata(meta);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
unsubWindows?.();
|
|
||||||
unsubMetadata?.();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<p>Active windows: {metadata?.count ?? 0}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## API Reference
|
|
||||||
|
|
||||||
### `CompositorAPI`
|
|
||||||
|
|
||||||
Exposed as `window.compositor` in Electron windows.
|
|
||||||
|
|
||||||
#### Methods
|
|
||||||
|
|
||||||
- `action(name: string, payload?: Record<string, unknown>): Promise<unknown>`
|
|
||||||
- Send an action to the compositor
|
|
||||||
|
|
||||||
- `actionSync(name: string, payload?: Record<string, unknown>): void`
|
|
||||||
- Send an action synchronously (fire and forget)
|
|
||||||
|
|
||||||
- `quit(code?: number): Promise<unknown>`
|
|
||||||
- Request the compositor to quit
|
|
||||||
|
|
||||||
- `reload(): Promise<unknown>`
|
|
||||||
- Reload the current window
|
|
||||||
|
|
||||||
- `toggleDevTools(): Promise<unknown>`
|
|
||||||
- Toggle DevTools for the current window
|
|
||||||
|
|
||||||
- `onFocusChange(callback: (window: string, focused: boolean) => void): () => void`
|
|
||||||
- Register focus change notification callback
|
|
||||||
|
|
||||||
- `onWindowMetadata(callback: (metadata: WindowMetadataEvent) => void): () => void`
|
|
||||||
- Register window metadata update callback
|
|
||||||
|
|
||||||
- `syscall(type: string, params: Record<string, unknown>): Promise<unknown>`
|
|
||||||
- Execute a syscall in the compositor
|
|
||||||
|
|
||||||
### `WoClientAPI`
|
|
||||||
|
|
||||||
Exposed as `window.woClient` in Electron windows.
|
|
||||||
|
|
||||||
#### Methods
|
|
||||||
|
|
||||||
- `getWindows(): Promise<WoWindow[]>`
|
|
||||||
- Get list of all windows
|
|
||||||
|
|
||||||
- `onWindows(callback: (windows: WoWindow[]) => void): () => void`
|
|
||||||
- Register window list update callback
|
|
||||||
|
|
||||||
- `sendAction(action: string): void`
|
|
||||||
- Send a keybind action to the compositor
|
|
||||||
|
|
||||||
## Types
|
|
||||||
|
|
||||||
### `WindowMetadata`
|
|
||||||
|
|
||||||
Describes a Wayland surface or application window.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface WindowMetadata {
|
|
||||||
name: string;
|
|
||||||
title?: string;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
z_order: number;
|
|
||||||
fps: number;
|
|
||||||
focused?: boolean;
|
|
||||||
focusable: boolean;
|
|
||||||
interactive: boolean;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### `WoWindow`
|
|
||||||
|
|
||||||
Window information returned by the client API.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface WoWindow {
|
|
||||||
name: string;
|
|
||||||
title?: string;
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
z_order: number;
|
|
||||||
fps: number;
|
|
||||||
focused: boolean;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### `WindowMetadataEvent`
|
|
||||||
|
|
||||||
Event data for window metadata updates.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface WindowMetadataEvent {
|
|
||||||
windows: WindowMetadata[];
|
|
||||||
count: number;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### `FocusChangeEvent`
|
|
||||||
|
|
||||||
Event data for focus change notifications.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface FocusChangeEvent {
|
|
||||||
window: string;
|
|
||||||
focused: boolean;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### `S2CAction`
|
|
||||||
|
|
||||||
Enum of server-to-client actions.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
enum S2CAction {
|
|
||||||
WindowMetadataUpdate = 'window-metadata-update',
|
|
||||||
FocusChange = 'focus-change',
|
|
||||||
SyscallResponse = 'syscall-response',
|
|
||||||
InputEvent = 'input-event',
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT
|
|
||||||
|
|
@ -21,6 +21,8 @@ export type {
|
||||||
DamagePatch,
|
DamagePatch,
|
||||||
RendererDamageHelper,
|
RendererDamageHelper,
|
||||||
RendererDamageHelperOptions,
|
RendererDamageHelperOptions,
|
||||||
|
PortalRequestEvent,
|
||||||
|
ScreencopyEvent,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
export { InteractionDispatcher } from './dispatcher';
|
export { InteractionDispatcher } from './dispatcher';
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"name": "@wl-wo/wo-types",
|
"name": "@wl-wo/wo-types",
|
||||||
"module": "index.ts",
|
"module": "index.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest"
|
"@types/bun": "latest"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
11
electron/types/types.d.ts
vendored
11
electron/types/types.d.ts
vendored
|
|
@ -110,6 +110,11 @@ export interface PortalRequestEvent {
|
||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ScreencopyEvent {
|
||||||
|
active: boolean;
|
||||||
|
clientCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Application information for launching
|
* Application information for launching
|
||||||
*/
|
*/
|
||||||
|
|
@ -288,6 +293,12 @@ export interface Compositor {
|
||||||
* Subscribe to generic portal requests so clients can provide custom UI.
|
* Subscribe to generic portal requests so clients can provide custom UI.
|
||||||
*/
|
*/
|
||||||
onPortalRequest(callback: (data: PortalRequestEvent) => void): Unsubscribe;
|
onPortalRequest(callback: (data: PortalRequestEvent) => void): Unsubscribe;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to screencopy capture events (active/inactive notifications).
|
||||||
|
* Fires when Wayland clients (e.g. OBS, grim) capture the screen via wlr-screencopy.
|
||||||
|
*/
|
||||||
|
onScreencopyEvent(callback: (data: ScreencopyEvent) => void): Unsubscribe;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Alias for Compositor to match preload.ts imports */
|
/** Alias for Compositor to match preload.ts imports */
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ 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
|
pub const MAGIC_ENV_UPDATE: u32 = 0x574F4555; // "WOEU" — environment variable update broadcast
|
||||||
|
pub const MAGIC_SCREENCOPY_EVENT: u32 = 0x574F5345; // "WOSE" — screencopy capture event notification
|
||||||
|
|
||||||
|
|
||||||
#[repr(C, packed)]
|
#[repr(C, packed)]
|
||||||
|
|
@ -146,6 +147,8 @@ pub enum ElectronInputEvent {
|
||||||
PointerLockRequest { window_name: String, lock: bool },
|
PointerLockRequest { window_name: String, lock: bool },
|
||||||
/// Environment variable update (e.g. DISPLAY after XWayland is ready).
|
/// Environment variable update (e.g. DISPLAY after XWayland is ready).
|
||||||
EnvUpdate { vars: String },
|
EnvUpdate { vars: String },
|
||||||
|
/// Screencopy capture event (active/inactive notification).
|
||||||
|
ScreencopyEvent { active: bool, client_count: u32 },
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A bidirectional connection to an Electron client.
|
/// A bidirectional connection to an Electron client.
|
||||||
|
|
@ -464,6 +467,13 @@ impl ElectronClientConnection {
|
||||||
buf[8..].copy_from_slice(var_bytes);
|
buf[8..].copy_from_slice(var_bytes);
|
||||||
self.write_bytes(&buf)?;
|
self.write_bytes(&buf)?;
|
||||||
},
|
},
|
||||||
|
ElectronInputEvent::ScreencopyEvent { active, client_count } => {
|
||||||
|
let mut buf = [0u8; 12];
|
||||||
|
buf[0..4].copy_from_slice(&MAGIC_SCREENCOPY_EVENT.to_le_bytes());
|
||||||
|
buf[4..8].copy_from_slice(&(*active as u32).to_le_bytes());
|
||||||
|
buf[8..12].copy_from_slice(&client_count.to_le_bytes());
|
||||||
|
self.write_bytes(&buf)?;
|
||||||
|
},
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -539,6 +549,13 @@ impl ElectronClientConnection {
|
||||||
buf[8..].copy_from_slice(var_bytes);
|
buf[8..].copy_from_slice(var_bytes);
|
||||||
self.write_bytes_nonblocking(&buf)
|
self.write_bytes_nonblocking(&buf)
|
||||||
}
|
}
|
||||||
|
ElectronInputEvent::ScreencopyEvent { active, client_count } => {
|
||||||
|
let mut buf = [0u8; 12];
|
||||||
|
buf[0..4].copy_from_slice(&MAGIC_SCREENCOPY_EVENT.to_le_bytes());
|
||||||
|
buf[4..8].copy_from_slice(&(*active as u32).to_le_bytes());
|
||||||
|
buf[8..12].copy_from_slice(&client_count.to_le_bytes());
|
||||||
|
self.write_bytes_nonblocking(&buf)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -725,6 +742,24 @@ impl ElectronIpc {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Broadcast screencopy capture event to all connected Electron clients.
|
||||||
|
pub fn broadcast_screencopy_event(&self, active: bool, client_count: u32) -> Result<()> {
|
||||||
|
let snapshot: Vec<_> = {
|
||||||
|
let clients = self.clients.lock().unwrap();
|
||||||
|
clients.values().cloned().collect()
|
||||||
|
};
|
||||||
|
let event = ElectronInputEvent::ScreencopyEvent {
|
||||||
|
active,
|
||||||
|
client_count,
|
||||||
|
};
|
||||||
|
for client in &snapshot {
|
||||||
|
if let Err(e) = client.try_send_input_event(&event) {
|
||||||
|
warn!("screencopy event 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
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ pub mod compositor;
|
||||||
pub mod dmabuf_handler;
|
pub mod dmabuf_handler;
|
||||||
pub mod input;
|
pub mod input;
|
||||||
pub mod output;
|
pub mod output;
|
||||||
|
pub mod screencopy;
|
||||||
pub mod xdg_decoration;
|
pub mod xdg_decoration;
|
||||||
pub mod xdg_shell;
|
pub mod xdg_shell;
|
||||||
pub mod xwayland;
|
pub mod xwayland;
|
||||||
|
|
|
||||||
252
src/handlers/screencopy.rs
Normal file
252
src/handlers/screencopy.rs
Normal file
|
|
@ -0,0 +1,252 @@
|
||||||
|
/// wlr-screencopy-unstable-v1 protocol handler for the Wo compositor.
|
||||||
|
///
|
||||||
|
/// Implements `zwlr_screencopy_manager_v1` allowing clients like grim, OBS, etc.
|
||||||
|
/// to request frame copies of outputs or output regions.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use tracing::{debug, info, warn};
|
||||||
|
use wayland_protocols_wlr::screencopy::v1::server::{
|
||||||
|
zwlr_screencopy_frame_v1::{self, ZwlrScreencopyFrameV1},
|
||||||
|
zwlr_screencopy_manager_v1::{self, ZwlrScreencopyManagerV1},
|
||||||
|
};
|
||||||
|
use wayland_server::{
|
||||||
|
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
|
||||||
|
protocol::{wl_buffer::WlBuffer, wl_output::WlOutput, wl_shm},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::state::WoState;
|
||||||
|
|
||||||
|
/// State for a pending screencopy frame capture.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ScreencopyFrameState {
|
||||||
|
/// Whether the cursor overlay was requested.
|
||||||
|
pub overlay_cursor: bool,
|
||||||
|
/// Capture region in output-logical coordinates (x, y, w, h).
|
||||||
|
pub region: (i32, i32, i32, i32),
|
||||||
|
/// Output width at frame creation time.
|
||||||
|
pub output_width: u32,
|
||||||
|
/// Output height at frame creation time.
|
||||||
|
pub output_height: u32,
|
||||||
|
/// Whether a buffer has been attached via `copy` or `copy_with_damage`.
|
||||||
|
pub buffer_attached: bool,
|
||||||
|
/// Whether we're waiting for damage (`copy_with_damage`).
|
||||||
|
pub with_damage: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compositor-wide screencopy manager state.
|
||||||
|
pub struct ScreencopyManagerState {
|
||||||
|
/// Pending frame requests waiting for the next render pass.
|
||||||
|
pub pending_frames: Arc<Mutex<Vec<PendingFrame>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A frame request queued for the next render pass.
|
||||||
|
pub struct PendingFrame {
|
||||||
|
pub frame: ZwlrScreencopyFrameV1,
|
||||||
|
pub buffer: WlBuffer,
|
||||||
|
pub region: (i32, i32, i32, i32),
|
||||||
|
pub overlay_cursor: bool,
|
||||||
|
pub with_damage: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScreencopyManagerState {
|
||||||
|
pub fn new<D>(display: &DisplayHandle) -> Self
|
||||||
|
where
|
||||||
|
D: GlobalDispatch<ZwlrScreencopyManagerV1, ()> + 'static,
|
||||||
|
D: Dispatch<ZwlrScreencopyManagerV1, ()> + 'static,
|
||||||
|
D: Dispatch<ZwlrScreencopyFrameV1, ScreencopyFrameState> + 'static,
|
||||||
|
{
|
||||||
|
display.create_global::<D, ZwlrScreencopyManagerV1, ()>(3, ());
|
||||||
|
info!("Registered zwlr_screencopy_manager_v1 global (v3)");
|
||||||
|
|
||||||
|
Self {
|
||||||
|
pending_frames: Arc::new(Mutex::new(Vec::new())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Take all pending frame requests for processing in the render loop.
|
||||||
|
pub fn take_pending_frames(&self) -> Vec<PendingFrame> {
|
||||||
|
let mut pending = self.pending_frames.lock().unwrap();
|
||||||
|
std::mem::take(&mut *pending)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if there are any pending frame requests.
|
||||||
|
pub fn has_pending_frames(&self) -> bool {
|
||||||
|
let pending = self.pending_frames.lock().unwrap();
|
||||||
|
!pending.is_empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── GlobalDispatch for the manager ──────────────────────────────────────────
|
||||||
|
|
||||||
|
impl GlobalDispatch<ZwlrScreencopyManagerV1, ()> for WoState {
|
||||||
|
fn bind(
|
||||||
|
_state: &mut Self,
|
||||||
|
_handle: &DisplayHandle,
|
||||||
|
_client: &Client,
|
||||||
|
resource: New<ZwlrScreencopyManagerV1>,
|
||||||
|
_global_data: &(),
|
||||||
|
data_init: &mut DataInit<'_, Self>,
|
||||||
|
) {
|
||||||
|
data_init.init(resource, ());
|
||||||
|
debug!("Client bound zwlr_screencopy_manager_v1");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Dispatch for the manager interface ──────────────────────────────────────
|
||||||
|
|
||||||
|
impl Dispatch<ZwlrScreencopyManagerV1, ()> for WoState {
|
||||||
|
fn request(
|
||||||
|
state: &mut Self,
|
||||||
|
_client: &Client,
|
||||||
|
_resource: &ZwlrScreencopyManagerV1,
|
||||||
|
request: zwlr_screencopy_manager_v1::Request,
|
||||||
|
_data: &(),
|
||||||
|
_dhandle: &DisplayHandle,
|
||||||
|
data_init: &mut DataInit<'_, Self>,
|
||||||
|
) {
|
||||||
|
match request {
|
||||||
|
zwlr_screencopy_manager_v1::Request::CaptureOutput {
|
||||||
|
frame,
|
||||||
|
overlay_cursor,
|
||||||
|
output: _,
|
||||||
|
} => {
|
||||||
|
let (w, h) = state.output_size;
|
||||||
|
let frame_state = ScreencopyFrameState {
|
||||||
|
overlay_cursor: overlay_cursor != 0,
|
||||||
|
region: (0, 0, w as i32, h as i32),
|
||||||
|
output_width: w,
|
||||||
|
output_height: h,
|
||||||
|
buffer_attached: false,
|
||||||
|
with_damage: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let frame_resource = data_init.init(frame, frame_state);
|
||||||
|
send_buffer_info(&frame_resource, w, h);
|
||||||
|
debug!(
|
||||||
|
"CaptureOutput: {}x{}, cursor_overlay={}",
|
||||||
|
w, h, overlay_cursor != 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
zwlr_screencopy_manager_v1::Request::CaptureOutputRegion {
|
||||||
|
frame,
|
||||||
|
overlay_cursor,
|
||||||
|
output: _,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
} => {
|
||||||
|
let (ow, oh) = state.output_size;
|
||||||
|
// Clamp region to output bounds
|
||||||
|
let rx = x.max(0);
|
||||||
|
let ry = y.max(0);
|
||||||
|
let rw = width.min(ow as i32 - rx).max(1);
|
||||||
|
let rh = height.min(oh as i32 - ry).max(1);
|
||||||
|
|
||||||
|
let frame_state = ScreencopyFrameState {
|
||||||
|
overlay_cursor: overlay_cursor != 0,
|
||||||
|
region: (rx, ry, rw, rh),
|
||||||
|
output_width: rw as u32,
|
||||||
|
output_height: rh as u32,
|
||||||
|
buffer_attached: false,
|
||||||
|
with_damage: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let frame_resource = data_init.init(frame, frame_state);
|
||||||
|
send_buffer_info(&frame_resource, rw as u32, rh as u32);
|
||||||
|
debug!(
|
||||||
|
"CaptureOutputRegion: ({},{}) {}x{}, cursor_overlay={}",
|
||||||
|
rx, ry, rw, rh, overlay_cursor != 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
zwlr_screencopy_manager_v1::Request::Destroy => {
|
||||||
|
debug!("Client destroyed screencopy manager");
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send buffer format info events to the frame client.
|
||||||
|
fn send_buffer_info(frame: &ZwlrScreencopyFrameV1, width: u32, height: u32) {
|
||||||
|
let stride = width * 4; // ARGB8888 / BGRx
|
||||||
|
|
||||||
|
// SHM buffer support (always available)
|
||||||
|
frame.buffer(
|
||||||
|
wl_shm::Format::Argb8888,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
stride,
|
||||||
|
);
|
||||||
|
|
||||||
|
// DMABUF support (v3)
|
||||||
|
if frame.version() >= 3 {
|
||||||
|
frame.linux_dmabuf(
|
||||||
|
drm_fourcc::DrmFourcc::Argb8888 as u32,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
);
|
||||||
|
frame.buffer_done();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Dispatch for the frame interface ────────────────────────────────────────
|
||||||
|
|
||||||
|
impl Dispatch<ZwlrScreencopyFrameV1, ScreencopyFrameState> for WoState {
|
||||||
|
fn request(
|
||||||
|
state: &mut Self,
|
||||||
|
_client: &Client,
|
||||||
|
resource: &ZwlrScreencopyFrameV1,
|
||||||
|
request: zwlr_screencopy_frame_v1::Request,
|
||||||
|
data: &ScreencopyFrameState,
|
||||||
|
_dhandle: &DisplayHandle,
|
||||||
|
_data_init: &mut DataInit<'_, Self>,
|
||||||
|
) {
|
||||||
|
match request {
|
||||||
|
zwlr_screencopy_frame_v1::Request::Copy { buffer } => {
|
||||||
|
queue_frame_copy(state, resource, buffer, data, false);
|
||||||
|
}
|
||||||
|
zwlr_screencopy_frame_v1::Request::CopyWithDamage { buffer } => {
|
||||||
|
queue_frame_copy(state, resource, buffer, data, true);
|
||||||
|
}
|
||||||
|
zwlr_screencopy_frame_v1::Request::Destroy => {
|
||||||
|
debug!("Screencopy frame destroyed");
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Queue a frame copy request for processing in the next render pass.
|
||||||
|
fn queue_frame_copy(
|
||||||
|
state: &mut WoState,
|
||||||
|
frame: &ZwlrScreencopyFrameV1,
|
||||||
|
buffer: WlBuffer,
|
||||||
|
data: &ScreencopyFrameState,
|
||||||
|
with_damage: bool,
|
||||||
|
) {
|
||||||
|
if let Some(ref screencopy_state) = state.screencopy_state {
|
||||||
|
let pending = PendingFrame {
|
||||||
|
frame: frame.clone(),
|
||||||
|
buffer,
|
||||||
|
region: data.region,
|
||||||
|
overlay_cursor: data.overlay_cursor,
|
||||||
|
with_damage,
|
||||||
|
};
|
||||||
|
|
||||||
|
screencopy_state
|
||||||
|
.pending_frames
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.push(pending);
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"Queued screencopy frame: region={:?}, with_damage={}",
|
||||||
|
data.region, with_damage
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
warn!("Screencopy frame copy requested but screencopy state not initialized");
|
||||||
|
frame.failed();
|
||||||
|
}
|
||||||
|
}
|
||||||
145
src/main.rs
145
src/main.rs
|
|
@ -54,6 +54,7 @@ use std::{
|
||||||
};
|
};
|
||||||
use tracing::{error, info, trace, warn};
|
use tracing::{error, info, trace, warn};
|
||||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
|
||||||
|
use wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_frame_v1;
|
||||||
|
|
||||||
pub mod state;
|
pub mod state;
|
||||||
pub mod syscall;
|
pub mod syscall;
|
||||||
|
|
@ -945,6 +946,150 @@ fn render_frame(data: &mut EventLoopData) {
|
||||||
renderer.with_context(|gl| unsafe { gl.Finish() })
|
renderer.with_context(|gl| unsafe { gl.Finish() })
|
||||||
.unwrap_or_else(|e| warn!("glFinish failed: {e}"));
|
.unwrap_or_else(|e| warn!("glFinish failed: {e}"));
|
||||||
|
|
||||||
|
// ── Fulfill pending wlr-screencopy frame requests ───────────────
|
||||||
|
// After glFinish the framebuffer pixels are ready. Read them back
|
||||||
|
// and copy into each waiting client's SHM buffer.
|
||||||
|
if let Some(ref screencopy) = data.state.screencopy_state {
|
||||||
|
let pending = screencopy.take_pending_frames();
|
||||||
|
if !pending.is_empty() {
|
||||||
|
// Read back the full framebuffer via glReadPixels
|
||||||
|
let (fb_w, fb_h) = (data.width as i32, data.height as i32);
|
||||||
|
let fb_size = (fb_w as usize) * (fb_h as usize) * 4;
|
||||||
|
let mut fb_pixels = vec![0u8; fb_size];
|
||||||
|
let read_ok = renderer.with_context(|gl| unsafe {
|
||||||
|
gl.ReadPixels(
|
||||||
|
0, 0, fb_w, fb_h,
|
||||||
|
gl::RGBA,
|
||||||
|
gl::UNSIGNED_BYTE,
|
||||||
|
fb_pixels.as_mut_ptr() as *mut _,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if read_ok.is_ok() {
|
||||||
|
// GL returns bottom-up rows; flip to top-down.
|
||||||
|
let stride = fb_w as usize * 4;
|
||||||
|
for y in 0..(fb_h as usize / 2) {
|
||||||
|
let top = y * stride;
|
||||||
|
let bot = (fb_h as usize - 1 - y) * stride;
|
||||||
|
for x in 0..stride {
|
||||||
|
fb_pixels.swap(top + x, bot + x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert RGBA→ARGB (wl_shm expects ARGB8888)
|
||||||
|
for pixel in fb_pixels.chunks_exact_mut(4) {
|
||||||
|
let (r, g, b, a) = (pixel[0], pixel[1], pixel[2], pixel[3]);
|
||||||
|
pixel[0] = b;
|
||||||
|
pixel[1] = g;
|
||||||
|
pixel[2] = r;
|
||||||
|
pixel[3] = a;
|
||||||
|
}
|
||||||
|
|
||||||
|
use smithay::wayland::shm::with_buffer_contents_mut;
|
||||||
|
use std::time::SystemTime;
|
||||||
|
|
||||||
|
let now = SystemTime::now()
|
||||||
|
.duration_since(SystemTime::UNIX_EPOCH)
|
||||||
|
.unwrap_or_default();
|
||||||
|
let tv_sec = now.as_secs();
|
||||||
|
let tv_nsec = now.subsec_nanos();
|
||||||
|
|
||||||
|
for pf in pending {
|
||||||
|
let (rx, ry, rw, rh) = pf.region;
|
||||||
|
|
||||||
|
// Copy the requested region into the client's SHM buffer
|
||||||
|
let copy_ok = with_buffer_contents_mut(&pf.buffer, |dst_ptr, dst_len, buffer_data| {
|
||||||
|
let dst = unsafe { std::slice::from_raw_parts_mut(dst_ptr, dst_len) };
|
||||||
|
let dst_stride = buffer_data.stride as usize;
|
||||||
|
let src_stride = fb_w as usize * 4;
|
||||||
|
|
||||||
|
for row in 0..rh as usize {
|
||||||
|
let src_y = ry as usize + row;
|
||||||
|
if src_y >= fb_h as usize { break; }
|
||||||
|
let src_off = src_y * src_stride + (rx as usize) * 4;
|
||||||
|
let dst_off = row * dst_stride;
|
||||||
|
let copy_len = (rw as usize * 4).min(dst_stride).min(src_stride - (rx as usize) * 4);
|
||||||
|
if src_off + copy_len <= fb_pixels.len() && dst_off + copy_len <= dst.len() {
|
||||||
|
dst[dst_off..dst_off + copy_len]
|
||||||
|
.copy_from_slice(&fb_pixels[src_off..src_off + copy_len]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if copy_ok.is_ok() {
|
||||||
|
if pf.with_damage {
|
||||||
|
pf.frame.damage(
|
||||||
|
0, 0,
|
||||||
|
rw as u32, rh as u32,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
pf.frame.flags(zwlr_screencopy_frame_v1::Flags::empty());
|
||||||
|
pf.frame.ready(
|
||||||
|
(tv_sec >> 32) as u32,
|
||||||
|
tv_sec as u32,
|
||||||
|
tv_nsec,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
pf.frame.failed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push pixels to portal PipeWire streams for screen sharing
|
||||||
|
if let Some(ref portal) = data.state.portal {
|
||||||
|
portal.push_pixels_to_streams(None, &fb_pixels);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for pf in pending {
|
||||||
|
pf.frame.failed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let Some(ref portal) = data.state.portal {
|
||||||
|
// No screencopy requests, but portal streams may be active.
|
||||||
|
// Only do the expensive readback if there are active portal streams.
|
||||||
|
if !portal.get_active_sessions().is_empty() {
|
||||||
|
let (fb_w, fb_h) = (data.width as i32, data.height as i32);
|
||||||
|
let fb_size = (fb_w as usize) * (fb_h as usize) * 4;
|
||||||
|
let mut fb_pixels = vec![0u8; fb_size];
|
||||||
|
let read_ok = renderer.with_context(|gl| unsafe {
|
||||||
|
gl.ReadPixels(
|
||||||
|
0, 0, fb_w, fb_h,
|
||||||
|
gl::RGBA,
|
||||||
|
gl::UNSIGNED_BYTE,
|
||||||
|
fb_pixels.as_mut_ptr() as *mut _,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
if read_ok.is_ok() {
|
||||||
|
let stride = fb_w as usize * 4;
|
||||||
|
for y in 0..(fb_h as usize / 2) {
|
||||||
|
let top = y * stride;
|
||||||
|
let bot = (fb_h as usize - 1 - y) * stride;
|
||||||
|
for x in 0..stride {
|
||||||
|
fb_pixels.swap(top + x, bot + x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for pixel in fb_pixels.chunks_exact_mut(4) {
|
||||||
|
let (r, g, b, a) = (pixel[0], pixel[1], pixel[2], pixel[3]);
|
||||||
|
pixel[0] = b;
|
||||||
|
pixel[1] = g;
|
||||||
|
pixel[2] = r;
|
||||||
|
pixel[3] = a;
|
||||||
|
}
|
||||||
|
portal.push_pixels_to_streams(None, &fb_pixels);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify Electron/JS when screencopy captures are active
|
||||||
|
if let Some(ref screencopy) = data.state.screencopy_state {
|
||||||
|
let has_pending = screencopy.has_pending_frames();
|
||||||
|
if has_pending {
|
||||||
|
if let Some(ref ipc) = data.state.electron_ipc {
|
||||||
|
let _ = ipc.broadcast_screencopy_event(true, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Unbind the render target so the BO is no longer GL-locked.
|
// Unbind the render target so the BO is no longer GL-locked.
|
||||||
drop(target);
|
drop(target);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ use crate::{
|
||||||
cursor::CursorThemeManager,
|
cursor::CursorThemeManager,
|
||||||
dmabuf::TextureCache,
|
dmabuf::TextureCache,
|
||||||
electron::{ElectronIpc, ElectronProcess},
|
electron::{ElectronIpc, ElectronProcess},
|
||||||
|
handlers::screencopy::ScreencopyManagerState,
|
||||||
syscall::SyscallHandler,
|
syscall::SyscallHandler,
|
||||||
};
|
};
|
||||||
use portal::WoPortal;
|
use portal::WoPortal;
|
||||||
|
|
@ -244,6 +245,8 @@ pub struct WoState {
|
||||||
pub electron_ipc: Option<ElectronIpc>,
|
pub electron_ipc: Option<ElectronIpc>,
|
||||||
pub syscall_handler: Option<SyscallHandler>,
|
pub syscall_handler: Option<SyscallHandler>,
|
||||||
pub portal: Option<std::sync::Arc<WoPortal>>,
|
pub portal: Option<std::sync::Arc<WoPortal>>,
|
||||||
|
/// wlr-screencopy protocol state for frame capture requests.
|
||||||
|
pub screencopy_state: Option<ScreencopyManagerState>,
|
||||||
/// Mapping state for named Electron windows. If false, the window should
|
/// Mapping state for named Electron windows. If false, the window should
|
||||||
/// not be rendered (unmapped/minimized).
|
/// not be rendered (unmapped/minimized).
|
||||||
pub window_mapped: std::collections::HashMap<String, bool>,
|
pub window_mapped: std::collections::HashMap<String, bool>,
|
||||||
|
|
@ -343,6 +346,9 @@ impl WoState {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Initialize wlr-screencopy protocol
|
||||||
|
let screencopy_state = Some(ScreencopyManagerState::new::<Self>(&dh));
|
||||||
|
|
||||||
let data_device_state = DataDeviceState::new::<Self>(&dh);
|
let data_device_state = DataDeviceState::new::<Self>(&dh);
|
||||||
let dmabuf_state = DmabufState::new();
|
let dmabuf_state = DmabufState::new();
|
||||||
|
|
||||||
|
|
@ -439,6 +445,7 @@ impl WoState {
|
||||||
electron_ipc: None,
|
electron_ipc: None,
|
||||||
syscall_handler,
|
syscall_handler,
|
||||||
portal,
|
portal,
|
||||||
|
screencopy_state,
|
||||||
xwayland: None,
|
xwayland: None,
|
||||||
xwm: None,
|
xwm: None,
|
||||||
xwayland_shell_state: None,
|
xwayland_shell_state: None,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue