Skip to content

fix(tao): match Linux windows to their desktop entry (Wayland app_id, X11 WM_CLASS) - #856

Open
sproctor wants to merge 5 commits into
NucleusFramework:nucleus-3.0from
sproctor:fix/wayland-app-id
Open

sproctor wants to merge 5 commits into
NucleusFramework:nucleus-3.0from
sproctor:fix/wayland-app-id

Conversation

@sproctor

@sproctor sproctor commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Draft: window matching is verified with WAYLAND_DEBUG (Wayland) and xprop (X11), and the GNOME dock icon on X11 (below), but the dock icon on Wayland has not been checked on a real session yet.

Linux desktops pair a window with its launcher entry, and so show the app's icon in the dock, by a name the window carries: the xdg-shell app_id on Wayland, WM_CLASS on X11. Tao windows got both wrong in common configurations. This PR fixes both halves.

Reproduction

Setup: nucleus-3.0, Ubuntu 26.04 with GNOME 50. The Lab's deb (packageName = "NucleusLab", mainClass = "dev.nucleusframework.lab.app.MainKt"), run from /opt/<app>/bin/NucleusLab.

Wayland, with linux { packageName = "nucleus-lab" }, run with WAYLAND_DEBUG=1 and looking for xdg_toplevel.set_app_id:

Package Desktop entry Before After
deb (/opt/nucleus-lab/nucleus-lab) nucleus-lab.desktop set_app_id("NucleusLab") set_app_id("nucleus-lab")
AppImage (APPIMAGE_EXTRACT_AND_RUN=1) nucleus-lab.desktop in $APPDIR "NucleusLab" "nucleus-lab"

Without linux.packageName the entry is NucleusLab.desktop and the app_id stays NucleusLab (re-checked after the X11 change).

X11, default Lab config, GDK_BACKEND=x11, reading xprop WM_CLASS on the toplevel:

Desktop entry Before After
NucleusLab.desktop: StartupWMClass=dev-nucleusframework-lab-app-MainKt WM_CLASS "NucleusLab", "NucleusLab" WM_CLASS "dev-nucleusframework-lab-app-MainKt", "dev-nucleusframework-lab-app-MainKt"

So on X11 the window never matched its entry's StartupWMClass unless the app set linux.startupWMClass to the launcher's name. A native image shows the same thing: an app with graalvm { imageName = "warlock" } gets StartupWMClass=warlockfe-warlock3-app-MainKt in its deb, while its window is WM_CLASS "warlock", "Warlock".

GNOME Shell hides this in the default setup: when StartupWMClass does not match, it also looks for a desktop file named after the WM_CLASS, and the Lab's NucleusLab.desktop is. The icon goes missing whenever the entry is named otherwise, which is the same linux.packageName case as on Wayland. Checked by eye in the dock, GNOME 50 on X11 (XWayland), Lab run with GDK_BACKEND=x11 against a user-level entry:

Build Entry file Entry StartupWMClass Window WM_CLASS Dock icon
before (310551d) nucleus-lab-demo.desktop dev-nucleusframework-lab-app-MainKt NucleusLab generic
before (310551d) nucleus-lab-demo.desktop NucleusLab (edited to match) NucleusLab Nucleus
after NucleusLab.desktop dev-nucleusframework-lab-app-MainKt dev-nucleusframework-lab-app-MainKt Nucleus

The second row confirms the mismatch is the cause. In the third, the filename cannot be what matched, since the window is no longer named NucleusLab.

Root cause

GTK 3 (Tao registers no GApplication id) derives both names from g_get_prgname(), the launcher binary's name:

  • Wayland: the app_id is the prgname. For the JVM packages that is the jpackage launcher bin/<packageName>, which the /opt/<app>/<executableName> alias execs, while electron-builder names the entry after executableName = linux.packageName ?: packageName.
  • X11: WM_CLASS is the prgname (instance) and gdk_get_program_class() (class, the prgname capitalised). The plugin writes StartupWMClass as linux.startupWMClass, else the main class with dots replaced by dashes, the name AWT's XToolkit gives its windows. That default was right for AWT apps; a Tao window is never named that way.

Fix

  • The plugin bakes linux.desktop.entry.id into nucleus-app.properties, read back as NucleusApp.linuxDesktopEntryId. It already baked startup.wm.class (the exact value it writes as StartupWMClass, for JVM and native packages alike), now read back as NucleusApp.linuxStartupWmClass.
  • TaoApplication hands both to the native bridge. Once GTK is up and its display chosen, before any window exists, the bridge applies the one for that display:
    • Wayland: g_set_prgname(app_id).
    • X11: g_set_prgname(wm_class) and gdk_set_program_class(wm_class), so both halves of WM_CLASS equal StartupWMClass (GNOME Shell matches either).
  • App id: Flatpak sends FLATPAK_ID. Snaps and native images keep GTK's default, since a native image's binary is already named after its entry (graalvm.imageName).
  • WM_CLASS: applied in every package type, native images included, since they all write the same StartupWMClass.

The default StartupWMClass derivation is unchanged, so AWT apps packaged by the plugin (which XToolkit names after the main class) keep matching.

Alternative considered: fixing it in the packager instead, by writing the launcher's name as StartupWMClass (the second row above shows that works too). But the plugin packages AWT apps as well, whose windows XToolkit names after the main class, and it cannot tell which backend an app uses. Applying the baked value at run time keeps the entry authoritative for both, and an explicit linux.startupWMClass keeps working.

This is the window-matching half of AppImage desktop integration (NucleusFramework/Nucleus#847): once the entry is installed, the desktop can only pair the windows with it when the app_id or WM_CLASS matches.

Testing

  • TaoWaylandAppIdTest: which app_id is chosen for each packaging case.
  • TaoX11WmClassTest (new): the baked StartupWMClass is used as written, and nothing baked keeps GTK's default.
  • The WAYLAND_DEBUG and xprop before/after above, from the packaged Lab deb, and the X11 dock-icon table.
  • ./gradlew :core-runtime:check :decorated-window-tao:check: 135 + 584 tests (1 skipped), apiCheck, detekt and ktlint clean.

Not tested: the dock icon on Wayland and on KDE (GNOME blocks the calls that would read the dock from a script, so it was checked by eye on X11 only), Flatpak and snap packages, a native image built from this branch (the native WM_CLASS mismatch above was observed on a native build of the published 3.0.0-dev artifacts), and macOS/Windows (the change is Linux-only).

Known gap: GDK's hidden 10×10 client-leader window keeps the launcher's name in its WM_CLASS. GDK creates it when the display opens, before the display type is known to the bridge. It is never mapped, so docks do not match against it.

🤖 Generated with Claude Code

Wayland compositors pair a toplevel with its launcher entry, and so show
the app's icon in the dock, only when the xdg-shell app_id equals the
.desktop file's basename. GTK 3 (Tao registers no GApplication id) sends
g_get_prgname(), i.e. the launcher binary's name. For the JVM packages
that is the jpackage launcher bin/<packageName>, which the
/opt/<app>/<executableName> alias execs, while electron-builder names the
entry after executableName = linux.packageName ?: packageName. Whenever
linux.packageName differs from packageName the two never match: GNOME
shows a generic icon and a separate dock item.

Repro (GNOME 50, Wayland, Ubuntu): the Lab with
linux { packageName = "nucleus-lab" }, WAYLAND_DEBUG=1:
- deb (/opt/nucleus-lab/nucleus-lab): nucleus-lab.desktop,
  before xdg_toplevel.set_app_id("NucleusLab"), after ("nucleus-lab")
- AppImage (APPIMAGE_EXTRACT_AND_RUN=1): nucleus-lab.desktop in
  $APPDIR, before "NucleusLab", after "nucleus-lab"
- X11 (GDK_BACKEND=x11): WM_CLASS "NucleusLab", "NucleusLab" both
  before and after

The plugin bakes linux.desktop.entry.id into nucleus-app.properties
(NucleusApp.linuxDesktopEntryId), and TaoApplication hands it to the
bridge, which calls g_set_prgname once GTK is up and only if its display
is Wayland: on X11 the prgname is also WM_CLASS, left as it is. Flatpak
sends FLATPAK_ID; snaps and native images keep GTK's default (a native
image's binary is already named after its entry, graalvm.imageName).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sproctor
sproctor marked this pull request as ready for review October 19, 2026 19:20
…guard

It covers the pure app_id selection in TaoWaylandAppId.resolve, with the
native-image case passed in as a parameter: no ComposeScene to run in the
native-image battery, like the other pure-function tests listed there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sproctor
sproctor marked this pull request as draft October 20, 2026 02:18
X11 desktops pair a window with its launcher entry by matching WM_CLASS
against the entry's StartupWMClass. GTK 3 names Tao windows after the launcher
binary (NucleusLab / NucleusLab), while the plugin writes StartupWMClass as
linux.startupWMClass or the main class with dots replaced by dashes (the AWT
XToolkit convention), so the two never agreed unless the app set
startupWMClass to the binary's name.

The plugin already bakes that value as startup.wm.class; NucleusApp now exposes
it as linuxStartupWmClass, and the bridge sets both halves of WM_CLASS
(g_set_prgname for the instance, gdk_set_program_class for the class) on an X11
display, next to the Wayland app_id. JVM packages and native images write the
same entry, so both get it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sproctor sproctor changed the title fix(tao): send the desktop entry's name as the Wayland app_id fix(tao): match Linux windows to their desktop entry (Wayland app_id, X11 WM_CLASS) Oct 20, 2026
@sproctor
sproctor marked this pull request as ready for review October 20, 2026 02:40
@kdroidFilter

Copy link
Copy Markdown
Collaborator

Could you check if this breaks hot reload?

@sproctor

Copy link
Copy Markdown
Contributor Author

It seems fine with hot reload. Did you have a problem with it? For me, hot reload is running the app through XWayland so just the X11 part would apply anyway.

sproctor and others added 2 commits October 27, 2026 13:34
Keeps both sides of the TaoApplication / NativeTaoBridge conflicts, and moves
nativeSetWaylandAppId / nativeSetX11WmClass into NativeTaoLinuxIdentityBridge:
with upstream's new natives NativeTaoBridge crossed detekt's LargeClass limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants