#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Copyright 2021 , Nils Hilbricht , Germany ( https : / / www . hilbricht . net )
This file is part of the Laborejo Software Suite ( https : / / www . laborejo . org ) ,
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
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
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 < http : / / www . gnu . org / licenses / > .
"""
import logging ; logger = logging . getLogger ( __name__ ) ; logger . info ( " import " )
from os import getenv
import os
import pathlib
import re
"""
https : / / specifications . freedesktop . org / icon - theme - spec / icon - theme - spec - latest . html
https : / / specifications . freedesktop . org / icon - theme - spec / icon - theme - spec - latest . html #icon_lookup
$ HOME / . icons ( for backwards compatibility )
$ XDG_DATA_DIRS / icons
/ usr / share / pixmaps
"""
EXTENSIONS = [ " .png " , " .xpm " , " .svg " ]
SEARCH_DIRECTORIES = [ pathlib . Path ( pathlib . Path . home ( ) , " .icons " ) ]
XDG_DATA_DIRS = getenv ( " XDG_DATA_DIRS " ) #colon : separated, most likely empty
if XDG_DATA_DIRS :
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 ( " : " ) ]
else :
#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/pixmaps " ) )
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 ) :
"""
https : / / stackoverflow . com / questions / 18394147 / recursive - sub - folder - search - and - return - files - in - a - list - python
"""
subfolders , files = [ ] , [ ]
try :
for f in os . scandir ( dir ) :
if f . is_dir ( ) :
subfolders . append ( f . path )
if f . is_file ( ) :
if os . path . splitext ( f . name ) [ 1 ] . lower ( ) in ext :
files . append ( f . path )
for dir in list ( subfolders ) :
sf , f = run_fast_scandir ( dir , ext )
subfolders . extend ( sf )
files . extend ( f )
except PermissionError :
pass
except FileNotFoundError :
pass
return subfolders , files
def _buildCache ( ) - > set :
result = [ ]
for basePath in SEARCH_DIRECTORIES :
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
logger . info ( " Filling icon cache with previously serialized data " )
_cache = set ( [ pathlib . Path ( r ) for r in serializedCache if pathlib . Path ( r ) . is_file ( ) ] )
else :
#Already real paths as a set
logger . info ( " Building icon cache from scratch " )
_cache = _buildCache ( )
logger . info ( " 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
else :
match = re . search ( rePattern , str ( f ) ) #find resolution dir like /48x48/
if match :
resolutionAsNumber = int ( match . group ( ) . split ( " x " ) [ 0 ] )
if resolutionAsNumber > bestr : #new best one
bestr = resolutionAsNumber
best = f
result . append ( 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 """
updateCache ( )
print ( " Search paths: " )
print ( SEARCH_DIRECTORIES )
print ( )
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 ] } " )
else :
print ( f " { exe } : No icon found " )