Skip to content

[FEAT] Add clipboard support for xwayland to support pixelflux #136

@visbits

Description

@visbits

Is this a new feature request?

  • I have searched the existing issues

Wanted change

Summary

In current Wayland/Pixelflux paths using KDE X11 session (startplasma-x11 under Xwayland), clipboard domains can be split between:

  • Wayland clipboard (used by Selkies wl-copy / wl-paste)
  • X11 clipboard (used by Plasma X11 apps via X selections / xclip)

This can cause clipboard sync to work in one direction/path but not consistently across both app domains without a manual bridge.


Environment

  • Repo: linuxserver/docker-baseimage-selkies
  • Distro path tested: ubuntunoble
  • Wayland mode: PIXELFLUX_WAYLAND=true
  • Session observed: startplasma-x11, kwin_x11, Xwayland :1
  • Wayland socket observed: wayland-1
  • Selkies clipboard monitor active

Problem

In this hybrid mode, users can observe:

  • Browser/Selkies clipboard content reaches the Wayland domain
  • X11 desktop apps do not always receive it automatically
  • Reverse direction can also be inconsistent

This is understandable due to separate domains, but currently requires hand-rolled custom scripts.


Why this matters

  • Wayland/Pixelflux support is newer and still evolving
  • Clipboard UX is a core expectation for remote desktop
  • Many “clipboard broken” reports are likely domain-bridge gaps in hybrid mode

Proposed solution

Add an optional custom service in baseimage:

  • Service name example: custom-svc-clipboard-bridge
  • Default: disabled (opt-in)
  • Env toggle:
    • SELKIES_CLIPBOARD_BRIDGE_ENABLED=true|false (default false)

Bridge behavior

  • Sync Wayland → X11 (wl-paste -> xclip)
  • Sync X11 → Wayland (xclip -o -> wl-copy)
  • Include loop suppression (hash/last-seen dedupe)
  • Keep CPU/memory conservative (watch or low-frequency poll)
  • Keep logs bounded/quiet

Why custom service

  • Fits existing s6 service architecture cleanly
  • No heavy new dependencies (wl-clipboard and xclip already present)
  • Easy to disable
  • Deterministic startup/runtime behavior

Acceptance criteria

  1. With SELKIES_CLIPBOARD_BRIDGE_ENABLED=false

    • behavior remains unchanged
  2. With SELKIES_CLIPBOARD_BRIDGE_ENABLED=true

    • Browser/Selkies clipboard becomes available in X11 desktop apps
    • X11 app clipboard becomes visible to Wayland-side clipboard path (and outbound Selkies if enabled)
  3. No feedback loop

    • no sustained high CPU from bridge
    • no unbounded memory growth caused by bridge
  4. Works on hybrid path

    • Wayland backend + Plasma X11/Xwayland session

Notes from local validation

  • Hybrid process model confirmed (startplasma-x11, kwin_x11, Xwayland :1) with wayland-1 socket present
  • Clipboard domains are separate without explicit bridging
  • Local custom service bridge resolves functional sync gap

Potential follow-up (optional)

  • Direction controls:
    • SELKIES_CLIPBOARD_BRIDGE_DIRECTION=both|wayland-to-x11|x11-to-wayland
  • Complementary to Selkies in/out clipboard controls

Reason for change

Clipboard currently does not work with pixelflux.

Proposed code change

`#!/usr/bin/with-contenv bash
set -eu

USER_NAME="abc"
BRIDGE_ENABLED="${SELKIES_CLIPBOARD_BRIDGE_ENABLED:-false}"

if [ "${BRIDGE_ENABLED}" != "true" ]; then
echo "[clipboard-bridge] disabled (SELKIES_CLIPBOARD_BRIDGE_ENABLED=${BRIDGE_ENABLED})" >&2
exec tail -f /dev/null
fi

DISPLAY="${DISPLAY:-:1}"
XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/config/.XDG}"
WAYLAND_DISPLAY="${WAYLAND_DISPLAY:-wayland-1}"
LOG_DIR="/config/.streamology/tmp"
LOG_FILE="${LOG_DIR}/clipboard-bridge.log"

mkdir -p "${LOG_DIR}"
chown "${USER_NAME}:${USER_NAME}" "${LOG_DIR}" || true

wait_for_endpoints() {
local tries=0
while [ ! -S "${XDG_RUNTIME_DIR}/${WAYLAND_DISPLAY}" ] || [ ! -S "/tmp/.X11-unix/X${DISPLAY#:}" ]; do
tries=$((tries + 1))
if [ $((tries % 20)) -eq 0 ]; then
echo "[clipboard-bridge] waiting for ${XDG_RUNTIME_DIR}/${WAYLAND_DISPLAY} and ${DISPLAY}" >&2
fi
sleep 1
done
}

clipboard_hash() {
printf '%s' "$1" | sha256sum | awk '{print $1}'
}

read_wayland_clipboard() {
s6-setuidgid "${USER_NAME}" env DISPLAY="${DISPLAY}" XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR}" WAYLAND_DISPLAY="${WAYLAND_DISPLAY}"
wl-paste --no-newline 2>/dev/null || true
}

read_x11_clipboard() {
s6-setuidgid "${USER_NAME}" env DISPLAY="${DISPLAY}" XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR}" WAYLAND_DISPLAY="${WAYLAND_DISPLAY}"
xclip -selection clipboard -o 2>/dev/null || true
}

write_wayland_clipboard() {
printf '%s' "$1" | s6-setuidgid "${USER_NAME}" env DISPLAY="${DISPLAY}" XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR}" WAYLAND_DISPLAY="${WAYLAND_DISPLAY}"
wl-copy >/dev/null 2>&1 || true
}

write_x11_clipboard() {
printf '%s' "$1" | s6-setuidgid "${USER_NAME}" env DISPLAY="${DISPLAY}" XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR}" WAYLAND_DISPLAY="${WAYLAND_DISPLAY}"
xclip -selection clipboard -in >/dev/null 2>&1 || true
}

echo "[clipboard-bridge] starting bidirectional bridge (Wayland <-> X11)" >&2

last_wayland_seen=""
last_x11_seen=""
last_from_wayland=""
last_from_x11=""

while true; do
wait_for_endpoints

wayland_text="$(read_wayland_clipboard)"
wayland_hash="$(clipboard_hash "${wayland_text}")"
if [ "${wayland_hash}" != "${last_wayland_seen}" ]; then
last_wayland_seen="${wayland_hash}"
if [ "${wayland_hash}" != "${last_from_x11}" ]; then
write_x11_clipboard "${wayland_text}"
last_from_wayland="${wayland_hash}"
last_x11_seen="${wayland_hash}"
echo "[clipboard-bridge] synced Wayland -> X11" >>"${LOG_FILE}" 2>&1 || true
fi
fi

x11_text="$(read_x11_clipboard)"
x11_hash="$(clipboard_hash "${x11_text}")"
if [ "${x11_hash}" != "${last_x11_seen}" ]; then
last_x11_seen="${x11_hash}"
if [ "${x11_hash}" != "${last_from_wayland}" ]; then
write_wayland_clipboard "${x11_text}"
last_from_x11="${x11_hash}"
last_wayland_seen="${x11_hash}"
echo "[clipboard-bridge] synced X11 -> Wayland" >>"${LOG_FILE}" 2>&1 || true
fi
fi

sleep 0.35
done`

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    Status

    Issues

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions