diff --git a/README.md b/README.md index 66de989..0cbfd8c 100644 --- a/README.md +++ b/README.md @@ -123,9 +123,9 @@ catch (IniLikeException e) { //Parsing error - file is not desktop file or has e Utility that can parse, execute and rewrites .desktop files. -This will start vlc with the first parameter set to $HOME/Music: +This will open $HOME/.bashrc in geany text editor: - dub run :util -- exec /usr/share/applications/vlc.desktop $HOME/Music + dub run :util -- exec /usr/share/applications/geany.desktop $HOME/.bashrc This should start command line application in terminal emulator (will be detected automatically): @@ -137,7 +137,7 @@ Additional application actions are supported too: Open link with preferred application: - dub run :util -- link /usr/share/desktop-base/debian-homepage.desktop + dub run :util -- open /usr/share/desktop-base/debian-homepage.desktop Starts .desktop file defined executable or opens link: @@ -150,6 +150,11 @@ Parse and write .desktop file to new location (to testing purposes): Read basic information about desktop file: dub run :util -- read /usr/share/applications/kde4/kate.desktop + +When passing base name of desktop file instead of path it's treated like desktop file id and desktop file is searched in system applications paths. + + dub run :util -- exec python2.7.desktop + dub run :util -- exec kde4-kate.desktop ### [Desktop test](examples/test/source/app.d) diff --git a/dub.json b/dub.json index 6c752c2..e6bc271 100644 --- a/dub.json +++ b/dub.json @@ -6,7 +6,8 @@ "authors": ["Roman Chistokhodov"], "dependencies": { "inilike": "~>0.6.2", - "xdgpaths" : "~>0.2.1" + "xdgpaths" : "~>0.2.1", + "findexecutable" : "~>0.1.0" }, "targetName" : "desktopfile", "targetPath" : "lib", diff --git a/dub.selections.json b/dub.selections.json index 4db3fee..e06c862 100644 --- a/dub.selections.json +++ b/dub.selections.json @@ -3,6 +3,7 @@ "versions": { "isfreedesktop": "0.1.0", "inilike": "0.6.2", - "xdgpaths": "0.2.1" + "xdgpaths": "0.2.1", + "findexecutable": "0.1.0" } } \ No newline at end of file diff --git a/examples/shoot/dub.selections.json b/examples/shoot/dub.selections.json index 4db3fee..e06c862 100644 --- a/examples/shoot/dub.selections.json +++ b/examples/shoot/dub.selections.json @@ -3,6 +3,7 @@ "versions": { "isfreedesktop": "0.1.0", "inilike": "0.6.2", - "xdgpaths": "0.2.1" + "xdgpaths": "0.2.1", + "findexecutable": "0.1.0" } } \ No newline at end of file diff --git a/examples/test/dub.selections.json b/examples/test/dub.selections.json index 811e240..5960f4e 100644 --- a/examples/test/dub.selections.json +++ b/examples/test/dub.selections.json @@ -4,6 +4,7 @@ "standardpaths": "0.4.0", "isfreedesktop": "0.1.0", "inilike": "0.6.2", - "xdgpaths": "0.2.1" + "xdgpaths": "0.2.1", + "findexecutable": "0.1.0" } } \ No newline at end of file diff --git a/examples/test/source/app.d b/examples/test/source/app.d index b0be565..00a88b9 100644 --- a/examples/test/source/app.d +++ b/examples/test/source/app.d @@ -26,7 +26,7 @@ void main(string[] args) string[] dataPaths = standardPaths(StandardPath.data); - desktopDirs = applicationsPaths() ~ dataPaths.map!(s => buildPath(s, "desktop-directories")).array ~ dataPaths.map!(s => buildPath(s, "templates")).array ~ dataPaths.map!(s => buildPath(s, "autostart")).array ~ writablePath(StandardPath.desktop); + desktopDirs = applicationsPaths() ~ dataPaths.map!(s => buildPath(s, "desktop-directories")).array ~ dataPaths.map!(s => buildPath(s, "templates")).array ~ standardPaths(StandardPath.startup) ~ writablePath(StandardPath.desktop); } version(Windows) { diff --git a/examples/util/dub.selections.json b/examples/util/dub.selections.json index 4db3fee..e06c862 100644 --- a/examples/util/dub.selections.json +++ b/examples/util/dub.selections.json @@ -3,6 +3,7 @@ "versions": { "isfreedesktop": "0.1.0", "inilike": "0.6.2", - "xdgpaths": "0.2.1" + "xdgpaths": "0.2.1", + "findexecutable": "0.1.0" } } \ No newline at end of file diff --git a/examples/util/source/app.d b/examples/util/source/app.d index 72ab7d2..5f958d5 100644 --- a/examples/util/source/app.d +++ b/examples/util/source/app.d @@ -1,6 +1,7 @@ import std.stdio; import std.getopt; import std.process; +import std.path; import desktopfile.file; import isfreedesktop; @@ -18,7 +19,7 @@ import isfreedesktop; void main(string[] args) { if (args.length < 3) { - writefln("Usage: %s ", args[0]); + writefln("Usage: %s ", args[0]); return; } @@ -26,6 +27,14 @@ void main(string[] args) string inFile = args[2]; string locale = currentLocale(); + if (inFile == inFile.baseName && inFile.extension == ".desktop") { + inFile = findDesktopFile(inFile); + if (inFile is null) { + stderr.writeln("Could not find desktop file with such id: ", inFile); + return; + } + } + if (command == "read") { auto df = new DesktopFile(inFile); @@ -62,14 +71,14 @@ void main(string[] args) } } else { string[] urls = args[3..$]; - writeln("Exec:", df.expandExecString(urls, locale)); + writefln("Exec: %(%s %)", df.expandExecString(urls, locale)); df.startApplication(urls, locale); } - } else if (command == "link") { + } else if (command == "open") { auto df = new DesktopFile(inFile); - writeln("Link:", df.url()); + writeln("Link: ", df.url()); df.startLink(); } else if (command == "start") { auto df = new DesktopFile(inFile); diff --git a/source/desktopfile/file.d b/source/desktopfile/file.d index e0d1c16..fc85264 100644 --- a/source/desktopfile/file.d +++ b/source/desktopfile/file.d @@ -480,9 +480,9 @@ final class DesktopEntry : IniLikeGroup @nogc @safe bool terminal() const nothrow pure { return isTrue(value("Terminal")); } - /// Sets "Terminal" field to true or false. + ///setter @safe bool terminal(bool t) { - this["Terminal"] = t ? "true" : "false"; + this["Terminal"] = boolToString(t); return t; } @@ -498,7 +498,7 @@ final class DesktopEntry : IniLikeGroup * Sets the list of values for the "Categories" list. */ void categories(Range)(Range values) if (isInputRange!Range && isSomeString!(ElementType!Range)) { - this["Categories"] = DesktopFile.joinValues(values); + this["Categories"] = DesktopFile.joinValues(values).escapeIfNeeded(); } /** @@ -521,7 +521,7 @@ final class DesktopEntry : IniLikeGroup * Sets the list of values for the "Keywords" list. */ void keywords(Range)(Range values) if (isInputRange!Range && isSomeString!(ElementType!Range)) { - this["Keywords"] = DesktopFile.joinValues(values); + this["Keywords"] = DesktopFile.joinValues(values).escapeIfNeeded(); } /** @@ -536,7 +536,7 @@ final class DesktopEntry : IniLikeGroup * Sets the list of values for the "MimeType" list. */ void mimeTypes(Range)(Range values) if (isInputRange!Range && isSomeString!(ElementType!Range)) { - this["MimeType"] = DesktopFile.joinValues(values); + this["MimeType"] = DesktopFile.joinValues(values).escapeIfNeeded(); } /** @@ -553,7 +553,7 @@ final class DesktopEntry : IniLikeGroup * Sets the list of values for "Actions" list. */ void actions(Range)(Range values) if (isInputRange!Range && isSomeString!(ElementType!Range)) { - this["Actions"] = DesktopFile.joinValues(values); + this["Actions"] = DesktopFile.joinValues(values).escapeIfNeeded(); } /** @@ -566,7 +566,7 @@ final class DesktopEntry : IniLikeGroup ///setter void onlyShowIn(Range)(Range values) if (isInputRange!Range && isSomeString!(ElementType!Range)) { - this["OnlyShowIn"] = DesktopFile.joinValues(values); + this["OnlyShowIn"] = DesktopFile.joinValues(values).escapeIfNeeded(); } /** @@ -579,7 +579,7 @@ final class DesktopEntry : IniLikeGroup ///setter void notShowIn(Range)(Range values) if (isInputRange!Range && isSomeString!(ElementType!Range)) { - this["NotShowIn"] = DesktopFile.joinValues(values); + this["NotShowIn"] = DesktopFile.joinValues(values).escapeIfNeeded(); } protected: @@ -1126,7 +1126,6 @@ private: unittest { import std.file; - //Test DesktopFile string desktopFileContents = `[Desktop Entry] # Comment diff --git a/source/desktopfile/paths.d b/source/desktopfile/paths.d index 125a453..8428f53 100644 --- a/source/desktopfile/paths.d +++ b/source/desktopfile/paths.d @@ -62,8 +62,8 @@ static if (isFreedesktop) } /** - * Path where .desktop files can be stored without requiring of root privileges. - * This function is defined only on freedesktop systems to avoid confusion with other systems that have data paths not compatible with Desktop Entry Spec. + * Path where .desktop files can be stored by user. + * This function is defined only on freedesktop systems. * Note: it does not check if returned path exists and appears to be directory. */ @safe string writableApplicationsPath() nothrow { diff --git a/source/desktopfile/utils.d b/source/desktopfile/utils.d index 1ac11d3..e3c0048 100644 --- a/source/desktopfile/utils.d +++ b/source/desktopfile/utils.d @@ -170,42 +170,57 @@ unittest string[] result; size_t i; + static string parseQuotedPart(ref size_t i, char delimeter, string value) + { + 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 DesktopExecException("Missing pair quote"); + } + return value[start..i].unescapeQuotedArgument(); + } + + string append; + bool wasInQuotes; 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; + if (!wasInQuotes && append.length >= 2 && append[$-2] == '\\' && append[$-1] == '\\') { + append = append[0..$-2] ~ value[i]; + } else { + if (append !is null) { + result ~= append; + append = null; } - wasSlash = false; - i++; - } - if (inQuotes) { - throw new DesktopExecException("Missing pair quote"); } - result ~= value[start..i].unescapeQuotedArgument(); - i++; - + wasInQuotes = false; + } else if (value[i] == '"' || value[i] == '\'') { + append ~= parseQuotedPart(i, value[i], value); + wasInQuotes = true; } else { - size_t start = i; - while(i < value.length && !isWhite(value[i])) { - i++; - } - result ~= value[start..i]; + append ~= value[i]; + wasInQuotes = false; } + i++; + } + + if (append !is null) { + result ~= append; } return result; @@ -234,6 +249,12 @@ unittest assert(equal(unquoteExecString(`'quoted cmd' arg`), [`quoted cmd`, `arg`])); + assert(equal(unquoteExecString(`test\\ "one""two"\\ more\\ \\ test `), [`test onetwo more test`])); + + assert(equal(unquoteExecString(`env WINEPREFIX="/home/freeslave/.wine" wine C:\\\\windows\\\\command\\\\start.exe /Unix /home/freeslave/.wine/dosdevices/c:/windows/profiles/freeslave/Start\\ Menu/Programs/True\\ Remembrance/True\\ Remembrance.lnk`), [ + "env", "WINEPREFIX=/home/freeslave/.wine", "wine", `C:\\\\windows\\\\command\\\\start.exe`, "/Unix", "/home/freeslave/.wine/dosdevices/c:/windows/profiles/freeslave/Start Menu/Programs/True Remembrance/True Remembrance.lnk" + ])); + assertThrown!DesktopExecException(unquoteExecString(`cmd "quoted arg`)); } @@ -530,44 +551,35 @@ unittest * Detect command which will run program in terminal emulator. * On Freedesktop it looks for x-terminal-emulator first. If found ["/path/to/x-terminal-emulator", "-e"] is returned. * Otherwise it looks for xdg-terminal. If found ["/path/to/xdg-terminal"] is returned. + * Otherwise it tries to detect your desktop environment and find default terminal emulator for it. * If all guesses failed, it uses ["xterm", "-e"] as fallback. * Note: This function always returns empty array on non-freedesktop systems. */ string[] getTerminalCommand() nothrow @trusted { static if (isFreedesktop) { - static string checkExecutable(string filePath) nothrow { - import core.sys.posix.unistd; - try { - if (filePath.isFile && access(toStringz(filePath), X_OK) == 0) { - return buildNormalizedPath(filePath); - } else { + static string getDefaultTerminal() nothrow + { + string xdgCurrentDesktop; + collectException(environment.get("XDG_DESKTOP_SESSION"), xdgCurrentDesktop); + switch(xdgCurrentDesktop) { + case "GNOME": + case "X-Cinnamon": + return "gnome-terminal"; + case "LXDE": + return "lxterminal"; + case "XFCE": + return "xfce4-terminal"; + case "MATE": + return "mate-terminal"; + case "KDE": + return "konsole"; + default: return null; - } - } - catch(Exception e) { - return null; } } - static string findExecutable(string fileName) nothrow { - try { - foreach(string path; std.algorithm.splitter(environment.get("PATH"), ':')) { - if (path.empty) { - continue; - } - - string candidate = checkExecutable(buildPath(absolutePath(path), fileName)); - if (candidate.length) { - return candidate; - } - } - } catch (Exception e) { - - } - return null; - } - + import findexecutable; string term = findExecutable("x-terminal-emulator"); if (!term.empty) { return [term, "-e"]; @@ -576,6 +588,13 @@ string[] getTerminalCommand() nothrow @trusted if (!term.empty) { return [term]; } + term = getDefaultTerminal(); + if (!term.empty) { + term = findExecutable(term); + if (!term.empty) { + return [term, "-e"]; + } + } return ["xterm", "-e"]; } else { return null; @@ -884,16 +903,16 @@ unittest /** * See $(LINK2 http://standards.freedesktop.org/desktop-entry-spec/latest/ape.html, Desktop File ID) * Params: - * fileName = path of desktop file. - * appPaths = range of base application paths to check if this file belongs to one of them. + * fileName = Desktop file. + * appsPaths = Range of base application paths. * Returns: Desktop file ID or empty string if file does not have an ID. * See_Also: desktopfile.paths.applicationsPaths */ -string desktopId(Range)(string fileName, Range appPaths) nothrow if (isInputRange!Range && is(ElementType!Range : string)) +string desktopId(Range)(string fileName, Range appsPaths) if (isInputRange!Range && is(ElementType!Range : string)) { try { string absolute = fileName.absolutePath; - foreach (path; appPaths) { + foreach (path; appsPaths) { auto pathSplit = pathSplitter(path); auto fileSplit = pathSplitter(absolute); @@ -946,8 +965,8 @@ static if (isFreedesktop) * See $(LINK2 http://standards.freedesktop.org/desktop-entry-spec/latest/ape.html, Desktop File ID) * Returns: Desktop file ID or empty string if file does not have an ID. * Params: - * fileName = path of desktop file. - * Note: This function retrieves applications paths each time it's called and therefore can impact performance. To avoid this issue use overload with argument. + * fileName = Desktop file. + * Note: This function retrieves applications paths each time it's called and therefore can impact performance. To avoid this issue use the overload with argument. * See_Also: desktopfile.paths.applicationsPaths */ @trusted string desktopId(string fileName) nothrow @@ -957,6 +976,60 @@ static if (isFreedesktop) } } +/** + * Find desktop file by Desktop File ID. + * Desktop file ID can be ambiguous when it has hyphen symbol, so this function can try both variants. + * Params: + * desktopId = Desktop file ID. + * appsPaths = Range of base application paths. + * Returns: The first found existing desktop file, or null if could not find any. + * Note: This does not ensure that file is valid .desktop file. + * See_Also: desktopfile.paths.applicationsPaths + */ +string findDesktopFile(Range)(string desktopId, Range appsPaths) if (isInputRange!Range && is(ElementType!Range : string)) +{ + if (desktopId != desktopId.baseName) { + return null; + } + + foreach(appsPath; appsPaths) { + auto filePath = buildPath(appsPath, desktopId); + bool fileExists = filePath.exists; + if (!fileExists && filePath.canFind('-')) { + filePath = buildPath(appsPath, desktopId.replace("-", "/")); + fileExists = filePath.exists; + } + if (fileExists) { + return filePath; + } + } + return null; +} + +/// +unittest +{ + assert(findDesktopFile("not base/path.desktop", ["/usr/share/applications"]) is null); + assert(findDesktopFile("valid.desktop", (string[]).init) is null); +} + +static if (isFreedesktop) +{ + /** + * ditto + * Note: This function retrieves applications paths each time it's called and therefore can impact performance. To avoid this issue use the overload with argument. + * See_Also: desktopfile.paths.applicationsPaths + */ + @trusted string findDesktopFile(string desktopId) nothrow + { + import desktopfile.paths; + try { + return findDesktopFile(desktopId, applicationsPaths()); + } catch(Exception e) { + return null; + } + } +} /** * Check if .desktop file is trusted. This is not actually part of Desktop File Specification but many file managers has this concept.