Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support to register AppImage With Additional Actions to the IntegrationManager #138

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions include/appimage/desktop_integration/IntegrationManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@

namespace appimage {
namespace desktop_integration {
/**
* Hold actions to be added to the Desktop Entries while being registered in the system.
* The first value must hold the _action name_ and the second value the _whole action section_
*/
typedef std::vector<std::pair<std::string, std::string>> ApplicationActionList;

class IntegrationManager {
public:
/**
Expand Down Expand Up @@ -51,6 +57,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 "<vendor id>_<appimage_path_md5>_<old_file_name>"
*
* 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.:
*
* ```
* ApplicationActionList additionalApplicationActions = {{"Remove",
azubieta marked this conversation as resolved.
Show resolved Hide resolved
* "[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, ApplicationActionList additionalApplicationActions) const;

/**
* @brief Unregister an AppImage in the system
*
Expand Down
15 changes: 15 additions & 0 deletions src/libappimage/desktop_integration/IntegrationManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,21 @@ namespace appimage {
}
}

void IntegrationManager::registerAppImage(const core::AppImage &appImage,
ApplicationActionList 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
#include <XdgUtils/DesktopEntry/DesktopEntryExecValue.h>
#include <XdgUtils/DesktopEntry/DesktopEntryStringsValue.h>
#include <XdgUtils/DesktopEntry/DesktopEntryKeyPath.h>
#include <map>
#include <utility>

// local
#include "DesktopEntryEditor.h"
Expand Down Expand Up @@ -36,6 +38,8 @@ namespace appimage {

appendVersionToName(desktopEntry);

appendApplicationActions(desktopEntry);

// set identifier
desktopEntry.set("Desktop Entry/X-AppImage-Identifier", identifier);
}
Expand Down Expand Up @@ -114,6 +118,10 @@ namespace appimage {
}
}

void DesktopEntryEditor::setAdditionalApplicationActions(ApplicationActionList additionalApplicationActions) {
DesktopEntryEditor::additionalApplicationActions = std::move(additionalApplicationActions);
}

void DesktopEntryEditor::setExecPaths(XdgUtils::DesktopEntry::DesktopEntry& desktopEntry) {
// Edit "Desktop Entry/Exec"
DesktopEntryExecValue execValue(desktopEntry.get("Desktop Entry/Exec"));
Expand All @@ -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<std::string>(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());
}
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

// local
#include <XdgUtils/DesktopEntry/DesktopEntry.h>
#include <appimage/desktop_integration/IntegrationManager.h>

namespace appimage {
namespace desktop_integration {
Expand Down Expand Up @@ -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(ApplicationActionList additionalApplicationActions);

/**
* Modifies the Desktop Entry according to the set parameters.
* @param desktopEntry
Expand All @@ -50,6 +57,7 @@ namespace appimage {
std::string vendorPrefix;
std::string appImagePath;
std::string appImageVersion;
ApplicationActionList additionalApplicationActions;

/**
* Set Exec and TryExec entries in the 'Desktop Entry' and 'Desktop Action' groups pointing to the
Expand All @@ -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 <entry>
* 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<std::string, std::string> 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);
};

}
Expand Down
10 changes: 10 additions & 0 deletions src/libappimage/desktop_integration/integrator/Integrator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include <appimage/core/AppImage.h>
#include <appimage/desktop_integration/exceptions.h>
#include <appimage/utils/ResourcesExtractor.h>
#include <appimage/desktop_integration/IntegrationManager.h>
#include <constants.h>
#include "utils/Logger.h"
#include "utils/hashlib.h"
Expand Down Expand Up @@ -47,6 +48,7 @@ namespace appimage {
core::AppImage appImage;
bf::path xdgDataHome;
std::string appImageId;
ApplicationActionList additionalApplicationActions;

ResourcesExtractor resourcesExtractor;
DesktopEntry desktopEntry;
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -302,6 +308,10 @@ namespace appimage {
d->deployMimeTypePackages();
d->setExecutionPermission();
}

void Integrator::setAdditionalApplicationActions(ApplicationActionList additionalApplicationActions) {
d->additionalApplicationActions = std::move(additionalApplicationActions);
}
}
}
}
3 changes: 3 additions & 0 deletions src/libappimage/desktop_integration/integrator/Integrator.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

// local
#include <appimage/core/AppImage.h>
#include <appimage/desktop_integration/IntegrationManager.h>
#include "constants.h"

namespace appimage {
Expand Down Expand Up @@ -42,6 +43,8 @@ namespace appimage {
*/
void integrate();

void setAdditionalApplicationActions(ApplicationActionList additionalApplicationActions);

private:
class Priv;
std::unique_ptr<Priv> d; // opaque pointer
Expand Down
35 changes: 34 additions & 1 deletion tests/libappimage/desktop_integration/TestIntegrationManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// library headers
#include <gtest/gtest.h>
#include <boost/filesystem.hpp>
#include <XdgUtils/DesktopEntry/DesktopEntry.h>

// local
#include "appimage/desktop_integration/exceptions.h"
Expand All @@ -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;
Expand All @@ -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);
ApplicationActionList 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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
appimage::desktop_integration::ApplicationActionList 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"));
}