xdg-xmenu.py
Dependencies:
python3
imagemagick
#!/usr/bin/env python3
#
# Original version by OliverLew: https://github.com/OliverLew/xdg-xmenu
#
from pathlib import Path
import argparse
import os, os.path
import re
import shutil
import subprocess
import sys
import time
XDG_DATA_HOME = os.getenv('XDG_DATA_HOME', os.path.join(os.getenv('HOME'), '.local/share'))
XDG_CONFIG_HOME = os.getenv('XDG_CONFIG_HOME', os.path.join(os.getenv('HOME'), '.config'))
XDG_DATA_DIRS = set(os.getenv('XDG_DATA_DIRS', '/usr/share:/usr/local/share').split(':'))
APPLICATION_DIRS = set([os.path.join(dir, 'applications') for dir in XDG_DATA_DIRS.copy()]\
+ [os.path.join(XDG_DATA_HOME, 'applications')])
CACHE_DIR = os.path.join(os.getenv('XDG_CACHE_HOME',
os.path.join(os.getenv('HOME'), '.cache')), os.path.basename(__file__))
TERMINAL = os.getenv('TERMINAL', 'xterm')
EXECFILTER = re.compile('%[a-zA-Z]')
ICON_SIZE = 24
IMAGEMAGICK_OPTIONS = ['-background', 'none', '-size', '{0}x{0}'.format(ICON_SIZE)]
MULTIPLE_CATEGORIES = True
PRINT_IMAGES = True
SORT_BY_CATEGORY = True
COMPILE_IMAGES = True
FORCE_REFRESH_CACHE = False
EXPIRY_DAYS = 7
EXPIRY_TIME = EXPIRY_DAYS * 24 * 60 * 60
UPDATEEXPIRED = False
USER_ICON_THEME = ''
imagefiles = []
icontheme = None
applications = {}
categories = {}
class Category:
label = ''
iconname = ''
iconpath = ''
match = re.compile('a^')
apps = {}
def format(self):
out = ''
if PRINT_IMAGES and self.iconpath != '':
out += 'IMG:{}\t'.format(self.iconpath)
out += self.label
return out
class Application:
categories = set()
execute = ''
genname = ''
iconname = ''
iconpath = ''
name = ''
terminal = False
nodisplay = False
onlyshowin = ''
def format(self):
out = ''
if PRINT_IMAGES and self.iconpath != '':
out += 'IMG:{}\t'.format(self.iconpath)
out += self.name
if self.genname != '':
out += ' ({})'.format(self.genname)
if self.terminal:
out += '\t{} -e {}'.format(TERMINAL, self.execute)
else:
out += '\t{}'.format(self.execute)
return out
def add_category(label, iconname, matchstr):
newcat = Category()
newcat.label = label
newcat.apps = {}
newcat.iconname = iconname
newcat.match = re.compile(matchstr)
categories[label] = newcat
def cache_is_expired():
now = time.time()
if not os.path.isdir(CACHE_DIR):
return False
for filename in os.listdir(CACHE_DIR):
filestamp = os.stat(os.path.join(CACHE_DIR, filename)).st_mtime
filecompare = now - EXPIRY_TIME
if filestamp >= filecompare:
return False
return True
def dict_to_application(dictionary:dict):
app = Application()
app.name = dictionary['Name']
app.execute = re.sub(EXECFILTER, '', dictionary['Exec'])
if 'GenericName' in dictionary:
app.genname = dictionary['GenericName']
if 'Icon' in dictionary and PRINT_IMAGES:
app.iconname = os.path.basename(dictionary['Icon'])
set_icon(app)
if 'Categories' in dictionary:
app.categories = dictionary['Categories'].split(';')
if 'Terminal' in dictionary:
app.terminal = dictionary['Terminal'] in ('true', 'True')
if 'NoDisplay' in dictionary:
app.nodisplay = dictionary['NoDisplay'] in ('true', 'True')
if 'OnlyShowIn' in dictionary:
app.onlyshowin = dictionary['OnlyShowIn'].split(';')
if SORT_BY_CATEGORY:
added = False
for appcat in app.categories:
if appcat in categories:
if not app in categories[appcat].apps:
categories[appcat].apps[app.name] = app
added = True
else:
for cat in categories.values():
if cat.match.match(appcat):
cat.apps[app.name] = app
added = True
break
if added and not MULTIPLE_CATEGORIES:
break
if not added:
categories['Other'].apps[app.name] = app
return app
def get_image_files():
filelist = []
for icondir in [os.path.join(dd, 'icons') for dd in XDG_DATA_DIRS]:
for root, dirs, files in os.walk(icondir):
for f in files:
file = os.path.join(root, f)
if os.path.isfile(file):
filelist.append(file)
return filelist
def get_icon_theme():
if USER_ICON_THEME != '':
return USER_ICON_THEME
path = os.path.join(XDG_CONFIG_HOME, 'gtk-3.0/settings.ini')
if os.path.isfile(path):
file = open(path, 'r')
for line in file.readlines():
if 'gtk-icon-theme-name' in line:
return line.strip().split('=')[1]
return ''
def load_desktop_file(filepath):
file = open(filepath, 'r')
application_dict = {}
for line in file.readlines():
try:
key, value = line.strip().split('=', 1)
# cancel before additional options are added
if key in application_dict:
break
application_dict[key] = value.strip()
except: pass
try:
application = dict_to_application(application_dict)
applications[application.name] = application
except KeyError: pass
def load_desktop_files():
for directory in APPLICATION_DIRS:
if not os.path.isdir(directory):
continue
for subfile in os.listdir(directory):
filepath = os.path.join(directory, subfile)
if os.path.islink(filepath):
filepath = os.readlink(filepath)
if os.path.isfile(filepath):
load_desktop_file(filepath)
def load_icon(name, ext, path):
if not os.path.isdir(CACHE_DIR):
os.makedirs(CACHE_DIR)
dest = os.path.join(CACHE_DIR, name + '.png')
if ext in ('png'):
shutil.copyfile(path, dest)
else:
proc = subprocess.Popen(['convert'] + IMAGEMAGICK_OPTIONS + [path, dest], stdout=subprocess.PIPE)
proc.wait()
if os.path.isfile(dest):
return dest
else:
return ''
def set_icon(entity):
global imagefiles, icontheme
name = entity.iconname
if not FORCE_REFRESH_CACHE and (os.path.isfile(os.path.join(CACHE_DIR, name + '.png'))):
entity.iconpath = os.path.join(CACHE_DIR, name + '.png')
elif not FORCE_REFRESH_CACHE and (os.path.isfile(os.path.join(CACHE_DIR, name + '.notfound'))):
pass
elif COMPILE_IMAGES or FORCE_REFRESH_CACHE:
image_found = False
if imagefiles == []:
imagefiles = get_image_files()
if icontheme == None:
icontheme = get_icon_theme()
for imagefile in imagefiles:
for ext in ['png', 'svg']:
if imagefile.endswith('/{}.{}'.format(name, ext)) \
and (entity.iconpath == '' or (type(icontheme) == str and icontheme in imagefile)):
entity.iconpath = load_icon(name, ext, imagefile)
print("{}.png <= {}".format(name, imagefile), file=sys.stderr)
image_found = True
if not image_found:
if not os.path.exists(CACHE_DIR):
os.makedirs(CACHE_DIR)
Path(os.path.join(CACHE_DIR, name + '.notfound')).touch()
print("=> {}.notfound".format(name), file=sys.stderr)
def format_xmenu(applications):
if SORT_BY_CATEGORY:
if PRINT_IMAGES:
for cat in categories.values():
set_icon(cat)
for cat in categories.values():
visible_apps = [app for app in cat.apps.values() if not app.nodisplay and app.onlyshowin == '']
if len(visible_apps) > 0:
print(cat.format())
for app in sorted(visible_apps, key=lambda x: x.name):
print('\t{}'.format(app.format()))
else:
for app in sorted(applications, key=lambda x: x.name):
print(app.format())
def parse_args():
parser = argparse.ArgumentParser(description='Generate application menu in xmenu format')
parser.add_argument('-a', '--applications', help='don\'t sort applications by category',
action='store_false', dest='sort_by_category')
parser.add_argument('-e', '--expired', help='refresh image cache if older than {} days'.format(EXPIRY_DAYS),
action='store_true', dest='updateexpired')
parser.add_argument('-f', '--force', help='force refresh image cache',
action='store_true', dest='force_refresh_cache')
parser.add_argument('-i', '--icontheme', help='select a custom icon theme', nargs=1, type=str,
default=[''], metavar='THEME', action='store', dest='user_icon_theme')
parser.add_argument('-l', '--lazy', help='don\'t compile images on demand (this is way faster)',
action='store_false', dest='compile_images')
parser.add_argument('-m', '--multiple', help='add applications to multiple categories',
action='store_true', dest='multiple_categories')
parser.add_argument('-t', '--text', help='no image output (implies -l)',
action='store_false', dest='print_images')
return parser.parse_args()
add_category('Multimedia', 'applications-multimedia', '(Audio|Video).*')
add_category('Development', 'applications-development', 'Development')
add_category('Education', 'applications-education', 'Education')
add_category('Games', 'applications-games', 'Game')
add_category('Graphics', 'applications-graphics', 'Graphics')
add_category('Internet', 'applications-internet', 'Network')
add_category('Office', 'applications-office', 'Office')
add_category('Science', 'applications-science', 'Science')
add_category('Settings', 'preferences-desktop', 'Settings')
add_category('System', 'applications-system', 'System')
add_category('Accessories', 'applications-accessories', 'Utility')
add_category('Others', 'applications-other', 'a^')
if __name__ == '__main__':
args = parse_args()
MULTIPLE_CATEGORIES = args.multiple_categories
SORT_BY_CATEGORY = args.sort_by_category
PRINT_IMAGES = args.print_images
COMPILE_IMAGES = args.compile_images and PRINT_IMAGES
UPDATEEXPIRED = args.updateexpired
FORCE_REFRESH_CACHE = args.force_refresh_cache or (UPDATEEXPIRED and cache_is_expired())
USER_ICON_THEME = args.user_icon_theme[0]
load_desktop_files()
format_xmenu(list(applications.values()))