Skip to content

Commit 43e6442

Browse files
committed
Refactor Linux outside-click watcher: replace JNA with JNI, add dynamic X11 support, and update reachability metadata.
1 parent aef04bb commit 43e6442

7 files changed

Lines changed: 169 additions & 79 deletions

File tree

demo/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ kotlin {
2929
}
3030

3131
nucleus.application {
32-
mainClass = "com.kdroid.composetray.demo.DynamicTrayMenuKt"
32+
mainClass = "com.kdroid.composetray.demo.TrayAppDemoKt"
3333

3434
buildTypes {
3535
release {

demo/src/jvmMain/kotlin/com/kdroid/composetray/demo/TrayAppDemo.kt

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,20 @@ import com.kdroid.composetray.utils.allowComposeNativeTrayLogging
2626
import composenativetray.demo.generated.resources.Res
2727
import composenativetray.demo.generated.resources.icon
2828
import io.github.kdroidfilter.nucleus.darkmodedetector.isSystemInDarkMode
29+
import io.github.kdroidfilter.nucleus.graalvm.GraalVmInitializer
2930
import kotlinx.coroutines.launch
3031
import org.jetbrains.compose.resources.painterResource
32+
import java.io.File
3133

3234
@OptIn(ExperimentalTrayAppApi::class)
3335
fun main() {
36+
GraalVmInitializer.initialize()
37+
if (System.getProperty("skiko.renderApi") == null) {
38+
val os = System.getProperty("os.name")?.lowercase() ?: ""
39+
if (os.contains("linux") && isNvidiaGpuPresent()) {
40+
System.setProperty("skiko.renderApi", "SOFTWARE")
41+
}
42+
}
3443
allowComposeNativeTrayLogging = true
3544
application {
3645
var isWindowVisible by remember { mutableStateOf(true) }
@@ -265,4 +274,21 @@ fun main() {
265274
}
266275
}
267276
}
268-
}
277+
}
278+
279+
private fun isNvidiaGpuPresent(): Boolean {
280+
// Check if NVIDIA driver is loaded by looking for the driver version file
281+
val nvidiaDriverFile = File("/proc/driver/nvidia/version")
282+
if (nvidiaDriverFile.exists()) return true
283+
284+
// Fallback: try running nvidia-smi
285+
return try {
286+
val process = ProcessBuilder("nvidia-smi", "-L")
287+
.redirectErrorStream(true)
288+
.start()
289+
val exitCode = process.waitFor()
290+
exitCode == 0
291+
} catch (_: Exception) {
292+
false
293+
}
294+
}

src/jvmMain/kotlin/com/kdroid/composetray/lib/linux/LinuxNativeBridge.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,18 @@ internal object LinuxNativeBridge {
6666
@JvmStatic external fun nativeItemCheck(handle: Long, id: Int)
6767
@JvmStatic external fun nativeItemUncheck(handle: Long, id: Int)
6868
@JvmStatic external fun nativeItemSetIcon(handle: Long, id: Int, iconBytes: ByteArray)
69+
70+
// -- X11 outside-click watcher -----------------------------------------------
71+
72+
/** Open X11 display. Returns handle, or 0 if X11 is unavailable. */
73+
@JvmStatic external fun nativeX11OpenDisplay(): Long
74+
75+
/** Get default root window for the display. */
76+
@JvmStatic external fun nativeX11DefaultRootWindow(displayHandle: Long): Long
77+
78+
/** Query pointer. Writes [rootX, rootY, mask] into outData. Returns 1 on success. */
79+
@JvmStatic external fun nativeX11QueryPointer(displayHandle: Long, rootWindow: Long, outData: IntArray): Int
80+
81+
/** Close X11 display. */
82+
@JvmStatic external fun nativeX11CloseDisplay(displayHandle: Long)
6983
}

src/jvmMain/kotlin/com/kdroid/composetray/lib/linux/LinuxOutsideClickWatcher.kt

Lines changed: 19 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,19 @@
11
package com.kdroid.composetray.lib.linux
22

33
import com.kdroid.composetray.utils.isPointWithinLinuxStatusItem
4-
import com.sun.jna.Library
5-
import com.sun.jna.Native
6-
import com.sun.jna.NativeLong
7-
import com.sun.jna.Pointer
8-
import com.sun.jna.ptr.IntByReference
9-
import com.sun.jna.ptr.NativeLongByReference
104
import io.github.kdroidfilter.platformtools.OperatingSystem
115
import io.github.kdroidfilter.platformtools.getOperatingSystem
126
import java.awt.Window
137
import java.util.concurrent.Executors
148
import java.util.concurrent.ScheduledExecutorService
159
import java.util.concurrent.TimeUnit
1610

17-
/**
18-
* Minimal X11 binding for what we need.
19-
*/
20-
internal interface X11 : Library {
21-
companion object {
22-
val INSTANCE: X11 = Native.load("X11", X11::class.java)
23-
}
24-
25-
fun XOpenDisplay(displayName: String?): Pointer?
26-
fun XDefaultRootWindow(display: Pointer?): NativeLong
27-
fun XQueryPointer(
28-
display: Pointer?,
29-
w: NativeLong,
30-
root_return: NativeLongByReference,
31-
child_return: NativeLongByReference,
32-
root_x_return: IntByReference,
33-
root_y_return: IntByReference,
34-
win_x_return: IntByReference,
35-
win_y_return: IntByReference,
36-
mask_return: IntByReference
37-
): Int
38-
39-
fun XCloseDisplay(display: Pointer?): Int
40-
}
41-
4211
/**
4312
* LinuxOutsideClickWatcher: X11/XWayland implementation that detects a left-click anywhere,
4413
* and if it is outside the supplied window (and not on the tray icon area), invokes a callback.
4514
*
15+
* Uses JNI via LinuxNativeBridge for X11 calls (no JNA dependency).
16+
*
4617
* Notes:
4718
* - Requires X11/XWayland (DISPLAY must be set). Will no-op on Wayland-only sessions without XWayland.
4819
* - Polls with XQueryPointer at ~60 Hz, reading Button1Mask for "left pressed".
@@ -55,25 +26,20 @@ class LinuxOutsideClickWatcher(
5526
private var scheduler: ScheduledExecutorService? = null
5627
private var prevLeft = false
5728

58-
// X11 state
59-
private var display: Pointer? = null
60-
private var rootWindow: NativeLong = NativeLong(0)
29+
// X11 state (native handles)
30+
private var displayHandle: Long = 0L
31+
private var rootWindow: Long = 0L
6132

6233
fun start() {
6334
if (getOperatingSystem() != OperatingSystem.LINUX) return
6435
if (scheduler != null) return
6536

6637
try {
67-
val x11 = X11.INSTANCE
68-
display = x11.XOpenDisplay(null)
69-
if (display == null) {
70-
// No X11 available (e.g., Wayland-only session) -> do nothing.
71-
return
72-
}
73-
rootWindow = x11.XDefaultRootWindow(display)
38+
displayHandle = LinuxNativeBridge.nativeX11OpenDisplay()
39+
if (displayHandle == 0L) return
40+
rootWindow = LinuxNativeBridge.nativeX11DefaultRootWindow(displayHandle)
7441
} catch (_: Throwable) {
75-
// Failed to connect to X11 -> do nothing.
76-
display = null
42+
displayHandle = 0L
7743
return
7844
}
7945

@@ -85,42 +51,21 @@ class LinuxOutsideClickWatcher(
8551
}
8652

8753
private fun pollOnce() {
88-
val dpy = display ?: return
54+
if (displayHandle == 0L) return
8955
try {
90-
val x11 = X11.INSTANCE
91-
92-
val rootRet = NativeLongByReference()
93-
val childRet = NativeLongByReference()
94-
val rootX = IntByReference()
95-
val rootY = IntByReference()
96-
val winX = IntByReference()
97-
val winY = IntByReference()
98-
val mask = IntByReference()
99-
100-
// Bool return (0 = False, non-zero = True)
101-
val ok = x11.XQueryPointer(
102-
dpy,
103-
rootWindow,
104-
rootRet,
105-
childRet,
106-
rootX,
107-
rootY,
108-
winX,
109-
winY,
110-
mask
111-
) != 0
112-
56+
val outData = IntArray(3) // [rootX, rootY, mask]
57+
val ok = LinuxNativeBridge.nativeX11QueryPointer(displayHandle, rootWindow, outData) != 0
11358
if (!ok) return
11459

115-
val left = (mask.value and BUTTON1_MASK) != 0
60+
val px = outData[0]
61+
val py = outData[1]
62+
val mask = outData[2]
63+
val left = (mask and BUTTON1_MASK) != 0
11664

11765
// Rising edge: only act once when the button goes down
11866
if (left && left != prevLeft) {
11967
val win = windowSupplier.invoke()
12068
if (win != null && win.isShowing) {
121-
val px = rootX.value
122-
val py = rootY.value
123-
12469
val winLoc = try { win.locationOnScreen } catch (_: Throwable) { null }
12570
if (winLoc != null) {
12671
val wx = winLoc.x
@@ -150,14 +95,12 @@ class LinuxOutsideClickWatcher(
15095
try { scheduler?.shutdownNow() } catch (_: Throwable) {}
15196
scheduler = null
15297

153-
// Close X11 display after stopping the poller
15498
try {
155-
display?.let { X11.INSTANCE.XCloseDisplay(it) }
99+
if (displayHandle != 0L) LinuxNativeBridge.nativeX11CloseDisplay(displayHandle)
156100
} catch (_: Throwable) {
157-
// ignore
158101
} finally {
159-
display = null
160-
rootWindow = NativeLong(0)
102+
displayHandle = 0L
103+
rootWindow = 0L
161104
}
162105
}
163106

src/jvmMain/resources/META-INF/native-image/io.github.kdroidfilter/composenativetray/reachability-metadata.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,16 @@
2828
}
2929
]
3030
},
31+
{
32+
"type": "sun.awt.X11GraphicsConfig",
33+
"jniAccessible": true,
34+
"methods": [
35+
{
36+
"name": "createDCM32",
37+
"parameterTypes": ["int", "int", "int", "int", "boolean"]
38+
}
39+
]
40+
},
3141
{
3242
"type": "java.lang.Runnable",
3343
"jniAccessible": true,

src/native/linux/build.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ gcc -shared -o "$OUTPUT_DIR/linux-x86-64/libLinuxTray.so" \
6464
"$SCRIPT_DIR/sni.o" \
6565
"$SCRIPT_DIR/jni_bridge.o" \
6666
$SDBUS_LIBS \
67-
-lpthread -lm
67+
-lpthread -lm -ldl
6868

6969
# Strip debug symbols for smaller binary
7070
strip --strip-unneeded "$OUTPUT_DIR/linux-x86-64/libLinuxTray.so"

src/native/linux/jni_bridge.c

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include <stdlib.h>
1616
#include <string.h>
1717
#include <stdint.h>
18+
#include <dlfcn.h>
1819

1920
#include "sni.h"
2021

@@ -489,3 +490,99 @@ Java_com_kdroid_composetray_lib_linux_LinuxNativeBridge_nativeItemSetIcon(
489490
sni_tray_item_set_icon(tray, (uint32_t)id, (const uint8_t *)buf, (size_t)len);
490491
(*env)->ReleaseByteArrayElements(env, iconBytes, buf, JNI_ABORT);
491492
}
493+
494+
/* ========================================================================== */
495+
/* X11 outside-click watcher (dynamically loaded to avoid hard dependency) */
496+
/* ========================================================================== */
497+
498+
/* X11 types */
499+
typedef void *X11Display;
500+
typedef unsigned long X11Window;
501+
typedef int X11Bool;
502+
503+
/* X11 function pointers (loaded via dlopen/dlsym) */
504+
typedef X11Display (*fn_XOpenDisplay)(const char *);
505+
typedef X11Window (*fn_XDefaultRootWindow)(X11Display);
506+
typedef X11Bool (*fn_XQueryPointer)(X11Display, X11Window,
507+
X11Window *, X11Window *,
508+
int *, int *, int *, int *,
509+
unsigned int *);
510+
typedef int (*fn_XCloseDisplay)(X11Display);
511+
512+
static void *g_x11_lib = NULL;
513+
static fn_XOpenDisplay g_XOpenDisplay = NULL;
514+
static fn_XDefaultRootWindow g_XDefaultRootWindow = NULL;
515+
static fn_XQueryPointer g_XQueryPointer = NULL;
516+
static fn_XCloseDisplay g_XCloseDisplay = NULL;
517+
518+
static int ensure_x11(void) {
519+
if (g_x11_lib) return 1;
520+
g_x11_lib = dlopen("libX11.so.6", RTLD_LAZY);
521+
if (!g_x11_lib) g_x11_lib = dlopen("libX11.so", RTLD_LAZY);
522+
if (!g_x11_lib) return 0;
523+
g_XOpenDisplay = (fn_XOpenDisplay)dlsym(g_x11_lib, "XOpenDisplay");
524+
g_XDefaultRootWindow = (fn_XDefaultRootWindow)dlsym(g_x11_lib, "XDefaultRootWindow");
525+
g_XQueryPointer = (fn_XQueryPointer)dlsym(g_x11_lib, "XQueryPointer");
526+
g_XCloseDisplay = (fn_XCloseDisplay)dlsym(g_x11_lib, "XCloseDisplay");
527+
if (!g_XOpenDisplay || !g_XDefaultRootWindow || !g_XQueryPointer || !g_XCloseDisplay) {
528+
dlclose(g_x11_lib);
529+
g_x11_lib = NULL;
530+
return 0;
531+
}
532+
return 1;
533+
}
534+
535+
JNIEXPORT jlong JNICALL
536+
Java_com_kdroid_composetray_lib_linux_LinuxNativeBridge_nativeX11OpenDisplay(
537+
JNIEnv *env, jclass clazz)
538+
{
539+
(void)env; (void)clazz;
540+
if (!ensure_x11()) return 0;
541+
X11Display dpy = g_XOpenDisplay(NULL);
542+
return (jlong)(uintptr_t)dpy;
543+
}
544+
545+
JNIEXPORT jlong JNICALL
546+
Java_com_kdroid_composetray_lib_linux_LinuxNativeBridge_nativeX11DefaultRootWindow(
547+
JNIEnv *env, jclass clazz, jlong displayHandle)
548+
{
549+
(void)env; (void)clazz;
550+
if (!g_XDefaultRootWindow) return 0;
551+
X11Display dpy = (X11Display)(uintptr_t)displayHandle;
552+
if (!dpy) return 0;
553+
return (jlong)g_XDefaultRootWindow(dpy);
554+
}
555+
556+
JNIEXPORT jint JNICALL
557+
Java_com_kdroid_composetray_lib_linux_LinuxNativeBridge_nativeX11QueryPointer(
558+
JNIEnv *env, jclass clazz, jlong displayHandle, jlong rootWindow, jintArray outData)
559+
{
560+
(void)clazz;
561+
if (!g_XQueryPointer) return 0;
562+
X11Display dpy = (X11Display)(uintptr_t)displayHandle;
563+
if (!dpy || !outData) return 0;
564+
565+
X11Window root_ret, child_ret;
566+
int root_x, root_y, win_x, win_y;
567+
unsigned int mask;
568+
569+
X11Bool ok = g_XQueryPointer(dpy, (X11Window)(unsigned long)rootWindow,
570+
&root_ret, &child_ret,
571+
&root_x, &root_y, &win_x, &win_y, &mask);
572+
573+
/* outData: [rootX, rootY, mask] */
574+
jint buf[3] = {(jint)root_x, (jint)root_y, (jint)mask};
575+
(*env)->SetIntArrayRegion(env, outData, 0, 3, buf);
576+
577+
return (jint)(ok != 0 ? 1 : 0);
578+
}
579+
580+
JNIEXPORT void JNICALL
581+
Java_com_kdroid_composetray_lib_linux_LinuxNativeBridge_nativeX11CloseDisplay(
582+
JNIEnv *env, jclass clazz, jlong displayHandle)
583+
{
584+
(void)env; (void)clazz;
585+
if (!g_XCloseDisplay) return;
586+
X11Display dpy = (X11Display)(uintptr_t)displayHandle;
587+
if (dpy) g_XCloseDisplay(dpy);
588+
}

0 commit comments

Comments
 (0)