From 93b53b7e8b090d11dc98b4f7c07f7bb9b8066cd3 Mon Sep 17 00:00:00 2001 From: Zoe Date: Tue, 16 Jun 2026 04:27:30 -0500 Subject: [PATCH] Initial commit --- wofi-better-search.patch | 155 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 wofi-better-search.patch diff --git a/wofi-better-search.patch b/wofi-better-search.patch new file mode 100644 index 0000000..76f1479 --- /dev/null +++ b/wofi-better-search.patch @@ -0,0 +1,155 @@ +diff --git a/src/match.c b/src/match.c +index 29936ce..e746a55 100644 +--- a/src/match.c ++++ b/src/match.c +@@ -29,7 +29,7 @@ + #define SCORE_MATCH_CONSECUTIVE 1.0 + // we got a consecutive match, but insensitive is on + // and we didn't match the case. +-#define SCORE_MATCH_NOT_MATCH_CASE 0.9 ++#define SCORE_MATCH_NOT_MATCH_CASE 0.95 + // we are matching after a slash + #define SCORE_MATCH_SLASH 0.9 + // we are matching after a space dash or hyphen +@@ -38,6 +38,10 @@ + #define SCORE_MATCH_CAPITAL 0.7 + // we are matching after a dot + #define SCORE_MATCH_DOT 0.6 ++// bonus for exact substring match at start ++#define SCORE_MATCH_STARTS_WITH 2.0 ++// bonus for exact substring match anywhere ++#define SCORE_MATCH_CONTAINS 1.5 + + #define SWAP(x, y, T) \ + do { \ +@@ -170,15 +174,25 @@ static inline bool match_with_case(char a, char b, bool insensitive) { + } + } + ++// Check if a character is just a separator (not a real gap) ++static inline bool is_separator(char c) { ++ return c == '.' || c == '-' || c == '_' || c == ' ' || c == ':' || c == '\''; ++} ++ + static inline void match_row(int row, score_t* curr_D, score_t* curr_M, + const score_t* last_D, const score_t* last_M, + const char* needle, const char* haystack, int n, int m, score_t* match_bonus, bool insensitive) { + int i = row; + + score_t prev_score = SCORE_MIN; +- score_t gap_score = i == n - 1 ? SCORE_GAP_TRAILING : SCORE_GAP_INNER; ++ // Base gap score ++ score_t base_gap_score = i == n - 1 ? SCORE_GAP_TRAILING : SCORE_GAP_INNER; + + for(int j = 0; j < m; j++) { ++ // Separators are FREE - they don't penalize at all ++ bool sep = is_separator(haystack[j]); ++ score_t gap_score = sep ? 0 : base_gap_score; ++ + if(match_with_case(needle[i], haystack[j], insensitive)) { + score_t score = SCORE_MIN; + if(!i) { +@@ -189,10 +203,15 @@ static inline void match_row(int row, score_t* curr_D, score_t* curr_M, + // our character isn't the same then we have a different case + score_t consecutive_bonus = needle[i] == haystack[j] ? SCORE_MATCH_CONSECUTIVE : SCORE_MATCH_NOT_MATCH_CASE; + +- score = max(last_M[j - 1] + match_bonus[j], ++ // Bonus for matching right after a separator (e.g., R.E.P.O) ++ // This treats separator-delimited matches like acronyms ++ bool after_separator = j > 0 && is_separator(haystack[j-1]); ++ score_t sep_bonus = after_separator ? 0.8 : 0; ++ ++ score = max(last_M[j - 1] + match_bonus[j] + sep_bonus, + /* consecutive match, doesn't stack + with match_bonus */ +- last_D[j - 1] + consecutive_bonus); ++ last_D[j - 1] + consecutive_bonus + sep_bonus); + } + curr_D[j] = score; + curr_M[j] = prev_score = max(score, prev_score + gap_score); +@@ -228,6 +247,38 @@ static inline void match_row(int row, score_t* curr_D, score_t* curr_M, + // Also, the reference algorithm does not take into account case sensitivity + // which has been implemented here. + ++// Check if needle matches haystack as a "substring" ignoring separators ++// e.g., "R.E.P.O" starts_with_ignore_sep("repo") = true ++static bool starts_with_ignore_sep(const char* haystack, const char* needle, bool insensitive) { ++ while(*needle) { ++ // Skip separators in haystack ++ while(*haystack && is_separator(*haystack)) haystack++; ++ if(!*haystack) return false; ++ if(insensitive) { ++ if(tolower(*haystack) != tolower(*needle)) return false; ++ } else { ++ if(*haystack != *needle) return false; ++ } ++ haystack++; ++ needle++; ++ } ++ return true; ++} ++ ++// Check if needle appears anywhere in haystack ignoring separators ++static bool contains_ignore_sep(const char* haystack, const char* needle, bool insensitive) { ++ int nlen = strlen(needle); ++ if(nlen == 0) return true; ++ ++ // Try starting at each position in haystack ++ for(const char* p = haystack; *p; p++) { ++ // Skip separators to find a potential start ++ if(is_separator(*p)) continue; ++ if(starts_with_ignore_sep(p, needle, insensitive)) return true; ++ } ++ return false; ++} ++ + static score_t fuzzy_score(const char* haystack, const char* needle, bool insensitive) { + if(*needle == 0) + return SCORE_MIN; +@@ -252,6 +303,22 @@ static score_t fuzzy_score(const char* haystack, const char* needle, bool insens + return SCORE_MAX; + } + ++ /* Give a bonus for substring matches - this makes the search ++ * prefer "firefox" over "fi...r...e...f...o...x" when searching ++ * for "firefox". This makes the results feel much more natural. ++ * ++ * IMPORTANT: We also check ignoring separators, so "R.E.P.O" ++ * gets the same bonus as "REPO" when searching for "repo". ++ */ ++ score_t substring_bonus = 0; ++ if(starts_with_ignore_sep(haystack, needle, insensitive)) { ++ /* Starts with the search term (ignoring separators) - highest bonus */ ++ substring_bonus = SCORE_MATCH_STARTS_WITH; ++ } else if(contains_ignore_sep(haystack, needle, insensitive)) { ++ /* Contains the search term as substring (ignoring separators) */ ++ substring_bonus = SCORE_MATCH_CONTAINS; ++ } ++ + /* + * D[][] Stores the best score for this position ending with a match. + * M[][] Stores the best possible score at this position. +@@ -273,7 +340,7 @@ static score_t fuzzy_score(const char* haystack, const char* needle, bool insens + SWAP(curr_M, last_M, score_t *); + } + +- return last_M[m - 1]; ++ return last_M[m - 1] + substring_bonus; + } + // end fuzzy matching + +diff --git a/src/wofi.c b/src/wofi.c +index 4b6442f..dd21839 100644 +--- a/src/wofi.c ++++ b/src/wofi.c +@@ -1871,8 +1871,8 @@ void wofi_init(struct map* _config) { + char* password_char = map_get(config, "password_char"); + exec_search = strcmp(config_get(config, "exec_search", "false"), "true") == 0; + bool hide_scroll = strcmp(config_get(config, "hide_scroll", "false"), "true") == 0; +- matching = config_get_mnemonic(config, "matching", "contains", 3, "contains", "multi-contains", "fuzzy"); +- insensitive = strcmp(config_get(config, "insensitive", "false"), "true") == 0; ++ matching = config_get_mnemonic(config, "matching", "fuzzy", 3, "contains", "multi-contains", "fuzzy"); ++ insensitive = strcmp(config_get(config, "insensitive", "true"), "true") == 0; + parse_search = strcmp(config_get(config, "parse_search", "false"), "true") == 0; + location = config_get_mnemonic(config, "location", "center", 18, + "center", "top_left", "top", "top_right", "right", "bottom_right", "bottom", "bottom_left", "left",