From 338b0637408f6f6e3c4d3082a6fadaa6929b496c Mon Sep 17 00:00:00 2001 From: wintermute Date: Wed, 20 Nov 2024 02:47:40 +0100 Subject: [PATCH] Feat: fuzzy matching flag -z Adds the option to use fuzzy matching when filtering menu items. Port of https://tools.suckless.org/dmenu/patches/fuzzymatch/dmenu-fuzzymatch-4.6.diff --- client/common/common.c | 9 +++++- client/common/common.h | 1 + lib/bemenu.h | 8 +++++ lib/filter.c | 71 +++++++++++++++++++++++++++++++++++++++--- lib/internal.h | 5 +++ lib/menu.c | 5 +++ man/bemenu.1.scd.in | 3 ++ 7 files changed, 96 insertions(+), 6 deletions(-) diff --git a/client/common/common.c b/client/common/common.c index 198cd053..41d9a3dc 100644 --- a/client/common/common.c +++ b/client/common/common.c @@ -172,6 +172,7 @@ usage(FILE *out, const char *name) " -h, --help display this help and exit.\n" " -v, --version display version.\n" " -i, --ignorecase match items case insensitively.\n" + " -z, --fuzzy enable fuzzy matching.\n" " -F, --filter filter entries for a given string before showing the menu.\n" " -w, --wrap wraps cursor selection.\n" " -l, --list list items vertically down or up with the given number of lines(number of lines down/up). (down (default), up)\n" @@ -272,6 +273,7 @@ do_getopt(struct client *client, int *argc, char **argv[]) { "version", no_argument, 0, 'v' }, { "ignorecase", no_argument, 0, 'i' }, + { "fuzzy", no_argument, 0, 'z' }, { "filter", required_argument, 0, 'F' }, { "wrap", no_argument, 0, 'w' }, { "list", required_argument, 0, 'l' }, @@ -340,7 +342,7 @@ do_getopt(struct client *client, int *argc, char **argv[]) for (optind = 0;;) { int32_t opt; - if ((opt = getopt_long(*argc, *argv, "hviwcl:I:p:P:I:x:bfF:m:H:M:W:B:R:nsCTK", opts, NULL)) < 0) + if ((opt = getopt_long(*argc, *argv, "hvizwcl:I:p:P:I:x:bfF:m:H:M:W:B:R:nsCTK", opts, NULL)) < 0) break; switch (opt) { @@ -353,6 +355,9 @@ do_getopt(struct client *client, int *argc, char **argv[]) case 'i': client->filter_mode = BM_FILTER_MODE_DMENU_CASE_INSENSITIVE; break; + case 'z': + client->fuzzy = true; + break; case 'F': client->initial_filter = optarg; break; @@ -594,6 +599,8 @@ menu_with_options(struct client *client) bm_menu_set_border_size(menu, client->border_size); bm_menu_set_border_radius(menu, client->border_radius); bm_menu_set_key_binding(menu, client->key_binding); + bm_menu_set_fuzzy_mode(menu, client->fuzzy); + if (client->center) { bm_menu_set_align(menu, BM_ALIGN_CENTER); diff --git a/client/common/common.h b/client/common/common.h index e5ed9ab2..1374f91b 100644 --- a/client/common/common.h +++ b/client/common/common.h @@ -46,6 +46,7 @@ struct client { enum bm_password_mode password; enum bm_key_binding key_binding; char *monitor_name; + bool fuzzy; }; char* cstrcopy(const char *str, size_t size); diff --git a/lib/bemenu.h b/lib/bemenu.h index 740783a3..6b133da9 100644 --- a/lib/bemenu.h +++ b/lib/bemenu.h @@ -934,6 +934,14 @@ BM_PUBLIC enum bm_password_mode bm_menu_get_password(struct bm_menu *menu); */ BM_PUBLIC void bm_menu_set_key_binding(struct bm_menu *menu, enum bm_key_binding); +/** + * Specify whether fuzzy matching should be used. + * + * @param menu bm_menu instance to set the fuzzy mode on. + * @param fuzzy true to enable fuzzy matching. + */ +BM_PUBLIC void bm_menu_set_fuzzy_mode(struct bm_menu *menu, bool fuzzy); + /** @} Properties */ diff --git a/lib/filter.c b/lib/filter.c index 2b4e4655..1d73b2ab 100644 --- a/lib/filter.c +++ b/lib/filter.c @@ -79,17 +79,32 @@ tokenize(struct bm_menu *menu, char ***out_tokv, uint32_t *out_tokc) return NULL; } +struct fuzzy_match { + struct bm_item *item; + int distance; +}; + +static int fuzzy_match_comparator(const void *a, const void *b) { + const struct fuzzy_match *fa = (const struct fuzzy_match *)a; + const struct fuzzy_match *fb = (const struct fuzzy_match *)b; + + return fa->distance - fb->distance; +} + + /** - * Dmenu filterer that accepts substring function. + * Dmenu filterer that accepts substring function or fuzzy match. * * @param menu bm_menu instance to filter. * @param addition This will be 1, if filter is same as previous filter with something appended. * @param fstrstr Substring function used to match items. + * @param fstrncmp String comparison function for exact matches. * @param out_nmemb uint32_t reference to filtered items count. + * @param fuzzy Boolean flag to toggle fuzzy matching. * @return Pointer to array of bm_item pointers. */ static struct bm_item** -filter_dmenu_fun(struct bm_menu *menu, char addition, char* (*fstrstr)(const char *a, const char *b), int (*fstrncmp)(const char *a, const char *b, size_t len), uint32_t *out_nmemb) +filter_dmenu_fun(struct bm_menu *menu, char addition, char* (*fstrstr)(const char *a, const char *b), int (*fstrncmp)(const char *a, const char *b, size_t len), uint32_t *out_nmemb, bool fuzzy) { assert(menu && fstrstr && fstrncmp && out_nmemb); *out_nmemb = 0; @@ -114,13 +129,48 @@ filter_dmenu_fun(struct bm_menu *menu, char addition, char* (*fstrstr)(const cha goto fail; const char *filter = menu->filter ? menu->filter : ""; + if (strlen(filter) == 0) { + goto fail; + } size_t len = (tokc ? strlen(tokv[0]) : 0); uint32_t i, f, e; - for (e = f = i = 0; i < count; ++i) { + f = e = 0; + + struct fuzzy_match *fuzzy_matches = NULL; + + int fuzzy_match_count = 0; + + if (fuzzy && !(fuzzy_matches = calloc(count, sizeof(*fuzzy_matches)))) + goto fail; + + for (i = 0; i < count; ++i) { struct bm_item *item = items[i]; if (!item->text && tokc != 0) continue; + if (fuzzy && tokc && item->text) { + const char *text = item->text; + int sidx = -1, eidx = -1, pidx = 0, text_len = strlen(text), distance = 0; + for (int j = 0; j < text_len && text[j]; ++j) { + if (!fstrncmp(&text[j], &filter[pidx], 1)) { + if (sidx == -1) + sidx = j; + pidx++; + if (pidx == strlen(filter)) { + eidx = j; + break; + } + } + } + if (eidx != -1) { + distance = eidx - sidx + (text_len - eidx + sidx) / 3; + fuzzy_matches[fuzzy_match_count++] = (struct fuzzy_match){ item, distance }; + continue; + } + } + + if (fuzzy) continue; + if (tokc && item->text) { uint32_t t; for (t = 0; t < tokc && fstrstr(item->text, tokv[t]); ++t); @@ -142,12 +192,23 @@ filter_dmenu_fun(struct bm_menu *menu, char addition, char* (*fstrstr)(const cha f++; /* where do all matches end */ } + if (fuzzy && fuzzy_match_count > 0) { + qsort(fuzzy_matches, fuzzy_match_count, sizeof(struct fuzzy_match), fuzzy_match_comparator); + + for (int j = 0; j < fuzzy_match_count; ++j) { + filtered[f++] = fuzzy_matches[j].item; + } + + free(fuzzy_matches); + } + free(buffer); free(tokv); return shrink_list(&filtered, menu->items.count, (*out_nmemb = f)); fail: free(filtered); + free(fuzzy_matches); free(buffer); return NULL; } @@ -163,7 +224,7 @@ filter_dmenu_fun(struct bm_menu *menu, char addition, char* (*fstrstr)(const cha struct bm_item** bm_filter_dmenu(struct bm_menu *menu, bool addition, uint32_t *out_nmemb) { - return filter_dmenu_fun(menu, addition, strstr, strncmp, out_nmemb); + return filter_dmenu_fun(menu, addition, strstr, strncmp, out_nmemb, menu->fuzzy); } /** @@ -177,7 +238,7 @@ bm_filter_dmenu(struct bm_menu *menu, bool addition, uint32_t *out_nmemb) struct bm_item** bm_filter_dmenu_case_insensitive(struct bm_menu *menu, bool addition, uint32_t *out_nmemb) { - return filter_dmenu_fun(menu, addition, bm_strupstr, bm_strnupcmp, out_nmemb); + return filter_dmenu_fun(menu, addition, bm_strupstr, bm_strnupcmp, out_nmemb, menu->fuzzy); } /* vim: set ts=8 sw=4 tw=0 :*/ diff --git a/lib/internal.h b/lib/internal.h index 056f16a6..81da2c08 100644 --- a/lib/internal.h +++ b/lib/internal.h @@ -457,6 +457,11 @@ struct bm_menu { */ char vim_mode; uint32_t vim_last_key; + + /** + * Should fuzzy matching be used? + */ + bool fuzzy; }; /* library.c */ diff --git a/lib/menu.c b/lib/menu.c index 741d7828..9a14adbe 100644 --- a/lib/menu.c +++ b/lib/menu.c @@ -779,6 +779,11 @@ bm_menu_set_key_binding(struct bm_menu *menu, enum bm_key_binding key_binding){ menu->key_binding = key_binding; } +void +bm_menu_set_fuzzy_mode(struct bm_menu *menu, bool fuzzy){ + menu->fuzzy = fuzzy; +} + struct bm_item** bm_menu_get_selected_items(const struct bm_menu *menu, uint32_t *out_nmemb) { diff --git a/man/bemenu.1.scd.in b/man/bemenu.1.scd.in index ea695aad..7a75152a 100644 --- a/man/bemenu.1.scd.in +++ b/man/bemenu.1.scd.in @@ -45,6 +45,9 @@ list of executables under PATH and the selected items are executed. *-i, --ignorecase* Filter items case-insensitively. +*-z, --fuzzy* + Filter items fuzzily. + *-K, --no-keyboard* Disable all keyboard events.