Skip to content

Commit aea8aad

Browse files
committed
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: QubesOS/qubes-issues#9973
1 parent 8f8c0a1 commit aea8aad

File tree

1 file changed

+100
-2
lines changed

1 file changed

+100
-2
lines changed

window-icon-updater/icon-sender

100644100755
Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ import time
3333

3434
import xcffib
3535
from xcffib import xproto
36+
from Xlib.display import Display
37+
from Xlib import X
38+
from Xlib.error import XError
3639

3740
ICON_MAX_SIZE = 256
3841

@@ -49,6 +52,7 @@ class IconRetriever(object):
4952
self.conn = xcffib.connect()
5053
self.setup = self.conn.get_setup()
5154
self.root = self.setup.roots[0].root
55+
self.display = Display()
5256

5357
# just created windows for which icon wasn't sent yet - should
5458
# be send on MapNotifyEvent
@@ -60,6 +64,7 @@ class IconRetriever(object):
6064
def disconnect(self):
6165
log.info('disconnecting from X')
6266
self.conn.disconnect()
67+
self.display.close()
6368

6469
def watch_window(self, w):
6570
self.conn.core.ChangeWindowAttributesChecked(
@@ -80,9 +85,102 @@ class IconRetriever(object):
8085
except xproto.BadWindow:
8186
# Window disappeared in the meantime
8287
raise NoIconError()
83-
8488
if icon.format == 0:
85-
raise NoIconError()
89+
# Ancient X11 applications (Xterm, Xcalc, Xlogo, Xeyes, Xclock, ...)
90+
try:
91+
window = self.display.create_resource_object('window', w)
92+
wm_hints = window.get_wm_hints()
93+
if not wm_hints:
94+
raise NoIconError()
95+
geometry = wm_hints.icon_pixmap.get_geometry()
96+
pixmap = wm_hints.icon_pixmap.get_image(
97+
0,
98+
0,
99+
geometry.width,
100+
geometry.height,
101+
X.ZPixmap,
102+
0xFFFFFFFF
103+
)
104+
pixmap_data = pixmap.data
105+
icons = {}
106+
try:
107+
mask = wm_hints.icon_mask.get_image(
108+
0,
109+
0,
110+
geometry.width,
111+
geometry.height,
112+
X.ZPixmap,
113+
0xFFFFFFFF
114+
)
115+
mask_geometry = wm_hints.icon_mask.get_geometry()
116+
mask_data = mask.data
117+
except:
118+
# Icons without transparency
119+
mask = None
120+
mask_geometry = None
121+
mask_data = []
122+
except XError:
123+
raise NoIconError()
124+
125+
# Finally we have the required data to construct icon
126+
if geometry.depth == 1:
127+
# 1 bit per pixel icons (i.e. xlogo, xeyes, xcalc, ...)
128+
icon_data = []
129+
# There might be trailing bytes at the end of each row since
130+
# each row should be multiples of 32 bits
131+
row_width = int(len(pixmap_data) / geometry.height)
132+
for y in range(0, geometry.height):
133+
offset = y * row_width
134+
for x in range(0, geometry.width):
135+
byte_offset = int(x / 8) + offset
136+
byte = int(pixmap_data[byte_offset])
137+
byte = byte >> (x % 8)
138+
bit = byte & 0x1
139+
if bit:
140+
# Can not decide the light/dark theme from vmside :/
141+
icon_data.append(0xff7f7f7f)
142+
else:
143+
icon_data.append(0x0)
144+
elif geometry.depth == 24:
145+
# 24 bit per pixel icons (i.e. Xterm)
146+
# Technically this could handle other programs as well
147+
icon_data = struct.unpack(
148+
"%dI" % (len(pixmap_data) / 4),
149+
pixmap_data
150+
)
151+
icon_data = [d | 0xff000000 for d in icon_data]
152+
else:
153+
# Could not find 8 bit icons of that era to work with
154+
raise NoIconError()
155+
if mask_data and mask_geometry.depth == 1:
156+
# Even Xterm uses 1 bit/pixel mask. I do not know why
157+
row_width = int(len(mask_data) / geometry.height)
158+
for y in range(0, geometry.height):
159+
offset = y * row_width
160+
for x in range(0, geometry.width):
161+
byte_offset = int(x/8) + offset
162+
byte = int(mask_data[byte_offset])
163+
byte = byte >> (x % 8)
164+
bit = byte & 0x1
165+
pixel = x + y * geometry.height
166+
if bit:
167+
icon_data[pixel] = icon_data[pixel] & 0xffffffff
168+
else:
169+
icon_data[pixel] = icon_data[pixel] & 0x00ffffff
170+
elif mask_data and mask_geometry.depth == 8:
171+
# Technically this is not tested (No X prog uses 8bit/pix mask)
172+
# At least not on Qubes OS 4.3 & default Xfwm4
173+
for y in range(0, geometry.height):
174+
offset = y * row_width
175+
for x in range(0, geometry.width):
176+
byte_offset = x + offset
177+
byte = int(mask_data[byte_offset])
178+
pixmask = (byte < 24) | 0x00ffffff
179+
icon_data[pixel] = icon_data[pixel] & pixmask
180+
size = (geometry.width, geometry.height)
181+
icons[size] = icon_data
182+
return icons
183+
86184
# convert it later to a proper int array
87185
icon_data = icon.value.buf()
88186
if icon.bytes_after:

0 commit comments

Comments
 (0)