Skip to content

Commit 39f6763

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

File tree

1 file changed

+82
-51
lines changed

1 file changed

+82
-51
lines changed

window-icon-updater/icon-sender

Lines changed: 82 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,10 @@ 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
3936

4037
ICON_MAX_SIZE = 256
38+
IconPixmapHint = 0b1 << 2
39+
IconMaskHint = 0b1 << 5
4140

4241

4342
log = logging.getLogger('icon-sender')
@@ -52,7 +51,6 @@ class IconRetriever(object):
5251
self.conn = xcffib.connect()
5352
self.setup = self.conn.get_setup()
5453
self.root = self.setup.roots[0].root
55-
self.display = Display()
5654

5755
# just created windows for which icon wasn't sent yet - should
5856
# be send on MapNotifyEvent
@@ -64,7 +62,6 @@ class IconRetriever(object):
6462
def disconnect(self):
6563
log.info('disconnecting from X')
6664
self.conn.disconnect()
67-
self.display.close()
6865

6966
def watch_window(self, w):
7067
self.conn.core.ChangeWindowAttributesChecked(
@@ -85,102 +82,136 @@ class IconRetriever(object):
8582
except xproto.BadWindow:
8683
# Window disappeared in the meantime
8784
raise NoIconError()
85+
8886
if icon.format == 0:
87+
# Legacy case
8988
# Ancient X11 applications (Xterm, Xcalc, Xlogo, Xeyes, Xclock, ...)
9089
try:
91-
window = self.display.create_resource_object('window', w)
92-
wm_hints = window.get_wm_hints()
93-
if not wm_hints:
90+
prop_cookie = self.conn.core.GetProperty(
91+
False, # delete
92+
w, # window
93+
xproto.Atom.WM_HINTS,
94+
xproto.GetPropertyType.Any,
95+
0, # long_offset
96+
48 # long_length -> XWMHints struct length on x86_64
97+
)
98+
reply = prop_cookie.reply()
99+
if not reply.value_len:
100+
raise NoIconError()
101+
atoms = reply.value.to_atoms()
102+
flags = atoms[0]
103+
if not flags & IconPixmapHint:
104+
# A menu, pop-up or similar icon-less window
105+
raise NoIconError()
106+
icon_pixmap_geometry = self.conn.core.GetGeometry(
107+
atoms[3]
108+
).reply()
109+
if (
110+
# Only 1bit/pixel & 24bit/pixel icons are tested
111+
not icon_pixmap_geometry.depth in [1, 24]
112+
or icon_pixmap_geometry.width > ICON_MAX_SIZE
113+
or icon_pixmap_geometry.height > ICON_MAX_SIZE
114+
):
94115
raise NoIconError()
95-
geometry = wm_hints.icon_pixmap.get_geometry()
96-
pixmap = wm_hints.icon_pixmap.get_image(
116+
image = self.conn.core.GetImage(
117+
xproto.ImageFormat.ZPixmap,
118+
atoms[3],
97119
0,
98120
0,
99-
geometry.width,
100-
geometry.height,
101-
X.ZPixmap,
121+
icon_pixmap_geometry.width,
122+
icon_pixmap_geometry.height,
102123
0xFFFFFFFF
103-
)
104-
pixmap_data = pixmap.data
105-
icons = {}
106-
try:
107-
mask = wm_hints.icon_mask.get_image(
124+
).reply()
125+
icon_pixmap_data = image.data.raw
126+
if not flags & IconMaskHint:
127+
# We have an icon without transparency (mask)
128+
icon_mask_geometry = None
129+
icon_mask_data = None
130+
else:
131+
icon_mask_geometry = self.conn.core.GetGeometry(
132+
atoms[7]
133+
).reply()
134+
image = self.conn.core.GetImage(
135+
xproto.ImageFormat.ZPixmap,
136+
atoms[7],
108137
0,
109138
0,
110-
geometry.width,
111-
geometry.height,
112-
X.ZPixmap,
139+
icon_mask_geometry.width,
140+
icon_mask_geometry.height,
113141
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()
142+
).reply()
143+
icon_mask_data = image.data.raw
144+
except (
145+
xcffib.xproto.WindowError,
146+
xcffib.xproto.AccessError,
147+
NoIconError
148+
):
149+
raise NoIconError
124150

125151
# Finally we have the required data to construct icon
126-
if geometry.depth == 1:
152+
icons = {}
153+
if icon_pixmap_geometry.depth == 1:
127154
# 1 bit per pixel icons (i.e. xlogo, xeyes, xcalc, ...)
128155
icon_data = []
129156
# There might be trailing bytes at the end of each row since
130157
# 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):
158+
row_width = int(
159+
len(icon_pixmap_data) / icon_pixmap_geometry.height
160+
)
161+
for y in range(0, icon_pixmap_geometry.height):
133162
offset = y * row_width
134-
for x in range(0, geometry.width):
163+
for x in range(0, icon_pixmap_geometry.width):
135164
byte_offset = int(x / 8) + offset
136-
byte = int(pixmap_data[byte_offset])
165+
byte = int(icon_pixmap_data[byte_offset])
137166
byte = byte >> (x % 8)
138167
bit = byte & 0x1
139168
if bit:
140169
# Can not decide the light/dark theme from vmside :/
141170
icon_data.append(0xff7f7f7f)
142171
else:
143172
icon_data.append(0x0)
144-
elif geometry.depth == 24:
173+
elif icon_pixmap_geometry.depth == 24:
145174
# 24 bit per pixel icons (i.e. Xterm)
146-
# Technically this could handle other programs as well
175+
# Technically this could handle modern programs as well
176+
# However, _NET_WM_ICON is faster
147177
icon_data = struct.unpack(
148-
"%dI" % (len(pixmap_data) / 4),
149-
pixmap_data
178+
"%dI" % (len(icon_pixmap_data) / 4),
179+
icon_pixmap_data
150180
)
151181
icon_data = [d | 0xff000000 for d in icon_data]
152182
else:
153183
# Could not find 8 bit icons of that era to work with
154184
raise NoIconError()
155-
if mask_data and mask_geometry.depth == 1:
185+
if icon_mask_data and icon_mask_geometry.depth == 1:
156186
# 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):
187+
row_width = int(len(icon_mask_data) / icon_mask_geometry.height)
188+
for y in range(0, icon_mask_geometry.height):
159189
offset = y * row_width
160-
for x in range(0, geometry.width):
190+
for x in range(0, icon_mask_geometry.width):
161191
byte_offset = int(x/8) + offset
162-
byte = int(mask_data[byte_offset])
192+
byte = int(icon_mask_data[byte_offset])
163193
byte = byte >> (x % 8)
164194
bit = byte & 0x1
165-
pixel = x + y * geometry.height
195+
pixel = x + y * icon_mask_geometry.height
166196
if bit:
167197
icon_data[pixel] = icon_data[pixel] & 0xffffffff
168198
else:
169199
icon_data[pixel] = icon_data[pixel] & 0x00ffffff
170-
elif mask_data and mask_geometry.depth == 8:
200+
elif icon_mask_data and icon_mask_geometry.depth == 8:
171201
# Technically this is not tested (No X prog uses 8bit/pix mask)
172202
# At least not on Qubes OS 4.3 & default Xfwm4
173-
for y in range(0, geometry.height):
203+
for y in range(0, icon_mask_geometry.height):
174204
offset = y * row_width
175-
for x in range(0, geometry.width):
205+
for x in range(0, icon_mask_geometry.width):
176206
byte_offset = x + offset
177-
byte = int(mask_data[byte_offset])
207+
byte = int(icon_mask_data[byte_offset])
178208
pixmask = (byte < 24) | 0x00ffffff
179209
icon_data[pixel] = icon_data[pixel] & pixmask
180-
size = (geometry.width, geometry.height)
210+
size = (icon_pixmap_geometry.width, icon_pixmap_geometry.height)
181211
icons[size] = icon_data
182212
return icons
183213

214+
# We have sane _NET_WM_ICON Atom for modern programs
184215
# convert it later to a proper int array
185216
icon_data = icon.value.buf()
186217
if icon.bytes_after:

0 commit comments

Comments
 (0)