nnn

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

nnn.c (255665B)


      1 /*
      2  * BSD 2-Clause License
      3  *
      4  * Copyright (C) 2014-2016, Lazaros Koromilas <lostd@2f30.org>
      5  * Copyright (C) 2014-2016, Dimitris Papastamos <sin@2f30.org>
      6  * Copyright (C) 2016-2026, Arun Prakash Jana <engineerarun@gmail.com>
      7  * All rights reserved.
      8  *
      9  * Redistribution and use in source and binary forms, with or without
     10  * modification, are permitted provided that the following conditions are met:
     11  *
     12  * * Redistributions of source code must retain the above copyright notice, this
     13  *   list of conditions and the following disclaimer.
     14  *
     15  * * Redistributions in binary form must reproduce the above copyright notice,
     16  *   this list of conditions and the following disclaimer in the documentation
     17  *   and/or other materials provided with the distribution.
     18  *
     19  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
     20  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
     21  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
     22  * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
     23  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
     24  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
     25  * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
     26  * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
     27  * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     28  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     29  */
     30 
     31 #define _FILE_OFFSET_BITS 64 /* Support large files on 32-bit glibc */
     32 
     33 #if defined(__linux__) || defined(MINGW) || defined(__MINGW32__) \
     34 	|| defined(__MINGW64__) || defined(__CYGWIN__)
     35 #ifndef _GNU_SOURCE
     36 #define _GNU_SOURCE
     37 #endif
     38 #ifdef __linux__
     39 #include <sys/inotify.h>
     40 #define LINUX_INOTIFY
     41 #endif
     42 #ifndef __GLIBC__
     43 #include <sys/types.h>
     44 #endif
     45 #endif
     46 #include <sys/resource.h>
     47 #include <sys/stat.h>
     48 #include <sys/statvfs.h>
     49 #if defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__) || defined(__DragonFly__)
     50 #include <sys/types.h>
     51 #include <sys/event.h>
     52 #include <sys/time.h>
     53 #define BSD_KQUEUE
     54 #elif defined(__HAIKU__)
     55 #include "../misc/haiku/haiku_interop.h"
     56 #define HAIKU_NM
     57 #else
     58 #include <sys/sysmacros.h>
     59 #endif
     60 #include <sys/wait.h>
     61 
     62 #ifdef __linux__ /* Fix failure due to mvaddnwstr() */
     63 #ifndef NCURSES_WIDECHAR
     64 #define NCURSES_WIDECHAR 1
     65 #endif
     66 #elif defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) \
     67 	|| defined(__APPLE__) || defined(__sun) || defined(__DragonFly__)
     68 #ifndef _XOPEN_SOURCE_EXTENDED
     69 #define _XOPEN_SOURCE_EXTENDED
     70 #endif
     71 #endif
     72 #ifndef __USE_XOPEN /* Fix wcswidth() failure, ncursesw/curses.h includes whcar.h on Ubuntu 14.04 */
     73 #define __USE_XOPEN
     74 #endif
     75 #include <dirent.h>
     76 #include <errno.h>
     77 #include <fcntl.h>
     78 #include <fts.h>
     79 #include <libgen.h>
     80 #include <limits.h>
     81 #ifndef NOLC
     82 #include <locale.h>
     83 #endif
     84 #include <pthread.h>
     85 #include <stdio.h>
     86 #ifndef NORL
     87 #include <readline/history.h>
     88 #include <readline/readline.h>
     89 #endif
     90 #ifdef PCRE2
     91 #define PCRE2_CODE_UNIT_WIDTH 8
     92 #include <pcre2.h>
     93 #else
     94 #include <regex.h>
     95 #endif
     96 #include <signal.h>
     97 #include <stdarg.h>
     98 #include <stdlib.h>
     99 #include <string.h>
    100 #include <strings.h>
    101 #include <time.h>
    102 #include <unistd.h>
    103 #include <stddef.h>
    104 #include <wctype.h>
    105 #include <stdalign.h>
    106 #include <stdbool.h>
    107 #ifndef __USE_XOPEN_EXTENDED
    108 #define __USE_XOPEN_EXTENDED 1
    109 #endif
    110 #include <ftw.h>
    111 #include <pwd.h>
    112 #include <grp.h>
    113 
    114 #ifdef MACOS_BELOW_1012
    115 #include "../misc/macos-legacy/mach_gettime.h"
    116 #endif
    117 
    118 #if !defined(alloca) && defined(__GNUC__)
    119 /*
    120  * GCC doesn't expand alloca() to __builtin_alloca() in standards mode
    121  * (-std=...) and not all standard libraries do or supply it, e.g.
    122  * NetBSD/arm64 so explicitly use the builtin.
    123  */
    124 #define alloca(size) __builtin_alloca(size)
    125 #endif
    126 
    127 #include "nnn.h"
    128 #include "dbg.h"
    129 
    130 #if defined(ICONS_IN_TERM) || defined(NERD) || defined(EMOJI)
    131 #define ICONS_ENABLED
    132 #include ICONS_INCLUDE
    133 #include "icons-hash.c"
    134 #include "icons.h"
    135 #endif
    136 
    137 #if defined(ICONS_ENABLED) && defined(__APPLE__)
    138 /*
    139  * For some reason, wcswidth returns 2 for certain icons on macOS
    140  * leading to duplicated first characters in filenames when navigating.
    141  * https://github.com/jarun/nnn/issues/1692
    142  * There might be a better way to fix it without requiring a refresh.
    143  */
    144 #define macos_icons_hack() do { clrtoeol(); refresh(); } while(0)
    145 #else
    146 #define macos_icons_hack()
    147 #endif
    148 
    149 #ifdef TOURBIN_QSORT
    150 #include "qsort.h"
    151 #endif
    152 
    153 /* Macro definitions */
    154 #define VERSION      "5.2"
    155 #define GENERAL_INFO "BSD 2-Clause\nhttps://github.com/jarun/nnn"
    156 
    157 #ifndef NOSSN
    158 #define SESSIONS_VERSION 1
    159 #endif
    160 
    161 #ifndef S_BLKSIZE
    162 #define S_BLKSIZE 512 /* S_BLKSIZE is missing on Android NDK (Termux) */
    163 #endif
    164 
    165 /*
    166  * NAME_MAX and PATH_MAX may not exist, e.g. with dirent.c_name being a
    167  * flexible array on Illumos. Use somewhat accommodating fallback values.
    168  */
    169 #ifndef NAME_MAX
    170 #define NAME_MAX 255
    171 #endif
    172 
    173 #ifndef PATH_MAX
    174 #define PATH_MAX 4096
    175 #endif
    176 
    177 #define _ABSSUB(N, M)   (((N) <= (M)) ? ((M) - (N)) : ((N) - (M)))
    178 #define ELEMENTS(x)     (sizeof(x) / sizeof(*(x)))
    179 #undef MIN
    180 #define MIN(x, y)       ((x) < (y) ? (x) : (y))
    181 #undef MAX
    182 #define MAX(x, y)       ((x) > (y) ? (x) : (y))
    183 #define ISODD(x)        ((x) & 1)
    184 #define ISBLANK(x)      ((x) == ' ' || (x) == '\t')
    185 #define ISSPACE(x)      (ISBLANK(x) || (x) == '\n' || (x) == '\r' || (x) == '\f' || (x) == '\v')
    186 #define TOUPPER(ch)     (((ch) >= 'a' && (ch) <= 'z') ? ((ch) - 'a' + 'A') : (ch))
    187 #define TOLOWER(ch)     (((ch) >= 'A' && (ch) <= 'Z') ? ((ch) - 'A' + 'a') : (ch))
    188 #define ISUPPER_(ch)    ((ch) >= 'A' && (ch) <= 'Z')
    189 #define ISLOWER_(ch)    ((ch) >= 'a' && (ch) <= 'z')
    190 #define CMD_LEN_MAX     (PATH_MAX + ((NAME_MAX + 1) << 1))
    191 #define ALIGN_UP(x, A)  ((((x) + (A) - 1) / (A)) * (A))
    192 #define READLINE_MAX    256UL
    193 #define FILTER          '/'
    194 #define RFILTER         '\\'
    195 #define CASE            ':'
    196 #define MSGWAIT         '$'
    197 #define SELECT          ' '
    198 #define PROMPT          ">>> "
    199 #undef NEWLINE
    200 #define NEWLINE         "\n"
    201 #define NEWLINE_CHAR    '\n'
    202 #define NUL_CHAR        '\0'
    203 #define REGEX_MAX       48
    204 #define ENTRY_INCR      64 /* Number of dir 'entry' structures to allocate per shot */
    205 #define ENTRY_INCR_DU   1024 /* Larger increment in du mode to reduce realloc and wait-for-threads */
    206 #define TASK_CAP_DU     256  /* Initial number of tasks for disk usage */
    207 #define NAMEBUF_INCR    0x800 /* 64 dir entries at once, avg. 32 chars per file name = 64*32B = 2KB */
    208 #define DESCRIPTOR_LEN  32
    209 #define _ALIGNMENT      0x10 /* 16-byte alignment */
    210 #define _ALIGNMENT_MASK 0xF
    211 #define TMP_LEN_MAX     64
    212 #define DOT_FILTER_LEN  7
    213 #define ASCII_MAX       128
    214 #define EXEC_ARGS_MAX   10
    215 #define LIST_FILES_MAX  (1 << 14) /* Support listing 16K files */
    216 #define LIST_INPUT_MAX  ((size_t)LIST_FILES_MAX * PATH_MAX)
    217 #define SCROLLOFF       3 /* Leave top 2 lines */
    218 #define ONSCREEN        (xlines - 4) /* Leave top 2 and bottom 2 lines */
    219 #define COLOR_256       256
    220 #define CREATE_NEW_KEY  (-1)
    221 #define SIZE_8KB        (8 * 1024) /* 8 KB in bytes */
    222 #define SIZE_16MB       (16 * 1024 * 1024) /* 16 MB in bytes */
    223 #define RM_RECONFIRM    (10) /* Reconfirm rm on deleting more than these number of files */
    224 
    225 /* Time intervals */
    226 #define DBLCLK_INTERVAL_NS (400000000)
    227 #define XDELAY_INTERVAL_MS (350000) /* 350 ms delay */
    228 
    229 /* Use 8 contexts */
    230 #define CTX_MAX 8
    231 
    232 #ifndef SED
    233 /* BSDs or Solaris or SunOS */
    234 #if defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__) || defined(sun) || defined(__sun) || defined(__DragonFly__)
    235 #define SED "gsed"
    236 #else
    237 #define SED "sed"
    238 #endif
    239 #endif
    240 
    241 /* Large selection threshold */
    242 #ifndef LARGESEL
    243 #define LARGESEL 1000
    244 #endif
    245 
    246 #define MIN_DISPLAY_COL (CTX_MAX * 2)
    247 #define ARCHIVE_CMD_LEN 16
    248 #define BLK_SHIFT_512   9
    249 
    250 /* Detect hardlinks in du */
    251 #define HASH_BITS   (0xFFFFFF)
    252 #define HASH_OCTETS (HASH_BITS >> 6) /* 2^6 = 64 */
    253 
    254 /* Entry flags */
    255 #define DIR_OR_DIRLNK 0x01
    256 #define HARD_LINK     0x02
    257 #define SYM_ORPHAN    0x04
    258 #define FILE_MISSING  0x08
    259 #define FILE_SELECTED 0x10
    260 #define FILE_SCANNED  0x20
    261 #define FILE_YOUNG    0x40
    262 
    263 #define IS_DIR_OR_DIRLNK(ent) (((ent)->flags & DIR_OR_DIRLNK) != 0)
    264 
    265 /* Macros to define process spawn behaviour as flags */
    266 #define F_NONE    0x00  /* no flag set */
    267 #define F_MULTI   0x01  /* first arg can be combination of args; to be used with F_NORMAL */
    268 #define F_NOWAIT  0x02  /* don't wait for child process (e.g. file manager) */
    269 #define F_NOTRACE 0x04  /* suppress stdout and stderr (no traces) */
    270 #define F_NORMAL  0x08  /* spawn child process in non-curses regular CLI mode */
    271 #define F_CONFIRM 0x10  /* run command - show results before exit (must have F_NORMAL) */
    272 #define F_CHKRTN  0x20  /* wait for user prompt if cmd returns failure status */
    273 #define F_NOSTDIN 0x40  /* suppress stdin */
    274 #define F_PAGE    0x80  /* page output in run-cmd-as-plugin mode */
    275 #define F_TTY     0x100 /* Force stdout to go to tty if redirected to a non-tty */
    276 #define F_WINDOW  0x200 /* Show the contents in a floating window */
    277 #define F_CLI     (F_NORMAL | F_MULTI)
    278 #define F_SILENT  (F_CLI | F_NOTRACE)
    279 
    280 /* Version compare macros */
    281 /*
    282  * states: S_N: normal, S_I: comparing integral part, S_F: comparing
    283  *         fractional parts, S_Z: idem but with leading Zeroes only
    284  */
    285 #define S_N 0x0
    286 #define S_I 0x3
    287 #define S_F 0x6
    288 #define S_Z 0x9
    289 
    290 /* result_type: VCMP: return diff; VLEN: compare using len_diff/diff */
    291 #define VCMP 2
    292 #define VLEN 3
    293 
    294 /* Command history */
    295 #define MAX_HISTORY 0x10
    296 #define INVALID_POS 0xFF
    297 
    298 /* Volume info */
    299 #define VFS_AVAIL 0
    300 #define VFS_USED  1
    301 #define VFS_SIZE  2
    302 
    303 /* TYPE DEFINITIONS */
    304 typedef unsigned int uint_t;
    305 typedef unsigned char uchar_t;
    306 typedef unsigned short ushort_t;
    307 typedef unsigned long long ullong_t;
    308 
    309 /* STRUCTURES */
    310 
    311 /* Directory entry */
    312 typedef struct entry {
    313 	char *name;  /* 8 bytes */
    314 	time_t sec;  /* 8 bytes */
    315 	uint_t nsec; /* 4 bytes (enough to store nanosec) */
    316 	mode_t mode; /* 4 bytes */
    317 	off_t size;  /* 8 bytes */
    318 	struct {
    319 		ullong_t blocks : 40; /* 5 bytes (enough for 512 TiB in 512B blocks allocated) */
    320 		ullong_t nlen   : 16; /* 2 bytes (length of file name) */
    321 		ullong_t flags  : 8;  /* 1 byte (flags specific to the file) */
    322 	};
    323 #ifndef NOUG
    324 	uid_t uid; /* 4 bytes */
    325 	gid_t gid; /* 4 bytes */
    326 #endif
    327 } *pEntry;
    328 
    329 /* Selection marker */
    330 typedef struct {
    331 	char *startpos;
    332 	size_t len;
    333 } selmark;
    334 
    335 /* Key-value pairs from env */
    336 typedef struct {
    337 	int key;
    338 	int off;
    339 } kv;
    340 
    341 typedef struct {
    342 #ifdef PCRE2
    343 	const pcre2_code *pcre2x;
    344 #else
    345 	const regex_t *regex;
    346 #endif
    347 	const char *str;
    348 } fltrexp_t;
    349 
    350 /*
    351  * Settings
    352  */
    353 typedef struct {
    354 	uint_t filtermode : 1;  /* Set to enter filter mode */
    355 	uint_t timeorder  : 1;  /* Set to sort by time */
    356 	uint_t sizeorder  : 1;  /* Set to sort by file size */
    357 	uint_t apparentsz : 1;  /* Set to sort by apparent size (disk usage) */
    358 	uint_t blkorder   : 1;  /* Set to sort by blocks used (disk usage) */
    359 	uint_t extnorder  : 1;  /* Order by extension */
    360 	uint_t showhidden : 1;  /* Set to show hidden files */
    361 	uint_t preview    : 1;  /* Show preview pane */
    362 	uint_t showdetail : 1;  /* Clear to show lesser file info */
    363 	uint_t ctxactive  : 1;  /* Context active or not */
    364 	uint_t reverse    : 1;  /* Reverse sort */
    365 	uint_t version    : 1;  /* Version sort */
    366 	uint_t reserved1  : 1;
    367 	/* The following settings are global */
    368 	uint_t curctx     : 3;  /* Current context number */
    369 	uint_t prefersel  : 1;  /* Prefer selection over current, if exists */
    370 	uint_t fileinfo   : 1;  /* Show file information on hover */
    371 	uint_t nonavopen  : 1;  /* Open file on right arrow or `l` */
    372 	uint_t autoenter  : 1;  /* auto-enter dir in type-to-nav mode */
    373 	uint_t reserved2  : 1;
    374 	uint_t useeditor  : 1;  /* Use VISUAL to open text files */
    375 	uint_t reserved3  : 2;
    376 	uint_t fuzzy      : 1;  /* Use fuzzy filters */
    377 	uint_t regex      : 1;  /* Use regex filters */
    378 	uint_t x11        : 1;  /* Copy to system clipboard, show notis, xterm title */
    379 	uint_t timetype   : 2;  /* Time sort type (0: access, 1: change, 2: modification) */
    380 	uint_t cliopener  : 1;  /* All-CLI app opener */
    381 	uint_t waitedit   : 1;  /* For ops that can't be detached, used EDITOR */
    382 	uint_t rollover   : 1;  /* Roll over at edges */
    383 } settings;
    384 
    385 /* Non-persistent program-internal states (alphabeical order) */
    386 typedef struct {
    387 	uint_t autofifo   : 1;  /* Auto-create NNN_FIFO */
    388 	uint_t autonext   : 1;  /* Auto-advance on file open */
    389 	uint_t dircolor   : 1;  /* Current status of dir color */
    390 	uint_t dirctx     : 1;  /* Show dirs in context color */
    391 	uint_t duinit     : 1;  /* Initialize disk usage */
    392 	uint_t fifomode   : 1;  /* FIFO notify mode: 0: preview, 1: explorer */
    393 	uint_t forcequit  : 1;  /* Do not prompt on quit */
    394 	uint_t initfile   : 1;  /* Positional arg is a file */
    395 	uint_t interrupt  : 1;  /* Program received an interrupt */
    396 	uint_t move       : 1;  /* Move operation */
    397 	uint_t oldcolor   : 1;  /* Use older colorscheme */
    398 	uint_t picked     : 1;  /* Plugin has picked files */
    399 	uint_t picker     : 1;  /* Write selection to user-specified file */
    400 	uint_t pluginit   : 1;  /* Plugin framework initialized */
    401 	uint_t prstssn    : 1;  /* Persistent session */
    402 	uint_t rangesel   : 1;  /* Range selection on */
    403 	uint_t runctx     : 3;  /* The context in which plugin is to be run */
    404 	uint_t runplugin  : 1;  /* Choose plugin mode */
    405 	uint_t selbm      : 1;  /* Select a bookmark from bookmarks directory */
    406 	uint_t selmode    : 1;  /* Set when selecting files */
    407 	uint_t stayonsel  : 1;  /* Disable auto-advance on selection */
    408 	uint_t uidgid     : 1;  /* Show owner and group info */
    409 	uint_t usebsdtar  : 1;  /* Use bsdtar as default archive utility */
    410 	uint_t xprompt    : 1;  /* Use native prompt instead of readline prompt */
    411 	uint_t showlines  : 1;  /* Show line numbers */
    412 	uint_t plugparsed : 1;  /* Plugin kv pairs parsed */
    413 	uint_t reserved   : 4;  /* Adjust when adding/removing a field */
    414 } runstate;
    415 
    416 /* Contexts or workspaces */
    417 typedef struct {
    418 	char c_path[PATH_MAX];     /* Current dir */
    419 	char c_last[PATH_MAX];     /* Last visited dir */
    420 	char c_name[NAME_MAX + 1]; /* Current file name */
    421 	char c_fltr[REGEX_MAX];    /* Current filter */
    422 	settings c_cfg;            /* Current configuration */
    423 	uint_t color;              /* Color code for directories */
    424 } context;
    425 
    426 #ifndef NOSSN
    427 typedef struct {
    428 	size_t ver;
    429 	size_t pathln[CTX_MAX];
    430 	size_t lastln[CTX_MAX];
    431 	size_t nameln[CTX_MAX];
    432 	size_t fltrln[CTX_MAX];
    433 } session_header_t;
    434 #endif
    435 
    436 /* GLOBALS */
    437 
    438 /* Configuration, contexts */
    439 static settings cfg = {
    440 	.ctxactive = 1,
    441 	.autoenter = 1,
    442 	.timetype = 2, /* T_MOD */
    443 	.rollover = 1,
    444 };
    445 
    446 alignas(max_align_t) static context g_ctx[CTX_MAX];
    447 
    448 static int ndents, cur, last, curscroll, last_curscroll, total_dents = ENTRY_INCR, scroll_lines = 1;
    449 static int nselected;
    450 #ifndef NOFIFO
    451 static int fifofd = -1;
    452 #endif
    453 static int devnullfd = -1;
    454 static time_t gtimesecs;
    455 static uint_t idletimeout, selbufpos, selbuflen;
    456 static ushort_t xlines, xcols;
    457 static ushort_t idle;
    458 static uchar_t maxbm, maxplug, maxorder;
    459 static uchar_t cfgsort[CTX_MAX + 1];
    460 static uchar_t selcmdpos = INVALID_POS, lastcmdpos = INVALID_POS;
    461 static char *cmd_hist[MAX_HISTORY] = {0};
    462 static char *bmstr;
    463 static char *pluginstr;
    464 static char *orderstr;
    465 static char *opener;
    466 static char *editor;
    467 static char *enveditor;
    468 static char *pager;
    469 static char *shell;
    470 static char *home;
    471 static char *initpath;
    472 static char *cfgpath;
    473 static char *selpath;
    474 static char *listpath;
    475 static char *listroot;
    476 static char *plgpath;
    477 static char *pnamebuf, *pselbuf, *findselpos;
    478 static char *mark;
    479 static char *trashcmd;
    480 static char *previewer = NULL;
    481 #ifndef NOX11
    482 static char hostname[_POSIX_HOST_NAME_MAX + 1];
    483 #endif
    484 #ifndef NOFIFO
    485 static char *fifopath;
    486 #endif
    487 static ullong_t *ihashbmp;
    488 static ullong_t *dir_dispatched_bmp; /* dir inodes already dispatched (avoid double-count same subtree) */
    489 static struct entry *pdents;
    490 static blkcnt_t dir_blocks;
    491 static kv *bookmark;
    492 static kv *plug;
    493 static kv *order;
    494 static uchar_t tmpfplen, homelen;
    495 static uchar_t blk_shift = BLK_SHIFT_512;
    496 #ifndef NOMOUSE
    497 static int middle_click_key;
    498 #endif
    499 #ifdef PCRE2
    500 static pcre2_code *archive_pcre2;
    501 #else
    502 static regex_t archive_re;
    503 #endif
    504 #ifndef NOSSN
    505 static char curssn[NAME_MAX + 1];
    506 #endif
    507 
    508 /* pthread related */
    509 #define NUM_DU_THREADS_MAX 32
    510 
    511 static int num_du_threads;  /* Set from sysconf(_SC_NPROCESSORS_ONLN), 2..NUM_DU_THREADS_MAX */
    512 static volatile int active_threads;
    513 static pthread_mutex_t running_mutex = PTHREAD_MUTEX_INITIALIZER;
    514 static pthread_mutex_t du_count_mutex = PTHREAD_MUTEX_INITIALIZER;
    515 static pthread_cond_t du_cond = PTHREAD_COND_INITIALIZER;
    516 static pthread_cond_t work_cond = PTHREAD_COND_INITIALIZER;
    517 static volatile bool work_ready[NUM_DU_THREADS_MAX];
    518 static volatile bool du_shutdown;
    519 static pthread_t worker_tids[NUM_DU_THREADS_MAX];
    520 #if !defined(__GNUC__) && !defined(__clang__)
    521 static pthread_mutex_t hardlink_mutex = PTHREAD_MUTEX_INITIALIZER;
    522 #endif
    523 static bool first_call;
    524 static ullong_t *core_files;
    525 static blkcnt_t *core_blocks;
    526 static ullong_t num_files;
    527 
    528 typedef struct {
    529 	blkcnt_t blocks;
    530 	ullong_t files;
    531 	size_t pending;
    532 	int entnum;
    533 	bool mntpoint;
    534 	bool no_aggregate;
    535 } du_group;
    536 
    537 typedef struct {
    538 	char *path;
    539 	du_group *group;
    540 	bool count_root;
    541 } du_task;
    542 
    543 static du_task *du_tasks;
    544 static size_t du_task_len;
    545 static size_t du_task_cap;
    546 static size_t du_tasks_pending;
    547 
    548 typedef struct {
    549 	char path[PATH_MAX];
    550 	int entnum;
    551 	ushort_t core;
    552 	bool mntpoint;
    553 	bool no_aggregate; /* same dir seen twice (e.g. symlink); fill pdents only, don't add to totals */
    554 } thread_data;
    555 
    556 static thread_data *core_data;
    557 
    558 /* Retain old signal handlers */
    559 static struct sigaction oldsighup;
    560 static struct sigaction oldsigtstp;
    561 static struct sigaction oldsigwinch;
    562 
    563 /* For use in functions which are isolated and don't return the buffer */
    564 alignas(max_align_t) static char g_buf[CMD_LEN_MAX];
    565 
    566 /* For use as a scratch buffer in selection manipulation */
    567 alignas(max_align_t) static char g_sel[PATH_MAX];
    568 
    569 /* Buffer to store tmp file path to show selection, file stats and help */
    570 alignas(max_align_t) static char g_tmpfpath[TMP_LEN_MAX];
    571 
    572 /* Buffer to store plugins control pipe location */
    573 alignas(max_align_t) static char g_pipepath[TMP_LEN_MAX];
    574 
    575 /* Non-persistent runtime states */
    576 static runstate g_state;
    577 
    578 /* Options to identify file MIME */
    579 #ifdef __APPLE__
    580 #define FILE_MIME_OPTS "-bIL"
    581 #elif !defined(__sun) /* no MIME option for 'file' */
    582 #define FILE_MIME_OPTS "-biL"
    583 #endif
    584 
    585 /* Macros for utilities */
    586 #define UTIL_OPENER    0
    587 #define UTIL_ATOOL     1
    588 #define UTIL_BSDTAR    2
    589 #define UTIL_UNZIP     3
    590 #define UTIL_TAR       4
    591 #define UTIL_LOCKER    5
    592 #define UTIL_LAUNCH    6
    593 #define UTIL_SH_EXEC   7
    594 #define UTIL_BASH      8
    595 #define UTIL_SSHFS     9
    596 #define UTIL_RCLONE    10
    597 #define UTIL_VI        11
    598 #define UTIL_LESS      12
    599 #define UTIL_SH        13
    600 #define UTIL_FZF       14
    601 #define UTIL_NTFY      15
    602 #define UTIL_CBCP      16
    603 #define UTIL_NMV       17
    604 #define UTIL_TRASH_CLI 18
    605 #define UTIL_GIO_TRASH 19
    606 #define UTIL_RM_RF     20
    607 #define UTIL_ARCHMNT   21
    608 #define UTIL_NPREVIEW  22
    609 
    610 /* Utilities to open files, run actions */
    611 static char * const utils[] = {
    612 #ifdef __APPLE__
    613 	"/usr/bin/open",
    614 #elif defined __CYGWIN__
    615 	"cygstart",
    616 #elif defined __HAIKU__
    617 	"open",
    618 #else
    619 	"xdg-open",
    620 #endif
    621 	"atool",
    622 	"bsdtar",
    623 	"unzip",
    624 	"tar",
    625 #ifdef __APPLE__
    626 	"bashlock",
    627 #elif defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__DragonFly__)
    628 	"lock",
    629 #elif defined __HAIKU__
    630 	"peaclock",
    631 #else
    632 	"vlock",
    633 #endif
    634 	"launch",
    635 	"sh -c",
    636 	"bash",
    637 	"sshfs",
    638 	"rclone",
    639 	"vi",
    640 	"less",
    641 	"sh",
    642 	"fzf",
    643 	".ntfy",
    644 	".cbcp",
    645 	".nmv",
    646 	"trash-put",
    647 	"gio trash",
    648 	"rm -rf --",
    649 	"archivemount",
    650 	".npreview",
    651 };
    652 
    653 /* Common strings */
    654 #define MSG_ZERO         0 /* Unused */
    655 #define MSG_0_ENTRIES    1
    656 #define STR_TMPFILE      2
    657 #define MSG_0_SELECTED   3
    658 #define MSG_CANCEL       4
    659 #define MSG_FAILED       5
    660 #define MSG_SSN_NAME     6
    661 #define MSG_CP_MV_AS     7
    662 #define MSG_CUR_SEL_OPTS 8
    663 #define MSG_FILE_LIMIT   9
    664 #define MSG_SIZE_LIMIT   10
    665 #define MSG_NEW_OPTS     11
    666 #define MSG_CLI_MODE     12
    667 #define MSG_OVERWRITE    13
    668 #define MSG_SSN_OPTS     14
    669 #define MSG_QUIT_ALL     15
    670 #define MSG_HOSTNAME     16
    671 #define MSG_ARCHIVE_NAME 17
    672 #define MSG_OPEN_WITH    18
    673 #define MSG_NEW_PATH     19
    674 #define MSG_LINK_PREFIX  20
    675 #define MSG_COPY_NAME    21
    676 #define MSG_ENTER        22
    677 #define MSG_SEL_MISSING  23
    678 #define MSG_ACCESS       24
    679 #define MSG_EMPTY_FILE   25
    680 #define MSG_UNSUPPORTED  26
    681 #define MSG_NOT_SET      27
    682 #define MSG_EXISTS       28
    683 #define MSG_FEW_COLUMNS  29
    684 #define MSG_REMOTE_OPTS  30
    685 #define MSG_RCLONE_DELAY 31
    686 #define MSG_APP_NAME     32
    687 #define MSG_ARCHIVE_OPTS 33
    688 #define MSG_KEYS         34
    689 #define MSG_INVALID_REG  35
    690 #define MSG_ORDER        36
    691 #define MSG_LAZY         37
    692 #define MSG_FIRST        38
    693 #define MSG_RM_TMP       39
    694 #define MSG_INVALID_KEY  40
    695 #define MSG_NOCHANGE     41
    696 #define MSG_DIR_CHANGED  42
    697 #define MSG_BM_NAME      43
    698 
    699 static const char * const messages[] = {
    700 	"",
    701 	"0 entries",
    702 	"/.nnnXXXXXX",
    703 	"0 selected",
    704 	"cancelled",
    705 	"failed!",
    706 	"session name: ",
    707 	"'c'p/'m'v as?",
    708 	"'c'urrent/'s'el?",
    709 	"file limit exceeded",
    710 	"size limit exceeded",
    711 	"['f'ile]/'d'ir/'s'ym/'h'ard?",
    712 	"['g'ui]/'c'li?",
    713 	"overwrite?",
    714 	"'s'ave/'l'oad/'r'estore?",
    715 	"Quit all contexts?",
    716 	"remote name (- for hovered): ",
    717 	"archive [path/]name: ",
    718 	"open with: ",
    719 	"[path/]name: ",
    720 	"link prefix [@ for none]: ",
    721 	"copy [path/]name: ",
    722 	"\n'Enter' to continue",
    723 	"open failed",
    724 	"dir inaccessible",
    725 	"empty! edit/open with",
    726 	"?",
    727 	"not set",
    728 	"entry exists",
    729 	"too few cols!",
    730 	"'s'shfs/'r'clone?",
    731 	"refresh if slow",
    732 	"app: ",
    733 	"['l's]/'o'pen/e'x'tract/'m'nt?",
    734 	"keys:",
    735 	"invalid regex",
    736 	"'a'u/'d'u/'e'xt/'r'ev/'s'z/'t'm/'v'er/'c'lr/'^T'?",
    737 	"unmount failed! try lazy?",
    738 	"first file (\')/char?",
    739 	"remove tmp file?",
    740 	"invalid key",
    741 	"unchanged",
    742 	"dir changed, range sel off",
    743 	"name: ",
    744 };
    745 
    746 /* Supported configuration environment variables */
    747 #define NNN_OPTS    0
    748 #define NNN_BMS     1
    749 #define NNN_PLUG    2
    750 #define NNN_OPENER  3
    751 #define NNN_COLORS  4
    752 #define NNN_FCOLORS 5
    753 #define NNNLVL      6
    754 #define NNN_PIPE    7
    755 #define NNN_MCLICK  8
    756 #define NNN_SEL     9
    757 #define NNN_ARCHIVE 10
    758 #define NNN_ORDER   11
    759 #define NNN_HELP    12
    760 #define NNN_TRASH   13
    761 
    762 static const char * const env_cfg[] = {
    763 	"NNN_OPTS",
    764 	"NNN_BMS",
    765 	"NNN_PLUG",
    766 	"NNN_OPENER",
    767 	"NNN_COLORS",
    768 	"NNN_FCOLORS",
    769 	"NNNLVL",
    770 	"NNN_PIPE",
    771 	"NNN_MCLICK",
    772 	"NNN_SEL",
    773 	"NNN_ARCHIVE",
    774 	"NNN_ORDER",
    775 	"NNN_HELP",
    776 	"NNN_TRASH",
    777 };
    778 
    779 /* Required environment variables */
    780 #define ENV_SHELL  0
    781 #define ENV_VISUAL 1
    782 #define ENV_EDITOR 2
    783 #define ENV_PAGER  3
    784 #define ENV_NCUR   4
    785 
    786 static const char * const envs[] = {
    787 	"SHELL",
    788 	"VISUAL",
    789 	"EDITOR",
    790 	"PAGER",
    791 	"nnn",
    792 };
    793 
    794 /* Time type used */
    795 #define T_ACCESS 0
    796 #define T_CHANGE 1
    797 #define T_MOD    2
    798 
    799 /* Platform-agnostic nanosecond timestamp accessors */
    800 #ifdef __APPLE__
    801 #define NSEC_MTIME(sb) ((uint_t)(sb).st_mtimespec.tv_nsec)
    802 #define NSEC_ATIME(sb) ((uint_t)(sb).st_atimespec.tv_nsec)
    803 #define NSEC_CTIME(sb) ((uint_t)(sb).st_ctimespec.tv_nsec)
    804 #else
    805 #define NSEC_MTIME(sb) ((uint_t)(sb).st_mtim.tv_nsec)
    806 #define NSEC_ATIME(sb) ((uint_t)(sb).st_atim.tv_nsec)
    807 #define NSEC_CTIME(sb) ((uint_t)(sb).st_ctim.tv_nsec)
    808 #endif
    809 
    810 #define PROGRESS_CP   "cpg -giRp --"
    811 #define PROGRESS_MV   "mvg -gi --"
    812 static char cp[sizeof PROGRESS_CP] = "cp -iRp --";
    813 static char mv[sizeof PROGRESS_MV] = "mv -i --";
    814 
    815 /* Archive commands */
    816 static char * const archive_cmd[] = {"atool -a", "bsdtar -acvf", "zip -r", "tar -acvf"};
    817 
    818 /* Tokens used for path creation */
    819 #define TOK_BM  0
    820 #define TOK_SSN 1
    821 #define TOK_MNT 2
    822 #define TOK_PLG 3
    823 
    824 static const char * const toks[] = {
    825 	"bookmarks",
    826 	"sessions",
    827 	"mounts",
    828 	"plugins", /* must be the last entry */
    829 };
    830 
    831 /* Patterns */
    832 #define P_CPMVFMT 0
    833 #define P_CPMVRNM 1
    834 #define P_ARCHIVE 2
    835 #define P_REPLACE 3
    836 #define P_ARCHIVE_CMD 4
    837 
    838 static const char * const patterns[] = {
    839 	SED" -i 's|^\\(\\(.*/\\)\\(.*\\)$\\)|#\\1\\n\\3|' '%s'",
    840 	SED" 's|^\\([^#/][^/]\\?.*\\)$|%s/\\1|;s|^#\\(/.*\\)$|\\1|' "
    841 		"'%s' | tr '\\n' '\\0' | xargs -0 -n2 sh -c '%s \"$0\" \"$@\" < /dev/tty'",
    842 	"\\.(bz|bz2|gz|tar|taz|tbz|tbz2|tgz|z|zip)$", /* Basic formats that don't need external tools */
    843 	SED" -i 's|^%s\\(.*\\)$|%s\\1|' '%s'",
    844 	"xargs -0 %s %s < %s",
    845 };
    846 
    847 /* Colors */
    848 #define C_BLK (CTX_MAX + 1) /* Block device: DarkSeaGreen1 */
    849 #define C_CHR (C_BLK + 1)   /* Character device: Yellow1 */
    850 #define C_DIR (C_CHR + 1)   /* Directory: DeepSkyBlue1 */
    851 #define C_EXE (C_DIR + 1)   /* Executable file: Green1 */
    852 #define C_FIL (C_EXE + 1)   /* Regular file: Normal */
    853 #define C_HRD (C_FIL + 1)   /* Hard link: Plum4 */
    854 #define C_LNK (C_HRD + 1)   /* Symbolic link: Cyan1 */
    855 #define C_MIS (C_LNK + 1)   /* Missing file OR file details: Grey62 */
    856 #define C_ORP (C_MIS + 1)   /* Orphaned symlink: DeepPink1 */
    857 #define C_PIP (C_ORP + 1)   /* Named pipe (FIFO): Orange1 */
    858 #define C_SOC (C_PIP + 1)   /* Socket: MediumOrchid1 */
    859 #define C_UND (C_SOC + 1)   /* Unknown OR 0B regular/exe file: Red1 */
    860 
    861 static char gcolors[] = "c1e2272e006033f7c6d6abc4";
    862 static uint_t fcolors[C_UND + 1] = {0};
    863 
    864 /* Event handling */
    865 #ifdef LINUX_INOTIFY
    866 #define NUM_EVENT_SLOTS 32 /* Make room for 32 events */
    867 #define EVENT_SIZE (sizeof(struct inotify_event))
    868 #define EVENT_BUF_LEN (EVENT_SIZE * NUM_EVENT_SLOTS)
    869 static int inotify_fd, inotify_wd = -1;
    870 static uint_t INOTIFY_MASK = /* IN_ATTRIB | */ IN_CREATE | IN_DELETE | IN_DELETE_SELF
    871 			   | IN_MODIFY | IN_MOVE_SELF | IN_MOVED_FROM | IN_MOVED_TO;
    872 #elif defined(BSD_KQUEUE)
    873 #define NUM_EVENT_SLOTS 1
    874 #define NUM_EVENT_FDS 1
    875 static int kq, event_fd = -1;
    876 static struct kevent events_to_monitor[NUM_EVENT_FDS];
    877 static uint_t KQUEUE_FFLAGS = NOTE_DELETE | NOTE_EXTEND | NOTE_LINK
    878 			    | NOTE_RENAME | NOTE_REVOKE | NOTE_WRITE;
    879 static struct timespec gtimeout;
    880 #elif defined(HAIKU_NM)
    881 static bool haiku_nm_active = FALSE;
    882 static haiku_nm_h haiku_hnd;
    883 #endif
    884 
    885 /* Function macros */
    886 #define tolastln() move(xlines - 1, 0)
    887 #define tocursor() move(cur + 2 - curscroll, 0)
    888 #define exitcurses() endwin()
    889 #define printwarn(presel) printwait(strerror(errno), presel)
    890 #define istopdir(path) ((path)[1] == '\0' && (path)[0] == '/')
    891 #define copycurname() xstrsncpy(lastname, ndents ? pdents[cur].name : "\0", NAME_MAX + 1)
    892 #define settimeout() timeout(1000)
    893 #define cleartimeout() timeout(-1)
    894 #define errexit() printerr(__LINE__)
    895 #define setdirwatch() (cfg.filtermode ? (presel = FILTER) : (watch = TRUE))
    896 #define filterset() (g_ctx[cfg.curctx].c_fltr[1])
    897 /* We don't care about the return value from strcmp() */
    898 #define xstrcmp(a, b)  (*(a) != *(b) ? -1 : strcmp((a), (b)))
    899 /* A faster version of xisdigit */
    900 #define xisdigit(c) ((unsigned int) (c) - '0' <= 9)
    901 #define xerror() perror(xitoa(__LINE__))
    902 
    903 #ifdef TOURBIN_QSORT
    904 #define ENTLESS(i, j) (entrycmpfn(pdents + (i), pdents + (j)) < 0)
    905 #define ENTSWAP(i, j) (swap_ent((i), (j)))
    906 #define ENTSORT(pdents, ndents, entrycmpfn) QSORT((ndents), ENTLESS, ENTSWAP)
    907 #else
    908 #define ENTSORT(pdents, ndents, entrycmpfn) qsort((pdents), (ndents), sizeof(*(pdents)), (entrycmpfn))
    909 #endif
    910 
    911 #ifndef __GLIBC__
    912 #define xstrlen(s) strlen(s)
    913 #else
    914 #define xstrlen(s) ((char *)rawmemchr(s, '\0') - (s))
    915 #endif
    916 
    917 /* Function forward declarations */
    918 static void redraw(char *path);
    919 static int spawn(char *command, char *arg1, char *arg2, char *arg3, ushort_t flag);
    920 static void move_cursor(int target, int ignore_scrolloff);
    921 static char *load_input(int fd, const char *path);
    922 static int set_sort_flags(int r);
    923 static void statusbar(char *path);
    924 static bool get_output(char *command, char *arg1, char *arg2, int fdout, bool page);
    925 #ifndef NOFIFO
    926 static void notify_fifo(bool force);
    927 #endif
    928 static inline bool selforparent(const char *path);
    929 static void dirwalk(char *path, int entnum, bool mountpoint, bool no_aggregate);
    930 
    931 /* Functions */
    932 
    933 static void sigint_handler(int sig)
    934 {
    935 	(void) sig;
    936 	g_state.interrupt = 1;
    937 }
    938 
    939 static void clean_exit_sighandler(int sig)
    940 {
    941 	(void) sig;
    942 	exitcurses();
    943 	/* This triggers cleanup() thanks to atexit() */
    944 	exit(EXIT_SUCCESS);
    945 }
    946 
    947 static char *xitoa(uint_t val)
    948 {
    949 	static char dst[32] = {'\0'};
    950 	static const char digits[201] =
    951 		"0001020304050607080910111213141516171819"
    952 		"2021222324252627282930313233343536373839"
    953 		"4041424344454647484950515253545556575859"
    954 		"6061626364656667686970717273747576777879"
    955 		"8081828384858687888990919293949596979899";
    956 	uint_t next = 30, quo, i;
    957 
    958 	while (val >= 100) {
    959 		quo = val / 100;
    960 		i = (val - (quo * 100)) * 2;
    961 		val = quo;
    962 		dst[next] = digits[i + 1];
    963 		dst[--next] = digits[i];
    964 		--next;
    965 	}
    966 
    967 	/* Handle last 1-2 digits */
    968 	if (val < 10)
    969 		dst[next] = '0' + val;
    970 	else {
    971 		i = val * 2;
    972 		dst[next] = digits[i + 1];
    973 		dst[--next] = digits[i];
    974 	}
    975 
    976 	return &dst[next];
    977 }
    978 
    979 /* Return the integer value of a char representing HEX */
    980 static uchar_t xchartohex(uchar_t c)
    981 {
    982 	if (xisdigit(c))
    983 		return c - '0';
    984 
    985 	if (c >= 'a' && c <= 'f')
    986 		return c - 'a' + 10;
    987 
    988 	if (c >= 'A' && c <= 'F')
    989 		return c - 'A' + 10;
    990 
    991 	return c;
    992 }
    993 
    994 /*
    995  * Source: https://elixir.bootlin.com/linux/latest/source/arch/alpha/include/asm/bitops.h
    996  * Optimized: atomic test-and-set per word so different inodes (different words) don't contend.
    997  */
    998 static inline bool test_set_bit(uint_t nr)
    999 {
   1000 	nr &= HASH_BITS;
   1001 	ullong_t *m = ihashbmp + (nr >> 6);
   1002 	ullong_t bit = 1ULL << (nr & 63);
   1003 
   1004 #if defined(__GNUC__) || defined(__clang__)
   1005 	/* Relaxed ordering is sufficient: only need atomicity for this bitmap */
   1006 	ullong_t old = __atomic_fetch_or(m, bit, __ATOMIC_RELAXED);
   1007 	return (old & bit) == 0;
   1008 #else
   1009 	pthread_mutex_lock(&hardlink_mutex);
   1010 	if (*m & bit) {
   1011 		pthread_mutex_unlock(&hardlink_mutex);
   1012 		return FALSE;
   1013 	}
   1014 	*m |= bit;
   1015 	pthread_mutex_unlock(&hardlink_mutex);
   1016 	return TRUE;
   1017 #endif
   1018 }
   1019 
   1020 /* Track directory (dev,ino) already dispatched; main-thread only, avoids double-counting same subtree */
   1021 static bool test_set_bit_dir(dev_t dev, ino_t ino)
   1022 {
   1023 	uint_t nr = (uint_t)((ullong_t)dev ^ (ullong_t)ino) & HASH_BITS;
   1024 	ullong_t *m = dir_dispatched_bmp + (nr >> 6);
   1025 	ullong_t bit = 1ULL << (nr & 63);
   1026 
   1027 	if (*m & bit)
   1028 		return FALSE;
   1029 	*m |= bit;
   1030 	return TRUE;
   1031 }
   1032 
   1033 #ifndef __APPLE__
   1034 /* Increase the limit on open file descriptors, if possible */
   1035 static void max_openfds(void)
   1036 {
   1037 	struct rlimit rl;
   1038 
   1039 	if (!getrlimit(RLIMIT_NOFILE, &rl))
   1040 		if (rl.rlim_cur < rl.rlim_max) {
   1041 			rl.rlim_cur = rl.rlim_max;
   1042 			setrlimit(RLIMIT_NOFILE, &rl);
   1043 		}
   1044 }
   1045 #endif
   1046 
   1047 /*
   1048  * Wrapper to realloc()
   1049  * Frees current memory if realloc() fails and returns NULL.
   1050  *
   1051  * The *alloc() family returns aligned address: https://man7.org/linux/man-pages/man3/malloc.3.html
   1052  */
   1053 static void *xrealloc(void *pcur, size_t len)
   1054 {
   1055 	void *pmem = realloc(pcur, len);
   1056 
   1057 	if (!pmem)
   1058 		free(pcur);
   1059 
   1060 	return pmem;
   1061 }
   1062 
   1063 /*
   1064  * Just a safe strncpy(3)
   1065  * Always null ('\0') terminates if both src and dest are valid pointers.
   1066  * Returns the number of bytes copied including terminating null byte.
   1067  */
   1068 static size_t xstrsncpy(char *restrict dst, const char *restrict src, size_t n)
   1069 {
   1070 	char *end = memccpy(dst, src, '\0', n);
   1071 
   1072 	if (!end) {
   1073 		if (n)
   1074 			dst[n - 1] = '\0';
   1075 		end = dst + n; /* If we return n here, binary size increases due to auto-inlining */
   1076 	}
   1077 
   1078 	return end - dst;
   1079 }
   1080 
   1081 static char *xstrdup(const char *restrict s)
   1082 {
   1083 	size_t len = xstrlen(s) + 1;
   1084 	char *ptr = malloc(len);
   1085 	return ptr ? memcpy(ptr, s, len) : NULL;
   1086 }
   1087 
   1088 static bool is_suffix(const char *restrict str, const char *restrict suffix)
   1089 {
   1090 	if (!str || !suffix)
   1091 		return FALSE;
   1092 
   1093 	size_t lenstr = xstrlen(str);
   1094 	size_t lensuffix = xstrlen(suffix);
   1095 
   1096 	if (lensuffix > lenstr)
   1097 		return FALSE;
   1098 
   1099 	return (xstrcmp(str + (lenstr - lensuffix), suffix) == 0);
   1100 }
   1101 
   1102 static inline bool is_prefix(const char *restrict str, const char *restrict prefix, size_t len)
   1103 {
   1104 	return !strncmp(str, prefix, len);
   1105 }
   1106 
   1107 static inline bool is_bad_len_or_dir(const char *restrict path)
   1108 {
   1109 	size_t len = xstrlen(path);
   1110 
   1111 	return ((len >= PATH_MAX) || (path[len - 1] == '/'));
   1112 }
   1113 
   1114 static char *get_cwd_entry(const char *restrict cwdpath, char *entrypath, size_t *tokenlen)
   1115 {
   1116 	size_t len = xstrlen(cwdpath);
   1117 	char *end;
   1118 
   1119 	if (!is_prefix(entrypath, cwdpath, len))
   1120 		return NULL;
   1121 
   1122 	entrypath += len + 1; /* Add 1 for trailing / */
   1123 	end = strchr(entrypath, '/');
   1124 	if (end)
   1125 		*tokenlen = end - entrypath;
   1126 	else
   1127 		*tokenlen = xstrlen(entrypath);
   1128 	DPRINTF_U(*tokenlen);
   1129 
   1130 	return entrypath;
   1131 }
   1132 
   1133 /*
   1134  * The poor man's implementation of memrchr(3).
   1135  * We are only looking for '/' and '.' in this program.
   1136  * And we are NOT expecting a '/' at the end.
   1137  * Ideally 0 < n <= xstrlen(s).
   1138  */
   1139 #if defined(__GLIBC__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__DragonFly__)
   1140 #define xmemrchr(s, ch, n) memrchr(s, ch, n)
   1141 #else
   1142 static void *xmemrchr(uchar_t *restrict s, uchar_t ch, size_t n)
   1143 {
   1144 	if (!s || !n)
   1145 		return NULL;
   1146 
   1147 	uchar_t *ptr = s + n;
   1148 
   1149 	do {
   1150 		if (*--ptr == ch)
   1151 			return ptr;
   1152 	} while (s != ptr);
   1153 
   1154 	return NULL;
   1155 }
   1156 #endif
   1157 
   1158 /* A very simplified implementation, changes path */
   1159 static char *xdirname(char *path)
   1160 {
   1161 	char *base = xmemrchr((uchar_t *)path, '/', xstrlen(path));
   1162 
   1163 	if (base == path)
   1164 		path[1] = '\0';
   1165 	else
   1166 		*base = '\0';
   1167 
   1168 	return path;
   1169 }
   1170 
   1171 static char *xbasename(char *path)
   1172 {
   1173 	char *base = xmemrchr((uchar_t *)path, '/', xstrlen(path)); // NOLINT
   1174 
   1175 	return base ? base + 1 : path;
   1176 }
   1177 
   1178 static inline char *xextension(const char *fname, size_t len)
   1179 {
   1180 	return xmemrchr((uchar_t *)fname, '.', len);
   1181 }
   1182 
   1183 #ifndef NOUG
   1184 /*
   1185  * One-shot cache for getpwuid/getgrgid. Returns the cached name if the
   1186  * provided uid is the same as the previous uid. Returns xitoa(guid) if
   1187  * the guid is not found in the password database.
   1188  */
   1189 static char *getpwname(uid_t uid)
   1190 {
   1191 	static uint_t uidcache = UINT_MAX;
   1192 	static char *namecache;
   1193 
   1194 	if (uidcache != uid) {
   1195 		struct passwd *pw = getpwuid(uid);
   1196 
   1197 		uidcache = uid;
   1198 		namecache = pw ? pw->pw_name : NULL;
   1199 	}
   1200 
   1201 	return namecache ? namecache : xitoa(uid);
   1202 }
   1203 
   1204 static char *getgrname(gid_t gid)
   1205 {
   1206 	static uint_t gidcache = UINT_MAX;
   1207 	static char *grpcache;
   1208 
   1209 	if (gidcache != gid) {
   1210 		struct group *gr = getgrgid(gid);
   1211 
   1212 		gidcache = gid;
   1213 		grpcache = gr ? gr->gr_name : NULL;
   1214 	}
   1215 
   1216 	return grpcache ? grpcache : xitoa(gid);
   1217 }
   1218 #endif
   1219 
   1220 static inline bool getutil(char *util)
   1221 {
   1222 	return spawn("which", util, NULL, NULL, F_NORMAL | F_NOTRACE) == 0;
   1223 }
   1224 
   1225 static inline bool tilde_is_home(const char *s)
   1226 {
   1227 	return s[0] == '~' && (s[1] == '\0' || s[1] == '/');
   1228 }
   1229 
   1230 static inline bool tilde_is_home_strict(const char *s)
   1231 {
   1232 	return s[0] == '~' && s[1] == '/';
   1233 }
   1234 
   1235 /*
   1236  * Updates out with "dir/name or "/name"
   1237  * Returns the number of bytes copied including the terminating NULL byte
   1238  *
   1239  * Note: dir and out must be PATH_MAX in length to avoid macOS fault
   1240  */
   1241 static size_t mkpath(const char *dir, const char *name, char *out)
   1242 {
   1243 	size_t len = 0;
   1244 
   1245 	/* same rational for being strict as abspath() */
   1246 	if (tilde_is_home_strict(name)) { //NOLINT
   1247 		len = xstrsncpy(out, home, PATH_MAX);
   1248 		--len;
   1249 		++name;
   1250 	} else if (name[0] != '/') { // NOLINT
   1251 		/* Handle root case */
   1252 		len = istopdir(dir) ? 1 : xstrsncpy(out, dir, PATH_MAX);
   1253 		out[len - 1] = '/'; // NOLINT
   1254 	}
   1255 	return (xstrsncpy(out + len, name, PATH_MAX - len) + len);
   1256 }
   1257 
   1258 /* Assumes both the paths passed are directories */
   1259 static char *common_prefix(const char *path, char *prefix)
   1260 {
   1261 	const char *x = path, *y = prefix;
   1262 	char *sep;
   1263 
   1264 	if (!path || !*path || !prefix)
   1265 		return NULL;
   1266 
   1267 	if (!*prefix) {
   1268 		xstrsncpy(prefix, path, PATH_MAX);
   1269 		return prefix;
   1270 	}
   1271 
   1272 	while (*x && *y && (*x == *y))
   1273 		++x, ++y;
   1274 
   1275 	/* Strings are same */
   1276 	if (!*x && !*y)
   1277 		return prefix;
   1278 
   1279 	/* Path is shorter */
   1280 	if (!*x && *y == '/') {
   1281 		xstrsncpy(prefix, path, y - path);
   1282 		return prefix;
   1283 	}
   1284 
   1285 	/* Prefix is shorter */
   1286 	if (!*y && *x == '/')
   1287 		return prefix;
   1288 
   1289 	/* Shorten prefix */
   1290 	prefix[y - prefix] = '\0';
   1291 
   1292 	sep = xmemrchr((uchar_t *)prefix, '/', y - prefix);
   1293 	if (sep != prefix)
   1294 		*sep = '\0';
   1295 	else /* Just '/' */
   1296 		prefix[1] = '\0';
   1297 
   1298 	return prefix;
   1299 }
   1300 
   1301 /*
   1302  * The library function realpath() resolves symlinks.
   1303  * If there's a symlink in file list we want to show the symlink not what it's points to.
   1304  * Resolves ./../~ in filepath
   1305  */
   1306 static char *abspath(const char *filepath, char *cwd, char *buf)
   1307 {
   1308 	const char *path = filepath;
   1309 	bool allocated = FALSE;
   1310 
   1311 	if (!path)
   1312 		return NULL;
   1313 
   1314 	/* when dealing with tilde, we need to be strict.
   1315 	 * otherwise a file named "~" can end up expanding to
   1316 	 * $HOME and causing disaster */
   1317 	if (tilde_is_home_strict(path)) {
   1318 		cwd = home;
   1319 		path += 2; /* advance 2 bytes past the "~/" */
   1320 	} else if ((path[0] != '/') && !cwd) {
   1321 		cwd = getcwd(NULL, 0); // NOLINT
   1322 		if (!cwd)
   1323 			return NULL;
   1324 		allocated = TRUE;
   1325 	}
   1326 
   1327 	size_t dst_size = 0, src_size = xstrlen(path), cwd_size = cwd ? xstrlen(cwd) : 0;
   1328 	size_t len = src_size;
   1329 	const char *src;
   1330 	char *dst;
   1331 	/*
   1332 	 * We need to add 2 chars at the end as relative paths may start with:
   1333 	 * ./ (find .)
   1334 	 * no separator (fd .): this needs an additional char for '/'
   1335 	 */
   1336 	char *resolved_path = buf ? buf : malloc(src_size + cwd_size + 2);
   1337 
   1338 	if (!resolved_path) {
   1339 		if (allocated)
   1340 			free(cwd);
   1341 		return NULL;
   1342 	}
   1343 
   1344 	/* Turn relative paths into absolute */
   1345 	if (path[0] != '/') {
   1346 		if (!cwd) {
   1347 			if (!buf)
   1348 				free(resolved_path);
   1349 			return NULL;
   1350 		}
   1351 		dst_size = xstrsncpy(resolved_path, cwd, cwd_size + 1) - 1;
   1352 		if (allocated)
   1353 			free(cwd);
   1354 	} else
   1355 		resolved_path[0] = '\0';
   1356 
   1357 	src = path;
   1358 	dst = resolved_path + dst_size;
   1359 	for (const char *next = NULL; next != path + src_size;) {
   1360 		next = memchr(src, '/', len);
   1361 		if (!next)
   1362 			next = path + src_size;
   1363 
   1364 		if (next - src == 2 && src[0] == '.' && src[1] == '.') {
   1365 			if (dst - resolved_path) {
   1366 				dst = xmemrchr((uchar_t *)resolved_path, '/', dst - resolved_path);
   1367 				*dst = '\0';
   1368 			}
   1369 		} else if (next - src == 1 && src[0] == '.') {
   1370 			/* NOP */
   1371 		} else if (next - src) {
   1372 			*(dst++) = '/';
   1373 			xstrsncpy(dst, src, next - src + 1);
   1374 			dst += next - src;
   1375 		}
   1376 
   1377 		src = next + 1;
   1378 		len = src_size - (src - path);
   1379 	}
   1380 
   1381 	if (*resolved_path == '\0') {
   1382 		resolved_path[0] = '/';
   1383 		resolved_path[1] = '\0';
   1384 	}
   1385 
   1386 	if (xstrlen(resolved_path) >= PATH_MAX) {
   1387 		if (!buf)
   1388 			free(resolved_path);
   1389 		else
   1390 			buf[0] = '\0';
   1391 		return NULL;
   1392 	}
   1393 
   1394 	return resolved_path;
   1395 }
   1396 
   1397 /* finds abspath of link pointed by filepath, taking cwd into account */
   1398 static char *bmtarget(const char *filepath, char *cwd, char *buf)
   1399 {
   1400 	char target[PATH_MAX + 1];
   1401 	ssize_t n = readlink(filepath, target, PATH_MAX);
   1402 	if (n != -1) {
   1403 		target[n] = '\0';
   1404 		return abspath(target, cwd, buf);
   1405 	}
   1406 	return NULL;
   1407 }
   1408 
   1409 /**
   1410  * shell_escape - Escape a string for safe shell usage with single quotes
   1411  *
   1412  * @poutbuf: Pointer to output buffer pointer. If *poutbuf is NULL, memory will be
   1413  *           dynamically allocated. If *poutbuf is non-NULL, it must point to a
   1414  *           buffer of at least *outlen bytes. In dynamic allocation mode, the
   1415  *           caller is responsible for freeing the allocated memory.
   1416  *
   1417  * @note The logic works correctly with Unicode. The function operates on raw bytes
   1418  *       (char by char), and its escaping strategy is single-quoting the entire string ('...').
   1419  *       Inside POSIX single quotes, every byte is treated literally — no interpretation occurs
   1420  *       at all. The only special case is the single quote character itself ('), which is handled.
   1421  *
   1422  *       Since UTF-8 encoded Unicode never produces a 0x27 (') byte in any multi-byte sequence
   1423  *       sequence (all continuation bytes have the high bit set, 0x80–0xBF), the byte-by-byte
   1424  *       copy in the default case faithfully preserves any UTF-8 content without corruption.
   1425  *
   1426  * @outlen: Pointer to the output buffer size. When input is provided:
   1427  *          - The caller allocates a buffer and sets *outlen to its size
   1428  *          - The buffer must be at least 3 bytes
   1429  *          When poutbuf is NULL (dynamic allocation):
   1430  *          - Set *outlen to the desired initial chunk size (or 0 for default 256)
   1431  *          - It will be updated to track the allocated size
   1432  *          - The caller must free the memory in *poutbuf when done
   1433  *
   1434  * @inbuf: The input string to escape (must not be NULL)
   1435  *
   1436  * Return value: On success, returns the length of the escaped string (not including
   1437  *               the null terminator). On error, returns -1 and sets errno:
   1438  *               - EINVAL: if outlen < 3 (for fixed buffers)
   1439  *               - ENAMETOOLONG: if the output buffer is too small (for fixed buffers)
   1440  *               - ENOMEM: if memory allocation fails (for dynamic buffers)
   1441  *
   1442  * Usage examples:
   1443  *
   1444  * Example 1: Using a fixed-size buffer (caller-allocated):
   1445  *     char buf[256];
   1446  *     size_t len = sizeof(buf);
   1447  *     ssize_t result = shell_escape(&buf, &len, "my file.txt");
   1448  *     if (result >= 0) {
   1449  *         printf("Escaped: %s", buf);
   1450  *     }
   1451  *
   1452  * Example 2: Using dynamic allocation (caller must free):
   1453  *     char *escaped = NULL;
   1454  *     size_t capacity = 0;  // 0 uses default initial size
   1455  *     ssize_t result = shell_escape(&escaped, &capacity, "my file.txt");
   1456  *     if (result >= 0) {
   1457  *         printf("Escaped: %s", escaped);
   1458  *         free(escaped);
   1459  *     } else {
   1460  *         fprintf(stderr, "Error: %s", strerror(errno));
   1461  *     }
   1462  *
   1463  * Example 3: Dynamic allocation with specific initial size:
   1464  *     char *escaped = NULL;
   1465  *     size_t capacity = 512;  // Start with 512 byte chunks
   1466  *     ssize_t result = shell_escape(&escaped, &capacity, input_path);
   1467  *     if (result >= 0) {
   1468  *         use_escaped_path(escaped);
   1469  *         free(escaped);
   1470  *     }
   1471  */
   1472 static ssize_t shell_escape(char **poutbuf, size_t *outlen, const char *inbuf)
   1473 {
   1474 	size_t n = xstrlen(inbuf), w = 0;
   1475 	char *buf = *poutbuf;
   1476 	size_t buflen = 0;
   1477 	bool is_dynamic = (buf == NULL);
   1478 
   1479 	if (!is_dynamic) {
   1480 		buf = *poutbuf;
   1481 		buflen = *outlen;
   1482 		if (buflen < 3) {
   1483 			errno = EINVAL;
   1484 			return -1;
   1485 		}
   1486 	}
   1487 
   1488 	/* r=0: opening quote, r=1..n: escaped chars, r=n+1: closing quote */
   1489 	for (size_t r = 0; r < (n + 2); ++r) {
   1490 		/* 4 for the single quote case, 1 from NULL terminator */
   1491 		if (w + 5 >= buflen) {
   1492 			if (is_dynamic) {
   1493 				/* Grow buffer; realloc(NULL, ...) acts as malloc */
   1494 				size_t newsize = buflen + (*outlen > 0 ? *outlen : 256);
   1495 				char *newbuf = realloc(buf, newsize);
   1496 				if (!newbuf) {
   1497 					free(buf);
   1498 					*poutbuf = NULL;
   1499 					errno = ENOMEM;
   1500 					return -1;
   1501 				}
   1502 				buf = newbuf;
   1503 				buflen = newsize;
   1504 				*poutbuf = buf;
   1505 				*outlen = buflen;
   1506 			} else {
   1507 				errno = ENAMETOOLONG;
   1508 				return -1;
   1509 			}
   1510 		}
   1511 
   1512 		if ((r == 0) || (r == (n + 1))) {
   1513 			buf[w++] = '\'';
   1514 			continue;
   1515 		}
   1516 
   1517 		switch (inbuf[r - 1]) {
   1518 		/* the only thing that has special meaning inside single
   1519 		 * quotes are single quotes themselves. */
   1520 		case '\'':
   1521 			buf[w++] = '\''; /* end single quote */
   1522 			buf[w++] = '\\'; /* put \' so it's treated as literal single quote */
   1523 			buf[w++] = '\'';
   1524 			buf[w++] = '\''; /* start single quoting again */
   1525 			break;
   1526 		default:
   1527 			buf[w++] = inbuf[r - 1];
   1528 			break;
   1529 		}
   1530 	}
   1531 
   1532 	buf[w] = '\0'; // NOLINT
   1533 	return w;
   1534 }
   1535 
   1536 static bool set_tilde_in_path(char *path)
   1537 {
   1538 	if (is_prefix(path, home, homelen)) {
   1539 		home[homelen] = path[homelen - 1];
   1540 		path[homelen - 1] = '~';
   1541 		return TRUE;
   1542 	}
   1543 
   1544 	return FALSE;
   1545 }
   1546 
   1547 static void reset_tilde_in_path(char *path)
   1548 {
   1549 	path[homelen - 1] = home[homelen];
   1550 	home[homelen] = '\0';
   1551 }
   1552 
   1553 #ifndef NOX11
   1554 static void xterm_cfg(char *path)
   1555 {
   1556 	if (cfg.x11 && !g_state.picker) {
   1557 		/* Signal CWD change to terminal */
   1558 		printf("\033]7;file://%s%s\033\\", hostname, path);
   1559 
   1560 		/* Set terminal window title */
   1561 		bool r = set_tilde_in_path(path);
   1562 
   1563 		printf("\033]2;%s\007", r ? &path[homelen - 1] : path);
   1564 		fflush(stdout);
   1565 
   1566 		if (r)
   1567 			reset_tilde_in_path(path);
   1568 	}
   1569 }
   1570 #endif
   1571 
   1572 static bool convert_tilde(const char *path, char *buf)
   1573 {
   1574 	if (tilde_is_home(path)) {
   1575 		ssize_t len = xstrlen(home);
   1576 		ssize_t loclen = xstrlen(path);
   1577 
   1578 		xstrsncpy(buf, home, len + 1);
   1579 		xstrsncpy(buf + len, path + 1, loclen);
   1580 		return true;
   1581 	}
   1582 	return false;
   1583 }
   1584 
   1585 static int create_tmp_file(void)
   1586 {
   1587 	xstrsncpy(g_tmpfpath + tmpfplen - 1, messages[STR_TMPFILE], TMP_LEN_MAX - tmpfplen);
   1588 
   1589 	int fd = mkstemp(g_tmpfpath);
   1590 
   1591 	if (fd == -1) {
   1592 		DPRINTF_S(strerror(errno));
   1593 	}
   1594 
   1595 	return fd;
   1596 }
   1597 
   1598 static void msg(const char *message)
   1599 {
   1600 	fprintf(stderr, "%s\n", message);
   1601 }
   1602 
   1603 #ifdef KEY_RESIZE
   1604 static void handle_key_resize(void)
   1605 {
   1606 	endwin();
   1607 	refresh();
   1608 }
   1609 
   1610 /* Clear the old prompt from the info line to the botton of the screen */
   1611 static void clearoldprompt(void)
   1612 {
   1613 	move(xlines - 2, 0);
   1614 	clrtobot();
   1615 	handle_key_resize();
   1616 }
   1617 #endif
   1618 
   1619 /* Messages show up at the bottom */
   1620 static inline void printmsg_nc(const char *msg)
   1621 {
   1622 	tolastln();
   1623 	addstr(msg);
   1624 	clrtoeol();
   1625 }
   1626 
   1627 static void printmsg(const char *msg)
   1628 {
   1629 	attron(COLOR_PAIR(cfg.curctx + 1));
   1630 	printmsg_nc(msg);
   1631 	attroff(COLOR_PAIR(cfg.curctx + 1));
   1632 }
   1633 
   1634 static void printwait(const char *msg, int *presel)
   1635 {
   1636 	printmsg(msg);
   1637 	if (presel) {
   1638 		*presel = MSGWAIT;
   1639 		if (ndents)
   1640 			xstrsncpy(g_ctx[cfg.curctx].c_name, pdents[cur].name, NAME_MAX + 1);
   1641 	}
   1642 }
   1643 
   1644 /* Kill curses and display error before exiting */
   1645 static void printerr(int linenum)
   1646 {
   1647 	exitcurses();
   1648 	perror(xitoa(linenum));
   1649 	if (!g_state.picker && selpath)
   1650 		unlink(selpath);
   1651 	free(pselbuf);
   1652 	exit(1);
   1653 }
   1654 
   1655 static inline bool xconfirm(int c)
   1656 {
   1657 	return (c == 'y' || c == 'Y');
   1658 }
   1659 
   1660 static int get_input(const char *prompt)
   1661 {
   1662 	wint_t ch[1];
   1663 
   1664 	if (prompt)
   1665 		printmsg(prompt);
   1666 	cleartimeout();
   1667 
   1668 	get_wch(ch);
   1669 
   1670 #ifdef KEY_RESIZE
   1671 	while (*ch == KEY_RESIZE) {
   1672 		if (prompt) {
   1673 			clearoldprompt();
   1674 			xlines = LINES;
   1675 			printmsg(prompt);
   1676 		}
   1677 
   1678 		get_wch(ch);
   1679 	}
   1680 #endif
   1681 	settimeout();
   1682 	return (int)*ch;
   1683 }
   1684 
   1685 static bool isselfileempty(void)
   1686 {
   1687 	struct stat sb;
   1688 
   1689 	return (stat(selpath, &sb) == -1) || (!sb.st_size);
   1690 }
   1691 
   1692 static int get_cur_or_sel(void)
   1693 {
   1694 	bool sel = (selbufpos || !isselfileempty());
   1695 
   1696 	/* Check both local buffer and selection file for external selection */
   1697 	if (sel && ndents) {
   1698 		/* If selection is preferred and we have a local selection, return selection.
   1699 		 * Always show the prompt in case of an external selection.
   1700 		 */
   1701 		if (cfg.prefersel && selbufpos)
   1702 			return 's';
   1703 
   1704 		int choice = get_input(messages[MSG_CUR_SEL_OPTS]);
   1705 
   1706 		return ((choice == 'c' || choice == 's') ? choice : 0);
   1707 	}
   1708 
   1709 	if (sel)
   1710 		return 's';
   1711 
   1712 	if (ndents)
   1713 		return 'c';
   1714 
   1715 	return 0;
   1716 }
   1717 
   1718 static void xdelay(useconds_t delay)
   1719 {
   1720 	refresh();
   1721 	usleep(delay);
   1722 }
   1723 
   1724 static uint_t entries_in_file(int fd, char *buf, size_t buflen, const char delim)
   1725 {
   1726 	ssize_t len;
   1727 	uint_t count = 0;
   1728 
   1729 	while ((len = read(fd, buf, buflen)) > 0)
   1730 		while (len)
   1731 			count += (buf[--len] == delim);
   1732 
   1733 	/* For all use cases 0 count is considered as error */
   1734 	return ((len < 0) ? 0 : ((delim != NUL_CHAR) ? count : ++count));
   1735 }
   1736 
   1737 static char confirm_force(bool selection, bool use_trash)
   1738 {
   1739 	char str[300];
   1740 
   1741 	/* Count selection from file when nothing is selected */
   1742 	if (selection && (nselected == 0)) {
   1743 		int fd = open(selpath, O_RDONLY);
   1744 
   1745 		if (fd != -1) {
   1746 			nselected = entries_in_file(fd, str, 300, NUL_CHAR);
   1747 			close(fd);
   1748 		} else {
   1749 			printwarn(NULL);
   1750 			return '\0';
   1751 		}
   1752 	}
   1753 
   1754 	/* Note: ideally we should use utils[UTIL_RM_RF] instead of the "rm -rf" string */
   1755 	int r = snprintf(str, 20, "%s", use_trash ? utils[UTIL_GIO_TRASH] + 4 : "rm -rf");
   1756 
   1757 	if (selection) {
   1758 		snprintf(str + r, 280, " %d file(s)?", nselected);
   1759 		r = get_input(str);
   1760 
   1761 		/* Double confirm on removal of more than 10 files */
   1762 		if (!use_trash && (nselected > RM_RECONFIRM) && (r == 'y' || r == 'Y')) {
   1763 			snprintf(str, 300, "Are you sure?");
   1764 			r = get_input(str);
   1765 		}
   1766 	} else {
   1767 		snprintf(str + r, 280, " '%s'?", pdents[cur].name);
   1768 		r = get_input(str);
   1769 	}
   1770 
   1771 	if (r == ESC)
   1772 		return '\0'; /* cancel */
   1773 	if (r == 'y' || r == 'Y')
   1774 		return 'f'; /* forceful for rm */
   1775 	if (r == 'n' || r == 'N')
   1776 		return '\0'; /* cancel */
   1777 	return (use_trash ? '\0' : 'i'); /* interactive for rm */
   1778 }
   1779 
   1780 /* Writes buflen char(s) from buf to a file */
   1781 static void writesel(const char *buf, const size_t buflen)
   1782 {
   1783 	if (!selpath)
   1784 		return;
   1785 
   1786 	int fd = open(selpath, O_CREAT | O_WRONLY | O_TRUNC, S_IWUSR | S_IRUSR);
   1787 
   1788 	if (fd != -1) {
   1789 		if (write(fd, buf, buflen) != (ssize_t)buflen)
   1790 			printwarn(NULL);
   1791 		close(fd);
   1792 	} else
   1793 		printwarn(NULL);
   1794 }
   1795 
   1796 static void appendfpath(const char *path, const size_t len)
   1797 {
   1798 	if ((selbufpos >= selbuflen) || ((len + 3) > (selbuflen - selbufpos))) {
   1799 		selbuflen += PATH_MAX;
   1800 		pselbuf = xrealloc(pselbuf, selbuflen);
   1801 		if (!pselbuf)
   1802 			errexit();
   1803 	}
   1804 
   1805 	selbufpos += xstrsncpy(pselbuf + selbufpos, path, len);
   1806 }
   1807 
   1808 static void selbufrealloc(const size_t alloclen)
   1809 {
   1810 	if ((selbufpos + alloclen) > selbuflen) {
   1811 		selbuflen = ALIGN_UP(selbufpos + alloclen, PATH_MAX);
   1812 		pselbuf = xrealloc(pselbuf, selbuflen);
   1813 		if (!pselbuf)
   1814 			errexit();
   1815 	}
   1816 }
   1817 
   1818 /* Write selected file paths to fd, linefeed separated */
   1819 static size_t seltofile(int fd, uint_t *pcount, const char *separator)
   1820 {
   1821 	uint_t lastpos, count = 0;
   1822 	char *pbuf = pselbuf;
   1823 	size_t pos = 0;
   1824 	ssize_t len, prefixlen = 0, initlen = 0;
   1825 
   1826 	if (pcount)
   1827 		*pcount = 0;
   1828 
   1829 	if (!selbufpos)
   1830 		return 0;
   1831 
   1832 	lastpos = selbufpos - 1;
   1833 
   1834 	if (listpath) {
   1835 		prefixlen = (ssize_t)xstrlen(listroot);
   1836 		initlen = (ssize_t)xstrlen(listpath);
   1837 	}
   1838 
   1839 	while (pos <= lastpos) {
   1840 		DPRINTF_S(pbuf);
   1841 		len = (ssize_t)xstrlen(pbuf);
   1842 
   1843 		if (!listpath || !is_prefix(pbuf, listpath, initlen)) {
   1844 			if (write(fd, pbuf, len) != len)
   1845 				return pos;
   1846 		} else {
   1847 			if (write(fd, listroot, prefixlen) != prefixlen)
   1848 				return pos;
   1849 			if (write(fd, pbuf + initlen, len - initlen) != (len - initlen))
   1850 				return pos;
   1851 		}
   1852 
   1853 		pos += len;
   1854 		if (pos <= lastpos) {
   1855 			if (write(fd, separator, 1) != 1)
   1856 				return pos;
   1857 			pbuf += len + 1;
   1858 		}
   1859 		++pos;
   1860 		++count;
   1861 	}
   1862 
   1863 	if (pcount)
   1864 		*pcount = count;
   1865 
   1866 	return pos;
   1867 }
   1868 
   1869 /* List selection from selection file (another instance) */
   1870 static bool listselfile(void)
   1871 {
   1872 	if (isselfileempty())
   1873 		return FALSE;
   1874 
   1875 	snprintf(g_buf, CMD_LEN_MAX, "tr \'\\0\' \'\\n\' < '%s'", selpath);
   1876 	spawn(utils[UTIL_SH_EXEC], g_buf, NULL, NULL, F_CLI | F_CONFIRM);
   1877 
   1878 	return TRUE;
   1879 }
   1880 
   1881 /* Reset selection indicators */
   1882 static void resetselind(void)
   1883 {
   1884 	for (int r = 0; r < ndents; ++r)
   1885 		if (pdents[r].flags & FILE_SELECTED)
   1886 			pdents[r].flags &= ~FILE_SELECTED;
   1887 }
   1888 
   1889 static void startselection(void)
   1890 {
   1891 	if (!g_state.selmode) {
   1892 		g_state.selmode = 1;
   1893 		nselected = 0;
   1894 
   1895 		if (selbufpos) {
   1896 			resetselind();
   1897 			writesel(NULL, 0);
   1898 			selbufpos = 0;
   1899 		}
   1900 	}
   1901 }
   1902 
   1903 static void clearselection(void)
   1904 {
   1905 	nselected = 0;
   1906 	selbufpos = 0;
   1907 	g_state.selmode = 0;
   1908 	writesel(NULL, 0);
   1909 }
   1910 
   1911 static char *findinsel(char *startpos, int len)
   1912 {
   1913 	if (!selbufpos)
   1914 		return FALSE;
   1915 
   1916 	if (!startpos)
   1917 		startpos = pselbuf;
   1918 
   1919 	char *found = startpos;
   1920 	size_t buflen = selbufpos - (startpos - pselbuf);
   1921 
   1922 	while (1) {
   1923 		/* memmem(3): not specified in POSIX.1, but present on a number of other systems. */
   1924 		found = memmem(found, buflen - (found - startpos), g_sel, len);
   1925 		if (!found)
   1926 			return NULL;
   1927 		if (found == startpos || *(found - 1) == '\0')
   1928 			return found;
   1929 		found += len; /* We found g_sel as a substring of a path, move forward */
   1930 		if (found >= startpos + buflen)
   1931 			return NULL;
   1932 	}
   1933 }
   1934 
   1935 static int markcmp(const void *va, const void *vb)
   1936 {
   1937 	const selmark *ma = (selmark*)va;
   1938 	const selmark *mb = (selmark*)vb;
   1939 
   1940 	return ma->startpos - mb->startpos;
   1941 }
   1942 
   1943 /* scanselforpath() must be called before calling this */
   1944 static inline void findmarkentry(size_t len, struct entry *dentp)
   1945 {
   1946 	if (!(dentp->flags & FILE_SCANNED)) {
   1947 		if (findinsel(findselpos, len + xstrsncpy(g_sel + len, dentp->name, dentp->nlen)))
   1948 			dentp->flags |= FILE_SELECTED;
   1949 		dentp->flags |= FILE_SCANNED;
   1950 	}
   1951 }
   1952 
   1953 /*
   1954  * scanselforpath() must be called before calling this
   1955  * pathlen = length of path + 1 (+1 for trailing slash)
   1956  */
   1957 static void invertselbuf(const int pathlen)
   1958 {
   1959 	size_t len, endpos, shrinklen = 0, alloclen = 0;
   1960 	char * const pbuf = g_sel + pathlen;
   1961 	char *found;
   1962 	int i, nmarked = 0, prev = 0;
   1963 	struct entry *dentp;
   1964 	selmark *marked = malloc(nselected * sizeof(selmark));
   1965 
   1966 	if (!marked) {
   1967 		printwarn(NULL);
   1968 		return;
   1969 	}
   1970 
   1971 	/* First pass: inversion */
   1972 	for (i = 0; i < ndents; ++i) {
   1973 		dentp = &pdents[i];
   1974 
   1975 		/* Ensure off-screen entries are reconciled with selbuf before inversion. */
   1976 		findmarkentry(pathlen, dentp);
   1977 
   1978 		if (dentp->flags & FILE_SELECTED) {
   1979 			dentp->flags ^= FILE_SELECTED; /* Clear selection status */
   1980 			len = pathlen + xstrsncpy(pbuf, dentp->name, NAME_MAX);
   1981 			found = findinsel(findselpos, len);
   1982 			if (found) {
   1983 				if (findselpos == found)
   1984 					findselpos += len;
   1985 
   1986 				if (nmarked && (found
   1987 				    == (marked[nmarked - 1].startpos + marked[nmarked - 1].len)))
   1988 					marked[nmarked - 1].len += len;
   1989 				else {
   1990 					marked[nmarked].startpos = found;
   1991 					marked[nmarked].len = len;
   1992 					++nmarked;
   1993 				}
   1994 
   1995 				--nselected;
   1996 				shrinklen += len; /* buffer size adjustment */
   1997 			} else {
   1998 				dentp->flags |= FILE_SELECTED;
   1999 				alloclen += pathlen + dentp->nlen;
   2000 			}
   2001 		} else {
   2002 			dentp->flags |= FILE_SELECTED;
   2003 			alloclen += pathlen + dentp->nlen;
   2004 		}
   2005 	}
   2006 
   2007 	/*
   2008 	 * Files marked for deselection could be found in arbitrary order.
   2009 	 * Sort by appearance in selection buffer.
   2010 	 * With entries sorted we can merge adjacent ones allowing us to
   2011 	 * move them in a single go.
   2012 	 */
   2013 	qsort(marked, nmarked, sizeof(selmark), &markcmp);
   2014 
   2015 	/* Some files might be adjacent. Merge them into a single entry */
   2016 	for (i = 1; i < nmarked; ++i) {
   2017 		if (marked[i].startpos == marked[prev].startpos + marked[prev].len)
   2018 			marked[prev].len += marked[i].len;
   2019 		else
   2020 			marked[++prev] = marked[i];
   2021 	}
   2022 
   2023 	/*
   2024 	 * Number of entries is increased by encountering a non-adjacent entry
   2025 	 * After we finish the loop we should increment it once more.
   2026 	 */
   2027 
   2028 	if (nmarked) /* Make sure there is something to deselect */
   2029 		nmarked = prev + 1;
   2030 
   2031 	/* Using merged entries remove unselected chunks from selection buffer */
   2032 	for (i = 0; i < nmarked; ++i) {
   2033 		/*
   2034 		 * found: points to where the current block starts
   2035 		 *        variable is recycled from previous for readability
   2036 		 * endpos: points to where the the next block starts
   2037 		 *         area between the end of current block (found + len)
   2038 		 *         and endpos is selected entries. This is what we are
   2039 		 *         moving back.
   2040 		 */
   2041 		found = marked[i].startpos;
   2042 		endpos = (i + 1 == nmarked ? selbufpos : marked[i + 1].startpos - pselbuf);
   2043 		len = marked[i].len;
   2044 
   2045 		/* Move back only selected entries. No selected memory is moved twice */
   2046 		memmove(found, found + len, endpos - (found + len - pselbuf));
   2047 	}
   2048 
   2049 	free(marked);
   2050 
   2051 	/* Buffer size adjustment */
   2052 	selbufpos -= shrinklen;
   2053 
   2054 	selbufrealloc(alloclen);
   2055 
   2056 	/* Second pass: append newly selected to buffer */
   2057 	for (i = 0; i < ndents; ++i) {
   2058 		if (pdents[i].flags & FILE_SELECTED) {
   2059 			len = pathlen + xstrsncpy(pbuf, pdents[i].name, NAME_MAX);
   2060 			appendfpath(g_sel, len);
   2061 			++nselected;
   2062 		}
   2063 	}
   2064 
   2065 	nselected ? writesel(pselbuf, selbufpos - 1) : clearselection();
   2066 }
   2067 
   2068 /*
   2069  * scanselforpath() must be called before calling this
   2070  * pathlen = length of path + 1 (+1 for trailing slash)
   2071  */
   2072 static void addtoselbuf(const int pathlen, int startid, int endid)
   2073 {
   2074 	int i;
   2075 	size_t len, alloclen = 0;
   2076 	struct entry *dentp;
   2077 	char *found;
   2078 	char * const pbuf = g_sel + pathlen;
   2079 
   2080 	/* Remember current selection buffer position */
   2081 	for (i = startid; i <= endid; ++i) {
   2082 		dentp = &pdents[i];
   2083 
   2084 		if (findselpos) {
   2085 			len = pathlen + xstrsncpy(pbuf, dentp->name, NAME_MAX);
   2086 			found = findinsel(findselpos, len);
   2087 			if (found) {
   2088 				dentp->flags |= (FILE_SCANNED | FILE_SELECTED);
   2089 				if (found == findselpos) {
   2090 					findselpos += len;
   2091 					if (findselpos == (pselbuf + selbufpos))
   2092 						findselpos = NULL;
   2093 				}
   2094 			} else
   2095 				alloclen += pathlen + dentp->nlen;
   2096 		} else
   2097 			alloclen += pathlen + dentp->nlen;
   2098 	}
   2099 
   2100 	selbufrealloc(alloclen);
   2101 
   2102 	for (i = startid; i <= endid; ++i) {
   2103 		if (!(pdents[i].flags & FILE_SELECTED)) {
   2104 			len = pathlen + xstrsncpy(pbuf, pdents[i].name, NAME_MAX);
   2105 			appendfpath(g_sel, len);
   2106 			++nselected;
   2107 			pdents[i].flags |= (FILE_SCANNED | FILE_SELECTED);
   2108 		}
   2109 	}
   2110 
   2111 	writesel(pselbuf, selbufpos - 1); /* Truncate NULL from end */
   2112 }
   2113 
   2114 /* Removes g_sel from selbuf */
   2115 static void rmfromselbuf(size_t len)
   2116 {
   2117 	char *found = findinsel(findselpos, len);
   2118 	if (!found)
   2119 		return;
   2120 
   2121 	memmove(found, found + len, selbufpos - (found + len - pselbuf));
   2122 	selbufpos -= len;
   2123 
   2124 	nselected ? writesel(pselbuf, selbufpos - 1) : clearselection();
   2125 }
   2126 
   2127 static int scanselforpath(const char *path, bool getsize)
   2128 {
   2129 	if (!path[1]) { /* path should always be at least two bytes (including NULL) */
   2130 		g_sel[0] = '/';
   2131 		findselpos = pselbuf;
   2132 		return 1; /* Length of '/' is 1 */
   2133 	}
   2134 
   2135 	size_t off = xstrsncpy(g_sel, path, PATH_MAX);
   2136 
   2137 	g_sel[off - 1] = '/';
   2138 	/*
   2139 	 * We set findselpos only here. Directories can be listed in arbitrary order.
   2140 	 * This is the best best we can do for remembering position.
   2141 	 */
   2142 	findselpos = findinsel(NULL, off);
   2143 
   2144 	if (getsize)
   2145 		return off;
   2146 	return (findselpos ? off : 0);
   2147 }
   2148 
   2149 /* Finish selection procedure before an operation */
   2150 static void rmtmpfile(void)
   2151 {
   2152 	if (unlink(g_tmpfpath)) {
   2153 		DPRINTF_S(strerror(errno));
   2154 		printwarn(NULL);
   2155 	}
   2156 }
   2157 
   2158 static void endselection(bool endselmode)
   2159 {
   2160 	int fd;
   2161 	ssize_t count;
   2162 	char buf[sizeof(patterns[P_REPLACE]) + PATH_MAX + (TMP_LEN_MAX << 1)];
   2163 
   2164 	if (endselmode && g_state.selmode)
   2165 		g_state.selmode = 0;
   2166 
   2167 	/* The code below is only for listing mode */
   2168 	if (!listpath || !selbufpos)
   2169 		return;
   2170 
   2171 	fd = create_tmp_file();
   2172 	if (fd == -1) {
   2173 		DPRINTF_S("couldn't create tmp file");
   2174 		return;
   2175 	}
   2176 
   2177 	seltofile(fd, NULL, NEWLINE);
   2178 	if (close(fd)) {
   2179 		DPRINTF_S(strerror(errno));
   2180 		printwarn(NULL);
   2181 		return;
   2182 	}
   2183 
   2184 	snprintf(buf, sizeof(buf), patterns[P_REPLACE], listpath, listroot, g_tmpfpath);
   2185 	spawn(utils[UTIL_SH_EXEC], buf, NULL, NULL, F_CLI);
   2186 
   2187 	fd = open(g_tmpfpath, O_RDONLY);
   2188 	if (fd == -1) {
   2189 		DPRINTF_S(strerror(errno));
   2190 		printwarn(NULL);
   2191 		rmtmpfile();
   2192 		return;
   2193 	}
   2194 
   2195 	count = read(fd, pselbuf, selbuflen);
   2196 	if (count < 0) {
   2197 		DPRINTF_S(strerror(errno));
   2198 		printwarn(NULL);
   2199 		if (close(fd) || unlink(g_tmpfpath)) {
   2200 			DPRINTF_S(strerror(errno));
   2201 		}
   2202 		return;
   2203 	}
   2204 
   2205 	if (close(fd) || unlink(g_tmpfpath)) {
   2206 		DPRINTF_S(strerror(errno));
   2207 		printwarn(NULL);
   2208 		return;
   2209 	}
   2210 
   2211 	selbufpos = count;
   2212 	pselbuf[--count] = '\0';
   2213 	for (--count; count > 0; --count)
   2214 		if (pselbuf[count] == '\n' && pselbuf[count+1] == '/')
   2215 			pselbuf[count] = '\0';
   2216 
   2217 	writesel(pselbuf, selbufpos - 1);
   2218 }
   2219 
   2220 /* Returns: 1 - success, 0 - none selected, -1 - other failure */
   2221 static int editselection(bool allowemptysel)
   2222 {
   2223 	int ret = -1;
   2224 	int fd, lines = 0;
   2225 	ssize_t count;
   2226 	struct stat sb;
   2227 	time_t mtime;
   2228 
   2229 	if (!allowemptysel && !selbufpos) /* External selection is only editable at source */
   2230 		return listselfile();
   2231 
   2232 	fd = create_tmp_file();
   2233 	if (fd == -1) {
   2234 		DPRINTF_S("couldn't create tmp file");
   2235 		return -1;
   2236 	}
   2237 
   2238 	if (!allowemptysel)
   2239 		seltofile(fd, NULL, NEWLINE);
   2240 	if (close(fd)) {
   2241 		DPRINTF_S(strerror(errno));
   2242 		return -1;
   2243 	}
   2244 
   2245 	/* Save the last modification time */
   2246 	if (stat(g_tmpfpath, &sb)) {
   2247 		DPRINTF_S(strerror(errno));
   2248 		rmtmpfile();
   2249 		return -1;
   2250 	}
   2251 	mtime = sb.st_mtime;
   2252 
   2253 	spawn((cfg.waitedit ? enveditor : editor), g_tmpfpath, NULL, NULL, F_CLI);
   2254 
   2255 	fd = open(g_tmpfpath, O_RDONLY);
   2256 	if (fd == -1) {
   2257 		DPRINTF_S(strerror(errno));
   2258 		rmtmpfile();
   2259 		return -1;
   2260 	}
   2261 
   2262 	fstat(fd, &sb);
   2263 
   2264 	if (mtime == sb.st_mtime) {
   2265 		DPRINTF_S("selection is not modified");
   2266 		if (unlink(g_tmpfpath)) {
   2267 			DPRINTF_S(strerror(errno));
   2268 			printwarn(NULL);
   2269 			goto emptyedit;
   2270 		}
   2271 		return 1;
   2272 	}
   2273 
   2274 	if (allowemptysel)
   2275 		selbufrealloc(sb.st_size);
   2276 	else if (sb.st_size > selbufpos) {
   2277 		DPRINTF_S("edited buffer larger than previous");
   2278 		rmtmpfile();
   2279 		goto emptyedit;
   2280 	}
   2281 
   2282 	count = read(fd, pselbuf, selbuflen);
   2283 	if (count < 0) {
   2284 		DPRINTF_S(strerror(errno));
   2285 		printwarn(NULL);
   2286 		close(fd);
   2287 		rmtmpfile();
   2288 		goto emptyedit;
   2289 	}
   2290 
   2291 	if (close(fd) || unlink(g_tmpfpath)) {
   2292 		DPRINTF_S(strerror(errno));
   2293 		printwarn(NULL);
   2294 		goto emptyedit;
   2295 	}
   2296 
   2297 	if (!count) {
   2298 		ret = 1;
   2299 		goto emptyedit;
   2300 	}
   2301 
   2302 	resetselind();
   2303 	selbufpos = count;
   2304 	/* The last character should be '\n' */
   2305 	pselbuf[--count] = '\0';
   2306 	for (--count; count > 0; --count) {
   2307 		/* Replace every '\n' that separates two paths */
   2308 		if (pselbuf[count] == '\n' && pselbuf[count + 1] == '/') {
   2309 			++lines;
   2310 			pselbuf[count] = '\0';
   2311 		}
   2312 	}
   2313 
   2314 	/* Add a line for the last file */
   2315 	++lines;
   2316 
   2317 	if (!allowemptysel && (lines > nselected)) {
   2318 		DPRINTF_S("files added to selection");
   2319 		goto emptyedit;
   2320 	}
   2321 
   2322 	nselected = lines;
   2323 	writesel(pselbuf, selbufpos - 1);
   2324 
   2325 	return 1;
   2326 
   2327 emptyedit:
   2328 	resetselind();
   2329 	clearselection();
   2330 	return ret;
   2331 }
   2332 
   2333 static bool selsafe(void)
   2334 {
   2335 	/* Fail if selection file path not generated */
   2336 	if (!selpath) {
   2337 		printmsg(messages[MSG_SEL_MISSING]);
   2338 		return FALSE;
   2339 	}
   2340 
   2341 	/* Fail if selection file path isn't accessible */
   2342 	if (access(selpath, R_OK | W_OK) == -1) {
   2343 		errno == ENOENT ? printmsg(messages[MSG_0_SELECTED]) : printwarn(NULL);
   2344 		return FALSE;
   2345 	}
   2346 
   2347 	return TRUE;
   2348 }
   2349 
   2350 static void export_file_list(void)
   2351 {
   2352 	if (!ndents)
   2353 		return;
   2354 
   2355 	struct entry *pdent = pdents;
   2356 	int fd = create_tmp_file();
   2357 
   2358 	if (fd == -1) {
   2359 		DPRINTF_S(strerror(errno));
   2360 		return;
   2361 	}
   2362 
   2363 	for (int r = 0; r < ndents; ++pdent, ++r) {
   2364 		if (write(fd, pdent->name, pdent->nlen - 1) != (pdent->nlen - 1))
   2365 			break;
   2366 
   2367 		if ((r != ndents - 1) && (write(fd, "\n", 1) != 1))
   2368 			break;
   2369 	}
   2370 
   2371 	if (close(fd)) {
   2372 		DPRINTF_S(strerror(errno));
   2373 	}
   2374 
   2375 	spawn(editor, g_tmpfpath, NULL, NULL, F_CLI);
   2376 
   2377 	if (xconfirm(get_input(messages[MSG_RM_TMP]))) {
   2378 		if (unlink(g_tmpfpath)) {
   2379 			DPRINTF_S(strerror(errno));
   2380 		}
   2381 	}
   2382 }
   2383 
   2384 static bool init_fcolors(void)
   2385 {
   2386 	char *f_colors = getenv(env_cfg[NNN_FCOLORS]);
   2387 
   2388 	if (!f_colors || !*f_colors)
   2389 		f_colors = gcolors;
   2390 
   2391 	for (uchar_t id = C_BLK; *f_colors && id <= C_UND; ++id) {
   2392 		fcolors[id] = xchartohex(*f_colors) << 4;
   2393 		if (*++f_colors) {
   2394 			fcolors[id] += xchartohex(*f_colors);
   2395 			if (fcolors[id])
   2396 				init_pair(id, fcolors[id], -1);
   2397 		} else
   2398 			return FALSE;
   2399 		++f_colors;
   2400 	}
   2401 
   2402 	return TRUE;
   2403 }
   2404 
   2405 /* Initialize curses mode */
   2406 static bool initcurses(void *oldmask)
   2407 {
   2408 #ifdef NOMOUSE
   2409 	(void) oldmask;
   2410 #endif
   2411 
   2412 	if (g_state.picker) {
   2413 		if (!newterm(NULL, stderr, stdin)) {
   2414 			msg("newterm!");
   2415 			return FALSE;
   2416 		}
   2417 	} else if (!initscr()) {
   2418 		msg("initscr!");
   2419 		DPRINTF_S(getenv("TERM"));
   2420 		return FALSE;
   2421 	}
   2422 
   2423 	cbreak();
   2424 	noecho();
   2425 	nonl();
   2426 	//intrflush(stdscr, FALSE);
   2427 	keypad(stdscr, TRUE);
   2428 #ifndef NOMOUSE
   2429 #if NCURSES_MOUSE_VERSION <= 1
   2430 	mousemask(BUTTON1_PRESSED | BUTTON1_DOUBLE_CLICKED | BUTTON2_PRESSED | BUTTON3_PRESSED,
   2431 		  (mmask_t *)oldmask);
   2432 #else
   2433 	mousemask(BUTTON1_PRESSED | BUTTON2_PRESSED | BUTTON3_PRESSED | BUTTON4_PRESSED
   2434 		  | BUTTON5_PRESSED, (mmask_t *)oldmask);
   2435 #endif
   2436 	mouseinterval(0);
   2437 #endif
   2438 	curs_set(FALSE); /* Hide cursor */
   2439 	leaveok(stdscr, TRUE); /* Skip precise cursor placement during normal redraws */
   2440 
   2441 	char *colors = getenv(env_cfg[NNN_COLORS]);
   2442 
   2443 	if (colors || !getenv("NO_COLOR")) {
   2444 		uint_t *pcode;
   2445 		bool ext = FALSE;
   2446 
   2447 		start_color();
   2448 		use_default_colors();
   2449 
   2450 		/* Initialize file colors */
   2451 		if (COLORS >= COLOR_256) {
   2452 			if (!(g_state.oldcolor || init_fcolors())) {
   2453 				exitcurses();
   2454 				msg(env_cfg[NNN_FCOLORS]);
   2455 				return FALSE;
   2456 			}
   2457 		} else
   2458 			g_state.oldcolor = 1;
   2459 
   2460 		DPRINTF_D(COLORS);
   2461 		DPRINTF_D(COLOR_PAIRS);
   2462 
   2463 		if (colors && *colors == '#') {
   2464 			char *sep = strchr(colors, ';');
   2465 
   2466 			if (!g_state.oldcolor && COLORS >= COLOR_256) {
   2467 				++colors;
   2468 				ext = TRUE;
   2469 
   2470 				/*
   2471 				 * If fallback colors are specified, set the separator
   2472 				 * to NULL so we don't interpret separator and fallback
   2473 				 * if fewer than CTX_MAX xterm 256 colors are specified.
   2474 				 */
   2475 				if (sep)
   2476 					*sep = '\0';
   2477 			} else {
   2478 				colors = sep; /* Detect if 8 colors fallback is appended */
   2479 				if (colors)
   2480 					++colors;
   2481 			}
   2482 		}
   2483 
   2484 		/* Get and set the context colors */
   2485 		for (uchar_t i = 0; i < CTX_MAX; ++i) {
   2486 			pcode = &g_ctx[i].color;
   2487 
   2488 			if (colors && *colors) {
   2489 				if (ext) {
   2490 					*pcode = xchartohex(*colors) << 4;
   2491 					if (*++colors)
   2492 						fcolors[i + 1] = *pcode += xchartohex(*colors);
   2493 					else { /* Each color code must be 2 hex symbols */
   2494 						exitcurses();
   2495 						msg(env_cfg[NNN_COLORS]);
   2496 						return FALSE;
   2497 					}
   2498 				} else {
   2499 					*pcode = (*colors < '0' || *colors > '7') ? 4 : *colors - '0';
   2500 					fcolors[i + 1] = *pcode;
   2501 				}
   2502 				++colors;
   2503 			} else
   2504 				*pcode = 4;
   2505 
   2506 			init_pair(i + 1, *pcode, -1);
   2507 		}
   2508 	}
   2509 #ifdef ICONS_ENABLED
   2510 	if (!g_state.oldcolor) {
   2511 		for (uint_t i = 0; i < ELEMENTS(init_colors); ++i)
   2512 			init_pair(C_UND + 1 + init_colors[i], init_colors[i], -1);
   2513 	}
   2514 #endif
   2515 
   2516 	settimeout(); /* One second */
   2517 	set_escdelay(25);
   2518 	return TRUE;
   2519 }
   2520 
   2521 /* No NULL check here as spawn() guards against it */
   2522 static char *parseargs(char *cmd, char **argv, int *pindex)
   2523 {
   2524 	int count = 0;
   2525 	size_t len = xstrlen(cmd) + 1;
   2526 	char *line = malloc(len);
   2527 
   2528 	if (!line) {
   2529 		DPRINTF_S("malloc()!");
   2530 		return NULL;
   2531 	}
   2532 
   2533 	xstrsncpy(line, cmd, len);
   2534 	argv[count++] = line;
   2535 	cmd = line;
   2536 
   2537 	while (*line) { // NOLINT
   2538 		if (ISBLANK(*line)) {
   2539 			*line++ = '\0';
   2540 
   2541 			if (!*line) // NOLINT
   2542 				break;
   2543 
   2544 			argv[count++] = line;
   2545 			if (count == EXEC_ARGS_MAX) {
   2546 				count = -1;
   2547 				break;
   2548 			}
   2549 		}
   2550 
   2551 		++line;
   2552 	}
   2553 
   2554 	if (count == -1 || count > (EXEC_ARGS_MAX - 4)) { /* 3 args and last NULL */
   2555 		free(cmd);
   2556 		cmd = NULL;
   2557 		DPRINTF_S("NULL or too many args");
   2558 	}
   2559 
   2560 	*pindex = count;
   2561 	return cmd;
   2562 }
   2563 
   2564 static void enable_signals(void)
   2565 {
   2566 	struct sigaction dfl_act = {.sa_handler = SIG_DFL};
   2567 
   2568 	sigaction(SIGHUP, &dfl_act, NULL);
   2569 	sigaction(SIGINT, &dfl_act, NULL);
   2570 	sigaction(SIGQUIT, &dfl_act, NULL);
   2571 	sigaction(SIGTSTP, &dfl_act, NULL);
   2572 	sigaction(SIGWINCH, &dfl_act, NULL);
   2573 }
   2574 
   2575 static pid_t xfork(uchar_t flag)
   2576 {
   2577 	pid_t p = fork();
   2578 
   2579 	if (p > 0) {
   2580 		/* the parent ignores the interrupt, quit and hangup signals */
   2581 		sigaction(SIGHUP, &(struct sigaction){.sa_handler = SIG_IGN}, &oldsighup);
   2582 		sigaction(SIGTSTP, &(struct sigaction){.sa_handler = SIG_DFL}, &oldsigtstp);
   2583 		sigaction(SIGWINCH, &(struct sigaction){.sa_handler = SIG_IGN}, &oldsigwinch);
   2584 	} else if (p == 0) {
   2585 		/* We create a grandchild to detach */
   2586 		if (flag & F_NOWAIT) {
   2587 			p = fork();
   2588 
   2589 			if (p > 0)
   2590 				_exit(EXIT_SUCCESS);
   2591 			else if (p == 0) {
   2592 				enable_signals();
   2593 				setsid();
   2594 				return p;
   2595 			}
   2596 
   2597 			perror("fork");
   2598 			_exit(EXIT_FAILURE);
   2599 		}
   2600 
   2601 		/* So they can be used to stop the child */
   2602 		enable_signals();
   2603 	}
   2604 
   2605 	/* This is the parent waiting for the child to create grandchild */
   2606 	if (flag & F_NOWAIT)
   2607 		waitpid(p, NULL, 0);
   2608 
   2609 	if (p == -1)
   2610 		perror("fork");
   2611 	return p;
   2612 }
   2613 
   2614 static int join(pid_t p, uchar_t flag)
   2615 {
   2616 	int status = 0xFFFF;
   2617 
   2618 	if (!(flag & F_NOWAIT)) {
   2619 		/* wait for the child to exit */
   2620 		do {
   2621 		} while (waitpid(p, &status, 0) == -1);
   2622 
   2623 		if (WIFEXITED(status)) {
   2624 			status = WEXITSTATUS(status);
   2625 			DPRINTF_D(status);
   2626 		}
   2627 	}
   2628 
   2629 	/* restore parent's signal handling */
   2630 	sigaction(SIGHUP, &oldsighup, NULL);
   2631 	sigaction(SIGTSTP, &oldsigtstp, NULL);
   2632 	sigaction(SIGWINCH, &oldsigwinch, NULL);
   2633 
   2634 	return status;
   2635 }
   2636 
   2637 /*
   2638  * Spawns a child process. Behaviour can be controlled using flag.
   2639  * Limited to 3 arguments to a program, flag works on bit set.
   2640  */
   2641 static int spawn(char *command, char *arg1, char *arg2, char *arg3, ushort_t flag)
   2642 {
   2643 	pid_t pid;
   2644 	int index = 0, retstatus = 0xFFFF;
   2645 	char *argv[EXEC_ARGS_MAX] = {0};
   2646 	char *cmd = NULL;
   2647 
   2648 	if (!command || !*command)
   2649 		return retstatus;
   2650 
   2651 	if (flag & F_MULTI) {
   2652 		cmd = parseargs(command, argv, &index);
   2653 		if (!cmd)
   2654 			return -1;
   2655 	} else
   2656 		argv[index++] = command;
   2657 
   2658 	if (arg1)
   2659 		argv[index++] = arg1;
   2660 
   2661 	if (arg2)
   2662 		argv[index++] = arg2;
   2663 
   2664 	if (arg3)
   2665 		argv[index] = arg3;
   2666 
   2667 	if (flag & F_NORMAL)
   2668 		exitcurses();
   2669 
   2670 	pid = xfork(flag);
   2671 	if (pid == 0) {
   2672 		/* Suppress stdout and stderr */
   2673 		if (flag & F_NOTRACE) {
   2674 			if (flag & F_NOSTDIN)
   2675 				dup2(devnullfd, STDIN_FILENO); // NOLINT
   2676 			dup2(devnullfd, STDOUT_FILENO); // NOLINT
   2677 			dup2(devnullfd, STDERR_FILENO); // NOLINT
   2678 		} else if (flag & F_TTY) {
   2679 			/* If stdout has been redirected to a non-tty, force output to tty */
   2680 			if (!isatty(STDOUT_FILENO)) {
   2681 				int fd = open(ctermid(NULL), O_WRONLY, 0200);
   2682 				dup2(fd, STDOUT_FILENO);
   2683 				close(fd);
   2684 			}
   2685 		}
   2686 
   2687 		execvp(*argv, argv);
   2688 		_exit(EXIT_SUCCESS);
   2689 	} else {
   2690 		retstatus = join(pid, flag);
   2691 		DPRINTF_D(pid);
   2692 
   2693 		if ((flag & F_CONFIRM) || ((flag & F_CHKRTN) && retstatus)) {
   2694 			int status = write(STDOUT_FILENO, messages[MSG_ENTER], xstrlen(messages[MSG_ENTER]));
   2695 			(void)status;
   2696 			while ((read(STDIN_FILENO, &status, 1) > 0) && (status != '\n'));
   2697 		}
   2698 
   2699 		if (flag & F_NORMAL)
   2700 			refresh();
   2701 
   2702 		free(cmd);
   2703 	}
   2704 
   2705 	return retstatus;
   2706 }
   2707 
   2708 /* Get program name from env var, else return fallback program */
   2709 static char *xgetenv(const char * const name, char *fallback)
   2710 {
   2711 	char *value = getenv(name);
   2712 
   2713 	return value && value[0] ? value : fallback;
   2714 }
   2715 
   2716 /* Check if a dir exists, IS a dir, and is readable */
   2717 static bool xdiraccess(const char *path)
   2718 {
   2719 	int fd = open(path, O_RDONLY | O_DIRECTORY);
   2720 
   2721 	if (fd < 0)
   2722 		return FALSE;
   2723 
   2724 	return close(fd) == 0;
   2725 }
   2726 
   2727 static bool plugscript(const char *plugin, uchar_t flags)
   2728 {
   2729 	mkpath(plgpath, plugin, g_buf);
   2730 	if (!access(g_buf, X_OK)) {
   2731 		spawn(g_buf, NULL, NULL, NULL, flags);
   2732 		return TRUE;
   2733 	}
   2734 
   2735 	return FALSE;
   2736 }
   2737 
   2738 static void opstr(char *buf, char *op)
   2739 {
   2740 	snprintf(buf, CMD_LEN_MAX, "xargs -0 sh -c '%s \"$0\" \"$@\" . < /dev/tty' < '%s'", op, selpath);
   2741 }
   2742 
   2743 static bool rmmulstr(char *buf, bool use_trash)
   2744 {
   2745 	char r = confirm_force(TRUE, use_trash);
   2746 	if (!r)
   2747 		return FALSE;
   2748 
   2749 	if (!use_trash)
   2750 		snprintf(buf, CMD_LEN_MAX, "xargs -0 sh -c 'rm -%cvr -- \"$0\" \"$@\" < /dev/tty' < '%s'",
   2751 			 r, selpath);
   2752 	else
   2753 		snprintf(buf, CMD_LEN_MAX, "xargs -0 %s < '%s'",
   2754 			 trashcmd, selpath);
   2755 
   2756 	return TRUE;
   2757 }
   2758 
   2759 /* Returns TRUE if file is removed, else FALSE */
   2760 static bool xrm(char * const fpath, bool use_trash)
   2761 {
   2762 	char r = confirm_force(FALSE, use_trash);
   2763 	if (!r)
   2764 		return FALSE;
   2765 
   2766 	if (!use_trash) {
   2767 		char rm_opts[5] = "-vr\0";
   2768 
   2769 		rm_opts[3] = r;
   2770 		spawn("rm", rm_opts, "--", fpath, F_NORMAL | F_CHKRTN);
   2771 	} else
   2772 		spawn(trashcmd, fpath, NULL, NULL, F_NORMAL | F_MULTI);
   2773 
   2774 	return (access(fpath, F_OK) == -1); /* File is removed */
   2775 }
   2776 
   2777 static void xrmfromsel(char *path, char *fpath)
   2778 {
   2779 #ifndef NOX11
   2780 	bool selected = TRUE;
   2781 #endif
   2782 
   2783 	if ((pdents[cur].flags & DIR_OR_DIRLNK) && scanselforpath(fpath, FALSE))
   2784 		clearselection();
   2785 	else if (pdents[cur].flags & FILE_SELECTED) {
   2786 		--nselected;
   2787 		rmfromselbuf(mkpath(path, pdents[cur].name, g_sel));
   2788 	}
   2789 #ifndef NOX11
   2790 	else
   2791 		selected = FALSE;
   2792 
   2793 	if (selected && cfg.x11)
   2794 		plugscript(utils[UTIL_CBCP], F_NOWAIT | F_NOTRACE);
   2795 #endif
   2796 }
   2797 
   2798 static bool cpmv_rename(int choice, const char *path)
   2799 {
   2800 	int fd;
   2801 	uint_t count = 0, lines = 0;
   2802 	bool ret = FALSE;
   2803 	char *cmd = (choice == 'c' ? cp : mv);
   2804 	char buf[sizeof(patterns[P_CPMVRNM]) + (MAX(sizeof(cp), sizeof(mv))) + (PATH_MAX << 1)];
   2805 
   2806 	fd = create_tmp_file();
   2807 	if (fd == -1)
   2808 		return ret;
   2809 
   2810 	/* selsafe() returned TRUE for this to be called */
   2811 	if (!selbufpos) {
   2812 		snprintf(buf, sizeof(buf), "tr '\\0' '\\n' < '%s' > '%s'", selpath, g_tmpfpath);
   2813 		spawn(utils[UTIL_SH_EXEC], buf, NULL, NULL, F_CLI);
   2814 
   2815 		count = entries_in_file(fd, buf, sizeof(buf), NEWLINE_CHAR);
   2816 		if (!count)
   2817 			goto finish;
   2818 	} else
   2819 		seltofile(fd, &count, NEWLINE);
   2820 
   2821 	close(fd);
   2822 
   2823 	snprintf(buf, sizeof(buf), patterns[P_CPMVFMT], g_tmpfpath);
   2824 	spawn(utils[UTIL_SH_EXEC], buf, NULL, NULL, F_CLI);
   2825 
   2826 	spawn((cfg.waitedit ? enveditor : editor), g_tmpfpath, NULL, NULL, F_CLI);
   2827 
   2828 	fd = open(g_tmpfpath, O_RDONLY);
   2829 	if (fd == -1)
   2830 		goto finish;
   2831 
   2832 	lines = entries_in_file(fd, buf, sizeof(buf), NEWLINE_CHAR);
   2833 	DPRINTF_U(count);
   2834 	DPRINTF_U(lines);
   2835 	if (!lines || (2 * count != lines)) {
   2836 		DPRINTF_S("num mismatch");
   2837 		goto finish;
   2838 	}
   2839 
   2840 	snprintf(buf, sizeof(buf), patterns[P_CPMVRNM], path, g_tmpfpath, cmd);
   2841 	if (!spawn(utils[UTIL_SH_EXEC], buf, NULL, NULL, F_CLI | F_CHKRTN))
   2842 		ret = TRUE;
   2843 finish:
   2844 	if (fd >= 0)
   2845 		close(fd);
   2846 
   2847 	return ret;
   2848 }
   2849 
   2850 static bool cpmvrm_selection(enum action sel, char *path)
   2851 {
   2852 	int r;
   2853 
   2854 	if ((sel == SEL_CP || sel == SEL_MV) && isselfileempty())
   2855 		editselection(TRUE);
   2856 
   2857 	if (isselfileempty()) {
   2858 		if (nselected)
   2859 			clearselection();
   2860 		printmsg(messages[MSG_0_SELECTED]);
   2861 		return FALSE;
   2862 	}
   2863 
   2864 	if (!selsafe())
   2865 		return FALSE;
   2866 
   2867 	switch (sel) {
   2868 	case SEL_CP:
   2869 		opstr(g_buf, cp);
   2870 		break;
   2871 	case SEL_MV:
   2872 		opstr(g_buf, mv);
   2873 		break;
   2874 	case SEL_CPMVAS:
   2875 		r = get_input(messages[MSG_CP_MV_AS]);
   2876 		if (r != 'c' && r != 'm') {
   2877 			printmsg(messages[MSG_INVALID_KEY]);
   2878 			return FALSE;
   2879 		}
   2880 
   2881 		if (!cpmv_rename(r, path)) {
   2882 			printmsg(messages[MSG_FAILED]);
   2883 			return FALSE;
   2884 		}
   2885 		break;
   2886 	default: /* SEL_TRASH, SEL_RM_RF */
   2887 		if (!rmmulstr(g_buf, trashcmd && (sel == SEL_TRASH))) {
   2888 			printmsg(messages[MSG_CANCEL]);
   2889 			return FALSE;
   2890 		}
   2891 	}
   2892 
   2893 	if (sel != SEL_CPMVAS && spawn(utils[UTIL_SH_EXEC], g_buf, NULL, NULL, F_CLI | F_CHKRTN)) {
   2894 		printmsg(messages[MSG_FAILED]);
   2895 		return FALSE;
   2896 	}
   2897 
   2898 	/* Clear selection */
   2899 	clearselection();
   2900 
   2901 	return TRUE;
   2902 }
   2903 
   2904 #ifndef NOBATCH
   2905 static bool batch_rename(void)
   2906 {
   2907 	int fd1, fd2;
   2908 	uint_t count = 0, lines = 0;
   2909 	bool dir = FALSE, ret = FALSE;
   2910 	char foriginal[TMP_LEN_MAX] = {0};
   2911 	static const char batchrenamecmd[] = "paste -d'\n' '%s' '%s' | "SED" 'N; /^\\(.*\\)\\n\\1$/!p;d' | "
   2912 					     "tr '\n' '\\0' | xargs -0 -n2 sh -c 'mv -i -- \"$0\" \"$@\" <"
   2913 					     " /dev/tty'";
   2914 	char buf[sizeof(batchrenamecmd) + (PATH_MAX << 1)];
   2915 	int i = get_cur_or_sel();
   2916 
   2917 	if (!i)
   2918 		return ret;
   2919 
   2920 	if (i == 'c') { /* Rename entries in current dir */
   2921 		selbufpos = 0;
   2922 		dir = TRUE;
   2923 	}
   2924 
   2925 	fd1 = create_tmp_file();
   2926 	if (fd1 == -1)
   2927 		return ret;
   2928 
   2929 	xstrsncpy(foriginal, g_tmpfpath, xstrlen(g_tmpfpath) + 1);
   2930 
   2931 	fd2 = create_tmp_file();
   2932 	if (fd2 == -1) {
   2933 		unlink(foriginal);
   2934 		close(fd1);
   2935 		return ret;
   2936 	}
   2937 
   2938 	if (dir)
   2939 		for (i = 0; i < ndents; ++i)
   2940 			appendfpath(pdents[i].name, NAME_MAX);
   2941 
   2942 	seltofile(fd1, &count, NEWLINE);
   2943 	seltofile(fd2, NULL, NEWLINE);
   2944 	close(fd2);
   2945 
   2946 	if (dir) /* Don't retain dir entries in selection */
   2947 		selbufpos = 0;
   2948 
   2949 	spawn((cfg.waitedit ? enveditor : editor), g_tmpfpath, NULL, NULL, F_CLI);
   2950 
   2951 	/* Reopen file descriptor to get updated contents */
   2952 	fd2 = open(g_tmpfpath, O_RDONLY);
   2953 	if (fd2 == -1)
   2954 		goto finish;
   2955 
   2956 	lines = entries_in_file(fd2, buf, sizeof(buf), NEWLINE_CHAR);
   2957 	DPRINTF_U(count);
   2958 	DPRINTF_U(lines);
   2959 	if (!lines || (count != lines)) {
   2960 		DPRINTF_S("cannot delete files");
   2961 		goto finish;
   2962 	}
   2963 
   2964 	snprintf(buf, sizeof(buf), batchrenamecmd, foriginal, g_tmpfpath);
   2965 	spawn(utils[UTIL_SH_EXEC], buf, NULL, NULL, F_CLI);
   2966 	ret = TRUE;
   2967 
   2968 finish:
   2969 	if (fd1 >= 0)
   2970 		close(fd1);
   2971 	unlink(foriginal);
   2972 
   2973 	if (fd2 >= 0)
   2974 		close(fd2);
   2975 	unlink(g_tmpfpath);
   2976 
   2977 	return ret;
   2978 }
   2979 #endif
   2980 
   2981 static char *get_archive_cmd(const char *archive)
   2982 {
   2983 	uchar_t i = 3;
   2984 
   2985 	if (!g_state.usebsdtar && getutil(utils[UTIL_ATOOL]))
   2986 		i = 0;
   2987 	else if (getutil(utils[UTIL_BSDTAR]))
   2988 		i = 1;
   2989 	else if (is_suffix(archive, ".zip"))
   2990 		i = 2;
   2991 	// else tar
   2992 
   2993 	return archive_cmd[i];
   2994 }
   2995 
   2996 static void archive_selection(const char *cmd, const char *archive)
   2997 {
   2998 	size_t archive_esc_size = 0;
   2999 	size_t selpath_esc_size = 0;
   3000 	char *archive_esc = NULL;
   3001 	char *selpath_esc = NULL;
   3002 	size_t len;
   3003 	char *buf = NULL;
   3004 
   3005 	if (shell_escape(&archive_esc, &archive_esc_size, archive) < 0
   3006 	    || shell_escape(&selpath_esc, &selpath_esc_size, selpath) < 0) {
   3007 		DPRINTF_S(strerror(errno));
   3008 		printwarn(NULL);
   3009 		goto cleanup;
   3010 	}
   3011 
   3012 	len = xstrlen(patterns[P_ARCHIVE_CMD]) + xstrlen(cmd) + xstrlen(archive_esc) + xstrlen(selpath_esc) + 1;
   3013 	buf = malloc(len);
   3014 	if (!buf) {
   3015 		DPRINTF_S(strerror(errno));
   3016 		printwarn(NULL);
   3017 		goto cleanup;
   3018 	}
   3019 
   3020 	snprintf(buf, len, patterns[P_ARCHIVE_CMD], cmd, archive_esc, selpath_esc);
   3021 	spawn(utils[UTIL_SH_EXEC], buf, NULL, NULL, F_CLI | F_CONFIRM);
   3022 
   3023 cleanup:
   3024 	free(buf);
   3025 	free(archive_esc);
   3026 	free(selpath_esc);
   3027 }
   3028 
   3029 static void write_lastdir(const char *curpath, const char *outfile)
   3030 {
   3031 	bool tilde = false, ok = false;
   3032 	if (!outfile)
   3033 		xstrsncpy(cfgpath + xstrlen(cfgpath), "/.lastd", 8);
   3034 	else
   3035 		tilde = convert_tilde(outfile, g_buf);
   3036 
   3037 	int fd = open(outfile
   3038 			? (tilde ? g_buf : outfile)
   3039 			: cfgpath, O_CREAT | O_WRONLY | O_TRUNC, S_IWUSR | S_IRUSR);
   3040 	if (fd >= 0) {
   3041 		char *bufptr = g_buf;
   3042 		size_t buflen = sizeof(g_buf) - 3;
   3043 
   3044 		*bufptr++ = 'c';
   3045 		*bufptr++ = 'd';
   3046 		*bufptr++ = ' ';
   3047 		*bufptr = '\0';
   3048 
   3049 		ssize_t l = shell_escape(&bufptr, &buflen, curpath);
   3050 		ok = l >= 0 && write(fd, g_buf, l + 3) == l + 3;
   3051 		close(fd);
   3052 	}
   3053 	if (!ok)
   3054 		errexit();
   3055 }
   3056 
   3057 /*
   3058  * We assume none of the strings are NULL.
   3059  *
   3060  * Let's have the logic to sort numeric names in numeric order.
   3061  * E.g., the order '1, 10, 2' doesn't make sense to human eyes.
   3062  *
   3063  * If the absolute numeric values are same, we fallback to alphasort.
   3064  */
   3065 static int xstricmp(const char * const s1, const char * const s2)
   3066 {
   3067 	char *p1, *p2;
   3068 
   3069 	long long v1 = strtoll(s1, &p1, 10);
   3070 	long long v2 = strtoll(s2, &p2, 10);
   3071 
   3072 	/* Check if at least 1 string is numeric */
   3073 	if (s1 != p1 || s2 != p2) {
   3074 		/* Handle both pure numeric */
   3075 		if (s1 != p1 && s2 != p2) {
   3076 			if (v2 > v1)
   3077 				return -1;
   3078 
   3079 			if (v1 > v2)
   3080 				return 1;
   3081 		}
   3082 
   3083 		/* Only first string non-numeric */
   3084 		if (s1 == p1)
   3085 			return 1;
   3086 
   3087 		/* Only second string non-numeric */
   3088 		if (s2 == p2)
   3089 			return -1;
   3090 	}
   3091 
   3092 	/* Handle 1. all non-numeric and 2. both same numeric value cases */
   3093 #ifndef NOLC
   3094 	return strcoll(s1, s2);
   3095 #else
   3096 	return strcasecmp(s1, s2);
   3097 #endif
   3098 }
   3099 
   3100 /*
   3101  * Version comparison
   3102  *
   3103  * The code for version compare is a modified version of the GLIBC
   3104  * and uClibc implementation of strverscmp(). The source is here:
   3105  * https://elixir.bootlin.com/uclibc-ng/latest/source/libc/string/strverscmp.c
   3106  */
   3107 
   3108 /*
   3109  * Compare S1 and S2 as strings holding indices/version numbers,
   3110  * returning less than, equal to or greater than zero if S1 is less than,
   3111  * equal to or greater than S2 (for more info, see the texinfo doc).
   3112  *
   3113  * Ignores case.
   3114  */
   3115 static int xstrverscasecmp(const char * const s1, const char * const s2)
   3116 {
   3117 	const uchar_t *p1 = (const uchar_t *)s1;
   3118 	const uchar_t *p2 = (const uchar_t *)s2;
   3119 	int state, diff;
   3120 	uchar_t c1, c2;
   3121 
   3122 	/*
   3123 	 * Symbol(s)    0       [1-9]   others
   3124 	 * Transition   (10) 0  (01) d  (00) x
   3125 	 */
   3126 	static const uint8_t next_state[] = {
   3127 		/* state    x    d    0  */
   3128 		/* S_N */  S_N, S_I, S_Z,
   3129 		/* S_I */  S_N, S_I, S_I,
   3130 		/* S_F */  S_N, S_F, S_F,
   3131 		/* S_Z */  S_N, S_F, S_Z
   3132 	};
   3133 
   3134 	alignas(max_align_t) static const int8_t result_type[] = {
   3135 		/* state   x/x  x/d  x/0  d/x  d/d  d/0  0/x  0/d  0/0  */
   3136 
   3137 		/* S_N */  VCMP, VCMP, VCMP, VCMP, VLEN, VCMP, VCMP, VCMP, VCMP,
   3138 		/* S_I */  VCMP,   -1,   -1,    1, VLEN, VLEN,    1, VLEN, VLEN,
   3139 		/* S_F */  VCMP, VCMP, VCMP, VCMP, VCMP, VCMP, VCMP, VCMP, VCMP,
   3140 		/* S_Z */  VCMP,    1,    1,   -1, VCMP, VCMP,   -1, VCMP, VCMP
   3141 	};
   3142 
   3143 	if (p1 == p2)
   3144 		return 0;
   3145 
   3146 	c1 = TOUPPER(*p1);
   3147 	++p1;
   3148 	c2 = TOUPPER(*p2);
   3149 	++p2;
   3150 
   3151 	/* Hint: '0' is a digit too.  */
   3152 	state = S_N + ((c1 == '0') + (xisdigit(c1) != 0));
   3153 
   3154 	while ((diff = c1 - c2) == 0) {
   3155 		if (c1 == '\0')
   3156 			return diff;
   3157 
   3158 		state = next_state[state];
   3159 		c1 = TOUPPER(*p1);
   3160 		++p1;
   3161 		c2 = TOUPPER(*p2);
   3162 		++p2;
   3163 		state += (c1 == '0') + (xisdigit(c1) != 0);
   3164 	}
   3165 
   3166 	state = result_type[state * 3 + (((c2 == '0') + (xisdigit(c2) != 0)))]; // NOLINT
   3167 
   3168 	switch (state) {
   3169 	case VCMP:
   3170 		return diff;
   3171 	case VLEN:
   3172 		while (xisdigit(*p1++))
   3173 			if (!xisdigit(*p2++))
   3174 				return 1;
   3175 		return xisdigit(*p2) ? -1 : diff;
   3176 	default:
   3177 		return state;
   3178 	}
   3179 }
   3180 
   3181 static int (*namecmpfn)(const char * const s1, const char * const s2) = &xstricmp;
   3182 
   3183 static char * (*fnstrstr)(const char *haystack, const char *needle) = &strcasestr;
   3184 #ifdef PCRE2
   3185 static const unsigned char *tables;
   3186 static int pcre2flags = PCRE2_NO_AUTO_CAPTURE | PCRE2_EXTENDED | PCRE2_CASELESS | PCRE2_UTF;
   3187 #else
   3188 static int regflags = REG_NOSUB | REG_EXTENDED | REG_ICASE;
   3189 #endif
   3190 
   3191 #ifdef PCRE2
   3192 static int setfilter(pcre2_code **pcre2x, const char *filter)
   3193 {
   3194 	int errcode;
   3195 	PCRE2_SIZE erroffset;
   3196 
   3197 	*pcre2x = pcre2_compile((PCRE2_SPTR)filter, PCRE2_ZERO_TERMINATED, pcre2flags, &errcode, &erroffset, NULL);
   3198 	return *pcre2x ? 0 : -1;
   3199 }
   3200 #else
   3201 static int setfilter(regex_t *regex, const char *filter)
   3202 {
   3203 	return regcomp(regex, filter, regflags);
   3204 }
   3205 #endif
   3206 
   3207 /* Normalize space, underscore, and hyphen to the same character for fuzzy matching */
   3208 static inline wchar_t normalize_char(wchar_t c)
   3209 {
   3210 	if (c == L' ' || c == L'_' || c == L'-')
   3211 		return L' ';
   3212 	return c;
   3213 }
   3214 
   3215 /*
   3216  * Fuzzy match: check if all characters in filter appear in order in fname
   3217  * Case-sensitivity is controlled by fnstrstr function pointer
   3218  * Supports wide characters and Unicode
   3219  */
   3220 static int fuzzy_match(const char *filter, const char *fname)
   3221 {
   3222 	wchar_t filter_wcs[NAME_MAX], fname_wcs[NAME_MAX];
   3223 	wchar_t *f, *n;
   3224 	size_t filter_len, fname_len;
   3225 	bool case_insensitive = (fnstrstr == &strcasestr);
   3226 
   3227 	/* Convert multi-byte strings to wide character strings */
   3228 	filter_len = mbstowcs(filter_wcs, filter, NAME_MAX - 1);
   3229 	if (filter_len == (size_t)-1)
   3230 		return 0;
   3231 	filter_wcs[filter_len] = L'\0';
   3232 
   3233 	if (!filter_len)
   3234 		return 1;
   3235 
   3236 	fname_len = mbstowcs(fname_wcs, fname, NAME_MAX - 1);
   3237 	if (fname_len == (size_t)-1)
   3238 		return 0;
   3239 	fname_wcs[fname_len] = L'\0';
   3240 
   3241 	/* Convert to lowercase if case-insensitive matching */
   3242 	if (case_insensitive) {
   3243 		for (size_t i = 0; i < filter_len; ++i)
   3244 			filter_wcs[i] = towlower(filter_wcs[i]);
   3245 		for (size_t i = 0; i < fname_len; ++i)
   3246 			fname_wcs[i] = towlower(fname_wcs[i]);
   3247 	}
   3248 
   3249 	f = filter_wcs;
   3250 	n = fname_wcs;
   3251 
   3252 	/* Match characters in order */
   3253 	while (*f && *n) {
   3254 		if (normalize_char(*f) == normalize_char(*n))
   3255 			++f;
   3256 		++n;
   3257 	}
   3258 
   3259 	return !*f;
   3260 }
   3261 
   3262 static int visible_re(const fltrexp_t *fltrexp, const char *fname)
   3263 {
   3264 #ifdef PCRE2
   3265 	int r = 0;
   3266 	pcre2_match_data *match_data = pcre2_match_data_create_from_pattern(fltrexp->pcre2x, NULL);
   3267 
   3268 	if (match_data) {
   3269 		r = pcre2_match(fltrexp->pcre2x, (PCRE2_SPTR)fname, xstrlen(fname), 0, 0, match_data, NULL);
   3270 		pcre2_match_data_free(match_data);
   3271 	}
   3272 
   3273 	return r > 0;
   3274 #else
   3275 	return regexec(fltrexp->regex, fname, 0, NULL, 0) == 0;
   3276 #endif
   3277 }
   3278 
   3279 static int visible_str(const fltrexp_t *fltrexp, const char *fname)
   3280 {
   3281 	return fnstrstr(fname, fltrexp->str) != NULL;
   3282 }
   3283 
   3284 #ifdef DIM_FILTERED
   3285 /*
   3286  * Get match positions for fuzzy matching
   3287  * Populates matched array with 1 for matched bytes, 0 otherwise
   3288  */
   3289 static void fuzzy_match_positions(const char *filter, const char *fname, uchar_t *matched)
   3290 {
   3291 	wchar_t filter_wcs[NAME_MAX], fname_wcs[NAME_MAX];
   3292 	size_t filter_len, fname_len;
   3293 	bool case_insensitive = (fnstrstr == &strcasestr);
   3294 	size_t f_idx, n_idx;
   3295 
   3296 	/* Clear matched array */
   3297 	memset(matched, 0, NAME_MAX);
   3298 
   3299 	/* Convert multi-byte strings to wide character strings */
   3300 	filter_len = mbstowcs(filter_wcs, filter, NAME_MAX - 1);
   3301 	if (filter_len == (size_t)-1)
   3302 		return;
   3303 	filter_wcs[filter_len] = L'\0';
   3304 
   3305 	if (!filter_len)
   3306 		return;
   3307 
   3308 	fname_len = mbstowcs(fname_wcs, fname, NAME_MAX - 1);
   3309 	if (fname_len == (size_t)-1)
   3310 		return;
   3311 	fname_wcs[fname_len] = L'\0';
   3312 
   3313 	/* Convert to lowercase if case-insensitive matching */
   3314 	if (case_insensitive) {
   3315 		for (size_t i = 0; i < filter_len; ++i)
   3316 			filter_wcs[i] = towlower(filter_wcs[i]);
   3317 		for (size_t i = 0; i < fname_len; ++i)
   3318 			fname_wcs[i] = towlower(fname_wcs[i]);
   3319 	}
   3320 
   3321 	f_idx = 0;
   3322 	n_idx = 0;
   3323 
   3324 	/* Match characters in order and mark them */
   3325 	while (f_idx < filter_len && n_idx < fname_len) {
   3326 		if (normalize_char(filter_wcs[f_idx]) == normalize_char(fname_wcs[n_idx])) {
   3327 			/* Mark this wide character position as matched */
   3328 			matched[n_idx] = 1;
   3329 			++f_idx;
   3330 		}
   3331 		++n_idx;
   3332 	}
   3333 }
   3334 
   3335 /*
   3336  * Get match positions for string matching (substring)
   3337  * Populates matched array with 1 for matched bytes, 0 otherwise
   3338  */
   3339 static void string_match_positions(const char *filter, const char *fname, uchar_t *matched)
   3340 {
   3341 	const char *pos;
   3342 	size_t filter_len, i;
   3343 
   3344 	/* Clear matched array */
   3345 	memset(matched, 0, NAME_MAX);
   3346 
   3347 	filter_len = strlen(filter);
   3348 	if (!filter_len)
   3349 		return;
   3350 
   3351 	/* Find the substring */
   3352 	pos = fnstrstr(fname, filter);
   3353 	if (pos) {
   3354 		/* Convert string to wide characters to track positions */
   3355 		wchar_t fname_wcs[NAME_MAX];
   3356 		size_t fname_wcs_len = mbstowcs(fname_wcs, fname, NAME_MAX - 1);
   3357 		if (fname_wcs_len == (size_t)-1)
   3358 			return;
   3359 
   3360 		/* Find position in wide character string */
   3361 		size_t matched_pos = 0;
   3362 		const char *name_ptr = fname;
   3363 		while (name_ptr < pos && matched_pos < fname_wcs_len) {
   3364 			int mb_len = mblen(name_ptr, MB_LEN_MAX);
   3365 			if (mb_len <= 0)
   3366 				break;
   3367 			name_ptr += mb_len;
   3368 			++matched_pos;
   3369 		}
   3370 
   3371 		/* Mark matched positions */
   3372 		wchar_t filter_wcs[NAME_MAX];
   3373 		size_t filter_wcs_len = mbstowcs(filter_wcs, filter, NAME_MAX - 1);
   3374 		if (filter_wcs_len == (size_t)-1)
   3375 			return;
   3376 
   3377 		for (i = 0; i < filter_wcs_len && matched_pos + i < NAME_MAX; ++i)
   3378 			matched[matched_pos + i] = 1;
   3379 	}
   3380 }
   3381 #endif
   3382 
   3383 static int visible_fuzzy(const fltrexp_t *fltrexp, const char *fname)
   3384 {
   3385 	return fuzzy_match(fltrexp->str, fname);
   3386 }
   3387 
   3388 static int (*filterfn)(const fltrexp_t *fltr, const char *fname) = &visible_str;
   3389 static int (*entrycmpfn)(const void *va, const void *vb);
   3390 static const char *fuzzy_sort_fltr;
   3391 
   3392 static int fuzzy_match_score(const char *filter, const char *fname)
   3393 {
   3394 	wchar_t filter_wcs[NAME_MAX], fname_wcs[NAME_MAX];
   3395 	size_t match_pos[NAME_MAX];
   3396 	size_t filter_len, fname_len, f_idx, n_idx;
   3397 	bool case_insensitive = (fnstrstr == &strcasestr);
   3398 
   3399 	filter_len = mbstowcs(filter_wcs, filter, NAME_MAX - 1);
   3400 	if (filter_len == (size_t)-1)
   3401 		return INT_MAX;
   3402 	filter_wcs[filter_len] = L'\0';
   3403 
   3404 	if (!filter_len)
   3405 		return 0;
   3406 
   3407 	fname_len = mbstowcs(fname_wcs, fname, NAME_MAX - 1);
   3408 	if (fname_len == (size_t)-1)
   3409 		return INT_MAX;
   3410 	fname_wcs[fname_len] = L'\0';
   3411 
   3412 	if (case_insensitive) {
   3413 		for (size_t i = 0; i < filter_len; ++i)
   3414 			filter_wcs[i] = towlower(filter_wcs[i]);
   3415 		for (size_t i = 0; i < fname_len; ++i)
   3416 			fname_wcs[i] = towlower(fname_wcs[i]);
   3417 	}
   3418 
   3419 	f_idx = 0;
   3420 	n_idx = 0;
   3421 
   3422 	while (f_idx < filter_len && n_idx < fname_len) {
   3423 		if (normalize_char(filter_wcs[f_idx]) == normalize_char(fname_wcs[n_idx])) {
   3424 			match_pos[f_idx] = n_idx;
   3425 			++f_idx;
   3426 		}
   3427 		++n_idx;
   3428 	}
   3429 
   3430 	if (f_idx != filter_len)
   3431 		return INT_MAX;
   3432 
   3433 	{
   3434 		size_t first = match_pos[0], endpos = match_pos[filter_len - 1], span = (endpos - first) + 1;
   3435 		size_t gaps = span - filter_len, consec = 0, word_starts = 0;
   3436 
   3437 		for (size_t i = 0; i < filter_len; ++i) {
   3438 			if (i > 0 && match_pos[i] == match_pos[i - 1] + 1)
   3439 				++consec;
   3440 
   3441 			if (match_pos[i] == 0 || normalize_char(fname_wcs[match_pos[i] - 1]) == L' ')
   3442 				++word_starts;
   3443 		}
   3444 
   3445 		return (int)((gaps * 1000) + (first * 100) + ((span - consec) * 10) - (word_starts * 5));
   3446 	}
   3447 }
   3448 
   3449 static int fuzzyentrycmp(const void *va, const void *vb)
   3450 {
   3451 	const struct entry *pa = (const struct entry *)va;
   3452 	const struct entry *pb = (const struct entry *)vb;
   3453 	int sa, sb;
   3454 
   3455 	if (IS_DIR_OR_DIRLNK(pb) != IS_DIR_OR_DIRLNK(pa))
   3456 		return IS_DIR_OR_DIRLNK(pb) ? 1 : -1;
   3457 
   3458 	sa = fuzzy_match_score(fuzzy_sort_fltr, pa->name);
   3459 	sb = fuzzy_match_score(fuzzy_sort_fltr, pb->name);
   3460 
   3461 	if (sa != sb)
   3462 		return cfg.reverse ? (sa < sb ? 1 : -1) : (sa < sb ? -1 : 1);
   3463 
   3464 	return cfg.reverse ? -namecmpfn(pa->name, pb->name) : namecmpfn(pa->name, pb->name);
   3465 }
   3466 
   3467 static void clearfilter(void)
   3468 {
   3469 	char * const fltr = g_ctx[cfg.curctx].c_fltr;
   3470 
   3471 	if (fltr[1]) {
   3472 		fltr[REGEX_MAX - 1] = fltr[1];
   3473 		fltr[1] = '\0';
   3474 	}
   3475 }
   3476 
   3477 static int entrycmp(const void *va, const void *vb)
   3478 {
   3479 	const struct entry *pa = (pEntry)va;
   3480 	const struct entry *pb = (pEntry)vb;
   3481 
   3482 	if (IS_DIR_OR_DIRLNK(pb) != IS_DIR_OR_DIRLNK(pa))
   3483 		return IS_DIR_OR_DIRLNK(pb) ? 1 : -1;
   3484 
   3485 	/* Sort based on specified order */
   3486 	if (cfg.timeorder) {
   3487 		if (pb->sec > pa->sec)
   3488 			return 1;
   3489 		if (pb->sec < pa->sec)
   3490 			return -1;
   3491 		/* If sec matches, comare nsec */
   3492 		if (pb->nsec > pa->nsec)
   3493 			return 1;
   3494 		if (pb->nsec < pa->nsec)
   3495 			return -1;
   3496 	} else if (cfg.sizeorder) {
   3497 		if (pb->size > pa->size)
   3498 			return 1;
   3499 		if (pb->size < pa->size)
   3500 			return -1;
   3501 	} else if (cfg.blkorder) {
   3502 		if (pb->blocks > pa->blocks)
   3503 			return 1;
   3504 		if (pb->blocks < pa->blocks)
   3505 			return -1;
   3506 	} else if (cfg.extnorder && !IS_DIR_OR_DIRLNK(pb)) {
   3507 		char *extna = xextension(pa->name, pa->nlen - 1);
   3508 		char *extnb = xextension(pb->name, pb->nlen - 1);
   3509 
   3510 		if (extna || extnb) {
   3511 			if (!extna)
   3512 				return -1;
   3513 
   3514 			if (!extnb)
   3515 				return 1;
   3516 
   3517 			int ret = strcasecmp(extna, extnb);
   3518 
   3519 			if (ret)
   3520 				return ret;
   3521 		}
   3522 	}
   3523 
   3524 	return namecmpfn(pa->name, pb->name);
   3525 }
   3526 
   3527 static int reventrycmp(const void *va, const void *vb)
   3528 {
   3529 	if (IS_DIR_OR_DIRLNK((pEntry)vb) != IS_DIR_OR_DIRLNK((pEntry)va))
   3530 		return IS_DIR_OR_DIRLNK((pEntry)vb) ? 1 : -1;
   3531 
   3532 	return -entrycmp(va, vb);
   3533 }
   3534 
   3535 static int (*entrycmpfn)(const void *va, const void *vb) = &entrycmp;
   3536 
   3537 /* In case of an error, resets *wch to Esc */
   3538 static int handle_alt_key(wint_t *wch)
   3539 {
   3540 	timeout(0);
   3541 
   3542 	int r = get_wch(wch);
   3543 
   3544 	if (r == ERR)
   3545 		*wch = ESC;
   3546 	cleartimeout();
   3547 
   3548 	return r;
   3549 }
   3550 
   3551 static inline int handle_event(void)
   3552 {
   3553 	if (nselected && isselfileempty())
   3554 		clearselection();
   3555 	return CONTROL('L');
   3556 }
   3557 
   3558 /*
   3559  * Returns SEL_* if key is bound and 0 otherwise.
   3560  * Also modifies the run and env pointers (used on SEL_{RUN,RUNARG}).
   3561  * The next keyboard input can be simulated by presel.
   3562  */
   3563 static int nextsel(int presel)
   3564 {
   3565 	wint_t c = presel;
   3566 	int i = 0;
   3567 	bool escaped = FALSE;
   3568 
   3569 	if (c == 0 || c == MSGWAIT) {
   3570 try_quit:
   3571 		i = get_wch(&c);
   3572 		//DPRINTF_D(c);
   3573 		//DPRINTF_S(keyname(c));
   3574 
   3575 #ifdef KEY_RESIZE
   3576 		if (c == KEY_RESIZE)
   3577 			handle_key_resize();
   3578 #endif
   3579 
   3580 		/* Handle Alt+key */
   3581 		if (c == ESC) {
   3582 			timeout(0);
   3583 			i = get_wch(&c);
   3584 			if (i != ERR) {
   3585 				if (c == ESC)
   3586 					c = 'q'; /* Quit context */
   3587 				else {
   3588 					unget_wch(c);
   3589 					c = ';';
   3590 				}
   3591 				settimeout();
   3592 			} else if (escaped) {
   3593 				settimeout();
   3594 				c = CONTROL('Q');
   3595 			} else {
   3596 #ifndef NOFIFO
   3597 				if (!g_state.fifomode)
   3598 					notify_fifo(TRUE); /* Send hovered path to NNN_FIFO */
   3599 #endif
   3600 				escaped = TRUE;
   3601 				settimeout();
   3602 				goto try_quit;
   3603 			}
   3604 		}
   3605 
   3606 		if (i == ERR && presel == MSGWAIT)
   3607 			c = (cfg.filtermode || filterset()) ? FILTER : CONTROL('L');
   3608 		else if (c == FILTER || c == CONTROL('L'))
   3609 			/* Clear previous filter when manually starting */
   3610 			clearfilter();
   3611 	}
   3612 
   3613 	if (i == ERR) {
   3614 		++idle;
   3615 
   3616 		/*
   3617 		 * Do not check for directory changes in du mode.
   3618 		 * A redraw forces du calculation.
   3619 		 * Check for changes every odd second.
   3620 		 */
   3621 #ifdef LINUX_INOTIFY
   3622 		if (!cfg.blkorder && inotify_wd >= 0 && (idle & 1)) {
   3623 			struct inotify_event *event;
   3624 			char inotify_buf[EVENT_BUF_LEN] = {0};
   3625 
   3626 			i = read(inotify_fd, inotify_buf, EVENT_BUF_LEN);
   3627 			if (i > 0) {
   3628 				for (char *ptr = inotify_buf;
   3629 				     ptr + ((struct inotify_event *)ptr)->len < inotify_buf + i;
   3630 				     ptr += sizeof(struct inotify_event) + event->len) {
   3631 					event = (struct inotify_event *)ptr;
   3632 					DPRINTF_D(event->wd);
   3633 					DPRINTF_D(event->mask);
   3634 					if (!event->wd)
   3635 						break;
   3636 
   3637 					if (event->mask & INOTIFY_MASK) {
   3638 						c = handle_event();
   3639 						break;
   3640 					}
   3641 				}
   3642 				DPRINTF_S("inotify read done");
   3643 			}
   3644 		}
   3645 #elif defined(BSD_KQUEUE)
   3646 		if (!cfg.blkorder && event_fd >= 0 && (idle & 1)) {
   3647 			struct kevent event_data[NUM_EVENT_SLOTS] = {0};
   3648 
   3649 			if (kevent(kq, events_to_monitor, NUM_EVENT_SLOTS,
   3650 				   event_data, NUM_EVENT_FDS, &gtimeout) > 0)
   3651 				c = handle_event();
   3652 		}
   3653 #elif defined(HAIKU_NM)
   3654 		if (!cfg.blkorder && haiku_nm_active && (idle & 1) && haiku_is_update_needed(haiku_hnd))
   3655 			c = handle_event();
   3656 #endif
   3657 	} else
   3658 		idle = 0;
   3659 
   3660 	for (i = 0; i < (int)ELEMENTS(bindings); ++i)
   3661 		if (c == bindings[i].sym)
   3662 			return bindings[i].act;
   3663 
   3664 	return 0;
   3665 }
   3666 
   3667 static int getorderstr(char *sort)
   3668 {
   3669 	int i = 0;
   3670 
   3671 	if (cfg.showhidden)
   3672 		sort[i++] = 'H';
   3673 
   3674 	if (cfg.timeorder)
   3675 		sort[i++] = (cfg.timetype == T_MOD) ? 'M' : ((cfg.timetype == T_ACCESS) ? 'A' : 'C');
   3676 	else if (cfg.sizeorder)
   3677 		sort[i++] = 'S';
   3678 	else if (cfg.extnorder)
   3679 		sort[i++] = 'E';
   3680 
   3681 	if (entrycmpfn == &reventrycmp)
   3682 		sort[i++] = 'R';
   3683 
   3684 	if (namecmpfn == &xstrverscasecmp)
   3685 		sort[i++] = 'V';
   3686 
   3687 	if (i)
   3688 		sort[i] = ' ';
   3689 
   3690 	return i;
   3691 }
   3692 
   3693 static void showfilterinfo(void)
   3694 {
   3695 	int i = 0;
   3696 	char info[REGEX_MAX] = "\0\0\0\0\0";
   3697 
   3698 	i = getorderstr(info);
   3699 
   3700 	if (cfg.fileinfo && ndents && get_output("file", "-b", pdents[cur].name, -1, FALSE))
   3701 		mvaddstr(xlines - 2, 2, g_buf);
   3702 	else {
   3703 		const char *mode = cfg.regex ? "reg" : (cfg.fuzzy ? "fzy" : "str");
   3704 		snprintf(info + i, REGEX_MAX - i - 1, "  %s [/], %s [:]",
   3705 			 mode, ((fnstrstr == &strcasestr) ? "ic" : "noic"));
   3706 	}
   3707 
   3708 	mvaddstr(xlines - 2, xcols - xstrlen(info), info);
   3709 }
   3710 
   3711 static void showfilter(char *str)
   3712 {
   3713 	attron(COLOR_PAIR(cfg.curctx + 1));
   3714 	showfilterinfo();
   3715 	printmsg_nc(str);
   3716 	attroff(COLOR_PAIR(cfg.curctx + 1));
   3717 }
   3718 
   3719 static inline void swap_ent(int id1, int id2)
   3720 {
   3721 	struct entry _dent, *pdent1 = &pdents[id1], *pdent2 =  &pdents[id2];
   3722 
   3723 	*(&_dent) = *pdent1;
   3724 	*pdent1 = *pdent2;
   3725 	*pdent2 = *(&_dent);
   3726 }
   3727 
   3728 #ifdef PCRE2
   3729 static int fill(const char *fltr, pcre2_code *pcre2x)
   3730 #else
   3731 static int fill(const char *fltr, regex_t *re)
   3732 #endif
   3733 {
   3734 #ifdef PCRE2
   3735 	fltrexp_t fltrexp = { .pcre2x = pcre2x, .str = fltr };
   3736 #else
   3737 	fltrexp_t fltrexp = { .regex = re, .str = fltr };
   3738 #endif
   3739 
   3740 	int count = 0;
   3741 
   3742 	while (count < ndents) {
   3743 		if (filterfn(&fltrexp, pdents[count].name) == 0) {
   3744 			if (count != --ndents)
   3745 				swap_ent(count, ndents);
   3746 		} else
   3747 			++count;
   3748 	}
   3749 
   3750 	return ndents;
   3751 }
   3752 
   3753 static int matches(const char *fltr)
   3754 {
   3755 #ifdef PCRE2
   3756 	pcre2_code *pcre2x = NULL;
   3757 
   3758 	/* Search filter */
   3759 	if (cfg.regex && setfilter(&pcre2x, fltr))
   3760 		return -1;
   3761 
   3762 	ndents = fill(fltr, pcre2x);
   3763 
   3764 	if (cfg.regex)
   3765 		pcre2_code_free(pcre2x);
   3766 #else
   3767 	regex_t re;
   3768 
   3769 	/* Search filter */
   3770 	if (cfg.regex && setfilter(&re, fltr))
   3771 		return -1;
   3772 
   3773 	ndents = fill(fltr, &re);
   3774 
   3775 	if (cfg.regex)
   3776 		regfree(&re);
   3777 #endif
   3778 
   3779 	if (cfg.fuzzy && fltr[0]) {
   3780 		fuzzy_sort_fltr = fltr;
   3781 		ENTSORT(pdents, ndents, fuzzyentrycmp);
   3782 		fuzzy_sort_fltr = NULL;
   3783 	} else
   3784 		ENTSORT(pdents, ndents, entrycmpfn);
   3785 
   3786 	return ndents;
   3787 }
   3788 
   3789 /*
   3790  * Return the position of the matching entry or 0 otherwise
   3791  * Note there's no NULL check for fname
   3792  */
   3793 static int dentfind(const char *fname, int n)
   3794 {
   3795 	for (int i = 0; i < n; ++i)
   3796 		if (xstrcmp(fname, pdents[i].name) == 0)
   3797 			return i;
   3798 
   3799 	return 0;
   3800 }
   3801 
   3802 static int filterentries(char *path, char *lastname)
   3803 {
   3804 	alignas(max_align_t) wchar_t wln[REGEX_MAX];
   3805 	char *ln = g_ctx[cfg.curctx].c_fltr;
   3806 	wint_t ch[1];
   3807 	int r, total = ndents, len;
   3808 	char *pln = g_ctx[cfg.curctx].c_fltr + 1;
   3809 
   3810 	DPRINTF_S(__func__);
   3811 
   3812 	if (ndents && (ln[0] == FILTER || ln[0] == RFILTER) && *pln) {
   3813 		if (matches(pln) != -1) {
   3814 			move_cursor(dentfind(lastname, ndents), 0);
   3815 			redraw(path);
   3816 		}
   3817 
   3818 		if (!cfg.filtermode) {
   3819 			statusbar(path);
   3820 			return 0;
   3821 		}
   3822 
   3823 		len = mbstowcs(wln, ln, REGEX_MAX);
   3824 	} else {
   3825 		ln[0] = wln[0] = cfg.regex ? RFILTER : FILTER;
   3826 		ln[1] = wln[1] = '\0';
   3827 		len = 1;
   3828 	}
   3829 
   3830 	cleartimeout();
   3831 	curs_set(TRUE);
   3832 	leaveok(stdscr, FALSE);
   3833 	showfilter(ln);
   3834 
   3835 	while ((r = get_wch(ch)) != ERR) {
   3836 		//DPRINTF_D(*ch);
   3837 		//DPRINTF_S(keyname(*ch));
   3838 
   3839 		switch (*ch) {
   3840 #ifdef KEY_RESIZE
   3841 		case 0: // fallthrough
   3842 		case KEY_RESIZE:
   3843 			clearoldprompt();
   3844 			redraw(path);
   3845 			showfilter(ln);
   3846 			continue;
   3847 #endif
   3848 		case KEY_DC: // fallthrough
   3849 		case KEY_BACKSPACE: // fallthrough
   3850 		case '\b': // fallthrough
   3851 		case DEL: /* handle DEL */
   3852 			if (len != 1) {
   3853 				wln[--len] = '\0';
   3854 				wcstombs(ln, wln, REGEX_MAX);
   3855 				ndents = total;
   3856 			} else {
   3857 				*ch = FILTER;
   3858 				goto end;
   3859 			}
   3860 			// fallthrough
   3861 		case CONTROL('L'):
   3862 			if (*ch == CONTROL('L')) {
   3863 				if (wln[1]) {
   3864 					ln[REGEX_MAX - 1] = ln[1];
   3865 					ln[1] = wln[1] = '\0';
   3866 					len = 1;
   3867 					ndents = total;
   3868 				} else if (ln[REGEX_MAX - 1]) { /* Show the previous filter */
   3869 					ln[1] = ln[REGEX_MAX - 1];
   3870 					ln[REGEX_MAX - 1] = '\0';
   3871 					len = mbstowcs(wln, ln, REGEX_MAX);
   3872 				} else
   3873 					goto end;
   3874 			}
   3875 
   3876 			/* Go to the top, we don't know if the hovered file will match the filter */
   3877 			cur = 0;
   3878 
   3879 			if (matches(pln) != -1)
   3880 				redraw(path);
   3881 
   3882 			showfilter(ln);
   3883 			continue;
   3884 #ifndef NOMOUSE
   3885 		case KEY_MOUSE:
   3886 		{
   3887 			MEVENT event = {0};
   3888 			getmouse(&event);
   3889 			if (event.bstate == 0)
   3890 				continue;
   3891 			ungetmouse(&event);
   3892 			goto end;
   3893 		}
   3894 #endif
   3895 		case ESC:
   3896 			if (handle_alt_key(ch) != ERR) {
   3897 				if (*ch == ESC) /* Handle Alt+Esc */
   3898 					*ch = 'q'; /* Quit context */
   3899 				else {
   3900 					unget_wch(*ch);
   3901 					*ch = ';'; /* Run plugin */
   3902 				}
   3903 			}
   3904 			goto end;
   3905 		}
   3906 
   3907 		if (r != OK) /* Handle Fn keys in main loop */
   3908 			break;
   3909 
   3910 		/* Handle all control chars in main loop */
   3911 		if (*ch < ASCII_MAX && keyname(*ch)[0] == '^' && *ch != '^')
   3912 			goto end;
   3913 
   3914 		if (len == 1) {
   3915 			if (*ch == '?') /* Help and config key, '?' is an invalid regex */
   3916 				goto end;
   3917 
   3918 			if (cfg.filtermode) {
   3919 				switch (*ch) {
   3920 				case '\'': // fallthrough /* Go to first non-dir file */
   3921 				case '+': // fallthrough /* Toggle file selection */
   3922 				case ',': // fallthrough /* Mark CWD */
   3923 				case '-': // fallthrough /* Visit last visited dir */
   3924 				case '.': // fallthrough /* Show hidden files */
   3925 				case ';': // fallthrough /* Run plugin key */
   3926 				case '=': // fallthrough /* Launch app */
   3927 				case '>': // fallthrough /* Export file list */
   3928 				case '@': // fallthrough /* Visit start dir */
   3929 				case ']': // fallthorugh /* Prompt key */
   3930 				case '`': // fallthrough /* Visit / */
   3931 				case '~': /* Go HOME */
   3932 					goto end;
   3933 				}
   3934 			}
   3935 
   3936 			/* Toggle case-sensitivity */
   3937 			if (*ch == CASE) {
   3938 				fnstrstr = (fnstrstr == &strcasestr) ? &strstr : &strcasestr;
   3939 #ifdef PCRE2
   3940 				pcre2flags ^= PCRE2_CASELESS;
   3941 #else
   3942 				regflags ^= REG_ICASE;
   3943 #endif
   3944 				showfilter(ln);
   3945 				continue;
   3946 			}
   3947 
   3948 			/* Toggle string/fuzzy/regex filter */
   3949 			if (*ch == FILTER) {
   3950 				if (cfg.regex) {
   3951 					/* regex -> string */
   3952 					ln[0] = FILTER;
   3953 					cfg.regex = 0;
   3954 					filterfn = &visible_str;
   3955 				} else if (!cfg.fuzzy) {
   3956 					/* string -> fuzzy */
   3957 					ln[0] = FILTER;
   3958 					cfg.fuzzy = 1;
   3959 					filterfn = &visible_fuzzy;
   3960 				} else {
   3961 					/* fuzzy -> regex */
   3962 					ln[0] = RFILTER;
   3963 					cfg.regex = 1;
   3964 					cfg.fuzzy = 0;
   3965 					filterfn = &visible_re;
   3966 				}
   3967 				wln[0] = (uchar_t)ln[0];
   3968 				showfilter(ln);
   3969 				continue;
   3970 			}
   3971 
   3972 			/* Reset cur in case it's a repeat search */
   3973 			cur = 0;
   3974 		} else if (len == REGEX_MAX - 1)
   3975 			continue;
   3976 
   3977 		if (*ch != FILTER) {
   3978 			wln[len] = (wchar_t)*ch;
   3979 			wln[++len] = '\0';
   3980 			wcstombs(ln, wln, REGEX_MAX);
   3981 		}
   3982 
   3983 		/* Forward-filtering optimization:
   3984 		 * - new matches can only be a subset of current matches.
   3985 		 */
   3986 		/* ndents = total; */
   3987 		r = matches(pln);
   3988 		if (r <= 0) {
   3989 			if (r == 0)
   3990 				unget_wch(KEY_BACKSPACE);
   3991 			attron((COLOR_PAIR(cfg.curctx + 1)) | A_REVERSE | A_STANDOUT);
   3992 			printmsg_nc(ln);
   3993 			attroff((COLOR_PAIR(cfg.curctx + 1)) | A_REVERSE | A_STANDOUT);
   3994 			continue;
   3995 		}
   3996 
   3997 		if (cfg.autoenter) {
   3998 			/* If the only match is a dir, cd into it */
   3999 			if ((ndents == 1) && (pdents[0].flags & DIR_OR_DIRLNK) && xdiraccess(pdents[0].name)) {
   4000 				*ch = KEY_ENTER;
   4001 				cur = 0;
   4002 				goto end;
   4003 			} else if ((*ch == FILTER) && *pln) {
   4004 				/* If an exactly matching dir is present and filter key pressed, cd into it */
   4005 				r = dentfind(pln, ndents);
   4006 				if (((r > 0) || (xstrcmp(pln, pdents[r].name) == 0))
   4007 						&& (pdents[r].flags & DIR_OR_DIRLNK)
   4008 						&& xdiraccess(pdents[r].name)) {
   4009 					*ch = KEY_ENTER;
   4010 					cur = r;
   4011 					goto end;
   4012 				}
   4013 			}
   4014 		}
   4015 
   4016 		/*
   4017 		 * redraw() should be above the auto-enter optimization, for
   4018 		 * the case where there's an issue with dir auto-enter, say,
   4019 		 * due to a permission problem. The transition is _jumpy_ in
   4020 		 * case of such an error. However, we optimize for successful
   4021 		 * cases where the dir has permissions. This skips a redraw().
   4022 		 */
   4023 		redraw(path);
   4024 		showfilter(ln);
   4025 	}
   4026 end:
   4027 	/* Clear the info line after the down arrow */
   4028 	move(xlines - 2, 2);
   4029 	clrtoeol();
   4030 
   4031 	/* Save last working filter in-filter */
   4032 	if (ln[1])
   4033 		ln[REGEX_MAX - 1] = ln[1];
   4034 
   4035 	/* Save current */
   4036 	copycurname();
   4037 
   4038 	curs_set(FALSE);
   4039 	leaveok(stdscr, TRUE);
   4040 	settimeout();
   4041 
   4042 	/* Return keys for navigation etc. */
   4043 	return *ch;
   4044 }
   4045 
   4046 static char *getcmdfromhist(bool up)
   4047 {
   4048 	if (lastcmdpos == INVALID_POS) /* No entry in history */
   4049 		return NULL;
   4050 
   4051 	if (selcmdpos == INVALID_POS) /* Initial position at prompt */
   4052 		selcmdpos = up ? lastcmdpos : 0;
   4053 	else if (up)
   4054 		selcmdpos = (selcmdpos == 0) ? lastcmdpos : selcmdpos - 1;
   4055 	else
   4056 		selcmdpos = (selcmdpos == lastcmdpos) ? 0 : selcmdpos + 1;
   4057 
   4058 	return cmd_hist[selcmdpos];
   4059 }
   4060 
   4061 static void addcmdtohist(char *cmd)
   4062 {
   4063 	bool new = FALSE;
   4064 
   4065 	if (lastcmdpos == INVALID_POS)
   4066 		new = TRUE;
   4067 	else if ((selcmdpos == INVALID_POS) || (xstrcmp(cmd_hist[selcmdpos], cmd) != 0)) { /* New or not matching */
   4068 		if (lastcmdpos == (MAX_HISTORY - 1)) {
   4069 			free(cmd_hist[0]);
   4070 
   4071 			for (uchar_t pos = 0; pos < lastcmdpos; ++pos)
   4072 				cmd_hist[pos] = cmd_hist[pos + 1];
   4073 			--lastcmdpos;
   4074 		}
   4075 
   4076 		new = TRUE;
   4077 	} else if (selcmdpos != lastcmdpos) { /* Command matches, make it the latest if not already */
   4078 		cmd = cmd_hist[selcmdpos];
   4079 
   4080 		for (uchar_t pos = selcmdpos; pos < lastcmdpos; ++pos)
   4081 			cmd_hist[pos] = cmd_hist[pos + 1];
   4082 		cmd_hist[lastcmdpos] = cmd;
   4083 	}
   4084 
   4085 	if (new)
   4086 		cmd_hist[++lastcmdpos] = xstrdup(cmd);
   4087 }
   4088 
   4089 #ifdef NORL
   4090 /* Reads the history file and populates command history */
   4091 static void xread_history(const char *histpath)
   4092 {
   4093 	int linesread = 0;
   4094 	size_t len = 0;
   4095 	ssize_t read;
   4096 	char *line = NULL;
   4097 	FILE *fptr = fopen(histpath, "r");
   4098 
   4099 	/* We want to open for reading, ignore errors */
   4100 	if (fptr) {
   4101 		while ((linesread++ < MAX_HISTORY) && ((read = getline(&line, &len, fptr)) != -1))
   4102 		{
   4103 			if (line[read - 1] == NEWLINE_CHAR)
   4104 				line[read - 1] = '\0';
   4105 			addcmdtohist(line);
   4106 		}
   4107 
   4108 		free(line);
   4109 	}
   4110 }
   4111 
   4112 /* Populates the history file from the command history */
   4113 static void xwrite_history(const char *histpath)
   4114 {
   4115 	if (lastcmdpos == INVALID_POS)
   4116 		return;
   4117 
   4118 	FILE *fptr = fopen(histpath, "w");
   4119 
   4120 	if (fptr)
   4121 		for (uchar_t pos = 0; pos <= lastcmdpos; ++pos)
   4122 			fprintf(fptr, "%s\n", cmd_hist[pos]);
   4123 	else
   4124 		xerror();
   4125 }
   4126 #endif
   4127 
   4128 /* Show a prompt with input string and return the changes */
   4129 static char *xreadline(const char *prefill, const char *prompt)
   4130 {
   4131 	size_t len, pos, lpos;
   4132 	int x, r;
   4133 	const int WCHAR_T_WIDTH = sizeof(wchar_t);
   4134 	wint_t ch[1];
   4135 	wchar_t * const buf = malloc(sizeof(wchar_t) * (READLINE_MAX + 1)); // 1 element extra for handling full-width characters
   4136 	int tab_cycle = -1;   /* index into pdents for Tab cycling, -1 = inactive */
   4137 	size_t tab_basepos = 0; /* position in buf before Tab insertion */
   4138 
   4139 	if (!buf)
   4140 		errexit();
   4141 
   4142 	cleartimeout();
   4143 	printmsg(prompt);
   4144 
   4145 	if (prefill) {
   4146 		DPRINTF_S(prefill);
   4147 		len = pos = mbstowcs(buf, prefill, READLINE_MAX);
   4148 	} else
   4149 		len = (size_t)-1;
   4150 
   4151 	if (len == (size_t)-1) {
   4152 		buf[0] = '\0';
   4153 		len = pos = 0;
   4154 	}
   4155 
   4156 	x = getcurx(stdscr);
   4157 	curs_set(TRUE);
   4158 	leaveok(stdscr, FALSE);
   4159 
   4160 	while (1) {
   4161 		buf[len] = ' ';
   4162 		buf[len + 1] = ' '; // Handle full-width characters
   4163 
   4164 		move(xlines - 1, x);
   4165 		clrtoeol();
   4166 		attron(COLOR_PAIR(cfg.curctx + 1));
   4167 		if (pos > (size_t)(xcols - x)) {
   4168 			mvaddnwstr(xlines - 1, x, buf + (pos - (xcols - x) + 1), xcols - x + 1);
   4169 			move(xlines - 1, xcols - 1);
   4170 		} else {
   4171 			mvaddnwstr(xlines - 1, x, buf, len + 2);
   4172 			move(xlines - 1, x + wcswidth(buf, pos));
   4173 		}
   4174 		attroff(COLOR_PAIR(cfg.curctx + 1));
   4175 
   4176 		r = get_wch(ch);
   4177 		if (r == ERR)
   4178 			continue;
   4179 
   4180 		if (!(r == OK && *ch == '\t'))
   4181 			tab_cycle = -1;
   4182 
   4183 		if (r == OK) {
   4184 			switch (*ch) {
   4185 			case KEY_ENTER: // fallthrough
   4186 			case '\n': // fallthrough
   4187 			case '\r':
   4188 				goto END;
   4189 			case CONTROL('D'):
   4190 				if (pos < len)
   4191 					++pos;
   4192 				else if (!(pos || len)) { /* Exit on ^D at empty prompt */
   4193 					len = 0;
   4194 					goto END;
   4195 				} else
   4196 					continue;
   4197 				// fallthrough
   4198 			case DEL: // fallthrough
   4199 			case '\b': /* rhel25 sends '\b' for backspace */
   4200 				if (pos > 0) {
   4201 					memmove(buf + pos - 1, buf + pos,
   4202 						(len - pos) * WCHAR_T_WIDTH);
   4203 					--len, --pos;
   4204 				}
   4205 				continue;
   4206 			case '\t':
   4207 				if (ndents) {
   4208 					int idx;
   4209 
   4210 					if (tab_cycle < 0) {
   4211 						/* First Tab: start cycling from cur */
   4212 						tab_basepos = pos;
   4213 						/* Only allow appending at end */
   4214 						if (len != pos)
   4215 							continue;
   4216 						idx = cur;
   4217 					} else {
   4218 						/* Subsequent Tab: remove previous insertion, advance */
   4219 						len = pos = tab_basepos;
   4220 						idx = (tab_cycle + 1) % ndents;
   4221 					}
   4222 
   4223 					char *escptr = g_buf;
   4224 					size_t esclen = CMD_LEN_MAX;
   4225 					ssize_t elen = shell_escape(&escptr, &esclen, pdents[idx].name);
   4226 
   4227 					if (elen > 0 && (tab_basepos < (READLINE_MAX - (size_t)elen))) {
   4228 						buf[tab_basepos] = '\0';
   4229 						lpos = mbstowcs(NULL, g_buf, 0);
   4230 						pos = tab_basepos + mbstowcs(buf + tab_basepos, g_buf, lpos + 1);
   4231 						len = pos;
   4232 						tab_cycle = idx;
   4233 					}
   4234 				}
   4235 				continue;
   4236 			case CONTROL('F'):
   4237 				if (pos < len)
   4238 					++pos;
   4239 				continue;
   4240 			case CONTROL('B'):
   4241 				if (pos > 0)
   4242 					--pos;
   4243 				continue;
   4244 			case CONTROL('W'):
   4245 				printmsg(prompt);
   4246 				lpos = pos;
   4247 				while (pos > 0 && ISSPACE(buf[pos-1]))
   4248 					--pos;
   4249 				while (pos > 0 && !ISSPACE(buf[pos-1]))
   4250 					--pos;
   4251 				memmove(buf + pos, buf + lpos, (len - lpos) * WCHAR_T_WIDTH);
   4252 				len -= lpos - pos;
   4253 				continue;
   4254 			case CONTROL('K'):
   4255 				printmsg(prompt);
   4256 				len = pos;
   4257 				continue;
   4258 			case CONTROL('L'):
   4259 				printmsg(prompt);
   4260 				len = pos = 0;
   4261 				continue;
   4262 			case CONTROL('A'):
   4263 				pos = 0;
   4264 				continue;
   4265 			case CONTROL('E'):
   4266 				pos = len;
   4267 				continue;
   4268 			case CONTROL('U'):
   4269 				printmsg(prompt);
   4270 				memmove(buf, buf + pos, (len - pos) * WCHAR_T_WIDTH);
   4271 				len -= pos;
   4272 				pos = 0;
   4273 				continue;
   4274 			case ESC: /* Exit prompt on Esc, but just filter out Alt+key */
   4275 				if (handle_alt_key(ch) != ERR) {
   4276 					switch (*ch) {
   4277 					case 'd':
   4278 						printmsg(prompt);
   4279 						lpos = pos;
   4280 						while (pos < len && !iswalnum(buf[pos + 1]))
   4281 							++pos;
   4282 						while (pos < len && iswalnum(buf[++pos]));
   4283 						memmove(buf + lpos, buf + pos, (len - pos) * WCHAR_T_WIDTH);
   4284 						len -= pos - lpos;
   4285 						pos = lpos;
   4286 						continue;
   4287 					case KEY_BACKSPACE:
   4288 						printmsg(prompt);
   4289 						lpos = pos;
   4290 						while (pos > 0 && !iswalnum(buf[pos - 1]))
   4291 							--pos;
   4292 						while (pos > 0 && iswalnum(buf[pos - 1]))
   4293 							--pos;
   4294 						memmove(buf + pos, buf + lpos, (len - lpos) * WCHAR_T_WIDTH);
   4295 						len -= lpos - pos;
   4296 						continue;
   4297 					case 'f':
   4298 						while (pos < len && !iswalnum(buf[pos + 1]))
   4299 							++pos;
   4300 						while (pos < len && iswalnum(buf[++pos]));
   4301 						continue;
   4302 					case 'b':
   4303 						while (pos > 0 && !iswalnum(buf[pos - 1]))
   4304 							--pos;
   4305 						while (pos > 0 && iswalnum(buf[pos - 1]))
   4306 							--pos;
   4307 						continue;
   4308 					}
   4309 					continue;
   4310 				}
   4311 
   4312 				len = 0;
   4313 				goto END;
   4314 			}
   4315 
   4316 			/* Filter out all other control chars */
   4317 			if (*ch < ASCII_MAX && keyname(*ch)[0] == '^')
   4318 				continue;
   4319 
   4320 			if (pos < READLINE_MAX - 1) {
   4321 				memmove(buf + pos + 1, buf + pos,
   4322 					(len - pos) * WCHAR_T_WIDTH);
   4323 				buf[pos] = *ch;
   4324 				++len, ++pos;
   4325 				continue;
   4326 			}
   4327 		} else {
   4328 			switch (*ch) {
   4329 #ifdef KEY_RESIZE
   4330 			case KEY_RESIZE:
   4331 				clearoldprompt();
   4332 				xlines = LINES;
   4333 				printmsg(prompt);
   4334 				break;
   4335 #endif
   4336 			case KEY_LEFT:
   4337 				if (pos > 0)
   4338 					--pos;
   4339 				break;
   4340 			case KEY_RIGHT:
   4341 				if (pos < len)
   4342 					++pos;
   4343 				break;
   4344 			case KEY_BACKSPACE:
   4345 				if (pos > 0) {
   4346 					memmove(buf + pos - 1, buf + pos,
   4347 						(len - pos) * WCHAR_T_WIDTH);
   4348 					--len, --pos;
   4349 				}
   4350 				break;
   4351 			case KEY_DC:
   4352 				if (pos < len) {
   4353 					memmove(buf + pos, buf + pos + 1,
   4354 						(len - pos - 1) * WCHAR_T_WIDTH);
   4355 					--len;
   4356 				}
   4357 				break;
   4358 			case KEY_END:
   4359 				pos = len;
   4360 				break;
   4361 			case KEY_HOME:
   4362 				pos = 0;
   4363 				break;
   4364 			case KEY_UP: // fallthrough
   4365 			case KEY_DOWN:
   4366 			{
   4367 				char *cmd = getcmdfromhist(*ch == KEY_UP);
   4368 
   4369 				if (prompt && cmd && (xstrcmp(prompt, PROMPT) == 0)) {
   4370 					printmsg(prompt);
   4371 					len = pos = mbstowcs(buf, cmd, READLINE_MAX); // fallthrough
   4372 				}
   4373 			}
   4374 			default:
   4375 				break;
   4376 			}
   4377 		}
   4378 	}
   4379 
   4380 END:
   4381 	curs_set(FALSE);
   4382 	leaveok(stdscr, TRUE);
   4383 	settimeout();
   4384 	printmsg("");
   4385 
   4386 	buf[len] = '\0';
   4387 
   4388 	pos = wcstombs(g_buf, buf, READLINE_MAX - 1);
   4389 	if (pos >= READLINE_MAX - 1)
   4390 		g_buf[READLINE_MAX - 1] = '\0';
   4391 
   4392 	free(buf);
   4393 	return g_buf;
   4394 }
   4395 
   4396 #ifndef NORL
   4397 /*
   4398  * Caller should check the value of presel to confirm if it needs to wait to show warning
   4399  */
   4400 static char *getreadline(const char *prompt)
   4401 {
   4402 	exitcurses();
   4403 
   4404 	char *input = readline(prompt);
   4405 
   4406 	refresh();
   4407 
   4408 	if (input && input[0]) {
   4409 		add_history(input);
   4410 		xstrsncpy(g_buf, input, CMD_LEN_MAX);
   4411 		free(input);
   4412 		return g_buf;
   4413 	}
   4414 
   4415 	free(input);
   4416 	return NULL;
   4417 }
   4418 #endif
   4419 
   4420 /*
   4421  * Create symbolic/hard link(s) to file(s) in selection list
   4422  * Returns the number of links created, -1 on error
   4423  */
   4424 static int xlink(char *prefix, char *path, char *curfname, char *buf, int type)
   4425 {
   4426 	int count = 0, choice;
   4427 	char *psel = pselbuf, *fname;
   4428 	size_t pos = 0, len, r;
   4429 	int (*link_fn)(const char *, const char *) = NULL;
   4430 	char lnpath[PATH_MAX];
   4431 
   4432 	choice = get_cur_or_sel();
   4433 	if (!choice)
   4434 		return -1;
   4435 
   4436 	if (type == 's') /* symbolic link */
   4437 		link_fn = &symlink;
   4438 	else /* hard link */
   4439 		link_fn = &link;
   4440 
   4441 	if (choice == 'c' || (nselected == 1)) {
   4442 		prefix = abspath(prefix, path, lnpath); /* Generate link path */
   4443 		if (!prefix)
   4444 			return -1;
   4445 
   4446 		if (choice == 'c')
   4447 			mkpath(path, curfname, buf); /* Generate target file path */
   4448 
   4449 		if (!link_fn((choice == 'c') ? buf : pselbuf, lnpath)) {
   4450 			if (choice == 's')
   4451 				clearselection();
   4452 			return 1; /* One link created */
   4453 		}
   4454 		return 0;
   4455 	}
   4456 
   4457 	r = xstrsncpy(buf, prefix, NAME_MAX + 1); /* Copy prefix */
   4458 
   4459 	while (pos < selbufpos) {
   4460 		len = xstrlen(psel);
   4461 		fname = xbasename(psel);
   4462 
   4463 		xstrsncpy(buf + r - 1, fname, NAME_MAX - r); /* Suffix target file name */
   4464 		mkpath(path, buf, lnpath); /* Generate link path */
   4465 
   4466 		if (!link_fn(psel, lnpath))
   4467 			++count;
   4468 
   4469 		pos += len + 1;
   4470 		psel += len + 1;
   4471 	}
   4472 
   4473 	if (count == nselected) /* Clear selection if all links are generated */
   4474 		clearselection();
   4475 	return count;
   4476 }
   4477 
   4478 static bool parsekvpair(kv **arr, char **envcpy, const uchar_t id, uchar_t *items)
   4479 {
   4480 	bool new = TRUE;
   4481 	const uchar_t INCR = 8;
   4482 	uint_t i = 0;
   4483 	kv *kvarr = NULL;
   4484 	char *ptr = getenv(env_cfg[id]);
   4485 
   4486 	if (!ptr || !*ptr)
   4487 		return TRUE;
   4488 
   4489 	*envcpy = xstrdup(ptr);
   4490 	if (!*envcpy) {
   4491 		xerror();
   4492 		return FALSE;
   4493 	}
   4494 
   4495 	ptr = *envcpy;
   4496 
   4497 	while (*ptr && i < 100) {
   4498 		if (new) {
   4499 			if (!(i & (INCR - 1))) {
   4500 				kvarr = xrealloc(kvarr, sizeof(kv) * (i + INCR));
   4501 				*arr = kvarr;
   4502 				if (!kvarr) {
   4503 					xerror();
   4504 					return FALSE;
   4505 				}
   4506 				memset(kvarr + i, 0, sizeof(kv) * INCR);
   4507 			}
   4508 			kvarr[i].key = (uchar_t)*ptr;
   4509 			if (*++ptr != ':' || *++ptr == '\0' || *ptr == ';')
   4510 				return FALSE;
   4511 			kvarr[i].off = ptr - *envcpy;
   4512 			++i;
   4513 
   4514 			new = FALSE;
   4515 		}
   4516 
   4517 		if (*ptr == ';') {
   4518 			*ptr = '\0';
   4519 			new = TRUE;
   4520 		}
   4521 
   4522 		++ptr;
   4523 	}
   4524 
   4525 	*items = i;
   4526 	return ((i != 0) && (i <= 100));
   4527 }
   4528 
   4529 /*
   4530  * Get the value corresponding to a key
   4531  *
   4532  * NULL is returned in case of no match, path resolution failure etc.
   4533  * buf would be modified, so check return value before access
   4534  */
   4535 static char *get_kv_val(kv *kvarr, char *buf, int key, uchar_t max, uchar_t id)
   4536 {
   4537 	char *val;
   4538 
   4539 	if (!kvarr)
   4540 		return NULL;
   4541 
   4542 	for (int r = 0; r < max && kvarr[r].key; ++r) {
   4543 		if (kvarr[r].key == key) {
   4544 			/* Do not allocate new memory for plugin */
   4545 			if (id == NNN_PLUG)
   4546 				return pluginstr + kvarr[r].off;
   4547 
   4548 			val = bmstr + kvarr[r].off;
   4549 			bool tilde = convert_tilde(val, g_buf);
   4550 			return abspath((tilde ? g_buf : val), NULL, buf);
   4551 		}
   4552 	}
   4553 
   4554 	DPRINTF_S("Invalid key");
   4555 	return NULL;
   4556 }
   4557 
   4558 static int get_kv_key(kv *kvarr, char *val, uchar_t max, uchar_t id)
   4559 {
   4560 	if (!kvarr)
   4561 		return -1;
   4562 
   4563 	if (id != NNN_ORDER) /* For now this function supports only order string */
   4564 		return -1;
   4565 
   4566 	for (int r = 0; r < max && kvarr[r].key; ++r) {
   4567 		if (xstrcmp((orderstr + kvarr[r].off), val) == 0)
   4568 			return kvarr[r].key;
   4569 	}
   4570 
   4571 	return -1;
   4572 }
   4573 
   4574 static void resetdircolor(int flags)
   4575 {
   4576 	/* Directories are always shown on top, clear the color when moving to first file */
   4577 	if (g_state.dircolor && !(flags & DIR_OR_DIRLNK)) {
   4578 		attroff(COLOR_PAIR(cfg.curctx + 1) | A_BOLD);
   4579 		g_state.dircolor = 0;
   4580 	}
   4581 }
   4582 
   4583 /*
   4584  * Replace escape characters in a string with '?'
   4585  * Adjust string length to maxcols if > 0;
   4586  * Max supported str length: NAME_MAX;
   4587  */
   4588 #ifdef NOLC
   4589 static char *unescape(const char *str, uint_t maxcols)
   4590 {
   4591 	char * const wbuf = g_buf;
   4592 	char *buf = wbuf;
   4593 
   4594 	xstrsncpy(wbuf, str, maxcols);
   4595 #else
   4596 static wchar_t *unescape(const char *str, uint_t maxcols)
   4597 {
   4598 	wchar_t * const wbuf = (wchar_t *)g_buf;
   4599 	wchar_t *buf = wbuf;
   4600 	size_t len = mbstowcs(wbuf, str, maxcols); /* Convert multi-byte to wide char */
   4601 
   4602 	len = wcswidth(wbuf, len);
   4603 
   4604 	if (len >= maxcols) {
   4605 		size_t lencount = maxcols;
   4606 
   4607 		while (len > maxcols) /* Reduce wide chars one by one till it fits */
   4608 			len = wcswidth(wbuf, --lencount);
   4609 
   4610 		wbuf[lencount] = L'\0'; // NOLINT
   4611 	}
   4612 #endif
   4613 
   4614 	while (*buf) {
   4615 		if (*buf <= '\x1f' || *buf == '\x7f')
   4616 			*buf = '\?';
   4617 
   4618 		++buf;
   4619 	}
   4620 
   4621 	return wbuf;
   4622 }
   4623 
   4624 static off_t get_size(off_t size, off_t *pval, int comp)
   4625 {
   4626 	off_t rem = *pval;
   4627 	off_t quo = rem / 10;
   4628 
   4629 	if ((rem - (quo * 10)) >= 5) {
   4630 		rem = quo + 1;
   4631 		if (rem == comp) {
   4632 			++size;
   4633 			rem = 0;
   4634 		}
   4635 	} else
   4636 		rem = quo;
   4637 
   4638 	*pval = rem;
   4639 	return size;
   4640 }
   4641 
   4642 static char *coolsize(off_t size)
   4643 {
   4644 	const char * const U = "BKMGTPEZY";
   4645 	static char size_buf[16]; /* Buffer to hold human readable size */
   4646 	off_t rem = 0;
   4647 	size_t ret;
   4648 	int i = 0;
   4649 
   4650 	while (size >= 1024) {
   4651 		rem = size & (0x3FF); /* 1024 - 1 = 0x3FF */
   4652 		size >>= 10;
   4653 		++i;
   4654 	}
   4655 
   4656 	if (i == 1) {
   4657 		rem = (rem * 1000) >> 10;
   4658 		rem /= 10;
   4659 		size = get_size(size, &rem, 10);
   4660 	} else if (i == 2) {
   4661 		rem = (rem * 1000) >> 10;
   4662 		size = get_size(size, &rem, 100);
   4663 	} else if (i > 2) {
   4664 		rem = (rem * 10000) >> 10;
   4665 		size = get_size(size, &rem, 1000);
   4666 	}
   4667 
   4668 	if (i > 0 && i < 6 && rem) {
   4669 		ret = xstrsncpy(size_buf, xitoa(size), 12);
   4670 		size_buf[ret - 1] = '.';
   4671 
   4672 		char *frac = xitoa(rem);
   4673 		size_t toprint = i > 3 ? 3 : i;
   4674 		size_t len = xstrlen(frac);
   4675 
   4676 		if (len < toprint) {
   4677 			size_buf[ret] = size_buf[ret + 1] = size_buf[ret + 2] = '0';
   4678 			xstrsncpy(size_buf + ret + (toprint - len), frac, len + 1);
   4679 		} else
   4680 			xstrsncpy(size_buf + ret, frac, 12 - ret);
   4681 
   4682 		ret += toprint;
   4683 	} else {
   4684 		ret = xstrsncpy(size_buf, size ? xitoa(size) : "0", 12);
   4685 		--ret;
   4686 	}
   4687 
   4688 	size_buf[ret] = U[i];
   4689 	size_buf[ret + 1] = '\0';
   4690 
   4691 	return size_buf;
   4692 }
   4693 
   4694 /* Convert a mode field into "ls -l" type perms field. */
   4695 static char *get_lsperms(mode_t mode)
   4696 {
   4697 	static const char * const rwx[] = {"---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx"};
   4698 	static char bits[11] = {'\0'};
   4699 
   4700 	switch (mode & S_IFMT) {
   4701 	case S_IFREG:
   4702 		bits[0] = '-';
   4703 		break;
   4704 	case S_IFDIR:
   4705 		bits[0] = 'd';
   4706 		break;
   4707 	case S_IFLNK:
   4708 		bits[0] = 'l';
   4709 		break;
   4710 	case S_IFSOCK:
   4711 		bits[0] = 's';
   4712 		break;
   4713 	case S_IFIFO:
   4714 		bits[0] = 'p';
   4715 		break;
   4716 	case S_IFBLK:
   4717 		bits[0] = 'b';
   4718 		break;
   4719 	case S_IFCHR:
   4720 		bits[0] = 'c';
   4721 		break;
   4722 	default:
   4723 		bits[0] = '?';
   4724 		break;
   4725 	}
   4726 
   4727 	xstrsncpy(&bits[1], rwx[(mode >> 6) & 7], 4);
   4728 	xstrsncpy(&bits[4], rwx[(mode >> 3) & 7], 4);
   4729 	xstrsncpy(&bits[7], rwx[(mode & 7)], 4);
   4730 
   4731 	if (mode & S_ISUID)
   4732 		bits[3] = (mode & 0100) ? 's' : 'S';  /* user executable */
   4733 	if (mode & S_ISGID)
   4734 		bits[6] = (mode & 0010) ? 's' : 'l';  /* group executable */
   4735 	if (mode & S_ISVTX)
   4736 		bits[9] = (mode & 0001) ? 't' : 'T';  /* others executable */
   4737 
   4738 	return bits;
   4739 }
   4740 
   4741 #ifdef ICONS_ENABLED
   4742 static struct icon get_icon(const struct entry *ent)
   4743 {
   4744 	for (size_t i = 0; i < ELEMENTS(icons_name); ++i)
   4745 		if (strcasecmp(ent->name, icons_name[i].match) == 0)
   4746 			return (struct icon){ icons_name[i].icon, icons_name[i].color };
   4747 
   4748 	if (ent->flags & DIR_OR_DIRLNK)
   4749 		return dir_icon;
   4750 
   4751 	char *tmp = xextension(ent->name, ent->nlen);
   4752 
   4753 	if (tmp) {
   4754 		uint16_t z, k, h = icon_ext_hash(++tmp); /* ++tmp to skip '.' */
   4755 		for (k = 0; k < ICONS_PROBE_MAX; ++k) {
   4756 			z = (h + k) % ELEMENTS(icons_ext);
   4757 			if (strcasecmp(tmp, icons_ext[z].match) == 0)
   4758 				return (struct icon){ icons_ext_uniq[icons_ext[z].idx], icons_ext[z].color };
   4759 		}
   4760 	}
   4761 
   4762 	/* If there's no match and the file is executable, icon that */
   4763 	if (ent->mode & 0100)
   4764 		return exec_icon;
   4765 	return file_icon;
   4766 }
   4767 
   4768 static void print_icon(const struct entry *ent, const int attrs)
   4769 {
   4770 	const struct icon icon = get_icon(ent);
   4771 	addstr(ICON_PADDING_LEFT);
   4772 	if (icon.color)
   4773 		attron(COLOR_PAIR(C_UND + 1 + icon.color));
   4774 	else if (attrs)
   4775 		attron(attrs);
   4776 	addstr(icon.icon);
   4777 	if (icon.color)
   4778 		attroff(COLOR_PAIR(C_UND + 1 + icon.color));
   4779 	else if (attrs)
   4780 		attroff(attrs);
   4781 	addstr(ICON_PADDING_RIGHT);
   4782 }
   4783 #endif
   4784 
   4785 static void print_time(const time_t *timep, const uchar_t flags)
   4786 {
   4787 	struct tm t;
   4788 
   4789 	/* Highlight timestamp for entries 5 minutes young */
   4790 	if (flags & FILE_YOUNG)
   4791 		attron(A_REVERSE);
   4792 
   4793 	localtime_r(timep, &t);
   4794 	printw("%s-%02d-%02d %02d:%02d",
   4795 		xitoa(t.tm_year + 1900), t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min);
   4796 
   4797 	if (flags & FILE_YOUNG)
   4798 		attroff(A_REVERSE);
   4799 }
   4800 
   4801 static char get_detail_ind(const mode_t mode)
   4802 {
   4803 	switch (mode & S_IFMT) {
   4804 	case S_IFDIR:  // fallthrough
   4805 	case S_IFREG:  return ' ';
   4806 	case S_IFLNK:  return '@';
   4807 	case S_IFSOCK: return '=';
   4808 	case S_IFIFO:  return '|';
   4809 	case S_IFBLK:  return 'b';
   4810 	case S_IFCHR:  return 'c';
   4811 	}
   4812 	return '?';
   4813 }
   4814 
   4815 /* Note: attribute and indicator values must be initialized to 0 */
   4816 static uchar_t get_color_pair_name_ind(const struct entry *ent, char *pind, int *pattr)
   4817 {
   4818 	switch (ent->mode & S_IFMT) {
   4819 	case S_IFREG:
   4820 		if (!ent->size) {
   4821 			if (ent->mode & 0100)
   4822 				*pind = '*';
   4823 			return C_UND;
   4824 		}
   4825 		if (ent->flags & HARD_LINK) {
   4826 			if (ent->mode & 0100)
   4827 				*pind = '*';
   4828 			return C_HRD;
   4829 		}
   4830 		if (ent->mode & 0100) {
   4831 			*pind = '*';
   4832 			return C_EXE;
   4833 		}
   4834 		return C_FIL;
   4835 	case S_IFDIR:
   4836 		*pind = '/';
   4837 		if (g_state.oldcolor)
   4838 			return C_DIR;
   4839 		*pattr |= A_BOLD;
   4840 		return g_state.dirctx ? cfg.curctx + 1 : C_DIR;
   4841 	case S_IFLNK:
   4842 		if (ent->flags & DIR_OR_DIRLNK) {
   4843 			*pind = '/';
   4844 			*pattr |= g_state.oldcolor ? A_DIM : A_BOLD;
   4845 		} else {
   4846 			*pind = '@';
   4847 			if (g_state.oldcolor)
   4848 				*pattr |= A_DIM;
   4849 		}
   4850 		if (!g_state.oldcolor || cfg.showdetail)
   4851 			return (ent->flags & SYM_ORPHAN) ? C_ORP : C_LNK;
   4852 		return 0;
   4853 	case S_IFSOCK:
   4854 		*pind = '=';
   4855 		return C_SOC;
   4856 	case S_IFIFO:
   4857 		*pind = '|';
   4858 		return C_PIP;
   4859 	case S_IFBLK:
   4860 		return C_BLK;
   4861 	case S_IFCHR:
   4862 		return C_CHR;
   4863 	}
   4864 
   4865 	*pind = '?';
   4866 	return C_UND;
   4867 }
   4868 
   4869 #ifdef DIM_FILTERED
   4870 static void printent_name(const struct entry *ent, uint_t namecols)
   4871 {
   4872 	char * const fltr = g_ctx[cfg.curctx].c_fltr;
   4873 
   4874 	/* If there's a filter string, dim matching characters */
   4875 	if (fltr[1]) {
   4876 		uchar_t matched[NAME_MAX] = {0};
   4877 		int match_attrs = A_DIM;
   4878 
   4879 		/* Get match positions based on filter type */
   4880 		if (cfg.regex) {
   4881 			/* For regex, we don't dim - just print normally */
   4882 #ifndef NOLC
   4883 			addwstr(unescape(ent->name, namecols));
   4884 #else
   4885 			addstr(unescape(ent->name, MIN(namecols, ent->nlen) + 1));
   4886 #endif
   4887 		} else if (cfg.fuzzy) {
   4888 			/* Get fuzzy match positions */
   4889 			fuzzy_match_positions(fltr + 1, ent->name, matched);
   4890 #ifndef NOLC
   4891 			wchar_t * const wbuf = unescape(ent->name, namecols);
   4892 			uint_t col = 0;
   4893 			for (wchar_t *p = wbuf; *p && col < namecols; ++p, ++col) {
   4894 				if (matched[col]) {
   4895 					attron(match_attrs);
   4896 					addnwstr(p, 1);
   4897 					attroff(match_attrs);
   4898 				} else {
   4899 					addnwstr(p, 1);
   4900 				}
   4901 			}
   4902 #else
   4903 			/* Non-wide character version for fuzzy dimming */
   4904 			const char *name = unescape(ent->name, MIN(namecols, ent->nlen) + 1);
   4905 			for (uint_t i = 0; (i < namecols) && name[i]; ++i) {
   4906 				if (matched[i]) {
   4907 					attron(match_attrs);
   4908 					addch(name[i]);
   4909 					attroff(match_attrs);
   4910 				} else {
   4911 					addch(name[i]);
   4912 				}
   4913 			}
   4914 #endif
   4915 		} else {
   4916 			/* String match - dim the substring */
   4917 			string_match_positions(fltr + 1, ent->name, matched);
   4918 #ifndef NOLC
   4919 			wchar_t * const wbuf = unescape(ent->name, namecols);
   4920 			uint_t col = 0;
   4921 			for (wchar_t *p = wbuf; *p && col < namecols; ++p, ++col) {
   4922 				if (matched[col]) {
   4923 					attron(match_attrs);
   4924 					addnwstr(p, 1);
   4925 					attroff(match_attrs);
   4926 				} else {
   4927 					addnwstr(p, 1);
   4928 				}
   4929 			}
   4930 #else
   4931 			/* Non-wide character version for string dimming */
   4932 			const char *name = unescape(ent->name, MIN(namecols, ent->nlen) + 1);
   4933 			for (uint_t i = 0; (i < namecols) && name[i]; ++i) {
   4934 				if (matched[i]) {
   4935 					attron(match_attrs);
   4936 					addch(name[i]);
   4937 					attroff(match_attrs);
   4938 				} else {
   4939 					addch(name[i]);
   4940 				}
   4941 			}
   4942 #endif
   4943 		}
   4944 	} else {
   4945 		/* No filter or filter not active - print normally */
   4946 #ifndef NOLC
   4947 		addwstr(unescape(ent->name, namecols));
   4948 #else
   4949 		addstr(unescape(ent->name, MIN(namecols, ent->nlen) + 1));
   4950 #endif
   4951 	}
   4952 }
   4953 #else
   4954 /* Without dimming support, just print the name normally */
   4955 static inline void printent_name(const struct entry *ent, uint_t namecols)
   4956 {
   4957 #ifndef NOLC
   4958 	addwstr(unescape(ent->name, namecols));
   4959 #else
   4960 	addstr(unescape(ent->name, MIN(namecols, ent->nlen) + 1));
   4961 #endif
   4962 }
   4963 #endif
   4964 
   4965 static void printent(int pdents_index, uint_t namecols, bool sel)
   4966 {
   4967 	const struct entry *ent = &pdents[pdents_index];
   4968 	char ind = '\0';
   4969 	int attrs;
   4970 
   4971 	if (cfg.showdetail) {
   4972 		int type = ent->mode & S_IFMT;
   4973 		char perms[6] = {' ', ' ', (char)('0' + ((ent->mode >> 6) & 7)),
   4974 				(char)('0' + ((ent->mode >> 3) & 7)),
   4975 				(char)('0' + (ent->mode & 7)), '\0'};
   4976 
   4977 		addch(' ');
   4978 		attrs = g_state.oldcolor ? (resetdircolor(ent->flags), A_DIM)
   4979 					 : (fcolors[C_MIS] ? COLOR_PAIR(C_MIS) : 0);
   4980 		if (attrs)
   4981 			attron(attrs);
   4982 
   4983 		/* Print details */
   4984 		print_time(&ent->sec, ent->flags);
   4985 
   4986 		printw("%s%9s ", perms, (type == S_IFREG || type == S_IFDIR)
   4987 			? coolsize(cfg.blkorder ? (blkcnt_t)ent->blocks << blk_shift : ent->size)
   4988 			: (type = (uchar_t)get_detail_ind(ent->mode), (char *)&type));
   4989 
   4990 		if (attrs)
   4991 			attroff(attrs);
   4992 	}
   4993 
   4994 	if (g_state.showlines) {
   4995 		ptrdiff_t rel_num = pdents_index - cur;
   4996 		printw(rel_num == 0 ? "%4td" : "%+4td", rel_num);
   4997 	}
   4998 
   4999 	attrs = 0;
   5000 
   5001 	uchar_t color_pair = get_color_pair_name_ind(ent, &ind, &attrs);
   5002 
   5003 	addch((ent->flags & FILE_SELECTED) ? '+' | A_REVERSE | A_BOLD : ' ');
   5004 
   5005 	if (g_state.oldcolor)
   5006 		resetdircolor(ent->flags);
   5007 	else {
   5008 		if (ent->flags & FILE_MISSING)
   5009 			color_pair = C_MIS;
   5010 		if (color_pair && fcolors[color_pair])
   5011 			attrs |= COLOR_PAIR(color_pair);
   5012 #ifdef ICONS_ENABLED
   5013 		print_icon(ent, attrs);
   5014 #endif
   5015 	}
   5016 
   5017 	if (sel)
   5018 		attrs |= A_REVERSE | A_BOLD;
   5019 	if (attrs)
   5020 		attron(attrs);
   5021 	if (!ind)
   5022 		++namecols;
   5023 
   5024 	printent_name(ent, namecols);
   5025 
   5026 	if (attrs)
   5027 		attroff(attrs);
   5028 	if (ind)
   5029 		addch(ind);
   5030 }
   5031 
   5032 /**
   5033  * Sets the global cfg variable and restores related state to match the new
   5034  * cfg.
   5035  */
   5036 static void setcfg(settings newcfg)
   5037 {
   5038 	cfg = newcfg;
   5039 	/* Synchronize the global function pointers to match the new cfg. */
   5040 	entrycmpfn = cfg.reverse ? &reventrycmp : &entrycmp;
   5041 	namecmpfn = cfg.version ? &xstrverscasecmp : &xstricmp;
   5042 	filterfn = cfg.regex ? &visible_re : (cfg.fuzzy ? &visible_fuzzy : &visible_str);
   5043 }
   5044 
   5045 static void savecurctx(char *path, char *curname, int nextctx)
   5046 {
   5047 	settings tmpcfg = cfg;
   5048 	context *ctxr = &g_ctx[nextctx];
   5049 
   5050 	/* Save current context */
   5051 	if (curname)
   5052 		xstrsncpy(g_ctx[tmpcfg.curctx].c_name, curname, NAME_MAX + 1);
   5053 	else
   5054 		g_ctx[tmpcfg.curctx].c_name[0] = '\0';
   5055 
   5056 	g_ctx[tmpcfg.curctx].c_cfg = tmpcfg;
   5057 
   5058 	if (ctxr->c_cfg.ctxactive) { /* Switch to saved context */
   5059 		tmpcfg = ctxr->c_cfg;
   5060 		/* Skip ordering an open context */
   5061 		if (order) {
   5062 			cfgsort[CTX_MAX] = cfgsort[nextctx];
   5063 			cfgsort[nextctx] = '0';
   5064 		}
   5065 	} else { /* Set up a new context from current context */
   5066 		ctxr->c_cfg.ctxactive = 1;
   5067 		xstrsncpy(ctxr->c_path, path, PATH_MAX);
   5068 		ctxr->c_last[0] = ctxr->c_name[0] = ctxr->c_fltr[0] = ctxr->c_fltr[1] = '\0';
   5069 		ctxr->c_cfg = tmpcfg;
   5070 		/* If already in an ordered dir, clear ordering for the new context and let it order */
   5071 		if (cfgsort[cfg.curctx] == 'z')
   5072 			cfgsort[nextctx] = 'z';
   5073 	}
   5074 
   5075 	tmpcfg.curctx = nextctx;
   5076 	setcfg(tmpcfg);
   5077 }
   5078 
   5079 #ifndef NOSSN
   5080 static void save_session(const char *sname, int *presel)
   5081 {
   5082 	int fd, i;
   5083 	session_header_t header = {0};
   5084 	bool status = FALSE;
   5085 	char ssnpath[PATH_MAX];
   5086 	char spath[PATH_MAX];
   5087 
   5088 	header.ver = SESSIONS_VERSION;
   5089 
   5090 	for (i = 0; i < CTX_MAX; ++i) {
   5091 		if (g_ctx[i].c_cfg.ctxactive) {
   5092 			if (cfg.curctx == i && ndents)
   5093 				/* Update current file name, arrows don't update it */
   5094 				xstrsncpy(g_ctx[i].c_name, pdents[cur].name, NAME_MAX + 1);
   5095 			header.pathln[i] = MIN(xstrlen(g_ctx[i].c_path), PATH_MAX) + 1;
   5096 			header.lastln[i] = MIN(xstrlen(g_ctx[i].c_last), PATH_MAX) + 1;
   5097 			header.nameln[i] = MIN(xstrlen(g_ctx[i].c_name), NAME_MAX) + 1;
   5098 			header.fltrln[i] = REGEX_MAX;
   5099 		}
   5100 	}
   5101 
   5102 	mkpath(cfgpath, toks[TOK_SSN], ssnpath);
   5103 	mkpath(ssnpath, sname, spath);
   5104 
   5105 	fd = open(spath, O_CREAT | O_WRONLY | O_TRUNC, S_IWUSR | S_IRUSR);
   5106 	if (fd == -1) {
   5107 		printwait(messages[MSG_SEL_MISSING], presel);
   5108 		return;
   5109 	}
   5110 
   5111 	if ((write(fd, &header, sizeof(header)) != (ssize_t)sizeof(header))
   5112 		|| (write(fd, &cfg, sizeof(cfg)) != (ssize_t)sizeof(cfg)))
   5113 		goto END;
   5114 
   5115 	for (i = 0; i < CTX_MAX; ++i)
   5116 		if ((write(fd, &g_ctx[i].c_cfg, sizeof(settings)) != (ssize_t)sizeof(settings))
   5117 			|| (write(fd, &g_ctx[i].color, sizeof(uint_t)) != (ssize_t)sizeof(uint_t))
   5118 			|| (header.nameln[i] > 0
   5119 			    && write(fd, g_ctx[i].c_name, header.nameln[i]) != (ssize_t)header.nameln[i])
   5120 			|| (header.lastln[i] > 0
   5121 			    && write(fd, g_ctx[i].c_last, header.lastln[i]) != (ssize_t)header.lastln[i])
   5122 			|| (header.fltrln[i] > 0
   5123 			    && write(fd, g_ctx[i].c_fltr, header.fltrln[i]) != (ssize_t)header.fltrln[i])
   5124 			|| (header.pathln[i] > 0
   5125 			    && write(fd, g_ctx[i].c_path, header.pathln[i]) != (ssize_t)header.pathln[i]))
   5126 			goto END;
   5127 
   5128 	status = TRUE;
   5129 
   5130 END:
   5131 	close(fd);
   5132 
   5133 	if (!status)
   5134 		printwait(messages[MSG_FAILED], presel);
   5135 }
   5136 
   5137 static bool load_session(const char *sname, char **path, char **lastdir, char **lastname, bool restore)
   5138 {
   5139 	int fd, i = 0;
   5140 	session_header_t header;
   5141 	bool has_loaded_dynamically = !(sname || restore);
   5142 	bool status = (sname && g_state.picker); /* Picker mode with session program option */
   5143 	char ssnpath[PATH_MAX];
   5144 	char spath[PATH_MAX];
   5145 
   5146 	mkpath(cfgpath, toks[TOK_SSN], ssnpath);
   5147 
   5148 	if (!restore) {
   5149 		if (!sname || !sname[0])
   5150 			sname = xreadline(NULL, messages[MSG_SSN_NAME]);
   5151 		if (!sname[0])
   5152 			return FALSE;
   5153 
   5154 		mkpath(ssnpath, sname, spath);
   5155 
   5156 		/* If user is explicitly loading the "last session", skip auto-save */
   5157 		if ((sname[0] == '@') && !sname[1])
   5158 			has_loaded_dynamically = FALSE;
   5159 	} else
   5160 		mkpath(ssnpath, "@", spath);
   5161 
   5162 	if (has_loaded_dynamically)
   5163 		save_session("@", NULL);
   5164 
   5165 	fd = open(spath, O_RDONLY, S_IWUSR | S_IRUSR);
   5166 	if (fd == -1) {
   5167 		if (!status) {
   5168 			printmsg(messages[MSG_SEL_MISSING]);
   5169 			xdelay(XDELAY_INTERVAL_MS);
   5170 		}
   5171 		return FALSE;
   5172 	}
   5173 
   5174 	status = FALSE;
   5175 
   5176 	if ((read(fd, &header, sizeof(header)) != (ssize_t)sizeof(header))
   5177 		|| (header.ver != SESSIONS_VERSION)
   5178 		|| (read(fd, &cfg, sizeof(cfg)) != (ssize_t)sizeof(cfg)))
   5179 		goto END;
   5180 
   5181 	g_ctx[cfg.curctx].c_name[0] = g_ctx[cfg.curctx].c_last[0]
   5182 		= g_ctx[cfg.curctx].c_fltr[0] = g_ctx[cfg.curctx].c_fltr[1] = '\0';
   5183 
   5184 	for (; i < CTX_MAX; ++i)
   5185 		if ((read(fd, &g_ctx[i].c_cfg, sizeof(settings)) != (ssize_t)sizeof(settings))
   5186 			|| (read(fd, &g_ctx[i].color, sizeof(uint_t)) != (ssize_t)sizeof(uint_t))
   5187 			|| (header.nameln[i] > 0
   5188 			    && read(fd, g_ctx[i].c_name, header.nameln[i]) != (ssize_t)header.nameln[i])
   5189 			|| (header.lastln[i] > 0
   5190 			    && read(fd, g_ctx[i].c_last, header.lastln[i]) != (ssize_t)header.lastln[i])
   5191 			|| (header.fltrln[i] > 0
   5192 			    && read(fd, g_ctx[i].c_fltr, header.fltrln[i]) != (ssize_t)header.fltrln[i])
   5193 			|| (header.pathln[i] > 0
   5194 			    && read(fd, g_ctx[i].c_path, header.pathln[i]) != (ssize_t)header.pathln[i]))
   5195 			goto END;
   5196 
   5197 	*path = g_ctx[cfg.curctx].c_path;
   5198 	*lastdir = g_ctx[cfg.curctx].c_last;
   5199 	*lastname = g_ctx[cfg.curctx].c_name;
   5200 	/* Set correct sort and filter options */
   5201 	set_sort_flags('\0');
   5202 	filterfn = cfg.regex ? &visible_re : (cfg.fuzzy ? &visible_fuzzy : &visible_str);
   5203 	xstrsncpy(curssn, sname ? sname : "@", NAME_MAX);
   5204 	status = TRUE;
   5205 
   5206 END:
   5207 	close(fd);
   5208 
   5209 	if (!status) {
   5210 		printmsg(messages[MSG_FAILED]);
   5211 		xdelay(XDELAY_INTERVAL_MS);
   5212 	} else if (restore)
   5213 		unlink(spath);
   5214 
   5215 	return status;
   5216 }
   5217 #endif
   5218 
   5219 static uchar_t get_free_ctx(void)
   5220 {
   5221 	uchar_t r = cfg.curctx;
   5222 
   5223 	do
   5224 		r = (r + 1) & ~CTX_MAX;
   5225 	while (g_ctx[r].c_cfg.ctxactive && (r != cfg.curctx));
   5226 
   5227 	return r;
   5228 }
   5229 
   5230 /* ctx is absolute: 1 to 4, + for smart context */
   5231 static void set_smart_ctx(int ctx, char *nextpath, char **path, char *file, char **lastname, char **lastdir)
   5232 {
   5233 	if (ctx == '+') /* Get smart context */
   5234 		ctx = (int)(get_free_ctx() + 1);
   5235 
   5236 	if (ctx == 0 || ctx == cfg.curctx + 1) { /* Same context */
   5237 		clearfilter();
   5238 		xstrsncpy(*lastdir, *path, PATH_MAX);
   5239 		xstrsncpy(*path, nextpath, PATH_MAX);
   5240 	} else { /* New context */
   5241 		--ctx;
   5242 		/* Deactivate the new context and build from scratch */
   5243 		g_ctx[ctx].c_cfg.ctxactive = 0;
   5244 		DPRINTF_S(nextpath);
   5245 		savecurctx(nextpath, file, ctx);
   5246 		*path = g_ctx[ctx].c_path;
   5247 		*lastdir = g_ctx[ctx].c_last;
   5248 		*lastname = g_ctx[ctx].c_name;
   5249 	}
   5250 }
   5251 
   5252 static bool handle_cur_move(enum action sel)
   5253 {
   5254 	bool ret = TRUE;
   5255 
   5256 	if (sel == SEL_NEXT) {
   5257 		if (cfg.rollover || (cur != ndents - 1))
   5258 			move_cursor((cur + 1) % ndents, 0);
   5259 	} else if (sel == SEL_PREV) {
   5260 		if (cfg.rollover || cur)
   5261 			move_cursor((cur + ndents - 1) % ndents, 0);
   5262 	} else
   5263 		ret = FALSE;
   5264 
   5265 	return ret;
   5266 }
   5267 
   5268 /*
   5269  * This function does one of the following depending on the values of `fdout` and `page`:
   5270  *  1) fdout == -1 && !page: Write up to CMD_LEN_MAX bytes of command output into g_buf
   5271  *  2) fdout == -1 && page: Create a temp file, write full command output into it and show in pager.
   5272  *  3) fdout != -1 && !page: Write full command output into fdout.
   5273  *  4) fdout != -1 && page: Don't use! Returns FALSE.
   5274  *
   5275  * g_buf is modified only in case 1.
   5276  * g_tmpfpath is modified only in case 2.
   5277  */
   5278 static bool get_output(char *command, char *arg1, char *arg2, int fdout, bool page)
   5279 {
   5280 	pid_t pid;
   5281 	int pipefd[2];
   5282 	int index = 0, flags;
   5283 	bool ret = FALSE;
   5284 	bool have_file = fdout != -1;
   5285 	int cmd_in_fd = -1;
   5286 	int cmd_out_fd = -1;
   5287 
   5288 	if (!command || !*command)
   5289 		return ret;
   5290 
   5291 	/*
   5292 	 * In this case the logic of the function dictates that we should write the output of the command
   5293 	 * to `fd` and show it in the pager. But since we didn't open the file descriptor we have no right
   5294 	 * to close it, the caller must do it. We don't even know the path to pass to the pager and
   5295 	 * it's a real hassle to get it. In general this just invites problems so we are blocking it.
   5296 	 */
   5297 	if (have_file && page) {
   5298 		DPRINTF_S("invalid get_ouptput() call");
   5299 		return ret;
   5300 	}
   5301 
   5302 	/* Setup file descriptors for child command */
   5303 	if (page) {
   5304 		// Case 2
   5305 		fdout = create_tmp_file();
   5306 		if (fdout == -1)
   5307 			return ret;
   5308 
   5309 		cmd_in_fd = STDIN_FILENO;
   5310 		cmd_out_fd = fdout;
   5311 	} else if (have_file) {
   5312 		// Case 3
   5313 		cmd_in_fd = STDIN_FILENO;
   5314 		cmd_out_fd = fdout;
   5315 	} else {
   5316 		// Case 1
   5317 		if (pipe(pipefd) == -1)
   5318 			errexit();
   5319 
   5320 		for (index = 0; index < 2; ++index) {
   5321 			/* Get previous flags */
   5322 			flags = fcntl(pipefd[index], F_GETFL, 0);
   5323 
   5324 			/* Set bit for non-blocking flag */
   5325 			flags |= O_NONBLOCK;
   5326 
   5327 			/* Change flags on fd */
   5328 			fcntl(pipefd[index], F_SETFL, flags);
   5329 		}
   5330 
   5331 		cmd_in_fd = pipefd[0];
   5332 		cmd_out_fd = pipefd[1];
   5333 	}
   5334 
   5335 	/* Tokenize the command into an array */
   5336 	char *argv[EXEC_ARGS_MAX] = {0};
   5337 	char *cmd = parseargs(command, argv, &index);
   5338 	if (!cmd)
   5339 		return ret;
   5340 
   5341 	if (arg1)
   5342 		argv[index++] = arg1;
   5343 
   5344 	if (arg2)
   5345 		argv[index] = arg2;
   5346 
   5347 	pid = fork();
   5348 	if (pid == 0) {
   5349 		/* In child */
   5350 		close(cmd_in_fd);
   5351 		dup2(cmd_out_fd, STDOUT_FILENO);
   5352 		dup2(cmd_out_fd, STDERR_FILENO);
   5353 		close(cmd_out_fd);
   5354 
   5355 		execvp(*argv, argv);
   5356 		_exit(EXIT_SUCCESS);
   5357 	}
   5358 
   5359 	/* In parent */
   5360 	waitpid(pid, NULL, 0);
   5361 
   5362 	free(cmd);
   5363 
   5364 	/* Do what each case should do */
   5365 	if (page) {
   5366 		// Case 2
   5367 		close(fdout);
   5368 
   5369 		spawn(pager, g_tmpfpath, NULL, NULL, F_CLI | F_TTY);
   5370 
   5371 		unlink(g_tmpfpath);
   5372 		return TRUE;
   5373 	}
   5374 
   5375 	if (have_file)
   5376 		return TRUE; // Case 3
   5377 
   5378 	// Case 1
   5379 	if (read(pipefd[0], g_buf, CMD_LEN_MAX - 1) > 0)
   5380 		ret = TRUE;
   5381 
   5382 	close(pipefd[0]);
   5383 	close(pipefd[1]);
   5384 	return ret;
   5385 }
   5386 
   5387 /*
   5388  * Helper function to run numcmds commands and capture the output in a content buffer
   5389  */
   5390 static bool buffer_command_output(char * const cmds[], char *arg1, char *arg2, size_t numcmds, char **content_out, size_t *content_len_out)
   5391 {
   5392 	int pipefd[2];
   5393 	if (pipe(pipefd) == -1)
   5394 		return FALSE;
   5395 
   5396 	/* Execute all commands sequentially, writing to pipe */
   5397 	for (size_t i = numcmds; i > 0; --i) {
   5398 		char *command = cmds[i - 1];
   5399 		if (!command || !*command)
   5400 			continue;
   5401 
   5402 		char *argv[EXEC_ARGS_MAX] = {0};
   5403 		int index = 0;
   5404 		char *cmd = parseargs(command, argv, &index);
   5405 		if (!cmd)
   5406 			continue;
   5407 
   5408 		if (arg1)
   5409 			argv[index++] = arg1;
   5410 		if (arg2)
   5411 			argv[index] = arg2;
   5412 
   5413 		pid_t pid = fork();
   5414 		if (pid == 0) {
   5415 			close(pipefd[0]);
   5416 			dup2(pipefd[1], STDOUT_FILENO);
   5417 			dup2(pipefd[1], STDERR_FILENO);
   5418 			close(pipefd[1]);
   5419 			execvp(*argv, argv);
   5420 			_exit(EXIT_SUCCESS);
   5421 		}
   5422 
   5423 		if (pid > 0)
   5424 			waitpid(pid, NULL, 0);
   5425 		free(cmd);
   5426 	}
   5427 
   5428 	close(pipefd[1]);
   5429 
   5430 	/* Read directly from pipe into buffer */
   5431 	size_t content_size = (size_t) SIZE_8KB;
   5432 	size_t content_len = 0;
   5433 	char read_buf[SIZE_8KB];
   5434 
   5435 	char *content = malloc(content_size);
   5436 	if (!content) {
   5437 		close(pipefd[0]);
   5438 		return FALSE;
   5439 	}
   5440 
   5441 	while (content_len < (size_t) SIZE_16MB) {
   5442 		ssize_t nread = read(pipefd[0], read_buf, sizeof(read_buf));
   5443 		if (nread <= 0)
   5444 			break;
   5445 
   5446 		/* Grow buffer if needed (always reserve space for null terminator) */
   5447 		if (content_len + nread + 1 > content_size) {
   5448 			size_t new_size = content_size;
   5449 			do {
   5450 				new_size *= 2;
   5451 			} while (new_size < content_len + nread + 1);
   5452 
   5453 			char *new_content = realloc(content, new_size);
   5454 
   5455 			if (!new_content) {
   5456 				free(content);
   5457 				close(pipefd[0]);
   5458 				return FALSE;
   5459 			}
   5460 			content = new_content;
   5461 			content_size = new_size;
   5462 		}
   5463 
   5464 		memcpy(content + content_len, read_buf, nread);
   5465 		content_len += nread;
   5466 	}
   5467 
   5468 	close(pipefd[0]);
   5469 
   5470 	if (content_len == 0) {
   5471 		free(content);
   5472 		return FALSE;
   5473 	}
   5474 
   5475 	content[content_len] = '\0'; // NOLINT
   5476 	*content_out = content;
   5477 	*content_len_out = content_len;
   5478 	return TRUE;
   5479 }
   5480 
   5481 /*
   5482  * Shows the content of a buffer in a floating window.
   5483  * Helps with navigating the entries in the directory.
   5484  */
   5485 static bool show_content_in_floating_window(char *content, size_t content_len, enum action *action, bool perfile)
   5486 {
   5487 	/* Calculate window dimensions */
   5488 	int win_height = MIN(20, xlines - 4);
   5489 	int win_width = (xcols * 3) / 4; /* 75% of terminal width */
   5490 
   5491 	/* Ensure minimum window size */
   5492 	if (win_height < 5)
   5493 		win_height = 5;
   5494 	if (win_width < 20)
   5495 		win_width = 20;
   5496 
   5497 	int start_y = (xlines - win_height) / 2;
   5498 	int start_x = (xcols - win_width) / 2;
   5499 
   5500 	/* Create floating window */
   5501 	WINDOW *win = newwin(win_height, win_width, start_y, start_x);
   5502 	if (!win)
   5503 		return FALSE;
   5504 
   5505 	keypad(win, TRUE); /* Enable special keys */
   5506 
   5507 	/* Count total lines once */
   5508 	int line_count = 0;
   5509 	for (size_t i = 0; i < content_len; i++) {
   5510 		if (content[i] == '\n')
   5511 			line_count++;
   5512 	}
   5513 	if (content_len > 0 && content[content_len - 1] != '\n')
   5514 		line_count++;
   5515 
   5516 	/* Display content with scrolling */
   5517 	int max_lines = win_height - 3; /* Account for border and help line */
   5518 	int max_display_width = win_width - 2; /* Account for border */
   5519 	int scroll_offset = 0;
   5520 	int hscroll_offset = 0; /* Horizontal scroll offset */
   5521 	int max_line_width = 0; /* Maximum line width in content */
   5522 	int ret, x;
   5523 	wint_t ch;
   5524 	bool done = FALSE;
   5525 
   5526 	/* Calculate maximum line width */
   5527 	char *line_ptr = content;
   5528 	while (line_ptr < content + content_len) {
   5529 		char *next = strchr(line_ptr, '\n');
   5530 		size_t line_len = next ? (size_t)(next - line_ptr) : content_len - (line_ptr - content);
   5531 		if (line_len > (size_t)max_line_width)
   5532 			max_line_width = (int)line_len;
   5533 		if (next)
   5534 			line_ptr = next + 1;
   5535 		else
   5536 			break;
   5537 	}
   5538 
   5539 	while (!done) {
   5540 		werase(win);
   5541 		box(win, 0, 0);
   5542 
   5543 		if (perfile && ndents)
   5544 		{
   5545 			mvwaddch(win, 0, 2, ' ');
   5546 			waddstr(win, xitoa(cur + 1));
   5547 			waddch(win, '/');
   5548 			waddstr(win, xitoa(ndents));
   5549 			waddch(win, ' ');
   5550 			getyx(win, ret, x); // ret is used as a dummy
   5551 			wattron(win, A_BOLD);
   5552 			waddnstr(win, pdents[cur].name, max_display_width - x - 1);
   5553 			wattroff(win, A_BOLD);
   5554 			waddch(win, ' ');
   5555 		}
   5556 
   5557 		/* Skip to scroll offset */
   5558 		char *line_start = content;
   5559 		for (int i = 0; i < scroll_offset && line_start < content + content_len; i++) {
   5560 			char *next = strchr(line_start, '\n');
   5561 			if (!next)
   5562 				break;
   5563 			line_start = next + 1;
   5564 		}
   5565 
   5566 		/* Display visible lines */
   5567 		char *current = line_start;
   5568 		int display_line = 2; // Add a blank line, if possible (long file names can be multiline
   5569 
   5570 		while (display_line <= max_lines && current < content + content_len) {
   5571 			char *next = strchr(current, '\n');
   5572 			size_t line_len = next ? (size_t)(next - current) : content_len - (current - content);
   5573 
   5574 			/* Apply horizontal scrolling */
   5575 			char *display_start = current;
   5576 			size_t display_len = line_len;
   5577 
   5578 			if (hscroll_offset > 0 && (size_t)hscroll_offset < line_len) {
   5579 				display_start = current + hscroll_offset;
   5580 				display_len = line_len - hscroll_offset;
   5581 			}
   5582 
   5583 			/* Truncate if still too long for display */
   5584 			if (display_len > (size_t)max_display_width) {
   5585 				mvwaddnstr(win, display_line, 1, display_start, max_display_width);
   5586 			} else {
   5587 				mvwaddnstr(win, display_line, 1, display_start, display_len);
   5588 			}
   5589 
   5590 			if (next) {
   5591 				current = next + 1;
   5592 				display_line++;
   5593 			} else {
   5594 				break;
   5595 			}
   5596 		}
   5597 
   5598 		/* Show vertical scroll indicators if needed */
   5599 		if (line_count > max_lines) {
   5600 			if (scroll_offset > 0)
   5601 				mvwaddch(win, 1, win_width - 2, '^');
   5602 			if (scroll_offset + max_lines < line_count)
   5603 				mvwaddch(win, max_lines, win_width - 2, 'v');
   5604 		}
   5605 
   5606 		/* Show horizontal scroll indicators if needed */
   5607 		if (max_line_width > max_display_width) {
   5608 			if (hscroll_offset > 0)
   5609 				mvwaddch(win, max_lines + 1, 1, '<');
   5610 			if (hscroll_offset + max_display_width < max_line_width)
   5611 				mvwaddch(win, max_lines + 1, win_width - 2, '>');
   5612 		}
   5613 
   5614 		/* Show help hint */
   5615 		mvwaddstr(win, win_height - 1, 1, "q/ESC:close  arrows:scroll");
   5616 		if (perfile)
   5617 			waddstr(win, "  n/j:next  p/k:prev");
   5618 
   5619 		wrefresh(win);
   5620 
   5621 		/* Get user input from window */
   5622 		ret = get_wch(&ch);
   5623 		if (ret == ERR)
   5624 			continue;
   5625 
   5626 		switch (ch) {
   5627 		case 'q':
   5628 		case ESC:
   5629 			done = TRUE;
   5630 			break;
   5631 		case 'j':
   5632 		case 'n':
   5633 			if (perfile && action) {
   5634 				*action = SEL_NEXT;
   5635 				done = TRUE;
   5636 			}
   5637 			break;
   5638 		case 'k':
   5639 		case 'p':
   5640 			if (perfile && action) {
   5641 				*action = SEL_PREV;
   5642 				done = TRUE;
   5643 			}
   5644 			break;
   5645 		case KEY_UP:
   5646 			if (scroll_offset > 0)
   5647 				scroll_offset--;
   5648 			break;
   5649 		case KEY_DOWN:
   5650 			if (scroll_offset + max_lines < line_count)
   5651 				scroll_offset++;
   5652 			break;
   5653 		case KEY_PPAGE:
   5654 			scroll_offset = MAX(0, scroll_offset - max_lines);
   5655 			break;
   5656 		case KEY_NPAGE:
   5657 			scroll_offset = MIN(line_count - max_lines, scroll_offset + max_lines);
   5658 			break;
   5659 		case KEY_HOME:
   5660 			scroll_offset = 0;
   5661 			break;
   5662 		case KEY_END:
   5663 			scroll_offset = MAX(0, line_count - max_lines);
   5664 			break;
   5665 		case KEY_LEFT:
   5666 			if (hscroll_offset > 0)
   5667 				hscroll_offset = MAX(0, hscroll_offset - 10);
   5668 			break;
   5669 		case KEY_RIGHT:
   5670 			if (max_line_width > max_display_width)
   5671 				hscroll_offset = MIN(max_line_width - max_display_width, hscroll_offset + 10);
   5672 			break;
   5673 #ifdef KEY_RESIZE
   5674 		case KEY_RESIZE:
   5675 			done = TRUE;
   5676 			break;
   5677 #endif
   5678 		}
   5679 	}
   5680 
   5681 	delwin(win);
   5682 	refresh(); /* Refresh main screen */
   5683 	return TRUE;
   5684 }
   5685 
   5686 /*
   5687  * Follows the stat(1) output closely
   5688  * Displays output in a floating ncurses window
   5689  */
   5690 static bool show_stats(char *pathbuf, char *dir)
   5691 {
   5692 	char *content = NULL;
   5693 	size_t content_len = 0;
   5694 	enum action action;
   5695 	bool ret;
   5696 
   5697 	char * const cmds[] = {
   5698 #ifdef FILE_MIME_OPTS
   5699 		("file " FILE_MIME_OPTS),
   5700 #endif
   5701 		"file -b",
   5702 #if defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__) || defined(__DragonFly__)
   5703 		"stat -x",
   5704 #else
   5705 		"stat",
   5706 #endif
   5707 	};
   5708 
   5709 	do {
   5710 		mkpath(dir, pdents[cur].name, pathbuf);
   5711 
   5712 		if (!buffer_command_output(cmds, pathbuf, NULL, ELEMENTS(cmds), &content, &content_len)) {
   5713 			ret = FALSE;
   5714 			break;
   5715 		}
   5716 
   5717 		action = SEL_MAX;
   5718 
   5719 		ret = show_content_in_floating_window(content, content_len, &action, TRUE);
   5720 
   5721 		free(content);
   5722 		content = NULL;
   5723 		content_len = 0;
   5724 
   5725 		if (!ret)
   5726 			break;
   5727 	} while (handle_cur_move(action));
   5728 
   5729 	return ret;
   5730 }
   5731 
   5732 static bool xchmod(char *pathbuf, char *dir)
   5733 {
   5734 	struct stat sb;
   5735 	mode_t mode;
   5736 	int fd;
   5737 
   5738 	mkpath(dir, pdents[cur].name, pathbuf);
   5739 
   5740 #ifdef O_NOFOLLOW
   5741 	fd = open(pathbuf, O_RDONLY | O_NOFOLLOW);
   5742 #else
   5743 	fd = open(pathbuf, O_RDONLY);
   5744 #endif
   5745 	if (fd == -1)
   5746 		return FALSE;
   5747 
   5748 	if (fstat(fd, &sb) == -1) {
   5749 		close(fd);
   5750 		return FALSE;
   5751 	}
   5752 
   5753 	mode = sb.st_mode;
   5754 
   5755 	/* (Un)set (S_IXUSR | S_IXGRP | S_IXOTH) */
   5756 	(0100 & mode) ? (mode &= ~0111) : (mode |= 0111);
   5757 
   5758 	if (fchmod(fd, mode) == 0) {
   5759 		close(fd);
   5760 		pdents[cur].mode = mode;
   5761 		return TRUE;
   5762 	}
   5763 
   5764 	close(fd);
   5765 	return FALSE;
   5766 }
   5767 
   5768 static size_t get_fs_info(const char *path, uchar_t type)
   5769 {
   5770 	struct statvfs svb;
   5771 
   5772 	if (statvfs(path, &svb) == -1)
   5773 		return 0;
   5774 
   5775 	if (type == VFS_AVAIL)
   5776 		return (size_t)svb.f_bavail << ffs((int)(svb.f_frsize >> 1));
   5777 
   5778 	if (type == VFS_USED)
   5779 		return ((size_t)svb.f_blocks - (size_t)svb.f_bfree) << ffs((int)(svb.f_frsize >> 1));
   5780 
   5781 	return (size_t)svb.f_blocks << ffs((int)(svb.f_frsize >> 1)); /* VFS_SIZE */
   5782 }
   5783 
   5784 /* Create non-existent parents and a file or dir */
   5785 static bool xmktree(char *path, bool dir, bool tree)
   5786 {
   5787 	char *p = path;
   5788 	char *slash = path;
   5789 
   5790 	if (!p || !*p)
   5791 		return FALSE;
   5792 
   5793 	if (tree) {
   5794 		/* Skip the first '/' */
   5795 		++p;
   5796 
   5797 		while (*p != '\0') {
   5798 			if (*p == '/') {
   5799 				slash = p;
   5800 				*p = '\0';
   5801 			} else {
   5802 				++p;
   5803 				continue;
   5804 			}
   5805 
   5806 			/* Create folder from path to '\0' inserted at p */
   5807 			if (mkdir(path, 0777) == -1 && errno != EEXIST) {
   5808 #ifdef _HAIKU__
   5809 				// XDG_CONFIG_HOME contains a directory
   5810 				// that is read-only, but the full path
   5811 				// is writeable.
   5812 				// Try to continue and see what happens.
   5813 				// TODO: Find a more robust solution.
   5814 				if (errno == B_READ_ONLY_DEVICE)
   5815 					goto next;
   5816 #endif
   5817 				DPRINTF_S("mkdir1!");
   5818 				DPRINTF_S(path);
   5819 				DPRINTF_S(strerror(errno));
   5820 				*slash = '/';
   5821 				return FALSE;
   5822 			}
   5823 
   5824 #ifdef _HAIKU__
   5825 next:
   5826 #endif
   5827 			/* Restore path */
   5828 			*slash = '/';
   5829 			++p;
   5830 		}
   5831 	}
   5832 
   5833 	if (dir) {
   5834 		if (mkdir(path, 0777) == -1 && errno != EEXIST) {
   5835 			DPRINTF_S("mkdir2!");
   5836 			DPRINTF_S(path);
   5837 			DPRINTF_S(strerror(errno));
   5838 			return FALSE;
   5839 		}
   5840 	} else {
   5841 		int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, S_IWUSR | S_IRUSR); /* Forced create mode for files */
   5842 
   5843 		if (fd == -1 && errno != EEXIST) {
   5844 			DPRINTF_S("open!");
   5845 			DPRINTF_S(strerror(errno));
   5846 			return FALSE;
   5847 		}
   5848 
   5849 		close(fd);
   5850 	}
   5851 
   5852 	return TRUE;
   5853 }
   5854 
   5855 /* List or extract archive */
   5856 static bool handle_archive(char *fpath /* in-out param */, char op)
   5857 {
   5858 	char arg[] = "-tvf"; /* options for tar/bsdtar to list files */
   5859 	char *util, *outdir = NULL;
   5860 	bool x_to = FALSE;
   5861 	bool is_atool = (!g_state.usebsdtar && getutil(utils[UTIL_ATOOL]));
   5862 
   5863 	if (op == 'x') {
   5864 		outdir = xreadline(is_atool ? "." : xbasename(fpath), messages[MSG_NEW_PATH]);
   5865 		if (!outdir || !*outdir) { /* Cancelled */
   5866 			printwait(messages[MSG_CANCEL], NULL);
   5867 			return FALSE;
   5868 		}
   5869 		/* Do not create smart context for current dir */
   5870 		if (!(*outdir == '.' && outdir[1] == '\0')) {
   5871 			if (!xmktree(outdir, TRUE, TRUE) || (chdir(outdir) == -1)) {
   5872 				printwarn(NULL);
   5873 				return FALSE;
   5874 			}
   5875 			/* Copy the new dir path to open it in smart context */
   5876 			outdir = getcwd(NULL, 0); // NOLINT
   5877 			if (!outdir)
   5878 				return FALSE;
   5879 			x_to = TRUE;
   5880 		}
   5881 	}
   5882 
   5883 	if (is_atool) {
   5884 		util = utils[UTIL_ATOOL];
   5885 		arg[1] = op;
   5886 		arg[2] = '\0';
   5887 	} else if (getutil(utils[UTIL_BSDTAR])) {
   5888 		util = utils[UTIL_BSDTAR];
   5889 		if (op == 'x')
   5890 			arg[1] = op;
   5891 	} else if (is_suffix(fpath, ".zip")) {
   5892 		util = utils[UTIL_UNZIP];
   5893 		arg[1] = (op == 'l') ? 'v' /* verbose listing */ : '\0';
   5894 		arg[2] = '\0';
   5895 	} else {
   5896 		util = utils[UTIL_TAR];
   5897 		if (op == 'x')
   5898 			arg[1] = op;
   5899 	}
   5900 
   5901 	if (op == 'x') /* extract */
   5902 		spawn(util, arg, fpath, NULL, F_NORMAL | F_MULTI);
   5903 	else /* list */
   5904 		get_output(util, arg, fpath, -1, TRUE);
   5905 
   5906 	if (x_to) {
   5907 		if (chdir(xdirname(fpath)) == -1) {
   5908 			printwarn(NULL);
   5909 			free(outdir);
   5910 			return FALSE;
   5911 		}
   5912 		xstrsncpy(fpath, outdir, PATH_MAX);
   5913 		free(outdir);
   5914 	} else if (op == 'x')
   5915 		fpath[0] = '\0';
   5916 
   5917 	return TRUE;
   5918 }
   5919 
   5920 static char *visit_parent(char *path, char *newpath, int *presel)
   5921 {
   5922 	char *dir;
   5923 
   5924 	/* There is no going back */
   5925 	if (istopdir(path)) {
   5926 		/* Continue in type-to-nav mode, if enabled */
   5927 		if (cfg.filtermode && presel)
   5928 			*presel = FILTER;
   5929 		return NULL;
   5930 	}
   5931 
   5932 	/* Use a copy as xdirname() may change the string passed */
   5933 	if (newpath)
   5934 		xstrsncpy(newpath, path, PATH_MAX);
   5935 	else
   5936 		newpath = path;
   5937 
   5938 	dir = xdirname(newpath);
   5939 	if (chdir(dir) == -1) {
   5940 		printwarn(presel);
   5941 		return NULL;
   5942 	}
   5943 
   5944 	return dir;
   5945 }
   5946 
   5947 static void valid_parent(char *path, char *lastname)
   5948 {
   5949 	/* Save history */
   5950 	xstrsncpy(lastname, xbasename(path), NAME_MAX + 1);
   5951 
   5952 	while (!istopdir(path))
   5953 		if (visit_parent(path, NULL, NULL))
   5954 			break;
   5955 
   5956 	printwarn(NULL);
   5957 	xdelay(XDELAY_INTERVAL_MS);
   5958 }
   5959 
   5960 static bool archive_mount(char *newpath)
   5961 {
   5962 	char *dir, *cmd = xgetenv("NNN_ARCHMNT", utils[UTIL_ARCHMNT]);
   5963 	char *name = pdents[cur].name;
   5964 	size_t len = pdents[cur].nlen;
   5965 	char mntpath[PATH_MAX];
   5966 
   5967 	if (!getutil(cmd)) {
   5968 		printmsg("install utility (archivemount)");
   5969 		return FALSE;
   5970 	}
   5971 
   5972 	dir = xstrdup(name);
   5973 	if (!dir) {
   5974 		printmsg(messages[MSG_FAILED]);
   5975 		return FALSE;
   5976 	}
   5977 
   5978 	while (len > 1)
   5979 		if (dir[--len] == '.') {
   5980 			dir[len] = '\0';
   5981 			break;
   5982 		}
   5983 
   5984 	DPRINTF_S(dir);
   5985 
   5986 	/* Create the mount point */
   5987 	mkpath(cfgpath, toks[TOK_MNT], mntpath);
   5988 	mkpath(mntpath, dir, newpath);
   5989 	free(dir);
   5990 
   5991 	if (!xmktree(newpath, TRUE, TRUE)) {
   5992 		printwarn(NULL);
   5993 		return FALSE;
   5994 	}
   5995 
   5996 	/* Mount archive */
   5997 	DPRINTF_S(name);
   5998 	DPRINTF_S(newpath);
   5999 	if (spawn(cmd, name, newpath, NULL, F_NORMAL)) {
   6000 		printmsg(messages[MSG_FAILED]);
   6001 		return FALSE;
   6002 	}
   6003 
   6004 	return TRUE;
   6005 }
   6006 
   6007 static bool remote_mount(char *newpath)
   6008 {
   6009 	uchar_t flag = F_CLI;
   6010 	int opt;
   6011 	char *tmp, *env;
   6012 	bool r = getutil(utils[UTIL_RCLONE]), s = getutil(utils[UTIL_SSHFS]);
   6013 	char mntpath[PATH_MAX];
   6014 
   6015 	if (!(r || s)) {
   6016 		printmsg("install sshfs/rclone");
   6017 		return FALSE;
   6018 	}
   6019 
   6020 	if (r && s)
   6021 		opt = get_input(messages[MSG_REMOTE_OPTS]);
   6022 	else
   6023 		opt = (!s) ? 'r' : 's';
   6024 
   6025 	if (opt == 's')
   6026 		env = xgetenv("NNN_SSHFS", utils[UTIL_SSHFS]);
   6027 	else if (opt == 'r') {
   6028 		flag |= F_NOWAIT | F_NOTRACE;
   6029 		env = xgetenv("NNN_RCLONE", "rclone mount");
   6030 	} else {
   6031 		printmsg(messages[MSG_INVALID_KEY]);
   6032 		return FALSE;
   6033 	}
   6034 
   6035 	tmp = xreadline(NULL, "host[:dir] > ");
   6036 	if (!tmp[0]) {
   6037 		printmsg(messages[MSG_CANCEL]);
   6038 		return FALSE;
   6039 	}
   6040 
   6041 	char *div = strchr(tmp, ':');
   6042 
   6043 	if (div)
   6044 		*div = '\0';
   6045 
   6046 	/* Create the mount point */
   6047 	mkpath(cfgpath, toks[TOK_MNT], mntpath);
   6048 	mkpath(mntpath, tmp, newpath);
   6049 	if (!xmktree(newpath, TRUE, TRUE)) {
   6050 		printwarn(NULL);
   6051 		return FALSE;
   6052 	}
   6053 
   6054 	if (!div) { /* Convert "host" to "host:" */
   6055 		size_t len = xstrlen(tmp);
   6056 
   6057 		tmp[len] = ':';
   6058 		tmp[len + 1] = '\0';
   6059 	} else
   6060 		*div = ':';
   6061 
   6062 	/* Connect to remote */
   6063 	if (opt == 's') {
   6064 		if (spawn(env, tmp, newpath, NULL, flag)) {
   6065 			printmsg(messages[MSG_FAILED]);
   6066 			return FALSE;
   6067 		}
   6068 	} else {
   6069 		spawn(env, tmp, newpath, NULL, flag);
   6070 		printmsg(messages[MSG_RCLONE_DELAY]);
   6071 		xdelay(XDELAY_INTERVAL_MS << 2); /* Set 4 times the usual delay */
   6072 	}
   6073 
   6074 	return TRUE;
   6075 }
   6076 
   6077 /*
   6078  * Unmounts if the directory represented by name is a mount point.
   6079  * Otherwise, asks for hostname
   6080  * Returns TRUE if directory needs to be refreshed *.
   6081  */
   6082 static bool unmount(char *name, char *newpath, int *presel, char *currentpath)
   6083 {
   6084 #if defined(__APPLE__) || defined(__FreeBSD__) || defined(__DragonFly__)
   6085 	static char cmd[] = "umount";
   6086 #else
   6087 	static char cmd[] = "fusermount3"; /* Arch Linux utility */
   6088 	static bool found = FALSE;
   6089 #endif
   6090 	char *tmp = name;
   6091 	struct stat sb, psb;
   6092 	bool child = FALSE;
   6093 	bool parent = FALSE;
   6094 	bool hovered = FALSE;
   6095 	char mntpath[PATH_MAX];
   6096 
   6097 #if !defined(__APPLE__) && !defined(__FreeBSD__) && !defined(__DragonFly__)
   6098 	/* On Ubuntu it's fusermount */
   6099 	if (!found && !getutil(cmd)) {
   6100 		cmd[10] = '\0';
   6101 		found = TRUE;
   6102 	}
   6103 #endif
   6104 
   6105 	mkpath(cfgpath, toks[TOK_MNT], mntpath);
   6106 
   6107 	if (tmp && strcmp(mntpath, currentpath) == 0) {
   6108 		mkpath(mntpath, tmp, newpath);
   6109 		child = lstat(newpath, &sb) != -1;
   6110 		parent = lstat(xdirname(newpath), &psb) != -1;
   6111 		if (!child && !parent) {
   6112 			*presel = MSGWAIT;
   6113 			return FALSE;
   6114 		}
   6115 	}
   6116 
   6117 	if (!tmp || !child || !S_ISDIR(sb.st_mode) || (child && parent && sb.st_dev == psb.st_dev)) {
   6118 		tmp = xreadline(NULL, messages[MSG_HOSTNAME]);
   6119 		if (!tmp[0])
   6120 			return FALSE;
   6121 		if (name && (tmp[0] == '-') && (tmp[1] == '\0')) {
   6122 			mkpath(currentpath, name, newpath);
   6123 			hovered = TRUE;
   6124 		}
   6125 	}
   6126 
   6127 	if (!hovered)
   6128 		mkpath(mntpath, tmp, newpath);
   6129 
   6130 	if (!xdiraccess(newpath)) {
   6131 		*presel = MSGWAIT;
   6132 		return FALSE;
   6133 	}
   6134 
   6135 #if defined(__APPLE__) || defined(__FreeBSD__) || defined(__DragonFly__)
   6136 	if (spawn(cmd, newpath, NULL, NULL, F_NORMAL)) {
   6137 #else
   6138 	if (spawn(cmd, "-qu", newpath, NULL, F_NORMAL)) {
   6139 #endif
   6140 		if (!xconfirm(get_input(messages[MSG_LAZY])))
   6141 			return FALSE;
   6142 
   6143 #ifdef __APPLE__
   6144 		if (spawn(cmd, "-l", newpath, NULL, F_NORMAL)) {
   6145 #elif defined(__FreeBSD__) || defined(__DragonFly__)
   6146 		if (spawn(cmd, "-f", newpath, NULL, F_NORMAL)) {
   6147 #else
   6148 		if (spawn(cmd, "-quz", newpath, NULL, F_NORMAL)) {
   6149 #endif
   6150 			printwait(messages[MSG_FAILED], presel);
   6151 			return FALSE;
   6152 		}
   6153 	}
   6154 
   6155 	if (rmdir(newpath) == -1) {
   6156 		printwarn(presel);
   6157 		return FALSE;
   6158 	}
   6159 
   6160 	return TRUE;
   6161 }
   6162 
   6163 static void lock_terminal(void)
   6164 {
   6165 	spawn(xgetenv("NNN_LOCKER", utils[UTIL_LOCKER]), NULL, NULL, NULL, F_CLI);
   6166 }
   6167 
   6168 static void printkv(kv *kvarr, FILE *f, uchar_t max, uchar_t id)
   6169 {
   6170 	char *val = (id == NNN_BMS) ? bmstr : pluginstr;
   6171 
   6172 	for (uchar_t i = 0; i < max && kvarr[i].key; ++i)
   6173 		fprintf(f, " %c: %s\n", (char)kvarr[i].key, val + kvarr[i].off);
   6174 }
   6175 
   6176 static bool lazy_parse_plug(void)
   6177 {
   6178 	if (!g_state.plugparsed) {
   6179 		g_state.plugparsed = 1;
   6180 		if (!parsekvpair(&plug, &pluginstr, NNN_PLUG, &maxplug)) {
   6181 			printwait(env_cfg[NNN_PLUG], NULL);
   6182 			return FALSE;
   6183 		}
   6184 	}
   6185 	return TRUE;
   6186 }
   6187 
   6188 static void printkeys(kv *kvarr, char *buf, uchar_t max)
   6189 {
   6190 	uchar_t i = 0;
   6191 
   6192 	for (; i < max && kvarr[i].key; ++i) {
   6193 		buf[i << 1] = ' ';
   6194 		buf[(i << 1) + 1] = kvarr[i].key; // NOLINT
   6195 	}
   6196 
   6197 	buf[i << 1] = '\0';
   6198 }
   6199 
   6200 static size_t handle_bookmark(const char *bmark, char *newpath)
   6201 {
   6202 	int fd = '\r';
   6203 	size_t r;
   6204 
   6205 	if (maxbm || bmark) {
   6206 		r = xstrsncpy(g_buf, messages[MSG_KEYS], CMD_LEN_MAX - 2); // Leave 2 chars for a marked directory
   6207 
   6208 		if (bmark) { /* There is a marked directory */
   6209 			g_buf[--r] = ' ';
   6210 			g_buf[++r] = ',';
   6211 			g_buf[++r] = '\0';
   6212 			++r;
   6213 		}
   6214 		printkeys(bookmark, g_buf + r - 1, maxbm);
   6215 		printmsg(g_buf);
   6216 		fd = get_input(NULL);
   6217 	}
   6218 
   6219 	r = FALSE;
   6220 	if (fd == ',') /* Visit marked directory */
   6221 		bmark ? xstrsncpy(newpath, bmark, PATH_MAX) : (r = MSG_NOT_SET);
   6222 	else if (fd == '\r') { /* Visit bookmarks directory */
   6223 		mkpath(cfgpath, toks[TOK_BM], newpath);
   6224 		g_state.selbm = 1;
   6225 	} else if (!get_kv_val(bookmark, newpath, fd, maxbm, NNN_BMS))
   6226 		r = MSG_INVALID_KEY;
   6227 
   6228 	if (!r && chdir(newpath) == -1) {
   6229 		r = MSG_ACCESS;
   6230 		if (g_state.selbm)
   6231 			g_state.selbm = 0;
   6232 	}
   6233 
   6234 	return r;
   6235 }
   6236 
   6237 static void add_bookmark(char *path, char *newpath, int *presel)
   6238 {
   6239 	char *dir = xbasename(path);
   6240 
   6241 	dir = xreadline(dir[0] ? dir : NULL, messages[MSG_BM_NAME]);
   6242 	if (dir && *dir) {
   6243 		size_t r = mkpath(cfgpath, toks[TOK_BM], newpath);
   6244 
   6245 		newpath[r - 1] = '/';
   6246 		xstrsncpy(newpath + r, dir, PATH_MAX - r);
   6247 		printwait((symlink(path, newpath) == -1) ? strerror(errno) : newpath, presel);
   6248 	} else
   6249 		printwait(messages[MSG_CANCEL], presel);
   6250 }
   6251 
   6252 /*
   6253  * The help string tokens (each line) start with a HEX value which indicates
   6254  * the number of spaces to print before the particular token. In the middle,
   6255  * %NN can be used to insert a run of spaces, e.g %10 will print 10 spaces.
   6256  * %NN MUST be 2 characters long, e.g %05 for 5 spaces.
   6257  *
   6258  * This method was chosen instead of a flat string because the number of bytes
   6259  * in help was increasing the binary size by around a hundred bytes. This would
   6260  * only have increased as we keep adding new options.
   6261  */
   6262 static void show_help(const char *path)
   6263 {
   6264 	static const char helpstr[] = {
   6265 	"2|V\\_\n"
   6266 	"2/. \\\\\n"
   6267 	"1(;^; ||\n"
   6268 	"3/___3\n"
   6269 	"2(___n))\n"
   6270 	"0\n"
   6271 	"1NAVIGATION\n"
   6272 	       "9Up k  Prev%14PgUp ^U  Page up\n"
   6273 	       "9Dn j  Next%14PgDn ^D  Page down\n"
   6274 	       "9Lt h  Parent%12~ ` @ -  ~, /, start, prev\n"
   6275 	   "5Ret Rt l  Open%20'  First file/match\n"
   6276 	       "9g ^A  Top%21J  Jump to entry/offset\n"
   6277 	       "9G ^E  End%20^J  Toggle auto-advance on open\n"
   6278 	      "8B (,)  Book(mark)%11b ^/  Select bookmark\n"
   6279 		"a1-8  Context%11(Sh)Tab  Cycle/new context\n"
   6280 	    "62Esc ^Q  Quit%19^y  Next young\n"
   6281 		 "b^G  QuitCD%18Q  Pick/err, quit\n"
   6282 	  "4q Alt+Esc  Quit context%12d  Detail mode toggle\n"
   6283 	"0\n"
   6284 	"1FILTER & PROMPT\n"
   6285 		  "c/  Filter%17^N  Toggle type-to-nav\n"
   6286 		"aEsc  Exit prompt%12^L  Clear/apply filter\n"
   6287 		  "c.  Toggle hidden\n"
   6288 	"0\n"
   6289 	"1FILES\n"
   6290 	       "9o ^O  Open with%15n  Create new/link\n"
   6291 	       "9f ^F  File stats%14P  Preview toggle\n"
   6292 		 "b^R  Rename/dup%14r  Batch rename\n"
   6293 		  "cz  Archive%17e  Edit file\n"
   6294 		  "c*  Toggle exe%14>  Export list\n"
   6295 	    "6Space +  (Un)select%12m-m  Select range/clear\n"
   6296 	          "ca  Select all%14A  Invert sel\n"
   6297 	       "9p ^P  Copy here%12w ^W  Cp/mv sel as\n"
   6298 	       "9v ^V  Move here%15E  Edit sel list\n"
   6299 	       "9x ^X  Delete or trash%09S  Listed sel size\n"
   6300 		  "cX  Delete (rm -rf)%07Esc  Send to FIFO\n"
   6301 	"0\n"
   6302 	"1MISC\n"
   6303 	      "8Alt ;  Select plugin%11=  Launch app\n"
   6304 	       "9! ^]  Shell%19]  Cmd prompt\n"
   6305 		  "cc  Connect remote%10u  Unmount remote/archive\n"
   6306 	       "9t ^T  Sort toggles%12s  Manage session\n"
   6307 		  "cT  Set time type%110  Lock\n"
   6308 		 "b^L  Redraw%18?  Help, conf\n"
   6309 	};
   6310 
   6311 	int fd = create_tmp_file();
   6312 	if (fd == -1)
   6313 		return;
   6314 	FILE *f = fdopen(fd, "wb");
   6315 	if (f == NULL) {
   6316 		close(fd);
   6317 		unlink(g_tmpfpath);
   6318 		return;
   6319 	}
   6320 
   6321 	char *prog = xgetenv(env_cfg[NNN_HELP], NULL);
   6322 	if (prog)
   6323 		get_output(prog, NULL, NULL, fd, FALSE);
   6324 
   6325 	bool hex = true;
   6326 	const char *end = helpstr + sizeof(helpstr) - 1;
   6327 
   6328 	for (const char *s = helpstr; s < end; ++s) {
   6329 		if (hex) {
   6330 			for (int k = 0, n = xchartohex(*s); k < n; ++k)
   6331 				fputc(' ', f);
   6332 		} else if (*s == '%') {
   6333 			int n = ((s[1] - '0') * 10) + (s[2] - '0');
   6334 			for (int k = 0; k < n; ++k)
   6335 				fputc(' ', f);
   6336 			s += 2;
   6337 		} else {
   6338 			fputc(*s, f);
   6339 		}
   6340 		hex = (*s == '\n');
   6341 	}
   6342 
   6343 #ifndef NOSSN
   6344 	if (curssn[0])
   6345 		fprintf(f, "\nSESSION: %s\n", curssn);
   6346 #endif
   6347 
   6348 	fprintf(f, "\nCONTEXTS\n");
   6349 	for (uchar_t i = 0; i < CTX_MAX; ++i)
   6350 		if (g_ctx[i].c_cfg.ctxactive)
   6351 			fprintf(f, " %u%c %s\n", i + 1, (cfg.curctx == i) ? '*' : ' ', g_ctx[i].c_path);
   6352 
   6353 	fprintf(f, "\nVOLUME: avail:%s ", coolsize(get_fs_info(path, VFS_AVAIL)));
   6354 	fprintf(f, "used:%s ", coolsize(get_fs_info(path, VFS_USED)));
   6355 	fprintf(f, "size:%s\n\n", coolsize(get_fs_info(path, VFS_SIZE)));
   6356 
   6357 	if (bookmark || mark) {
   6358 		fprintf(f, "BOOKMARKS\n");
   6359 		printkv(bookmark, f, maxbm, NNN_BMS);
   6360 		if (mark)
   6361 			fprintf(f, " ,: %s\n", mark);
   6362 		fprintf(f, "\n");
   6363 	}
   6364 
   6365 	lazy_parse_plug();
   6366 	if (plug) {
   6367 		fprintf(f, "PLUGIN KEYS\n");
   6368 		printkv(plug, f, maxplug, NNN_PLUG);
   6369 		fprintf(f, "\n");
   6370 	}
   6371 
   6372 	for (uchar_t i = NNN_OPENER; i <= NNN_TRASH; ++i) {
   6373 		char *s = getenv(env_cfg[i]);
   6374 		if (s)
   6375 			fprintf(f, "%s: %s\n", env_cfg[i], s);
   6376 	}
   6377 
   6378 	if (selpath)
   6379 		fprintf(f, "SELECTION FILE: %s\n", selpath);
   6380 
   6381 	fprintf(f, "\nv%s\n%s\n", VERSION, GENERAL_INFO);
   6382 	fclose(f); // also closes fd
   6383 
   6384 	spawn(pager, g_tmpfpath, NULL, NULL, F_CLI | F_TTY);
   6385 	unlink(g_tmpfpath);
   6386 }
   6387 
   6388 static void setexports(const char *path)
   6389 {
   6390 	char dvar[] = "d0";
   6391 	char fvar[] = "f0";
   6392 
   6393 	if (ndents) {
   6394 		setenv(envs[ENV_NCUR], pdents[cur].name, 1);
   6395 		xstrsncpy(g_ctx[cfg.curctx].c_name, pdents[cur].name, NAME_MAX + 1);
   6396 	} else if (g_ctx[cfg.curctx].c_name[0])
   6397 		g_ctx[cfg.curctx].c_name[0] = '\0';
   6398 
   6399 	for (uchar_t i = 0; i < CTX_MAX; ++i) {
   6400 		if (g_ctx[i].c_cfg.ctxactive) {
   6401 			dvar[1] = fvar[1] = '1' + i;
   6402 			setenv(dvar, g_ctx[i].c_path, 1);
   6403 
   6404 			if (g_ctx[i].c_name[0]) {
   6405 				mkpath(g_ctx[i].c_path, g_ctx[i].c_name, g_buf);
   6406 				setenv(fvar, g_buf, 1);
   6407 			}
   6408 		}
   6409 	}
   6410 	setenv("NNN_INCLUDE_HIDDEN", xitoa(cfg.showhidden), 1);
   6411 	setenv("NNN_PREFER_SELECTION", xitoa(cfg.prefersel), 1);
   6412 	setenv("PWD", path, 1);
   6413 }
   6414 
   6415 static void run_cmd_as_plugin(const char *file, ushort_t flags, enum action *action)
   6416 {
   6417 	size_t len;
   6418 
   6419 	xstrsncpy(g_buf, file, PATH_MAX);
   6420 
   6421 	len = xstrlen(g_buf);
   6422 	if (len > 1 && g_buf[len - 1] == '*') {
   6423 		flags &= ~F_CONFIRM; /* Skip user confirmation */
   6424 		g_buf[len - 1] = '\0'; /* Get rid of trailing no confirmation symbol */
   6425 		--len;
   6426 	}
   6427 
   6428 	if (flags & F_PAGE)
   6429 		get_output(utils[UTIL_SH_EXEC], g_buf, NULL, -1, TRUE);
   6430 	else if (flags & F_WINDOW) {
   6431 		char *content = NULL;
   6432 		size_t content_len = 0;
   6433 		char * const cmds[] = { utils[UTIL_SH_EXEC], };
   6434 
   6435 		if (buffer_command_output(cmds, g_buf, NULL, ELEMENTS(cmds), &content, &content_len))
   6436 			show_content_in_floating_window(content, content_len, action, strstr(g_buf, "$nnn") != NULL);
   6437 		free(content);
   6438 	} else
   6439 		spawn(utils[UTIL_SH_EXEC], g_buf, NULL, NULL, flags);
   6440 }
   6441 
   6442 static bool plctrl_init(void)
   6443 {
   6444 	size_t len;
   6445 
   6446 	/* g_tmpfpath is used to generate tmp file names */
   6447 	g_tmpfpath[tmpfplen - 1] = '\0';
   6448 	len = xstrsncpy(g_pipepath, g_tmpfpath, TMP_LEN_MAX);
   6449 	g_pipepath[len - 1] = '/';
   6450 	len = xstrsncpy(g_pipepath + len, "nnn-pipe.", TMP_LEN_MAX - len) + len;
   6451 	xstrsncpy(g_pipepath + len - 1, xitoa(getpid()), TMP_LEN_MAX - len);
   6452 	setenv(env_cfg[NNN_PIPE], g_pipepath, TRUE);
   6453 
   6454 	return EXIT_SUCCESS;
   6455 }
   6456 
   6457 static void rmlistpath(void)
   6458 {
   6459 	if (listpath) {
   6460 		DPRINTF_S(__func__);
   6461 		DPRINTF_S(listpath);
   6462 		spawn(utils[UTIL_RM_RF], listpath, NULL, NULL, F_NOTRACE | F_MULTI);
   6463 		/* Do not free if program was started in list mode */
   6464 		if (listpath != initpath)
   6465 			free(listpath);
   6466 		listpath = NULL;
   6467 	}
   6468 }
   6469 
   6470 static ssize_t read_nointr(int fd, void *buf, size_t count)
   6471 {
   6472 	ssize_t len;
   6473 
   6474 	do
   6475 		len = read(fd, buf, count);
   6476 	while (len == -1 && errno == EINTR);
   6477 
   6478 	return len;
   6479 }
   6480 
   6481 static char *readpipe(int fd, char *ctxnum, char **path)
   6482 {
   6483 	char ctx, *nextpath = NULL;
   6484 
   6485 	if (read_nointr(fd, g_buf, 1) != 1)
   6486 		return NULL;
   6487 
   6488 	if (g_buf[0] == '-') { /* Clear selection on '-' */
   6489 		clearselection();
   6490 		if (read_nointr(fd, g_buf, 1) != 1)
   6491 			return NULL;
   6492 	}
   6493 
   6494 	if (g_buf[0] == '+')
   6495 		ctx = (char)(get_free_ctx() + 1);
   6496 	else if (g_buf[0] < '0')
   6497 		return NULL;
   6498 	else {
   6499 		ctx = g_buf[0] - '0';
   6500 		if (ctx > CTX_MAX)
   6501 			return NULL;
   6502 	}
   6503 
   6504 	if (read_nointr(fd, g_buf, 1) != 1)
   6505 		return NULL;
   6506 
   6507 	char op = g_buf[0];
   6508 
   6509 	if (op == 'c') {
   6510 		ssize_t len = read_nointr(fd, g_buf, PATH_MAX);
   6511 
   6512 		if (len <= 0)
   6513 			return NULL;
   6514 
   6515 		g_buf[len] = '\0'; /* Terminate the path read */
   6516 		if (g_buf[0] == '/') {
   6517 			nextpath = g_buf;
   6518 			len = xstrlen(g_buf);
   6519 			while (--len && (g_buf[len] == '/')) /* Trim all trailing '/' */
   6520 				g_buf[len] = '\0';
   6521 		}
   6522 	} else if (op == 'l') {
   6523 		rmlistpath(); /* Remove last list mode path, if any */
   6524 		nextpath = load_input(fd, *path);
   6525 	} else if (op == 'p') {
   6526 		free(selpath);
   6527 		selpath = NULL;
   6528 		clearselection();
   6529 		g_state.picker = 0;
   6530 		g_state.picked = 1;
   6531 	}
   6532 
   6533 	*ctxnum = ctx;
   6534 
   6535 	return nextpath;
   6536 }
   6537 
   6538 static bool run_plugin(char **path, const char *file, char *runfile, char **lastname, char **lastdir, enum action *action)
   6539 {
   6540 	pid_t p;
   6541 	char ctx = 0;
   6542 	ushort_t flags = 0;
   6543 	bool cmd_as_plugin = FALSE;
   6544 	char *nextpath;
   6545 
   6546 	if (!g_state.pluginit) {
   6547 		plctrl_init();
   6548 		g_state.pluginit = 1;
   6549 	}
   6550 
   6551 	setexports(*path);
   6552 
   6553 	/* Check for run-cmd-as-plugin mode */
   6554 	if (*file == '!') {
   6555 		flags = F_MULTI | F_CONFIRM;
   6556 		++file;
   6557 
   6558 		if (*file == '|') { /* Check if output should be paged */
   6559 			flags |= F_PAGE;
   6560 			++file;
   6561 		} else if (*file == '>') { /* Check if floating window should be used */
   6562 			flags |= F_WINDOW;
   6563 			++file;
   6564 			//*action = SEL_REDRAW;
   6565 		} else if (*file == '&') { /* Check if GUI flags are to be used */
   6566 			flags = F_MULTI | F_NOTRACE | F_NOWAIT;
   6567 			++file;
   6568 		}
   6569 
   6570 		if (!*file)
   6571 			return FALSE;
   6572 
   6573 		if ((flags & F_NOTRACE) || (flags & F_PAGE) || (flags & F_WINDOW)) {
   6574 			run_cmd_as_plugin(file, flags, action);
   6575 			return TRUE;
   6576 		}
   6577 
   6578 		cmd_as_plugin = TRUE;
   6579 	}
   6580 
   6581 	if (mkfifo(g_pipepath, 0600) != 0)
   6582 		return FALSE;
   6583 
   6584 	exitcurses();
   6585 
   6586 	p = fork();
   6587 
   6588 	if (!p) { // In child
   6589 		int wfd = open(g_pipepath, O_WRONLY | O_CLOEXEC);
   6590 
   6591 		if (wfd == -1)
   6592 			_exit(EXIT_FAILURE);
   6593 
   6594 		/* Restore sigpipe handler to default */
   6595 		sigaction(SIGPIPE, &(struct sigaction){.sa_handler = SIG_DFL}, NULL);
   6596 
   6597 		if (!cmd_as_plugin) {
   6598 			char *sel = NULL;
   6599 			char std[2] = "-";
   6600 
   6601 			/* Generate absolute path to plugin */
   6602 			mkpath(plgpath, file, g_buf);
   6603 
   6604 			if (g_state.picker)
   6605 				sel = selpath ? selpath : std;
   6606 
   6607 			if (runfile && runfile[0]) {
   6608 				xstrsncpy(*lastname, runfile, NAME_MAX);
   6609 				spawn(g_buf, *lastname, *path, sel, 0);
   6610 			} else
   6611 				spawn(g_buf, NULL, *path, sel, 0);
   6612 		} else
   6613 			run_cmd_as_plugin(file, flags, NULL);
   6614 
   6615 		close(wfd);
   6616 		_exit(EXIT_SUCCESS);
   6617 	}
   6618 
   6619 	int rfd;
   6620 
   6621 	do
   6622 		rfd = open(g_pipepath, O_RDONLY);
   6623 	while (rfd == -1 && errno == EINTR);
   6624 
   6625 	nextpath = readpipe(rfd, &ctx, path);
   6626 	if (nextpath)
   6627 		set_smart_ctx(ctx, nextpath, path, runfile, lastname, lastdir);
   6628 
   6629 	close(rfd);
   6630 
   6631 	/* wait for the child to finish. no zombies allowed */
   6632 	waitpid(p, NULL, 0);
   6633 
   6634 	refresh();
   6635 
   6636 	unlink(g_pipepath);
   6637 
   6638 	return TRUE;
   6639 }
   6640 
   6641 static bool launch_app(char *newpath)
   6642 {
   6643 	int r = F_NORMAL;
   6644 	char *tmp = newpath;
   6645 
   6646 	mkpath(plgpath, utils[UTIL_LAUNCH], newpath);
   6647 
   6648 	if (!getutil(utils[UTIL_FZF]) || access(newpath, X_OK) < 0) {
   6649 		tmp = xreadline(NULL, messages[MSG_APP_NAME]);
   6650 		r = F_NOWAIT | F_NOTRACE | F_MULTI;
   6651 	}
   6652 
   6653 	if (tmp && *tmp) // NOLINT
   6654 		spawn(tmp, (r == F_NORMAL) ? "0" : NULL, NULL, NULL, r);
   6655 
   6656 	return FALSE;
   6657 }
   6658 
   6659 /* Returns TRUE if at least one command was run */
   6660 static bool prompt_run(void)
   6661 {
   6662 	bool ret = FALSE;
   6663 	char *cmdline, *next;
   6664 	int cnt_j, cnt_J, cmd_ret;
   6665 	size_t len;
   6666 
   6667 	/* Run the command once per selected file */
   6668 	const char *xargs_j = "xargs -0 -I{} %s < '%s'";
   6669 	/* Append all selected files as arguments at once to the command */
   6670 	const char *xargs_J = "xargs -0 %s < '%s'";
   6671 	char cmd[CMD_LEN_MAX + 32]; // 32 for xargs format strings
   6672 
   6673 
   6674 	while (1) {
   6675 		selcmdpos = INVALID_POS;
   6676 #ifndef NORL
   6677 		if (g_state.picker || g_state.xprompt) {
   6678 #endif
   6679 			cmdline = xreadline(NULL, PROMPT);
   6680 #ifndef NORL
   6681 		} else
   6682 			cmdline = getreadline("\n"PROMPT);
   6683 #endif
   6684 		// Check for an empty command
   6685 		if (!cmdline || !cmdline[0])
   6686 			break;
   6687 
   6688 		addcmdtohist(cmdline);
   6689 		ret = TRUE;
   6690 		len = xstrlen(cmdline);
   6691 
   6692 		cnt_j = 0;
   6693 		next = cmdline;
   6694 		while ((next = strstr(next, "%j"))) {
   6695 			++cnt_j;
   6696 
   6697 			// replace %j with {} for xargs later
   6698 			next[0] = '{';
   6699 			next[1] = '}';
   6700 
   6701 			++next;
   6702 		}
   6703 
   6704 		cnt_J = 0;
   6705 		next = cmdline;
   6706 		while ((next = strstr(next, "%J"))) {
   6707 			++cnt_J;
   6708 
   6709 			// %J should be the last thing in the command
   6710 			if (next == cmdline + len - 2) {
   6711 				cmdline[len - 2] = '\0';
   6712 			}
   6713 
   6714 			++next;
   6715 		}
   6716 
   6717 		// We can't handle both %j and %J in a single command
   6718 		if (cnt_j && cnt_J)
   6719 			break;
   6720 
   6721 		if (cnt_j)
   6722 			snprintf(cmd, CMD_LEN_MAX + 32, xargs_j, cmdline, selpath);
   6723 		else if (cnt_J)
   6724 			snprintf(cmd, CMD_LEN_MAX + 32, xargs_J, cmdline, selpath);
   6725 
   6726 		cmd_ret = spawn(shell, "-c", (cnt_j || cnt_J) ? cmd : cmdline, NULL, F_CLI | F_CONFIRM);
   6727 		if ((cnt_j || cnt_J) && cmd_ret == 0)
   6728 			clearselection();
   6729 	}
   6730 
   6731 	return ret;
   6732 }
   6733 
   6734 static bool handle_cmd(enum action sel, char *path, char *newpath)
   6735 {
   6736 	endselection(FALSE);
   6737 
   6738 	if (sel == SEL_LAUNCH)
   6739 		return launch_app(newpath);
   6740 
   6741 	setexports(path);
   6742 
   6743 	if (sel == SEL_PROMPT)
   6744 		return prompt_run();
   6745 
   6746 	/* Set nnn nesting level */
   6747 	char *tmp = getenv(env_cfg[NNNLVL]);
   6748 	int r = tmp ? atoi(tmp) : 0;
   6749 
   6750 	setenv(env_cfg[NNNLVL], xitoa(r + 1), 1);
   6751 	spawn(shell, NULL, NULL, NULL, F_CLI);
   6752 	setenv(env_cfg[NNNLVL], xitoa(r), 1);
   6753 	return TRUE;
   6754 }
   6755 
   6756 static void dentfree(void)
   6757 {
   6758 	/* Shut down DU worker threads so they exit and we can join */
   6759 	if (g_state.duinit) {
   6760 		pthread_mutex_lock(&running_mutex);
   6761 		du_shutdown = true;
   6762 		for (size_t i = 0; i < du_task_len; ++i)
   6763 			free(du_tasks[i].path);
   6764 		du_task_len = 0;
   6765 		du_tasks_pending = 0;
   6766 		pthread_cond_broadcast(&work_cond);
   6767 		pthread_mutex_unlock(&running_mutex);
   6768 		for (int i = 0; i < num_du_threads; ++i)
   6769 			pthread_join(worker_tids[i], NULL);
   6770 	}
   6771 
   6772 	free(pnamebuf);
   6773 	free(pdents);
   6774 	free(mark);
   6775 
   6776 	/* Thread data cleanup */
   6777 	free(core_blocks);
   6778 	free(core_data);
   6779 	free(core_files);
   6780 	free(du_tasks);
   6781 }
   6782 
   6783 /* Walk a directory tree using readdir to reduce FTS overhead */
   6784 static bool du_queue_task(const char *path, du_group *group, bool count_root, bool inc_pending)
   6785 {
   6786 	if (inc_pending) {
   6787 		pthread_mutex_lock(&du_count_mutex);
   6788 		++group->pending;
   6789 		pthread_mutex_unlock(&du_count_mutex);
   6790 	}
   6791 
   6792 	pthread_mutex_lock(&running_mutex);
   6793 	if (g_state.interrupt) {
   6794 		pthread_mutex_unlock(&running_mutex);
   6795 		if (inc_pending) {
   6796 			pthread_mutex_lock(&du_count_mutex);
   6797 			--group->pending;
   6798 			pthread_mutex_unlock(&du_count_mutex);
   6799 		}
   6800 		return false;
   6801 	}
   6802 
   6803 	if (du_task_len == du_task_cap) {
   6804 		size_t newcap = du_task_cap ? (du_task_cap << 1) : TASK_CAP_DU;
   6805 		du_task *tmp = realloc(du_tasks, newcap * sizeof(*du_tasks));
   6806 		if (!tmp) {
   6807 			pthread_mutex_unlock(&running_mutex);
   6808 			if (inc_pending) {
   6809 				pthread_mutex_lock(&du_count_mutex);
   6810 				--group->pending;
   6811 				pthread_mutex_unlock(&du_count_mutex);
   6812 			}
   6813 			return false;
   6814 		}
   6815 		du_tasks = tmp;
   6816 		du_task_cap = newcap;
   6817 	}
   6818 
   6819 	du_tasks[du_task_len++] = (du_task){
   6820 		.path = xstrdup(path),
   6821 		.group = group,
   6822 		.count_root = count_root,
   6823 	};
   6824 	if (!du_tasks[du_task_len - 1].path) {
   6825 		--du_task_len;
   6826 		pthread_mutex_unlock(&running_mutex);
   6827 		if (inc_pending) {
   6828 			pthread_mutex_lock(&du_count_mutex);
   6829 			--group->pending;
   6830 			pthread_mutex_unlock(&du_count_mutex);
   6831 		}
   6832 		return false;
   6833 	}
   6834 	++du_tasks_pending;
   6835 	pthread_cond_signal(&work_cond);
   6836 	pthread_mutex_unlock(&running_mutex);
   6837 
   6838 	return true;
   6839 }
   6840 
   6841 /* Add blocks from stat to total */
   6842 static inline void add_blocks(blkcnt_t *tblocks, const struct stat *sb)
   6843 {
   6844 	if (cfg.apparentsz)
   6845 		*tblocks += sb->st_size;
   6846 	else if (sb->st_blocks)
   6847 		*tblocks += sb->st_blocks;
   6848 }
   6849 
   6850 /* Stat entry only if not already cached */
   6851 static inline bool lazy_stat(int fd, const char *name, struct stat *sb, bool *valid)
   6852 {
   6853 	if (!*valid) {
   6854 		if (fstatat(fd, name, sb, AT_SYMLINK_NOFOLLOW) == -1)
   6855 			return false;
   6856 		*valid = true;
   6857 	}
   6858 	return true;
   6859 }
   6860 
   6861 static void du_walk_dir(const char *root, du_group *group, bool count_root, ullong_t *tfiles, blkcnt_t *tblocks)
   6862 {
   6863 	struct stat sb_root;
   6864 	if (fstatat(AT_FDCWD, root, &sb_root, AT_SYMLINK_NOFOLLOW) == -1)
   6865 		return;
   6866 	if (!S_ISDIR(sb_root.st_mode))
   6867 		return;
   6868 
   6869 	const dev_t root_dev = sb_root.st_dev;
   6870 
   6871 	/* Count root dir itself */
   6872 	if (count_root) {
   6873 		add_blocks(tblocks, &sb_root);
   6874 		++(*tfiles);
   6875 	}
   6876 
   6877 	DIR *dirp = opendir(root);
   6878 	if (!dirp)
   6879 		return;
   6880 
   6881 	const int dfd = dirfd(dirp);
   6882 	struct dirent *dp;
   6883 	while ((dp = readdir(dirp)) && !g_state.interrupt) {
   6884 		if (selforparent(dp->d_name))
   6885 			continue;
   6886 
   6887 		const unsigned char dtype = dp->d_type;
   6888 		bool is_dir = (dtype == DT_DIR);
   6889 		bool is_reg = (dtype == DT_REG);
   6890 		bool sb_valid = false;
   6891 
   6892 		struct stat sb;
   6893 		if (dtype == DT_UNKNOWN) {
   6894 			if (fstatat(dfd, dp->d_name, &sb, AT_SYMLINK_NOFOLLOW) == -1)
   6895 				continue;
   6896 			sb_valid = true;
   6897 			is_dir = S_ISDIR(sb.st_mode);
   6898 			is_reg = S_ISREG(sb.st_mode);
   6899 		}
   6900 
   6901 		/* Count blocks for directories and regular files */
   6902 		if (is_dir) {
   6903 			if (!lazy_stat(dfd, dp->d_name, &sb, &sb_valid))
   6904 				continue;
   6905 			add_blocks(tblocks, &sb);
   6906 		} else if (is_reg) {
   6907 			if (!lazy_stat(dfd, dp->d_name, &sb, &sb_valid))
   6908 				continue;
   6909 			/* Do not recount hard links */
   6910 			if (sb.st_size && (sb.st_nlink <= 1 || test_set_bit((uint_t)sb.st_ino)))
   6911 				add_blocks(tblocks, &sb);
   6912 		}
   6913 
   6914 		++(*tfiles);
   6915 
   6916 		/* Add subdirectories to task queue for worker threads */
   6917 		if (is_dir) {
   6918 			if (!lazy_stat(dfd, dp->d_name, &sb, &sb_valid))
   6919 				continue;
   6920 			if (sb.st_dev == root_dev) {
   6921 				char childbuf[PATH_MAX];
   6922 				mkpath(root, dp->d_name, childbuf);
   6923 				du_queue_task(childbuf, group, false, true);
   6924 			}
   6925 		}
   6926 	}
   6927 
   6928 	closedir(dirp);
   6929 }
   6930 
   6931 static void *du_worker_loop(void *p_data)
   6932 {
   6933 	thread_data *pdata = (thread_data *)p_data;
   6934 	const int core = (int)pdata->core;
   6935 	du_task task = {0};
   6936 
   6937 #ifdef __linux__
   6938 #ifndef __TERMUX__
   6939 	/* Pin thread to specific CPU core for better cache locality and parallelism */
   6940 	cpu_set_t cpuset;
   6941 	CPU_ZERO(&cpuset);
   6942 	int num_cpus = (int)sysconf(_SC_NPROCESSORS_ONLN);
   6943 	if (num_cpus > 0)
   6944 		CPU_SET(core % num_cpus, &cpuset);
   6945 	pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpuset);
   6946 #endif
   6947 #endif
   6948 
   6949 	for (;;) {
   6950 		pthread_mutex_lock(&running_mutex);
   6951 		while (du_task_len == 0 && !du_shutdown)
   6952 			pthread_cond_wait(&work_cond, &running_mutex);
   6953 		if (du_shutdown) {
   6954 			pthread_mutex_unlock(&running_mutex);
   6955 			return NULL;
   6956 		}
   6957 
   6958 		task = du_tasks[--du_task_len];
   6959 		++active_threads;
   6960 		pthread_mutex_unlock(&running_mutex);
   6961 
   6962 		ullong_t tfiles = 0;
   6963 		blkcnt_t tblocks = 0;
   6964 		du_walk_dir(task.path, task.group, task.count_root, &tfiles, &tblocks);
   6965 		free(task.path);
   6966 
   6967 		/* Aggregate into the shared group and finalize when done */
   6968 		pthread_mutex_lock(&du_count_mutex);
   6969 		task.group->blocks += tblocks;
   6970 		task.group->files += tfiles;
   6971 		if (task.group->pending > 0)
   6972 			--task.group->pending;
   6973 		bool done = (task.group->pending == 0);
   6974 		if (done) {
   6975 			if (task.group->entnum >= 0)
   6976 				pdents[task.group->entnum].blocks = task.group->blocks;
   6977 
   6978 			if (!task.group->no_aggregate) {
   6979 				if (!task.group->mntpoint) {
   6980 					core_blocks[core] += task.group->blocks;
   6981 					core_files[core] += task.group->files;
   6982 				} else
   6983 					core_files[core] += 1;
   6984 			}
   6985 		}
   6986 		pthread_mutex_unlock(&du_count_mutex);
   6987 		if (done)
   6988 			free(task.group);
   6989 
   6990 		pthread_mutex_lock(&running_mutex);
   6991 		--active_threads;
   6992 		if (du_tasks_pending > 0)
   6993 			--du_tasks_pending;
   6994 		pthread_cond_signal(&du_cond); /* signal instead of broadcast for better performance */
   6995 		pthread_mutex_unlock(&running_mutex);
   6996 	}
   6997 	/* not reached */
   6998 	return NULL;
   6999 }
   7000 
   7001 /* Assign one subdirectory to a worker; subdirectories are scanned in parallel by the pool */
   7002 static void dirwalk(char *path, int entnum, bool mountpoint, bool no_aggregate)
   7003 {
   7004 	if (g_state.interrupt)
   7005 		return;
   7006 
   7007 	du_group *group = calloc(1, sizeof(*group));
   7008 	if (!group)
   7009 		return;
   7010 	group->pending = 1;
   7011 	group->entnum = entnum;
   7012 	group->mntpoint = mountpoint;
   7013 	group->no_aggregate = no_aggregate;
   7014 
   7015 	if (!du_queue_task(path, group, true, false)) {
   7016 		free(group);
   7017 		return;
   7018 	}
   7019 
   7020 	if (first_call) {
   7021 		tolastln();
   7022 		addstr("[^C aborts]\n");
   7023 		refresh();
   7024 		first_call = FALSE;
   7025 	}
   7026 }
   7027 
   7028 static bool prep_threads(void)
   7029 {
   7030 	if (!g_state.duinit) {
   7031 		long n = sysconf(_SC_NPROCESSORS_ONLN);
   7032 
   7033 		/* Create one thread per CPU core for optimal parallelism */
   7034 		num_du_threads = (n > 0 && n <= NUM_DU_THREADS_MAX) ? (int)n : 4;
   7035 		if (num_du_threads < 2)
   7036 			num_du_threads = 2;
   7037 		du_shutdown = false;
   7038 		for (int i = 0; i < num_du_threads; ++i)
   7039 			work_ready[i] = false;
   7040 		active_threads = 0;
   7041 		du_task_len = 0;
   7042 		du_tasks_pending = 0;
   7043 		if (!du_task_cap)
   7044 			du_task_cap = TASK_CAP_DU;
   7045 		if (!du_tasks)
   7046 			du_tasks = calloc(du_task_cap, sizeof(*du_tasks));
   7047 
   7048 		if (!core_blocks)
   7049 			core_blocks = calloc((size_t)num_du_threads, sizeof(blkcnt_t));
   7050 		if (!core_data)
   7051 			core_data = calloc((size_t)num_du_threads, sizeof(thread_data));
   7052 		if (!core_files)
   7053 			core_files = calloc((size_t)num_du_threads, sizeof(ullong_t));
   7054 
   7055 		if (!core_blocks || !core_data || !core_files || !du_tasks) {
   7056 			printwarn(NULL);
   7057 			return FALSE;
   7058 		}
   7059 		for (int i = 0; i < num_du_threads; ++i) {
   7060 			core_data[i].core = (ushort_t)i;
   7061 			if (pthread_create(&worker_tids[i], NULL, du_worker_loop,
   7062 			    (void *)&core_data[i]) != 0) {
   7063 				du_shutdown = true;
   7064 				pthread_cond_broadcast(&work_cond);
   7065 				while (i--)
   7066 					pthread_join(worker_tids[i], NULL);
   7067 				printwarn(NULL);
   7068 				return FALSE;
   7069 			}
   7070 		}
   7071 #ifndef __APPLE__
   7072 		/* Increase current open file descriptor limit */
   7073 		max_openfds();
   7074 #endif
   7075 		g_state.duinit = TRUE;
   7076 	} else {
   7077 		memset(core_blocks, 0, (size_t)num_du_threads * sizeof(blkcnt_t));
   7078 		memset(core_data, 0, (size_t)num_du_threads * sizeof(thread_data));
   7079 		memset(core_files, 0, (size_t)num_du_threads * sizeof(ullong_t));
   7080 		pthread_mutex_lock(&running_mutex);
   7081 		for (size_t i = 0; i < du_task_len; ++i)
   7082 			free(du_tasks[i].path);
   7083 		du_task_len = 0;
   7084 		du_tasks_pending = 0;
   7085 		pthread_mutex_unlock(&running_mutex);
   7086 	}
   7087 	return TRUE;
   7088 }
   7089 
   7090 /* Skip self and parent */
   7091 static inline bool selforparent(const char *path)
   7092 {
   7093 	return path[0] == '.' && (path[1] == '\0' || (path[1] == '.' && path[2] == '\0'));
   7094 }
   7095 
   7096 static int dentfill(char *path, struct entry **ppdents)
   7097 {
   7098 	uchar_t entflags = 0;
   7099 	int flags = 0;
   7100 	struct dirent *dp;
   7101 	char *namep, *pnb, *buf;
   7102 	struct entry *dentp;
   7103 	size_t off = 0, namebuflen = NAMEBUF_INCR;
   7104 	struct stat sb_path, sb;
   7105 	DIR *dirp = opendir(path);
   7106 
   7107 	ndents = 0;
   7108 	gtimesecs = time(NULL);
   7109 
   7110 	DPRINTF_S(__func__);
   7111 
   7112 	if (!dirp)
   7113 		return 0;
   7114 
   7115 	int fd = dirfd(dirp);
   7116 
   7117 	if (cfg.blkorder) {
   7118 		first_call = TRUE;
   7119 		num_files = 0;
   7120 		dir_blocks = 0;
   7121 		buf = g_buf;
   7122 
   7123 		if (fstatat(fd, path, &sb_path, 0) == -1)
   7124 			goto exit;
   7125 
   7126 		if (!ihashbmp) {
   7127 			ihashbmp = calloc(1, HASH_OCTETS << 3);
   7128 			if (!ihashbmp)
   7129 				goto exit;
   7130 		} else
   7131 			memset(ihashbmp, 0, HASH_OCTETS << 3);
   7132 
   7133 		if (!dir_dispatched_bmp) {
   7134 			dir_dispatched_bmp = calloc(1, HASH_OCTETS << 3);
   7135 			if (!dir_dispatched_bmp)
   7136 				goto exit;
   7137 		} else
   7138 			memset(dir_dispatched_bmp, 0, HASH_OCTETS << 3);
   7139 
   7140 		if (!prep_threads())
   7141 			goto exit;
   7142 
   7143 		attron(COLOR_PAIR(cfg.curctx + 1));
   7144 	}
   7145 
   7146 #if _POSIX_C_SOURCE >= 200112L
   7147 	posix_fadvise(fd, 0, 0, POSIX_FADV_SEQUENTIAL);
   7148 #endif
   7149 
   7150 	dp = readdir(dirp);
   7151 	if (!dp)
   7152 		goto exit;
   7153 
   7154 #if defined(__sun) || defined(__HAIKU__)
   7155 	flags = AT_SYMLINK_NOFOLLOW; /* no d_type */
   7156 #else
   7157 	if (cfg.blkorder || dp->d_type == DT_UNKNOWN) {
   7158 		/*
   7159 		 * Optimization added for filesystems which support dirent.d_type
   7160 		 * see readdir(3)
   7161 		 * Known drawbacks:
   7162 		 * - the symlink size is set to 0
   7163 		 * - the modification time of the symlink is set to that of the target file
   7164 		 */
   7165 		flags = AT_SYMLINK_NOFOLLOW;
   7166 	}
   7167 #endif
   7168 
   7169 	do {
   7170 		namep = dp->d_name;
   7171 
   7172 		if (selforparent(namep))
   7173 			continue;
   7174 
   7175 		if (!cfg.showhidden && namep[0] == '.') {
   7176 			if (!cfg.blkorder)
   7177 				continue;
   7178 
   7179 			if (fstatat(fd, namep, &sb, AT_SYMLINK_NOFOLLOW) == -1)
   7180 				continue;
   7181 
   7182 			/* Use resolved (dev,ino) for duplicate check (symlink-to-dir vs real dir) */
   7183 			struct stat sb_dir_h;
   7184 			if (S_ISLNK(sb.st_mode)) {
   7185 				sb_dir_h.st_mode = 0;
   7186 				fstatat(fd, namep, &sb_dir_h, 0);
   7187 			} else
   7188 				sb_dir_h = sb;
   7189 
   7190 			if (S_ISDIR(sb_dir_h.st_mode)) {
   7191 				if (sb_path.st_dev == sb_dir_h.st_dev) { // NOLINT
   7192 					mkpath(path, namep, buf); // NOLINT
   7193 					bool first = test_set_bit_dir(sb_dir_h.st_dev, sb_dir_h.st_ino);
   7194 					dirwalk(buf, -1, FALSE, !first);
   7195 
   7196 					if (g_state.interrupt)
   7197 						goto exit;
   7198 				}
   7199 				++num_files; /* Count directories */
   7200 			} else {
   7201 				/* Do not recount hard links */
   7202 				if (sb.st_size && S_ISREG(sb.st_mode) && (sb.st_nlink <= 1 || test_set_bit((uint_t)sb.st_ino)))
   7203 					dir_blocks += (cfg.apparentsz ? sb.st_size : sb.st_blocks);
   7204 				++num_files;
   7205 			}
   7206 
   7207 			continue;
   7208 		}
   7209 
   7210 		if (fstatat(fd, namep, &sb, flags) == -1) {
   7211 			if (flags || (fstatat(fd, namep, &sb, AT_SYMLINK_NOFOLLOW) == -1)) {
   7212 				/* Missing file */
   7213 				DPRINTF_U(flags);
   7214 				if (!flags) {
   7215 					DPRINTF_S(namep);
   7216 					DPRINTF_S(strerror(errno));
   7217 				}
   7218 
   7219 				entflags = FILE_MISSING;
   7220 				memset(&sb, 0, sizeof(struct stat));
   7221 			} else /* Orphaned symlink */
   7222 				entflags = SYM_ORPHAN;
   7223 		}
   7224 
   7225 		if (ndents == total_dents) {
   7226 			if (cfg.blkorder) {
   7227 				pthread_mutex_lock(&running_mutex);
   7228 				while (du_tasks_pending)
   7229 					pthread_cond_wait(&du_cond, &running_mutex);
   7230 				pthread_mutex_unlock(&running_mutex);
   7231 			}
   7232 
   7233 			total_dents += cfg.blkorder ? ENTRY_INCR_DU : ENTRY_INCR;
   7234 			*ppdents = xrealloc(*ppdents, total_dents * sizeof(**ppdents));
   7235 			if (!*ppdents) {
   7236 				free(pnamebuf);
   7237 				closedir(dirp);
   7238 				errexit();
   7239 			}
   7240 			DPRINTF_P(*ppdents);
   7241 		}
   7242 
   7243 		/* If not enough bytes left to copy a file name of length NAME_MAX, re-allocate */
   7244 		if (namebuflen - off < NAME_MAX + 1) {
   7245 			namebuflen += NAMEBUF_INCR;
   7246 
   7247 			pnb = pnamebuf;
   7248 			pnamebuf = (char *)xrealloc(pnamebuf, namebuflen);
   7249 			if (!pnamebuf) {
   7250 				free(*ppdents);
   7251 				closedir(dirp);
   7252 				errexit();
   7253 			}
   7254 			DPRINTF_P(pnamebuf);
   7255 
   7256 			/* realloc() may result in memory move, we must re-adjust if that happens */
   7257 			if (pnb != pnamebuf) {
   7258 				dentp = *ppdents;
   7259 				dentp->name = pnamebuf;
   7260 
   7261 				for (int count = 1; count < ndents; ++dentp, ++count)
   7262 					/* Current file name starts at last file name start + length */
   7263 					(dentp + 1)->name = (char *)((size_t)dentp->name + dentp->nlen);
   7264 			}
   7265 		}
   7266 
   7267 		dentp = *ppdents + ndents;
   7268 
   7269 		/* Selection file name */
   7270 		dentp->name = (char *)((size_t)pnamebuf + off);
   7271 		dentp->nlen = xstrsncpy(dentp->name, namep, NAME_MAX + 1);
   7272 		off += dentp->nlen;
   7273 
   7274 		/* Copy other fields */
   7275 		if (cfg.timetype == T_MOD) {
   7276 			dentp->sec = sb.st_mtime;
   7277 			dentp->nsec = NSEC_MTIME(sb);
   7278 		} else if (cfg.timetype == T_ACCESS) {
   7279 			dentp->sec = sb.st_atime;
   7280 			dentp->nsec = NSEC_ATIME(sb);
   7281 		} else {
   7282 			dentp->sec = sb.st_ctime;
   7283 			dentp->nsec = NSEC_CTIME(sb);
   7284 		}
   7285 
   7286 		if ((gtimesecs - sb.st_mtime <= 300) || (gtimesecs - sb.st_ctime <= 300))
   7287 			entflags |= FILE_YOUNG;
   7288 
   7289 #if !(defined(__sun) || defined(__HAIKU__))
   7290 		if (!flags && dp->d_type == DT_LNK) {
   7291 			 /* Do not add sizes for links */
   7292 			dentp->mode = (sb.st_mode & ~S_IFMT) | S_IFLNK;
   7293 			dentp->size = listpath ? sb.st_size : 0;
   7294 		} else {
   7295 			dentp->mode = sb.st_mode;
   7296 			dentp->size = sb.st_size;
   7297 		}
   7298 #else
   7299 		dentp->mode = sb.st_mode;
   7300 		dentp->size = sb.st_size;
   7301 #endif
   7302 
   7303 #ifndef NOUG
   7304 		dentp->uid = sb.st_uid;
   7305 		dentp->gid = sb.st_gid;
   7306 #endif
   7307 
   7308 		dentp->flags = S_ISDIR(sb.st_mode) ? 0 : ((sb.st_nlink > 1) ? HARD_LINK : 0);
   7309 		if (entflags) {
   7310 			dentp->flags |= entflags;
   7311 			entflags = 0;
   7312 		}
   7313 
   7314 		if (cfg.blkorder) {
   7315 			/* Use resolved (dev,ino) for duplicate check so symlink-to-dir and real dir count once when at / */
   7316 			struct stat sb_dir;
   7317 			if (S_ISLNK(sb.st_mode)) {
   7318 				sb_dir.st_mode = 0;
   7319 				fstatat(fd, namep, &sb_dir, 0);
   7320 			} else
   7321 				sb_dir = sb;
   7322 
   7323 			if (S_ISDIR(sb_dir.st_mode)) {
   7324 				mkpath(path, namep, buf); // NOLINT
   7325 
   7326 				/* Need to show the disk usage of this dir; skip adding to totals if same dir already dispatched (e.g. symlink) */
   7327 				bool first = test_set_bit_dir(sb_dir.st_dev, sb_dir.st_ino);
   7328 				dirwalk(buf, ndents, (sb_path.st_dev != sb_dir.st_dev), !first); // NOLINT
   7329 
   7330 				if (g_state.interrupt)
   7331 					goto exit;
   7332 				++num_files; /* Count directories */
   7333 			} else {
   7334 				dentp->blocks = (cfg.apparentsz ? sb.st_size : sb.st_blocks);
   7335 				/* Do not recount hard links */
   7336 				if (sb.st_size && S_ISREG(sb.st_mode) && (sb.st_nlink <= 1 || test_set_bit((uint_t)sb.st_ino)))
   7337 					dir_blocks += dentp->blocks;
   7338 				++num_files;
   7339 			}
   7340 		}
   7341 
   7342 		if (flags) {
   7343 			/* Flag if this is a dir or symlink to a dir */
   7344 			if (S_ISLNK(sb.st_mode)) {
   7345 				sb.st_mode = 0;
   7346 				fstatat(fd, namep, &sb, 0);
   7347 			}
   7348 
   7349 			if (S_ISDIR(sb.st_mode))
   7350 				dentp->flags |= DIR_OR_DIRLNK;
   7351 #if !(defined(__sun) || defined(__HAIKU__)) /* no d_type */
   7352 		} else if (dp->d_type == DT_DIR || ((dp->d_type == DT_LNK
   7353 			   || dp->d_type == DT_UNKNOWN) && S_ISDIR(sb.st_mode))) {
   7354 			dentp->flags |= DIR_OR_DIRLNK;
   7355 #endif
   7356 		}
   7357 
   7358 		++ndents;
   7359 	} while ((dp = readdir(dirp)));
   7360 
   7361 exit:
   7362 	if (g_state.duinit && cfg.blkorder) {
   7363 		pthread_mutex_lock(&running_mutex);
   7364 		while (du_tasks_pending)
   7365 			pthread_cond_wait(&du_cond, &running_mutex);
   7366 		pthread_mutex_unlock(&running_mutex);
   7367 
   7368 		attroff(COLOR_PAIR(cfg.curctx + 1));
   7369 		for (int i = 0; i < num_du_threads; ++i) {
   7370 			num_files += core_files[i];
   7371 			dir_blocks += core_blocks[i];
   7372 		}
   7373 	}
   7374 
   7375 	/* Should never be null */
   7376 	if (closedir(dirp) == -1)
   7377 		errexit();
   7378 
   7379 	return ndents;
   7380 }
   7381 
   7382 static void populate(char *path, char *lastname)
   7383 {
   7384 #ifdef DEBUG
   7385 	struct timespec ts1, ts2;
   7386 
   7387 	clock_gettime(CLOCK_REALTIME, &ts1); /* Use CLOCK_MONOTONIC on FreeBSD */
   7388 #endif
   7389 
   7390 	ndents = dentfill(path, &pdents);
   7391 	if (!ndents)
   7392 		return;
   7393 
   7394 #ifndef NOSORT
   7395 	ENTSORT(pdents, ndents, entrycmpfn);
   7396 #endif
   7397 
   7398 #ifdef DEBUG
   7399 	clock_gettime(CLOCK_REALTIME, &ts2);
   7400 	DPRINTF_U(ts2.tv_nsec - ts1.tv_nsec);
   7401 #endif
   7402 
   7403 	/* Find cur from history */
   7404 	/* No NULL check for lastname, always points to an array */
   7405 	move_cursor(*lastname ? dentfind(lastname, ndents) : 0, 0);
   7406 
   7407 	// Force full redraw
   7408 	last_curscroll = -1;
   7409 }
   7410 
   7411 #ifndef NOFIFO
   7412 static void notify_fifo(bool force)
   7413 {
   7414 	if (!fifopath)
   7415 		return;
   7416 
   7417 	if (fifofd == -1) {
   7418 		fifofd = open(fifopath, O_WRONLY|O_NONBLOCK|O_CLOEXEC);
   7419 		if (fifofd == -1) {
   7420 			if (errno != ENXIO)
   7421 				/* Unexpected error, the FIFO file might have been removed */
   7422 				/* We give up FIFO notification */
   7423 				fifopath = NULL;
   7424 			return;
   7425 		}
   7426 	}
   7427 
   7428 	static struct entry lastentry;
   7429 
   7430 	if (!force && !memcmp(&lastentry, &pdents[cur], sizeof(struct entry))) // NOLINT
   7431 		return;
   7432 
   7433 	lastentry = pdents[cur];
   7434 
   7435 	char path[PATH_MAX];
   7436 	size_t len = mkpath(g_ctx[cfg.curctx].c_path, ndents ? pdents[cur].name : "", path);
   7437 
   7438 	path[len - 1] = '\n';
   7439 
   7440 	ssize_t ret = write(fifofd, path, len);
   7441 
   7442 	if (ret != (ssize_t)len && !(ret == -1 && (errno == EAGAIN || errno == EPIPE))) {
   7443 		DPRINTF_S(strerror(errno));
   7444 	}
   7445 }
   7446 
   7447 static void send_to_explorer(int *presel)
   7448 {
   7449 	if (nselected) {
   7450 		int fd = open(fifopath, O_WRONLY|O_NONBLOCK|O_CLOEXEC, 0600);
   7451 		if ((fd == -1) || (seltofile(fd, NULL, NEWLINE) != (size_t)(selbufpos)))
   7452 			printwarn(presel);
   7453 		else {
   7454 			resetselind();
   7455 			clearselection();
   7456 		}
   7457 		if (fd > 1)
   7458 			close(fd);
   7459 	} else
   7460 		notify_fifo(TRUE); /* Send opened path to NNN_FIFO */
   7461 }
   7462 #endif
   7463 
   7464 static void move_cursor(int target, int ignore_scrolloff)
   7465 {
   7466 	target = MAX(0, MIN(ndents - 1, target));
   7467 	last_curscroll = curscroll;
   7468 	last = cur;
   7469 	cur = target;
   7470 
   7471 	if (!ignore_scrolloff) {
   7472 		int delta = target - last;
   7473 		int scrolloff = MIN(SCROLLOFF, ONSCREEN >> 1);
   7474 
   7475 		/*
   7476 		 * When ignore_scrolloff is 1, the cursor can jump into the scrolloff
   7477 		 * margin area, but when ignore_scrolloff is 0, act like a boa
   7478 		 * constrictor and squeeze the cursor towards the middle region of the
   7479 		 * screen by allowing it to move inward and disallowing it to move
   7480 		 * outward (deeper into the scrolloff margin area).
   7481 		 */
   7482 		if (((cur < (curscroll + scrolloff)) && delta < 0)
   7483 		    || ((cur > (curscroll + ONSCREEN - scrolloff - 1)) && delta > 0))
   7484 			curscroll += delta;
   7485 	}
   7486 	curscroll = MIN(curscroll, MIN(cur, ndents - ONSCREEN));
   7487 	curscroll = MAX(curscroll, MAX(cur - (ONSCREEN - 1), 0));
   7488 
   7489 #ifndef NOFIFO
   7490 	if (!g_state.fifomode)
   7491 		notify_fifo(FALSE); /* Send hovered path to NNN_FIFO */
   7492 #endif
   7493 }
   7494 
   7495 static void handle_screen_move(enum action sel)
   7496 {
   7497 	switch (sel) {
   7498 	case SEL_NEXT:
   7499 		if (cfg.rollover || (cur != ndents - 1))
   7500 			move_cursor((cur + 1) % ndents, 0);
   7501 		break;
   7502 	case SEL_PREV:
   7503 		if (cfg.rollover || cur)
   7504 			move_cursor((cur + ndents - 1) % ndents, 0);
   7505 		break;
   7506 	case SEL_PGDN:
   7507 		move_cursor(curscroll + (ONSCREEN - 1), 1);
   7508 		curscroll += ONSCREEN - 1;
   7509 		break;
   7510 	case SEL_CTRL_D:
   7511 		move_cursor(curscroll + (ONSCREEN - 1), 1);
   7512 		curscroll += ONSCREEN >> 1;
   7513 		break;
   7514 	case SEL_PGUP:
   7515 		move_cursor(curscroll, 1);
   7516 		curscroll -= ONSCREEN - 1;
   7517 		break;
   7518 	case SEL_CTRL_U:
   7519 		move_cursor(curscroll, 1);
   7520 		curscroll -= ONSCREEN >> 1;
   7521 		break;
   7522 	case SEL_JUMP:
   7523 	{
   7524 		char *input = xreadline(NULL, "jump (+n/-n/n): ");
   7525 
   7526 		if (!input || !*input)
   7527 			break;
   7528 		if (input[0] == '-') {
   7529 			cur -= atoi(input + 1);
   7530 			if (cur < 0)
   7531 				cur = 0;
   7532 		} else if (input[0] == '+') {
   7533 			cur += atoi(input + 1);
   7534 			if (cur >= ndents)
   7535 				cur = ndents - 1;
   7536 		} else {
   7537 			int index = atoi(input);
   7538 
   7539 			if ((index < 1) || (index > ndents))
   7540 				break;
   7541 			cur = index - 1;
   7542 		}
   7543 		move_cursor(cur, 1);
   7544 		curscroll -= ONSCREEN >> 1;
   7545 		break;
   7546 	}
   7547 	case SEL_HOME:
   7548 		move_cursor(0, 1);
   7549 		break;
   7550 	case SEL_END:
   7551 		move_cursor(ndents - 1, 1);
   7552 		break;
   7553 	case SEL_YOUNG:
   7554 	{
   7555 		for (int r = cur;;) {
   7556 			if (++r >= ndents)
   7557 				r = 0;
   7558 			if (r == cur)
   7559 				break;
   7560 			if (pdents[r].flags & FILE_YOUNG) {
   7561 				move_cursor(r, 0);
   7562 				break;
   7563 			}
   7564 		}
   7565 		break;
   7566 	}
   7567 	default: /* case SEL_FIRST */
   7568 	{
   7569 		int c = get_input(messages[MSG_FIRST]);
   7570 
   7571 		if (!c)
   7572 			break;
   7573 
   7574 		c = TOUPPER(c);
   7575 
   7576 		int r = (c == TOUPPER(*pdents[cur].name)) ? (cur + 1) : 0;
   7577 
   7578 		for (; r < ndents; ++r) {
   7579 			if (((c == '\'') && !(pdents[r].flags & DIR_OR_DIRLNK))
   7580 			    || (c == TOUPPER(*pdents[r].name))) {
   7581 				move_cursor((r) % ndents, 0);
   7582 				break;
   7583 			}
   7584 		}
   7585 		break;
   7586 	}
   7587 	}
   7588 }
   7589 
   7590 static void handle_openwith(const char *path, const char *name, char *newpath, char *tmp)
   7591 {
   7592 	/* Confirm if app is CLI or GUI */
   7593 	int r = get_input(messages[MSG_CLI_MODE]);
   7594 
   7595 	r = (r == 'c' ? F_CLI :
   7596 	     ((r == 'g' || r == '\r') ? (F_NOWAIT | F_NOTRACE | F_MULTI) : 0));
   7597 	if (r) {
   7598 		mkpath(path, name, newpath);
   7599 		spawn(tmp, newpath, NULL, NULL, r);
   7600 	}
   7601 }
   7602 
   7603 static void copynextname(char *lastname)
   7604 {
   7605 	if (cur) {
   7606 		cur += (cur != (ndents - 1)) ? 1 : -1;
   7607 		copycurname();
   7608 	} else
   7609 		lastname[0] = '\0';
   7610 }
   7611 
   7612 static int handle_context_switch(enum action sel)
   7613 {
   7614 	int r = -1;
   7615 
   7616 	switch (sel) {
   7617 	case SEL_CYCLE: // fallthrough
   7618 	case SEL_CYCLER:
   7619 		/* visit next and previous contexts */
   7620 		r = cfg.curctx;
   7621 		if (sel == SEL_CYCLE)
   7622 			do
   7623 				r = (r + 1) & ~CTX_MAX;
   7624 			while (!g_ctx[r].c_cfg.ctxactive);
   7625 		else {
   7626 			do /* Attempt to create a new context */
   7627 				r = (r + 1) & ~CTX_MAX;
   7628 			while (g_ctx[r].c_cfg.ctxactive && (r != cfg.curctx));
   7629 
   7630 			if (r == cfg.curctx) /* If all contexts are active, reverse cycle */
   7631 				do
   7632 					r = (r + (CTX_MAX - 1)) & (CTX_MAX - 1);
   7633 				while (!g_ctx[r].c_cfg.ctxactive);
   7634 		} // fallthrough
   7635 	default: /* SEL_CTXN */
   7636 		if (sel >= SEL_CTX1) /* CYCLE keys are lesser in value */
   7637 			r = sel - SEL_CTX1; /* Save the next context id */
   7638 
   7639 		if (cfg.curctx == r) {
   7640 			if (sel == SEL_CYCLE)
   7641 				(r == CTX_MAX - 1) ? (r = 0) : ++r;
   7642 			else if (sel == SEL_CYCLER)
   7643 				(r == 0) ? (r = CTX_MAX - 1) : --r;
   7644 			else
   7645 				return -1;
   7646 		}
   7647 	}
   7648 
   7649 	return r;
   7650 }
   7651 
   7652 static void reset_sort_flags(void)
   7653 {
   7654 	cfg.timeorder = 0;
   7655 	cfg.sizeorder = 0;
   7656 	cfg.apparentsz = 0;
   7657 	cfg.blkorder = 0;
   7658 	cfg.extnorder = 0;
   7659 }
   7660 
   7661 static int set_sort_flags(int r)
   7662 {
   7663 	bool session = (r == '\0');
   7664 	bool reverse = FALSE;
   7665 
   7666 	if (ISUPPER_(r) && (r != 'R') && (r != 'C')) {
   7667 		reverse = TRUE;
   7668 		r = TOLOWER(r);
   7669 	}
   7670 
   7671 	/* Set the correct input in case of a session load */
   7672 	if (session) {
   7673 		if (cfg.apparentsz) {
   7674 			cfg.apparentsz = 0;
   7675 			r = 'a';
   7676 		} else if (cfg.blkorder) {
   7677 			cfg.blkorder = 0;
   7678 			r = 'd';
   7679 		}
   7680 
   7681 		/* Ensure function pointers are in sync with cfg. */
   7682 		entrycmpfn = cfg.reverse ? &reventrycmp : &entrycmp;
   7683 		namecmpfn = cfg.version ? &xstrverscasecmp : &xstricmp;
   7684 	} else if (r == CONTROL('T')) {
   7685 		/* Cycling order: clear -> size -> time -> clear */
   7686 		if (cfg.timeorder)
   7687 			r = 's';
   7688 		else if (cfg.sizeorder)
   7689 			r = 'c';
   7690 		else
   7691 			r = 't';
   7692 	}
   7693 
   7694 	switch (r) {
   7695 	case 'a': /* Apparent du */
   7696 		cfg.apparentsz ^= 1;
   7697 		if (cfg.apparentsz) {
   7698 			cfg.blkorder = 1;
   7699 			blk_shift = 0;
   7700 		} else
   7701 			cfg.blkorder = 0;
   7702 		// fallthrough
   7703 	case 'd': /* Disk usage */
   7704 		if (r == 'd') {
   7705 			if (!cfg.apparentsz)
   7706 				cfg.blkorder ^= 1;
   7707 			cfg.apparentsz = 0;
   7708 			blk_shift = ffs(S_BLKSIZE) - 1;
   7709 		}
   7710 
   7711 		if (cfg.blkorder)
   7712 			cfg.showdetail = 1;
   7713 		cfg.timeorder = 0;
   7714 		cfg.sizeorder = 0;
   7715 		cfg.extnorder = 0;
   7716 		if (!session) {
   7717 			cfg.reverse = 0;
   7718 			entrycmpfn = &entrycmp;
   7719 		}
   7720 		endselection(TRUE); /* We are going to reload dir */
   7721 		break;
   7722 	case 'c':
   7723 		reset_sort_flags();
   7724 		cfg.reverse = 0;
   7725 		cfg.version = 0;
   7726 		entrycmpfn = &entrycmp;
   7727 		namecmpfn = &xstricmp;
   7728 		break;
   7729 	case 'e': /* File extension */ {
   7730 		bool val = cfg.extnorder ^ 1;
   7731 		reset_sort_flags();
   7732 		cfg.extnorder = val;
   7733 		cfg.reverse = 0;
   7734 		entrycmpfn = &entrycmp;
   7735 		break;
   7736 	}
   7737 	case 'r': /* Reverse sort */
   7738 		cfg.reverse ^= 1;
   7739 		entrycmpfn = cfg.reverse ? &reventrycmp : &entrycmp;
   7740 		break;
   7741 	case 's': /* File size */ {
   7742 		bool val = cfg.sizeorder ^ 1;
   7743 		reset_sort_flags();
   7744 		cfg.sizeorder = val;
   7745 		cfg.reverse = 0;
   7746 		entrycmpfn = &entrycmp;
   7747 		break;
   7748 	}
   7749 	case 't': /* Time */ {
   7750 		bool val = cfg.timeorder ^ 1;
   7751 		reset_sort_flags();
   7752 		cfg.timeorder = val;
   7753 		cfg.reverse = 0;
   7754 		entrycmpfn = &entrycmp;
   7755 		break;
   7756 	}
   7757 	case 'v': /* Version */
   7758 		cfg.version ^= 1;
   7759 		namecmpfn = cfg.version ? &xstrverscasecmp : &xstricmp;
   7760 		reset_sort_flags();
   7761 		break;
   7762 	default:
   7763 		return 0;
   7764 	}
   7765 
   7766 	if (reverse) {
   7767 		cfg.reverse = 1;
   7768 		entrycmpfn = &reventrycmp;
   7769 	}
   7770 
   7771 	cfgsort[cfg.curctx] = (uchar_t)r;
   7772 
   7773 	return r;
   7774 }
   7775 
   7776 static bool set_time_type(int *presel)
   7777 {
   7778 	bool ret = FALSE;
   7779 	char buf[] = "'a'ccess / 'c'hange / 'm'od [ ]";
   7780 
   7781 	buf[sizeof(buf) - 3] = cfg.timetype == T_MOD ? 'm' : (cfg.timetype == T_ACCESS ? 'a' : 'c');
   7782 
   7783 	int r = get_input(buf);
   7784 
   7785 	if (r == 'a' || r == 'c' || r == 'm') {
   7786 		r = (r == 'm') ? T_MOD : ((r == 'a') ? T_ACCESS : T_CHANGE);
   7787 		if (cfg.timetype != r) {
   7788 			cfg.timetype = r;
   7789 
   7790 			if (cfg.filtermode || g_ctx[cfg.curctx].c_fltr[1])
   7791 				*presel = FILTER;
   7792 
   7793 			ret = TRUE;
   7794 		} else
   7795 			r = MSG_NOCHANGE;
   7796 	} else
   7797 		r = MSG_INVALID_KEY;
   7798 
   7799 	if (!ret)
   7800 		printwait(messages[r], presel);
   7801 
   7802 	return ret;
   7803 }
   7804 
   7805 static void statusbar(char *path)
   7806 {
   7807 	int i = 0, len = 0;
   7808 	char *ptr;
   7809 	pEntry pent = &pdents[cur];
   7810 
   7811 	if (!ndents) {
   7812 		printmsg("0/0");
   7813 		return;
   7814 	}
   7815 
   7816 	/* Get the file extension for regular files */
   7817 	if (S_ISREG(pent->mode)) {
   7818 		i = (int)(pent->nlen - 1);
   7819 		ptr = xextension(pent->name, i);
   7820 		if (ptr)
   7821 			len = i - (ptr - pent->name);
   7822 		if (!ptr || len > 5 || len < 2)
   7823 			ptr = "\b";
   7824 	} else
   7825 		ptr = "\b";
   7826 
   7827 	attron(COLOR_PAIR(cfg.curctx + 1));
   7828 
   7829 	if (cfg.fileinfo && get_output("file", "-b", pdents[cur].name, -1, FALSE))
   7830 		mvaddstr(xlines - 2, 2, g_buf);
   7831 
   7832 	tolastln();
   7833 
   7834 	printw("%d/%d ", cur + 1, ndents);
   7835 
   7836 	if (g_state.selmode || nselected) {
   7837 		attron(A_REVERSE);
   7838 		addch(' ');
   7839 		if (g_state.rangesel)
   7840 			addch('*');
   7841 		else if (g_state.selmode)
   7842 			addch('+');
   7843 		if (nselected)
   7844 			addstr(xitoa(nselected));
   7845 		addch(' ');
   7846 		attroff(A_REVERSE);
   7847 		addch(' ');
   7848 	}
   7849 
   7850 	if (cfg.blkorder) { /* du mode */
   7851 		char buf[24];
   7852 
   7853 		xstrsncpy(buf, coolsize(dir_blocks << blk_shift), 12);
   7854 
   7855 		printw("%cu:%s avail:%s files:%llu %lluB %s\n",
   7856 		       (cfg.apparentsz ? 'a' : 'd'), buf, coolsize(get_fs_info(path, VFS_AVAIL)),
   7857 		       num_files, (ullong_t)pent->blocks << blk_shift, ptr);
   7858 	} else { /* light or detail mode */
   7859 		char sort[] = "\0\0\0\0\0";
   7860 
   7861 		if (cfg.filtermode)
   7862 			addch('F');
   7863 
   7864 		getorderstr(sort) ? addstr(sort) : addch(' ');
   7865 
   7866 		/* Timestamp */
   7867 		print_time(&pent->sec, pent->flags);
   7868 
   7869 		addch(' ');
   7870 		addstr(get_lsperms(pent->mode));
   7871 		addch(' ');
   7872 #ifndef NOUG
   7873 		if (g_state.uidgid) {
   7874 			addstr(getpwname(pent->uid));
   7875 			addch(':');
   7876 			addstr(getgrname(pent->gid));
   7877 			addch(' ');
   7878 		}
   7879 #endif
   7880 		if (S_ISLNK(pent->mode)) {
   7881 			if (!cfg.fileinfo) {
   7882 				i = readlink(pent->name, g_buf, PATH_MAX);
   7883 				addstr(coolsize(i >= 0 ? i : pent->size)); /* Show symlink size */
   7884 				if (i > 1) { /* Show symlink target */
   7885 					int y;
   7886 
   7887 					addstr(" ->");
   7888 					getyx(stdscr, len, y);
   7889 					i = MIN(i, xcols - y);
   7890 					g_buf[i] = '\0';
   7891 					addstr(g_buf);
   7892 				}
   7893 			}
   7894 		} else {
   7895 			addstr(coolsize(pent->size));
   7896 			addch(' ');
   7897 			addstr(ptr);
   7898 			if (pent->flags & HARD_LINK) {
   7899 				struct stat sb;
   7900 
   7901 				if (stat(pent->name, &sb) != -1) {
   7902 					addch(' ');
   7903 					addstr(xitoa((int)sb.st_nlink)); /* Show number of links */
   7904 					addch('-');
   7905 					addstr(xitoa((int)sb.st_ino)); /* Show inode number */
   7906 				}
   7907 			}
   7908 		}
   7909 		clrtoeol();
   7910 	}
   7911 
   7912 	attroff(COLOR_PAIR(cfg.curctx + 1));
   7913 	/* Place HW cursor on current for Braille systems */
   7914 	tocursor();
   7915 }
   7916 
   7917 static inline void markhovered(void)
   7918 {
   7919 	if (cfg.showdetail && ndents) { /* Bold forward arrowhead */
   7920 		tocursor();
   7921 		addch('>' | A_BOLD);
   7922 	}
   7923 }
   7924 
   7925 #define PREVIEW_BORDER_COL (xcols / 2)
   7926 #define PREVIEW_COL        (PREVIEW_BORDER_COL + 2)
   7927 #define PREVIEW_WIDTH      (xcols - PREVIEW_COL - 1)
   7928 #define MIN_PREVIEW_COLS   40
   7929 #define PREVIEW_MAX_LINE   4096
   7930 
   7931 /* Check if a file is likely text by reading initial bytes */
   7932 static bool is_text_file(const char *fpath)
   7933 {
   7934 	int fd = open(fpath, O_RDONLY);
   7935 	if (fd == -1)
   7936 		return FALSE;
   7937 
   7938 	unsigned char buf[512];
   7939 	ssize_t n = read(fd, buf, sizeof(buf));
   7940 	close(fd);
   7941 
   7942 	if (n <= 0)
   7943 		return FALSE;
   7944 
   7945 	/* Reject known binary magic signatures */
   7946 	static const struct {
   7947 		const unsigned char bytes[8];
   7948 		unsigned char len;
   7949 	} magics[] = {
   7950 		{ { '%',  'P',  'D',  'F'              }, 4 }, /* PDF */
   7951 		{ { 0x89, 'P',  'N',  'G'              }, 4 }, /* PNG */
   7952 		{ { 'G',  'I',  'F',  '8'              }, 4 }, /* GIF */
   7953 		{ { 0xFF, 0xD8, 0xFF                   }, 3 }, /* JPEG */
   7954 		{ { 'P',  'K',  0x03, 0x04             }, 4 }, /* ZIP */
   7955 		{ { 0x7F, 'E',  'L',  'F'              }, 4 }, /* ELF */
   7956 		{ { 0x1F, 0x8B                         }, 2 }, /* Gzip */
   7957 		{ { 'B',  'M'                          }, 2 }, /* BMP */
   7958 		{ { 'R',  'I',  'F',  'F'              }, 4 }, /* RIFF */
   7959 		{ { 'O',  'g',  'g',  'S'              }, 4 }, /* OGG */
   7960 		{ { 'f',  'L',  'a',  'C'              }, 4 }, /* FLAC */
   7961 		{ { 'I',  'D',  '3'                    }, 3 }, /* MP3 ID3 */
   7962 		{ { 0xFE, 0xED, 0xFA                   }, 3 }, /* Mach-O */
   7963 		{ { 0xFD, '7',  'z',  'X',  'Z',  0x00 }, 6 }, /* XZ */
   7964 		{ { '7',  'z',  0xBC, 0xAF, 0x27, 0x1C }, 6 }, /* 7z */
   7965 		{ { 'S',  'Q',  'L',  'i',  't',  'e'  }, 6 }, /* SQLite */
   7966 	};
   7967 
   7968 	for (size_t i = 0; i < sizeof(magics) / sizeof(magics[0]); ++i) {
   7969 		if (n >= magics[i].len
   7970 		    && !memcmp(buf, magics[i].bytes, magics[i].len))
   7971 			return FALSE;
   7972 	}
   7973 
   7974 	/* MP3 sync word (needs mask check) */
   7975 	if (n >= 2 && buf[0] == 0xFF && (buf[1] & 0xE0) == 0xE0)
   7976 		return FALSE;
   7977 
   7978 	/* Check for NUL bytes and non-text control characters */
   7979 	for (ssize_t i = 0; i < n; ++i) {
   7980 		unsigned char c = buf[i];
   7981 		if (c == '\0')
   7982 			return FALSE;
   7983 		/* Reject most control chars except common text ones */
   7984 		if (c < 0x20 && c != '\n' && c != '\r' && c != '\t'
   7985 		    && c != '\f' && c != '\033') /* ESC for ANSI */
   7986 			return FALSE;
   7987 	}
   7988 
   7989 	return TRUE;
   7990 }
   7991 
   7992 /* Draw the preview pane for the currently hovered file */
   7993 static void preview_pane(const char *path)
   7994 {
   7995 	if (!ndents || !cfg.preview)
   7996 		return;
   7997 
   7998 	int previewcol = PREVIEW_BORDER_COL;
   7999 	int previewwidth = PREVIEW_WIDTH;
   8000 
   8001 	if (previewwidth < MIN_PREVIEW_COLS)
   8002 		return;
   8003 
   8004 	/* Draw vertical border */
   8005 	for (int i = 0; i < xlines - 1; ++i) {
   8006 		move(i, previewcol);
   8007 		addch(ACS_VLINE | A_DIM);
   8008 	}
   8009 
   8010 	char fpath[PATH_MAX];
   8011 	mkpath(path, pdents[cur].name, fpath);
   8012 
   8013 	/* Auto-detect .npreview plugin */
   8014 	if (!previewer) {
   8015 		previewer = malloc(xstrlen(plgpath) + xstrlen(utils[UTIL_NPREVIEW]) + 1);
   8016 		mkpath(plgpath, utils[UTIL_NPREVIEW], previewer);
   8017 		if (access(previewer, X_OK)) {
   8018 			free(previewer);
   8019 			previewer = NULL;
   8020 		}
   8021 	}
   8022 
   8023 	if (previewer) {
   8024 		int pipefd[2];
   8025 		if (pipe(pipefd) == -1)
   8026 			return;
   8027 
   8028 		pid_t pid = fork();
   8029 		if (pid == 0) {
   8030 			close(pipefd[0]);
   8031 			dup2(pipefd[1], STDOUT_FILENO);
   8032 			dup2(pipefd[1], STDERR_FILENO);
   8033 			close(pipefd[1]);
   8034 
   8035 			char widthbuf[16], heightbuf[16];
   8036 			snprintf(widthbuf, sizeof(widthbuf), "%d", previewwidth);
   8037 			snprintf(heightbuf, sizeof(heightbuf), "%d", xlines - 2);
   8038 
   8039 			execlp(previewer, previewer, fpath,
   8040 			       widthbuf, heightbuf,
   8041 			       (char *)NULL);
   8042 			_exit(EXIT_FAILURE);
   8043 		}
   8044 		close(pipefd[1]);
   8045 
   8046 		if (pid > 0) {
   8047 			FILE *fp = fdopen(pipefd[0], "r");
   8048 			if (fp) {
   8049 				char line[PREVIEW_MAX_LINE];
   8050 				int row = 1;
   8051 				int maxrows = xlines - 2;
   8052 
   8053 				while (row < maxrows && fgets(line, sizeof(line), fp)) {
   8054 					size_t len = xstrlen(line);
   8055 					if (len > 0 && line[len - 1] == '\n')
   8056 						line[--len] = '\0';
   8057 					if (len > 0 && line[len - 1] == '\r')
   8058 						line[--len] = '\0';
   8059 
   8060 					/* Replace tabs with spaces */
   8061 					for (size_t i = 0; i < len; ++i)
   8062 						if (line[i] == '\t')
   8063 							line[i] = ' ';
   8064 
   8065 					/* Wrap long lines across multiple rows */
   8066 					int off = 0;
   8067 					do {
   8068 						char save = '\0';
   8069 						if ((int)(len - off) > previewwidth) {
   8070 							save = line[off + previewwidth];
   8071 							line[off + previewwidth] = '\0';
   8072 						}
   8073 						mvaddstr(row, PREVIEW_COL, line + off);
   8074 						if (save)
   8075 							line[off + previewwidth] = save;
   8076 						++row;
   8077 						off += previewwidth;
   8078 					} while (off < (int)len && row < maxrows);
   8079 				}
   8080 				fclose(fp);
   8081 			} else {
   8082 				close(pipefd[0]);
   8083 			}
   8084 			waitpid(pid, NULL, 0);
   8085 		} else {
   8086 			close(pipefd[0]);
   8087 		}
   8088 		return;
   8089 	}
   8090 
   8091 	struct stat sb;
   8092 	if (lstat(fpath, &sb) == -1)
   8093 		return;
   8094 
   8095 	/* For directories, show entry count */
   8096 	if (S_ISDIR(sb.st_mode)) {
   8097 		DIR *dirp = opendir(fpath);
   8098 		if (!dirp)
   8099 			return;
   8100 
   8101 		int count = 0;
   8102 		struct dirent *dp;
   8103 		char namebuf[PATH_MAX];
   8104 		int maxlines = xlines - 4; /* Leave header and status lines */
   8105 
   8106 		mvaddstr(1, PREVIEW_COL, "[directory]");
   8107 
   8108 		while ((dp = readdir(dirp)) && count < maxlines) {
   8109 			if (dp->d_name[0] == '.' && (!dp->d_name[1]
   8110 			    || (dp->d_name[1] == '.' && !dp->d_name[2])))
   8111 				continue;
   8112 			snprintf(namebuf, sizeof(namebuf), "%.*s",
   8113 				 previewwidth, dp->d_name);
   8114 			mvaddstr(2 + count, PREVIEW_COL, namebuf);
   8115 			++count;
   8116 		}
   8117 		closedir(dirp);
   8118 		return;
   8119 	}
   8120 
   8121 	/* For symlinks, resolve and show target */
   8122 	if (S_ISLNK(sb.st_mode)) {
   8123 		char target[PATH_MAX];
   8124 		ssize_t len = readlink(fpath, target, sizeof(target) - 1);
   8125 		if (len > 0) {
   8126 			target[len] = '\0';
   8127 			mvprintw(1, PREVIEW_COL, "-> %.*s", previewwidth - 3, target);
   8128 		}
   8129 		/* Resolve symlink for further preview */
   8130 		if (stat(fpath, &sb) == -1)
   8131 			return;
   8132 		if (!S_ISREG(sb.st_mode))
   8133 			return;
   8134 	}
   8135 
   8136 	/* For regular files, show content preview if text */
   8137 	if (S_ISREG(sb.st_mode)) {
   8138 		if (sb.st_size == 0) {
   8139 			mvaddstr(1, PREVIEW_COL, "[empty file]");
   8140 			return;
   8141 		}
   8142 
   8143 		if (!is_text_file(fpath)) {
   8144 			char szbuf[32];
   8145 			mvprintw(1, PREVIEW_COL, "[binary %s]",
   8146 				 coolsize(sb.st_size));
   8147 			(void)szbuf;
   8148 			return;
   8149 		}
   8150 
   8151 		FILE *fp = fopen(fpath, "r");
   8152 		if (!fp)
   8153 			return;
   8154 
   8155 		char line[PREVIEW_MAX_LINE];
   8156 		int row = 1;
   8157 		int maxrows = xlines - 2; /* Leave top and bottom lines */
   8158 
   8159 		while (row < maxrows && fgets(line, sizeof(line), fp)) {
   8160 			/* Strip trailing newline */
   8161 			size_t len = xstrlen(line);
   8162 			if (len > 0 && line[len - 1] == '\n')
   8163 				line[--len] = '\0';
   8164 			if (len > 0 && line[len - 1] == '\r')
   8165 				line[--len] = '\0';
   8166 
   8167 			/* Replace tabs with spaces for consistent display */
   8168 			for (size_t i = 0; i < len; ++i)
   8169 				if (line[i] == '\t')
   8170 					line[i] = ' ';
   8171 
   8172 			/* Wrap long lines across multiple rows */
   8173 			int off = 0;
   8174 			do {
   8175 				char save = '\0';
   8176 				if ((int)(len - off) > previewwidth) {
   8177 					save = line[off + previewwidth];
   8178 					line[off + previewwidth] = '\0';
   8179 				}
   8180 				mvaddstr(row, PREVIEW_COL, line + off);
   8181 				if (save)
   8182 					line[off + previewwidth] = save;
   8183 				++row;
   8184 				off += previewwidth;
   8185 			} while (off < (int)len && row < maxrows);
   8186 		}
   8187 		fclose(fp);
   8188 	}
   8189 }
   8190 
   8191 static int adjust_cols(int n)
   8192 {
   8193 	/* Calculate the number of cols available to print entry name */
   8194 #ifdef ICONS_ENABLED
   8195 	n -= (g_state.oldcolor ? 0 : ICON_SIZE + ICON_PADDING_LEFT_LEN + ICON_PADDING_RIGHT_LEN);
   8196 #endif
   8197 	if (cfg.showdetail) {
   8198 		/* Fallback to light mode if less than 35 columns */
   8199 		if (n < 36)
   8200 			cfg.showdetail ^= 1;
   8201 		else /* 2 more accounted for below */
   8202 			n -= 32;
   8203 	}
   8204 
   8205 	/* 2 columns for preceding space and indicator */
   8206 	return (n - 2);
   8207 }
   8208 
   8209 static void draw_line(int ncols)
   8210 {
   8211 	bool dir = FALSE;
   8212 
   8213 	ncols = adjust_cols(ncols);
   8214 
   8215 	if (g_state.oldcolor && (pdents[last].flags & DIR_OR_DIRLNK)) {
   8216 		attron(COLOR_PAIR(cfg.curctx + 1) | A_BOLD);
   8217 		dir = TRUE;
   8218 	}
   8219 
   8220 	move(2 + last - curscroll, 0);
   8221 	macos_icons_hack();
   8222 	printent(last, ncols, FALSE);
   8223 
   8224 	if (g_state.oldcolor && (pdents[cur].flags & DIR_OR_DIRLNK)) {
   8225 		if (!dir)  {/* First file is not a directory */
   8226 			attron(COLOR_PAIR(cfg.curctx + 1) | A_BOLD);
   8227 			dir = TRUE;
   8228 		}
   8229 	} else if (dir) { /* Second file is not a directory */
   8230 		attroff(COLOR_PAIR(cfg.curctx + 1) | A_BOLD);
   8231 		dir = FALSE;
   8232 	}
   8233 
   8234 	move(2 + cur - curscroll, 0);
   8235 	macos_icons_hack();
   8236 	printent(cur, ncols, TRUE);
   8237 
   8238 	/* Must reset e.g. no files in dir */
   8239 	if (dir)
   8240 		attroff(COLOR_PAIR(cfg.curctx + 1) | A_BOLD);
   8241 
   8242 	markhovered();
   8243 }
   8244 
   8245 static void redraw(char *path)
   8246 {
   8247 	getmaxyx(stdscr, xlines, xcols);
   8248 
   8249 	int ncols = (xcols <= PATH_MAX) ? xcols : PATH_MAX;
   8250 	/* Limit listing width when preview pane is active */
   8251 	if (cfg.preview && xcols >= ((MIN_PREVIEW_COLS * 2) + 1)) {
   8252 		int listcols = PREVIEW_BORDER_COL;
   8253 		if (ncols > listcols)
   8254 			ncols = listcols;
   8255 	}
   8256 	int i, j = 1;
   8257 
   8258 	// Fast redraw
   8259 	if (g_state.move) {
   8260 		g_state.move = 0;
   8261 
   8262 		if (ndents && (last_curscroll == curscroll)
   8263 		    && !cfg.preview)
   8264 			return draw_line(ncols);
   8265 	}
   8266 
   8267 	DPRINTF_S(__func__);
   8268 
   8269 	/* Clear screen */
   8270 	erase();
   8271 
   8272 	/* Enforce scroll/cursor invariants */
   8273 	move_cursor(cur, 1);
   8274 
   8275 	/* Fail redraw if < than 10 columns, context info prints 10 chars */
   8276 	if (ncols <= MIN_DISPLAY_COL) {
   8277 		printmsg(messages[MSG_FEW_COLUMNS]);
   8278 		return;
   8279 	}
   8280 
   8281 	//DPRINTF_D(cur);
   8282 	DPRINTF_S(path);
   8283 
   8284 	for (i = 0; i < CTX_MAX; ++i) { /* 8 chars printed for contexts - "1 2 3 4 " */
   8285 		if (!g_ctx[i].c_cfg.ctxactive)
   8286 			addch(i + '1');
   8287 		else
   8288 			addch((i + '1') | (COLOR_PAIR(i + 1) | A_BOLD
   8289 				/* active: underline, current: reverse */
   8290 				| ((cfg.curctx != i) ? A_UNDERLINE : A_REVERSE)));
   8291 
   8292 		addch(' ');
   8293 	}
   8294 
   8295 	attron(A_UNDERLINE | COLOR_PAIR(cfg.curctx + 1));
   8296 
   8297 	/* Print path */
   8298 	bool in_home = set_tilde_in_path(path);
   8299 	char *ptr = in_home ? &path[homelen - 1] : path;
   8300 
   8301 	i = (int)xstrlen(ptr);
   8302 	if ((i + MIN_DISPLAY_COL) <= ncols)
   8303 		addnstr(ptr, ncols - MIN_DISPLAY_COL);
   8304 	else {
   8305 		char *base = xmemrchr((uchar_t *)ptr, '/', i);
   8306 
   8307 		if (in_home) {
   8308 			addch(*ptr);
   8309 			++ptr;
   8310 			i = 1;
   8311 		} else
   8312 			i = 0;
   8313 
   8314 		if (ptr && (base != ptr)) {
   8315 			while (ptr < base) {
   8316 				if (*ptr == '/') {
   8317 					i += 2; /* 2 characters added */
   8318 					if (ncols < i + MIN_DISPLAY_COL) {
   8319 						base = NULL; /* Can't print more characters */
   8320 						break;
   8321 					}
   8322 
   8323 					addch(*ptr);
   8324 					addch(*(++ptr));
   8325 				}
   8326 				++ptr;
   8327 			}
   8328 		}
   8329 
   8330 		if (base)
   8331 			addnstr(base, ncols - (MIN_DISPLAY_COL + i));
   8332 	}
   8333 
   8334 	if (in_home)
   8335 		reset_tilde_in_path(path);
   8336 
   8337 	attroff(A_UNDERLINE | COLOR_PAIR(cfg.curctx + 1));
   8338 
   8339 	/* Go to first entry */
   8340 	if (curscroll > 0) {
   8341 		move(1, 0);
   8342 #ifdef ICONS_ENABLED
   8343 		addstr(ICON_ARROW_UP);
   8344 #else
   8345 		addch('^');
   8346 #endif
   8347 	}
   8348 
   8349 	if (g_state.oldcolor) {
   8350 		attron(COLOR_PAIR(cfg.curctx + 1) | A_BOLD);
   8351 		g_state.dircolor = 1;
   8352 	}
   8353 
   8354 	int onscreen = MIN(ONSCREEN + curscroll, ndents);
   8355 	int len = scanselforpath(path, FALSE);
   8356 
   8357 	ncols = adjust_cols(ncols);
   8358 
   8359 	/* Print listing */
   8360 	for (i = curscroll; i < onscreen; ++i) {
   8361 		move(++j, 0);
   8362 
   8363 		if (len)
   8364 			findmarkentry(len, &pdents[i]);
   8365 
   8366 		printent(i, ncols, i == cur);
   8367 	}
   8368 
   8369 	/* Must reset e.g. no files in dir */
   8370 	if (g_state.dircolor) {
   8371 		attroff(COLOR_PAIR(cfg.curctx + 1) | A_BOLD);
   8372 		g_state.dircolor = 0;
   8373 	}
   8374 
   8375 	/* Go to last entry */
   8376 	if (onscreen < ndents) {
   8377 		move(xlines - 2, 0);
   8378 #ifdef ICONS_ENABLED
   8379 		addstr(ICON_ARROW_DOWN);
   8380 #else
   8381 		addch('v');
   8382 #endif
   8383 	}
   8384 
   8385 	markhovered();
   8386 
   8387 	if (cfg.preview)
   8388 		preview_pane(path);
   8389 }
   8390 
   8391 static bool cdprep(char *lastdir, char *lastname, char *path, char *newpath)
   8392 {
   8393 	if (lastname)
   8394 		lastname[0] =  '\0';
   8395 
   8396 	/* Save last working directory */
   8397 	xstrsncpy(lastdir, path, PATH_MAX);
   8398 
   8399 	/* Save the newly opted dir in path */
   8400 	xstrsncpy(path, newpath, PATH_MAX);
   8401 	DPRINTF_S(path);
   8402 
   8403 	clearfilter();
   8404 	return cfg.filtermode;
   8405 }
   8406 
   8407 static void showselsize(const char *path)
   8408 {
   8409 	off_t sz = 0;
   8410 	int len = scanselforpath(path, FALSE);
   8411 
   8412 	for (int r = 0, selcount = nselected; (r < ndents) && selcount; ++r)
   8413 		if (findinsel(findselpos, len + xstrsncpy(g_sel + len, pdents[r].name, pdents[r].nlen))) {
   8414 			sz += cfg.blkorder ? pdents[r].blocks : pdents[r].size;
   8415 			--selcount;
   8416 		}
   8417 
   8418 	printmsg(coolsize(cfg.blkorder ? sz << blk_shift : sz));
   8419 }
   8420 
   8421 static bool browse(char *ipath, int pkey)
   8422 {
   8423 	alignas(max_align_t) char newpath[PATH_MAX];
   8424 	alignas(max_align_t) char runfile[NAME_MAX + 1];
   8425 	char *path, *lastdir, *lastname, *dir, *tmp;
   8426 	pEntry pent;
   8427 	enum action sel;
   8428 	struct stat sb;
   8429 	int r = -1, presel, selstartid = 0, selendid = 0;
   8430 	const uchar_t opener_flags = (cfg.cliopener ? F_CLI : (F_NOTRACE | F_NOSTDIN | F_NOWAIT));
   8431 	bool watch = FALSE, cd = TRUE;
   8432 	ino_t inode = 0;
   8433 
   8434 #ifndef NOMOUSE
   8435 	MEVENT event = {0};
   8436 	struct timespec mousetimings[2] = {{.tv_sec = 0, .tv_nsec = 0}, {.tv_sec = 0, .tv_nsec = 0}};
   8437 	int mousedent[2] = {-1, -1};
   8438 	bool currentmouse = 1, rightclicksel = 0;
   8439 #endif
   8440 
   8441 	atexit(dentfree);
   8442 
   8443 	getmaxyx(stdscr, xlines, xcols);
   8444 
   8445 #ifndef NOSSN
   8446 	/* set-up first context */
   8447 	if (!curssn[0] || !load_session(curssn, &path, &lastdir, &lastname, FALSE)) {
   8448 #endif
   8449 		g_ctx[0].c_last[0] = '\0';
   8450 		lastdir = g_ctx[0].c_last; /* last visited directory */
   8451 
   8452 		if (g_state.initfile) {
   8453 			xstrsncpy(g_ctx[0].c_name, xbasename(ipath), sizeof(g_ctx[0].c_name));
   8454 			xdirname(ipath);
   8455 		} else
   8456 			g_ctx[0].c_name[0] = '\0';
   8457 
   8458 		lastname = g_ctx[0].c_name; /* last visited file name */
   8459 
   8460 		xstrsncpy(g_ctx[0].c_path, ipath, PATH_MAX);
   8461 		/* If the initial path is a file, retain a way to return to start dir */
   8462 		if (g_state.initfile) {
   8463 			free(initpath);
   8464 			initpath = ipath = getcwd(NULL, 0); // NOLINT
   8465 			if (!initpath)
   8466 				return FALSE;
   8467 		}
   8468 		path = g_ctx[0].c_path; /* current directory */
   8469 
   8470 		g_ctx[0].c_fltr[0] = g_ctx[0].c_fltr[1] = '\0';
   8471 		g_ctx[0].c_cfg = cfg; /* current configuration */
   8472 #ifndef NOSSN
   8473 	}
   8474 #endif
   8475 
   8476 	newpath[0] = runfile[0] = '\0';
   8477 
   8478 	presel = pkey ? ((pkey == CREATE_NEW_KEY) ? 'n' : ';') : (((cfg.filtermode
   8479 #ifndef NOSSN
   8480 			|| (curssn[0] && (g_ctx[cfg.curctx].c_fltr[0] == FILTER
   8481 				|| g_ctx[cfg.curctx].c_fltr[0] == RFILTER)
   8482 				&& g_ctx[cfg.curctx].c_fltr[1])
   8483 #endif
   8484 			) && !cfg.blkorder) ? FILTER : 0);
   8485 
   8486 	pdents = xrealloc(pdents, total_dents * sizeof(struct entry));
   8487 	if (!pdents)
   8488 		errexit();
   8489 
   8490 	/* Allocate buffer to hold names */
   8491 	pnamebuf = (char *)xrealloc(pnamebuf, NAMEBUF_INCR);
   8492 	if (!pnamebuf)
   8493 		errexit();
   8494 
   8495 	/* The following call is added to handle a broken window at start */
   8496 	if (presel == FILTER)
   8497 		handle_key_resize();
   8498 
   8499 begin:
   8500 	/*
   8501 	 * Can fail when permissions change while browsing.
   8502 	 * It's assumed that path IS a directory when we are here.
   8503 	 */
   8504 	if (chdir(path) == -1) {
   8505 		DPRINTF_S("directory inaccessible");
   8506 		valid_parent(path, lastname);
   8507 		setdirwatch();
   8508 	}
   8509 
   8510 #ifndef NOX11
   8511 	xterm_cfg(path);
   8512 #endif
   8513 
   8514 #ifdef LINUX_INOTIFY
   8515 	if ((presel == FILTER || watch) && inotify_wd >= 0) {
   8516 		inotify_rm_watch(inotify_fd, inotify_wd);
   8517 		inotify_wd = -1;
   8518 		watch = FALSE;
   8519 	}
   8520 #elif defined(BSD_KQUEUE)
   8521 	if ((presel == FILTER || watch) && event_fd >= 0) {
   8522 		close(event_fd);
   8523 		event_fd = -1;
   8524 		watch = FALSE;
   8525 	}
   8526 #elif defined(HAIKU_NM)
   8527 	if ((presel == FILTER || watch) && haiku_hnd != NULL) {
   8528 		haiku_stop_watch(haiku_hnd);
   8529 		haiku_nm_active = FALSE;
   8530 		watch = FALSE;
   8531 	}
   8532 #endif
   8533 
   8534 	if (order && cd) {
   8535 		if (cfgsort[cfg.curctx] != '0') {
   8536 			if (cfgsort[cfg.curctx] == 'z')
   8537 				set_sort_flags('c');
   8538 			if ((!cfgsort[cfg.curctx] || (cfgsort[cfg.curctx] == 'c'))
   8539 			    && ((r = get_kv_key(order, path, maxorder, NNN_ORDER)) > 0)) { // NOLINT
   8540 				set_sort_flags(r);
   8541 				cfgsort[cfg.curctx] = 'z';
   8542 			}
   8543 		} else
   8544 			cfgsort[cfg.curctx] = cfgsort[CTX_MAX];
   8545 	}
   8546 	cd = TRUE;
   8547 
   8548 	populate(path, lastname);
   8549 	if (g_state.interrupt) {
   8550 		g_state.interrupt = cfg.apparentsz = cfg.blkorder = 0;
   8551 		blk_shift = BLK_SHIFT_512;
   8552 		presel = CONTROL('L');
   8553 	}
   8554 
   8555 #ifdef LINUX_INOTIFY
   8556 	if (presel != FILTER && inotify_wd == -1)
   8557 		inotify_wd = inotify_add_watch(inotify_fd, path, INOTIFY_MASK);
   8558 #elif defined(BSD_KQUEUE)
   8559 	if (presel != FILTER && event_fd == -1) {
   8560 #ifdef O_EVTONLY
   8561 		event_fd = open(path, O_EVTONLY);
   8562 #else
   8563 		event_fd = open(path, O_RDONLY);
   8564 #endif
   8565 		if (event_fd >= 0)
   8566 			EV_SET(&events_to_monitor[0], event_fd, EVFILT_VNODE,
   8567 			       EV_ADD | EV_CLEAR, KQUEUE_FFLAGS, 0, path);
   8568 	}
   8569 #elif defined(HAIKU_NM)
   8570 	haiku_nm_active = haiku_watch_dir(haiku_hnd, path) == EXIT_SUCCESS;
   8571 #endif
   8572 
   8573 	while (1) {
   8574 		/* Do not do a double redraw in filterentries */
   8575 		if ((presel != FILTER) || !filterset()) {
   8576 			redraw(path);
   8577 			statusbar(path);
   8578 		}
   8579 
   8580 #ifdef BENCH
   8581 		/* Lod and exit for performance profiling e.g. to run 'time nnn -T d /' */
   8582 		return EXIT_SUCCESS;
   8583 #endif
   8584 
   8585 nochange:
   8586 		/* Exit if parent has exited */
   8587 		if (getppid() == 1)
   8588 			_exit(EXIT_FAILURE);
   8589 
   8590 		/* If CWD is deleted or moved or perms changed, find an accessible parent */
   8591 		if (chdir(path) == -1)
   8592 			goto begin;
   8593 
   8594 		/* If STDIN is no longer a tty (closed) we should exit */
   8595 		if (!isatty(STDIN_FILENO) && !g_state.picker)
   8596 			return EXIT_FAILURE;
   8597 
   8598 		sel = nextsel(presel);
   8599 		if (presel)
   8600 			presel = 0;
   8601 
   8602 		switch (sel) {
   8603 #ifndef NOMOUSE
   8604 		case SEL_CLICK:
   8605 			if (getmouse(&event) != OK)
   8606 				goto nochange;
   8607 
   8608 			/* Handle clicking on a context at the top */
   8609 			if (event.bstate == BUTTON1_PRESSED && event.y == 0) {
   8610 				/* Get context from: "[1 2 3 4]..." */
   8611 				r = event.x >> 1;
   8612 
   8613 				/* If clicked after contexts, go to parent */
   8614 				if (r >= CTX_MAX)
   8615 					sel = SEL_BACK;
   8616 				else if (r >= 0 && r != cfg.curctx) {
   8617 					savecurctx(path, ndents ? pdents[cur].name : NULL, r);
   8618 
   8619 					/* Reset the pointers */
   8620 					path = g_ctx[r].c_path;
   8621 					lastdir = g_ctx[r].c_last;
   8622 					lastname = g_ctx[r].c_name;
   8623 
   8624 					setdirwatch();
   8625 					goto begin;
   8626 				}
   8627 			}
   8628 #endif
   8629 			// fallthrough
   8630 		case SEL_BACK:
   8631 #ifndef NOMOUSE
   8632 			if (sel == SEL_BACK) {
   8633 #endif
   8634 				dir = visit_parent(path, newpath, &presel);
   8635 				if (!dir)
   8636 					goto nochange;
   8637 
   8638 				/* Save history */
   8639 				xstrsncpy(lastname, xbasename(path), NAME_MAX + 1);
   8640 
   8641 				cdprep(lastdir, NULL, path, dir) ? (presel = FILTER) : (watch = TRUE);
   8642 				goto begin;
   8643 #ifndef NOMOUSE
   8644 			}
   8645 #endif
   8646 
   8647 #ifndef NOMOUSE
   8648 			/* Middle click action */
   8649 			if (event.bstate == BUTTON2_PRESSED) {
   8650 				presel = middle_click_key;
   8651 				goto nochange;
   8652 			}
   8653 #if NCURSES_MOUSE_VERSION > 1
   8654 			/* Scroll up */
   8655 			if (event.bstate == BUTTON4_PRESSED && ndents && (cfg.rollover || cur)) {
   8656 				move_cursor((!cfg.rollover && cur < scroll_lines
   8657 						? 0 : (cur + ndents - scroll_lines) % ndents), 0);
   8658 				break;
   8659 			}
   8660 
   8661 			/* Scroll down */
   8662 			if (event.bstate == BUTTON5_PRESSED && ndents
   8663 			    && (cfg.rollover || (cur != ndents - 1))) {
   8664 				move_cursor((!cfg.rollover && cur >= ndents - scroll_lines)
   8665 						? (ndents - 1) : ((cur + scroll_lines) % ndents), 0);
   8666 				break;
   8667 			}
   8668 #endif
   8669 
   8670 			/* Toggle filter mode on left click on last 2 lines */
   8671 			if (event.y >= xlines - 2 && event.bstate == BUTTON1_PRESSED) {
   8672 				clearfilter();
   8673 				cfg.filtermode ^= 1;
   8674 				if (cfg.filtermode) {
   8675 					presel = FILTER;
   8676 					goto nochange;
   8677 				}
   8678 
   8679 				/* Start watching the directory */
   8680 				watch = TRUE;
   8681 				copycurname();
   8682 				cd = FALSE;
   8683 				goto begin;
   8684 			}
   8685 
   8686 			/* Handle clicking on a file */
   8687 			if (event.y >= 2 && event.y <= ndents + 1 &&
   8688 					(event.bstate == BUTTON1_PRESSED ||
   8689 					 event.bstate == BUTTON3_PRESSED)) {
   8690 				r = curscroll + (event.y - 2);
   8691 				if (r != cur)
   8692 					move_cursor(r, 1);
   8693 #ifndef NOFIFO
   8694 				else if ((event.bstate == BUTTON1_PRESSED) && !g_state.fifomode)
   8695 					notify_fifo(TRUE); /* Send clicked path to NNN_FIFO */
   8696 #endif
   8697 				/* Handle right click selection */
   8698 				if (event.bstate == BUTTON3_PRESSED) {
   8699 					rightclicksel = 1;
   8700 					presel = SELECT;
   8701 					goto nochange;
   8702 				}
   8703 
   8704 				currentmouse ^= 1;
   8705 				clock_gettime(
   8706 #ifdef CLOCK_MONOTONIC_RAW
   8707 				    CLOCK_MONOTONIC_RAW,
   8708 #elif defined(CLOCK_MONOTONIC)
   8709 				    CLOCK_MONOTONIC,
   8710 #else
   8711 				    CLOCK_REALTIME,
   8712 #endif
   8713 				    &mousetimings[currentmouse]);
   8714 				mousedent[currentmouse] = cur;
   8715 
   8716 				/* Single click just selects, double click falls through to SEL_OPEN */
   8717 				if ((mousedent[0] != mousedent[1]) ||
   8718 				  (((_ABSSUB(mousetimings[0].tv_sec, mousetimings[1].tv_sec) << 30)
   8719 				  + (_ABSSUB(mousetimings[0].tv_nsec, mousetimings[1].tv_nsec)))
   8720 					> DBLCLK_INTERVAL_NS))
   8721 					break;
   8722 				/* Double click */
   8723 				mousetimings[currentmouse].tv_sec = 0;
   8724 				mousedent[currentmouse] = -1;
   8725 				sel = SEL_OPEN;
   8726 			} else {
   8727 				if (cfg.filtermode || filterset())
   8728 					presel = FILTER;
   8729 				copycurname();
   8730 				goto nochange;
   8731 			}
   8732 #endif
   8733 			// fallthrough
   8734 		case SEL_NAV_IN: // fallthrough
   8735 		case SEL_OPEN:
   8736 			/* Cannot descend in empty directories */
   8737 			if (!ndents) {
   8738 				cd = FALSE;
   8739 				g_state.selbm = g_state.runplugin = 0;
   8740 				goto begin;
   8741 			}
   8742 
   8743 			pent = &pdents[cur];
   8744 			if (!g_state.selbm || !(S_ISLNK(pent->mode) &&
   8745 			                        bmtarget(pent->name, path, newpath) &&
   8746 			                        xstrsncpy(path, lastdir, PATH_MAX)))
   8747 				mkpath(path, pent->name, newpath);
   8748 			g_state.selbm = 0;
   8749 			DPRINTF_S(newpath);
   8750 
   8751 			/* Visit directory */
   8752 			if (pent->flags & DIR_OR_DIRLNK) {
   8753 				if (chdir(newpath) == -1) {
   8754 					printwarn(&presel);
   8755 					goto nochange;
   8756 				}
   8757 
   8758 				cdprep(lastdir, lastname, path, newpath) ? (presel = FILTER) : (watch = TRUE);
   8759 				goto begin;
   8760 			}
   8761 
   8762 			/* Cannot use stale data in entry, file may be missing by now */
   8763 			if (stat(newpath, &sb) == -1) {
   8764 				printwarn(&presel);
   8765 				goto nochange;
   8766 			}
   8767 			DPRINTF_U(sb.st_mode);
   8768 
   8769 			/* Do not open non-regular files */
   8770 			if (!S_ISREG(sb.st_mode)) {
   8771 				printwait(messages[MSG_UNSUPPORTED], &presel);
   8772 				goto nochange;
   8773 			}
   8774 
   8775 			/* Handle plugin selection mode */
   8776 			if (g_state.runplugin) {
   8777 				g_state.runplugin = 0;
   8778 				/* Must be in plugin dir and same context to select plugin */
   8779 				if ((g_state.runctx == cfg.curctx) && !strcmp(path, plgpath)) {
   8780 					endselection(FALSE);
   8781 					/* Copy path so we can return back to earlier dir */
   8782 					xstrsncpy(path, lastdir, PATH_MAX);
   8783 					clearfilter();
   8784 
   8785 					if (chdir(path) == -1
   8786 					    || !run_plugin(&path, pent->name, runfile, &lastname, &lastdir, NULL)) {
   8787 						DPRINTF_S("plugin failed!");
   8788 					}
   8789 
   8790 					if (g_state.picked)
   8791 						return EXIT_SUCCESS;
   8792 
   8793 					if (runfile[0]) {
   8794 						xstrsncpy(lastname, runfile, NAME_MAX + 1);
   8795 						runfile[0] = '\0';
   8796 					}
   8797 					setdirwatch();
   8798 					goto begin;
   8799 				}
   8800 			}
   8801 
   8802 #ifndef NOFIFO
   8803 			if (g_state.fifomode && (sel == SEL_OPEN)) {
   8804 				send_to_explorer(&presel); /* Write selection to explorer fifo */
   8805 				break;
   8806 			}
   8807 #endif
   8808 			/* If opened as vim plugin and Enter/^M pressed, pick */
   8809 			if (g_state.picker && (sel == SEL_OPEN)) {
   8810 				if (nselected == 0) /* Pick if none selected */
   8811 					appendfpath(newpath, mkpath(path, pent->name, newpath));
   8812 				return EXIT_SUCCESS;
   8813 			}
   8814 
   8815 			if (sel == SEL_NAV_IN) {
   8816 				/* If in listing dir, go to target on `l` or Right on symlink */
   8817 				if (listpath && S_ISLNK(pent->mode)
   8818 				    && is_prefix(path, listpath, xstrlen(listpath))) {
   8819 					if (!realpath(pent->name, newpath)) {
   8820 						printwarn(&presel);
   8821 						goto nochange;
   8822 					}
   8823 
   8824 					xdirname(newpath);
   8825 
   8826 					if (chdir(newpath) == -1) {
   8827 						printwarn(&presel);
   8828 						goto nochange;
   8829 					}
   8830 
   8831 					cdprep(lastdir, NULL, path, newpath)
   8832 					       ? (presel = FILTER) : (watch = TRUE);
   8833 					xstrsncpy(lastname, pent->name, NAME_MAX + 1);
   8834 					goto begin;
   8835 				}
   8836 
   8837 				/* Open file disabled on right arrow or `l` */
   8838 				if (cfg.nonavopen)
   8839 					goto nochange;
   8840 			}
   8841 
   8842 			if (!sb.st_size) {
   8843 				printwait(messages[MSG_EMPTY_FILE], &presel);
   8844 				goto nochange;
   8845 			}
   8846 
   8847 			if (cfg.useeditor
   8848 #ifdef FILE_MIME_OPTS
   8849 			    && get_output("file", FILE_MIME_OPTS, newpath, -1, FALSE)
   8850 			    && is_prefix(g_buf, "text/", 5)
   8851 #else
   8852 			    /* no MIME option; guess from description instead */
   8853 			    && get_output("file", "-bL", newpath, -1, FALSE)
   8854 			    && strstr(g_buf, "text")
   8855 #endif
   8856 			) {
   8857 				spawn(editor, newpath, NULL, NULL, F_CLI);
   8858 				if (cfg.filtermode) {
   8859 					presel = FILTER;
   8860 					clearfilter();
   8861 				}
   8862 				continue;
   8863 			}
   8864 
   8865 			/* Get the extension for regex match */
   8866 			tmp = xextension(pent->name, pent->nlen - 1);
   8867 #ifdef PCRE2
   8868 			if (tmp) {
   8869 				pcre2_match_data *match_data = pcre2_match_data_create_from_pattern(archive_pcre2, NULL);
   8870 
   8871 				if (match_data) {
   8872 					r = pcre2_match(archive_pcre2, (PCRE2_SPTR)tmp, pent->nlen - (tmp - pent->name) - 1, 0, 0, match_data, NULL);
   8873 					pcre2_match_data_free(match_data);
   8874 				}
   8875 			} else
   8876 				r = 0;
   8877 
   8878 			if (r > 0) {
   8879 #else
   8880 			if (tmp && !regexec(&archive_re, tmp, 0, NULL, 0)) {
   8881 #endif
   8882 				r = get_input(messages[MSG_ARCHIVE_OPTS]);
   8883 				if (r == '\r')
   8884 					r = 'l';
   8885 				if (r == 'l' || r == 'x') {
   8886 					mkpath(path, pent->name, newpath);
   8887 					if (!handle_archive(newpath, r)) {
   8888 						presel = MSGWAIT;
   8889 						goto nochange;
   8890 					}
   8891 					if (r == 'l') {
   8892 						statusbar(path);
   8893 						goto nochange;
   8894 					}
   8895 				}
   8896 
   8897 				if ((r == 'm') && !archive_mount(newpath)) {
   8898 					presel = MSGWAIT;
   8899 					goto nochange;
   8900 				}
   8901 
   8902 				if (r == 'x' || r == 'm') {
   8903 					if (newpath[0])
   8904 						set_smart_ctx('+', newpath, &path,
   8905 							      ndents ? pdents[cur].name : NULL,
   8906 							      &lastname, &lastdir);
   8907 					else
   8908 						copycurname();
   8909 					clearfilter();
   8910 					goto begin;
   8911 				}
   8912 
   8913 				if (r != 'o') {
   8914 					printwait(messages[MSG_INVALID_KEY], &presel);
   8915 					goto nochange;
   8916 				}
   8917 			}
   8918 
   8919 			/* Invoke desktop opener as last resort */
   8920 			spawn(opener, newpath, NULL, NULL, opener_flags);
   8921 
   8922 			/* Move cursor to the next entry if not the last entry */
   8923 			if (g_state.autonext && cur != ndents - 1)
   8924 				move_cursor((cur + 1) % ndents, 0);
   8925 			if (cfg.filtermode) {
   8926 				presel = FILTER;
   8927 				clearfilter();
   8928 			}
   8929 			continue;
   8930 		case SEL_NEXT: // fallthrough
   8931 		case SEL_PREV: // fallthrough
   8932 		case SEL_PGDN: // fallthrough
   8933 		case SEL_CTRL_D: // fallthrough
   8934 		case SEL_PGUP: // fallthrough
   8935 		case SEL_CTRL_U: // fallthrough
   8936 		case SEL_HOME: // fallthrough
   8937 		case SEL_END: // fallthrough
   8938 		case SEL_FIRST: // fallthrough
   8939 		case SEL_YOUNG:
   8940 			if (ndents) {
   8941 				g_state.move = 1;
   8942 				handle_screen_move(sel);
   8943 			}
   8944 			break;
   8945 		case SEL_JUMP:
   8946 			if (ndents) {
   8947 				g_state.showlines = 1;
   8948 				redraw(path);
   8949 				handle_screen_move(sel);
   8950 				g_state.showlines = 0;
   8951 			}
   8952 			break;
   8953 		case SEL_CDHOME: // fallthrough
   8954 		case SEL_CDBEGIN: // fallthrough
   8955 		case SEL_CDLAST: // fallthrough
   8956 		case SEL_CDROOT:
   8957 			dir = (sel == SEL_CDHOME) ? home
   8958 				: ((sel == SEL_CDBEGIN) ? ipath
   8959 				: ((sel == SEL_CDLAST) ? lastdir
   8960 				: "/" /* SEL_CDROOT */));
   8961 
   8962 			if (!dir || !*dir) {
   8963 				printwait(messages[MSG_NOT_SET], &presel);
   8964 				goto nochange;
   8965 			}
   8966 
   8967 			g_state.selbm = 0;
   8968 			if (strcmp(path, dir) == 0) {
   8969 				if (dir == ipath) {
   8970 					if (cfg.filtermode)
   8971 						presel = FILTER;
   8972 					goto nochange;
   8973 				}
   8974 				dir = lastdir; /* Go to last dir on home/root key repeat */
   8975 			}
   8976 
   8977 			if (chdir(dir) == -1) {
   8978 				presel = MSGWAIT;
   8979 				goto nochange;
   8980 			}
   8981 
   8982 			/* SEL_CDLAST: dir pointing to lastdir */
   8983 			xstrsncpy(newpath, dir, PATH_MAX); // fallthrough
   8984 		case SEL_BMOPEN:
   8985 			if (sel == SEL_BMOPEN) {
   8986 				r = (int)handle_bookmark(mark, newpath);
   8987 				if (r) {
   8988 					printwait(messages[r], &presel);
   8989 					goto nochange;
   8990 				}
   8991 
   8992 				if (g_state.selbm == 1) /* Allow filtering in bookmarks directory */
   8993 					presel = FILTER;
   8994 				if (strcmp(path, newpath) == 0)
   8995 					break;
   8996 			}
   8997 
   8998 			/* In list mode, retain the last file name to highlight it, if possible */
   8999 			cdprep(lastdir, listpath && sel == SEL_CDLAST ? NULL : lastname, path, newpath)
   9000 			       ? (presel = FILTER) : (watch = TRUE);
   9001 			goto begin;
   9002 		case SEL_REMOTE:
   9003 			if ((sel == SEL_REMOTE) && !remote_mount(newpath)) {
   9004 				presel = MSGWAIT;
   9005 				goto nochange;
   9006 			}
   9007 
   9008 			set_smart_ctx('+', newpath, &path,
   9009 				      ndents ? pdents[cur].name : NULL, &lastname, &lastdir);
   9010 			clearfilter();
   9011 			goto begin;
   9012 		case SEL_CYCLE: // fallthrough
   9013 		case SEL_CYCLER: // fallthrough
   9014 		case SEL_CTX1: // fallthrough
   9015 		case SEL_CTX2: // fallthrough
   9016 		case SEL_CTX3: // fallthrough
   9017 		case SEL_CTX4:
   9018 		case SEL_CTX5:
   9019 		case SEL_CTX6:
   9020 		case SEL_CTX7:
   9021 		case SEL_CTX8:
   9022 			r = handle_context_switch(sel);
   9023 			if (r < 0)
   9024 				continue;
   9025 			savecurctx(path, ndents ? pdents[cur].name : NULL, r);
   9026 
   9027 			/* Reset the pointers */
   9028 			path = g_ctx[r].c_path;
   9029 			lastdir = g_ctx[r].c_last;
   9030 			lastname = g_ctx[r].c_name;
   9031 			tmp = g_ctx[r].c_fltr;
   9032 
   9033 			if (cfg.filtermode || ((tmp[0] == FILTER || tmp[0] == RFILTER) && tmp[1]))
   9034 				presel = FILTER;
   9035 			else
   9036 				watch = TRUE;
   9037 
   9038 			goto begin;
   9039 		case SEL_MARK:
   9040 			free(mark);
   9041 			mark = xstrdup(path);
   9042 			printwait(mark, &presel);
   9043 			goto nochange;
   9044 		case SEL_BMARK:
   9045 			add_bookmark(path, newpath, &presel);
   9046 			goto nochange;
   9047 		case SEL_FLTR:
   9048 			/* Unwatch dir if we are still in a filtered view */
   9049 #ifdef LINUX_INOTIFY
   9050 			if (inotify_wd >= 0) {
   9051 				inotify_rm_watch(inotify_fd, inotify_wd);
   9052 				inotify_wd = -1;
   9053 			}
   9054 #elif defined(BSD_KQUEUE)
   9055 			if (event_fd >= 0) {
   9056 				close(event_fd);
   9057 				event_fd = -1;
   9058 			}
   9059 #elif defined(HAIKU_NM)
   9060 			if (haiku_nm_active) {
   9061 				haiku_stop_watch(haiku_hnd);
   9062 				haiku_nm_active = FALSE;
   9063 			}
   9064 #endif
   9065 			presel = filterentries(path, lastname);
   9066 			if (presel == ESC) {
   9067 				presel = 0;
   9068 				break;
   9069 			}
   9070 			if (presel == FILTER) { /* Refresh dir and filter again */
   9071 				cd = FALSE;
   9072 				goto begin;
   9073 			}
   9074 			goto nochange;
   9075 		case SEL_MFLTR: // fallthrough
   9076 		case SEL_HIDDEN: // fallthrough
   9077 		case SEL_DETAIL: // fallthrough
   9078 		case SEL_PREVIEW: // fallthrough
   9079 		case SEL_SORT:
   9080 			switch (sel) {
   9081 			case SEL_MFLTR:
   9082 				cfg.filtermode ^= 1;
   9083 				if (cfg.filtermode) {
   9084 					presel = FILTER;
   9085 					clearfilter();
   9086 					goto nochange;
   9087 				}
   9088 
   9089 				watch = TRUE; // fallthrough
   9090 			case SEL_HIDDEN:
   9091 				if (sel == SEL_HIDDEN) {
   9092 					cfg.showhidden ^= 1;
   9093 					if (cfg.filtermode)
   9094 						presel = FILTER;
   9095 					clearfilter();
   9096 				}
   9097 				copycurname();
   9098 				cd = FALSE;
   9099 				goto begin;
   9100 			case SEL_DETAIL:
   9101 				cfg.showdetail ^= 1;
   9102 				cfg.blkorder = 0;
   9103 				continue;
   9104 			case SEL_PREVIEW:
   9105 				cfg.preview ^= 1;
   9106 				continue;
   9107 			default: /* SEL_SORT */
   9108 				r = set_sort_flags(get_input(messages[MSG_ORDER]));
   9109 				if (!r) {
   9110 					printwait(messages[MSG_INVALID_KEY], &presel);
   9111 					goto nochange;
   9112 				}
   9113 			}
   9114 
   9115 			if (cfg.filtermode || filterset())
   9116 				presel = FILTER;
   9117 
   9118 			if (ndents) {
   9119 				copycurname();
   9120 
   9121 				if (r == 'd' || r == 'a') {
   9122 					presel = 0;
   9123 					goto begin;
   9124 				}
   9125 
   9126 				ENTSORT(pdents, ndents, entrycmpfn);
   9127 				move_cursor(ndents ? dentfind(lastname, ndents) : 0, 0);
   9128 			}
   9129 			continue;
   9130 		case SEL_STATS: // fallthrough
   9131 		case SEL_CHMODX:
   9132 			if (ndents) {
   9133 				tmp = (listpath && xstrcmp(path, listpath) == 0) ? listroot : path;
   9134 
   9135 				if ((sel == SEL_STATS && !show_stats(newpath, tmp))
   9136 				    || (sel == SEL_CHMODX && !xchmod(newpath, tmp))) {
   9137 					printwarn(&presel);
   9138 					goto nochange;
   9139 				}
   9140 			}
   9141 			break;
   9142 		case SEL_REDRAW: // fallthrough
   9143 		case SEL_RENAMEMUL: // fallthrough
   9144 		case SEL_HELP: // fallthrough
   9145 		case SEL_AUTONEXT: // fallthrough
   9146 		case SEL_EDIT: // fallthrough
   9147 		case SEL_LOCK:
   9148 		{
   9149 			bool refresh = FALSE;
   9150 
   9151 			if (ndents)
   9152 				mkpath(path, pdents[cur].name, newpath);
   9153 			else if (sel == SEL_EDIT) /* Avoid trying to edit a non-existing file */
   9154 				goto nochange;
   9155 
   9156 			switch (sel) {
   9157 			case SEL_REDRAW:
   9158 				refresh = TRUE;
   9159 				break;
   9160 			case SEL_RENAMEMUL:
   9161 				endselection(TRUE);
   9162 				setenv("NNN_INCLUDE_HIDDEN", xitoa(cfg.showhidden), 1);
   9163 				setenv("NNN_PREFER_SELECTION", xitoa(cfg.prefersel), 1);
   9164 				setenv("NNN_LIST", listpath ? listroot : "", 1);
   9165 
   9166 				if (!(getutil(utils[UTIL_BASH])
   9167 				      && plugscript(utils[UTIL_NMV], F_CLI))
   9168 #ifndef NOBATCH
   9169 				    && !batch_rename()
   9170 #endif
   9171 				) {
   9172 					printwait(messages[MSG_FAILED], &presel);
   9173 					goto nochange;
   9174 				}
   9175 				clearselection();
   9176 				refresh = TRUE;
   9177 				break;
   9178 			case SEL_HELP:
   9179 				show_help(path); // fallthrough
   9180 			case SEL_AUTONEXT:
   9181 				if (sel == SEL_AUTONEXT)
   9182 					g_state.autonext ^= 1;
   9183 				if (cfg.filtermode)
   9184 					presel = FILTER;
   9185 				copycurname();
   9186 				goto nochange;
   9187 			case SEL_EDIT:
   9188 				if (!(g_state.picker || g_state.fifomode))
   9189 					spawn(editor, newpath, NULL, NULL, F_CLI);
   9190 				continue;
   9191 			default: /* SEL_LOCK */
   9192 				lock_terminal();
   9193 				break;
   9194 			}
   9195 
   9196 			/* In case of successful operation, reload contents */
   9197 
   9198 			/* Continue in type-to-nav mode, if enabled */
   9199 			if ((cfg.filtermode || filterset()) && !refresh) {
   9200 				presel = FILTER;
   9201 				goto nochange;
   9202 			}
   9203 
   9204 			/* Save current */
   9205 			copycurname();
   9206 			/* Repopulate as directory content may have changed */
   9207 			cd = FALSE;
   9208 			goto begin;
   9209 		}
   9210 		case SEL_SEL:
   9211 			if (!ndents)
   9212 				goto nochange;
   9213 
   9214 			startselection();
   9215 			if (g_state.rangesel)
   9216 				g_state.rangesel = 0;
   9217 
   9218 			/* Toggle selection status */
   9219 			pdents[cur].flags ^= FILE_SELECTED;
   9220 
   9221 			if (pdents[cur].flags & FILE_SELECTED) {
   9222 				++nselected;
   9223 				appendfpath(newpath, mkpath(path, pdents[cur].name, newpath));
   9224 				writesel(pselbuf, selbufpos - 1); /* Truncate NULL from end */
   9225 			} else {
   9226 				--nselected;
   9227 				rmfromselbuf(mkpath(path, pdents[cur].name, g_sel));
   9228 			}
   9229 
   9230 #ifndef NOX11
   9231 			if (cfg.x11)
   9232 				plugscript(utils[UTIL_CBCP], F_NOWAIT | F_NOTRACE);
   9233 #endif
   9234 #ifndef NOMOUSE
   9235 			if (rightclicksel)
   9236 				rightclicksel = 0;
   9237 			else
   9238 #endif
   9239 				/* move cursor to the next entry if this is not the last entry */
   9240 				if (!g_state.stayonsel && (cur != ndents - 1))
   9241 					move_cursor((cur + 1) % ndents, 0);
   9242 			break;
   9243 		case SEL_SELMUL:
   9244 			if (!ndents)
   9245 				goto nochange;
   9246 
   9247 			startselection();
   9248 			g_state.rangesel ^= 1;
   9249 
   9250 			if (stat(path, &sb) == -1) {
   9251 				printwarn(&presel);
   9252 				goto nochange;
   9253 			}
   9254 
   9255 			if (g_state.rangesel) { /* Range selection started */
   9256 				inode = sb.st_ino;
   9257 				selstartid = cur;
   9258 				continue;
   9259 			}
   9260 
   9261 			if (inode != sb.st_ino) {
   9262 				printwait(messages[MSG_DIR_CHANGED], &presel);
   9263 				goto nochange;
   9264 			}
   9265 
   9266 			if (cur < selstartid) {
   9267 				selendid = selstartid;
   9268 				selstartid = cur;
   9269 			} else
   9270 				selendid = cur;
   9271 
   9272 			/* Clear selection on repeat on same file */
   9273 			if (selstartid == selendid) {
   9274 				resetselind();
   9275 				clearselection();
   9276 				break;
   9277 			} // fallthrough
   9278 		case SEL_SELALL: // fallthrough
   9279 		case SEL_SELINV:
   9280 			if (sel == SEL_SELALL || sel == SEL_SELINV) {
   9281 				if (!ndents)
   9282 					goto nochange;
   9283 
   9284 				startselection();
   9285 				if (g_state.rangesel)
   9286 					g_state.rangesel = 0;
   9287 
   9288 				selstartid = 0;
   9289 				selendid = ndents - 1;
   9290 			}
   9291 
   9292 			if ((nselected > LARGESEL) || (nselected && (ndents > LARGESEL))) {
   9293 				printmsg("processing...");
   9294 				refresh();
   9295 			}
   9296 
   9297 			r = scanselforpath(path, TRUE); /* Get path length suffixed by '/' */
   9298 			((sel == SEL_SELINV) && findselpos)
   9299 				? invertselbuf(r) : addtoselbuf(r, selstartid, selendid);
   9300 
   9301 #ifndef NOX11
   9302 			if (cfg.x11)
   9303 				plugscript(utils[UTIL_CBCP], F_NOWAIT | F_NOTRACE);
   9304 #endif
   9305 			continue;
   9306 		case SEL_SELEDIT:
   9307 			r = editselection(FALSE);
   9308 			if (r <= 0) {
   9309 				r = !r ? MSG_0_SELECTED : MSG_FAILED;
   9310 				printwait(messages[r], &presel);
   9311 			} else {
   9312 #ifndef NOX11
   9313 				if (cfg.x11)
   9314 					plugscript(utils[UTIL_CBCP], F_NOWAIT | F_NOTRACE);
   9315 #endif
   9316 				cfg.filtermode ?  presel = FILTER : statusbar(path);
   9317 			}
   9318 			goto begin;
   9319 		case SEL_CP: // fallthrough
   9320 		case SEL_MV: // fallthrough
   9321 		case SEL_CPMVAS: // fallthrough
   9322 		case SEL_TRASH: // fallthrough
   9323 		case SEL_RM_RF:
   9324 		{
   9325 			if ((sel == SEL_TRASH) || (sel == SEL_RM_RF)) {
   9326 				r = get_cur_or_sel();
   9327 				if (!r) {
   9328 					statusbar(path);
   9329 					goto nochange;
   9330 				}
   9331 
   9332 				if (r == 'c') {
   9333 					tmp = (listpath && xstrcmp(path, listpath) == 0)
   9334 					      ? listroot : path;
   9335 					mkpath(tmp, pdents[cur].name, newpath);
   9336 					if (!xrm(newpath, trashcmd && sel == SEL_TRASH))
   9337 						continue;
   9338 
   9339 					xrmfromsel(tmp, newpath);
   9340 
   9341 					copynextname(lastname);
   9342 
   9343 					if (cfg.filtermode || filterset())
   9344 						presel = FILTER;
   9345 					cd = FALSE;
   9346 					goto begin;
   9347 				}
   9348 			}
   9349 
   9350 			(nselected == 1 && (sel == SEL_CP || sel == SEL_MV))
   9351 				? mkpath(path, xbasename(pselbuf), newpath)
   9352 				: (newpath[0] = '\0');
   9353 
   9354 			endselection(TRUE);
   9355 
   9356 			if (!cpmvrm_selection(sel, path)) {
   9357 				presel = MSGWAIT;
   9358 				goto nochange;
   9359 			}
   9360 
   9361 			if (cfg.filtermode)
   9362 				presel = FILTER;
   9363 			clearfilter();
   9364 
   9365 #ifndef NOX11
   9366 			/* Show notification on operation complete */
   9367 			if (cfg.x11)
   9368 				plugscript(utils[UTIL_NTFY], F_NOWAIT | F_NOTRACE);
   9369 #endif
   9370 
   9371 			if (newpath[0] && !access(newpath, F_OK))
   9372 				xstrsncpy(lastname, xbasename(newpath), NAME_MAX+1);
   9373 			else
   9374 				copycurname();
   9375 			cd = FALSE;
   9376 			goto begin;
   9377 		}
   9378 		case SEL_ARCHIVE: // fallthrough
   9379 		case SEL_OPENWITH: // fallthrough
   9380 		case SEL_NEW: // fallthrough
   9381 		case SEL_RENAME:
   9382 		{
   9383 			int ret = 'n';
   9384 			size_t len;
   9385 
   9386 			if (!ndents && (sel == SEL_OPENWITH || sel == SEL_RENAME))
   9387 				break;
   9388 
   9389 			if (sel != SEL_OPENWITH)
   9390 				endselection(TRUE);
   9391 
   9392 			switch (sel) {
   9393 			case SEL_ARCHIVE:
   9394 				r = get_cur_or_sel();
   9395 				if (!r) {
   9396 					statusbar(path);
   9397 					goto nochange;
   9398 				}
   9399 
   9400 				if (r == 's') {
   9401 					if (!selsafe()) {
   9402 						presel = MSGWAIT;
   9403 						goto nochange;
   9404 					}
   9405 
   9406 					tmp = NULL;
   9407 				} else
   9408 					tmp = pdents[cur].name;
   9409 
   9410 				tmp = xreadline(tmp, messages[MSG_ARCHIVE_NAME]);
   9411 				break;
   9412 			case SEL_OPENWITH:
   9413 #ifndef NORL
   9414 				if (g_state.picker || g_state.xprompt) {
   9415 #endif
   9416 					tmp = xreadline(NULL, messages[MSG_OPEN_WITH]);
   9417 #ifndef NORL
   9418 				} else
   9419 					tmp = getreadline(messages[MSG_OPEN_WITH]);
   9420 #endif
   9421 				break;
   9422 			case SEL_NEW:
   9423 				if (!pkey) {
   9424 					r = get_input(messages[MSG_NEW_OPTS]);
   9425 					if (r == '\r')
   9426 						r = 'f';
   9427 					tmp = NULL;
   9428 				} else {
   9429 					r = 'f';
   9430 					tmp = g_ctx[0].c_name;
   9431 					pkey = '\0';
   9432 				}
   9433 
   9434 				if (r == 'f' || r == 'd')
   9435 					tmp = xreadline(tmp, messages[MSG_NEW_PATH]);
   9436 				else if (r == 's' || r == 'h')
   9437 					tmp = xreadline((nselected == 1 && cfg.prefersel) ? xbasename(pselbuf) : NULL,
   9438 						messages[nselected <= 1 ? MSG_NEW_PATH : MSG_LINK_PREFIX]);
   9439 				else
   9440 					tmp = NULL;
   9441 				break;
   9442 			default: /* SEL_RENAME */
   9443 				tmp = xreadline(pdents[cur].name, "");
   9444 				break;
   9445 			}
   9446 
   9447 			if (!tmp || !*tmp || is_bad_len_or_dir(tmp))
   9448 				break;
   9449 
   9450 			switch (sel) {
   9451 			case SEL_ARCHIVE:
   9452 				if (r == 'c' && strcmp(tmp, pdents[cur].name) == 0)
   9453 					continue; /* Cannot overwrite the hovered file */
   9454 
   9455 				tmp = abspath(tmp, NULL, newpath);
   9456 				if (!tmp)
   9457 					continue;
   9458 				if (access(tmp, F_OK) == 0) {
   9459 					if (!xconfirm(get_input(messages[MSG_OVERWRITE]))) {
   9460 						statusbar(path);
   9461 						goto nochange;
   9462 					}
   9463 				}
   9464 
   9465 				(r == 's') ? archive_selection(get_archive_cmd(tmp), tmp)
   9466 					   : spawn(get_archive_cmd(tmp), tmp, pdents[cur].name,
   9467 						   NULL, F_CLI | F_CONFIRM);
   9468 
   9469 				if (tmp && (access(tmp, F_OK) == 0)) { /* File created */
   9470 					if (r == 's')
   9471 						clearselection(); /* Archive operation complete */
   9472 
   9473 					/* Check if any entry is created in the current directory */
   9474 					tmp = get_cwd_entry(path, tmp, &len);
   9475 					if (tmp) {
   9476 						xstrsncpy(lastname, tmp, len + 1);
   9477 						clearfilter(); /* Archive name may not match */
   9478 					} if (cfg.filtermode)
   9479 						presel = FILTER;
   9480 					cd = FALSE;
   9481 					goto begin;
   9482 				}
   9483 				continue;
   9484 			case SEL_OPENWITH:
   9485 				handle_openwith(path, pdents[cur].name, newpath, tmp);
   9486 
   9487 				cfg.filtermode ?  presel = FILTER : statusbar(path);
   9488 				copycurname();
   9489 				goto nochange;
   9490 			case SEL_RENAME:
   9491 				r = 0;
   9492 				/* Skip renaming to same name */
   9493 				if (strcmp(tmp, pdents[cur].name) == 0) {
   9494 					tmp = xreadline(pdents[cur].name, messages[MSG_COPY_NAME]);
   9495 					if (!tmp || !tmp[0] || is_bad_len_or_dir(tmp)
   9496 					    || !strcmp(tmp, pdents[cur].name)) {
   9497 						cfg.filtermode ?  presel = FILTER : statusbar(path);
   9498 						copycurname();
   9499 						goto nochange;
   9500 					}
   9501 					ret = 'd';
   9502 				}
   9503 				break;
   9504 			default: /* SEL_NEW */
   9505 				break;
   9506 			}
   9507 
   9508 			if (!(r == 's' || r == 'h')) {
   9509 				tmp = abspath(tmp, path, newpath);
   9510 				if (!tmp) {
   9511 					printwarn(&presel);
   9512 					goto nochange;
   9513 				}
   9514 			}
   9515 
   9516 			/* Check if another file with same name exists */
   9517 			if (lstat(tmp, &sb) == 0) {
   9518 				if ((sel == SEL_RENAME) || ((r == 'f') && (S_ISREG(sb.st_mode)))) {
   9519 					/* Overwrite file with same name? */
   9520 					if (!xconfirm(get_input(messages[MSG_OVERWRITE])))
   9521 						break;
   9522 				} else {
   9523 					/* Do nothing for SEL_NEW if a non-regular entry exists */
   9524 					printwait(messages[MSG_EXISTS], &presel);
   9525 					goto nochange;
   9526 				}
   9527 			}
   9528 
   9529 			if (sel == SEL_RENAME) {
   9530 				/* Rename the file */
   9531 				if (ret == 'd')
   9532 					spawn("cp -rp --", pdents[cur].name, tmp, NULL, F_SILENT);
   9533 				else if (rename(pdents[cur].name, tmp) != 0) {
   9534 					printwarn(&presel);
   9535 					goto nochange;
   9536 				}
   9537 
   9538 				/* Check if any entry is created in the current directory */
   9539 				tmp = get_cwd_entry(path, tmp, &len);
   9540 				if (tmp)
   9541 					xstrsncpy(lastname, tmp, len + 1);
   9542 				/* Directory must be reloeaded for rename case */
   9543 			} else { /* SEL_NEW */
   9544 				presel = 0;
   9545 
   9546 				/* Check if it's a dir or file */
   9547 				if (r == 'f' || r == 'd') {
   9548 					ret = xmktree(tmp, r == 'f' ? FALSE : TRUE, TRUE);
   9549 				} else if (r == 's' || r == 'h') {
   9550 					if (nselected > 1 && tmp[0] == '@' && tmp[1] == '\0')
   9551 						tmp[0] = '\0';
   9552 					ret = xlink(tmp, path, (ndents ? pdents[cur].name : NULL), newpath, r);
   9553 				}
   9554 
   9555 				if (!ret)
   9556 					printwarn(&presel);
   9557 
   9558 				if (ret <= 0)
   9559 					goto nochange;
   9560 
   9561 				if (r == 'f' || r == 'd') {
   9562 					tmp = get_cwd_entry(path, tmp, &len);
   9563 					if (tmp)
   9564 						xstrsncpy(lastname, tmp, len + 1);
   9565 					else
   9566 						continue; /* No change in directory */
   9567 				} else if (ndents) {
   9568 					if (cfg.filtermode)
   9569 						presel = FILTER;
   9570 					copycurname();
   9571 				}
   9572 			}
   9573 			clearfilter();
   9574 
   9575 			cd = FALSE;
   9576 			goto begin;
   9577 		}
   9578 		case SEL_PLUGIN:
   9579 			/* Check if directory is accessible */
   9580 			if (!xdiraccess(plgpath)) {
   9581 				printwarn(&presel);
   9582 				goto nochange;
   9583 			}
   9584 
   9585 			lazy_parse_plug();
   9586 			if (!pkey) {
   9587 				r = xstrsncpy(g_buf, messages[MSG_KEYS], CMD_LEN_MAX);
   9588 				printkeys(plug, g_buf + r - 1, maxplug);
   9589 				printmsg(g_buf);
   9590 				r = get_input(NULL);
   9591 				move(xlines - 1, 0); // Clear the line
   9592 				clrtoeol();
   9593 			} else {
   9594 				r = pkey;
   9595 				pkey = '\0';
   9596 			}
   9597 
   9598 			if (r != '\r') {
   9599 				endselection(FALSE);
   9600 				tmp = get_kv_val(plug, NULL, r, maxplug, NNN_PLUG);
   9601 				if (!tmp) {
   9602 					printwait(messages[MSG_INVALID_KEY], &presel);
   9603 					goto nochange;
   9604 				}
   9605 
   9606 				if (tmp[0] == '-' && tmp[1]) {
   9607 					++tmp;
   9608 					r = FALSE; /* Do not refresh dir after completion */
   9609 				} else
   9610 					r = TRUE;
   9611 
   9612 				enum action action;
   9613 
   9614 				do {
   9615 					action = SEL_MAX;
   9616 
   9617 					if (!run_plugin(&path, tmp, (ndents ? pdents[cur].name : NULL),
   9618 								 &lastname, &lastdir, &action)) {
   9619 						printwait(messages[MSG_FAILED], &presel);
   9620 						goto nochange;
   9621 					}
   9622 
   9623 					if (g_state.picked)
   9624 						return EXIT_SUCCESS;
   9625 				} while (handle_cur_move(action));
   9626 
   9627 				copycurname();
   9628 
   9629 				if (!r) {
   9630 					cfg.filtermode ? presel = FILTER : statusbar(path);
   9631 					break;
   9632 				}
   9633 			} else { /* 'Return/Enter' enters the plugin directory */
   9634 				g_state.runplugin ^= 1;
   9635 				if (!g_state.runplugin) {
   9636 					/*
   9637 					 * If toggled, and still in the plugin dir,
   9638 					 * switch to original directory
   9639 					 */
   9640 					if (strcmp(path, plgpath) == 0) {
   9641 						xstrsncpy(path, lastdir, PATH_MAX);
   9642 						xstrsncpy(lastname, runfile, NAME_MAX + 1);
   9643 						runfile[0] = '\0';
   9644 						setdirwatch();
   9645 						goto begin;
   9646 					}
   9647 
   9648 					/* Otherwise, initiate choosing plugin again */
   9649 					g_state.runplugin = 1;
   9650 				}
   9651 
   9652 				xstrsncpy(lastdir, path, PATH_MAX);
   9653 				xstrsncpy(path, plgpath, PATH_MAX);
   9654 				if (ndents)
   9655 					xstrsncpy(runfile, pdents[cur].name, NAME_MAX);
   9656 				g_state.runctx = cfg.curctx;
   9657 				lastname[0] = '\0';
   9658 				clearfilter();
   9659 			}
   9660 			setdirwatch();
   9661 			if (g_state.runplugin == 1) /* Allow filtering in plugins directory */
   9662 				presel = FILTER;
   9663 			goto begin;
   9664 		case SEL_SELSIZE:
   9665 			showselsize(path);
   9666 			goto nochange;
   9667 		case SEL_SHELL: // fallthrough
   9668 		case SEL_LAUNCH: // fallthrough
   9669 		case SEL_PROMPT:
   9670 			r = handle_cmd(sel, path, newpath);
   9671 
   9672 			/* Continue in type-to-nav mode, if enabled */
   9673 			if (cfg.filtermode)
   9674 				presel = FILTER;
   9675 
   9676 			/* Save current */
   9677 			copycurname();
   9678 
   9679 			if (!r)
   9680 				goto nochange;
   9681 
   9682 			/* Repopulate as directory content may have changed */
   9683 			cd = FALSE;
   9684 			goto begin;
   9685 		case SEL_UMOUNT:
   9686 			presel = MSG_ZERO;
   9687 			if (!unmount((ndents ? pdents[cur].name : NULL), newpath, &presel, path)) {
   9688 				if (presel == MSG_ZERO)
   9689 					statusbar(path);
   9690 				goto nochange;
   9691 			}
   9692 
   9693 			/* Dir removed, go to next entry */
   9694 			copynextname(lastname);
   9695 			cd = FALSE;
   9696 			goto begin;
   9697 #ifndef NOSSN
   9698 		case SEL_SESSIONS:
   9699 			r = get_input(messages[MSG_SSN_OPTS]);
   9700 
   9701 			if (r == 's') {
   9702 				tmp = xreadline(!curssn[0] || ((curssn[0] == '@') && !curssn[1]) ? NULL : curssn,
   9703 					messages[MSG_SSN_NAME]);
   9704 				if (tmp && *tmp)
   9705 					save_session(tmp, &presel);
   9706 			} else if (r == 'l' || r == 'r') {
   9707 				if (load_session(NULL, &path, &lastdir, &lastname, r == 'r')) {
   9708 					setdirwatch();
   9709 					goto begin;
   9710 				}
   9711 			}
   9712 
   9713 			statusbar(path);
   9714 			goto nochange;
   9715 #endif
   9716 		case SEL_EXPORT:
   9717 			export_file_list();
   9718 			cfg.filtermode ?  presel = FILTER : statusbar(path);
   9719 			goto nochange;
   9720 		case SEL_TIMETYPE:
   9721 			if (!set_time_type(&presel))
   9722 				goto nochange;
   9723 			cd = FALSE;
   9724 			goto begin;
   9725 		case SEL_QUITCTX: // fallthrough
   9726 		case SEL_QUITCD: // fallthrough
   9727 		case SEL_QUIT:
   9728 		case SEL_QUITERR:
   9729 			if (sel == SEL_QUITCTX) {
   9730 				int ctx = cfg.curctx;
   9731 
   9732 				for (r = (ctx - 1) & (CTX_MAX - 1);
   9733 				     (r != ctx) && !g_ctx[r].c_cfg.ctxactive;
   9734 				     r = ((r - 1) & (CTX_MAX - 1))) {
   9735 				};
   9736 
   9737 				if (r != ctx) {
   9738 					g_ctx[ctx].c_cfg.ctxactive = 0;
   9739 
   9740 					/* Switch to next active context */
   9741 					path = g_ctx[r].c_path;
   9742 					lastdir = g_ctx[r].c_last;
   9743 					lastname = g_ctx[r].c_name;
   9744 
   9745 					g_ctx[r].c_cfg.curctx = r;
   9746 					setcfg(g_ctx[r].c_cfg);
   9747 
   9748 					setdirwatch();
   9749 					goto begin;
   9750 				}
   9751 			} else if (!g_state.forcequit) {
   9752 				for (r = 0; r < CTX_MAX; ++r)
   9753 					if (r != cfg.curctx && g_ctx[r].c_cfg.ctxactive) {
   9754 						r = get_input(messages[MSG_QUIT_ALL]);
   9755 						break;
   9756 					}
   9757 
   9758 				if (!(r == CTX_MAX || xconfirm(r)))
   9759 					break; // fallthrough
   9760 			}
   9761 
   9762 			/* CD on Quit */
   9763 			tmp = getenv("NNN_TMPFILE");
   9764 			if ((sel == SEL_QUITCD) || tmp) {
   9765 				write_lastdir(path, tmp);
   9766 				/* ^G is a way to quit picker mode without picking anything */
   9767 				if ((sel == SEL_QUITCD) && g_state.picker)
   9768 					selbufpos = 0;
   9769 			}
   9770 
   9771 			if (sel != SEL_QUITERR)
   9772 				return EXIT_SUCCESS;
   9773 
   9774 			if (selbufpos && !g_state.picker) {
   9775 				/* Pick files to stdout and exit */
   9776 				g_state.picker = 1;
   9777 				free(selpath);
   9778 				selpath = NULL;
   9779 				return EXIT_SUCCESS;
   9780 			}
   9781 
   9782 			return EXIT_FAILURE;
   9783 		default:
   9784 			if (xlines != LINES || xcols != COLS)
   9785 				continue;
   9786 
   9787 			if (idletimeout && idle == idletimeout) {
   9788 				lock_terminal(); /* Locker */
   9789 				idle = 0;
   9790 			}
   9791 
   9792 			copycurname();
   9793 			goto nochange;
   9794 		} /* switch (sel) */
   9795 	}
   9796 }
   9797 
   9798 static char *make_tmp_tree(char **paths, ssize_t entries, const char *prefix)
   9799 {
   9800 	/* tmpdir holds the full path */
   9801 	/* tmp holds the path without the tmp dir prefix */
   9802 	int err;
   9803 	struct stat sb;
   9804 	char *slash, *tmp;
   9805 	ssize_t len = xstrlen(prefix);
   9806 	char *tmpdir = malloc(PATH_MAX);
   9807 
   9808 	if (!tmpdir) {
   9809 		DPRINTF_S(strerror(errno));
   9810 		return NULL;
   9811 	}
   9812 
   9813 	tmp = tmpdir + tmpfplen - 1;
   9814 	xstrsncpy(tmpdir, g_tmpfpath, tmpfplen);
   9815 	xstrsncpy(tmp, "/nnnXXXXXX", 11);
   9816 
   9817 	/* Points right after the base tmp dir */
   9818 	tmp += 10;
   9819 
   9820 	/* handle the case where files are directly under / */
   9821 	if (!prefix[1] && (prefix[0] == '/'))
   9822 		len = 0;
   9823 
   9824 	if (!mkdtemp(tmpdir)) {
   9825 		free(tmpdir);
   9826 
   9827 		DPRINTF_S(strerror(errno));
   9828 		return NULL;
   9829 	}
   9830 
   9831 	listpath = tmpdir;
   9832 
   9833 	for (ssize_t i = 0; i < entries; ++i) {
   9834 		if (!paths[i])
   9835 			continue;
   9836 
   9837 		err = stat(paths[i], &sb);
   9838 		if (err && errno == ENOENT)
   9839 			continue;
   9840 
   9841 		/* Don't copy the common prefix */
   9842 		xstrsncpy(tmp, paths[i] + len, xstrlen(paths[i]) - len + 1);
   9843 
   9844 		/* Get the dir containing the path */
   9845 		slash = xmemrchr((uchar_t *)tmp, '/', xstrlen(paths[i]) - len);
   9846 		if (slash)
   9847 			*slash = '\0';
   9848 
   9849 		if (access(tmpdir, F_OK)) /* Create directory if it doesn't exist */
   9850 			xmktree(tmpdir, TRUE, TRUE);
   9851 
   9852 		if (slash)
   9853 			*slash = '/';
   9854 
   9855 		if (symlink(paths[i], tmpdir)) {
   9856 			DPRINTF_S(paths[i]);
   9857 			DPRINTF_S(strerror(errno));
   9858 		}
   9859 	}
   9860 
   9861 	/* Get the dir in which to start */
   9862 	*tmp = '\0';
   9863 	return tmpdir;
   9864 }
   9865 
   9866 static char *load_input(int fd, const char *path)
   9867 {
   9868 	size_t i, chunk_count = 1, chunk = 512UL * 1024 /* 512 KiB chunk size */, entries = 0;
   9869 	char *input = malloc(sizeof(char) * chunk), *tmpdir = NULL;
   9870 	char cwd[PATH_MAX], *next;
   9871 	size_t offsets[LIST_FILES_MAX];
   9872 	char **paths = NULL;
   9873 	ssize_t input_read, total_read = 0, off = 0;
   9874 	int msgnum = 0;
   9875 
   9876 	if (!input) {
   9877 		DPRINTF_S(strerror(errno));
   9878 		return NULL;
   9879 	}
   9880 
   9881 	if (!path) {
   9882 		if (!getcwd(cwd, PATH_MAX)) {
   9883 			free(input);
   9884 			return NULL;
   9885 		}
   9886 	} else
   9887 		xstrsncpy(cwd, path, PATH_MAX);
   9888 
   9889 	while (chunk_count < LIST_INPUT_MAX / chunk && !msgnum) {
   9890 		input_read = read(fd, input + total_read, chunk);
   9891 		if (input_read < 0) {
   9892 			if (errno == EINTR)
   9893 				continue;
   9894 
   9895 			DPRINTF_S(strerror(errno));
   9896 			goto malloc_1;
   9897 		}
   9898 
   9899 		if (input_read == 0)
   9900 			break;
   9901 
   9902 		total_read += input_read;
   9903 
   9904 		while (off < total_read) {
   9905 			next = memchr(input + off, '\0', total_read - off);
   9906 			if (!next)
   9907 				break;
   9908 			++next;
   9909 
   9910 			if (next - input == off + 1) {
   9911 				off = next - input;
   9912 				continue;
   9913 			}
   9914 
   9915 			if (entries == LIST_FILES_MAX) {
   9916 				msgnum = MSG_FILE_LIMIT;
   9917 				break;
   9918 			}
   9919 
   9920 			offsets[entries++] = off;
   9921 			off = next - input;
   9922 		}
   9923 
   9924 		/* We don't need to allocate another chunk */
   9925 		if (chunk_count > (total_read + chunk - 1) / chunk)
   9926 			continue;
   9927 
   9928 		/* We can never need more than one additional chunk */
   9929 		++chunk_count;
   9930 		if (chunk_count > LIST_INPUT_MAX / chunk) {
   9931 			msgnum = MSG_SIZE_LIMIT;
   9932 			break;
   9933 		}
   9934 
   9935 		input = xrealloc(input, chunk_count * chunk);
   9936 		if (!input)
   9937 			goto malloc_1;
   9938 	}
   9939 
   9940 	/* We close fd outside this function. Any extra data is left to the kernel to handle */
   9941 
   9942 	if (off != total_read) {
   9943 		if (entries == LIST_FILES_MAX)
   9944 			msgnum = MSG_FILE_LIMIT;
   9945 		else
   9946 			offsets[entries++] = off;
   9947 	}
   9948 
   9949 	DPRINTF_D(entries);
   9950 	DPRINTF_D(total_read);
   9951 	DPRINTF_D(chunk_count);
   9952 
   9953 	if (!entries) {
   9954 		msgnum = MSG_0_ENTRIES;
   9955 		goto malloc_1;
   9956 	}
   9957 
   9958 	input[total_read] = '\0'; // NOLINT
   9959 
   9960 	paths = malloc(entries * sizeof(char *));
   9961 	if (!paths)
   9962 		goto malloc_1;
   9963 
   9964 	for (i = 0; i < entries; ++i)
   9965 		paths[i] = input + offsets[i];
   9966 
   9967 	listroot = malloc(sizeof(char) * PATH_MAX);
   9968 	if (!listroot)
   9969 		goto malloc_1;
   9970 	listroot[0] = '\0';
   9971 
   9972 	DPRINTF_S(paths[0]);
   9973 
   9974 	for (i = 0; i < entries; ++i) {
   9975 		if (paths[i][0] == '\n' || selforparent(paths[i])) {
   9976 			paths[i] = NULL;
   9977 			continue;
   9978 		}
   9979 
   9980 		paths[i] = abspath(paths[i], cwd, NULL);
   9981 		if (!paths[i]) {
   9982 			entries = i; // free from the previous entry
   9983 			goto malloc_2;
   9984 		}
   9985 
   9986 		DPRINTF_S(paths[i]);
   9987 
   9988 		xstrsncpy(g_buf, paths[i], PATH_MAX);
   9989 		if (!common_prefix(xdirname(g_buf), listroot)) {
   9990 			entries = i + 1; // free from the current entry
   9991 			goto malloc_2;
   9992 		}
   9993 
   9994 		DPRINTF_S(listroot);
   9995 	}
   9996 
   9997 	DPRINTF_S(listroot);
   9998 
   9999 	if (listroot[0])
  10000 		tmpdir = make_tmp_tree(paths, entries, listroot);
  10001 
  10002 malloc_2:
  10003 	for (i = 0; i < entries; ++i)
  10004 		free(paths[i]);
  10005 malloc_1:
  10006 	if (msgnum) { /* Check if we are past init stage and show msg */
  10007 		if (home) {
  10008 			printmsg(messages[msgnum]);
  10009 			xdelay(XDELAY_INTERVAL_MS << 2);
  10010 		} else {
  10011 			msg(messages[msgnum]);
  10012 			usleep(XDELAY_INTERVAL_MS << 2);
  10013 		}
  10014 	}
  10015 
  10016 	free(input);
  10017 	free(paths);
  10018 	return tmpdir;
  10019 }
  10020 
  10021 static void check_key_collision(void)
  10022 {
  10023 	wint_t key;
  10024 	bool bitmap[KEY_MAX] = {FALSE};
  10025 
  10026 	for (ullong_t i = 0; i < ELEMENTS(bindings); ++i) {
  10027 		key = bindings[i].sym;
  10028 
  10029 		if (bitmap[key])
  10030 			fprintf(stderr, "key collision! [%s]\n", keyname(key));
  10031 		else
  10032 			bitmap[key] = TRUE;
  10033 	}
  10034 }
  10035 
  10036 static void usage(void)
  10037 {
  10038 	fprintf(stdout,
  10039 		"%s: nnn [OPTIONS] [PATH]\n\n"
  10040 		"The unorthodox terminal file manager.\n\n"
  10041 		"positional args:\n"
  10042 		"  PATH   start dir/file [default: .]\n\n"
  10043 		"optional args:\n"
  10044 #ifndef NOFIFO
  10045 		" -a      auto NNN_FIFO\n"
  10046 #endif
  10047 		" -A      no dir auto-enter during filter\n"
  10048 		" -b key  open bookmark key (trumps -s/S)\n"
  10049 		" -B      use bsdtar for archives\n"
  10050 		" -c      cli-only NNN_OPENER (trumps -e)\n"
  10051 		" -C      8-color scheme\n"
  10052 		" -d      detail mode\n"
  10053 		" -D      dirs in context color\n"
  10054 		" -e      text in $VISUAL/$EDITOR/vi\n"
  10055 		" -E      internal edits in EDITOR\n"
  10056 		" -f      use history file\n"
  10057 #ifndef NOFIFO
  10058 		" -F val  fifo mode [0:preview 1:explore]\n"
  10059 #endif
  10060 		" -g      regex filters\n"
  10061 		" -H      show hidden files\n"
  10062 		" -i      show current file info\n"
  10063 		" -J      no auto-advance on selection\n"
  10064 		" -K      detect key collision and exit\n"
  10065 		" -l val  set scroll lines\n"
  10066 		" -n      type-to-nav mode\n"
  10067 #ifndef NORL
  10068 		" -N      use native prompt\n"
  10069 #endif
  10070 		" -o      open files only on Enter\n"
  10071 		" -p file selection file [-:stdout]\n"
  10072 		" -P key  run plugin key\n"
  10073 		" -Q      no quit confirmation\n"
  10074 		" -r      use advcpmv patched cp, mv\n"
  10075 		" -R      no rollover at edges\n"
  10076 #ifndef NOSSN
  10077 		" -s name load session by name\n"
  10078 		" -S      persistent session\n"
  10079 #endif
  10080 		" -t secs timeout to lock\n"
  10081 		" -T key  sort order [a/d/e/r/s/t/v]\n"
  10082 		" -u      use selection (no prompt)\n"
  10083 #ifndef NOUG
  10084 		" -U      show user and group\n"
  10085 #endif
  10086 		" -V      show version\n"
  10087 #ifndef NOX11
  10088 		" -x      notis, selection sync, xterm title\n"
  10089 #endif
  10090 		" -z      in order fuzzy filters\n"
  10091 		" -0      null separator in picker mode\n"
  10092 		" -h      show help\n\n"
  10093 		"v%s\n%s\n", __func__, VERSION, GENERAL_INFO);
  10094 }
  10095 
  10096 static bool setup_config(void)
  10097 {
  10098 	size_t r, len;
  10099 	char *xdgcfg = getenv("XDG_CONFIG_HOME");
  10100 	bool xdg = FALSE;
  10101 
  10102 	/* Set up configuration file paths */
  10103 	if (xdgcfg && xdgcfg[0]) {
  10104 		DPRINTF_S(xdgcfg);
  10105 		if (tilde_is_home(xdgcfg)) {
  10106 			r = xstrsncpy(g_buf, home, PATH_MAX);
  10107 			xstrsncpy(g_buf + r - 1, xdgcfg + 1, PATH_MAX - r);
  10108 			xdgcfg = g_buf;
  10109 			DPRINTF_S(xdgcfg);
  10110 		}
  10111 
  10112 		if (!xdiraccess(xdgcfg)) {
  10113 			xerror();
  10114 			return FALSE;
  10115 		}
  10116 
  10117 		len = xstrlen(xdgcfg) + xstrlen("/nnn/bookmarks") + 1;
  10118 		xdg = TRUE;
  10119 	}
  10120 
  10121 	if (!xdg)
  10122 		len = xstrlen(home) + xstrlen("/.config/nnn/bookmarks") + 1;
  10123 
  10124 	cfgpath = malloc(len);
  10125 	plgpath = malloc(len);
  10126 	if (!cfgpath || !plgpath) {
  10127 		xerror();
  10128 		return FALSE;
  10129 	}
  10130 
  10131 	if (xdg) {
  10132 		xstrsncpy(cfgpath, xdgcfg, len);
  10133 		r = len - xstrlen("/nnn/bookmarks");
  10134 	} else {
  10135 		r = xstrsncpy(cfgpath, home, len);
  10136 
  10137 		/* Create ~/.config */
  10138 		xstrsncpy(cfgpath + r - 1, "/.config", len - r);
  10139 		DPRINTF_S(cfgpath);
  10140 		r += 8; /* length of "/.config" */
  10141 	}
  10142 
  10143 	/* Create ~/.config/nnn */
  10144 	xstrsncpy(cfgpath + r - 1, "/nnn", len - r);
  10145 	DPRINTF_S(cfgpath);
  10146 
  10147 	if (!xmktree(cfgpath, TRUE, TRUE)) {
  10148 		DPRINTF_S(cfgpath);
  10149 		xerror();
  10150 		return FALSE;
  10151 	}
  10152 
  10153 	/* Create bookmarks, sessions, mounts and plugins directories */
  10154 	for (r = 0; r < ELEMENTS(toks); ++r) {
  10155 		mkpath(cfgpath, toks[r], plgpath);
  10156 		/* The dirs are created on first run, check if they already exist */
  10157 		if (!xmktree(plgpath, TRUE, FALSE)) {
  10158 			DPRINTF_S(toks[r]);
  10159 			xerror();
  10160 			return FALSE;
  10161 		}
  10162 	}
  10163 
  10164 	/* Set selection file path */
  10165 	if (!g_state.picker) {
  10166 		char *env_sel = xgetenv(env_cfg[NNN_SEL], NULL);
  10167 
  10168 		selpath = env_sel ? xstrdup(env_sel)
  10169 				  : malloc(len + 3); /* Length of "/.config/nnn/.selection" */
  10170 
  10171 		if (!selpath) {
  10172 			xerror();
  10173 			return FALSE;
  10174 		}
  10175 
  10176 		if (!env_sel) {
  10177 			r = xstrsncpy(selpath, cfgpath, len + 3);
  10178 			xstrsncpy(selpath + r - 1, "/.selection", 12);
  10179 			DPRINTF_S(selpath);
  10180 		}
  10181 	}
  10182 
  10183 	return TRUE;
  10184 }
  10185 
  10186 static bool set_tmp_path(void)
  10187 {
  10188 	char *tmp = "/tmp";
  10189 	char *path = xdiraccess(tmp) ? tmp : getenv("TMPDIR");
  10190 
  10191 	if (!path) {
  10192 		msg("set TMPDIR");
  10193 		return FALSE;
  10194 	}
  10195 
  10196 	tmpfplen = (uchar_t)xstrsncpy(g_tmpfpath, path, TMP_LEN_MAX);
  10197 	DPRINTF_S(g_tmpfpath);
  10198 	DPRINTF_U(tmpfplen);
  10199 
  10200 	return TRUE;
  10201 }
  10202 
  10203 static void cleanup(void)
  10204 {
  10205 #ifndef NOX11
  10206 	if (cfg.x11 && !g_state.picker) {
  10207 		printf("\033[23;0t"); /* reset terminal window title */
  10208 		fflush(stdout);
  10209 	}
  10210 #endif
  10211 	free(selpath);
  10212 	free(plgpath);
  10213 	free(cfgpath);
  10214 	free(initpath);
  10215 	free(bmstr);
  10216 	free(pluginstr);
  10217 	free(listroot);
  10218 	free(ihashbmp);
  10219 	free(dir_dispatched_bmp);
  10220 	free(bookmark);
  10221 	free(plug);
  10222 	free(previewer);
  10223 	if (lastcmdpos != INVALID_POS)
  10224 		for (uchar_t pos = 0; pos <= lastcmdpos; ++pos)
  10225 			free(cmd_hist[pos]);
  10226 #ifndef NOFIFO
  10227 	if (g_state.autofifo)
  10228 		unlink(fifopath);
  10229 #endif
  10230 	if (g_state.pluginit)
  10231 		unlink(g_pipepath);
  10232 #ifdef DEBUG
  10233 	disabledbg();
  10234 #endif
  10235 }
  10236 
  10237 int main(int argc, char *argv[])
  10238 {
  10239 	char *arg = NULL;
  10240 	int fd, opt, sort = 0, pkey = '\0'; /* Plugin key */
  10241 	bool sepnul = FALSE;
  10242 #ifndef NOMOUSE
  10243 	mmask_t mask;
  10244 	char *middle_click_env = xgetenv(env_cfg[NNN_MCLICK], "\0");
  10245 
  10246 	middle_click_key = ((middle_click_env[0] == '^') && middle_click_env[1])
  10247 			    ? CONTROL(middle_click_env[1])
  10248 			    : (uchar_t)middle_click_env[0];
  10249 #endif
  10250 
  10251 	const char * const env_opts = xgetenv(env_cfg[NNN_OPTS], NULL);
  10252 	int env_opts_id = env_opts ? (int)xstrlen(env_opts) : -1;
  10253 	bool hist_file = FALSE;
  10254 
  10255 	while ((opt = (env_opts_id > 0
  10256 		       ? env_opts[--env_opts_id]
  10257 		       : getopt(argc, argv, "aAb:BcCdDeEfF:gHiJKl:nNop:P:QrRs:St:T:uUVxz0h"))) != -1) {
  10258 		switch (opt) {
  10259 #ifndef NOFIFO
  10260 		case 'a':
  10261 			g_state.autofifo = 1;
  10262 			break;
  10263 #endif
  10264 		case 'A':
  10265 			cfg.autoenter = 0;
  10266 			break;
  10267 		case 'b':
  10268 			if (env_opts_id < 0)
  10269 				arg = optarg;
  10270 			break;
  10271 		case 'B':
  10272 			g_state.usebsdtar = 1;
  10273 			break;
  10274 		case 'c':
  10275 			cfg.cliopener = 1;
  10276 			break;
  10277 		case 'C':
  10278 			g_state.oldcolor = 1;
  10279 			break;
  10280 		case 'd':
  10281 			cfg.showdetail = 1;
  10282 			break;
  10283 		case 'D':
  10284 			g_state.dirctx = 1;
  10285 			break;
  10286 		case 'e':
  10287 			cfg.useeditor = 1;
  10288 			break;
  10289 		case 'E':
  10290 			cfg.waitedit = 1;
  10291 			break;
  10292 		case 'f':
  10293 			hist_file = TRUE;
  10294 			break;
  10295 #ifndef NOFIFO
  10296 		case 'F':
  10297 			if (env_opts_id < 0) {
  10298 				fd = atoi(optarg);
  10299 				if ((fd < 0) || (fd > 1))
  10300 					return EXIT_FAILURE;
  10301 				g_state.fifomode = fd;
  10302 			}
  10303 			break;
  10304 #endif
  10305 		case 'g':
  10306 			if (cfg.fuzzy)
  10307 				return EXIT_FAILURE;
  10308 			cfg.regex = 1;
  10309 			filterfn = &visible_re;
  10310 			break;
  10311 		case 'H':
  10312 			cfg.showhidden = 1;
  10313 			break;
  10314 		case 'i':
  10315 			cfg.fileinfo = 1;
  10316 			break;
  10317 		case 'J':
  10318 			g_state.stayonsel = 1;
  10319 			break;
  10320 		case 'K':
  10321 			check_key_collision();
  10322 			return EXIT_SUCCESS;
  10323 		case 'l':
  10324 			if (env_opts_id < 0)
  10325 				scroll_lines = atoi(optarg);
  10326 			break;
  10327 		case 'n':
  10328 			cfg.filtermode = 1;
  10329 			break;
  10330 #ifndef NORL
  10331 		case 'N':
  10332 			g_state.xprompt = 1;
  10333 			break;
  10334 #endif
  10335 		case 'o':
  10336 			cfg.nonavopen = 1;
  10337 			break;
  10338 		case 'p':
  10339 			if (env_opts_id >= 0)
  10340 				break;
  10341 
  10342 			g_state.picker = 1;
  10343 			if (!(optarg[0] == '-' && optarg[1] == '\0')) {
  10344 				fd = open(optarg, O_WRONLY | O_CREAT, 0600);
  10345 				if (fd == -1) {
  10346 					xerror();
  10347 					return EXIT_FAILURE;
  10348 				}
  10349 
  10350 				close(fd);
  10351 				selpath = abspath(optarg, NULL, NULL);
  10352 				if (!selpath) {
  10353 					xerror();
  10354 					return EXIT_FAILURE;
  10355 				}
  10356 
  10357 				unlink(selpath);
  10358 			}
  10359 			break;
  10360 		case 'P':
  10361 			if (env_opts_id < 0 && !optarg[1])
  10362 				pkey = (uchar_t)optarg[0];
  10363 			break;
  10364 		case 'Q':
  10365 			g_state.forcequit = 1;
  10366 			break;
  10367 		case 'r':
  10368 #ifdef __linux__
  10369 			memcpy(cp, PROGRESS_CP, sizeof PROGRESS_CP);
  10370 			memcpy(mv, PROGRESS_MV, sizeof PROGRESS_MV);
  10371 #endif
  10372 			break;
  10373 		case 'R':
  10374 			cfg.rollover = 0;
  10375 			break;
  10376 #ifndef NOSSN
  10377 		case 's':
  10378 			if (env_opts_id < 0)
  10379 				xstrsncpy(curssn, optarg, NAME_MAX);
  10380 			break;
  10381 		case 'S':
  10382 			g_state.prstssn = 1;
  10383 			if (!curssn[0]) /* Support named persistent sessions */
  10384 				curssn[0] = '@';
  10385 			break;
  10386 #endif
  10387 		case 't':
  10388 			if (env_opts_id < 0)
  10389 				idletimeout = atoi(optarg);
  10390 			break;
  10391 		case 'T':
  10392 			if (env_opts_id < 0)
  10393 				sort = (uchar_t)optarg[0];
  10394 			break;
  10395 		case 'u':
  10396 			cfg.prefersel = 1;
  10397 			break;
  10398 		case 'U':
  10399 			g_state.uidgid = 1;
  10400 			break;
  10401 		case 'V':
  10402 			fprintf(stdout, "%s\n", VERSION);
  10403 			return EXIT_SUCCESS;
  10404 		case 'x':
  10405 			cfg.x11 = 1;
  10406 			break;
  10407 		case 'z':
  10408 			if (cfg.regex)
  10409 				return EXIT_FAILURE;
  10410 			cfg.fuzzy = 1;
  10411 			filterfn = &visible_fuzzy;
  10412 			break;
  10413 		case '0':
  10414 			sepnul = TRUE;
  10415 			break;
  10416 		case 'h':
  10417 			usage();
  10418 			return EXIT_SUCCESS;
  10419 		default:
  10420 			usage();
  10421 			return EXIT_FAILURE;
  10422 		}
  10423 		if (env_opts_id == 0)
  10424 			env_opts_id = -1;
  10425 	}
  10426 
  10427 #ifdef DEBUG
  10428 	enabledbg();
  10429 	DPRINTF_S(VERSION);
  10430 #endif
  10431 
  10432 	devnullfd = open("/dev/null", O_RDWR | O_CLOEXEC);
  10433 	if (devnullfd < 0) {
  10434 		xerror();
  10435 		return EXIT_FAILURE;
  10436 	}
  10437 
  10438 	/* Prefix for temporary files */
  10439 	if (!set_tmp_path())
  10440 		return EXIT_FAILURE;
  10441 
  10442 	atexit(cleanup);
  10443 
  10444 	/* Check if we are in path list mode */
  10445 	if (!isatty(STDIN_FILENO)) {
  10446 		/* This is the same as listpath */
  10447 		initpath = load_input(STDIN_FILENO, NULL);
  10448 		if (!initpath)
  10449 			return EXIT_FAILURE;
  10450 
  10451 		/* We return to tty */
  10452 		if (!isatty(STDOUT_FILENO)) {
  10453 			fd = open(ctermid(NULL), O_RDONLY, 0400);
  10454 			if (fd != -1) {
  10455 				dup2(fd, STDIN_FILENO);
  10456 				close(fd);
  10457 			}
  10458 		} else
  10459 			dup2(STDOUT_FILENO, STDIN_FILENO);
  10460 
  10461 #ifndef NOSSN
  10462 		if (curssn[0])
  10463 			curssn[0] = '\0';
  10464 #endif
  10465 	}
  10466 
  10467 	home = getenv("HOME");
  10468 	if (!home) {
  10469 		msg("set HOME");
  10470 		return EXIT_FAILURE;
  10471 	}
  10472 	DPRINTF_S(home);
  10473 	homelen = (uchar_t)xstrlen(home);
  10474 
  10475 	if (!setup_config())
  10476 		return EXIT_FAILURE;
  10477 
  10478 	/* Get custom opener, if set */
  10479 	opener = xgetenv(env_cfg[NNN_OPENER], utils[UTIL_OPENER]);
  10480 	DPRINTF_S(opener);
  10481 
  10482 	/* Parse bookmarks string */
  10483 	if (!parsekvpair(&bookmark, &bmstr, NNN_BMS, &maxbm)) {
  10484 		msg(env_cfg[NNN_BMS]);
  10485 		return EXIT_FAILURE;
  10486 	}
  10487 
  10488 	/* Parse order string */
  10489 	if (!parsekvpair(&order, &orderstr, NNN_ORDER, &maxorder)) {
  10490 		msg(env_cfg[NNN_ORDER]);
  10491 		return EXIT_FAILURE;
  10492 	}
  10493 
  10494 	if (!initpath) {
  10495 		if (arg) { /* Open a bookmark directly */
  10496 			if (!arg[1]) /* Bookmarks keys are single char */
  10497 				initpath = get_kv_val(bookmark, NULL, *arg, maxbm, NNN_BMS);
  10498 
  10499 			if (!initpath) {
  10500 				msg(messages[MSG_INVALID_KEY]);
  10501 				return EXIT_FAILURE;
  10502 			}
  10503 
  10504 #ifndef NOSSN
  10505 			if (curssn[0])
  10506 				curssn[0] = '\0';
  10507 #endif
  10508 		} else if (argc == optind) {
  10509 			/* Start in the current directory */
  10510 			char *startpath = getenv("PWD");
  10511 
  10512 			initpath = (startpath && *startpath) ? xstrdup(startpath) : getcwd(NULL, 0); // NOLINT
  10513 			if (!initpath)
  10514 				initpath = "/";
  10515 		} else { /* Open a file */
  10516 			arg = argv[optind];
  10517 			DPRINTF_S(arg);
  10518 			size_t len = xstrlen(arg);
  10519 
  10520 			if (len > 7 && is_prefix(arg, "file://", 7)) {
  10521 				arg = arg + 7;
  10522 				len -= 7;
  10523 			}
  10524 			initpath = abspath(arg, NULL, NULL);
  10525 			DPRINTF_S(initpath);
  10526 			if (!initpath) {
  10527 				xerror();
  10528 				return EXIT_FAILURE;
  10529 			}
  10530 
  10531 			/* If the file is hidden, enable hidden option */
  10532 			if (*xbasename(initpath) == '.')
  10533 				cfg.showhidden = 1;
  10534 
  10535 			/*
  10536 			 * If nnn is set as the file manager, applications may try to open
  10537 			 * files by invoking nnn. In that case pass the file path to the
  10538 			 * desktop opener and exit.
  10539 			 */
  10540 			struct stat sb;
  10541 
  10542 			if (stat(initpath, &sb) == -1) {
  10543 				bool dir = (arg[len - 1] == '/');
  10544 
  10545 				if (!dir) {
  10546 					arg = xbasename(initpath);
  10547 					initpath = xdirname(initpath);
  10548 
  10549 					pkey = CREATE_NEW_KEY; /* Override plugin key */
  10550 					g_state.initfile = 1;
  10551 				}
  10552 				if (dir || (arg != initpath)) { /* We have a directory */
  10553 					if (!xdiraccess(initpath) && !xmktree(initpath, TRUE, TRUE)) {
  10554 						xerror(); /* Fail if directory cannot be created */
  10555 						return EXIT_FAILURE;
  10556 					}
  10557 					if (!dir) /* Restore the complete path */
  10558 						*--arg = '/';
  10559 				}
  10560 			} else if (!S_ISDIR(sb.st_mode))
  10561 				g_state.initfile = 1;
  10562 
  10563 #ifndef NOSSN
  10564 			if (curssn[0])
  10565 				curssn[0] = '\0';
  10566 #endif
  10567 		}
  10568 	}
  10569 
  10570 	/* Set archive handling (enveditor used as tmp var) */
  10571 	enveditor = getenv(env_cfg[NNN_ARCHIVE]);
  10572 #ifdef PCRE2
  10573 	if (setfilter(&archive_pcre2, (enveditor ? enveditor : patterns[P_ARCHIVE]))) {
  10574 #else
  10575 	if (setfilter(&archive_re, (enveditor ? enveditor : patterns[P_ARCHIVE]))) {
  10576 #endif
  10577 		msg(messages[MSG_INVALID_REG]);
  10578 		return EXIT_FAILURE;
  10579 	}
  10580 
  10581 	/* An all-CLI opener overrides option -e) */
  10582 	if (cfg.cliopener)
  10583 		cfg.useeditor = 0;
  10584 
  10585 	/* Get VISUAL/EDITOR */
  10586 	enveditor = xgetenv(envs[ENV_EDITOR], utils[UTIL_VI]);
  10587 	editor = xgetenv(envs[ENV_VISUAL], enveditor);
  10588 	DPRINTF_S(getenv(envs[ENV_VISUAL]));
  10589 	DPRINTF_S(getenv(envs[ENV_EDITOR]));
  10590 	DPRINTF_S(editor);
  10591 
  10592 	/* Get PAGER */
  10593 	pager = xgetenv(envs[ENV_PAGER], utils[UTIL_LESS]);
  10594 	DPRINTF_S(pager);
  10595 
  10596 	/* Get SHELL */
  10597 	shell = xgetenv(envs[ENV_SHELL], utils[UTIL_SH]);
  10598 	DPRINTF_S(shell);
  10599 
  10600 	DPRINTF_S(getenv("PWD"));
  10601 
  10602 #ifndef NOFIFO
  10603 	/* Create fifo */
  10604 	if (g_state.autofifo) {
  10605 		g_tmpfpath[tmpfplen - 1] = '\0';
  10606 
  10607 		size_t r = mkpath(g_tmpfpath, "nnn-fifo.", g_buf);
  10608 
  10609 		xstrsncpy(g_buf + r - 1, xitoa(getpid()), PATH_MAX - r);
  10610 		setenv("NNN_FIFO", g_buf, TRUE);
  10611 	}
  10612 
  10613 	fifopath = xgetenv("NNN_FIFO", NULL);
  10614 	if (fifopath) {
  10615 		if (mkfifo(fifopath, 0600) != 0 && !(errno == EEXIST && access(fifopath, W_OK) == 0)) {
  10616 			xerror();
  10617 			return EXIT_FAILURE;
  10618 		}
  10619 
  10620 		sigaction(SIGPIPE, &(struct sigaction){.sa_handler = SIG_IGN}, NULL);
  10621 	}
  10622 #endif
  10623 
  10624 #ifdef LINUX_INOTIFY
  10625 	/* Initialize inotify */
  10626 	inotify_fd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC);
  10627 	if (inotify_fd < 0) {
  10628 		xerror();
  10629 		return EXIT_FAILURE;
  10630 	}
  10631 #elif defined(BSD_KQUEUE)
  10632 	kq = kqueue();
  10633 	if (kq < 0) {
  10634 		xerror();
  10635 		return EXIT_FAILURE;
  10636 	}
  10637 #elif defined(HAIKU_NM)
  10638 	haiku_hnd = haiku_init_nm();
  10639 	if (!haiku_hnd) {
  10640 		xerror();
  10641 		return EXIT_FAILURE;
  10642 	}
  10643 #endif
  10644 
  10645 	/* Configure trash preference */
  10646 	trashcmd = getenv(env_cfg[NNN_TRASH]);
  10647 	if (trashcmd) {
  10648 		if ((trashcmd[0] == '1') && (trashcmd[1] == '\0'))
  10649 			trashcmd = utils[UTIL_TRASH_CLI];
  10650 		else if ((trashcmd[0] == '2') && (trashcmd[1] == '\0'))
  10651 			trashcmd = utils[UTIL_GIO_TRASH];
  10652 	}
  10653 
  10654 	/* Ignore/handle certain signals */
  10655 	struct sigaction act = {.sa_handler = sigint_handler};
  10656 
  10657 	if (sigaction(SIGINT, &act, NULL) < 0) {
  10658 		xerror();
  10659 		return EXIT_FAILURE;
  10660 	}
  10661 
  10662 	act.sa_handler = clean_exit_sighandler;
  10663 
  10664 	if (sigaction(SIGTERM, &act, NULL) < 0 || sigaction(SIGHUP, &act, NULL) < 0) {
  10665 		xerror();
  10666 		return EXIT_FAILURE;
  10667 	}
  10668 
  10669 	act.sa_handler = SIG_IGN;
  10670 
  10671 	if (sigaction(SIGQUIT, &act, NULL) < 0) {
  10672 		xerror();
  10673 		return EXIT_FAILURE;
  10674 	}
  10675 
  10676 #ifndef NOLC
  10677 	/* Set locale */
  10678 	setlocale(LC_COLLATE, "");
  10679 	setlocale(LC_CTYPE, "");
  10680 #ifdef PCRE2
  10681 	tables = pcre2_maketables(NULL);
  10682 #endif
  10683 #endif
  10684 
  10685 #ifndef NORL
  10686 #if RL_READLINE_VERSION >= 0x0603
  10687 	/* readline would overwrite the WINCH signal hook */
  10688 	rl_change_environment = 0;
  10689 #endif
  10690 	/* Bind TAB to cycling */
  10691 	rl_variable_bind("completion-ignore-case", "on");
  10692 #ifdef __linux__
  10693 	rl_bind_key('\t', rl_menu_complete);
  10694 #else
  10695 	rl_bind_key('\t', rl_complete);
  10696 #endif
  10697 #endif
  10698 
  10699 	/* Keep the history file management separate for readline and
  10700 	 * native even if xprompt is set. The file location is the same.
  10701 	 */
  10702 	if (hist_file) {
  10703 		mkpath(cfgpath, ".history", g_buf);
  10704 #ifndef NORL
  10705 		read_history(g_buf);
  10706 #else
  10707 		xread_history(g_buf);
  10708 #endif
  10709 	}
  10710 
  10711 #ifndef NOX11
  10712 	if (cfg.x11 && !g_state.picker) {
  10713 		/* Save terminal window title */
  10714 		printf("\033[22;0t");
  10715 		fflush(stdout);
  10716 		gethostname(hostname, sizeof(hostname));
  10717 		hostname[sizeof(hostname) - 1] = '\0';
  10718 	}
  10719 #endif
  10720 
  10721 #ifndef NOMOUSE
  10722 	if (!initcurses(&mask))
  10723 #else
  10724 	if (!initcurses(NULL))
  10725 #endif
  10726 		return EXIT_FAILURE;
  10727 
  10728 	if (sort)
  10729 		set_sort_flags(sort);
  10730 
  10731 	opt = browse(initpath, pkey);
  10732 
  10733 #ifndef NOSSN
  10734 	if (curssn[0] && g_state.prstssn)
  10735 		save_session(curssn, NULL);
  10736 #endif
  10737 
  10738 #ifndef NOMOUSE
  10739 	mousemask(mask, NULL);
  10740 #endif
  10741 
  10742 	exitcurses();
  10743 
  10744 #ifndef NORL
  10745 	if (hist_file && !(g_state.picker || g_state.xprompt)) {
  10746 		mkpath(cfgpath, ".history", g_buf);
  10747 		write_history(g_buf);
  10748 	}
  10749 #else
  10750 	if (hist_file)
  10751 	{
  10752 		mkpath(cfgpath, ".history", g_buf);
  10753 		xwrite_history(g_buf);
  10754 	}
  10755 #endif
  10756 
  10757 	if (g_state.picker) {
  10758 		if (selbufpos) {
  10759 			fd = selpath ? open(selpath, O_WRONLY | O_CREAT | O_TRUNC, 0600) : STDOUT_FILENO;
  10760 			if ((fd == -1) || (seltofile(fd, NULL, sepnul ? "\0" : NEWLINE) != (size_t)(selbufpos)))
  10761 				xerror();
  10762 
  10763 			if (fd > 1)
  10764 				close(fd);
  10765 		}
  10766 	} else if (selpath)
  10767 		unlink(selpath);
  10768 
  10769 	/* Remove tmp dir in list mode */
  10770 	rmlistpath();
  10771 
  10772 	/* Free the regex */
  10773 #ifdef PCRE2
  10774 	pcre2_code_free(archive_pcre2);
  10775 #else
  10776 	regfree(&archive_re);
  10777 #endif
  10778 
  10779 	/* Free the selection buffer */
  10780 	free(pselbuf);
  10781 
  10782 #ifdef LINUX_INOTIFY
  10783 	/* Shutdown inotify */
  10784 	if (inotify_wd >= 0)
  10785 		inotify_rm_watch(inotify_fd, inotify_wd);
  10786 	close(inotify_fd);
  10787 #elif defined(BSD_KQUEUE)
  10788 	if (event_fd >= 0)
  10789 		close(event_fd);
  10790 	close(kq);
  10791 #elif defined(HAIKU_NM)
  10792 	haiku_close_nm(haiku_hnd);
  10793 #endif
  10794 
  10795 #ifndef NOFIFO
  10796 	if (!g_state.fifomode)
  10797 		notify_fifo(FALSE);
  10798 	if (fifofd != -1)
  10799 		close(fifofd);
  10800 #endif
  10801 
  10802 	return opt;
  10803 }