nnn

mirror of https://github.com/jarun/nnn
Log | Files | Refs | README | LICENSE

commit 83624f6349fb672a809afc02d691cf4ac8c08019
parent 37771e3d00457bb2b0e66eaae852f7b4ef687472
Author: Arun Prakash Jana <engineerarun@gmail.com>
Date:   Tue,  3 Feb 2026 00:59:33 +0530

Support in order fuzzy search

Diffstat:
MREADME.md | 2+-
Mmisc/auto-completion/bash/nnn-completion.bash | 1+
Mmisc/auto-completion/fish/nnn.fish | 1+
Mmisc/auto-completion/zsh/_nnn | 1+
Mnnn.1 | 5++++-
Msrc/nnn.c | 98+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
6 files changed, 96 insertions(+), 12 deletions(-)

diff --git a/README.md b/README.md @@ -68,7 +68,7 @@ Runs on the Pi, [Termux](https://www.youtube.com/embed/AbaauM7gUJw) (Android), L - Proceed to next file on file open and selection - Search - Instant filtering with *search-as-you-type* - - Regex (POSIX/PCRE2) and string (default) filters + - Fuzzy, regex (POSIX/PCRE2) and string (default) filters - Subtree search plugin to open or edit files - Sort - Ordered pure numeric names by default (visit `/proc`) diff --git a/misc/auto-completion/bash/nnn-completion.bash b/misc/auto-completion/bash/nnn-completion.bash @@ -44,6 +44,7 @@ _nnn () -U -V -x + -z -0 -h ) diff --git a/misc/auto-completion/fish/nnn.fish b/misc/auto-completion/fish/nnn.fish @@ -43,5 +43,6 @@ complete -c nnn -s u -d 'use selection (no prompt)' complete -c nnn -s U -d 'show user and group' complete -c nnn -s V -d 'show program version and exit' complete -c nnn -s x -d 'notis, sel to system clipboard, xterm title' +complete -c nnn -s z -d 'in order fuzzy filters' complete -c nnn -s 0 -d 'use null separator in picker mode' complete -c nnn -s h -d 'show program help' diff --git a/misc/auto-completion/zsh/_nnn b/misc/auto-completion/zsh/_nnn @@ -42,6 +42,7 @@ args=( '(-U)-U[show user and group]' '(-V)-V[show program version and exit]' '(-x)-x[notis, sel to system clipboard, xterm title]' + '(-z)-z[in order fuzzy filters]' '(-0)-0[use null separator in picker mode]' '(-h)-h[show program help]' '*:filename:_files' diff --git a/nnn.1 b/nnn.1 @@ -6,7 +6,7 @@ .Nd The unorthodox terminal file manager. .Sh SYNOPSIS .Nm -.Op Ar -aAcCdDeEfgHJKnQrRSuUVxh +.Op Ar -aAcCdDeEfgHJKnQrRSuUVxz0h .Op Ar -b key .Op Ar -F val .Op Ar -l val @@ -165,6 +165,9 @@ supports the following options: copy path to system clipboard on selection (requires the \fB.cbcp\fR plugin) show xterm title (if non-picker mode) .Pp +.Fl z + use fuzzy filters (fuzzy match: all characters in filter appear in order) +.Pp .Fl 0 use null separator (instead of newline) to separate file paths in picker mode outout .Pp diff --git a/src/nnn.c b/src/nnn.c @@ -370,7 +370,8 @@ typedef struct { uint_t autoenter : 1; /* auto-enter dir in type-to-nav mode */ uint_t reserved2 : 1; uint_t useeditor : 1; /* Use VISUAL to open text files */ - uint_t reserved3 : 3; + uint_t reserved3 : 2; + uint_t fuzzy : 1; /* Use fuzzy filters */ uint_t regex : 1; /* Use regex filters */ uint_t x11 : 1; /* Copy to system clipboard, show notis, xterm title */ uint_t timetype : 2; /* Time sort type (0: access, 1: change, 2: modification) */ @@ -3075,6 +3076,53 @@ static int setfilter(regex_t *regex, const char *filter) } #endif +/* + * Fuzzy match: check if all characters in filter appear in order in fname + * Case-sensitivity is controlled by fnstrstr function pointer + * Supports wide characters and Unicode + */ +static int fuzzy_match(const char *filter, const char *fname) +{ + wchar_t filter_wcs[NAME_MAX], fname_wcs[NAME_MAX]; + wchar_t *f, *n; + size_t filter_len, fname_len; + bool case_insensitive = (fnstrstr == &strcasestr); + + /* Convert multi-byte strings to wide character strings */ + filter_len = mbstowcs(filter_wcs, filter, NAME_MAX - 1); + if (filter_len == (size_t)-1) + return 0; + filter_wcs[filter_len] = L'\0'; + + if (!filter_len) + return 1; + + fname_len = mbstowcs(fname_wcs, fname, NAME_MAX - 1); + if (fname_len == (size_t)-1) + return 0; + fname_wcs[fname_len] = L'\0'; + + /* Convert to lowercase if case-insensitive matching */ + if (case_insensitive) { + for (size_t i = 0; i < filter_len; ++i) + filter_wcs[i] = towlower(filter_wcs[i]); + for (size_t i = 0; i < fname_len; ++i) + fname_wcs[i] = towlower(fname_wcs[i]); + } + + f = filter_wcs; + n = fname_wcs; + + /* Match characters in order */ + while (*f && *n) { + if (*f == *n) + ++f; + ++n; + } + + return !*f; +} + static int visible_re(const fltrexp_t *fltrexp, const char *fname) { #ifdef PCRE2 @@ -3097,6 +3145,11 @@ static int visible_str(const fltrexp_t *fltrexp, const char *fname) return fnstrstr(fname, fltrexp->str) != NULL; } +static int visible_fuzzy(const fltrexp_t *fltrexp, const char *fname) +{ + return fuzzy_match(fltrexp->str, fname); +} + static int (*filterfn)(const fltrexp_t *fltr, const char *fname) = &visible_str; static void clearfilter(void) @@ -3344,9 +3397,11 @@ static void showfilterinfo(void) if (cfg.fileinfo && ndents && get_output("file", "-b", pdents[cur].name, -1, FALSE)) mvaddstr(xlines - 2, 2, g_buf); - else + else { + const char *mode = cfg.regex ? "reg" : (cfg.fuzzy ? "fuzzy" : "str"); snprintf(info + i, REGEX_MAX - i - 1, " %s [/], %s [:]", - (cfg.regex ? "reg" : "str"), ((fnstrstr == &strcasestr) ? "ic" : "noic")); + mode, ((fnstrstr == &strcasestr) ? "ic" : "noic")); + } mvaddstr(xlines - 2, xcols - xstrlen(info), info); } @@ -3582,12 +3637,26 @@ static int filterentries(char *path, char *lastname) continue; } - /* Toggle string or regex filter */ + /* Toggle string/fuzzy/regex filter */ if (*ch == FILTER) { - ln[0] = (ln[0] == FILTER) ? RFILTER : FILTER; + if (cfg.regex) { + /* regex -> string */ + ln[0] = FILTER; + cfg.regex = 0; + filterfn = &visible_str; + } else if (!cfg.fuzzy) { + /* string -> fuzzy */ + ln[0] = FILTER; + cfg.fuzzy = 1; + filterfn = &visible_fuzzy; + } else { + /* fuzzy -> regex */ + ln[0] = RFILTER; + cfg.regex = 1; + cfg.fuzzy = 0; + filterfn = &visible_re; + } wln[0] = (uchar_t)ln[0]; - cfg.regex ^= 1; - filterfn = cfg.regex ? &visible_re : &visible_str; showfilter(ln); continue; } @@ -4525,7 +4594,7 @@ static void setcfg(settings newcfg) /* Synchronize the global function pointers to match the new cfg. */ entrycmpfn = cfg.reverse ? &reventrycmp : &entrycmp; namecmpfn = cfg.version ? &xstrverscasecmp : &xstricmp; - filterfn = cfg.regex ? &visible_re : &visible_str; + filterfn = cfg.regex ? &visible_re : (cfg.fuzzy ? &visible_fuzzy : &visible_str); } static void savecurctx(char *path, char *curname, int nextctx) @@ -4685,7 +4754,7 @@ static bool load_session(const char *sname, char **path, char **lastdir, char ** *lastname = g_ctx[cfg.curctx].c_name; /* Set correct sort and filter options */ set_sort_flags('\0'); - filterfn = cfg.regex ? &visible_re : &visible_str; + filterfn = cfg.regex ? &visible_re : (cfg.fuzzy ? &visible_fuzzy : &visible_str); xstrsncpy(curssn, sname ? sname : "@", NAME_MAX); status = TRUE; @@ -9283,6 +9352,7 @@ static void usage(void) #ifndef NOX11 " -x notis, selection sync, xterm title\n" #endif + " -z in order fuzzy filters\n" " -0 null separator in picker mode\n" " -h show help\n\n" "v%s\n%s\n", __func__, VERSION, GENERAL_INFO); @@ -9442,7 +9512,7 @@ int main(int argc, char *argv[]) while ((opt = (env_opts_id > 0 ? env_opts[--env_opts_id] - : getopt(argc, argv, "aAb:BcCdDeEfF:gHiJKl:nNop:P:QrRs:St:T:uUVx0h"))) != -1) { + : getopt(argc, argv, "aAb:BcCdDeEfF:gHiJKl:nNop:P:QrRs:St:T:uUVxz0h"))) != -1) { switch (opt) { #ifndef NOFIFO case 'a': @@ -9491,6 +9561,8 @@ int main(int argc, char *argv[]) break; #endif case 'g': + if (cfg.fuzzy) + return EXIT_FAILURE; cfg.regex = 1; filterfn = &visible_re; break; @@ -9590,6 +9662,12 @@ int main(int argc, char *argv[]) case 'x': cfg.x11 = 1; break; + case 'z': + if (cfg.regex) + return EXIT_FAILURE; + cfg.fuzzy = 1; + filterfn = &visible_fuzzy; + break; case '0': sepnul = TRUE; break;