Skip to content

Commit 9a9ca98

Browse files
lukemarsdenclaude
andcommitted
fix: add Xwayland startup and dynamic DISPLAY detection for GNOME screenshots
- Start Xwayland explicitly on :99 after gnome-shell's wayland-0 is ready - Add findXwaylandDisplay() to detect X11 sockets dynamically - Check displays :0, :1, :2, :99 in both XDG_RUNTIME_DIR and /tmp - Add x11-utils package for xdpyinfo - Better logging for screenshot fallback debugging Note: Xwayland+scrot approach has limitations - only captures X11 windows, not native Wayland apps. May need to switch to PipeWire-based capture via remotedesktop-session.py for proper GNOME screenshots. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 16c6342 commit 9a9ca98

File tree

2 files changed

+64
-8
lines changed

2 files changed

+64
-8
lines changed

Dockerfile.ubuntu-helix

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ RUN apt-get update && apt-get install -y \
289289
fonts-noto-color-emoji fonts-dejavu-core \
290290
imagemagick \
291291
kitty \
292-
scrot xclip \
292+
scrot xclip x11-utils \
293293
grim slurp \
294294
&& rm -rf /var/lib/apt/lists/*
295295

@@ -519,7 +519,24 @@ fi
519519

520520
if [ -x /usr/local/bin/screenshot-server ]; then
521521
gow_log "[start] Starting screenshot server with GNOME environment..."
522-
# Uses org.gnome.Shell.Screenshot D-Bus API
522+
# Start Xwayland explicitly on DISPLAY=:99 for scrot fallback
523+
# GNOME headless may not auto-start Xwayland without X11 app demand
524+
(
525+
sleep 3 # Wait for gnome-shell's wayland-0 socket to be ready
526+
if [ -S "\$XDG_RUNTIME_DIR/wayland-0" ]; then
527+
gow_log "[start] Starting Xwayland on :99 for screenshot support..."
528+
WAYLAND_DISPLAY=wayland-0 Xwayland :99 -auth /tmp/.Xauthority &
529+
sleep 1
530+
if [ -S "/tmp/.X11-unix/X99" ]; then
531+
gow_log "[start] Xwayland started successfully on DISPLAY=:99"
532+
else
533+
gow_log "[start] WARNING: Xwayland may not have started properly"
534+
fi
535+
else
536+
gow_log "[start] WARNING: wayland-0 not found, cannot start Xwayland"
537+
fi
538+
) &
539+
# Uses org.gnome.Shell.Screenshot D-Bus API, falls back to scrot via Xwayland
523540
WAYLAND_DISPLAY=wayland-0 XDG_CURRENT_DESKTOP=GNOME /usr/local/bin/screenshot-server >> /tmp/screenshot-server.log 2>&1 &
524541
fi
525542

api/cmd/screenshot-server/main.go

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -476,19 +476,57 @@ func captureScreenshotGNOME(format string, quality int) ([]byte, string, error)
476476
return pngData, "png", nil
477477
}
478478

479+
// findXwaylandDisplay tries to detect the Xwayland display from socket files
480+
// GNOME/Mutter starts Xwayland on-demand, so we check for X socket files
481+
func findXwaylandDisplay() string {
482+
// Check environment first
483+
if display := os.Getenv("DISPLAY"); display != "" {
484+
return display
485+
}
486+
487+
// Check for X socket files in XDG_RUNTIME_DIR (Xwayland creates these)
488+
xdgRuntimeDir := os.Getenv("XDG_RUNTIME_DIR")
489+
if xdgRuntimeDir == "" {
490+
xdgRuntimeDir = "/run/user/1000"
491+
}
492+
493+
// Display numbers to check - includes :99 which we start explicitly for GNOME headless
494+
displayNumbers := []int{0, 1, 2, 99}
495+
496+
// Look for X socket files (format: X{N} where N is display number)
497+
for _, i := range displayNumbers {
498+
socketPath := filepath.Join(xdgRuntimeDir, fmt.Sprintf(".X11-unix/X%d", i))
499+
if _, err := os.Stat(socketPath); err == nil {
500+
display := fmt.Sprintf(":%d", i)
501+
log.Printf("[GNOME/X11] Found Xwayland socket at %s, using DISPLAY=%s", socketPath, display)
502+
return display
503+
}
504+
}
505+
506+
// Also check /tmp/.X11-unix (traditional location)
507+
for _, i := range displayNumbers {
508+
socketPath := fmt.Sprintf("/tmp/.X11-unix/X%d", i)
509+
if _, err := os.Stat(socketPath); err == nil {
510+
display := fmt.Sprintf(":%d", i)
511+
log.Printf("[GNOME/X11] Found X socket at %s, using DISPLAY=%s", socketPath, display)
512+
return display
513+
}
514+
}
515+
516+
log.Printf("[GNOME/X11] No X11 display socket found, defaulting to :99")
517+
return ":99"
518+
}
519+
479520
// captureScreenshotGNOMEScreenCast captures a screenshot using scrot via X11.
480521
// DISABLED: The PipeWire-based gnome-screenshot.py conflicts with Wolf's video capture.
481522
// Reading from the same PipeWire node as Wolf causes stream interference and crashes.
482523
// Fall back to scrot which uses X11/Xwayland instead.
483524
func captureScreenshotGNOMEScreenCast(format string, quality int) ([]byte, string, error) {
484525
log.Printf("[GNOME/X11] Screenshot D-Bus blocked, falling back to scrot via Xwayland...")
485526

486-
// Use scrot via X11 - this doesn't interfere with Wolf's PipeWire capture
487-
// GNOME runs on Xwayland at DISPLAY=:0 (or :9 in some configs)
488-
display := os.Getenv("DISPLAY")
489-
if display == "" {
490-
display = ":0" // Default for GNOME's Xwayland
491-
}
527+
// Detect Xwayland display from socket files
528+
display := findXwaylandDisplay()
529+
log.Printf("[GNOME/X11] Using DISPLAY=%s for scrot", display)
492530

493531
// Create temporary file for screenshot
494532
tmpDir := os.TempDir()
@@ -510,6 +548,7 @@ func captureScreenshotGNOMEScreenCast(format string, quality int) ([]byte, strin
510548

511549
output, err := cmd.CombinedOutput()
512550
if err != nil {
551+
log.Printf("[GNOME/X11] scrot failed with DISPLAY=%s: %v, output: %s", display, err, string(output))
513552
return nil, "", fmt.Errorf("scrot failed: %v, output: %s", err, string(output))
514553
}
515554

0 commit comments

Comments
 (0)