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:
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;