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 }