Skip to content

Commit e89207c

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 e89207c

File tree

1 file changed

+82
-52
lines changed

1 file changed

+82
-52
lines changed

window-icon-updater/icon-sender

Lines changed: 82 additions & 52 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,135 @@ 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 (xproto.BadWindow, xproto.WindowError, xproto.AccessError):
145+
raise NoIconError
146+
except Exception: # pylint: disable=bare-except
147+
raise NoIconError
124148

125149
# Finally we have the required data to construct icon
126-
if geometry.depth == 1:
150+
icons = {}
151+
if icon_pixmap_geometry.depth == 1:
127152
# 1 bit per pixel icons (i.e. xlogo, xeyes, xcalc, ...)
128153
icon_data = []
129154
# There might be trailing bytes at the end of each row since
130155
# 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):
156+
row_width = int(
157+
len(icon_pixmap_data) / icon_pixmap_geometry.height
158+
)
159+
for y in range(0, icon_pixmap_geometry.height):
133160
offset = y * row_width
134-
for x in range(0, geometry.width):
161+
for x in range(0, icon_pixmap_geometry.width):
135162
byte_offset = int(x / 8) + offset
136-
byte = int(pixmap_data[byte_offset])
163+
byte = int(icon_pixmap_data[byte_offset])
137164
byte = byte >> (x % 8)
138165
bit = byte & 0x1
139166
if bit:
140167
# Can not decide the light/dark theme from vmside :/
141168
icon_data.append(0xff7f7f7f)
142169
else:
143170
icon_data.append(0x0)
144-
elif geometry.depth == 24:
171+
elif icon_pixmap_geometry.depth == 24:
145172
# 24 bit per pixel icons (i.e. Xterm)
146-
# Technically this could handle other programs as well
173+
# Technically this could handle modern programs as well
174+
# However, _NET_WM_ICON is faster
147175
icon_data = struct.unpack(
148-
"%dI" % (len(pixmap_data) / 4),
149-
pixmap_data
176+
"%dI" % (len(icon_pixmap_data) / 4),
177+
icon_pixmap_data
150178
)
151179
icon_data = [d | 0xff000000 for d in icon_data]
152180
else:
153181
# Could not find 8 bit icons of that era to work with
154182
raise NoIconError()
155-
if mask_data and mask_geometry.depth == 1:
183+
if icon_mask_data and icon_mask_geometry.depth == 1:
156184
# 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):
185+
row_width = int(len(icon_mask_data) / icon_mask_geometry.height)
186+
for y in range(0, icon_mask_geometry.height):
159187
offset = y * row_width
160-
for x in range(0, geometry.width):
188+
for x in range(0, icon_mask_geometry.width):
161189
byte_offset = int(x/8) + offset
162-
byte = int(mask_data[byte_offset])
190+
byte = int(icon_mask_data[byte_offset])
163191
byte = byte >> (x % 8)
164192
bit = byte & 0x1
165-
pixel = x + y * geometry.height
193+
pixel = x + y * icon_mask_geometry.height
166194
if bit:
167195
icon_data[pixel] = icon_data[pixel] & 0xffffffff
168196
else:
169197
icon_data[pixel] = icon_data[pixel] & 0x00ffffff
170-
elif mask_data and mask_geometry.depth == 8:
198+
elif icon_mask_data and icon_mask_geometry.depth == 8:
171199
# Technically this is not tested (No X prog uses 8bit/pix mask)
172200
# At least not on Qubes OS 4.3 & default Xfwm4
173-
for y in range(0, geometry.height):
201+
row_width = int(len(icon_mask_data) / icon_mask_geometry.height)
202+
for y in range(0, icon_mask_geometry.height):
174203
offset = y * row_width
175-
for x in range(0, geometry.width):
204+
for x in range(0, icon_mask_geometry.width):
176205
byte_offset = x + offset
177-
byte = int(mask_data[byte_offset])
178-
pixmask = (byte < 24) | 0x00ffffff
206+
byte = int(icon_mask_data[byte_offset])
207+
pixmask = (byte << 24) | 0x00ffffff
179208
icon_data[pixel] = icon_data[pixel] & pixmask
180-
size = (geometry.width, geometry.height)
209+
size = (icon_pixmap_geometry.width, icon_pixmap_geometry.height)
181210
icons[size] = icon_data
182211
return icons
183212

213+
# We have sane _NET_WM_ICON Atom for modern programs
184214
# convert it later to a proper int array
185215
icon_data = icon.value.buf()
186216
if icon.bytes_after:

0 commit comments

Comments
 (0)