Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: fuzzy matching flag -z #432

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion client/common/common.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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' },
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions client/common/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
8 changes: 8 additions & 0 deletions lib/bemenu.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 */

Expand Down
71 changes: 66 additions & 5 deletions lib/filter.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand All @@ -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;
}
Expand All @@ -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);
}

/**
Expand All @@ -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 :*/
5 changes: 5 additions & 0 deletions lib/internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,11 @@ struct bm_menu {
*/
char vim_mode;
uint32_t vim_last_key;

/**
* Should fuzzy matching be used?
*/
bool fuzzy;
};

/* library.c */
Expand Down
5 changes: 5 additions & 0 deletions lib/menu.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
3 changes: 3 additions & 0 deletions man/bemenu.1.scd.in
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down