diff --git a/src/dlangui/core/filemanager.d b/src/dlangui/core/filemanager.d new file mode 100644 index 00000000..1ed684d5 --- /dev/null +++ b/src/dlangui/core/filemanager.d @@ -0,0 +1,505 @@ +module dlangui.core.filemanager; +import dlangui.core.logger; + +/** + * Show and select directory or file in OS file manager. + * + * On Windows this shows file in File Exporer. + * + * On macOS it reveals file in Finder. + * + * On Freedesktop systems this function finds user preferred program that used to open directories. + * If found file manager is known to this function, it uses file manager specific way to select file. + * Otherwise it fallbacks to opening $(D pathName) if it's directory or parent directory of $(D pathName) if it's file. + */ +@trusted bool showInFileManager(string pathName) { + import std.process; + import std.path; + import std.file; + Log.i("showInFileManager(", pathName, ")"); + string normalized = buildNormalizedPath(pathName); + if (!normalized.exists) { + Log.e("showInFileManager failed - file or directory does not exist"); + return false; + } + import std.string; + try { + version (Windows) { + import core.sys.windows.windows; + import dlangui.core.files; + import std.utf : toUTF16z; + + string explorerPath = findExecutablePath("explorer.exe"); + if (!explorerPath.length) { + Log.e("showInFileManager failed - cannot find explorer.exe"); + return false; + } + string arg = "/select,\"" ~ normalized ~ "\""; + STARTUPINFO si; + si.cb = si.sizeof; + PROCESS_INFORMATION pi; + Log.d("showInFileManager: ", explorerPath, " ", arg); + arg = "\"" ~ explorerPath ~ "\" " ~ arg; + auto res = CreateProcessW(null, //explorerPath.toUTF16z, + cast(wchar*)arg.toUTF16z, + null, null, false, DETACHED_PROCESS, + null, null, &si, &pi); + if (!res) { + Log.e("showInFileManager failed to run explorer.exe"); + return false; + } + return true; + } else version (OSX) { + string exe = "/usr/bin/osascript"; + string[] args; + args ~= exe; + args ~= "-e"; + args ~= "tell application \"Finder\" to reveal (POSIX file \"" ~ normalized ~ "\")"; + Log.d("Executing command: ", args); + auto pid = spawnProcess(args); + wait(pid); + args[2] = "tell application \"Finder\" to activate"; + Log.d("Executing command: ", args); + pid = spawnProcess(args); + wait(pid); + return true; + } else version(Android) { + Log.w("showInFileManager is not implemented for current platform"); + } else version(Posix) { + import std.stdio : File; + import std.algorithm : map, filter, splitter, find, canFind, equal, findSplit; + import std.ascii : isAlpha; + import std.exception : collectException, assumeUnique; + import std.path : buildPath, absolutePath, isAbsolute, dirName, baseName; + import std.range; + import std.string : toStringz; + import std.typecons : Tuple, tuple; + static import std.stdio; + + string toOpen = pathName; + + static inout(char)[] doUnescape(inout(char)[] value, in Tuple!(char, char)[] pairs) nothrow pure { + //little optimization to avoid unneeded allocations. + size_t i = 0; + for (; i < value.length; i++) { + if (value[i] == '\\') { + break; + } + } + if (i == value.length) { + return value; + } + + auto toReturn = appender!(typeof(value))(); + toReturn.put(value[0..i]); + + for (; i < value.length; i++) { + if (value[i] == '\\' && i+1 < value.length) { + const char c = value[i+1]; + auto t = pairs.find!"a[0] == b[0]"(tuple(c,c)); + if (!t.empty) { + toReturn.put(t.front[1]); + i++; + continue; + } + } + toReturn.put(value[i]); + } + return toReturn.data; + } + + static auto unescapeValue(string arg) nothrow pure + { + static immutable Tuple!(char, char)[] pairs = [ + tuple('s', ' '), + tuple('n', '\n'), + tuple('r', '\r'), + tuple('t', '\t'), + tuple('\\', '\\') + ]; + return doUnescape(arg, pairs); + } + + static string unescapeQuotedArgument(string value) nothrow pure + { + static immutable Tuple!(char, char)[] pairs = [ + tuple('`', '`'), + tuple('$', '$'), + tuple('"', '"'), + tuple('\\', '\\') + ]; + return doUnescape(value, pairs); + } + + static auto unquoteExec(string unescapedValue) pure + { + auto value = unescapedValue; + string[] result; + size_t i; + + static string parseQuotedPart(ref size_t i, char delimeter, string value) + { + size_t start = ++i; + bool inQuotes = true; + + while(i < value.length && inQuotes) { + if (value[i] == '\\' && value.length > i+1 && value[i+1] == '\\') { + i+=2; + continue; + } + + inQuotes = !(value[i] == delimeter && (value[i-1] != '\\' || (i>=2 && value[i-1] == '\\' && value[i-2] == '\\') )); + if (inQuotes) { + i++; + } + } + if (inQuotes) { + throw new Exception("Missing pair quote"); + } + return unescapeQuotedArgument(value[start..i]); + } + + char[] append; + bool wasInQuotes; + while(i < value.length) { + if (value[i] == ' ' || value[i] == '\t') { + if (!wasInQuotes && append.length >= 1 && append[$-1] == '\\') { + append[$-1] = value[i]; + } else { + if (append !is null) { + result ~= append.assumeUnique; + append = null; + } + } + wasInQuotes = false; + } else if (value[i] == '"' || value[i] == '\'') { + append ~= parseQuotedPart(i, value[i], value); + wasInQuotes = true; + } else { + append ~= value[i]; + wasInQuotes = false; + } + i++; + } + + if (append !is null) { + result ~= append.assumeUnique; + } + + return result; + } + + static string urlToFilePath(string url) nothrow pure + { + enum protocol = "file://"; + if (url.length > protocol.length && url[0..protocol.length] == protocol) { + return url[protocol.length..$]; + } else { + return url; + } + } + + static string[] expandExecArgs(in string[] unquotedArgs, in string[] urls = null, string iconName = null, string displayName = null, string fileName = null) pure + { + string[] toReturn; + foreach(token; unquotedArgs) { + if (token == "%F") { + toReturn ~= urls.map!(url => urlToFilePath(url)).array; + } else if (token == "%U") { + toReturn ~= urls; + } else if (token == "%i") { + if (iconName.length) { + toReturn ~= "--icon"; + toReturn ~= iconName; + } + } else { + static void expand(string token, ref string expanded, ref size_t restPos, ref size_t i, string insert) + { + if (token.length == 2) { + expanded = insert; + } else { + expanded ~= token[restPos..i] ~ insert; + } + restPos = i+2; + i++; + } + + string expanded; + size_t restPos = 0; + bool ignore; + loop: for(size_t i=0; i p.length > 0); + auto appDirs = environment.get("XDG_DATA_DIRS", "/usr/local/share:/usr/share").splitter(':').filter!(p => p.length > 0).map!(p => buildPath(p, "applications")); + + auto allAppDirs = chain(only(appHome), appDirs).array; + auto binPaths = environment.get("PATH").splitter(':').filter!(p => p.length > 0).array; + + string[] fileManagerArgs; + foreach(mimeappsList; chain(only(configHome), only(appHome), configDirs, appDirs).map!(p => buildPath(p, "mimeapps.list"))) { + try { + parseConfigFile(mimeappsList, "[Default Applications]", delegate bool(in char[] key, in char[] value) { + if (key.equal("inode/directory") && value.length) { + auto app = value.idup; + fileManagerArgs = findFileManagerCommand(app, allAppDirs, binPaths); + return false; + } + return true; + }); + } catch(Exception e) { + + } + + if (fileManagerArgs.length) { + execShowInFileManager(fileManagerArgs, toOpen); + return true; + } + } + + foreach(mimeinfoCache; allAppDirs.map!(p => buildPath(p, "mimeinfo.cache"))) { + try { + parseConfigFile(mimeinfoCache, "[MIME Cache]", delegate bool(in char[] key, in char[] value) { + if (key > "inode/directory") { //no need to proceed, since MIME types are sorted in alphabetical order. + return false; + } + if (key.equal("inode/directory") && value.length) { + auto alternatives = value.splitter(';').filter!(p => p.length > 0); + foreach(alternative; alternatives) { + fileManagerArgs = findFileManagerCommand(alternative.idup, allAppDirs, binPaths); + if (fileManagerArgs.length) { + break; + } + } + return false; + } + return true; + }); + } catch(Exception e) { + + } + + if (fileManagerArgs.length) { + execShowInFileManager(fileManagerArgs, toOpen); + return true; + } + } + + Log.e("showInFileManager -- could not find application to open directory"); + return false; + } else { + Log.w("showInFileManager is not implemented for current platform"); + } + } catch (Exception e) { + Log.e("showInFileManager -- exception while trying to open file browser"); + } + return false; +} diff --git a/src/dlangui/platforms/common/platform.d b/src/dlangui/platforms/common/platform.d index 4c25ce74..4158b408 100644 --- a/src/dlangui/platforms/common/platform.d +++ b/src/dlangui/platforms/common/platform.d @@ -1557,8 +1557,8 @@ class Platform { /// show directory or file in OS file manager (explorer, finder, etc...) bool showInFileManager(string pathName) { - Log.w("showInFileManager is not implemented for current platform"); - return false; + static import dlangui.core.filemanager; + return dlangui.core.filemanager.showInFileManager(pathName); } /// handle theme change: e.g. reload some themed resources diff --git a/src/dlangui/platforms/sdl/sdlapp.d b/src/dlangui/platforms/sdl/sdlapp.d index bce24a69..a7cc31ca 100644 --- a/src/dlangui/platforms/sdl/sdlapp.d +++ b/src/dlangui/platforms/sdl/sdlapp.d @@ -1424,430 +1424,6 @@ class SDLPlatform : Platform { string s = toUTF8(text); SDL_SetClipboardText(s.toStringz); } - - /// show directory or file in OS file manager (explorer, finder, etc...) - override bool showInFileManager(string pathName) { - import std.process; - import std.path; - import std.file; - string normalized = buildNormalizedPath(pathName); - if (!normalized.exists) { - Log.e("showInFileManager failed - file or directory does not exist"); - return false; - } - import std.string; - try { - version (Windows) { - Log.i("showInFileManager(", pathName, ")"); - import core.sys.windows.windows; - import dlangui.core.files; - - string explorerPath = findExecutablePath("explorer.exe"); - if (!explorerPath.length) { - Log.e("showInFileManager failed - cannot find explorer.exe"); - return false; - } - string arg = "/select,\"" ~ normalized ~ "\""; - STARTUPINFO si; - si.cb = si.sizeof; - PROCESS_INFORMATION pi; - Log.d("showInFileManager: ", explorerPath, " ", arg); - arg = "\"" ~ explorerPath ~ "\" " ~ arg; - auto res = CreateProcessW(null, //explorerPath.toUTF16z, - cast(wchar*)arg.toUTF16z, - null, null, false, DETACHED_PROCESS, - null, null, &si, &pi); - if (!res) { - Log.e("showInFileManager failed to run explorer.exe"); - return false; - } - return true; - } else version (OSX) { - string exe = "/usr/bin/osascript"; - string[] args; - args ~= exe; - args ~= "-e"; - args ~= "tell application \"Finder\" to reveal (POSIX file \"" ~ normalized ~ "\")"; - Log.d("Executing command: ", args); - auto pid = spawnProcess(args); - wait(pid); - args[2] = "tell application \"Finder\" to activate"; - Log.d("Executing command: ", args); - pid = spawnProcess(args); - wait(pid); - return true; - } else { - import std.stdio : File; - import std.algorithm : map, filter, splitter, find, canFind, equal, findSplit; - import std.ascii : isAlpha; - import std.exception : collectException; - import std.file : isDir, isFile; - import std.path : buildPath, absolutePath, isAbsolute, dirName, baseName; - import std.process : environment, spawnProcess; - import std.range; - import std.string : toStringz; - import std.typecons : Tuple, tuple; - - string toOpen = pathName; - - static inout(char)[] doUnescape(inout(char)[] value, in Tuple!(char, char)[] pairs) nothrow pure { - auto toReturn = appender!(typeof(value))(); - for (size_t i = 0; i < value.length; i++) { - if (value[i] == '\\') { - if (i < value.length - 1) { - char c = value[i+1]; - auto t = pairs.find!"a[0] == b[0]"(tuple(c,c)); - if (!t.empty) { - toReturn.put(t.front[1]); - i++; - continue; - } - } - } - toReturn.put(value[i]); - } - return toReturn.data; - } - - static auto unescapeValue(string arg) nothrow pure - { - static immutable Tuple!(char, char)[] pairs = [ - tuple('s', ' '), - tuple('n', '\n'), - tuple('r', '\r'), - tuple('t', '\t'), - tuple('\\', '\\') - ]; - return doUnescape(arg, pairs); - } - - static string unescapeQuotedArgument(string value) nothrow pure - { - static immutable Tuple!(char, char)[] pairs = [ - tuple('`', '`'), - tuple('$', '$'), - tuple('"', '"'), - tuple('\\', '\\') - ]; - return doUnescape(value, pairs); - } - - static string[] unquoteExecString(string value) pure - { - import std.uni : isWhite; - string[] result; - size_t i; - - while(i < value.length) { - if (isWhite(value[i])) { - i++; - } else if (value[i] == '"' || value[i] == '\'') { - char delimeter = value[i]; - size_t start = ++i; - bool inQuotes = true; - bool wasSlash; - - while(i < value.length) { - if (value[i] == '\\' && value.length > i+1 && value[i+1] == '\\') { - i+=2; - wasSlash = true; - continue; - } - - if (value[i] == delimeter && (value[i-1] != '\\' || (value[i-1] == '\\' && wasSlash) )) { - inQuotes = false; - break; - } - wasSlash = false; - i++; - } - if (inQuotes) { - throw new Exception("Missing pair quote"); - } - result ~= unescapeQuotedArgument(value[start..i]); - i++; - } else { - size_t start = i; - while(i < value.length && !isWhite(value[i])) { - i++; - } - result ~= value[start..i]; - } - } - - return result; - } - - static string[] parseExecString(string execString) pure - { - return unquoteExecString(execString).map!(unescapeValue).array; - } - - static string[] expandExecArgs(in string[] execArgs, in string[] urls = null, string iconName = null, string name = null, string fileName = null) pure - { - string[] toReturn; - foreach(token; execArgs) { - if (token == "%f") { - if (urls.length) { - toReturn ~= urls.front; - } - } else if (token == "%F") { - toReturn ~= urls; - } else if (token == "%u") { - if (urls.length) { - toReturn ~= urls.front; - } - } else if (token == "%U") { - toReturn ~= urls; - } else if (token == "%i") { - if (iconName.length) { - toReturn ~= "--icon"; - toReturn ~= iconName; - } - } else if (token == "%c") { - toReturn ~= name; - } else if (token == "%k") { - toReturn ~= fileName; - } else if (token == "%d" || token == "%D" || token == "%n" || token == "%N" || token == "%m" || token == "%v") { - continue; - } else { - if (token.length >= 2 && token[0] == '%') { - if (token[1] == '%') { - toReturn ~= token[1..$]; - } else { - throw new Exception("Unknown field code: " ~ token); - } - } else { - toReturn ~= token; - } - } - } - - return toReturn; - } - - static bool isExecutable(string program) nothrow - { - import core.sys.posix.unistd; - return access(program.toStringz, X_OK) == 0; - } - - static string findExecutable(string program, const(string)[] binPaths) nothrow - { - if (program.isAbsolute && isExecutable(program)) { - return program; - } else if (program.baseName == program) { - foreach(path; binPaths) { - auto candidate = buildPath(path, program); - if (isExecutable(candidate)) { - return candidate; - } - } - } - return null; - } - - static void parseConfigFile(string fileName, string wantedGroup, bool delegate (in char[], in char[]) onKeyValue) - { - bool inNeededGroup; - foreach(line; File(fileName).byLine()) { - if (!line.length || line[0] == '#') { - continue; - } else if (line[0] == '[') { - if (line.equal(wantedGroup)) { - inNeededGroup = true; - } else { - if (inNeededGroup) { - break; - } - inNeededGroup = false; - } - } else if (line[0].isAlpha) { - if (inNeededGroup) { - auto splitted = findSplit(line, "="); - if (splitted[1].length) { - auto key = splitted[0]; - auto value = splitted[2]; - if (!onKeyValue(key, value)) { - return; - } - } - } - } else { - //unexpected line content - break; - } - } - } - - static string[] findFileManagerCommand(string app, const(string)[] appDirs, const(string)[] binPaths) nothrow - { - foreach(appDir; appDirs) { - bool fileExists; - auto appPath = buildPath(appDir, app); - collectException(appPath.isFile, fileExists); - if (!fileExists) { - //check if file in subdirectory exist. E.g. kde4-dolphin.desktop refers to kde4/dolphin.desktop - auto appSplitted = findSplit(app, "-"); - if (appSplitted[1].length && appSplitted[2].length) { - appPath = buildPath(appDir, appSplitted[0], appSplitted[2]); - collectException(appPath.isFile, fileExists); - } - } - - if (fileExists) { - try { - bool canOpenDirectory; //not used for now. Some file managers does not have MimeType in their .desktop file. - string exec; - string tryExec; - - parseConfigFile(appPath, "[Desktop Entry]", delegate bool(in char[] key, in char[] value) { - if (key.equal("MimeType")) { - canOpenDirectory = value.splitter(';').canFind("inode/directory"); - } else if (key.equal("Exec")) { - exec = value.idup; - } else if (key.equal("TryExec")) { - tryExec = value.idup; - } - return true; - }); - - if (exec.length) { - if (tryExec.length) { - auto program = findExecutable(tryExec, binPaths); - if (!program.length) { - continue; - } - } - return expandExecArgs(parseExecString(exec)); - } - - } catch(Exception e) { - - } - } - } - - return null; - } - - static void execShowInFileManager(string[] fileManagerArgs, string toOpen) - { - toOpen = toOpen.absolutePath(); - switch(fileManagerArgs[0].baseName) { - //nautilus and nemo selects item if it's file - case "nautilus": - case "nemo": - fileManagerArgs ~= toOpen; - break; - //dolphin needs --select option - case "dolphin": - case "konqueror": - fileManagerArgs ~= ["--select", toOpen]; - break; - default: - { - bool pathIsDir; - collectException(toOpen.isDir, pathIsDir); - if (!pathIsDir) { - fileManagerArgs ~= toOpen.dirName; - } else { - fileManagerArgs ~= toOpen; - } - } - break; - } - - File devNullOut; - try { - devNullOut = File("/dev/null", "wb"); - } catch(Exception) { - devNullOut = std.stdio.stdout; - } - - File devNullErr; - try { - devNullErr = File("/dev/null", "wb"); - } catch(Exception) { - devNullErr = std.stdio.stderr; - } - - File devNullIn; - try { - devNullIn = File("/dev/null", "rb"); - } catch(Exception) { - devNullIn = std.stdio.stdin; - } - - spawnProcess(fileManagerArgs, devNullIn, devNullOut, devNullErr); - } - - string configHome = environment.get("XDG_CONFIG_HOME", buildPath(environment.get("HOME"), ".config")); - string appHome = environment.get("XDG_DATA_HOME", buildPath(environment.get("HOME"), ".local/share")).buildPath("applications"); - - auto configDirs = environment.get("XDG_CONFIG_DIRS", "/etc/xdg").splitter(':').find!(p => p.length > 0); - auto appDirs = environment.get("XDG_DATA_DIRS", "/usr/local/share:/usr/share").splitter(':').filter!(p => p.length > 0).map!(p => buildPath(p, "applications")); - - auto allAppDirs = chain(only(appHome), appDirs).array; - auto binPaths = environment.get("PATH").splitter(':').filter!(p => p.length > 0).array; - - string[] fileManagerArgs; - foreach(mimeappsList; chain(only(configHome), only(appHome), configDirs, appDirs).map!(p => buildPath(p, "mimeapps.list"))) { - try { - parseConfigFile(mimeappsList, "[Default Applications]", delegate bool(in char[] key, in char[] value) { - if (key.equal("inode/directory") && value.length) { - auto app = value.idup; - fileManagerArgs = findFileManagerCommand(app, allAppDirs, binPaths); - return false; - } - return true; - }); - } catch(Exception e) { - - } - - if (fileManagerArgs.length) { - execShowInFileManager(fileManagerArgs, toOpen); - return true; - } - } - - foreach(mimeinfoCache; allAppDirs.map!(p => buildPath(p, "mimeinfo.cache"))) { - try { - parseConfigFile(mimeinfoCache, "[MIME Cache]", delegate bool(in char[] key, in char[] value) { - if (key > "inode/directory") { //no need to proceed, since MIME types are sorted in alphabetical order. - return false; - } - if (key.equal("inode/directory") && value.length) { - auto alternatives = value.splitter(';').filter!(p => p.length > 0); - foreach(alternative; alternatives) { - fileManagerArgs = findFileManagerCommand(alternative.idup, allAppDirs, binPaths); - if (fileManagerArgs.length) { - break; - } - } - return false; - } - return true; - }); - } catch(Exception e) { - - } - - if (fileManagerArgs.length) { - execShowInFileManager(fileManagerArgs, toOpen); - return true; - } - } - - Log.e("showInFileManager -- could not find application to open directory"); - return false; - } - } catch (Exception e) { - Log.e("showInFileManager -- exception while trying to open file browser"); - } - return false; - } } version (Windows) { diff --git a/src/dlangui/platforms/windows/winapp.d b/src/dlangui/platforms/windows/winapp.d index 77f110fd..c5115477 100644 --- a/src/dlangui/platforms/windows/winapp.d +++ b/src/dlangui/platforms/windows/winapp.d @@ -428,7 +428,7 @@ class Win32Window : Window { } override void show() { if (!_mainWidget) { - Log.e("Window is shown without main widget"); + Log.e("Window is shown without main widget"); _mainWidget = new Widget(); } ReleaseCapture(); @@ -1093,37 +1093,6 @@ class Win32Platform : Platform { CloseClipboard(); } - - /// show directory or file in OS file manager (explorer, finder, etc...) - override bool showInFileManager(string pathName) { - Log.i("showInFileManager(", pathName, ")"); - import dlangui.core.files; - import std.path; - import std.string; - - string explorerPath = findExecutablePath("explorer.exe"); - if (!explorerPath.length) { - Log.e("showInFileManager failed - cannot find explorer.exe"); - return false; - } - string normalized = buildNormalizedPath(pathName); - string arg = "/select,\"" ~ normalized ~ "\""; - STARTUPINFO si; - si.cb = si.sizeof; - PROCESS_INFORMATION pi; - Log.d("showInFileManager: ", explorerPath, " ", arg); - arg = "\"" ~ explorerPath ~ "\" " ~ arg; - auto res = CreateProcessW(null, //explorerPath.toUTF16z, - cast(wchar*)arg.toUTF16z, - null, null, false, DETACHED_PROCESS, - null, null, &si, &pi); - if (!res) { - Log.e("showInFileManager failed to run explorer.exe"); - return false; - } - return true; - } - } extern(Windows)