1 module dlangui.core.filemanager; 2 import dlangui.core.logger; 3 import isfreedesktop; 4 5 /** 6 * Show and select directory or file in OS file manager. 7 * 8 * On Windows this shows file in File Exporer. 9 * 10 * On macOS it reveals file in Finder. 11 * 12 * On Freedesktop systems this function finds user preferred program that used to open directories. 13 * If found file manager is known to this function, it uses file manager specific way to select file. 14 * Otherwise it fallbacks to opening $(D pathName) if it's directory or parent directory of $(D pathName) if it's file. 15 */ 16 @trusted bool showInFileManager(string pathName) { 17 import std.process; 18 import std.path; 19 import std.file; 20 Log.i("showInFileManager(", pathName, ")"); 21 string normalized = buildNormalizedPath(pathName); 22 if (!normalized.exists) { 23 Log.e("showInFileManager failed - file or directory does not exist"); 24 return false; 25 } 26 import std.string; 27 try { 28 version (Windows) { 29 import core.sys.windows.windows; 30 import dlangui.core.files; 31 import std.utf : toUTF16z; 32 33 string explorerPath = findExecutablePath("explorer.exe"); 34 if (!explorerPath.length) { 35 Log.e("showInFileManager failed - cannot find explorer.exe"); 36 return false; 37 } 38 string arg = "/select,\"" ~ normalized ~ "\""; 39 STARTUPINFO si; 40 si.cb = si.sizeof; 41 PROCESS_INFORMATION pi; 42 Log.d("showInFileManager: ", explorerPath, " ", arg); 43 arg = "\"" ~ explorerPath ~ "\" " ~ arg; 44 auto res = CreateProcessW(null, //explorerPath.toUTF16z, 45 cast(wchar*)arg.toUTF16z, 46 null, null, false, DETACHED_PROCESS, 47 null, null, &si, &pi); 48 if (!res) { 49 Log.e("showInFileManager failed to run explorer.exe"); 50 return false; 51 } 52 return true; 53 } else version (OSX) { 54 string exe = "/usr/bin/osascript"; 55 string[] args; 56 args ~= exe; 57 args ~= "-e"; 58 args ~= "tell application \"Finder\" to reveal (POSIX file \"" ~ normalized ~ "\")"; 59 Log.d("Executing command: ", args); 60 auto pid = spawnProcess(args); 61 wait(pid); 62 args[2] = "tell application \"Finder\" to activate"; 63 Log.d("Executing command: ", args); 64 pid = spawnProcess(args); 65 wait(pid); 66 return true; 67 } else version(Android) { 68 Log.w("showInFileManager is not implemented for current platform"); 69 } else static if (isFreedesktop) { 70 import std.stdio : File; 71 import std.algorithm : map, filter, splitter, canFind, equal, findSplit; 72 import std.exception : collectException, assumeUnique; 73 import std.path : buildPath, absolutePath, isAbsolute, dirName, baseName; 74 import std.range; 75 import std.string : toStringz; 76 import std.typecons : Tuple, tuple; 77 static import std.stdio; 78 import inilike.common; 79 import inilike.range; 80 import xdgpaths; 81 82 string toOpen = pathName; 83 84 static string unescapeQuotedArgument(string value) nothrow pure 85 { 86 static immutable Tuple!(char, char)[] pairs = [ 87 tuple('`', '`'), 88 tuple('$', '$'), 89 tuple('"', '"'), 90 tuple('\\', '\\') 91 ]; 92 return doUnescape(value, pairs); 93 } 94 95 static auto unquoteExec(string unescapedValue) pure 96 { 97 auto value = unescapedValue; 98 string[] result; 99 size_t i; 100 101 static string parseQuotedPart(ref size_t i, char delimeter, string value) 102 { 103 size_t start = ++i; 104 bool inQuotes = true; 105 106 while(i < value.length && inQuotes) { 107 if (value[i] == '\\' && value.length > i+1 && value[i+1] == '\\') { 108 i+=2; 109 continue; 110 } 111 112 inQuotes = !(value[i] == delimeter && (value[i-1] != '\\' || (i>=2 && value[i-1] == '\\' && value[i-2] == '\\') )); 113 if (inQuotes) { 114 i++; 115 } 116 } 117 if (inQuotes) { 118 throw new Exception("Missing pair quote"); 119 } 120 return unescapeQuotedArgument(value[start..i]); 121 } 122 123 char[] append; 124 bool wasInQuotes; 125 while(i < value.length) { 126 if (value[i] == ' ' || value[i] == '\t') { 127 if (!wasInQuotes && append.length >= 1 && append[$-1] == '\\') { 128 append[$-1] = value[i]; 129 } else { 130 if (append !is null) { 131 result ~= append.assumeUnique; 132 append = null; 133 } 134 } 135 wasInQuotes = false; 136 } else if (value[i] == '"' || value[i] == '\'') { 137 append ~= parseQuotedPart(i, value[i], value); 138 wasInQuotes = true; 139 } else { 140 append ~= value[i]; 141 wasInQuotes = false; 142 } 143 i++; 144 } 145 146 if (append !is null) { 147 result ~= append.assumeUnique; 148 } 149 150 return result; 151 } 152 153 static string urlToFilePath(string url) nothrow pure 154 { 155 enum protocol = "file://"; 156 if (url.length > protocol.length && url[0..protocol.length] == protocol) { 157 return url[protocol.length..$]; 158 } else { 159 return url; 160 } 161 } 162 163 static string[] expandExecArgs(in string[] unquotedArgs, in string[] urls = null, string iconName = null, string displayName = null, string fileName = null) pure 164 { 165 string[] toReturn; 166 foreach(token; unquotedArgs) { 167 if (token == "%F") { 168 toReturn ~= urls.map!(url => urlToFilePath(url)).array; 169 } else if (token == "%U") { 170 toReturn ~= urls; 171 } else if (token == "%i") { 172 if (iconName.length) { 173 toReturn ~= "--icon"; 174 toReturn ~= iconName; 175 } 176 } else { 177 static void expand(string token, ref string expanded, ref size_t restPos, ref size_t i, string insert) 178 { 179 if (token.length == 2) { 180 expanded = insert; 181 } else { 182 expanded ~= token[restPos..i] ~ insert; 183 } 184 restPos = i+2; 185 i++; 186 } 187 188 string expanded; 189 size_t restPos = 0; 190 bool ignore; 191 loop: for(size_t i=0; i<token.length; ++i) { 192 if (token[i] == '%' && i<token.length-1) { 193 switch(token[i+1]) { 194 case 'f': case 'u': 195 { 196 if (urls.length) { 197 string arg = urls.front; 198 if (token[i+1] == 'f') { 199 arg = urlToFilePath(arg); 200 } 201 expand(token, expanded, restPos, i, arg); 202 } else { 203 ignore = true; 204 break loop; 205 } 206 } 207 break; 208 case 'c': 209 { 210 expand(token, expanded, restPos, i, displayName); 211 } 212 break; 213 case 'k': 214 { 215 expand(token, expanded, restPos, i, fileName); 216 } 217 break; 218 case 'd': case 'D': case 'n': case 'N': case 'm': case 'v': 219 { 220 ignore = true; 221 break loop; 222 } 223 case '%': 224 { 225 expand(token, expanded, restPos, i, "%"); 226 } 227 break; 228 default: 229 { 230 throw new Exception("Unknown or misplaced field code: " ~ token); 231 } 232 } 233 } 234 } 235 236 if (!ignore) { 237 toReturn ~= expanded ~ token[restPos..$]; 238 } 239 } 240 } 241 242 return toReturn; 243 } 244 245 static bool isExecutable(string program) nothrow 246 { 247 import core.sys.posix.unistd; 248 return access(program.toStringz, X_OK) == 0; 249 } 250 251 static string findExecutable(string program, const(string)[] binPaths) nothrow 252 { 253 if (program.isAbsolute && isExecutable(program)) { 254 return program; 255 } else if (program.baseName == program) { 256 foreach(path; binPaths) { 257 auto candidate = buildPath(path, program); 258 if (isExecutable(candidate)) { 259 return candidate; 260 } 261 } 262 } 263 return null; 264 } 265 266 static void parseConfigFile(string fileName, string wantedGroup, bool delegate (string, string) onKeyValue) 267 { 268 auto r = iniLikeFileReader(fileName); 269 foreach(group; r.byGroup()) 270 { 271 if (group.groupName == wantedGroup) 272 { 273 foreach(entry; group.byEntry()) 274 { 275 if (entry.length && !isComment(entry)) { 276 auto pair = parseKeyValue(entry); 277 if (!isValidKey(pair.key)) { 278 return; 279 } 280 if (!onKeyValue(pair.key, pair.value.unescapeValue)) { 281 return; 282 } 283 } 284 } 285 return; 286 } 287 } 288 } 289 290 static string[] findFileManagerCommand(string app, const(string)[] appDirs, const(string)[] binPaths) nothrow 291 { 292 foreach(appDir; appDirs) { 293 bool fileExists; 294 auto appPath = buildPath(appDir, app); 295 collectException(appPath.isFile, fileExists); 296 if (!fileExists) { 297 //check if file in subdirectory exist. E.g. kde4-dolphin.desktop refers to kde4/dolphin.desktop 298 auto appSplitted = findSplit(app, "-"); 299 if (appSplitted[1].length && appSplitted[2].length) { 300 appPath = buildPath(appDir, appSplitted[0], appSplitted[2]); 301 collectException(appPath.isFile, fileExists); 302 } 303 } 304 305 if (fileExists) { 306 try { 307 bool canOpenDirectory; //not used for now. Some file managers does not have MimeType in their .desktop file. 308 string exec, tryExec, icon, displayName; 309 310 parseConfigFile(appPath, "Desktop Entry", delegate bool(string key, string value) { 311 if (key.equal("MimeType")) { 312 canOpenDirectory = value.splitter(';').canFind("inode/directory"); 313 } else if (key.equal("Exec")) { 314 exec = value; 315 } else if (key.equal("TryExec")) { 316 tryExec = value; 317 } else if (key.equal("Icon")) { 318 icon = value; 319 } else if (key.equal("Name")) { 320 displayName = value; 321 } 322 return true; 323 }); 324 325 if (exec.length) { 326 if (tryExec.length) { 327 auto program = findExecutable(tryExec, binPaths); 328 if (!program.length) { 329 continue; 330 } 331 } 332 return expandExecArgs(unquoteExec(exec), null, icon, displayName, appPath); 333 } 334 335 } catch(Exception e) { 336 337 } 338 } 339 } 340 341 return null; 342 } 343 344 static void execShowInFileManager(string[] fileManagerArgs, string toOpen) 345 { 346 toOpen = toOpen.absolutePath(); 347 switch(fileManagerArgs[0].baseName) { 348 //nautilus and nemo select item if it's a file 349 case "nautilus": 350 case "nemo": 351 fileManagerArgs ~= toOpen; 352 break; 353 //dolphin needs --select option 354 case "dolphin": 355 case "konqueror": 356 fileManagerArgs ~= ["--select", toOpen]; 357 break; 358 default: 359 { 360 bool pathIsDir; 361 collectException(toOpen.isDir, pathIsDir); 362 if (!pathIsDir) { 363 fileManagerArgs ~= toOpen.dirName; 364 } else { 365 fileManagerArgs ~= toOpen; 366 } 367 } 368 break; 369 } 370 371 File inFile, outFile, errFile; 372 try { 373 inFile = File("/dev/null", "rb"); 374 } catch(Exception) { 375 inFile = std.stdio.stdin; 376 } 377 try { 378 auto nullFile = File("/dev/null", "wb"); 379 outFile = nullFile; 380 errFile = nullFile; 381 } catch(Exception) { 382 outFile = std.stdio.stdout; 383 errFile = std.stdio.stderr; 384 } 385 386 auto processConfig = Config.none; 387 static if (is(typeof(Config.detached))) 388 { 389 processConfig |= Config.detached; 390 } 391 spawnProcess(fileManagerArgs, inFile, outFile, errFile, null, processConfig); 392 } 393 394 string configHome = xdgConfigHome(); 395 string appHome = xdgDataHome("applications"); 396 397 auto configDirs = xdgConfigDirs(); 398 auto appDirs = xdgDataDirs("applications"); 399 400 auto allAppDirs = xdgAllDataDirs("applications"); 401 auto binPaths = environment.get("PATH").splitter(':').filter!(p => p.length > 0).array; 402 403 string[] fileManagerArgs; 404 foreach(mimeappsList; chain(only(configHome), only(appHome), configDirs, appDirs).map!(p => buildPath(p, "mimeapps.list"))) { 405 try { 406 parseConfigFile(mimeappsList, "Default Applications", delegate bool(string key, string value) { 407 if (key.equal("inode/directory") && value.length) { 408 auto app = value; 409 fileManagerArgs = findFileManagerCommand(app, allAppDirs, binPaths); 410 return false; 411 } 412 return true; 413 }); 414 } catch(Exception e) { 415 416 } 417 418 if (fileManagerArgs.length) { 419 execShowInFileManager(fileManagerArgs, toOpen); 420 return true; 421 } 422 } 423 424 foreach(mimeinfoCache; allAppDirs.map!(p => buildPath(p, "mimeinfo.cache"))) { 425 try { 426 parseConfigFile(mimeinfoCache, "MIME Cache", delegate bool(string key, string value) { 427 if (key > "inode/directory") { //no need to proceed, since MIME types are sorted in alphabetical order. 428 return false; 429 } 430 if (key.equal("inode/directory") && value.length) { 431 auto alternatives = value.splitter(';').filter!(p => p.length > 0); 432 foreach(alternative; alternatives) { 433 fileManagerArgs = findFileManagerCommand(alternative, allAppDirs, binPaths); 434 if (fileManagerArgs.length) { 435 break; 436 } 437 } 438 return false; 439 } 440 return true; 441 }); 442 } catch(Exception e) { 443 444 } 445 446 if (fileManagerArgs.length) { 447 execShowInFileManager(fileManagerArgs, toOpen); 448 return true; 449 } 450 } 451 452 Log.e("showInFileManager -- could not find application to open directory"); 453 return false; 454 } else { 455 Log.w("showInFileManager is not implemented for current platform"); 456 } 457 } catch (Exception e) { 458 Log.e("showInFileManager -- exception while trying to open file browser"); 459 } 460 return false; 461 }