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) {
31             envVar = env;
32             envValue = environment.get(env);
33         }
34 
35         ~this() {
36             if (envValue is null) {
37                 environment.remove(envVar);
38             } else {
39                 environment[envVar] = envValue;
40             }
41         }
42 
43         string envVar;
44         string envValue;
45     }
46 }
47 
48 
49 static if (isFreedesktop) {
50     import xdgpaths;
51 
52     /**
53     * 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).
54     *
55     * $(BLUE This function is Freedesktop only).
56     * Note: This function does not provide any caching of its results. This function does not check if directories exist.
57     */
58     @safe string[] baseIconDirs() nothrow
59     {
60         string[] toReturn;
61         string homePath;
62         collectException(environment.get("HOME"), homePath);
63         if (homePath.length) {
64             toReturn ~= buildPath(homePath, ".icons");
65         }
66         toReturn ~= xdgAllDataDirs("icons");
67         toReturn ~= "/usr/share/pixmaps";
68         return toReturn;
69     }
70 
71     ///
72     unittest
73     {
74         auto homeGuard = EnvGuard("HOME");
75         auto dataHomeGuard = EnvGuard("XDG_DATA_HOME");
76         auto dataDirsGuard = EnvGuard("XDG_DATA_DIRS");
77 
78         environment["HOME"] = "/home/user";
79         environment["XDG_DATA_HOME"] = "/home/user/data";
80         environment["XDG_DATA_DIRS"] = "/usr/local/data:/usr/data";
81 
82         assert(baseIconDirs() == ["/home/user/.icons", "/home/user/data/icons", "/usr/local/data/icons", "/usr/data/icons", "/usr/share/pixmaps"]);
83     }
84 
85     /**
86      * Writable base icon path. Depends on XDG_DATA_HOME, so this is $HOME/.local/share/icons rather than $HOME/.icons
87      *
88      * $(BLUE This function is Freedesktop only).
89      * Note: it does not check if returned path exists and appears to be directory.
90      */
91     @safe string writableIconsPath() nothrow {
92         return xdgDataHome("icons");
93     }
94 
95     ///
96     unittest
97     {
98         auto dataHomeGuard = EnvGuard("XDG_DATA_HOME");
99         environment["XDG_DATA_HOME"] = "/home/user/data";
100         assert(writableIconsPath() == "/home/user/data/icons");
101     }
102 
103     ///
104     enum IconThemeNameDetector
105     {
106         none = 0,
107         fallback = 1, /// Use hardcoded fallback to detect icon theme name depending on the current desktop environment. Has lower priority than other methods.
108         gtk2 = 2, /// Use gtk2 settings to detect icon theme name. Has lower priority than gtk3.
109         gtk3 = 4, /// Use gtk3 settings to detect icon theme name.
110         automatic =  fallback | gtk2 | gtk3 /// Use all known means to detect icon theme name.
111     }
112     /**
113     * Try to detect the current icon name configured by user.
114     *
115     * $(BLUE This function is Freedesktop only).
116     * Note: There's no any specification on that so some heuristics are applied.
117     * Another note: It does not check if the icon theme with the detected name really exists on the file system.
118     */
119     @safe string currentIconThemeName(IconThemeNameDetector detector = IconThemeNameDetector.automatic) nothrow
120     {
121         @trusted static string fallbackIconThemeName()
122         {
123             string xdgCurrentDesktop = environment.get("XDG_CURRENT_DESKTOP");
124             switch(xdgCurrentDesktop) {
125                 case "GNOME":
126                 case "X-Cinnamon":
127                 case "MATE":
128                     return "gnome";
129                 case "LXDE":
130                     return "Adwaita";
131                 case "XFCE":
132                     return "Tango";
133                 case "KDE":
134                     return "oxygen"; //TODO: detect KDE version and set breeze if it's KDE5
135                 default:
136                     return "Tango";
137             }
138         }
139         @trusted static string gtk2IconThemeName() nothrow
140         {
141             import std.stdio : File;
142             try {
143                 auto home = environment.get("HOME");
144                 if (!home.length) {
145                     return null;
146                 }
147                 string themeName;
148                 auto gtkConfig = buildPath(home, ".gtkrc-2.0");
149                 auto f = File(gtkConfig, "r");
150                 foreach(line; f.byLine()) {
151                     auto splitted = line.findSplit("=");
152                     if (splitted[0] == "gtk-icon-theme-name") {
153                         if (splitted[2].length > 2 && splitted[2][0] == '"' && splitted[2][$-1] == '"') {
154                             return splitted[2][1..$-1].idup;
155                         }
156                         break;
157                     }
158                 }
159             } catch(Exception e) {
160 
161             }
162             return null;
163         }
164         @trusted static string gtk3IconThemeName() nothrow
165         {
166             import inilike.file;
167             try {
168                 auto f = new IniLikeFile(xdgConfigHome("gtk-3.0/settings.ini"), IniLikeFile.ReadOptions(No.preserveComments));
169                 auto settings = f.group("Settings");
170                 if (settings)
171                     return settings.readEntry("gtk-icon-theme-name");
172             } catch(Exception e) {
173 
174             }
175             return null;
176         }
177 
178         try {
179             string themeName;
180             if (detector & IconThemeNameDetector.gtk3) {
181                 themeName = gtk3IconThemeName();
182             }
183             if (!themeName.length && (detector & IconThemeNameDetector.gtk2)) {
184                 themeName = gtk2IconThemeName();
185             }
186             if (!themeName.length && (detector & IconThemeNameDetector.fallback)) {
187                 themeName = fallbackIconThemeName();
188             }
189             return themeName;
190         } catch(Exception e) {
191 
192         }
193         return null;
194     }
195 
196     unittest
197     {
198         auto desktopGuard = EnvGuard("XDG_CURRENT_DESKTOP");
199         environment["XDG_CURRENT_DESKTOP"] = "";
200         assert(currentIconThemeName(IconThemeNameDetector.fallback).length);
201         assert(currentIconThemeName(IconThemeNameDetector.none).length == 0);
202 
203         version(iconthemeFileTest)
204         {
205             auto homeGuard = EnvGuard("HOME");
206             environment["HOME"] = "./test";
207 
208             auto configGuard = EnvGuard("XDG_CONFIG_HOME");
209             environment["XDG_CONFIG_HOME"] = "./test";
210 
211             assert(currentIconThemeName() == "gnome");
212             assert(currentIconThemeName(IconThemeNameDetector.gtk3) == "gnome");
213             assert(currentIconThemeName(IconThemeNameDetector.gtk2) == "oxygen");
214         }
215     }
216 }
217 
218 /**
219  * The list of icon theme directories based on data paths.
220  * Returns: Array of paths with "icons" subdirectory appended to each data path.
221  * Note: This function does not check if directories exist.
222  */
223 @trusted string[] baseIconDirs(Range)(Range dataPaths) if (isInputRange!Range && is(ElementType!Range : string))
224 {
225     return dataPaths.map!(p => buildPath(p, "icons")).array;
226 }
227 
228 ///
229 unittest
230 {
231     auto dataPaths = ["share", buildPath("local", "share")];
232     assert(equal(baseIconDirs(dataPaths), [buildPath("share", "icons"), buildPath("local", "share", "icons")]));
233 }