Skip to content

Commit 34ab7a5

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 34ab7a5

File tree

1 file changed

+85
-52
lines changed

1 file changed

+85
-52
lines changed

window-icon-updater/icon-sender

Lines changed: 85 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,138 @@ 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+
xproto.BadWindow,
146+
xproto.WindowError,
147+
xproto.AccessError,
148+
xproto.DrawableError
149+
):
150+
raise NoIconError
124151

125152
# Finally we have the required data to construct icon
126-
if geometry.depth == 1:
153+
icons = {}
154+
if icon_pixmap_geometry.depth == 1:
127155
# 1 bit per pixel icons (i.e. xlogo, xeyes, xcalc, ...)
128156
icon_data = []
129157
# There might be trailing bytes at the end of each row since
130158
# 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):
159+
row_width = int(
160+
len(icon_pixmap_data) / icon_pixmap_geometry.height
161+
)
162+
for y in range(0, icon_pixmap_geometry.height):
133163
offset = y * row_width
134-
for x in range(0, geometry.width):
164+
for x in range(0, icon_pixmap_geometry.width):
135165
byte_offset = int(x / 8) + offset
136-
byte = int(pixmap_data[byte_offset])
166+
byte = int(icon_pixmap_data[byte_offset])
137167
byte = byte >> (x % 8)
138168
bit = byte & 0x1
139169
if bit:
140170
# Can not decide the light/dark theme from vmside :/
141171
icon_data.append(0xff7f7f7f)
142172
else:
143173
icon_data.append(0x0)
144-
elif geometry.depth == 24:
174+
elif icon_pixmap_geometry.depth == 24:
145175
# 24 bit per pixel icons (i.e. Xterm)
146-
# Technically this could handle other programs as well
176+
# Technically this could handle modern programs as well
177+
# However, _NET_WM_ICON is faster
147178
icon_data = struct.unpack(
148-
"%dI" % (len(pixmap_data) / 4),
149-
pixmap_data
179+
"%dI" % (len(icon_pixmap_data) / 4),
180+
icon_pixmap_data
150181
)
151182
icon_data = [d | 0xff000000 for d in icon_data]
152183
else:
153184
# Could not find 8 bit icons of that era to work with
154185
raise NoIconError()
155-
if mask_data and mask_geometry.depth == 1:
186+
if icon_mask_data and icon_mask_geometry.depth == 1:
156187
# 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):
188+
row_width = int(len(icon_mask_data) / icon_mask_geometry.height)
189+
for y in range(0, icon_mask_geometry.height):
159190
offset = y * row_width
160-
for x in range(0, geometry.width):
191+
for x in range(0, icon_mask_geometry.width):
161192
byte_offset = int(x/8) + offset
162-
byte = int(mask_data[byte_offset])
193+
byte = int(icon_mask_data[byte_offset])
163194
byte = byte >> (x % 8)
164195
bit = byte & 0x1
165-
pixel = x + y * geometry.height
196+
pixel = x + y * icon_mask_geometry.height
166197
if bit:
167198
icon_data[pixel] = icon_data[pixel] & 0xffffffff
168199
else:
169200
icon_data[pixel] = icon_data[pixel] & 0x00ffffff
170-
elif mask_data and mask_geometry.depth == 8:
201+
elif icon_mask_data and icon_mask_geometry.depth == 8:
171202
# Technically this is not tested (No X prog uses 8bit/pix mask)
172203
# At least not on Qubes OS 4.3 & default Xfwm4
173-
for y in range(0, geometry.height):
204+
row_width = int(len(icon_mask_data) / icon_mask_geometry.height)
205+
for y in range(0, icon_mask_geometry.height):
174206
offset = y * row_width
175-
for x in range(0, geometry.width):
207+
for x in range(0, icon_mask_geometry.width):
176208
byte_offset = x + offset
177-
byte = int(mask_data[byte_offset])
178-
pixmask = (byte < 24) | 0x00ffffff
209+
byte = int(icon_mask_data[byte_offset])
210+
pixmask = (byte << 24) | 0x00ffffff
179211
icon_data[pixel] = icon_data[pixel] & pixmask
180-
size = (geometry.width, geometry.height)
212+
size = (icon_pixmap_geometry.width, icon_pixmap_geometry.height)
181213
icons[size] = icon_data
182214
return icons
183215

216+
# We have sane _NET_WM_ICON Atom for modern programs
184217
# convert it later to a proper int array
185218
icon_data = icon.value.buf()
186219
if icon.bytes_after:

0 commit comments

Comments
 (0)