summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--completions/tofi2
-rw-r--r--doc/tofi.1.md15
-rw-r--r--doc/tofi.1.scd13
-rw-r--r--doc/tofi.5.md7
-rw-r--r--doc/tofi.5.scd6
-rw-r--r--meson.build12
-rw-r--r--src/compgen.c6
-rw-r--r--src/config.c2
-rw-r--r--src/desktop_vec.c362
-rw-r--r--src/desktop_vec.h36
-rw-r--r--src/drun.c287
-rw-r--r--src/drun.h11
-rw-r--r--src/entry.h3
-rw-r--r--src/history.c25
-rw-r--r--src/history.h5
-rw-r--r--src/main.c42
-rw-r--r--src/string_vec.c2
-rw-r--r--src/tofi.h1
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 */
diff --git a/src/main.c b/src/main.c
index 96140f5..f0aaac3 100644
--- a/src/main.c
+++ b/src/main.c
@@ -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++;
}
diff --git a/src/tofi.h b/src/tofi.h
index 7ad202c..4ad9f75 100644
--- a/src/tofi.h
+++ b/src/tofi.h
@@ -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];
};