From 557892d99421fb86049bf15211336948add4f258 Mon Sep 17 00:00:00 2001 From: FreeSlave Date: Wed, 18 May 2016 00:40:08 +0300 Subject: [PATCH] One big update --- .travis.yml | 11 +- README.md | 33 +++- download_doveralls.sh | 6 - dub.selections.json | 2 +- examples/shoot/dub.selections.json | 2 +- examples/shoot/source/app.d | 39 +++- examples/test/dub.json | 2 +- examples/test/dub.selections.json | 4 +- examples/test/source/app.d | 6 +- examples/util/dub.selections.json | 2 +- examples/util/source/app.d | 4 +- source/desktopfile/file.d | 260 +++++++++++++------------- source/desktopfile/utils.d | 282 +++++++++++++++++++---------- start_doveralls.sh | 5 - travis-script.sh | 10 + 15 files changed, 396 insertions(+), 272 deletions(-) delete mode 100755 download_doveralls.sh delete mode 100755 start_doveralls.sh create mode 100755 travis-script.sh diff --git a/.travis.yml b/.travis.yml index ae0ad2b..9c368ef 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,17 +4,14 @@ matrix: - d: gdc-4.8.2 - d: ldc-0.14.0 include: + - d: dmd-2.069.2 + env: USE_DOVERALLS=true - d: dmd-2.067.1 - d: gdc-4.9.2 - d: ldc-0.15.1 - d: gdc-4.8.2 - d: ldc-0.14.0 - -install: - - chmod +x download_doveralls.sh - - ./download_doveralls.sh script: - - dub test -b unittest-cov --compiler=${DC} - - chmod +x start_doveralls.sh - - ./start_doveralls.sh + - chmod +x travis-script.sh + - ./travis-script.sh diff --git a/README.md b/README.md index 4eaf24e..cf63a02 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,11 @@ The library is crossplatform for the most part, though there's little sense to u **desktopfile** provides basic features like reading and executing desktop files, and more: * [Exec](http://standards.freedesktop.org/desktop-entry-spec/latest/ar01s06.html) value unquoting and unescaping. Expanding field codes. +* Starting several instances of application if it supports only %f or %u and not %F or %U. * Can rewrite desktop files preserving all comments and the original order of groups [as required by spec](https://specifications.freedesktop.org/desktop-entry-spec/latest/ar01s02.html). * Retrieving [Desktop file ID](http://standards.freedesktop.org/desktop-entry-spec/latest/ape.html). * Support for [Additional application actions](http://standards.freedesktop.org/desktop-entry-spec/latest/ar01s10.html). -* Determining default terminal command to run applications with Terminal=true. Note that default terminal detector may not work properly on particular system (e.g. Fedora which does not have xterm installed by default) since there's no standard way to find default terminal emulator that would work on every distribution and desktop environment. If you strive for better terminal emulator detection you may look at [xdg-terminal.sh](https://src.chromium.org/svn/trunk/deps/third_party/xdg-utils/scripts/xdg-terminal). +* Determining default terminal command to run applications with Terminal=true. Note that default terminal detector may not work properly on particular system since there's no standard way to find default terminal emulator that would work on every distribution and desktop environment. If you strive for better terminal emulator detection you may look at [xdg-terminal.sh](https://src.chromium.org/svn/trunk/deps/third_party/xdg-utils/scripts/xdg-terminal). ### Missing features @@ -31,8 +32,8 @@ Features that currently should be handled by user, but may be implemented in the * [D-Bus Activation](http://standards.freedesktop.org/desktop-entry-spec/latest/ar01s07.html). * Startup Notification Protocol. -* Copying files to local file system when %f field code is used. -* Starting several instances of application if it supports only %f or %u and not %F or %U. +* Copying files to local file system when %f or %F field code is used. +* Ayatana Desktop Shortcuts used in Unity. This is not part of Desktop Entry and actually violates the specification. ## Generating documentation @@ -84,13 +85,13 @@ try { string[] keywords = df.keywords().array; //Keywords can be used to improve searching of the application. foreach(action; df.byAction()) { //Supported actions. - string actionName = action.name(); + string actionName = action.localizedDisplayName(locale); action.start(locale); } if (df.type() == DesktopFile.Type.Application) { //This is application - string commandLine = df.execString(); //Command line pattern used to start the application. + string commandLine = df.execValue(); //Command line pattern used to start the application. try { df.startApplication(urls, locale); //Start application using given arguments and specified locale. It will be automatically started in terminal emulator if required. } @@ -125,7 +126,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 $HOME/.bashrc + dub run :util -- exec /usr/share/applications/geany.desktop dub.json This should start command line application in terminal emulator (will be detected automatically): @@ -135,6 +136,10 @@ 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 @@ -161,6 +166,11 @@ On non-freedesktop systems appPath should be passed and PATH variable prepared. set PATH=C:\ProgramData\KDE\bin dub run :util -- --appPath=C:\ProgramData\KDE\share\applications exec kde4-gwenview.desktop +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. @@ -186,9 +196,14 @@ Example using cmd on Windows (KDE installed): 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. - dub run :shoot -- $HOME/Desktop/vlc.desktop - dub run :shoot -- /usr/share/applications/python2.7.desktop + 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 On Windows (KDE installed): - dub run :shoot -- C:\ProgramData\KDE\share\applications\kde4\gwenview.desktop \ No newline at end of file + dub run :shoot -- C:\ProgramData\KDE\share\applications\kde4\gwenview.desktop diff --git a/download_doveralls.sh b/download_doveralls.sh deleted file mode 100755 index f8b1fc7..0000000 --- a/download_doveralls.sh +++ /dev/null @@ -1,6 +0,0 @@ -# This is for travis-ci to run doveralls only on dmd build. - -if [ $DC = "dmd" ]; then - wget -O doveralls "https://github.com/ColdenCullen/doveralls/releases/download/v1.1.6/doveralls_linux_travis" - chmod +x doveralls -fi diff --git a/dub.selections.json b/dub.selections.json index e06c862..5d21bff 100644 --- a/dub.selections.json +++ b/dub.selections.json @@ -2,7 +2,7 @@ "fileVersion": 1, "versions": { "isfreedesktop": "0.1.0", - "inilike": "0.6.2", + "inilike": "0.7.0", "xdgpaths": "0.2.1", "findexecutable": "0.1.0" } diff --git a/examples/shoot/dub.selections.json b/examples/shoot/dub.selections.json index e06c862..5d21bff 100644 --- a/examples/shoot/dub.selections.json +++ b/examples/shoot/dub.selections.json @@ -2,7 +2,7 @@ "fileVersion": 1, "versions": { "isfreedesktop": "0.1.0", - "inilike": "0.6.2", + "inilike": "0.7.0", "xdgpaths": "0.2.1", "findexecutable": "0.1.0" } diff --git a/examples/shoot/source/app.d b/examples/shoot/source/app.d index 98e8ac6..9c39768 100644 --- a/examples/shoot/source/app.d +++ b/examples/shoot/source/app.d @@ -7,23 +7,54 @@ int main(string[] args) { bool onlyExec; bool notFollow; + string[] appPaths; getopt( args, "onlyExec", "Only start applications, don't open links", &onlyExec, - "notFollow", "Don't follow desktop files", ¬Follow); + "notFollow", "Don't follow desktop files", ¬Follow, + "appPath", "Path of applications directory", &appPaths); - string fileName; + string inFile; if (args.length > 1) { - fileName = args[1]; + inFile = args[1]; } else { stderr.writeln("Must provide path to desktop file"); return 1; } + if (appPaths.length == 0) { + static if (isFreedesktop) { + import desktopfile.paths; + appPaths = applicationsPaths(); + } + version(Windows) { + try { + auto root = environment.get("SYSTEMDRIVE", "C:"); + auto kdeAppDir = root ~ `\ProgramData\KDE\share\applications`; + if (kdeAppDir.isDir) { + appPaths = [kdeAppDir]; + } + } catch(Exception e) { + + } + } + } + + if (inFile == inFile.baseName && inFile.extension == ".desktop") { + string desktopId = inFile; + inFile = findDesktopFile(desktopId, appPaths); + if (inFile is null) { + stderr.writeln("Could not find desktop file with such id: ", desktopId); + return 1; + } + } + ShootOptions options; + options.urls = args[2..$]; + if (onlyExec) { options.flags = options.flags & ~ShootOptions.Link; } @@ -33,7 +64,7 @@ int main(string[] args) } try { - shootDesktopFile(fileName, options); + shootDesktopFile(inFile, options); } catch(Exception e) { stderr.writeln(e.msg); diff --git a/examples/test/dub.json b/examples/test/dub.json index 48740be..c4f5d11 100644 --- a/examples/test/dub.json +++ b/examples/test/dub.json @@ -5,7 +5,7 @@ "authors": ["freeslave"], "dependencies": { "desktopfile" : "*", - "standardpaths": "~>0.4.0" + "standardpaths": "~>0.5.0" }, "targetPath" : "bin", "targetType" : "executable" diff --git a/examples/test/dub.selections.json b/examples/test/dub.selections.json index 5960f4e..ab05e6d 100644 --- a/examples/test/dub.selections.json +++ b/examples/test/dub.selections.json @@ -1,9 +1,9 @@ { "fileVersion": 1, "versions": { - "standardpaths": "0.4.0", + "standardpaths": "0.5.0", "isfreedesktop": "0.1.0", - "inilike": "0.6.2", + "inilike": "0.7.0", "xdgpaths": "0.2.1", "findexecutable": "0.1.0" } diff --git a/examples/test/source/app.d b/examples/test/source/app.d index 00a88b9..b9247be 100644 --- a/examples/test/source/app.d +++ b/examples/test/source/app.d @@ -57,11 +57,11 @@ void main(string[] args) } try { auto df = new DesktopFile(entry, DesktopFile.ReadOptions.noOptions); - if (!df.execString().empty) { - auto execArgs = df.expandExecString(); + if (!df.execValue().empty) { + auto execArgs = df.expandExecValue(); } } - catch(IniLikeException e) { + catch(IniLikeReadException e) { stderr.writefln("Error reading %s: at %s: %s", entry, e.lineNumber, e.msg); } catch(DesktopExecException e) { diff --git a/examples/util/dub.selections.json b/examples/util/dub.selections.json index 9ad191b..7d8c32a 100644 --- a/examples/util/dub.selections.json +++ b/examples/util/dub.selections.json @@ -3,7 +3,7 @@ "versions": { "isfreedesktop": "0.1.0", "findexecutable": "0.1.0", - "inilike": "0.6.2", + "inilike": "0.7.0", "xdgpaths": "0.2.1" } } \ No newline at end of file diff --git a/examples/util/source/app.d b/examples/util/source/app.d index 4a407fd..484f2a6 100644 --- a/examples/util/source/app.d +++ b/examples/util/source/app.d @@ -78,7 +78,7 @@ void main(string[] args) writefln("MimeTypes: %(%s %)", df.mimeTypes()); if (df.type() == DesktopFile.Type.Application) { - writeln("Exec: ", df.execString()); + writeln("Exec: ", df.execValue()); writeln("In terminal: ", df.terminal()); writeln("Trusted: ", isTrusted(df.fileName)); } @@ -96,7 +96,7 @@ void main(string[] args) } } else { string[] urls = args[3..$]; - string[] appArgs = df.expandExecString(urls, locale); + string[] appArgs = df.expandExecValue(urls, locale); writefln("Exec: %(%s %)", appArgs); df.startApplication(urls, locale); } diff --git a/source/desktopfile/file.d b/source/desktopfile/file.d index 4715b37..649dbbf 100644 --- a/source/desktopfile/file.d +++ b/source/desktopfile/file.d @@ -15,17 +15,9 @@ module desktopfile.file; public import inilike.file; public import desktopfile.utils; -private @trusted void validateKeyValueImpl(string key, string value) { +private @trusted void validateKeyImpl(string groupName, string key, string value) { if (!isValidKey(key)) { - throw new IniLikeEntryException("key is invalid", key, value); - } -} - -private @trusted string escapeIfNeeded(string value) pure { - if (value.needEscaping()) { - return value.replace("\r", `\r`).replace("\n", `\n`); - } else { - return value; + throw new IniLikeEntryException("key is invalid", groupName, key, value); } } @@ -35,8 +27,8 @@ private @trusted string escapeIfNeeded(string value) pure { final class DesktopAction : IniLikeGroup { protected: - @trusted override void validateKeyValue(string key, string value) const { - validateKeyValueImpl(key, value); + @trusted override void validateKey(string key, string value) const { + validateKeyImpl(groupName(), key, value); } public: package @nogc @safe this(string groupName) nothrow { @@ -47,8 +39,8 @@ public: * Label that will be shown to the user. * Returns: The value associated with "Name" key. */ - @nogc @safe string displayName() const nothrow pure { - return value("Name"); + @safe string displayName() const nothrow pure { + return readEntry("Name"); } /** @@ -56,15 +48,15 @@ public: * Returns: The value associated with "Name" key and given locale. */ @safe string localizedDisplayName(string locale) const nothrow pure { - return localizedValue("Name", locale); + return readEntry("Name", locale); } /** * Icon name of action. * Returns: The value associated with "Icon" key. */ - @nogc @safe string iconName() const nothrow pure { - return value("Icon"); + @safe string iconName() const nothrow pure { + return readEntry("Icon"); } /** @@ -72,14 +64,14 @@ public: * See_Also: iconName */ @safe string localizedIconName(string locale) const nothrow pure { - return localizedValue("Icon", locale); + return readEntry("Icon", locale); } /** - * Returns: The value associated with "Exec" key and given locale. + * Returns: The value associated with "Exec" key. */ - @nogc @safe string execString() const nothrow pure { - return value("Exec"); + @safe string execValue() const nothrow pure { + return readEntry("Exec"); } /** @@ -89,10 +81,16 @@ public: * Throws: * ProcessException on failure to start the process. * DesktopExecException if exec string is invalid. - * See_Also: execString + * See_Also: execValue */ @safe Pid start(string locale = null) const { - return execProcess(expandExecString(execString, null, localizedIconName(locale), localizedDisplayName(locale))); + auto unquotedArgs = unquoteExec(execValue()); + + SpawnParams params; + params.iconName = localizedIconName(locale); + params.displayName = localizedDisplayName(locale); + + return spawnApplication(unquotedArgs, params); } } @@ -188,15 +186,15 @@ final class DesktopEntry : IniLikeGroup * Returns: The value associated with "Name" key. * See_Also: localizedDisplayName */ - @nogc @safe string displayName() const nothrow pure { - return value("Name"); + @safe string displayName() const nothrow pure { + return readEntry("Name"); } /** * Set "Name" to name escaping the value if needed. */ @safe string displayName(string name) { - return this["Name"] = escapeIfNeeded(name); + return writeEntry("Name", name); } /** @@ -204,7 +202,7 @@ final class DesktopEntry : IniLikeGroup * See_Also: displayName */ @safe string localizedDisplayName(string locale) const nothrow pure { - return localizedValue("Name", locale); + return readEntry("Name", locale); } /** @@ -212,22 +210,22 @@ final class DesktopEntry : IniLikeGroup * Returns: The value associated with "GenericName" key. * See_Also: localizedGenericName */ - @nogc @safe string genericName() const nothrow pure { - return value("GenericName"); + @safe string genericName() const nothrow pure { + return readEntry("GenericName"); } /** * Set "GenericName" to name escaping the value if needed. */ @safe string genericName(string name) { - return this["GenericName"] = escapeIfNeeded(name); + return writeEntry("GenericName", name); } /** * Returns: Localized generic name * See_Also: genericName */ @safe string localizedGenericName(string locale) const nothrow pure { - return localizedValue("GenericName", locale); + return readEntry("GenericName", locale); } /** @@ -235,15 +233,15 @@ final class DesktopEntry : IniLikeGroup * Returns: The value associated with "Comment" key. * See_Also: localizedComment */ - @nogc @safe string comment() const nothrow pure { - return value("Comment"); + @safe string comment() const nothrow pure { + return readEntry("Comment"); } /** * Set "Comment" to commentary escaping the value if needed. */ @safe string comment(string commentary) { - return this["Comment"] = escapeIfNeeded(commentary); + return writeEntry("Comment", commentary); } /** @@ -251,42 +249,39 @@ final class DesktopEntry : IniLikeGroup * See_Also: comment */ @safe string localizedComment(string locale) const nothrow pure { - return localizedValue("Comment", locale); + return readEntry("Comment", locale); } /** - * Exec command as it's defined in desktop file. - * Returns: the value associated with "Exec" key (in escaped form). - * Note: To get arguments from exec string use expandExecString. - * See_Also: expandExecString, startApplication, tryExecString + * Exec value of desktop file. + * Returns: the value associated with "Exec" key. + * See_Also: expandExecValue, startApplication, tryExecValue */ - @nogc @safe string execString() const nothrow pure { - return value("Exec"); + @safe string execValue() const nothrow pure { + return readEntry("Exec"); } /** - * Setter for Exec value. - * Params: - * exec = String to set as "Exec" value. Should be properly escaped and quoted. + * Set "Exec" to exec escaping the value if needed. * See_Also: desktopfile.utils.ExecBuilder. */ - @safe string execString(string exec) { - return this["Exec"] = exec; + @safe string execValue(string exec) { + return writeEntry("Exec", exec); } /** * URL to access. * Returns: The value associated with "URL" key. */ - @nogc @safe string url() const nothrow pure { - return value("URL"); + @safe string url() const nothrow pure { + return readEntry("URL"); } /** * Set "URL" to link escaping the value if needed. */ @safe string url(string link) { - return this["URL"] = escapeIfNeeded(link); + return writeEntry("URL", link); } /// @@ -299,34 +294,34 @@ final class DesktopEntry : IniLikeGroup /** * 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: execString + * See_Also: execValue */ - @nogc @safe string tryExecString() const nothrow pure { - return value("TryExec"); + @safe string tryExecValue() const nothrow pure { + return readEntry("TryExec"); } /** - * Set TryExec value. + * Set TryExec value escaping it if needed.. * Throws: * IniLikeEntryException if tryExec is not abolute path nor base name. */ - @safe string tryExecString(string tryExec) { + @safe string tryExecValue(string tryExec) { if (!tryExec.isAbsolute && tryExec.baseName != tryExec) { - throw new IniLikeEntryException("TryExec must be absolute path or base name", "TryExec", tryExec); + throw new IniLikeEntryException("TryExec must be absolute path or base name", groupName(), "TryExec", tryExec); } - return this["TryExec"] = escapeIfNeeded(tryExec); + return writeEntry("TryExec", tryExec); } /// unittest { auto df = new DesktopFile(); - assertNotThrown(df.tryExecString = "base"); + assertNotThrown(df.tryExecValue = "base"); version(Posix) { - assertNotThrown(df.tryExecString = "/absolute/path"); + assertNotThrown(df.tryExecValue = "/absolute/path"); } - assertThrown(df.tryExecString = "not/absolute"); - assertThrown(df.tryExecString = "./relative"); + assertThrown(df.tryExecValue = "not/absolute"); + assertThrown(df.tryExecValue = "./relative"); } /** @@ -336,8 +331,8 @@ final class DesktopEntry : IniLikeGroup * 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/MyLittleRobo/icontheme, icontheme library). */ - @nogc @safe string iconName() const nothrow pure { - return value("Icon"); + @safe string iconName() const nothrow pure { + return readEntry("Icon"); } /** @@ -347,9 +342,9 @@ final class DesktopEntry : IniLikeGroup */ @safe string iconName(string icon) { if (!icon.isAbsolute && icon.baseName != icon) { - throw new IniLikeEntryException("Icon must be absolute path or base name", "Icon", icon); + throw new IniLikeEntryException("Icon must be absolute path or base name", groupName(), "Icon", icon); } - return this["Icon"] = escapeIfNeeded(icon); + return writeEntry("Icon", icon); } /// @@ -369,17 +364,7 @@ final class DesktopEntry : IniLikeGroup * See_Also: iconName */ @safe string localizedIconName(string locale) const nothrow pure { - return localizedValue("Icon", locale); - } - - private @nogc @safe static string boolToString(bool b) nothrow pure { - return b ? "true" : "false"; - } - - unittest - { - assert(boolToString(false) == "false"); - assert(boolToString(true) == "true"); + return readEntry("Icon", locale); } /** @@ -443,8 +428,8 @@ final class DesktopEntry : IniLikeGroup * The working directory to run the program in. * Returns: The value associated with "Path" key. */ - @nogc @safe string workingDirectory() const nothrow pure { - return value("Path"); + @safe string workingDirectory() const nothrow pure { + return readEntry("Path"); } /** @@ -454,14 +439,14 @@ final class DesktopEntry : IniLikeGroup */ @safe string workingDirectory(string wd) { if (!wd.isValidPath) { - throw new IniLikeEntryException("Working directory must be valid path", "Path", wd); + throw new IniLikeEntryException("Working directory must be valid path", groupName(), "Path", wd); } version(Posix) { if (!wd.isAbsolute) { - throw new IniLikeEntryException("Working directory must be absolute path", "Path", wd); + throw new IniLikeEntryException("Working directory must be absolute path", groupName(), "Path", wd); } } - return this["Path"] = escapeIfNeeded(wd); + return writeEntry("Path", wd); } /// @@ -493,14 +478,14 @@ final class DesktopEntry : IniLikeGroup * Returns: The range of multiple values associated with "Categories" key. */ @safe auto categories() const nothrow pure { - return DesktopFile.splitValues(value("Categories")); + return DesktopFile.splitValues(readEntry("Categories")); } /** * 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).escapeIfNeeded(); + string categories(Range)(Range values) if (isInputRange!Range && isSomeString!(ElementType!Range)) { + return writeEntry("Categories", DesktopFile.joinValues(values)); } /** @@ -508,7 +493,7 @@ final class DesktopEntry : IniLikeGroup * Returns: The range of multiple values associated with "Keywords" key. */ @safe auto keywords() const nothrow pure { - return DesktopFile.splitValues(value("Keywords")); + return DesktopFile.splitValues(readEntry("Keywords")); } /** @@ -516,14 +501,14 @@ final class DesktopEntry : IniLikeGroup * Returns: The range of multiple values associated with "Keywords" key in given locale. */ @safe auto localizedKeywords(string locale) const nothrow pure { - return DesktopFile.splitValues(localizedValue("Keywords", locale)); + return DesktopFile.splitValues(readEntry("Keywords", locale)); } /** * 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).escapeIfNeeded(); + string keywords(Range)(Range values) if (isInputRange!Range && isSomeString!(ElementType!Range)) { + return writeEntry("Keywords", DesktopFile.joinValues(values)); } /** @@ -531,14 +516,14 @@ final class DesktopEntry : IniLikeGroup * Returns: The range of multiple values associated with "MimeType" key. */ @safe auto mimeTypes() nothrow const pure { - return DesktopFile.splitValues(value("MimeType")); + return DesktopFile.splitValues(readEntry("MimeType")); } /** * 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).escapeIfNeeded(); + string mimeTypes(Range)(Range values) if (isInputRange!Range && isSomeString!(ElementType!Range)) { + return writeEntry("MimeType", DesktopFile.joinValues(values)); } /** @@ -548,14 +533,14 @@ final class DesktopEntry : IniLikeGroup * See_Also: byAction, action */ @safe auto actions() nothrow const pure { - return DesktopFile.splitValues(value("Actions")); + return DesktopFile.splitValues(readEntry("Actions")); } /** * Sets the list of values for "Actions" list. */ - void actions(Range)(Range values) if (isInputRange!Range && isSomeString!(ElementType!Range)) { - this["Actions"] = DesktopFile.joinValues(values).escapeIfNeeded(); + string actions(Range)(Range values) if (isInputRange!Range && isSomeString!(ElementType!Range)) { + return writeEntry("Actions", DesktopFile.joinValues(values)); } /** @@ -563,12 +548,12 @@ final class DesktopEntry : IniLikeGroup * Returns: The range of multiple values associated with "OnlyShowIn" key. */ @safe auto onlyShowIn() nothrow const pure { - return DesktopFile.splitValues(value("OnlyShowIn")); + return DesktopFile.splitValues(readEntry("OnlyShowIn")); } ///setter - void onlyShowIn(Range)(Range values) if (isInputRange!Range && isSomeString!(ElementType!Range)) { - this["OnlyShowIn"] = DesktopFile.joinValues(values).escapeIfNeeded(); + string onlyShowIn(Range)(Range values) if (isInputRange!Range && isSomeString!(ElementType!Range)) { + return writeEntry("OnlyShowIn", DesktopFile.joinValues(values)); } /** @@ -576,12 +561,12 @@ final class DesktopEntry : IniLikeGroup * Returns: The range of multiple values associated with "NotShowIn" key. */ @safe auto notShowIn() nothrow const pure { - return DesktopFile.splitValues(value("NotShowIn")); + return DesktopFile.splitValues(readEntry("NotShowIn")); } ///setter - void notShowIn(Range)(Range values) if (isInputRange!Range && isSomeString!(ElementType!Range)) { - this["NotShowIn"] = DesktopFile.joinValues(values).escapeIfNeeded(); + string notShowIn(Range)(Range values) if (isInputRange!Range && isSomeString!(ElementType!Range)) { + return writeEntry("NotShowIn", DesktopFile.joinValues(values)); } /** @@ -618,8 +603,8 @@ final class DesktopEntry : IniLikeGroup } protected: - @trusted override void validateKeyValue(string key, string value) const { - validateKeyValueImpl(key, value); + @trusted override void validateKey(string key, string value) const { + validateKeyImpl(groupName(), key, value); } } @@ -663,7 +648,7 @@ protected: @trusted override void addCommentForGroup(string comment, IniLikeGroup currentGroup, string groupName) { if (currentGroup && (_options & ReadOptions.preserveComments)) { - currentGroup.addComment(comment); + currentGroup.appendComment(comment); } } @@ -726,7 +711,7 @@ public: * Reads desktop file from file. * Throws: * $(B ErrnoException) if file could not be opened. - * $(B IniLikeException) if error occured while reading the file or "Desktop Entry" group is missing. + * $(B IniLikeReadException) if error occured while reading the file or "Desktop Entry" group is missing. */ @trusted this(string fileName, ReadOptions options = defaultReadOptions) { this(iniLikeFileReader(fileName), options, fileName); @@ -735,20 +720,20 @@ public: /** * Reads desktop file from IniLikeReader, e.g. acquired from iniLikeFileReader or iniLikeStringReader. * Throws: - * $(B IniLikeException) if error occured while parsing or "Desktop Entry" group is missing. + * $(B IniLikeReadException) if error occured while parsing or "Desktop Entry" group is missing. */ this(IniLikeReader)(IniLikeReader reader, ReadOptions options = defaultReadOptions, string fileName = null) { _options = options; super(reader, fileName); - enforce(_desktopEntry !is null, new IniLikeException("No \"Desktop Entry\" group", 0)); + enforce(_desktopEntry !is null, new IniLikeReadException("No \"Desktop Entry\" group", 0)); _options = ReadOptions.ignoreUnknownGroups | ReadOptions.preserveComments; } /** * Reads desktop file from IniLikeReader, e.g. acquired from iniLikeFileReader or iniLikeStringReader. * Throws: - * $(B IniLikeException) if error occured while parsing or "Desktop Entry" group is missing. + * $(B IniLikeReadException) if error occured while parsing or "Desktop Entry" group is missing. */ this(IniLikeReader)(IniLikeReader reader, string fileName, ReadOptions options = defaultReadOptions) { @@ -777,10 +762,11 @@ public: /** * Removes group by name. You can't remove "Desktop Entry" group with this function. */ - @safe override void removeGroup(string groupName) nothrow { - if (groupName != "Desktop Entry") { - super.removeGroup(groupName); + @safe override bool removeGroup(string groupName) nothrow { + if (groupName == "Desktop Entry") { + return false; } + return super.removeGroup(groupName); } /// @@ -795,10 +781,11 @@ public: assert(df.desktopEntry() !is null); } - @trusted override void addLeadingComment(string line) nothrow { + @trusted override string appendLeadingComment(string line) nothrow { if (_options & ReadOptions.preserveComments) { - super.addLeadingComment(line); + return super.appendLeadingComment(line); } + return null; } /** @@ -967,7 +954,7 @@ Type=Directory`; * Note: If some value of range contains ';' character it's automatically escaped. */ static string joinValues(Range)(Range values) if (isInputRange!Range && isSomeString!(ElementType!Range)) { - auto result = values.filter!( s => !s.empty ).map!( s => s.replace(";", "\\;").escapeIfNeeded() ).joiner(";"); + auto result = values.filter!( s => !s.empty ).map!( s => s.replace(";", "\\;")).joiner(";"); if (result.empty) { return null; } else { @@ -1023,11 +1010,11 @@ Type=Directory`; /** * Expand "Exec" value into the array of command line arguments to use to start the program. * It applies unquoting and unescaping. - * See_Also: execString, desktopfile.utils.expandExecArgs, startApplication + * See_Also: execValue, desktopfile.utils.expandExecArgs, startApplication */ - @safe string[] expandExecString(in string[] urls = null, string locale = null) const + @safe string[] expandExecValue(in string[] urls = null, string locale = null) const { - return .expandExecString(execString(), urls, localizedIconName(locale), localizedDisplayName(locale), fileName()); + return expandExecArgs(unquoteExec(execValue()), urls, locale); } /// @@ -1041,10 +1028,15 @@ 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), DesktopFile.ReadOptions.noOptions, "/example.desktop"); - assert(df.expandExecString(["one", "two"], "ru") == + assert(df.expandExecValue(["one", "two"], "ru") == ["quoted program", "--icon", "folder_ru", "-w", "Программа", "-f", "/example.desktop", "one", "two", "one", "one", "one", "two"]); } + private @safe string[] expandExecArgs(in string[] execArgs, in string[] urls = null, string locale = null) const + { + return .expandExecArgs(execArgs, urls, localizedIconName(locale), localizedDisplayName(locale), fileName()); + } + /** * 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. @@ -1059,16 +1051,24 @@ Icon[ru]=folder_ru`; * Throws: * ProcessException on failure to start the process. * DesktopExecException if exec string is invalid. - * See_Also: desktopfile.utils.getTerminalCommand, start, expandExecString + * See_Also: desktopfile.utils.getTerminalCommand, start, expandExecValue */ @trusted Pid startApplication(in string[] urls = null, string locale = null, lazy const(string)[] terminalCommand = getTerminalCommand) const { - auto args = expandExecString(urls, locale); + 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()) { - auto termCmd = terminalCommand(); - args = termCmd ~ args; + params.terminalCommand = terminalCommand(); } - return execProcess(args, workingDirectory()); + + return spawnApplication(unquotedArgs, params); } /// @@ -1217,7 +1217,7 @@ Name=Notspecified Action`; assert(df.localizedComment("ru_RU") == "Double Commander - кроссплатформенный файловый менеджер."); assert(df.iconName() == "doublecmd"); assert(df.localizedIconName("ru_RU") == "doublecmd_ru"); - assert(df.tryExecString() == "doublecmd"); + assert(df.tryExecValue() == "doublecmd"); assert(!df.terminal()); assert(!df.noDisplay()); assert(!df.hidden()); @@ -1234,7 +1234,7 @@ Name=Notspecified Action`; assert(equal(df.notShowIn(), ["KDE"])); assert(equal(df.byAction().map!(desktopAction => - tuple(desktopAction.displayName(), desktopAction.localizedDisplayName("ru"), desktopAction.iconName(), desktopAction.execString())), + 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); @@ -1253,11 +1253,11 @@ Name=Notspecified Action`; df = new DesktopFile(); df.terminal = true; df.type = DesktopFile.Type.Application; - df.categories = ["Development", "Compilers"]; + 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"])); + assert(equal(df.categories(), ["Development", "Compilers", "One;Two", "Three\\;Four","New\nLine"])); string contents = `# First comment @@ -1274,7 +1274,7 @@ Key=Value assert(equal(df.desktopEntry().byIniLine(), [IniLikeLine.fromKeyValue("Key", "Value")])); //after constructing can add comments - df.addLeadingComment("# Another comment"); + df.appendLeadingComment("# Another comment"); assert(equal(df.leadingComments(), ["# Another comment"])); // and add unknown groups assert(df.addGroup("Some unknown name") !is null); @@ -1287,7 +1287,7 @@ Key=Value `[X-SomeGroup] Key=Value`; - auto thrown = collectException!IniLikeException(new DesktopFile(iniLikeStringReader(contents), DesktopFile.ReadOptions.noOptions)); + auto thrown = collectException!IniLikeReadException(new DesktopFile(iniLikeStringReader(contents), DesktopFile.ReadOptions.noOptions)); assert(thrown !is null); assert(thrown.lineNumber == 0); @@ -1348,10 +1348,10 @@ Name=Name2`; df.genericName = "Program"; assert(df.genericName() == "Program"); df.comment = "Do\nthings"; - assert(df.comment() == `Do\nthings`); + assert(df.comment() == "Do\nthings"); - df.execString = "utilname"; - assert(df.execString() == "utilname"); + df.execValue = "utilname"; + assert(df.execValue() == "utilname"); df.noDisplay = true; assert(df.noDisplay()); diff --git a/source/desktopfile/utils.d b/source/desktopfile/utils.d index 163bcb7..5a06d2e 100644 --- a/source/desktopfile/utils.d +++ b/source/desktopfile/utils.d @@ -80,14 +80,45 @@ package @trusted File getNullStderr() } } -package @trusted Pid execProcess(string[] args, string workingDirectory = null) +/** + * Exception thrown when "Exec" value of DesktopFile or DesktopAction is invalid. + */ +class DesktopExecException : Exception { - version(Windows) { - if (args.length && args[0].baseName == args[0]) { - args[0] = findExecutable(args[0]); - } + this(string msg, string file = __FILE__, size_t line = __LINE__, Throwable next = null) pure nothrow @safe { + super(msg, file, line, next); } +} + +/** + * Parameters for spawnApplication. + */ +struct SpawnParams +{ + /// Urls 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; +} + +private @trusted Pid execProcess(in string[] args, string workingDirectory = null) +{ static if( __VERSION__ < 2066 ) { return spawnProcess(args, getNullStdin(), getNullStdout(), getNullStderr(), null, Config.none); } else { @@ -96,18 +127,44 @@ package @trusted Pid execProcess(string[] args, string workingDirectory = null) } /** - * Exception thrown when "Exec" value of DesktopFile or DesktopAction is invalid. + * Spawn application with given params. + * Params: + * unquotedArgs = Unescaped unquoted arguments parsed from "Exec" value. + * params = Field codes values and other properties to spawn application. + * Throws: + * ProcessException if could not start process. + * DesktopExecException if unquotedArgs is empty. + * See_Also: SpawnParams */ -class DesktopExecException : Exception +@trusted Pid spawnApplication(const(string)[] unquotedArgs, const SpawnParams params) { - this(string msg, string file = __FILE__, size_t line = __LINE__, Throwable next = null) pure nothrow @safe { - super(msg, file, line, next); + if (!unquotedArgs.length) { + throw new DesktopExecException("No arguments. Missing or empty Exec value"); + } + + version(Windows) { + if (unquotedArgs.length && unquotedArgs[0].baseName == unquotedArgs[0]) { + unquotedArgs[0] = findExecutable(unquotedArgs[0]); + } + } + + if (params.terminalCommand) { + unquotedArgs = params.terminalCommand ~ unquotedArgs; + } + + if (params.urls.length && params.allowMultipleInstances && needMultipleInstances(unquotedArgs)) { + Pid pid; + for(size_t i=0; i= 2 && append[$-2] == '\\' && append[$-1] == '\\') { - append = append[0..$-2] ~ value[i]; + if (value[i] == ' ' || value[i] == '\t') { + if (!wasInQuotes && append.length >= 1 && append[$-1] == '\\') { + append = append[0..$-1] ~ value[i]; } else { if (append !is null) { result ~= append; @@ -236,77 +293,68 @@ unittest /// unittest { - assert(equal(unquoteExecString(``), string[].init)); - assert(equal(unquoteExecString(` `), string[].init)); - assert(equal(unquoteExecString(`"" " "`), [``, ` `])); + assert(equal(unquoteExec(``), string[].init)); + assert(equal(unquoteExec(` `), string[].init)); + assert(equal(unquoteExec(`"" " "`), [``, ` `])); - assert(equal(unquoteExecString(`cmd arg1 arg2 arg3 `), [`cmd`, `arg1`, `arg2`, `arg3`])); - assert(equal(unquoteExecString(`"cmd" arg1 arg2 `), [`cmd`, `arg1`, `arg2`])); + assert(equal(unquoteExec(`cmd arg1 arg2 arg3 `), [`cmd`, `arg1`, `arg2`, `arg3`])); + assert(equal(unquoteExec(`"cmd" arg1 arg2 `), [`cmd`, `arg1`, `arg2`])); - assert(equal(unquoteExecString(`"quoted cmd" arg1 "quoted arg" `), [`quoted cmd`, `arg1`, `quoted arg`])); - assert(equal(unquoteExecString(`"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(`"quoted \"cmd\"" arg1 "quoted \"arg\""`), [`quoted "cmd"`, `arg1`, `quoted "arg"`])); - assert(equal(unquoteExecString(`"\\\$" `), [`\$`])); - assert(equal(unquoteExecString(`"\\$" `), [`\$`])); - assert(equal(unquoteExecString(`"\$" `), [`$`])); - assert(equal(unquoteExecString(`"$"`), [`$`])); + assert(equal(unquoteExec(`"\\\$" `), [`\$`])); + assert(equal(unquoteExec(`"\\$" `), [`\$`])); + assert(equal(unquoteExec(`"\$" `), [`$`])); + assert(equal(unquoteExec(`"$"`), [`$`])); - assert(equal(unquoteExecString(`"\\" `), [`\`])); - assert(equal(unquoteExecString(`"\\\\" `), [`\\`])); + assert(equal(unquoteExec(`"\\" `), [`\`])); + assert(equal(unquoteExec(`"\\\\" `), [`\\`])); - assert(equal(unquoteExecString(`'quoted cmd' arg`), [`quoted cmd`, `arg`])); + assert(equal(unquoteExec(`'quoted cmd' arg`), [`quoted cmd`, `arg`])); - assert(equal(unquoteExecString(`test\\ "one""two"\\ more\\ \\ test `), [`test onetwo more test`])); + assert(equal(unquoteExec(`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" + 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(unquoteExecString(`cmd "quoted arg`)); + assertThrown!DesktopExecException(unquoteExec(`cmd "quoted arg`)); } - -/** - * Convenient function used to unquote and unescape Exec value into an array of arguments. - * Note: - * Parsed arguments still may contain field codes and double percent symbols that should be appropriately expanded before passing to spawnProcess. - * Throws: - * DesktopExecException if string can't be unquoted. - * See_Also: - * unquoteExecString, expandExecArgs - */ -@trusted string[] parseExecString(string execString) pure +private @trusted string urlToFilePath(string url) nothrow pure { - return execString.unquoteExecString().map!(s => unescapeValue(s)).array; -} - -/// -unittest -{ - assert(equal(parseExecString(`"quoted cmd" new\nline "quoted\\\\arg" slash\\arg`), ["quoted cmd", "new\nline", `quoted\arg`, `slash\arg`])); + enum protocol = "file://"; + if (url.length > protocol.length && url[0..protocol.length] == protocol) { + return url[protocol.length..$]; + } else { + return url; + } } /** - * Expand Exec arguments (usually returned by parseExecString) replacing field codes with given values, making the array suitable for passing to spawnProcess. Deprecated field codes are ignored. + * Expand Exec arguments (usually returned by unquoteExec) replacing field codes with given values, making the array suitable for passing to spawnProcess. Deprecated field codes are ignored. * Note: * Returned array may be empty and should be checked before passing to spawnProcess. * Params: - * execArgs = array of unquoted and unescaped arguments. - * 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. - * iconName = icon name used to substitute %i field code by --icon iconName. - * displayName = name of application used that inserted in the place of %c field code. - * fileName = name of desktop file that inserted in the place of %k field code. + * 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. + * 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. + * displayName = Name of application used that inserted in the place of %c field code. + * fileName = Name of desktop file that inserted in the place of %k field code. * Throws: * DesktopExecException if command line contains unknown field code. * See_Also: - * parseExecString + * unquoteExec */ -@trusted string[] expandExecArgs(in string[] execArgs, in string[] urls = null, string iconName = null, string displayName = null, string fileName = null) pure +@trusted string[] expandExecArgs(in string[] unquotedArgs, in string[] urls = null, string iconName = null, string displayName = null, string fileName = null) pure { string[] toReturn; - foreach(token; execArgs) { + foreach(token; unquotedArgs) { if (token == "%F") { - toReturn ~= urls; + toReturn ~= urls.map!(url => urlToFilePath(url)).array; } else if (token == "%U") { toReturn ~= urls; } else if (token == "%i") { @@ -335,7 +383,11 @@ unittest case 'f': case 'u': { if (urls.length) { - expand(token, expanded, restPos, i, urls.front); + string arg = urls.front; + if (token[i+1] == 'f') { + arg = urlToFilePath(arg); + } + expand(token, expanded, restPos, i, arg); } else { ignore = true; break loop; @@ -389,10 +441,15 @@ unittest ) == ["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"]); + assert(expandExecArgs(["program path", "%f"], ["file:///usr/share"]) == ["program path", "/usr/share"]); + assert(expandExecArgs(["program path", "%u"], ["file:///usr/share"]) == ["program path", "file:///usr/share"]); assert(expandExecArgs(["program path"], ["one", "two"]) == ["program path"]); assert(expandExecArgs(["program path", "%f"], ["one", "two"]) == ["program path", "one"]); 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", "", ""]); @@ -402,28 +459,42 @@ unittest } /** - * Unquote, unescape Exec string and expand field codes substituting them with appropriate values. - * Throws: - * DesktopExecException if string can't be unquoted, unquoted command line is empty or it has unknown field code. - * See_Also: - * expandExecArgs, parseExecString + * Check if application should be started multiple times to open multiple urls. + * Params: + * execArgs = Array of unescaped and unquoted arguments. + * Returns: true if execArgs have only %f or %u and not %F or %U,. Otherwise false is returned. */ -@trusted string[] expandExecString(string execString, in string[] urls = null, string iconName = null, string displayName = null, string fileName = null) pure +@nogc @trusted bool needMultipleInstances(in string[] execArgs) pure nothrow { - auto execArgs = parseExecString(execString); - if (execArgs.empty) { - throw new DesktopExecException("No arguments. Missing or empty Exec value"); + bool need; + foreach(token; execArgs) { + if (token == "%F" || token == "%U") { + return false; + } + + if (!need) { + for(size_t i=0; i