From aea8aad5d643df911d10c3421fa2e1f149cf1617 Mon Sep 17 00:00:00 2001 From: Ali Mirjamali Date: Tue, 27 May 2025 05:19:28 +0330 Subject: [PATCH 1/2] Send icons for legacy X11 Apps Since very old X11 Apps (Xterm, Xcalc, Xlogo, Xclock, Xeyes, ...) do not have the `_NET_WM_ICON` Atom, revert to `WM_HINT` and extract icons and their transparency with that technology. This is mostly useful for Xterm fixes: https://github.com/QubesOS/qubes-issues/issues/9973 --- window-icon-updater/icon-sender | 102 +++++++++++++++++++++++++++++++- 1 file changed, 100 insertions(+), 2 deletions(-) mode change 100644 => 100755 window-icon-updater/icon-sender diff --git a/window-icon-updater/icon-sender b/window-icon-updater/icon-sender old mode 100644 new mode 100755 index d2d16e8e..789140f8 --- a/window-icon-updater/icon-sender +++ b/window-icon-updater/icon-sender @@ -33,6 +33,9 @@ import time import xcffib from xcffib import xproto +from Xlib.display import Display +from Xlib import X +from Xlib.error import XError ICON_MAX_SIZE = 256 @@ -49,6 +52,7 @@ class IconRetriever(object): self.conn = xcffib.connect() self.setup = self.conn.get_setup() self.root = self.setup.roots[0].root + self.display = Display() # just created windows for which icon wasn't sent yet - should # be send on MapNotifyEvent @@ -60,6 +64,7 @@ class IconRetriever(object): def disconnect(self): log.info('disconnecting from X') self.conn.disconnect() + self.display.close() def watch_window(self, w): self.conn.core.ChangeWindowAttributesChecked( @@ -80,9 +85,102 @@ class IconRetriever(object): except xproto.BadWindow: # Window disappeared in the meantime raise NoIconError() - if icon.format == 0: - raise NoIconError() + # Ancient X11 applications (Xterm, Xcalc, Xlogo, Xeyes, Xclock, ...) + try: + window = self.display.create_resource_object('window', w) + wm_hints = window.get_wm_hints() + if not wm_hints: + raise NoIconError() + geometry = wm_hints.icon_pixmap.get_geometry() + pixmap = wm_hints.icon_pixmap.get_image( + 0, + 0, + geometry.width, + geometry.height, + X.ZPixmap, + 0xFFFFFFFF + ) + pixmap_data = pixmap.data + icons = {} + try: + mask = wm_hints.icon_mask.get_image( + 0, + 0, + geometry.width, + geometry.height, + X.ZPixmap, + 0xFFFFFFFF + ) + mask_geometry = wm_hints.icon_mask.get_geometry() + mask_data = mask.data + except: + # Icons without transparency + mask = None + mask_geometry = None + mask_data = [] + except XError: + raise NoIconError() + + # Finally we have the required data to construct icon + if geometry.depth == 1: + # 1 bit per pixel icons (i.e. xlogo, xeyes, xcalc, ...) + icon_data = [] + # There might be trailing bytes at the end of each row since + # each row should be multiples of 32 bits + row_width = int(len(pixmap_data) / geometry.height) + for y in range(0, geometry.height): + offset = y * row_width + for x in range(0, geometry.width): + byte_offset = int(x / 8) + offset + byte = int(pixmap_data[byte_offset]) + byte = byte >> (x % 8) + bit = byte & 0x1 + if bit: + # Can not decide the light/dark theme from vmside :/ + icon_data.append(0xff7f7f7f) + else: + icon_data.append(0x0) + elif geometry.depth == 24: + # 24 bit per pixel icons (i.e. Xterm) + # Technically this could handle other programs as well + icon_data = struct.unpack( + "%dI" % (len(pixmap_data) / 4), + pixmap_data + ) + icon_data = [d | 0xff000000 for d in icon_data] + else: + # Could not find 8 bit icons of that era to work with + raise NoIconError() + if mask_data and mask_geometry.depth == 1: + # Even Xterm uses 1 bit/pixel mask. I do not know why + row_width = int(len(mask_data) / geometry.height) + for y in range(0, geometry.height): + offset = y * row_width + for x in range(0, geometry.width): + byte_offset = int(x/8) + offset + byte = int(mask_data[byte_offset]) + byte = byte >> (x % 8) + bit = byte & 0x1 + pixel = x + y * geometry.height + if bit: + icon_data[pixel] = icon_data[pixel] & 0xffffffff + else: + icon_data[pixel] = icon_data[pixel] & 0x00ffffff + elif mask_data and mask_geometry.depth == 8: + # Technically this is not tested (No X prog uses 8bit/pix mask) + # At least not on Qubes OS 4.3 & default Xfwm4 + for y in range(0, geometry.height): + offset = y * row_width + for x in range(0, geometry.width): + byte_offset = x + offset + byte = int(mask_data[byte_offset]) + pixmask = (byte < 24) | 0x00ffffff + icon_data[pixel] = icon_data[pixel] & pixmask + size = (geometry.width, geometry.height) + icons[size] = icon_data + return icons + # convert it later to a proper int array icon_data = icon.value.buf() if icon.bytes_after: From 34ab7a5fcaa76599ea3b8f50e3c164926f4807a2 Mon Sep 17 00:00:00 2001 From: Ali Mirjamali Date: Sun, 1 Jun 2025 15:20:13 +0330 Subject: [PATCH 2/2] Rewrite legacy X11 App icons extraction in xcffib Since xcffib is preferred over Xlib and the method for `_NET_WM_ICON` is already implemented via xcffib, rewrite the previous patch with xcffib related: https://github.com/QubesOS/qubes-issues/issues/9973 --- window-icon-updater/icon-sender | 137 ++++++++++++++++++++------------ 1 file changed, 85 insertions(+), 52 deletions(-) diff --git a/window-icon-updater/icon-sender b/window-icon-updater/icon-sender index 789140f8..dd8ea55d 100755 --- a/window-icon-updater/icon-sender +++ b/window-icon-updater/icon-sender @@ -33,11 +33,10 @@ import time import xcffib from xcffib import xproto -from Xlib.display import Display -from Xlib import X -from Xlib.error import XError ICON_MAX_SIZE = 256 +IconPixmapHint = 0b1 << 2 +IconMaskHint = 0b1 << 5 log = logging.getLogger('icon-sender') @@ -52,7 +51,6 @@ class IconRetriever(object): self.conn = xcffib.connect() self.setup = self.conn.get_setup() self.root = self.setup.roots[0].root - self.display = Display() # just created windows for which icon wasn't sent yet - should # be send on MapNotifyEvent @@ -64,7 +62,6 @@ class IconRetriever(object): def disconnect(self): log.info('disconnecting from X') self.conn.disconnect() - self.display.close() def watch_window(self, w): self.conn.core.ChangeWindowAttributesChecked( @@ -85,55 +82,88 @@ class IconRetriever(object): except xproto.BadWindow: # Window disappeared in the meantime raise NoIconError() + if icon.format == 0: + # Legacy case # Ancient X11 applications (Xterm, Xcalc, Xlogo, Xeyes, Xclock, ...) try: - window = self.display.create_resource_object('window', w) - wm_hints = window.get_wm_hints() - if not wm_hints: + prop_cookie = self.conn.core.GetProperty( + False, # delete + w, # window + xproto.Atom.WM_HINTS, + xproto.GetPropertyType.Any, + 0, # long_offset + 48 # long_length -> XWMHints struct length on x86_64 + ) + reply = prop_cookie.reply() + if not reply.value_len: + raise NoIconError() + atoms = reply.value.to_atoms() + flags = atoms[0] + if not flags & IconPixmapHint: + # A menu, pop-up or similar icon-less window + raise NoIconError() + icon_pixmap_geometry = self.conn.core.GetGeometry( + atoms[3] + ).reply() + if ( + # Only 1bit/pixel & 24bit/pixel icons are tested + not icon_pixmap_geometry.depth in [1, 24] + or icon_pixmap_geometry.width > ICON_MAX_SIZE + or icon_pixmap_geometry.height > ICON_MAX_SIZE + ): raise NoIconError() - geometry = wm_hints.icon_pixmap.get_geometry() - pixmap = wm_hints.icon_pixmap.get_image( + image = self.conn.core.GetImage( + xproto.ImageFormat.ZPixmap, + atoms[3], 0, 0, - geometry.width, - geometry.height, - X.ZPixmap, + icon_pixmap_geometry.width, + icon_pixmap_geometry.height, 0xFFFFFFFF - ) - pixmap_data = pixmap.data - icons = {} - try: - mask = wm_hints.icon_mask.get_image( + ).reply() + icon_pixmap_data = image.data.raw + if not flags & IconMaskHint: + # We have an icon without transparency (mask) + icon_mask_geometry = None + icon_mask_data = None + else: + icon_mask_geometry = self.conn.core.GetGeometry( + atoms[7] + ).reply() + image = self.conn.core.GetImage( + xproto.ImageFormat.ZPixmap, + atoms[7], 0, 0, - geometry.width, - geometry.height, - X.ZPixmap, + icon_mask_geometry.width, + icon_mask_geometry.height, 0xFFFFFFFF - ) - mask_geometry = wm_hints.icon_mask.get_geometry() - mask_data = mask.data - except: - # Icons without transparency - mask = None - mask_geometry = None - mask_data = [] - except XError: - raise NoIconError() + ).reply() + icon_mask_data = image.data.raw + except ( + xproto.BadWindow, + xproto.WindowError, + xproto.AccessError, + xproto.DrawableError + ): + raise NoIconError # Finally we have the required data to construct icon - if geometry.depth == 1: + icons = {} + if icon_pixmap_geometry.depth == 1: # 1 bit per pixel icons (i.e. xlogo, xeyes, xcalc, ...) icon_data = [] # There might be trailing bytes at the end of each row since # each row should be multiples of 32 bits - row_width = int(len(pixmap_data) / geometry.height) - for y in range(0, geometry.height): + row_width = int( + len(icon_pixmap_data) / icon_pixmap_geometry.height + ) + for y in range(0, icon_pixmap_geometry.height): offset = y * row_width - for x in range(0, geometry.width): + for x in range(0, icon_pixmap_geometry.width): byte_offset = int(x / 8) + offset - byte = int(pixmap_data[byte_offset]) + byte = int(icon_pixmap_data[byte_offset]) byte = byte >> (x % 8) bit = byte & 0x1 if bit: @@ -141,46 +171,49 @@ class IconRetriever(object): icon_data.append(0xff7f7f7f) else: icon_data.append(0x0) - elif geometry.depth == 24: + elif icon_pixmap_geometry.depth == 24: # 24 bit per pixel icons (i.e. Xterm) - # Technically this could handle other programs as well + # Technically this could handle modern programs as well + # However, _NET_WM_ICON is faster icon_data = struct.unpack( - "%dI" % (len(pixmap_data) / 4), - pixmap_data + "%dI" % (len(icon_pixmap_data) / 4), + icon_pixmap_data ) icon_data = [d | 0xff000000 for d in icon_data] else: # Could not find 8 bit icons of that era to work with raise NoIconError() - if mask_data and mask_geometry.depth == 1: + if icon_mask_data and icon_mask_geometry.depth == 1: # Even Xterm uses 1 bit/pixel mask. I do not know why - row_width = int(len(mask_data) / geometry.height) - for y in range(0, geometry.height): + row_width = int(len(icon_mask_data) / icon_mask_geometry.height) + for y in range(0, icon_mask_geometry.height): offset = y * row_width - for x in range(0, geometry.width): + for x in range(0, icon_mask_geometry.width): byte_offset = int(x/8) + offset - byte = int(mask_data[byte_offset]) + byte = int(icon_mask_data[byte_offset]) byte = byte >> (x % 8) bit = byte & 0x1 - pixel = x + y * geometry.height + pixel = x + y * icon_mask_geometry.height if bit: icon_data[pixel] = icon_data[pixel] & 0xffffffff else: icon_data[pixel] = icon_data[pixel] & 0x00ffffff - elif mask_data and mask_geometry.depth == 8: + elif icon_mask_data and icon_mask_geometry.depth == 8: # Technically this is not tested (No X prog uses 8bit/pix mask) # At least not on Qubes OS 4.3 & default Xfwm4 - for y in range(0, geometry.height): + row_width = int(len(icon_mask_data) / icon_mask_geometry.height) + for y in range(0, icon_mask_geometry.height): offset = y * row_width - for x in range(0, geometry.width): + for x in range(0, icon_mask_geometry.width): byte_offset = x + offset - byte = int(mask_data[byte_offset]) - pixmask = (byte < 24) | 0x00ffffff + byte = int(icon_mask_data[byte_offset]) + pixmask = (byte << 24) | 0x00ffffff icon_data[pixel] = icon_data[pixel] & pixmask - size = (geometry.width, geometry.height) + size = (icon_pixmap_geometry.width, icon_pixmap_geometry.height) icons[size] = icon_data return icons + # We have sane _NET_WM_ICON Atom for modern programs # convert it later to a proper int array icon_data = icon.value.buf() if icon.bytes_after: