Skip to content

Commit 441bc43

Browse files
lukemarsdenclaude
andcommitted
fix: Ubuntu desktop input and PipeWire mode improvements
- Fix D-Bus button signature in remotedesktop-session.py: use (ib) instead of (ub) for NotifyPointerButton. GNOME RemoteDesktop API expects signed int for button parameter. - Switch startup-app.sh to use --headless mode for PipeWire capture instead of --devkit. Headless mode creates virtual monitor without display output, allowing ScreenCast to capture via pipewiresrc. - Don't mount /dev/uinput for Ubuntu desktop in wolf_executor.go. Ubuntu uses D-Bus RemoteDesktop API via InputBridge - mounting uinput causes inputtino fallback devices to leak to host X11. - Add debug logging to input.ts for troubleshooting mouse events. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent e9a62ab commit 441bc43

File tree

6 files changed

+49
-15
lines changed

6 files changed

+49
-15
lines changed

api/pkg/external-agent/wolf_executor.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -307,12 +307,21 @@ func (w *WolfExecutor) computeZedImageFromVersion(desktopType DesktopType, wolfI
307307
func (w *WolfExecutor) createDesktopWolfApp(config DesktopWolfAppConfig) *wolf.App {
308308
// GOW_REQUIRED_DEVICES tells the GOW container launcher which device files to pass through.
309309
// We include all possible GPU devices - the glob won't match non-existent devices.
310-
// - /dev/uinput: User-space input device (for virtual keyboard/mouse from streaming client)
311-
// - /dev/input/*: Input devices (event*, mice, mouse*)
312310
// - /dev/dri/*: DRM render nodes (Intel/AMD/software)
313311
// - /dev/nvidia*: NVIDIA GPU devices
314312
// - /dev/kfd: AMD ROCm Kernel Fusion Driver
315-
gpuDevices := "/dev/uinput /dev/input/* /dev/dri/* /dev/nvidia* /dev/kfd"
313+
//
314+
// NOTE: /dev/uinput is ONLY needed for Sway desktop (uses inputtino for input injection).
315+
// Ubuntu/GNOME desktop uses D-Bus RemoteDesktop API via InputBridge - no uinput needed.
316+
// If uinput is mounted from host, inputtino fallback devices leak to host X11 session!
317+
var gpuDevices string
318+
if config.DesktopType == DesktopUbuntu {
319+
// Ubuntu uses D-Bus RemoteDesktop API - no uinput needed
320+
gpuDevices = "/dev/dri/* /dev/nvidia* /dev/kfd"
321+
} else {
322+
// Sway and other desktops use inputtino (uinput) for input
323+
gpuDevices = "/dev/uinput /dev/input/* /dev/dri/* /dev/nvidia* /dev/kfd"
324+
}
316325

317326
// Extract host:port and TLS setting from API URL for Zed WebSocket connection
318327
zedHelixURL, zedHelixTLS := extractHostPortAndTLS(w.helixAPIURL)

frontend/src/components/external-agent/MoonlightStreamViewer.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2859,7 +2859,10 @@ const MoonlightStreamViewer: React.FC<MoonlightStreamViewerProps> = ({
28592859
// Input event handlers
28602860
const handleMouseDown = useCallback((event: React.MouseEvent) => {
28612861
event.preventDefault();
2862-
getInputHandler()?.onMouseDown(event.nativeEvent, getStreamRect());
2862+
const handler = getInputHandler();
2863+
const rect = getStreamRect();
2864+
console.log(`[MoonlightStreamViewer] handleMouseDown: handler=${!!handler}, rect=${rect.width}x${rect.height}`);
2865+
handler?.onMouseDown(event.nativeEvent, rect);
28632866
}, [getStreamRect, getInputHandler]);
28642867

28652868
const handleMouseUp = useCallback((event: React.MouseEvent) => {
@@ -3697,8 +3700,14 @@ const MoonlightStreamViewer: React.FC<MoonlightStreamViewerProps> = ({
36973700
{/* Canvas Element (WebSocket mode only) - centered with proper aspect ratio */}
36983701
<canvas
36993702
ref={canvasRef}
3700-
onMouseDown={handleMouseDown}
3701-
onMouseUp={handleMouseUp}
3703+
onMouseDown={(e) => {
3704+
console.log('[CANVAS] onMouseDown fired, button=', e.button);
3705+
handleMouseDown(e);
3706+
}}
3707+
onMouseUp={(e) => {
3708+
console.log('[CANVAS] onMouseUp fired, button=', e.button);
3709+
handleMouseUp(e);
3710+
}}
37023711
onMouseMove={handleMouseMove}
37033712
onMouseEnter={resetInputState}
37043713
onContextMenu={handleContextMenu}

frontend/src/lib/moonlight-web-ts/stream/input.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,12 +206,16 @@ export class StreamInput {
206206

207207
// -- Mouse
208208
onMouseDown(event: MouseEvent, rect: DOMRect) {
209+
console.log(`[StreamInput] onMouseDown: event.button=${event.button}, mouseMode=${this.config.mouseMode}`)
209210
const button = convertToButton(event)
210211
if (button == null) {
212+
console.warn(`[StreamInput] onMouseDown: convertToButton returned null for event.button=${event.button}`)
211213
return
212214
}
215+
console.log(`[StreamInput] onMouseDown: converted button=${button}`)
213216

214217
if (this.config.mouseMode == "relative" || this.config.mouseMode == "follow") {
218+
console.log(`[StreamInput] onMouseDown: calling sendMouseButton(true, ${button})`)
215219
this.sendMouseButton(true, button)
216220
} else if (this.config.mouseMode == "pointAndDrag") {
217221
this.sendMousePositionClientCoordinates(event.clientX, event.clientY, rect, button)
@@ -391,7 +395,9 @@ export class StreamInput {
391395
}
392396
// Note: button = StreamMouseButton.
393397
sendMouseButton(isDown: boolean, button: number) {
394-
console.log(`[INPUT_DEBUG] sendMouseButton: isDown=${isDown} button=${button} (1=left, 2=middle, 3=right, 4=X1, 5=X2)`);
398+
// If this log appears, the WebSocket patching is NOT working!
399+
// In WebSocket mode, this method should be replaced by WebSocketStream.sendMouseButton
400+
console.error(`[StreamInput] sendMouseButton CALLED DIRECTLY (patching failed?): isDown=${isDown} button=${button}`);
395401

396402
this.buffer.reset()
397403

frontend/src/lib/moonlight-web-ts/stream/websocket-stream.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1321,6 +1321,7 @@ export class WebSocketStream {
13211321

13221322
private sendInputMessage(type: number, payload: Uint8Array) {
13231323
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
1324+
console.warn(`[WebSocketStream] sendInputMessage: WS not ready (ws=${!!this.ws}, state=${this.ws?.readyState}), dropping input type=0x${type.toString(16)}`)
13241325
return
13251326
}
13261327

@@ -1538,6 +1539,7 @@ export class WebSocketStream {
15381539
}
15391540

15401541
sendMouseButton(isDown: boolean, button: number) {
1542+
console.log(`[WebSocketStream] sendMouseButton: isDown=${isDown} button=${button} (1=left, 2=middle, 3=right)`)
15411543
// Format: subType(1) + isDown(1) + button(1)
15421544
this.inputBuffer[0] = 2 // sub-type for button
15431545
this.inputBuffer[1] = isDown ? 1 : 0

wolf/ubuntu-config/remotedesktop-session.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,7 @@ def _handle_input(self, data: dict):
425425
log(f"[INPUT_DEBUG] Button event: evdev_button={button} state={state}")
426426
self.rd_session_proxy.call_sync(
427427
"NotifyPointerButton",
428-
GLib.Variant("(ub)", (button, state)), # Must be unsigned int, not signed
428+
GLib.Variant("(ib)", (button, state)), # button: signed int (i), state: boolean (b)
429429
Gio.DBusCallFlags.NONE,
430430
-1, None
431431
)

wolf/ubuntu-config/startup-app.sh

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -698,13 +698,21 @@ sleep 0.5
698698
fi
699699
) &
700700
701-
echo "[gnome-session] Starting GNOME Shell in devkit mode..."
702-
echo "[gnome-session] Resolution: ${GAMESCOPE_WIDTH:-1920}x${GAMESCOPE_HEIGHT:-1080}"
703-
704-
# Start GNOME Shell in devkit mode with virtual monitor at correct resolution
705-
# --virtual-monitor WxH creates a persistent virtual monitor at the specified size
706-
# gnome-shell creates wayland-0 for client apps
707-
gnome-shell --devkit --virtual-monitor ${GAMESCOPE_WIDTH:-1920}x${GAMESCOPE_HEIGHT:-1080}
701+
# Determine GNOME Shell mode based on video source
702+
# - PipeWire mode: Use --headless (no display output, capture via pipewiresrc)
703+
# - Wayland mode: Use --nested (outputs to Wolf's Wayland display for waylanddisplaysrc)
704+
if [ "\$VIDEO_SOURCE_MODE" = "pipewire" ]; then
705+
echo "[gnome-session] Starting GNOME Shell in HEADLESS mode (PipeWire capture)..."
706+
echo "[gnome-session] Resolution: ${GAMESCOPE_WIDTH:-1920}x${GAMESCOPE_HEIGHT:-1080}@${GAMESCOPE_REFRESH:-60}"
707+
# --headless: No display output (we capture via pipewiresrc ScreenCast)
708+
# --virtual-monitor WxH@R: Creates a virtual monitor at specified size and refresh rate
709+
gnome-shell --headless --virtual-monitor ${GAMESCOPE_WIDTH:-1920}x${GAMESCOPE_HEIGHT:-1080}@${GAMESCOPE_REFRESH:-60}
710+
else
711+
echo "[gnome-session] Starting GNOME Shell in NESTED mode (Wayland capture)..."
712+
echo "[gnome-session] Resolution: ${GAMESCOPE_WIDTH:-1920}x${GAMESCOPE_HEIGHT:-1080}@${GAMESCOPE_REFRESH:-60}"
713+
# --nested: Outputs to parent Wayland display (Wolf's waylanddisplaysrc captures this)
714+
gnome-shell --nested --virtual-monitor ${GAMESCOPE_WIDTH:-1920}x${GAMESCOPE_HEIGHT:-1080}@${GAMESCOPE_REFRESH:-60}
715+
fi
708716
GNOME_SESSION_EOF
709717

710718
chmod +x /tmp/gnome-session.sh

0 commit comments

Comments
 (0)