• R/O
  • HTTP
  • SSH
  • HTTPS

Commit

Tags
No Tags

Frequently used words (click to add to your profile)

javac++androidlinuxc#windowsobjective-ccocoa誰得qtpythonphprubygameguibathyscaphec計画中(planning stage)翻訳omegatframeworktwitterdomtestvb.netdirectxゲームエンジンbtronarduinopreviewer

CLI interface to medialist (fossil mirror)


Commit MetaInfo

Revisión47a6efbebc0ef1cd9aa5f6d6fe509b52ba295bae (tree)
Tiempo2023-03-28 16:12:03
Autormio <stigma@disr...>
Commitermio

Log Message

Update mlib

FossilOrigin-Name: 78c7e26f7d9b54bd742ff35fe98afb936c56386e8a5e653c3d804ce24485dfc7

Cambiar Resumen

Diferencia incremental

--- /dev/null
+++ b/mlib/LICENSE
@@ -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.
--- /dev/null
+++ b/mlib/README
@@ -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.
--- a/mlib/dub.sdl
+++ b/mlib/dub.sdl
@@ -3,4 +3,4 @@ description "A package containing my single-file modules"
33 authors "mio"
44 license "0BSD"
55
6-targetType "library"
6+targetType "library"
\ No newline at end of file
--- a/mlib/source/mlib/cni.d
+++ b/mlib/source/mlib/cni.d
@@ -208,7 +208,7 @@ private:
208208 moveForward(1);
209209 }
210210
211- value = app[];
211+ value = app.data();
212212 moveForward(1); // move past ending `
213213 inRawValue = false;
214214 } else {
--- /dev/null
+++ b/mlib/source/mlib/configparser.d
@@ -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+}
--- a/mlib/source/mlib/directories.d
+++ b/mlib/source/mlib/directories.d
@@ -14,65 +14,1139 @@
1414 /**
1515 * This module provides quick & easy access to the common directories
1616 * 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.
1919 *
2020 * The main goal of this module is to provide a minimal and simple API.
2121 *
22- * API
22+ * ## Example
23+ *
2324 * ---
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+ * }
3342 *
34- * DirEntry open(Directory);
3543 * ---
3644 *
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.
3847 *
3948 * Authors: nemophila
40- * Date: February 18, 2023
49+ * Date: March 6, 2023
4150 * 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
4558 *
4659 * History:
4760 * 0.X.X was the initial version (June 12, 2021)
4861 *
4962 * 0.1.0 adds support for runtime and state (February 18, 2023)
5063 *
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)
5465 */
5566 module mlib.directories;
5667
5768 import std.file : DirEntry;
5869
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)")
5976 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+}
701140
711141 /++
721142 Return a DirEntry pointing to `dt`.
73-
1143+
741144 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+
761150 Examples:
771151 ----
781152 import directories;
@@ -92,72 +1166,55 @@ public enum Directory
921166 }
931167 ----
941168 +/
1169+deprecated("Use new get{Base,Project,User}Directories functions (remove 0.3.0)")
951170 public DirEntry open(Directory dt) nothrow// @safe
961171 {
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;
1081179
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.");
1121182 }
1131183
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+ }
1211191 }
1221192
1231193 ///
1241194 unittest
1251195 {
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;
1491198
150-private:
1199+ DirEntry emptyDir;
1511200
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+ }
1591214 }
1601215
1216+private:
1217+
1611218 immutable(string) getPath(in Directory dt) nothrow @safe
1621219 {
1631220 switch (dt)
@@ -181,104 +1238,77 @@ immutable(string) getPath(in Directory dt) nothrow @safe
1811238
1821239 immutable(string) home() nothrow @trusted
1831240 {
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+ }
2041248 }
2051249
2061250 immutable(string) data() nothrow @safe
2071251 {
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+ }
2231259 }
2241260
2251261 immutable(string) config() nothrow @safe
2261262 {
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+ }
2421270 }
2431271
2441272 /* TODO: xdgState() nothrow @safe */
2451273
2461274 immutable(string) cache() nothrow @safe
2471275 {
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+ }
2631283 }
2641284
2651285 immutable(string) state() nothrow @safe
2661286 {
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;
2691295
270- string stateEnvValue;
1296+ string stateEnvValue;
2711297
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+ }
2771306
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");
2801309
281- return stateEnvValue;
1310+ return stateEnvValue;
1311+ }
2821312 }
2831313
2841314 ///
@@ -292,70 +1322,369 @@ immutable(string) state() nothrow @safe
2921322 ///
2931323 immutable(string) runtime() nothrow @safe
2941324 {
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+ }
3021332 }
3031333
304-/* Helpers */
305-
306-const(char)* fallbackHome() nothrow @trusted
1334+version (Posix)
3071335 {
308- import core.stdc.string : strdup;
1336+ import std.path : buildPath, isAbsolute;
1337+ import std.process : environment;
1338+ import std.string : empty;
3091339
310- passwd* pw;
311- char* home;
1340+ immutable(string) posixHome() nothrow
1341+ {
1342+ string homeE;
3121343
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+ }
3161352
317- if (pw is null || pw.pw_dir is null)
318- return null;
1353+ if (homeE is null)
1354+ {
1355+ import std.string : fromStringz;
3191356
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+ }
3231365
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
3321421 {
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)
--- /dev/null
+++ b/mlib/source/mlib/package.d
@@ -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;
--- a/mlib/source/mlib/trash.d
+++ b/mlib/source/mlib/trash.d
@@ -14,16 +14,18 @@
1414 /**
1515 * Common 'Trash' operations for the OS's Recycle Bin.
1616 *
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.
1819 *
1920 * Authors: nemophila
2021 * Date: January 29, 2023
2122 * Homepage: https://osdn.net/users/nemophila/pf/mlib
2223 * License: 0BSD
2324 * Standards: The FreeDesktop.org Trash Specification 1.0
24- * Version: 0.1.0
25+ * Version: 0.2.0
2526 *
2627 * History:
28+ * 0.2.0 added support for Windows
2729 * 0.1.0 is the initial version
2830 *
2931 * Macros:
@@ -94,6 +96,20 @@ void trash(string path)
9496 trash(path, pathInTrash);
9597 }
9698
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+
97113 /**
98114 * Trash the file or directory at *path*, and sets *pathInTrash* to the
99115 * path at which the file can be found within the trash.
@@ -102,6 +118,8 @@ void trash(string path)
102118 * path = The path to move to the trash.
103119 * pathInTrash = The path at which the newly trashed item can be found.
104120 *
121+ * Bugs: The *pathInTrash* parameter isn't supported on Windows.
122+ *
105123 * Throws:
106124 * - $(DREF std_file, FileException) if the file cannot be trashed.
107125 */
@@ -109,6 +127,8 @@ void trash(string path, out string pathInTrash)
109127 {
110128 version (Posix) {
111129 _posix_trash(path, pathInTrash);
130+ } else version (Windows) {
131+ _windows_trash(path);
112132 } else {
113133 throw new Exception(__PRETTY_FUNCTION__ ~ " is not supported on your OS");
114134 }
@@ -141,12 +161,18 @@ private:
141161 ulong getDevice(string path) {
142162 version (Posix) {
143163 return _posix_getDevice(path);
164+ } else {
165+ // Not used on Windows
166+ return 0;
144167 }
145168 }
146169
147170 string getHomeDirectory() {
148171 version (Posix) {
149172 return environment["HOME"];
173+ } else {
174+ // Not used on Windows
175+ return "";
150176 }
151177 }
152178
@@ -304,3 +330,46 @@ version(Posix) {
304330 }
305331 }
306332 } // 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)
--- a/update_deps.sh
+++ b/update_deps.sh
@@ -6,34 +6,25 @@
66 FALSE=0
77 TRUE=1
88
9-_HAVE_GIT=$FALSE
109 _HAVE_WGET=$FALSE
1110 _HAVE_CURL=$FALSE
1211
13-which git2 >/dev/null && _HAVE_GIT=$TRUE
1412 which wget >/dev/null && _HAVE_WGET=$TRUE
1513 which curl >/dev/null && _HAVE_CURL=$TRUE
1614
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"
2121
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"
2226
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 ]
3728 then
3829 # Update by fetching archive with wget
3930 [ ! -d mlib/source/mlib ] && mkdir -p mlib/source/mlib
@@ -46,6 +37,11 @@ then
4637 elif [ $TRUE -eq $_HAVE_CURL ]
4738 then
4839 # 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"
5047 fi
51-