rofi/source/dialogs/run.c

589 lines
17 KiB
C

/*
* rofi
*
* MIT/X11 License
* Copyright © 2013-2021 Qball Cow <qball@gmpclient.org>
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
*/
/**
* \ingroup RUNMode
* @{
*/
/** The log domain of this dialog. */
#define G_LOG_DOMAIN "Dialogs.Run"
#include <config.h>
#include <stdio.h>
#include <stdlib.h>
#include <dirent.h>
#include <errno.h>
#include <limits.h>
#include <signal.h>
#include <string.h>
#include <strings.h>
#include <sys/types.h>
#include <unistd.h>
#include "dialogs/filebrowser.h"
#include "dialogs/run.h"
#include "helper.h"
#include "history.h"
#include "rofi.h"
#include "settings.h"
#include "mode-private.h"
#include "rofi-icon-fetcher.h"
#include "timings.h"
/**
* Name of the history file where previously chosen commands are stored.
*/
#define RUN_CACHE_FILE "rofi-3.runcache"
typedef struct {
char *entry;
uint32_t icon_fetch_uid;
/* Surface holding the icon. */
cairo_surface_t *icon;
} RunEntry;
/**
* The internal data structure holding the private data of the Run Mode.
*/
typedef struct {
/** list of available commands. */
RunEntry *cmd_list;
/** Length of the #cmd_list. */
unsigned int cmd_list_length;
/** Current mode. */
gboolean file_complete;
uint32_t selected_line;
char *old_input;
Mode *completer;
char *old_completer_input;
/** fallback icon */
uint32_t fallback_icon_fetch_uid;
cairo_surface_t *fallback_icon;
} RunModePrivateData;
/**
* @param cmd The cmd to execute
* @param run_in_term Indicate if command should be run in a terminal
*
* Execute command and add to history.
*/
static gboolean exec_cmd(const char *cmd, int run_in_term) {
GError *error = NULL;
if (!cmd || !cmd[0]) {
return FALSE;
}
gsize lf_cmd_size = 0;
gchar *lf_cmd = g_locale_from_utf8(cmd, -1, NULL, &lf_cmd_size, &error);
if (error != NULL) {
g_warning("Failed to convert command to locale encoding: %s",
error->message);
g_error_free(error);
return FALSE;
}
char *path = g_build_filename(cache_dir, RUN_CACHE_FILE, NULL);
RofiHelperExecuteContext context = {.name = NULL};
// FIXME: assume startup notification support for terminals
if (helper_execute_command(NULL, lf_cmd, run_in_term,
run_in_term ? &context : NULL)) {
/**
* This happens in non-critical time (After launching app)
* It is allowed to be a bit slower.
*/
history_set(path, cmd);
g_free(path);
g_free(lf_cmd);
return TRUE;
}
history_remove(path, cmd);
g_free(path);
g_free(lf_cmd);
return FALSE;
}
/**
* @param cmd The command to remove from history
*
* Remove command from history.
*/
static void delete_entry(const RunEntry *cmd) {
char *path = g_build_filename(cache_dir, RUN_CACHE_FILE, NULL);
history_remove(path, cmd->entry);
g_free(path);
}
/**
* @param a The First key to compare
* @param b The second key to compare
* @param data Unused.
*
* Function used for sorting.
*
* @returns returns less then, equal to and greater than zero is a is less than,
* is a match or greater than b.
*/
static int sort_func(const void *a, const void *b, G_GNUC_UNUSED void *data) {
const RunEntry *astr = (const RunEntry *)a;
const RunEntry *bstr = (const RunEntry *)b;
if (astr->entry == NULL && bstr->entry == NULL) {
return 0;
}
if (astr->entry == NULL) {
return 1;
}
if (bstr->entry == NULL) {
return -1;
}
return g_strcmp0(astr->entry, bstr->entry);
}
/**
* External spider to get list of executables.
*/
static RunEntry *get_apps_external(RunEntry *retv, unsigned int *length,
unsigned int num_favorites) {
int fd = execute_generator(config.run_list_command);
if (fd >= 0) {
FILE *inp = fdopen(fd, "r");
if (inp) {
char *buffer = NULL;
size_t buffer_length = 0;
while (getline(&buffer, &buffer_length, inp) > 0) {
int found = 0;
// Filter out line-end.
if (buffer[strlen(buffer) - 1] == '\n') {
buffer[strlen(buffer) - 1] = '\0';
}
// This is a nice little penalty, but doable? time will tell.
// given num_favorites is max 25.
for (unsigned int j = 0; found == 0 && j < num_favorites; j++) {
if (strcasecmp(buffer, retv[j].entry) == 0) {
found = 1;
}
}
if (found == 1) {
continue;
}
// No duplicate, add it.
retv = g_realloc(retv, ((*length) + 2) * sizeof(RunEntry));
retv[(*length)].entry = g_strdup(buffer);
retv[(*length)].icon = NULL;
retv[(*length)].icon_fetch_uid = 0;
(*length)++;
}
if (buffer != NULL) {
free(buffer);
}
if (fclose(inp) != 0) {
g_warning("Failed to close stdout off executor script: '%s'",
g_strerror(errno));
}
}
}
retv[(*length)].entry = NULL;
retv[(*length)].icon = NULL;
retv[(*length)].icon_fetch_uid = 0;
return retv;
}
/**
* Internal spider used to get list of executables.
*/
static RunEntry *get_apps(unsigned int *length) {
GError *error = NULL;
RunEntry *retv = NULL;
unsigned int num_favorites = 0;
char *path;
if (g_getenv("PATH") == NULL) {
return NULL;
}
TICK_N("start");
path = g_build_filename(cache_dir, RUN_CACHE_FILE, NULL);
char **hretv = history_get_list(path, length);
retv = (RunEntry *)g_malloc0((*length + 1) * sizeof(RunEntry));
for (unsigned int i = 0; i < *length; i++) {
retv[i].entry = hretv[i];
}
g_free(hretv);
g_free(path);
// Keep track of how many where loaded as favorite.
num_favorites = (*length);
path = g_strdup(g_getenv("PATH"));
gsize l = 0;
gchar *homedir = g_locale_to_utf8(g_get_home_dir(), -1, NULL, &l, &error);
if (error != NULL) {
g_debug("Failed to convert homedir to UTF-8: %s", error->message);
for (unsigned int i = 0; retv[i].entry != NULL; i++) {
g_free(retv[i].entry);
}
g_free(retv);
g_clear_error(&error);
g_free(homedir);
return NULL;
}
const char *const sep = ":";
char *strtok_savepointer = NULL;
for (const char *dirname = strtok_r(path, sep, &strtok_savepointer);
dirname != NULL; dirname = strtok_r(NULL, sep, &strtok_savepointer)) {
char *fpath = rofi_expand_path(dirname);
DIR *dir = opendir(fpath);
g_debug("Checking path %s for executable.", fpath);
g_free(fpath);
if (dir != NULL) {
struct dirent *dent;
gsize dirn_len = 0;
gchar *dirn = g_locale_to_utf8(dirname, -1, NULL, &dirn_len, &error);
if (error != NULL) {
g_debug("Failed to convert directory name to UTF-8: %s",
error->message);
g_clear_error(&error);
closedir(dir);
continue;
}
gboolean is_homedir = g_str_has_prefix(dirn, homedir);
g_free(dirn);
while ((dent = readdir(dir)) != NULL) {
if (dent->d_type != DT_REG && dent->d_type != DT_LNK &&
dent->d_type != DT_UNKNOWN) {
continue;
}
// Skip dot files.
if (dent->d_name[0] == '.') {
continue;
}
if (is_homedir) {
gchar *full_path = g_build_filename(dirname, dent->d_name, NULL);
gboolean b = g_file_test(full_path, G_FILE_TEST_IS_EXECUTABLE);
g_free(full_path);
if (!b) {
continue;
}
}
gsize name_len;
gchar *name =
g_filename_to_utf8(dent->d_name, -1, NULL, &name_len, &error);
if (error != NULL) {
g_debug("Failed to convert filename to UTF-8: %s", error->message);
g_clear_error(&error);
g_free(name);
continue;
}
// This is a nice little penalty, but doable? time will tell.
// given num_favorites is max 25.
int found = 0;
for (unsigned int j = 0; found == 0 && j < num_favorites; j++) {
if (g_strcmp0(name, retv[j].entry) == 0) {
found = 1;
}
}
if (found == 1) {
g_free(name);
continue;
}
retv = g_realloc(retv, ((*length) + 2) * sizeof(RunEntry));
retv[(*length)].entry = name;
retv[(*length)].icon = NULL;
retv[(*length)].icon_fetch_uid = 0;
retv[(*length) + 1].entry = NULL;
retv[(*length) + 1].icon = NULL;
retv[(*length) + 1].icon_fetch_uid = 0;
(*length)++;
}
closedir(dir);
}
}
g_free(homedir);
// Get external apps.
if (config.run_list_command != NULL && config.run_list_command[0] != '\0') {
retv = get_apps_external(retv, length, num_favorites);
}
// No sorting needed.
if ((*length) == 0) {
return retv;
}
// TODO: check this is still fast enough. (takes 1ms on laptop.)
if ((*length) > num_favorites) {
g_qsort_with_data(&(retv[num_favorites]), (*length) - num_favorites,
sizeof(RunEntry), sort_func, NULL);
}
g_free(path);
unsigned int removed = 0;
for (unsigned int index = num_favorites; index < ((*length) - 1); index++) {
if (g_strcmp0(retv[index].entry, retv[index + 1].entry) == 0) {
g_free(retv[index].entry);
retv[index].entry = NULL;
removed++;
}
}
if ((*length) > num_favorites) {
g_qsort_with_data(&(retv[num_favorites]), (*length) - num_favorites,
sizeof(RunEntry), sort_func, NULL);
}
// Reduce array length;
(*length) -= removed;
TICK_N("stop");
return retv;
}
static int run_mode_init(Mode *sw) {
if (sw->private_data == NULL) {
RunModePrivateData *pd = g_malloc0(sizeof(*pd));
sw->private_data = (void *)pd;
pd->cmd_list = get_apps(&(pd->cmd_list_length));
pd->completer = NULL;
}
return TRUE;
}
static void run_mode_destroy(Mode *sw) {
RunModePrivateData *rmpd = (RunModePrivateData *)sw->private_data;
if (rmpd != NULL) {
for (unsigned int i = 0; i < rmpd->cmd_list_length; i++) {
g_free(rmpd->cmd_list[i].entry);
if (rmpd->cmd_list[i].icon != NULL) {
cairo_surface_destroy(rmpd->cmd_list[i].icon);
}
}
g_free(rmpd->cmd_list);
g_free(rmpd->old_input);
g_free(rmpd->old_completer_input);
if (rmpd->completer != NULL) {
mode_destroy(rmpd->completer);
g_free(rmpd->completer);
}
g_free(rmpd);
sw->private_data = NULL;
}
}
static unsigned int run_mode_get_num_entries(const Mode *sw) {
const RunModePrivateData *rmpd = (const RunModePrivateData *)sw->private_data;
if (rmpd->file_complete) {
return rmpd->completer->_get_num_entries(rmpd->completer);
}
return rmpd->cmd_list_length;
}
static ModeMode run_mode_result(Mode *sw, int mretv, char **input,
unsigned int selected_line) {
RunModePrivateData *rmpd = (RunModePrivateData *)sw->private_data;
ModeMode retv = MODE_EXIT;
gboolean run_in_term = ((mretv & MENU_CUSTOM_ACTION) == MENU_CUSTOM_ACTION);
if (rmpd->file_complete == TRUE) {
retv = RELOAD_DIALOG;
if ((mretv & (MENU_COMPLETE))) {
g_free(rmpd->old_completer_input);
rmpd->old_completer_input = *input;
*input = NULL;
if (rmpd->selected_line < rmpd->cmd_list_length) {
(*input) = g_strdup(rmpd->old_input);
}
rmpd->file_complete = FALSE;
} else if ((mretv & MENU_CANCEL)) {
retv = MODE_EXIT;
} else {
char *path = NULL;
retv = file_browser_mode_completer(rmpd->completer, mretv, input,
selected_line, &path);
if (retv == MODE_EXIT) {
if (path == NULL) {
exec_cmd(rmpd->cmd_list[rmpd->selected_line].entry, run_in_term);
} else {
char *arg = g_strdup_printf(
"%s '%s'", rmpd->cmd_list[rmpd->selected_line].entry, path);
exec_cmd(arg, run_in_term);
g_free(arg);
}
}
g_free(path);
}
return retv;
}
if ((mretv & MENU_OK) && rmpd->cmd_list[selected_line].entry != NULL) {
if (!exec_cmd(rmpd->cmd_list[selected_line].entry, run_in_term)) {
retv = RELOAD_DIALOG;
}
} else if ((mretv & MENU_CUSTOM_INPUT) && *input != NULL &&
*input[0] != '\0') {
if (!exec_cmd(*input, run_in_term)) {
retv = RELOAD_DIALOG;
}
} else if ((mretv & MENU_ENTRY_DELETE) &&
rmpd->cmd_list[selected_line].entry) {
delete_entry(&(rmpd->cmd_list[selected_line]));
// Clear the list.
retv = RELOAD_DIALOG;
run_mode_destroy(sw);
run_mode_init(sw);
} else if (mretv & MENU_CUSTOM_COMMAND) {
retv = (mretv & MENU_LOWER_MASK);
} else if ((mretv & MENU_COMPLETE)) {
retv = RELOAD_DIALOG;
if (selected_line < rmpd->cmd_list_length) {
rmpd->selected_line = selected_line;
g_free(rmpd->old_input);
rmpd->old_input = g_strdup(*input);
if (*input)
g_free(*input);
*input = g_strdup(rmpd->old_completer_input);
rmpd->completer = create_new_file_browser();
mode_init(rmpd->completer);
rmpd->file_complete = TRUE;
}
}
return retv;
}
static char *_get_display_value(const Mode *sw, unsigned int selected_line,
G_GNUC_UNUSED int *state,
G_GNUC_UNUSED GList **list, int get_entry) {
const RunModePrivateData *rmpd = (const RunModePrivateData *)sw->private_data;
if (rmpd->file_complete) {
return rmpd->completer->_get_display_value(rmpd->completer, selected_line,
state, list, get_entry);
}
return get_entry ? g_strdup(rmpd->cmd_list[selected_line].entry) : NULL;
}
static int run_token_match(const Mode *sw, rofi_int_matcher **tokens,
unsigned int index) {
const RunModePrivateData *rmpd = (const RunModePrivateData *)sw->private_data;
if (rmpd->file_complete) {
return rmpd->completer->_token_match(rmpd->completer, tokens, index);
}
return helper_token_match(tokens, rmpd->cmd_list[index].entry);
}
static char *run_get_message(const Mode *sw) {
RunModePrivateData *pd = sw->private_data;
if (pd->file_complete) {
if (pd->selected_line < pd->cmd_list_length) {
char *msg = mode_get_message(pd->completer);
if (msg) {
char *retv =
g_strdup_printf("File complete for: %s\n%s",
pd->cmd_list[pd->selected_line].entry, msg);
g_free(msg);
return retv;
}
return g_strdup_printf("File complete for: %s",
pd->cmd_list[pd->selected_line].entry);
}
}
return NULL;
}
static cairo_surface_t *fallback_icon(RunModePrivateData *pd, int height) {
if (config.application_fallback_icon) {
// FALLBACK
if (pd->fallback_icon_fetch_uid > 0) {
return rofi_icon_fetcher_get(pd->fallback_icon_fetch_uid);
}
pd->fallback_icon_fetch_uid =
rofi_icon_fetcher_query(config.application_fallback_icon, height);
}
return NULL;
}
static cairo_surface_t *_get_icon(const Mode *sw, unsigned int selected_line,
int height) {
RunModePrivateData *pd = (RunModePrivateData *)mode_get_private_data(sw);
if (pd->file_complete) {
return pd->completer->_get_icon(pd->completer, selected_line, height);
}
g_return_val_if_fail(pd->cmd_list != NULL, NULL);
RunEntry *dr = &(pd->cmd_list[selected_line]);
if (dr->icon_fetch_uid > 0) {
cairo_surface_t *icon = rofi_icon_fetcher_get(dr->icon_fetch_uid);
if (icon) {
return icon;
}
return fallback_icon(pd, height);
}
/** lookup icon */
char **str = g_strsplit(dr->entry, " ", 2);
if (str) {
dr->icon_fetch_uid = rofi_icon_fetcher_query(str[0], height);
g_strfreev(str);
cairo_surface_t *icon = rofi_icon_fetcher_get(dr->icon_fetch_uid);
if (icon) {
return icon;
}
}
return fallback_icon(pd, height);
}
#include "mode-private.h"
Mode run_mode = {.name = "run",
.cfg_name_key = "display-run",
._init = run_mode_init,
._get_num_entries = run_mode_get_num_entries,
._result = run_mode_result,
._destroy = run_mode_destroy,
._token_match = run_token_match,
._get_message = run_get_message,
._get_display_value = _get_display_value,
._get_icon = _get_icon,
._get_completion = NULL,
._preprocess_input = NULL,
.private_data = NULL,
.free = NULL};
/** @}*/