1 /** 2 * Functions for retrieving standard paths in cross-platform manner. 3 * Authors: 4 * $(LINK2 https://github.com/FreeSlave, Roman Chistokhodov) 5 * License: 6 * $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0). 7 * Copyright: 8 * Roman Chistokhodov 2015-2016 9 */ 10 11 module standardpaths; 12 13 private { 14 import std.process : environment; 15 import std.path; 16 import std.file; 17 import std.exception; 18 import std.range; 19 20 import isfreedesktop; 21 22 debug { 23 import std.stdio : stderr; 24 } 25 26 static if( __VERSION__ < 2066 ) enum nogc = 1; 27 28 string verifyIfNeeded(string path, bool shouldVerify) nothrow @trusted 29 { 30 if (path.length && shouldVerify) { 31 bool dirExists; 32 collectException(path.isDir, dirExists); 33 return dirExists ? path : null; 34 } else { 35 return path; 36 } 37 } 38 39 string createIfNeeded(string path, bool shouldCreate) nothrow @trusted 40 { 41 if (path.length && shouldCreate) { 42 bool pathExist; 43 collectException(path.isDir, pathExist); 44 if (pathExist || collectException(mkdirRecurse(path)) is null) { 45 return path; 46 } else { 47 return null; 48 } 49 } else { 50 return path; 51 } 52 } 53 } 54 55 version(Windows) { 56 private { 57 static if (__VERSION__ < 2070) { 58 import std.c.windows.windows; 59 } else { 60 import core.sys.windows.windows; 61 } 62 63 import std.utf; 64 } 65 } else version(Posix) { 66 private { 67 import std.string : toStringz; 68 69 static if (is(typeof({import std.string : fromStringz;}))) { 70 import std.string : fromStringz; 71 } else { //own fromStringz implementation for compatibility reasons 72 import std.c.string : strlen; 73 @system pure inout(char)[] fromStringz(inout(char)* cString) { 74 return cString ? cString[0..strlen(cString)] : null; 75 } 76 } 77 78 //Concat two strings, but if the first one is empty, then null string is returned. 79 string maybeConcat(string start, string path) nothrow @safe 80 { 81 return start.empty ? null : start ~ path; 82 } 83 84 string maybeBuild(string start, string path) nothrow @safe 85 { 86 return start.empty ? null : buildPath(start, path); 87 } 88 } 89 } else { 90 static assert(false, "Unsupported platform"); 91 } 92 93 /** 94 * Location types that can be passed to $(D writablePath) and $(D standardPaths) functions. 95 * 96 * Not all these paths are suggested for showing in file managers or file dialogs. 97 * Some of them are meant for internal application usage or should be treated in special way. 98 * On usual circumstances user wants to see Desktop, Documents, Downloads, Pictures, Music and Videos directories. 99 * 100 * See_Also: 101 * $(D writablePath), $(D standardPaths) 102 */ 103 enum StandardPath { 104 /** 105 * General location of persisted application data. Every application should have its own subdirectory here. 106 * Note: on Windows it's the same as $(D config) path. 107 */ 108 data, 109 /** 110 * General location of configuration files. Every application should have its own subdirectory here. 111 * Note: on Windows it's the same as $(D data) path. 112 */ 113 config, 114 /** 115 * Location of cached data. 116 * Note: Not available on Windows. 117 */ 118 cache, 119 ///User's desktop directory. 120 desktop, 121 ///User's documents. 122 documents, 123 ///User's pictures. 124 pictures, 125 126 ///User's music. 127 music, 128 129 ///User's videos (movies). 130 videos, 131 132 ///Directory for user's downloaded files. 133 downloads, 134 135 /** 136 * Location of file templates (e.g. office suite document templates). 137 * Note: Not available on OS X. 138 */ 139 templates, 140 141 /** 142 * Public share folder. 143 * Note: Not available on Windows. 144 */ 145 publicShare, 146 /** 147 * Location of fonts files. 148 * Note: don't rely on this on freedesktop, since it uses hardcoded paths there. Better consider using $(LINK2 http://www.freedesktop.org/wiki/Software/fontconfig/, fontconfig library) 149 */ 150 fonts, 151 /** 152 * User's applications. This has different meaning across platforms. 153 * On Windows it's directory where links (.lnk) to programs for Start menu are stored. 154 * On OS X it's folder where applications are typically put. 155 * On Freedesktop it's directory where .desktop files are put. 156 */ 157 applications, 158 159 /** 160 * Automatically started applications. 161 * On Windows it's directory where links (.lnk) to autostarted programs are stored. 162 * On OSX it's not available. 163 * On Freedesktop it's directory where autostarted .desktop files are stored. 164 */ 165 startup 166 } 167 168 /** 169 * Control behavior of functions. 170 * See_Also: $(D writablePath) 171 */ 172 enum FolderFlag 173 { 174 none = 0, /// Don't verify that folder exist. 175 /** 176 * Create if folder does not exist. 177 * On Windows and OS X directory will be created using platform specific API, so it will have appropriate icon and other settings special for this kind of folder. 178 */ 179 create = 1, 180 /** 181 * Verify that folder exists. 182 * On Windows directory is verified using platform specific API. 183 */ 184 verify = 2 185 } 186 187 /** 188 * Current user home directory. 189 * Returns: Path to user home directory, or an empty string if could not determine home directory. 190 * Relies on environment variables. 191 * Note: This function does not cache its result. 192 */ 193 string homeDir() nothrow @safe 194 { 195 try { 196 version(Windows) { 197 //Use GetUserProfileDirectoryW from Userenv.dll? 198 string home = environment.get("USERPROFILE"); 199 if (home.empty) { 200 string homeDrive = environment.get("HOMEDRIVE"); 201 string homePath = environment.get("HOMEPATH"); 202 if (homeDrive.length && homePath.length) { 203 home = homeDrive ~ homePath; 204 } 205 } 206 return home; 207 } else { 208 string home = environment.get("HOME"); 209 return home; 210 } 211 } 212 catch (Exception e) { 213 debug { 214 @trusted void writeException(Exception e) nothrow { 215 collectException(stderr.writefln("Error when getting home directory %s", e.msg)); 216 } 217 writeException(e); 218 } 219 return null; 220 } 221 } 222 223 /** 224 * Get writable path for specific location. 225 * Returns: Path where files of $(U type) should be written to by current user, or an empty string if could not determine path. 226 * Params: 227 * type = Location to lookup. 228 * params = Union of $(D FolderFlag)s. 229 * Note: This function does not cache its results. 230 * Example: 231 -------------------- 232 string downloadsDir = writablePath(StandardPath.downloads, FolderFlag.verify); 233 if (downloadsDir.length) { 234 //Open file dialog with this directory. 235 } else { 236 //Could not detect default downloads directory. 237 //Ask user to choose default downloads directory for this application. 238 } 239 -------------------- 240 * See_Also: $(D StandardPath), $(D FolderFlag), $(D standardPaths) 241 */ 242 string writablePath(StandardPath type, FolderFlag params = FolderFlag.none) nothrow @safe; 243 244 /** 245 * Get paths for various locations. 246 * Returns: Array of paths where files of $(U type) belong including one returned by $(D writablePath), or an empty array if no paths are defined for $(U type). 247 * This function does not ensure if all returned paths exist and appear to be accessible directories. Returned strings are not required to be unique. 248 * Note: This function does cache its results. 249 * It may cause performance impact to call this function often since retrieving some paths can be relatively expensive operation. 250 * Example: 251 -------------------- 252 string[] templateDirs = standardPaths(StandardPath.templates); 253 //List all available file templates including system defined and user created ones. 254 -------------------- 255 * See_Also: $(D StandardPath), $(D writablePath) 256 */ 257 string[] standardPaths(StandardPath type) nothrow @safe; 258 259 /** 260 * Evaluate writable path for specific location and append subfolder. 261 * This can be used with $(D StandardPath.config) and $(D StandardPath.data) to retrieve folder specific for this application instead of generic path. 262 * Returns: Path where files of $(U type) should be written to by current user concatenated with subfolder, 263 * or an empty string if could not determine path. 264 * Params: 265 * type = Location to lookup. 266 * subfolder = Subfolder that will be appended to base writable path. 267 * params = Union of $(D FolderFlag)s. This affects both base path and sub path. 268 * Note: This function does not cache its results. 269 * Example: 270 -------------------- 271 enum organizationName = "MyLittleCompany"; 272 enum applicationName = "MyLittleApplication"; 273 274 string configDir = writablePath(StandardPath.config, buildPath(organizationName, applicationName), FolderFlag.create); 275 if (configDir.length) { 276 string configFile = buildPath(configDir, "config.conf"); 277 //read or write configuration file. 278 } else { 279 throw new Exception("Could not create application config directory"); 280 } 281 -------------------- 282 */ 283 string writablePath(StandardPath type, string subfolder, FolderFlag params = FolderFlag.none) nothrow @safe 284 { 285 string baseWritablePath = writablePath(type, params); 286 if (baseWritablePath.length) { 287 string toReturn = buildPath(baseWritablePath, subfolder); 288 const bool shouldCreate = (params & FolderFlag.create) != 0; 289 const bool shouldVerify = (params & FolderFlag.verify) != 0; 290 return toReturn.createIfNeeded(shouldCreate).verifyIfNeeded(shouldVerify); 291 } else { 292 return null; 293 } 294 } 295 296 /** 297 * Evaluate paths for various locations and append subfolder. 298 * Example: 299 -------------------- 300 enum organizationName = "MyLittleCompany"; 301 enum applicationName = "MyLittleApplication"; 302 303 string[] appDataDirs = standardPaths(StandardPath.data, buildPath(organizationName, applicationName)); 304 //Gather data files for this application from each found directory. 305 -------------------- 306 */ 307 string[] standardPaths(StandardPath type, string subfolder) nothrow @safe 308 { 309 auto toReturn = standardPaths(type); 310 foreach(ref s; toReturn) { 311 s = buildPath(s, subfolder); 312 } 313 return toReturn; 314 } 315 316 version(D_Ddoc) 317 { 318 /** 319 * Path to $(B Roaming) data directory. Windows only. 320 * Returns: User's Roaming directory. On fail returns an empty string. 321 * See_Also: $(D writablePath), $(D FolderFlag) 322 */ 323 string roamingPath(FolderFlag params = FolderFlag.none) nothrow @safe; 324 325 /** 326 * Location where games may store their saves. Windows only. 327 * Note: This is common path for games. One should use subfolder for their game saves. 328 * Returns: User's Saved Games directory. On fail returns an empty string. 329 * See_Also: $(D writablePath), $(D FolderFlag) 330 */ 331 string savedGames(FolderFlag params = FolderFlag.none) nothrow @safe; 332 } 333 334 version(Windows) { 335 336 private { 337 enum { 338 CSIDL_DESKTOP = 0, 339 CSIDL_INTERNET, 340 CSIDL_PROGRAMS, 341 CSIDL_CONTROLS, 342 CSIDL_PRINTERS, 343 CSIDL_PERSONAL, 344 CSIDL_FAVORITES, 345 CSIDL_STARTUP, 346 CSIDL_RECENT, 347 CSIDL_SENDTO, 348 CSIDL_BITBUCKET, 349 CSIDL_STARTMENU, // = 11 350 CSIDL_MYMUSIC = 13, 351 CSIDL_MYVIDEO, // = 14 352 CSIDL_DESKTOPDIRECTORY = 16, 353 CSIDL_DRIVES, 354 CSIDL_NETWORK, 355 CSIDL_NETHOOD, 356 CSIDL_FONTS, 357 CSIDL_TEMPLATES, 358 CSIDL_COMMON_STARTMENU, 359 CSIDL_COMMON_PROGRAMS, 360 CSIDL_COMMON_STARTUP, 361 CSIDL_COMMON_DESKTOPDIRECTORY, 362 CSIDL_APPDATA, 363 CSIDL_PRINTHOOD, 364 CSIDL_LOCAL_APPDATA, 365 CSIDL_ALTSTARTUP, 366 CSIDL_COMMON_ALTSTARTUP, 367 CSIDL_COMMON_FAVORITES, 368 CSIDL_INTERNET_CACHE, 369 CSIDL_COOKIES, 370 CSIDL_HISTORY, 371 CSIDL_COMMON_APPDATA, 372 CSIDL_WINDOWS, 373 CSIDL_SYSTEM, 374 CSIDL_PROGRAM_FILES, 375 CSIDL_MYPICTURES, 376 CSIDL_PROFILE, 377 CSIDL_SYSTEMX86, 378 CSIDL_PROGRAM_FILESX86, 379 CSIDL_PROGRAM_FILES_COMMON, 380 CSIDL_PROGRAM_FILES_COMMONX86, 381 CSIDL_COMMON_TEMPLATES, 382 CSIDL_COMMON_DOCUMENTS, 383 CSIDL_COMMON_ADMINTOOLS, 384 CSIDL_ADMINTOOLS, 385 CSIDL_CONNECTIONS, // = 49 386 CSIDL_COMMON_MUSIC = 53, 387 CSIDL_COMMON_PICTURES, 388 CSIDL_COMMON_VIDEO, 389 CSIDL_RESOURCES, 390 CSIDL_RESOURCES_LOCALIZED, 391 CSIDL_COMMON_OEM_LINKS, 392 CSIDL_CDBURN_AREA, // = 59 393 CSIDL_COMPUTERSNEARME = 61, 394 CSIDL_FLAG_DONT_VERIFY = 0x4000, 395 CSIDL_FLAG_CREATE = 0x8000, 396 CSIDL_FLAG_MASK = 0xFF00 397 } 398 399 enum { 400 KF_FLAG_SIMPLE_IDLIST = 0x00000100, 401 KF_FLAG_NOT_PARENT_RELATIVE = 0x00000200, 402 KF_FLAG_DEFAULT_PATH = 0x00000400, 403 KF_FLAG_INIT = 0x00000800, 404 KF_FLAG_NO_ALIAS = 0x00001000, 405 KF_FLAG_DONT_UNEXPAND = 0x00002000, 406 KF_FLAG_DONT_VERIFY = 0x00004000, 407 KF_FLAG_CREATE = 0x00008000, 408 KF_FLAG_NO_APPCONTAINER_REDIRECTION = 0x00010000, 409 KF_FLAG_ALIAS_ONLY = 0x80000000 410 }; 411 412 alias GUID KNOWNFOLDERID; 413 414 enum KNOWNFOLDERID FOLDERID_LocalAppData = {0xf1b32785, 0x6fba, 0x4fcf, [0x9d,0x55,0x7b,0x8e,0x7f,0x15,0x70,0x91]}; 415 enum KNOWNFOLDERID FOLDERID_RoamingAppData = {0x3eb685db, 0x65f9, 0x4cf6, [0xa0,0x3a,0xe3,0xef,0x65,0x72,0x9f,0x3d]}; 416 417 enum KNOWNFOLDERID FOLDERID_Desktop = {0xb4bfcc3a, 0xdb2c, 0x424c, [0xb0,0x29,0x7f,0xe9,0x9a,0x87,0xc6,0x41]}; 418 enum KNOWNFOLDERID FOLDERID_Documents = {0xfdd39ad0, 0x238f, 0x46af, [0xad,0xb4,0x6c,0x85,0x48,0x3,0x69,0xc7]}; 419 enum KNOWNFOLDERID FOLDERID_Downloads = {0x374de290, 0x123f, 0x4565, [0x91,0x64,0x39,0xc4,0x92,0x5e,0x46,0x7b]}; 420 enum KNOWNFOLDERID FOLDERID_Favorites = {0x1777f761, 0x68ad, 0x4d8a, [0x87,0xbd,0x30,0xb7,0x59,0xfa,0x33,0xdd]}; 421 enum KNOWNFOLDERID FOLDERID_Links = {0xbfb9d5e0, 0xc6a9, 0x404c, [0xb2,0xb2,0xae,0x6d,0xb6,0xaf,0x49,0x68]}; 422 enum KNOWNFOLDERID FOLDERID_Music = {0x4bd8d571, 0x6d19, 0x48d3, [0xbe,0x97,0x42,0x22,0x20,0x8,0xe,0x43]}; 423 enum KNOWNFOLDERID FOLDERID_Pictures = {0x33e28130, 0x4e1e, 0x4676, [0x83,0x5a,0x98,0x39,0x5c,0x3b,0xc3,0xbb]}; 424 enum KNOWNFOLDERID FOLDERID_Programs = {0xa77f5d77, 0x2e2b, 0x44c3, [0xa6,0xa2,0xab,0xa6,0x1,0x5,0x4a,0x51]}; 425 enum KNOWNFOLDERID FOLDERID_SavedGames = {0x4c5c32ff, 0xbb9d, 0x43b0, [0xb5,0xb4,0x2d,0x72,0xe5,0x4e,0xaa,0xa4]}; 426 enum KNOWNFOLDERID FOLDERID_Startup = {0xb97d20bb, 0xf46a, 0x4c97, [0xba,0x10,0x5e,0x36,0x8,0x43,0x8,0x54]}; 427 enum KNOWNFOLDERID FOLDERID_Templates = {0xa63293e8, 0x664e, 0x48db, [0xa0,0x79,0xdf,0x75,0x9e,0x5,0x9,0xf7]}; 428 enum KNOWNFOLDERID FOLDERID_Videos = {0x18989b1d, 0x99b5, 0x455b, [0x84,0x1c,0xab,0x7c,0x74,0xe4,0xdd,0xfc]}; 429 430 enum KNOWNFOLDERID FOLDERID_Fonts = {0xfd228cb7, 0xae11, 0x4ae3, [0x86,0x4c,0x16,0xf3,0x91,0xa,0xb8,0xfe]}; 431 enum KNOWNFOLDERID FOLDERID_ProgramData = {0x62ab5d82, 0xfdc1, 0x4dc3, [0xa9,0xdd,0x7,0xd,0x1d,0x49,0x5d,0x97]}; 432 enum KNOWNFOLDERID FOLDERID_CommonPrograms = {0x139d44e, 0x6afe, 0x49f2, [0x86,0x90,0x3d,0xaf,0xca,0xe6,0xff,0xb8]}; 433 enum KNOWNFOLDERID FOLDERID_CommonStartup = {0x82a5ea35, 0xd9cd, 0x47c5, [0x96,0x29,0xe1,0x5d,0x2f,0x71,0x4e,0x6e]}; 434 enum KNOWNFOLDERID FOLDERID_CommonTemplates = {0xb94237e7, 0x57ac, 0x4347, [0x91,0x51,0xb0,0x8c,0x6c,0x32,0xd1,0xf7]}; 435 436 enum KNOWNFOLDERID FOLDERID_PublicDesktop = {0xc4aa340d, 0xf20f, 0x4863, [0xaf,0xef,0xf8,0x7e,0xf2,0xe6,0xba,0x25]}; 437 enum KNOWNFOLDERID FOLDERID_PublicDocuments = {0xed4824af, 0xdce4, 0x45a8, [0x81,0xe2,0xfc,0x79,0x65,0x8,0x36,0x34]}; 438 enum KNOWNFOLDERID FOLDERID_PublicDownloads = {0x3d644c9b, 0x1fb8, 0x4f30, [0x9b,0x45,0xf6,0x70,0x23,0x5f,0x79,0xc0]}; 439 enum KNOWNFOLDERID FOLDERID_PublicMusic = {0x3214fab5, 0x9757, 0x4298, [0xbb,0x61,0x92,0xa9,0xde,0xaa,0x44,0xff]}; 440 enum KNOWNFOLDERID FOLDERID_PublicPictures = {0xb6ebfb86, 0x6907, 0x413c, [0x9a,0xf7,0x4f,0xc2,0xab,0xf0,0x7c,0xc5]}; 441 enum KNOWNFOLDERID FOLDERID_PublicVideos = {0x2400183a, 0x6185, 0x49fb, [0xa2,0xd8,0x4a,0x39,0x2a,0x60,0x2b,0xa3]}; 442 } 443 444 private { 445 extern(Windows) @nogc @system BOOL _dummy_SHGetSpecialFolderPath(HWND, wchar*, int, BOOL) nothrow { return 0; } 446 extern(Windows) @nogc @system HRESULT _dummy_SHGetKnownFolderPath(const(KNOWNFOLDERID)* rfid, DWORD dwFlags, HANDLE hToken, wchar** ppszPath) nothrow { return 0; } 447 extern(Windows) @nogc @system void _dummy_CoTaskMemFree(void* pv) nothrow {return;} 448 449 __gshared typeof(&_dummy_SHGetSpecialFolderPath) ptrSHGetSpecialFolderPath = null; 450 __gshared typeof(&_dummy_SHGetKnownFolderPath) ptrSHGetKnownFolderPath = null; 451 __gshared typeof(&_dummy_CoTaskMemFree) ptrCoTaskMemFree = null; 452 453 @nogc @trusted bool hasSHGetSpecialFolderPath() nothrow { 454 return ptrSHGetSpecialFolderPath !is null; 455 } 456 457 @nogc @trusted bool hasSHGetKnownFolderPath() nothrow { 458 return ptrSHGetKnownFolderPath !is null && ptrCoTaskMemFree !is null; 459 } 460 } 461 462 shared static this() 463 { 464 HMODULE shellLib = LoadLibraryA("Shell32"); 465 if (shellLib !is null) { 466 ptrSHGetKnownFolderPath = cast(typeof(ptrSHGetKnownFolderPath))enforce(GetProcAddress(shellLib, "SHGetKnownFolderPath")); 467 if (ptrSHGetKnownFolderPath) { 468 HMODULE ole = LoadLibraryA("Ole32"); 469 if (ole !is null) { 470 ptrCoTaskMemFree = cast(typeof(ptrCoTaskMemFree))enforce(GetProcAddress(ole, "CoTaskMemFree")); 471 if (!ptrCoTaskMemFree) { 472 FreeLibrary(ole); 473 } 474 } 475 } 476 477 if (!hasSHGetKnownFolderPath()) { 478 ptrSHGetSpecialFolderPath = cast(typeof(ptrSHGetSpecialFolderPath))GetProcAddress(shellLib, "SHGetSpecialFolderPathW"); 479 } 480 } 481 } 482 483 private string getCSIDLFolder(int csidl, FolderFlag params = FolderFlag.none) nothrow @trusted { 484 import core.stdc.wchar_ : wcslen; 485 486 if (params & FolderFlag.create) { 487 csidl |= CSIDL_FLAG_CREATE; 488 } 489 if (!(params & FolderFlag.verify)) { 490 csidl |= CSIDL_FLAG_DONT_VERIFY; 491 } 492 wchar[MAX_PATH] path = void; 493 if (hasSHGetSpecialFolderPath() && ptrSHGetSpecialFolderPath(null, path.ptr, csidl, FALSE)) { 494 size_t len = wcslen(path.ptr); 495 try { 496 return toUTF8(path[0..len]); 497 } catch(Exception e) { 498 499 } 500 } 501 return null; 502 } 503 504 private string getKnownFolder(const(KNOWNFOLDERID) folder, FolderFlag params = FolderFlag.none) nothrow @trusted { 505 import core.stdc.wchar_ : wcslen; 506 507 wchar* str; 508 509 DWORD flags = 0; 510 if (params & FolderFlag.create) { 511 flags |= KF_FLAG_CREATE; 512 } 513 if (!(params & FolderFlag.verify)) { 514 flags |= KF_FLAG_DONT_VERIFY; 515 } 516 517 if (hasSHGetKnownFolderPath() && ptrSHGetKnownFolderPath(&folder, flags, null, &str) == S_OK) { 518 scope(exit) ptrCoTaskMemFree(str); 519 try { 520 return str[0..wcslen(str)].toUTF8; 521 } catch(Exception e) { 522 523 } 524 } 525 return null; 526 } 527 528 string roamingPath(FolderFlag params = FolderFlag.none) nothrow @safe 529 { 530 if (hasSHGetKnownFolderPath()) { 531 return getKnownFolder(FOLDERID_RoamingAppData, params); 532 } else if (hasSHGetSpecialFolderPath()) { 533 return getCSIDLFolder(CSIDL_APPDATA, params); 534 } else { 535 return null; 536 } 537 } 538 539 string savedGames(FolderFlag params = FolderFlag.none) nothrow @safe 540 { 541 if (hasSHGetKnownFolderPath()) { 542 return getKnownFolder(FOLDERID_SavedGames, params); 543 } else { 544 return null; 545 } 546 } 547 548 string writablePath(StandardPath type, FolderFlag params = FolderFlag.none) nothrow @safe 549 { 550 if (hasSHGetKnownFolderPath()) { 551 final switch(type) { 552 case StandardPath.config: 553 case StandardPath.data: 554 return getKnownFolder(FOLDERID_LocalAppData, params); 555 case StandardPath.cache: 556 return null; 557 case StandardPath.desktop: 558 return getKnownFolder(FOLDERID_Desktop, params); 559 case StandardPath.documents: 560 return getKnownFolder(FOLDERID_Documents, params); 561 case StandardPath.pictures: 562 return getKnownFolder(FOLDERID_Pictures, params); 563 case StandardPath.music: 564 return getKnownFolder(FOLDERID_Music, params); 565 case StandardPath.videos: 566 return getKnownFolder(FOLDERID_Videos, params); 567 case StandardPath.downloads: 568 return getKnownFolder(FOLDERID_Downloads, params); 569 case StandardPath.templates: 570 return getKnownFolder(FOLDERID_Templates, params); 571 case StandardPath.publicShare: 572 return null; 573 case StandardPath.fonts: 574 return null; 575 case StandardPath.applications: 576 return getKnownFolder(FOLDERID_Programs, params); 577 case StandardPath.startup: 578 return getKnownFolder(FOLDERID_Startup, params); 579 } 580 } else if (hasSHGetSpecialFolderPath()) { 581 final switch(type) { 582 case StandardPath.config: 583 case StandardPath.data: 584 return getCSIDLFolder(CSIDL_LOCAL_APPDATA, params); 585 case StandardPath.cache: 586 return null; 587 case StandardPath.desktop: 588 return getCSIDLFolder(CSIDL_DESKTOPDIRECTORY, params); 589 case StandardPath.documents: 590 return getCSIDLFolder(CSIDL_PERSONAL, params); 591 case StandardPath.pictures: 592 return getCSIDLFolder(CSIDL_MYPICTURES, params); 593 case StandardPath.music: 594 return getCSIDLFolder(CSIDL_MYMUSIC, params); 595 case StandardPath.videos: 596 return getCSIDLFolder(CSIDL_MYVIDEO, params); 597 case StandardPath.downloads: 598 return null; 599 case StandardPath.templates: 600 return getCSIDLFolder(CSIDL_TEMPLATES, params); 601 case StandardPath.publicShare: 602 return null; 603 case StandardPath.fonts: 604 return null; 605 case StandardPath.applications: 606 return getCSIDLFolder(CSIDL_PROGRAMS, params); 607 case StandardPath.startup: 608 return getCSIDLFolder(CSIDL_STARTUP, params); 609 } 610 } else { 611 return null; 612 } 613 } 614 615 string[] standardPaths(StandardPath type) nothrow @safe 616 { 617 string commonPath; 618 619 if (hasSHGetKnownFolderPath()) { 620 switch(type) { 621 case StandardPath.config: 622 case StandardPath.data: 623 commonPath = getKnownFolder(FOLDERID_ProgramData); 624 break; 625 case StandardPath.desktop: 626 commonPath = getKnownFolder(FOLDERID_PublicDesktop); 627 break; 628 case StandardPath.documents: 629 commonPath = getKnownFolder(FOLDERID_PublicDocuments); 630 break; 631 case StandardPath.pictures: 632 commonPath = getKnownFolder(FOLDERID_PublicPictures); 633 break; 634 case StandardPath.music: 635 commonPath = getKnownFolder(FOLDERID_PublicMusic); 636 break; 637 case StandardPath.videos: 638 commonPath = getKnownFolder(FOLDERID_PublicVideos); 639 break; 640 case StandardPath.downloads: 641 commonPath = getKnownFolder(FOLDERID_PublicDownloads); 642 break; 643 case StandardPath.templates: 644 commonPath = getKnownFolder(FOLDERID_CommonTemplates); 645 break; 646 case StandardPath.fonts: 647 commonPath = getKnownFolder(FOLDERID_Fonts); 648 break; 649 case StandardPath.applications: 650 commonPath = getKnownFolder(FOLDERID_CommonPrograms); 651 break; 652 case StandardPath.startup: 653 commonPath = getKnownFolder(FOLDERID_CommonStartup); 654 break; 655 default: 656 break; 657 } 658 } else if (hasSHGetSpecialFolderPath()) { 659 switch(type) { 660 case StandardPath.config: 661 case StandardPath.data: 662 commonPath = getCSIDLFolder(CSIDL_COMMON_APPDATA); 663 break; 664 case StandardPath.desktop: 665 commonPath = getCSIDLFolder(CSIDL_COMMON_DESKTOPDIRECTORY); 666 break; 667 case StandardPath.documents: 668 commonPath = getCSIDLFolder(CSIDL_COMMON_DOCUMENTS); 669 break; 670 case StandardPath.pictures: 671 commonPath = getCSIDLFolder(CSIDL_COMMON_PICTURES); 672 break; 673 case StandardPath.music: 674 commonPath = getCSIDLFolder(CSIDL_COMMON_MUSIC); 675 break; 676 case StandardPath.videos: 677 commonPath = getCSIDLFolder(CSIDL_COMMON_VIDEO); 678 break; 679 case StandardPath.templates: 680 commonPath = getCSIDLFolder(CSIDL_COMMON_TEMPLATES); 681 break; 682 case StandardPath.fonts: 683 commonPath = getCSIDLFolder(CSIDL_FONTS); 684 break; 685 case StandardPath.applications: 686 commonPath = getCSIDLFolder(CSIDL_COMMON_PROGRAMS); 687 break; 688 case StandardPath.startup: 689 commonPath = getCSIDLFolder(CSIDL_COMMON_STARTUP); 690 break; 691 default: 692 break; 693 } 694 } 695 696 string[] paths; 697 string userPath = writablePath(type); 698 if (userPath.length) 699 paths ~= userPath; 700 if (commonPath.length) 701 paths ~= commonPath; 702 return paths; 703 } 704 } else version(OSX) { 705 private { 706 version(StandardPathsCocoa) { 707 alias size_t NSUInteger; 708 709 710 enum objectiveC_declarations = q{ 711 extern (Objective-C) 712 interface NSString 713 { 714 NSString initWithUTF8String(in char* str) @selector("initWithUTF8String:"); 715 const(char)* UTF8String() @selector("UTF8String"); 716 void release() @selector("release"); 717 } 718 719 extern(Objective-C) 720 interface NSArray 721 { 722 NSString objectAtIndex(size_t) @selector("objectAtIndex:"); 723 NSString firstObject() @selector("firstObject"); 724 NSUInteger count() @selector("count"); 725 void release() @selector("release"); 726 } 727 728 extern(Objective-C) 729 interface NSURL 730 { 731 NSString absoluteString() @selector("absoluteString"); 732 void release() @selector("release"); 733 } 734 735 extern(Objective-C) 736 interface NSError 737 { 738 739 } 740 741 extern (C) NSFileManager objc_lookUpClass(in char* name); 742 743 extern(Objective-C) 744 interface NSFileManager 745 { 746 NSFileManager defaultManager() @selector("defaultManager"); 747 NSURL URLForDirectory(NSSearchPathDirectory, NSSearchPathDomainMask domain, NSURL url, int shouldCreate, NSError* error) @selector("URLForDirectory:inDomain:appropriateForURL:create:error:"); 748 } 749 }; 750 751 mixin(objectiveC_declarations); 752 753 enum : NSUInteger { 754 NSApplicationDirectory = 1, 755 NSDemoApplicationDirectory, 756 NSDeveloperApplicationDirectory, 757 NSAdminApplicationDirectory, 758 NSLibraryDirectory, 759 NSDeveloperDirectory, 760 NSUserDirectory, 761 NSDocumentationDirectory, 762 NSDocumentDirectory, 763 NSCoreServiceDirectory, 764 NSAutosavedInformationDirectory = 11, 765 NSDesktopDirectory = 12, 766 NSCachesDirectory = 13, 767 NSApplicationSupportDirectory = 14, 768 NSDownloadsDirectory = 15, 769 NSInputMethodsDirectory = 16, 770 NSMoviesDirectory = 17, 771 NSMusicDirectory = 18, 772 NSPicturesDirectory = 19, 773 NSPrinterDescriptionDirectory = 20, 774 NSSharedPublicDirectory = 21, 775 NSPreferencePanesDirectory = 22, 776 NSItemReplacementDirectory = 99, 777 NSAllApplicationsDirectory = 100, 778 NSAllLibrariesDirectory = 101, 779 }; 780 781 alias NSUInteger NSSearchPathDirectory; 782 783 enum : NSUInteger { 784 NSUserDomainMask = 1, 785 NSLocalDomainMask = 2, 786 NSNetworkDomainMask = 4, 787 NSSystemDomainMask = 8, 788 NSAllDomainsMask = 0x0ffff, 789 }; 790 791 alias NSUInteger NSSearchPathDomainMask; 792 793 string domainDir(NSSearchPathDirectory dir, NSSearchPathDomainMask domain, bool shouldCreate = false) nothrow @trusted 794 { 795 import std.uri; 796 import std.algorithm : startsWith; 797 798 try { 799 auto managerInterface = objc_lookUpClass("NSFileManager"); 800 if (!managerInterface) { 801 return null; 802 } 803 804 auto manager = managerInterface.defaultManager(); 805 if (!manager) { 806 return null; 807 } 808 809 NSURL url = manager.URLForDirectory(dir, domain, null, shouldCreate, null); 810 if (!url) { 811 return null; 812 } 813 scope(exit) url.release(); 814 NSString nsstr = url.absoluteString(); 815 scope(exit) nsstr.release(); 816 817 string str = fromStringz(nsstr.UTF8String()).idup; 818 819 enum fileProtocol = "file://"; 820 if (str.startsWith(fileProtocol)) { 821 str = str.decode()[fileProtocol.length..$]; 822 if (str.length > 1 && str[$-1] == '/') { 823 return str[0..$-1]; 824 } else { 825 return str; 826 } 827 } 828 } catch(Exception e) { 829 830 } 831 return null; 832 } 833 } else { 834 private enum : short { 835 kOnSystemDisk = -32768L, /* previously was 0x8000 but that is an unsigned value whereas vRefNum is signed*/ 836 kOnAppropriateDisk = -32767, /* Generally, the same as kOnSystemDisk, but it's clearer that this isn't always the 'boot' disk.*/ 837 /* Folder Domains - Carbon only. The constants above can continue to be used, but the folder/volume returned will*/ 838 /* be from one of the domains below.*/ 839 kSystemDomain = -32766, /* Read-only system hierarchy.*/ 840 kLocalDomain = -32765, /* All users of a single machine have access to these resources.*/ 841 kNetworkDomain = -32764, /* All users configured to use a common network server has access to these resources.*/ 842 kUserDomain = -32763, /* Read/write. Resources that are private to the user.*/ 843 kClassicDomain = -32762, /* Domain referring to the currently configured Classic System Folder. Not supported in Mac OS X Leopard and later.*/ 844 kFolderManagerLastDomain = -32760 845 } 846 847 private @nogc int k(string s) nothrow { 848 return s[0] << 24 | s[1] << 16 | s[2] << 8 | s[3]; 849 } 850 851 private enum { 852 kDesktopFolderType = k("desk"), /* the desktop folder; objects in this folder show on the desktop. */ 853 kTrashFolderType = k("trsh"), /* the trash folder; objects in this folder show up in the trash */ 854 kWhereToEmptyTrashFolderType = k("empt"), /* the "empty trash" folder; Finder starts empty from here down */ 855 kFontsFolderType = k("font"), /* Fonts go here */ 856 kPreferencesFolderType = k("pref"), /* preferences for applications go here */ 857 kSystemPreferencesFolderType = k("sprf"), /* the PreferencePanes folder, where Mac OS X Preference Panes go */ 858 kTemporaryFolderType = k("temp"), /* On Mac OS X, each user has their own temporary items folder, and the Folder Manager attempts to set permissions of these*/ 859 /* folders such that other users can not access the data inside. On Mac OS X 10.4 and later the data inside the temporary*/ 860 /* items folder is deleted at logout and at boot, but not otherwise. Earlier version of Mac OS X would delete items inside*/ 861 /* the temporary items folder after a period of inaccess. You can ask for a temporary item in a specific domain or on a */ 862 /* particular volume by FSVolumeRefNum. If you want a location for temporary items for a short time, then use either*/ 863 /* ( kUserDomain, kkTemporaryFolderType ) or ( kSystemDomain, kTemporaryFolderType ). The kUserDomain varient will always be*/ 864 /* on the same volume as the user's home folder, while the kSystemDomain version will be on the same volume as /var/tmp/ ( and*/ 865 /* will probably be on the local hard drive in case the user's home is a network volume ). If you want a location for a temporary*/ 866 /* file or folder to use for saving a document, especially if you want to use FSpExchangeFile() to implement a safe-save, then*/ 867 /* ask for the temporary items folder on the same volume as the file you are safe saving.*/ 868 /* However, be prepared for a failure to find a temporary folder in any domain or on any volume. Some volumes may not have*/ 869 /* a location for a temporary folder, or the permissions of the volume may be such that the Folder Manager can not return*/ 870 /* a temporary folder for the volume.*/ 871 /* If your application creates an item in a temporary items older you should delete that item as soon as it is not needed,*/ 872 /* and certainly before your application exits, since otherwise the item is consuming disk space until the user logs out or*/ 873 /* restarts. Any items left inside a temporary items folder should be moved into a folder inside the Trash folder on the disk*/ 874 /* when the user logs in, inside a folder named "Recovered items", in case there is anything useful to the end user.*/ 875 kChewableItemsFolderType = k("flnt"), /* similar to kTemporaryItemsFolderType, except items in this folder are deleted at boot or when the disk is unmounted */ 876 kTemporaryItemsInCacheDataFolderType = k("vtmp"), /* A folder inside the kCachedDataFolderType for the given domain which can be used for transient data*/ 877 kApplicationsFolderType = k("apps"), /* Applications on Mac OS X are typically put in this folder ( or a subfolder ).*/ 878 kVolumeRootFolderType = k("root"), /* root folder of a volume or domain */ 879 kDomainTopLevelFolderType = k("dtop"), /* The top-level of a Folder domain, e.g. "/System"*/ 880 kDomainLibraryFolderType = k("dlib"), /* the Library subfolder of a particular domain*/ 881 kUsersFolderType = k("usrs"), /* "Users" folder, usually contains one folder for each user. */ 882 kCurrentUserFolderType = k("cusr"), /* The folder for the currently logged on user; domain passed in is ignored. */ 883 kSharedUserDataFolderType = k("sdat"), /* A Shared folder, readable & writeable by all users */ 884 kCachedDataFolderType = k("cach"), /* Contains various cache files for different clients*/ 885 kDownloadsFolderType = k("down"), /* Refers to the ~/Downloads folder*/ 886 kApplicationSupportFolderType = k("asup"), /* third-party items and folders */ 887 888 889 kDocumentsFolderType = k("docs"), /* User documents are typically put in this folder ( or a subfolder ).*/ 890 kPictureDocumentsFolderType = k("pdoc"), /* Refers to the "Pictures" folder in a users home directory*/ 891 kMovieDocumentsFolderType = k("mdoc"), /* Refers to the "Movies" folder in a users home directory*/ 892 kMusicDocumentsFolderType = 0xB5646F63/*'µdoc'*/, /* Refers to the "Music" folder in a users home directory*/ 893 kInternetSitesFolderType = k("site"), /* Refers to the "Sites" folder in a users home directory*/ 894 kPublicFolderType = k("pubb"), /* Refers to the "Public" folder in a users home directory*/ 895 896 kDropBoxFolderType = k("drop") /* Refers to the "Drop Box" folder inside the user's home directory*/ 897 }; 898 899 struct FSRef { 900 char[80] hidden; /* private to File Manager*/ 901 }; 902 903 alias ubyte Boolean; 904 alias int OSType; 905 alias short OSErr; 906 alias int OSStatus; 907 908 extern(C) @nogc @system OSErr _dummy_FSFindFolder(short, OSType, Boolean, FSRef*) nothrow { return 0; } 909 extern(C) @nogc @system OSStatus _dummy_FSRefMakePath(const(FSRef)*, char*, uint) nothrow { return 0; } 910 911 __gshared typeof(&_dummy_FSFindFolder) ptrFSFindFolder = null; 912 __gshared typeof(&_dummy_FSRefMakePath) ptrFSRefMakePath = null; 913 } 914 } 915 916 version(StandardPathsCocoa) { 917 918 } else { 919 shared static this() 920 { 921 enum carbonPath = "CoreServices.framework/Versions/A/CoreServices"; 922 923 import core.sys.posix.dlfcn; 924 925 void* handle = dlopen(toStringz(carbonPath), RTLD_NOW | RTLD_LOCAL); 926 if (handle) { 927 ptrFSFindFolder = cast(typeof(ptrFSFindFolder))dlsym(handle, "FSFindFolder"); 928 ptrFSRefMakePath = cast(typeof(ptrFSRefMakePath))dlsym(handle, "FSRefMakePath"); 929 } 930 if (ptrFSFindFolder == null || ptrFSRefMakePath == null) { 931 debug collectException(stderr.writeln("Could not load carbon functions")); 932 if (handle) dlclose(handle); 933 } 934 } 935 936 private @nogc @trusted bool isCarbonLoaded() nothrow 937 { 938 return ptrFSFindFolder != null && ptrFSRefMakePath != null; 939 } 940 941 private enum OSErr noErr = 0; 942 943 private string fsPath(short domain, OSType type, bool shouldCreate = false) nothrow @trusted 944 { 945 import std.stdio; 946 FSRef fsref; 947 if (isCarbonLoaded() && ptrFSFindFolder(domain, type, shouldCreate, &fsref) == noErr) { 948 949 char[2048] buf; 950 char* path = buf.ptr; 951 if (ptrFSRefMakePath(&fsref, path, buf.sizeof) == noErr) { 952 try { 953 return fromStringz(path).idup; 954 } 955 catch(Exception e) { 956 957 } 958 } 959 } 960 return null; 961 } 962 } 963 964 private string writablePathImpl(StandardPath type, bool shouldCreate = false) nothrow @safe 965 { 966 version(StandardPathsCocoa) { 967 final switch(type) { 968 case StandardPath.config: 969 return domainDir(NSLibraryDirectory, NSUserDomainMask, shouldCreate).maybeBuild("Preferences").createIfNeeded(shouldCreate); 970 case StandardPath.cache: 971 return domainDir(NSCachesDirectory, NSUserDomainMask, shouldCreate); 972 case StandardPath.data: 973 return domainDir(NSApplicationSupportDirectory, NSUserDomainMask, shouldCreate); 974 case StandardPath.desktop: 975 return domainDir(NSDesktopDirectory, NSUserDomainMask, shouldCreate); 976 case StandardPath.documents: 977 return domainDir(NSDocumentDirectory, NSUserDomainMask, shouldCreate); 978 case StandardPath.pictures: 979 return domainDir(NSPicturesDirectory, NSUserDomainMask, shouldCreate); 980 case StandardPath.music: 981 return domainDir(NSMusicDirectory, NSUserDomainMask, shouldCreate); 982 case StandardPath.videos: 983 return domainDir(NSMoviesDirectory, NSUserDomainMask, shouldCreate); 984 case StandardPath.downloads: 985 return domainDir(NSDownloadsDirectory, NSUserDomainMask, shouldCreate); 986 case StandardPath.templates: 987 return null; 988 case StandardPath.publicShare: 989 return domainDir(NSSharedPublicDirectory, NSUserDomainMask, shouldCreate); 990 case StandardPath.fonts: 991 return domainDir(NSLibraryDirectory, NSUserDomainMask, shouldCreate).maybeBuild("Fonts").createIfNeeded(shouldCreate); 992 case StandardPath.applications: 993 return domainDir(NSApplicationDirectory, NSUserDomainMask, shouldCreate); 994 case StandardPath.startup: 995 return null; 996 } 997 } else { 998 final switch(type) { 999 case StandardPath.config: 1000 return fsPath(kUserDomain, kPreferencesFolderType, shouldCreate); 1001 case StandardPath.cache: 1002 return fsPath(kUserDomain, kCachedDataFolderType, shouldCreate); 1003 case StandardPath.data: 1004 return fsPath(kUserDomain, kApplicationSupportFolderType, shouldCreate); 1005 case StandardPath.desktop: 1006 return fsPath(kUserDomain, kDesktopFolderType, shouldCreate); 1007 case StandardPath.documents: 1008 return fsPath(kUserDomain, kDocumentsFolderType, shouldCreate); 1009 case StandardPath.pictures: 1010 return fsPath(kUserDomain, kPictureDocumentsFolderType, shouldCreate); 1011 case StandardPath.music: 1012 return fsPath(kUserDomain, kMusicDocumentsFolderType, shouldCreate); 1013 case StandardPath.videos: 1014 return fsPath(kUserDomain, kMovieDocumentsFolderType, shouldCreate); 1015 case StandardPath.downloads: 1016 return fsPath(kUserDomain, kDownloadsFolderType, shouldCreate); 1017 case StandardPath.templates: 1018 return null; 1019 case StandardPath.publicShare: 1020 return fsPath(kUserDomain, kPublicFolderType, shouldCreate); 1021 case StandardPath.fonts: 1022 return fsPath(kUserDomain, kFontsFolderType, shouldCreate); 1023 case StandardPath.applications: 1024 return fsPath(kUserDomain, kApplicationsFolderType, shouldCreate); 1025 case StandardPath.startup: 1026 return null; 1027 } 1028 } 1029 } 1030 1031 string writablePath(StandardPath type, FolderFlag params = FolderFlag.none) nothrow @safe 1032 { 1033 const bool shouldCreate = (params & FolderFlag.create) != 0; 1034 const bool shouldVerify = (params & FolderFlag.verify) != 0; 1035 return writablePathImpl(type, shouldCreate).verifyIfNeeded(shouldVerify); 1036 } 1037 1038 string[] standardPaths(StandardPath type) nothrow @safe 1039 { 1040 string commonPath; 1041 1042 version(StandardPathsCocoa) { 1043 switch(type) { 1044 case StandardPath.fonts: 1045 commonPath = domainDir(NSLibraryDirectory, NSSystemDomainMask).maybeBuild("Fonts"); 1046 break; 1047 case StandardPath.applications: 1048 commonPath = domainDir(NSApplicationDirectory, NSSystemDomainMask); 1049 break; 1050 case StandardPath.data: 1051 commonPath = domainDir(NSApplicationSupportDirectory, NSSystemDomainMask); 1052 break; 1053 case StandardPath.cache: 1054 commonPath = domainDir(NSCachesDirectory, NSSystemDomainMask); 1055 break; 1056 default: 1057 break; 1058 } 1059 } else { 1060 switch(type) { 1061 case StandardPath.fonts: 1062 commonPath = fsPath(kOnAppropriateDisk, kFontsFolderType); 1063 break; 1064 case StandardPath.applications: 1065 commonPath = fsPath(kOnAppropriateDisk, kApplicationsFolderType); 1066 break; 1067 case StandardPath.data: 1068 commonPath = fsPath(kOnAppropriateDisk, kApplicationSupportFolderType); 1069 break; 1070 case StandardPath.cache: 1071 commonPath = fsPath(kOnAppropriateDisk, kCachedDataFolderType); 1072 break; 1073 default: 1074 break; 1075 } 1076 } 1077 1078 string[] paths; 1079 string userPath = writablePath(type); 1080 if (userPath.length) 1081 paths ~= userPath; 1082 if (commonPath.length) 1083 paths ~= commonPath; 1084 return paths; 1085 } 1086 1087 } else { 1088 1089 static if (!isFreedesktop) { 1090 static assert(false, "Unsupported platform"); 1091 } else { 1092 public import xdgpaths; 1093 1094 private { 1095 import std.stdio : File; 1096 import std.algorithm : startsWith; 1097 import std.string; 1098 import std.traits; 1099 } 1100 1101 unittest 1102 { 1103 assert(maybeConcat(null, "path") == string.init); 1104 assert(maybeConcat("path", "/file") == "path/file"); 1105 } 1106 1107 private @trusted string getFromUserDirs(Range)(string xdgdir, string home, Range range) if (isInputRange!Range && isSomeString!(ElementType!Range)) 1108 { 1109 foreach(line; range) { 1110 line = strip(line); 1111 auto index = xdgdir.length; 1112 if (line.startsWith(xdgdir) && line.length > index && line[index] == '=') { 1113 line = line[index+1..$]; 1114 if (line.length > 2 && line[0] == '"' && line[$-1] == '"') 1115 { 1116 line = line[1..$-1]; 1117 1118 if (line.startsWith("$HOME")) { 1119 return maybeConcat(home, assumeUnique(line[5..$])); 1120 } 1121 if (line.length == 0 || line[0] != '/') { 1122 continue; 1123 } 1124 return assumeUnique(line); 1125 } 1126 } 1127 } 1128 return null; 1129 } 1130 1131 1132 unittest 1133 { 1134 string content = 1135 `# Comment 1136 1137 XDG_DOCUMENTS_DIR="$HOME/My Documents" 1138 XDG_MUSIC_DIR="/data/Music" 1139 XDG_VIDEOS_DIR="data/Video" 1140 `; 1141 string home = "/home/user"; 1142 1143 assert(getFromUserDirs("XDG_DOCUMENTS_DIR", home, content.splitLines) == "/home/user/My Documents"); 1144 assert(getFromUserDirs("XDG_MUSIC_DIR", home, content.splitLines) == "/data/Music"); 1145 assert(getFromUserDirs("XDG_DOWNLOAD_DIR", home, content.splitLines).empty); 1146 assert(getFromUserDirs("XDG_VIDEOS_DIR", home, content.splitLines).empty); 1147 } 1148 1149 private @trusted string getFromDefaultDirs(Range)(string key, string home, Range range) if (isInputRange!Range && isSomeString!(ElementType!Range)) 1150 { 1151 foreach(line; range) { 1152 line = strip(line); 1153 auto index = key.length; 1154 if (line.startsWith(key) && line.length > index && line[index] == '=') 1155 { 1156 line = line[index+1..$]; 1157 return home ~ "/" ~ assumeUnique(line); 1158 } 1159 } 1160 return null; 1161 } 1162 1163 unittest 1164 { 1165 string content = 1166 `# Comment 1167 1168 DOCUMENTS=MyDocuments 1169 PICTURES=Images 1170 `; 1171 string home = "/home/user"; 1172 assert(getFromDefaultDirs("DOCUMENTS", home, content.splitLines) == "/home/user/MyDocuments"); 1173 assert(getFromDefaultDirs("PICTURES", home, content.splitLines) == "/home/user/Images"); 1174 assert(getFromDefaultDirs("VIDEOS", home, content.splitLines).empty); 1175 } 1176 1177 private string xdgUserDir(string key, string fallback = null) nothrow @trusted { 1178 string fileName = maybeConcat(writablePath(StandardPath.config), "/user-dirs.dirs"); 1179 string home = homeDir(); 1180 try { 1181 auto f = File(fileName, "r"); 1182 auto xdgdir = "XDG_" ~ key ~ "_DIR"; 1183 auto path = getFromUserDirs(xdgdir, home, f.byLine()); 1184 if (path.length) { 1185 return path; 1186 } 1187 } catch(Exception e) { 1188 1189 } 1190 1191 if (home.length) { 1192 try { 1193 auto f = File("/etc/xdg/user-dirs.defaults", "r"); 1194 auto path = getFromDefaultDirs(key, home, f.byLine()); 1195 if (path.length) { 1196 return path; 1197 } 1198 } catch (Exception e) { 1199 1200 } 1201 if (fallback.length) { 1202 return home ~ fallback; 1203 } 1204 } 1205 return null; 1206 } 1207 1208 private string homeFontsPath() nothrow @trusted { 1209 return maybeConcat(homeDir(), "/.fonts"); 1210 } 1211 1212 private string[] fontPaths() nothrow @trusted 1213 { 1214 enum localShare = "/usr/local/share/fonts"; 1215 enum share = "/usr/share/fonts"; 1216 1217 string homeFonts = homeFontsPath(); 1218 if (homeFonts.length) { 1219 return [homeFonts, localShare, share]; 1220 } else { 1221 return [localShare, share]; 1222 } 1223 } 1224 1225 private string writablePathImpl(StandardPath type, bool shouldCreate) nothrow @safe 1226 { 1227 final switch(type) { 1228 case StandardPath.config: 1229 return xdgConfigHome(null, shouldCreate); 1230 case StandardPath.cache: 1231 return xdgCacheHome(null, shouldCreate); 1232 case StandardPath.data: 1233 return xdgDataHome(null, shouldCreate); 1234 case StandardPath.desktop: 1235 return xdgUserDir("DESKTOP", "/Desktop").createIfNeeded(shouldCreate); 1236 case StandardPath.documents: 1237 return xdgUserDir("DOCUMENTS").createIfNeeded(shouldCreate); 1238 case StandardPath.pictures: 1239 return xdgUserDir("PICTURES").createIfNeeded(shouldCreate); 1240 case StandardPath.music: 1241 return xdgUserDir("MUSIC").createIfNeeded(shouldCreate); 1242 case StandardPath.videos: 1243 return xdgUserDir("VIDEOS").createIfNeeded(shouldCreate); 1244 case StandardPath.downloads: 1245 return xdgUserDir("DOWNLOAD").createIfNeeded(shouldCreate); 1246 case StandardPath.templates: 1247 return xdgUserDir("TEMPLATES", "/Templates").createIfNeeded(shouldCreate); 1248 case StandardPath.publicShare: 1249 return xdgUserDir("PUBLICSHARE", "/Public").createIfNeeded(shouldCreate); 1250 case StandardPath.fonts: 1251 return homeFontsPath().createIfNeeded(shouldCreate); 1252 case StandardPath.applications: 1253 return xdgDataHome("applications", shouldCreate); 1254 case StandardPath.startup: 1255 return xdgConfigHome("autostart", shouldCreate); 1256 } 1257 } 1258 1259 string writablePath(StandardPath type, FolderFlag params = FolderFlag.none) nothrow @safe 1260 { 1261 const bool shouldCreate = (params & FolderFlag.create) != 0; 1262 const bool shouldVerify = (params & FolderFlag.verify) != 0; 1263 return writablePathImpl(type, shouldCreate).verifyIfNeeded(shouldVerify); 1264 } 1265 1266 string[] standardPaths(StandardPath type) nothrow @safe 1267 { 1268 string[] paths; 1269 1270 switch(type) { 1271 case StandardPath.data: 1272 return xdgAllDataDirs(); 1273 case StandardPath.config: 1274 return xdgAllConfigDirs(); 1275 case StandardPath.applications: 1276 return xdgAllDataDirs("applications"); 1277 case StandardPath.startup: 1278 return xdgAllConfigDirs("autostart"); 1279 case StandardPath.fonts: 1280 return fontPaths(); 1281 default: 1282 break; 1283 } 1284 1285 string userPath = writablePath(type); 1286 if (userPath.length) { 1287 return [userPath]; 1288 } 1289 return null; 1290 } 1291 } 1292 }