Browse Source

Replace cached and saved program database with faster, dynamic XDG lookup. also icons. introduces dependency for pyxdg

Nils 6 months ago
  1. 25
  2. 187
  3. 301
  4. 17
  5. 17604
  6. 32
  7. 54


@ -257,30 +257,23 @@ def systemProgramsSetBlacklist(executableNames:tuple):
programDatabase.userBlacklist = tuple(executableNames)
#Needs rebuild through the GUI. We have no callback for this.
def getCache()->dict:
"""Returns the cached database from buildProgramDatabase. No automatic update. Empty on program
db = {
"programs" : list of dicts
iconPaths : list of strings
return programDatabase.getCache()
def setCache(cache:dict):
def getNsmClients()->list:
"""Returns a, probably cached, list of dicts that represent all nsm capable clients
on this system. Including the list of clients the user added themselves"""
return programDatabase.getNsmClients()
def getNsmExecutables()->set:
"""Cached access fort fast membership tests. Is this program in the PATH?"""
"""Cached access fort fast membership tests. Is this program in the PATH?
This is just to check if an executable is available on this system.
The content of the set are executable names, the same as ["agordejoExec"] from getNsmClients elements
return programDatabase.nsmExecutables
def getUnfilteredExecutables()->list:
"""Return a list of unique names without paths or directories of all exectuables in users $PATH.
This is intended for a program starter prompt. GUI needs to supply tab completition or search
programDatabase.unfilteredExecutables = programDatabase.buildCache_unfilteredExecutables() #quick call
return programDatabase.unfilteredExecutables


@ -1,187 +0,0 @@
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
Copyright 2022, Nils Hilbricht, Germany ( )
This file is part of the Laborejo Software Suite ( ),
This is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <>.
import logging; logger = logging.getLogger(__name__);"import")
from os import getenv
import os
import pathlib
import re
$HOME/.icons (for backwards compatibility)
EXTENSIONS = [".png", ".xpm", ".svg"]
SEARCH_DIRECTORIES = [pathlib.Path(pathlib.Path.home(), ".icons")]
XDG_DATA_DIRS = getenv("XDG_DATA_DIRS") #colon : separated, most likely empty
SEARCH_DIRECTORIES += [pathlib.Path(p, "icons/hicolor") for p in XDG_DATA_DIRS.split(":")]
SEARCH_DIRECTORIES += [pathlib.Path(p, "icons/scalable") for p in XDG_DATA_DIRS.split(":")]
#If $XDG_DATA_DIRS is either not set or empty, a value equal to /usr/local/share/:/usr/share/ should be used.
SEARCH_DIRECTORIES += [pathlib.Path(p, "icons/hicolor") for p in "/usr/local/share/:/usr/share/".split(":")]
SEARCH_DIRECTORIES += [pathlib.Path(p, "icons/scalable") for p in "/usr/local/share/:/usr/share/".split(":")]
SEARCH_DIRECTORIES.append(pathlib.Path("/usr/share/icons")) #for icons wrongly put directly there. But many do.
SEARCH_DIRECTORIES.append(pathlib.Path("/usr/local/share/icons")) #for icons wrongly put directly there.
#TODO: this may become a problem in the future. If a user has *MANY* icon themes installed this might take too long. And all because we wanted the Shuriken icon in Archlinux...
SEARCH_DIRECTORIES = set(p.resolve() for p in SEARCH_DIRECTORIES) #resolve follows symlinks, set() makes it unique
def run_fast_scandir(dir, ext):
subfolders, files = [], []
for f in os.scandir(dir):
if f.is_dir():
if f.is_file():
if os.path.splitext([1].lower() in ext:
for dir in list(subfolders):
sf, f = run_fast_scandir(dir, ext)
except PermissionError:
except FileNotFoundError:
return subfolders, files
def _buildCache()->set:
result = []
forget, files = run_fast_scandir(basePath, EXTENSIONS)
result += files
#Convert str to real paths
result = [pathlib.Path(r) for r in result if pathlib.Path(r).is_file()]
return set(result)
global _cache
_cache = None
def updateCache(serializedCache:list=None):
global _cache
if serializedCache:
#Convert str to real paths"Filling icon cache with previously serialized data")
_cache = set([pathlib.Path(r) for r in serializedCache if pathlib.Path(r).is_file()])
#Already real paths as a set"Building icon cache from scratch")
_cache = _buildCache()"Icon cache complete")
def getSerializedCache()->list: #list of strings, not paths. This is for saving in global system config for faster startup
global _cache
return [str(p) for p in _cache]
rePattern = re.compile("\d+x\d+") #we don't put .* around this because we are searching for the subpattern
def findIconPath(executableName:str)->list:
Parameter executableName can be a direct icon name as well, from the .desktop icon path.
Return order is: svg first, then highest resolution first, then the rest unordered.
so you can use result[0] for the best variant.
It is not guaranteed that [1], or even [0] exists.
This is not a sorted list, these extra variants are just added to the front of the list again"""
global _cache
if not _cache:
raise ValueError("You need to call updateCache() first")
svg = None
bestr = 0 #resolution
best = None
#Did we get an icon name directly? Remove the extension
#For example "ams_32.xpm" becomes "ams_32"
exeAsPath = pathlib.Path(executableName)
if exeAsPath.suffix in EXTENSIONS:
executableName = exeAsPath.stem
#for ext in EXTENSIONS: #all extensions
# if executableName.endswith(ext):
# executableName = executableName[:-4]
result = []
for f in _cache:
if f.stem == executableName:
if f.suffix == ".svg":
svg = f
match =, str(f)) #find resolution dir like /48x48/
if match:
resolutionAsNumber = int("x")[0])
if resolutionAsNumber > bestr: #new best one
bestr = resolutionAsNumber
best = f
if best:
result.insert(0, best)
if svg:
result.insert(0, svg)
if not result:
logger.warning(f"Did not find an icon for {executableName}")
return result
if __name__ == "__main__":
"""Example that tries to find a few icons"""
print("Search paths:")
for exe in ("zynaddsubfx", "patroneo", "jack_mixer", "carla", "ardour6", "synthv1", "ams_32.xpm", "shuriken.png"):
r = findIconPath(exe)
if r:
print (f"{exe} Best resolution: {r[0]}")
print (f"{exe}: No icon found ")


@ -23,12 +23,13 @@ import logging; logger = logging.getLogger(__name__);"import")
import pathlib
import configparser
import subprocess
import os
import stat
import xdg.DesktopEntry #pyxdg
import xdg.IconTheme #pyxdg
from engine.start import PATHS
import engine.findicons as findicons
def nothing(*args): pass
@ -36,156 +37,120 @@ class SupportedProgramsDatabase(object):
"""Find all binaries with NSM support. Resources are:
* Agordejo internal program list of known working programs.
* Internal blacklist of known redundant programs (such as non-daw) or nonsense entries, like Agordejo itself
* A search through the users path to find stray programs that contain NSM announce messages
* Finally, local to a users system: User whitelist for any program, user blacklist.
Those two have the highest priority.
We generate the same format as configParser does with .desktop files as _sections dict.
{ 'categories': 'AudioVideo;Audio;X-Recorders;X-Multitrack;X-Jack;',
'comment': 'Easy to use pattern sequencer for JACK and NSM',
'comment[de]': 'Einfach zu bedienender Pattern-Sequencer',
'exec': 'patroneo',
'genericname': 'Sequencer',
'icon': 'patroneo',
'name': 'Patroneo',
'startupnotify': 'false',
'terminal': 'false',
'type': 'Application',
'x-nsm-capable': 'true'}
In case there is a file in PATH or database but has no .desktop we create our own entry with
missing data.
We add two keys ourselves:
"agordejoExec" : this is the one we will send to nsmd.
"agordejoIconPath" : absolute path as str if we found an icon, so that a GUI does need to search on its own
def __init__(self):
self.progressHook = nothing #prevents the initial programstart from sending meaningless messages for the cached data. Set and reverted in
self.grepexcluded = (pathlib.Path(PATHS["share"], "grepexcluded.txt")) #created by hand. see docstring
#assert self.grepexcluded.exists()
self.blacklist = ("nsmd", "non-daw", "carla", "agordejo", "adljack", "agordejo.bin") #only programs that have to do with audio and music. There is another general blacklist that speeds up discovery
self.whiteList = ("thisdoesnotexisttest", "patroneo", "vico",
self.blackList = set(("nsmd", "non-daw", "carla", "agordejo", "adljack", "agordejo.bin")) #only programs that have to do with audio and music. There is another general blacklist that speeds up discovery
self.whiteList = set(("thisdoesnotexisttest", "patroneo", "vico", "tembro", "laborejo",
"fluajho", "carla-rack", "carla-patchbay", "carla-jack-multi", "carla-jack-single",
"ardour5", "ardour6", "nsm-data", "jackpatch", "nsm-proxy", "ADLplug", "ams",
"drumkv1_jack", "synthv1_jack", "samplv1_jack", "padthv1_jack",
"luppp", "non-mixer", "non-timeline", "non-sequencer", "non-midi-mapper", "non-mixer-noui",
"OPNplug", "qmidiarp", "qtractor", "zynaddsubfx", "jack_mixer",
"hydrogen", "mfp", "shuriken", "laborejo", "guitarix", "radium",
"ray-proxy", "ray-jackpatch", "amsynth", "mamba", "qseq66"
) #shortcut list and programs not found by buildCache_grepExecutablePaths because they are just shellscripts and do not contain /nsm/server/announce.
self.userWhitelist = () #added dynamically to morePrograms. highest priority
self.userBlacklist = () #added dynamically to blacklist. highest priority
self.knownDesktopFiles = { #shortcuts to the correct desktop files. Reverse lookup binary->desktop creates false entries, for example ZynAddSubFx and Carla.
"zynaddsubfx": "zynaddsubfx-jack.desktop", #value will later get replaced with the .desktop entry
#"carla-jack-single" : "carla.desktop", #We CANNOT add them here because both key and value must be unique and hashable. We create a reverse dict from this.
#"carla-jack-patchbay" : "carla.desktop",
#"carla-jack-rack" : "carla.desktop",
"ams" : "ams.desktop",
"amsynth" : "amsynth.desktop",
self._reverseKnownDesktopFiles = dict(zip(self.knownDesktopFiles.values(),self.knownDesktopFiles.keys())) #to lookup the exe by desktoip name
self.programs = [] #list of dicts. guaranteed keys: agordejoExec, name, agordejoFullPath. And probably others, like description and version.
"hydrogen", "mfp", "shuriken", "guitarix", "radium",
"ray-proxy", "ray-jackpatch", "amsynth", "mamba", "qseq66", "zynaddsubfx-jack"
)) #shortcut list and programs not found by buildCache_grepExecutablePaths because they are just shellscripts and do not contain /nsm/server/announce.
self.userWhitelist = () #added dynamically by api.systemProgramsSetWhitelist add to morePrograms. highest priority
self.userBlacklist = () #added dynamically by api.systemProgramsSetBlacklist as blacklist. highest priority
self.programs = [] #main data structure of this file. list of dicts. guaranteed keys: agordejoExec, name, agordejoFullPath. And probably others, like description and version.
self.nsmExecutables = set() #set of executables for fast membership, if a GUI wants to know if they are available. Needs to be build "manually" with self.programs. no auto-property for a list. at least we don't want to do the work. needs to be called from the api/GUI.
#self.unfilteredExecutables = self.buildCache_unfilteredExecutables() #This doesn't take too long. we can start that every time. It will get updated in build as well.
self.unfilteredExecutables = None #in build() #fills self.programs and
def buildCache_grepExecutablePaths(self)->list:
"""return a list of executable names in the path (not the path itself)
Grep explained:
-s silent. No errors, eventhough subprocess uses stdout only
-R recursive with symlinks. We don't want to look in subdirs because that is not allowed by
PATH and nsm, but we want to follow symlinks
If you have a custom user path that does not mean that all its executables will
automatically show up here. They still need to contain /nsm/server/announce
Your binaries will be in unfilteredExecutables though needs to be called when the program is ready, e.g. a GUI is set up and has the progressHook ready
def _isexe(self, path):
"""executable by owner"""
return path.is_file() and stat.S_IXUSR & os.stat(path)[stat.ST_MODE] == 64
def _executableNameToFullPath(self, exeName:str, executableSystemPaths:set)->pathlib.Path:
for directory in executableSystemPaths:
p = pathlib.Path(directory, exeName)
if p.exists():
return p
return None
def gatherAllNsmClients(self)->list:
We parse .desktop files for the nsm flag
and export our own list of dicts with various entries. see below.
result = []
testpaths = os.environ["PATH"].split(os.pathsep) + ["/bin", "/sbin"]
executablePaths = set([pathlib.Path(p).resolve() for p in os.environ["PATH"].split(os.pathsep)]) #resolve filters out symlinks, like arches /sbin and /bin. set() makes it unique
excludeFromProcessingSet = set(self.blacklist + self.userBlacklist)
whiteSet = set(self.whiteList + self.userWhitelist)
executableSystemPaths = set([pathlib.Path(p).resolve() for p in os.environ["PATH"].split(os.pathsep)]) #resolve filters out symlinks, like ArchLinux's /sbin and /bin. set() makes it unique
for path in executablePaths:
command = f"grep --exclude-from {self.grepexcluded} -iRsnl {path} -e /nsm/server/announce"
#command = f"grep -iRsnl {path} -e /nsm/server/announce"
#Py>=3.7 completedProcess =, capture_output=True, text=True, shell=True)
#Look into executable files. Slow and didn't bring up any new programs. Better to keep the database up to date ourselves.
completedProcess =, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, shell=True) #universal_newlines is an alias for text, which was deprecated in 3.7 because text is more understandable. capture_output replaces the two PIPEs in 3.7
for fullPath in completedProcess.stdout.split():
exe = pathlib.Path(fullPath).relative_to(path)
if not str(exe) in excludeFromProcessingSet: #skip over any known file, good or bad
result.append((str(exe), str(fullPath)))
for prg in whiteSet:
for path in executablePaths:
if pathlib.Path(path, prg).is_file(): #check if this actually exists
result.append((str(prg), str(pathlib.Path(path, prg))))
break #inner loop
return list(set(result)) #make unique
def buildCache_DesktopEntries(self):
"""Go through all dirs including subdirs"""
xdgPaths = (
pathlib.Path(pathlib.Path.home(), ".local/share/applications"),
result = []
config = configparser.ConfigParser()
allDesktopEntries = []
for basePath in xdgPaths:
try: #TODO: this whole part of the program is a mess. Confiparser and Qt in a bundle segfault too often.
except Exception as e:
for f in basePath.glob('**/*'):
try: #TODO: this whole part of the program is a mess. Confiparser and Qt in a bundle segfault too often.
except Exception as e:
if f.is_file() and f.suffix == ".desktop":
desktopEntry = xdg.DesktopEntry.DesktopEntry(f)
#Don't validate. This is over-zealous and will mark deprecation and unknown categories.
entryDict = dict(config._sections["Desktop Entry"])
#Replace simple names in our shortcut list with full data
if in self.knownDesktopFiles.values():
key = self._reverseKnownDesktopFiles[]
self.knownDesktopFiles[key] = entryDict
#in any case:
allDesktopEntries.append(entryDict) #_sections 'DesktopEntry':{dictOfActualData)
except: #any bad config means skip
logger.warning(f"Bad desktop file. Skipping: {f}")
return allDesktopEntries
def setCache(self, cache:dict):
"""Qt Settings will send us this"""
self.programs = cache["programs"] #list of dicts
findicons.updateCache(cache["iconPaths"])"Restoring program list from serialized cache")
self.nsmExecutables = set(d["agordejoExec"] for d in self.programs)
except xdg.Exceptions.ValidationError as e:
logger.error(f"Desktop file {f} has problems: {e}")
agorExec = desktopEntry.get("X-NSM-Exec")
if not agorExec:
n = pathlib.Path(desktopEntry.getExec()).name
agorExec = n.split(" ")[0].strip() # this will fail with special filenames, such as spaces in filenames. But it is already the fallback for programs not adhering to the nsm specs. not our problem anymore.
blacklisted = agorExec in self.blackList or agorExec in self.userBlacklist
if blacklisted:"{agorExec}] is blacklisted. Skip.")
isNSM = ( bool(desktopEntry.get("X-NSM-Capable"))
or bool(desktopEntry.get("X-NSM-capable"))
or agorExec in self.whiteList
or agorExec in self.userWhitelist
if isNSM:
absExecPath = self._executableNameToFullPath(agorExec, executableSystemPaths)
if absExecPath is None:
logger.warning(f"Couldn't find actual path for {agorExec} eventhough we searched with the name from it's desktop file. If this program is not installed at all this is a false-negative error. Don't worry.")
if not self._isexe(absExecPath):
logger.error(f"{absExecPath} was derived from .desktop file and exist, but it not executable!")
data = {
"agordejoName" : desktopEntry.getName(),
"agordejoExec" : agorExec , #to prevent 'carla-rack %u'. This is what nsm will call.
"agordejoIconPath" : xdg.IconTheme.getIconPath(desktopEntry.getIcon()),
"agordejoFullPath" : absExecPath, #This is only for information. nsm calls agordejoExec
"agordejoDescription" : desktopEntry.getComment(),
return result
def getCache(self)->dict:
"""To carry the DB over restarts. Saved by Qt Settings at the moment"""
cache = {
"programs" : self.programs, #list of dicts
"iconPaths" : findicons.getSerializedCache(), #list
return cache
def build(self, progressHook=None):
"""Can be called at any time by the user to update after installing new programs"""
@ -199,96 +164,32 @@ class SupportedProgramsDatabase(object):"Building launcher database. This might take a minute")
self.programs = self._build() #builds iconPaths as side-effect
self.programs = self.gatherAllNsmClients()
self.unfilteredExecutables = self.buildCache_unfilteredExecutables()
self.nsmExecutables = set(d["agordejoExec"] for d in self.programs)
self.progressHook = nothing"Building launcher database done.")
def _exeToDesktopEntry(self, exe:str)->dict:
"""Assumes self.desktopEntries is up to date
Convert one exe (not full path!) to one dict entry. """
if exe in self.knownDesktopFiles and type(self.knownDesktopFiles[exe]) is dict : #Shortcut through internal database
entry = self.knownDesktopFiles[exe]
return entry
else: #Reverse Search desktop files.
for entry in self.desktopEntries:
if "exec" in entry and exe.lower() in entry["exec"].lower():
return entry
#else: #Foor loop ended. Did not find any matching desktop file
return None
def _build(self):
self.executables = self.buildCache_grepExecutablePaths()
self.desktopEntries = self.buildCache_DesktopEntries()
leftovers = set(self.executables)
matches = [] #list of dicts
for exe, fullPath in self.executables:
entry = self._exeToDesktopEntry(exe)
if entry and type(entry) is dict: #Found match!
entry["agordejoFullPath"] = fullPath
#We don't want .desktop syntax like "qmidiarp %F"
entry["agordejoExec"] = exe
if entry["icon"]:
foundIcon = findicons.findIconPath(entry["icon"])
foundIcon = findicons.findIconPath(entry["agordejoExec"])
if foundIcon:
entry["agordejoIconPath"] = str(foundIcon[0]) #pick best resolution
entry["agordejoIconPath"] = None
except KeyError:
pass #Double entries like zyn-jack zyn-alsa etc.
elif entry and not type(entry) is dict:
#There is a strange bug I can't reproduce. At least catch it.
logger.error(f"Wrong entry type: {type(entry)} for {entry}")
for exe,fullPath in leftovers:
pseudoEntry = {"name": exe.title(), "agordejoExec":exe, "agordejoFullPath":fullPath}
return matches
def getNsmClients(self)->list:
"""Return the main data structure of this file:
a list of dicts
return self.programs
def buildCache_unfilteredExecutables(self):
def isexe(path):
"""executable by owner"""
return path.is_file() and stat.S_IXUSR & os.stat(path)[stat.ST_MODE] == 64
"""Just a list of all exectuables of this systems PATH.
This is used for the GUIs "start any program" with auto completion.
result = []
executablePaths = [pathlib.Path(p) for p in os.environ["PATH"].split(os.pathsep)]
for path in executablePaths:
result += [str(pathlib.Path(f).relative_to(path)) for f in path.glob("*") if isexe(f)]
result += [str(pathlib.Path(f).relative_to(path)) for f in path.glob("*") if self._isexe(f)]
return sorted(list(set(result)))
def _buildWhitelist(self):
"""For reliable, fast and easy selection this is the whitelist.
It will be populated from a template-list of well-working clients and then all binaries not
in the path are filtered out. This can be presented to the user without worries."""
#Assumes to be called only from
startexecutables = set(self.whiteList + self.userWhitelist)
for prog in self.programs:
prog["whitelist"] = prog["agordejoExec"] in startexecutables
matches = []
for exe in startexecutables:
entry = self._exeToDesktopEntry(exe)
if entry: #Found match!
#We don't want .desktop syntax like "qmidiarp %F"
entry["agordejoExec"] = exe
return matches
programDatabase = SupportedProgramsDatabase()


@ -1,17 +0,0 @@
sudo pacman -Fy
pacman -Fx "/usr/bin/([A-Z])" | cut -d " " -f1 | uniq | sort > allexe.txt
pacman -Fl $(pacman -Sg pro-audio |cut -d " " -f2) | cut -d " " -f2 | grep "usr/bin" | uniq | sort > audioexe.txt
sed -i -e 's/usr\/bin\///g' audioexe.txt #strip usr/bin yes, the initial / from usr is missing
sed '/^[[:space:]]*$/d' -i audioexe.txt #remove empty lines
grep -vFf audioexe.txt allexe.txt > grepexcluded2.txt
grep '^usr\/bin\/' grepexcluded2.txt > grepexcluded.txt #only keep lines that start with usr/bin. There are some false positives in there
sed -i -e 's/usr\/bin\///g' grepexcluded.txt #strip usr/bin yes, the initial / from usr is missing
sed '/^[[:space:]]*$/d' -i grepexcluded.txt #remove empty lines
rm grepexcluded2.txt
rm allexe.txt
rm audioexe.txt


File diff suppressed because it is too large


@ -174,24 +174,9 @@ class MainWindow(QtWidgets.QMainWindow):
#Handle the application data cache.
#This must happen before engineStart. If a session is already running a set of initial
#client-callbacks will arrive immediately, even before the eventLoop starts.
#If not present instruct the engine to build one.
#This is also needed by the prompt in sessionController and the icons"Trying to restore cached program database")
settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"])
if settings.contains("engineCache"):
engineCache = settings.value("engineCache", type=dict)
api.setCache(engineCache)"Restored program database from qt cache to engine")
else: #First or fresh start
#A single shot timer with 0 durations is executed only after the app starts, thus the main window is ready."First run. Instructing engine to build program database")
QtCore.QTimer.singleShot(0, self.updateProgramDatabase) #includes self._updateGUIWithCachedPrograms()
#Use our wait-dialog to build the program database."Instructing engine to build program database")
QtCore.QTimer.singleShot(0, self.updateProgramDatabase) #includes self._updateGUIWithCachedPrograms()
#Starting the engine sends initial GUI data. Every window and widget must be ready to receive callbacks here
@ -262,12 +247,8 @@ class MainWindow(QtWidgets.QMainWindow):
def _updateIcons(self):"Creating icon database")
engineCache = api.getCache()
assert engineCache
programs = engineCache["programs"]
for entry in programs:
for entry in api.getNsmClients():
exe = entry["agordejoExec"]
icon = None
@ -296,13 +277,12 @@ class MainWindow(QtWidgets.QMainWindow):
text = QtCore.QCoreApplication.translate("mainWindow", "Updating Program Database.\nThank you for your patience.\nIf progress freezes please kill and restart the whole program.")
settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"])
if settings.contains("engineCache"): #remove haywire data from previous releases
settings.remove("engineCache")"Asking api to generate program and icon database while waiting")
diag = WaitDialog(self, text, api.buildSystemPrograms) #save in local var to keep alive
assert api.getCache()
settings.setValue("engineCache", api.getCache()) # dict
def reactCallback_sessionClosed(self):


@ -25,10 +25,11 @@ import logging; logger = logging.getLogger(__name__);"import")
#Third Party
from PyQt5 import QtCore, QtGui, QtWidgets
import xdg.IconTheme #pyxdg
import engine.api as api
import engine.findicons as findicons
from .descriptiontextwidget import DescriptionController
@ -122,16 +123,16 @@ class ClientItem(QtWidgets.QTreeWidgetItem):
assert icon, icon
self.setIcon(iconColumn, icon) #reported name is correct here. this is just the column.
else: #Not NSM client added by the prompt widget
result = findicons.findIconPath(clientDict["executable"])
result = xdg.IconTheme.getIconPath(clientDict["executable"]) #First attempt: let's hope the icon name has something to do with the executable name
if result:
icon = QtGui.QIcon.fromTheme(str(result[0]))
icon = QtGui.QIcon.fromTheme(result)
icon = QtGui.QIcon.fromTheme(clientDict["executable"])
if not icon.isNull():
self.setIcon(iconColumn, icon)
self.setIcon(iconColumn, iconFromString(clientDict["executable"]))
self.setIcon(iconColumn, iconFromString(clientDict["executable"])) #draw our own.
class ClientTable(object):
"""Controls the QTreeWidget that holds loaded clients"""
@ -190,6 +191,12 @@ class ClientTable(object):
for index in range(self.clientsTreeWidget.columnCount()):
#And a bit more extra space
for index in range(self.clientsTreeWidget.columnCount()):
self.clientsTreeWidget.setColumnWidth(index, self.clientsTreeWidget.columnWidth(index)+25)
def _cleanClients(self, nsmSessionExportDict:dict):
"""Reset everything to the initial, empty state.
We do not reset in in openReady because that signifies that the session is ready.
@ -389,25 +396,6 @@ class ClientTable(object):
class LauncherProgram(QtWidgets.QTreeWidgetItem):
An item on the left side of the window. Used to start programs and show info, but nothing more.
{ 'categories': 'AudioVideo;Audio;X-Recorders;X-Multitrack;X-Jack;',
'comment': 'Easy to use pattern sequencer for JACK and NSM',
'comment[de]': 'Einfach zu bedienender Pattern-Sequencer',
'exec': 'patroneo',
'genericname': 'Sequencer',
'icon': 'patroneo',
'name': 'Patroneo',
'startupnotify': 'false',
'terminal': 'false',
'type': 'Application',
'version': '1.0', #desktop spec version, not progra,
'x-nsm-capable': 'true'}
'agordejoExec' : the actual nsm exe
'agordejoIconPath' : a priority path the engine found for us
allItems = {} # clientId : ClientItem
@ -418,8 +406,6 @@ class LauncherProgram(QtWidgets.QTreeWidgetItem):
self.launcherDict = launcherDict
self.executable = launcherDict["agordejoExec"]
parameterList = [] #later in update
super().__init__(parameterList, type=1000) #type 0 is default qt type. 1000 is subclassed user type)
@ -439,7 +425,7 @@ class LauncherProgram(QtWidgets.QTreeWidgetItem):
assert programIcons
if launcherDict["agordejoExec"] in programIcons:
icon = programIcons[launcherDict["agordejoExec"]]
self.setIcon(self.parentController.columns.index("name"), icon) #name is correct here. this is just the column.
self.setIcon(self.parentController.columns.index("agordejoName"), icon) #name is correct here. this is just the column.
class LauncherTable(object):
"""Controls the QTreeWidget that holds programs in the PATH.
@ -453,7 +439,7 @@ class LauncherTable(object):
self.launcherWidget = self.mainWindow.ui.loadedSessionsLauncher
self.columns = ("name", "comment", "agordejoFullPath") #basically an enum
self.columns = ("agordejoName", "agordejoDescription", "agordejoFullPath") #basically an enum
self.headerLables = [
QtCore.QCoreApplication.translate("Launcher", "Name"),
QtCore.QCoreApplication.translate("Launcher", "Description"),
@ -478,27 +464,23 @@ class LauncherTable(object):
for index in range(self.launcherWidget.columnCount()):
#And a bit more extra space
for index in range(self.launcherWidget.columnCount()):
self.launcherWidget.setColumnWidth(index, self.launcherWidget.columnWidth(index)+25)
def _reactSignal_launcherItemDoubleClicked(self, item):
def buildPrograms(self):
"""Called by mainWindow.updateProgramDatabase
Receive entries from the engine.
Entry is a dict modelled after a .desktop file.
But not all entries have all data. Some are barebones executable name and path.
Only guaranteed keys are agordejoExec and agordejoFullPath, which in turn are files
guaranteed to exist in the path.
engineCache = api.getCache()
programs = engineCache["programs"]
programs = api.getNsmClients()
for entry in programs:
item = LauncherProgram(parentController=self, launcherDict=entry)
class OpenSessionController(object):