From 9e8af9d25106b615dabf956305f4ef80496ed412 Mon Sep 17 00:00:00 2001 From: Phil Jones Date: Sun, 24 Jul 2022 12:31:09 +0100 Subject: Add drun mode. This is a pretty simple implementation, but it should work for most use cases. Notably, generic application names aren't used (though that could be added without too much hassle), and neither are keywords (that would be more difficult). --- src/drun.c | 287 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 287 insertions(+) create mode 100644 src/drun.c (limited to 'src/drun.c') diff --git a/src/drun.c b/src/drun.c new file mode 100644 index 0000000..bc755fb --- /dev/null +++ b/src/drun.c @@ -0,0 +1,287 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "drun.h" +#include "history.h" +#include "log.h" +#include "mkdirp.h" +#include "string_vec.h" +#include "xmalloc.h" + +static const char *default_data_dir = ".local/share/"; +static const char *default_cache_dir = ".cache/"; +static const char *cache_basename = "tofi-drun"; + +[[nodiscard("memory leaked")]] +static char *get_cache_path() { + char *cache_name = NULL; + const char *state_path = getenv("XDG_CACHE_HOME"); + if (state_path == NULL) { + const char *home = getenv("HOME"); + if (home == NULL) { + log_error("Couldn't retrieve HOME from environment.\n"); + return NULL; + } + size_t len = strlen(home) + 1 + + strlen(default_cache_dir) + 1 + + strlen(cache_basename) + 1; + cache_name = xmalloc(len); + snprintf( + cache_name, + len, + "%s/%s/%s", + home, + default_cache_dir, + cache_basename); + } else { + size_t len = strlen(state_path) + 1 + + strlen(cache_basename) + 1; + cache_name = xmalloc(len); + snprintf( + cache_name, + len, + "%s/%s", + state_path, + cache_basename); + } + return cache_name; +} + +[[nodiscard("memory leaked")]] +static struct string_vec get_application_paths() { + char *base_paths = NULL; + const char *xdg_data_dirs = getenv("XDG_DATA_DIRS"); + if (xdg_data_dirs == NULL) { + xdg_data_dirs = "/usr/local/share/:/usr/share/"; + } + const char *xdg_data_home = getenv("XDG_DATA_HOME"); + if (xdg_data_home == NULL) { + const char *home = getenv("HOME"); + if (home == NULL) { + log_error("Couldn't retrieve HOME from environment.\n"); + exit(EXIT_FAILURE); + } + size_t len = strlen(home) + 1 + + strlen(default_data_dir) + 1 + + strlen(xdg_data_dirs) + 1; + base_paths = xmalloc(len); + snprintf( + base_paths, + len, + "%s/%s:%s", + home, + default_data_dir, + xdg_data_dirs); + } else { + size_t len = strlen(xdg_data_home) + 1 + + strlen(xdg_data_dirs) + 1; + base_paths = xmalloc(len); + snprintf( + base_paths, + len, + "%s:%s", + xdg_data_home, + xdg_data_dirs); + } + + + /* Append /applications/ to each entry. */ + struct string_vec paths = string_vec_create(); + char *saveptr = NULL; + char *path_entry = strtok_r(base_paths, ":", &saveptr); + while (path_entry != NULL) { + const char *subdir = "applications/"; + size_t len = strlen(path_entry) + 1 + strlen(subdir) + 1; + char *apps = xmalloc(len); + snprintf(apps, len, "%s/%s", path_entry, subdir); + string_vec_add(&paths, apps); + free(apps); + path_entry = strtok_r(NULL, ":", &saveptr); + } + free(base_paths); + + return paths; +} + +static void parse_desktop_file(gpointer key, gpointer value, void *data) +{ + const char *id = key; + const char *path = value; + struct desktop_vec *apps = data; + + desktop_vec_add_file(apps, id, path); +} + +struct desktop_vec drun_generate(void) +{ + log_debug("Retrieving application dirs.\n"); + struct string_vec paths = get_application_paths(); + struct string_vec desktop_files = string_vec_create(); + log_debug("Scanning for .desktop files.\n"); + for (size_t i = 0; i < paths.count; i++) { + const char *path_entry = paths.buf[i].string; + DIR *dir = opendir(path_entry); + if (dir != NULL) { + struct dirent *d; + while ((d = readdir(dir)) != NULL) { + const char *extension = strrchr(d->d_name, '.'); + if (extension == NULL) { + continue; + } + if (strcmp(extension, ".desktop")) { + continue; + } + string_vec_add(&desktop_files, d->d_name); + } + closedir(dir); + } + } + log_debug("Found %zu files.\n", desktop_files.count); + + + log_debug("Parsing .desktop files.\n"); + /* + * The Desktop Entry Specification says that only the highest + * precedence application file with a given ID should be used, so store + * the id / path pairs into a hash table to enforce uniqueness. + */ + GHashTable *id_hash = g_hash_table_new_full(g_str_hash, g_str_equal, free, free); + struct desktop_vec apps = desktop_vec_create(); + for (size_t i = 0; i < paths.count; i++) { + char *path_entry = paths.buf[i].string; + char *tree[2] = { path_entry, NULL }; + size_t prefix_len = strlen(path_entry); + FTS *fts = fts_open(tree, FTS_LOGICAL, NULL); + FTSENT *entry = fts_read(fts); + for (; entry != NULL; entry = fts_read(fts)) { + const char *extension = strrchr(entry->fts_name, '.'); + if (extension == NULL) { + continue; + } + if (strcmp(extension, ".desktop")) { + continue; + } + char *id = xstrdup(&entry->fts_path[prefix_len]); + char *slash = strchr(id, '/'); + while (slash != NULL) { + *slash = '-'; + slash = strchr(slash, '/'); + } + /* + * We're iterating from highest to lowest precedence, + * so only the first file with a given ID should be + * stored. + */ + if (!g_hash_table_contains(id_hash, id)) { + char *path = xstrdup(entry->fts_path); + g_hash_table_insert(id_hash, id, path); + } else { + free(id); + } + + } + fts_close(fts); + } + + /* Parse the remaining files into our desktop_vec. */ + g_hash_table_foreach(id_hash, parse_desktop_file, &apps); + g_hash_table_unref(id_hash); + + log_debug("Found %zu apps.\n", apps.count); + + /* + * It's now safe to sort the desktop file vector, as the rules about + * file precedence have been taken care of. + */ + log_debug("Sorting results.\n"); + desktop_vec_sort(&apps); + + string_vec_destroy(&desktop_files); + string_vec_destroy(&paths); + return apps; +} + +struct desktop_vec drun_generate_cached() +{ + log_debug("Retrieving cache location.\n"); + char *cache_path = get_cache_path(); + + struct stat sb; + if (cache_path == NULL) { + return drun_generate(); + } + + /* If the cache doesn't exist, create it and return */ + errno = 0; + if (stat(cache_path, &sb) == -1) { + if (errno == ENOENT) { + struct desktop_vec apps = drun_generate(); + if (!mkdirp(cache_path)) { + free(cache_path); + return apps; + } + FILE *cache = fopen(cache_path, "wb"); + desktop_vec_save(&apps, cache); + fclose(cache); + free(cache_path); + return apps; + } + free(cache_path); + return drun_generate(); + } + + log_debug("Retrieving application dirs.\n"); + struct string_vec application_path = get_application_paths();; + + /* The cache exists, so check if it's still in date */ + bool out_of_date = false; + for (size_t i = 0; i < application_path.count; i++) { + struct stat path_sb; + if (stat(application_path.buf[i].string, &path_sb) == 0) { + if (path_sb.st_mtim.tv_sec > sb.st_mtim.tv_sec) { + out_of_date = true; + break; + } + } + } + string_vec_destroy(&application_path); + + struct desktop_vec apps; + if (out_of_date) { + log_debug("Cache out of date, updating.\n"); + log_indent(); + apps = drun_generate(); + log_unindent(); + FILE *cache = fopen(cache_path, "wb"); + desktop_vec_save(&apps, cache); + fclose(cache); + } else { + log_debug("Cache up to date, loading.\n"); + FILE *cache = fopen(cache_path, "rb"); + apps = desktop_vec_load(cache); + fclose(cache); + } + free(cache_path); + return apps; +} + +void drun_launch(const char *filename) +{ + GDesktopAppInfo *info = g_desktop_app_info_new_from_filename(filename); + GAppLaunchContext *context = g_app_launch_context_new(); + + if (!g_app_info_launch((GAppInfo *)info, NULL, context, NULL)) { + log_error("Failed to launch %s.\n", filename); + } + + g_object_unref(context); + g_object_unref(info); +} -- cgit v1.2.3