diff options
-rw-r--r-- | completions/tofi | 1 | ||||
-rw-r--r-- | doc/config | 3 | ||||
-rw-r--r-- | doc/tofi.5.md | 8 | ||||
-rw-r--r-- | doc/tofi.5.scd | 7 | ||||
-rw-r--r-- | meson.build | 1 | ||||
-rw-r--r-- | src/config.c | 2 | ||||
-rw-r--r-- | src/desktop_vec.c | 43 | ||||
-rw-r--r-- | src/desktop_vec.h | 4 | ||||
-rw-r--r-- | src/fuzzy_match.c | 92 | ||||
-rw-r--r-- | src/fuzzy_match.h | 8 | ||||
-rw-r--r-- | src/main.c | 10 | ||||
-rw-r--r-- | src/string_vec.c | 34 | ||||
-rw-r--r-- | src/string_vec.h | 6 | ||||
-rw-r--r-- | src/tofi.h | 1 |
14 files changed, 193 insertions, 27 deletions
diff --git a/completions/tofi b/completions/tofi index 048aafc..33cc222 100644 --- a/completions/tofi +++ b/completions/tofi @@ -40,6 +40,7 @@ _tofi() --horizontal --hide-cursor --history + --fuzzy-match --drun-launch --hint-font --late-keyboard-init @@ -121,6 +121,9 @@ # Sort results by number of usages in run and drun modes. history = true + # Use fuzzy matching for searches. + fuzzy-match = false + # If true, directly launch applications on selection when in drun mode. # Otherwise, just print the command line to stdout. drun-launch = false diff --git a/doc/tofi.5.md b/doc/tofi.5.md index 23f4cb2..446ec8e 100644 --- a/doc/tofi.5.md +++ b/doc/tofi.5.md @@ -240,6 +240,14 @@ options. > > Default: true +**fuzzy-match**=*true\|false* + +> If true, searching is performed via a simple fuzzy matching algorithm. +> If false, substring matching is used, weighted to favour matches +> closer to the beginning of the string. +> +> Default: false + **drun-launch**=*true\|false* > If true, directly launch applications on selection when in drun mode. diff --git a/doc/tofi.5.scd b/doc/tofi.5.scd index c36f98c..2cb4dad 100644 --- a/doc/tofi.5.scd +++ b/doc/tofi.5.scd @@ -208,6 +208,13 @@ options. Default: true +*fuzzy-match*=_true|false_ + If true, searching is performed via a simple fuzzy matching algorithm. + If false, substring matching is used, weighted to favour matches closer + to the beginning of the string. + + Default: false + *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. diff --git a/meson.build b/meson.build index 4dd3857..4f6640b 100644 --- a/meson.build +++ b/meson.build @@ -100,6 +100,7 @@ tofi_sources = files( 'src/entry.c', 'src/entry_backend/pango.c', 'src/entry_backend/harfbuzz.c', + 'src/fuzzy_match.c', 'src/history.c', 'src/log.c', 'src/mkdirp.c', diff --git a/src/config.c b/src/config.c index 7ee29e8..801a5bf 100644 --- a/src/config.c +++ b/src/config.c @@ -321,6 +321,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, "fuzzy-match") == 0) { + tofi->fuzzy_match = 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, "drun-print-exec") == 0) { diff --git a/src/desktop_vec.c b/src/desktop_vec.c index f4363f8..5d63390 100644 --- a/src/desktop_vec.c +++ b/src/desktop_vec.c @@ -1,6 +1,7 @@ #include <glib.h> #include <stdbool.h> #include "desktop_vec.h" +#include "fuzzy_match.h" #include "log.h" #include "string_vec.h" #include "xmalloc.h" @@ -119,10 +120,10 @@ static int cmpscorep(const void *restrict a, const void *restrict b) { struct scored_string *restrict str1 = (struct scored_string *)a; struct scored_string *restrict str2 = (struct scored_string *)b; - if (str1->history_score != str2->history_score) { - return str2->history_score - str1->history_score; - } - return str1->search_score - str2->search_score; + + int hist_diff = str2->history_score - str1->history_score; + int search_diff = str2->search_score - str1->search_score; + return hist_diff + search_diff; } void desktop_vec_sort(struct desktop_vec *restrict vec) @@ -142,29 +143,49 @@ struct desktop_entry *desktop_vec_find(struct desktop_vec *restrict vec, const c struct string_vec desktop_vec_filter( const struct desktop_vec *restrict vec, - const char *restrict substr) + const char *restrict substr, + bool fuzzy) { struct string_vec filt = string_vec_create(); for (size_t i = 0; i < vec->count; i++) { - char *c = strcasestr(vec->buf[i].name, substr); - if (c != NULL) { + int32_t search_score; + if (fuzzy) { + search_score = fuzzy_match(substr, vec->buf[i].name); + } else { + char *c = strcasestr(vec->buf[i].name, substr); + if (c == NULL) { + search_score = INT32_MIN; + } else { + search_score = vec->buf[i].name - c; + } + } + if (search_score != INT32_MIN) { string_vec_add(&filt, vec->buf[i].name); /* * Store the position of the match in the string as * its search_score, for later sorting. */ - filt.buf[filt.count - 1].search_score = c - vec->buf[i].name; + filt.buf[filt.count - 1].search_score = search_score; filt.buf[filt.count - 1].history_score = vec->buf[i].history_score; } else { /* If we didn't match the name, check the keywords. */ - c = strcasestr(vec->buf[i].keywords, substr); - if (c != NULL) { + if (fuzzy) { + search_score = fuzzy_match(substr, vec->buf[i].keywords); + } else { + char *c = strcasestr(vec->buf[i].keywords, substr); + if (c == NULL) { + search_score = INT32_MIN; + } else { + search_score = vec->buf[i].keywords - c; + } + } + if (search_score != INT32_MIN) { string_vec_add(&filt, vec->buf[i].name); /* * Arbitrary score addition to make name * matches preferred over keyword matches. */ - filt.buf[filt.count - 1].search_score = 10 + c - vec->buf[i].keywords; + filt.buf[filt.count - 1].search_score = search_score - 20; filt.buf[filt.count - 1].history_score = vec->buf[i].history_score; } } diff --git a/src/desktop_vec.h b/src/desktop_vec.h index 85c885e..a731566 100644 --- a/src/desktop_vec.h +++ b/src/desktop_vec.h @@ -1,6 +1,7 @@ #ifndef DESKTOP_VEC_H #define DESKTOP_VEC_H +#include <stdbool.h> #include <stddef.h> #include <stdio.h> #include <stdint.h> @@ -35,7 +36,8 @@ void desktop_vec_sort(struct desktop_vec *restrict vec); struct desktop_entry *desktop_vec_find(struct desktop_vec *restrict vec, const char *name); struct string_vec desktop_vec_filter( const struct desktop_vec *restrict vec, - const char *restrict substr); + const char *restrict substr, + bool fuzzy); struct desktop_vec desktop_vec_load(FILE *file); void desktop_vec_save(struct desktop_vec *restrict vec, FILE *restrict file); diff --git a/src/fuzzy_match.c b/src/fuzzy_match.c new file mode 100644 index 0000000..1283151 --- /dev/null +++ b/src/fuzzy_match.c @@ -0,0 +1,92 @@ +#include <ctype.h> +#include <stdbool.h> +#include <stddef.h> +#include <stdint.h> +#include <string.h> + +#include "fuzzy_match.h" + +#undef MAX +#define MAX(a, b) ((a) > (b) ? (a) : (b)) + +/* + * Returns score if each character in pattern is found sequentially within str. + * Returns INT32_MIN otherwise. + * + * The scoring system is taken from fts_fuzzy_match v0.1.0 by Forrest Smith, + * which is licensed to the public domain. + * + * The factors affecting score are: + * - Bonuses: + * - If there are multiple adjacent matches. + * - If a match occurs after a separator character. + * - If a match is uppercase, and the previous character is lowercase. + * + * - Penalties: + * - If there are letters before the first match. + * - If there are superfluous characters in str. + */ +int32_t fuzzy_match(const char *restrict pattern, const char *restrict str) +{ + if (*pattern == '\0') { + return 0; + } + if (strlen(str) < strlen(pattern)) { + return INT32_MIN; + } + + const int adjacency_bonus = 5; + const int separator_bonus = 10; + const int camel_bonus = 10; + + const int leading_letter_penalty = -3; + const int max_leading_letter_penalty = -9; + const int unmatched_letter_penalty = -1; + + int32_t score = 0; + const char *last_match = str; + + /* + * Iterate through pattern, jumping to the next matching character in + * str. This will only ever find the first fuzzy match, not the best, + * but it's good enough and very quick. + */ + for (size_t pidx = 0; pattern[pidx] != '\0'; pidx++) { + char search[2] = { pattern[pidx], '\0' }; + char *match = strcasestr(last_match, search); + if (match == NULL) { + return INT32_MIN; + } + + int32_t jump = match - last_match; + + /* Apply bonuses. */ + if (pidx > 0 && jump == 0) { + score += adjacency_bonus; + } + if (pidx > 0 || jump > 0) { + if (isupper(*match) && islower(*(match - 1))) { + score += camel_bonus; + } + if (isalnum(*match) && !isalnum(*(match - 1))) { + score += separator_bonus; + } + } + if (pidx == 0 && jump == 0) { + /* Match at start of string gets separator bonus. */ + score += separator_bonus; + } + + /* Apply penalties. */ + if (pidx == 0) { + score += MAX(leading_letter_penalty * jump, + max_leading_letter_penalty); + } + + last_match = match + 1; + } + + score += unmatched_letter_penalty * (strlen(str) - strlen(pattern)); + + return score; +} diff --git a/src/fuzzy_match.h b/src/fuzzy_match.h new file mode 100644 index 0000000..df4d298 --- /dev/null +++ b/src/fuzzy_match.h @@ -0,0 +1,8 @@ +#ifndef FUZZY_MATCH_H +#define FUZZY_MATCH_H + +#include <stdint.h> + +int32_t fuzzy_match(const char *restrict pattern, const char *restrict str); + +#endif /* FUZZY_MATCH_H */ @@ -168,12 +168,12 @@ static void handle_keypress(struct tofi *tofi, xkb_keycode_t keycode) N_ELEM(buf)); entry->input_mb_length += len; if (entry->drun) { - struct string_vec results = desktop_vec_filter(&entry->apps, entry->input_mb); + struct string_vec results = desktop_vec_filter(&entry->apps, entry->input_mb, tofi->fuzzy_match); string_vec_destroy(&entry->results); entry->results = results; } else { struct string_vec tmp = entry->results; - entry->results = string_vec_filter(&entry->results, entry->input_mb); + entry->results = string_vec_filter(&entry->results, entry->input_mb, tofi->fuzzy_match); string_vec_destroy(&tmp); } } @@ -189,9 +189,9 @@ static void handle_keypress(struct tofi *tofi, xkb_keycode_t keycode) entry->input_mb_length = siz; string_vec_destroy(&entry->results); if (entry->drun) { - entry->results = desktop_vec_filter(&entry->apps, entry->input_mb); + entry->results = desktop_vec_filter(&entry->apps, entry->input_mb, tofi->fuzzy_match); } else { - entry->results = string_vec_filter(&entry->commands, entry->input_mb); + entry->results = string_vec_filter(&entry->commands, entry->input_mb, tofi->fuzzy_match); } } else if (sym == XKB_KEY_Escape || (sym == XKB_KEY_c @@ -775,6 +775,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" +" --fuzzy-match <true|false> Use fuzzy matching for searching.\n" " --drun-launch <true|false> Launch apps directly in drun mode.\n" " --drun-print-exec <true|false> Print a command line in drun mode.\n" " This is now always the case,\n" @@ -821,6 +822,7 @@ const struct option long_options[] = { {"horizontal", required_argument, NULL, 0}, {"hide-cursor", required_argument, NULL, 0}, {"history", required_argument, NULL, 0}, + {"fuzzy-match", required_argument, NULL, 0}, {"drun-launch", required_argument, NULL, 0}, {"drun-print-exec", required_argument, NULL, 0}, {"hint-font", required_argument, NULL, 0}, diff --git a/src/string_vec.c b/src/string_vec.c index c337c61..464c450 100644 --- a/src/string_vec.c +++ b/src/string_vec.c @@ -1,8 +1,10 @@ +#include <stdbool.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/mman.h> +#include "fuzzy_match.h" #include "string_vec.h" #include "xmalloc.h" @@ -31,10 +33,10 @@ static int cmpscorep(const void *restrict a, const void *restrict b) { struct scored_string *restrict str1 = (struct scored_string *)a; struct scored_string *restrict str2 = (struct scored_string *)b; - if (str1->history_score != str2->history_score) { - return str2->history_score - str1->history_score; - } - return str1->search_score - str2->search_score; + + int hist_diff = str2->history_score - str1->history_score; + int search_diff = str2->search_score - str1->search_score; + return hist_diff + search_diff; } struct string_vec string_vec_create(void) @@ -55,7 +57,7 @@ void string_vec_destroy(struct string_vec *restrict vec) free(vec->buf); } -struct string_vec string_vec_copy(struct string_vec *restrict vec) +struct string_vec string_vec_copy(const struct string_vec *restrict vec) { struct string_vec copy = { .count = vec->count, @@ -110,18 +112,32 @@ char **string_vec_find(struct string_vec *restrict vec, const char * str) struct string_vec string_vec_filter( const struct string_vec *restrict vec, - const char *restrict substr) + const char *restrict substr, + bool fuzzy) { + if (substr[0] == '\0') { + return string_vec_copy(vec); + } struct string_vec filt = string_vec_create(); for (size_t i = 0; i < vec->count; i++) { - char *c = strcasestr(vec->buf[i].string, substr); - if (c != NULL) { + int32_t search_score; + if (fuzzy) { + search_score = fuzzy_match(substr, vec->buf[i].string); + } else { + char *c = strcasestr(vec->buf[i].string, substr); + if (c == NULL) { + search_score = INT32_MIN; + } else { + search_score = vec->buf[i].string - c; + } + } + if (search_score != INT32_MIN) { string_vec_add(&filt, vec->buf[i].string); /* * Store the position of the match in the string as * its search_score, for later sorting. */ - filt.buf[filt.count - 1].search_score = c - vec->buf[i].string; + filt.buf[filt.count - 1].search_score = search_score; filt.buf[filt.count - 1].history_score = vec->buf[i].history_score; } } diff --git a/src/string_vec.h b/src/string_vec.h index 7402052..f4e0bb9 100644 --- a/src/string_vec.h +++ b/src/string_vec.h @@ -1,6 +1,7 @@ #ifndef STRING_VEC_H #define STRING_VEC_H +#include <stdbool.h> #include <stddef.h> #include <stdint.h> #include <stdio.h> @@ -23,7 +24,7 @@ struct string_vec string_vec_create(void); void string_vec_destroy(struct string_vec *restrict vec); [[nodiscard("memory leaked")]] -struct string_vec string_vec_copy(struct string_vec *restrict vec); +struct string_vec string_vec_copy(const struct string_vec *restrict vec); void string_vec_add(struct string_vec *restrict vec, const char *restrict str); @@ -36,7 +37,8 @@ char **string_vec_find(struct string_vec *restrict vec, const char *str); [[nodiscard("memory leaked")]] struct string_vec string_vec_filter( const struct string_vec *restrict vec, - const char *restrict substr); + const char *restrict substr, + bool fuzzy); [[nodiscard("memory leaked")]] struct string_vec string_vec_load(FILE *file); @@ -82,6 +82,7 @@ struct tofi { bool late_keyboard_init; bool drun_launch; bool drun_print_exec; + bool fuzzy_match; char target_output_name[MAX_OUTPUT_NAME_LEN]; }; |