CLI interface to medialist (fossil mirror)
Révision | 47a6efbebc0ef1cd9aa5f6d6fe509b52ba295bae (tree) |
---|---|
l'heure | 2023-03-28 16:12:03 |
Auteur | mio <stigma@disr...> |
Commiter | mio |
Update mlib
FossilOrigin-Name: 78c7e26f7d9b54bd742ff35fe98afb936c56386e8a5e653c3d804ce24485dfc7
@@ -0,0 +1,13 @@ | ||
1 | +Zero-Clause BSD | |
2 | +=============== | |
3 | + | |
4 | +Permission to use, copy, modify, and/or distribute this software for | |
5 | +any purpose with or without fee is hereby granted. | |
6 | + | |
7 | +THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL | |
8 | +WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES | |
9 | +OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE | |
10 | +FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY | |
11 | +DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN | |
12 | +AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT | |
13 | +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |
@@ -0,0 +1,44 @@ | ||
1 | +... a collection of personal modules for the D Programming Language. | |
2 | + | |
3 | +This file was last updated 11. March 2023 | |
4 | + | |
5 | +There files here are just the ones that I'm commonly copying from one of my | |
6 | +projects to another. The purpose is more to provide a central location that | |
7 | +I can keep the files updated while making it easier for myself to update them | |
8 | +in my various projects. | |
9 | + | |
10 | + | |
11 | + | |
12 | + CONFIGPARSER | |
13 | + | |
14 | +The configparser module will simply parse a INI-like file and provide a | |
15 | +simple API to access the various settings within the parsed file. The | |
16 | +API isn't complete yet. | |
17 | + | |
18 | + | |
19 | + | |
20 | + CNI | |
21 | + | |
22 | +The replacement of CONFIGPARSER that has been used in the medialist-cli project. | |
23 | +While very similar in concept to an INI file, CNI has a defined specification, | |
24 | +https://github.com/libuconf/cni. | |
25 | + | |
26 | + | |
27 | + | |
28 | + DIRECTORIES | |
29 | + | |
30 | +Contains a simple API for retrieving the "common directories" for your operating | |
31 | +system. For example, on Linux (and most other POSIX systems) DIRECTORIES will | |
32 | +use the XDG Base Directory Specification. | |
33 | + | |
34 | +* This module only compiles under 'Posix' as defined by the D compiler. | |
35 | + | |
36 | + | |
37 | + TRASH | |
38 | + | |
39 | +While this could be an extension on to DIRECTORIES, I chose to keep it separate | |
40 | +since the specifications they follow are different enough. In short, this | |
41 | +module will utilise your operating systems "Recycle Bin" concept rather than | |
42 | +simple purging the file from existance. | |
43 | + | |
44 | +* This module only compiles under 'Posix' as defined by the D compiler. |
@@ -3,4 +3,4 @@ description "A package containing my single-file modules" | ||
3 | 3 | authors "mio" |
4 | 4 | license "0BSD" |
5 | 5 | |
6 | -targetType "library" | |
6 | +targetType "library" | |
\ No newline at end of file |
@@ -208,7 +208,7 @@ private: | ||
208 | 208 | moveForward(1); |
209 | 209 | } |
210 | 210 | |
211 | - value = app[]; | |
211 | + value = app.data(); | |
212 | 212 | moveForward(1); // move past ending ` |
213 | 213 | inRawValue = false; |
214 | 214 | } else { |
@@ -0,0 +1,775 @@ | ||
1 | +/* | |
2 | + * Permission to use, copy, modify, and/or distribute this software for any | |
3 | + * purpose with or without fee is herby granted. | |
4 | + * | |
5 | + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES | |
6 | + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF | |
7 | + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR | |
8 | + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES | |
9 | + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN | |
10 | + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF | |
11 | + * OR IN CONNECTION WITH THE USE OR PEFORMANCE OF THIS SOFTWARE. | |
12 | + */ | |
13 | + | |
14 | + | |
15 | +/** | |
16 | + * An incomplete single-file INI parser for D. | |
17 | + * | |
18 | + * The API should be similar to python's configparse module. Internally it | |
19 | + * uses the standard D associative array. | |
20 | + * | |
21 | + * Example: | |
22 | + * --- | |
23 | + * import configparser; | |
24 | + * | |
25 | + * auto config = new ConfigParser(); | |
26 | + * // no sections initially | |
27 | + * assert(config.sections.length == 0); | |
28 | + * // Section names ("Program Settings") are case-sensitive | |
29 | + * conf.addSection("Storage Paths"); | |
30 | + * // Option names ("CONFIG_PATH") are case-insensitive | |
31 | + * // (internally, they are all converted to lower-case) | |
32 | + * conf.set("Program Settings", "CONFIG_PATH", "/home/user/.local/config"); | |
33 | + * --- | |
34 | + * | |
35 | + * Authors: nemophila | |
36 | + * Date: 2023-03-19 | |
37 | + * Homepage: https://osdn.net/users/nemophila/pf/mlib | |
38 | + * License: 0BSD | |
39 | + * Version: 0.4 | |
40 | + * | |
41 | + * History: | |
42 | + * 0.4 Add .write() | |
43 | + * 0.3 Fix option values not always being treated as lowercase. | |
44 | + * 0.2 Add .getBool() | |
45 | + * 0.1 Initial release | |
46 | + */ | |
47 | +module mlib.configparser; | |
48 | + | |
49 | +private | |
50 | +{ | |
51 | + import std.conv : ConvException; | |
52 | + import std.stdio : File; | |
53 | +} | |
54 | + | |
55 | +public class DuplicateSectionException : Exception | |
56 | +{ | |
57 | + private string m_section; | |
58 | + | |
59 | + this(string section) | |
60 | + { | |
61 | + string msg = "Section " ~ section ~ " already exists."; | |
62 | + m_section = section; | |
63 | + super(msg); | |
64 | + } | |
65 | + | |
66 | + string section() | |
67 | + { | |
68 | + return m_section; | |
69 | + } | |
70 | +} | |
71 | + | |
72 | +/// | |
73 | +/// An exception that is thrown by a strict parser which indicates | |
74 | +/// that an option appears twice within any one section. | |
75 | +/// | |
76 | +public class DuplicateOptionException : Exception | |
77 | +{ | |
78 | + private string m_option; | |
79 | + private string m_section; | |
80 | + | |
81 | + this(string option, string section) | |
82 | + { | |
83 | + string msg = "Option " ~ option ~ " in section " ~ section ~ | |
84 | + " already exists."; | |
85 | + m_option = option; | |
86 | + m_section = section; | |
87 | + super(msg); | |
88 | + } | |
89 | + | |
90 | + string option() | |
91 | + { | |
92 | + return m_option; | |
93 | + } | |
94 | + | |
95 | + string section() | |
96 | + { | |
97 | + return m_section; | |
98 | + } | |
99 | +} | |
100 | + | |
101 | +public class NoSectionException : Exception | |
102 | +{ | |
103 | + private string m_section; | |
104 | + | |
105 | + this(string section) | |
106 | + { | |
107 | + string msg = "Section '" ~ section ~ "' does not exist."; | |
108 | + m_section = section; | |
109 | + super(msg); | |
110 | + } | |
111 | + | |
112 | + string section() | |
113 | + { | |
114 | + return m_section; | |
115 | + } | |
116 | +} | |
117 | + | |
118 | +public class NoOptionException : Exception | |
119 | +{ | |
120 | + private string m_section; | |
121 | + private string m_option; | |
122 | + | |
123 | + this(string section, string option) | |
124 | + { | |
125 | + string msg = "Section '" ~ section ~ "' does not have option '" ~ | |
126 | + option ~ "'."; | |
127 | + m_section = section; | |
128 | + m_option = option; | |
129 | + super(msg); | |
130 | + } | |
131 | + | |
132 | + string section() { return m_section; } | |
133 | + string option() { return m_option; } | |
134 | +} | |
135 | + | |
136 | +/** | |
137 | + * The main configuration parser. | |
138 | + */ | |
139 | +public class ConfigParser | |
140 | +{ | |
141 | + private char[] m_delimiters; | |
142 | + private char[] m_commentPrefixes; | |
143 | + private bool m_strict; | |
144 | + | |
145 | + /** current section for parsing */ | |
146 | + private string m_currentSection; | |
147 | + private string[string][string] m_sections; | |
148 | + | |
149 | + /** | |
150 | + * Creates a new instance of ConfigParser. | |
151 | + */ | |
152 | + this(char[] delimiters = ['=', ':'], | |
153 | + char[] commentPrefixes = ['#', ';'], | |
154 | + bool strict = true) | |
155 | + { | |
156 | + m_delimiters = delimiters; | |
157 | + m_commentPrefixes = commentPrefixes; | |
158 | + m_strict = strict; | |
159 | + } | |
160 | + | |
161 | + /** | |
162 | + * Return an array containing the available sections. | |
163 | + */ | |
164 | + string[] sections() | |
165 | + { | |
166 | + return m_sections.keys(); | |
167 | + } | |
168 | + | |
169 | + /// | |
170 | + unittest | |
171 | + { | |
172 | + auto conf = new ConfigParser(); | |
173 | + | |
174 | + assert(0 == conf.sections().length); | |
175 | + | |
176 | + conf.addSection("Section"); | |
177 | + | |
178 | + assert(1 == conf.sections().length); | |
179 | + } | |
180 | + | |
181 | + /** | |
182 | + * Add a section named `section` to the instance. | |
183 | + * | |
184 | + * Throws: | |
185 | + * - DuplicateSectionError if a section by the given name already | |
186 | + * exists. | |
187 | + */ | |
188 | + void addSection(string section) | |
189 | + { | |
190 | + if (section in m_sections) | |
191 | + throw new DuplicateSectionException(section); | |
192 | + m_sections[section] = null; | |
193 | + } | |
194 | + | |
195 | + /// | |
196 | + unittest | |
197 | + { | |
198 | + import std.exception : assertNotThrown, assertThrown; | |
199 | + | |
200 | + auto conf = new ConfigParser(); | |
201 | + | |
202 | + /* doesn't yet exist */ | |
203 | + assertNotThrown!DuplicateSectionException(conf.addSection("sample")); | |
204 | + /* already exists */ | |
205 | + assertThrown!DuplicateSectionException(conf.addSection("sample")); | |
206 | + } | |
207 | + | |
208 | + /** | |
209 | + * Indicates whether the named `section` is present in the configuration. | |
210 | + * | |
211 | + * Params: | |
212 | + * section = The section to check for in the configuration. | |
213 | + * | |
214 | + * Returns: `true` if the section exists, `false` otherwise. | |
215 | + */ | |
216 | + bool hasSection(string section) | |
217 | + { | |
218 | + auto exists = (section in m_sections); | |
219 | + return (exists !is null); | |
220 | + } | |
221 | + | |
222 | + /// | |
223 | + unittest | |
224 | + { | |
225 | + auto conf = new ConfigParser(); | |
226 | + conf.addSection("nExt"); | |
227 | + assert(true == conf.hasSection("nExt"), "Close the world."); | |
228 | + assert(false == conf.hasSection("world"), "Open the nExt."); | |
229 | + } | |
230 | + | |
231 | + string[] options(string section) | |
232 | + { | |
233 | + if (false == this.hasSection(section)) | |
234 | + throw new NoSectionException(section); | |
235 | + return m_sections[section].keys(); | |
236 | + } | |
237 | + | |
238 | + /// | |
239 | + unittest | |
240 | + { | |
241 | + import std.exception : assertNotThrown, assertThrown; | |
242 | + | |
243 | + auto conf = new ConfigParser(); | |
244 | + | |
245 | + conf.addSection("Settings"); | |
246 | + | |
247 | + assertNotThrown!NoSectionException(conf.options("Settings")); | |
248 | + assertThrown!NoSectionException(conf.options("void")); | |
249 | + | |
250 | + string[] options = conf.options("Settings"); | |
251 | + assert(0 == options.length, "More keys than we need"); | |
252 | + } | |
253 | + | |
254 | + bool hasOption(string section, string option) | |
255 | + { | |
256 | + import std.string : toLower; | |
257 | + | |
258 | + if (false == this.hasSection(section)) | |
259 | + return false; | |
260 | + | |
261 | + scope lowercaseOption = toLower(option); | |
262 | + auto exists = (lowercaseOption in m_sections[section]); | |
263 | + return (exists !is null); | |
264 | + } | |
265 | + /* | |
266 | + string[] read(string[] filenames) | |
267 | + { | |
268 | + return null; | |
269 | + }*/ | |
270 | + | |
271 | + void read(string filename) | |
272 | + { | |
273 | + File file = File(filename, "r"); | |
274 | + scope(exit) { file.close(); } | |
275 | + read(file, false); | |
276 | + } | |
277 | + | |
278 | + /// | |
279 | + unittest | |
280 | + { | |
281 | + import std.file : remove; | |
282 | + import std.stdio : File; | |
283 | + | |
284 | + auto configFile = File("test.conf", "w+"); | |
285 | + configFile.writeln("[Section 1]"); | |
286 | + configFile.writeln("key=value"); | |
287 | + configFile.writeln("\n[Section 2]"); | |
288 | + configFile.writeln("key2 = value"); | |
289 | + configFile.close(); | |
290 | + | |
291 | + auto conf = new ConfigParser(); | |
292 | + conf.read("test.conf"); | |
293 | + | |
294 | + assert(2 == conf.sections.length, "Incorrect Sections length"); | |
295 | + assert(true == conf.hasSection("Section 1"), | |
296 | + "Config file doesn't have Section 1"); | |
297 | + assert(true == conf.hasOption("Section 1", "key"), | |
298 | + "Config file doesn't have 'key' in 'Section 1'"); | |
299 | + | |
300 | + remove("test.conf"); | |
301 | + } | |
302 | + | |
303 | + /** | |
304 | + * Parse a config file. | |
305 | + * | |
306 | + * Params: | |
307 | + * file = Reference to the file from which to read. | |
308 | + * close = Close the file when finished parsing. | |
309 | + */ | |
310 | + void read(ref File file, bool close = true) | |
311 | + { | |
312 | + import std.array : array; | |
313 | + import std.algorithm.searching : canFind; | |
314 | + import std.string : strip; | |
315 | + | |
316 | + scope(exit) { if (close) file.close(); } | |
317 | + | |
318 | + string[] lines = file.byLineCopy.array; | |
319 | + | |
320 | + for (auto i = 0; i < lines.length; i++) { | |
321 | + string line = lines[i].strip(); | |
322 | + | |
323 | + if (line == "") | |
324 | + continue; | |
325 | + | |
326 | + if ('[' == lines[i][0]) { | |
327 | + parseSectionHeader(lines[i]); | |
328 | + } else if (false == canFind(m_commentPrefixes, lines[i][0])) { | |
329 | + parseLine(lines[i]); | |
330 | + } | |
331 | + /* ignore comments */ | |
332 | + } | |
333 | + } | |
334 | + | |
335 | + /*void readString(string str) | |
336 | + { | |
337 | + }*/ | |
338 | + | |
339 | + /** | |
340 | + * Get an `option` value for the named `section`. | |
341 | + * | |
342 | + * Params: | |
343 | + * section = The section to look for the given `option`. | |
344 | + * option = The option to return the value of | |
345 | + * fallback = Fallback value if the `option` is not found. Can be null. | |
346 | + * | |
347 | + * Returns: | |
348 | + * - The value for `option` if it is found. | |
349 | + * - `null` if the `option` is not found and `fallback` is not provided. | |
350 | + * - `fallback` if the `option` is not found and `fallback` is provided. | |
351 | + * | |
352 | + * Throws: | |
353 | + * - NoSectionException if the `section` does not exist and no fallback is provided. | |
354 | + * - NoOptionException if the `option` does not exist and no fallback is provided. | |
355 | + */ | |
356 | + string get(string section, string option) | |
357 | + { | |
358 | + import std.string : toLower; | |
359 | + | |
360 | + scope lowercaseOption = toLower(option); | |
361 | + | |
362 | + if (false == this.hasSection(section)) | |
363 | + throw new NoSectionException(section); | |
364 | + | |
365 | + if (false == this.hasOption(section, lowercaseOption)) | |
366 | + throw new NoOptionException(section, lowercaseOption); | |
367 | + | |
368 | + return m_sections[section][lowercaseOption]; | |
369 | + } | |
370 | + | |
371 | + /// | |
372 | + unittest | |
373 | + { | |
374 | + import std.exception : assertThrown; | |
375 | + | |
376 | + auto conf = new ConfigParser(); | |
377 | + conf.addSection("Section"); | |
378 | + conf.set("Section", "option", "value"); | |
379 | + | |
380 | + assert(conf.get("Section", "option") == "value"); | |
381 | + assertThrown!NoSectionException(conf.get("section", "option")); | |
382 | + assertThrown!NoOptionException(conf.get("Section", "void")); | |
383 | + } | |
384 | + | |
385 | + /// Ditto | |
386 | + string get(string section, string option, string fallback) | |
387 | + { | |
388 | + string res = fallback; | |
389 | + | |
390 | + try { | |
391 | + res = get(section, option); | |
392 | + } catch (NoSectionException e) { | |
393 | + return res; | |
394 | + } catch (NoOptionException e) { | |
395 | + return res; | |
396 | + } | |
397 | + | |
398 | + return res; | |
399 | + } | |
400 | + | |
401 | + /// | |
402 | + unittest | |
403 | + { | |
404 | + import std.exception : assertThrown; | |
405 | + | |
406 | + auto conf = new ConfigParser(); | |
407 | + conf.addSection("Section"); | |
408 | + conf.set("Section", "option", "value"); | |
409 | + | |
410 | + assert("value" == conf.get("Section", "option")); | |
411 | + assert("fallback" == conf.get("section", "option", "fallback")); | |
412 | + assert("fallback" == conf.get("Section", "void", "fallback")); | |
413 | + | |
414 | + /* can use null for fallback */ | |
415 | + assert(null == conf.get("section", "option", null)); | |
416 | + assert(null == conf.get("Section", "void", null)); | |
417 | + } | |
418 | + | |
419 | + /** | |
420 | + * A convenience method which casts the value of `option` in `section` | |
421 | + * to an integer. | |
422 | + * | |
423 | + * Params: | |
424 | + * section = The section to look for the given `option`. | |
425 | + * option = The option to return the value for. | |
426 | + * fallback = The fallback value to use if `option` isn't found. | |
427 | + * | |
428 | + * Returns: | |
429 | + * | |
430 | + * | |
431 | + * Throws: | |
432 | + * - NoSectionFoundException if `section` doesn't exist. | |
433 | + * - NoOptionFoundException if the `section` doesn't contain `option`. | |
434 | + * - ConvException if it failed to parse the value to an int. | |
435 | + * - ConvOverflowException if the value would overflow an int. | |
436 | + * | |
437 | + * See_Also: get() | |
438 | + */ | |
439 | + int getInt(string section, string option) | |
440 | + { | |
441 | + import std.conv : parse; | |
442 | + | |
443 | + string res; | |
444 | + | |
445 | + res = get(section, option); | |
446 | + | |
447 | + return parse!int(res); | |
448 | + } | |
449 | + | |
450 | + /// Ditto | |
451 | + int getInt(string section, string option, int fallback) | |
452 | + { | |
453 | + int res = fallback; | |
454 | + | |
455 | + try { | |
456 | + res = getInt(section, option); | |
457 | + } catch (Exception e) { | |
458 | + return res; | |
459 | + } | |
460 | + | |
461 | + return res; | |
462 | + } | |
463 | + | |
464 | + /* | |
465 | + double getDouble(string section, string option) | |
466 | + { | |
467 | + } | |
468 | + | |
469 | + double getDouble(string section, string option, double fallback) | |
470 | + { | |
471 | + } | |
472 | + | |
473 | + float getFloat(string section, string option) | |
474 | + { | |
475 | + } | |
476 | + | |
477 | + float getFloat(string section, string option, float fallback) | |
478 | + { | |
479 | + }*/ | |
480 | + | |
481 | + /** | |
482 | + * A convenience method which coerces the $(I option) in the | |
483 | + * specified $(I section) to a boolean value. | |
484 | + * | |
485 | + * Note that the accepted values for the option are "1", "yes", | |
486 | + * "true", and "on", which cause this method to return `true`, and | |
487 | + * "0", "no", "false", and "off", which cause it to return `false`. | |
488 | + * | |
489 | + * These string values are checked in a case-insensitive manner. | |
490 | + * | |
491 | + * Params: | |
492 | + * section = The section to look for the given option. | |
493 | + * option = The option to return the value for. | |
494 | + * fallback = The fallback value to use if the option was not found. | |
495 | + * | |
496 | + * Throws: | |
497 | + * - NoSectionFoundException if `section` doesn't exist. | |
498 | + * - NoOptionFoundException if the `section` doesn't contain `option`. | |
499 | + * - ConvException if any other value was found. | |
500 | + */ | |
501 | + bool getBool(string section, string option) | |
502 | + { | |
503 | + import std.string : toLower; | |
504 | + | |
505 | + string value = get(section, option); | |
506 | + | |
507 | + switch (value.toLower) | |
508 | + { | |
509 | + case "1": | |
510 | + case "yes": | |
511 | + case "true": | |
512 | + case "on": | |
513 | + return true; | |
514 | + case "0": | |
515 | + case "no": | |
516 | + case "false": | |
517 | + case "off": | |
518 | + return false; | |
519 | + default: | |
520 | + throw new ConvException("No valid boolean value found"); | |
521 | + } | |
522 | + } | |
523 | + | |
524 | + /// Ditto | |
525 | + bool getBool(string section, string option, bool fallback) | |
526 | + { | |
527 | + try { | |
528 | + return getBool(section, option); | |
529 | + } catch (Exception e) { | |
530 | + return fallback; | |
531 | + } | |
532 | + } | |
533 | + | |
534 | + /* | |
535 | + string[string] items(string section) | |
536 | + { | |
537 | + }*/ | |
538 | + | |
539 | + /** | |
540 | + * Remove the specified `option` from the specified `section`. | |
541 | + * | |
542 | + * Params: | |
543 | + * section = The section to remove from. | |
544 | + * option = The option to remove from section. | |
545 | + * | |
546 | + * Retruns: | |
547 | + * `true` if option existed, false otherwise. | |
548 | + * | |
549 | + * Throws: | |
550 | + * - NoSectionException if the specified section doesn't exist. | |
551 | + */ | |
552 | + bool removeOption(string section, string option) | |
553 | + { | |
554 | + if ((section in m_sections) is null) { | |
555 | + throw new NoSectionException(section); | |
556 | + } | |
557 | + | |
558 | + if (option in m_sections[section]) { | |
559 | + m_sections[section].remove(option); | |
560 | + return true; | |
561 | + } | |
562 | + | |
563 | + return false; | |
564 | + } | |
565 | + | |
566 | + /// | |
567 | + unittest | |
568 | + { | |
569 | + import std.exception : assertThrown; | |
570 | + | |
571 | + auto conf = new ConfigParser(); | |
572 | + conf.addSection("Default"); | |
573 | + conf.set("Default", "exists", "true"); | |
574 | + | |
575 | + assertThrown!NoSectionException(conf.removeOption("void", "false")); | |
576 | + assert(false == conf.removeOption("Default", "void")); | |
577 | + assert(true == conf.removeOption("Default", "exists")); | |
578 | + } | |
579 | + | |
580 | + /** | |
581 | + * Remove the specified `section` from the config. | |
582 | + * | |
583 | + * Params: | |
584 | + * section = The section to remove. | |
585 | + * | |
586 | + * Returns: | |
587 | + * `true` if the section existed, `false` otherwise. | |
588 | + */ | |
589 | + bool removeSection(string section) | |
590 | + { | |
591 | + if (section in m_sections) { | |
592 | + m_sections.remove(section); | |
593 | + return true; | |
594 | + } | |
595 | + return false; | |
596 | + } | |
597 | + | |
598 | + /// | |
599 | + unittest | |
600 | + { | |
601 | + auto conf = new ConfigParser(); | |
602 | + conf.addSection("Exists"); | |
603 | + assert(false == conf.removeSection("DoesNotExist")); | |
604 | + assert(true == conf.removeSection("Exists")); | |
605 | + } | |
606 | + | |
607 | + void set(string section, string option, string value) | |
608 | + { | |
609 | + import std.string : toLower; | |
610 | + | |
611 | + if (false == this.hasSection(section)) | |
612 | + throw new NoSectionException(section); | |
613 | + | |
614 | + scope lowercaseOption = toLower(option); | |
615 | + m_sections[section][lowercaseOption] = value; | |
616 | + } | |
617 | + | |
618 | + /// | |
619 | + unittest | |
620 | + { | |
621 | + import std.exception : assertThrown; | |
622 | + | |
623 | + auto conf = new ConfigParser(); | |
624 | + | |
625 | + assertThrown!NoSectionException(conf.set("Section", "option", | |
626 | + "value")); | |
627 | + | |
628 | + conf.addSection("Section"); | |
629 | + conf.set("Section", "option", "value"); | |
630 | + assert(conf.get("Section", "option") == "value"); | |
631 | + } | |
632 | + | |
633 | + /// | |
634 | + /// Write a representation of the configuration to the | |
635 | + /// provided *file*. | |
636 | + /// | |
637 | + /// This representation can be parsed by future calls to | |
638 | + /// `read`. This does **not** close the file after writing. | |
639 | + /// | |
640 | + /// Params: | |
641 | + /// file = An open file which was opened in text mode. | |
642 | + /// spaceAroundDelimiters = The delimiters between keys and | |
643 | + /// values are surrounded by spaces. | |
644 | + /// | |
645 | + /// Note: Comments from the original file are not preserved when | |
646 | + /// writing the configuration back. | |
647 | + /// | |
648 | + void write(ref File file, bool spaceAroundDelimiters = true) | |
649 | + { | |
650 | + string del = spaceAroundDelimiters ? " = " : "="; | |
651 | + | |
652 | + foreach(string section, string[string] options; m_sections) { | |
653 | + file.writefln("[%s]", section); | |
654 | + | |
655 | + foreach(string option, string value; options) { | |
656 | + file.writefln("%s%s%s", option, del, value); | |
657 | + } | |
658 | + } | |
659 | + } | |
660 | + | |
661 | + /// | |
662 | + unittest | |
663 | + { | |
664 | + import std.file : remove; | |
665 | + import std.stdio : File; | |
666 | + | |
667 | + auto writer = new ConfigParser(); | |
668 | + writer.addSection("general"); | |
669 | + | |
670 | + writer.addSection("GUI"); | |
671 | + writer.set("GUI", "WINDOW_WIDTH", "848"); | |
672 | + writer.set("GUI", "WINDOW_HEIGHT", "480"); | |
673 | + | |
674 | + auto file = File("test.ini", "w+"); | |
675 | + scope(exit) remove(file.name); | |
676 | + writer.write(file); | |
677 | + | |
678 | + file.rewind(); | |
679 | + | |
680 | + auto reader = new ConfigParser(); | |
681 | + reader.read(file); | |
682 | + | |
683 | + assert(reader.hasSection("general"), "reader does not contain general section"); | |
684 | + | |
685 | + assert(reader.hasSection("GUI"), "reader does not contain GUI section"); | |
686 | + assert(reader.get("GUI", "WINDOW_WIDTH") == "848", "reader GUI.WINDOW_WIDTH is not 848"); | |
687 | + assert(reader.getInt("GUI", "WINDOW_WIDTH") == 848, "reader GUI.WINDOW_WIDTH is not 848 (int)"); | |
688 | + | |
689 | + assert(reader.get("GUI", "WINDOW_HEIGHT") == "480", "reader GUI.WINDOW_HEIGHT is not 480"); | |
690 | + assert(reader.getInt("GUI", "WINDOW_HEIGHT") == 480, "reader GUI.WINDOW_HEIGHT is not 480 (int)"); | |
691 | + } | |
692 | + | |
693 | + private: | |
694 | + | |
695 | + void parseSectionHeader(ref string line) | |
696 | + { | |
697 | + import std.array : appender, assocArray; | |
698 | + | |
699 | + auto sectionHeader = appender!string; | |
700 | + /* presume that the last character is ] */ | |
701 | + sectionHeader.reserve(line.length - 1); | |
702 | + string popped = line[1 .. $]; | |
703 | + | |
704 | + foreach(c; popped) { | |
705 | + if (c != ']') | |
706 | + sectionHeader.put(c); | |
707 | + else | |
708 | + break; | |
709 | + } | |
710 | + | |
711 | + m_currentSection = sectionHeader.data(); | |
712 | + | |
713 | + if (m_currentSection in m_sections && m_strict) | |
714 | + throw new DuplicateSectionException(m_currentSection); | |
715 | + | |
716 | + try { | |
717 | + this.addSection(m_currentSection); | |
718 | + } catch (DuplicateSectionException) { | |
719 | + } | |
720 | + } | |
721 | + | |
722 | + void parseLine(ref string line) | |
723 | + { | |
724 | + import std.string : indexOfAny, toLower, strip; | |
725 | + | |
726 | + ptrdiff_t idx = line.indexOfAny(m_delimiters); | |
727 | + if (-1 == idx) return; | |
728 | + string option = line[0 .. idx].dup.strip.toLower; | |
729 | + string value = line[idx + 1 .. $].dup.strip; | |
730 | + | |
731 | + if (option in m_sections[m_currentSection] && m_strict) | |
732 | + throw new DuplicateOptionException(option, m_currentSection); | |
733 | + | |
734 | + m_sections[m_currentSection][option] = value; | |
735 | + } | |
736 | + | |
737 | + unittest | |
738 | + { | |
739 | + import std.exception : assertThrown, assertNotThrown; | |
740 | + import std.file : remove; | |
741 | + | |
742 | + auto f = File("config.cfg", "w+"); | |
743 | + f.writeln("[section]"); | |
744 | + f.writeln("option = value"); | |
745 | + f.writeln("Option = value"); | |
746 | + f.close(); | |
747 | + scope(exit) remove("config.cfg"); | |
748 | + | |
749 | + // Duplicate option | |
750 | + scope parser = new ConfigParser(); | |
751 | + assertThrown!DuplicateOptionException(parser.read("config.cfg")); | |
752 | + | |
753 | + // Duplicate section | |
754 | + f = File("config.cfg", "w+"); | |
755 | + f.writeln("[section]"); | |
756 | + f.writeln("option = value"); | |
757 | + f.writeln("[section]"); | |
758 | + f.close(); | |
759 | + | |
760 | + assertThrown!DuplicateSectionException(parser.read("config.cfg")); | |
761 | + | |
762 | + // not strict | |
763 | + scope relaxedParser = new ConfigParser(['='], [], false); | |
764 | + | |
765 | + assertNotThrown!DuplicateSectionException(relaxedParser.read("config.cfg")); | |
766 | + assert(relaxedParser.hasSection("section")); | |
767 | + | |
768 | + f = File("config.cfg", "a+"); | |
769 | + f.writeln("option = newValue"); | |
770 | + f.close(); | |
771 | + | |
772 | + assertNotThrown!DuplicateOptionException(relaxedParser.read("config.cfg")); | |
773 | + assert(relaxedParser.get("section", "option") == "newValue"); | |
774 | + } | |
775 | +} |
@@ -14,65 +14,1139 @@ | ||
14 | 14 | /** |
15 | 15 | * This module provides quick & easy access to the common directories |
16 | 16 | * for each operating system. Currently, only POSIX (XDG Base Directory |
17 | - * Specification) is supported. OS X (Standard Directories) and Windows | |
18 | - * (Known Folder) will be supported at a later date. | |
17 | + * Specification) and Windows (Known Folder) are supported. | |
18 | + * OS X (Standard Directories) will be supported at a later date. | |
19 | 19 | * |
20 | 20 | * The main goal of this module is to provide a minimal and simple API. |
21 | 21 | * |
22 | - * API | |
22 | + * ## Example | |
23 | + * | |
23 | 24 | * --- |
24 | - * enum Directory | |
25 | - * { | |
26 | - * home, | |
27 | - * data, | |
28 | - * config, | |
29 | - * state, | |
30 | - * cache, | |
31 | - * runtime | |
32 | - * } | |
25 | + * import std.stdio : writefln; | |
26 | + * import std.path : buildPath; | |
27 | + * | |
28 | + * import mlib.directories : getProjectDirectories; | |
29 | + * | |
30 | + * void main(string[] args) | |
31 | + * { | |
32 | + * ProjectDirectories projectDirs = getProjectDirectories("org", "Example ORG", "My Program"); | |
33 | + * auto config = readConfig(projectDirs.configDir); | |
34 | + * | |
35 | + * // ...rest of program | |
36 | + * } | |
37 | + * | |
38 | + * auto readConfig(string path) | |
39 | + * { | |
40 | + * // ...some implementation | |
41 | + * } | |
33 | 42 | * |
34 | - * DirEntry open(Directory); | |
35 | 43 | * --- |
36 | 44 | * |
37 | - * This module supports D version greater than or equal to 2.068.0. | |
45 | + * | |
46 | + * This module supports D version greater than or equal to 2.076.0. | |
38 | 47 | * |
39 | 48 | * Authors: nemophila |
40 | - * Date: February 18, 2023 | |
49 | + * Date: March 6, 2023 | |
41 | 50 | * Homepage: https://osdn.net/users/nemophila/pf/mlib |
42 | - * License: 0BSD | |
43 | - * Standards: XDG Base Directory Specification 0.8 | |
44 | - * Version: 0.1.0 | |
51 | + * License: $(LINK2 https://osdn.net/users/nemophila/pf/mlib/scm/blobs/trunk/LICENSE, 0BSD) | |
52 | + * Standards: | |
53 | + * $(UL | |
54 | + * $(LI $(LINK2 https://specifications.freedesktop.org/basedir-spec/0.8/, XDG Base Directory Specification 0.8)) | |
55 | + * $(LI $(LINK2 https://learn.microsoft.com/en-us/previous-versions/windows/desktop/legacy/bb776911(v=vs.85), Known Folders)) | |
56 | + * ) | |
57 | + * Version: 0.2.0 | |
45 | 58 | * |
46 | 59 | * History: |
47 | 60 | * 0.X.X was the initial version (June 12, 2021) |
48 | 61 | * |
49 | 62 | * 0.1.0 adds support for runtime and state (February 18, 2023) |
50 | 63 | * |
51 | - * Bugs: | |
52 | - * - Doesn't support compiling on operating systems other that Posix | |
53 | - * - Doesn't check if the environment variable value is empty. | |
64 | + * 0.2.0 Re-wrote the API. Now supports Windows. Bump minimum D version to 2.076.0 (March 26, 2023) | |
54 | 65 | */ |
55 | 66 | module mlib.directories; |
56 | 67 | |
57 | 68 | import std.file : DirEntry; |
58 | 69 | |
70 | +/// | |
71 | +/// Deprecated: Use the new `getBaseDirectories`, `getProjectDirectories`, | |
72 | +/// and `getUserDirectories` functions. This will be removed | |
73 | +/// in version 0.3.0. | |
74 | +/// | |
75 | +deprecated("Use new get{Base,Project,User}Directories functions (remove 0.3.0)") | |
59 | 76 | public enum Directory |
60 | - { | |
61 | - home, | |
62 | - data, | |
63 | - config, | |
64 | - state, | |
65 | - // dataDirs, /* XDG */ | |
66 | - // configDirs, /* XDG */ | |
67 | - cache, | |
68 | - runtime, | |
69 | - } | |
77 | +{ | |
78 | + home, | |
79 | + data, | |
80 | + config, | |
81 | + state, | |
82 | + // dataDirs, /* XDG */ | |
83 | + // configDirs, /* XDG */ | |
84 | + cache, | |
85 | + runtime, | |
86 | +} | |
87 | + | |
88 | +/// | |
89 | +/// Provides paths of user-invisible standard directories, following the | |
90 | +/// conventions of the operating system the library is running on. | |
91 | +/// | |
92 | +/// To compute the location of cache, config or data directories for | |
93 | +/// individual projects or applications, use ProjectDirectories instead. | |
94 | +/// | |
95 | +/// ## Examples | |
96 | +/// | |
97 | +/// All examples on this page are computed with a user named | |
98 | +/// $(I Elq). | |
99 | +/// | |
100 | +/// An example of `BaseDirectories#configDir`: | |
101 | +/// | |
102 | +/// $(UL | |
103 | +/// $(LI $(B Posix): `/home/elq/.config`) | |
104 | +/// $(LI $(B macOS): $(RED Currently not supported)) | |
105 | +/// $(LI $(B Windows): `C:\Users\Elq\AppData\Roaming`) | |
106 | +/// ) | |
107 | +/// | |
108 | +struct BaseDirectories | |
109 | +{ | |
110 | + /// | |
111 | + /// Returns the path to the user's home directory. | |
112 | + /// | |
113 | + /// $(TABLE | |
114 | + /// $(TR | |
115 | + /// $(TH Platform) | |
116 | + /// $(TH Value) | |
117 | + /// $(TH Example) | |
118 | + /// ) | |
119 | + /// $(TR | |
120 | + /// $(TD Posix) | |
121 | + /// $(TD `$HOME`) | |
122 | + /// $(TD /home/elq) | |
123 | + /// ) | |
124 | + /// $(TR | |
125 | + /// $(TD macOS) | |
126 | + /// $(TD $(RED Platform not supported)) | |
127 | + /// $(TD $(RED Platform not supported)) | |
128 | + /// ) | |
129 | + /// $(TR | |
130 | + /// $(TD Windows) | |
131 | + /// $(TD `FOLDERID_Profile`) | |
132 | + /// $(TD C:\Users\Elq) | |
133 | + /// ) | |
134 | + /// ) | |
135 | + /// | |
136 | + immutable string homeDir; | |
137 | + | |
138 | + /// | |
139 | + /// Returns the path to the user's cache directory. | |
140 | + /// | |
141 | + /// $(TABLE | |
142 | + /// $(TR | |
143 | + /// $(TH Platform) | |
144 | + /// $(TH Value) | |
145 | + /// $(TH Example) | |
146 | + /// ) | |
147 | + /// $(TR | |
148 | + /// $(TD Posix) | |
149 | + /// $(TD `$XDG_CACHE_HOME` (fallback: `$HOME/.cache`)) | |
150 | + /// $(TD /home/elq/.cache) | |
151 | + /// ) | |
152 | + /// $(TR | |
153 | + /// $(TD macOS) | |
154 | + /// $(TD $(RED Platform not supported)) | |
155 | + /// $(TD $(RED Platform not supported)) | |
156 | + /// ) | |
157 | + /// $(TR | |
158 | + /// $(TD Windows) | |
159 | + /// $(TD `FOLDERID_LocalAppData`) | |
160 | + /// $(TD C:\Users\Elq\AppData\Local) | |
161 | + /// ) | |
162 | + /// ) | |
163 | + /// | |
164 | + immutable string cacheDir; | |
165 | + | |
166 | + /// | |
167 | + /// Returns the path to the user's configuration directory. | |
168 | + /// | |
169 | + /// $(TABLE | |
170 | + /// $(TR | |
171 | + /// $(TH Platform) | |
172 | + /// $(TH Value) | |
173 | + /// $(TH Example) | |
174 | + /// ) | |
175 | + /// $(TR | |
176 | + /// $(TD Posix) | |
177 | + /// $(TD `$XDG_CONFIG_HOME` (fallback: `$HOME/.config`)) | |
178 | + /// $(TD /home/elq/.config) | |
179 | + /// ) | |
180 | + /// $(TR | |
181 | + /// $(TD macOS) | |
182 | + /// $(TD $(RED Platform not supported)) | |
183 | + /// $(TD $(RED Platform not supported)) | |
184 | + /// ) | |
185 | + /// $(TR | |
186 | + /// $(TD Windows) | |
187 | + /// $(TD `FOLDERID_RoamingAppData`) | |
188 | + /// $(TD C:\Users\Elq\AppData\Roaming) | |
189 | + /// ) | |
190 | + /// ) | |
191 | + /// | |
192 | + immutable string configDir; | |
193 | + | |
194 | + /// | |
195 | + /// Returns the path to the user's data directory. | |
196 | + /// | |
197 | + /// $(TABLE | |
198 | + /// $(TR | |
199 | + /// $(TH Platform) | |
200 | + /// $(TH Value) | |
201 | + /// $(TH Example) | |
202 | + /// ) | |
203 | + /// $(TR | |
204 | + /// $(TD Posix) | |
205 | + /// $(TD `$XDG_DATA_HOME` (fallback: `$HOME/.local/share`)) | |
206 | + /// $(TD /home/elq/.local/share) | |
207 | + /// ) | |
208 | + /// $(TR | |
209 | + /// $(TD macOS) | |
210 | + /// $(TD $(RED Platform not supported)) | |
211 | + /// $(TD $(RED Platform not supported)) | |
212 | + /// ) | |
213 | + /// $(TR | |
214 | + /// $(TD Windows) | |
215 | + /// $(TD `FOLDERID_RoamingAppData`) | |
216 | + /// $(TD C:\Users\Elq\AppData\Roaming) | |
217 | + /// ) | |
218 | + /// ) | |
219 | + /// | |
220 | + immutable string dataDir; | |
221 | + | |
222 | + /// | |
223 | + /// Returns the path to the user's local data directory. | |
224 | + /// | |
225 | + /// $(TABLE | |
226 | + /// $(TR | |
227 | + /// $(TH Platform) | |
228 | + /// $(TH Value) | |
229 | + /// $(TH Example) | |
230 | + /// ) | |
231 | + /// $(TR | |
232 | + /// $(TD Posix) | |
233 | + /// $(TD `$XDG_DATA_HOME` (fallback: `$HOME/.local/share`)) | |
234 | + /// $(TD /home/elq/.local/share) | |
235 | + /// ) | |
236 | + /// $(TR | |
237 | + /// $(TD macOS) | |
238 | + /// $(TD $(RED Platform not supported)) | |
239 | + /// $(TD $(RED Platform not supported)) | |
240 | + /// ) | |
241 | + /// $(TR | |
242 | + /// $(TD Windows) | |
243 | + /// $(TD `FOLDERID_LocalAppData`) | |
244 | + /// $(TD C:\Users\Elq\AppData\Local) | |
245 | + /// ) | |
246 | + /// ) | |
247 | + /// | |
248 | + immutable string dataLocalDir; | |
249 | + | |
250 | + /// | |
251 | + /// Returns the path to the user's local executable directory. | |
252 | + /// | |
253 | + /// $(TABLE | |
254 | + /// $(TR | |
255 | + /// $(TH Platform) | |
256 | + /// $(TH Value) | |
257 | + /// $(TH Example) | |
258 | + /// ) | |
259 | + /// $(TR | |
260 | + /// $(TD Posix) | |
261 | + /// $(TD `$HOME/.local/bin`) | |
262 | + /// $(TD /home/elq/.local/bin) | |
263 | + /// ) | |
264 | + /// $(TR | |
265 | + /// $(TD macOS) | |
266 | + /// $(TD $(RED Platform not supported)) | |
267 | + /// $(TD $(RED Platform not supported)) | |
268 | + /// ) | |
269 | + /// $(TR | |
270 | + /// $(TD Windows) | |
271 | + /// $(TD N/A) | |
272 | + /// $(TD `""`) | |
273 | + /// ) | |
274 | + /// ) | |
275 | + /// | |
276 | + immutable string executableDir; | |
277 | + | |
278 | + /// | |
279 | + /// Returns the path to the user's preference directory. | |
280 | + /// | |
281 | + /// $(TABLE | |
282 | + /// $(TR | |
283 | + /// $(TH Platform) | |
284 | + /// $(TH Value) | |
285 | + /// $(TH Example) | |
286 | + /// ) | |
287 | + /// $(TR | |
288 | + /// $(TD Posix) | |
289 | + /// $(TD `$XDG_CONFIG_HOME` (fallback: `$HOME/.config`)) | |
290 | + /// $(TD /home/elq/.config) | |
291 | + /// ) | |
292 | + /// $(TR | |
293 | + /// $(TD macOS) | |
294 | + /// $(TD $(RED Platform not supported)) | |
295 | + /// $(TD $(RED Platform not supported)) | |
296 | + /// ) | |
297 | + /// $(TR | |
298 | + /// $(TD Windows) | |
299 | + /// $(TD `FOLDERID_RoamingAppData`) | |
300 | + /// $(TD C:\Users\Elq\AppData\Roaming) | |
301 | + /// ) | |
302 | + /// ) | |
303 | + /// | |
304 | + immutable string preferenceDir; | |
305 | + | |
306 | + /// | |
307 | + /// Returns the path to the user's runtime directory. | |
308 | + /// | |
309 | + /// $(TABLE | |
310 | + /// $(TR | |
311 | + /// $(TH Platform) | |
312 | + /// $(TH Value) | |
313 | + /// $(TH Example) | |
314 | + /// ) | |
315 | + /// $(TR | |
316 | + /// $(TD Posix) | |
317 | + /// $(TD `$XDG_RUNTIME_DIR`) | |
318 | + /// $(TD /run/user/1000) | |
319 | + /// ) | |
320 | + /// $(TR | |
321 | + /// $(TD macOS) | |
322 | + /// $(TD $(RED Platform not supported)) | |
323 | + /// $(TD $(RED Platform not supported)) | |
324 | + /// ) | |
325 | + /// $(TR | |
326 | + /// $(TD Windows) | |
327 | + /// $(TD N/A) | |
328 | + /// $(TD `""`) | |
329 | + /// ) | |
330 | + /// ) | |
331 | + /// | |
332 | + immutable string runtimeDir; | |
333 | + | |
334 | + string toString() const @safe pure nothrow | |
335 | + { | |
336 | + version (OSX) { | |
337 | + enum platform = "OSX"; | |
338 | + } else version (Posix) { | |
339 | + enum platform = "Posix"; | |
340 | + } else version (Windows) { | |
341 | + enum platform = "Windows"; | |
342 | + } | |
343 | + | |
344 | + return "BaseDirectories(" ~ platform ~ "):\n" ~ | |
345 | + " homeDir = '" ~ homeDir ~ "'\n" ~ | |
346 | + " cacheDir = '" ~ cacheDir ~ "'\n" ~ | |
347 | + " configDir = '" ~ configDir ~ "'\n" ~ | |
348 | + " dataDir = '" ~ dataDir ~ "'\n" ~ | |
349 | + " dataLocalDir = '" ~ dataLocalDir ~ "'\n" ~ | |
350 | + " executableDir = '" ~ executableDir ~ "'\n" ~ | |
351 | + " preferenceDir = '" ~ preferenceDir ~ "'\n" ~ | |
352 | + " runtimeDir = '" ~ runtimeDir ~ "'\n"; | |
353 | + } | |
354 | +} | |
355 | + | |
356 | +/// | |
357 | +/// Returns a new instance of `BaseDirectories`. | |
358 | +/// | |
359 | +/// The instance is an immutable snapshop of the state of the system at | |
360 | +/// the time this function is called. Subsequent changes to the state | |
361 | +/// of the system are not reflected in instances created prior to such | |
362 | +/// a change. | |
363 | +/// | |
364 | +nothrow BaseDirectories getBaseDirectories() | |
365 | +{ | |
366 | + // OS X first so it doesn't get mixed with Posix. | |
367 | + version (OSX) { | |
368 | + // Support will be added. | |
369 | + static assert(false, "mlib.directories: Unsupported operating system."); | |
370 | + } else version (Posix) { | |
371 | + return BaseDirectories( | |
372 | + posixHome(), | |
373 | + xdgCache(), | |
374 | + xdgConfig(), | |
375 | + xdgData(), | |
376 | + xdgData(), | |
377 | + // from spec: | |
378 | + // User-specific executable files may be stored in $HOME/.local/bin. | |
379 | + buildPath(posixHome(), ".local", "bin"), | |
380 | + xdgConfig(), | |
381 | + xdgRuntime() | |
382 | + ); | |
383 | + } else version (Windows) { | |
384 | + string dataDir = windowsRoamingData(); | |
385 | + string localDataDir = windowsLocalData(); | |
386 | + | |
387 | + return BaseDirectories( | |
388 | + windowsHome(), | |
389 | + localDataDir, | |
390 | + dataDir, | |
391 | + dataDir, | |
392 | + localDataDir, | |
393 | + "", | |
394 | + dataDir, | |
395 | + "" | |
396 | + ); | |
397 | + } else { | |
398 | + static assert(false, "mlib.directories: Unsupported operating system."); | |
399 | + } | |
400 | +} | |
401 | + | |
402 | +/// | |
403 | +unittest | |
404 | +{ | |
405 | + import std.stdio : writeln; | |
406 | + | |
407 | + BaseDirectories baseDirs = getBaseDirectories(); | |
408 | + writeln(baseDirs); | |
409 | +} | |
410 | + | |
411 | +/// | |
412 | +/// `ProjectDirectories ` computes the location of cache, config, or | |
413 | +/// data directories for a specific application, which are derived from | |
414 | +/// the standard directories and the name of the project/organisation. | |
415 | +/// | |
416 | +/// ## Examples | |
417 | +/// | |
418 | +/// All examples in this section are computed with a user named *Elq*, | |
419 | +/// and a `ProjectDirectories` instance created with the following | |
420 | +/// information: | |
421 | +/// | |
422 | +/// ```d | |
423 | +/// ProjectDirectories projectDirs = getProjectDirectories("com", "Foo Corp", "Bar App"); | |
424 | +/// ``` | |
425 | +/// | |
426 | +/// Example of `ProjectDirectories#configDir` value in different | |
427 | +/// operating systems: | |
428 | +/// | |
429 | +/// $(UL | |
430 | +/// $(LI $(B Posix): `/home/elq/.config/barapp`) | |
431 | +/// $(LI $(B macOS): $(RED Platform not supported)) | |
432 | +/// $(LI $(B Windows): `C:\Users\Elq\AppData\Roaming\Foo Corp\Bar App\config`) | |
433 | +/// ) | |
434 | +/// | |
435 | +struct ProjectDirectories | |
436 | +{ | |
437 | + /// | |
438 | + /// The path to the project's cache directory in which | |
439 | + /// `<project_path>` is the value of `ProjectDirectories#projectPath`. | |
440 | + /// | |
441 | + /// $(TABLE | |
442 | + /// $(TR | |
443 | + /// $(TH Platform) | |
444 | + /// $(TH Value) | |
445 | + /// $(TH Example) | |
446 | + /// ) | |
447 | + /// $(TR | |
448 | + /// $(TD Posix) | |
449 | + /// $(TD `$XDG_CACHE_HOME/<project_path>` (fallback: `$HOME/.cache/<project_path>`)) | |
450 | + /// $(TD /home/elq/.cache/barapp) | |
451 | + /// ) | |
452 | + /// $(TR | |
453 | + /// $(TD macOS) | |
454 | + /// $(TD $(RED Platform not supported)) | |
455 | + /// $(TD $(RED Platform not supported)) | |
456 | + /// ) | |
457 | + /// $(TR | |
458 | + /// $(TD Windows) | |
459 | + /// $(TD `FOLDERID_LocalAppData\<project_path>\cache`) | |
460 | + /// $(TD C:\Users\Elq\AppData\Local\Foo Corp\Bar App\cache ) | |
461 | + /// ) | |
462 | + /// ) | |
463 | + /// | |
464 | + immutable string cacheDir; | |
465 | + | |
466 | + /// | |
467 | + /// The path to the project's configuration directory, in which | |
468 | + /// `<project_path>` is the value of `ProjectDirectories#projectPath`. | |
469 | + /// | |
470 | + /// $(TABLE | |
471 | + /// $(TR | |
472 | + /// $(TH Platform) | |
473 | + /// $(TH Value) | |
474 | + /// $(TH Example) | |
475 | + /// ) | |
476 | + /// $(TR | |
477 | + /// $(TD Posix) | |
478 | + /// $(TD `$XDG_CONFIG_HOME/<project_path>` (fallback: `$HOME/.config/<project_path>/`)) | |
479 | + /// $(TD /home/elq/.config/barapp) | |
480 | + /// ) | |
481 | + /// $(TR | |
482 | + /// $(TD macOS) | |
483 | + /// $(TD $(RED Platform not supported)) | |
484 | + /// $(TD $(RED Platform not supported)) | |
485 | + /// ) | |
486 | + /// $(TR | |
487 | + /// $(TD Windows) | |
488 | + /// $(TD `FOLDERID_RoamingAppData\<project_path>\config`) | |
489 | + /// $(TD C:\Users\Elq\AppData\Roaming\Foo Corp\Bar App\config ) | |
490 | + /// ) | |
491 | + /// ) | |
492 | + /// | |
493 | + immutable string configDir; | |
494 | + | |
495 | + /// | |
496 | + /// The path to the project's data directory, in which | |
497 | + /// `<project_path>` is the value of `ProjectDirectories#projectPath`. | |
498 | + /// | |
499 | + /// $(TABLE | |
500 | + /// $(TR | |
501 | + /// $(TH Platform) | |
502 | + /// $(TH Value) | |
503 | + /// $(TH Example) | |
504 | + /// ) | |
505 | + /// $(TR | |
506 | + /// $(TD Posix) | |
507 | + /// $(TD `$XDG_DATA_HOME/<project_path>` (fallback: `$HOME/.local/share/<project_path>/`)) | |
508 | + /// $(TD /home/elq/.local/share/barapp) | |
509 | + /// ) | |
510 | + /// $(TR | |
511 | + /// $(TD macOS) | |
512 | + /// $(TD $(RED Platform not supported)) | |
513 | + /// $(TD $(RED Platform not supported)) | |
514 | + /// ) | |
515 | + /// $(TR | |
516 | + /// $(TD Windows) | |
517 | + /// $(TD `FOLDERID_RoamingAppData\<project_path>\data`) | |
518 | + /// $(TD C:\Users\Elq\AppData\Roaming\Foo Corp\Bar App\data ) | |
519 | + /// ) | |
520 | + /// ) | |
521 | + /// | |
522 | + immutable string dataDir; | |
523 | + | |
524 | + /// | |
525 | + /// The path to the project's local data directory, in which | |
526 | + /// `<project_path>` is the value of `ProjectDirectories#projectPath`. | |
527 | + /// | |
528 | + /// $(TABLE | |
529 | + /// $(TR | |
530 | + /// $(TH Platform) | |
531 | + /// $(TH Value) | |
532 | + /// $(TH Example) | |
533 | + /// ) | |
534 | + /// $(TR | |
535 | + /// $(TD Posix) | |
536 | + /// $(TD `$XDG_DATA_HOME/<project_path>` (fallback: `$HOME/.local/share/<project_path>/`)) | |
537 | + /// $(TD /home/elq/.local/share/barapp) | |
538 | + /// ) | |
539 | + /// $(TR | |
540 | + /// $(TD macOS) | |
541 | + /// $(TD $(RED Platform not supported)) | |
542 | + /// $(TD $(RED Platform not supported)) | |
543 | + /// ) | |
544 | + /// $(TR | |
545 | + /// $(TD Windows) | |
546 | + /// $(TD `FOLDERID_LocalAppData\<project_path>\data`) | |
547 | + /// $(TD C:\Users\Elq\AppData\Local\Foo Corp\Bar App\data ) | |
548 | + /// ) | |
549 | + /// ) | |
550 | + /// | |
551 | + immutable string dataLocalDir; | |
552 | + | |
553 | + /// | |
554 | + /// The path to the project's preference directory, in which | |
555 | + /// `<project_path>` is the value of `ProjectDirectories#projectPath`. | |
556 | + /// | |
557 | + /// $(TABLE | |
558 | + /// $(TR | |
559 | + /// $(TH Platform) | |
560 | + /// $(TH Value) | |
561 | + /// $(TH Example) | |
562 | + /// ) | |
563 | + /// $(TR | |
564 | + /// $(TD Posix) | |
565 | + /// $(TD `$XDG_CONFIG_HOME/<project_path>` (fallback: `$HOME/.config/<project_path>/`)) | |
566 | + /// $(TD /home/elq/.config/barapp) | |
567 | + /// ) | |
568 | + /// $(TR | |
569 | + /// $(TD macOS) | |
570 | + /// $(TD $(RED Platform not supported)) | |
571 | + /// $(TD $(RED Platform not supported)) | |
572 | + /// ) | |
573 | + /// $(TR | |
574 | + /// $(TD Windows) | |
575 | + /// $(TD `FOLDERID_RoamingAppData\<project_path>\config`) | |
576 | + /// $(TD C:\Users\Elq\AppData\Roaming\Foo Corp\Bar App\config) | |
577 | + /// ) | |
578 | + /// ) | |
579 | + /// | |
580 | + immutable string preferenceDir; | |
581 | + | |
582 | + /// | |
583 | + /// The path to the project's data directory, in which | |
584 | + /// `<project_path>` is the value of `ProjectDirectories#projectPath`. | |
585 | + /// | |
586 | + /// $(TABLE | |
587 | + /// $(TR | |
588 | + /// $(TH Platform) | |
589 | + /// $(TH Value) | |
590 | + /// $(TH Example) | |
591 | + /// ) | |
592 | + /// $(TR | |
593 | + /// $(TD Posix) | |
594 | + /// $(TD `$XDG_RUNTIME_DIR`) | |
595 | + /// $(TD /run/user/1001/bareapp) | |
596 | + /// ) | |
597 | + /// $(TR | |
598 | + /// $(TD macOS) | |
599 | + /// $(TD $(RED Platform not supported)) | |
600 | + /// $(TD $(RED Platform not supported)) | |
601 | + /// ) | |
602 | + /// $(TR | |
603 | + /// $(TD Windows) | |
604 | + /// $(TD N/A) | |
605 | + /// $(TD `""`) | |
606 | + /// ) | |
607 | + /// ) | |
608 | + /// | |
609 | + immutable string runtimeDir; | |
610 | + | |
611 | + /// | |
612 | + /// The project path fragment used to compute the project's | |
613 | + /// cache/config/data directories. | |
614 | + /// | |
615 | + /// The value is derived from the arguments provided to the | |
616 | + /// `getProjectDirectories()` function and is platform-dependent. | |
617 | + /// | |
618 | + immutable string projectPath; | |
619 | + | |
620 | + string toString() const @safe pure nothrow | |
621 | + { | |
622 | + version (OSX) { | |
623 | + enum platform = "OSX"; | |
624 | + } else version (Posix) { | |
625 | + enum platform = "Posix"; | |
626 | + } else version (Windows) { | |
627 | + enum platform = "Windows"; | |
628 | + } | |
629 | + | |
630 | + return "ProjectDirectories(" ~ platform ~ ")\n" ~ | |
631 | + " projectPath = '" ~ projectPath ~ "'\n" ~ | |
632 | + " cacheDir = '" ~ cacheDir ~ "'\n" ~ | |
633 | + " configDir = '" ~ configDir ~ "'\n" ~ | |
634 | + " dataDir = '" ~ dataDir ~ "'\n" ~ | |
635 | + " dataLocalDir = '" ~ dataLocalDir ~ "'\n" ~ | |
636 | + " preferenceDir = '" ~ preferenceDir ~ "'\n" ~ | |
637 | + " runtimeDir = '" ~ runtimeDir ~ "'\n"; | |
638 | + } | |
639 | +} | |
640 | + | |
641 | +/// | |
642 | +/// Return an instance of `ProjectDirectories` from values describing | |
643 | +/// the project. | |
644 | +/// | |
645 | +/// Params: | |
646 | +/// qualifier = The reverse domain name notation of the application, | |
647 | +/// excluding the organisation or application name itself. $(BR) | |
648 | +/// An example string can be passed if no qualifier should | |
649 | +/// be used (only affects macOS). $(BR) | |
650 | +/// Example values: `"com.example"`, `"org"`, `"co.uk"`, `""`. | |
651 | +/// | |
652 | +/// organisation = The name of the organisation that develops this application, | |
653 | +/// or for which the application is developed.$(BR) | |
654 | +/// An empty string can be passed if no organisation should be | |
655 | +/// used (only affects macOS and Windows).$(BR) | |
656 | +/// Example values: `"Foo Corp"`, `"Alice and Bob Inc"`, `""`. | |
657 | +/// | |
658 | +/// application = The name of the application itself.$(BR) | |
659 | +/// Example values: `"Bar App"`, `"ExampleProgram"`, `"Unicorn-Programme"`. | |
660 | +/// | |
661 | +/// Returns: An instance of `ProjectDirectories`, whose directory field values are | |
662 | +/// based on the `qualifier`, `organisation`, and `application` arguments. | |
663 | +/// | |
664 | +ProjectDirectories getProjectDirectories(string qualifier, string organisation, string application) nothrow | |
665 | +in | |
666 | +{ | |
667 | + assert(!empty(organisation) && !empty(application), | |
668 | + "The organisation and the application arguments cannot both be empty"); | |
669 | +} | |
670 | +do | |
671 | +{ | |
672 | + version (OSX) | |
673 | + { | |
674 | + // Support will be added. | |
675 | + static assert(false, "mlib.directories: Unsupported operating system."); | |
676 | + } | |
677 | + else version (Posix) | |
678 | + { | |
679 | + import std.uni : isWhite, toLower; | |
680 | + | |
681 | + // Yes, we could call toLower(replace(string(application), " ", "-")) | |
682 | + // but that could throw. This is nothrow. | |
683 | + string subPath; | |
684 | + bool reachedNonWhitespace = false; | |
685 | + | |
686 | + foreach(c; application) { | |
687 | + if (' ' == c) { | |
688 | + // Check it's not a newline or something silly. | |
689 | + if (reachedNonWhitespace && false == isWhite(c)) { | |
690 | + subPath ~= '-'; | |
691 | + // We only want one '-', so make sure | |
692 | + // to skip any succeeding spaces. | |
693 | + reachedNonWhitespace = false; | |
694 | + } | |
695 | + } else { | |
696 | + subPath ~= toLower(c); | |
697 | + reachedNonWhitespace = true; | |
698 | + } | |
699 | + } | |
700 | + | |
701 | + string configDir = buildPath(xdgConfig(), subPath); | |
702 | + string dataDir = buildPath(xdgData(), subPath); | |
703 | + | |
704 | + return ProjectDirectories( | |
705 | + buildPath(xdgCache(), subPath), | |
706 | + configDir, | |
707 | + dataDir, | |
708 | + dataDir, | |
709 | + configDir, | |
710 | + xdgRuntime(subPath), | |
711 | + subPath | |
712 | + ); | |
713 | + } | |
714 | + else version (Windows) | |
715 | + { | |
716 | + bool hasOrg = !empty(organisation); | |
717 | + bool hasApp = !empty(application); | |
718 | + string subPath; | |
719 | + if (hasOrg) | |
720 | + { | |
721 | + subPath = organisation; | |
722 | + if (hasApp) | |
723 | + { | |
724 | + subPath ~= "\\"; | |
725 | + } | |
726 | + } | |
727 | + if (hasApp) | |
728 | + { | |
729 | + subPath ~= application; | |
730 | + } | |
731 | + | |
732 | + string roamingAppData = buildPath(GetKnownFolder(FOLDERID_RoamingAppData), subPath); | |
733 | + string localAppData = buildPath(GetKnownFolder(FOLDERID_LocalAppData), subPath); | |
734 | + string configDir = buildPath(roamingAppData, "config"); | |
735 | + | |
736 | + return ProjectDirectories( | |
737 | + buildPath(localAppData, "cache"), | |
738 | + configDir, | |
739 | + buildPath(roamingAppData, "data"), | |
740 | + buildPath(localAppData, "data"), | |
741 | + configDir, | |
742 | + "", | |
743 | + subPath | |
744 | + ); | |
745 | + } | |
746 | + else | |
747 | + { | |
748 | + static assert(false, "mlib.directories: Unsupported operating system."); | |
749 | + } | |
750 | +} | |
751 | + | |
752 | +/// | |
753 | +unittest | |
754 | +{ | |
755 | + import std.stdio : writeln; | |
756 | + | |
757 | + ProjectDirectories projectDirs = getProjectDirectories("net", "Sporadic Programmers", "Foo bar-baz"); | |
758 | + writeln(projectDirs); | |
759 | +} | |
760 | + | |
761 | +/// | |
762 | +/// Provides the paths of user-facing standard directories, following | |
763 | +/// the conventions of the operating system the library is running on. | |
764 | +/// | |
765 | +/// ## Examples | |
766 | +/// | |
767 | +/// All examples in this section are computed with a user named $(I Elq). | |
768 | +/// | |
769 | +/// Example of `UserDirectories#audioDir` value in different operating systems: | |
770 | +/// | |
771 | +/// $(UL | |
772 | +/// $(LI $(B Posix): `/home/elq/Music`) | |
773 | +/// $(LI $(B macOS): $(RED Platform not supported.)) | |
774 | +/// $(LI $(B Windows): `C:\Users\Elq\Music`) | |
775 | +/// ) | |
776 | +/// | |
777 | +struct UserDirectories | |
778 | +{ | |
779 | + /// | |
780 | + /// The path to the user's home directory. | |
781 | + /// | |
782 | + /// $(TABLE | |
783 | + /// $(TR | |
784 | + /// $(TH Platform) | |
785 | + /// $(TH Value) | |
786 | + /// $(TH Example) | |
787 | + /// ) | |
788 | + /// $(TR | |
789 | + /// $(TD Posix) | |
790 | + /// $(TD `$HOME`) | |
791 | + /// $(TD /home/elq) | |
792 | + /// ) | |
793 | + /// $(TR | |
794 | + /// $(TD macOS) | |
795 | + /// $(TD $(RED Platform not supported)) | |
796 | + /// $(TD $(RED Platform not supported)) | |
797 | + /// ) | |
798 | + /// $(TR | |
799 | + /// $(TD Windows) | |
800 | + /// $(TD `FOLDERID_Profile`) | |
801 | + /// $(TD C:\Users\Elq) | |
802 | + /// ) | |
803 | + /// ) | |
804 | + /// | |
805 | + immutable string homeDir; | |
806 | + | |
807 | + /// | |
808 | + /// The path to the user's audio directory. | |
809 | + /// | |
810 | + /// $(TABLE | |
811 | + /// $(TR | |
812 | + /// $(TH Platform) | |
813 | + /// $(TH Value) | |
814 | + /// $(TH Example) | |
815 | + /// ) | |
816 | + /// $(TR | |
817 | + /// $(TD Posix) | |
818 | + /// $(TD `$XDG_MUSIC_DIR`) | |
819 | + /// $(TD /home/elq/Music) | |
820 | + /// ) | |
821 | + /// $(TR | |
822 | + /// $(TD macOS) | |
823 | + /// $(TD $(RED Platform not supported)) | |
824 | + /// $(TD $(RED Platform not supported)) | |
825 | + /// ) | |
826 | + /// $(TR | |
827 | + /// $(TD Windows) | |
828 | + /// $(TD `FOLDERID_Music`) | |
829 | + /// $(TD C:\Users\Elq\Music) | |
830 | + /// ) | |
831 | + /// ) | |
832 | + /// | |
833 | + immutable string audioDir; | |
834 | + | |
835 | + /// | |
836 | + /// The path to the user's desktop directory. | |
837 | + /// | |
838 | + /// $(TABLE | |
839 | + /// $(TR | |
840 | + /// $(TH Platform) | |
841 | + /// $(TH Value) | |
842 | + /// $(TH Example) | |
843 | + /// ) | |
844 | + /// $(TR | |
845 | + /// $(TD Posix) | |
846 | + /// $(TD `$XDG_DESKTOP_DIR`) | |
847 | + /// $(TD /home/elq/Desktop) | |
848 | + /// ) | |
849 | + /// $(TR | |
850 | + /// $(TD macOS) | |
851 | + /// $(TD $(RED Platform not supported)) | |
852 | + /// $(TD $(RED Platform not supported)) | |
853 | + /// ) | |
854 | + /// $(TR | |
855 | + /// $(TD Windows) | |
856 | + /// $(TD `FOLDERID_Desktop`) | |
857 | + /// $(TD C:\Users\Elq\Desktop) | |
858 | + /// ) | |
859 | + /// ) | |
860 | + /// | |
861 | + immutable string desktopDir; | |
862 | + | |
863 | + /// | |
864 | + /// The path to the user's documents directory. | |
865 | + /// | |
866 | + /// $(TABLE | |
867 | + /// $(TR | |
868 | + /// $(TH Platform) | |
869 | + /// $(TH Value) | |
870 | + /// $(TH Example) | |
871 | + /// ) | |
872 | + /// $(TR | |
873 | + /// $(TD Posix) | |
874 | + /// $(TD `$XDG_DOCUMENTS_DIR`) | |
875 | + /// $(TD /home/elq/Documents) | |
876 | + /// ) | |
877 | + /// $(TR | |
878 | + /// $(TD macOS) | |
879 | + /// $(TD $(RED Platform not supported)) | |
880 | + /// $(TD $(RED Platform not supported)) | |
881 | + /// ) | |
882 | + /// $(TR | |
883 | + /// $(TD Windows) | |
884 | + /// $(TD `FOLDERID_Documents`) | |
885 | + /// $(TD C:\Users\Elq\Documents) | |
886 | + /// ) | |
887 | + /// ) | |
888 | + /// | |
889 | + immutable string documentDir; | |
890 | + | |
891 | + /// | |
892 | + /// The path to the user's download directory. | |
893 | + /// | |
894 | + /// $(TABLE | |
895 | + /// $(TR | |
896 | + /// $(TH Platform) | |
897 | + /// $(TH Value) | |
898 | + /// $(TH Example) | |
899 | + /// ) | |
900 | + /// $(TR | |
901 | + /// $(TD Posix) | |
902 | + /// $(TD `$XDG_DOWNLOAD_DIR`) | |
903 | + /// $(TD /home/elq/Downloads) | |
904 | + /// ) | |
905 | + /// $(TR | |
906 | + /// $(TD macOS) | |
907 | + /// $(TD $(RED Platform not supported)) | |
908 | + /// $(TD $(RED Platform not supported)) | |
909 | + /// ) | |
910 | + /// $(TR | |
911 | + /// $(TD Windows) | |
912 | + /// $(TD `FOLDERID_Downloads`) | |
913 | + /// $(TD C:\Users\Elq\Downloads) | |
914 | + /// ) | |
915 | + /// ) | |
916 | + /// | |
917 | + immutable string downloadDir; | |
918 | + | |
919 | + /// | |
920 | + /// The path to the user's fonts directory. | |
921 | + /// | |
922 | + /// $(TABLE | |
923 | + /// $(TR | |
924 | + /// $(TH Platform) | |
925 | + /// $(TH Value) | |
926 | + /// $(TH Example) | |
927 | + /// ) | |
928 | + /// $(TR | |
929 | + /// $(TD Posix) | |
930 | + /// $(TD `$XDG_DATA_HOME/fonts` (fallback: `$HOME/.local/share/fonts`)) | |
931 | + /// $(TD /home/elq/.local/share/fonts) | |
932 | + /// ) | |
933 | + /// $(TR | |
934 | + /// $(TD macOS) | |
935 | + /// $(TD $(RED Platform not supported)) | |
936 | + /// $(TD $(RED Platform not supported)) | |
937 | + /// ) | |
938 | + /// $(TR | |
939 | + /// $(TD Windows) | |
940 | + /// $(TD N/A) | |
941 | + /// $(TD `""`) | |
942 | + /// ) | |
943 | + /// ) | |
944 | + /// | |
945 | + immutable string fontDir; | |
946 | + | |
947 | + /// | |
948 | + /// The path to the user's pictures directory. | |
949 | + /// | |
950 | + /// $(TABLE | |
951 | + /// $(TR | |
952 | + /// $(TH Platform) | |
953 | + /// $(TH Value) | |
954 | + /// $(TH Example) | |
955 | + /// ) | |
956 | + /// $(TR | |
957 | + /// $(TD Posix) | |
958 | + /// $(TD `$XDG_PICTURES_DIR`) | |
959 | + /// $(TD /home/elq/Pictures) | |
960 | + /// ) | |
961 | + /// $(TR | |
962 | + /// $(TD macOS) | |
963 | + /// $(TD $(RED Platform not supported)) | |
964 | + /// $(TD $(RED Platform not supported)) | |
965 | + /// ) | |
966 | + /// $(TR | |
967 | + /// $(TD Windows) | |
968 | + /// $(TD `FOLDERID_Pictures`) | |
969 | + /// $(TD C:\Users\Elq\Pictures) | |
970 | + /// ) | |
971 | + /// ) | |
972 | + /// | |
973 | + immutable string pictureDir; | |
974 | + | |
975 | + /// | |
976 | + /// The path to the user's public directory. | |
977 | + /// | |
978 | + /// $(TABLE | |
979 | + /// $(TR | |
980 | + /// $(TH Platform) | |
981 | + /// $(TH Value) | |
982 | + /// $(TH Example) | |
983 | + /// ) | |
984 | + /// $(TR | |
985 | + /// $(TD Posix) | |
986 | + /// $(TD `$XDG_PUBLICSHARE_DIR`) | |
987 | + /// $(TD /home/elq/Public) | |
988 | + /// ) | |
989 | + /// $(TR | |
990 | + /// $(TD macOS) | |
991 | + /// $(TD $(RED Platform not supported)) | |
992 | + /// $(TD $(RED Platform not supported)) | |
993 | + /// ) | |
994 | + /// $(TR | |
995 | + /// $(TD Windows) | |
996 | + /// $(TD `FOLDERID_Public`) | |
997 | + /// $(TD C:\Users\Public) | |
998 | + /// ) | |
999 | + /// ) | |
1000 | + /// | |
1001 | + immutable string publicDir; | |
1002 | + | |
1003 | + /// | |
1004 | + /// The path to the user's template directory. | |
1005 | + /// | |
1006 | + /// $(TABLE | |
1007 | + /// $(TR | |
1008 | + /// $(TH Platform) | |
1009 | + /// $(TH Value) | |
1010 | + /// $(TH Example) | |
1011 | + /// ) | |
1012 | + /// $(TR | |
1013 | + /// $(TD Posix) | |
1014 | + /// $(TD `$XDG_TEMPLATES_DIR`) | |
1015 | + /// $(TD /home/elq/Templates) | |
1016 | + /// ) | |
1017 | + /// $(TR | |
1018 | + /// $(TD macOS) | |
1019 | + /// $(TD $(RED Platform not supported)) | |
1020 | + /// $(TD $(RED Platform not supported)) | |
1021 | + /// ) | |
1022 | + /// $(TR | |
1023 | + /// $(TD Windows) | |
1024 | + /// $(TD `FOLDERID_Templates`) | |
1025 | + /// $(TD C:\Users\Elq\AppData\Roaming\Microsoft\Windows\Templates) | |
1026 | + /// ) | |
1027 | + /// ) | |
1028 | + /// | |
1029 | + immutable string templateDir; | |
1030 | + | |
1031 | + /// | |
1032 | + /// The path to the user's video directory. | |
1033 | + /// | |
1034 | + /// $(TABLE | |
1035 | + /// $(TR | |
1036 | + /// $(TH Platform) | |
1037 | + /// $(TH Value) | |
1038 | + /// $(TH Example) | |
1039 | + /// ) | |
1040 | + /// $(TR | |
1041 | + /// $(TD Posix) | |
1042 | + /// $(TD `$XDG_VIDEOS_DIR`) | |
1043 | + /// $(TD /home/elq/Videos) | |
1044 | + /// ) | |
1045 | + /// $(TR | |
1046 | + /// $(TD macOS) | |
1047 | + /// $(TD $(RED Platform not supported)) | |
1048 | + /// $(TD $(RED Platform not supported)) | |
1049 | + /// ) | |
1050 | + /// $(TR | |
1051 | + /// $(TD Windows) | |
1052 | + /// $(TD `FOLDERID_Videos`) | |
1053 | + /// $(TD C:\Users\Elq\Videos) | |
1054 | + /// ) | |
1055 | + /// ) | |
1056 | + /// | |
1057 | + immutable string videoDir; | |
1058 | + | |
1059 | + string toString() const @safe pure nothrow | |
1060 | + { | |
1061 | + version (OSX) | |
1062 | + { | |
1063 | + enum platform = "OSX"; | |
1064 | + } | |
1065 | + else version (Posix) | |
1066 | + { | |
1067 | + enum platform = "Posix"; | |
1068 | + } | |
1069 | + else version (Windows) | |
1070 | + { | |
1071 | + enum platform = "Windows"; | |
1072 | + } | |
1073 | + return "UserDirectories(" ~ platform ~ ")\n" ~ | |
1074 | + " homeDir = '" ~ homeDir ~ "'\n" ~ | |
1075 | + " audioDir = '" ~ audioDir ~ "'\n" ~ | |
1076 | + " desktopDir = '" ~ desktopDir ~ "'\n" ~ | |
1077 | + " documentDir = '" ~ documentDir ~ "'\n" ~ | |
1078 | + " downloadDir = '" ~ downloadDir ~ "'\n" ~ | |
1079 | + " fontDir = '" ~ fontDir ~ "'\n" ~ | |
1080 | + " pictureDir = '" ~ pictureDir ~ "'\n" ~ | |
1081 | + " publicDir = '" ~ publicDir ~ "'\n" ~ | |
1082 | + " templateDir = '" ~ templateDir ~ "'\n" ~ | |
1083 | + " videoDir = '" ~ videoDir ~ "'\n"; | |
1084 | + } | |
1085 | +} | |
1086 | + | |
1087 | +/// | |
1088 | +/// Get a new instance of `UserDirectories`. | |
1089 | +/// | |
1090 | +/// The instance is an immutable snapshop of the current state of the | |
1091 | +/// system at the time this function was called. Subsequent changes | |
1092 | +/// to the state of the system are not reflected in instances created | |
1093 | +/// prior to such a change. | |
1094 | +/// | |
1095 | +nothrow UserDirectories getUserDirectories() | |
1096 | +{ | |
1097 | + version (OSX) { | |
1098 | + // Support will be added. | |
1099 | + static assert(false, "mlib.directories: Unsupported operating system."); | |
1100 | + } else version (Posix) { | |
1101 | + // Fallbacks are from on | |
1102 | + // https://cgit.freedesktop.org/xdg/xdg-user-dirs/tree/user-dirs.defaults | |
1103 | + return UserDirectories( | |
1104 | + posixHome(), | |
1105 | + xdgDir("MUSIC", buildPath(posixHome(), "Music")), | |
1106 | + xdgDir("DESKTOP", buildPath(posixHome(), "Desktop")), | |
1107 | + xdgDir("DOCUMENTS", buildPath(posixHome(), "Documents")), | |
1108 | + xdgDir("DOWNLOAD", buildPath(posixHome(), "Downloads")), | |
1109 | + buildPath(xdgData(), "fonts"), | |
1110 | + xdgDir("PICTURES", buildPath(posixHome(), "Pictures")), | |
1111 | + xdgDir("PUBLICSHARE", buildPath(posixHome(), "Public")), | |
1112 | + xdgDir("TEMPLATES", buildPath(posixHome(), "Templates")), | |
1113 | + xdgDir("VIDEOS", buildPath(posixHome(), "Videos")) | |
1114 | + ); | |
1115 | + } else version (Windows) { | |
1116 | + return UserDirectories( | |
1117 | + windowsHome(), | |
1118 | + GetKnownFolder(FOLDERID_Music), | |
1119 | + GetKnownFolder(FOLDERID_Desktop), | |
1120 | + GetKnownFolder(FOLDERID_Documents), | |
1121 | + GetKnownFolder(FOLDERID_Downloads), | |
1122 | + "", | |
1123 | + GetKnownFolder(FOLDERID_Pictures), | |
1124 | + GetKnownFolder(FOLDERID_Public), | |
1125 | + GetKnownFolder(FOLDERID_Templates), | |
1126 | + GetKnownFolder(FOLDERID_Videos) | |
1127 | + ); | |
1128 | + } else { | |
1129 | + static assert(false, "mlib.directories: Unsupported operating system."); | |
1130 | + } | |
1131 | +} | |
1132 | + | |
1133 | +/// | |
1134 | +unittest | |
1135 | +{ | |
1136 | + import std.stdio : writeln; | |
1137 | + UserDirectories userDirs = getUserDirectories(); | |
1138 | + writeln(userDirs); | |
1139 | +} | |
70 | 1140 | |
71 | 1141 | /++ |
72 | 1142 | Return a DirEntry pointing to `dt`. |
73 | - | |
1143 | + | |
74 | 1144 | If the folder couldn't be found, an empty DirEntry is returned. |
75 | - | |
1145 | + | |
1146 | + Deprecated: Use the new `getBaseDirectories`, `getProjectDirectories`, | |
1147 | + and `getUserDirectories` functions. This will be removed | |
1148 | + in version 0.3.0. | |
1149 | + | |
76 | 1150 | Examples: |
77 | 1151 | ---- |
78 | 1152 | import directories; |
@@ -92,72 +1166,55 @@ public enum Directory | ||
92 | 1166 | } |
93 | 1167 | ---- |
94 | 1168 | +/ |
1169 | +deprecated("Use new get{Base,Project,User}Directories functions (remove 0.3.0)") | |
95 | 1170 | public DirEntry open(Directory dt) nothrow// @safe |
96 | 1171 | { |
97 | - /* | |
98 | - * Posix covers a few operating systems, if yours has a alternate | |
99 | - * structure that is preferred over XDG, let me know. | |
100 | - */ | |
101 | - version (Posix) enum supported = true; | |
102 | - else enum supported = false; | |
103 | - /* More operating systems will be supported soon-ish. */ | |
104 | - | |
105 | - static if (false == supported) | |
106 | - { | |
107 | - import core.stdc.stdio : fprintf, stderr; | |
1172 | + /* | |
1173 | + * The Posix version covers a few operating systems, if your uses an | |
1174 | + * an alternative to XDG, let me know. | |
1175 | + */ | |
1176 | + version (Posix) enum supported = true; | |
1177 | + else version (Windows) enum supported = true; | |
1178 | + else enum supported = false; | |
108 | 1179 | |
109 | - fprintf(stderr, "*** error: The operating system you're running isn't supported. ***\n"); | |
110 | - // TODO: webpage for general contribution guide reporting. | |
111 | - assert(false, "Unsupported platform."); | |
1180 | + static if (false == supported) { | |
1181 | + static assert(0, "Unsupported platform."); | |
112 | 1182 | } |
113 | 1183 | |
114 | - immutable string path = getPath(dt); | |
115 | - if (path is null) return DirEntry(); | |
116 | - try { | |
117 | - return DirEntry(path); | |
118 | - } catch (Exception e) { | |
119 | - return DirEntry(); | |
120 | - } | |
1184 | + immutable string path = getPath(dt); | |
1185 | + if (path is null) return DirEntry(); | |
1186 | + try { | |
1187 | + return DirEntry(path); | |
1188 | + } catch (Exception e) { | |
1189 | + return DirEntry(); | |
1190 | + } | |
121 | 1191 | } |
122 | 1192 | |
123 | 1193 | /// |
124 | 1194 | unittest |
125 | 1195 | { |
126 | - import std.process : environment; | |
127 | - import std.path : buildPath; | |
128 | - | |
129 | - /* | |
130 | - * Environment Variables (for checking against) | |
131 | - * Currently only tests on Posix since that's the only supported | |
132 | - * platform. | |
133 | - */ | |
134 | - immutable string homeE = environment.get("HOME"); | |
135 | - immutable string configE = environment.get("XDG_CONFIG_HOME", | |
136 | - buildPath(homeE, ".config")); | |
137 | - immutable string dataE = environment.get("XDG_DATA_HOME", | |
138 | - buildPath(homeE, ".local", "share")); | |
139 | - immutable string cacheE = environment.get("XDG_CACHE_HOME", | |
140 | - buildPath(homeE, ".cache")); | |
141 | - | |
142 | - // Compare against folders.d (note that folders is cross-platform, so | |
143 | - // some errors may occur on non-POSIX) | |
144 | - assert(homeE == open(Directory.home)); | |
145 | - assert(configE == open(Directory.config)); | |
146 | - assert(dataE == open(Directory.data)); | |
147 | - assert(cacheE == open(Directory.cache)); | |
148 | -} | |
1196 | + import std.conv : to; | |
1197 | + import std.stdio : stderr; | |
149 | 1198 | |
150 | -private: | |
1199 | + DirEntry emptyDir; | |
151 | 1200 | |
152 | -void errLog(string msg) nothrow @trusted | |
153 | -{ | |
154 | - import core.stdc.stdio : fprintf, stderr; | |
155 | - | |
156 | - // TODO: webpage for general issue reporting. | |
157 | - // fprintf(stderr, "** info: report bugs to https://yume-neru.neocities.org/bugs.html **\n"); | |
158 | - fprintf(stderr, "*** error: %s ***\n", msg.ptr); | |
1201 | + foreach(dir; Directory.min..Directory.max) | |
1202 | + { | |
1203 | + string dirAsString = to!string(dir); | |
1204 | + DirEntry dirEntry = open(dir); | |
1205 | + version (Windows) | |
1206 | + { | |
1207 | + // Neither of these directories are supported on Windows. | |
1208 | + if (dir == Directory.state || dir == Directory.runtime) | |
1209 | + continue; | |
1210 | + } | |
1211 | + assert(emptyDir != dirEntry, "Failed to open(Directory." ~ dirAsString ~ ")"); | |
1212 | + stderr.writefln("Successfully opened Directory.%s: %s", dirAsString, dirEntry.name()); | |
1213 | + } | |
159 | 1214 | } |
160 | 1215 | |
1216 | +private: | |
1217 | + | |
161 | 1218 | immutable(string) getPath(in Directory dt) nothrow @safe |
162 | 1219 | { |
163 | 1220 | switch (dt) |
@@ -181,104 +1238,77 @@ immutable(string) getPath(in Directory dt) nothrow @safe | ||
181 | 1238 | |
182 | 1239 | immutable(string) home() nothrow @trusted |
183 | 1240 | { |
184 | - import std.path : isAbsolute; | |
185 | - import std.process : environment; | |
186 | - | |
187 | - string homeE; | |
188 | - | |
189 | - try { | |
190 | - homeE = environment.get("HOME"); | |
191 | - } catch (Exception e) { | |
192 | - homeE = null; | |
193 | - } | |
194 | - | |
195 | - if (homeE is null) { | |
196 | - import std.string : fromStringz; | |
197 | - const(char)* pwdHome = fallbackHome(); | |
198 | - if (pwdHome !is null) | |
199 | - homeE = cast(string)(pwdHome.fromStringz).dup; | |
200 | - if (false == homeE.isAbsolute) | |
201 | - homeE = null; | |
202 | - } | |
203 | - return homeE; | |
1241 | + version (Posix) { | |
1242 | + return posixHome(); | |
1243 | + } else version (Windows) { | |
1244 | + return windowsHome(); | |
1245 | + } else { | |
1246 | + static assert(false, "mlib.directories: Unsupported operating system."); | |
1247 | + } | |
204 | 1248 | } |
205 | 1249 | |
206 | 1250 | immutable(string) data() nothrow @safe |
207 | 1251 | { |
208 | - import std.path : buildPath, isAbsolute; | |
209 | - import std.process : environment; | |
210 | - | |
211 | - string dataE; | |
212 | - | |
213 | - try { | |
214 | - dataE = environment.get("XDG_DATA_HOME"); | |
215 | - } catch (Exception e) { | |
216 | - dataE = null; | |
217 | - } | |
218 | - | |
219 | - if (dataE is null || false == dataE.isAbsolute) | |
220 | - dataE = buildPath(home(), ".local", "share"); | |
221 | - | |
222 | - return dataE; | |
1252 | + version (Posix) { | |
1253 | + return xdgData(); | |
1254 | + } else version (Windows) { | |
1255 | + return windowsRoamingData(); | |
1256 | + } else { | |
1257 | + static assert(false, "mlib.directories: Unsupported operating system."); | |
1258 | + } | |
223 | 1259 | } |
224 | 1260 | |
225 | 1261 | immutable(string) config() nothrow @safe |
226 | 1262 | { |
227 | - import std.path : buildPath, isAbsolute; | |
228 | - import std.process : environment; | |
229 | - | |
230 | - string configE; | |
231 | - | |
232 | - try { | |
233 | - configE = environment.get("XDG_CONFIG_HOME"); | |
234 | - } catch (Exception e) { | |
235 | - configE = null; | |
236 | - } | |
237 | - | |
238 | - if (configE is null || false == configE.isAbsolute) | |
239 | - configE = buildPath(home(), ".config"); | |
240 | - | |
241 | - return configE; | |
1263 | + version (Posix) { | |
1264 | + return xdgConfig(); | |
1265 | + } else version (Windows) { | |
1266 | + return GetKnownFolder(FOLDERID_RoamingAppData); | |
1267 | + } else { | |
1268 | + static assert(false, "mlib.directories: Unsupported operating system."); | |
1269 | + } | |
242 | 1270 | } |
243 | 1271 | |
244 | 1272 | /* TODO: xdgState() nothrow @safe */ |
245 | 1273 | |
246 | 1274 | immutable(string) cache() nothrow @safe |
247 | 1275 | { |
248 | - import std.path : buildPath, isAbsolute; | |
249 | - import std.process : environment; | |
250 | - | |
251 | - string cacheE; | |
252 | - | |
253 | - try { | |
254 | - cacheE = environment.get("XDG_CACHE_HOME"); | |
255 | - } catch (Exception e) { | |
256 | - cacheE = null; | |
257 | - } | |
258 | - | |
259 | - if (cacheE is null || false == cacheE.isAbsolute) | |
260 | - cacheE = buildPath(home(), ".cache"); | |
261 | - | |
262 | - return cacheE; | |
1276 | + version (Posix) { | |
1277 | + return xdgCache(); | |
1278 | + } else version (Windows) { | |
1279 | + return GetKnownFolder(FOLDERID_LocalAppData); | |
1280 | + } else { | |
1281 | + static assert(false, "mlib.directories: Unsupported operating system."); | |
1282 | + } | |
263 | 1283 | } |
264 | 1284 | |
265 | 1285 | immutable(string) state() nothrow @safe |
266 | 1286 | { |
267 | - import std.path : buildPath, isAbsolute; | |
268 | - import std.process : environment; | |
1287 | + version (Windows) | |
1288 | + { | |
1289 | + return ""; | |
1290 | + } | |
1291 | + else | |
1292 | + { | |
1293 | + import std.path : buildPath, isAbsolute; | |
1294 | + import std.process : environment; | |
269 | 1295 | |
270 | - string stateEnvValue; | |
1296 | + string stateEnvValue; | |
271 | 1297 | |
272 | - try { | |
273 | - stateEnvValue = environment.get("XDG_STATE_HOME"); | |
274 | - } catch (Exception e) { | |
275 | - stateEnvValue = null; | |
276 | - } | |
1298 | + try | |
1299 | + { | |
1300 | + stateEnvValue = environment.get("XDG_STATE_HOME"); | |
1301 | + } | |
1302 | + catch (Exception e) | |
1303 | + { | |
1304 | + stateEnvValue = null; | |
1305 | + } | |
277 | 1306 | |
278 | - if (stateEnvValue is null || false == stateEnvValue.isAbsolute) | |
279 | - stateEnvValue = buildPath(home(), ".local", "state"); | |
1307 | + if (stateEnvValue is null || false == stateEnvValue.isAbsolute) | |
1308 | + stateEnvValue = buildPath(home(), ".local", "state"); | |
280 | 1309 | |
281 | - return stateEnvValue; | |
1310 | + return stateEnvValue; | |
1311 | + } | |
282 | 1312 | } |
283 | 1313 | |
284 | 1314 | /// |
@@ -292,70 +1322,369 @@ immutable(string) state() nothrow @safe | ||
292 | 1322 | /// |
293 | 1323 | immutable(string) runtime() nothrow @safe |
294 | 1324 | { |
295 | - import std.process : environment; | |
296 | - | |
297 | - try { | |
298 | - return environment.get("XDG_RUNTIME_DIR"); | |
299 | - } catch (Exception e) { | |
300 | - return null; | |
301 | - } | |
1325 | + version (Posix) { | |
1326 | + return xdgRuntime(""); | |
1327 | + } else version (Windows) { | |
1328 | + return ""; | |
1329 | + } else { | |
1330 | + static assert(false, "mlib.directories: Unsupported operating system."); | |
1331 | + } | |
302 | 1332 | } |
303 | 1333 | |
304 | -/* Helpers */ | |
305 | - | |
306 | -const(char)* fallbackHome() nothrow @trusted | |
1334 | +version (Posix) | |
307 | 1335 | { |
308 | - import core.stdc.string : strdup; | |
1336 | + import std.path : buildPath, isAbsolute; | |
1337 | + import std.process : environment; | |
1338 | + import std.string : empty; | |
309 | 1339 | |
310 | - passwd* pw; | |
311 | - char* home; | |
1340 | + immutable(string) posixHome() nothrow | |
1341 | + { | |
1342 | + string homeE; | |
312 | 1343 | |
313 | - setpwent(); | |
314 | - pw = getpwuid(getuid()); | |
315 | - endpwent(); | |
1344 | + try | |
1345 | + { | |
1346 | + homeE = environment.get("HOME"); | |
1347 | + } | |
1348 | + catch (Exception e) | |
1349 | + { | |
1350 | + homeE = null; | |
1351 | + } | |
316 | 1352 | |
317 | - if (pw is null || pw.pw_dir is null) | |
318 | - return null; | |
1353 | + if (homeE is null) | |
1354 | + { | |
1355 | + import std.string : fromStringz; | |
319 | 1356 | |
320 | - home = strdup(pw.pw_dir); | |
321 | - return home; | |
322 | -} | |
1357 | + const(char)* pwdHome = _posix_fallback_home(); | |
1358 | + if (pwdHome !is null) | |
1359 | + homeE = cast(string)(pwdHome.fromStringz).dup; | |
1360 | + if (false == homeE.isAbsolute) | |
1361 | + homeE = null; | |
1362 | + } | |
1363 | + return homeE; | |
1364 | + } | |
323 | 1365 | |
324 | -@system @nogc extern(C) nothrow | |
325 | -{ | |
326 | - /* <bits/types.h> */ | |
327 | - alias gid_t = uint; | |
328 | - alias uid_t = uint; | |
329 | - | |
330 | - /* <pwd.h> */ | |
331 | - struct passwd | |
1366 | + /// | |
1367 | + /// Retrieve the XDG User Directory for the specified $(I dirName). | |
1368 | + /// | |
1369 | + /// This will attempt to read the XDG_$(I dirName)_DIR environment | |
1370 | + /// variable. If it is not found, then we attempt to call the | |
1371 | + /// xdg-user-dir program. Should this not be installed, then | |
1372 | + /// the result of $(I fallback) (if provided) is returned or the | |
1373 | + /// empty string. | |
1374 | + /// | |
1375 | + /// Please note: If xdg-user-dir is installed, then $(I fallback) | |
1376 | + /// will never be called. xdg-user-dir always succeeds and will | |
1377 | + /// return the user's home directory if $(I dirName) isn't a valid | |
1378 | + /// value. | |
1379 | + /// | |
1380 | + /// Params: | |
1381 | + /// dirName = The XDG Directory name. | |
1382 | + /// fallback = The fallback to use if the environment variable | |
1383 | + /// is not set and xdg-user-dir is not installed. | |
1384 | + /// | |
1385 | + /// | |
1386 | + nothrow string xdgDir(string dirName, lazy string fallback = null) { | |
1387 | + import std.process : execute; | |
1388 | + import std.string : strip; | |
1389 | + | |
1390 | + string varValue; | |
1391 | + | |
1392 | + try { | |
1393 | + varValue = environment.get("XDG_" ~ dirName ~ "_DIR"); | |
1394 | + } catch (Exception) { | |
1395 | + varValue = ""; | |
1396 | + } | |
1397 | + | |
1398 | + try { | |
1399 | + if (empty(varValue)) { | |
1400 | + auto xdgRes = execute(["xdg-user-dir", dirName]); | |
1401 | + if (xdgRes.status == 0) { | |
1402 | + return strip(xdgRes.output); | |
1403 | + } | |
1404 | + } | |
1405 | + } catch (Exception) { | |
1406 | + varValue = ""; | |
1407 | + } | |
1408 | + | |
1409 | + try { | |
1410 | + if (null !is fallback) { | |
1411 | + varValue = fallback(); | |
1412 | + } | |
1413 | + } catch (Exception) { | |
1414 | + varValue = ""; | |
1415 | + } | |
1416 | + | |
1417 | + return varValue; | |
1418 | + } | |
1419 | + | |
1420 | + string xdgCache() nothrow @safe | |
332 | 1421 | { |
333 | - /// Username | |
334 | - char* pw_name; | |
335 | - /// Hashed passphrase, if shadow database is not in use | |
336 | - char* pw_password; | |
337 | - /// User ID | |
338 | - uid_t pw_uid; | |
339 | - /// Group ID | |
340 | - gid_t pw_gid; | |
341 | - /// "Real" name | |
342 | - char* pw_gecos; | |
343 | - /// Home directory | |
344 | - char* pw_dir; | |
345 | - /// Shell program | |
346 | - char* pw_shell; | |
347 | - } | |
348 | - | |
349 | - /// Rewind the user database stream | |
350 | - extern void setpwent(); | |
351 | - | |
352 | - /// Close the user database stream | |
353 | - extern void endpwent(); | |
354 | - | |
355 | - /// Retrieve the user database entry for the given user ID | |
356 | - extern passwd* getpwuid(uid_t uid); | |
357 | - | |
358 | - /* <unistd.h> */ | |
359 | - /// Returns the real user ID of the calling process. | |
360 | - extern uid_t getuid(); | |
361 | -} | |
1422 | + string cacheDir; | |
1423 | + | |
1424 | + try { | |
1425 | + cacheDir = environment.get("XDG_CACHE_HOME"); | |
1426 | + } catch (Exception) { | |
1427 | + cacheDir = ""; | |
1428 | + } | |
1429 | + | |
1430 | + if (empty(cacheDir) || !isAbsolute(cacheDir)) { | |
1431 | + cacheDir = buildPath(home(), ".cache"); | |
1432 | + } | |
1433 | + | |
1434 | + return cacheDir; | |
1435 | + } | |
1436 | + | |
1437 | + string xdgConfig() nothrow @safe | |
1438 | + { | |
1439 | + string configDir; | |
1440 | + | |
1441 | + try { | |
1442 | + configDir = environment.get("XDG_CONFIG_HOME"); | |
1443 | + } catch (Exception) { | |
1444 | + configDir = ""; | |
1445 | + } | |
1446 | + | |
1447 | + if (empty(configDir) || !isAbsolute(configDir)) | |
1448 | + configDir = buildPath(home(), ".config"); | |
1449 | + | |
1450 | + return configDir; | |
1451 | + } | |
1452 | + | |
1453 | + string xdgData() nothrow @safe | |
1454 | + { | |
1455 | + import std.path : buildPath, isAbsolute; | |
1456 | + import std.process : environment; | |
1457 | + | |
1458 | + string dataE; | |
1459 | + | |
1460 | + try | |
1461 | + { | |
1462 | + dataE = environment.get("XDG_DATA_HOME"); | |
1463 | + } | |
1464 | + catch (Exception e) | |
1465 | + { | |
1466 | + dataE = null; | |
1467 | + } | |
1468 | + | |
1469 | + if (dataE is null || false == dataE.isAbsolute) | |
1470 | + dataE = buildPath(home(), ".local", "share"); | |
1471 | + | |
1472 | + return dataE; | |
1473 | + } | |
1474 | + | |
1475 | + string xdgRuntime(string subPath = null) nothrow @safe | |
1476 | + { | |
1477 | + string runtimeDir; | |
1478 | + | |
1479 | + try { | |
1480 | + runtimeDir = environment.get("XDG_RUNTIME_DIR"); | |
1481 | + } catch (Exception e) { | |
1482 | + return ""; | |
1483 | + } | |
1484 | + | |
1485 | + if (subPath !is null) { | |
1486 | + runtimeDir = buildPath(runtimeDir, subPath); | |
1487 | + } | |
1488 | + | |
1489 | + return runtimeDir; | |
1490 | + } | |
1491 | + | |
1492 | + /* Unit tests */ | |
1493 | + unittest | |
1494 | + { | |
1495 | + import std.process : environment; | |
1496 | + import std.path : buildPath; | |
1497 | + | |
1498 | + /* | |
1499 | + * Environment Variables (for checking against) | |
1500 | + */ | |
1501 | + immutable string homeE = environment.get("HOME"); | |
1502 | + immutable string configE = environment.get("XDG_CONFIG_HOME", | |
1503 | + buildPath(homeE, ".config")); | |
1504 | + immutable string dataE = environment.get("XDG_DATA_HOME", | |
1505 | + buildPath(homeE, ".local", "share")); | |
1506 | + immutable string cacheE = environment.get("XDG_CACHE_HOME", | |
1507 | + buildPath(homeE, ".cache")); | |
1508 | + | |
1509 | + // Compare against directories.d | |
1510 | + assert(homeE == open(Directory.home)); | |
1511 | + assert(configE == open(Directory.config)); | |
1512 | + assert(dataE == open(Directory.data)); | |
1513 | + assert(cacheE == open(Directory.cache)); | |
1514 | + } | |
1515 | + | |
1516 | + /* Helpers */ | |
1517 | + const(char)* _posix_fallback_home() nothrow @trusted | |
1518 | + { | |
1519 | + import core.stdc.string : strdup; | |
1520 | + | |
1521 | + passwd* pw; | |
1522 | + char* home; | |
1523 | + | |
1524 | + setpwent(); | |
1525 | + pw = getpwuid(getuid()); | |
1526 | + endpwent(); | |
1527 | + | |
1528 | + if (pw is null || pw.pw_dir is null) | |
1529 | + return null; | |
1530 | + | |
1531 | + home = strdup(pw.pw_dir); | |
1532 | + return home; | |
1533 | + } | |
1534 | + | |
1535 | + @system @nogc extern (C) nothrow | |
1536 | + { | |
1537 | + /* <bits/types.h> */ | |
1538 | + alias gid_t = uint; | |
1539 | + alias uid_t = uint; | |
1540 | + | |
1541 | + /* <pwd.h> */ | |
1542 | + struct passwd | |
1543 | + { | |
1544 | + /// Username | |
1545 | + char* pw_name; | |
1546 | + /// Hashed passphrase, if shadow database is not in use | |
1547 | + char* pw_password; | |
1548 | + /// User ID | |
1549 | + uid_t pw_uid; | |
1550 | + /// Group ID | |
1551 | + gid_t pw_gid; | |
1552 | + /// "Real" name | |
1553 | + char* pw_gecos; | |
1554 | + /// Home directory | |
1555 | + char* pw_dir; | |
1556 | + /// Shell program | |
1557 | + char* pw_shell; | |
1558 | + } | |
1559 | + | |
1560 | + /// Rewind the user database stream | |
1561 | + extern void setpwent(); | |
1562 | + | |
1563 | + /// Close the user database stream | |
1564 | + extern void endpwent(); | |
1565 | + | |
1566 | + /// Retrieve the user database entry for the given user ID | |
1567 | + extern passwd* getpwuid(uid_t uid); | |
1568 | + | |
1569 | + /* <unistd.h> */ | |
1570 | + /// Returns the real user ID of the calling process. | |
1571 | + extern uid_t getuid(); | |
1572 | + } | |
1573 | +} // end of version (Posix) | |
1574 | + | |
1575 | +version (Windows) { | |
1576 | + import std.path : buildPath; | |
1577 | + import std.process : environment; | |
1578 | + import std.string : empty; | |
1579 | + | |
1580 | + pragma(lib, "ole32"); | |
1581 | + | |
1582 | + string windowsHome() nothrow { | |
1583 | + string home; | |
1584 | + | |
1585 | + try { | |
1586 | + home = environment["USERPROFILE"]; | |
1587 | + } catch (Exception) { | |
1588 | + home = ""; | |
1589 | + } | |
1590 | + | |
1591 | + if (false == empty(home)) { | |
1592 | + return home; | |
1593 | + } | |
1594 | + | |
1595 | + try { | |
1596 | + scope homeDrive = environment["HOMEDRIVE"]; | |
1597 | + scope homePath = environment["HOMEPATH"]; | |
1598 | + home = homeDrive ~ homePath; | |
1599 | + } catch (Exception) { | |
1600 | + home = ""; | |
1601 | + } | |
1602 | + | |
1603 | + if (false == empty(home)) { | |
1604 | + return home; | |
1605 | + } | |
1606 | + | |
1607 | + return GetKnownFolder(FOLDERID_Profile); | |
1608 | + } | |
1609 | + | |
1610 | + string windowsRoamingData() nothrow @trusted | |
1611 | + { | |
1612 | + return GetKnownFolder(FOLDERID_RoamingAppData); | |
1613 | + } | |
1614 | + | |
1615 | + string windowsLocalData() nothrow @trusted | |
1616 | + { | |
1617 | + return GetKnownFolder(FOLDERID_LocalAppData); | |
1618 | + } | |
1619 | + | |
1620 | + /* Helpers */ | |
1621 | + import core.sys.windows.basetyps; | |
1622 | + import core.sys.windows.objbase; | |
1623 | + import core.sys.windows.windef; | |
1624 | + import core.sys.windows.winnls; | |
1625 | + | |
1626 | + alias KNOWNFOLDERID = GUID; | |
1627 | + alias REFKNOWNFOLDERID = KNOWNFOLDERID*; | |
1628 | + | |
1629 | + extern(C) nothrow @nogc @system | |
1630 | + HRESULT SHGetKnownFolderPath(REFKNOWNFOLDERID rfid, DWORD dwFlags, HANDLE hToken, PWSTR *ppszPath); | |
1631 | + | |
1632 | + // {B4BFCC3A-DB2C-424C-B029-7FE99A87C641} | |
1633 | + GUID FOLDERID_Desktop = GUID(0xB4BFCC3A, 0xDB2C, 0x424C, [0xB0, 0x29, 0x7F, 0xE9, 0x9A, 0x87, 0xC6, 0x41]); | |
1634 | + | |
1635 | + // {FDD39AD0-238F-46AF-ADB4-6C85480369C7} | |
1636 | + GUID FOLDERID_Documents = GUID(0xFDD39AD0, 0x238F, 0x46AF, [0xAD, 0xB4, 0x6C, 0x85, 0x48, 0x03, 0x69, 0xC7]); | |
1637 | + | |
1638 | + // {A63293E8-664E-48DB-A079-DF759E0509F7} | |
1639 | + GUID FOLDERID_Templates = GUID(0xA63293E8, 0x664E, 0x48DB, [0xA0, 0x79, 0xDF, 0x75, 0x9E, 0x05, 0x09, 0xF7]); | |
1640 | + | |
1641 | + // {3EB685DB-65F9-4CF6-A03A-E3EF65729F3D} | |
1642 | + GUID FOLDERID_RoamingAppData = GUID(0x3EB685DB, 0x65F9, 0x4CF6, [0xA0, 0x3A, 0xE3, 0xEF, 0x65, 0x72, 0x9F, 0x3D]); | |
1643 | + | |
1644 | + // {F1B32785-6FBA-4FCF-9D55-7B8E7F157091} | |
1645 | + GUID FOLDERID_LocalAppData = GUID(0xF1B32785, 0x6FBA, 0x4FCF, [0x9D, 0x55, 0x7B, 0x8E, 0x7F, 0x15, 0x70, 0x91]); | |
1646 | + | |
1647 | + // {5E6C858F-0E22-4760-9AFE-EA3317B67173} | |
1648 | + GUID FOLDERID_Profile = GUID(0x5E6C858F, 0x0E22, 0x4760, [0x9A, 0xFE, 0xEA, 0x33, 0x17, 0xB6, 0x71, 0x73]); | |
1649 | + | |
1650 | + // {33E28130-4E1E-4676-835A-98395C3BC3BB} | |
1651 | + GUID FOLDERID_Pictures = GUID(0x33E28130, 0x4E1E, 0x4676, [0x83, 0x5A, 0x98, 0x39, 0x5C, 0x3B, 0xC3, 0xBB]); | |
1652 | + | |
1653 | + // {4BD8D571-6D19-48D3-BE97-422220080E43} | |
1654 | + GUID FOLDERID_Music = GUID(0x4BD8D571, 0x6D19, 0x48D3, [0xBE, 0x97, 0x42, 0x22, 0x20, 0x08, 0x0E, 0x43]); | |
1655 | + | |
1656 | + // {18989B1D-99B5-455B-841C-AB7C74E4DDFC} | |
1657 | + GUID FOLDERID_Videos = GUID(0x18989B1D, 0x99B5, 0x455B, [0x84, 0x1C, 0xAB, 0x7C, 0x74, 0xE4, 0xDD, 0xFC]); | |
1658 | + | |
1659 | + // {DFDF76A2-C82A-4D63-906A-5644AC457385} | |
1660 | + GUID FOLDERID_Public = GUID(0xDFDF76A2, 0xC82A, 0x4D63, [0x90, 0x6A, 0x56, 0x44, 0xAC, 0x45, 0x73, 0x85]); | |
1661 | + | |
1662 | + // {374DE290-123F-4565-9164-39C4925E467B} | |
1663 | + GUID FOLDERID_Downloads = GUID(0x374de290, 0x123f, 0x4565, [0x91, 0x64, 0x39, 0xc4, 0x92, 0x5e, 0x46, 0x7b]); | |
1664 | + | |
1665 | + string GetKnownFolder(KNOWNFOLDERID rdif) nothrow @trusted | |
1666 | + { | |
1667 | + PWSTR path; | |
1668 | + HRESULT status; | |
1669 | + ULONG bufferSize = 0; | |
1670 | + | |
1671 | + status = SHGetKnownFolderPath(&rdif, 0, null, &path); | |
1672 | + if (status != S_OK) { | |
1673 | + CoTaskMemFree(path); | |
1674 | + return ""; | |
1675 | + } | |
1676 | + scope(exit) CoTaskMemFree(path); | |
1677 | + | |
1678 | + UnicodeToAnsiSize(path, bufferSize); | |
1679 | + // -1 to remove the null character which D doesn't use | |
1680 | + char[] str = new char[bufferSize - 1]; | |
1681 | + WideCharToMultiByte(CP_UTF8, 0, path, -1, str.ptr, bufferSize - 1, null, null); | |
1682 | + | |
1683 | + return cast(string)str; | |
1684 | + } | |
1685 | + | |
1686 | + void UnicodeToAnsiSize(in PWCHAR UnicodeString, out ULONG AnsiSizeInBytes) nothrow | |
1687 | + { | |
1688 | + AnsiSizeInBytes = WideCharToMultiByte(CP_UTF8, 0, UnicodeString, -1, null, 0, null, null); | |
1689 | + } | |
1690 | +} // end of version (Windows) |
@@ -0,0 +1,7 @@ | ||
1 | +/// | |
2 | +/// A collection of public domain modules for the | |
3 | +/// $(LINK2 https://dlang.org, D Programming Language). | |
4 | +/// | |
5 | +/// All modules are compatible with D versions 2.076.0 and newer. | |
6 | +/// | |
7 | +module mlib; |
@@ -14,16 +14,18 @@ | ||
14 | 14 | /** |
15 | 15 | * Common 'Trash' operations for the OS's Recycle Bin. |
16 | 16 | * |
17 | - * Currently only for POSIX system. It follows the XDG specification. | |
17 | + * Supports POSIX (XDG Specification) and Windows. Proper support for | |
18 | + * macOS will be implemented in a future version. | |
18 | 19 | * |
19 | 20 | * Authors: nemophila |
20 | 21 | * Date: January 29, 2023 |
21 | 22 | * Homepage: https://osdn.net/users/nemophila/pf/mlib |
22 | 23 | * License: 0BSD |
23 | 24 | * Standards: The FreeDesktop.org Trash Specification 1.0 |
24 | - * Version: 0.1.0 | |
25 | + * Version: 0.2.0 | |
25 | 26 | * |
26 | 27 | * History: |
28 | + * 0.2.0 added support for Windows | |
27 | 29 | * 0.1.0 is the initial version |
28 | 30 | * |
29 | 31 | * Macros: |
@@ -94,6 +96,20 @@ void trash(string path) | ||
94 | 96 | trash(path, pathInTrash); |
95 | 97 | } |
96 | 98 | |
99 | +/// | |
100 | +unittest | |
101 | +{ | |
102 | + import std.stdio : File; | |
103 | + import std.exception : assertNotThrown; | |
104 | + | |
105 | + // Create a file with some basic text | |
106 | + auto file = File("hello.txt", "w+"); | |
107 | + file.writeln("hello, world!"); | |
108 | + file.close(); | |
109 | + | |
110 | + assertNotThrown!Exception(trash("hello.txt")); | |
111 | +} | |
112 | + | |
97 | 113 | /** |
98 | 114 | * Trash the file or directory at *path*, and sets *pathInTrash* to the |
99 | 115 | * path at which the file can be found within the trash. |
@@ -102,6 +118,8 @@ void trash(string path) | ||
102 | 118 | * path = The path to move to the trash. |
103 | 119 | * pathInTrash = The path at which the newly trashed item can be found. |
104 | 120 | * |
121 | + * Bugs: The *pathInTrash* parameter isn't supported on Windows. | |
122 | + * | |
105 | 123 | * Throws: |
106 | 124 | * - $(DREF std_file, FileException) if the file cannot be trashed. |
107 | 125 | */ |
@@ -109,6 +127,8 @@ void trash(string path, out string pathInTrash) | ||
109 | 127 | { |
110 | 128 | version (Posix) { |
111 | 129 | _posix_trash(path, pathInTrash); |
130 | + } else version (Windows) { | |
131 | + _windows_trash(path); | |
112 | 132 | } else { |
113 | 133 | throw new Exception(__PRETTY_FUNCTION__ ~ " is not supported on your OS"); |
114 | 134 | } |
@@ -141,12 +161,18 @@ private: | ||
141 | 161 | ulong getDevice(string path) { |
142 | 162 | version (Posix) { |
143 | 163 | return _posix_getDevice(path); |
164 | + } else { | |
165 | + // Not used on Windows | |
166 | + return 0; | |
144 | 167 | } |
145 | 168 | } |
146 | 169 | |
147 | 170 | string getHomeDirectory() { |
148 | 171 | version (Posix) { |
149 | 172 | return environment["HOME"]; |
173 | + } else { | |
174 | + // Not used on Windows | |
175 | + return ""; | |
150 | 176 | } |
151 | 177 | } |
152 | 178 |
@@ -304,3 +330,46 @@ version(Posix) { | ||
304 | 330 | } |
305 | 331 | } |
306 | 332 | } // End of version(Posix) |
333 | + | |
334 | +/* | |
335 | + * Disclaimer: | |
336 | + * | |
337 | + * I don't use Windows. As such, this may not be the _best_ way | |
338 | + * to send a file to the recycle bin. In theory it shouldn't | |
339 | + * break (given Windows' tendency for backwards support), but | |
340 | + * if there is an error, you'll either have to let me know | |
341 | + * or send a patch yourself. | |
342 | + */ | |
343 | +version(Windows) { | |
344 | + import core.sys.windows.windows; | |
345 | + | |
346 | + import std.utf : toUTF16z; | |
347 | + | |
348 | + // There doesn't seem to be a way to determine the path of a | |
349 | + // file in the Recycle Bin. | |
350 | + void _windows_trash(string path) { | |
351 | + // If the path is not absolute, then it won't be recycled. | |
352 | + string absPath = absolutePath(path); | |
353 | + | |
354 | + SHFILEOPSTRUCT fileOp = SHFILEOPSTRUCTW(null, FO_DELETE); | |
355 | + | |
356 | + /* | |
357 | + * NOTE: | |
358 | + * While toUTF16z appends a null character to the input string, | |
359 | + * SHFILEOPSTRUCT treats pFrom (and pTo) as a list of strings | |
360 | + * separated by a single '\0'. To specify the end of the list, | |
361 | + * the string must end with double null terminator. | |
362 | + * | |
363 | + * See: https://learn.microsoft.com/en-us/windows/win32/api/shellapi/ns-shellapi-shfileopstructa#remarks | |
364 | + */ | |
365 | + fileOp.pFrom = toUTF16z(absPath ~ '\0'); | |
366 | + fileOp.pTo = null; | |
367 | + fileOp.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_SILENT; | |
368 | + fileOp.fAnyOperationsAborted = FALSE; | |
369 | + fileOp.lpszProgressTitle = null; | |
370 | + | |
371 | + if (0 != SHFileOperation(&fileOp)) { | |
372 | + throw new FileException(path, "File could not be deleted"); | |
373 | + } | |
374 | + } | |
375 | +} // End of version(Windows) |
@@ -6,34 +6,25 @@ | ||
6 | 6 | FALSE=0 |
7 | 7 | TRUE=1 |
8 | 8 | |
9 | -_HAVE_GIT=$FALSE | |
10 | 9 | _HAVE_WGET=$FALSE |
11 | 10 | _HAVE_CURL=$FALSE |
12 | 11 | |
13 | -which git2 >/dev/null && _HAVE_GIT=$TRUE | |
14 | 12 | which wget >/dev/null && _HAVE_WGET=$TRUE |
15 | 13 | which curl >/dev/null && _HAVE_CURL=$TRUE |
16 | 14 | |
17 | -_DUB_RAW_URL="https://osdn.net/users/nemophila/pf/mlib/scm/blobs/trunk/dub.sdl?export=raw" | |
18 | -_CNI_RAW_URL="https://osdn.net/users/nemophila/pf/mlib/scm/blobs/trunk/source/mlib/cni.d?export=raw" | |
19 | -_DIR_RAW_URL="https://osdn.net/users/nemophila/pf/mlib/scm/blobs/trunk/source/mlib/directories.d?export=raw" | |
20 | -_TSH_RAW_URL="https://osdn.net/users/nemophila/pf/mlib/scm/blobs/trunk/source/mlib/trash.d?export=raw" | |
15 | +# Using separate values since there could be bugs with | |
16 | +# a specific version. | |
17 | +_DUB_COMMIT="8f1f7e3c05abc2734020727892988051ce25f29e" | |
18 | +_CNI_COMMIT="8f1f7e3c05abc2734020727892988051ce25f29e" | |
19 | +_DIR_COMMIT="8f1f7e3c05abc2734020727892988051ce25f29e" | |
20 | +_TSH_COMMIT="8f1f7e3c05abc2734020727892988051ce25f29e" | |
21 | 21 | |
22 | +_DUB_RAW_URL="https://osdn.net/users/nemophila/pf/mlib/scm/blobs/$_DUB_COMMIT/dub.sdl?export=raw" | |
23 | +_CNI_RAW_URL="https://osdn.net/users/nemophila/pf/mlib/scm/blobs/$_CNI_COMMIT/source/mlib/cni.d?export=raw" | |
24 | +_DIR_RAW_URL="https://osdn.net/users/nemophila/pf/mlib/scm/blobs/$_DIR_COMMIT/source/mlib/directories.d?export=raw" | |
25 | +_TSH_RAW_URL="https://osdn.net/users/nemophila/pf/mlib/scm/blobs/$_TSH_COMMIT/source/mlib/trash.d?export=raw" | |
22 | 26 | |
23 | -if [ $TRUE -eq $_HAVE_GIT ] | |
24 | -then | |
25 | - # Update using GIT | |
26 | - if [ ! -d "mlib" ] | |
27 | - then | |
28 | - git clone git://git.pf.osdn.net/gitroot/n/ne/nemophila/mlib.git | |
29 | - cd mlib | |
30 | - else | |
31 | - cd mlib | |
32 | - git fetch && git pull --rebase | |
33 | - fi | |
34 | - _latest_tag="$(git tag --list v* | sort -r | head -n1)" | |
35 | - git checkout "$_latest_tag" | |
36 | -elif [ $TRUE -eq $_HAVE_WGET ] | |
27 | +if [ $TRUE -eq $_HAVE_WGET ] | |
37 | 28 | then |
38 | 29 | # Update by fetching archive with wget |
39 | 30 | [ ! -d mlib/source/mlib ] && mkdir -p mlib/source/mlib |
@@ -46,6 +37,11 @@ then | ||
46 | 37 | elif [ $TRUE -eq $_HAVE_CURL ] |
47 | 38 | then |
48 | 39 | # Update by fetching archive with curl |
49 | - echo 's' | |
40 | + [ ! -d mlib/source/mlib ] && mkdir -p mlib/source/mlib | |
41 | + cd mlib | |
42 | + curl -o "dub.sdl" "$_DUB_RAW_URL" && sleep 2 | |
43 | + cd source/mlib | |
44 | + curl -o "cni.d" "$_CNI_RAW_URL" && sleep 2 | |
45 | + curl -o "directories.d" "$_DIR_RAW_URL" && sleep 2 | |
46 | + curl -o "trash.d" "$_TSH_RAW_URL" | |
50 | 47 | fi |
51 | - |