1 /** 2 * Getting paths where icon themes and icons are stored. 3 * 4 * Authors: 5 * $(LINK2 https://github.com/FreeSlave, Roman Chistokhodov) 6 * Copyright: 7 * Roman Chistokhodov, 2015-2017 8 * License: 9 * $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0). 10 * See_Also: 11 * $(LINK2 http://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html, Icon Theme Specification) 12 */ 13 14 module icontheme.paths; 15 16 private { 17 import std.algorithm; 18 import std.array; 19 import std.exception; 20 import std.path; 21 import std.range; 22 import std.traits; 23 import std.process : environment; 24 import isfreedesktop; 25 } 26 27 version(unittest) { 28 package struct EnvGuard 29 { 30 this(string env, string newValue) { 31 envVar = env; 32 envValue = environment.get(env); 33 environment[env] = newValue; 34 } 35 36 ~this() { 37 if (envValue is null) { 38 environment.remove(envVar); 39 } else { 40 environment[envVar] = envValue; 41 } 42 } 43 44 string envVar; 45 string envValue; 46 } 47 } 48 49 50 static if (isFreedesktop) { 51 import xdgpaths; 52 53 /** 54 * The list of base directories where icon thems should be looked for as described in $(LINK2 http://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html#directory_layout, Icon Theme Specification). 55 * 56 * $(BLUE This function is Freedesktop only). 57 * Note: This function does not provide any caching of its results. This function does not check if directories exist. 58 */ 59 @safe string[] baseIconDirs() nothrow 60 { 61 string[] toReturn; 62 string homePath; 63 collectException(environment.get("HOME"), homePath); 64 if (homePath.length) { 65 toReturn ~= buildPath(homePath, ".icons"); 66 } 67 toReturn ~= xdgAllDataDirs("icons"); 68 toReturn ~= "/usr/share/pixmaps"; 69 return toReturn; 70 } 71 72 /// 73 unittest 74 { 75 auto homeGuard = EnvGuard("HOME", "/home/user"); 76 auto dataHomeGuard = EnvGuard("XDG_DATA_HOME", "/home/user/data"); 77 auto dataDirsGuard = EnvGuard("XDG_DATA_DIRS", "/usr/local/data:/usr/data"); 78 79 assert(baseIconDirs() == ["/home/user/.icons", "/home/user/data/icons", "/usr/local/data/icons", "/usr/data/icons", "/usr/share/pixmaps"]); 80 } 81 82 /** 83 * Writable base icon path. Depends on XDG_DATA_HOME, so this is $HOME/.local/share/icons rather than $HOME/.icons 84 * 85 * $(BLUE This function is Freedesktop only). 86 * Note: it does not check if returned path exists and appears to be directory. 87 */ 88 @safe string writableIconsPath() nothrow { 89 return xdgDataHome("icons"); 90 } 91 92 /// 93 unittest 94 { 95 auto dataHomeGuard = EnvGuard("XDG_DATA_HOME", "/home/user/data"); 96 assert(writableIconsPath() == "/home/user/data/icons"); 97 } 98 99 /// 100 enum IconThemeNameDetector 101 { 102 none = 0, 103 fallback = 1, /// Use hardcoded fallback to detect icon theme name depending on the current desktop environment. Has the lowest priority. 104 gtk2 = 2, /// Use gtk2 settings to detect icon theme name. Has lower priority than gtk3 when using both flags. 105 gtk3 = 4, /// Use gtk3 settings to detect icon theme name. 106 kde = 8, /// Use kde settings to detect icon theme name when the current desktop is KDE4 or KDE5. Has the highest priority when using with other flags. 107 automatic = fallback | gtk2 | gtk3 | kde /// Use all known means to detect icon theme name. 108 } 109 110 private @trusted string xdgCurrentDesktop() nothrow { 111 string currentDesktop; 112 collectException(environment.get("XDG_CURRENT_DESKTOP"), currentDesktop); 113 return currentDesktop; 114 } 115 116 private string getIniLikeValue(string fileName, string groupName, string key) 117 { 118 import inilike.read; 119 auto onGroup = delegate ActionOnGroup(string currentGroupName) { 120 if (groupName == currentGroupName) { 121 return ActionOnGroup.stopAfter; 122 } else { 123 return ActionOnGroup.skip; 124 } 125 }; 126 string foundValue = null; 127 auto onKeyValue = delegate void(string currentKey, string value, string currentGroupName) { 128 if (foundValue is null && groupName == currentGroupName && key == currentKey) { 129 foundValue = value; 130 } 131 }; 132 readIniLike(iniLikeFileReader(fileName), null, onGroup, onKeyValue, null, fileName); 133 return foundValue; 134 } 135 136 private @trusted ubyte getKdeVersion() nothrow 137 { 138 import std.conv : to; 139 ubyte kdeVersion; 140 if (collectException(environment.get("KDE_SESSION_VERSION").to!ubyte, kdeVersion) !is null) 141 return 0; 142 if (kdeVersion < 4) 143 return 0; 144 return kdeVersion; 145 } 146 147 /** 148 * Try to detect the current icon name configured by user. 149 * 150 * $(BLUE This function is Freedesktop only). 151 * Note: There's no any specification on that so some heuristics are applied. 152 * Another note: It does not check if the icon theme with the detected name really exists on the file system. 153 */ 154 @safe string currentIconThemeName(IconThemeNameDetector detector = IconThemeNameDetector.automatic) nothrow 155 { 156 @trusted static string fallbackIconThemeName() 157 { 158 import std.utf : byCodeUnit; 159 foreach(desktop; xdgCurrentDesktop().byCodeUnit.splitter(':')) 160 { 161 switch(desktop.source) { 162 case "GNOME": 163 case "X-Cinnamon": 164 case "Cinnamon": 165 case "MATE": 166 return "gnome"; 167 case "LXDE": 168 return "Adwaita"; 169 case "XFCE": 170 return "Tango"; 171 case "KDE": 172 { 173 if (getKdeVersion() == 5) 174 return "breeze"; 175 else 176 return "oxygen"; 177 } 178 default: 179 break; 180 } 181 } 182 return "Tango"; 183 } 184 @trusted static string gtk2IconThemeName() 185 { 186 import std.stdio : File; 187 import std.string : stripLeft, stripRight; 188 auto home = environment.get("HOME"); 189 if (!home.length) { 190 return null; 191 } 192 auto gtkConfigs = [buildPath(home, ".gtkrc-2.0"), "/etc/gtk-2.0/gtkrc"]; 193 foreach(gtkConfig; gtkConfigs) { 194 try { 195 auto f = File(gtkConfig, "r"); 196 foreach(line; f.byLine()) { 197 auto splitted = line.findSplit("="); 198 splitted[0] = splitted[0].stripRight; 199 if (splitted[0] == "gtk-icon-theme-name") { 200 splitted[2] = splitted[2].stripLeft; 201 if (splitted[2].length > 2 && splitted[2][0] == '"' && splitted[2][$-1] == '"') { 202 return splitted[2][1..$-1].idup; 203 } 204 break; 205 } 206 } 207 } catch(Exception e) { 208 continue; 209 } 210 } 211 return null; 212 } 213 @trusted static string gtk3IconThemeName() 214 { 215 import inilike.read; 216 import inilike.common; 217 auto gtkConfigs = [xdgConfigHome("gtk-3.0/settings.ini"), "/etc/gtk-3.0/settings.ini"]; 218 foreach(gtkConfig; gtkConfigs) { 219 try { 220 string iconThemeName = getIniLikeValue(gtkConfig, "Settings", "gtk-icon-theme-name").unescapeValue(); 221 if (iconThemeName.length && baseName(iconThemeName) == iconThemeName) { 222 return iconThemeName; 223 } 224 } catch(Exception e) { 225 continue; 226 } 227 } 228 return null; 229 } 230 @trusted static string kdeIconThemeName() 231 { 232 import inilike.read; 233 import inilike.common; 234 ubyte kdeVersion = getKdeVersion(); 235 string[] kdeConfigPaths; 236 immutable kdeglobals = "kdeglobals"; 237 if (kdeVersion >= 5) { 238 kdeConfigPaths = xdgAllConfigDirs(kdeglobals); 239 } else { 240 auto home = environment.get("HOME"); 241 if (home.length) { 242 kdeConfigPaths ~= buildPath(home, ".kde4", kdeglobals); 243 kdeConfigPaths ~= buildPath(home, ".kde", kdeglobals); 244 kdeConfigPaths ~= "/etc/kde4/kdeglobals"; 245 } 246 } 247 foreach(kdeConfigPath; kdeConfigPaths) { 248 try { 249 import std.file :exists; 250 string iconThemeName = getIniLikeValue(kdeConfigPath, "Icons", "Theme").unescapeValue(); 251 if (iconThemeName.length && baseName(iconThemeName) == iconThemeName) { 252 return iconThemeName; 253 } 254 } catch(Exception e) { 255 continue; 256 } 257 } 258 return null; 259 } 260 261 string themeName; 262 if (xdgCurrentDesktop() == "KDE" && (detector & IconThemeNameDetector.kde)) { 263 collectException(kdeIconThemeName(), themeName); 264 } 265 if (!themeName.length && (detector & IconThemeNameDetector.gtk3)) { 266 collectException(gtk3IconThemeName(), themeName); 267 } 268 if (!themeName.length && (detector & IconThemeNameDetector.gtk2)) { 269 collectException(gtk2IconThemeName(), themeName); 270 } 271 if (!themeName.length && (detector & IconThemeNameDetector.fallback)) { 272 collectException(fallbackIconThemeName(), themeName); 273 } 274 return themeName; 275 } 276 277 unittest 278 { 279 { 280 auto desktopGuard = EnvGuard("XDG_CURRENT_DESKTOP", "unity:GNOME"); 281 assert(currentIconThemeName(IconThemeNameDetector.fallback) == "gnome"); 282 } 283 auto desktopGuard = EnvGuard("XDG_CURRENT_DESKTOP", ""); 284 assert(currentIconThemeName(IconThemeNameDetector.fallback).length); 285 assert(currentIconThemeName(IconThemeNameDetector.none).length == 0); 286 assert(currentIconThemeName(IconThemeNameDetector.kde).length == 0); 287 288 version(iconthemeFileTest) 289 { 290 auto homeGuard = EnvGuard("HOME", "./test"); 291 auto configGuard = EnvGuard("XDG_CONFIG_HOME", "./test"); 292 293 assert(currentIconThemeName() == "gnome"); 294 assert(currentIconThemeName(IconThemeNameDetector.gtk3) == "gnome"); 295 assert(currentIconThemeName(IconThemeNameDetector.gtk2) == "oxygen"); 296 297 { 298 auto desktop = EnvGuard("XDG_CURRENT_DESKTOP", "KDE"); 299 auto kdeVersion = EnvGuard("KDE_SESSION_VERSION", "5"); 300 assert(currentIconThemeName(IconThemeNameDetector.kde) == "breeze"); 301 } 302 303 { 304 auto desktop = EnvGuard("XDG_CURRENT_DESKTOP", "KDE"); 305 auto kdeVersion = EnvGuard("KDE_SESSION_VERSION", "4"); 306 assert(currentIconThemeName(IconThemeNameDetector.kde) == "default.kde4"); 307 } 308 } 309 } 310 } 311 312 /** 313 * The list of icon theme directories based on data paths. 314 * Returns: Array of paths with "icons" subdirectory appended to each data path. 315 * Note: This function does not check if directories exist. 316 */ 317 @trusted string[] baseIconDirs(Range)(Range dataPaths) if (isInputRange!Range && is(ElementType!Range : string)) 318 { 319 return dataPaths.map!(p => buildPath(p, "icons")).array; 320 } 321 322 /// 323 unittest 324 { 325 auto dataPaths = ["share", buildPath("local", "share")]; 326 assert(equal(baseIconDirs(dataPaths), [buildPath("share", "icons"), buildPath("local", "share", "icons")])); 327 }