You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
libfm-qt-packaging/src/customactions/fileaction.cpp

616 lines
24 KiB

#include "fileaction.h"
#include <unordered_map>
#include <QDebug>
using namespace std;
namespace Fm {
static const char* desktop_env = nullptr; // current desktop environment
static bool actions_loaded = false; // all actions are loaded?
static unordered_map<const char*, shared_ptr<FileActionObject>, CStrHash, CStrEqual> all_actions; // cache all loaded actions
FileActionObject::FileActionObject() {
}
FileActionObject::FileActionObject(GKeyFile* kf) {
name = CStrPtr{g_key_file_get_locale_string(kf, "Desktop Entry", "Name", nullptr, nullptr)};
tooltip = CStrPtr{g_key_file_get_locale_string(kf, "Desktop Entry", "Tooltip", nullptr, nullptr)};
icon = CStrPtr{g_key_file_get_locale_string(kf, "Desktop Entry", "Icon", nullptr, nullptr)};
desc = CStrPtr{g_key_file_get_locale_string(kf, "Desktop Entry", "Description", nullptr, nullptr)};
GErrorPtr err;
enabled = g_key_file_get_boolean(kf, "Desktop Entry", "Enabled", &err);
if(err) { // key not found, default to true
err.reset();
enabled = true;
}
hidden = g_key_file_get_boolean(kf, "Desktop Entry", "Hidden", nullptr);
suggested_shortcut = CStrPtr{g_key_file_get_string(kf, "Desktop Entry", "SuggestedShortcut", nullptr)};
condition = unique_ptr<FileActionCondition> {new FileActionCondition(kf, "Desktop Entry")};
has_parent = false;
}
FileActionObject::~FileActionObject() {
}
//static
bool FileActionObject::is_plural_exec(const char* exec) {
if(!exec) {
return false;
}
// the first relevent code encountered in Exec parameter
// determines whether the command accepts singular or plural forms
for(int i = 0; exec[i]; ++i) {
char ch = exec[i];
if(ch == '%') {
++i;
ch = exec[i];
switch(ch) {
case 'B':
case 'D':
case 'F':
case 'M':
case 'O':
case 'U':
case 'W':
case 'X':
return true; // plural
case 'b':
case 'd':
case 'f':
case 'm':
case 'o':
case 'u':
case 'w':
case 'x':
return false; // singular
default:
// irrelevent code, skip
break;
}
}
}
return false; // singular form by default
}
std::string FileActionObject::expand_str(const char* templ, const FileInfoList& files, bool for_display, std::shared_ptr<const FileInfo> first_file) {
if(!templ) {
return string{};
}
string result;
if(!first_file) {
first_file = files.front();
}
for(int i = 0; templ[i]; ++i) {
char ch = templ[i];
if(ch == '%') {
++i;
ch = templ[i];
switch(ch) {
case 'b': // (first) basename
if(for_display) {
result += first_file->name();
}
else {
CStrPtr quoted{g_shell_quote(first_file->name().c_str())};
result += quoted.get();
}
break;
case 'B': // space-separated list of basenames
for(auto& fi : files) {
if(for_display) {
result += fi->name();
}
else {
CStrPtr quoted{g_shell_quote(fi->name().c_str())};
result += quoted.get();
}
result += ' ';
}
if(result[result.length() - 1] == ' ') { // remove trailing space
result.erase(result.length() - 1);
}
break;
case 'c': // count of selected items
result += to_string(files.size());
break;
case 'd': { // (first) base directory
// FIXME: should the base dir be a URI?
auto base_dir = first_file->dirPath();
auto str = base_dir.toString();
if(for_display) {
// FIXME: str = Filename.display_name(str);
}
CStrPtr quoted{g_shell_quote(str.get())};
result += quoted.get();
break;
}
case 'D': // space-separated list of base directory of each selected items
for(auto& fi : files) {
auto base_dir = fi->dirPath();
auto str = base_dir.toString();
if(for_display) {
// str = Filename.display_name(str);
}
CStrPtr quoted{g_shell_quote(str.get())};
result += quoted.get();
result += ' ';
}
if(result[result.length() - 1] == ' ') { // remove trailing space
result.erase(result.length() - 1);
}
break;
case 'f': { // (first) file name
auto filename = first_file->path().toString();
if(for_display) {
// filename = Filename.display_name(filename);
}
CStrPtr quoted{g_shell_quote(filename.get())};
result += quoted.get();
break;
}
case 'F': // space-separated list of selected file names
for(auto& fi : files) {
auto filename = fi->path().toString();
if(for_display) {
// filename = Filename.display_name(filename);
}
CStrPtr quoted{g_shell_quote(filename.get())};
result += quoted.get();
result += ' ';
}
if(result[result.length() - 1] == ' ') { // remove trailing space
result.erase(result.length() - 1);
}
break;
case 'h': // hostname of the (first) URI
// FIXME: how to support this correctly?
// FIXME: currently we pass g_get_host_name()
result += g_get_host_name();
break;
case 'm': // mimetype of the (first) selected item
result += first_file->mimeType()->name();
break;
case 'M': // space-separated list of the mimetypes of the selected items
for(auto& fi : files) {
result += fi->mimeType()->name();
result += ' ';
}
break;
case 'n': // username of the (first) URI
// FIXME: how to support this correctly?
result += g_get_user_name();
break;
case 'o': // no-op operator which forces a singular form of execution when specified as first parameter,
case 'O': // no-op operator which forces a plural form of execution when specified as first parameter,
break;
case 'p': // port number of the (first) URI
// FIXME: how to support this correctly?
// result.append("0");
break;
case 's': // scheme of the (first) URI
result += first_file->path().uriScheme().get();
break;
case 'u': // (first) URI
result += first_file->path().uri().get();
break;
case 'U': // space-separated list of selected URIs
for(auto& fi : files) {
result += fi->path().uri().get();
result += ' ';
}
if(result[result.length() - 1] == ' ') { // remove trailing space
result.erase(result.length() - 1);
}
break;
case 'w': { // (first) basename without the extension
auto basename = first_file->name();
int pos = basename.rfind('.');
// FIXME: handle non-UTF8 filenames
if(pos != -1) {
basename.erase(pos, string::npos);
}
CStrPtr quoted{g_shell_quote(basename.c_str())};
result += quoted.get();
break;
}
case 'W': // space-separated list of basenames without their extension
for(auto& fi : files) {
auto basename = fi->name();
int pos = basename.rfind('.');
// FIXME: for_display ? Shell.quote(str) : str);
if(pos != -1) {
basename.erase(pos, string::npos);
}
CStrPtr quoted{g_shell_quote(basename.c_str())};
result += quoted.get();
result += ' ';
}
if(result[result.length() - 1] == ' ') { // remove trailing space
result.erase(result.length() - 1);
}
break;
case 'x': { // (first) extension
auto basename = first_file->name();
int pos = basename.rfind('.');
const char* ext = "";
if(pos >= 0) {
ext = basename.c_str() + pos + 1;
}
CStrPtr quoted{g_shell_quote(ext)};
result += quoted.get();
break;
}
case 'X': // space-separated list of extensions
for(auto& fi : files) {
auto basename = fi->name();
int pos = basename.rfind('.');
const char* ext = "";
if(pos >= 0) {
ext = basename.c_str() + pos + 1;
}
CStrPtr quoted{g_shell_quote(ext)};
result += quoted.get();
result += ' ';
}
if(result[result.length() - 1] == ' ') { // remove trailing space
result.erase(result.length() - 1);
}
break;
case '%': // the % character
result += '%';
break;
case '\0':
break;
}
}
else {
result += ch;
}
}
return result;
}
FileAction::FileAction(GKeyFile* kf): FileActionObject{kf}, target{FILE_ACTION_TARGET_CONTEXT} {
type = FileActionType::ACTION;
GErrorPtr err;
if(g_key_file_get_boolean(kf, "Desktop Entry", "TargetContext", &err)) { // default to true
target |= FILE_ACTION_TARGET_CONTEXT;
}
else if(!err) { // error means the key is abscent
target &= ~FILE_ACTION_TARGET_CONTEXT;
}
if(g_key_file_get_boolean(kf, "Desktop Entry", "TargetLocation", nullptr)) {
target |= FILE_ACTION_TARGET_LOCATION;
}
if(g_key_file_get_boolean(kf, "Desktop Entry", "TargetToolbar", nullptr)) {
target |= FILE_ACTION_TARGET_TOOLBAR;
}
toolbar_label = CStrPtr{g_key_file_get_locale_string(kf, "Desktop Entry", "ToolbarLabel", nullptr, nullptr)};
auto profile_names = CStrArrayPtr{g_key_file_get_string_list(kf, "Desktop Entry", "Profiles", nullptr, nullptr)};
if(profile_names != nullptr) {
for(auto profile_name = profile_names.get(); *profile_name; ++profile_name) {
// stdout.printf("%s", profile);
profiles.push_back(make_shared<FileActionProfile>(kf, *profile_name));
}
}
}
std::shared_ptr<FileActionProfile> FileAction::match(const FileInfoList& files) const {
//qDebug() << "FileAction.match: " << id.get();
if(hidden || !enabled) {
return nullptr;
}
if(!condition->match(files)) {
return nullptr;
}
for(const auto& profile : profiles) {
if(profile->match(files)) {
//qDebug() << " profile matched!\n\n";
return profile;
}
}
// stdout.printf("\n");
return nullptr;
}
FileActionMenu::FileActionMenu(GKeyFile* kf): FileActionObject{kf} {
type = FileActionType::MENU;
items_list = CStrArrayPtr{g_key_file_get_string_list(kf, "Desktop Entry", "ItemsList", nullptr, nullptr)};
}
bool FileActionMenu::match(const FileInfoList& files) const {
// stdout.printf("FileActionMenu.match: %s\n", id);
if(hidden || !enabled) {
return false;
}
if(!condition->match(files)) {
return false;
}
// stdout.printf("menu matched!: %s\n\n", id);
return true;
}
void FileActionMenu::cache_children(const FileInfoList& files, const char** items_list) {
for(; *items_list; ++items_list) {
const char* item_id_prefix = *items_list;
size_t len = strlen(item_id_prefix);
if(item_id_prefix[0] == '[' && item_id_prefix[len - 1] == ']') {
// runtime dynamic item list
char* output;
int exit_status;
string prefix{item_id_prefix + 1, len - 2}; // skip [ and ]
auto command = expand_str(prefix.c_str(), files);
if(g_spawn_command_line_sync(command.c_str(), &output, nullptr, &exit_status, nullptr) && exit_status == 0) {
CStrArrayPtr item_ids{g_strsplit(output, ";", -1)};
g_free(output);
cache_children(files, (const char**)item_ids.get());
}
}
else if(strcmp(item_id_prefix, "SEPARATOR") == 0) {
// separator item
cached_children.push_back(nullptr);
}
else {
CStrPtr item_id{g_strconcat(item_id_prefix, ".desktop", nullptr)};
auto it = all_actions.find(item_id.get());
if(it != all_actions.end()) {
auto child_action = it->second;
child_action->has_parent = true;
cached_children.push_back(child_action);
// stdout.printf("add child: %s to menu: %s\n", item_id, id);
}
}
}
}
std::shared_ptr<FileActionItem> FileActionItem::fromActionObject(std::shared_ptr<FileActionObject> action_obj, const FileInfoList& files) {
std::shared_ptr<FileActionItem> item;
if(action_obj->type == FileActionType::MENU) {
auto menu = static_pointer_cast<FileActionMenu>(action_obj);
if(menu->match(files)) {
item = make_shared<FileActionItem>(menu, files);
// eliminate empty menus
if(item->children.empty()) {
item = nullptr;
}
}
}
else {
// handle profiles here
auto action = static_pointer_cast<FileAction>(action_obj);
auto profile = action->match(files);
if(profile) {
item = make_shared<FileActionItem>(action, profile, files);
}
}
return item;
}
FileActionItem::FileActionItem(std::shared_ptr<FileAction> _action, std::shared_ptr<FileActionProfile> _profile, const FileInfoList& files):
FileActionItem{static_pointer_cast<FileActionObject>(_action), files} {
profile = _profile;
}
FileActionItem::FileActionItem(std::shared_ptr<FileActionMenu> menu, const FileInfoList& files):
FileActionItem{static_pointer_cast<FileActionObject>(menu), files} {
for(auto& action_obj : menu->cached_children) {
if(action_obj == nullptr) { // separator
children.push_back(nullptr);
}
else { // action item or menu
auto subitem = fromActionObject(action_obj, files);
if(subitem != nullptr) {
children.push_back(subitem);
}
}
}
}
FileActionItem::FileActionItem(std::shared_ptr<FileActionObject> _action, const FileInfoList& files) {
action = std::move(_action);
name = FileActionObject::expand_str(action->name.get(), files, true);
desc = FileActionObject::expand_str(action->desc.get(), files, true);
icon = FileActionObject::expand_str(action->icon.get(), files, false);
}
bool FileActionItem::launch(GAppLaunchContext* ctx, const FileInfoList& files, CStrPtr& output) const {
if(action->type == FileActionType::ACTION) {
if(profile != nullptr) {
profile->launch(ctx, files, output);
}
return true;
}
return false;
}
static void load_actions_from_dir(const char* dirname, const char* id_prefix) {
//qDebug() << "loading from: " << dirname << endl;
auto dir = g_dir_open(dirname, 0, nullptr);
if(dir != nullptr) {
for(;;) {
const char* name = g_dir_read_name(dir);
if(name == nullptr) {
break;
}
// found a file in file-manager/actions dir, get its full path
CStrPtr full_path{g_build_filename(dirname, name, nullptr)};
// stdout.printf("\nfound %s\n", full_path);
// see if it's a sub dir
if(g_file_test(full_path.get(), G_FILE_TEST_IS_DIR)) {
// load sub dirs recursively
CStrPtr new_id_prefix;
if(id_prefix) {
new_id_prefix = CStrPtr{g_strconcat(id_prefix, "-", name, nullptr)};
}
load_actions_from_dir(full_path.get(), id_prefix ? new_id_prefix.get() : name);
}
else if(g_str_has_suffix(name, ".desktop")) {
CStrPtr new_id_prefix;
if(id_prefix) {
new_id_prefix = CStrPtr{g_strconcat(id_prefix, "-", name, nullptr)};
}
const char* id = id_prefix ? new_id_prefix.get() : name;
// ensure that it's not already in the cache
if(all_actions.find(id) == all_actions.cend()) {
auto kf = g_key_file_new();
if(g_key_file_load_from_file(kf, full_path.get(), G_KEY_FILE_NONE, nullptr)) {
auto type = CStrPtr{g_key_file_get_string(kf, "Desktop Entry", "Type", nullptr)};
if(!type) {
continue;
}
std::shared_ptr<FileActionObject> action;
if(strcmp(type.get(), "Action") == 0) {
action = static_pointer_cast<FileActionObject>(make_shared<FileAction>(kf));
// stdout.printf("load action: %s\n", id);
}
else if(strcmp(type.get(), "Menu") == 0) {
action = static_pointer_cast<FileActionObject>(make_shared<FileActionMenu>(kf));
// stdout.printf("load menu: %s\n", id);
}
else {
continue;
}
action->setId(id);
all_actions.insert(make_pair(action->id.get(), action)); // add the id/action pair to hash table
// stdout.printf("add to cache %s\n", id);
}
g_key_file_free(kf);
}
else {
// stdout.printf("cache found for action: %s\n", id);
}
}
}
g_dir_close(dir);
}
}
void file_actions_set_desktop_env(const char* env) {
desktop_env = env;
}
static void load_all_actions() {
all_actions.clear();
auto dirs = g_get_system_data_dirs();
for(auto dir = dirs; *dir; ++dir) {
CStrPtr dir_path{g_build_filename(*dir, "file-manager/actions", nullptr)};
load_actions_from_dir(dir_path.get(), nullptr);
}
CStrPtr dir_path{g_build_filename(g_get_user_data_dir(), "file-manager/actions", nullptr)};
load_actions_from_dir(dir_path.get(), nullptr);
actions_loaded = true;
}
bool FileActionItem::compare_items(std::shared_ptr<const FileActionItem> a, std::shared_ptr<const FileActionItem> b)
{
// first get the list of level-zero item names (http://www.nautilus-actions.org/?q=node/377)
static QStringList itemNamesList;
static bool level_zero_checked = false;
if(!level_zero_checked) {
level_zero_checked = true;
auto level_zero = CStrPtr{g_build_filename(g_get_user_data_dir(),
"file-manager/actions/level-zero.directory", nullptr)};
if(g_file_test(level_zero.get(), G_FILE_TEST_IS_REGULAR)) {
GKeyFile* kf = g_key_file_new();
if(g_key_file_load_from_file(kf, level_zero.get(), G_KEY_FILE_NONE, nullptr)) {
auto itemsList = CStrArrayPtr{g_key_file_get_string_list(kf,
"Desktop Entry",
"ItemsList", nullptr, nullptr)};
if(itemsList) {
for(uint i = 0; i < g_strv_length(itemsList.get()); ++i) {
CStrPtr desktop_file_name{g_strconcat(itemsList.get()[i], ".desktop", nullptr)};
auto desktop_file = CStrPtr{g_build_filename(g_get_user_data_dir(),
"file-manager/actions",
desktop_file_name.get(), nullptr)};
GKeyFile* desktop_file_key = g_key_file_new();
if(g_key_file_load_from_file(desktop_file_key, desktop_file.get(), G_KEY_FILE_NONE, nullptr)) {
auto actionName = CStrPtr{g_key_file_get_string(desktop_file_key,
"Desktop Entry",
"Name", NULL)};
if(actionName) {
itemNamesList << QString::fromUtf8(actionName.get());
}
}
g_key_file_free(desktop_file_key);
}
}
}
g_key_file_free(kf);
}
}
if(!itemNamesList.isEmpty()) {
int first = itemNamesList.indexOf(QString::fromStdString(a->get_name()));
int second = itemNamesList.indexOf(QString::fromStdString(b->get_name()));
if(first > -1) {
if(second > -1) {
return (first < second);
}
else {
return true; // list items have priority
}
}
else if(second > -1) {
return false;
}
}
return (a->get_name().compare(b->get_name()) < 0);
}
FileActionItemList FileActionItem::get_actions_for_files(const FileInfoList& files) {
if(!actions_loaded) {
load_all_actions();
}
// Iterate over all actions to establish association between parent menu
// and children actions, and to find out toplevel ones which are not
// attached to any parent menu
for(auto& item : all_actions) {
auto& action_obj = item.second;
// stdout.printf("id = %s\n", action_obj.id);
if(action_obj->type == FileActionType::MENU) { // this is a menu
auto menu = static_pointer_cast<FileActionMenu>(action_obj);
// stdout.printf("menu: %s\n", menu.name);
// associate child items with menus
menu->cache_children(files, (const char**)menu->items_list.get());
}
}
// Output the menus
FileActionItemList items;
for(auto& item : all_actions) {
auto& action_obj = item.second;
// only output toplevel items here
if(action_obj->has_parent == false) { // this is a toplevel item
auto item = FileActionItem::fromActionObject(action_obj, files);
if(item != nullptr) {
items.push_back(item);
}
}
}
// cleanup temporary data cached during menu generation
for(auto& item : all_actions) {
auto& action_obj = item.second;
action_obj->has_parent = false;
if(action_obj->type == FileActionType::MENU) {
auto menu = static_pointer_cast<FileActionMenu>(action_obj);
menu->cached_children.clear();
}
}
std::sort(items.begin(), items.end(), compare_items);
return items;
}
} // namespace Fm