diff --git a/include/appimage/desktop_integration/IntegrationManager.h b/include/appimage/desktop_integration/IntegrationManager.h index 0edcd9c7..a845f7e8 100644 --- a/include/appimage/desktop_integration/IntegrationManager.h +++ b/include/appimage/desktop_integration/IntegrationManager.h @@ -4,12 +4,14 @@ #include #include #include +#include // local #include #include #include + namespace appimage { namespace desktop_integration { class IntegrationManager { @@ -51,6 +53,28 @@ namespace appimage { */ void registerAppImage(const core::AppImage& appImage) const; + /** + * @brief Register an AppImage in the system adding custom desktop entry actions + * + * Extract the application main desktop entry, icons and mime type packages. Modifies their content to + * properly match the AppImage file location and deploy them into the use XDG_DATA_HOME appending a + * prefix to each file. Such prefix is composed as "__" + * + * The desktop entry actions must follow the specification for additional application actions at https://specifications.freedesktop.org/desktop-entry-spec/latest/ar01s11.html. + * The map key should be the action identifier and the value the action fields in a plain string i.e.: + * + * ``` + * std::unordered_map additionalApplicationActions = {{"Remove", + * "[Desktop Action Remove]\n" + * "Name=\"Remove application\"\n" + * "Icon=remove\n" + * "Exec=remove-appimage-helper /path/to/the/AppImage\n"}}; + *``` + * @param appImage + * @param additionalApplicationActions desktop entry actions to be added. + */ + void registerAppImage(const core::AppImage& appImage, std::unordered_map additionalApplicationActions) const; + /** * @brief Unregister an AppImage in the system * diff --git a/src/libappimage/desktop_integration/IntegrationManager.cpp b/src/libappimage/desktop_integration/IntegrationManager.cpp index bd098a6f..24c5edc7 100644 --- a/src/libappimage/desktop_integration/IntegrationManager.cpp +++ b/src/libappimage/desktop_integration/IntegrationManager.cpp @@ -79,6 +79,21 @@ namespace appimage { } } + void IntegrationManager::registerAppImage(const core::AppImage &appImage, + std::unordered_map additionalApplicationActions) const { + try { + integrator::Integrator i(appImage, d->xdgDataHome); + i.setAdditionalApplicationActions(additionalApplicationActions); + i.integrate(); + } catch (...) { + // Remove any file created during the integration process + unregisterAppImage(appImage.getPath()); + + // Rethrow + throw; + } + } + bool IntegrationManager::isARegisteredAppImage(const std::string& appImagePath) const { // Generate AppImage Id const auto& appImageId = d->generateAppImageId(appImagePath); diff --git a/src/libappimage/desktop_integration/integrator/DesktopEntryEditor.cpp b/src/libappimage/desktop_integration/integrator/DesktopEntryEditor.cpp index 71728a51..ab2c123d 100644 --- a/src/libappimage/desktop_integration/integrator/DesktopEntryEditor.cpp +++ b/src/libappimage/desktop_integration/integrator/DesktopEntryEditor.cpp @@ -7,6 +7,8 @@ #include #include #include +#include +#include // local #include "DesktopEntryEditor.h" @@ -36,6 +38,8 @@ namespace appimage { appendVersionToName(desktopEntry); + appendApplicationActions(desktopEntry); + // set identifier desktopEntry.set("Desktop Entry/X-AppImage-Identifier", identifier); } @@ -114,6 +118,10 @@ namespace appimage { } } + void DesktopEntryEditor::setAdditionalApplicationActions(std::unordered_map additionalApplicationActions) { + DesktopEntryEditor::additionalApplicationActions = std::move(additionalApplicationActions); + } + void DesktopEntryEditor::setExecPaths(XdgUtils::DesktopEntry::DesktopEntry& desktopEntry) { // Edit "Desktop Entry/Exec" DesktopEntryExecValue execValue(desktopEntry.get("Desktop Entry/Exec")); @@ -133,6 +141,30 @@ namespace appimage { desktopEntry.set(keyPath, actionExecValue.dump()); } } + + void DesktopEntryEditor::appendApplicationActions(XdgUtils::DesktopEntry::DesktopEntry &entry) { + for (auto itr = additionalApplicationActions.begin(); itr != additionalApplicationActions.end(); ++itr) { + try { + // validate correctness of the action specification + std::stringstream stringstream(itr->second); + XdgUtils::DesktopEntry::DesktopEntry action(stringstream); + + // Add action + std::string actionsString = static_cast(entry["Desktop Entry/Actions"]); + DesktopEntryStringsValue actions(actionsString); + + actions.append(itr->first); + entry.set("Desktop Entry/Actions", actions.dump()); + + // Add action definition + for (const auto &path: action.paths()) + entry[path] = action.get(path); + + } catch (const DesktopEntryError &error) { + throw DesktopEntryEditError(std::string("Malformed action: ") + error.what()); + } + } + } } } } diff --git a/src/libappimage/desktop_integration/integrator/DesktopEntryEditor.h b/src/libappimage/desktop_integration/integrator/DesktopEntryEditor.h index 33a499fc..e186e8af 100644 --- a/src/libappimage/desktop_integration/integrator/DesktopEntryEditor.h +++ b/src/libappimage/desktop_integration/integrator/DesktopEntryEditor.h @@ -6,6 +6,7 @@ // local #include +#include namespace appimage { namespace desktop_integration { @@ -39,6 +40,12 @@ namespace appimage { */ void setIdentifier(const std::string& uuid); + /** + * Set the application actions that must be appended to the desktop entry on edit. + * @param additionalApplicationActions + */ + void setAdditionalApplicationActions(std::unordered_map additionalApplicationActions); + /** * Modifies the Desktop Entry according to the set parameters. * @param desktopEntry @@ -50,6 +57,7 @@ namespace appimage { std::string vendorPrefix; std::string appImagePath; std::string appImageVersion; + std::unordered_map additionalApplicationActions; /** * Set Exec and TryExec entries in the 'Desktop Entry' and 'Desktop Action' groups pointing to the @@ -71,6 +79,22 @@ namespace appimage { * If none of both options are valid the names will remain unchanged. */ void appendVersionToName(XdgUtils::DesktopEntry::DesktopEntry& entry); + + /** + * @brief Append the additionalApplicationActions to + * The desktop entry actions must follow the specification for additional application actions at https://specifications.freedesktop.org/desktop-entry-spec/latest/ar01s11.html. + * The map key should be the action identifier and the value the action fields in a plain string i.e.: + * + * std::map applicationActions = {{"Remove", + * "[Desktop Action Remove]\n" + * "Name=\"Remove application\"\n" + * "Icon=remove\n" + * "Exec=remove-appimage-helper /path/to/the/AppImage\n"}}; + * + * + * @param entry + */ + void appendApplicationActions(XdgUtils::DesktopEntry::DesktopEntry& entry); }; } diff --git a/src/libappimage/desktop_integration/integrator/Integrator.cpp b/src/libappimage/desktop_integration/integrator/Integrator.cpp index 86ca32b8..26dfbad3 100644 --- a/src/libappimage/desktop_integration/integrator/Integrator.cpp +++ b/src/libappimage/desktop_integration/integrator/Integrator.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include "utils/Logger.h" #include "utils/hashlib.h" @@ -47,6 +48,7 @@ namespace appimage { core::AppImage appImage; bf::path xdgDataHome; std::string appImageId; + std::unordered_map additionalApplicationActions; ResourcesExtractor resourcesExtractor; DesktopEntry desktopEntry; @@ -151,6 +153,10 @@ namespace appimage { editor.setAppImagePath(appImage.getPath()); // Set the identifier to be used while prefixing the icon files editor.setIdentifier(md5str); + + // Set the additional applications actions to be appended + editor.setAdditionalApplicationActions(additionalApplicationActions); + // Apply changes to the desktop entry editor.edit(entry); } @@ -302,6 +308,10 @@ namespace appimage { d->deployMimeTypePackages(); d->setExecutionPermission(); } + + void Integrator::setAdditionalApplicationActions(std::unordered_map additionalApplicationActions) { + d->additionalApplicationActions = std::move(additionalApplicationActions); + } } } } diff --git a/src/libappimage/desktop_integration/integrator/Integrator.h b/src/libappimage/desktop_integration/integrator/Integrator.h index 835c40ae..8a593c63 100644 --- a/src/libappimage/desktop_integration/integrator/Integrator.h +++ b/src/libappimage/desktop_integration/integrator/Integrator.h @@ -6,6 +6,7 @@ // local #include +#include #include "constants.h" namespace appimage { @@ -33,6 +34,12 @@ namespace appimage { virtual ~Integrator(); + /** + * Sets to be added to the application desktop entry. + * @param additionalApplicationActions + */ + void setAdditionalApplicationActions(std::unordered_map additionalApplicationActions); + /** * @brief Perform the AppImage integration into the Desktop Environment * diff --git a/tests/libappimage/desktop_integration/TestIntegrationManager.cpp b/tests/libappimage/desktop_integration/TestIntegrationManager.cpp index 54762213..7cbbe43a 100644 --- a/tests/libappimage/desktop_integration/TestIntegrationManager.cpp +++ b/tests/libappimage/desktop_integration/TestIntegrationManager.cpp @@ -4,6 +4,7 @@ // library headers #include #include +#include // local #include "appimage/desktop_integration/exceptions.h" @@ -29,7 +30,7 @@ class TestIntegrationManager : public ::testing::Test { bf::remove_all(userDirPath); } - void createStubFile(const bf::path& path, const std::string& content = "") { + void createStubFile(const bf::path &path, const std::string &content = "") { bf::create_directories(path.parent_path()); bf::ofstream f(path); f << content; @@ -52,6 +53,38 @@ TEST_F(TestIntegrationManager, registerAppImage) { ASSERT_TRUE(bf::exists(expectedIconFilePath)); } +TEST_F(TestIntegrationManager, registerAppImageWithAdditionalActions) { + std::string appImagePath = TEST_DATA_DIR "Echo-x86_64.AppImage"; + IntegrationManager manager(userDirPath.string()); + appimage::core::AppImage appImage(appImagePath); + std::unordered_map applicationActions = {{"Remove", + "[Desktop Action Remove]\n" + "Name=\"Remove application\"\n" + "Name[es]=\"Eliminar aplicación\"\n" + "Icon=remove\n" + "Exec=remove-appimage-helper /path/to/the/AppImage\n"}}; + + manager.registerAppImage(appImage, applicationActions); + + std::string md5 = appimage::utils::hashPath(appImagePath.c_str()); + + bf::path expectedDesktopFilePath = userDirPath / ("applications/appimagekit_" + md5 + "-Echo.desktop"); + ASSERT_TRUE(bf::exists(expectedDesktopFilePath)); + + bf::path expectedIconFilePath = + userDirPath / ("icons/hicolor/scalable/apps/appimagekit_" + md5 + "_utilities-terminal.svg"); + ASSERT_TRUE(bf::exists(expectedIconFilePath)); + + std::ifstream fin(expectedDesktopFilePath.string()); + XdgUtils::DesktopEntry::DesktopEntry entry(fin); + + ASSERT_EQ(std::string("Remove;"), entry.get("Desktop Entry/Actions")); + ASSERT_EQ(std::string("\"Remove application\""), entry.get("Desktop Action Remove/Name")); + ASSERT_EQ(std::string("\"Eliminar aplicación\""), entry.get("Desktop Action Remove/Name[es]")); + ASSERT_EQ(std::string("remove"), entry.get("Desktop Action Remove/Icon")); + ASSERT_EQ(std::string("remove-appimage-helper /path/to/the/AppImage"), entry.get("Desktop Action Remove/Exec")); +} + TEST_F(TestIntegrationManager, isARegisteredAppImage) { std::string appImagePath = TEST_DATA_DIR "Echo-x86_64.AppImage"; IntegrationManager manager(userDirPath.string()); @@ -74,11 +107,14 @@ TEST_F(TestIntegrationManager, shallAppImageBeRegistered) { IntegrationManager manager; ASSERT_TRUE(manager.shallAppImageBeRegistered( - appimage::core::AppImage(TEST_DATA_DIR "Echo-x86_64.AppImage"))); + appimage::core::AppImage(TEST_DATA_DIR + "Echo-x86_64.AppImage"))); ASSERT_FALSE(manager.shallAppImageBeRegistered( - appimage::core::AppImage(TEST_DATA_DIR "Echo-no-integrate-x86_64.AppImage"))); + appimage::core::AppImage(TEST_DATA_DIR + "Echo-no-integrate-x86_64.AppImage"))); ASSERT_THROW(manager.shallAppImageBeRegistered( - appimage::core::AppImage(TEST_DATA_DIR "elffile")), appimage::core::AppImageError); + appimage::core::AppImage(TEST_DATA_DIR + "elffile")), appimage::core::AppImageError); } diff --git a/tests/libappimage/desktop_integration/integrator/TestDesktopEntryEditor.cpp b/tests/libappimage/desktop_integration/integrator/TestDesktopEntryEditor.cpp index 55bf533e..887290d2 100644 --- a/tests/libappimage/desktop_integration/integrator/TestDesktopEntryEditor.cpp +++ b/tests/libappimage/desktop_integration/integrator/TestDesktopEntryEditor.cpp @@ -110,3 +110,29 @@ TEST_F(DesktopEntryEditorTests, setIdentifier) { ASSERT_EQ(entry.get("Desktop Entry/X-AppImage-Identifier"), "uuid"); } + +TEST_F(DesktopEntryEditorTests, setAdditionalApplicationActions) { + XdgUtils::DesktopEntry::DesktopEntry entry(originalData); + + DesktopEntryEditor editor; + editor.setVendorPrefix("prefix"); + editor.setIdentifier("uuid"); + editor.setAppImageVersion("0.1.1"); + std::unordered_map applicationActions = {{"Remove", + "[Desktop Action Remove]\n" + "Name=\"Remove application\"\n" + "Name[es]=\"Eliminar aplicación\"\n" + "Icon=remove\n" + "Exec=remove-appimage-helper /path/to/the/AppImage\n"}}; + + editor.setAdditionalApplicationActions(applicationActions); + editor.edit(entry); + + ASSERT_EQ(entry.get("Desktop Entry/X-AppImage-Identifier"), "uuid"); + + ASSERT_EQ(std::string("Gallery;Create;Remove;"), entry.get("Desktop Entry/Actions")); + ASSERT_EQ(std::string("\"Remove application\""), entry.get("Desktop Action Remove/Name")); + ASSERT_EQ(std::string("\"Eliminar aplicación\""), entry.get("Desktop Action Remove/Name[es]")); + ASSERT_EQ(std::string("remove"), entry.get("Desktop Action Remove/Icon")); + ASSERT_EQ(std::string("remove-appimage-helper /path/to/the/AppImage"), entry.get("Desktop Action Remove/Exec")); +}