mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-14 19:27:09 -04:00
Add Android clipboard text paste into Steam sessions
This commit is contained in:
1 parent
3404bbc128
commit
eda5ece382
11 files changed
+309
-3
No files matched your search
@@ -157,7 +157,7 @@ jobs:
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: app/src/main/assets/linuxfs
|
||||
key: preloads-${{ env.PRELOAD_CACHE_VERSION }}-${{ hashFiles('app/src/main/cpp/fakeinput_steam.cpp', 'tools/linuxfs/preload/*.c', 'tools/proot/fastpath/*.c', 'tools/linuxfs/overlay/usr/local/bin/*', 'tools/linuxfs/overlay/usr/bin/**', 'tools/linuxfs/desktop/*', 'tools/gamescope/release.env', 'tools/wlroots/release.env', 'tools/mangoapp/*') }}
|
||||
key: preloads-${{ env.PRELOAD_CACHE_VERSION }}-${{ hashFiles('app/src/main/cpp/fakeinput_steam.cpp', 'tools/linuxfs/preload/*.c', 'tools/linuxfs/clipboard/*.c', 'tools/proot/fastpath/*.c', 'tools/linuxfs/overlay/usr/local/bin/*', 'tools/linuxfs/overlay/usr/bin/**', 'tools/linuxfs/desktop/*', 'tools/gamescope/release.env', 'tools/wlroots/release.env', 'tools/mangoapp/*') }}
|
||||
|
||||
- name: Build libfakeinput.so and libblsession.so (glibc aarch64)
|
||||
if: steps.preloads.outputs.cache-hit != 'true'
|
||||
@@ -179,6 +179,9 @@ jobs:
|
||||
aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -pthread \
|
||||
-o "$D/libblfastpath.so" tools/proot/fastpath/fastpath.c -ldl
|
||||
aarch64-linux-gnu-strip --strip-unneeded "$D/libblfastpath.so"
|
||||
mkdir -p "$D/usr/local/bin"
|
||||
aarch64-linux-gnu-gcc -O2 -Wall -Wextra -o "$D/usr/local/bin/droiddeck-clipboard" tools/linuxfs/clipboard/clipboard.c -ldl
|
||||
aarch64-linux-gnu-strip --strip-unneeded "$D/usr/local/bin/droiddeck-clipboard"
|
||||
for script in tools/linuxfs/overlay/usr/local/bin/bannerlator-*; do
|
||||
install -Dm644 "$script" "$D/usr/local/bin/$(basename "$script")"
|
||||
done
|
||||
|
||||
@@ -41,6 +41,7 @@ import com.droiddeck.launcher.gpu.Lossless
|
||||
import com.droiddeck.launcher.gpu.TurnipDriver
|
||||
import com.droiddeck.launcher.input.EvdevKeys
|
||||
import com.droiddeck.launcher.input.KeyboardHost
|
||||
import com.droiddeck.launcher.input.SessionClipboard
|
||||
import com.droiddeck.launcher.input.OnScreenControls
|
||||
import com.droiddeck.launcher.input.PadBridge
|
||||
import com.droiddeck.launcher.input.PointerGestures
|
||||
@@ -125,6 +126,11 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
}
|
||||
private var onScreenControls: OnScreenControls? = null
|
||||
private var keyboard: KeyboardHost? = null
|
||||
private val sessionClipboard by lazy {
|
||||
SessionClipboard(this) {
|
||||
window.decorView.hasWindowFocus() || secondScreenPresentation?.window?.decorView?.hasWindowFocus() == true
|
||||
}
|
||||
}
|
||||
private var watching = true
|
||||
private lateinit var touchpad: TouchpadGestures
|
||||
private var touchMode by mutableStateOf(SessionPrefs.TOUCH_AUTO)
|
||||
@@ -1657,12 +1663,14 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
onScreenControls?.reload()
|
||||
updateOnScreenControls()
|
||||
resumed = true
|
||||
sessionClipboard.start()
|
||||
updatePadMotion()
|
||||
readPrefs()
|
||||
if (CompositorHost.isStarted) applyFrameGen()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
sessionClipboard.stop()
|
||||
// Do not carry transient session UI across an app/display transition. In particular, the
|
||||
// drawer's dim layer can otherwise remain over Steam when this activity returns.
|
||||
drawerOpen = false
|
||||
@@ -1687,7 +1695,15 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
|
||||
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||
super.onWindowFocusChanged(hasFocus)
|
||||
if (hasFocus) goFullscreen()
|
||||
if (hasFocus) {
|
||||
goFullscreen()
|
||||
refreshClipboard()
|
||||
}
|
||||
}
|
||||
|
||||
/** The keyboard/trackpad presentation can own focus instead of the main display. */
|
||||
fun refreshClipboard() {
|
||||
if (resumed) sessionClipboard.refresh()
|
||||
}
|
||||
|
||||
private fun goFullscreen() {
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.droiddeck.launcher.input
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.system.Os
|
||||
import android.util.Log
|
||||
import com.droiddeck.launcher.wayland.WaylandCompositor
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
/** Text selections shared by Android and the guest while the session is visible. */
|
||||
class SessionClipboard(context: Context, private val hasFocus: () -> Boolean) {
|
||||
private val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private val hostFile = File(context.filesDir, "session/android-clipboard")
|
||||
private var active = false
|
||||
private val androidListener = ClipboardManager.OnPrimaryClipChangedListener { refresh() }
|
||||
private val guestListener = object : WaylandCompositor.ClipboardListener {
|
||||
override fun onGuestClipboardText(text: String) {
|
||||
// Native callbacks arrive on the compositor thread. Do not publish a delayed callback
|
||||
// after this activity has paused or been replaced.
|
||||
handler.post {
|
||||
if (active && hasFocus() && WaylandCompositor.isClipboardListener(this)) {
|
||||
val current = clipboard.primaryClip?.takeIf { it.itemCount > 0 }?.getItemAt(0)?.text
|
||||
if (current?.toString() != text) {
|
||||
clipboard.setPrimaryClip(ClipData.newPlainText("DroidDeck", text))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun start() {
|
||||
if (active) return
|
||||
active = true
|
||||
clipboard.addPrimaryClipChangedListener(androidListener)
|
||||
WaylandCompositor.setClipboardListener(guestListener)
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
if (!active) return
|
||||
active = false
|
||||
clipboard.removePrimaryClipChangedListener(androidListener)
|
||||
WaylandCompositor.clearClipboardListener(guestListener)
|
||||
handler.removeCallbacksAndMessages(null)
|
||||
}
|
||||
|
||||
/** Android 10+ permits clipboard reads only with window focus. Resume can precede focus. */
|
||||
fun refresh() {
|
||||
if (!active || !hasFocus()) return
|
||||
val clip = clipboard.primaryClip
|
||||
// Only share literal text, never resolve content URIs or load copied files.
|
||||
if (clip == null) {
|
||||
publish(null)
|
||||
} else if (clip.itemCount > 0) {
|
||||
publish(clip.getItemAt(0).text?.toString())
|
||||
}
|
||||
}
|
||||
|
||||
private fun publish(text: String?) {
|
||||
WaylandCompositor.setClipboardText(text)
|
||||
// Gamescope needs the same selection in its own Xwayland. Rename complete UTF-8 text
|
||||
// into place so its helper never sees half a launch command. Kept in private app data.
|
||||
val pending = File(hostFile.parentFile, "android-clipboard.pending")
|
||||
try {
|
||||
hostFile.parentFile?.mkdirs()
|
||||
pending.writeText(text.orEmpty(), Charsets.UTF_8)
|
||||
Os.rename(pending.path, hostFile.path)
|
||||
} catch (e: IOException) {
|
||||
Log.w("SessionClipboard", "Could not share text with Xwayland", e)
|
||||
} catch (e: android.system.ErrnoException) {
|
||||
Log.w("SessionClipboard", "Could not publish text for Xwayland", e)
|
||||
} finally {
|
||||
pending.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ object SessionFiles {
|
||||
"libfakeinput.so" to "usr/local/lib/libfakeinput.so",
|
||||
"libblfastpath.so" to "usr/local/lib/libblfastpath.so",
|
||||
"usr/local/bin/bannerlator-session" to "usr/local/bin/bannerlator-session",
|
||||
"usr/local/bin/droiddeck-clipboard" to "usr/local/bin/droiddeck-clipboard",
|
||||
"usr/local/bin/bannerlator-steam-compat" to "usr/local/bin/bannerlator-steam-compat",
|
||||
"usr/local/bin/bannerlator-steam-install" to "usr/local/bin/bannerlator-steam-install",
|
||||
"usr/local/bin/bannerlator-steam-library" to "usr/local/bin/bannerlator-steam-library",
|
||||
|
||||
@@ -514,6 +514,7 @@ class SessionService : Service() {
|
||||
guest.add("XDG_RUNTIME_DIR=" + LinuxRuntime.GUEST_RUNTIME_DIR)
|
||||
guest.add("XDG_SESSION_TYPE=wayland")
|
||||
guest.add("WAYLAND_DISPLAY=wayland-0")
|
||||
guest.add("BL_ANDROID_CLIPBOARD=" + File(filesDir, "session/android-clipboard").path)
|
||||
guest.add("GAMESCOPE_FORCE_GENERAL_QUEUE=1")
|
||||
// Steam's CEF needs GL and the rootfs ships no native GL driver: route it through Zink.
|
||||
guest.add("MESA_LOADER_DRIVER_OVERRIDE=zink")
|
||||
|
||||
@@ -100,7 +100,10 @@ class SecondScreenPresentation(
|
||||
|
||||
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||
super.onWindowFocusChanged(hasFocus)
|
||||
if (hasFocus) applyImmersiveMode()
|
||||
if (hasFocus) {
|
||||
applyImmersiveMode()
|
||||
sessionActivity.refreshClipboard()
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderMode() {
|
||||
|
||||
@@ -132,6 +132,13 @@ public final class WaylandCompositor {
|
||||
|
||||
public static void setClipboardListener(ClipboardListener l) { clipboardListener = l; }
|
||||
|
||||
public static boolean isClipboardListener(ClipboardListener l) { return clipboardListener == l; }
|
||||
|
||||
/** A replaced activity must not unhook the clipboard listener of its successor. */
|
||||
public static void clearClipboardListener(ClipboardListener l) {
|
||||
if (clipboardListener == l) clipboardListener = null;
|
||||
}
|
||||
|
||||
/** Invoked from native (banner_on_clipboard_text) with UTF-8 bytes. */
|
||||
@SuppressWarnings("unused")
|
||||
static void onClipboardText(byte[] utf8) {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Android clipboard text
|
||||
|
||||
Copy text in an Android app, return to DroidDeck, and paste into Steam with Ctrl+V.
|
||||
The session's PC keyboard supports this through its sticky Ctrl key and V.
|
||||
|
||||
Android clipboard reads follow window focus, including the secondary-display presentation.
|
||||
The existing Wayland text selection handles native guest clients. Gamescope does not relay
|
||||
that selection to its inner Xwayland, so a session helper publishes Android text there.
|
||||
It waits for file and X11 events, and exits when its parent session exits.
|
||||
|
||||
The Gamescope bridge currently supports Android-to-guest plain text up to 64 KiB.
|
||||
It does not transfer images/files or export Steam's copied text back to Android.
|
||||
Oversized text clears an earlier Android selection rather than pasting a truncated command.
|
||||
Clipboard text stays in private app data and is never included in session logs.
|
||||
|
||||
## Thor validation, 2026-10-02
|
||||
|
||||
AYN Thor, serial `d234a848`, Android 13, release APK installed in place.
|
||||
Built with `tools/build_local.sh`; shell syntax, workflow YAML, and diff checks passed.
|
||||
Installed APK SHA-256 matched the local artifact:
|
||||
`624782af4319af538a9ae5dba6e012ebb51c7fd43a3955c4f568d214f9c32c07`.
|
||||
|
||||
- The supplied launch-options string pasted through Ctrl+V into Steam's actual Launch Options field exactly. The field was restored to its original empty value.
|
||||
- Unicode (`café 日本語 😀`, quotes, dollar sign, and backslash) also pasted exactly into Steam. The field was restored again.
|
||||
- Final-build X11 selection checks passed for launch options, multiline text, Unicode, 8 KiB, clearing, and replacing the clipboard across Android app switches.
|
||||
- Copying on Thor's second display transferred the text while Steam remained visible on the first display.
|
||||
- Copying the same Android text after a guest owned the X11 selection restored the Android selection.
|
||||
- A 65,537-byte clipboard was rejected without truncation; a subsequent normal clipboard transferred correctly.
|
||||
- The helper exited during APK replacement. The temporary Android test app, guest probe, and local ADB forward were removed.
|
||||
|
||||
Local screenshots, field values, build log, and check results: `/tmp/droiddeck-clipboard-evidence`.
|
||||
@@ -120,6 +120,9 @@ docker run --rm --platform linux/amd64 \
|
||||
aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -pthread \
|
||||
-o "$d/libblfastpath.so" tools/proot/fastpath/fastpath.c -ldl
|
||||
aarch64-linux-gnu-strip --strip-unneeded "$d/libblfastpath.so"
|
||||
mkdir -p "$d/usr/local/bin"
|
||||
aarch64-linux-gnu-gcc -O2 -Wall -Wextra -o "$d/usr/local/bin/droiddeck-clipboard" tools/linuxfs/clipboard/clipboard.c -ldl
|
||||
aarch64-linux-gnu-strip --strip-unneeded "$d/usr/local/bin/droiddeck-clipboard"
|
||||
for script in tools/linuxfs/overlay/usr/local/bin/bannerlator-*; do
|
||||
install -Dm644 "$script" "$d/usr/local/bin/$(basename "$script")"
|
||||
done
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
/* Android text -> Gamescope's Xwayland selection. Gamescope's Wayland backend does not
|
||||
* relay the outer compositor's clipboard. libX11 is loaded from the guest at runtime;
|
||||
* the small stable Xlib ABI below keeps this helper independent of cross X11 headers.
|
||||
* The private host file is atomically replaced, never put in session logs. */
|
||||
#define _GNU_SOURCE
|
||||
#include <sys/inotify.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/prctl.h>
|
||||
#include <signal.h>
|
||||
#include <poll.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <dlfcn.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <limits.h>
|
||||
|
||||
typedef struct _XDisplay Display;
|
||||
typedef unsigned long Window, Atom, Time;
|
||||
typedef struct {
|
||||
int type; unsigned long serial; int send_event; Display *display;
|
||||
Window owner, requestor; Atom selection, target, property; Time time;
|
||||
} SelectionRequest;
|
||||
typedef struct {
|
||||
int type; unsigned long serial; int send_event; Display *display;
|
||||
Window requestor; Atom selection, target, property; Time time;
|
||||
} SelectionNotify;
|
||||
typedef union { int type; SelectionRequest request; SelectionNotify selection; long pad[24]; } Event;
|
||||
|
||||
#define API(ret, name, args) static ret (*name) args
|
||||
API(Display *, XOpenDisplay, (const char *));
|
||||
API(Window, XDefaultRootWindow, (Display *));
|
||||
API(Window, XCreateSimpleWindow, (Display *, Window, int, int, unsigned, unsigned, unsigned, unsigned long, unsigned long));
|
||||
API(Atom, XInternAtom, (Display *, const char *, int));
|
||||
API(int, XSetSelectionOwner, (Display *, Atom, Window, Time));
|
||||
API(Window, XGetSelectionOwner, (Display *, Atom));
|
||||
API(int, XChangeProperty, (Display *, Window, Atom, Atom, int, int, const unsigned char *, int));
|
||||
API(int, XSendEvent, (Display *, Window, int, long, Event *));
|
||||
API(int, XPending, (Display *));
|
||||
API(int, XNextEvent, (Display *, Event *));
|
||||
API(int, XFlush, (Display *));
|
||||
API(int, XConnectionNumber, (Display *));
|
||||
API(int, XCloseDisplay, (Display *));
|
||||
API(void *, XSetErrorHandler, (int (*)(Display *, void *)));
|
||||
|
||||
/* Stay below X11's normal maximum property request. Never paste a truncated command. */
|
||||
#define MAX_TEXT (64 * 1024)
|
||||
static int ignore_x_error(Display *d, void *event) { (void)d; (void)event; return 0; }
|
||||
|
||||
/* The host renames complete files into place. The inode distinguishes copying the same
|
||||
* text again after a guest program has taken ownership of the clipboard. */
|
||||
static int refresh(Display *d, Window w, Atom clipboard, const char *path,
|
||||
char **text, size_t *len, ino_t *inode) {
|
||||
int fd = open(path, O_RDONLY | O_CLOEXEC | O_NOFOLLOW);
|
||||
if (fd < 0) return 0;
|
||||
struct stat st;
|
||||
if (fstat(fd, &st) || !S_ISREG(st.st_mode) || st.st_ino == *inode) { close(fd); return 0; }
|
||||
*inode = st.st_ino;
|
||||
if (st.st_size < 0 || st.st_size > MAX_TEXT) {
|
||||
fprintf(stderr, "== clipboard: text exceeds %d bytes, not pasted\n", MAX_TEXT);
|
||||
if (XGetSelectionOwner(d, clipboard) == w) XSetSelectionOwner(d, clipboard, 0, 0);
|
||||
XFlush(d);
|
||||
free(*text); *text = NULL; *len = 0;
|
||||
close(fd); return 0;
|
||||
}
|
||||
char *copy = malloc((size_t)st.st_size + 1);
|
||||
if (!copy) { close(fd); return 0; }
|
||||
size_t got = 0;
|
||||
while (got < (size_t)st.st_size) {
|
||||
ssize_t n = read(fd, copy + got, (size_t)st.st_size - got);
|
||||
if (n > 0) got += n;
|
||||
else if (n < 0 && errno == EINTR) continue;
|
||||
else break;
|
||||
}
|
||||
close(fd);
|
||||
if (got != (size_t)st.st_size) { free(copy); return 0; }
|
||||
copy[got] = 0;
|
||||
free(*text); *text = copy; *len = got;
|
||||
if (got) XSetSelectionOwner(d, clipboard, w, 0);
|
||||
else if (XGetSelectionOwner(d, clipboard) == w) XSetSelectionOwner(d, clipboard, 0, 0);
|
||||
XFlush(d);
|
||||
fprintf(stderr, "== clipboard: Android text available in Xwayland (%zu bytes)\n", got);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc != 2 || !getenv("DISPLAY")) return 1;
|
||||
pid_t parent_pid = getppid();
|
||||
if (parent_pid == 1 || prctl(PR_SET_PDEATHSIG, SIGTERM) || getppid() != parent_pid) return 1;
|
||||
void *lib = dlopen("libX11.so.6", RTLD_NOW | RTLD_LOCAL);
|
||||
if (!lib) { fprintf(stderr, "== clipboard: libX11 unavailable\n"); return 1; }
|
||||
#define LOAD(name) do { *(void **)(&name) = dlsym(lib, #name); if (!name) return 1; } while (0)
|
||||
LOAD(XOpenDisplay); LOAD(XDefaultRootWindow); LOAD(XCreateSimpleWindow); LOAD(XInternAtom);
|
||||
LOAD(XSetSelectionOwner); LOAD(XGetSelectionOwner); LOAD(XChangeProperty); LOAD(XSendEvent);
|
||||
LOAD(XPending); LOAD(XNextEvent); LOAD(XFlush); LOAD(XConnectionNumber); LOAD(XCloseDisplay);
|
||||
LOAD(XSetErrorHandler);
|
||||
Display *d = XOpenDisplay(NULL);
|
||||
if (!d) { fprintf(stderr, "== clipboard: cannot open Xwayland\n"); return 1; }
|
||||
XSetErrorHandler(ignore_x_error); /* A paste requester can close its window at any time. */
|
||||
Window w = XCreateSimpleWindow(d, XDefaultRootWindow(d), 0, 0, 1, 1, 0, 0, 0);
|
||||
Atom clipboard = XInternAtom(d, "CLIPBOARD", 0);
|
||||
Atom targets = XInternAtom(d, "TARGETS", 0), utf8 = XInternAtom(d, "UTF8_STRING", 0);
|
||||
Atom plain = XInternAtom(d, "text/plain;charset=utf-8", 0), text_atom = XInternAtom(d, "TEXT", 0);
|
||||
Atom string = XInternAtom(d, "STRING", 0);
|
||||
char parent[PATH_MAX];
|
||||
if (strlen(argv[1]) >= sizeof(parent)) return 1;
|
||||
strcpy(parent, argv[1]); char *slash = strrchr(parent, '/');
|
||||
if (!slash) return 1;
|
||||
*slash = 0;
|
||||
int watch = inotify_init1(IN_CLOEXEC | IN_NONBLOCK);
|
||||
if (watch < 0 || inotify_add_watch(watch, parent, IN_MOVED_TO | IN_CLOSE_WRITE) < 0) return 1;
|
||||
char *text = NULL; size_t len = 0; ino_t inode = 0;
|
||||
refresh(d, w, clipboard, argv[1], &text, &len, &inode);
|
||||
struct pollfd fds[] = {{XConnectionNumber(d), POLLIN, 0}, {watch, POLLIN, 0}};
|
||||
for (;;) {
|
||||
while (XPending(d)) {
|
||||
Event e; XNextEvent(d, &e);
|
||||
if (e.type != 30) continue; /* SelectionRequest */
|
||||
SelectionRequest *r = &e.request;
|
||||
Atom property = r->property ? r->property : r->target;
|
||||
Event reply = {0};
|
||||
reply.selection = (SelectionNotify){31, 0, 1, d, r->requestor, r->selection, r->target, 0, r->time};
|
||||
if (r->selection == clipboard && text && len) {
|
||||
size_t ascii; for (ascii = 0; ascii < len && (unsigned char)text[ascii] < 128; ascii++) {}
|
||||
if (r->target == targets) {
|
||||
Atom supported[] = {targets, utf8, plain, text_atom, string};
|
||||
XChangeProperty(d, r->requestor, property, 4 /* XA_ATOM */, 32, 0,
|
||||
(const unsigned char *)supported, ascii == len ? 5 : 4);
|
||||
reply.selection.property = property;
|
||||
} else if (r->target == utf8 || r->target == plain || r->target == text_atom) {
|
||||
XChangeProperty(d, r->requestor, property, utf8, 8, 0, (const unsigned char *)text, (int)len);
|
||||
reply.selection.property = property;
|
||||
} else if (r->target == string) {
|
||||
/* STRING is Latin-1. Offer it only for ASCII rather than corrupt UTF-8. */
|
||||
if (ascii == len) {
|
||||
XChangeProperty(d, r->requestor, property, string, 8, 0, (const unsigned char *)text, (int)len);
|
||||
reply.selection.property = property;
|
||||
}
|
||||
}
|
||||
}
|
||||
XSendEvent(d, r->requestor, 0, 0, &reply); XFlush(d);
|
||||
}
|
||||
if (poll(fds, 2, -1) < 0) { if (errno == EINTR) continue; break; }
|
||||
if (fds[0].revents & (POLLERR | POLLHUP | POLLNVAL)) break;
|
||||
if (fds[1].revents & POLLIN) {
|
||||
char events[4096]; while (read(watch, events, sizeof(events)) > 0) {}
|
||||
refresh(d, w, clipboard, argv[1], &text, &len, &inode);
|
||||
}
|
||||
}
|
||||
free(text); close(watch); XCloseDisplay(d); dlclose(lib);
|
||||
return 0;
|
||||
}
|
||||
@@ -216,6 +216,12 @@ reap_orphans() {
|
||||
done
|
||||
}
|
||||
|
||||
# Gamescope's Wayland backend does not forward the outer clipboard into Xwayland.
|
||||
# This helper owns only Android text selections and exits with this guest session.
|
||||
if [ -n "${BL_ANDROID_CLIPBOARD:-}" ] && [ -x /usr/local/bin/droiddeck-clipboard ]; then
|
||||
/usr/local/bin/droiddeck-clipboard "$BL_ANDROID_CLIPBOARD" &
|
||||
fi
|
||||
|
||||
case $mode in
|
||||
desktop)
|
||||
exec pcmanfm
|
||||
|
||||
Reference in new issue
Block a user