Skip to content

Commit fafc8eb

Browse files
wintermute-cellCloudef
authored andcommitted
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
1 parent 249f18f commit fafc8eb

File tree

7 files changed

+96
-6
lines changed

7 files changed

+96
-6
lines changed

client/common/common.c

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ usage(FILE *out, const char *name)
172172
" -h, --help display this help and exit.\n"
173173
" -v, --version display version.\n"
174174
" -i, --ignorecase match items case insensitively.\n"
175+
" -z, --fuzzy enable fuzzy matching.\n"
175176
" -F, --filter filter entries for a given string before showing the menu.\n"
176177
" -w, --wrap wraps cursor selection.\n"
177178
" -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[])
272273
{ "version", no_argument, 0, 'v' },
273274

274275
{ "ignorecase", no_argument, 0, 'i' },
276+
{ "fuzzy", no_argument, 0, 'z' },
275277
{ "filter", required_argument, 0, 'F' },
276278
{ "wrap", no_argument, 0, 'w' },
277279
{ "list", required_argument, 0, 'l' },
@@ -340,7 +342,7 @@ do_getopt(struct client *client, int *argc, char **argv[])
340342
for (optind = 0;;) {
341343
int32_t opt;
342344

343-
if ((opt = getopt_long(*argc, *argv, "hviwcl:I:p:P:I:x:bfF:m:H:M:W:B:R:nsCTK", opts, NULL)) < 0)
345+
if ((opt = getopt_long(*argc, *argv, "hvizwcl:I:p:P:I:x:bfF:m:H:M:W:B:R:nsCTK", opts, NULL)) < 0)
344346
break;
345347

346348
switch (opt) {
@@ -353,6 +355,9 @@ do_getopt(struct client *client, int *argc, char **argv[])
353355
case 'i':
354356
client->filter_mode = BM_FILTER_MODE_DMENU_CASE_INSENSITIVE;
355357
break;
358+
case 'z':
359+
client->fuzzy = true;
360+
break;
356361
case 'F':
357362
client->initial_filter = optarg;
358363
break;
@@ -594,6 +599,8 @@ menu_with_options(struct client *client)
594599
bm_menu_set_border_size(menu, client->border_size);
595600
bm_menu_set_border_radius(menu, client->border_radius);
596601
bm_menu_set_key_binding(menu, client->key_binding);
602+
bm_menu_set_fuzzy_mode(menu, client->fuzzy);
603+
597604

598605
if (client->center) {
599606
bm_menu_set_align(menu, BM_ALIGN_CENTER);

client/common/common.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ struct client {
4646
enum bm_password_mode password;
4747
enum bm_key_binding key_binding;
4848
char *monitor_name;
49+
bool fuzzy;
4950
};
5051

5152
char* cstrcopy(const char *str, size_t size);

lib/bemenu.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -934,6 +934,14 @@ BM_PUBLIC enum bm_password_mode bm_menu_get_password(struct bm_menu *menu);
934934
*/
935935
BM_PUBLIC void bm_menu_set_key_binding(struct bm_menu *menu, enum bm_key_binding);
936936

937+
/**
938+
* Specify whether fuzzy matching should be used.
939+
*
940+
* @param menu bm_menu instance to set the fuzzy mode on.
941+
* @param fuzzy true to enable fuzzy matching.
942+
*/
943+
BM_PUBLIC void bm_menu_set_fuzzy_mode(struct bm_menu *menu, bool fuzzy);
944+
937945

938946
/** @} Properties */
939947

lib/filter.c

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,17 +79,32 @@ tokenize(struct bm_menu *menu, char ***out_tokv, uint32_t *out_tokc)
7979
return NULL;
8080
}
8181

82+
struct fuzzy_match {
83+
struct bm_item *item;
84+
int distance;
85+
};
86+
87+
static int fuzzy_match_comparator(const void *a, const void *b) {
88+
const struct fuzzy_match *fa = (const struct fuzzy_match *)a;
89+
const struct fuzzy_match *fb = (const struct fuzzy_match *)b;
90+
91+
return fa->distance - fb->distance;
92+
}
93+
94+
8295
/**
83-
* Dmenu filterer that accepts substring function.
96+
* Dmenu filterer that accepts substring function or fuzzy match.
8497
*
8598
* @param menu bm_menu instance to filter.
8699
* @param addition This will be 1, if filter is same as previous filter with something appended.
87100
* @param fstrstr Substring function used to match items.
101+
* @param fstrncmp String comparison function for exact matches.
88102
* @param out_nmemb uint32_t reference to filtered items count.
103+
* @param fuzzy Boolean flag to toggle fuzzy matching.
89104
* @return Pointer to array of bm_item pointers.
90105
*/
91106
static struct bm_item**
92-
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)
107+
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)
93108
{
94109
assert(menu && fstrstr && fstrncmp && out_nmemb);
95110
*out_nmemb = 0;
@@ -114,13 +129,48 @@ filter_dmenu_fun(struct bm_menu *menu, char addition, char* (*fstrstr)(const cha
114129
goto fail;
115130

116131
const char *filter = menu->filter ? menu->filter : "";
132+
if (strlen(filter) == 0) {
133+
goto fail;
134+
}
117135
size_t len = (tokc ? strlen(tokv[0]) : 0);
118136
uint32_t i, f, e;
119-
for (e = f = i = 0; i < count; ++i) {
137+
f = e = 0;
138+
139+
struct fuzzy_match *fuzzy_matches = NULL;
140+
141+
int fuzzy_match_count = 0;
142+
143+
if (fuzzy && !(fuzzy_matches = calloc(count, sizeof(*fuzzy_matches))))
144+
goto fail;
145+
146+
for (i = 0; i < count; ++i) {
120147
struct bm_item *item = items[i];
121148
if (!item->text && tokc != 0)
122149
continue;
123150

151+
if (fuzzy && tokc && item->text) {
152+
const char *text = item->text;
153+
int sidx = -1, eidx = -1, pidx = 0, text_len = strlen(text), distance = 0;
154+
for (int j = 0; j < text_len && text[j]; ++j) {
155+
if (!fstrncmp(&text[j], &filter[pidx], 1)) {
156+
if (sidx == -1)
157+
sidx = j;
158+
pidx++;
159+
if (pidx == strlen(filter)) {
160+
eidx = j;
161+
break;
162+
}
163+
}
164+
}
165+
if (eidx != -1) {
166+
distance = eidx - sidx + (text_len - eidx + sidx) / 3;
167+
fuzzy_matches[fuzzy_match_count++] = (struct fuzzy_match){ item, distance };
168+
continue;
169+
}
170+
}
171+
172+
if (fuzzy) continue;
173+
124174
if (tokc && item->text) {
125175
uint32_t t;
126176
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
142192
f++; /* where do all matches end */
143193
}
144194

195+
if (fuzzy && fuzzy_match_count > 0) {
196+
qsort(fuzzy_matches, fuzzy_match_count, sizeof(struct fuzzy_match), fuzzy_match_comparator);
197+
198+
for (int j = 0; j < fuzzy_match_count; ++j) {
199+
filtered[f++] = fuzzy_matches[j].item;
200+
}
201+
202+
free(fuzzy_matches);
203+
}
204+
145205
free(buffer);
146206
free(tokv);
147207
return shrink_list(&filtered, menu->items.count, (*out_nmemb = f));
148208

149209
fail:
150210
free(filtered);
211+
free(fuzzy_matches);
151212
free(buffer);
152213
return NULL;
153214
}
@@ -163,7 +224,7 @@ filter_dmenu_fun(struct bm_menu *menu, char addition, char* (*fstrstr)(const cha
163224
struct bm_item**
164225
bm_filter_dmenu(struct bm_menu *menu, bool addition, uint32_t *out_nmemb)
165226
{
166-
return filter_dmenu_fun(menu, addition, strstr, strncmp, out_nmemb);
227+
return filter_dmenu_fun(menu, addition, strstr, strncmp, out_nmemb, menu->fuzzy);
167228
}
168229

169230
/**
@@ -177,7 +238,7 @@ bm_filter_dmenu(struct bm_menu *menu, bool addition, uint32_t *out_nmemb)
177238
struct bm_item**
178239
bm_filter_dmenu_case_insensitive(struct bm_menu *menu, bool addition, uint32_t *out_nmemb)
179240
{
180-
return filter_dmenu_fun(menu, addition, bm_strupstr, bm_strnupcmp, out_nmemb);
241+
return filter_dmenu_fun(menu, addition, bm_strupstr, bm_strnupcmp, out_nmemb, menu->fuzzy);
181242
}
182243

183244
/* vim: set ts=8 sw=4 tw=0 :*/

lib/internal.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,11 @@ struct bm_menu {
457457
*/
458458
char vim_mode;
459459
uint32_t vim_last_key;
460+
461+
/**
462+
* Should fuzzy matching be used?
463+
*/
464+
bool fuzzy;
460465
};
461466

462467
/* library.c */

lib/menu.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -779,6 +779,11 @@ bm_menu_set_key_binding(struct bm_menu *menu, enum bm_key_binding key_binding){
779779
menu->key_binding = key_binding;
780780
}
781781

782+
void
783+
bm_menu_set_fuzzy_mode(struct bm_menu *menu, bool fuzzy){
784+
menu->fuzzy = fuzzy;
785+
}
786+
782787
struct bm_item**
783788
bm_menu_get_selected_items(const struct bm_menu *menu, uint32_t *out_nmemb)
784789
{

man/bemenu.1.scd.in

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ list of executables under PATH and the selected items are executed.
4545
*-i, --ignorecase*
4646
Filter items case-insensitively.
4747

48+
*-z, --fuzzy*
49+
Filter items fuzzily.
50+
4851
*-K, --no-keyboard*
4952
Disable all keyboard events.
5053

0 commit comments

Comments
 (0)