diff options
-rw-r--r-- | completions/tofi | 2 | ||||
-rw-r--r-- | doc/tofi.1.md | 15 | ||||
-rw-r--r-- | doc/tofi.1.scd | 13 | ||||
-rw-r--r-- | doc/tofi.5.md | 7 | ||||
-rw-r--r-- | doc/tofi.5.scd | 6 | ||||
-rw-r--r-- | meson.build | 12 | ||||
-rw-r--r-- | src/compgen.c | 6 | ||||
-rw-r--r-- | src/config.c | 2 | ||||
-rw-r--r-- | src/desktop_vec.c | 362 | ||||
-rw-r--r-- | src/desktop_vec.h | 36 | ||||
-rw-r--r-- | src/drun.c | 287 | ||||
-rw-r--r-- | src/drun.h | 11 | ||||
-rw-r--r-- | src/entry.h | 3 | ||||
-rw-r--r-- | src/history.c | 25 | ||||
-rw-r--r-- | src/history.h | 5 | ||||
-rw-r--r-- | src/main.c | 42 | ||||
-rw-r--r-- | src/string_vec.c | 2 | ||||
-rw-r--r-- | src/tofi.h | 1 |
18 files changed, 818 insertions, 19 deletions
diff --git a/completions/tofi b/completions/tofi index ea9b6de..5a1d392 100644 --- a/completions/tofi +++ b/completions/tofi @@ -38,6 +38,7 @@ _tofi() --horizontal --hide-cursor --history + --drun-launch --hint-font --late-keyboard-init ) @@ -69,3 +70,4 @@ _tofi() } complete -F _tofi tofi complete -F _tofi tofi-run +complete -F _tofi tofi-drun diff --git a/doc/tofi.1.md b/doc/tofi.1.md index 6d2d83f..6de0027 100644 --- a/doc/tofi.1.md +++ b/doc/tofi.1.md @@ -9,6 +9,8 @@ tofi - Tiny dynamic menu for Wayland, inspired by **rofi**(1) and **tofi-run** \[options...\] +**tofi-drun** \[options...\] + **tofi-compgen** # DESCRIPTION @@ -21,6 +23,10 @@ printed to stdout. When invoked via the name **tofi-run**, **tofi** will not accept items on stdin, instead presenting a list of executables in the user's $PATH. +When invoked via the name **tofi-drun**, **tofi** will not accept items +on stdin, and will generate a list of applications from desktop files as +described in the Desktop Entry Specification. + **tofi-compgen** just prints the list of executables used by **tofi-run**. @@ -76,11 +82,20 @@ the form **--key=value**. > Cached list of executables under $PATH, regenerated as necessary. +*$XDG_CACHE_HOME/tofi-drun* + +> Cached list of desktop applications, regenerated as necessary. + *$XDG_STATE_HOME/tofi-history* > Numeric count of commands selected in **tofi-run**, to enable sorting > results by run count. +*$XDG_STATE_HOME/tofi-drun-history* + +> Numeric count of commands selected in **tofi-drun**, to enable sorting +> results by run count. + # AUTHORS Philip Jones \<philj56@gmail.com\> diff --git a/doc/tofi.1.scd b/doc/tofi.1.scd index 0e48639..d15d394 100644 --- a/doc/tofi.1.scd +++ b/doc/tofi.1.scd @@ -10,6 +10,8 @@ tofi - Tiny dynamic menu for Wayland, inspired by *rofi*(1) and *dmenu*(1). *tofi-run* [options...] +*tofi-drun* [options...] + *tofi-compgen* # DESCRIPTION @@ -21,6 +23,10 @@ a graphical selection menu. When a selection is made, it is printed to stdout. When invoked via the name *tofi-run*, *tofi* will not accept items on stdin, instead presenting a list of executables in the user's $PATH. +When invoked via the name *tofi-drun*, *tofi* will not accept items on stdin, +and will generate a list of applications from desktop files as described in the +Desktop Entry Specification. + *tofi-compgen* just prints the list of executables used by *tofi-run*. # OPTIONS @@ -65,10 +71,17 @@ _$XDG_CONFIG_HOME/tofi/config_ _$XDG_CACHE_HOME/tofi-compgen_ Cached list of executables under $PATH, regenerated as necessary. +_$XDG_CACHE_HOME/tofi-drun_ + Cached list of desktop applications, regenerated as necessary. + _$XDG_STATE_HOME/tofi-history_ Numeric count of commands selected in *tofi-run*, to enable sorting results by run count. +_$XDG_STATE_HOME/tofi-drun-history_ + Numeric count of commands selected in *tofi-drun*, to enable sorting + results by run count. + # AUTHORS Philip Jones <philj56@gmail.com> diff --git a/doc/tofi.5.md b/doc/tofi.5.md index f5df5cf..3137dc3 100644 --- a/doc/tofi.5.md +++ b/doc/tofi.5.md @@ -216,6 +216,13 @@ options. > > Default: true +**drun-launch**=*true\|false* + +> If true, directly launch applications on selection when in drun mode. +> Otherwise, just print the path of the .desktop file to stdout. +> +> Default: false + **hint-font**=*true\|false* > Perform font hinting. Only applies when a path to a font has been diff --git a/doc/tofi.5.scd b/doc/tofi.5.scd index 7946cf1..7b0dd9b 100644 --- a/doc/tofi.5.scd +++ b/doc/tofi.5.scd @@ -187,6 +187,12 @@ options. Default: true +*drun-launch*=_true|false_ + If true, directly launch applications on selection when in drun mode. + Otherwise, just print the path of the .desktop file to stdout. + + Default: false + *hint-font*=_true|false_ Perform font hinting. Only applies when a path to a font has been specified via *font-name*. Disabling font hinting speeds up text diff --git a/meson.build b/meson.build index 1c71eef..617e9ef 100644 --- a/meson.build +++ b/meson.build @@ -44,6 +44,12 @@ install_symlink( pointing_to: 'tofi', ) +install_symlink( + 'tofi-drun', + install_dir: get_option('bindir'), + pointing_to: 'tofi', +) + add_project_arguments( [ '-pedantic', @@ -61,6 +67,8 @@ tofi_sources = files( 'src/color.c', 'src/compgen.c', 'src/config.c', + 'src/desktop_vec.c', + 'src/drun.c', 'src/entry.c', 'src/entry_backend/pango.c', 'src/entry_backend/harfbuzz.c', @@ -94,6 +102,8 @@ wayland_client = dependency('wayland-client') wayland_protocols = dependency('wayland-protocols', native: true) wayland_scanner_dep = dependency('wayland-scanner', native: true) xkbcommon = dependency('xkbcommon') +glib = dependency('glib-2.0') +gio_unix = dependency('gio-unix-2.0') # Generate the necessary Wayland headers / sources with wayland-scanner @@ -127,7 +137,7 @@ endforeach executable( 'tofi', tofi_sources, wl_proto_src, wl_proto_headers, - dependencies: [librt, libm, freetype, harfbuzz, cairo, pangocairo, wayland_client, xkbcommon], + dependencies: [librt, libm, freetype, harfbuzz, cairo, pangocairo, wayland_client, xkbcommon, glib, gio_unix], install: true ) diff --git a/src/compgen.c b/src/compgen.c index f656f32..b4bcc4c 100644 --- a/src/compgen.c +++ b/src/compgen.c @@ -12,7 +12,7 @@ #include "string_vec.h" #include "xmalloc.h" -static const char *default_state_dir = ".cache"; +static const char *default_cache_dir = ".cache"; static const char *cache_basename = "tofi-compgen"; [[nodiscard("memory leaked")]] @@ -26,7 +26,7 @@ static char *get_cache_path() { return NULL; } size_t len = strlen(home) + 1 - + strlen(default_state_dir) + 1 + + strlen(default_cache_dir) + 1 + strlen(cache_basename) + 1; cache_name = xmalloc(len); snprintf( @@ -34,7 +34,7 @@ static char *get_cache_path() { len, "%s/%s/%s", home, - default_state_dir, + default_cache_dir, cache_basename); } else { size_t len = strlen(state_path) + 1 diff --git a/src/config.c b/src/config.c index 713cc7e..a2e0f03 100644 --- a/src/config.c +++ b/src/config.c @@ -291,6 +291,8 @@ bool parse_option(struct tofi *tofi, const char *filename, size_t lineno, const tofi->hide_cursor = parse_bool(filename, lineno, value, &err); } else if (strcasecmp(option, "history") == 0) { tofi->use_history = parse_bool(filename, lineno, value, &err); + } else if (strcasecmp(option, "drun-launch") == 0) { + tofi->drun_launch = parse_bool(filename, lineno, value, &err); } else if (strcasecmp(option, "hint-font") == 0) { tofi->window.entry.harfbuzz.disable_hinting = !parse_bool(filename, lineno, value, &err); } else if (strcasecmp(option, "late-keyboard-init") == 0) { diff --git a/src/desktop_vec.c b/src/desktop_vec.c new file mode 100644 index 0000000..d02b2ac --- /dev/null +++ b/src/desktop_vec.c @@ -0,0 +1,362 @@ +#include <glib.h> +#include <stdbool.h> +#include "desktop_vec.h" +#include "log.h" +#include "string_vec.h" +#include "xmalloc.h" + +static bool match_current_desktop(char * const *desktop_list, gsize length); + +[[nodiscard("memory leaked")]] +struct desktop_vec desktop_vec_create(void) +{ + struct desktop_vec vec = { + .count = 0, + .size = 128, + .buf = xcalloc(128, sizeof(*vec.buf)), + }; + return vec; +} + +void desktop_vec_destroy(struct desktop_vec *restrict vec) +{ + for (size_t i = 0; i < vec->count; i++) { + free(vec->buf[i].id); + free(vec->buf[i].name); + free(vec->buf[i].path); + } + free(vec->buf); +} + +void desktop_vec_add( + struct desktop_vec *restrict vec, + const char *restrict id, + const char *restrict name, + const char *restrict path) +{ + if (vec->count == vec->size) { + vec->size *= 2; + vec->buf = xrealloc(vec->buf, vec->size * sizeof(vec->buf[0])); + } + vec->buf[vec->count].id = xstrdup(id); + vec->buf[vec->count].name = xstrdup(name); + vec->buf[vec->count].path = xstrdup(path); + vec->count++; +} + +void desktop_vec_add_file(struct desktop_vec *vec, const char *id, const char *path) +{ + GKeyFile *file = g_key_file_new(); + if (!g_key_file_load_from_file(file, path, G_KEY_FILE_NONE, NULL)) { + log_error("Failed to open %s.\n", path); + return; + } + + const char *group = "Desktop Entry"; + + if (g_key_file_get_boolean(file, group, "Hidden", NULL) + || g_key_file_get_boolean(file, group, "NoDisplay", NULL)) { + goto cleanup_file; + } + + char *name = g_key_file_get_locale_string(file, group, "Name", NULL, NULL); + if (name == NULL) { + log_error("%s: No name found.\n", path); + goto cleanup_file; + } + + gsize length; + gchar **list = g_key_file_get_string_list(file, group, "OnlyShowIn", &length, NULL); + if (list) { + bool match = match_current_desktop(list, length); + g_strfreev(list); + list = NULL; + if (!match) { + goto cleanup_name; + } + } + + list = g_key_file_get_string_list(file, group, "NotShowIn", &length, NULL); + if (list) { + bool match = match_current_desktop(list, length); + g_strfreev(list); + list = NULL; + if (match) { + goto cleanup_name; + } + } + + desktop_vec_add(vec, id, name, path); + +cleanup_name: + free(name); +cleanup_file: + g_key_file_unref(file); +} + +static int cmpdesktopp(const void *restrict a, const void *restrict b) +{ + struct desktop_entry *restrict d1 = (struct desktop_entry *)a; + struct desktop_entry *restrict d2 = (struct desktop_entry *)b; + return strcmp(d1->name, d2->name); +} + +void desktop_vec_sort(struct desktop_vec *restrict vec) +{ + qsort(vec->buf, vec->count, sizeof(vec->buf[0]), cmpdesktopp); +} + +struct desktop_entry *desktop_vec_find(struct desktop_vec *restrict vec, const char *name) +{ + /* + * Explicitly cast away const-ness, as even though we won't modify the + * name, the compiler rightly complains that we might. + */ + struct desktop_entry tmp = { .name = (char *)name }; + return bsearch(&tmp, vec->buf, vec->count, sizeof(vec->buf[0]), cmpdesktopp); +} + +struct desktop_vec desktop_vec_load(FILE *file) +{ + struct desktop_vec vec = desktop_vec_create(); + if (file == NULL) { + return vec; + } + + ssize_t bytes_read; + char *line = NULL; + size_t len; + while ((bytes_read = getline(&line, &len, file)) != -1) { + if (line[bytes_read - 1] == '\n') { + line[bytes_read - 1] = '\0'; + } + char *id = line; + size_t sublen = strlen(line); + char *name = &line[sublen + 1]; + sublen = strlen(name); + char *path = &name[sublen + 1]; + desktop_vec_add(&vec, id, name, path); + } + free(line); + + return vec; +} + +void desktop_vec_save(struct desktop_vec *restrict vec, FILE *restrict file) +{ + /* + * Using null bytes for field separators is a bit odd, but it makes + * parsing very quick and easy. + */ + for (size_t i = 0; i < vec->count; i++) { + fputs(vec->buf[i].id, file); + fputc('\0', file); + fputs(vec->buf[i].name, file); + fputc('\0', file); + fputs(vec->buf[i].path, file); + fputc('\n', file); + } +} + +bool match_current_desktop(char * const *desktop_list, gsize length) +{ + const char *xdg_current_desktop = getenv("XDG_CURRENT_DESKTOP"); + if (xdg_current_desktop == NULL) { + return false; + } + + struct string_vec desktops = string_vec_create(); + + char *saveptr = NULL; + char *tmp = xstrdup(xdg_current_desktop); + char *desktop = strtok_r(tmp, ":", &saveptr); + while (desktop != NULL) { + string_vec_add(&desktops, desktop); + desktop = strtok_r(NULL, ":", &saveptr); + } + free(tmp); + + for (gsize i = 0; i < length; i++) { + if (string_vec_find(&desktops, desktop_list[i])) { + return true; + } + } + + string_vec_destroy(&desktops); + return false; +} + +/* + * Checking-in commented-out code is generally bad practice, but this may be + * needed in the near future. Using the various GKeyFile functions above + * ensures correct behaviour, but is relatively slow (~3-4 ms for 60 desktop + * files). Below are some quick and dirty replacement functions, which work + * correctly except for name localisation, and are ~4x faster. If we go a while + * without needing these, they should be deleted. + */ + +// static char *strip(const char *str) +// { +// size_t start = 0; +// size_t end = strlen(str); +// while (start <= end && isspace(str[start])) { +// start++; +// } +// if (start == end) { +// return NULL; +// } +// while (end > start && (isspace(str[end]) || str[end] == '\0')) { +// end--; +// } +// if (end < start) { +// return NULL; +// } +// if (str[start] == '"' && str[end] == '"' && end > start) { +// start++; +// end--; +// } +// size_t len = end - start + 1; +// char *buf = xcalloc(len + 1, 1); +// strncpy(buf, str + start, len); +// buf[len] = '\0'; +// return buf; +// } +// +// static char *get_option(const char *line) +// { +// size_t index = 0; +// while (line[index] != '=' && index < strlen(line)) { +// index++; +// } +// if (index >= strlen(line)) { +// return NULL; +// } +// index++; +// while (isspace(line[index]) && index < strlen(line)) { +// index++; +// } +// if (index >= strlen(line)) { +// return NULL; +// } +// return strip(&line[index]); +// } +// static bool match_current_desktop2(const char *desktop_list) +// { +// const char *xdg_current_desktop = getenv("XDG_CURRENT_DESKTOP"); +// if (xdg_current_desktop == NULL) { +// return false; +// } +// +// struct string_vec desktops = string_vec_create(); +// +// char *saveptr = NULL; +// char *tmp = xstrdup(xdg_current_desktop); +// char *desktop = strtok_r(tmp, ":", &saveptr); +// while (desktop != NULL) { +// string_vec_add(&desktops, desktop); +// desktop = strtok_r(NULL, ":", &saveptr); +// } +// free(tmp); +// +// /* +// * Technically this will fail if the desktop list contains an escaped +// * \;, but I don't know of any desktops with semicolons in their names. +// */ +// saveptr = NULL; +// tmp = xstrdup(desktop_list); +// desktop = strtok_r(tmp, ";", &saveptr); +// while (desktop != NULL) { +// if (string_vec_find(&desktops, desktop)) { +// return true; +// } +// desktop = strtok_r(NULL, ";", &saveptr); +// } +// free(tmp); +// +// string_vec_destroy(&desktops); +// return false; +// } +// +// static void desktop_vec_add_file2(struct desktop_vec *desktop, const char *id, const char *path) +// { +// FILE *file = fopen(path, "rb"); +// if (!file) { +// log_error("Failed to open %s.\n", path); +// return; +// } +// +// char *line = NULL; +// size_t len; +// bool found = false; +// while(getline(&line, &len, file) > 0) { +// if (!strncmp(line, "[Desktop Entry]", strlen("[Desktop Entry]"))) { +// found = true; +// break; +// } +// } +// if (!found) { +// log_error("%s: No [Desktop Entry] section found.\n", path); +// goto cleanup_file; +// } +// +// /* Please forgive the macro usage. */ +// #define OPTION(key) (!strncmp(line, (key), strlen((key)))) +// char *name = NULL; +// found = false; +// while(getline(&line, &len, file) > 0) { +// /* We've left the [Desktop Entry] section, stop parsing. */ +// if (line[0] == '[') { +// break; +// } +// if (OPTION("Name")) { +// if (line[4] == ' ' || line[4] == '=') { +// found = true; +// name = get_option(line); +// } +// } else if (OPTION("Hidden") +// || OPTION("NoDisplay")) { +// char *option = get_option(line); +// if (option != NULL) { +// bool match = !strcmp(option, "true"); +// free(option); +// if (match) { +// goto cleanup_file; +// } +// } +// } else if (OPTION("OnlyShowIn")) { +// char *option = get_option(line); +// if (option != NULL) { +// bool match = match_current_desktop2(option); +// free(option); +// if (!match) { +// goto cleanup_file; +// } +// } +// } else if (OPTION("NotShowIn")) { +// char *option = get_option(line); +// if (option != NULL) { +// bool match = match_current_desktop2(option); +// free(option); +// if (match) { +// goto cleanup_file; +// } +// } +// } +// } +// if (!found) { +// log_error("%s: No name found.\n", path); +// goto cleanup_name; +// } +// if (name == NULL) { +// log_error("%s: Malformed name key.\n", path); +// goto cleanup_file; +// } +// +// desktop_vec_add(desktop, id, name, path); +// +// cleanup_name: +// free(name); +// cleanup_file: +// free(line); +// fclose(file); +// } diff --git a/src/desktop_vec.h b/src/desktop_vec.h new file mode 100644 index 0000000..9c15ad9 --- /dev/null +++ b/src/desktop_vec.h @@ -0,0 +1,36 @@ +#ifndef DESKTOP_VEC_H +#define DESKTOP_VEC_H + +#include <stddef.h> +#include <stdio.h> + +struct desktop_entry { + char *id; + char *name; + char *path; +}; + +struct desktop_vec { + size_t count; + size_t size; + struct desktop_entry *buf; +}; + +[[nodiscard("memory leaked")]] +struct desktop_vec desktop_vec_create(void); +void desktop_vec_destroy(struct desktop_vec *restrict vec); +void desktop_vec_add( + struct desktop_vec *restrict vec, + const char *restrict id, + const char *restrict name, + const char *restrict path); +void desktop_vec_add_file(struct desktop_vec *desktop, const char *id, const char *path); + +void desktop_vec_sort(struct desktop_vec *restrict vec); +struct desktop_entry *desktop_vec_find(struct desktop_vec *restrict vec, const char *name); + +struct desktop_vec desktop_vec_load(FILE *file); +void desktop_vec_save(struct desktop_vec *restrict vec, FILE *restrict file); + + +#endif /* DESKTOP_VEC_H */ 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 <ctype.h> +#include <dirent.h> +#include <errno.h> +#include <fts.h> +#include <glib.h> +#include <gio/gdesktopappinfo.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#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); +} diff --git a/src/drun.h b/src/drun.h new file mode 100644 index 0000000..2e14599 --- /dev/null +++ b/src/drun.h @@ -0,0 +1,11 @@ +#ifndef DRUN_H +#define DRUN_H + +#include "desktop_vec.h" +#include "string_vec.h" + +struct desktop_vec drun_generate(void); +struct desktop_vec drun_generate_cached(void); +void drun_launch(const char *filename); + +#endif /* DRUN_H */ diff --git a/src/entry.h b/src/entry.h index 4f01252..63aad8a 100644 --- a/src/entry.h +++ b/src/entry.h @@ -7,6 +7,7 @@ #include <cairo/cairo.h> #include <wchar.h> #include "color.h" +#include "desktop_vec.h" #include "history.h" #include "image.h" #include "surface.h" @@ -35,10 +36,12 @@ struct entry { uint32_t selection; struct string_vec results; struct string_vec commands; + struct desktop_vec apps; struct history history; bool use_pango; /* Options */ + bool drun; bool horizontal; uint32_t num_results; int32_t result_spacing; diff --git a/src/history.c b/src/history.c index 7e2a84b..95e7760 100644 --- a/src/history.c +++ b/src/history.c @@ -13,11 +13,18 @@ static const char *default_state_dir = ".local/state"; static const char *histfile_basename = "tofi-history"; +static const char *drun_histfile_basename = "tofi-drun-history"; [[nodiscard("memory leaked")]] static struct history history_create(void); -static char *get_histfile_path() { +static char *get_histfile_path(bool drun) { + const char *basename; + if (drun) { + basename = drun_histfile_basename; + } else { + basename = histfile_basename; + } char *histfile_name = NULL; const char *state_path = getenv("XDG_STATE_HOME"); if (state_path == NULL) { @@ -28,7 +35,7 @@ static char *get_histfile_path() { } size_t len = strlen(home) + 1 + strlen(default_state_dir) + 1 - + strlen(histfile_basename) + 1; + + strlen(basename) + 1; histfile_name = xmalloc(len); snprintf( histfile_name, @@ -36,25 +43,25 @@ static char *get_histfile_path() { "%s/%s/%s", home, default_state_dir, - histfile_basename); + basename); } else { size_t len = strlen(state_path) + 1 - + strlen(histfile_basename) + 1; + + strlen(basename) + 1; histfile_name = xmalloc(len); snprintf( histfile_name, len, "%s/%s", state_path, - histfile_basename); + basename); } return histfile_name; } -struct history history_load() +struct history history_load(bool drun) { struct history vec = history_create(); - char *histfile_name = get_histfile_path(); + char *histfile_name = get_histfile_path(drun); if (histfile_name == NULL) { return vec; } @@ -108,9 +115,9 @@ struct history history_load() return vec; } -void history_save(struct history *history) +void history_save(struct history *history, bool drun) { - char *histfile_name = get_histfile_path(); + char *histfile_name = get_histfile_path(drun); if (histfile_name == NULL) { return; } diff --git a/src/history.h b/src/history.h index 6fedecc..199e028 100644 --- a/src/history.h +++ b/src/history.h @@ -1,6 +1,7 @@ #ifndef HISTORY_H #define HISTORY_H +#include <stdbool.h> #include <stddef.h> struct program { @@ -24,8 +25,8 @@ void history_add(struct history *restrict vec, const char *restrict str); //void history_remove(struct history *restrict vec, const char *restrict str); [[nodiscard("memory leaked")]] -struct history history_load(void); +struct history history_load(bool drun); -void history_save(struct history *history); +void history_save(struct history *history, bool drun); #endif /* HISTORY_H */ @@ -16,6 +16,7 @@ #include <xkbcommon/xkbcommon.h> #include "tofi.h" #include "compgen.h" +#include "drun.h" #include "config.h" #include "entry.h" #include "image.h" @@ -160,7 +161,7 @@ static void wl_keyboard_key( sizeof(buf)); wchar_t ch; mbtowc(&ch, buf, sizeof(buf)); - if (len > 0 && iswprint(ch) && !iswblank(ch)) { + if (len > 0 && iswprint(ch) && (tofi->window.entry.drun || !iswblank(ch))) { if (entry->input_length < N_ELEM(entry->input) - 1) { entry->input[entry->input_length] = ch; entry->input_length++; @@ -686,6 +687,7 @@ static void usage() " --hide-cursor <true|false> Hide the cursor.\n" " --horizontal <true|false> List results horizontally.\n" " --history <true|false> Sort results by number of usages.\n" +" --drun-launch <true|false> Launch apps directly in drun mode.\n" " --hint-font <true|false> Perform font hinting.\n" " --late-keyboard-init (EXPERIMENTAL) Delay keyboard\n" " initialisation until after the first\n" @@ -726,6 +728,7 @@ const struct option long_options[] = { {"horizontal", required_argument, NULL, 0}, {"hide-cursor", required_argument, NULL, 0}, {"history", required_argument, NULL, 0}, + {"drun-launch", required_argument, NULL, 0}, {"hint-font", required_argument, NULL, 0}, {"output", required_argument, NULL, 'o'}, {"late-keyboard-init", no_argument, NULL, 'k'}, @@ -1012,6 +1015,7 @@ int main(int argc, char *argv[]) /* * If we were invoked as tofi-run, generate the command list. + * If we were invoked as tofi-drun, generate the desktop app list. * Otherwise, just read standard input. */ if (strstr(argv[0], "-run")) { @@ -1020,6 +1024,19 @@ int main(int argc, char *argv[]) tofi.window.entry.commands = compgen_cached(); log_unindent(); log_debug("Command list generated.\n"); + } else if (strstr(argv[0], "-drun")) { + log_debug("Generating desktop app list.\n"); + log_indent(); + tofi.window.entry.drun = true; + struct desktop_vec apps = drun_generate_cached(); + struct string_vec commands = string_vec_create(); + for (size_t i = 0; i < apps.count; i++) { + string_vec_add(&commands, apps.buf[i].name); + } + tofi.window.entry.commands = commands; + tofi.window.entry.apps = apps; + log_unindent(); + log_debug("App list generated.\n"); } else { char *line = NULL; size_t n = 0; @@ -1035,7 +1052,7 @@ int main(int argc, char *argv[]) tofi.use_history = false; } if (tofi.use_history) { - tofi.window.entry.history = history_load(); + tofi.window.entry.history = history_load(tofi.window.entry.drun); compgen_history_sort(&tofi.window.entry.commands, &tofi.window.entry.history); } tofi.window.entry.results = string_vec_copy(&tofi.window.entry.commands); @@ -1187,12 +1204,26 @@ int main(int argc, char *argv[]) tofi.submit = false; if (tofi.window.entry.results.count > 0) { uint32_t selection = tofi.window.entry.selection; - printf("%s\n", tofi.window.entry.results.buf[selection].string); + char *res = tofi.window.entry.results.buf[selection].string; + if (tofi.window.entry.drun) { + struct desktop_entry *app = desktop_vec_find(&tofi.window.entry.apps, res); + if (app == NULL) { + log_error("Couldn't find application file! This shouldn't happen.\n"); + } else { + res = app->path; + } + }; + if (tofi.window.entry.drun && tofi.drun_launch) { + drun_launch(res); + } else { + printf("%s\n", res); + } if (tofi.use_history) { history_add( &tofi.window.entry.history, tofi.window.entry.results.buf[selection].string); - history_save(&tofi.window.entry.history); + history_save(&tofi.window.entry.history, + tofi.window.entry.drun); } break; } @@ -1235,6 +1266,9 @@ int main(int argc, char *argv[]) xkb_keymap_unref(tofi.xkb_keymap); xkb_context_unref(tofi.xkb_context); wl_registry_destroy(tofi.wl_registry); + if (tofi.window.entry.drun) { + desktop_vec_destroy(&tofi.window.entry.apps); + } string_vec_destroy(&tofi.window.entry.commands); string_vec_destroy(&tofi.window.entry.results); if (tofi.use_history) { diff --git a/src/string_vec.c b/src/string_vec.c index 8806705..c337c61 100644 --- a/src/string_vec.c +++ b/src/string_vec.c @@ -79,6 +79,8 @@ void string_vec_add(struct string_vec *restrict vec, const char *restrict str) vec->buf = xrealloc(vec->buf, vec->size * sizeof(vec->buf[0])); } vec->buf[vec->count].string = xstrdup(str); + vec->buf[vec->count].search_score = 0; + vec->buf[vec->count].history_score = 0; vec->count++; } @@ -64,6 +64,7 @@ struct tofi { bool hide_cursor; bool use_history; bool late_keyboard_init; + bool drun_launch; char target_output_name[MAX_OUTPUT_NAME_LEN]; }; |