wo/electron/src/wo-input-receiver.ts
Stephen Hellings 33359b814e Initial commit
2026-03-08 23:04:02 -04:00

199 lines
6.1 KiB
TypeScript

/**
* Wo Composer Input Event Receiver for Electron
*
* This module provides utilities for Electron apps to receive input events
* from the Wo compositor via the IPC protocol.
*
* Usage in your Electron renderer process:
*
* const { WoInputReceiver } = require('./wo-input-receiver');
* const receiver = new WoInputReceiver();
*
* receiver.on('mouseMove', (x, y) => {
* console.log(`Mouse moved to: ${x}, ${y}`);
* });
*
* receiver.on('mouseButton', (button, pressed, time) => {
* console.log(`Button ${button} ${pressed ? 'pressed' : 'released'}`);
* });
*
* receiver.on('keyboard', (key, pressed, time) => {
* console.log(`Key ${key} ${pressed ? 'pressed' : 'released'}`);
* });
*
* receiver.on('scroll', (vertical, horizontal, time) => {
* console.log(`Scrolled V:${vertical} H:${horizontal}`);
* });
*
* receiver.startListening();
*
* Or in the client app:
*
* // Access via window.__WO_INPUT_RECEIVER__
* window.__WO_INPUT_RECEIVER__.on('mouseMove', (x, y) => {
* // Handle mouse move
* });
*/
import { EventEmitter } from 'events';
import net from 'net';
const MAGIC_MOUSE_MOVE = 0x574F4D4D; // "WOMM"
const MAGIC_MOUSE_BUTTON = 0x574F4D42; // "WOMB"
const MAGIC_KEYBOARD = 0x574F4B42; // "WOKB"
const MAGIC_SCROLL = 0x574F5343; // "WOSC"
/**
* Receives input events from the Wo compositor via IPC socket.
*
* Events:
* - mouseMove(x, y) - Pointer moved
* - mouseButton(button, pressed, time) - Mouse button event
* - keyboard(key, pressed, time) - Keyboard event
* - scroll(vertical, horizontal, time) - Scroll wheel event
* - connected() - Connected to compositor
* - disconnected(reason) - Disconnected from compositor
* - error(error) - Error occurred
*/
class WoInputReceiver extends EventEmitter {
private socket: net.Socket | null;
private ipcSocket: string;
private windowName: string;
private buffer: Buffer;
private connected: boolean;
constructor(ipcSocket?: string, windowName?: string) {
super();
this.socket = null;
this.ipcSocket = ipcSocket || process.env.WO_IPC_SOCKET!;
this.windowName = windowName || process.env.WO_WINDOW_NAME || 'unknown';
this.buffer = Buffer.alloc(0);
this.connected = false;
}
/**
* Start listening for input events from the Wo compositor.
* This is typically called automatically when connecting to the compositor.
*/
startListening() {
if (this.socket) {
this.socket.removeAllListeners();
this.socket.destroy();
}
// Create a new socket connection just for receiving input events
// This reuses the DMABUF connection through parameter passing
this.socket = net.createConnection(this.ipcSocket);
this.socket.on('connect', () => {
this.connected = true;
console.log('[Wo] Input event receiver connected');
this.emit('connected');
});
this.socket.on('data', (chunk: Buffer) => {
this.buffer = Buffer.concat([this.buffer, chunk]);
this.processEventBuffer();
});
this.socket.on('error', (error: Error) => {
console.error('[Wo] Input receiver error:', error);
this.emit('error', error);
});
this.socket.on('close', () => {
this.connected = false;
console.log('[Wo] Input event receiver disconnected');
this.emit('disconnected');
});
}
/**
* Process all complete events in the buffer.
* Events are variable-length, so we parse them as they arrive.
*/
processEventBuffer() {
while (this.buffer.length >= 4) {
const magic = this.buffer.readUInt32LE(0);
let eventSize = 0;
let parsed = false;
// Mouse move: 4 (magic) + 8 (x) + 8 (y) = 20 bytes
if (magic === MAGIC_MOUSE_MOVE) { // "WOMM"
if (this.buffer.length >= 20) {
const x = this.buffer.readDoubleLE(4);
const y = this.buffer.readDoubleLE(12);
this.emit('mouseMove', x, y);
this.buffer = this.buffer.slice(20);
parsed = true;
}
}
// Mouse button: 4 (magic) + 4 (button) + 4 (pressed) + 4 (time) = 16 bytes
else if (magic === MAGIC_MOUSE_BUTTON) { // "WOMB"
if (this.buffer.length >= 16) {
const button = this.buffer.readUInt32LE(4);
const pressed = this.buffer.readUInt32LE(8) !== 0;
const time = this.buffer.readUInt32LE(12);
this.emit('mouseButton', button, pressed, time);
this.buffer = this.buffer.slice(16);
parsed = true;
}
}
// Keyboard: 4 (magic) + 4 (key) + 4 (pressed) + 4 (time) = 16 bytes
else if (magic === MAGIC_KEYBOARD) { // "WOKB"
if (this.buffer.length >= 16) {
const key = this.buffer.readUInt32LE(4);
const pressed = this.buffer.readUInt32LE(8) !== 0;
const time = this.buffer.readUInt32LE(12);
this.emit('keyboard', key, pressed, time);
this.buffer = this.buffer.slice(16);
parsed = true;
}
}
// Scroll: 4 (magic) + 4 (vertical) + 4 (horizontal) + 4 (time) = 16 bytes
else if (magic === MAGIC_SCROLL) { // "WOSC"
if (this.buffer.length >= 16) {
const vertical = this.buffer.readInt32LE(4);
const horizontal = this.buffer.readInt32LE(8);
const time = this.buffer.readUInt32LE(12);
this.emit('scroll', vertical, horizontal, time);
this.buffer = this.buffer.slice(16);
parsed = true;
}
}
else {
// Unknown magic, skip this byte and try again
console.warn('[Wo] Unknown message magic: 0x' + magic.toString(16));
this.buffer = this.buffer.slice(1);
parsed = true;
}
// If we couldn't parse a complete event, wait for more data
if (!parsed) {
break;
}
}
}
/**
* Stop listening for input events.
*/
stop() {
if (this.socket) {
this.socket.destroy();
this.socket = null;
}
}
/**
* Destroy the receiver and clean up resources.
*/
destroy() {
this.stop();
this.removeAllListeners();
}
}
export { WoInputReceiver };