diff --git a/.travis.yml b/.travis.yml index d78c73d..43b8fe4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,12 @@ language: d matrix: allow_failures: - - d: ldc-0.16.1 + - d: ldc include: - - d: dmd-2.069.2 + - d: dmd env: USE_DOVERALLS=true - - d: ldc-0.16.1 - + - d: ldc + script: - chmod +x travis-script.sh - ./travis-script.sh diff --git a/README.md b/README.md index ca0af07..f1ae258 100644 --- a/README.md +++ b/README.md @@ -51,32 +51,32 @@ string[] urls = ...; try { auto df = new DesktopFile(filePath); - + //Detect current locale. string locale = environment.get("LC_CTYPE", environment.get("LC_ALL", environment.get("LANG"))); - + string name = df.localizedDisplayName(locale); //Specific name of the application. string genericName = df.localizedGenericName(locale); //Generic name of the application. Show it in menu under the specific name. string comment = df.localizedComment(locale); //Show it as tooltip or description. - + string iconName = df.iconName(); //Freedesktop icon name. - + if (df.hidden()) { //User uninstalled desktop file and it should not be shown in menus. } - + string[] onlyShowIn = df.onlyShowIn().array; //If not empty, show this application only in listed desktop environments. string[] notShowIn = df.notShowIn().array; //Don't show this application in listed desktop environments. - + string[] mimeTypes = df.mimeTypes().array; //MIME types supported by application. string[] categories = df.categories().array; //Menu entries where this application should be shown. string[] keywords = df.keywords().array; //Keywords can be used to improve searching of the application. - + foreach(action; df.byAction()) { //Supported actions. string actionName = action.localizedDisplayName(locale); action.start(locale); } - + if (df.type() == DesktopFile.Type.Application) { //This is application string commandLine = df.execValue(); //Command line pattern used to start the application. @@ -84,24 +84,24 @@ try { df.startApplication(urls, locale); //Start application using given arguments and specified locale. It will be automatically started in terminal emulator if required. } catch(ProcessException e) { //Failed to start the application. - stderr.writeln(e.msg); + stderr.writeln(e.msg); } catch(DesktopExecException e) { //Malformed command line pattern. - stderr.writeln(e.msg); + stderr.writeln(e.msg); } } else if (df.type() == DesktopFile.Type.Link) { //This is link to file or web resource. string url = df.url(); //URL to open - + } else if (df.type() == DesktopFile.Type.Directory) { //This is directory or menu section description. } else { //Type is not defined or unknown, e.g. KDE Service. string type = df.value("Type"); //Retrieve value manually as string if you know how to deal with non-standard types. } -} +} catch (IniLikeException e) { //Parsing error - file is not desktop file or has errors. - stderr.writeln(e.msg); + stderr.writeln(e.msg); } ``` @@ -115,7 +115,7 @@ Utility that can parse, execute and rewrites .desktop files. This will open $HOME/.bashrc in geany text editor: dub run :util -- exec /usr/share/applications/geany.desktop dub.json - + This should start command line application in terminal emulator (will be detected automatically): dub run :util -- exec /usr/share/applications/python2.7.desktop @@ -123,11 +123,11 @@ This should start command line application in terminal emulator (will be detecte Additional application actions are supported too: dub run :util -- exec /usr/share/applications/steam.desktop --action=Settings - + Running of multiple application instances if it does not support handling multiple urls: dub run :util -- exec /usr/share/applications/leafpad.desktop dub.json README.md - + Open link with preferred application: dub run :util -- open /usr/share/desktop-base/debian-homepage.desktop @@ -135,7 +135,7 @@ Open link with preferred application: Starts .desktop file defined executable or opens link: dub run :util -- start /path/to/file.desktop - + Parse and write .desktop file to new location (for testing purposes): dub run :util -- write /usr/share/applications/vlc.desktop $HOME/Desktop/vlc.desktop @@ -143,7 +143,7 @@ Parse and write .desktop file to new location (for 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 @@ -158,7 +158,7 @@ Executing .desktop files with complicated Exec lines: dub run :util -- exec "$HOME/.local/share/applications/wine/Programs/True Remembrance/True Remembrance.desktop" # launcher that was generated by wine dub run :util -- exec $HOME/TorBrowser/tor-browser_en-US/start-tor-browser.desktop # Tor browser launcher - + ### [Desktop test](examples/test/source/app.d) Parses all .desktop files in system's applications paths (usually /usr/local/share/applicatons and /usr/share/applications) and on the user's Desktop. @@ -174,12 +174,12 @@ To print all directories examined by desktoptest to stdout, add --verbose flag: Start desktoptest on specified directories: dub run :test -- /path/to/applications /anotherpath/to/applications - + Example using cmd on Windows (KDE installed): set KDE_SHARE="%SYSTEMDRIVE%\ProgramData\KDE\share" dub run :test -- %KDE_SHARE%\applications %KDE_SHARE%\templates %KDE_SHARE%\desktop-directories %KDE_SHARE%\autostart - + ### [Shoot desktop file](examples/shoot/source/app.d) Uses the alternative way of starting desktop file. Instead of constructing DesktopFile object it just starts the application or opens link after read enough information from file. @@ -187,7 +187,7 @@ Uses the alternative way of starting desktop file. Instead of constructing Deskt dub run :shoot -- vlc.desktop dub run :shoot -- python2.7.desktop dub run :shoot -- geany.desktop dub.json - + Running of multiple application instances if it does not support handling multiple urls: dub run :shoot -- leafpad.desktop dub.json README.md diff --git a/dub.json b/dub.json index 65749c2..1802edb 100644 --- a/dub.json +++ b/dub.json @@ -5,13 +5,13 @@ "copyright": "Copyright © 2015-2016, Roman Chistokhodov", "authors": ["Roman Chistokhodov"], "dependencies": { - "inilike": "~>1.0.0", - "xdgpaths" : "~>0.2.2", - "detached" : "~>0.1.1" + "inilike": "~>1.0.3", + "xdgpaths" : "~>0.2.4", + "detached" : "~>0.1.7" }, "targetName" : "desktopfile", - "targetPath" : "lib", + "targetPath" : "lib", "targetType" : "library", - + "versions" : ["desktopfileFileTest"], "subPackages" : ["./examples/test", "./examples/util", "./examples/shoot"] } diff --git a/dub.selections.json b/dub.selections.json index 33030a5..d0f22f5 100644 --- a/dub.selections.json +++ b/dub.selections.json @@ -1,10 +1,10 @@ { "fileVersion": 1, "versions": { - "detached": "0.1.1", - "findexecutable": "0.1.0", - "inilike": "1.0.0", - "isfreedesktop": "0.1.0", - "xdgpaths": "0.2.2" + "detached": "0.1.7", + "findexecutable": "0.1.1", + "inilike": "1.0.3", + "isfreedesktop": "0.1.1", + "xdgpaths": "0.2.4" } } diff --git a/examples/shoot/dub.selections.json b/examples/shoot/dub.selections.json index 5b6c7da..d0f22f5 100644 --- a/examples/shoot/dub.selections.json +++ b/examples/shoot/dub.selections.json @@ -1,10 +1,10 @@ { "fileVersion": 1, "versions": { - "detached": "0.1.1", - "findexecutable" : "0.1.0", - "inilike": "1.0.0", - "isfreedesktop": "0.1.0", - "xdgpaths": "0.2.2" + "detached": "0.1.7", + "findexecutable": "0.1.1", + "inilike": "1.0.3", + "isfreedesktop": "0.1.1", + "xdgpaths": "0.2.4" } } diff --git a/examples/shoot/source/app.d b/examples/shoot/source/app.d index 9c39768..f995a8b 100644 --- a/examples/shoot/source/app.d +++ b/examples/shoot/source/app.d @@ -8,14 +8,14 @@ int main(string[] args) bool onlyExec; bool notFollow; string[] appPaths; - + getopt( - args, + args, "onlyExec", "Only start applications, don't open links", &onlyExec, "notFollow", "Don't follow desktop files", ¬Follow, "appPath", "Path of applications directory", &appPaths); - - + + string inFile; if (args.length > 1) { inFile = args[1]; @@ -23,7 +23,7 @@ int main(string[] args) stderr.writeln("Must provide path to desktop file"); return 1; } - + if (appPaths.length == 0) { static if (isFreedesktop) { import desktopfile.paths; @@ -37,11 +37,11 @@ int main(string[] args) appPaths = [kdeAppDir]; } } catch(Exception e) { - + } } } - + if (inFile == inFile.baseName && inFile.extension == ".desktop") { string desktopId = inFile; inFile = findDesktopFile(desktopId, appPaths); @@ -50,19 +50,19 @@ int main(string[] args) return 1; } } - + ShootOptions options; - + options.urls = args[2..$]; - + if (onlyExec) { options.flags = options.flags & ~ShootOptions.Link; } - + if (notFollow) { options.flags = options.flags & ~ ShootOptions.FollowLink; } - + try { shootDesktopFile(inFile, options); } @@ -70,6 +70,6 @@ int main(string[] args) stderr.writeln(e.msg); return 1; } - + return 0; } diff --git a/examples/test/dub.json b/examples/test/dub.json index 57a2100..ed24f83 100644 --- a/examples/test/dub.json +++ b/examples/test/dub.json @@ -5,7 +5,7 @@ "authors": ["freeslave"], "dependencies": { "desktopfile" : "*", - "standardpaths": "~>0.6.0" + "standardpaths": "~>0.8.0" }, "targetPath" : "bin", "targetType" : "executable" diff --git a/examples/test/dub.selections.json b/examples/test/dub.selections.json index 7449443..653c73e 100644 --- a/examples/test/dub.selections.json +++ b/examples/test/dub.selections.json @@ -1,11 +1,11 @@ { "fileVersion": 1, "versions": { - "detached": "0.1.1", - "findexecutable": "0.1.0", - "inilike": "1.0.0", - "isfreedesktop": "0.1.0", - "standardpaths": "0.7.0", - "xdgpaths": "0.2.2" + "detached": "0.1.7", + "findexecutable": "0.1.1", + "inilike": "1.0.3", + "isfreedesktop": "0.1.1", + "standardpaths": "0.8.0", + "xdgpaths": "0.2.4" } } diff --git a/examples/test/source/app.d b/examples/test/source/app.d index 43ede26..280e59f 100644 --- a/examples/test/source/app.d +++ b/examples/test/source/app.d @@ -13,22 +13,22 @@ import isfreedesktop; void main(string[] args) { string[] desktopDirs; - + bool verbose; - + getopt(args, "verbose", "Print name of each examined desktop file to standard output", &verbose); - + if (args.length > 1) { desktopDirs = args[1..$]; } else { static if (isFreedesktop) { import standardpaths; - + string[] dataPaths = standardPaths(StandardPath.data); - + 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) { try { auto root = environment.get("SYSTEMDRIVE", "C:"); @@ -37,17 +37,17 @@ void main(string[] args) desktopDirs = [buildPath(kdeDir, `applications`), buildPath(kdeDir, `desktop-directories`), buildPath(kdeDir, `templates`), buildPath(kdeDir, `autostart`)]; } } catch(Exception e) { - + } } } - + if (!desktopDirs.length) { stderr.writeln("No desktop directories given nor could be detected"); stderr.writefln("Usage: %s [DIRECTORY]...", args[0]); return; } - + writefln("Using directories: %-(%s, %)", desktopDirs); foreach(dir; desktopDirs.filter!(s => s.exists && s.isDir())) { diff --git a/examples/util/dub.selections.json b/examples/util/dub.selections.json index 5b6c7da..d0f22f5 100644 --- a/examples/util/dub.selections.json +++ b/examples/util/dub.selections.json @@ -1,10 +1,10 @@ { "fileVersion": 1, "versions": { - "detached": "0.1.1", - "findexecutable" : "0.1.0", - "inilike": "1.0.0", - "isfreedesktop": "0.1.0", - "xdgpaths": "0.2.2" + "detached": "0.1.7", + "findexecutable": "0.1.1", + "inilike": "1.0.3", + "isfreedesktop": "0.1.1", + "xdgpaths": "0.2.4" } } diff --git a/examples/util/source/app.d b/examples/util/source/app.d index 6b1d992..70b1896 100644 --- a/examples/util/source/app.d +++ b/examples/util/source/app.d @@ -21,20 +21,20 @@ void main(string[] args) { string action; string[] appPaths; - getopt(args, + getopt(args, "action", "Action to run", &action, "appPath", "Path of applications directory", &appPaths ); - + if (args.length < 3) { writefln("Usage: %s ", args[0]); return; } - + string command = args[1]; string inFile = args[2]; string locale = currentLocale(); - + if (appPaths.length == 0) { static if (isFreedesktop) { import desktopfile.paths; @@ -48,11 +48,11 @@ void main(string[] args) appPaths = [kdeAppDir]; } } catch(Exception e) { - + } } } - + if (inFile == inFile.baseName && inFile.extension == ".desktop") { string desktopId = inFile; inFile = findDesktopFile(desktopId, appPaths); @@ -61,10 +61,10 @@ void main(string[] args) return; } } - + if (command == "read") { auto df = new DesktopFile(inFile); - + writefln("Name: %s. Localized: %s", df.displayName(), df.localizedDisplayName(locale)); writefln("GenericName: %s. Localized: %s", df.genericName(), df.localizedGenericName(locale)); writefln("Comment: %s. Localized: %s", df.comment(), df.localizedComment(locale)); @@ -76,7 +76,7 @@ void main(string[] args) writefln("Actions: %(%s %)", df.actions()); writefln("Categories: %(%s %)", df.categories()); writefln("MimeTypes: %(%s %)", df.mimeTypes()); - + if (df.type() == DesktopFile.Type.Application) { writeln("Exec: ", df.execValue()); writeln("In terminal: ", df.terminal()); diff --git a/source/desktopfile/file.d b/source/desktopfile/file.d index 7259f08..d541e3d 100644 --- a/source/desktopfile/file.d +++ b/source/desktopfile/file.d @@ -1,12 +1,12 @@ /** * Class representation of desktop file. - * Authors: + * Authors: * $(LINK2 https://github.com/FreeSlave, Roman Chistokhodov) * Copyright: * Roman Chistokhodov, 2015-2016 - * License: + * License: * $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0). - * See_Also: + * See_Also: * $(LINK2 https://www.freedesktop.org/wiki/Specifications/desktop-entry-spec/, Desktop Entry Specification) */ @@ -34,7 +34,7 @@ public: package @nogc @safe this(string groupName) nothrow { super(groupName); } - + /** * Label that will be shown to the user. * Returns: The value associated with "Name" key. @@ -42,7 +42,7 @@ public: @safe string displayName() const nothrow pure { return readEntry("Name"); } - + /** * Label that will be shown to the user in given locale. * Returns: The value associated with "Name" key and given locale. @@ -51,7 +51,7 @@ public: @safe string localizedDisplayName(string locale) const nothrow pure { return readEntry("Name", locale); } - + /** * Icon name of action. * Returns: The value associated with "Icon" key. @@ -59,7 +59,7 @@ public: @safe string iconName() const nothrow pure { return readEntry("Icon"); } - + /** * Returns: Localized icon name * See_Also: $(D iconName) @@ -67,14 +67,14 @@ public: @safe string localizedIconName(string locale) const nothrow pure { return readEntry("Icon", locale); } - + /** * Returns: The value associated with "Exec" key. */ @safe string execValue() const nothrow pure { return readEntry("Exec"); } - + /** * Start this action. * Throws: @@ -84,11 +84,11 @@ public: */ @safe void start(string locale = null) const { auto unquotedArgs = unquoteExec(execValue()); - + SpawnParams params; params.iconName = localizedIconName(locale); params.displayName = localizedDisplayName(locale); - + return spawnApplication(unquotedArgs, params); } } @@ -106,11 +106,11 @@ final class DesktopEntry : IniLikeGroup Link, ///Desktop describes URL Directory ///Desktop entry describes directory settings } - + protected @nogc @safe this() nothrow { super("Desktop Entry"); } - + /** * Type of desktop entry. * Returns: Type of desktop entry. @@ -128,21 +128,21 @@ final class DesktopEntry : IniLikeGroup } return Type.Unknown; } - + /// unittest { string contents = "[Desktop Entry]\nType=Application"; auto desktopFile = new DesktopFile(iniLikeStringReader(contents)); assert(desktopFile.type == Type.Application); - + desktopFile.desktopEntry["Type"] = "Link"; assert(desktopFile.type == Type.Link); - + desktopFile.desktopEntry["Type"] = "Directory"; assert(desktopFile.type == Type.Directory); } - + /** * Sets "Type" field to type * Note: Setting the $(D Type.Unknown) removes type field. @@ -164,7 +164,7 @@ final class DesktopEntry : IniLikeGroup } return t; } - + /// unittest { @@ -175,11 +175,11 @@ final class DesktopEntry : IniLikeGroup assert(desktopFile.desktopEntry.value("Type") == "Link"); desktopFile.type = Type.Directory; assert(desktopFile.desktopEntry.value("Type") == "Directory"); - + desktopFile.type = Type.Unknown; assert(desktopFile.desktopEntry.value("Type").empty); } - + /** * Specific name of the application, for example "Qupzilla". * Returns: The value associated with "Name" key. @@ -188,14 +188,14 @@ final class DesktopEntry : IniLikeGroup @safe string displayName() const nothrow pure { return readEntry("Name"); } - + /** * Set "Name" to name escaping the value if needed. */ @safe string displayName(string name) { return writeEntry("Name", name); } - + /** * Returns: Localized name. * See_Also: $(D displayName) @@ -203,7 +203,7 @@ final class DesktopEntry : IniLikeGroup @safe string localizedDisplayName(string locale) const nothrow pure { return readEntry("Name", locale); } - + /** * Generic name of the application, for example "Web Browser". * Returns: The value associated with "GenericName" key. @@ -212,7 +212,7 @@ final class DesktopEntry : IniLikeGroup @safe string genericName() const nothrow pure { return readEntry("GenericName"); } - + /** * Set "GenericName" to name escaping the value if needed. */ @@ -226,7 +226,7 @@ final class DesktopEntry : IniLikeGroup @safe string localizedGenericName(string locale) const nothrow pure { return readEntry("GenericName", locale); } - + /** * Tooltip for the entry, for example "View sites on the Internet". * Returns: The value associated with "Comment" key. @@ -235,14 +235,14 @@ final class DesktopEntry : IniLikeGroup @safe string comment() const nothrow pure { return readEntry("Comment"); } - + /** * Set "Comment" to commentary escaping the value if needed. */ @safe string comment(string commentary) { return writeEntry("Comment", commentary); } - + /** * Returns: Localized comment * See_Also: $(D comment) @@ -250,8 +250,8 @@ final class DesktopEntry : IniLikeGroup @safe string localizedComment(string locale) const nothrow pure { return readEntry("Comment", locale); } - - /** + + /** * Exec value of desktop file. * Returns: the value associated with "Exec" key. * See_Also: $(D expandExecValue), $(D startApplication), $(D tryExecValue) @@ -259,7 +259,7 @@ final class DesktopEntry : IniLikeGroup @safe string execValue() const nothrow pure { return readEntry("Exec"); } - + /** * Set "Exec" to exec escaping the value if needed. * See_Also: $(D desktopfile.utils.ExecBuilder). @@ -267,7 +267,7 @@ final class DesktopEntry : IniLikeGroup @safe string execValue(string exec) { return writeEntry("Exec", exec); } - + /** * URL to access. * Returns: The value associated with "URL" key. @@ -275,24 +275,24 @@ final class DesktopEntry : IniLikeGroup @safe string url() const nothrow pure { return readEntry("URL"); } - + /** * Set "URL" to link escaping the value if needed. */ @safe string url(string link) { return writeEntry("URL", link); } - + /// unittest { auto df = new DesktopFile(iniLikeStringReader("[Desktop Entry]\nType=Link\nURL=https://github.com/")); assert(df.url() == "https://github.com/"); } - + /** - * Value used to determine if the program is actually installed. - * + * Value used to determine if the program is actually installed. + * * If the path is not an absolute path, the file should be looked up in the $(B PATH) environment variable. If the file is not present or if it is not executable, the entry may be ignored (not be used in menus, for example). * Returns: The value associated with "TryExec" key. * See_Also: $(D execValue) @@ -300,7 +300,7 @@ final class DesktopEntry : IniLikeGroup @safe string tryExecValue() const nothrow pure { return readEntry("TryExec"); } - + /** * Set TryExec value escaping it if needed. * Throws: @@ -312,7 +312,7 @@ final class DesktopEntry : IniLikeGroup } return writeEntry("TryExec", tryExec); } - + /// unittest { @@ -324,18 +324,18 @@ final class DesktopEntry : IniLikeGroup assertThrown(df.tryExecValue = "not/absolute"); assertThrown(df.tryExecValue = "./relative"); } - + /** * Icon to display in file manager, menus, etc. * Returns: The value associated with "Icon" key. - * Note: This function returns Icon as it's defined in .desktop file. + * Note: This function returns Icon as it's defined in .desktop file. * It does not provide any lookup of actual icon file on the system if the name if not an absolute path. * To find the path to icon file refer to $(LINK2 http://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html, Icon Theme Specification) or consider using $(LINK2 https://github.com/FreeSlave/icontheme, icontheme library). */ @safe string iconName() const nothrow pure { return readEntry("Icon"); } - + /** * Set Icon value. * Throws: @@ -347,7 +347,7 @@ final class DesktopEntry : IniLikeGroup } return writeEntry("Icon", icon); } - + /// unittest { @@ -359,7 +359,7 @@ final class DesktopEntry : IniLikeGroup assertThrown(df.iconName = "not/absolute"); assertThrown(df.iconName = "./relative"); } - + /** * Returns: Localized icon name * See_Also: $(D iconName) @@ -367,7 +367,7 @@ final class DesktopEntry : IniLikeGroup @safe string localizedIconName(string locale) const nothrow pure { return readEntry("Icon", locale); } - + /** * NoDisplay means "this application exists, but don't display it in the menus". * Returns: The value associated with "NoDisplay" key converted to bool using isTrue. @@ -375,28 +375,28 @@ final class DesktopEntry : IniLikeGroup @nogc @safe bool noDisplay() const nothrow pure { return isTrue(value("NoDisplay")); } - + ///setter @safe bool noDisplay(bool notDisplay) { this["NoDisplay"] = boolToString(notDisplay); return notDisplay; } - + /** - * Hidden means the user deleted (at his level) something that was present (at an upper level, e.g. in the system dirs). - * It's strictly equivalent to the .desktop file not existing at all, as far as that user is concerned. + * Hidden means the user deleted (at his level) something that was present (at an upper level, e.g. in the system dirs). + * It's strictly equivalent to the .desktop file not existing at all, as far as that user is concerned. * Returns: The value associated with "Hidden" key converted to bool using isTrue. */ @nogc @safe bool hidden() const nothrow pure { return isTrue(value("Hidden")); } - + ///setter @safe bool hidden(bool hide) { this["Hidden"] = boolToString(hide); return hide; } - + /** * A boolean value specifying if D-Bus activation is supported for this application. * Returns: The value associated with "DBusActivable" key converted to bool using isTrue. @@ -404,13 +404,13 @@ final class DesktopEntry : IniLikeGroup @nogc @safe bool dbusActivable() const nothrow pure { return isTrue(value("DBusActivatable")); } - + ///setter @safe bool dbusActivable(bool activable) { this["DBusActivatable"] = boolToString(activable); return activable; } - + /** * A boolean value specifying if an application uses Startup Notification Protocol. * Returns: The value associated with "StartupNotify" key converted to bool using isTrue. @@ -418,13 +418,13 @@ final class DesktopEntry : IniLikeGroup @nogc @safe bool startupNotify() const nothrow pure { return isTrue(value("StartupNotify")); } - + ///setter @safe bool startupNotify(bool notify) { this["StartupNotify"] = boolToString(notify); return notify; } - + /** * The working directory to run the program in. * Returns: The value associated with "Path" key. @@ -432,7 +432,7 @@ final class DesktopEntry : IniLikeGroup @safe string workingDirectory() const nothrow pure { return readEntry("Path"); } - + /** * Set Path value. * Throws: @@ -449,7 +449,7 @@ final class DesktopEntry : IniLikeGroup } return writeEntry("Path", wd); } - + /// unittest { @@ -460,7 +460,7 @@ final class DesktopEntry : IniLikeGroup } assertThrown(df.workingDirectory = "/foo\0/bar"); } - + /** * Whether the program runs in a terminal window. * Returns: The value associated with "Terminal" key converted to bool using isTrue. @@ -473,7 +473,7 @@ final class DesktopEntry : IniLikeGroup this["Terminal"] = boolToString(t); return t; } - + /** * Categories this program belongs to. * Returns: The range of multiple values associated with "Categories" key. @@ -481,14 +481,14 @@ final class DesktopEntry : IniLikeGroup @safe auto categories() const nothrow pure { return DesktopFile.splitValues(readEntry("Categories")); } - + /** * Sets the list of values for the "Categories" list. */ string categories(Range)(Range values) if (isInputRange!Range && isSomeString!(ElementType!Range)) { return writeEntry("Categories", DesktopFile.joinValues(values)); } - + /** * A list of strings which may be used in addition to other metadata to describe this entry. * Returns: The range of multiple values associated with "Keywords" key. @@ -496,7 +496,7 @@ final class DesktopEntry : IniLikeGroup @safe auto keywords() const nothrow pure { return DesktopFile.splitValues(readEntry("Keywords")); } - + /** * A list of localied strings which may be used in addition to other metadata to describe this entry. * Returns: The range of multiple values associated with "Keywords" key in given locale. @@ -504,14 +504,14 @@ final class DesktopEntry : IniLikeGroup @safe auto localizedKeywords(string locale) const nothrow pure { return DesktopFile.splitValues(readEntry("Keywords", locale)); } - + /** * Sets the list of values for the "Keywords" list. */ string keywords(Range)(Range values) if (isInputRange!Range && isSomeString!(ElementType!Range)) { return writeEntry("Keywords", DesktopFile.joinValues(values)); } - + /** * The MIME type(s) supported by this application. * Returns: The range of multiple values associated with "MimeType" key. @@ -519,14 +519,14 @@ final class DesktopEntry : IniLikeGroup @safe auto mimeTypes() nothrow const pure { return DesktopFile.splitValues(readEntry("MimeType")); } - + /** * Sets the list of values for the "MimeType" list. */ string mimeTypes(Range)(Range values) if (isInputRange!Range && isSomeString!(ElementType!Range)) { return writeEntry("MimeType", DesktopFile.joinValues(values)); } - + /** * Actions supported by application. * Returns: Range of multiple values associated with "Actions" key. @@ -536,14 +536,14 @@ final class DesktopEntry : IniLikeGroup @safe auto actions() nothrow const pure { return DesktopFile.splitValues(readEntry("Actions")); } - + /** * Sets the list of values for "Actions" list. */ string actions(Range)(Range values) if (isInputRange!Range && isSomeString!(ElementType!Range)) { return writeEntry("Actions", DesktopFile.joinValues(values)); } - + /** * A list of strings identifying the desktop environments that should display a given desktop entry. * Returns: The range of multiple values associated with "OnlyShowIn" key. @@ -552,12 +552,12 @@ final class DesktopEntry : IniLikeGroup @safe auto onlyShowIn() nothrow const pure { return DesktopFile.splitValues(readEntry("OnlyShowIn")); } - + ///setter string onlyShowIn(Range)(Range values) if (isInputRange!Range && isSomeString!(ElementType!Range)) { return writeEntry("OnlyShowIn", DesktopFile.joinValues(values)); } - + /** * A list of strings identifying the desktop environments that should not display a given desktop entry. * Returns: The range of multiple values associated with "NotShowIn" key. @@ -566,12 +566,12 @@ final class DesktopEntry : IniLikeGroup @safe auto notShowIn() nothrow const pure { return DesktopFile.splitValues(readEntry("NotShowIn")); } - + ///setter string notShowIn(Range)(Range values) if (isInputRange!Range && isSomeString!(ElementType!Range)) { return writeEntry("NotShowIn", DesktopFile.joinValues(values)); } - + /** * Check if desktop file should be shown in menu of specific desktop environment. * Params: @@ -586,7 +586,7 @@ final class DesktopEntry : IniLikeGroup auto onlyIn = onlyShowIn(); return onlyIn.empty || onlyIn.canFind(desktopEnvironment); } - + /// unittest { @@ -604,7 +604,7 @@ final class DesktopEntry : IniLikeGroup assert(!df.showIn("KDE")); assert(!df.showIn("MATE")); } - + protected: @trusted override void validateKey(string key, string value) const { validateDesktopKeyImpl(groupName(), key, value); @@ -621,7 +621,7 @@ public: * Alias for backward compatibility. */ alias DesktopEntry.Type Type; - + /** * Policy about reading Desktop Action groups. */ @@ -629,7 +629,7 @@ public: skip, ///Don't save Desktop Action groups. preserve ///Save Desktop Action groups. } - + /** * Policy about reading extension groups (those start with 'X-'). */ @@ -637,7 +637,7 @@ public: skip, ///Don't save extension groups. preserve ///Save extension groups. } - + /** * Policy about reading groups with names which meaning is unknown, i.e. it's not extension nor Desktop Action. */ @@ -646,34 +646,34 @@ public: preserve, ///Save unknown groups. throwError ///Throw error when unknown group is encountered. } - + ///Options to manage desktop file reading static struct DesktopReadOptions { ///Base $(D inilike.file.IniLikeFile.ReadOptions). IniLikeFile.ReadOptions baseOptions; - + alias baseOptions this; - + /** * Set policy about unknown groups. By default they are skipped without errors. * Note that all groups still need to be preserved if desktop file must be rewritten. */ UnknownGroupPolicy unknownGroupPolicy = UnknownGroupPolicy.skip; - + /** - * Set policy about extension groups. By default they are all preserved. + * Set policy about extension groups. By default they are all preserved. * Set it to skip if you're not willing to support any extensions in your applications. * Note that all groups still need to be preserved if desktop file must be rewritten. */ ExtensionGroupPolicy extensionGroupPolicy = ExtensionGroupPolicy.preserve; - + /** - * Set policy about desktop action groups. By default they are all preserved. + * Set policy about desktop action groups. By default they are all preserved. * Note that all groups still need to be preserved if desktop file must be rewritten. */ ActionGroupPolicy actionGroupPolicy = ActionGroupPolicy.preserve; - + ///Setting parameters in any order, leaving not mentioned ones in default state. @nogc @safe this(Args...)(Args args) nothrow pure { foreach(arg; args) { @@ -691,18 +691,18 @@ public: } } } - + /// unittest { DesktopReadOptions options; - + options = DesktopReadOptions( ExtensionGroupPolicy.skip, - UnknownGroupPolicy.preserve, - ActionGroupPolicy.skip, - DuplicateKeyPolicy.skip, - DuplicateGroupPolicy.preserve, + UnknownGroupPolicy.preserve, + ActionGroupPolicy.skip, + DuplicateKeyPolicy.skip, + DuplicateGroupPolicy.preserve, No.preserveComments ); assert(options.unknownGroupPolicy == UnknownGroupPolicy.preserve); @@ -713,11 +713,11 @@ public: assert(!options.preserveComments); } } - + /// - unittest + unittest { - string contents = + string contents = `[Desktop Entry] Key=Value Actions=Action1; @@ -728,8 +728,8 @@ Key=Value`; auto df = new DesktopFile(iniLikeStringReader(contents), DesktopReadOptions(ActionGroupPolicy.skip)); assert(df.action("Action1") is null); - - contents = + + contents = `[Desktop Entry] Key=Value Actions=Action1; @@ -738,11 +738,11 @@ Key=Value`; df = new DesktopFile(iniLikeStringReader(contents)); assert(df.group("X-SomeGroup") !is null); - + df = new DesktopFile(iniLikeStringReader(contents), DesktopReadOptions(ExtensionGroupPolicy.skip)); assert(df.group("X-SomeGroup") is null); - - contents = + + contents = `[Desktop Entry] Valid=Key $=Invalid`; @@ -752,27 +752,27 @@ $=Invalid`; assert(thrown.entryException !is null); assert(thrown.entryException.key == "$"); assert(thrown.entryException.value == "Invalid"); - + assertNotThrown(new DesktopFile(iniLikeStringReader(contents), DesktopReadOptions(IniLikeGroup.InvalidKeyPolicy.skip))); - + df = new DesktopFile(iniLikeStringReader(contents), DesktopReadOptions(IniLikeGroup.InvalidKeyPolicy.save)); assert(df.value("$") == "Invalid"); - - contents = + + contents = `[Desktop Entry] Name=Name [Unknown] Key=Value`; assertThrown(new DesktopFile(iniLikeStringReader(contents), DesktopReadOptions(UnknownGroupPolicy.throwError))); - + assertNotThrown(df = new DesktopFile(iniLikeStringReader(contents), DesktopReadOptions(UnknownGroupPolicy.preserve))); assert(df.group("Unknown") !is null); - + df = new DesktopFile(iniLikeStringReader(contents), DesktopReadOptions(UnknownGroupPolicy.skip)); assert(df.group("Unknown") is null); - - contents = + + contents = `[Desktop Entry] Name=One [Desktop Entry] @@ -782,14 +782,14 @@ Name=Two`; assert(df.displayName() == "One"); assert(df.byGroup().map!(g => g["Name"]).equal(["One", "Two"])); } - + protected: ///Check if groupName is name of Desktop Action group. @trusted static bool isActionName(string groupName) { return groupName.startsWith("Desktop Action "); } - + @trusted override IniLikeGroup createGroupByName(string groupName) { if (groupName == "Desktop Entry") { return new DesktopEntry(); @@ -816,7 +816,7 @@ protected: } } } - + public: /** * Reads desktop file from file. @@ -827,20 +827,20 @@ public: @trusted this(string fileName, DesktopReadOptions options = DesktopReadOptions.init) { this(iniLikeFileReader(fileName), options, fileName); } - + /** * Reads desktop file from IniLikeReader, e.g. acquired from iniLikeFileReader or iniLikeStringReader. * Throws: * $(D inilike.file.IniLikeReadException) if error occured while parsing or "Desktop Entry" group is missing. */ this(IniLikeReader)(IniLikeReader reader, DesktopReadOptions options = DesktopReadOptions.init, string fileName = null) - { + { _options = options; super(reader, fileName, options.baseOptions); _desktopEntry = cast(DesktopEntry)group("Desktop Entry"); enforce(_desktopEntry !is null, new IniLikeReadException("No \"Desktop Entry\" group", 0)); } - + /** * Reads desktop file from IniLikeReader, e.g. acquired from iniLikeFileReader or iniLikeStringReader. * Throws: @@ -850,7 +850,7 @@ public: { this(reader, options, fileName); } - + /** * Constructs DesktopFile with "Desktop Entry" group and Version set to 1.0 */ @@ -860,7 +860,7 @@ public: insertGroup(_desktopEntry); _desktopEntry["Version"] = "1.0"; } - + /// unittest { @@ -870,7 +870,7 @@ public: assert(df.categories().empty); assert(df.type() == DesktopFile.Type.Unknown); } - + /** * Removes group by name. You can't remove "Desktop Entry" group with this function. */ @@ -880,7 +880,7 @@ public: } return super.removeGroup(groupName); } - + /// unittest { @@ -892,7 +892,7 @@ public: df.removeGroup("Desktop Entry"); assert(df.desktopEntry() !is null); } - + /** * Type of desktop entry. * Returns: Type of desktop entry. @@ -905,20 +905,20 @@ public: } return t; } - + @safe Type type(Type t) { return desktopEntry().type(t); } - + /// unittest - { + { auto desktopFile = new DesktopFile(iniLikeStringReader("[Desktop Entry]"), ".directory"); assert(desktopFile.type == DesktopFile.Type.Directory); } - + 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. * Note: This function retrieves applications paths each time it's called and therefore can impact performance. To avoid this issue use overload with argument. @@ -927,43 +927,42 @@ public: @safe string id() const nothrow { return desktopId(fileName); } - + /// unittest { import desktopfile.paths; - + string contents = "[Desktop Entry]\nType=Directory"; auto df = new DesktopFile(iniLikeStringReader(contents), "/home/user/data/applications/test/example.desktop"); - auto dataHomeGuard = EnvGuard("XDG_DATA_HOME"); - environment["XDG_DATA_HOME"] = "/home/user/data"; + auto dataHomeGuard = EnvGuard("XDG_DATA_HOME", "/home/user/data"); assert(df.id() == "test-example.desktop"); } } - + /** * See $(LINK2 http://standards.freedesktop.org/desktop-entry-spec/latest/ape.html, Desktop File ID) - * Params: + * Params: * appPaths = range of base application paths to check if this file belongs to one of them. * Returns: Desktop file ID or empty string if file does not have an ID. * See_Also: $(D desktopfile.paths.applicationsPaths), $(D desktopfile.utils.desktopId) */ - string id(Range)(Range appPaths) const nothrow if (isInputRange!Range && is(ElementType!Range : string)) + string id(Range)(Range appPaths) const nothrow if (isInputRange!Range && is(ElementType!Range : string)) { return desktopId(fileName, appPaths); } - + /// - unittest + unittest { - string contents = + string contents = `[Desktop Entry] Name=Program Type=Directory`; - + string[] appPaths; string filePath, nestedFilePath, wrongFilePath; - + version(Windows) { appPaths = [`C:\ProgramData\KDE\share\applications`, `C:\Users\username\.kde\share\applications`]; filePath = `C:\ProgramData\KDE\share\applications\example.desktop`; @@ -975,20 +974,20 @@ Type=Directory`; nestedFilePath = "/usr/share/applications/kde/example.desktop"; wrongFilePath = "/etc/desktop/example.desktop"; } - + auto df = new DesktopFile(iniLikeStringReader(contents), nestedFilePath); assert(df.id(appPaths) == "kde-example.desktop"); - + df = new DesktopFile(iniLikeStringReader(contents), filePath); assert(df.id(appPaths) == "example.desktop"); - + df = new DesktopFile(iniLikeStringReader(contents), wrongFilePath); assert(df.id(appPaths).empty); - + df = new DesktopFile(iniLikeStringReader(contents)); assert(df.id(appPaths).empty); } - + private static struct SplitValues { @trusted this(string value) nothrow pure { @@ -1024,7 +1023,7 @@ Type=Directory`; } static assert(isForwardRange!SplitValues); - + /** * Some keys can have multiple values, separated by semicolon. This function helps to parse such kind of strings into the range. * Returns: The range of multiple nonempty values. @@ -1034,16 +1033,16 @@ Type=Directory`; @trusted static auto splitValues(string values) nothrow pure { return SplitValues(values).filter!(s => !s.empty); } - + /// - unittest + unittest { assert(DesktopFile.splitValues("").empty); assert(DesktopFile.splitValues(";").empty); assert(DesktopFile.splitValues(";;;").empty); assert(equal(DesktopFile.splitValues("Application;Utility;FileManager;"), ["Application", "Utility", "FileManager"])); assert(equal(DesktopFile.splitValues("I\\;Me;\\;You\\;We\\;"), ["I;Me", ";You;We;"])); - + auto values = DesktopFile.splitValues("Application;Utility;FileManager;"); assert(values.front == "Application"); values.popFront(); @@ -1053,7 +1052,7 @@ Type=Directory`; assert(equal(values, ["FileManager"])); assert(equal(saved, ["Utility", "FileManager"])); } - + /** * Join range of multiple values into a string using semicolon as separator. Adds trailing semicolon. * Returns: Values of range joined into one string with ';' after each value or empty string if range is empty. @@ -1068,7 +1067,7 @@ Type=Directory`; return text(result) ~ ";"; } } - + /// unittest { @@ -1076,7 +1075,7 @@ Type=Directory`; assert(equal(DesktopFile.joinValues(["Application", "Utility", "FileManager"]), "Application;Utility;FileManager;")); assert(equal(DesktopFile.joinValues(["I;Me", ";You;We;"]), "I\\;Me;\\;You\\;We\\;;")); } - + /** * Get $(LINK2 http://standards.freedesktop.org/desktop-entry-spec/latest/ar01s10.html, additional application action) by name. * Returns: $(D DesktopAction) with given action name or null if not found or found section does not have a name. @@ -1091,7 +1090,7 @@ Type=Directory`; } return null; } - + /** * Iterating over existing actions. * Returns: Range of DesktopAction. @@ -1100,7 +1099,7 @@ Type=Directory`; @safe auto byAction() const { return actions().map!(actionName => action(actionName)).filter!(desktopAction => desktopAction !is null); } - + /** * Returns: instance of "Desktop Entry" group. * Note: Usually you don't need to call this function since you can rely on alias this. @@ -1108,26 +1107,26 @@ Type=Directory`; @nogc @safe inout(DesktopEntry) desktopEntry() nothrow inout { return _desktopEntry; } - + /** * This alias allows to call functions related to "Desktop Entry" group without need to call desktopEntry explicitly. */ alias desktopEntry this; - + /** * Expand "Exec" value into the array of command line arguments to use to start the program. * It applies unquoting and unescaping. * See_Also: $(D execValue), $(D desktopfile.utils.expandExecArgs), $(D startApplication) */ @safe string[] expandExecValue(in string[] urls = null, string locale = null) const - { + { return expandExecArgs(unquoteExec(execValue()), urls, localizedIconName(locale), localizedDisplayName(locale), fileName()); } - + /// - unittest + unittest { - string contents = + string contents = `[Desktop Entry] Name=Program Name[ru]=Программа @@ -1135,13 +1134,13 @@ Exec="quoted program" %i -w %c -f %k %U %D %u %f %F Icon=folder Icon[ru]=folder_ru`; auto df = new DesktopFile(iniLikeStringReader(contents), "/example.desktop"); - assert(df.expandExecValue(["one", "two"], "ru") == + assert(df.expandExecValue(["one", "two"], "ru") == ["quoted program", "--icon", "folder_ru", "-w", "Программа", "-f", "/example.desktop", "one", "two", "one", "one", "one", "two"]); } - + /** * Starts the application associated with this .desktop file using urls as command line params. - * + * * If the program should be run in terminal it tries to find system defined terminal emulator to run in. * Params: * urls = urls application will start with. @@ -1157,46 +1156,46 @@ Icon[ru]=folder_ru`; @trusted void startApplication(in string[] urls = null, string locale = null, lazy const(string)[] terminalCommand = getTerminalCommand) const { auto unquotedArgs = unquoteExec(execValue()); - + SpawnParams params; params.urls = urls; params.iconName = localizedIconName(locale); params.displayName = localizedDisplayName(locale); params.fileName = fileName; params.workingDirectory = workingDirectory(); - + if (terminal()) { params.terminalCommand = terminalCommand(); } - + return spawnApplication(unquotedArgs, params); } - + /// unittest { auto df = new DesktopFile(); assertThrown(df.startApplication(string[].init)); - + version(Posix) { static string[] emptyTerminalCommand() nothrow { return null; } - + df = new DesktopFile(iniLikeStringReader("[Desktop Entry]\nTerminal=true\nType=Application\nExec=whoami")); try { df.startApplication((string[]).init, null, emptyTerminalCommand); } catch(Exception e) { - + } } } - + ///Starts the application associated with this .desktop file using url as command line params. @trusted void startApplication(string url, string locale = null, lazy const(string)[] terminalCommand = getTerminalCommand) const { return startApplication([url], locale, terminalCommand); } - + /** * Opens url defined in .desktop file using $(LINK2 https://portland.freedesktop.org/doc/xdg-open.html, xdg-open). * Note: @@ -1211,14 +1210,14 @@ Icon[ru]=folder_ru`; enforce(myurl.length, "No URL to open"); xdgOpen(myurl); } - + /// unittest { auto df = new DesktopFile(); assertThrown(df.startLink()); } - + /** * Starts application or open link depending on desktop entry type. * Throws: @@ -1241,28 +1240,28 @@ Icon[ru]=folder_ru`; throw new Exception("Unknown desktop entry type"); } } - + /// unittest { string contents = "[Desktop Entry]\nType=Directory"; auto df = new DesktopFile(iniLikeStringReader(contents)); assertThrown(df.start()); - + df = new DesktopFile(); assertThrown(df.start()); } - + private: DesktopEntry _desktopEntry; DesktopReadOptions _options; } /// -unittest +unittest { import std.file; - string desktopFileContents = + string desktopFileContents = `[Desktop Entry] # Comment Name=Double Commander @@ -1307,7 +1306,7 @@ Exec=doublecmd settings [Desktop Action Notspecified] Name=Notspecified Action`; - + auto df = new DesktopFile(iniLikeStringReader(desktopFileContents), "doublecmd.desktop"); assert(df.desktopEntry().groupName() == "Desktop Entry"); assert(df.fileName() == "doublecmd.desktop"); @@ -1335,25 +1334,25 @@ Name=Notspecified Action`; assert(equal(df.onlyShowIn(), ["GNOME", "XFCE", "LXDE"])); assert(equal(df.notShowIn(), ["KDE"])); assert(df.group("X-NoName") !is null); - - assert(equal(df.byAction().map!(desktopAction => - tuple(desktopAction.displayName(), desktopAction.localizedDisplayName("ru"), desktopAction.iconName(), desktopAction.execValue())), + + assert(equal(df.byAction().map!(desktopAction => + tuple(desktopAction.displayName(), desktopAction.localizedDisplayName("ru"), desktopAction.iconName(), desktopAction.execValue())), [tuple("Open directory", "Открыть папку", "open", "doublecmd %u"), tuple("Settings", "Настройки", "edit", "doublecmd settings")])); - + assert(df.action("NotPresented") is null); assert(df.action("Notspecified") is null); assert(df.action("X-NoName") is null); assert(df.action("Settings") !is null); - + assert(df.saveToString() == desktopFileContents); - + assert(df.contains("Icon")); df.removeEntry("Icon"); assert(!df.contains("Icon")); df["Icon"] = "files"; assert(df.contains("Icon")); - - string contents = + + string contents = `# First comment [Desktop Entry] Key=Value @@ -1364,15 +1363,15 @@ Key=Value df.removeGroup("Desktop Entry"); assert(df.group("Desktop Entry") !is null); assert(df.desktopEntry() !is null); - - contents = + + contents = `[X-SomeGroup] Key=Value`; auto thrown = collectException!IniLikeReadException(new DesktopFile(iniLikeStringReader(contents))); assert(thrown !is null); assert(thrown.lineNumber == 0); - + df = new DesktopFile(); df.desktopEntry().writeEntry("$Invalid", "Valid value", IniLikeGroup.InvalidKeyPolicy.save); assert(df.desktopEntry().value("$Invalid") == "Valid value"); @@ -1381,21 +1380,21 @@ Key=Value`; df.terminal = true; df.type = DesktopFile.Type.Application; df.categories = ["Development", "Compilers", "One;Two", "Three\\;Four", "New\nLine"]; - + assert(df.terminal() == true); assert(df.type() == DesktopFile.Type.Application); assert(equal(df.categories(), ["Development", "Compilers", "One;Two", "Three\\;Four","New\nLine"])); - + df.displayName = "Program name"; assert(df.displayName() == "Program name"); df.genericName = "Program"; assert(df.genericName() == "Program"); df.comment = "Do\nthings"; assert(df.comment() == "Do\nthings"); - + df.execValue = "utilname"; assert(df.execValue() == "utilname"); - + df.noDisplay = true; assert(df.noDisplay()); df.hidden = true; @@ -1404,7 +1403,7 @@ Key=Value`; assert(df.dbusActivable()); df.startupNotify = true; assert(df.startupNotify()); - + df.url = "/some/url"; assert(df.url == "/some/url"); } diff --git a/source/desktopfile/paths.d b/source/desktopfile/paths.d index 99aefd5..020a76e 100644 --- a/source/desktopfile/paths.d +++ b/source/desktopfile/paths.d @@ -1,13 +1,13 @@ /** * Getting applications paths where desktop files are stored. - * - * Authors: + * + * Authors: * $(LINK2 https://github.com/FreeSlave, Roman Chistokhodov) * Copyright: - * Roman Chistokhodov, 2015-2016 - * License: + * Roman Chistokhodov, 2015-2017 + * License: * $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0). - * See_Also: + * See_Also: * $(LINK2 https://www.freedesktop.org/wiki/Specifications/desktop-entry-spec/, Desktop Entry Specification) */ @@ -16,7 +16,7 @@ module desktopfile.paths; private { import isfreedesktop; import xdgpaths; - + import std.algorithm; import std.array; import std.path; @@ -25,14 +25,15 @@ private { version(unittest) { import std.process : environment; - + package struct EnvGuard { - this(string env) { + this(string env, string newValue) { envVar = env; envValue = environment.get(env); + environment[env] = newValue; } - + ~this() { if (envValue is null) { environment.remove(envVar); @@ -40,14 +41,14 @@ version(unittest) { environment[envVar] = envValue; } } - + string envVar; string envValue; } } /** - * Applications paths based on data paths. + * Applications paths based on data paths. * This function is available on all platforms, but requires dataPaths argument (e.g. C:\ProgramData\KDE\share on Windows) * Returns: Array of paths, based on dataPaths with "applications" directory appended. */ @@ -70,20 +71,17 @@ static if (isFreedesktop) @trusted string[] applicationsPaths() nothrow { return xdgAllDataDirs("applications"); } - + /// unittest { import std.process : environment; - auto dataHomeGuard = EnvGuard("XDG_DATA_HOME"); - auto dataDirsGuard = EnvGuard("XDG_DATA_DIRS"); - - environment["XDG_DATA_HOME"] = "/home/user/data"; - environment["XDG_DATA_DIRS"] = "/usr/local/data:/usr/data"; - + auto dataHomeGuard = EnvGuard("XDG_DATA_HOME", "/home/user/data"); + auto dataDirsGuard = EnvGuard("XDG_DATA_DIRS", "/usr/local/data:/usr/data"); + assert(applicationsPaths() == ["/home/user/data/applications", "/usr/local/data/applications", "/usr/data/applications"]); } - + /** * Path where .desktop files can be stored by user. * This function is defined only on freedesktop systems. @@ -92,13 +90,12 @@ static if (isFreedesktop) @safe string writableApplicationsPath() nothrow { return xdgDataHome("applications"); } - + /// unittest { import std.process : environment; - auto dataHomeGuard = EnvGuard("XDG_DATA_HOME"); - environment["XDG_DATA_HOME"] = "/home/user/data"; + auto dataHomeGuard = EnvGuard("XDG_DATA_HOME", "/home/user/data"); assert(writableApplicationsPath() == "/home/user/data/applications"); } } diff --git a/source/desktopfile/utils.d b/source/desktopfile/utils.d index d7777f2..ba02183 100644 --- a/source/desktopfile/utils.d +++ b/source/desktopfile/utils.d @@ -1,12 +1,12 @@ /** * Utility functions for reading and executing desktop files. - * Authors: + * Authors: * $(LINK2 https://github.com/FreeSlave, Roman Chistokhodov) * Copyright: * Roman Chistokhodov, 2015-2016 - * License: + * License: * $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0). - * See_Also: + * See_Also: * $(LINK2 https://www.freedesktop.org/wiki/Specifications/desktop-entry-spec/, Desktop Entry Specification) */ @@ -28,9 +28,9 @@ package { import std.string; import std.traits; import std.typecons; - + static if( __VERSION__ < 2066 ) enum nogc = 1; - + import findexecutable; import detached; import isfreedesktop; @@ -43,7 +43,7 @@ package @trusted File getNullStdin() try { toReturn = File("/dev/null", "rb"); } catch(Exception e) { - + } return toReturn; } else { @@ -58,7 +58,7 @@ package @trusted File getNullStdout() try { toReturn = File("/dev/null", "wb"); } catch(Exception e) { - + } return toReturn; } else { @@ -73,7 +73,7 @@ package @trusted File getNullStderr() try { toReturn = File("/dev/null", "wb"); } catch(Exception e) { - + } return toReturn; } else { @@ -98,22 +98,22 @@ struct SpawnParams { /// Urls of file paths to open const(string)[] urls; - + /// Icon to use in place of %i field code. string iconName; - + /// Name to use in place of %c field code. string displayName; - + /// File name to use in place of %k field code. string fileName; - + /// Working directory of starting process. string workingDirectory; - + /// Terminal command to prepend to exec arguments. const(string)[] terminalCommand; - + /// Allow starting multiple instances of application if needed. bool allowMultipleInstances = true; } @@ -138,11 +138,11 @@ private @trusted void execProcess(in string[] args, string workingDirectory = nu if (!unquotedArgs.length) { throw new DesktopExecException("No arguments. Missing or empty Exec value"); } - + if (params.terminalCommand) { unquotedArgs = params.terminalCommand ~ unquotedArgs; } - + if (params.urls.length && params.allowMultipleInstances && needMultipleInstances(unquotedArgs)) { for(size_t i=0; i': case '<': case '~': - case '|': case '&': case ';': case '$': case '*': + case '|': case '&': case ';': case '$': case '*': case '?': case '#': case '(': case ')': case '`': return true; default: @@ -207,26 +207,26 @@ private @trusted string escapeQuotedArgument(string value) pure { * $(D DesktopExecException) if string can't be unquoted (e.g. no pair quote). * Note: * Although Desktop Entry Specification says that arguments must be quoted by double quote, for compatibility reasons this implementation also recognizes single quotes. - * See_Also: + * See_Also: * $(LINK2 http://standards.freedesktop.org/desktop-entry-spec/latest/ar01s06.html, specification) */ @trusted 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++; @@ -237,7 +237,7 @@ private @trusted string escapeQuotedArgument(string value) pure { } return value[start..i].unescapeQuotedArgument(); } - + char[] append; bool wasInQuotes; while(i < value.length) { @@ -260,45 +260,45 @@ private @trusted string escapeQuotedArgument(string value) pure { } i++; } - + if (append !is null) { result ~= append.assumeUnique; } - + return result; } /// -unittest +unittest { assert(equal(unquoteExec(``), string[].init)); assert(equal(unquoteExec(` `), string[].init)); assert(equal(unquoteExec(`""`), [``])); assert(equal(unquoteExec(`"" " "`), [``, ` `])); - + assert(equal(unquoteExec(`cmd arg1 arg2 arg3 `), [`cmd`, `arg1`, `arg2`, `arg3`])); assert(equal(unquoteExec(`"cmd" arg1 arg2 `), [`cmd`, `arg1`, `arg2`])); - + assert(equal(unquoteExec(`"quoted cmd" arg1 "quoted arg" `), [`quoted cmd`, `arg1`, `quoted arg`])); assert(equal(unquoteExec(`"quoted \"cmd\"" arg1 "quoted \"arg\""`), [`quoted "cmd"`, `arg1`, `quoted "arg"`])); - + assert(equal(unquoteExec(`"\\\$" `), [`\$`])); assert(equal(unquoteExec(`"\\$" `), [`\$`])); assert(equal(unquoteExec(`"\$" `), [`$`])); assert(equal(unquoteExec(`"$"`), [`$`])); - + assert(equal(unquoteExec(`"\\" `), [`\`])); assert(equal(unquoteExec(`"\\\\" `), [`\\`])); - + assert(equal(unquoteExec(`'quoted cmd' arg`), [`quoted cmd`, `arg`])); - + assert(equal(unquoteExec(`test\ "one""two"\ more\ \ test `), [`test onetwo more test`])); assert(equal(unquoteExec(`"one"two"three"`), [`onetwothree`])); - + assert(equal(unquoteExec(`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(unquoteExec(`cmd "quoted arg`)); assertThrown!DesktopExecException(unquoteExec(`"`)); } @@ -314,13 +314,13 @@ private @trusted string urlToFilePath(string url) nothrow pure } /** - * Expand Exec arguments (usually returned by $(D unquoteExec)) replacing field codes with given values, making the array suitable for passing to spawnProcess or spawnProcessDetached. + * Expand Exec arguments (usually returned by $(D unquoteExec)) replacing field codes with given values, making the array suitable for passing to spawnProcess or spawnProcessDetached. * Deprecated field codes are ignored. * Note: * Returned array may be empty and must be checked before passing to spawning the process. * Params: * unquotedArgs = Array of unescaped and unquoted arguments. - * urls = Array of urls or file names that inserted in the place of %f, %F, %u or %U field codes. + * urls = Array of urls or file names that inserted in the place of %f, %F, %u or %U field codes. * For %f and %u only the first element of array is used. * For %f and %F every url started with 'file://' will be replaced with normal path. * iconName = Icon name used to substitute %i field code by --icon iconName. @@ -354,7 +354,7 @@ private @trusted string urlToFilePath(string url) nothrow pure restPos = i+2; i++; } - + string expanded; size_t restPos = 0; bool ignore; @@ -402,13 +402,13 @@ private @trusted string urlToFilePath(string url) nothrow pure } } } - + if (!ignore) { toReturn ~= expanded ~ token[restPos..$]; } } } - + return toReturn; } @@ -416,11 +416,11 @@ private @trusted string urlToFilePath(string url) nothrow pure unittest { assert(expandExecArgs( - ["program path", "%%f", "%%i", "%D", "--deprecated=%d", "%n", "%N", "%m", "%v", "--file=%f", "%i", "%F", "--myname=%c", "--mylocation=%k", "100%%"], - ["one"], + ["program path", "%%f", "%%i", "%D", "--deprecated=%d", "%n", "%N", "%m", "%v", "--file=%f", "%i", "%F", "--myname=%c", "--mylocation=%k", "100%%"], + ["one"], "folder", "program", "location" ) == ["program path", "%f", "%i", "--file=one", "--icon", "folder", "one", "--myname=program", "--mylocation=location", "100%"]); - + assert(expandExecArgs(["program path", "many%%%%"]) == ["program path", "many%%"]); assert(expandExecArgs(["program path", "%f"]) == ["program path"]); assert(expandExecArgs(["program path", "%f%%%f"], ["file"]) == ["program path", "file%file"]); @@ -431,7 +431,7 @@ unittest assert(expandExecArgs(["program path", "%F"], ["one", "two"]) == ["program path", "one", "two"]); assert(expandExecArgs(["program path", "%F"], ["file://one", "file://two"]) == ["program path", "one", "two"]); assert(expandExecArgs(["program path", "%U"], ["file://one", "file://two"]) == ["program path", "file://one", "file://two"]); - + assert(expandExecArgs(["program path", "--location=%k", "--myname=%c"]) == ["program path", "--location=", "--myname="]); assert(expandExecArgs(["program path", "%k", "%c"]) == ["program path", "", ""]); assertThrown!DesktopExecException(expandExecArgs(["program name", "%y"])); @@ -440,7 +440,7 @@ unittest } /** - * Flag set of parameter kinds supported by application. + * Flag set of parameter kinds supported by application. * Having more than one flag means that Exec command is ambiguous. * See_Also: $(D paramSupport) */ @@ -450,7 +450,7 @@ enum ParamSupport * Application does not support parameters. */ none = 0, - + /** * Application can open single file at once. */ @@ -556,8 +556,8 @@ private struct ExecToken /** * Helper struct to build Exec string for desktop file. - * Note: - * While Desktop Entry Specification says that field codes must not be inside quoted argument, + * Note: + * While Desktop Entry Specification says that field codes must not be inside quoted argument, * ExecBuilder does not consider it as error and may create quoted argument if field code is prepended by the string that needs quotation. */ struct ExecBuilder @@ -573,7 +573,7 @@ struct ExecBuilder enforce(executable.isAbsolute || executable.baseName == executable, "Program part of Exec must be absolute path or base name"); execTokens ~= ExecToken(executable, executable.needQuoting()); } - + /** * Add literal argument which is not field code. * Params: @@ -585,7 +585,7 @@ struct ExecBuilder execTokens ~= ExecToken(arg.doublePercentSymbol(), arg.needQuoting() || forceQuoting); return this; } - + /** * Add "%i" field code. * Returns: this object for chained calls. @@ -594,8 +594,8 @@ struct ExecBuilder execTokens ~= ExecToken("%i", false); return this; } - - + + /** * Add "%f" field code. * Returns: this object for chained calls. @@ -603,7 +603,7 @@ struct ExecBuilder @safe ref ExecBuilder file(string prepend = null) { return fieldCode(prepend, "%f"); } - + /** * Add "%F" field code. * Returns: this object for chained calls. @@ -612,7 +612,7 @@ struct ExecBuilder execTokens ~= ExecToken("%F"); return this; } - + /** * Add "%u" field code. * Returns: this object for chained calls. @@ -620,7 +620,7 @@ struct ExecBuilder @safe ref ExecBuilder url(string prepend = null) { return fieldCode(prepend, "%u"); } - + /** * Add "%U" field code. * Returns: this object for chained calls. @@ -629,7 +629,7 @@ struct ExecBuilder execTokens ~= ExecToken("%U"); return this; } - + /** * Add "%c" field code (name of application). * Returns: this object for chained calls. @@ -637,7 +637,7 @@ struct ExecBuilder @safe ref ExecBuilder displayName(string prepend = null) { return fieldCode(prepend, "%c"); } - + /** * Add "%k" field code (location of desktop file). * Returns: this object for chained calls. @@ -645,14 +645,14 @@ struct ExecBuilder @safe ref ExecBuilder location(string prepend = null) { return fieldCode(prepend, "%k"); } - + /** * Get resulting string that can be set to Exec field of Desktop Entry. The returned string is escaped. */ @trusted string result() const { return execTokens.map!(t => (t.needQuotes ? ('"' ~ t.token.escapeQuotedArgument() ~ '"') : t.token)).join(" ").escapeValue(); } - + private: @safe ref ExecBuilder fieldCode(string prepend, string code) { @@ -660,7 +660,7 @@ private: execTokens ~= ExecToken(token, token.needQuoting()); return this; } - + ExecToken[] execTokens; } @@ -674,22 +674,22 @@ unittest .argument("100%") .location("--location=") .urls().url().file("--file=").files().result() == `"quoted program" %i -w %c "\\$value" "slash\\\\" 100%% --location=%k %U %u --file=%f %F`); - + assert(ExecBuilder("program").argument("").url("my url ").result() == `program "" "my url %u"`); - + assertThrown(ExecBuilder("./relative/path")); } /** * 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 +string[] getTerminalCommand() nothrow @trusted { static if (isFreedesktop) { static string getDefaultTerminal() nothrow @@ -712,10 +712,10 @@ string[] getTerminalCommand() nothrow @trusted return null; } } - + string[] paths; collectException(binPaths().array, paths); - + string term = findExecutable("x-terminal-emulator", paths); if (!term.empty) { return [term, "-e"]; @@ -742,46 +742,44 @@ unittest import isfreedesktop; static if (isFreedesktop) { import desktopfile.paths; - - auto pathGuard = EnvGuard("PATH"); - + try { static void changeMod(string fileName, uint mode) { import core.sys.posix.sys.stat; enforce(chmod(fileName.toStringz, cast(mode_t)mode) == 0); } - + string tempPath = buildPath(tempDir(), "desktopfile-unittest-tempdir"); - + if (!tempPath.exists) { mkdir(tempPath); } scope(exit) rmdir(tempPath); - - environment["PATH"] = tempPath; - + + auto pathGuard = EnvGuard("PATH", tempPath); + string tempXTerminalEmulatorFile = buildPath(tempPath, "x-terminal-emulator"); string tempXdgTerminalFile = buildPath(tempPath, "xdg-terminal"); - + File(tempXdgTerminalFile, "w"); scope(exit) remove(tempXdgTerminalFile); changeMod(tempXdgTerminalFile, octal!755); enforce(getTerminalCommand() == [buildPath(tempPath, "xdg-terminal")]); - + changeMod(tempXdgTerminalFile, octal!644); enforce(getTerminalCommand() == ["xterm", "-e"]); - + File(tempXTerminalEmulatorFile, "w"); scope(exit) remove(tempXTerminalEmulatorFile); changeMod(tempXTerminalEmulatorFile, octal!755); enforce(getTerminalCommand() == [buildPath(tempPath, "x-terminal-emulator"), "-e"]); - + environment["PATH"] = ":"; enforce(getTerminalCommand() == ["xterm", "-e"]); - + } catch(Exception e) { - + } } else { assert(getTerminalCommand().empty); @@ -809,32 +807,32 @@ struct ShootOptions FollowLink = 4, /// If desktop file is link and url points to another desktop file shootDesktopFile will be called on this url with the same options. All = Exec|Link|FollowLink /// All flags described above. } - + /** * Flags * By default is set to use all flags. */ auto flags = All; - + /** * Urls to pass to the program is desktop file points to application. * Empty by default. */ const(string)[] urls; - + /** * Locale of environment. * Empty by default. */ string locale; - + /** * Delegate that will be used to open url if desktop file is link. * To set static function use std.functional.toDelegate. * If it's null shootDesktopFile will use xdg-open. */ void delegate(string) opener = null; - + /** * Delegate that will be used to get terminal command if desktop file is application and needs to ran in terminal. * To set static function use std.functional.toDelegate. @@ -842,25 +840,25 @@ struct ShootOptions * See_Also: $(D getTerminalCommand) */ const(string)[] delegate() terminalDetector = null; - + /** * Allow to run multiple instances of application if it does not support opening multiple urls in one instance. */ bool allowMultipleInstances = true; } -package void readNeededKeys(Group)(Group g, string locale, - out string iconName, out string name, - out string execValue, out string url, +package void readNeededKeys(Group)(Group g, string locale, + out string iconName, out string name, + out string execValue, out string url, out string workingDirectory, out bool terminal) { string bestLocale; foreach(e; g.byEntry) { auto t = parseKeyValue(e); - + string key = t[0]; string value = t[1]; - + if (key.length) { switch(key) { case "Exec": execValue = value.unescapeValue(); break; @@ -886,7 +884,7 @@ unittest { string contents = "[Desktop Entry]\nExec=whoami\nURL=http://example.org\nIcon=folder\nPath=/usr/bin\nTerminal=true\nName=Example\nName[ru]=Пример"; auto reader = iniLikeStringReader(contents); - + string iconName, name, execValue, url, workingDirectory; bool terminal; readNeededKeys(reader.byGroup().front, "ru_RU", iconName, name , execValue, url, workingDirectory, terminal); @@ -914,26 +912,26 @@ unittest void shootDesktopFile(IniLikeReader)(IniLikeReader reader, string fileName = null, ShootOptions options = ShootOptions.init) { enforce(options.flags & (ShootOptions.Exec|ShootOptions.Link), "At least one of the options Exec or Link must be provided"); - + string iconName, name, execValue, url, workingDirectory; bool terminal; - + foreach(g; reader.byGroup) { if (g.groupName == "Desktop Entry") { readNeededKeys(g, options.locale, iconName, name, execValue, url, workingDirectory, terminal); - + import std.functional : toDelegate; - + if (execValue.length && (options.flags & ShootOptions.Exec)) { auto unquotedArgs = unquoteExec(execValue); - + SpawnParams params; params.urls = options.urls; params.iconName = iconName; params.displayName = name; params.fileName = fileName; params.workingDirectory = workingDirectory; - + if (terminal) { if (options.terminalDetector == null) { options.terminalDetector = toDelegate(&getTerminalCommand); @@ -958,11 +956,11 @@ void shootDesktopFile(IniLikeReader)(IniLikeReader reader, string fileName = nul } throw new Exception("Desktop file is neither application nor link"); } - + return; } } - + throw new Exception("File does not have Desktop Entry group"); } @@ -971,15 +969,15 @@ unittest { string contents; ShootOptions options; - + contents = "[Desktop Entry]\nURL=testurl"; options.flags = ShootOptions.FollowLink; assertThrown(shootDesktopFile(iniLikeStringReader(contents), null, options)); - + contents = "[Group]\nKey=Value"; options = ShootOptions.init; assertThrown(shootDesktopFile(iniLikeStringReader(contents), null, options)); - + contents = "[Desktop Entry]\nURL=testurl"; options = ShootOptions.init; bool wasCalled; @@ -987,22 +985,22 @@ unittest assert(url == "testurl"); wasCalled = true; }; - + shootDesktopFile(iniLikeStringReader(contents), null, options); assert(wasCalled); - + contents = "[Desktop Entry]"; options = ShootOptions.init; assertThrown(shootDesktopFile(iniLikeStringReader(contents), null, options)); - + contents = "[Desktop Entry]\nURL=testurl"; options.flags = ShootOptions.Exec; assertThrown(shootDesktopFile(iniLikeStringReader(contents), null, options)); - + contents = "[Desktop Entry]\nExec=whoami"; options.flags = ShootOptions.Link; assertThrown(shootDesktopFile(iniLikeStringReader(contents), null, options)); - + static if (isFreedesktop) { try { contents = "[Desktop Entry]\nExec=whoami\nTerminal=true"; @@ -1011,34 +1009,34 @@ unittest options.terminalDetector = delegate string[] () {wasCalled = true; return null;}; shootDesktopFile(iniLikeStringReader(contents), null, options); assert(wasCalled); - + string tempPath = buildPath(tempDir(), "desktopfile-unittest-tempdir"); if (!tempPath.exists) { mkdir(tempPath); } scope(exit) rmdir(tempPath); - + string tempDesktopFile = buildPath(tempPath, "followtest.desktop"); auto f = File(tempDesktopFile, "w"); scope(exit) remove(tempDesktopFile); f.rawWrite("[Desktop Entry]\nURL=testurl"); f.flush(); - + contents = "[Desktop Entry]\nURL=" ~ tempDesktopFile; options.flags = ShootOptions.Link | ShootOptions.FollowLink; options.opener = delegate void (string url) { assert(url == "testurl"); wasCalled = true; }; - + shootDesktopFile(iniLikeStringReader(contents), null, options); assert(wasCalled); } catch(Exception e) { - + } } - - + + } /// ditto, but automatically create IniLikeReader from the file. @@ -1049,7 +1047,7 @@ unittest /** * See $(LINK2 http://standards.freedesktop.org/desktop-entry-spec/latest/ape.html, Desktop File ID) - * Params: + * Params: * fileName = Desktop file. * appsPaths = Range of base application paths. * Returns: Desktop file ID or empty string if file does not have an ID. @@ -1062,12 +1060,12 @@ string desktopId(Range)(string fileName, Range appsPaths) if (isInputRange!Range foreach (path; appsPaths) { auto pathSplit = pathSplitter(path); auto fileSplit = pathSplitter(absolute); - + while (!pathSplit.empty && !fileSplit.empty && pathSplit.front == fileSplit.front) { pathSplit.popFront(); fileSplit.popFront(); } - + if (pathSplit.empty) { static if( __VERSION__ < 2066 ) { return to!string(fileSplit.map!(s => to!string(s)).join("-")); @@ -1077,7 +1075,7 @@ string desktopId(Range)(string fileName, Range appsPaths) if (isInputRange!Range } } } catch(Exception e) { - + } return null; } @@ -1087,7 +1085,7 @@ unittest { string[] appPaths; string filePath, nestedFilePath, wrongFilePath; - + version(Windows) { appPaths = [`C:\ProgramData\KDE\share\applications`, `C:\Users\username\.kde\share\applications`]; filePath = `C:\ProgramData\KDE\share\applications\example.desktop`; @@ -1099,7 +1097,7 @@ unittest nestedFilePath = "/usr/share/applications/kde/example.desktop"; wrongFilePath = "/etc/desktop/example.desktop"; } - + assert(desktopId(nestedFilePath, appPaths) == "kde-example.desktop"); assert(desktopId(filePath, appPaths) == "example.desktop"); assert(desktopId(wrongFilePath, appPaths).empty); @@ -1108,7 +1106,7 @@ unittest 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: @@ -1138,7 +1136,7 @@ string findDesktopFile(Range)(string desktopId, Range appsPaths) if (isInputRang if (desktopId != desktopId.baseName) { return null; } - + foreach(appsPath; appsPaths) { auto filePath = buildPath(appsPath, desktopId); bool fileExists = filePath.exists; @@ -1160,7 +1158,7 @@ unittest assert(findDesktopFile("valid.desktop", (string[]).init) is null); } -static if (isFreedesktop) +static if (isFreedesktop) { /** * ditto @@ -1179,8 +1177,8 @@ static if (isFreedesktop) } /** - * Check if .desktop file is trusted. - * + * Check if .desktop file is trusted. + * * This is not actually part of Desktop File Specification but many desktop envrionments have this concept. * The trusted .desktop file is a file the current user has executable access on or the owner of which is root. * This function should be applicable only to desktop files of $(D DesktopEntry.Type.Application) type. @@ -1191,13 +1189,13 @@ static if (isFreedesktop) version(Posix) { import core.sys.posix.sys.stat; import core.sys.posix.unistd; - + try { // try for outdated compilers auto namez = toStringz(appFileName); if (access(namez, X_OK) == 0) { return true; } - + stat_t statbuf; auto result = stat(namez, &statbuf); return (result == 0 && statbuf.st_uid == 0);