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