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