331 lines
12 KiB
Python
Executable File
331 lines
12 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
#
|
|
# Version 3.0.0
|
|
# Revised: onuronsekiz (overlord)
|
|
# Original Author: rmoe (v1.1.7)
|
|
#
|
|
# / revisions and additions:
|
|
#
|
|
# - recoded for python 3.9+
|
|
# - menu sort for both categories and programs
|
|
# - finding all possible icons by searching deeply in themes
|
|
# - icon search algorithm for faster approach
|
|
# - desktop item ignored if Exec command not found in system
|
|
# - automatic and direct theme selection if possible
|
|
# - flatpak applications support
|
|
# - duplicate icon handling
|
|
#
|
|
# ----- config ---
|
|
|
|
import subprocess, glob, os
|
|
|
|
userhome = os.path.expanduser('~')
|
|
applications_dirs = ("/usr/share/applications", userhome + "/.local/share/applications","/var/lib/flatpak/exports/share/applications")
|
|
image_dir_base = ("/usr/share", "/var/lib/flatpak/exports/share") # without "pixmaps" -/usr/local/share in FreeBSD, /usr/share on linux
|
|
try: #automatic theme selection
|
|
with open(userhome + "/.gtkrc-2.0", 'r') as readobj:
|
|
for line in readobj:
|
|
if "gtk-icon-theme-name" in line:
|
|
selected_theme=line.split("\"")[1]
|
|
except IOError:
|
|
selected_theme = "Adwaita" #fallback theme
|
|
|
|
selected_theme = "Adwaita" # direct theme selection, don't make it hicolor. ***** SOME DISTRIBUTIONS REQUIRES THIS OPTION UNCOMMENTED.
|
|
application_groups = ("AudioVideo", "Development", "Editors", "Engineering", "Games", "Graphics", "Internet", "Multimedia", "Office", "Other", "Settings", "System", "Utilities") # enter here new category as you wish, it will be sorted
|
|
group_aliases = {"Audio":"Multimedia","Video":"Multimedia","AudioVideo":"Multimedia","Network":"Internet","Game":"Games", "Utility":"Utilities", "Development":"Editors","GTK":"", "GNOME":""}
|
|
ignoreList = ("gtk3-icon-browser","evince-previewer", "Ted", "wingide3.2", "python3.4", "feh","xfce4-power-manager-settings", "picom","compton","yad-icon-browser" )
|
|
prefixes = ("legacy","categories","apps","devices","mimetypes","places","preferences","actions", "status","emblems") #added for prefered icon dirs and sizes. could be gathered automatically but wouldn't be sorted like this
|
|
iconSizes = ("48","32","24","16","48x48","40x40","36x36","32x32","24x24","64x64","72x72","96x96","16x16","128x128","256x256","scalable","apps","symbolic")
|
|
terminal_string = "xfce4-terminal -e " # your favourites terminal exec string
|
|
simpleOBheader = True # print full xml style OB header
|
|
# --- End of user config ---
|
|
|
|
#constants and list for icon list generating
|
|
image_file_prefix = (".png", ".svg", ".xpm")
|
|
image_cat_prefix = ("applications-", "accessories-dictionary", "accessories-text-editor","preferences-desktop.","audio-speakers")
|
|
iconThemes=os.listdir(image_dir_base[0]+"/icons")
|
|
tmplst=[s for s in iconThemes if selected_theme in s]
|
|
selected_theme = iconThemes[0] if tmplst == [] else tmplst[0]
|
|
iconThemes.sort(key=str.lower)
|
|
#iconThemes = ("hicolor", "breeze", "Adwaita", "Papirus", "Tango") #you can manually enter icon names here with your own sorting
|
|
iconThemes.remove(selected_theme)
|
|
iconThemes.remove('hicolor') if 'hicolor' in iconThemes else False
|
|
iconThemes.insert(0, selected_theme) if selected_theme != 'hicolor' else False
|
|
iconThemes.insert(0, "hicolor")
|
|
iconList=[]
|
|
|
|
#getting icons to lists for faster menu generate
|
|
def addIconsToList(List, theme): # skip to next icon theme if any icon couldn't found on current
|
|
for path in reversed(image_dir_base):
|
|
for prfx in prefixes:
|
|
for size in iconSizes:
|
|
tmp = path + "/icons/" + theme + "/" + size + "/" + prfx
|
|
if theme == "breeze" or theme == "breeze-dark":
|
|
tmp = path + "/icons/" + theme + "/" + prfx + "/" + size
|
|
try:
|
|
List.extend(tmp + "/" + x for x in os.listdir(tmp) if x.lower().endswith(image_file_prefix))
|
|
except IOError:
|
|
continue
|
|
return List
|
|
|
|
def which(program): #check if program exist
|
|
def is_exe(fpath):
|
|
return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
|
|
fpath, fname = os.path.split(program)
|
|
if fpath:
|
|
if is_exe(program):
|
|
return program
|
|
else:
|
|
for path in os.environ["PATH"].split(os.pathsep):
|
|
exe_file = os.path.join(path, program)
|
|
if is_exe(exe_file):
|
|
return exe_file
|
|
return None
|
|
|
|
class dtItem(object):
|
|
def __init__(self, fName):
|
|
self.fileName = fName
|
|
self.Name = ""
|
|
self.Comment = ""
|
|
self.Exec = ""
|
|
self.Terminal = None
|
|
self.Type = ""
|
|
self.Icon = ""
|
|
self.Categories = ()
|
|
|
|
def addName(self, data):
|
|
self.Name = xescape(data)
|
|
|
|
def addComment(self, data):
|
|
self.Comment = data
|
|
|
|
def addExec(self, data):
|
|
if len(data) > 3 and data[-2] == '%': # get rid of filemanager arguments in dt files
|
|
data = data[:-2].strip()
|
|
self.Exec = data
|
|
|
|
def addIcon(self, data):
|
|
self.Icon = ""
|
|
if image_cat_prefix == "":
|
|
return
|
|
image_dir = image_dir_base[0] + "/pixmaps/"
|
|
di = data.strip()
|
|
if len(di) < 3:
|
|
#"Error in %s: Invalid or no icon '%s'" % (self.fileName, di)
|
|
return
|
|
dix = di.find("/") # is it a full path?
|
|
if dix >= 0 and dix <= 2: # yes, its a path (./path or ../path or /path ...)
|
|
self.Icon = di
|
|
return
|
|
#else a short name like "myapp"
|
|
tmp = glob.glob(image_dir + di + ".*")
|
|
if len(tmp) == 0: #if there is not correct icon in pixmap, check for icon theme
|
|
for theme in iconThemes:
|
|
tmp=[s for s in iconList if di in s] #check program name in icon list
|
|
if len(tmp) > 0:
|
|
break # end loop if found
|
|
else:
|
|
addIconsToList(iconList, theme)
|
|
if len(tmp) == 1 and tmp[0] != "/":
|
|
self.Icon = tmp[0]
|
|
if len(tmp) > 1: # if there are duplicated icons take one that has the shortest name
|
|
temp=tmp[0] # assign first item to a temp path
|
|
flen=len(temp.split("/")[-1]) # split filepath with "/" and take last element of list
|
|
for fpath in tmp: # check filepath list for shortest filename
|
|
tlen=len(fpath.split("/")[-1]) # split filepath with / and take last element of list
|
|
if tlen<flen: # replace icon path with shorter filename path
|
|
flen=tlen # reallocate shortest filename length
|
|
temp=fpath # reallocate temp path
|
|
self.Icon = temp
|
|
return
|
|
|
|
def addTerminal(self, data):
|
|
if data == "True" or data == "true":
|
|
self.Terminal = True
|
|
else:
|
|
self.Terminal = False
|
|
|
|
def addType(self, data):
|
|
self.Type = data
|
|
|
|
def addCategories(self, data):
|
|
self.Categories = data
|
|
|
|
def getCatIcon(cat):
|
|
theme = selected_theme
|
|
cat = image_cat_prefix[0] + cat.lower()
|
|
#some category icons don't appear, for major icon themes, fixes for them
|
|
if theme == "breeze" or theme == "breeze-dark":
|
|
if cat == "applications-editors":
|
|
cat = "applications-education-language"
|
|
if cat == "applications-settings":
|
|
cat = "applications-development"
|
|
if theme != "Adwaita" and theme != "gnome":
|
|
if cat == "applications-editors":
|
|
cat = "applications-development"
|
|
if theme == "Adwaita":
|
|
if cat == "applications-multimedia":
|
|
cat = "audio-speakers"
|
|
if theme == "Adwaita" or theme == "Papirus" or theme == "gnome":
|
|
if cat == "applications-editors":
|
|
cat = "accessories-text-editor"
|
|
if cat == "applications-settings":
|
|
cat = "preferences-desktop"
|
|
if cat == "applications-education":
|
|
cat = "accessories-dictionary"
|
|
if theme != "breeze" or theme != "breeze-dark":
|
|
if cat == "applications-settings":
|
|
cat = "preferences-desktop"
|
|
if theme == "Tango":
|
|
if cat == "applications-utilities":
|
|
cat = "applications-accessories"
|
|
tmp = [s for s in iconList if cat in s]
|
|
if len(tmp) > 0:
|
|
return tmp[0]
|
|
return ""
|
|
|
|
def xescape(s):
|
|
Rep = {"&":"&", "<":"<", ">":">", "'":"'", "\"":"""}
|
|
for p in ("&", "<", ">", "'","\""):
|
|
sl = len(s); last = -1
|
|
while last < sl:
|
|
i = s.find(p, last+1)
|
|
if i < 0:
|
|
done = True
|
|
break
|
|
last = i
|
|
l = s[:i]
|
|
r = s[i+1:]
|
|
s = l + Rep[p] + r
|
|
return s
|
|
|
|
def process_category(cat, curCats, aliases=group_aliases, appGroups=application_groups):
|
|
# first process aliases
|
|
if aliases.__contains__(cat):
|
|
if aliases[cat] == "":
|
|
return "" # ignore this one
|
|
cat = aliases[cat]
|
|
if cat in appGroups and cat not in curCats: # valid categories only and no doublettes, please
|
|
curCats.append(cat)
|
|
return cat
|
|
return ""
|
|
|
|
def process_dtfile(dtf, catDict): # process this file & extract relevant info
|
|
active = False # parse only after "[Desktop Entry]" line
|
|
fh = open(dtf, "r")
|
|
lines = fh.readlines()
|
|
this = dtItem(dtf)
|
|
for l in lines:
|
|
l = l.strip()
|
|
if l == "[Desktop Entry]":
|
|
active = True
|
|
continue
|
|
if active == False: # we don't care about licenses or other comments
|
|
continue
|
|
if l == None or len(l) < 1 or l[0] == '#':
|
|
continue
|
|
if l[0] == '[' and l != "[Desktop Entry]":
|
|
active = False
|
|
continue
|
|
# else
|
|
eqi = l.split('=',1)
|
|
if len(eqi) < 2:
|
|
print ("Error: Invalid .desktop line'" + l + "'")
|
|
continue
|
|
# Check what it is ...
|
|
if eqi[0] == "Name":
|
|
this.addName(eqi[1])
|
|
elif eqi[0] == "Comment":
|
|
this.addComment(eqi[1])
|
|
elif eqi[0] == "Exec":
|
|
# check if appExec command in desktop file is installed in system
|
|
eqx=eqi[1].split(" ", 1)[0]
|
|
if which(eqx) == None:
|
|
return #don't add anything to menu if executable not found, goto next desktop file
|
|
this.addExec(eqi[1]) # add appExec to list
|
|
elif eqi[0] == "Icon":
|
|
this.addIcon(eqi[1])
|
|
elif eqi[0] == "Terminal":
|
|
this.addTerminal(eqi[1])
|
|
elif eqi[0] == "Type":
|
|
if eqi[1] != "Application":
|
|
continue
|
|
this.addType(eqi[1])
|
|
elif eqi[0] == "Categories":
|
|
if eqi[1] == '':
|
|
eqi[1] = "Other"
|
|
if eqi[1][-1] == ';':
|
|
eqi[1] = eqi[1][0:-1]
|
|
cats = []
|
|
dtCats = eqi[1].split(';')
|
|
for cat in dtCats:
|
|
result = process_category(cat, cats)
|
|
this.addCategories(cats)
|
|
else:
|
|
continue
|
|
# add to catDict
|
|
#this.dprint()
|
|
if len(this.Categories) > 0: # don't care about stuff w/o category
|
|
for cat in this.Categories:
|
|
catDict[cat].append(this)
|
|
#catDict[cat].sort() #python2 code, not working on py3
|
|
|
|
addIconsToList(iconList, selected_theme) # getting first icons for list
|
|
categoryDict = {}
|
|
|
|
if __name__ == "__main__":
|
|
# init the application group dict (which will contain list of apps)
|
|
application_groups=sorted(application_groups, key=str.lower)
|
|
for appGroup in application_groups:
|
|
categoryDict[appGroup] = []
|
|
# now let's look into the app dirs ...
|
|
#changed desktop files processing loops to add flatpak applications and sorting
|
|
dtFiles=[]
|
|
for appDir in applications_dirs:
|
|
appDir += "/*.desktop"
|
|
dtFiles+=glob.glob(appDir)
|
|
# process each .desktop file in dir
|
|
for dtf in dtFiles:
|
|
skipFlag = False
|
|
for ifn in ignoreList:
|
|
if dtf.find(ifn) >= 0:
|
|
skipFlag = True
|
|
if skipFlag == False:
|
|
process_dtfile(dtf, categoryDict)
|
|
# now, generate jwm menu include
|
|
if simpleOBheader == True:
|
|
print ("<openbox_pipe_menu>") # magic header
|
|
else:
|
|
print ('<?xml version="1.0" encoding="UTF-8" ?><openbox_pipe_menu xmlns="http://openbox.org/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://openbox.org/" >') #magic header
|
|
appGroupLen = len(application_groups)
|
|
for ag in range(appGroupLen):
|
|
catList = categoryDict[application_groups[ag]]
|
|
if len(catList) < 1:
|
|
continue # don't create empty menus
|
|
# sort list
|
|
tmpList=[] #blank list to convert to tuple for sorting purpose
|
|
for app in catList:
|
|
app.Name= ' '.join([word[0].upper()+word[1:] for word in app.Name.split(' ')]) # fancy way to capitalize first letters
|
|
tmpList.append([app.Name, [app.Icon, app.Terminal, app.Exec]]) #creating a tuple to sort
|
|
catList=sorted(tmpList, key = lambda x: x[0].lower()) #recreating catList with sorted tuple list
|
|
#catList=sorted(tmpList, key=lambda (a,b): (a.lower(), b)) # sort with case ignore, py2 code, not working on py3
|
|
# end of sort
|
|
catStr = "<menu id=\"openbox-%s\" label=\"%s\" " % (application_groups[ag], application_groups[ag])
|
|
tmp = getCatIcon(application_groups[ag])
|
|
if tmp != "":
|
|
catStr += "icon=\"%s\"" % tmp
|
|
print (catStr + ">")
|
|
for app in catList:
|
|
progStr = "<item "
|
|
progStr += "label=\"%s\" " % app[0] #add app's name
|
|
if app[1][0] != "": #check if there is app icon
|
|
progStr += "icon=\"%s\" " % app[1][0] #adding icons path
|
|
progStr += "><action name=\"Execute\"><command><![CDATA["
|
|
if app[1][1] == True: #check if app run in terminal
|
|
progStr += terminal_string + " "
|
|
progStr += "%s]]></command></action></item>" % app[1][2] #adding exec command
|
|
print (progStr)
|
|
print ("</menu>")
|
|
print ("</openbox_pipe_menu>") # magic footer
|
|
pass # done/debug break
|