Coverage for /home/antoine/projects/xpra-git/dist/python3/lib64/python/xpra/platform/xposix/xdg_helper.py : 69%
Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# This file is part of Xpra.
2# Copyright (C) 2018-2020 Antoine Martin <antoine@xpra.org>
3# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
4# later version. See the file COPYING for details.
6"""
7Utility functions for loading xdg menus
8using python-xdg
9"""
11import os
12import re
13import sys
14import glob
15from io import BytesIO
16from typing import Generator as generator #@UnresolvedImport, @UnusedImport
17from threading import Lock
19from xpra.util import envbool, envint, print_nested_dict, first_time, engs, ellipsizer
20from xpra.os_util import load_binary_file, monotonic_time, OSEnvContext
21from xpra.log import Logger, add_debug_category
23log = Logger("exec", "menu")
25LOAD_GLOB = envbool("XPRA_XDG_LOAD_GLOB", True)
26EXPORT_ICONS = envbool("XPRA_XDG_EXPORT_ICONS", True)
27MAX_ICON_SIZE = envint("XPRA_XDG_MAX_ICON_SIZE", 65536)
28DEBUG_COMMANDS = os.environ.get("XPRA_XDG_DEBUG_COMMANDS", "").split(",")
30large_icons = []
32INKSCAPE_RE = b'\sinkscape:[a-zA-Z]*=["a-zA-Z0-9]*'
34def isvalidtype(v):
35 if isinstance(v, (list, tuple, generator)):
36 if not v:
37 return True
38 return all(isvalidtype(x) for x in v)
39 return isinstance(v, (bytes, str, bool, int))
41def export(entry, properties):
42 name = entry.getName()
43 props = {}
44 if any(x and name.lower().find(x.lower())>=0 for x in DEBUG_COMMANDS):
45 l = log.info
46 else:
47 l = log
48 for prop in properties:
49 fn_name = "get%s" % prop
50 try:
51 fn = getattr(entry, fn_name, None)
52 if fn:
53 v = fn()
54 if isinstance(v, (list, tuple, generator)):
55 l("%s=%s (%s)", prop, v, type(x for x in v))
56 else:
57 l("%s=%s (%s)", prop, v, type(v))
58 if not isvalidtype(v):
59 log.warn("Warning: found invalid type for '%s': %s", v, type(v))
60 else:
61 props[prop] = v
62 except Exception as e:
63 l("error on %s", entry, exc_info=True)
64 log.error("Error parsing '%s': %s", prop, e)
65 l("properties(%s)=%s", name, props)
66 #load icon binary data:
67 icon = props.get("Icon")
68 icondata = load_icon_from_theme(icon)
69 if icondata:
70 bdata, ext = icondata
71 props["IconData"] = bdata
72 props["IconType"] = ext
73 return props
75_Rsvg = None
76def load_Rsvg():
77 global _Rsvg
78 if _Rsvg is None:
79 import gi
80 try:
81 gi.require_version('Rsvg', '2.0')
82 from gi.repository import Rsvg
83 log("load_Rsvg() Rsvg=%s", Rsvg)
84 _Rsvg = Rsvg
85 except (ValueError, ImportError) as e:
86 if first_time("no-rsvg"):
87 log.warn("Warning: cannot resize svg icons,")
88 log.warn(" the Rsvg bindings were not found:")
89 log.warn(" %s", e)
90 _Rsvg = False
91 return _Rsvg
94def load_icon_from_file(filename):
95 log("load_icon_from_file(%s)", filename)
96 if filename.endswith("xpm"):
97 from PIL import Image
98 try:
99 img = Image.open(filename)
100 buf = BytesIO()
101 img.save(buf, "PNG")
102 pngicondata = buf.getvalue()
103 buf.close()
104 return pngicondata, "png"
105 except ValueError as e:
106 log("Image.open(%s)", filename, exc_info=True)
107 except Exception as e:
108 log("Image.open(%s)", filename, exc_info=True)
109 log.error("Error loading '%s':", filename)
110 log.error(" %s", e)
111 #fallback to PixbufLoader:
112 try:
113 from xpra.gtk_common.gtk_util import pixbuf_save_to_memory
114 data = load_binary_file(filename)
115 from gi.repository import GdkPixbuf
116 loader = GdkPixbuf.PixbufLoader()
117 loader.write(data)
118 loader.close()
119 pixbuf = loader.get_pixbuf()
120 pngicondata = pixbuf_save_to_memory(pixbuf, "png")
121 return pngicondata, "png"
122 except Exception as e:
123 log("pixbuf error loading %s", filename, exc_info=True)
124 log.error("Error loading '%s':", filename)
125 log.error(" %s", e)
126 icondata = load_binary_file(filename)
127 if not icondata:
128 return None
129 if filename.endswith("svg") and len(icondata)>MAX_ICON_SIZE//2:
130 #try to resize it
131 size = len(icondata)
132 pngdata = svg_to_png(filename, icondata)
133 if pngdata:
134 log("reduced size of SVG icon %s, from %i bytes to %i bytes as PNG",
135 filename, size, len(pngdata))
136 icondata = pngdata
137 filename = filename[:-3]+"png"
138 log("got icon data from '%s': %i bytes", filename, len(icondata))
139 if len(icondata)>MAX_ICON_SIZE and first_time("icon-size-warning-%s" % filename):
140 global large_icons
141 large_icons.append((filename, len(icondata)))
142 return icondata, os.path.splitext(filename)[1].lstrip(".")
144def svg_to_png(filename, icondata, w=128, h=128):
145 Rsvg = load_Rsvg()
146 if not Rsvg:
147 return None
148 try:
149 import cairo
150 #'\sinkscape:[a-zA-Z]*=["a-zA-Z0-9]*'
151 img = cairo.ImageSurface(cairo.FORMAT_ARGB32, 128, 128)
152 ctx = cairo.Context(img)
153 handle = Rsvg.Handle.new_from_data(icondata)
154 handle.render_cairo(ctx)
155 buf = BytesIO()
156 img.write_to_png(buf)
157 icondata = buf.getvalue()
158 buf.close()
159 return icondata
160 except Exception:
161 log("svg_to_png%s", (icondata, w, h), exc_info=True)
162 if re.findall(INKSCAPE_RE, icondata):
163 #try again after stripping the bogus inkscape attributes
164 #as some rsvg versions can't handle that (ie: Debian Bullseye)
165 icondata = re.sub(INKSCAPE_RE, b"", icondata)
166 return svg_to_png(filename, icondata, w, h)
167 log.error("Error: failed to convert svg icon")
168 log.error(" '%s':", filename)
169 log.error(" %i bytes, %s", len(icondata), ellipsizer(icondata))
172def load_icon_from_theme(icon_name, theme=None):
173 if not EXPORT_ICONS or not icon_name:
174 return None
175 from xdg import IconTheme
176 filename = IconTheme.getIconPath(icon_name, theme=theme)
177 if not filename:
178 return None
179 return load_icon_from_file(filename)
181def load_glob_icon(submenu_data, main_dirname="categories"):
182 if not LOAD_GLOB or not EXPORT_ICONS:
183 return None
184 #doesn't work with IconTheme.getIconPath,
185 #so do it the hard way:
186 from xdg import IconTheme
187 icondirs = getattr(IconTheme, "icondirs", [])
188 if not icondirs:
189 return None
190 for x in ("Icon", "Name", "GenericName"):
191 name = submenu_data.get(x)
192 if name:
193 icondata = find_icon(main_dirname, icondirs, name)
194 if icondata:
195 return icondata
196 return None
198def find_icon(main_dirname, icondirs, name):
199 extensions = ("png", "svg", "xpm")
200 pathnames = []
201 for dn in (main_dirname, "*"):
202 for d in icondirs:
203 for ext in extensions:
204 pathnames += [
205 os.path.join(d, "*", "*", dn, "%s.%s" % (name, ext)),
206 os.path.join(d, "*", dn, "*", "%s.%s" % (name, ext)),
207 ]
208 for pathname in pathnames:
209 filenames = glob.glob(pathname)
210 log("glob(%s) matches %i filenames", pathname, len(filenames))
211 if filenames:
212 for f in filenames:
213 icondata = load_icon_from_file(f)
214 if icondata:
215 log("found icon for '%s' with glob '%s': %s", name, pathname, f)
216 return icondata
217 return None
220def load_xdg_entry(de):
221 #not exposed:
222 #"MimeType" is an re
223 #"Version" is a float
224 props = export(de, (
225 "Type", "VersionString", "Name", "GenericName", "NoDisplay",
226 "Comment", "Icon", "Hidden", "OnlyShowIn", "NotShowIn",
227 "Exec", "TryExec", "Path", "Terminal", "MimeTypes",
228 "Categories", "StartupNotify", "StartupWMClass", "URL",
229 ))
230 if de.getTryExec():
231 try:
232 command = de.findTryExec()
233 except Exception:
234 command = de.getTryExec()
235 else:
236 command = de.getExec()
237 props["command"] = command
238 icondata = props.get("IconData")
239 if not icondata:
240 #try harder:
241 icondata = load_glob_icon(de, "apps")
242 if icondata:
243 bdata, ext = icondata
244 props["IconData"] = bdata
245 props["IconType"] = ext
246 return props
248def load_xdg_menu(submenu):
249 #log.info("submenu %s: %s, %s", name, submenu, dir(submenu))
250 submenu_data = export(submenu, [
251 "Name", "GenericName", "Comment",
252 "Path", "Icon",
253 ])
254 icondata = submenu_data.get("IconData")
255 if not icondata:
256 #try harder:
257 icondata = load_glob_icon(submenu_data, "categories")
258 if icondata:
259 bdata, ext = icondata
260 submenu_data["IconData"] = bdata
261 submenu_data["IconType"] = ext
262 entries_data = submenu_data.setdefault("Entries", {})
263 for entry in submenu.getEntries():
264 #can we have more than 2 levels of submenus?
265 from xdg.Menu import MenuEntry
266 if isinstance(entry, MenuEntry):
267 de = entry.DesktopEntry
268 name = de.getName()
269 try:
270 entries_data[name] = load_xdg_entry(de)
271 except Exception as e:
272 log("load_xdg_menu(%s)", submenu, exc_info=True)
273 log.error("Error loading desktop entry '%s':", name)
274 log.error(" %s", e)
275 return submenu_data
277def remove_icons(menu_data):
278 def noicondata(d):
279 return dict((k,v) for k,v in d.items() if k!="IconData")
280 filt = {}
281 for category, cdef in menu_data.items():
282 fcdef = dict(cdef)
283 entries = dict(fcdef.get("Entries", {}))
284 for entry, edef in tuple(entries.items()):
285 entries[entry] = noicondata(edef)
286 fcdef["Entries"] = entries
287 filt[category] = fcdef
288 return filt
291load_lock = Lock()
292xdg_menu_data = None
293def load_xdg_menu_data(force_reload=False):
294 global xdg_menu_data, large_icons
295 with load_lock:
296 if not xdg_menu_data or force_reload:
297 large_icons = []
298 start = monotonic_time()
299 xdg_menu_data = do_load_xdg_menu_data()
300 end = monotonic_time()
301 if xdg_menu_data:
302 l = sum(len(x) for x in xdg_menu_data.values())
303 log.info("%s %i start menu entries from %i sub-menus in %.1f seconds",
304 "reloaded" if force_reload else "loaded", l, len(xdg_menu_data), end-start)
305 if large_icons:
306 log.warn("Warning: found %i large icon%s:", len(large_icons), engs(large_icons))
307 for filename, size in large_icons:
308 log.warn(" '%s' (%i KB)", filename, size//1024)
309 log.warn(" more bandwidth will be used by the start menu data")
310 return xdg_menu_data
312def do_load_xdg_menu_data():
313 try:
314 from xdg.Menu import parse, Menu
315 except ImportError:
316 log("do_load_xdg_menu_data()", exc_info=True)
317 if first_time("no-python-xdg"):
318 log.warn("Warning: cannot use application menu data:")
319 log.warn(" no python-xdg module")
320 return None
321 menu = None
322 error = None
323 with OSEnvContext():
324 #see ticket #2340,
325 #invalid values for XDG_CONFIG_DIRS can cause problems,
326 #so try unsetting it if we can't load the menus with it:
327 for cd in (False, True):
328 if cd:
329 os.environ.pop("XDG_CONFIG_DIRS", None)
330 #see ticket #2174,
331 #things may break if the prefix is not set,
332 #and it isn't set when logging in via ssh
333 for prefix in (None, "", "gnome-", "kde-"):
334 if prefix is not None:
335 os.environ["XDG_MENU_PREFIX"] = prefix
336 try:
337 menu = parse()
338 break
339 except Exception as e:
340 log("do_load_xdg_menu_data()", exc_info=True)
341 error = e
342 menu = None
343 if menu is None:
344 if error:
345 log.error("Error parsing xdg menu data:")
346 log.error(" %s", error)
347 log.error(" this is either a bug in python-xdg,")
348 log.error(" or an invalid system menu configuration")
349 return None
350 menu_data = {}
351 for submenu in menu.getEntries():
352 if isinstance(submenu, Menu) and submenu.Visible:
353 name = submenu.getName()
354 try:
355 menu_data[name] = load_xdg_menu(submenu)
356 except Exception as e:
357 log("load_xdg_menu_data()", exc_info=True)
358 log.error("Error loading submenu '%s':", name)
359 log.error(" %s", e)
360 return menu_data
363def main():
364 from xpra.platform import program_context
365 with program_context("XDG-Menu-Helper", "XDG Menu Helper"):
366 for x in list(sys.argv):
367 if x in ("-v", "--verbose"):
368 sys.argv.remove(x)
369 add_debug_category("menu")
370 log.enable_debug()
371 def icon_fmt(icondata):
372 return "%i bytes" % len(icondata)
373 if len(sys.argv)>1:
374 for x in sys.argv[1:]:
375 if os.path.isabs(x):
376 v = load_icon_from_file(x)
377 print("load_icon_from_file(%s)=%s" % (x, v))
378 else:
379 menu = load_xdg_menu_data()
380 if menu:
381 print_nested_dict(menu, vformat={"IconData" : icon_fmt})
382 else:
383 print("no menu data found")
384 return 0
386if __name__ == "__main__":
387 r = main()
388 sys.exit(r)