Skip to content

Per-window panel icon support via raw _NET_WM_ICON#35

Open
nynjapirate wants to merge 1 commit into
klangman:mainfrom
nynjapirate:feature/per-window-icon
Open

Per-window panel icon support via raw _NET_WM_ICON#35
nynjapirate wants to merge 1 commit into
klangman:mainfrom
nynjapirate:feature/per-window-icon

Conversation

@nynjapirate

Copy link
Copy Markdown

Problem

CassiaWindowList renders the panel button icon from the .desktop file
matched against WM_CLASS. For applications that update
_NET_WM_ICON per-state during the window's lifetime — e.g. Nemo or
Files showing a custom-iconed folder, a browser showing the focused
tab's favicon, anything that calls gtk_window_set_icon after window
creation — the panel button is stuck on the app icon and never
reflects the live state.

Confirmed comparable behavior exists in the stock Cinnamon
window-list@cinnamon.org and grouped-window-list@cinnamon.org
applets (they listen on notify::icon), but Cinnamon's
Cinnamon.App.create_icon_texture_for_window ignores the for_window
arg when the window matches a .desktop, and Meta.Window.icon is a
dead WM_HINTS stub for modern apps. So the obvious notify::icon
listener fires but the resulting property value is useless. Working
around Cinnamon's preference for app icons needs an alternative
icon-loading path that goes straight to the raw _NET_WM_ICON.

Approach

Read _NET_WM_ICON directly via an async xprop subprocess:

xprop -id <xid> -len 1048576 -notype 32c _NET_WM_ICON

Parse the {w0,h0,ARGB...,w1,h1,ARGB...} cardinal list, pick the size
closest to (but not exceeding) the panel icon size, swizzle ARGB→RGBA,
build a GdkPixbuf via new_from_bytes, cache it per-xid, and render
the result as the panel icon.

notify::title is the trigger (since notify::icon doesn't fire for
_NET_WM_ICON-only changes per the dead-stub problem above). An 80ms
debounce coalesces title bursts; first paint of each window misses the
cache and falls through to the standard app-icon path, then the async
xprop returns and the panel re-renders with the per-window icon.

Performance / sanity:

  • Signature dedup — 200-char sample hash of the cardinals string
    skips repaints when _NET_WM_ICON content is unchanged across a
    refetch. xfce4-terminal and similar apps update title every command
    without touching their icon; without this the icon would thrash.
  • Reference-identity skip on the displayed pixbuf avoids
    destroy-and-recreate when nothing changed visually.
  • 30s very-stale floor as a safety net for any path that bypasses
    the title hook.

For windows that don't update _NET_WM_ICON during their lifetime
(most apps), behavior is unchanged: the cache stays empty for that
xid, the existing app-icon path is the fallback.

Why xprop and not Gdk.property_get

Gdk.property_get is non-introspectable in gjs/cjs (the CARDINAL
out-arg type) — segfaults under cjs, throws under gjs. GIRepository
inspection of GdkX11-3.0 confirms only X11Window.foreign_new_for_display
and lookup_for_display are reachable from JS; nothing on the instance
reads property data. xprop is part of x11-utils, a stock dep of
every desktop Linux install, so no new dependency.

Scope

Only 5.4/applet.js is touched. 6.0/applet.js is a symlink to
5.4/applet.js in this layout. 4.0/applet.js is left for a possible
backport in a follow-up — the API surface used here (Gio.Subprocess,
GdkPixbuf.Pixbuf.new_from_bytes, metaWindow.get_xwindow) is
available on 4.0 too but I haven't tested.

Verification

Tested on Cinnamon 6.6.7 with Lancelot (a Nemo fork that updates
_NET_WM_ICON per folder via gtk_window_set_icon in
nemo_window_slot_update_icon). Panel button reflects the
custom-iconed folder icon on navigation; falls back to the app icon
for ordinary folders and other apps; no panel-button flash on
unchanged-icon title bursts.

🤖 Generated with Claude Code

CassiaWindowList currently always renders the panel button icon from
the .desktop file matched against WM_CLASS.  For windows that update
_NET_WM_ICON per-state — e.g. Nemo / Files / Lancelot showing a
custom-iconed folder, a browser showing a per-tab favicon, anything
that calls gtk_window_set_icon during its lifetime — the panel button
is stuck on the app icon and never reflects the live state.

Cinnamon's standard Cinnamon.App.create_icon_texture_for_window
ignores the for_window arg when the window matches a .desktop, and
Meta.Window.icon is a dead WM_HINTS stub (NULL for modern apps), so
the obvious fixes don't work.  This commit reads _NET_WM_ICON
directly via an async xprop subprocess, parses the {w,h,ARGB...}
cardinal list, picks the size closest to (but not exceeding) the
panel icon size, swizzles ARGB→RGBA, builds a GdkPixbuf, caches it
per-xid, and renders the result as the panel icon.

Mechanics:
  - notify::title is the trigger (since notify::icon never fires for
    _NET_WM_ICON-only changes).  An 80ms debounce coalesces title
    bursts; first paint of each window misses cache and falls through
    to the standard app icon, then the async xprop returns and the
    panel re-renders with the per-window icon.
  - Signature dedup (200-char sample hash of the cardinals string)
    skips repaints when _NET_WM_ICON content is unchanged across a
    refetch — avoids panel-button flash for apps like xfce4-terminal
    that update title without touching the icon.
  - Reference-identity skip on the displayed pixbuf avoids
    destroy-and-recreate when nothing changed.
  - 30s very-stale floor as a safety net.

For windows without a _NET_WM_ICON update during their lifetime
(most apps), behavior is unchanged: the standard app-icon path is
the fallback when the cache has no pixbuf for the xid.

No new permissions or D-Bus dependencies.  xprop is part of x11-utils
which is a stock dep of every desktop Linux install.
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.

1 participant