sistema_progs

Programas para customizar o meu entorno de traballo nos meus equipos persoais
Log | Files | Refs

nnn.c (198790B)


      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-2022, 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 #if defined(__linux__) || defined(MINGW) || defined(__MINGW32__) \
     32 	|| defined(__MINGW64__) || defined(__CYGWIN__)
     33 #ifndef _GNU_SOURCE
     34 #define _GNU_SOURCE
     35 #endif
     36 #if defined(__arm__) || defined(__i386__)
     37 #define _FILE_OFFSET_BITS 64 /* Support large files on 32-bit */
     38 #endif
     39 #if defined(__linux__)
     40 #include <sys/inotify.h>
     41 #define LINUX_INOTIFY
     42 #endif
     43 #if !defined(__GLIBC__)
     44 #include <sys/types.h>
     45 #endif
     46 #endif
     47 #include <sys/resource.h>
     48 #include <sys/stat.h>
     49 #include <sys/statvfs.h>
     50 #if defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__)
     51 #include <sys/types.h>
     52 #include <sys/event.h>
     53 #include <sys/time.h>
     54 #define BSD_KQUEUE
     55 #elif defined(__HAIKU__)
     56 #include "../misc/haiku/haiku_interop.h"
     57 #define HAIKU_NM
     58 #else
     59 #include <sys/sysmacros.h>
     60 #endif
     61 #include <sys/wait.h>
     62 
     63 #ifdef __linux__ /* Fix failure due to mvaddnwstr() */
     64 #ifndef NCURSES_WIDECHAR
     65 #define NCURSES_WIDECHAR 1
     66 #endif
     67 #elif defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) \
     68 	|| defined(__APPLE__) || defined(__sun)
     69 #ifndef _XOPEN_SOURCE_EXTENDED
     70 #define _XOPEN_SOURCE_EXTENDED
     71 #endif
     72 #endif
     73 #ifndef __USE_XOPEN /* Fix wcswidth() failure, ncursesw/curses.h includes whcar.h on Ubuntu 14.04 */
     74 #define __USE_XOPEN
     75 #endif
     76 #include <dirent.h>
     77 #include <errno.h>
     78 #include <fcntl.h>
     79 #include <fts.h>
     80 #include <libgen.h>
     81 #include <limits.h>
     82 #ifndef NOLC
     83 #include <locale.h>
     84 #endif
     85 #include <pthread.h>
     86 #include <stdio.h>
     87 #ifndef NORL
     88 #include <readline/history.h>
     89 #include <readline/readline.h>
     90 #endif
     91 #ifdef PCRE
     92 #include <pcre.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 #ifndef __USE_XOPEN_EXTENDED
    104 #define __USE_XOPEN_EXTENDED 1
    105 #endif
    106 #include <ftw.h>
    107 #include <wchar.h>
    108 #include <pwd.h>
    109 #include <grp.h>
    110 
    111 #ifdef MACOS_BELOW_1012
    112 #include "../misc/macos-legacy/mach_gettime.h"
    113 #endif
    114 
    115 #if !defined(alloca) && defined(__GNUC__)
    116 /*
    117  * GCC doesn't expand alloca() to __builtin_alloca() in standards mode
    118  * (-std=...) and not all standard libraries do or supply it, e.g.
    119  * NetBSD/arm64 so explicitly use the builtin.
    120  */
    121 #define alloca(size) __builtin_alloca(size)
    122 #endif
    123 
    124 #include "nnn.h"
    125 #include "dbg.h"
    126 
    127 #if defined(ICONS) || defined(NERD)
    128 #include "icons.h"
    129 #define ICONS_ENABLED
    130 #endif
    131 
    132 #ifdef TOURBIN_QSORT
    133 #include "qsort.h"
    134 #endif
    135 
    136 /* Macro definitions */
    137 #define VERSION      "4.4"
    138 #define GENERAL_INFO "BSD 2-Clause\nhttps://github.com/jarun/nnn"
    139 
    140 #ifndef NOSSN
    141 #define SESSIONS_VERSION 1
    142 #endif
    143 
    144 #ifndef S_BLKSIZE
    145 #define S_BLKSIZE 512 /* S_BLKSIZE is missing on Android NDK (Termux) */
    146 #endif
    147 
    148 /*
    149  * NAME_MAX and PATH_MAX may not exist, e.g. with dirent.c_name being a
    150  * flexible array on Illumos. Use somewhat accommodating fallback values.
    151  */
    152 #ifndef NAME_MAX
    153 #define NAME_MAX 255
    154 #endif
    155 
    156 #ifndef PATH_MAX
    157 #define PATH_MAX 4096
    158 #endif
    159 
    160 #define _ABSSUB(N, M)   (((N) <= (M)) ? ((M) - (N)) : ((N) - (M)))
    161 #define ELEMENTS(x)     (sizeof(x) / sizeof(*(x)))
    162 #undef MIN
    163 #define MIN(x, y)       ((x) < (y) ? (x) : (y))
    164 #undef MAX
    165 #define MAX(x, y)       ((x) > (y) ? (x) : (y))
    166 #define ISODD(x)        ((x) & 1)
    167 #define ISBLANK(x)      ((x) == ' ' || (x) == '\t')
    168 #define TOUPPER(ch)     (((ch) >= 'a' && (ch) <= 'z') ? ((ch) - 'a' + 'A') : (ch))
    169 #define TOLOWER(ch)     (((ch) >= 'A' && (ch) <= 'Z') ? ((ch) - 'A' + 'a') : (ch))
    170 #define ISUPPER_(ch)    ((ch) >= 'A' && (ch) <= 'Z')
    171 #define ISLOWER_(ch)    ((ch) >= 'a' && (ch) <= 'z')
    172 #define CMD_LEN_MAX     (PATH_MAX + ((NAME_MAX + 1) << 1))
    173 #define ALIGN_UP(x, A)  ((((x) + (A) - 1) / (A)) * (A))
    174 #define READLINE_MAX    256
    175 #define FILTER          '/'
    176 #define RFILTER         '\\'
    177 #define CASE            ':'
    178 #define MSGWAIT         '$'
    179 #define SELECT          ' '
    180 #define PROMPT          ">>> "
    181 #define REGEX_MAX       48
    182 #define ENTRY_INCR      64 /* Number of dir 'entry' structures to allocate per shot */
    183 #define NAMEBUF_INCR    0x800 /* 64 dir entries at once, avg. 32 chars per file name = 64*32B = 2KB */
    184 #define DESCRIPTOR_LEN  32
    185 #define _ALIGNMENT      0x10 /* 16-byte alignment */
    186 #define _ALIGNMENT_MASK 0xF
    187 #define TMP_LEN_MAX     64
    188 #define DOT_FILTER_LEN  7
    189 #define ASCII_MAX       128
    190 #define EXEC_ARGS_MAX   10
    191 #define LIST_FILES_MAX  (1 << 16)
    192 #define SCROLLOFF       3
    193 #define COLOR_256       256
    194 
    195 /* Time intervals */
    196 #define DBLCLK_INTERVAL_NS (400000000)
    197 #define XDELAY_INTERVAL_MS (350000) /* 350 ms delay */
    198 
    199 #ifndef CTX8
    200 #define CTX_MAX 4
    201 #else
    202 #define CTX_MAX 8
    203 #endif
    204 
    205 #ifndef SED
    206 /* BSDs or Solaris or SunOS */
    207 #if defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__) || defined(sun) || defined(__sun)
    208 #define SED "gsed"
    209 #else
    210 #define SED "sed"
    211 #endif
    212 #endif
    213 
    214 /* Large selection threshold */
    215 #ifndef LARGESEL
    216 #define LARGESEL 1000
    217 #endif
    218 
    219 #define MIN_DISPLAY_COL (CTX_MAX * 2)
    220 #define ARCHIVE_CMD_LEN 16
    221 #define BLK_SHIFT_512   9
    222 
    223 /* Detect hardlinks in du */
    224 #define HASH_BITS   (0xFFFFFF)
    225 #define HASH_OCTETS (HASH_BITS >> 6) /* 2^6 = 64 */
    226 
    227 /* Entry flags */
    228 #define DIR_OR_DIRLNK 0x01
    229 #define HARD_LINK     0x02
    230 #define SYM_ORPHAN    0x04
    231 #define FILE_MISSING  0x08
    232 #define FILE_SELECTED 0x10
    233 #define FILE_SCANNED  0x20
    234 
    235 /* Macros to define process spawn behaviour as flags */
    236 #define F_NONE    0x00  /* no flag set */
    237 #define F_MULTI   0x01  /* first arg can be combination of args; to be used with F_NORMAL */
    238 #define F_NOWAIT  0x02  /* don't wait for child process (e.g. file manager) */
    239 #define F_NOTRACE 0x04  /* suppress stdout and stderr (no traces) */
    240 #define F_NORMAL  0x08  /* spawn child process in non-curses regular CLI mode */
    241 #define F_CONFIRM 0x10  /* run command - show results before exit (must have F_NORMAL) */
    242 #define F_CHKRTN  0x20  /* wait for user prompt if cmd returns failure status */
    243 #define F_NOSTDIN 0x40  /* suppress stdin */
    244 #define F_PAGE    0x80  /* page output in run-cmd-as-plugin mode */
    245 #define F_TTY     0x100 /* Force stdout to go to tty if redirected to a non-tty */
    246 #define F_CLI     (F_NORMAL | F_MULTI)
    247 #define F_SILENT  (F_CLI | F_NOTRACE)
    248 
    249 /* Version compare macros */
    250 /*
    251  * states: S_N: normal, S_I: comparing integral part, S_F: comparing
    252  *         fractional parts, S_Z: idem but with leading Zeroes only
    253  */
    254 #define S_N 0x0
    255 #define S_I 0x3
    256 #define S_F 0x6
    257 #define S_Z 0x9
    258 
    259 /* result_type: VCMP: return diff; VLEN: compare using len_diff/diff */
    260 #define VCMP 2
    261 #define VLEN 3
    262 
    263 /* Volume info */
    264 #define FREE     0
    265 #define CAPACITY 1
    266 
    267 /* TYPE DEFINITIONS */
    268 typedef unsigned int uint_t;
    269 typedef unsigned char uchar_t;
    270 typedef unsigned short ushort_t;
    271 typedef unsigned long long ullong_t;
    272 
    273 /* STRUCTURES */
    274 
    275 /* Directory entry */
    276 typedef struct entry {
    277 	char *name;  /* 8 bytes */
    278 	time_t sec;  /* 8 bytes */
    279 	uint_t nsec; /* 4 bytes (enough to store nanosec) */
    280 	mode_t mode; /* 4 bytes */
    281 	off_t size;  /* 8 bytes */
    282 	struct {
    283 		ullong_t blocks : 40; /* 5 bytes (enough for 512 TiB in 512B blocks allocated) */
    284 		ullong_t nlen   : 16; /* 2 bytes (length of file name) */
    285 		ullong_t flags  : 8;  /* 1 byte (flags specific to the file) */
    286 	};
    287 #ifndef NOUG
    288 	uid_t uid; /* 4 bytes */
    289 	gid_t gid; /* 4 bytes */
    290 #endif
    291 } *pEntry;
    292 
    293 /* Selection marker */
    294 typedef struct {
    295 	char *startpos;
    296 	size_t len;
    297 } selmark;
    298 
    299 /* Key-value pairs from env */
    300 typedef struct {
    301 	int key;
    302 	int off;
    303 } kv;
    304 
    305 typedef struct {
    306 #ifdef PCRE
    307 	const pcre *pcrex;
    308 #else
    309 	const regex_t *regex;
    310 #endif
    311 	const char *str;
    312 } fltrexp_t;
    313 
    314 /*
    315  * Settings
    316  * NOTE: update default values if changing order
    317  */
    318 typedef struct {
    319 	uint_t filtermode : 1;  /* Set to enter filter mode */
    320 	uint_t timeorder  : 1;  /* Set to sort by time */
    321 	uint_t sizeorder  : 1;  /* Set to sort by file size */
    322 	uint_t apparentsz : 1;  /* Set to sort by apparent size (disk usage) */
    323 	uint_t blkorder   : 1;  /* Set to sort by blocks used (disk usage) */
    324 	uint_t extnorder  : 1;  /* Order by extension */
    325 	uint_t showhidden : 1;  /* Set to show hidden files */
    326 	uint_t reserved0  : 1;
    327 	uint_t showdetail : 1;  /* Clear to show lesser file info */
    328 	uint_t ctxactive  : 1;  /* Context active or not */
    329 	uint_t reverse    : 1;  /* Reverse sort */
    330 	uint_t version    : 1;  /* Version sort */
    331 	uint_t reserved1  : 1;
    332 	/* The following settings are global */
    333 	uint_t curctx     : 3;  /* Current context number */
    334 	uint_t prefersel  : 1;  /* Prefer selection over current, if exists */
    335 	uint_t fileinfo   : 1;  /* Show file information on hover */
    336 	uint_t nonavopen  : 1;  /* Open file on right arrow or `l` */
    337 	uint_t autoenter  : 1;  /* auto-enter dir in type-to-nav mode */
    338 	uint_t reserved2  : 1;
    339 	uint_t useeditor  : 1;  /* Use VISUAL to open text files */
    340 	uint_t reserved3  : 3;
    341 	uint_t regex      : 1;  /* Use regex filters */
    342 	uint_t x11        : 1;  /* Copy to system clipboard, show notis, xterm title */
    343 	uint_t timetype   : 2;  /* Time sort type (0: access, 1: change, 2: modification) */
    344 	uint_t cliopener  : 1;  /* All-CLI app opener */
    345 	uint_t waitedit   : 1;  /* For ops that can't be detached, used EDITOR */
    346 	uint_t rollover   : 1;  /* Roll over at edges */
    347 } settings;
    348 
    349 /* Non-persistent program-internal states (alphabeical order) */
    350 typedef struct {
    351 	uint_t autofifo   : 1;  /* Auto-create NNN_FIFO */
    352 	uint_t autonext   : 1;  /* Auto-proceed on open */
    353 	uint_t dircolor   : 1;  /* Current status of dir color */
    354 	uint_t dirctx     : 1;  /* Show dirs in context color */
    355 	uint_t duinit     : 1;  /* Initialize disk usage */
    356 	uint_t fifomode   : 1;  /* FIFO notify mode: 0: preview, 1: explore */
    357 	uint_t forcequit  : 1;  /* Do not prompt on quit */
    358 	uint_t initfile   : 1;  /* Positional arg is a file */
    359 	uint_t interrupt  : 1;  /* Program received an interrupt */
    360 	uint_t move       : 1;  /* Move operation */
    361 	uint_t oldcolor   : 1;  /* Use older colorscheme */
    362 	uint_t picked     : 1;  /* Plugin has picked files */
    363 	uint_t picker     : 1;  /* Write selection to user-specified file */
    364 	uint_t pluginit   : 1;  /* Plugin framework initialized */
    365 	uint_t prstssn    : 1;  /* Persistent session */
    366 	uint_t rangesel   : 1;  /* Range selection on */
    367 	uint_t runctx     : 3;  /* The context in which plugin is to be run */
    368 	uint_t runplugin  : 1;  /* Choose plugin mode */
    369 	uint_t selmode    : 1;  /* Set when selecting files */
    370 	uint_t stayonsel  : 1;  /* Disable auto-proceed on select */
    371 	uint_t trash      : 2;  /* Trash method 0: rm -rf, 1: trash-cli, 2: gio trash */
    372 	uint_t uidgid     : 1;  /* Show owner and group info */
    373 	uint_t reserved   : 7;  /* Adjust when adding/removing a field */
    374 } runstate;
    375 
    376 /* Contexts or workspaces */
    377 typedef struct {
    378 	char c_path[PATH_MAX];     /* Current dir */
    379 	char c_last[PATH_MAX];     /* Last visited dir */
    380 	char c_name[NAME_MAX + 1]; /* Current file name */
    381 	char c_fltr[REGEX_MAX];    /* Current filter */
    382 	settings c_cfg;            /* Current configuration */
    383 	uint_t color;              /* Color code for directories */
    384 } context;
    385 
    386 #ifndef NOSSN
    387 typedef struct {
    388 	size_t ver;
    389 	size_t pathln[CTX_MAX];
    390 	size_t lastln[CTX_MAX];
    391 	size_t nameln[CTX_MAX];
    392 	size_t fltrln[CTX_MAX];
    393 } session_header_t;
    394 #endif
    395 
    396 /* GLOBALS */
    397 
    398 /* Configuration, contexts */
    399 static settings cfg = {
    400 	0, /* filtermode */
    401 	0, /* timeorder */
    402 	0, /* sizeorder */
    403 	0, /* apparentsz */
    404 	0, /* blkorder */
    405 	0, /* extnorder */
    406 	0, /* showhidden */
    407 	0, /* reserved0 */
    408 	0, /* showdetail */
    409 	1, /* ctxactive */
    410 	0, /* reverse */
    411 	0, /* version */
    412 	0, /* reserved1 */
    413 	0, /* curctx */
    414 	0, /* prefersel */
    415 	0, /* fileinfo */
    416 	0, /* nonavopen */
    417 	1, /* autoenter */
    418 	0, /* reserved2 */
    419 	0, /* useeditor */
    420 	0, /* reserved3 */
    421 	0, /* regex */
    422 	0, /* x11 */
    423 	2, /* timetype (T_MOD) */
    424 	0, /* cliopener */
    425 	0, /* waitedit */
    426 	1, /* rollover */
    427 };
    428 
    429 static context g_ctx[CTX_MAX] __attribute__ ((aligned));
    430 
    431 static int ndents, cur, last, curscroll, last_curscroll, total_dents = ENTRY_INCR, scroll_lines = 1;
    432 static int nselected;
    433 #ifndef NOFIFO
    434 static int fifofd = -1;
    435 #endif
    436 static uint_t idletimeout, selbufpos, selbuflen;
    437 static ushort_t xlines, xcols;
    438 static ushort_t idle;
    439 static uchar_t maxbm, maxplug, maxorder;
    440 static uchar_t cfgsort[CTX_MAX + 1];
    441 static char *bmstr;
    442 static char *pluginstr;
    443 static char *orderstr;
    444 static char *opener;
    445 static char *editor;
    446 static char *enveditor;
    447 static char *pager;
    448 static char *shell;
    449 static char *home;
    450 static char *initpath;
    451 static char *cfgpath;
    452 static char *selpath;
    453 static char *listpath;
    454 static char *listroot;
    455 static char *plgpath;
    456 static char *pnamebuf, *pselbuf, *findselpos;
    457 static char *mark;
    458 #ifndef NOX11
    459 static char *hostname;
    460 #endif
    461 #ifndef NOFIFO
    462 static char *fifopath;
    463 #endif
    464 static char *lastcmd;
    465 static ullong_t *ihashbmp;
    466 static struct entry *pdents;
    467 static blkcnt_t dir_blocks;
    468 static kv *bookmark;
    469 static kv *plug;
    470 static kv *order;
    471 static uchar_t tmpfplen, homelen;
    472 static uchar_t blk_shift = BLK_SHIFT_512;
    473 #ifndef NOMOUSE
    474 static int middle_click_key;
    475 #endif
    476 #ifdef PCRE
    477 static pcre *archive_pcre;
    478 #else
    479 static regex_t archive_re;
    480 #endif
    481 
    482 /* pthread related */
    483 #define NUM_DU_THREADS (4) /* Can use sysconf(_SC_NPROCESSORS_ONLN) */
    484 #define DU_TEST (((node->fts_info & FTS_F) && \
    485 		(sb->st_nlink <= 1 || test_set_bit((uint_t)sb->st_ino))) || node->fts_info & FTS_DP)
    486 
    487 static int threadbmp = -1; /* Has 1 in the bit position for idle threads */
    488 static volatile int active_threads;
    489 static pthread_mutex_t running_mutex = PTHREAD_MUTEX_INITIALIZER;
    490 static pthread_mutex_t hardlink_mutex = PTHREAD_MUTEX_INITIALIZER;
    491 static ullong_t *core_files;
    492 static blkcnt_t *core_blocks;
    493 static ullong_t num_files;
    494 
    495 typedef struct {
    496 	char path[PATH_MAX];
    497 	int entnum;
    498 	ushort_t core;
    499 	bool mntpoint;
    500 } thread_data;
    501 
    502 static thread_data *core_data;
    503 
    504 /* Retain old signal handlers */
    505 static struct sigaction oldsighup;
    506 static struct sigaction oldsigtstp;
    507 static struct sigaction oldsigwinch;
    508 
    509 /* For use in functions which are isolated and don't return the buffer */
    510 static char g_buf[CMD_LEN_MAX] __attribute__ ((aligned));
    511 
    512 /* For use as a scratch buffer in selection manipulation */
    513 static char g_sel[PATH_MAX] __attribute__ ((aligned));
    514 
    515 /* Buffer to store tmp file path to show selection, file stats and help */
    516 static char g_tmpfpath[TMP_LEN_MAX] __attribute__ ((aligned));
    517 
    518 /* Buffer to store plugins control pipe location */
    519 static char g_pipepath[TMP_LEN_MAX] __attribute__ ((aligned));
    520 
    521 /* Non-persistent runtime states */
    522 static runstate g_state;
    523 
    524 /* Options to identify file MIME */
    525 #if defined(__APPLE__)
    526 #define FILE_MIME_OPTS "-bIL"
    527 #elif !defined(__sun) /* no MIME option for 'file' */
    528 #define FILE_MIME_OPTS "-biL"
    529 #endif
    530 
    531 /* Macros for utilities */
    532 #define UTIL_OPENER    0
    533 #define UTIL_ATOOL     1
    534 #define UTIL_BSDTAR    2
    535 #define UTIL_UNZIP     3
    536 #define UTIL_TAR       4
    537 #define UTIL_LOCKER    5
    538 #define UTIL_LAUNCH    6
    539 #define UTIL_SH_EXEC   7
    540 #define UTIL_BASH      8
    541 #define UTIL_SSHFS     9
    542 #define UTIL_RCLONE    10
    543 #define UTIL_VI        11
    544 #define UTIL_LESS      12
    545 #define UTIL_SH        13
    546 #define UTIL_FZF       14
    547 #define UTIL_NTFY      15
    548 #define UTIL_CBCP      16
    549 #define UTIL_NMV       17
    550 #define UTIL_TRASH_CLI 18
    551 #define UTIL_GIO_TRASH 19
    552 #define UTIL_RM_RF     20
    553 
    554 /* Utilities to open files, run actions */
    555 static char * const utils[] = {
    556 #ifdef __APPLE__
    557 	"/usr/bin/open",
    558 #elif defined __CYGWIN__
    559 	"cygstart",
    560 #elif defined __HAIKU__
    561 	"open",
    562 #else
    563 	"xdg-open",
    564 #endif
    565 	"atool",
    566 	"bsdtar",
    567 	"unzip",
    568 	"tar",
    569 #ifdef __APPLE__
    570 	"bashlock",
    571 #elif defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__)
    572 	"lock",
    573 #elif defined __HAIKU__
    574 	"peaclock",
    575 #else
    576 	"vlock",
    577 #endif
    578 	"launch",
    579 	"sh -c",
    580 	"bash",
    581 	"sshfs",
    582 	"rclone",
    583 	"vi",
    584 	"less",
    585 	"sh",
    586 	"fzf",
    587 	".ntfy",
    588 	".cbcp",
    589 	".nmv",
    590 	"trash-put",
    591 	"gio trash",
    592 	"rm -rf",
    593 };
    594 
    595 /* Common strings */
    596 #define MSG_ZERO         0 /* Unused */
    597 #define MSG_0_ENTRIES    1
    598 #define STR_TMPFILE      2
    599 #define MSG_0_SELECTED   3
    600 #define MSG_CANCEL       4
    601 #define MSG_FAILED       5
    602 #define MSG_SSN_NAME     6
    603 #define MSG_CP_MV_AS     7
    604 #define MSG_CUR_SEL_OPTS 8
    605 #define MSG_FORCE_RM     9
    606 #define MSG_LIMIT        10
    607 #define MSG_NEW_OPTS     11
    608 #define MSG_CLI_MODE     12
    609 #define MSG_OVERWRITE    13
    610 #define MSG_SSN_OPTS     14
    611 #define MSG_QUIT_ALL     15
    612 #define MSG_HOSTNAME     16
    613 #define MSG_ARCHIVE_NAME 17
    614 #define MSG_OPEN_WITH    18
    615 #define MSG_NEW_PATH     19
    616 #define MSG_LINK_PREFIX  20
    617 #define MSG_COPY_NAME    21
    618 #define MSG_ENTER        22
    619 #define MSG_SEL_MISSING  23
    620 #define MSG_ACCESS       24
    621 #define MSG_EMPTY_FILE   25
    622 #define MSG_UNSUPPORTED  26
    623 #define MSG_NOT_SET      27
    624 #define MSG_EXISTS       28
    625 #define MSG_FEW_COLUMNS  29
    626 #define MSG_REMOTE_OPTS  30
    627 #define MSG_RCLONE_DELAY 31
    628 #define MSG_APP_NAME     32
    629 #define MSG_ARCHIVE_OPTS 33
    630 #define MSG_KEYS         34
    631 #define MSG_INVALID_REG  35
    632 #define MSG_ORDER        36
    633 #define MSG_LAZY         37
    634 #define MSG_FIRST        38
    635 #define MSG_RM_TMP       39
    636 #define MSG_INVALID_KEY  40
    637 #define MSG_NOCHANGE     41
    638 #define MSG_DIR_CHANGED  42
    639 
    640 static const char * const messages[] = {
    641 	"",
    642 	"0 entries",
    643 	"/.nnnXXXXXX",
    644 	"0 selected",
    645 	"cancelled",
    646 	"failed!",
    647 	"session name: ",
    648 	"'c'p / 'm'v as?",
    649 	"'c'urrent / 's'el?",
    650 	"%s %s? [Esc cancels]",
    651 	"limit exceeded",
    652 	"'f'ile / 'd'ir / 's'ym / 'h'ard?",
    653 	"'c'li / 'g'ui?",
    654 	"overwrite?",
    655 	"'s'ave / 'l'oad / 'r'estore?",
    656 	"Quit all contexts?",
    657 	"remote name (- for hovered): ",
    658 	"archive [path/]name: ",
    659 	"open with: ",
    660 	"[path/]name: ",
    661 	"link prefix [@ for none]: ",
    662 	"copy [path/]name: ",
    663 	"\n'Enter' to continue",
    664 	"open failed",
    665 	"dir inaccessible",
    666 	"empty! edit/open with",
    667 	"?",
    668 	"not set",
    669 	"entry exists",
    670 	"too few cols!",
    671 	"'s'shfs / 'r'clone?",
    672 	"refresh if slow",
    673 	"app name: ",
    674 	"'o'pen / e'x'tract / 'l's / 'm'nt?",
    675 	"keys:",
    676 	"invalid regex",
    677 	"'a'u / 'd'u / 'e'xt / 'r'ev / 's'z / 't'm / 'v'er / 'c'lr / '^T'?",
    678 	"unmount failed! try lazy?",
    679 	"first file (\')/char?",
    680 	"remove tmp file?",
    681 	"invalid key",
    682 	"unchanged",
    683 	"dir changed, range sel off",
    684 };
    685 
    686 /* Supported configuration environment variables */
    687 #define NNN_OPTS    0
    688 #define NNN_BMS     1
    689 #define NNN_PLUG    2
    690 #define NNN_OPENER  3
    691 #define NNN_COLORS  4
    692 #define NNN_FCOLORS 5
    693 #define NNNLVL      6
    694 #define NNN_PIPE    7
    695 #define NNN_MCLICK  8
    696 #define NNN_SEL     9
    697 #define NNN_ARCHIVE 10
    698 #define NNN_ORDER   11
    699 #define NNN_HELP    12 /* strings end here */
    700 #define NNN_TRASH   13 /* flags begin here */
    701 
    702 static const char * const env_cfg[] = {
    703 	"NNN_OPTS",
    704 	"NNN_BMS",
    705 	"NNN_PLUG",
    706 	"NNN_OPENER",
    707 	"NNN_COLORS",
    708 	"NNN_FCOLORS",
    709 	"NNNLVL",
    710 	"NNN_PIPE",
    711 	"NNN_MCLICK",
    712 	"NNN_SEL",
    713 	"NNN_ARCHIVE",
    714 	"NNN_ORDER",
    715 	"NNN_HELP",
    716 	"NNN_TRASH",
    717 };
    718 
    719 /* Required environment variables */
    720 #define ENV_SHELL  0
    721 #define ENV_VISUAL 1
    722 #define ENV_EDITOR 2
    723 #define ENV_PAGER  3
    724 #define ENV_NCUR   4
    725 
    726 static const char * const envs[] = {
    727 	"SHELL",
    728 	"VISUAL",
    729 	"EDITOR",
    730 	"PAGER",
    731 	"nnn",
    732 };
    733 
    734 /* Time type used */
    735 #define T_ACCESS 0
    736 #define T_CHANGE 1
    737 #define T_MOD    2
    738 
    739 #ifdef __linux__
    740 static char cp[] = "cp   -iRp";
    741 static char mv[] = "mv   -i";
    742 #else
    743 static char cp[] = "cp -iRp";
    744 static char mv[] = "mv -i";
    745 #endif
    746 
    747 /* Archive commands */
    748 static const char * const archive_cmd[] = {"atool -a", "bsdtar -acvf", "zip -r", "tar -acvf"};
    749 
    750 /* Tokens used for path creation */
    751 #define TOK_BM  0
    752 #define TOK_SSN 1
    753 #define TOK_MNT 2
    754 #define TOK_PLG 3
    755 
    756 static const char * const toks[] = {
    757 	"bookmarks",
    758 	"sessions",
    759 	"mounts",
    760 	"plugins", /* must be the last entry */
    761 };
    762 
    763 /* Patterns */
    764 #define P_CPMVFMT 0
    765 #define P_CPMVRNM 1
    766 #define P_ARCHIVE 2
    767 #define P_REPLACE 3
    768 #define P_ARCHIVE_CMD 4
    769 
    770 static const char * const patterns[] = {
    771 	SED" -i 's|^\\(\\(.*/\\)\\(.*\\)$\\)|#\\1\\n\\3|' %s",
    772 	SED" 's|^\\([^#/][^/]\\?.*\\)$|%s/\\1|;s|^#\\(/.*\\)$|\\1|' "
    773 		"%s | tr '\\n' '\\0' | xargs -0 -n2 sh -c '%s \"$0\" \"$@\" < /dev/tty'",
    774 	"\\.(bz|bz2|gz|tar|taz|tbz|tbz2|tgz|z|zip)$",
    775 	SED" -i 's|^%s\\(.*\\)$|%s\\1|' %s",
    776 	SED" -ze 's|^%s/||' '%s' | xargs -0 %s %s",
    777 };
    778 
    779 /* Colors */
    780 #define C_BLK (CTX_MAX + 1) /* Block device: DarkSeaGreen1 */
    781 #define C_CHR (C_BLK + 1)   /* Character device: Yellow1 */
    782 #define C_DIR (C_CHR + 1)   /* Directory: DeepSkyBlue1 */
    783 #define C_EXE (C_DIR + 1)   /* Executable file: Green1 */
    784 #define C_FIL (C_EXE + 1)   /* Regular file: Normal */
    785 #define C_HRD (C_FIL + 1)   /* Hard link: Plum4 */
    786 #define C_LNK (C_HRD + 1)   /* Symbolic link: Cyan1 */
    787 #define C_MIS (C_LNK + 1)   /* Missing file OR file details: Grey62 */
    788 #define C_ORP (C_MIS + 1)   /* Orphaned symlink: DeepPink1 */
    789 #define C_PIP (C_ORP + 1)   /* Named pipe (FIFO): Orange1 */
    790 #define C_SOC (C_PIP + 1)   /* Socket: MediumOrchid1 */
    791 #define C_UND (C_SOC + 1)   /* Unknown OR 0B regular/exe file: Red1 */
    792 
    793 #ifdef ICONS_ENABLED
    794 /* 0-9, A-Z, OTHER = 36. */
    795 static ushort_t icon_positions[37];
    796 #endif
    797 
    798 static char gcolors[] = "c1e2272e006033f7c6d6abc4";
    799 static uint_t fcolors[C_UND + 1] = {0};
    800 
    801 /* Event handling */
    802 #ifdef LINUX_INOTIFY
    803 #define NUM_EVENT_SLOTS 32 /* Make room for 32 events */
    804 #define EVENT_SIZE (sizeof(struct inotify_event))
    805 #define EVENT_BUF_LEN (EVENT_SIZE * NUM_EVENT_SLOTS)
    806 static int inotify_fd, inotify_wd = -1;
    807 static uint_t INOTIFY_MASK = /* IN_ATTRIB | */ IN_CREATE | IN_DELETE | IN_DELETE_SELF
    808 			   | IN_MODIFY | IN_MOVE_SELF | IN_MOVED_FROM | IN_MOVED_TO;
    809 #elif defined(BSD_KQUEUE)
    810 #define NUM_EVENT_SLOTS 1
    811 #define NUM_EVENT_FDS 1
    812 static int kq, event_fd = -1;
    813 static struct kevent events_to_monitor[NUM_EVENT_FDS];
    814 static uint_t KQUEUE_FFLAGS = NOTE_DELETE | NOTE_EXTEND | NOTE_LINK
    815 			    | NOTE_RENAME | NOTE_REVOKE | NOTE_WRITE;
    816 static struct timespec gtimeout;
    817 #elif defined(HAIKU_NM)
    818 static bool haiku_nm_active = FALSE;
    819 static haiku_nm_h haiku_hnd;
    820 #endif
    821 
    822 /* Function macros */
    823 #define tolastln() move(xlines - 1, 0)
    824 #define tocursor() move(cur + 2 - curscroll, 0)
    825 #define exitcurses() endwin()
    826 #define printwarn(presel) printwait(strerror(errno), presel)
    827 #define istopdir(path) ((path)[1] == '\0' && (path)[0] == '/')
    828 #define copycurname() xstrsncpy(lastname, ndents ? pdents[cur].name : "\0", NAME_MAX + 1)
    829 #define settimeout() timeout(1000)
    830 #define cleartimeout() timeout(-1)
    831 #define errexit() printerr(__LINE__)
    832 #define setdirwatch() (cfg.filtermode ? (presel = FILTER) : (watch = TRUE))
    833 #define filterset() (g_ctx[cfg.curctx].c_fltr[1])
    834 /* We don't care about the return value from strcmp() */
    835 #define xstrcmp(a, b)  (*(a) != *(b) ? -1 : strcmp((a), (b)))
    836 /* A faster version of xisdigit */
    837 #define xisdigit(c) ((unsigned int) (c) - '0' <= 9)
    838 #define xerror() perror(xitoa(__LINE__))
    839 
    840 #ifdef TOURBIN_QSORT
    841 #define ENTLESS(i, j) (entrycmpfn(pdents + (i), pdents + (j)) < 0)
    842 #define ENTSWAP(i, j) (swap_ent((i), (j)))
    843 #define ENTSORT(pdents, ndents, entrycmpfn) QSORT((ndents), ENTLESS, ENTSWAP)
    844 #else
    845 #define ENTSORT(pdents, ndents, entrycmpfn) qsort((pdents), (ndents), sizeof(*(pdents)), (entrycmpfn))
    846 #endif
    847 
    848 /* Forward declarations */
    849 static void redraw(char *path);
    850 static int spawn(char *file, char *arg1, char *arg2, char *arg3, ushort_t flag);
    851 static void move_cursor(int target, int ignore_scrolloff);
    852 static char *load_input(int fd, const char *path);
    853 static int set_sort_flags(int r);
    854 static void statusbar(char *path);
    855 static bool get_output(char *file, char *arg1, char *arg2, int fdout, bool multi, bool page);
    856 #ifndef NOFIFO
    857 static void notify_fifo(bool force);
    858 #endif
    859 
    860 /* Functions */
    861 
    862 static void sigint_handler(int sig)
    863 {
    864 	(void) sig;
    865 	g_state.interrupt = 1;
    866 }
    867 
    868 static void clean_exit_sighandler(int sig)
    869 {
    870 	(void) sig;
    871 	exitcurses();
    872 	/* This triggers cleanup() thanks to atexit() */
    873 	exit(EXIT_SUCCESS);
    874 }
    875 
    876 static char *xitoa(uint_t val)
    877 {
    878 	static char dst[32] = {'\0'};
    879 	static const char digits[201] =
    880 		"0001020304050607080910111213141516171819"
    881 		"2021222324252627282930313233343536373839"
    882 		"4041424344454647484950515253545556575859"
    883 		"6061626364656667686970717273747576777879"
    884 		"8081828384858687888990919293949596979899";
    885 	uint_t next = 30, quo, i;
    886 
    887 	while (val >= 100) {
    888 		quo = val / 100;
    889 		i = (val - (quo * 100)) * 2;
    890 		val = quo;
    891 		dst[next] = digits[i + 1];
    892 		dst[--next] = digits[i];
    893 		--next;
    894 	}
    895 
    896 	/* Handle last 1-2 digits */
    897 	if (val < 10)
    898 		dst[next] = '0' + val;
    899 	else {
    900 		i = val * 2;
    901 		dst[next] = digits[i + 1];
    902 		dst[--next] = digits[i];
    903 	}
    904 
    905 	return &dst[next];
    906 }
    907 
    908 /* Return the integer value of a char representing HEX */
    909 static uchar_t xchartohex(uchar_t c)
    910 {
    911 	if (xisdigit(c))
    912 		return c - '0';
    913 
    914 	if (c >= 'a' && c <= 'f')
    915 		return c - 'a' + 10;
    916 
    917 	if (c >= 'A' && c <= 'F')
    918 		return c - 'A' + 10;
    919 
    920 	return c;
    921 }
    922 
    923 /*
    924  * Source: https://elixir.bootlin.com/linux/latest/source/arch/alpha/include/asm/bitops.h
    925  */
    926 static bool test_set_bit(uint_t nr)
    927 {
    928 	nr &= HASH_BITS;
    929 
    930 	pthread_mutex_lock(&hardlink_mutex);
    931 	ullong_t *m = ((ullong_t *)ihashbmp) + (nr >> 6);
    932 
    933 	if (*m & (1 << (nr & 63))) {
    934 		pthread_mutex_unlock(&hardlink_mutex);
    935 		return FALSE;
    936 	}
    937 
    938 	*m |= 1 << (nr & 63);
    939 	pthread_mutex_unlock(&hardlink_mutex);
    940 
    941 	return TRUE;
    942 }
    943 
    944 #ifndef __APPLE__
    945 /* Increase the limit on open file descriptors, if possible */
    946 static void max_openfds(void)
    947 {
    948 	struct rlimit rl;
    949 
    950 	if (!getrlimit(RLIMIT_NOFILE, &rl))
    951 		if (rl.rlim_cur < rl.rlim_max) {
    952 			rl.rlim_cur = rl.rlim_max;
    953 			setrlimit(RLIMIT_NOFILE, &rl);
    954 		}
    955 }
    956 #endif
    957 
    958 /*
    959  * Wrapper to realloc()
    960  * Frees current memory if realloc() fails and returns NULL.
    961  *
    962  * The *alloc() family returns aligned address: https://man7.org/linux/man-pages/man3/malloc.3.html
    963  */
    964 static void *xrealloc(void *pcur, size_t len)
    965 {
    966 	void *pmem = realloc(pcur, len);
    967 
    968 	if (!pmem)
    969 		free(pcur);
    970 
    971 	return pmem;
    972 }
    973 
    974 /*
    975  * Just a safe strncpy(3)
    976  * Always null ('\0') terminates if both src and dest are valid pointers.
    977  * Returns the number of bytes copied including terminating null byte.
    978  */
    979 static size_t xstrsncpy(char *restrict dst, const char *restrict src, size_t n)
    980 {
    981 	char *end = memccpy(dst, src, '\0', n);
    982 
    983 	if (!end) {
    984 		dst[n - 1] = '\0'; // NOLINT
    985 		end = dst + n; /* If we return n here, binary size increases due to auto-inlining */
    986 	}
    987 
    988 	return end - dst;
    989 }
    990 
    991 static inline size_t xstrlen(const char *restrict s)
    992 {
    993 #if !defined(__GLIBC__)
    994 	return strlen(s); // NOLINT
    995 #else
    996 	return (char *)rawmemchr(s, '\0') - s; // NOLINT
    997 #endif
    998 }
    999 
   1000 static char *xstrdup(const char *restrict s)
   1001 {
   1002 	size_t len = xstrlen(s) + 1;
   1003 	char *ptr = malloc(len);
   1004 
   1005 	if (ptr)
   1006 		xstrsncpy(ptr, s, len);
   1007 	return ptr;
   1008 }
   1009 
   1010 static bool is_suffix(const char *restrict str, const char *restrict suffix)
   1011 {
   1012 	if (!str || !suffix)
   1013 		return FALSE;
   1014 
   1015 	size_t lenstr = xstrlen(str);
   1016 	size_t lensuffix = xstrlen(suffix);
   1017 
   1018 	if (lensuffix > lenstr)
   1019 		return FALSE;
   1020 
   1021 	return (xstrcmp(str + (lenstr - lensuffix), suffix) == 0);
   1022 }
   1023 
   1024 static inline bool is_prefix(const char *restrict str, const char *restrict prefix, size_t len)
   1025 {
   1026 	return !strncmp(str, prefix, len);
   1027 }
   1028 
   1029 /*
   1030  * The poor man's implementation of memrchr(3).
   1031  * We are only looking for '/' in this program.
   1032  * And we are NOT expecting a '/' at the end.
   1033  * Ideally 0 < n <= xstrlen(s).
   1034  */
   1035 static void *xmemrchr(uchar_t *restrict s, uchar_t ch, size_t n)
   1036 {
   1037 #if defined(__GLIBC__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__)
   1038 	return memrchr(s, ch, n);
   1039 #else
   1040 
   1041 	if (!s || !n)
   1042 		return NULL;
   1043 
   1044 	uchar_t *ptr = s + n;
   1045 
   1046 	do {
   1047 		if (*--ptr == ch)
   1048 			return ptr;
   1049 	} while (s != ptr);
   1050 
   1051 	return NULL;
   1052 #endif
   1053 }
   1054 
   1055 /* A very simplified implementation, changes path */
   1056 static char *xdirname(char *path)
   1057 {
   1058 	char *base = xmemrchr((uchar_t *)path, '/', xstrlen(path));
   1059 
   1060 	if (base == path)
   1061 		path[1] = '\0';
   1062 	else
   1063 		*base = '\0';
   1064 
   1065 	return path;
   1066 }
   1067 
   1068 static char *xbasename(char *path)
   1069 {
   1070 	char *base = xmemrchr((uchar_t *)path, '/', xstrlen(path)); // NOLINT
   1071 
   1072 	return base ? base + 1 : path;
   1073 }
   1074 
   1075 static inline char *xextension(const char *fname, size_t len)
   1076 {
   1077 	return xmemrchr((uchar_t *)fname, '.', len);
   1078 }
   1079 
   1080 #ifndef NOUG
   1081 /*
   1082  * One-shot cache for getpwuid/getgrgid. Returns the cached name if the
   1083  * provided uid is the same as the previous uid. Returns xitoa(guid) if
   1084  * the guid is not found in the password database.
   1085  */
   1086 static char *getpwname(uid_t uid)
   1087 {
   1088 	static uint_t uidcache = UINT_MAX;
   1089 	static char *namecache;
   1090 
   1091 	if (uidcache != uid) {
   1092 		struct passwd *pw = getpwuid(uid);
   1093 
   1094 		uidcache = uid;
   1095 		namecache = pw ? pw->pw_name : NULL;
   1096 	}
   1097 
   1098 	return namecache ? namecache : xitoa(uid);
   1099 }
   1100 
   1101 static char *getgrname(gid_t gid)
   1102 {
   1103 	static uint_t gidcache = UINT_MAX;
   1104 	static char *grpcache;
   1105 
   1106 	if (gidcache != gid) {
   1107 		struct group *gr = getgrgid(gid);
   1108 
   1109 		gidcache = gid;
   1110 		grpcache = gr ? gr->gr_name : NULL;
   1111 	}
   1112 
   1113 	return grpcache ? grpcache : xitoa(gid);
   1114 }
   1115 #endif
   1116 
   1117 static inline bool getutil(char *util)
   1118 {
   1119 	return spawn("which", util, NULL, NULL, F_NORMAL | F_NOTRACE) == 0;
   1120 }
   1121 
   1122 /*
   1123  * Updates out with "dir/name or "/name"
   1124  * Returns the number of bytes copied including the terminating NULL byte
   1125  *
   1126  * Note: dir and out must be PATH_MAX in length to avoid macOS fault
   1127  */
   1128 static size_t mkpath(const char *dir, const char *name, char *out)
   1129 {
   1130 	size_t len = 0;
   1131 
   1132 	if (name[0] != '/') { // NOLINT
   1133 		/* Handle root case */
   1134 		if (istopdir(dir))
   1135 			len = 1;
   1136 		else
   1137 			len = xstrsncpy(out, dir, PATH_MAX);
   1138 
   1139 		out[len - 1] = '/'; // NOLINT
   1140 	}
   1141 	return (xstrsncpy(out + len, name, PATH_MAX - len) + len);
   1142 }
   1143 
   1144 /* Assumes both the paths passed are directories */
   1145 static char *common_prefix(const char *path, char *prefix)
   1146 {
   1147 	const char *x = path, *y = prefix;
   1148 	char *sep;
   1149 
   1150 	if (!path || !*path || !prefix)
   1151 		return NULL;
   1152 
   1153 	if (!*prefix) {
   1154 		xstrsncpy(prefix, path, PATH_MAX);
   1155 		return prefix;
   1156 	}
   1157 
   1158 	while (*x && *y && (*x == *y))
   1159 		++x, ++y;
   1160 
   1161 	/* Strings are same */
   1162 	if (!*x && !*y)
   1163 		return prefix;
   1164 
   1165 	/* Path is shorter */
   1166 	if (!*x && *y == '/') {
   1167 		xstrsncpy(prefix, path, y - path);
   1168 		return prefix;
   1169 	}
   1170 
   1171 	/* Prefix is shorter */
   1172 	if (!*y && *x == '/')
   1173 		return prefix;
   1174 
   1175 	/* Shorten prefix */
   1176 	prefix[y - prefix] = '\0';
   1177 
   1178 	sep = xmemrchr((uchar_t *)prefix, '/', y - prefix);
   1179 	if (sep != prefix)
   1180 		*sep = '\0';
   1181 	else /* Just '/' */
   1182 		prefix[1] = '\0';
   1183 
   1184 	return prefix;
   1185 }
   1186 
   1187 /*
   1188  * The library function realpath() resolves symlinks.
   1189  * If there's a symlink in file list we want to show the symlink not what it's points to.
   1190  * Resolves ./../~ in path
   1191  */
   1192 static char *abspath(const char *path, const char *cwd, char *buf)
   1193 {
   1194 	if (!path)
   1195 		return NULL;
   1196 
   1197 	if (path[0] == '~')
   1198 		cwd = home;
   1199 	else if ((path[0] != '/') && !cwd)
   1200 		cwd = getcwd(NULL, 0);
   1201 
   1202 	size_t dst_size = 0, src_size = xstrlen(path), cwd_size = cwd ? xstrlen(cwd) : 0;
   1203 	size_t len = src_size;
   1204 	const char *src;
   1205 	char *dst;
   1206 	/*
   1207 	 * We need to add 2 chars at the end as relative paths may start with:
   1208 	 * ./ (find .)
   1209 	 * no separator (fd .): this needs an additional char for '/'
   1210 	 */
   1211 	char *resolved_path = buf ? buf : malloc(src_size + cwd_size + 2);
   1212 
   1213 	if (!resolved_path)
   1214 		return NULL;
   1215 
   1216 	/* Turn relative paths into absolute */
   1217 	if (path[0] != '/') {
   1218 		if (!cwd) {
   1219 			if (!buf)
   1220 				free(resolved_path);
   1221 			return NULL;
   1222 		}
   1223 		dst_size = xstrsncpy(resolved_path, cwd, cwd_size + 1) - 1;
   1224 	} else
   1225 		resolved_path[0] = '\0';
   1226 
   1227 	src = path;
   1228 	dst = resolved_path + dst_size;
   1229 	for (const char *next = NULL; next != path + src_size;) {
   1230 		next = memchr(src, '/', len);
   1231 		if (!next)
   1232 			next = path + src_size;
   1233 
   1234 		if (next - src == 2 && src[0] == '.' && src[1] == '.') {
   1235 			if (dst - resolved_path) {
   1236 				dst = xmemrchr((uchar_t *)resolved_path, '/', dst - resolved_path);
   1237 				*dst = '\0';
   1238 			}
   1239 		} else if (next - src == 1 && src[0] == '.') {
   1240 			/* NOP */
   1241 		} else if (next - src) {
   1242 			*(dst++) = '/';
   1243 			xstrsncpy(dst, src, next - src + 1);
   1244 			dst += next - src;
   1245 		}
   1246 
   1247 		src = next + 1;
   1248 		len = src_size - (src - path);
   1249 	}
   1250 
   1251 	if (*resolved_path == '\0') {
   1252 		resolved_path[0] = '/';
   1253 		resolved_path[1] = '\0';
   1254 	}
   1255 
   1256 	return resolved_path;
   1257 }
   1258 
   1259 static bool set_tilde_in_path(char *path)
   1260 {
   1261 	if (is_prefix(path, home, homelen)) {
   1262 		home[homelen] = path[homelen - 1];
   1263 		path[homelen - 1] = '~';
   1264 		return TRUE;
   1265 	}
   1266 
   1267 	return FALSE;
   1268 }
   1269 
   1270 static void reset_tilde_in_path(char *path)
   1271 {
   1272 	path[homelen - 1] = home[homelen];
   1273 	home[homelen] = '\0';
   1274 }
   1275 
   1276 #ifndef NOX11
   1277 static void xterm_cfg(char *path)
   1278 {
   1279 	if (cfg.x11 && !g_state.picker) {
   1280 		/* Signal CWD change to terminal */
   1281 		printf("\033]7;file://%s%s\033\\", hostname, path);
   1282 
   1283 		/* Set terminal window title */
   1284 		bool r = set_tilde_in_path(path);
   1285 
   1286 		printf("\033]2;%s\007", r ? &path[homelen - 1] : path);
   1287 		fflush(stdout);
   1288 
   1289 		if (r)
   1290 			reset_tilde_in_path(path);
   1291 	}
   1292 }
   1293 #endif
   1294 
   1295 static void convert_tilde(const char *path, char *buf)
   1296 {
   1297 	if (path[0] == '~') {
   1298 		ssize_t len = xstrlen(home);
   1299 		ssize_t loclen = xstrlen(path);
   1300 
   1301 		xstrsncpy(buf, home, len + 1);
   1302 		xstrsncpy(buf + len, path + 1, loclen);
   1303 	}
   1304 }
   1305 
   1306 static int create_tmp_file(void)
   1307 {
   1308 	xstrsncpy(g_tmpfpath + tmpfplen - 1, messages[STR_TMPFILE], TMP_LEN_MAX - tmpfplen);
   1309 
   1310 	int fd = mkstemp(g_tmpfpath);
   1311 
   1312 	if (fd == -1) {
   1313 		DPRINTF_S(strerror(errno));
   1314 	}
   1315 
   1316 	return fd;
   1317 }
   1318 
   1319 static void msg(const char *message)
   1320 {
   1321 	dprintf(STDERR_FILENO, "%s\n", message);
   1322 }
   1323 
   1324 #ifdef KEY_RESIZE
   1325 static void handle_key_resize(void)
   1326 {
   1327 	endwin();
   1328 	refresh();
   1329 }
   1330 
   1331 /* Clear the old prompt */
   1332 static void clearoldprompt(void)
   1333 {
   1334 	// clear info line
   1335 	move(xlines - 2, 0);
   1336 	clrtoeol();
   1337 
   1338 	tolastln();
   1339 	clrtoeol();
   1340 	handle_key_resize();
   1341 }
   1342 #endif
   1343 
   1344 /* Messages show up at the bottom */
   1345 static inline void printmsg_nc(const char *msg)
   1346 {
   1347 	tolastln();
   1348 	addstr(msg);
   1349 	clrtoeol();
   1350 }
   1351 
   1352 static void printmsg(const char *msg)
   1353 {
   1354 	attron(COLOR_PAIR(cfg.curctx + 1));
   1355 	printmsg_nc(msg);
   1356 	attroff(COLOR_PAIR(cfg.curctx + 1));
   1357 }
   1358 
   1359 static void printwait(const char *msg, int *presel)
   1360 {
   1361 	printmsg(msg);
   1362 	if (presel) {
   1363 		*presel = MSGWAIT;
   1364 		if (ndents)
   1365 			xstrsncpy(g_ctx[cfg.curctx].c_name, pdents[cur].name, NAME_MAX + 1);
   1366 	}
   1367 }
   1368 
   1369 /* Kill curses and display error before exiting */
   1370 static void printerr(int linenum)
   1371 {
   1372 	exitcurses();
   1373 	perror(xitoa(linenum));
   1374 	if (!g_state.picker && selpath)
   1375 		unlink(selpath);
   1376 	free(pselbuf);
   1377 	exit(1);
   1378 }
   1379 
   1380 static inline bool xconfirm(int c)
   1381 {
   1382 	return (c == 'y' || c == 'Y');
   1383 }
   1384 
   1385 static int get_input(const char *prompt)
   1386 {
   1387 	if (prompt)
   1388 		printmsg(prompt);
   1389 	cleartimeout();
   1390 
   1391 	int r = getch();
   1392 
   1393 #ifdef KEY_RESIZE
   1394 	while (r == KEY_RESIZE) {
   1395 		if (prompt) {
   1396 			clearoldprompt();
   1397 			xlines = LINES;
   1398 			printmsg(prompt);
   1399 		}
   1400 
   1401 		r = getch();
   1402 	}
   1403 #endif
   1404 	settimeout();
   1405 	return r;
   1406 }
   1407 
   1408 static bool isselfileempty(void)
   1409 {
   1410 	struct stat sb;
   1411 
   1412 	return (stat(selpath, &sb) == -1) || (!sb.st_size);
   1413 }
   1414 
   1415 static int get_cur_or_sel(void)
   1416 {
   1417 	bool sel = (selbufpos || !isselfileempty());
   1418 
   1419 	/* Check both local buffer and selection file for external selection */
   1420 	if (sel && ndents) {
   1421 		/* If selection is preferred and we have a local selection, return selection.
   1422 		 * Always show the prompt in case of an external selection.
   1423 		 */
   1424 		if (cfg.prefersel && selbufpos)
   1425 			return 's';
   1426 
   1427 		int choice = get_input(messages[MSG_CUR_SEL_OPTS]);
   1428 
   1429 		return ((choice == 'c' || choice == 's') ? choice : 0);
   1430 	}
   1431 
   1432 	if (sel)
   1433 		return 's';
   1434 
   1435 	if (ndents)
   1436 		return 'c';
   1437 
   1438 	return 0;
   1439 }
   1440 
   1441 static void xdelay(useconds_t delay)
   1442 {
   1443 	refresh();
   1444 	usleep(delay);
   1445 }
   1446 
   1447 static char confirm_force(bool selection)
   1448 {
   1449 	char str[64];
   1450 
   1451 	snprintf(str, 64, messages[MSG_FORCE_RM],
   1452 		 g_state.trash ? utils[UTIL_GIO_TRASH] + 4 : utils[UTIL_RM_RF],
   1453 		 (selection ? "selection" : "hovered"));
   1454 
   1455 	int r = get_input(str);
   1456 
   1457 	if (r == ESC)
   1458 		return '\0'; /* cancel */
   1459 	if (r == 'y' || r == 'Y')
   1460 		return 'f'; /* forceful for rm */
   1461 	return (g_state.trash ? '\0' : 'i'); /* interactive for rm */
   1462 }
   1463 
   1464 /* Writes buflen char(s) from buf to a file */
   1465 static void writesel(const char *buf, const size_t buflen)
   1466 {
   1467 	if (!selpath)
   1468 		return;
   1469 
   1470 	int fd = open(selpath, O_CREAT | O_WRONLY | O_TRUNC, 0666);
   1471 
   1472 	if (fd != -1) {
   1473 		if (write(fd, buf, buflen) != (ssize_t)buflen)
   1474 			printwarn(NULL);
   1475 		close(fd);
   1476 	} else
   1477 		printwarn(NULL);
   1478 }
   1479 
   1480 static void appendfpath(const char *path, const size_t len)
   1481 {
   1482 	if ((selbufpos >= selbuflen) || ((len + 3) > (selbuflen - selbufpos))) {
   1483 		selbuflen += PATH_MAX;
   1484 		pselbuf = xrealloc(pselbuf, selbuflen);
   1485 		if (!pselbuf)
   1486 			errexit();
   1487 	}
   1488 
   1489 	selbufpos += xstrsncpy(pselbuf + selbufpos, path, len);
   1490 }
   1491 
   1492 static void selbufrealloc(const size_t alloclen)
   1493 {
   1494 	if ((selbufpos + alloclen) > selbuflen) {
   1495 		selbuflen = ALIGN_UP(selbufpos + alloclen, PATH_MAX);
   1496 		pselbuf = xrealloc(pselbuf, selbuflen);
   1497 		if (!pselbuf)
   1498 			errexit();
   1499 	}
   1500 }
   1501 
   1502 /* Write selected file paths to fd, linefeed separated */
   1503 static size_t seltofile(int fd, uint_t *pcount)
   1504 {
   1505 	uint_t lastpos, count = 0;
   1506 	char *pbuf = pselbuf;
   1507 	size_t pos = 0;
   1508 	ssize_t len, prefixlen = 0, initlen = 0;
   1509 
   1510 	if (pcount)
   1511 		*pcount = 0;
   1512 
   1513 	if (!selbufpos)
   1514 		return 0;
   1515 
   1516 	lastpos = selbufpos - 1;
   1517 
   1518 	if (listpath) {
   1519 		prefixlen = (ssize_t)xstrlen(listroot);
   1520 		initlen = (ssize_t)xstrlen(listpath);
   1521 	}
   1522 
   1523 	while (pos <= lastpos) {
   1524 		DPRINTF_S(pbuf);
   1525 		len = (ssize_t)xstrlen(pbuf);
   1526 
   1527 		if (!listpath || !is_prefix(pbuf, listpath, initlen)) {
   1528 			if (write(fd, pbuf, len) != len)
   1529 				return pos;
   1530 		} else {
   1531 			if (write(fd, listroot, prefixlen) != prefixlen)
   1532 				return pos;
   1533 			if (write(fd, pbuf + initlen, len - initlen) != (len - initlen))
   1534 				return pos;
   1535 		}
   1536 
   1537 		pos += len;
   1538 		if (pos <= lastpos) {
   1539 			if (write(fd, "\n", 1) != 1)
   1540 				return pos;
   1541 			pbuf += len + 1;
   1542 		}
   1543 		++pos;
   1544 		++count;
   1545 	}
   1546 
   1547 	if (pcount)
   1548 		*pcount = count;
   1549 
   1550 	return pos;
   1551 }
   1552 
   1553 /* List selection from selection file (another instance) */
   1554 static bool listselfile(void)
   1555 {
   1556 	if (isselfileempty())
   1557 		return FALSE;
   1558 
   1559 	snprintf(g_buf, CMD_LEN_MAX, "tr \'\\0\' \'\\n\' < %s", selpath);
   1560 	spawn(utils[UTIL_SH_EXEC], g_buf, NULL, NULL, F_CLI | F_CONFIRM);
   1561 
   1562 	return TRUE;
   1563 }
   1564 
   1565 /* Reset selection indicators */
   1566 static void resetselind(void)
   1567 {
   1568 	for (int r = 0; r < ndents; ++r)
   1569 		if (pdents[r].flags & FILE_SELECTED)
   1570 			pdents[r].flags &= ~FILE_SELECTED;
   1571 }
   1572 
   1573 static void startselection(void)
   1574 {
   1575 	if (!g_state.selmode) {
   1576 		g_state.selmode = 1;
   1577 		nselected = 0;
   1578 
   1579 		if (selbufpos) {
   1580 			resetselind();
   1581 			writesel(NULL, 0);
   1582 			selbufpos = 0;
   1583 		}
   1584 	}
   1585 }
   1586 
   1587 static void clearselection(void)
   1588 {
   1589 	nselected = 0;
   1590 	selbufpos = 0;
   1591 	g_state.selmode = 0;
   1592 	writesel(NULL, 0);
   1593 }
   1594 
   1595 static char *findinsel(char *startpos, int len)
   1596 {
   1597 	if (!selbufpos)
   1598 		return FALSE;
   1599 
   1600 	if (!startpos)
   1601 		startpos = pselbuf;
   1602 
   1603 	char *found = startpos;
   1604 	size_t buflen = selbufpos - (startpos - pselbuf);
   1605 
   1606 	while (1) {
   1607 		/* memmem(3): not specified in POSIX.1, but present on a number of other systems. */
   1608 		found = memmem(found, buflen - (found - startpos), g_sel, len);
   1609 		if (!found)
   1610 			return NULL;
   1611 		if (found == startpos || *(found - 1) == '\0')
   1612 			return found;
   1613 		found += len; /* We found g_sel as a substring of a path, move forward */
   1614 		if (found >= startpos + buflen)
   1615 			return NULL;
   1616 	}
   1617 }
   1618 
   1619 static int markcmp(const void *va, const void *vb)
   1620 {
   1621 	const selmark *ma = (selmark*)va;
   1622 	const selmark *mb = (selmark*)vb;
   1623 
   1624 	return ma->startpos - mb->startpos;
   1625 }
   1626 
   1627 /* scanselforpath() must be called before calling this */
   1628 static inline void findmarkentry(size_t len, struct entry *dentp)
   1629 {
   1630 	if (!(dentp->flags & FILE_SCANNED)) {
   1631 		if (findinsel(findselpos, len + xstrsncpy(g_sel + len, dentp->name, dentp->nlen)))
   1632 			dentp->flags |= FILE_SELECTED;
   1633 		dentp->flags |= FILE_SCANNED;
   1634 	}
   1635 }
   1636 
   1637 /*
   1638  * scanselforpath() must be called before calling this
   1639  * pathlen = length of path + 1 (+1 for trailing slash)
   1640  */
   1641 static void invertselbuf(const int pathlen)
   1642 {
   1643 	size_t len, endpos, shrinklen = 0, alloclen = 0;
   1644 	char * const pbuf = g_sel + pathlen;
   1645 	char *found;
   1646 	int i, nmarked = 0, prev = 0;
   1647 	struct entry *dentp;
   1648 	selmark *marked = malloc(nselected * sizeof(selmark));
   1649 	bool scan = FALSE;
   1650 
   1651 	/* First pass: inversion */
   1652 	for (i = 0; i < ndents; ++i) {
   1653 		dentp = &pdents[i];
   1654 
   1655 		if (dentp->flags & FILE_SCANNED) {
   1656 			if (dentp->flags & FILE_SELECTED) {
   1657 				dentp->flags ^= FILE_SELECTED; /* Clear selection status */
   1658 				scan = TRUE;
   1659 			} else {
   1660 				dentp->flags |= FILE_SELECTED;
   1661 				alloclen += pathlen + dentp->nlen;
   1662 			}
   1663 		} else {
   1664 			dentp->flags |= FILE_SCANNED;
   1665 			scan = TRUE;
   1666 		}
   1667 
   1668 		if (scan) {
   1669 			len = pathlen + xstrsncpy(pbuf, dentp->name, NAME_MAX);
   1670 			found = findinsel(findselpos, len);
   1671 			if (found) {
   1672 				if (findselpos == found)
   1673 					findselpos += len;
   1674 
   1675 				if (nmarked && (found
   1676 				    == (marked[nmarked - 1].startpos + marked[nmarked - 1].len)))
   1677 					marked[nmarked - 1].len += len;
   1678 				else {
   1679 					marked[nmarked].startpos = found;
   1680 					marked[nmarked].len = len;
   1681 					++nmarked;
   1682 				}
   1683 
   1684 				--nselected;
   1685 				shrinklen += len; /* buffer size adjustment */
   1686 			} else {
   1687 				dentp->flags |= FILE_SELECTED;
   1688 				alloclen += pathlen + dentp->nlen;
   1689 			}
   1690 			scan = FALSE;
   1691 		}
   1692 	}
   1693 
   1694 	/*
   1695 	 * Files marked for deselection could be found in arbitrary order.
   1696 	 * Sort by appearance in selection buffer.
   1697 	 * With entries sorted we can merge adjacent ones allowing us to
   1698 	 * move them in a single go.
   1699 	 */
   1700 	qsort(marked, nmarked, sizeof(selmark), &markcmp);
   1701 
   1702 	/* Some files might be adjacent. Merge them into a single entry */
   1703 	for (i = 1; i < nmarked; ++i) {
   1704 		if (marked[i].startpos == marked[prev].startpos + marked[prev].len)
   1705 			marked[prev].len += marked[i].len;
   1706 		else {
   1707 			++prev;
   1708 			marked[prev].startpos = marked[i].startpos;
   1709 			marked[prev].len = marked[i].len;
   1710 		}
   1711 	}
   1712 
   1713 	/*
   1714 	 * Number of entries is increased by encountering a non-adjacent entry
   1715 	 * After we finish the loop we should increment it once more.
   1716 	 */
   1717 
   1718 	if (nmarked) /* Make sure there is something to deselect */
   1719 		nmarked = prev + 1;
   1720 
   1721 	/* Using merged entries remove unselected chunks from selection buffer */
   1722 	for (i = 0; i < nmarked; ++i) {
   1723 		/*
   1724 		 * found: points to where the current block starts
   1725 		 *        variable is recycled from previous for readability
   1726 		 * endpos: points to where the the next block starts
   1727 		 *         area between the end of current block (found + len)
   1728 		 *         and endpos is selected entries. This is what we are
   1729 		 *         moving back.
   1730 		 */
   1731 		found = marked[i].startpos;
   1732 		endpos = (i + 1 == nmarked ? selbufpos : marked[i + 1].startpos - pselbuf);
   1733 		len = marked[i].len;
   1734 
   1735 		/* Move back only selected entries. No selected memory is moved twice */
   1736 		memmove(found, found + len, endpos - (found + len - pselbuf));
   1737 	}
   1738 
   1739 	free(marked);
   1740 
   1741 	/* Buffer size adjustment */
   1742 	selbufpos -= shrinklen;
   1743 
   1744 	selbufrealloc(alloclen);
   1745 
   1746 	/* Second pass: append newly selected to buffer */
   1747 	for (i = 0; i < ndents; ++i) {
   1748 		if (pdents[i].flags & FILE_SELECTED) {
   1749 			len = pathlen + xstrsncpy(pbuf, pdents[i].name, NAME_MAX);
   1750 			appendfpath(g_sel, len);
   1751 			++nselected;
   1752 		}
   1753 	}
   1754 
   1755 	nselected ? writesel(pselbuf, selbufpos - 1) : clearselection();
   1756 }
   1757 
   1758 /*
   1759  * scanselforpath() must be called before calling this
   1760  * pathlen = length of path + 1 (+1 for trailing slash)
   1761  */
   1762 static void addtoselbuf(const int pathlen, int startid, int endid)
   1763 {
   1764 	int i;
   1765 	size_t len, alloclen = 0;
   1766 	struct entry *dentp;
   1767 	char *found;
   1768 	char * const pbuf = g_sel + pathlen;
   1769 
   1770 	/* Remember current selection buffer position */
   1771 	for (i = startid; i <= endid; ++i) {
   1772 		dentp = &pdents[i];
   1773 
   1774 		if (findselpos) {
   1775 			len = pathlen + xstrsncpy(pbuf, dentp->name, NAME_MAX);
   1776 			found = findinsel(findselpos, len);
   1777 			if (found) {
   1778 				dentp->flags |= (FILE_SCANNED | FILE_SELECTED);
   1779 				if (found == findselpos) {
   1780 					findselpos += len;
   1781 					if (findselpos == (pselbuf + selbufpos))
   1782 						findselpos = NULL;
   1783 				}
   1784 			} else
   1785 				alloclen += pathlen + dentp->nlen;
   1786 		} else
   1787 			alloclen += pathlen + dentp->nlen;
   1788 	}
   1789 
   1790 	selbufrealloc(alloclen);
   1791 
   1792 	for (i = startid; i <= endid; ++i) {
   1793 		if (!(pdents[i].flags & FILE_SELECTED)) {
   1794 			len = pathlen + xstrsncpy(pbuf, pdents[i].name, NAME_MAX);
   1795 			appendfpath(g_sel, len);
   1796 			++nselected;
   1797 			pdents[i].flags |= (FILE_SCANNED | FILE_SELECTED);
   1798 		}
   1799 	}
   1800 
   1801 	writesel(pselbuf, selbufpos - 1); /* Truncate NULL from end */
   1802 }
   1803 
   1804 /* Removes g_sel from selbuf */
   1805 static void rmfromselbuf(size_t len)
   1806 {
   1807 	char *found = findinsel(findselpos, len);
   1808 	if (!found)
   1809 		return;
   1810 
   1811 	memmove(found, found + len, selbufpos - (found + len - pselbuf));
   1812 	selbufpos -= len;
   1813 
   1814 	nselected ? writesel(pselbuf, selbufpos - 1) : clearselection();
   1815 }
   1816 
   1817 static int scanselforpath(const char *path, bool getsize)
   1818 {
   1819 	if (!path[1]) { /* path should always be at least two bytes (including NULL) */
   1820 		g_sel[0] = '/';
   1821 		findselpos = pselbuf;
   1822 		return 1; /* Length of '/' is 1 */
   1823 	}
   1824 
   1825 	size_t off = xstrsncpy(g_sel, path, PATH_MAX);
   1826 
   1827 	g_sel[off - 1] = '/';
   1828 	/*
   1829 	 * We set findselpos only here. Directories can be listed in arbitrary order.
   1830 	 * This is the best best we can do for remembering position.
   1831 	 */
   1832 	findselpos = findinsel(NULL, off);
   1833 
   1834 	if (getsize)
   1835 		return off;
   1836 	return (findselpos ? off : 0);
   1837 }
   1838 
   1839 /* Finish selection procedure before an operation */
   1840 static void endselection(bool endselmode)
   1841 {
   1842 	int fd;
   1843 	ssize_t count;
   1844 	char buf[sizeof(patterns[P_REPLACE]) + PATH_MAX + (TMP_LEN_MAX << 1)];
   1845 
   1846 	if (endselmode && g_state.selmode)
   1847 		g_state.selmode = 0;
   1848 
   1849 	/* The code below is only for listing mode */
   1850 	if (!listpath || !selbufpos)
   1851 		return;
   1852 
   1853 	fd = create_tmp_file();
   1854 	if (fd == -1) {
   1855 		DPRINTF_S("couldn't create tmp file");
   1856 		return;
   1857 	}
   1858 
   1859 	seltofile(fd, NULL);
   1860 	if (close(fd)) {
   1861 		DPRINTF_S(strerror(errno));
   1862 		printwarn(NULL);
   1863 		return;
   1864 	}
   1865 
   1866 	snprintf(buf, sizeof(buf), patterns[P_REPLACE], listpath, listroot, g_tmpfpath);
   1867 	spawn(utils[UTIL_SH_EXEC], buf, NULL, NULL, F_CLI);
   1868 
   1869 	fd = open(g_tmpfpath, O_RDONLY);
   1870 	if (fd == -1) {
   1871 		DPRINTF_S(strerror(errno));
   1872 		printwarn(NULL);
   1873 		if (unlink(g_tmpfpath)) {
   1874 			DPRINTF_S(strerror(errno));
   1875 			printwarn(NULL);
   1876 		}
   1877 		return;
   1878 	}
   1879 
   1880 	count = read(fd, pselbuf, selbuflen);
   1881 	if (count < 0) {
   1882 		DPRINTF_S(strerror(errno));
   1883 		printwarn(NULL);
   1884 		if (close(fd) || unlink(g_tmpfpath)) {
   1885 			DPRINTF_S(strerror(errno));
   1886 		}
   1887 		return;
   1888 	}
   1889 
   1890 	if (close(fd) || unlink(g_tmpfpath)) {
   1891 		DPRINTF_S(strerror(errno));
   1892 		printwarn(NULL);
   1893 		return;
   1894 	}
   1895 
   1896 	selbufpos = count;
   1897 	pselbuf[--count] = '\0';
   1898 	for (--count; count > 0; --count)
   1899 		if (pselbuf[count] == '\n' && pselbuf[count+1] == '/')
   1900 			pselbuf[count] = '\0';
   1901 
   1902 	writesel(pselbuf, selbufpos - 1);
   1903 }
   1904 
   1905 /* Returns: 1 - success, 0 - none selected, -1 - other failure */
   1906 static int editselection(void)
   1907 {
   1908 	int ret = -1;
   1909 	int fd, lines = 0;
   1910 	ssize_t count;
   1911 	struct stat sb;
   1912 	time_t mtime;
   1913 
   1914 	if (!selbufpos) /* External selection is only editable at source */
   1915 		return listselfile();
   1916 
   1917 	fd = create_tmp_file();
   1918 	if (fd == -1) {
   1919 		DPRINTF_S("couldn't create tmp file");
   1920 		return -1;
   1921 	}
   1922 
   1923 	seltofile(fd, NULL);
   1924 	if (close(fd)) {
   1925 		DPRINTF_S(strerror(errno));
   1926 		return -1;
   1927 	}
   1928 
   1929 	/* Save the last modification time */
   1930 	if (stat(g_tmpfpath, &sb)) {
   1931 		DPRINTF_S(strerror(errno));
   1932 		unlink(g_tmpfpath);
   1933 		return -1;
   1934 	}
   1935 	mtime = sb.st_mtime;
   1936 
   1937 	spawn((cfg.waitedit ? enveditor : editor), g_tmpfpath, NULL, NULL, F_CLI);
   1938 
   1939 	fd = open(g_tmpfpath, O_RDONLY);
   1940 	if (fd == -1) {
   1941 		DPRINTF_S(strerror(errno));
   1942 		unlink(g_tmpfpath);
   1943 		return -1;
   1944 	}
   1945 
   1946 	fstat(fd, &sb);
   1947 
   1948 	if (mtime == sb.st_mtime) {
   1949 		DPRINTF_S("selection is not modified");
   1950 		unlink(g_tmpfpath);
   1951 		return 1;
   1952 	}
   1953 
   1954 	if (sb.st_size > selbufpos) {
   1955 		DPRINTF_S("edited buffer larger than previous");
   1956 		unlink(g_tmpfpath);
   1957 		goto emptyedit;
   1958 	}
   1959 
   1960 	count = read(fd, pselbuf, selbuflen);
   1961 	if (count < 0) {
   1962 		DPRINTF_S(strerror(errno));
   1963 		printwarn(NULL);
   1964 		if (close(fd) || unlink(g_tmpfpath)) {
   1965 			DPRINTF_S(strerror(errno));
   1966 			printwarn(NULL);
   1967 		}
   1968 		goto emptyedit;
   1969 	}
   1970 
   1971 	if (close(fd) || unlink(g_tmpfpath)) {
   1972 		DPRINTF_S(strerror(errno));
   1973 		printwarn(NULL);
   1974 		goto emptyedit;
   1975 	}
   1976 
   1977 	if (!count) {
   1978 		ret = 1;
   1979 		goto emptyedit;
   1980 	}
   1981 
   1982 	resetselind();
   1983 	selbufpos = count;
   1984 	/* The last character should be '\n' */
   1985 	pselbuf[--count] = '\0';
   1986 	for (--count; count > 0; --count) {
   1987 		/* Replace every '\n' that separates two paths */
   1988 		if (pselbuf[count] == '\n' && pselbuf[count + 1] == '/') {
   1989 			++lines;
   1990 			pselbuf[count] = '\0';
   1991 		}
   1992 	}
   1993 
   1994 	/* Add a line for the last file */
   1995 	++lines;
   1996 
   1997 	if (lines > nselected) {
   1998 		DPRINTF_S("files added to selection");
   1999 		goto emptyedit;
   2000 	}
   2001 
   2002 	nselected = lines;
   2003 	writesel(pselbuf, selbufpos - 1);
   2004 
   2005 	return 1;
   2006 
   2007 emptyedit:
   2008 	resetselind();
   2009 	clearselection();
   2010 	return ret;
   2011 }
   2012 
   2013 static bool selsafe(void)
   2014 {
   2015 	/* Fail if selection file path not generated */
   2016 	if (!selpath) {
   2017 		printmsg(messages[MSG_SEL_MISSING]);
   2018 		return FALSE;
   2019 	}
   2020 
   2021 	/* Fail if selection file path isn't accessible */
   2022 	if (access(selpath, R_OK | W_OK) == -1) {
   2023 		errno == ENOENT ? printmsg(messages[MSG_0_SELECTED]) : printwarn(NULL);
   2024 		return FALSE;
   2025 	}
   2026 
   2027 	return TRUE;
   2028 }
   2029 
   2030 static void export_file_list(void)
   2031 {
   2032 	if (!ndents)
   2033 		return;
   2034 
   2035 	struct entry *pdent = pdents;
   2036 	int fd = create_tmp_file();
   2037 
   2038 	if (fd == -1) {
   2039 		DPRINTF_S(strerror(errno));
   2040 		return;
   2041 	}
   2042 
   2043 	for (int r = 0; r < ndents; ++pdent, ++r) {
   2044 		if (write(fd, pdent->name, pdent->nlen - 1) != (pdent->nlen - 1))
   2045 			break;
   2046 
   2047 		if ((r != ndents - 1) && (write(fd, "\n", 1) != 1))
   2048 			break;
   2049 	}
   2050 
   2051 	if (close(fd)) {
   2052 		DPRINTF_S(strerror(errno));
   2053 	}
   2054 
   2055 	spawn(editor, g_tmpfpath, NULL, NULL, F_CLI);
   2056 
   2057 	if (xconfirm(get_input(messages[MSG_RM_TMP])))
   2058 		unlink(g_tmpfpath);
   2059 }
   2060 
   2061 static bool init_fcolors(void)
   2062 {
   2063 	char *f_colors = getenv(env_cfg[NNN_FCOLORS]);
   2064 
   2065 	if (!f_colors || !*f_colors)
   2066 		f_colors = gcolors;
   2067 
   2068 	for (uchar_t id = C_BLK; *f_colors && id <= C_UND; ++id) {
   2069 		fcolors[id] = xchartohex(*f_colors) << 4;
   2070 		if (*++f_colors) {
   2071 			fcolors[id] += xchartohex(*f_colors);
   2072 			if (fcolors[id])
   2073 				init_pair(id, fcolors[id], -1);
   2074 		} else
   2075 			return FALSE;
   2076 		++f_colors;
   2077 	}
   2078 
   2079 	return TRUE;
   2080 }
   2081 
   2082 /* Initialize curses mode */
   2083 static bool initcurses(void *oldmask)
   2084 {
   2085 #ifdef NOMOUSE
   2086 	(void) oldmask;
   2087 #endif
   2088 
   2089 	if (g_state.picker) {
   2090 		if (!newterm(NULL, stderr, stdin)) {
   2091 			msg("newterm!");
   2092 			return FALSE;
   2093 		}
   2094 	} else if (!initscr()) {
   2095 		msg("initscr!");
   2096 		DPRINTF_S(getenv("TERM"));
   2097 		return FALSE;
   2098 	}
   2099 
   2100 	cbreak();
   2101 	noecho();
   2102 	nonl();
   2103 	//intrflush(stdscr, FALSE);
   2104 	keypad(stdscr, TRUE);
   2105 #ifndef NOMOUSE
   2106 #if NCURSES_MOUSE_VERSION <= 1
   2107 	mousemask(BUTTON1_PRESSED | BUTTON1_DOUBLE_CLICKED | BUTTON2_PRESSED | BUTTON3_PRESSED,
   2108 		  (mmask_t *)oldmask);
   2109 #else
   2110 	mousemask(BUTTON1_PRESSED | BUTTON2_PRESSED | BUTTON3_PRESSED | BUTTON4_PRESSED
   2111 		  | BUTTON5_PRESSED, (mmask_t *)oldmask);
   2112 #endif
   2113 	mouseinterval(0);
   2114 #endif
   2115 	curs_set(FALSE); /* Hide cursor */
   2116 
   2117 	char *colors = getenv(env_cfg[NNN_COLORS]);
   2118 
   2119 	if (colors || !getenv("NO_COLOR")) {
   2120 		uint_t *pcode;
   2121 		bool ext = FALSE;
   2122 
   2123 		start_color();
   2124 		use_default_colors();
   2125 
   2126 		/* Initialize file colors */
   2127 		if (COLORS >= COLOR_256) {
   2128 			if (!(g_state.oldcolor || init_fcolors())) {
   2129 				exitcurses();
   2130 				msg(env_cfg[NNN_FCOLORS]);
   2131 				return FALSE;
   2132 			}
   2133 		} else
   2134 			g_state.oldcolor = 1;
   2135 
   2136 		DPRINTF_D(COLORS);
   2137 		DPRINTF_D(COLOR_PAIRS);
   2138 
   2139 		if (colors && *colors == '#') {
   2140 			char *sep = strchr(colors, ';');
   2141 
   2142 			if (!g_state.oldcolor && COLORS >= COLOR_256) {
   2143 				++colors;
   2144 				ext = TRUE;
   2145 
   2146 				/*
   2147 				 * If fallback colors are specified, set the separator
   2148 				 * to NULL so we don't interpret separator and fallback
   2149 				 * if fewer than CTX_MAX xterm 256 colors are specified.
   2150 				 */
   2151 				if (sep)
   2152 					*sep = '\0';
   2153 			} else {
   2154 				colors = sep; /* Detect if 8 colors fallback is appended */
   2155 				if (colors)
   2156 					++colors;
   2157 			}
   2158 		}
   2159 
   2160 		/* Get and set the context colors */
   2161 		for (uchar_t i = 0; i <  CTX_MAX; ++i) {
   2162 			pcode = &g_ctx[i].color;
   2163 
   2164 			if (colors && *colors) {
   2165 				if (ext) {
   2166 					*pcode = xchartohex(*colors) << 4;
   2167 					if (*++colors)
   2168 						fcolors[i + 1] = *pcode += xchartohex(*colors);
   2169 					else { /* Each color code must be 2 hex symbols */
   2170 						exitcurses();
   2171 						msg(env_cfg[NNN_COLORS]);
   2172 						return FALSE;
   2173 					}
   2174 				} else
   2175 					*pcode = (*colors < '0' || *colors > '7') ? 4 : *colors - '0';
   2176 				++colors;
   2177 			} else
   2178 				*pcode = 4;
   2179 
   2180 			init_pair(i + 1, *pcode, -1);
   2181 		}
   2182 	}
   2183 
   2184 #ifdef ICONS_ENABLED
   2185 	if (!g_state.oldcolor) {
   2186 		uchar_t icolors[COLOR_256] = {0};
   2187 		char c;
   2188 
   2189 		memset(icon_positions, 0x7f, sizeof(icon_positions));
   2190 
   2191 		for (uint_t i = 0; i < sizeof(icons_ext)/sizeof(struct icon_pair); ++i) {
   2192 			c = TOUPPER(icons_ext[i].match[0]);
   2193 			if (c >= 'A' && c <= 'Z') {
   2194 				if (icon_positions[c - 'A' + 10] == 0x7f7f)
   2195 					icon_positions[c - 'A' + 10] = i;
   2196 			} else if (c >= '0' && c <= '9') {
   2197 				if (icon_positions[c - '0'] == 0x7f7f)
   2198 					icon_positions[c - '0'] = i;
   2199 			} else if (icon_positions[36] == 0x7f7f)
   2200 				icon_positions[36] = i;
   2201 
   2202 			if (icons_ext[i].color && !icolors[icons_ext[i].color]) {
   2203 				init_pair(C_UND + 1 + icons_ext[i].color, icons_ext[i].color, -1);
   2204 				icolors[icons_ext[i].color] = 1;
   2205 			}
   2206 		}
   2207 	}
   2208 #endif
   2209 
   2210 	settimeout(); /* One second */
   2211 	set_escdelay(25);
   2212 	return TRUE;
   2213 }
   2214 
   2215 /* No NULL check here as spawn() guards against it */
   2216 static char *parseargs(char *cmd, char **argv, int *pindex)
   2217 {
   2218 	int count = 0;
   2219 	size_t len = xstrlen(cmd) + 1;
   2220 	char *line = (char *)malloc(len);
   2221 
   2222 	if (!line) {
   2223 		DPRINTF_S("malloc()!");
   2224 		return NULL;
   2225 	}
   2226 
   2227 	xstrsncpy(line, cmd, len);
   2228 	argv[count++] = line;
   2229 	cmd = line;
   2230 
   2231 	while (*line) { // NOLINT
   2232 		if (ISBLANK(*line)) {
   2233 			*line++ = '\0';
   2234 
   2235 			if (!*line) // NOLINT
   2236 				break;
   2237 
   2238 			argv[count++] = line;
   2239 			if (count == EXEC_ARGS_MAX) {
   2240 				count = -1;
   2241 				break;
   2242 			}
   2243 		}
   2244 
   2245 		++line;
   2246 	}
   2247 
   2248 	if (count == -1 || count > (EXEC_ARGS_MAX - 4)) { /* 3 args and last NULL */
   2249 		free(cmd);
   2250 		cmd = NULL;
   2251 		DPRINTF_S("NULL or too many args");
   2252 	}
   2253 
   2254 	*pindex = count;
   2255 	return cmd;
   2256 }
   2257 
   2258 static void enable_signals(void)
   2259 {
   2260 	struct sigaction dfl_act = {.sa_handler = SIG_DFL};
   2261 
   2262 	sigaction(SIGHUP, &dfl_act, NULL);
   2263 	sigaction(SIGINT, &dfl_act, NULL);
   2264 	sigaction(SIGQUIT, &dfl_act, NULL);
   2265 	sigaction(SIGTSTP, &dfl_act, NULL);
   2266 	sigaction(SIGWINCH, &dfl_act, NULL);
   2267 }
   2268 
   2269 static pid_t xfork(uchar_t flag)
   2270 {
   2271 	pid_t p = fork();
   2272 
   2273 	if (p > 0) {
   2274 		/* the parent ignores the interrupt, quit and hangup signals */
   2275 		sigaction(SIGHUP, &(struct sigaction){.sa_handler = SIG_IGN}, &oldsighup);
   2276 		sigaction(SIGTSTP, &(struct sigaction){.sa_handler = SIG_DFL}, &oldsigtstp);
   2277 		sigaction(SIGWINCH, &(struct sigaction){.sa_handler = SIG_IGN}, &oldsigwinch);
   2278 	} else if (p == 0) {
   2279 		/* We create a grandchild to detach */
   2280 		if (flag & F_NOWAIT) {
   2281 			p = fork();
   2282 
   2283 			if (p > 0)
   2284 				_exit(EXIT_SUCCESS);
   2285 			else if (p == 0) {
   2286 				enable_signals();
   2287 				setsid();
   2288 				return p;
   2289 			}
   2290 
   2291 			perror("fork");
   2292 			_exit(EXIT_FAILURE);
   2293 		}
   2294 
   2295 		/* So they can be used to stop the child */
   2296 		enable_signals();
   2297 	}
   2298 
   2299 	/* This is the parent waiting for the child to create grandchild */
   2300 	if (flag & F_NOWAIT)
   2301 		waitpid(p, NULL, 0);
   2302 
   2303 	if (p == -1)
   2304 		perror("fork");
   2305 	return p;
   2306 }
   2307 
   2308 static int join(pid_t p, uchar_t flag)
   2309 {
   2310 	int status = 0xFFFF;
   2311 
   2312 	if (!(flag & F_NOWAIT)) {
   2313 		/* wait for the child to exit */
   2314 		do {
   2315 		} while (waitpid(p, &status, 0) == -1);
   2316 
   2317 		if (WIFEXITED(status)) {
   2318 			status = WEXITSTATUS(status);
   2319 			DPRINTF_D(status);
   2320 		}
   2321 	}
   2322 
   2323 	/* restore parent's signal handling */
   2324 	sigaction(SIGHUP, &oldsighup, NULL);
   2325 	sigaction(SIGTSTP, &oldsigtstp, NULL);
   2326 	sigaction(SIGWINCH, &oldsigwinch, NULL);
   2327 
   2328 	return status;
   2329 }
   2330 
   2331 /*
   2332  * Spawns a child process. Behaviour can be controlled using flag.
   2333  * Limited to 3 arguments to a program, flag works on bit set.
   2334  */
   2335 static int spawn(char *file, char *arg1, char *arg2, char *arg3, ushort_t flag)
   2336 {
   2337 	pid_t pid;
   2338 	int status = 0, retstatus = 0xFFFF;
   2339 	char *argv[EXEC_ARGS_MAX] = {0};
   2340 	char *cmd = NULL;
   2341 
   2342 	if (!file || !*file)
   2343 		return retstatus;
   2344 
   2345 	/* Swap args if the first arg is NULL and the other 2 aren't */
   2346 	if (!arg1 && arg2) {
   2347 		arg1 = arg2;
   2348 		if (arg3) {
   2349 			arg2 = arg3;
   2350 			arg3 = NULL;
   2351 		} else
   2352 			arg2 = NULL;
   2353 	}
   2354 
   2355 	if (flag & F_MULTI) {
   2356 		cmd = parseargs(file, argv, &status);
   2357 		if (!cmd)
   2358 			return -1;
   2359 	} else
   2360 		argv[status++] = file;
   2361 
   2362 	argv[status] = arg1;
   2363 	argv[++status] = arg2;
   2364 	argv[++status] = arg3;
   2365 
   2366 	if (flag & F_NORMAL)
   2367 		exitcurses();
   2368 
   2369 	pid = xfork(flag);
   2370 	if (pid == 0) {
   2371 		/* Suppress stdout and stderr */
   2372 		if (flag & F_NOTRACE) {
   2373 			int fd = open("/dev/null", O_WRONLY, 0200);
   2374 
   2375 			if (flag & F_NOSTDIN)
   2376 				dup2(fd, STDIN_FILENO);
   2377 			dup2(fd, STDOUT_FILENO);
   2378 			dup2(fd, STDERR_FILENO);
   2379 			close(fd);
   2380 		} else if (flag & F_TTY) {
   2381 			/* If stdout has been redirected to a non-tty, force output to tty */
   2382 			if (!isatty(STDOUT_FILENO)) {
   2383 				int fd = open(ctermid(NULL), O_WRONLY, 0200);
   2384 				dup2(fd, STDOUT_FILENO);
   2385 				close(fd);
   2386 			}
   2387 		}
   2388 
   2389 		execvp(*argv, argv);
   2390 		_exit(EXIT_SUCCESS);
   2391 	} else {
   2392 		retstatus = join(pid, flag);
   2393 		DPRINTF_D(pid);
   2394 
   2395 		if ((flag & F_CONFIRM) || ((flag & F_CHKRTN) && retstatus)) {
   2396 			status = write(STDOUT_FILENO, messages[MSG_ENTER], xstrlen(messages[MSG_ENTER]));
   2397 			(void)status;
   2398 			while ((read(STDIN_FILENO, &status, 1) > 0) && (status != '\n'));
   2399 		}
   2400 
   2401 		if (flag & F_NORMAL)
   2402 			refresh();
   2403 
   2404 		free(cmd);
   2405 	}
   2406 
   2407 	return retstatus;
   2408 }
   2409 
   2410 /* Get program name from env var, else return fallback program */
   2411 static char *xgetenv(const char * const name, char *fallback)
   2412 {
   2413 	char *value = getenv(name);
   2414 
   2415 	return value && value[0] ? value : fallback;
   2416 }
   2417 
   2418 /* Checks if an env variable is set to 1 */
   2419 static inline uint_t xgetenv_val(const char *name)
   2420 {
   2421 	char *str = getenv(name);
   2422 
   2423 	if (str && str[0])
   2424 		return atoi(str);
   2425 
   2426 	return 0;
   2427 }
   2428 
   2429 /* Check if a dir exists, IS a dir, and is readable */
   2430 static bool xdiraccess(const char *path)
   2431 {
   2432 	DIR *dirp = opendir(path);
   2433 
   2434 	if (!dirp) {
   2435 		printwarn(NULL);
   2436 		return FALSE;
   2437 	}
   2438 
   2439 	closedir(dirp);
   2440 	return TRUE;
   2441 }
   2442 
   2443 static bool plugscript(const char *plugin, uchar_t flags)
   2444 {
   2445 	mkpath(plgpath, plugin, g_buf);
   2446 	if (!access(g_buf, X_OK)) {
   2447 		spawn(g_buf, NULL, NULL, NULL, flags);
   2448 		return TRUE;
   2449 	}
   2450 
   2451 	return FALSE;
   2452 }
   2453 
   2454 static void opstr(char *buf, char *op)
   2455 {
   2456 	snprintf(buf, CMD_LEN_MAX, "xargs -0 sh -c '%s \"$0\" \"$@\" . < /dev/tty' < %s",
   2457 		 op, selpath);
   2458 }
   2459 
   2460 static bool rmmulstr(char *buf)
   2461 {
   2462 	char r = confirm_force(TRUE);
   2463 	if (!r)
   2464 		return FALSE;
   2465 
   2466 	if (!g_state.trash)
   2467 		snprintf(buf, CMD_LEN_MAX, "xargs -0 sh -c 'rm -%cr \"$0\" \"$@\" < /dev/tty' < %s",
   2468 			 r, selpath);
   2469 	else
   2470 		snprintf(buf, CMD_LEN_MAX, "xargs -0 %s < %s",
   2471 			 utils[(g_state.trash == 1) ? UTIL_TRASH_CLI : UTIL_GIO_TRASH], selpath);
   2472 
   2473 	return TRUE;
   2474 }
   2475 
   2476 /* Returns TRUE if file is removed, else FALSE */
   2477 static bool xrm(char * const fpath)
   2478 {
   2479 	char r = confirm_force(FALSE);
   2480 	if (!r)
   2481 		return FALSE;
   2482 
   2483 	if (!g_state.trash) {
   2484 		char rm_opts[] = "-ir";
   2485 
   2486 		rm_opts[1] = r;
   2487 		spawn("rm", rm_opts, fpath, NULL, F_NORMAL | F_CHKRTN);
   2488 	} else
   2489 		spawn(utils[(g_state.trash == 1) ? UTIL_TRASH_CLI : UTIL_GIO_TRASH],
   2490 		      fpath, NULL, NULL, F_NORMAL | F_MULTI);
   2491 
   2492 	return (access(fpath, F_OK) == -1); /* File is removed */
   2493 }
   2494 
   2495 static void xrmfromsel(char *path, char *fpath)
   2496 {
   2497 #ifndef NOX11
   2498 	bool selected = TRUE;
   2499 #endif
   2500 
   2501 	if ((pdents[cur].flags & DIR_OR_DIRLNK) && scanselforpath(fpath, FALSE))
   2502 		clearselection();
   2503 	else if (pdents[cur].flags & FILE_SELECTED) {
   2504 		--nselected;
   2505 		rmfromselbuf(mkpath(path, pdents[cur].name, g_sel));
   2506 	}
   2507 #ifndef NOX11
   2508 	else
   2509 		selected = FALSE;
   2510 
   2511 	if (selected && cfg.x11)
   2512 		plugscript(utils[UTIL_CBCP], F_NOWAIT | F_NOTRACE);
   2513 #endif
   2514 }
   2515 
   2516 static uint_t lines_in_file(int fd, char *buf, size_t buflen)
   2517 {
   2518 	ssize_t len;
   2519 	uint_t count = 0;
   2520 
   2521 	while ((len = read(fd, buf, buflen)) > 0)
   2522 		while (len)
   2523 			count += (buf[--len] == '\n');
   2524 
   2525 	/* For all use cases 0 linecount is considered as error */
   2526 	return ((len < 0) ? 0 : count);
   2527 }
   2528 
   2529 static bool cpmv_rename(int choice, const char *path)
   2530 {
   2531 	int fd;
   2532 	uint_t count = 0, lines = 0;
   2533 	bool ret = FALSE;
   2534 	char *cmd = (choice == 'c' ? cp : mv);
   2535 	char buf[sizeof(patterns[P_CPMVRNM]) + sizeof(cmd) + (PATH_MAX << 1)];
   2536 
   2537 	fd = create_tmp_file();
   2538 	if (fd == -1)
   2539 		return ret;
   2540 
   2541 	/* selsafe() returned TRUE for this to be called */
   2542 	if (!selbufpos) {
   2543 		snprintf(buf, sizeof(buf), "tr '\\0' '\\n' < %s > %s", selpath, g_tmpfpath);
   2544 		spawn(utils[UTIL_SH_EXEC], buf, NULL, NULL, F_CLI);
   2545 
   2546 		count = lines_in_file(fd, buf, sizeof(buf));
   2547 		if (!count)
   2548 			goto finish;
   2549 	} else
   2550 		seltofile(fd, &count);
   2551 
   2552 	close(fd);
   2553 
   2554 	snprintf(buf, sizeof(buf), patterns[P_CPMVFMT], g_tmpfpath);
   2555 	spawn(utils[UTIL_SH_EXEC], buf, NULL, NULL, F_CLI);
   2556 
   2557 	spawn((cfg.waitedit ? enveditor : editor), g_tmpfpath, NULL, NULL, F_CLI);
   2558 
   2559 	fd = open(g_tmpfpath, O_RDONLY);
   2560 	if (fd == -1)
   2561 		goto finish;
   2562 
   2563 	lines = lines_in_file(fd, buf, sizeof(buf));
   2564 	DPRINTF_U(count);
   2565 	DPRINTF_U(lines);
   2566 	if (!lines || (2 * count != lines)) {
   2567 		DPRINTF_S("num mismatch");
   2568 		goto finish;
   2569 	}
   2570 
   2571 	snprintf(buf, sizeof(buf), patterns[P_CPMVRNM], path, g_tmpfpath, cmd);
   2572 	if (!spawn(utils[UTIL_SH_EXEC], buf, NULL, NULL, F_CLI | F_CHKRTN))
   2573 		ret = TRUE;
   2574 finish:
   2575 	if (fd >= 0)
   2576 		close(fd);
   2577 
   2578 	return ret;
   2579 }
   2580 
   2581 static bool cpmvrm_selection(enum action sel, char *path)
   2582 {
   2583 	int r;
   2584 
   2585 	if (isselfileempty()) {
   2586 		if (nselected)
   2587 			clearselection();
   2588 		printmsg(messages[MSG_0_SELECTED]);
   2589 		return FALSE;
   2590 	}
   2591 
   2592 	if (!selsafe())
   2593 		return FALSE;
   2594 
   2595 	switch (sel) {
   2596 	case SEL_CP:
   2597 		opstr(g_buf, cp);
   2598 		break;
   2599 	case SEL_MV:
   2600 		opstr(g_buf, mv);
   2601 		break;
   2602 	case SEL_CPMVAS:
   2603 		r = get_input(messages[MSG_CP_MV_AS]);
   2604 		if (r != 'c' && r != 'm') {
   2605 			printmsg(messages[MSG_INVALID_KEY]);
   2606 			return FALSE;
   2607 		}
   2608 
   2609 		if (!cpmv_rename(r, path)) {
   2610 			printmsg(messages[MSG_FAILED]);
   2611 			return FALSE;
   2612 		}
   2613 		break;
   2614 	default: /* SEL_RM */
   2615 		if (!rmmulstr(g_buf)) {
   2616 			printmsg(messages[MSG_CANCEL]);
   2617 			return FALSE;
   2618 		}
   2619 	}
   2620 
   2621 	if (sel != SEL_CPMVAS && spawn(utils[UTIL_SH_EXEC], g_buf, NULL, NULL, F_CLI | F_CHKRTN)) {
   2622 		printmsg(messages[MSG_FAILED]);
   2623 		return FALSE;
   2624 	}
   2625 
   2626 	/* Clear selection */
   2627 	clearselection();
   2628 
   2629 	return TRUE;
   2630 }
   2631 
   2632 #ifndef NOBATCH
   2633 static bool batch_rename(void)
   2634 {
   2635 	int fd1, fd2;
   2636 	uint_t count = 0, lines = 0;
   2637 	bool dir = FALSE, ret = FALSE;
   2638 	char foriginal[TMP_LEN_MAX] = {0};
   2639 	static const char batchrenamecmd[] = "paste -d'\n' %s %s | "SED" 'N; /^\\(.*\\)\\n\\1$/!p;d' | "
   2640 					     "tr '\n' '\\0' | xargs -0 -n2 sh -c 'mv -i \"$0\" \"$@\" <"
   2641 					     " /dev/tty'";
   2642 	char buf[sizeof(batchrenamecmd) + (PATH_MAX << 1)];
   2643 	int i = get_cur_or_sel();
   2644 
   2645 	if (!i)
   2646 		return ret;
   2647 
   2648 	if (i == 'c') { /* Rename entries in current dir */
   2649 		selbufpos = 0;
   2650 		dir = TRUE;
   2651 	}
   2652 
   2653 	fd1 = create_tmp_file();
   2654 	if (fd1 == -1)
   2655 		return ret;
   2656 
   2657 	xstrsncpy(foriginal, g_tmpfpath, xstrlen(g_tmpfpath) + 1);
   2658 
   2659 	fd2 = create_tmp_file();
   2660 	if (fd2 == -1) {
   2661 		unlink(foriginal);
   2662 		close(fd1);
   2663 		return ret;
   2664 	}
   2665 
   2666 	if (dir)
   2667 		for (i = 0; i < ndents; ++i)
   2668 			appendfpath(pdents[i].name, NAME_MAX);
   2669 
   2670 	seltofile(fd1, &count);
   2671 	seltofile(fd2, NULL);
   2672 	close(fd2);
   2673 
   2674 	if (dir) /* Don't retain dir entries in selection */
   2675 		selbufpos = 0;
   2676 
   2677 	spawn((cfg.waitedit ? enveditor : editor), g_tmpfpath, NULL, NULL, F_CLI);
   2678 
   2679 	/* Reopen file descriptor to get updated contents */
   2680 	fd2 = open(g_tmpfpath, O_RDONLY);
   2681 	if (fd2 == -1)
   2682 		goto finish;
   2683 
   2684 	lines = lines_in_file(fd2, buf, sizeof(buf));
   2685 	DPRINTF_U(count);
   2686 	DPRINTF_U(lines);
   2687 	if (!lines || (count != lines)) {
   2688 		DPRINTF_S("cannot delete files");
   2689 		goto finish;
   2690 	}
   2691 
   2692 	snprintf(buf, sizeof(buf), batchrenamecmd, foriginal, g_tmpfpath);
   2693 	spawn(utils[UTIL_SH_EXEC], buf, NULL, NULL, F_CLI);
   2694 	ret = TRUE;
   2695 
   2696 finish:
   2697 	if (fd1 >= 0)
   2698 		close(fd1);
   2699 	unlink(foriginal);
   2700 
   2701 	if (fd2 >= 0)
   2702 		close(fd2);
   2703 	unlink(g_tmpfpath);
   2704 
   2705 	return ret;
   2706 }
   2707 #endif
   2708 
   2709 static void get_archive_cmd(char *cmd, const char *archive)
   2710 {
   2711 	uchar_t i = 3;
   2712 
   2713 	if (getutil(utils[UTIL_ATOOL]))
   2714 		i = 0;
   2715 	else if (getutil(utils[UTIL_BSDTAR]))
   2716 		i = 1;
   2717 	else if (is_suffix(archive, ".zip"))
   2718 		i = 2;
   2719 	// else tar
   2720 
   2721 	xstrsncpy(cmd, archive_cmd[i], ARCHIVE_CMD_LEN);
   2722 }
   2723 
   2724 static void archive_selection(const char *cmd, const char *archive, const char *curpath)
   2725 {
   2726 	char *buf = malloc((xstrlen(patterns[P_ARCHIVE_CMD]) + xstrlen(cmd) + xstrlen(archive)
   2727 	                   + xstrlen(curpath) + xstrlen(selpath)) * sizeof(char));
   2728 	if (!buf) {
   2729 		DPRINTF_S(strerror(errno));
   2730 		printwarn(NULL);
   2731 		return;
   2732 	}
   2733 
   2734 	snprintf(buf, CMD_LEN_MAX, patterns[P_ARCHIVE_CMD], curpath, selpath, cmd, archive);
   2735 	spawn(utils[UTIL_SH_EXEC], buf, NULL, NULL, F_CLI | F_CONFIRM);
   2736 	free(buf);
   2737 }
   2738 
   2739 static void write_lastdir(const char *curpath, const char *outfile)
   2740 {
   2741 	if (!outfile)
   2742 		xstrsncpy(cfgpath + xstrlen(cfgpath), "/.lastd", 8);
   2743 	else
   2744 		convert_tilde(outfile, g_buf);
   2745 
   2746 	int fd = open(outfile
   2747 			? (outfile[0] == '~' ? g_buf : outfile)
   2748 			: cfgpath, O_CREAT | O_WRONLY | O_TRUNC, 0666);
   2749 
   2750 	if (fd != -1) {
   2751 		dprintf(fd, "cd \"%s\"", curpath);
   2752 		close(fd);
   2753 	}
   2754 }
   2755 
   2756 /*
   2757  * We assume none of the strings are NULL.
   2758  *
   2759  * Let's have the logic to sort numeric names in numeric order.
   2760  * E.g., the order '1, 10, 2' doesn't make sense to human eyes.
   2761  *
   2762  * If the absolute numeric values are same, we fallback to alphasort.
   2763  */
   2764 static int xstricmp(const char * const s1, const char * const s2)
   2765 {
   2766 	char *p1, *p2;
   2767 
   2768 	long long v1 = strtoll(s1, &p1, 10);
   2769 	long long v2 = strtoll(s2, &p2, 10);
   2770 
   2771 	/* Check if at least 1 string is numeric */
   2772 	if (s1 != p1 || s2 != p2) {
   2773 		/* Handle both pure numeric */
   2774 		if (s1 != p1 && s2 != p2) {
   2775 			if (v2 > v1)
   2776 				return -1;
   2777 
   2778 			if (v1 > v2)
   2779 				return 1;
   2780 		}
   2781 
   2782 		/* Only first string non-numeric */
   2783 		if (s1 == p1)
   2784 			return 1;
   2785 
   2786 		/* Only second string non-numeric */
   2787 		if (s2 == p2)
   2788 			return -1;
   2789 	}
   2790 
   2791 	/* Handle 1. all non-numeric and 2. both same numeric value cases */
   2792 #ifndef NOLC
   2793 	return strcoll(s1, s2);
   2794 #else
   2795 	return strcasecmp(s1, s2);
   2796 #endif
   2797 }
   2798 
   2799 /*
   2800  * Version comparison
   2801  *
   2802  * The code for version compare is a modified version of the GLIBC
   2803  * and uClibc implementation of strverscmp(). The source is here:
   2804  * https://elixir.bootlin.com/uclibc-ng/latest/source/libc/string/strverscmp.c
   2805  */
   2806 
   2807 /*
   2808  * Compare S1 and S2 as strings holding indices/version numbers,
   2809  * returning less than, equal to or greater than zero if S1 is less than,
   2810  * equal to or greater than S2 (for more info, see the texinfo doc).
   2811  *
   2812  * Ignores case.
   2813  */
   2814 static int xstrverscasecmp(const char * const s1, const char * const s2)
   2815 {
   2816 	const uchar_t *p1 = (const uchar_t *)s1;
   2817 	const uchar_t *p2 = (const uchar_t *)s2;
   2818 	int state, diff;
   2819 	uchar_t c1, c2;
   2820 
   2821 	/*
   2822 	 * Symbol(s)    0       [1-9]   others
   2823 	 * Transition   (10) 0  (01) d  (00) x
   2824 	 */
   2825 	static const uint8_t next_state[] = {
   2826 		/* state    x    d    0  */
   2827 		/* S_N */  S_N, S_I, S_Z,
   2828 		/* S_I */  S_N, S_I, S_I,
   2829 		/* S_F */  S_N, S_F, S_F,
   2830 		/* S_Z */  S_N, S_F, S_Z
   2831 	};
   2832 
   2833 	static const int8_t result_type[] __attribute__ ((aligned)) = {
   2834 		/* state   x/x  x/d  x/0  d/x  d/d  d/0  0/x  0/d  0/0  */
   2835 
   2836 		/* S_N */  VCMP, VCMP, VCMP, VCMP, VLEN, VCMP, VCMP, VCMP, VCMP,
   2837 		/* S_I */  VCMP,   -1,   -1,    1, VLEN, VLEN,    1, VLEN, VLEN,
   2838 		/* S_F */  VCMP, VCMP, VCMP, VCMP, VCMP, VCMP, VCMP, VCMP, VCMP,
   2839 		/* S_Z */  VCMP,    1,    1,   -1, VCMP, VCMP,   -1, VCMP, VCMP
   2840 	};
   2841 
   2842 	if (p1 == p2)
   2843 		return 0;
   2844 
   2845 	c1 = TOUPPER(*p1);
   2846 	++p1;
   2847 	c2 = TOUPPER(*p2);
   2848 	++p2;
   2849 
   2850 	/* Hint: '0' is a digit too.  */
   2851 	state = S_N + ((c1 == '0') + (xisdigit(c1) != 0));
   2852 
   2853 	while ((diff = c1 - c2) == 0) {
   2854 		if (c1 == '\0')
   2855 			return diff;
   2856 
   2857 		state = next_state[state];
   2858 		c1 = TOUPPER(*p1);
   2859 		++p1;
   2860 		c2 = TOUPPER(*p2);
   2861 		++p2;
   2862 		state += (c1 == '0') + (xisdigit(c1) != 0);
   2863 	}
   2864 
   2865 	state = result_type[state * 3 + (((c2 == '0') + (xisdigit(c2) != 0)))]; // NOLINT
   2866 
   2867 	switch (state) {
   2868 	case VCMP:
   2869 		return diff;
   2870 	case VLEN:
   2871 		while (xisdigit(*p1++))
   2872 			if (!xisdigit(*p2++))
   2873 				return 1;
   2874 		return xisdigit(*p2) ? -1 : diff;
   2875 	default:
   2876 		return state;
   2877 	}
   2878 }
   2879 
   2880 static int (*namecmpfn)(const char * const s1, const char * const s2) = &xstricmp;
   2881 
   2882 static char * (*fnstrstr)(const char *haystack, const char *needle) = &strcasestr;
   2883 #ifdef PCRE
   2884 static const unsigned char *tables;
   2885 static int pcreflags = PCRE_NO_AUTO_CAPTURE | PCRE_EXTENDED | PCRE_CASELESS | PCRE_UTF8;
   2886 #else
   2887 static int regflags = REG_NOSUB | REG_EXTENDED | REG_ICASE;
   2888 #endif
   2889 
   2890 #ifdef PCRE
   2891 static int setfilter(pcre **pcrex, const char *filter)
   2892 {
   2893 	const char *errstr = NULL;
   2894 	int erroffset = 0;
   2895 
   2896 	*pcrex = pcre_compile(filter, pcreflags, &errstr, &erroffset, tables);
   2897 
   2898 	return errstr ? -1 : 0;
   2899 }
   2900 #else
   2901 static int setfilter(regex_t *regex, const char *filter)
   2902 {
   2903 	return regcomp(regex, filter, regflags);
   2904 }
   2905 #endif
   2906 
   2907 static int visible_re(const fltrexp_t *fltrexp, const char *fname)
   2908 {
   2909 #ifdef PCRE
   2910 	return pcre_exec(fltrexp->pcrex, NULL, fname, xstrlen(fname), 0, 0, NULL, 0) == 0;
   2911 #else
   2912 	return regexec(fltrexp->regex, fname, 0, NULL, 0) == 0;
   2913 #endif
   2914 }
   2915 
   2916 static int visible_str(const fltrexp_t *fltrexp, const char *fname)
   2917 {
   2918 	return fnstrstr(fname, fltrexp->str) != NULL;
   2919 }
   2920 
   2921 static int (*filterfn)(const fltrexp_t *fltr, const char *fname) = &visible_str;
   2922 
   2923 static void clearfilter(void)
   2924 {
   2925 	char *fltr = g_ctx[cfg.curctx].c_fltr;
   2926 
   2927 	if (fltr[1]) {
   2928 		fltr[REGEX_MAX - 1] = fltr[1];
   2929 		fltr[1] = '\0';
   2930 	}
   2931 }
   2932 
   2933 static int entrycmp(const void *va, const void *vb)
   2934 {
   2935 	const struct entry *pa = (pEntry)va;
   2936 	const struct entry *pb = (pEntry)vb;
   2937 
   2938 	if ((pb->flags & DIR_OR_DIRLNK) != (pa->flags & DIR_OR_DIRLNK)) {
   2939 		if (pb->flags & DIR_OR_DIRLNK)
   2940 			return 1;
   2941 		return -1;
   2942 	}
   2943 
   2944 	/* Sort based on specified order */
   2945 	if (cfg.timeorder) {
   2946 		if (pb->sec > pa->sec)
   2947 			return 1;
   2948 		if (pb->sec < pa->sec)
   2949 			return -1;
   2950 		/* If sec matches, comare nsec */
   2951 		if (pb->nsec > pa->nsec)
   2952 			return 1;
   2953 		if (pb->nsec < pa->nsec)
   2954 			return -1;
   2955 	} else if (cfg.sizeorder) {
   2956 		if (pb->size > pa->size)
   2957 			return 1;
   2958 		if (pb->size < pa->size)
   2959 			return -1;
   2960 	} else if (cfg.blkorder) {
   2961 		if (pb->blocks > pa->blocks)
   2962 			return 1;
   2963 		if (pb->blocks < pa->blocks)
   2964 			return -1;
   2965 	} else if (cfg.extnorder && !(pb->flags & DIR_OR_DIRLNK)) {
   2966 		char *extna = xextension(pa->name, pa->nlen - 1);
   2967 		char *extnb = xextension(pb->name, pb->nlen - 1);
   2968 
   2969 		if (extna || extnb) {
   2970 			if (!extna)
   2971 				return -1;
   2972 
   2973 			if (!extnb)
   2974 				return 1;
   2975 
   2976 			int ret = strcasecmp(extna, extnb);
   2977 
   2978 			if (ret)
   2979 				return ret;
   2980 		}
   2981 	}
   2982 
   2983 	return namecmpfn(pa->name, pb->name);
   2984 }
   2985 
   2986 static int reventrycmp(const void *va, const void *vb)
   2987 {
   2988 	if ((((pEntry)vb)->flags & DIR_OR_DIRLNK)
   2989 	    != (((pEntry)va)->flags & DIR_OR_DIRLNK)) {
   2990 		if (((pEntry)vb)->flags & DIR_OR_DIRLNK)
   2991 			return 1;
   2992 		return -1;
   2993 	}
   2994 
   2995 	return -entrycmp(va, vb);
   2996 }
   2997 
   2998 static int (*entrycmpfn)(const void *va, const void *vb) = &entrycmp;
   2999 
   3000 /* In case of an error, resets *wch to Esc */
   3001 static int handle_alt_key(wint_t *wch)
   3002 {
   3003 	timeout(0);
   3004 
   3005 	int r = get_wch(wch);
   3006 
   3007 	if (r == ERR)
   3008 		*wch = ESC;
   3009 	cleartimeout();
   3010 
   3011 	return r;
   3012 }
   3013 
   3014 static inline int handle_event(void)
   3015 {
   3016 	if (nselected && isselfileempty())
   3017 		clearselection();
   3018 	return CONTROL('L');
   3019 }
   3020 
   3021 /*
   3022  * Returns SEL_* if key is bound and 0 otherwise.
   3023  * Also modifies the run and env pointers (used on SEL_{RUN,RUNARG}).
   3024  * The next keyboard input can be simulated by presel.
   3025  */
   3026 static int nextsel(int presel)
   3027 {
   3028 #ifdef BENCH
   3029 	return SEL_QUIT;
   3030 #endif
   3031 	int c = presel;
   3032 	uint_t i;
   3033 	bool escaped = FALSE;
   3034 
   3035 	if (c == 0 || c == MSGWAIT) {
   3036 try_quit:
   3037 		c = getch();
   3038 		//DPRINTF_D(c);
   3039 		//DPRINTF_S(keyname(c));
   3040 
   3041 #ifdef KEY_RESIZE
   3042 		if (c == KEY_RESIZE)
   3043 			handle_key_resize();
   3044 #endif
   3045 
   3046 		/* Handle Alt+key */
   3047 		if (c == ESC) {
   3048 			timeout(0);
   3049 			c = getch();
   3050 			if (c != ERR) {
   3051 				if (c == ESC)
   3052 					c = CONTROL('L');
   3053 				else {
   3054 					ungetch(c);
   3055 					c = ';';
   3056 				}
   3057 				settimeout();
   3058 			} else if (escaped) {
   3059 				settimeout();
   3060 				c = CONTROL('Q');
   3061 			} else {
   3062 #ifndef NOFIFO
   3063 				if (!g_state.fifomode)
   3064 					notify_fifo(TRUE); /* Send hovered path to NNN_FIFO */
   3065 #endif
   3066 				escaped = TRUE;
   3067 				settimeout();
   3068 				goto try_quit;
   3069 			}
   3070 		}
   3071 
   3072 		if (c == ERR && presel == MSGWAIT)
   3073 			c = (cfg.filtermode || filterset()) ? FILTER : CONTROL('L');
   3074 		else if (c == FILTER || c == CONTROL('L'))
   3075 			/* Clear previous filter when manually starting */
   3076 			clearfilter();
   3077 	}
   3078 
   3079 	if (c == -1) {
   3080 		++idle;
   3081 
   3082 		/*
   3083 		 * Do not check for directory changes in du mode.
   3084 		 * A redraw forces du calculation.
   3085 		 * Check for changes every odd second.
   3086 		 */
   3087 #ifdef LINUX_INOTIFY
   3088 		if (!cfg.blkorder && inotify_wd >= 0 && (idle & 1)) {
   3089 			struct inotify_event *event;
   3090 			char inotify_buf[EVENT_BUF_LEN];
   3091 
   3092 			memset((void *)inotify_buf, 0x0, EVENT_BUF_LEN);
   3093 			i = read(inotify_fd, inotify_buf, EVENT_BUF_LEN);
   3094 			if (i > 0) {
   3095 				for (char *ptr = inotify_buf;
   3096 				     ptr + ((struct inotify_event *)ptr)->len < inotify_buf + i;
   3097 				     ptr += sizeof(struct inotify_event) + event->len) {
   3098 					event = (struct inotify_event *)ptr;
   3099 					DPRINTF_D(event->wd);
   3100 					DPRINTF_D(event->mask);
   3101 					if (!event->wd)
   3102 						break;
   3103 
   3104 					if (event->mask & INOTIFY_MASK) {
   3105 						c = handle_event();
   3106 						break;
   3107 					}
   3108 				}
   3109 				DPRINTF_S("inotify read done");
   3110 			}
   3111 		}
   3112 #elif defined(BSD_KQUEUE)
   3113 		if (!cfg.blkorder && event_fd >= 0 && (idle & 1)) {
   3114 			struct kevent event_data[NUM_EVENT_SLOTS];
   3115 
   3116 			memset((void *)event_data, 0x0, sizeof(struct kevent) * NUM_EVENT_SLOTS);
   3117 			if (kevent(kq, events_to_monitor, NUM_EVENT_SLOTS,
   3118 				   event_data, NUM_EVENT_FDS, &gtimeout) > 0)
   3119 				c = handle_event();
   3120 		}
   3121 #elif defined(HAIKU_NM)
   3122 		if (!cfg.blkorder && haiku_nm_active && (idle & 1) && haiku_is_update_needed(haiku_hnd))
   3123 			c = handle_event();
   3124 #endif
   3125 	} else
   3126 		idle = 0;
   3127 
   3128 	for (i = 0; i < (int)ELEMENTS(bindings); ++i)
   3129 		if (c == bindings[i].sym)
   3130 			return bindings[i].act;
   3131 
   3132 	return 0;
   3133 }
   3134 
   3135 static int getorderstr(char *sort)
   3136 {
   3137 	int i = 0;
   3138 
   3139 	if (cfg.showhidden)
   3140 		sort[i++] = 'H';
   3141 
   3142 	if (cfg.timeorder)
   3143 		sort[i++] = (cfg.timetype == T_MOD) ? 'M' : ((cfg.timetype == T_ACCESS) ? 'A' : 'C');
   3144 	else if (cfg.sizeorder)
   3145 		sort[i++] = 'S';
   3146 	else if (cfg.extnorder)
   3147 		sort[i++] = 'E';
   3148 
   3149 	if (entrycmpfn == &reventrycmp)
   3150 		sort[i++] = 'R';
   3151 
   3152 	if (namecmpfn == &xstrverscasecmp)
   3153 		sort[i++] = 'V';
   3154 
   3155 	if (i)
   3156 		sort[i] = ' ';
   3157 
   3158 	return i;
   3159 }
   3160 
   3161 static void showfilterinfo(void)
   3162 {
   3163 	int i = 0;
   3164 	char info[REGEX_MAX] = "\0\0\0\0\0";
   3165 
   3166 	i = getorderstr(info);
   3167 
   3168 	if (cfg.fileinfo && ndents && get_output("file", "-b", pdents[cur].name, -1, FALSE, FALSE))
   3169 		mvaddstr(xlines - 2, 2, g_buf);
   3170 	else {
   3171 		snprintf(info + i, REGEX_MAX - i - 1, "  %s [/], %s [:]",
   3172 			 (cfg.regex ? "reg" : "str"),
   3173 			 ((fnstrstr == &strcasestr) ? "ic" : "noic"));
   3174 	}
   3175 
   3176 	mvaddstr(xlines - 2, xcols - xstrlen(info), info);
   3177 }
   3178 
   3179 static void showfilter(char *str)
   3180 {
   3181 	attron(COLOR_PAIR(cfg.curctx + 1));
   3182 	showfilterinfo();
   3183 	printmsg(str);
   3184 	// printmsg calls attroff()
   3185 }
   3186 
   3187 static inline void swap_ent(int id1, int id2)
   3188 {
   3189 	struct entry _dent, *pdent1 = &pdents[id1], *pdent2 =  &pdents[id2];
   3190 
   3191 	*(&_dent) = *pdent1;
   3192 	*pdent1 = *pdent2;
   3193 	*pdent2 = *(&_dent);
   3194 }
   3195 
   3196 #ifdef PCRE
   3197 static int fill(const char *fltr, pcre *pcrex)
   3198 #else
   3199 static int fill(const char *fltr, regex_t *re)
   3200 #endif
   3201 {
   3202 #ifdef PCRE
   3203 	fltrexp_t fltrexp = { .pcrex = pcrex, .str = fltr };
   3204 #else
   3205 	fltrexp_t fltrexp = { .regex = re, .str = fltr };
   3206 #endif
   3207 
   3208 	for (int count = 0; count < ndents; ++count) {
   3209 		if (filterfn(&fltrexp, pdents[count].name) == 0) {
   3210 			if (count != --ndents) {
   3211 				swap_ent(count, ndents);
   3212 				--count;
   3213 			}
   3214 
   3215 			continue;
   3216 		}
   3217 	}
   3218 
   3219 	return ndents;
   3220 }
   3221 
   3222 static int matches(const char *fltr)
   3223 {
   3224 #ifdef PCRE
   3225 	pcre *pcrex = NULL;
   3226 
   3227 	/* Search filter */
   3228 	if (cfg.regex && setfilter(&pcrex, fltr))
   3229 		return -1;
   3230 
   3231 	ndents = fill(fltr, pcrex);
   3232 
   3233 	if (cfg.regex)
   3234 		pcre_free(pcrex);
   3235 #else
   3236 	regex_t re;
   3237 
   3238 	/* Search filter */
   3239 	if (cfg.regex && setfilter(&re, fltr))
   3240 		return -1;
   3241 
   3242 	ndents = fill(fltr, &re);
   3243 
   3244 	if (cfg.regex)
   3245 		regfree(&re);
   3246 #endif
   3247 
   3248 	ENTSORT(pdents, ndents, entrycmpfn);
   3249 
   3250 	return ndents;
   3251 }
   3252 
   3253 /*
   3254  * Return the position of the matching entry or 0 otherwise
   3255  * Note there's no NULL check for fname
   3256  */
   3257 static int dentfind(const char *fname, int n)
   3258 {
   3259 	for (int i = 0; i < n; ++i)
   3260 		if (xstrcmp(fname, pdents[i].name) == 0)
   3261 			return i;
   3262 
   3263 	return 0;
   3264 }
   3265 
   3266 static int filterentries(char *path, char *lastname)
   3267 {
   3268 	wchar_t *wln = (wchar_t *)alloca(sizeof(wchar_t) * REGEX_MAX);
   3269 	char *ln = g_ctx[cfg.curctx].c_fltr;
   3270 	wint_t ch[2] = {0};
   3271 	int r, total = ndents, len;
   3272 	char *pln = g_ctx[cfg.curctx].c_fltr + 1;
   3273 
   3274 	DPRINTF_S(__func__);
   3275 
   3276 	if (ndents && (ln[0] == FILTER || ln[0] == RFILTER) && *pln) {
   3277 		if (matches(pln) != -1) {
   3278 			move_cursor(dentfind(lastname, ndents), 0);
   3279 			redraw(path);
   3280 		}
   3281 
   3282 		if (!cfg.filtermode) {
   3283 			statusbar(path);
   3284 			return 0;
   3285 		}
   3286 
   3287 		len = mbstowcs(wln, ln, REGEX_MAX);
   3288 	} else {
   3289 		ln[0] = wln[0] = cfg.regex ? RFILTER : FILTER;
   3290 		ln[1] = wln[1] = '\0';
   3291 		len = 1;
   3292 	}
   3293 
   3294 	cleartimeout();
   3295 	curs_set(TRUE);
   3296 	showfilter(ln);
   3297 
   3298 	while ((r = get_wch(ch)) != ERR) {
   3299 		//DPRINTF_D(*ch);
   3300 		//DPRINTF_S(keyname(*ch));
   3301 
   3302 		switch (*ch) {
   3303 #ifdef KEY_RESIZE
   3304 		case 0: // fallthrough
   3305 		case KEY_RESIZE:
   3306 			clearoldprompt();
   3307 			redraw(path);
   3308 			showfilter(ln);
   3309 			continue;
   3310 #endif
   3311 		case KEY_DC: // fallthrough
   3312 		case KEY_BACKSPACE: // fallthrough
   3313 		case '\b': // fallthrough
   3314 		case DEL: /* handle DEL */
   3315 			if (len != 1) {
   3316 				wln[--len] = '\0';
   3317 				wcstombs(ln, wln, REGEX_MAX);
   3318 				ndents = total;
   3319 			} else {
   3320 				*ch = FILTER;
   3321 				goto end;
   3322 			}
   3323 			// fallthrough
   3324 		case CONTROL('L'):
   3325 			if (*ch == CONTROL('L')) {
   3326 				if (wln[1]) {
   3327 					ln[REGEX_MAX - 1] = ln[1];
   3328 					ln[1] = wln[1] = '\0';
   3329 					len = 1;
   3330 					ndents = total;
   3331 				} else if (ln[REGEX_MAX - 1]) { /* Show the previous filter */
   3332 					ln[1] = ln[REGEX_MAX - 1];
   3333 					ln[REGEX_MAX - 1] = '\0';
   3334 					len = mbstowcs(wln, ln, REGEX_MAX);
   3335 				} else
   3336 					goto end;
   3337 			}
   3338 
   3339 			/* Go to the top, we don't know if the hovered file will match the filter */
   3340 			cur = 0;
   3341 
   3342 			if (matches(pln) != -1)
   3343 				redraw(path);
   3344 
   3345 			showfilter(ln);
   3346 			continue;
   3347 #ifndef NOMOUSE
   3348 		case KEY_MOUSE:
   3349 			goto end;
   3350 #endif
   3351 		case ESC:
   3352 			if (handle_alt_key(ch) != ERR) {
   3353 				if (*ch == ESC) /* Handle Alt+Esc */
   3354 					*ch = 'q'; /* Quit context */
   3355 				else {
   3356 					unget_wch(*ch);
   3357 					*ch = ';'; /* Run plugin */
   3358 				}
   3359 			}
   3360 			goto end;
   3361 		}
   3362 
   3363 		if (r != OK) /* Handle Fn keys in main loop */
   3364 			break;
   3365 
   3366 		/* Handle all control chars in main loop */
   3367 		if (*ch < ASCII_MAX && keyname(*ch)[0] == '^' && *ch != '^')
   3368 			goto end;
   3369 
   3370 		if (len == 1) {
   3371 			if (*ch == '?') /* Help and config key, '?' is an invalid regex */
   3372 				goto end;
   3373 
   3374 			if (cfg.filtermode) {
   3375 				switch (*ch) {
   3376 				case '\'': // fallthrough /* Go to first non-dir file */
   3377 				case '+': // fallthrough /* Toggle auto-proceed on open */
   3378 				case ',': // fallthrough /* Mark CWD */
   3379 				case '-': // fallthrough /* Visit last visited dir */
   3380 				case '.': // fallthrough /* Show hidden files */
   3381 				case ';': // fallthrough /* Run plugin key */
   3382 				case '=': // fallthrough /* Launch app */
   3383 				case '>': // fallthrough /* Export file list */
   3384 				case '@': // fallthrough /* Visit start dir */
   3385 				case ']': // fallthorugh /* Prompt key */
   3386 				case '`': // fallthrough /* Visit / */
   3387 				case '~': /* Go HOME */
   3388 					goto end;
   3389 				}
   3390 			}
   3391 
   3392 			/* Toggle case-sensitivity */
   3393 			if (*ch == CASE) {
   3394 				fnstrstr = (fnstrstr == &strcasestr) ? &strstr : &strcasestr;
   3395 #ifdef PCRE
   3396 				pcreflags ^= PCRE_CASELESS;
   3397 #else
   3398 				regflags ^= REG_ICASE;
   3399 #endif
   3400 				showfilter(ln);
   3401 				continue;
   3402 			}
   3403 
   3404 			/* Toggle string or regex filter */
   3405 			if (*ch == FILTER) {
   3406 				ln[0] = (ln[0] == FILTER) ? RFILTER : FILTER;
   3407 				wln[0] = (uchar_t)ln[0];
   3408 				cfg.regex ^= 1;
   3409 				filterfn = cfg.regex ? &visible_re : &visible_str;
   3410 				showfilter(ln);
   3411 				continue;
   3412 			}
   3413 
   3414 			/* Reset cur in case it's a repeat search */
   3415 			cur = 0;
   3416 		} else if (len == REGEX_MAX - 1)
   3417 			continue;
   3418 
   3419 		wln[len] = (wchar_t)*ch;
   3420 		wln[++len] = '\0';
   3421 		wcstombs(ln, wln, REGEX_MAX);
   3422 
   3423 		/* Forward-filtering optimization:
   3424 		 * - new matches can only be a subset of current matches.
   3425 		 */
   3426 		/* ndents = total; */
   3427 #ifdef MATCHFLTR
   3428 		r = matches(pln);
   3429 		if (r <= 0) {
   3430 			!r ? unget_wch(KEY_BACKSPACE) : showfilter(ln);
   3431 #else
   3432 		if (matches(pln) == -1) {
   3433 			showfilter(ln);
   3434 #endif
   3435 			continue;
   3436 		}
   3437 
   3438 		/* If the only match is a dir, auto-enter and cd into it */
   3439 		if (ndents == 1 && cfg.filtermode
   3440 		    && cfg.autoenter && (pdents[0].flags & DIR_OR_DIRLNK)) {
   3441 			*ch = KEY_ENTER;
   3442 			cur = 0;
   3443 			goto end;
   3444 		}
   3445 
   3446 		/*
   3447 		 * redraw() should be above the auto-enter optimization, for
   3448 		 * the case where there's an issue with dir auto-enter, say,
   3449 		 * due to a permission problem. The transition is _jumpy_ in
   3450 		 * case of such an error. However, we optimize for successful
   3451 		 * cases where the dir has permissions. This skips a redraw().
   3452 		 */
   3453 		redraw(path);
   3454 		showfilter(ln);
   3455 	}
   3456 end:
   3457 
   3458 	/* Save last working filter in-filter */
   3459 	if (ln[1])
   3460 		ln[REGEX_MAX - 1] = ln[1];
   3461 
   3462 	/* Save current */
   3463 	copycurname();
   3464 
   3465 	curs_set(FALSE);
   3466 	settimeout();
   3467 
   3468 	/* Return keys for navigation etc. */
   3469 	return *ch;
   3470 }
   3471 
   3472 /* Show a prompt with input string and return the changes */
   3473 static char *xreadline(const char *prefill, const char *prompt)
   3474 {
   3475 	size_t len, pos;
   3476 	int x, r;
   3477 	const int WCHAR_T_WIDTH = sizeof(wchar_t);
   3478 	wint_t ch[2] = {0};
   3479 	wchar_t * const buf = malloc(sizeof(wchar_t) * READLINE_MAX);
   3480 
   3481 	if (!buf)
   3482 		errexit();
   3483 
   3484 	cleartimeout();
   3485 	printmsg(prompt);
   3486 
   3487 	if (prefill) {
   3488 		DPRINTF_S(prefill);
   3489 		len = pos = mbstowcs(buf, prefill, READLINE_MAX);
   3490 	} else
   3491 		len = (size_t)-1;
   3492 
   3493 	if (len == (size_t)-1) {
   3494 		buf[0] = '\0';
   3495 		len = pos = 0;
   3496 	}
   3497 
   3498 	x = getcurx(stdscr);
   3499 	curs_set(TRUE);
   3500 
   3501 	while (1) {
   3502 		buf[len] = ' ';
   3503 		attron(COLOR_PAIR(cfg.curctx + 1));
   3504 		if (pos > (size_t)(xcols - x)) {
   3505 			mvaddnwstr(xlines - 1, x, buf + (pos - (xcols - x) + 1), xcols - x);
   3506 			move(xlines - 1, xcols - 1);
   3507 		} else {
   3508 			mvaddnwstr(xlines - 1, x, buf, len + 1);
   3509 			move(xlines - 1, x + wcswidth(buf, pos));
   3510 		}
   3511 		attroff(COLOR_PAIR(cfg.curctx + 1));
   3512 
   3513 		r = get_wch(ch);
   3514 		if (r == ERR)
   3515 			continue;
   3516 
   3517 		if (r == OK) {
   3518 			switch (*ch) {
   3519 			case KEY_ENTER: // fallthrough
   3520 			case '\n': // fallthrough
   3521 			case '\r':
   3522 				goto END;
   3523 			case CONTROL('D'):
   3524 				if (pos < len)
   3525 					++pos;
   3526 				else if (!(pos || len)) { /* Exit on ^D at empty prompt */
   3527 					len = 0;
   3528 					goto END;
   3529 				} else
   3530 					continue;
   3531 				// fallthrough
   3532 			case DEL: // fallthrough
   3533 			case '\b': /* rhel25 sends '\b' for backspace */
   3534 				if (pos > 0) {
   3535 					memmove(buf + pos - 1, buf + pos,
   3536 						(len - pos) * WCHAR_T_WIDTH);
   3537 					--len, --pos;
   3538 				}
   3539 				continue;
   3540 			case '\t':
   3541 				if (!(len || pos) && ndents)
   3542 					len = pos = mbstowcs(buf, pdents[cur].name, READLINE_MAX);
   3543 				continue;
   3544 			case CONTROL('F'):
   3545 				if (pos < len)
   3546 					++pos;
   3547 				continue;
   3548 			case CONTROL('B'):
   3549 				if (pos > 0)
   3550 					--pos;
   3551 				continue;
   3552 			case CONTROL('W'):
   3553 				printmsg(prompt);
   3554 				do {
   3555 					if (pos == 0)
   3556 						break;
   3557 					memmove(buf + pos - 1, buf + pos,
   3558 						(len - pos) * WCHAR_T_WIDTH);
   3559 					--pos, --len;
   3560 				} while (buf[pos - 1] != ' ' && buf[pos - 1] != '/'); // NOLINT
   3561 				continue;
   3562 			case CONTROL('K'):
   3563 				printmsg(prompt);
   3564 				len = pos;
   3565 				continue;
   3566 			case CONTROL('L'):
   3567 				printmsg(prompt);
   3568 				len = pos = 0;
   3569 				continue;
   3570 			case CONTROL('A'):
   3571 				pos = 0;
   3572 				continue;
   3573 			case CONTROL('E'):
   3574 				pos = len;
   3575 				continue;
   3576 			case CONTROL('U'):
   3577 				printmsg(prompt);
   3578 				memmove(buf, buf + pos, (len - pos) * WCHAR_T_WIDTH);
   3579 				len -= pos;
   3580 				pos = 0;
   3581 				continue;
   3582 			case ESC: /* Exit prompt on Esc, but just filter out Alt+key */
   3583 				if (handle_alt_key(ch) != ERR)
   3584 					continue;
   3585 
   3586 				len = 0;
   3587 				goto END;
   3588 			}
   3589 
   3590 			/* Filter out all other control chars */
   3591 			if (*ch < ASCII_MAX && keyname(*ch)[0] == '^')
   3592 				continue;
   3593 
   3594 			if (pos < READLINE_MAX - 1) {
   3595 				memmove(buf + pos + 1, buf + pos,
   3596 					(len - pos) * WCHAR_T_WIDTH);
   3597 				buf[pos] = *ch;
   3598 				++len, ++pos;
   3599 				continue;
   3600 			}
   3601 		} else {
   3602 			switch (*ch) {
   3603 #ifdef KEY_RESIZE
   3604 			case KEY_RESIZE:
   3605 				clearoldprompt();
   3606 				xlines = LINES;
   3607 				printmsg(prompt);
   3608 				break;
   3609 #endif
   3610 			case KEY_LEFT:
   3611 				if (pos > 0)
   3612 					--pos;
   3613 				break;
   3614 			case KEY_RIGHT:
   3615 				if (pos < len)
   3616 					++pos;
   3617 				break;
   3618 			case KEY_BACKSPACE:
   3619 				if (pos > 0) {
   3620 					memmove(buf + pos - 1, buf + pos,
   3621 						(len - pos) * WCHAR_T_WIDTH);
   3622 					--len, --pos;
   3623 				}
   3624 				break;
   3625 			case KEY_DC:
   3626 				if (pos < len) {
   3627 					memmove(buf + pos, buf + pos + 1,
   3628 						(len - pos - 1) * WCHAR_T_WIDTH);
   3629 					--len;
   3630 				}
   3631 				break;
   3632 			case KEY_END:
   3633 				pos = len;
   3634 				break;
   3635 			case KEY_HOME:
   3636 				pos = 0;
   3637 				break;
   3638 			case KEY_UP: // fallthrough
   3639 			case KEY_DOWN:
   3640 				if (prompt && lastcmd && (xstrcmp(prompt, PROMPT) == 0)) {
   3641 					printmsg(prompt);
   3642 					len = pos = mbstowcs(buf, lastcmd, READLINE_MAX); // fallthrough
   3643 				}
   3644 			default:
   3645 				break;
   3646 			}
   3647 		}
   3648 	}
   3649 
   3650 END:
   3651 	curs_set(FALSE);
   3652 	settimeout();
   3653 	printmsg("");
   3654 
   3655 	buf[len] = '\0';
   3656 
   3657 	pos = wcstombs(g_buf, buf, READLINE_MAX - 1);
   3658 	if (pos >= READLINE_MAX - 1)
   3659 		g_buf[READLINE_MAX - 1] = '\0';
   3660 
   3661 	free(buf);
   3662 	return g_buf;
   3663 }
   3664 
   3665 #ifndef NORL
   3666 /*
   3667  * Caller should check the value of presel to confirm if it needs to wait to show warning
   3668  */
   3669 static char *getreadline(const char *prompt)
   3670 {
   3671 	exitcurses();
   3672 
   3673 	char *input = readline(prompt);
   3674 
   3675 	refresh();
   3676 
   3677 	if (input && input[0]) {
   3678 		add_history(input);
   3679 		xstrsncpy(g_buf, input, CMD_LEN_MAX);
   3680 		free(input);
   3681 		return g_buf;
   3682 	}
   3683 
   3684 	free(input);
   3685 	return NULL;
   3686 }
   3687 #endif
   3688 
   3689 /*
   3690  * Create symbolic/hard link(s) to file(s) in selection list
   3691  * Returns the number of links created, -1 on error
   3692  */
   3693 static int xlink(char *prefix, char *path, char *curfname, char *buf, int *presel, int type)
   3694 {
   3695 	int count = 0, choice;
   3696 	char *psel = pselbuf, *fname;
   3697 	size_t pos = 0, len, r;
   3698 	int (*link_fn)(const char *, const char *) = NULL;
   3699 	char lnpath[PATH_MAX];
   3700 
   3701 	choice = get_cur_or_sel();
   3702 	if (!choice)
   3703 		return -1;
   3704 
   3705 	if (type == 's') /* symbolic link */
   3706 		link_fn = &symlink;
   3707 	else /* hard link */
   3708 		link_fn = &link;
   3709 
   3710 	if (choice == 'c' || (nselected == 1)) {
   3711 		mkpath(path, prefix, lnpath); /* Generate link path */
   3712 		mkpath(path, (choice == 'c') ? curfname : pselbuf, buf); /* Generate target file path */
   3713 
   3714 		if (!link_fn(buf, lnpath)) {
   3715 			if (choice == 's')
   3716 				clearselection();
   3717 			return 1; /* One link created */
   3718 		}
   3719 
   3720 		printwarn(presel);
   3721 		return -1;
   3722 	}
   3723 
   3724 	while (pos < selbufpos) {
   3725 		len = xstrlen(psel);
   3726 		fname = xbasename(psel);
   3727 
   3728 		r = xstrsncpy(buf, prefix, NAME_MAX + 1); /* Copy prefix */
   3729 		xstrsncpy(buf + r - 1, fname, NAME_MAX - r); /* Suffix target file name */
   3730 		mkpath(path, buf, lnpath); /* Generate link path */
   3731 
   3732 		if (!link_fn(psel, lnpath))
   3733 			++count;
   3734 
   3735 		pos += len + 1;
   3736 		psel += len + 1;
   3737 	}
   3738 
   3739 	clearselection();
   3740 	return count;
   3741 }
   3742 
   3743 static bool parsekvpair(kv **arr, char **envcpy, const uchar_t id, uchar_t *items)
   3744 {
   3745 	bool new = TRUE;
   3746 	const uchar_t INCR = 8;
   3747 	uint_t i = 0;
   3748 	kv *kvarr = NULL;
   3749 	char *ptr = getenv(env_cfg[id]);
   3750 
   3751 	if (!ptr || !*ptr)
   3752 		return TRUE;
   3753 
   3754 	*envcpy = xstrdup(ptr);
   3755 	if (!*envcpy) {
   3756 		xerror();
   3757 		return FALSE;
   3758 	}
   3759 
   3760 	ptr = *envcpy;
   3761 
   3762 	while (*ptr && i < 100) {
   3763 		if (new) {
   3764 			if (!(i & (INCR - 1))) {
   3765 				kvarr = xrealloc(kvarr, sizeof(kv) * (i + INCR));
   3766 				*arr = kvarr;
   3767 				if (!kvarr) {
   3768 					xerror();
   3769 					return FALSE;
   3770 				}
   3771 				memset(kvarr + i, 0, sizeof(kv) * INCR);
   3772 			}
   3773 			kvarr[i].key = (uchar_t)*ptr;
   3774 			if (*++ptr != ':' || *++ptr == '\0' || *ptr == ';')
   3775 				return FALSE;
   3776 			kvarr[i].off = ptr - *envcpy;
   3777 			++i;
   3778 
   3779 			new = FALSE;
   3780 		}
   3781 
   3782 		if (*ptr == ';') {
   3783 			*ptr = '\0';
   3784 			new = TRUE;
   3785 		}
   3786 
   3787 		++ptr;
   3788 	}
   3789 
   3790 	*items = i;
   3791 	return (i != 0);
   3792 }
   3793 
   3794 /*
   3795  * Get the value corresponding to a key
   3796  *
   3797  * NULL is returned in case of no match, path resolution failure etc.
   3798  * buf would be modified, so check return value before access
   3799  */
   3800 static char *get_kv_val(kv *kvarr, char *buf, int key, uchar_t max, uchar_t id)
   3801 {
   3802 	char *val;
   3803 
   3804 	if (!kvarr)
   3805 		return NULL;
   3806 
   3807 	for (int r = 0; r < max && kvarr[r].key; ++r) {
   3808 		if (kvarr[r].key == key) {
   3809 			/* Do not allocate new memory for plugin */
   3810 			if (id == NNN_PLUG)
   3811 				return pluginstr + kvarr[r].off;
   3812 
   3813 			val = bmstr + kvarr[r].off;
   3814 			convert_tilde(val, g_buf);
   3815 			return abspath(((val[0] == '~') ? g_buf : val), NULL, buf);
   3816 		}
   3817 	}
   3818 
   3819 	DPRINTF_S("Invalid key");
   3820 	return NULL;
   3821 }
   3822 
   3823 static int get_kv_key(kv *kvarr, char *val, uchar_t max, uchar_t id)
   3824 {
   3825 	if (!kvarr)
   3826 		return -1;
   3827 
   3828 	if (id != NNN_ORDER) /* For now this function supports only order string */
   3829 		return -1;
   3830 
   3831 	for (int r = 0; r < max && kvarr[r].key; ++r) {
   3832 		if (xstrcmp((orderstr + kvarr[r].off), val) == 0)
   3833 			return kvarr[r].key;
   3834 	}
   3835 
   3836 	return -1;
   3837 }
   3838 
   3839 static void resetdircolor(int flags)
   3840 {
   3841 	/* Directories are always shown on top, clear the color when moving to first file */
   3842 	if (g_state.dircolor && !(flags & DIR_OR_DIRLNK)) {
   3843 		attroff(COLOR_PAIR(cfg.curctx + 1) | A_BOLD);
   3844 		g_state.dircolor = 0;
   3845 	}
   3846 }
   3847 
   3848 /*
   3849  * Replace escape characters in a string with '?'
   3850  * Adjust string length to maxcols if > 0;
   3851  * Max supported str length: NAME_MAX;
   3852  */
   3853 #ifdef NOLC
   3854 static char *unescape(const char *str, uint_t maxcols)
   3855 {
   3856 	char * const wbuf = g_buf;
   3857 	char *buf = wbuf;
   3858 
   3859 	xstrsncpy(wbuf, str, maxcols);
   3860 #else
   3861 static wchar_t *unescape(const char *str, uint_t maxcols)
   3862 {
   3863 	wchar_t * const wbuf = (wchar_t *)g_buf;
   3864 	wchar_t *buf = wbuf;
   3865 	size_t len = mbstowcs(wbuf, str, maxcols); /* Convert multi-byte to wide char */
   3866 
   3867 	len = wcswidth(wbuf, len);
   3868 
   3869 	if (len >= maxcols) {
   3870 		size_t lencount = maxcols;
   3871 
   3872 		while (len > maxcols) /* Reduce wide chars one by one till it fits */
   3873 			len = wcswidth(wbuf, --lencount);
   3874 
   3875 		wbuf[lencount] = L'\0';
   3876 	}
   3877 #endif
   3878 
   3879 	while (*buf) {
   3880 		if (*buf <= '\x1f' || *buf == '\x7f')
   3881 			*buf = '\?';
   3882 
   3883 		++buf;
   3884 	}
   3885 
   3886 	return wbuf;
   3887 }
   3888 
   3889 static off_t get_size(off_t size, off_t *pval, int comp)
   3890 {
   3891 	off_t rem = *pval;
   3892 	off_t quo = rem / 10;
   3893 
   3894 	if ((rem - (quo * 10)) >= 5) {
   3895 		rem = quo + 1;
   3896 		if (rem == comp) {
   3897 			++size;
   3898 			rem = 0;
   3899 		}
   3900 	} else
   3901 		rem = quo;
   3902 
   3903 	*pval = rem;
   3904 	return size;
   3905 }
   3906 
   3907 static char *coolsize(off_t size)
   3908 {
   3909 	const char * const U = "BKMGTPEZY";
   3910 	static char size_buf[12]; /* Buffer to hold human readable size */
   3911 	off_t rem = 0;
   3912 	size_t ret;
   3913 	int i = 0;
   3914 
   3915 	while (size >= 1024) {
   3916 		rem = size & (0x3FF); /* 1024 - 1 = 0x3FF */
   3917 		size >>= 10;
   3918 		++i;
   3919 	}
   3920 
   3921 	if (i == 1) {
   3922 		rem = (rem * 1000) >> 10;
   3923 		rem /= 10;
   3924 		size = get_size(size, &rem, 10);
   3925 	} else if (i == 2) {
   3926 		rem = (rem * 1000) >> 10;
   3927 		size = get_size(size, &rem, 100);
   3928 	} else if (i > 2) {
   3929 		rem = (rem * 10000) >> 10;
   3930 		size = get_size(size, &rem, 1000);
   3931 	}
   3932 
   3933 	if (i > 0 && i < 6 && rem) {
   3934 		ret = xstrsncpy(size_buf, xitoa(size), 12);
   3935 		size_buf[ret - 1] = '.';
   3936 
   3937 		char *frac = xitoa(rem);
   3938 		size_t toprint = i > 3 ? 3 : i;
   3939 		size_t len = xstrlen(frac);
   3940 
   3941 		if (len < toprint) {
   3942 			size_buf[ret] = size_buf[ret + 1] = size_buf[ret + 2] = '0';
   3943 			xstrsncpy(size_buf + ret + (toprint - len), frac, len + 1);
   3944 		} else
   3945 			xstrsncpy(size_buf + ret, frac, toprint + 1);
   3946 
   3947 		ret += toprint;
   3948 	} else {
   3949 		ret = xstrsncpy(size_buf, size ? xitoa(size) : "0", 12);
   3950 		--ret;
   3951 	}
   3952 
   3953 	size_buf[ret] = U[i];
   3954 	size_buf[ret + 1] = '\0';
   3955 
   3956 	return size_buf;
   3957 }
   3958 
   3959 /* Convert a mode field into "ls -l" type perms field. */
   3960 static char *get_lsperms(mode_t mode)
   3961 {
   3962 	static const char * const rwx[] = {"---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx"};
   3963 	static char bits[11] = {'\0'};
   3964 
   3965 	switch (mode & S_IFMT) {
   3966 	case S_IFREG:
   3967 		bits[0] = '-';
   3968 		break;
   3969 	case S_IFDIR:
   3970 		bits[0] = 'd';
   3971 		break;
   3972 	case S_IFLNK:
   3973 		bits[0] = 'l';
   3974 		break;
   3975 	case S_IFSOCK:
   3976 		bits[0] = 's';
   3977 		break;
   3978 	case S_IFIFO:
   3979 		bits[0] = 'p';
   3980 		break;
   3981 	case S_IFBLK:
   3982 		bits[0] = 'b';
   3983 		break;
   3984 	case S_IFCHR:
   3985 		bits[0] = 'c';
   3986 		break;
   3987 	default:
   3988 		bits[0] = '?';
   3989 		break;
   3990 	}
   3991 
   3992 	xstrsncpy(&bits[1], rwx[(mode >> 6) & 7], 4);
   3993 	xstrsncpy(&bits[4], rwx[(mode >> 3) & 7], 4);
   3994 	xstrsncpy(&bits[7], rwx[(mode & 7)], 4);
   3995 
   3996 	if (mode & S_ISUID)
   3997 		bits[3] = (mode & 0100) ? 's' : 'S';  /* user executable */
   3998 	if (mode & S_ISGID)
   3999 		bits[6] = (mode & 0010) ? 's' : 'l';  /* group executable */
   4000 	if (mode & S_ISVTX)
   4001 		bits[9] = (mode & 0001) ? 't' : 'T';  /* others executable */
   4002 
   4003 	return bits;
   4004 }
   4005 
   4006 #ifdef ICONS_ENABLED
   4007 static const struct icon_pair *get_icon(const struct entry *ent)
   4008 {
   4009 	ushort_t i = 0;
   4010 
   4011 	for (; i < sizeof(icons_name)/sizeof(struct icon_pair); ++i)
   4012 		if (strcasecmp(ent->name, icons_name[i].match) == 0)
   4013 			return &icons_name[i];
   4014 
   4015 	if (ent->flags & DIR_OR_DIRLNK)
   4016 		return &dir_icon;
   4017 
   4018 	char *tmp = xextension(ent->name, ent->nlen);
   4019 
   4020 	if (!tmp) {
   4021 		if (ent->mode & 0100)
   4022 			return &exec_icon;
   4023 
   4024 		return &file_icon;
   4025 	}
   4026 
   4027 	/* Skip the . */
   4028 	++tmp;
   4029 
   4030 	if (*tmp >= '0' && *tmp <= '9')
   4031 		i = *tmp - '0'; /* NUMBER 0-9 */
   4032 	else if (TOUPPER(*tmp) >= 'A' && TOUPPER(*tmp) <= 'Z')
   4033 		i = TOUPPER(*tmp) - 'A' + 10; /* LETTER A-Z */
   4034 	else
   4035 		i = 36; /* OTHER */
   4036 
   4037 	for (ushort_t j = icon_positions[i]; j < sizeof(icons_ext)/sizeof(struct icon_pair) &&
   4038 			icons_ext[j].match[0] == icons_ext[icon_positions[i]].match[0]; ++j)
   4039 		if (strcasecmp(tmp, icons_ext[j].match) == 0)
   4040 			return &icons_ext[j];
   4041 
   4042 	/* If there's no match and the file is executable, icon that */
   4043 	if (ent->mode & 0100)
   4044 		return &exec_icon;
   4045 
   4046 	return &file_icon;
   4047 }
   4048 
   4049 static void print_icon(const struct entry *ent, const int attrs)
   4050 {
   4051 	const struct icon_pair *picon = get_icon(ent);
   4052 
   4053 	addstr(ICON_PADDING_LEFT);
   4054 	if (picon->color)
   4055 		attron(COLOR_PAIR(C_UND + 1 + picon->color));
   4056 	else if (attrs)
   4057 		attron(attrs);
   4058 	addstr(picon->icon);
   4059 	if (picon->color)
   4060 		attroff(COLOR_PAIR(C_UND + 1 + picon->color));
   4061 	else if (attrs)
   4062 		attroff(attrs);
   4063 	addstr(ICON_PADDING_RIGHT);
   4064 }
   4065 #endif
   4066 
   4067 static void print_time(const time_t *timep)
   4068 {
   4069 	struct tm t;
   4070 
   4071 	localtime_r(timep, &t);
   4072 	printw("%s-%02d-%02d %02d:%02d",
   4073 		xitoa(t.tm_year + 1900), t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min);
   4074 }
   4075 
   4076 static char get_detail_ind(const mode_t mode)
   4077 {
   4078 	switch (mode & S_IFMT) {
   4079 	case S_IFDIR:  // fallthrough
   4080 	case S_IFREG:  return ' ';
   4081 	case S_IFLNK:  return '@';
   4082 	case S_IFSOCK: return '=';
   4083 	case S_IFIFO:  return '|';
   4084 	case S_IFBLK:  return 'b';
   4085 	case S_IFCHR:  return 'c';
   4086 	}
   4087 	return '?';
   4088 }
   4089 
   4090 /* Note: attribute and indicator values must be initialized to 0 */
   4091 static uchar_t get_color_pair_name_ind(const struct entry *ent, char *pind, int *pattr)
   4092 {
   4093 	switch (ent->mode & S_IFMT) {
   4094 	case S_IFREG:
   4095 		if (!ent->size) {
   4096 			if (ent->mode & 0100)
   4097 				*pind = '*';
   4098 			return C_UND;
   4099 		}
   4100 		if (ent->flags & HARD_LINK) {
   4101 			if (ent->mode & 0100)
   4102 				*pind = '*';
   4103 			return C_HRD;
   4104 		}
   4105 		if (ent->mode & 0100) {
   4106 			*pind = '*';
   4107 			return C_EXE;
   4108 		}
   4109 		return C_FIL;
   4110 	case S_IFDIR:
   4111 		*pind = '/';
   4112 		if (g_state.oldcolor)
   4113 			return C_DIR;
   4114 		*pattr |= A_BOLD;
   4115 		return g_state.dirctx ? cfg.curctx + 1 : C_DIR;
   4116 	case S_IFLNK:
   4117 		if (ent->flags & DIR_OR_DIRLNK) {
   4118 			*pind = '/';
   4119 			*pattr |= g_state.oldcolor ? A_DIM : A_BOLD;
   4120 		} else {
   4121 			*pind = '@';
   4122 			if (g_state.oldcolor)
   4123 				*pattr |= A_DIM;
   4124 		}
   4125 		if (!g_state.oldcolor || cfg.showdetail)
   4126 			return (ent->flags & SYM_ORPHAN) ? C_ORP : C_LNK;
   4127 		return 0;
   4128 	case S_IFSOCK:
   4129 		*pind = '=';
   4130 		return C_SOC;
   4131 	case S_IFIFO:
   4132 		*pind = '|';
   4133 		return C_PIP;
   4134 	case S_IFBLK:
   4135 		return C_BLK;
   4136 	case S_IFCHR:
   4137 		return C_CHR;
   4138 	}
   4139 
   4140 	*pind = '?';
   4141 	return C_UND;
   4142 }
   4143 
   4144 static void printent(const struct entry *ent, uint_t namecols, bool sel)
   4145 {
   4146 	char ind = '\0';
   4147 	int attrs;
   4148 
   4149 	if (cfg.showdetail) {
   4150 		int type = ent->mode & S_IFMT;
   4151 		char perms[6] = {' ', ' ', (char)('0' + ((ent->mode >> 6) & 7)),
   4152 				(char)('0' + ((ent->mode >> 3) & 7)),
   4153 				(char)('0' + (ent->mode & 7)), '\0'};
   4154 
   4155 		addch(' ');
   4156 		attrs = g_state.oldcolor ? (resetdircolor(ent->flags), A_DIM)
   4157 					 : (fcolors[C_MIS] ? COLOR_PAIR(C_MIS) : 0);
   4158 		if (attrs)
   4159 			attron(attrs);
   4160 
   4161 		/* Print details */
   4162 		print_time(&ent->sec);
   4163 
   4164 		printw("%s%9s ", perms, (type == S_IFREG || type == S_IFDIR)
   4165 			? coolsize(cfg.blkorder ? (blkcnt_t)ent->blocks << blk_shift : ent->size)
   4166 			: (type = (uchar_t)get_detail_ind(ent->mode), (char *)&type));
   4167 
   4168 		if (attrs)
   4169 			attroff(attrs);
   4170 	}
   4171 
   4172 	attrs = 0;
   4173 
   4174 	uchar_t color_pair = get_color_pair_name_ind(ent, &ind, &attrs);
   4175 
   4176 	addch((ent->flags & FILE_SELECTED) ? '+' | A_REVERSE | A_BOLD : ' ');
   4177 
   4178 	if (g_state.oldcolor)
   4179 		resetdircolor(ent->flags);
   4180 	else {
   4181 		if (ent->flags & FILE_MISSING)
   4182 			color_pair = C_MIS;
   4183 		if (color_pair && fcolors[color_pair])
   4184 			attrs |= COLOR_PAIR(color_pair);
   4185 #ifdef ICONS_ENABLED
   4186 		print_icon(ent, attrs);
   4187 #endif
   4188 	}
   4189 
   4190 	if (sel)
   4191 		attrs |= A_REVERSE;
   4192 	if (attrs)
   4193 		attron(attrs);
   4194 	if (!ind)
   4195 		++namecols;
   4196 
   4197 #ifndef NOLC
   4198 	addwstr(unescape(ent->name, namecols));
   4199 #else
   4200 	addstr(unescape(ent->name, MIN(namecols, ent->nlen) + 1));
   4201 #endif
   4202 
   4203 	if (attrs)
   4204 		attroff(attrs);
   4205 	if (ind)
   4206 		addch(ind);
   4207 }
   4208 
   4209 static void savecurctx(char *path, char *curname, int nextctx)
   4210 {
   4211 	settings tmpcfg = cfg;
   4212 	context *ctxr = &g_ctx[nextctx];
   4213 
   4214 	/* Save current context */
   4215 	if (curname)
   4216 		xstrsncpy(g_ctx[tmpcfg.curctx].c_name, curname, NAME_MAX + 1);
   4217 	else
   4218 		g_ctx[tmpcfg.curctx].c_name[0] = '\0';
   4219 
   4220 	g_ctx[tmpcfg.curctx].c_cfg = tmpcfg;
   4221 
   4222 	if (ctxr->c_cfg.ctxactive) { /* Switch to saved context */
   4223 		tmpcfg = ctxr->c_cfg;
   4224 		/* Skip ordering an open context */
   4225 		if (order) {
   4226 			cfgsort[CTX_MAX] = cfgsort[nextctx];
   4227 			cfgsort[nextctx] = '0';
   4228 		}
   4229 	} else { /* Set up a new context from current context */
   4230 		ctxr->c_cfg.ctxactive = 1;
   4231 		xstrsncpy(ctxr->c_path, path, PATH_MAX);
   4232 		ctxr->c_last[0] = ctxr->c_name[0] = ctxr->c_fltr[0] = ctxr->c_fltr[1] = '\0';
   4233 		ctxr->c_cfg = tmpcfg;
   4234 		/* If already in an ordered dir, clear ordering for the new context and let it order */
   4235 		if (cfgsort[cfg.curctx] == 'z')
   4236 			cfgsort[nextctx] = 'z';
   4237 	}
   4238 
   4239 	tmpcfg.curctx = nextctx;
   4240 	cfg = tmpcfg;
   4241 }
   4242 
   4243 #ifndef NOSSN
   4244 static void save_session(const char *sname, int *presel)
   4245 {
   4246 	int fd, i;
   4247 	session_header_t header;
   4248 	bool status = FALSE;
   4249 	char ssnpath[PATH_MAX];
   4250 	char spath[PATH_MAX];
   4251 
   4252 	memset(&header, 0, sizeof(session_header_t));
   4253 
   4254 	header.ver = SESSIONS_VERSION;
   4255 
   4256 	for (i = 0; i < CTX_MAX; ++i) {
   4257 		if (g_ctx[i].c_cfg.ctxactive) {
   4258 			if (cfg.curctx == i && ndents)
   4259 				/* Update current file name, arrows don't update it */
   4260 				xstrsncpy(g_ctx[i].c_name, pdents[cur].name, NAME_MAX + 1);
   4261 			header.pathln[i] = strnlen(g_ctx[i].c_path, PATH_MAX) + 1;
   4262 			header.lastln[i] = strnlen(g_ctx[i].c_last, PATH_MAX) + 1;
   4263 			header.nameln[i] = strnlen(g_ctx[i].c_name, NAME_MAX) + 1;
   4264 			header.fltrln[i] = REGEX_MAX;
   4265 		}
   4266 	}
   4267 
   4268 	mkpath(cfgpath, toks[TOK_SSN], ssnpath);
   4269 	mkpath(ssnpath, sname, spath);
   4270 
   4271 	fd = open(spath, O_CREAT | O_WRONLY | O_TRUNC, 0666);
   4272 	if (fd == -1) {
   4273 		printwait(messages[MSG_SEL_MISSING], presel);
   4274 		return;
   4275 	}
   4276 
   4277 	if ((write(fd, &header, sizeof(header)) != (ssize_t)sizeof(header))
   4278 		|| (write(fd, &cfg, sizeof(cfg)) != (ssize_t)sizeof(cfg)))
   4279 		goto END;
   4280 
   4281 	for (i = 0; i < CTX_MAX; ++i)
   4282 		if ((write(fd, &g_ctx[i].c_cfg, sizeof(settings)) != (ssize_t)sizeof(settings))
   4283 			|| (write(fd, &g_ctx[i].color, sizeof(uint_t)) != (ssize_t)sizeof(uint_t))
   4284 			|| (header.nameln[i] > 0
   4285 			    && write(fd, g_ctx[i].c_name, header.nameln[i]) != (ssize_t)header.nameln[i])
   4286 			|| (header.lastln[i] > 0
   4287 			    && write(fd, g_ctx[i].c_last, header.lastln[i]) != (ssize_t)header.lastln[i])
   4288 			|| (header.fltrln[i] > 0
   4289 			    && write(fd, g_ctx[i].c_fltr, header.fltrln[i]) != (ssize_t)header.fltrln[i])
   4290 			|| (header.pathln[i] > 0
   4291 			    && write(fd, g_ctx[i].c_path, header.pathln[i]) != (ssize_t)header.pathln[i]))
   4292 			goto END;
   4293 
   4294 	status = TRUE;
   4295 
   4296 END:
   4297 	close(fd);
   4298 
   4299 	if (!status)
   4300 		printwait(messages[MSG_FAILED], presel);
   4301 }
   4302 
   4303 static bool load_session(const char *sname, char **path, char **lastdir, char **lastname, bool restore)
   4304 {
   4305 	int fd, i = 0;
   4306 	session_header_t header;
   4307 	bool has_loaded_dynamically = !(sname || restore);
   4308 	bool status = (sname && g_state.picker); /* Picker mode with session program option */
   4309 	char ssnpath[PATH_MAX];
   4310 	char spath[PATH_MAX];
   4311 
   4312 	mkpath(cfgpath, toks[TOK_SSN], ssnpath);
   4313 
   4314 	if (!restore) {
   4315 		sname = sname ? sname : xreadline(NULL, messages[MSG_SSN_NAME]);
   4316 		if (!sname[0])
   4317 			return FALSE;
   4318 
   4319 		mkpath(ssnpath, sname, spath);
   4320 
   4321 		/* If user is explicitly loading the "last session", skip auto-save */
   4322 		if ((sname[0] == '@') && !sname[1])
   4323 			has_loaded_dynamically = FALSE;
   4324 	} else
   4325 		mkpath(ssnpath, "@", spath);
   4326 
   4327 	if (has_loaded_dynamically)
   4328 		save_session("@", NULL);
   4329 
   4330 	fd = open(spath, O_RDONLY, 0666);
   4331 	if (fd == -1) {
   4332 		if (!status) {
   4333 			printmsg(messages[MSG_SEL_MISSING]);
   4334 			xdelay(XDELAY_INTERVAL_MS);
   4335 		}
   4336 		return FALSE;
   4337 	}
   4338 
   4339 	status = FALSE;
   4340 
   4341 	if ((read(fd, &header, sizeof(header)) != (ssize_t)sizeof(header))
   4342 		|| (header.ver != SESSIONS_VERSION)
   4343 		|| (read(fd, &cfg, sizeof(cfg)) != (ssize_t)sizeof(cfg)))
   4344 		goto END;
   4345 
   4346 	g_ctx[cfg.curctx].c_name[0] = g_ctx[cfg.curctx].c_last[0]
   4347 		= g_ctx[cfg.curctx].c_fltr[0] = g_ctx[cfg.curctx].c_fltr[1] = '\0';
   4348 
   4349 	for (; i < CTX_MAX; ++i)
   4350 		if ((read(fd, &g_ctx[i].c_cfg, sizeof(settings)) != (ssize_t)sizeof(settings))
   4351 			|| (read(fd, &g_ctx[i].color, sizeof(uint_t)) != (ssize_t)sizeof(uint_t))
   4352 			|| (header.nameln[i] > 0
   4353 			    && read(fd, g_ctx[i].c_name, header.nameln[i]) != (ssize_t)header.nameln[i])
   4354 			|| (header.lastln[i] > 0
   4355 			    && read(fd, g_ctx[i].c_last, header.lastln[i]) != (ssize_t)header.lastln[i])
   4356 			|| (header.fltrln[i] > 0
   4357 			    && read(fd, g_ctx[i].c_fltr, header.fltrln[i]) != (ssize_t)header.fltrln[i])
   4358 			|| (header.pathln[i] > 0
   4359 			    && read(fd, g_ctx[i].c_path, header.pathln[i]) != (ssize_t)header.pathln[i]))
   4360 			goto END;
   4361 
   4362 	*path = g_ctx[cfg.curctx].c_path;
   4363 	*lastdir = g_ctx[cfg.curctx].c_last;
   4364 	*lastname = g_ctx[cfg.curctx].c_name;
   4365 	set_sort_flags('\0'); /* Set correct sort options */
   4366 	status = TRUE;
   4367 
   4368 END:
   4369 	close(fd);
   4370 
   4371 	if (!status) {
   4372 		printmsg(messages[MSG_FAILED]);
   4373 		xdelay(XDELAY_INTERVAL_MS);
   4374 	} else if (restore)
   4375 		unlink(spath);
   4376 
   4377 	return status;
   4378 }
   4379 #endif
   4380 
   4381 static uchar_t get_free_ctx(void)
   4382 {
   4383 	uchar_t r = cfg.curctx;
   4384 
   4385 	do
   4386 		r = (r + 1) & ~CTX_MAX;
   4387 	while (g_ctx[r].c_cfg.ctxactive && (r != cfg.curctx));
   4388 
   4389 	return r;
   4390 }
   4391 
   4392 /* ctx is absolute: 1 to 4, + for smart context */
   4393 static void set_smart_ctx(int ctx, char *nextpath, char **path, char *file, char **lastname, char **lastdir)
   4394 {
   4395 	if (ctx == '+') /* Get smart context */
   4396 		ctx = (int)(get_free_ctx() + 1);
   4397 
   4398 	if (ctx == 0 || ctx == cfg.curctx + 1) { /* Same context */
   4399 		xstrsncpy(*lastdir, *path, PATH_MAX);
   4400 		xstrsncpy(*path, nextpath, PATH_MAX);
   4401 	} else { /* New context */
   4402 		--ctx;
   4403 		/* Deactivate the new context and build from scratch */
   4404 		g_ctx[ctx].c_cfg.ctxactive = 0;
   4405 		DPRINTF_S(nextpath);
   4406 		savecurctx(nextpath, file, ctx);
   4407 		*path = g_ctx[ctx].c_path;
   4408 		*lastdir = g_ctx[ctx].c_last;
   4409 		*lastname = g_ctx[ctx].c_name;
   4410 	}
   4411 }
   4412 
   4413 /*
   4414  * Gets only a single line (that's what we need for now) or shows full command output in pager.
   4415  * Uses g_buf internally.
   4416  */
   4417 static bool get_output(char *file, char *arg1, char *arg2, int fdout, bool multi, bool page)
   4418 {
   4419 	pid_t pid;
   4420 	int pipefd[2];
   4421 	int index = 0, flags;
   4422 	bool ret = FALSE;
   4423 	bool tmpfile = ((fdout == -1) && page);
   4424 	char *argv[EXEC_ARGS_MAX] = {0};
   4425 	char *cmd = NULL;
   4426 	int fd = -1;
   4427 	ssize_t len;
   4428 
   4429 	if (tmpfile) {
   4430 		fdout = create_tmp_file();
   4431 		if (fdout == -1)
   4432 			return FALSE;
   4433 	}
   4434 
   4435 	if (multi) {
   4436 		cmd = parseargs(file, argv, &index);
   4437 		if (!cmd)
   4438 			return FALSE;
   4439 	} else
   4440 		argv[index++] = file;
   4441 
   4442 	argv[index] = arg1;
   4443 	argv[++index] = arg2;
   4444 
   4445 	if (pipe(pipefd) == -1) {
   4446 		free(cmd);
   4447 		errexit();
   4448 	}
   4449 
   4450 	for (index = 0; index < 2; ++index) {
   4451 		/* Get previous flags */
   4452 		flags = fcntl(pipefd[index], F_GETFL, 0);
   4453 
   4454 		/* Set bit for non-blocking flag */
   4455 		flags |= O_NONBLOCK;
   4456 
   4457 		/* Change flags on fd */
   4458 		fcntl(pipefd[index], F_SETFL, flags);
   4459 	}
   4460 
   4461 	pid = fork();
   4462 	if (pid == 0) {
   4463 		/* In child */
   4464 		close(pipefd[0]);
   4465 		dup2(pipefd[1], STDOUT_FILENO);
   4466 		dup2(pipefd[1], STDERR_FILENO);
   4467 		close(pipefd[1]);
   4468 		execvp(*argv, argv);
   4469 		_exit(EXIT_SUCCESS);
   4470 	}
   4471 
   4472 	/* In parent */
   4473 	waitpid(pid, NULL, 0);
   4474 	close(pipefd[1]);
   4475 	free(cmd);
   4476 
   4477 	while ((len = read(pipefd[0], g_buf, CMD_LEN_MAX - 1)) > 0) {
   4478 		ret = TRUE;
   4479 		if (fdout == -1) /* Read only the first line of output to buffer */
   4480 			break;
   4481 		if (write(fdout, g_buf, len) != len)
   4482 			break;
   4483 	}
   4484 
   4485 	close(pipefd[0]);
   4486 	if (!page)
   4487 		return ret;
   4488 
   4489 	if (tmpfile) {
   4490 		close(fdout);
   4491 		close(fd);
   4492 	}
   4493 
   4494 	spawn(pager, g_tmpfpath, NULL, NULL, F_CLI | F_TTY);
   4495 
   4496 	if (tmpfile)
   4497 		unlink(g_tmpfpath);
   4498 
   4499 	return TRUE;
   4500 }
   4501 
   4502 /*
   4503  * Follows the stat(1) output closely
   4504  */
   4505 static bool show_stats(char *fpath)
   4506 {
   4507 	static char * const cmds[] = {
   4508 #ifdef FILE_MIME_OPTS
   4509 		("file " FILE_MIME_OPTS),
   4510 #endif
   4511 		"file -b",
   4512 		"stat",
   4513 	};
   4514 
   4515 	size_t r = ELEMENTS(cmds);
   4516 	int fd = create_tmp_file();
   4517 	if (fd == -1)
   4518 		return FALSE;
   4519 
   4520 	while (r)
   4521 		get_output(cmds[--r], fpath, NULL, fd, TRUE, FALSE);
   4522 
   4523 	close(fd);
   4524 
   4525 	spawn(pager, g_tmpfpath, NULL, NULL, F_CLI | F_TTY);
   4526 	unlink(g_tmpfpath);
   4527 	return TRUE;
   4528 }
   4529 
   4530 static bool xchmod(const char *fpath, mode_t mode)
   4531 {
   4532 	/* (Un)set (S_IXUSR | S_IXGRP | S_IXOTH) */
   4533 	(0100 & mode) ? (mode &= ~0111) : (mode |= 0111);
   4534 
   4535 	return (chmod(fpath, mode) == 0);
   4536 }
   4537 
   4538 static size_t get_fs_info(const char *path, bool type)
   4539 {
   4540 	struct statvfs svb;
   4541 
   4542 	if (statvfs(path, &svb) == -1)
   4543 		return 0;
   4544 
   4545 	if (type == CAPACITY)
   4546 		return (size_t)svb.f_blocks << ffs((int)(svb.f_frsize >> 1));
   4547 
   4548 	return (size_t)svb.f_bavail << ffs((int)(svb.f_frsize >> 1));
   4549 }
   4550 
   4551 /* Create non-existent parents and a file or dir */
   4552 static bool xmktree(char *path, bool dir)
   4553 {
   4554 	char *p = path;
   4555 	char *slash = path;
   4556 
   4557 	if (!p || !*p)
   4558 		return FALSE;
   4559 
   4560 	/* Skip the first '/' */
   4561 	++p;
   4562 
   4563 	while (*p != '\0') {
   4564 		if (*p == '/') {
   4565 			slash = p;
   4566 			*p = '\0';
   4567 		} else {
   4568 			++p;
   4569 			continue;
   4570 		}
   4571 
   4572 		/* Create folder from path to '\0' inserted at p */
   4573 		if (mkdir(path, 0777) == -1 && errno != EEXIST) {
   4574 #ifdef __HAIKU__
   4575 			// XDG_CONFIG_HOME contains a directory
   4576 			// that is read-only, but the full path
   4577 			// is writeable.
   4578 			// Try to continue and see what happens.
   4579 			// TODO: Find a more robust solution.
   4580 			if (errno == B_READ_ONLY_DEVICE)
   4581 				goto next;
   4582 #endif
   4583 			DPRINTF_S("mkdir1!");
   4584 			DPRINTF_S(strerror(errno));
   4585 			*slash = '/';
   4586 			return FALSE;
   4587 		}
   4588 
   4589 #ifdef __HAIKU__
   4590 next:
   4591 #endif
   4592 		/* Restore path */
   4593 		*slash = '/';
   4594 		++p;
   4595 	}
   4596 
   4597 	if (dir) {
   4598 		if (mkdir(path, 0777) == -1 && errno != EEXIST) {
   4599 			DPRINTF_S("mkdir2!");
   4600 			DPRINTF_S(strerror(errno));
   4601 			return FALSE;
   4602 		}
   4603 	} else {
   4604 		int fd = open(path, O_CREAT, 0666);
   4605 
   4606 		if (fd == -1 && errno != EEXIST) {
   4607 			DPRINTF_S("open!");
   4608 			DPRINTF_S(strerror(errno));
   4609 			return FALSE;
   4610 		}
   4611 
   4612 		close(fd);
   4613 	}
   4614 
   4615 	return TRUE;
   4616 }
   4617 
   4618 /* List or extract archive */
   4619 static bool handle_archive(char *fpath /* in-out param */, char op)
   4620 {
   4621 	char arg[] = "-tvf"; /* options for tar/bsdtar to list files */
   4622 	char *util, *outdir = NULL;
   4623 	bool x_to = FALSE;
   4624 	bool is_atool = getutil(utils[UTIL_ATOOL]);
   4625 
   4626 	if (op == 'x') {
   4627 		outdir = xreadline(is_atool ? "." : xbasename(fpath), messages[MSG_NEW_PATH]);
   4628 		if (!outdir || !*outdir) { /* Cancelled */
   4629 			printwait(messages[MSG_CANCEL], NULL);
   4630 			return FALSE;
   4631 		}
   4632 		/* Do not create smart context for current dir */
   4633 		if (!(*outdir == '.' && outdir[1] == '\0')) {
   4634 			if (!xmktree(outdir, TRUE) || (chdir(outdir) == -1)) {
   4635 				printwarn(NULL);
   4636 				return FALSE;
   4637 			}
   4638 			/* Copy the new dir path to open it in smart context */
   4639 			outdir = getcwd(NULL, 0);
   4640 			x_to = TRUE;
   4641 		}
   4642 	}
   4643 
   4644 	if (is_atool) {
   4645 		util = utils[UTIL_ATOOL];
   4646 		arg[1] = op;
   4647 		arg[2] = '\0';
   4648 	} else if (getutil(utils[UTIL_BSDTAR])) {
   4649 		util = utils[UTIL_BSDTAR];
   4650 		if (op == 'x')
   4651 			arg[1] = op;
   4652 	} else if (is_suffix(fpath, ".zip")) {
   4653 		util = utils[UTIL_UNZIP];
   4654 		arg[1] = (op == 'l') ? 'v' /* verbose listing */ : '\0';
   4655 		arg[2] = '\0';
   4656 	} else {
   4657 		util = utils[UTIL_TAR];
   4658 		if (op == 'x')
   4659 			arg[1] = op;
   4660 	}
   4661 
   4662 	if (op == 'x') /* extract */
   4663 		spawn(util, arg, fpath, NULL, F_NORMAL | F_MULTI);
   4664 	else /* list */
   4665 		get_output(util, arg, fpath, -1, TRUE, TRUE);
   4666 
   4667 	if (x_to) {
   4668 		if (chdir(xdirname(fpath)) == -1) {
   4669 			printwarn(NULL);
   4670 			free(outdir);
   4671 			return FALSE;
   4672 		}
   4673 		xstrsncpy(fpath, outdir, PATH_MAX);
   4674 		free(outdir);
   4675 	} else if (op == 'x')
   4676 		fpath[0] = '\0';
   4677 
   4678 	return TRUE;
   4679 }
   4680 
   4681 static char *visit_parent(char *path, char *newpath, int *presel)
   4682 {
   4683 	char *dir;
   4684 
   4685 	/* There is no going back */
   4686 	if (istopdir(path)) {
   4687 		/* Continue in type-to-nav mode, if enabled */
   4688 		if (cfg.filtermode && presel)
   4689 			*presel = FILTER;
   4690 		return NULL;
   4691 	}
   4692 
   4693 	/* Use a copy as xdirname() may change the string passed */
   4694 	if (newpath)
   4695 		xstrsncpy(newpath, path, PATH_MAX);
   4696 	else
   4697 		newpath = path;
   4698 
   4699 	dir = xdirname(newpath);
   4700 	if (chdir(dir) == -1) {
   4701 		printwarn(presel);
   4702 		return NULL;
   4703 	}
   4704 
   4705 	return dir;
   4706 }
   4707 
   4708 static void valid_parent(char *path, char *lastname)
   4709 {
   4710 	/* Save history */
   4711 	xstrsncpy(lastname, xbasename(path), NAME_MAX + 1);
   4712 
   4713 	while (!istopdir(path))
   4714 		if (visit_parent(path, NULL, NULL))
   4715 			break;
   4716 
   4717 	printwarn(NULL);
   4718 	xdelay(XDELAY_INTERVAL_MS);
   4719 }
   4720 
   4721 static bool archive_mount(char *newpath)
   4722 {
   4723 	char *str = "install archivemount";
   4724 	char *dir, *cmd = str + 8; /* Start of "archivemount" */
   4725 	char *name = pdents[cur].name;
   4726 	size_t len = pdents[cur].nlen;
   4727 	char mntpath[PATH_MAX];
   4728 
   4729 	if (!getutil(cmd)) {
   4730 		printmsg(str);
   4731 		return FALSE;
   4732 	}
   4733 
   4734 	dir = xstrdup(name);
   4735 	if (!dir) {
   4736 		printmsg(messages[MSG_FAILED]);
   4737 		return FALSE;
   4738 	}
   4739 
   4740 	while (len > 1)
   4741 		if (dir[--len] == '.') {
   4742 			dir[len] = '\0';
   4743 			break;
   4744 		}
   4745 
   4746 	DPRINTF_S(dir);
   4747 
   4748 	/* Create the mount point */
   4749 	mkpath(cfgpath, toks[TOK_MNT], mntpath);
   4750 	mkpath(mntpath, dir, newpath);
   4751 	free(dir);
   4752 
   4753 	if (!xmktree(newpath, TRUE)) {
   4754 		printwarn(NULL);
   4755 		return FALSE;
   4756 	}
   4757 
   4758 	/* Mount archive */
   4759 	DPRINTF_S(name);
   4760 	DPRINTF_S(newpath);
   4761 	if (spawn(cmd, name, newpath, NULL, F_NORMAL)) {
   4762 		printmsg(messages[MSG_FAILED]);
   4763 		return FALSE;
   4764 	}
   4765 
   4766 	return TRUE;
   4767 }
   4768 
   4769 static bool remote_mount(char *newpath)
   4770 {
   4771 	uchar_t flag = F_CLI;
   4772 	int opt;
   4773 	char *tmp, *env;
   4774 	bool r = getutil(utils[UTIL_RCLONE]), s = getutil(utils[UTIL_SSHFS]);
   4775 	char mntpath[PATH_MAX];
   4776 
   4777 	if (!(r || s)) {
   4778 		printmsg("install sshfs/rclone");
   4779 		return FALSE;
   4780 	}
   4781 
   4782 	if (r && s)
   4783 		opt = get_input(messages[MSG_REMOTE_OPTS]);
   4784 	else
   4785 		opt = (!s) ? 'r' : 's';
   4786 
   4787 	if (opt == 's')
   4788 		env = xgetenv("NNN_SSHFS", utils[UTIL_SSHFS]);
   4789 	else if (opt == 'r') {
   4790 		flag |= F_NOWAIT | F_NOTRACE;
   4791 		env = xgetenv("NNN_RCLONE", "rclone mount");
   4792 	} else {
   4793 		printmsg(messages[MSG_INVALID_KEY]);
   4794 		return FALSE;
   4795 	}
   4796 
   4797 	tmp = xreadline(NULL, "host[:dir] > ");
   4798 	if (!tmp[0]) {
   4799 		printmsg(messages[MSG_CANCEL]);
   4800 		return FALSE;
   4801 	}
   4802 
   4803 	char *div = strchr(tmp, ':');
   4804 
   4805 	if (div)
   4806 		*div = '\0';
   4807 
   4808 	/* Create the mount point */
   4809 	mkpath(cfgpath, toks[TOK_MNT], mntpath);
   4810 	mkpath(mntpath, tmp, newpath);
   4811 	if (!xmktree(newpath, TRUE)) {
   4812 		printwarn(NULL);
   4813 		return FALSE;
   4814 	}
   4815 
   4816 	if (!div) { /* Convert "host" to "host:" */
   4817 		size_t len = xstrlen(tmp);
   4818 
   4819 		tmp[len] = ':';
   4820 		tmp[len + 1] = '\0';
   4821 	} else
   4822 		*div = ':';
   4823 
   4824 	/* Connect to remote */
   4825 	if (opt == 's') {
   4826 		if (spawn(env, tmp, newpath, NULL, flag)) {
   4827 			printmsg(messages[MSG_FAILED]);
   4828 			return FALSE;
   4829 		}
   4830 	} else {
   4831 		spawn(env, tmp, newpath, NULL, flag);
   4832 		printmsg(messages[MSG_RCLONE_DELAY]);
   4833 		xdelay(XDELAY_INTERVAL_MS << 2); /* Set 4 times the usual delay */
   4834 	}
   4835 
   4836 	return TRUE;
   4837 }
   4838 
   4839 /*
   4840  * Unmounts if the directory represented by name is a mount point.
   4841  * Otherwise, asks for hostname
   4842  * Returns TRUE if directory needs to be refreshed *.
   4843  */
   4844 static bool unmount(char *name, char *newpath, int *presel, char *currentpath)
   4845 {
   4846 #if defined(__APPLE__) || defined(__FreeBSD__)
   4847 	static char cmd[] = "umount";
   4848 #else
   4849 	static char cmd[] = "fusermount3"; /* Arch Linux utility */
   4850 	static bool found = FALSE;
   4851 #endif
   4852 	char *tmp = name;
   4853 	struct stat sb, psb;
   4854 	bool child = FALSE;
   4855 	bool parent = FALSE;
   4856 	bool hovered = FALSE;
   4857 	char mntpath[PATH_MAX];
   4858 
   4859 #if !defined(__APPLE__) && !defined(__FreeBSD__)
   4860 	/* On Ubuntu it's fusermount */
   4861 	if (!found && !getutil(cmd)) {
   4862 		cmd[10] = '\0';
   4863 		found = TRUE;
   4864 	}
   4865 #endif
   4866 
   4867 	mkpath(cfgpath, toks[TOK_MNT], mntpath);
   4868 
   4869 	if (tmp && strcmp(mntpath, currentpath) == 0) {
   4870 		mkpath(mntpath, tmp, newpath);
   4871 		child = lstat(newpath, &sb) != -1;
   4872 		parent = lstat(xdirname(newpath), &psb) != -1;
   4873 		if (!child && !parent) {
   4874 			*presel = MSGWAIT;
   4875 			return FALSE;
   4876 		}
   4877 	}
   4878 
   4879 	if (!tmp || !child || !S_ISDIR(sb.st_mode) || (child && parent && sb.st_dev == psb.st_dev)) {
   4880 		tmp = xreadline(NULL, messages[MSG_HOSTNAME]);
   4881 		if (!tmp[0])
   4882 			return FALSE;
   4883 		if (name && (tmp[0] == '-') && (tmp[1] == '\0')) {
   4884 			mkpath(currentpath, name, newpath);
   4885 			hovered = TRUE;
   4886 		}
   4887 	}
   4888 
   4889 	if (!hovered)
   4890 		mkpath(mntpath, tmp, newpath);
   4891 
   4892 	if (!xdiraccess(newpath)) {
   4893 		*presel = MSGWAIT;
   4894 		return FALSE;
   4895 	}
   4896 
   4897 #if defined(__APPLE__) || defined(__FreeBSD__)
   4898 	if (spawn(cmd, newpath, NULL, NULL, F_NORMAL)) {
   4899 #else
   4900 	if (spawn(cmd, "-qu", newpath, NULL, F_NORMAL)) {
   4901 #endif
   4902 		if (!xconfirm(get_input(messages[MSG_LAZY])))
   4903 			return FALSE;
   4904 
   4905 #ifdef __APPLE__
   4906 		if (spawn(cmd, "-l", newpath, NULL, F_NORMAL)) {
   4907 #elif defined(__FreeBSD__)
   4908 		if (spawn(cmd, "-f", newpath, NULL, F_NORMAL)) {
   4909 #else
   4910 		if (spawn(cmd, "-quz", newpath, NULL, F_NORMAL)) {
   4911 #endif
   4912 			printwait(messages[MSG_FAILED], presel);
   4913 			return FALSE;
   4914 		}
   4915 	}
   4916 
   4917 	if (rmdir(newpath) == -1) {
   4918 		printwarn(presel);
   4919 		return FALSE;
   4920 	}
   4921 
   4922 	return TRUE;
   4923 }
   4924 
   4925 static void lock_terminal(void)
   4926 {
   4927 	spawn(xgetenv("NNN_LOCKER", utils[UTIL_LOCKER]), NULL, NULL, NULL, F_CLI);
   4928 }
   4929 
   4930 static void printkv(kv *kvarr, int fd, uchar_t max, uchar_t id)
   4931 {
   4932 	char *val = (id == NNN_BMS) ? bmstr : pluginstr;
   4933 
   4934 	for (uchar_t i = 0; i < max && kvarr[i].key; ++i)
   4935 		dprintf(fd, " %c: %s\n", (char)kvarr[i].key, val + kvarr[i].off);
   4936 }
   4937 
   4938 static void printkeys(kv *kvarr, char *buf, uchar_t max)
   4939 {
   4940 	uchar_t i = 0;
   4941 
   4942 	for (; i < max && kvarr[i].key; ++i) {
   4943 		buf[i << 1] = ' ';
   4944 		buf[(i << 1) + 1] = kvarr[i].key;
   4945 	}
   4946 
   4947 	buf[i << 1] = '\0';
   4948 }
   4949 
   4950 static size_t handle_bookmark(const char *bmark, char *newpath)
   4951 {
   4952 	int fd = '\r';
   4953 	size_t r;
   4954 
   4955 	if (maxbm || bmark) {
   4956 		r = xstrsncpy(g_buf, messages[MSG_KEYS], CMD_LEN_MAX);
   4957 
   4958 		if (bmark) { /* There is a marked directory */
   4959 			g_buf[--r] = ' ';
   4960 			g_buf[++r] = ',';
   4961 			g_buf[++r] = '\0';
   4962 			++r;
   4963 		}
   4964 		printkeys(bookmark, g_buf + r - 1, maxbm);
   4965 		printmsg(g_buf);
   4966 		fd = get_input(NULL);
   4967 	}
   4968 
   4969 	r = FALSE;
   4970 	if (fd == ',') /* Visit marked directory */
   4971 		bmark ? xstrsncpy(newpath, bmark, PATH_MAX) : (r = MSG_NOT_SET);
   4972 	else if (fd == '\r') /* Visit bookmarks directory */
   4973 		mkpath(cfgpath, toks[TOK_BM], newpath);
   4974 	else if (!get_kv_val(bookmark, newpath, fd, maxbm, NNN_BMS))
   4975 		r = MSG_INVALID_KEY;
   4976 
   4977 	if (!r && chdir(newpath) == -1)
   4978 		r = MSG_ACCESS;
   4979 
   4980 	return r;
   4981 }
   4982 
   4983 static void add_bookmark(char *path, char *newpath, int *presel)
   4984 {
   4985 	char *dir = xbasename(path);
   4986 
   4987 	dir = xreadline(dir[0] ? dir : NULL, "name: ");
   4988 	if (dir && *dir) {
   4989 		size_t r = mkpath(cfgpath, toks[TOK_BM], newpath);
   4990 
   4991 		newpath[r - 1] = '/';
   4992 		xstrsncpy(newpath + r, dir, PATH_MAX - r);
   4993 		printwait((symlink(path, newpath) == -1) ? strerror(errno) : newpath, presel);
   4994 	} else
   4995 		printwait(messages[MSG_CANCEL], presel);
   4996 }
   4997 
   4998 /*
   4999  * The help string tokens (each line) start with a HEX value
   5000  * which indicates the number of spaces to print before the
   5001  * particular token. This method was chosen instead of a flat
   5002  * string because the number of bytes in help was increasing
   5003  * the binary size by around a hundred bytes. This would only
   5004  * have increased as we keep adding new options.
   5005  */
   5006 static void show_help(const char *path)
   5007 {
   5008 	const char *start, *end;
   5009 	const char helpstr[] = {
   5010 	"0\n"
   5011 	"1NAVIGATION\n"
   5012 	       "9Up k  Up%-16cPgUp ^U  Page up\n"
   5013 	       "9Dn j  Down%-14cPgDn ^D  Page down\n"
   5014 	       "9Lt h  Parent%-12c~ ` @ -  ~, /, start, prev\n"
   5015 	   "5Ret Rt l  Open%-20c'  First file/match\n"
   5016 	       "9g ^A  Top%-21c.  Toggle hidden\n"
   5017 	       "9G ^E  End%-21c+  Toggle auto-proceed on open\n"
   5018 	      "8B (,)  Book(mark)%-11cb ^/  Select bookmark\n"
   5019 		"a1-4  Context%-11c(Sh)Tab  Cycle/new context\n"
   5020 	    "62Esc ^Q  Quit%-20cq  Quit context\n"
   5021 		 "b^G  QuitCD%-18cQ  Pick/err, quit\n"
   5022 	"0\n"
   5023 	"1FILTER & PROMPT\n"
   5024 		  "c/  Filter%-17c^N  Toggle type-to-nav\n"
   5025 		"aEsc  Exit prompt%-12c^L  Toggle last filter\n"
   5026 			"d%-20cAlt+Esc  Unfilter, quit context\n"
   5027 	"0\n"
   5028 	"1FILES\n"
   5029 	       "9o ^O  Open with%-15cn  Create new/link\n"
   5030 	       "9f ^F  File stats%-14cd  Detail mode toggle\n"
   5031 		 "b^R  Rename/dup%-14cr  Batch rename\n"
   5032 		  "cz  Archive%-17ce  Edit file\n"
   5033 		  "c*  Toggle exe%-14c>  Export list\n"
   5034 	   "5Space ^J  (Un)select%-12cm-m  Select range/clear\n"
   5035 	          "ca  Select all%-14cA  Invert sel\n"
   5036 	       "9p ^P  Copy here%-12cw ^W  Cp/mv sel as\n"
   5037 	       "9v ^V  Move here%-15cE  Edit sel list\n"
   5038 	       "9x ^X  Delete%-16cEsc  Send to FIFO\n"
   5039 	"0\n"
   5040 	"1MISC\n"
   5041 	      "8Alt ;  Select plugin%-11c=  Launch app\n"
   5042 	       "9! ^]  Shell%-19c]  Cmd prompt\n"
   5043 		  "cc  Connect remote%-10cu  Unmount remote/archive\n"
   5044 	       "9t ^T  Sort toggles%-12cs  Manage session\n"
   5045 		  "cT  Set time type%-11c0  Lock\n"
   5046 		 "b^L  Redraw%-18c?  Help, conf\n"
   5047 	};
   5048 
   5049 	int fd = create_tmp_file();
   5050 	if (fd == -1)
   5051 		return;
   5052 
   5053 	dprintf(fd, "  |V\\_\n"
   5054 		    "  /. \\\\\n"
   5055 		    " (;^; ||\n"
   5056 		    "   /___3\n"
   5057 		    "  (___n))\n");
   5058 
   5059 	char *prog = xgetenv(env_cfg[NNN_HELP], NULL);
   5060 	if (prog)
   5061 		get_output(prog, NULL, NULL, fd, TRUE, FALSE);
   5062 
   5063 	start = end = helpstr;
   5064 	while (*end) {
   5065 		if (*end == '\n') {
   5066 			snprintf(g_buf, CMD_LEN_MAX, "%*c%.*s",
   5067 				 xchartohex(*start), ' ', (int)(end - start), start + 1);
   5068 			dprintf(fd, g_buf, ' ');
   5069 			start = end + 1;
   5070 		}
   5071 
   5072 		++end;
   5073 	}
   5074 
   5075 	dprintf(fd, "\nLOCATIONS:\n");
   5076 	for (uchar_t i = 0; i < CTX_MAX; ++i)
   5077 		if (g_ctx[i].c_cfg.ctxactive)
   5078 			dprintf(fd, " %u: %s\n", i + 1, g_ctx[i].c_path);
   5079 
   5080 	dprintf(fd, "\nVOLUME: %s of ", coolsize(get_fs_info(path, FREE)));
   5081 	dprintf(fd, "%s free\n\n", coolsize(get_fs_info(path, CAPACITY)));
   5082 
   5083 	if (bookmark) {
   5084 		dprintf(fd, "BOOKMARKS\n");
   5085 		printkv(bookmark, fd, maxbm, NNN_BMS);
   5086 		dprintf(fd, "\n");
   5087 	}
   5088 
   5089 	if (plug) {
   5090 		dprintf(fd, "PLUGIN KEYS\n");
   5091 		printkv(plug, fd, maxplug, NNN_PLUG);
   5092 		dprintf(fd, "\n");
   5093 	}
   5094 
   5095 	for (uchar_t i = NNN_OPENER; i <= NNN_TRASH; ++i) {
   5096 		start = getenv(env_cfg[i]);
   5097 		if (start)
   5098 			dprintf(fd, "%s: %s\n", env_cfg[i], start);
   5099 	}
   5100 
   5101 	if (selpath)
   5102 		dprintf(fd, "SELECTION FILE: %s\n", selpath);
   5103 
   5104 	dprintf(fd, "\nv%s\n%s\n", VERSION, GENERAL_INFO);
   5105 	close(fd);
   5106 
   5107 	spawn(pager, g_tmpfpath, NULL, NULL, F_CLI | F_TTY);
   5108 	unlink(g_tmpfpath);
   5109 }
   5110 
   5111 static void setexports(void)
   5112 {
   5113 	char dvar[] = "d0";
   5114 	char fvar[] = "f0";
   5115 
   5116 	if (ndents) {
   5117 		setenv(envs[ENV_NCUR], pdents[cur].name, 1);
   5118 		xstrsncpy(g_ctx[cfg.curctx].c_name, pdents[cur].name, NAME_MAX + 1);
   5119 	} else if (g_ctx[cfg.curctx].c_name[0])
   5120 		g_ctx[cfg.curctx].c_name[0] = '\0';
   5121 
   5122 	for (uchar_t i = 0; i < CTX_MAX; ++i) {
   5123 		if (g_ctx[i].c_cfg.ctxactive) {
   5124 			dvar[1] = fvar[1] = '1' + i;
   5125 			setenv(dvar, g_ctx[i].c_path, 1);
   5126 
   5127 			if (g_ctx[i].c_name[0]) {
   5128 				mkpath(g_ctx[i].c_path, g_ctx[i].c_name, g_buf);
   5129 				setenv(fvar, g_buf, 1);
   5130 			}
   5131 		}
   5132 	}
   5133 	setenv("NNN_INCLUDE_HIDDEN", xitoa(cfg.showhidden), 1);
   5134 }
   5135 
   5136 static bool run_cmd_as_plugin(const char *file, char *runfile, uchar_t flags)
   5137 {
   5138 	size_t len;
   5139 
   5140 	xstrsncpy(g_buf, file, PATH_MAX);
   5141 
   5142 	len = xstrlen(g_buf);
   5143 	if (len > 1 && g_buf[len - 1] == '*') {
   5144 		flags &= ~F_CONFIRM; /* Skip user confirmation */
   5145 		g_buf[len - 1] = '\0'; /* Get rid of trailing no confirmation symbol */
   5146 		--len;
   5147 	}
   5148 
   5149 	if (flags & F_PAGE)
   5150 		get_output(g_buf, NULL, NULL, -1, TRUE, TRUE);
   5151 	else if (flags & F_NOTRACE) {
   5152 		if (is_suffix(g_buf, " $nnn"))
   5153 			g_buf[len - 5] = '\0';
   5154 		else
   5155 			runfile = NULL;
   5156 		spawn(g_buf, runfile, NULL, NULL, flags);
   5157 	} else
   5158 		spawn(utils[UTIL_SH_EXEC], g_buf, NULL, NULL, flags);
   5159 
   5160 	return TRUE;
   5161 }
   5162 
   5163 static bool plctrl_init(void)
   5164 {
   5165 	size_t len;
   5166 
   5167 	/* g_tmpfpath is used to generate tmp file names */
   5168 	g_tmpfpath[tmpfplen - 1] = '\0';
   5169 	len = xstrsncpy(g_pipepath, g_tmpfpath, TMP_LEN_MAX);
   5170 	g_pipepath[len - 1] = '/';
   5171 	len = xstrsncpy(g_pipepath + len, "nnn-pipe.", TMP_LEN_MAX - len) + len;
   5172 	xstrsncpy(g_pipepath + len - 1, xitoa(getpid()), TMP_LEN_MAX - len);
   5173 	setenv(env_cfg[NNN_PIPE], g_pipepath, TRUE);
   5174 
   5175 	return EXIT_SUCCESS;
   5176 }
   5177 
   5178 static void rmlistpath(void)
   5179 {
   5180 	if (listpath) {
   5181 		DPRINTF_S(__func__);
   5182 		DPRINTF_S(listpath);
   5183 		spawn(utils[UTIL_RM_RF], listpath, NULL, NULL, F_NOTRACE | F_MULTI);
   5184 		/* Do not free if program was started in list mode */
   5185 		if (listpath != initpath)
   5186 			free(listpath);
   5187 		listpath = NULL;
   5188 	}
   5189 }
   5190 
   5191 static ssize_t read_nointr(int fd, void *buf, size_t count)
   5192 {
   5193 	ssize_t len;
   5194 
   5195 	do
   5196 		len = read(fd, buf, count);
   5197 	while (len == -1 && errno == EINTR);
   5198 
   5199 	return len;
   5200 }
   5201 
   5202 static char *readpipe(int fd, char *ctxnum, char **path)
   5203 {
   5204 	char ctx, *nextpath = NULL;
   5205 
   5206 	if (read_nointr(fd, g_buf, 1) != 1)
   5207 		return NULL;
   5208 
   5209 	if (g_buf[0] == '-') { /* Clear selection on '-' */
   5210 		clearselection();
   5211 		if (read_nointr(fd, g_buf, 1) != 1)
   5212 			return NULL;
   5213 	}
   5214 
   5215 	if (g_buf[0] == '+')
   5216 		ctx = (char)(get_free_ctx() + 1);
   5217 	else if (g_buf[0] < '0')
   5218 		return NULL;
   5219 	else {
   5220 		ctx = g_buf[0] - '0';
   5221 		if (ctx > CTX_MAX)
   5222 			return NULL;
   5223 	}
   5224 
   5225 	if (read_nointr(fd, g_buf, 1) != 1)
   5226 		return NULL;
   5227 
   5228 	char op = g_buf[0];
   5229 
   5230 	if (op == 'c') {
   5231 		ssize_t len = read_nointr(fd, g_buf, PATH_MAX);
   5232 
   5233 		if (len <= 0)
   5234 			return NULL;
   5235 
   5236 		g_buf[len] = '\0'; /* Terminate the path read */
   5237 		if (g_buf[0] == '/') {
   5238 			nextpath = g_buf;
   5239 			len = xstrlen(g_buf);
   5240 			while (--len && (g_buf[len] == '/')) /* Trim all trailing '/' */
   5241 				g_buf[len] = '\0';
   5242 		}
   5243 	} else if (op == 'l') {
   5244 		rmlistpath(); /* Remove last list mode path, if any */
   5245 		nextpath = load_input(fd, *path);
   5246 	} else if (op == 'p') {
   5247 		free(selpath);
   5248 		selpath = NULL;
   5249 		clearselection();
   5250 		g_state.picker = 0;
   5251 		g_state.picked = 1;
   5252 	}
   5253 
   5254 	*ctxnum = ctx;
   5255 
   5256 	return nextpath;
   5257 }
   5258 
   5259 static bool run_plugin(char **path, const char *file, char *runfile, char **lastname, char **lastdir)
   5260 {
   5261 	pid_t p;
   5262 	char ctx = 0;
   5263 	uchar_t flags = 0;
   5264 	bool cmd_as_plugin = FALSE;
   5265 	char *nextpath;
   5266 
   5267 	if (!g_state.pluginit) {
   5268 		plctrl_init();
   5269 		g_state.pluginit = 1;
   5270 	}
   5271 
   5272 	setexports();
   5273 
   5274 	/* Check for run-cmd-as-plugin mode */
   5275 	if (*file == '!') {
   5276 		flags = F_MULTI | F_CONFIRM;
   5277 		++file;
   5278 
   5279 		if (*file == '|') { /* Check if output should be paged */
   5280 			flags |= F_PAGE;
   5281 			++file;
   5282 		} else if (*file == '&') { /* Check if GUI flags are to be used */
   5283 			flags = F_NOTRACE | F_NOWAIT;
   5284 			++file;
   5285 		}
   5286 
   5287 		if (!*file)
   5288 			return FALSE;
   5289 
   5290 		if ((flags & F_NOTRACE) || (flags & F_PAGE))
   5291 			return run_cmd_as_plugin(file, runfile, flags);
   5292 
   5293 		cmd_as_plugin = TRUE;
   5294 	}
   5295 
   5296 	if (mkfifo(g_pipepath, 0600) != 0)
   5297 		return FALSE;
   5298 
   5299 	exitcurses();
   5300 
   5301 	p = fork();
   5302 
   5303 	if (!p) { // In child
   5304 		int wfd = open(g_pipepath, O_WRONLY | O_CLOEXEC);
   5305 
   5306 		if (wfd == -1)
   5307 			_exit(EXIT_FAILURE);
   5308 
   5309 		if (!cmd_as_plugin) {
   5310 			char *sel = NULL;
   5311 			char std[2] = "-";
   5312 
   5313 			/* Generate absolute path to plugin */
   5314 			mkpath(plgpath, file, g_buf);
   5315 
   5316 			if (g_state.picker)
   5317 				sel = selpath ? selpath : std;
   5318 
   5319 			if (runfile && runfile[0]) {
   5320 				xstrsncpy(*lastname, runfile, NAME_MAX);
   5321 				spawn(g_buf, *lastname, *path, sel, 0);
   5322 			} else
   5323 				spawn(g_buf, NULL, *path, sel, 0);
   5324 		} else
   5325 			run_cmd_as_plugin(file, NULL, flags);
   5326 
   5327 		close(wfd);
   5328 		_exit(EXIT_SUCCESS);
   5329 	}
   5330 
   5331 	int rfd;
   5332 
   5333 	do
   5334 		rfd = open(g_pipepath, O_RDONLY);
   5335 	while (rfd == -1 && errno == EINTR);
   5336 
   5337 	nextpath = readpipe(rfd, &ctx, path);
   5338 	if (nextpath)
   5339 		set_smart_ctx(ctx, nextpath, path, runfile, lastname, lastdir);
   5340 
   5341 	close(rfd);
   5342 
   5343 	/* wait for the child to finish. no zombies allowed */
   5344 	waitpid(p, NULL, 0);
   5345 
   5346 	refresh();
   5347 
   5348 	unlink(g_pipepath);
   5349 
   5350 	return TRUE;
   5351 }
   5352 
   5353 static bool launch_app(char *newpath)
   5354 {
   5355 	int r = F_NORMAL;
   5356 	char *tmp = newpath;
   5357 
   5358 	mkpath(plgpath, utils[UTIL_LAUNCH], newpath);
   5359 
   5360 	if (!getutil(utils[UTIL_FZF]) || access(newpath, X_OK) < 0) {
   5361 		tmp = xreadline(NULL, messages[MSG_APP_NAME]);
   5362 		r = F_NOWAIT | F_NOTRACE | F_MULTI;
   5363 	}
   5364 
   5365 	if (tmp && *tmp) // NOLINT
   5366 		spawn(tmp, (r == F_NORMAL) ? "0" : NULL, NULL, NULL, r);
   5367 
   5368 	return FALSE;
   5369 }
   5370 
   5371 /* Returns TRUE if at least one command was run */
   5372 static bool prompt_run(void)
   5373 {
   5374 	bool ret = FALSE;
   5375 	char *cmdline, *next;
   5376 	int cnt_j, cnt_J;
   5377 	size_t len;
   5378 
   5379 	const char *xargs_j = "xargs -0 -I{} %s < %s";
   5380 	const char *xargs_J = "xargs -0 %s < %s";
   5381 	char cmd[CMD_LEN_MAX + 32]; // 32 for xargs format strings
   5382 
   5383 	while (1) {
   5384 #ifndef NORL
   5385 		if (g_state.picker) {
   5386 #endif
   5387 			cmdline = xreadline(NULL, PROMPT);
   5388 #ifndef NORL
   5389 		} else
   5390 			cmdline = getreadline("\n"PROMPT);
   5391 #endif
   5392 		// Check for an empty command
   5393 		if (!cmdline || !cmdline[0])
   5394 			break;
   5395 
   5396 		free(lastcmd);
   5397 		lastcmd = xstrdup(cmdline);
   5398 		ret = TRUE;
   5399 
   5400 		len = xstrlen(cmdline);
   5401 
   5402 		cnt_j = 0;
   5403 		next = cmdline;
   5404 		while ((next = strstr(next, "%j"))) {
   5405 			++cnt_j;
   5406 
   5407 			// replace %j with {} for xargs later
   5408 			next[0] = '{';
   5409 			next[1] = '}';
   5410 
   5411 			++next;
   5412 		}
   5413 
   5414 		cnt_J = 0;
   5415 		next = cmdline;
   5416 		while ((next = strstr(next, "%J"))) {
   5417 			++cnt_J;
   5418 
   5419 			// %J should be the last thing in the command
   5420 			if (next == cmdline + len - 2) {
   5421 				cmdline[len - 2] = '\0';
   5422 			}
   5423 
   5424 			++next;
   5425 		}
   5426 
   5427 		// We can't handle both %j and %J in a single command
   5428 		if (cnt_j && cnt_J)
   5429 			break;
   5430 
   5431 		if (cnt_j)
   5432 			snprintf(cmd, CMD_LEN_MAX + 32, xargs_j, cmdline, selpath);
   5433 		else if (cnt_J)
   5434 			snprintf(cmd, CMD_LEN_MAX + 32, xargs_J, cmdline, selpath);
   5435 
   5436 		spawn(shell, "-c", (cnt_j || cnt_J) ? cmd : cmdline, NULL, F_CLI | F_CONFIRM);
   5437 	}
   5438 
   5439 	return ret;
   5440 }
   5441 
   5442 static bool handle_cmd(enum action sel, char *newpath)
   5443 {
   5444 	endselection(FALSE);
   5445 
   5446 	if (sel == SEL_LAUNCH)
   5447 		return launch_app(newpath);
   5448 
   5449 	setexports();
   5450 
   5451 	if (sel == SEL_PROMPT)
   5452 		return prompt_run();
   5453 
   5454 	/* Set nnn nesting level */
   5455 	char *tmp = getenv(env_cfg[NNNLVL]);
   5456 	int r = tmp ? atoi(tmp) : 0;
   5457 
   5458 	setenv(env_cfg[NNNLVL], xitoa(r + 1), 1);
   5459 	spawn(shell, NULL, NULL, NULL, F_CLI);
   5460 	setenv(env_cfg[NNNLVL], xitoa(r), 1);
   5461 	return TRUE;
   5462 }
   5463 
   5464 static void dentfree(void)
   5465 {
   5466 	free(pnamebuf);
   5467 	free(pdents);
   5468 	free(mark);
   5469 
   5470 	/* Thread data cleanup */
   5471 	free(core_blocks);
   5472 	free(core_data);
   5473 	free(core_files);
   5474 }
   5475 
   5476 static void *du_thread(void *p_data)
   5477 {
   5478 	thread_data *pdata = (thread_data *)p_data;
   5479 	char *path[2] = {pdata->path, NULL};
   5480 	ullong_t tfiles = 0;
   5481 	blkcnt_t tblocks = 0;
   5482 	struct stat *sb;
   5483 	FTS *tree = fts_open(path, FTS_PHYSICAL | FTS_XDEV | FTS_NOCHDIR, 0);
   5484 	FTSENT *node;
   5485 
   5486 	while ((node = fts_read(tree))) {
   5487 		if (node->fts_info & FTS_D) {
   5488 			if (g_state.interrupt)
   5489 				break;
   5490 			continue;
   5491 		}
   5492 
   5493 		sb = node->fts_statp;
   5494 
   5495 		if (cfg.apparentsz) {
   5496 			if (sb->st_size && DU_TEST)
   5497 				tblocks += sb->st_size;
   5498 		} else if (sb->st_blocks && DU_TEST)
   5499 			tblocks += sb->st_blocks;
   5500 
   5501 		++tfiles;
   5502 	}
   5503 
   5504 	fts_close(tree);
   5505 
   5506 	if (pdata->entnum >= 0)
   5507 		pdents[pdata->entnum].blocks = tblocks;
   5508 
   5509 	if (!pdata->mntpoint) {
   5510 		core_blocks[pdata->core] += tblocks;
   5511 		core_files[pdata->core] += tfiles;
   5512 	} else
   5513 		core_files[pdata->core] += 1;
   5514 
   5515 	pthread_mutex_lock(&running_mutex);
   5516 	threadbmp |= (1 << pdata->core);
   5517 	--active_threads;
   5518 	pthread_mutex_unlock(&running_mutex);
   5519 
   5520 	return NULL;
   5521 }
   5522 
   5523 static void dirwalk(char *path, int entnum, bool mountpoint)
   5524 {
   5525 	/* Loop till any core is free */
   5526 	while (active_threads == NUM_DU_THREADS);
   5527 
   5528 	if (g_state.interrupt)
   5529 		return;
   5530 
   5531 	pthread_mutex_lock(&running_mutex);
   5532 	int core = ffs(threadbmp) - 1;
   5533 
   5534 	threadbmp &= ~(1 << core);
   5535 	++active_threads;
   5536 	pthread_mutex_unlock(&running_mutex);
   5537 
   5538 	xstrsncpy(core_data[core].path, path, PATH_MAX);
   5539 	core_data[core].entnum = entnum;
   5540 	core_data[core].core = (ushort_t)core;
   5541 	core_data[core].mntpoint = mountpoint;
   5542 
   5543 	pthread_t tid = 0;
   5544 
   5545 	pthread_create(&tid, NULL, du_thread, (void *)&(core_data[core]));
   5546 
   5547 	tolastln();
   5548 	addstr(xbasename(path));
   5549 	addstr(" [^C aborts]\n");
   5550 	refresh();
   5551 }
   5552 
   5553 static void prep_threads(void)
   5554 {
   5555 	if (!g_state.duinit) {
   5556 		/* drop MSB 1s */
   5557 		threadbmp >>= (32 - NUM_DU_THREADS);
   5558 
   5559 		core_blocks = calloc(NUM_DU_THREADS, sizeof(blkcnt_t));
   5560 		core_data = calloc(NUM_DU_THREADS, sizeof(thread_data));
   5561 		core_files = calloc(NUM_DU_THREADS, sizeof(ullong_t));
   5562 
   5563 #ifndef __APPLE__
   5564 		/* Increase current open file descriptor limit */
   5565 		max_openfds();
   5566 #endif
   5567 		g_state.duinit = TRUE;
   5568 	} else {
   5569 		memset(core_blocks, 0, NUM_DU_THREADS * sizeof(blkcnt_t));
   5570 		memset(core_data, 0, NUM_DU_THREADS * sizeof(thread_data));
   5571 		memset(core_files, 0, NUM_DU_THREADS * sizeof(ullong_t));
   5572 	}
   5573 }
   5574 
   5575 /* Skip self and parent */
   5576 static bool selforparent(const char *path)
   5577 {
   5578 	return path[0] == '.' && (path[1] == '\0' || (path[1] == '.' && path[2] == '\0'));
   5579 }
   5580 
   5581 static int dentfill(char *path, struct entry **ppdents)
   5582 {
   5583 	uchar_t entflags = 0;
   5584 	int flags = 0;
   5585 	struct dirent *dp;
   5586 	char *namep, *pnb, *buf;
   5587 	struct entry *dentp;
   5588 	size_t off = 0, namebuflen = NAMEBUF_INCR;
   5589 	struct stat sb_path, sb;
   5590 	DIR *dirp = opendir(path);
   5591 
   5592 	ndents = 0;
   5593 
   5594 	DPRINTF_S(__func__);
   5595 
   5596 	if (!dirp)
   5597 		return 0;
   5598 
   5599 	int fd = dirfd(dirp);
   5600 
   5601 	if (cfg.blkorder) {
   5602 		num_files = 0;
   5603 		dir_blocks = 0;
   5604 		buf = g_buf;
   5605 
   5606 		if (fstatat(fd, path, &sb_path, 0) == -1)
   5607 			goto exit;
   5608 
   5609 		if (!ihashbmp) {
   5610 			ihashbmp = calloc(1, HASH_OCTETS << 3);
   5611 			if (!ihashbmp)
   5612 				goto exit;
   5613 		} else
   5614 			memset(ihashbmp, 0, HASH_OCTETS << 3);
   5615 
   5616 		prep_threads();
   5617 
   5618 		attron(COLOR_PAIR(cfg.curctx + 1));
   5619 	}
   5620 
   5621 #if _POSIX_C_SOURCE >= 200112L
   5622 	posix_fadvise(fd, 0, 0, POSIX_FADV_SEQUENTIAL);
   5623 #endif
   5624 
   5625 	dp = readdir(dirp);
   5626 	if (!dp)
   5627 		goto exit;
   5628 
   5629 #if defined(__sun) || defined(__HAIKU__)
   5630 	flags = AT_SYMLINK_NOFOLLOW; /* no d_type */
   5631 #else
   5632 	if (cfg.blkorder || dp->d_type == DT_UNKNOWN) {
   5633 		/*
   5634 		 * Optimization added for filesystems which support dirent.d_type
   5635 		 * see readdir(3)
   5636 		 * Known drawbacks:
   5637 		 * - the symlink size is set to 0
   5638 		 * - the modification time of the symlink is set to that of the target file
   5639 		 */
   5640 		flags = AT_SYMLINK_NOFOLLOW;
   5641 	}
   5642 #endif
   5643 
   5644 	do {
   5645 		namep = dp->d_name;
   5646 
   5647 		if (selforparent(namep))
   5648 			continue;
   5649 
   5650 		if (!cfg.showhidden && namep[0] == '.') {
   5651 			if (!cfg.blkorder)
   5652 				continue;
   5653 
   5654 			if (fstatat(fd, namep, &sb, AT_SYMLINK_NOFOLLOW) == -1)
   5655 				continue;
   5656 
   5657 			if (S_ISDIR(sb.st_mode)) {
   5658 				if (sb_path.st_dev == sb.st_dev) { // NOLINT
   5659 					mkpath(path, namep, buf); // NOLINT
   5660 					dirwalk(buf, -1, FALSE);
   5661 
   5662 					if (g_state.interrupt)
   5663 						goto exit;
   5664 				}
   5665 			} else {
   5666 				/* Do not recount hard links */
   5667 				if (sb.st_nlink <= 1 || test_set_bit((uint_t)sb.st_ino))
   5668 					dir_blocks += (cfg.apparentsz ? sb.st_size : sb.st_blocks);
   5669 				++num_files;
   5670 			}
   5671 
   5672 			continue;
   5673 		}
   5674 
   5675 		if (fstatat(fd, namep, &sb, flags) == -1) {
   5676 			if (flags || (fstatat(fd, namep, &sb, AT_SYMLINK_NOFOLLOW) == -1)) {
   5677 				/* Missing file */
   5678 				DPRINTF_U(flags);
   5679 				if (!flags) {
   5680 					DPRINTF_S(namep);
   5681 					DPRINTF_S(strerror(errno));
   5682 				}
   5683 
   5684 				entflags = FILE_MISSING;
   5685 				memset(&sb, 0, sizeof(struct stat));
   5686 			} else /* Orphaned symlink */
   5687 				entflags = SYM_ORPHAN;
   5688 		}
   5689 
   5690 		if (ndents == total_dents) {
   5691 			if (cfg.blkorder)
   5692 				while (active_threads);
   5693 
   5694 			total_dents += ENTRY_INCR;
   5695 			*ppdents = xrealloc(*ppdents, total_dents * sizeof(**ppdents));
   5696 			if (!*ppdents) {
   5697 				free(pnamebuf);
   5698 				closedir(dirp);
   5699 				errexit();
   5700 			}
   5701 			DPRINTF_P(*ppdents);
   5702 		}
   5703 
   5704 		/* If not enough bytes left to copy a file name of length NAME_MAX, re-allocate */
   5705 		if (namebuflen - off < NAME_MAX + 1) {
   5706 			namebuflen += NAMEBUF_INCR;
   5707 
   5708 			pnb = pnamebuf;
   5709 			pnamebuf = (char *)xrealloc(pnamebuf, namebuflen);
   5710 			if (!pnamebuf) {
   5711 				free(*ppdents);
   5712 				closedir(dirp);
   5713 				errexit();
   5714 			}
   5715 			DPRINTF_P(pnamebuf);
   5716 
   5717 			/* realloc() may result in memory move, we must re-adjust if that happens */
   5718 			if (pnb != pnamebuf) {
   5719 				dentp = *ppdents;
   5720 				dentp->name = pnamebuf;
   5721 
   5722 				for (int count = 1; count < ndents; ++dentp, ++count)
   5723 					/* Current file name starts at last file name start + length */
   5724 					(dentp + 1)->name = (char *)((size_t)dentp->name + dentp->nlen);
   5725 			}
   5726 		}
   5727 
   5728 		dentp = *ppdents + ndents;
   5729 
   5730 		/* Selection file name */
   5731 		dentp->name = (char *)((size_t)pnamebuf + off);
   5732 		dentp->nlen = xstrsncpy(dentp->name, namep, NAME_MAX + 1);
   5733 		off += dentp->nlen;
   5734 
   5735 		/* Copy other fields */
   5736 		if (cfg.timetype == T_MOD) {
   5737 			dentp->sec = sb.st_mtime;
   5738 #ifdef __APPLE__
   5739 			dentp->nsec = (uint_t)sb.st_mtimespec.tv_nsec;
   5740 #else
   5741 			dentp->nsec = (uint_t)sb.st_mtim.tv_nsec;
   5742 #endif
   5743 		} else if (cfg.timetype == T_ACCESS) {
   5744 			dentp->sec = sb.st_atime;
   5745 #ifdef __APPLE__
   5746 			dentp->nsec = (uint_t)sb.st_atimespec.tv_nsec;
   5747 #else
   5748 			dentp->nsec = (uint_t)sb.st_atim.tv_nsec;
   5749 #endif
   5750 		} else {
   5751 			dentp->sec = sb.st_ctime;
   5752 #ifdef __APPLE__
   5753 			dentp->nsec = (uint_t)sb.st_ctimespec.tv_nsec;
   5754 #else
   5755 			dentp->nsec = (uint_t)sb.st_ctim.tv_nsec;
   5756 #endif
   5757 		}
   5758 
   5759 #if !(defined(__sun) || defined(__HAIKU__))
   5760 		if (!flags && dp->d_type == DT_LNK) {
   5761 			 /* Do not add sizes for links */
   5762 			dentp->mode = (sb.st_mode & ~S_IFMT) | S_IFLNK;
   5763 			dentp->size = listpath ? sb.st_size : 0;
   5764 		} else {
   5765 			dentp->mode = sb.st_mode;
   5766 			dentp->size = sb.st_size;
   5767 		}
   5768 #else
   5769 		dentp->mode = sb.st_mode;
   5770 		dentp->size = sb.st_size;
   5771 #endif
   5772 
   5773 #ifndef NOUG
   5774 		dentp->uid = sb.st_uid;
   5775 		dentp->gid = sb.st_gid;
   5776 #endif
   5777 
   5778 		dentp->flags = S_ISDIR(sb.st_mode) ? 0 : ((sb.st_nlink > 1) ? HARD_LINK : 0);
   5779 		if (entflags) {
   5780 			dentp->flags |= entflags;
   5781 			entflags = 0;
   5782 		}
   5783 
   5784 		if (cfg.blkorder) {
   5785 			if (S_ISDIR(sb.st_mode)) {
   5786 				mkpath(path, namep, buf); // NOLINT
   5787 
   5788 				/* Need to show the disk usage of this dir */
   5789 				dirwalk(buf, ndents, (sb_path.st_dev != sb.st_dev)); // NOLINT
   5790 
   5791 				if (g_state.interrupt)
   5792 					goto exit;
   5793 			} else {
   5794 				dentp->blocks = (cfg.apparentsz ? sb.st_size : sb.st_blocks);
   5795 				/* Do not recount hard links */
   5796 				if (sb.st_nlink <= 1 || test_set_bit((uint_t)sb.st_ino))
   5797 					dir_blocks += dentp->blocks;
   5798 				++num_files;
   5799 			}
   5800 		}
   5801 
   5802 		if (flags) {
   5803 			/* Flag if this is a dir or symlink to a dir */
   5804 			if (S_ISLNK(sb.st_mode)) {
   5805 				sb.st_mode = 0;
   5806 				fstatat(fd, namep, &sb, 0);
   5807 			}
   5808 
   5809 			if (S_ISDIR(sb.st_mode))
   5810 				dentp->flags |= DIR_OR_DIRLNK;
   5811 #if !(defined(__sun) || defined(__HAIKU__)) /* no d_type */
   5812 		} else if (dp->d_type == DT_DIR || ((dp->d_type == DT_LNK
   5813 			   || dp->d_type == DT_UNKNOWN) && S_ISDIR(sb.st_mode))) {
   5814 			dentp->flags |= DIR_OR_DIRLNK;
   5815 #endif
   5816 		}
   5817 
   5818 		++ndents;
   5819 	} while ((dp = readdir(dirp)));
   5820 
   5821 exit:
   5822 	if (cfg.blkorder) {
   5823 		while (active_threads);
   5824 
   5825 		attroff(COLOR_PAIR(cfg.curctx + 1));
   5826 		for (int i = 0; i < NUM_DU_THREADS; ++i) {
   5827 			num_files += core_files[i];
   5828 			dir_blocks += core_blocks[i];
   5829 		}
   5830 	}
   5831 
   5832 	/* Should never be null */
   5833 	if (closedir(dirp) == -1)
   5834 		errexit();
   5835 
   5836 	return ndents;
   5837 }
   5838 
   5839 static void populate(char *path, char *lastname)
   5840 {
   5841 #ifdef DEBUG
   5842 	struct timespec ts1, ts2;
   5843 
   5844 	clock_gettime(CLOCK_REALTIME, &ts1); /* Use CLOCK_MONOTONIC on FreeBSD */
   5845 #endif
   5846 
   5847 	ndents = dentfill(path, &pdents);
   5848 	if (!ndents)
   5849 		return;
   5850 
   5851 #ifndef NOSORT
   5852 	ENTSORT(pdents, ndents, entrycmpfn);
   5853 #endif
   5854 
   5855 #ifdef DEBUG
   5856 	clock_gettime(CLOCK_REALTIME, &ts2);
   5857 	DPRINTF_U(ts2.tv_nsec - ts1.tv_nsec);
   5858 #endif
   5859 
   5860 	/* Find cur from history */
   5861 	/* No NULL check for lastname, always points to an array */
   5862 	move_cursor(*lastname ? dentfind(lastname, ndents) : 0, 0);
   5863 
   5864 	// Force full redraw
   5865 	last_curscroll = -1;
   5866 }
   5867 
   5868 #ifndef NOFIFO
   5869 static void notify_fifo(bool force)
   5870 {
   5871 	if (!fifopath)
   5872 		return;
   5873 
   5874 	if (fifofd == -1) {
   5875 		fifofd = open(fifopath, O_WRONLY|O_NONBLOCK|O_CLOEXEC);
   5876 		if (fifofd == -1) {
   5877 			if (errno != ENXIO)
   5878 				/* Unexpected error, the FIFO file might have been removed */
   5879 				/* We give up FIFO notification */
   5880 				fifopath = NULL;
   5881 			return;
   5882 		}
   5883 	}
   5884 
   5885 	static struct entry lastentry;
   5886 
   5887 	if (!force && !memcmp(&lastentry, &pdents[cur], sizeof(struct entry)))
   5888 		return;
   5889 
   5890 	lastentry = pdents[cur];
   5891 
   5892 	char path[PATH_MAX];
   5893 	size_t len = mkpath(g_ctx[cfg.curctx].c_path, ndents ? pdents[cur].name : "", path);
   5894 
   5895 	path[len - 1] = '\n';
   5896 
   5897 	ssize_t ret = write(fifofd, path, len);
   5898 
   5899 	if (ret != (ssize_t)len && !(ret == -1 && (errno == EAGAIN || errno == EPIPE))) {
   5900 		DPRINTF_S(strerror(errno));
   5901 	}
   5902 }
   5903 
   5904 static void send_to_explorer(int *presel)
   5905 {
   5906 	if (nselected) {
   5907 		int fd = open(fifopath, O_WRONLY|O_NONBLOCK|O_CLOEXEC, 0600);
   5908 		if ((fd == -1) || (seltofile(fd, NULL) != (size_t)(selbufpos)))
   5909 			printwarn(presel);
   5910 		else {
   5911 			resetselind();
   5912 			clearselection();
   5913 		}
   5914 		if (fd > 1)
   5915 			close(fd);
   5916 	} else
   5917 		notify_fifo(TRUE); /* Send opened path to NNN_FIFO */
   5918 }
   5919 #endif
   5920 
   5921 static void move_cursor(int target, int ignore_scrolloff)
   5922 {
   5923 	int onscreen = xlines - 4; /* Leave top 2 and bottom 2 lines */
   5924 
   5925 	target = MAX(0, MIN(ndents - 1, target));
   5926 	last_curscroll = curscroll;
   5927 	last = cur;
   5928 	cur = target;
   5929 
   5930 	if (!ignore_scrolloff) {
   5931 		int delta = target - last;
   5932 		int scrolloff = MIN(SCROLLOFF, onscreen >> 1);
   5933 
   5934 		/*
   5935 		 * When ignore_scrolloff is 1, the cursor can jump into the scrolloff
   5936 		 * margin area, but when ignore_scrolloff is 0, act like a boa
   5937 		 * constrictor and squeeze the cursor towards the middle region of the
   5938 		 * screen by allowing it to move inward and disallowing it to move
   5939 		 * outward (deeper into the scrolloff margin area).
   5940 		 */
   5941 		if (((cur < (curscroll + scrolloff)) && delta < 0)
   5942 		    || ((cur > (curscroll + onscreen - scrolloff - 1)) && delta > 0))
   5943 			curscroll += delta;
   5944 	}
   5945 	curscroll = MIN(curscroll, MIN(cur, ndents - onscreen));
   5946 	curscroll = MAX(curscroll, MAX(cur - (onscreen - 1), 0));
   5947 
   5948 #ifndef NOFIFO
   5949 	if (!g_state.fifomode)
   5950 		notify_fifo(FALSE); /* Send hovered path to NNN_FIFO */
   5951 #endif
   5952 }
   5953 
   5954 static void handle_screen_move(enum action sel)
   5955 {
   5956 	int onscreen;
   5957 
   5958 	switch (sel) {
   5959 	case SEL_NEXT:
   5960 		if (ndents && (cfg.rollover || (cur != ndents - 1)))
   5961 			move_cursor((cur + 1) % ndents, 0);
   5962 		break;
   5963 	case SEL_PREV:
   5964 		if (ndents && (cfg.rollover || cur))
   5965 			move_cursor((cur + ndents - 1) % ndents, 0);
   5966 		break;
   5967 	case SEL_PGDN:
   5968 		onscreen = xlines - 4;
   5969 		move_cursor(curscroll + (onscreen - 1), 1);
   5970 		curscroll += onscreen - 1;
   5971 		break;
   5972 	case SEL_CTRL_D:
   5973 		onscreen = xlines - 4;
   5974 		move_cursor(curscroll + (onscreen - 1), 1);
   5975 		curscroll += onscreen >> 1;
   5976 		break;
   5977 	case SEL_PGUP: // fallthrough
   5978 		onscreen = xlines - 4;
   5979 		move_cursor(curscroll, 1);
   5980 		curscroll -= onscreen - 1;
   5981 		break;
   5982 	case SEL_CTRL_U:
   5983 		onscreen = xlines - 4;
   5984 		move_cursor(curscroll, 1);
   5985 		curscroll -= onscreen >> 1;
   5986 		break;
   5987 	case SEL_HOME:
   5988 		move_cursor(0, 1);
   5989 		break;
   5990 	case SEL_END:
   5991 		move_cursor(ndents - 1, 1);
   5992 		break;
   5993 	default: /* case SEL_FIRST */
   5994 	{
   5995 		int c = get_input(messages[MSG_FIRST]);
   5996 
   5997 		if (!c)
   5998 			break;
   5999 
   6000 		c = TOUPPER(c);
   6001 
   6002 		int r = (c == TOUPPER(*pdents[cur].name)) ? (cur + 1) : 0;
   6003 
   6004 		for (; r < ndents; ++r) {
   6005 			if (((c == '\'') && !(pdents[r].flags & DIR_OR_DIRLNK))
   6006 			    || (c == TOUPPER(*pdents[r].name))) {
   6007 				move_cursor((r) % ndents, 0);
   6008 				break;
   6009 			}
   6010 		}
   6011 		break;
   6012 	}
   6013 	}
   6014 }
   6015 
   6016 static void handle_openwith(const char *path, const char *name, char *newpath, char *tmp)
   6017 {
   6018 	/* Confirm if app is CLI or GUI */
   6019 	int r = get_input(messages[MSG_CLI_MODE]);
   6020 
   6021 	r = (r == 'c' ? F_CLI :
   6022 	     (r == 'g' ? F_NOWAIT | F_NOTRACE | F_MULTI : 0));
   6023 	if (r) {
   6024 		mkpath(path, name, newpath);
   6025 		spawn(tmp, newpath, NULL, NULL, r);
   6026 	}
   6027 }
   6028 
   6029 static void copynextname(char *lastname)
   6030 {
   6031 	if (cur) {
   6032 		cur += (cur != (ndents - 1)) ? 1 : -1;
   6033 		copycurname();
   6034 	} else
   6035 		lastname[0] = '\0';
   6036 }
   6037 
   6038 static int handle_context_switch(enum action sel)
   6039 {
   6040 	int r = -1;
   6041 
   6042 	switch (sel) {
   6043 	case SEL_CYCLE: // fallthrough
   6044 	case SEL_CYCLER:
   6045 		/* visit next and previous contexts */
   6046 		r = cfg.curctx;
   6047 		if (sel == SEL_CYCLE)
   6048 			do
   6049 				r = (r + 1) & ~CTX_MAX;
   6050 			while (!g_ctx[r].c_cfg.ctxactive);
   6051 		else {
   6052 			do /* Attempt to create a new context */
   6053 				r = (r + 1) & ~CTX_MAX;
   6054 			while (g_ctx[r].c_cfg.ctxactive && (r != cfg.curctx));
   6055 
   6056 			if (r == cfg.curctx) /* If all contexts are active, reverse cycle */
   6057 				do
   6058 					r = (r + (CTX_MAX - 1)) & (CTX_MAX - 1);
   6059 				while (!g_ctx[r].c_cfg.ctxactive);
   6060 		} // fallthrough
   6061 	default: /* SEL_CTXN */
   6062 		if (sel >= SEL_CTX1) /* CYCLE keys are lesser in value */
   6063 			r = sel - SEL_CTX1; /* Save the next context id */
   6064 
   6065 		if (cfg.curctx == r) {
   6066 			if (sel == SEL_CYCLE)
   6067 				(r == CTX_MAX - 1) ? (r = 0) : ++r;
   6068 			else if (sel == SEL_CYCLER)
   6069 				(r == 0) ? (r = CTX_MAX - 1) : --r;
   6070 			else
   6071 				return -1;
   6072 		}
   6073 	}
   6074 
   6075 	return r;
   6076 }
   6077 
   6078 static int set_sort_flags(int r)
   6079 {
   6080 	bool session = (r == '\0');
   6081 	bool reverse = FALSE;
   6082 
   6083 	if (ISUPPER_(r) && (r != 'R') && (r != 'C')) {
   6084 		reverse = TRUE;
   6085 		r = TOLOWER(r);
   6086 	}
   6087 
   6088 	/* Set the correct input in case of a session load */
   6089 	if (session) {
   6090 		if (cfg.apparentsz) {
   6091 			cfg.apparentsz = 0;
   6092 			r = 'a';
   6093 		} else if (cfg.blkorder) {
   6094 			cfg.blkorder = 0;
   6095 			r = 'd';
   6096 		}
   6097 
   6098 		if (cfg.version)
   6099 			namecmpfn = &xstrverscasecmp;
   6100 
   6101 		if (cfg.reverse)
   6102 			entrycmpfn = &reventrycmp;
   6103 	} else if (r == CONTROL('T')) {
   6104 		/* Cycling order: clear -> size -> time -> clear */
   6105 		if (cfg.timeorder)
   6106 			r = 's';
   6107 		else if (cfg.sizeorder)
   6108 			r = 'c';
   6109 		else
   6110 			r = 't';
   6111 	}
   6112 
   6113 	switch (r) {
   6114 	case 'a': /* Apparent du */
   6115 		cfg.apparentsz ^= 1;
   6116 		if (cfg.apparentsz) {
   6117 			cfg.blkorder = 1;
   6118 			blk_shift = 0;
   6119 		} else
   6120 			cfg.blkorder = 0;
   6121 		// fallthrough
   6122 	case 'd': /* Disk usage */
   6123 		if (r == 'd') {
   6124 			if (!cfg.apparentsz)
   6125 				cfg.blkorder ^= 1;
   6126 			cfg.apparentsz = 0;
   6127 			blk_shift = ffs(S_BLKSIZE) - 1;
   6128 		}
   6129 
   6130 		if (cfg.blkorder)
   6131 			cfg.showdetail = 1;
   6132 		cfg.timeorder = 0;
   6133 		cfg.sizeorder = 0;
   6134 		cfg.extnorder = 0;
   6135 		if (!session) {
   6136 			cfg.reverse = 0;
   6137 			entrycmpfn = &entrycmp;
   6138 		}
   6139 		endselection(TRUE); /* We are going to reload dir */
   6140 		break;
   6141 	case 'c':
   6142 		cfg.timeorder = 0;
   6143 		cfg.sizeorder = 0;
   6144 		cfg.apparentsz = 0;
   6145 		cfg.blkorder = 0;
   6146 		cfg.extnorder = 0;
   6147 		cfg.reverse = 0;
   6148 		cfg.version = 0;
   6149 		entrycmpfn = &entrycmp;
   6150 		namecmpfn = &xstricmp;
   6151 		break;
   6152 	case 'e': /* File extension */
   6153 		cfg.extnorder ^= 1;
   6154 		cfg.sizeorder = 0;
   6155 		cfg.timeorder = 0;
   6156 		cfg.apparentsz = 0;
   6157 		cfg.blkorder = 0;
   6158 		cfg.reverse = 0;
   6159 		entrycmpfn = &entrycmp;
   6160 		break;
   6161 	case 'r': /* Reverse sort */
   6162 		cfg.reverse ^= 1;
   6163 		entrycmpfn = cfg.reverse ? &reventrycmp : &entrycmp;
   6164 		break;
   6165 	case 's': /* File size */
   6166 		cfg.sizeorder ^= 1;
   6167 		cfg.timeorder = 0;
   6168 		cfg.apparentsz = 0;
   6169 		cfg.blkorder = 0;
   6170 		cfg.extnorder = 0;
   6171 		cfg.reverse = 0;
   6172 		entrycmpfn = &entrycmp;
   6173 		break;
   6174 	case 't': /* Time */
   6175 		cfg.timeorder ^= 1;
   6176 		cfg.sizeorder = 0;
   6177 		cfg.apparentsz = 0;
   6178 		cfg.blkorder = 0;
   6179 		cfg.extnorder = 0;
   6180 		cfg.reverse = 0;
   6181 		entrycmpfn = &entrycmp;
   6182 		break;
   6183 	case 'v': /* Version */
   6184 		cfg.version ^= 1;
   6185 		namecmpfn = cfg.version ? &xstrverscasecmp : &xstricmp;
   6186 		cfg.timeorder = 0;
   6187 		cfg.sizeorder = 0;
   6188 		cfg.apparentsz = 0;
   6189 		cfg.blkorder = 0;
   6190 		cfg.extnorder = 0;
   6191 		break;
   6192 	default:
   6193 		return 0;
   6194 	}
   6195 
   6196 	if (reverse) {
   6197 		cfg.reverse = 1;
   6198 		entrycmpfn = &reventrycmp;
   6199 	}
   6200 
   6201 	cfgsort[cfg.curctx] = (uchar_t)r;
   6202 
   6203 	return r;
   6204 }
   6205 
   6206 static bool set_time_type(int *presel)
   6207 {
   6208 	bool ret = FALSE;
   6209 	char buf[] = "'a'ccess / 'c'hange / 'm'od [ ]";
   6210 
   6211 	buf[sizeof(buf) - 3] = cfg.timetype == T_MOD ? 'm' : (cfg.timetype == T_ACCESS ? 'a' : 'c');
   6212 
   6213 	int r = get_input(buf);
   6214 
   6215 	if (r == 'a' || r == 'c' || r == 'm') {
   6216 		r = (r == 'm') ? T_MOD : ((r == 'a') ? T_ACCESS : T_CHANGE);
   6217 		if (cfg.timetype != r) {
   6218 			cfg.timetype = r;
   6219 
   6220 			if (cfg.filtermode || g_ctx[cfg.curctx].c_fltr[1])
   6221 				*presel = FILTER;
   6222 
   6223 			ret = TRUE;
   6224 		} else
   6225 			r = MSG_NOCHANGE;
   6226 	} else
   6227 		r = MSG_INVALID_KEY;
   6228 
   6229 	if (!ret)
   6230 		printwait(messages[r], presel);
   6231 
   6232 	return ret;
   6233 }
   6234 
   6235 static void statusbar(char *path)
   6236 {
   6237 	int i = 0, len = 0;
   6238 	char *ptr;
   6239 	pEntry pent = &pdents[cur];
   6240 
   6241 	if (!ndents) {
   6242 		printmsg("0/0");
   6243 		return;
   6244 	}
   6245 
   6246 	/* Get the file extension for regular files */
   6247 	if (S_ISREG(pent->mode)) {
   6248 		i = (int)(pent->nlen - 1);
   6249 		ptr = xextension(pent->name, i);
   6250 		if (ptr)
   6251 			len = i - (ptr - pent->name);
   6252 		if (!ptr || len > 5 || len < 2)
   6253 			ptr = "\b";
   6254 	} else
   6255 		ptr = "\b";
   6256 
   6257 	attron(COLOR_PAIR(cfg.curctx + 1));
   6258 
   6259 	if (cfg.fileinfo && get_output("file", "-b", pdents[cur].name, -1, FALSE, FALSE))
   6260 		mvaddstr(xlines - 2, 2, g_buf);
   6261 
   6262 	tolastln();
   6263 
   6264 	printw("%d/%s ", cur + 1, xitoa(ndents));
   6265 
   6266 	if (g_state.selmode || nselected) {
   6267 		attron(A_REVERSE);
   6268 		addch(' ');
   6269 		if (g_state.rangesel)
   6270 			addch('*');
   6271 		else if (g_state.selmode)
   6272 			addch('+');
   6273 		if (nselected)
   6274 			addstr(xitoa(nselected));
   6275 		addch(' ');
   6276 		attroff(A_REVERSE);
   6277 		addch(' ');
   6278 	}
   6279 
   6280 	if (cfg.blkorder) { /* du mode */
   6281 		char buf[24];
   6282 
   6283 		xstrsncpy(buf, coolsize(dir_blocks << blk_shift), 12);
   6284 
   6285 		printw("%cu:%s free:%s files:%llu %lluB %s\n",
   6286 		       (cfg.apparentsz ? 'a' : 'd'), buf, coolsize(get_fs_info(path, FREE)),
   6287 		       num_files, (ullong_t)pent->blocks << blk_shift, ptr);
   6288 	} else { /* light or detail mode */
   6289 		char sort[] = "\0\0\0\0\0";
   6290 
   6291 		if (getorderstr(sort))
   6292 			addstr(sort);
   6293 
   6294 		/* Timestamp */
   6295 		print_time(&pent->sec);
   6296 
   6297 		addch(' ');
   6298 		addstr(get_lsperms(pent->mode));
   6299 		addch(' ');
   6300 #ifndef NOUG
   6301 		if (g_state.uidgid) {
   6302 			addstr(getpwname(pent->uid));
   6303 			addch(':');
   6304 			addstr(getgrname(pent->gid));
   6305 			addch(' ');
   6306 		}
   6307 #endif
   6308 		if (S_ISLNK(pent->mode)) {
   6309 			if (!cfg.fileinfo) {
   6310 				i = readlink(pent->name, g_buf, PATH_MAX);
   6311 				addstr(coolsize(i >= 0 ? i : pent->size)); /* Show symlink size */
   6312 				if (i > 1) { /* Show symlink target */
   6313 					int y;
   6314 
   6315 					addstr(" ->");
   6316 					getyx(stdscr, len, y);
   6317 					i = MIN(i, xcols - y);
   6318 					g_buf[i] = '\0';
   6319 					addstr(g_buf);
   6320 				}
   6321 			}
   6322 		} else {
   6323 			addstr(coolsize(pent->size));
   6324 			addch(' ');
   6325 			addstr(ptr);
   6326 			if (pent->flags & HARD_LINK) {
   6327 				struct stat sb;
   6328 
   6329 				if (stat(pent->name, &sb) != -1) {
   6330 					addch(' ');
   6331 					addstr(xitoa((int)sb.st_nlink)); /* Show number of links */
   6332 					addch('-');
   6333 					addstr(xitoa((int)sb.st_ino)); /* Show inode number */
   6334 				}
   6335 			}
   6336 		}
   6337 		clrtoeol();
   6338 	}
   6339 
   6340 	attroff(COLOR_PAIR(cfg.curctx + 1));
   6341 	/* Plase HW cursor on current for Braille systems */
   6342 	tocursor();
   6343 }
   6344 
   6345 static inline void markhovered(void)
   6346 {
   6347 	if (cfg.showdetail && ndents) { /* Reversed block for hovered entry */
   6348 		tocursor();
   6349 #ifdef ICONS_ENABLED
   6350 		addstr(MD_ARROW_FORWARD);
   6351 #else
   6352 		addch(' ' | A_REVERSE);
   6353 #endif
   6354 	}
   6355 }
   6356 
   6357 static int adjust_cols(int n)
   6358 {
   6359 	/* Calculate the number of cols available to print entry name */
   6360 #ifdef ICONS_ENABLED
   6361 	n -= (g_state.oldcolor ? 0 : 1 + xstrlen(ICON_PADDING_LEFT) + xstrlen(ICON_PADDING_RIGHT));
   6362 #endif
   6363 	if (cfg.showdetail) {
   6364 		/* Fallback to light mode if less than 35 columns */
   6365 		if (n < 36)
   6366 			cfg.showdetail ^= 1;
   6367 		else /* 2 more accounted for below */
   6368 			n -= 32;
   6369 	}
   6370 
   6371 	/* 2 columns for preceding space and indicator */
   6372 	return (n - 2);
   6373 }
   6374 
   6375 static void draw_line(int ncols)
   6376 {
   6377 	bool dir = FALSE;
   6378 
   6379 	ncols = adjust_cols(ncols);
   6380 
   6381 	if (g_state.oldcolor && (pdents[last].flags & DIR_OR_DIRLNK)) {
   6382 		attron(COLOR_PAIR(cfg.curctx + 1) | A_BOLD);
   6383 		dir = TRUE;
   6384 	}
   6385 
   6386 	move(2 + last - curscroll, 0);
   6387 	printent(&pdents[last], ncols, FALSE);
   6388 
   6389 	if (g_state.oldcolor && (pdents[cur].flags & DIR_OR_DIRLNK)) {
   6390 		if (!dir)  {/* First file is not a directory */
   6391 			attron(COLOR_PAIR(cfg.curctx + 1) | A_BOLD);
   6392 			dir = TRUE;
   6393 		}
   6394 	} else if (dir) { /* Second file is not a directory */
   6395 		attroff(COLOR_PAIR(cfg.curctx + 1) | A_BOLD);
   6396 		dir = FALSE;
   6397 	}
   6398 
   6399 	move(2 + cur - curscroll, 0);
   6400 	printent(&pdents[cur], ncols, TRUE);
   6401 
   6402 	/* Must reset e.g. no files in dir */
   6403 	if (dir)
   6404 		attroff(COLOR_PAIR(cfg.curctx + 1) | A_BOLD);
   6405 
   6406 	markhovered();
   6407 }
   6408 
   6409 static void redraw(char *path)
   6410 {
   6411 	getmaxyx(stdscr, xlines, xcols);
   6412 
   6413 	int ncols = (xcols <= PATH_MAX) ? xcols : PATH_MAX;
   6414 	int onscreen = xlines - 4;
   6415 	int i, j = 1;
   6416 
   6417 	// Fast redraw
   6418 	if (g_state.move) {
   6419 		g_state.move = 0;
   6420 
   6421 		if (ndents && (last_curscroll == curscroll))
   6422 			return draw_line(ncols);
   6423 	}
   6424 
   6425 	DPRINTF_S(__func__);
   6426 
   6427 	/* Clear screen */
   6428 	erase();
   6429 
   6430 	/* Enforce scroll/cursor invariants */
   6431 	move_cursor(cur, 1);
   6432 
   6433 	/* Fail redraw if < than 10 columns, context info prints 10 chars */
   6434 	if (ncols <= MIN_DISPLAY_COL) {
   6435 		printmsg(messages[MSG_FEW_COLUMNS]);
   6436 		return;
   6437 	}
   6438 
   6439 	//DPRINTF_D(cur);
   6440 	DPRINTF_S(path);
   6441 
   6442 	for (i = 0; i < CTX_MAX; ++i) { /* 8 chars printed for contexts - "1 2 3 4 " */
   6443 		if (!g_ctx[i].c_cfg.ctxactive)
   6444 			addch(i + '1');
   6445 		else
   6446 			addch((i + '1') | (COLOR_PAIR(i + 1) | A_BOLD
   6447 				/* active: underline, current: reverse */
   6448 				| ((cfg.curctx != i) ? A_UNDERLINE : A_REVERSE)));
   6449 
   6450 		addch(' ');
   6451 	}
   6452 
   6453 	attron(A_UNDERLINE | COLOR_PAIR(cfg.curctx + 1));
   6454 
   6455 	/* Print path */
   6456 	bool in_home = set_tilde_in_path(path);
   6457 	char *ptr = in_home ? &path[homelen - 1] : path;
   6458 
   6459 	i = (int)xstrlen(ptr);
   6460 	if ((i + MIN_DISPLAY_COL) <= ncols)
   6461 		addnstr(ptr, ncols - MIN_DISPLAY_COL);
   6462 	else {
   6463 		char *base = xmemrchr((uchar_t *)ptr, '/', i);
   6464 
   6465 		if (in_home) {
   6466 			addch(*ptr);
   6467 			++ptr;
   6468 			i = 1;
   6469 		} else
   6470 			i = 0;
   6471 
   6472 		if (ptr && (base != ptr)) {
   6473 			while (ptr < base) {
   6474 				if (*ptr == '/') {
   6475 					i += 2; /* 2 characters added */
   6476 					if (ncols < i + MIN_DISPLAY_COL) {
   6477 						base = NULL; /* Can't print more characters */
   6478 						break;
   6479 					}
   6480 
   6481 					addch(*ptr);
   6482 					addch(*(++ptr));
   6483 				}
   6484 				++ptr;
   6485 			}
   6486 		}
   6487 
   6488 		if (base)
   6489 			addnstr(base, ncols - (MIN_DISPLAY_COL + i));
   6490 	}
   6491 
   6492 	if (in_home)
   6493 		reset_tilde_in_path(path);
   6494 
   6495 	attroff(A_UNDERLINE | COLOR_PAIR(cfg.curctx + 1));
   6496 
   6497 	/* Go to first entry */
   6498 	if (curscroll > 0) {
   6499 		move(1, 0);
   6500 #ifdef ICONS_ENABLED
   6501 		addstr(MD_ARROW_UPWARD);
   6502 #else
   6503 		addch('^');
   6504 #endif
   6505 	}
   6506 
   6507 	if (g_state.oldcolor) {
   6508 		attron(COLOR_PAIR(cfg.curctx + 1) | A_BOLD);
   6509 		g_state.dircolor = 1;
   6510 	}
   6511 
   6512 	onscreen = MIN(onscreen + curscroll, ndents);
   6513 
   6514 	ncols = adjust_cols(ncols);
   6515 
   6516 	int len = scanselforpath(path, FALSE);
   6517 
   6518 	/* Print listing */
   6519 	for (i = curscroll; i < onscreen; ++i) {
   6520 		move(++j, 0);
   6521 
   6522 		if (len)
   6523 			findmarkentry(len, &pdents[i]);
   6524 
   6525 		printent(&pdents[i], ncols, i == cur);
   6526 	}
   6527 
   6528 	/* Must reset e.g. no files in dir */
   6529 	if (g_state.dircolor) {
   6530 		attroff(COLOR_PAIR(cfg.curctx + 1) | A_BOLD);
   6531 		g_state.dircolor = 0;
   6532 	}
   6533 
   6534 	/* Go to last entry */
   6535 	if (onscreen < ndents) {
   6536 		move(xlines - 2, 0);
   6537 #ifdef ICONS_ENABLED
   6538 		addstr(MD_ARROW_DOWNWARD);
   6539 #else
   6540 		addch('v');
   6541 #endif
   6542 	}
   6543 
   6544 	markhovered();
   6545 }
   6546 
   6547 static bool cdprep(char *lastdir, char *lastname, char *path, char *newpath)
   6548 {
   6549 	if (lastname)
   6550 		lastname[0] =  '\0';
   6551 
   6552 	/* Save last working directory */
   6553 	xstrsncpy(lastdir, path, PATH_MAX);
   6554 
   6555 	/* Save the newly opted dir in path */
   6556 	xstrsncpy(path, newpath, PATH_MAX);
   6557 	DPRINTF_S(path);
   6558 
   6559 	clearfilter();
   6560 	return cfg.filtermode;
   6561 }
   6562 
   6563 static bool browse(char *ipath, const char *session, int pkey)
   6564 {
   6565 	char newpath[PATH_MAX] __attribute__ ((aligned)),
   6566 	     rundir[PATH_MAX] __attribute__ ((aligned)),
   6567 	     runfile[NAME_MAX + 1] __attribute__ ((aligned));
   6568 	char *path, *lastdir, *lastname, *dir, *tmp;
   6569 	pEntry pent;
   6570 	enum action sel;
   6571 	struct stat sb;
   6572 	int r = -1, presel, selstartid = 0, selendid = 0;
   6573 	const uchar_t opener_flags = (cfg.cliopener ? F_CLI : (F_NOTRACE | F_NOSTDIN | F_NOWAIT));
   6574 	bool watch = FALSE, cd = TRUE;
   6575 	ino_t inode = 0;
   6576 
   6577 #ifndef NOMOUSE
   6578 	MEVENT event = {0};
   6579 	struct timespec mousetimings[2] = {{.tv_sec = 0, .tv_nsec = 0}, {.tv_sec = 0, .tv_nsec = 0} };
   6580 	int mousedent[2] = {-1, -1};
   6581 	bool currentmouse = 1, rightclicksel = 0;
   6582 #endif
   6583 
   6584 	atexit(dentfree);
   6585 
   6586 	getmaxyx(stdscr, xlines, xcols);
   6587 
   6588 #ifndef NOSSN
   6589 	/* set-up first context */
   6590 	if (!session || !load_session(session, &path, &lastdir, &lastname, FALSE)) {
   6591 #else
   6592 		(void)session;
   6593 #endif
   6594 		g_ctx[0].c_last[0] = '\0';
   6595 		lastdir = g_ctx[0].c_last; /* last visited directory */
   6596 
   6597 		if (g_state.initfile) {
   6598 			xstrsncpy(g_ctx[0].c_name, xbasename(ipath), sizeof(g_ctx[0].c_name));
   6599 			xdirname(ipath);
   6600 		} else
   6601 			g_ctx[0].c_name[0] = '\0';
   6602 
   6603 		lastname = g_ctx[0].c_name; /* last visited file name */
   6604 
   6605 		xstrsncpy(g_ctx[0].c_path, ipath, PATH_MAX);
   6606 		/* If the initial path is a file, retain a way to return to start dir */
   6607 		if (g_state.initfile) {
   6608 			free(initpath);
   6609 			initpath = ipath = getcwd(NULL, 0);
   6610 		}
   6611 		path = g_ctx[0].c_path; /* current directory */
   6612 
   6613 		g_ctx[0].c_fltr[0] = g_ctx[0].c_fltr[1] = '\0';
   6614 		g_ctx[0].c_cfg = cfg; /* current configuration */
   6615 #ifndef NOSSN
   6616 	}
   6617 #endif
   6618 
   6619 	newpath[0] = rundir[0] = runfile[0] = '\0';
   6620 
   6621 	presel = pkey ? ';' : ((cfg.filtermode
   6622 			|| (session && (g_ctx[cfg.curctx].c_fltr[0] == FILTER
   6623 				|| g_ctx[cfg.curctx].c_fltr[0] == RFILTER)
   6624 				&& g_ctx[cfg.curctx].c_fltr[1])) ? FILTER : 0);
   6625 
   6626 	pdents = xrealloc(pdents, total_dents * sizeof(struct entry));
   6627 	if (!pdents)
   6628 		errexit();
   6629 
   6630 	/* Allocate buffer to hold names */
   6631 	pnamebuf = (char *)xrealloc(pnamebuf, NAMEBUF_INCR);
   6632 	if (!pnamebuf)
   6633 		errexit();
   6634 
   6635 begin:
   6636 	/*
   6637 	 * Can fail when permissions change while browsing.
   6638 	 * It's assumed that path IS a directory when we are here.
   6639 	 */
   6640 	if (chdir(path) == -1) {
   6641 		DPRINTF_S("directory inaccessible");
   6642 		valid_parent(path, lastname);
   6643 		setdirwatch();
   6644 	}
   6645 
   6646 #ifndef NOX11
   6647 	xterm_cfg(path);
   6648 #endif
   6649 
   6650 #ifdef LINUX_INOTIFY
   6651 	if ((presel == FILTER || watch) && inotify_wd >= 0) {
   6652 		inotify_rm_watch(inotify_fd, inotify_wd);
   6653 		inotify_wd = -1;
   6654 		watch = FALSE;
   6655 	}
   6656 #elif defined(BSD_KQUEUE)
   6657 	if ((presel == FILTER || watch) && event_fd >= 0) {
   6658 		close(event_fd);
   6659 		event_fd = -1;
   6660 		watch = FALSE;
   6661 	}
   6662 #elif defined(HAIKU_NM)
   6663 	if ((presel == FILTER || watch) && haiku_hnd != NULL) {
   6664 		haiku_stop_watch(haiku_hnd);
   6665 		haiku_nm_active = FALSE;
   6666 		watch = FALSE;
   6667 	}
   6668 #endif
   6669 
   6670 	if (order && cd) {
   6671 		if (cfgsort[cfg.curctx] != '0') {
   6672 			if (cfgsort[cfg.curctx] == 'z')
   6673 				set_sort_flags('c');
   6674 			if ((!cfgsort[cfg.curctx] || (cfgsort[cfg.curctx] == 'c'))
   6675 			    && ((r = get_kv_key(order, path, maxorder, NNN_ORDER)) > 0)) {
   6676 				set_sort_flags(r);
   6677 				cfgsort[cfg.curctx] = 'z';
   6678 			}
   6679 		} else
   6680 			cfgsort[cfg.curctx] = cfgsort[CTX_MAX];
   6681 	}
   6682 	cd = TRUE;
   6683 
   6684 	populate(path, lastname);
   6685 	if (g_state.interrupt) {
   6686 		g_state.interrupt = cfg.apparentsz = cfg.blkorder = 0;
   6687 		blk_shift = BLK_SHIFT_512;
   6688 		presel = CONTROL('L');
   6689 	}
   6690 
   6691 #ifdef LINUX_INOTIFY
   6692 	if (presel != FILTER && inotify_wd == -1)
   6693 		inotify_wd = inotify_add_watch(inotify_fd, path, INOTIFY_MASK);
   6694 #elif defined(BSD_KQUEUE)
   6695 	if (presel != FILTER && event_fd == -1) {
   6696 #if defined(O_EVTONLY)
   6697 		event_fd = open(path, O_EVTONLY);
   6698 #else
   6699 		event_fd = open(path, O_RDONLY);
   6700 #endif
   6701 		if (event_fd >= 0)
   6702 			EV_SET(&events_to_monitor[0], event_fd, EVFILT_VNODE,
   6703 			       EV_ADD | EV_CLEAR, KQUEUE_FFLAGS, 0, path);
   6704 	}
   6705 #elif defined(HAIKU_NM)
   6706 	haiku_nm_active = haiku_watch_dir(haiku_hnd, path) == EXIT_SUCCESS;
   6707 #endif
   6708 
   6709 	while (1) {
   6710 		/* Do not do a double redraw in filterentries */
   6711 		if ((presel != FILTER) || !filterset()) {
   6712 			redraw(path);
   6713 			statusbar(path);
   6714 		}
   6715 
   6716 nochange:
   6717 		/* Exit if parent has exited */
   6718 		if (getppid() == 1)
   6719 			_exit(EXIT_FAILURE);
   6720 
   6721 		/* If CWD is deleted or moved or perms changed, find an accessible parent */
   6722 		if (chdir(path) == -1)
   6723 			goto begin;
   6724 
   6725 		/* If STDIN is no longer a tty (closed) we should exit */
   6726 		if (!isatty(STDIN_FILENO) && !g_state.picker)
   6727 			return EXIT_FAILURE;
   6728 
   6729 		sel = nextsel(presel);
   6730 		if (presel)
   6731 			presel = 0;
   6732 
   6733 		switch (sel) {
   6734 #ifndef NOMOUSE
   6735 		case SEL_CLICK:
   6736 			if (getmouse(&event) != OK)
   6737 				goto nochange;
   6738 
   6739 			/* Handle clicking on a context at the top */
   6740 			if (event.bstate == BUTTON1_PRESSED && event.y == 0) {
   6741 				/* Get context from: "[1 2 3 4]..." */
   6742 				r = event.x >> 1;
   6743 
   6744 				/* If clicked after contexts, go to parent */
   6745 				if (r >= CTX_MAX)
   6746 					sel = SEL_BACK;
   6747 				else if (r >= 0 && r != cfg.curctx) {
   6748 					savecurctx(path, ndents ? pdents[cur].name : NULL, r);
   6749 
   6750 					/* Reset the pointers */
   6751 					path = g_ctx[r].c_path;
   6752 					lastdir = g_ctx[r].c_last;
   6753 					lastname = g_ctx[r].c_name;
   6754 
   6755 					setdirwatch();
   6756 					goto begin;
   6757 				}
   6758 			}
   6759 #endif
   6760 			// fallthrough
   6761 		case SEL_BACK:
   6762 #ifndef NOMOUSE
   6763 			if (sel == SEL_BACK) {
   6764 #endif
   6765 				dir = visit_parent(path, newpath, &presel);
   6766 				if (!dir)
   6767 					goto nochange;
   6768 
   6769 				/* Save history */
   6770 				xstrsncpy(lastname, xbasename(path), NAME_MAX + 1);
   6771 
   6772 				cdprep(lastdir, NULL, path, dir) ? (presel = FILTER) : (watch = TRUE);
   6773 				goto begin;
   6774 #ifndef NOMOUSE
   6775 			}
   6776 #endif
   6777 
   6778 #ifndef NOMOUSE
   6779 			/* Middle click action */
   6780 			if (event.bstate == BUTTON2_PRESSED) {
   6781 				presel = middle_click_key;
   6782 				goto nochange;
   6783 			}
   6784 #if NCURSES_MOUSE_VERSION > 1
   6785 			/* Scroll up */
   6786 			if (event.bstate == BUTTON4_PRESSED && ndents && (cfg.rollover || cur)) {
   6787 				move_cursor((!cfg.rollover && cur < scroll_lines
   6788 						? 0 : (cur + ndents - scroll_lines) % ndents), 0);
   6789 				break;
   6790 			}
   6791 
   6792 			/* Scroll down */
   6793 			if (event.bstate == BUTTON5_PRESSED && ndents
   6794 			    && (cfg.rollover || (cur != ndents - 1))) {
   6795 				move_cursor((!cfg.rollover && cur >= ndents - scroll_lines)
   6796 						? (ndents - 1) : ((cur + scroll_lines) % ndents), 0);
   6797 				break;
   6798 			}
   6799 #endif
   6800 
   6801 			/* Toggle filter mode on left click on last 2 lines */
   6802 			if (event.y >= xlines - 2 && event.bstate == BUTTON1_PRESSED) {
   6803 				clearfilter();
   6804 				cfg.filtermode ^= 1;
   6805 				if (cfg.filtermode) {
   6806 					presel = FILTER;
   6807 					goto nochange;
   6808 				}
   6809 
   6810 				/* Start watching the directory */
   6811 				watch = TRUE;
   6812 				copycurname();
   6813 				cd = FALSE;
   6814 				goto begin;
   6815 			}
   6816 
   6817 			/* Handle clicking on a file */
   6818 			if (event.y >= 2 && event.y <= ndents + 1 &&
   6819 					(event.bstate == BUTTON1_PRESSED ||
   6820 					 event.bstate == BUTTON3_PRESSED)) {
   6821 				r = curscroll + (event.y - 2);
   6822 				if (r != cur)
   6823 					move_cursor(r, 1);
   6824 #ifndef NOFIFO
   6825 				else if ((event.bstate == BUTTON1_PRESSED) && !g_state.fifomode)
   6826 					notify_fifo(TRUE); /* Send clicked path to NNN_FIFO */
   6827 #endif
   6828 				/* Handle right click selection */
   6829 				if (event.bstate == BUTTON3_PRESSED) {
   6830 					rightclicksel = 1;
   6831 					presel = SELECT;
   6832 					goto nochange;
   6833 				}
   6834 
   6835 				currentmouse ^= 1;
   6836 				clock_gettime(
   6837 #if defined(CLOCK_MONOTONIC_RAW)
   6838 				    CLOCK_MONOTONIC_RAW,
   6839 #elif defined(CLOCK_MONOTONIC)
   6840 				    CLOCK_MONOTONIC,
   6841 #else
   6842 				    CLOCK_REALTIME,
   6843 #endif
   6844 				    &mousetimings[currentmouse]);
   6845 				mousedent[currentmouse] = cur;
   6846 
   6847 				/* Single click just selects, double click falls through to SEL_OPEN */
   6848 				if ((mousedent[0] != mousedent[1]) ||
   6849 				  (((_ABSSUB(mousetimings[0].tv_sec, mousetimings[1].tv_sec) << 30)
   6850 				  + (_ABSSUB(mousetimings[0].tv_nsec, mousetimings[1].tv_nsec)))
   6851 					> DBLCLK_INTERVAL_NS))
   6852 					break;
   6853 				/* Double click */
   6854 				mousetimings[currentmouse].tv_sec = 0;
   6855 				mousedent[currentmouse] = -1;
   6856 				sel = SEL_OPEN;
   6857 			} else {
   6858 				if (cfg.filtermode || filterset())
   6859 					presel = FILTER;
   6860 				copycurname();
   6861 				goto nochange;
   6862 			}
   6863 #endif
   6864 			// fallthrough
   6865 		case SEL_NAV_IN: // fallthrough
   6866 		case SEL_OPEN:
   6867 			/* Cannot descend in empty directories */
   6868 			if (!ndents) {
   6869 				cd = FALSE;
   6870 				goto begin;
   6871 			}
   6872 
   6873 			pent = &pdents[cur];
   6874 			mkpath(path, pent->name, newpath);
   6875 			DPRINTF_S(newpath);
   6876 
   6877 			/* Visit directory */
   6878 			if (pent->flags & DIR_OR_DIRLNK) {
   6879 				if (chdir(newpath) == -1) {
   6880 					printwarn(&presel);
   6881 					goto nochange;
   6882 				}
   6883 
   6884 				cdprep(lastdir, lastname, path, newpath)
   6885 					? (presel = FILTER) : (watch = TRUE);
   6886 				goto begin;
   6887 			}
   6888 
   6889 			/* Cannot use stale data in entry, file may be missing by now */
   6890 			if (stat(newpath, &sb) == -1) {
   6891 				printwarn(&presel);
   6892 				goto nochange;
   6893 			}
   6894 			DPRINTF_U(sb.st_mode);
   6895 
   6896 			/* Do not open non-regular files */
   6897 			if (!S_ISREG(sb.st_mode)) {
   6898 				printwait(messages[MSG_UNSUPPORTED], &presel);
   6899 				goto nochange;
   6900                         }
   6901 
   6902 			/* Handle plugin selection mode */
   6903 			if (g_state.runplugin) {
   6904 				g_state.runplugin = 0;
   6905 				/* Must be in plugin dir and same context to select plugin */
   6906 				if ((g_state.runctx == cfg.curctx) && !strcmp(path, plgpath)) {
   6907 					endselection(FALSE);
   6908 					/* Copy path so we can return back to earlier dir */
   6909 					xstrsncpy(path, rundir, PATH_MAX);
   6910 					rundir[0] = '\0';
   6911 					clearfilter();
   6912 
   6913 					if (chdir(path) == -1
   6914 					    || !run_plugin(&path, pent->name,
   6915 								    runfile, &lastname, &lastdir)) {
   6916 						DPRINTF_S("plugin failed!");
   6917 					}
   6918 
   6919 					if (g_state.picked)
   6920 						return EXIT_SUCCESS;
   6921 
   6922 					if (runfile[0]) {
   6923 						xstrsncpy(lastname, runfile, NAME_MAX + 1);
   6924 						runfile[0] = '\0';
   6925 					}
   6926 					setdirwatch();
   6927 					goto begin;
   6928 				}
   6929 			}
   6930 
   6931 #ifndef NOFIFO
   6932 			if (g_state.fifomode && (sel == SEL_OPEN)) {
   6933 				send_to_explorer(&presel); /* Write selection to explorer fifo */
   6934 				break;
   6935 			}
   6936 #endif
   6937 			/* If opened as vim plugin and Enter/^M pressed, pick */
   6938 			if (g_state.picker && (sel == SEL_OPEN)) {
   6939 				if (nselected == 0) /* Pick if none selected */
   6940 					appendfpath(newpath, mkpath(path, pent->name, newpath));
   6941 				return EXIT_SUCCESS;
   6942 			}
   6943 
   6944 			if (sel == SEL_NAV_IN) {
   6945 				/* If in listing dir, go to target on `l` or Right on symlink */
   6946 				if (listpath && S_ISLNK(pent->mode)
   6947 				    && is_prefix(path, listpath, xstrlen(listpath))) {
   6948 					if (!realpath(pent->name, newpath)) {
   6949 						printwarn(&presel);
   6950 						goto nochange;
   6951 					}
   6952 
   6953 					xdirname(newpath);
   6954 
   6955 					if (chdir(newpath) == -1) {
   6956 						printwarn(&presel);
   6957 						goto nochange;
   6958 					}
   6959 
   6960 					cdprep(lastdir, NULL, path, newpath)
   6961 					       ? (presel = FILTER) : (watch = TRUE);
   6962 					xstrsncpy(lastname, pent->name, NAME_MAX + 1);
   6963 					goto begin;
   6964 				}
   6965 
   6966 				/* Open file disabled on right arrow or `l` */
   6967 				if (cfg.nonavopen)
   6968 					goto nochange;
   6969 			}
   6970 
   6971 			if (!sb.st_size) {
   6972 				printwait(messages[MSG_EMPTY_FILE], &presel);
   6973 				goto nochange;
   6974 			}
   6975 
   6976 			if (cfg.useeditor
   6977 #ifdef FILE_MIME_OPTS
   6978 			    && get_output("file", FILE_MIME_OPTS, newpath, -1, FALSE, FALSE)
   6979 			    && is_prefix(g_buf, "text/", 5)
   6980 #else
   6981 			    /* no MIME option; guess from description instead */
   6982 			    && get_output("file", "-bL", newpath, -1, FALSE, FALSE)
   6983 			    && strstr(g_buf, "text")
   6984 #endif
   6985 			) {
   6986 				spawn(editor, newpath, NULL, NULL, F_CLI);
   6987 				if (cfg.filtermode) {
   6988 					presel = FILTER;
   6989 					clearfilter();
   6990 				}
   6991 				continue;
   6992 			}
   6993 
   6994 			/* Get the extension for regex match */
   6995 			tmp = xextension(pent->name, pent->nlen - 1);
   6996 #ifdef PCRE
   6997 			if (tmp && !pcre_exec(archive_pcre, NULL, tmp,
   6998 					      pent->nlen - (tmp - pent->name) - 1, 0, 0, NULL, 0)) {
   6999 #else
   7000 			if (tmp && !regexec(&archive_re, tmp, 0, NULL, 0)) {
   7001 #endif
   7002 				r = get_input(messages[MSG_ARCHIVE_OPTS]);
   7003 				if (r == 'l' || r == 'x') {
   7004 					mkpath(path, pent->name, newpath);
   7005 					if (!handle_archive(newpath, r)) {
   7006 						presel = MSGWAIT;
   7007 						goto nochange;
   7008 					}
   7009 					if (r == 'l') {
   7010 						statusbar(path);
   7011 						goto nochange;
   7012 					}
   7013 				}
   7014 
   7015 				if ((r == 'm') && !archive_mount(newpath)) {
   7016 					presel = MSGWAIT;
   7017 					goto nochange;
   7018 				}
   7019 
   7020 				if (r == 'x' || r == 'm') {
   7021 					if (newpath[0])
   7022 						set_smart_ctx('+', newpath, &path,
   7023 							      ndents ? pdents[cur].name : NULL,
   7024 							      &lastname, &lastdir);
   7025 					else
   7026 						copycurname();
   7027 					clearfilter();
   7028 					goto begin;
   7029 				}
   7030 
   7031 				if (r != 'o') {
   7032 					printwait(messages[MSG_INVALID_KEY], &presel);
   7033 					goto nochange;
   7034 				}
   7035 			}
   7036 
   7037 			/* Invoke desktop opener as last resort */
   7038 			spawn(opener, newpath, NULL, NULL, opener_flags);
   7039 
   7040 			/* Move cursor to the next entry if not the last entry */
   7041 			if (g_state.autonext && cur != ndents - 1)
   7042 				move_cursor((cur + 1) % ndents, 0);
   7043 			if (cfg.filtermode) {
   7044 				presel = FILTER;
   7045 				clearfilter();
   7046 			}
   7047 			continue;
   7048 		case SEL_NEXT: // fallthrough
   7049 		case SEL_PREV: // fallthrough
   7050 		case SEL_PGDN: // fallthrough
   7051 		case SEL_CTRL_D: // fallthrough
   7052 		case SEL_PGUP: // fallthrough
   7053 		case SEL_CTRL_U: // fallthrough
   7054 		case SEL_HOME: // fallthrough
   7055 		case SEL_END: // fallthrough
   7056 		case SEL_FIRST:
   7057 			if (ndents) {
   7058 				g_state.move = 1;
   7059 				handle_screen_move(sel);
   7060 			}
   7061 			break;
   7062 		case SEL_CDHOME: // fallthrough
   7063 		case SEL_CDBEGIN: // fallthrough
   7064 		case SEL_CDLAST: // fallthrough
   7065 		case SEL_CDROOT:
   7066 			dir = (sel == SEL_CDHOME) ? home
   7067 				: ((sel == SEL_CDBEGIN) ? ipath
   7068 				: ((sel == SEL_CDLAST) ? lastdir
   7069 				: "/" /* SEL_CDROOT */));
   7070 
   7071 			if (!dir || !*dir) {
   7072 				printwait(messages[MSG_NOT_SET], &presel);
   7073 				goto nochange;
   7074 			}
   7075 
   7076 			if (strcmp(path, dir) == 0) {
   7077 				if (dir == ipath) {
   7078 					if (cfg.filtermode)
   7079 						presel = FILTER;
   7080 					goto nochange;
   7081 				}
   7082 				dir = lastdir; /* Go to last dir on home/root key repeat */
   7083 			}
   7084 
   7085 			if (chdir(dir) == -1) {
   7086 				presel = MSGWAIT;
   7087 				goto nochange;
   7088 			}
   7089 
   7090 			/* SEL_CDLAST: dir pointing to lastdir */
   7091 			xstrsncpy(newpath, dir, PATH_MAX); // fallthrough
   7092 		case SEL_BMOPEN:
   7093 			if (sel == SEL_BMOPEN) {
   7094 				r = (int)handle_bookmark(mark, newpath);
   7095 				if (r) {
   7096 					printwait(messages[r], &presel);
   7097 					goto nochange;
   7098 				}
   7099 
   7100 				if (strcmp(path, newpath) == 0)
   7101 					break;
   7102 			}
   7103 
   7104 			/* In list mode, retain the last file name to highlight it, if possible */
   7105 			cdprep(lastdir, listpath && sel == SEL_CDLAST ? NULL : lastname, path, newpath)
   7106 			       ? (presel = FILTER) : (watch = TRUE);
   7107 			goto begin;
   7108 		case SEL_REMOTE:
   7109 			if ((sel == SEL_REMOTE) && !remote_mount(newpath)) {
   7110 				presel = MSGWAIT;
   7111 				goto nochange;
   7112 			}
   7113 
   7114 			set_smart_ctx('+', newpath, &path,
   7115 				      ndents ? pdents[cur].name : NULL, &lastname, &lastdir);
   7116 			clearfilter();
   7117 			goto begin;
   7118 		case SEL_CYCLE: // fallthrough
   7119 		case SEL_CYCLER: // fallthrough
   7120 		case SEL_CTX1: // fallthrough
   7121 		case SEL_CTX2: // fallthrough
   7122 		case SEL_CTX3: // fallthrough
   7123 		case SEL_CTX4:
   7124 #ifdef CTX8
   7125 		case SEL_CTX5:
   7126 		case SEL_CTX6:
   7127 		case SEL_CTX7:
   7128 		case SEL_CTX8:
   7129 #endif
   7130 			r = handle_context_switch(sel);
   7131 			if (r < 0)
   7132 				continue;
   7133 			savecurctx(path, ndents ? pdents[cur].name : NULL, r);
   7134 
   7135 			/* Reset the pointers */
   7136 			path = g_ctx[r].c_path;
   7137 			lastdir = g_ctx[r].c_last;
   7138 			lastname = g_ctx[r].c_name;
   7139 			tmp = g_ctx[r].c_fltr;
   7140 
   7141 			if (cfg.filtermode || ((tmp[0] == FILTER || tmp[0] == RFILTER) && tmp[1]))
   7142 				presel = FILTER;
   7143 			else
   7144 				watch = TRUE;
   7145 
   7146 			goto begin;
   7147 		case SEL_MARK:
   7148 			free(mark);
   7149 			mark = xstrdup(path);
   7150 			printwait(mark, &presel);
   7151 			goto nochange;
   7152 		case SEL_BMARK:
   7153 			add_bookmark(path, newpath, &presel);
   7154 			goto nochange;
   7155 		case SEL_FLTR:
   7156 			/* Unwatch dir if we are still in a filtered view */
   7157 #ifdef LINUX_INOTIFY
   7158 			if (inotify_wd >= 0) {
   7159 				inotify_rm_watch(inotify_fd, inotify_wd);
   7160 				inotify_wd = -1;
   7161 			}
   7162 #elif defined(BSD_KQUEUE)
   7163 			if (event_fd >= 0) {
   7164 				close(event_fd);
   7165 				event_fd = -1;
   7166 			}
   7167 #elif defined(HAIKU_NM)
   7168 			if (haiku_nm_active) {
   7169 				haiku_stop_watch(haiku_hnd);
   7170 				haiku_nm_active = FALSE;
   7171 			}
   7172 #endif
   7173 			presel = filterentries(path, lastname);
   7174 			if (presel == ESC) {
   7175 				presel = 0;
   7176 				break;
   7177 			}
   7178 			if (presel == FILTER) { /* Refresh dir and filter again */
   7179 				cd = FALSE;
   7180 				goto begin;
   7181 			}
   7182 			goto nochange;
   7183 		case SEL_MFLTR: // fallthrough
   7184 		case SEL_HIDDEN: // fallthrough
   7185 		case SEL_DETAIL: // fallthrough
   7186 		case SEL_SORT:
   7187 			switch (sel) {
   7188 			case SEL_MFLTR:
   7189 				cfg.filtermode ^= 1;
   7190 				if (cfg.filtermode) {
   7191 					presel = FILTER;
   7192 					clearfilter();
   7193 					goto nochange;
   7194 				}
   7195 
   7196 				watch = TRUE; // fallthrough
   7197 			case SEL_HIDDEN:
   7198 				if (sel == SEL_HIDDEN) {
   7199 					cfg.showhidden ^= 1;
   7200 					if (cfg.filtermode)
   7201 						presel = FILTER;
   7202 					clearfilter();
   7203 				}
   7204 				copycurname();
   7205 				cd = FALSE;
   7206 				goto begin;
   7207 			case SEL_DETAIL:
   7208 				cfg.showdetail ^= 1;
   7209 				cfg.blkorder = 0;
   7210 				continue;
   7211 			default: /* SEL_SORT */
   7212 				r = set_sort_flags(get_input(messages[MSG_ORDER]));
   7213 				if (!r) {
   7214 					printwait(messages[MSG_INVALID_KEY], &presel);
   7215 					goto nochange;
   7216 				}
   7217 			}
   7218 
   7219 			if (cfg.filtermode || filterset())
   7220 				presel = FILTER;
   7221 
   7222 			if (ndents) {
   7223 				copycurname();
   7224 
   7225 				if (r == 'd' || r == 'a') {
   7226 					presel = 0;
   7227 					goto begin;
   7228 				}
   7229 
   7230 				ENTSORT(pdents, ndents, entrycmpfn);
   7231 				move_cursor(ndents ? dentfind(lastname, ndents) : 0, 0);
   7232 			}
   7233 			continue;
   7234 		case SEL_STATS: // fallthrough
   7235 		case SEL_CHMODX:
   7236 			if (ndents) {
   7237 				tmp = (listpath && xstrcmp(path, listpath) == 0) ? listroot : path;
   7238 				mkpath(tmp, pdents[cur].name, newpath);
   7239 
   7240 				if ((sel == SEL_STATS && !show_stats(newpath))
   7241 				    || (lstat(newpath, &sb) == -1)
   7242 				    || (sel == SEL_CHMODX && !xchmod(newpath, sb.st_mode))) {
   7243 					printwarn(&presel);
   7244 					goto nochange;
   7245 				}
   7246 
   7247 				if (sel == SEL_CHMODX)
   7248 					pdents[cur].mode ^= 0111;
   7249 			}
   7250 			break;
   7251 		case SEL_REDRAW: // fallthrough
   7252 		case SEL_RENAMEMUL: // fallthrough
   7253 		case SEL_HELP: // fallthrough
   7254 		case SEL_AUTONEXT: // fallthrough
   7255 		case SEL_EDIT: // fallthrough
   7256 		case SEL_LOCK:
   7257 		{
   7258 			bool refresh = FALSE;
   7259 
   7260 			if (ndents)
   7261 				mkpath(path, pdents[cur].name, newpath);
   7262 			else if (sel == SEL_EDIT) /* Avoid trying to edit a non-existing file */
   7263 				goto nochange;
   7264 
   7265 			switch (sel) {
   7266 			case SEL_REDRAW:
   7267 				refresh = TRUE;
   7268 				break;
   7269 			case SEL_RENAMEMUL:
   7270 				endselection(TRUE);
   7271 				setenv("NNN_INCLUDE_HIDDEN", xitoa(cfg.showhidden), 1);
   7272 				setenv("NNN_LIST", listpath ? listroot : "", 1);
   7273 
   7274 				if (!(getutil(utils[UTIL_BASH])
   7275 				      && plugscript(utils[UTIL_NMV], F_CLI))
   7276 #ifndef NOBATCH
   7277 				    && !batch_rename()
   7278 #endif
   7279 				) {
   7280 					printwait(messages[MSG_FAILED], &presel);
   7281 					goto nochange;
   7282 				}
   7283 				clearselection();
   7284 				refresh = TRUE;
   7285 				break;
   7286 			case SEL_HELP:
   7287 				show_help(path); // fallthrough
   7288 			case SEL_AUTONEXT:
   7289 				if (sel == SEL_AUTONEXT)
   7290 					g_state.autonext ^= 1;
   7291 				if (cfg.filtermode)
   7292 					presel = FILTER;
   7293 				copycurname();
   7294 				goto nochange;
   7295 			case SEL_EDIT:
   7296 				if (!g_state.picker)
   7297 					spawn(editor, newpath, NULL, NULL, F_CLI);
   7298 				continue;
   7299 			default: /* SEL_LOCK */
   7300 				lock_terminal();
   7301 				break;
   7302 			}
   7303 
   7304 			/* In case of successful operation, reload contents */
   7305 
   7306 			/* Continue in type-to-nav mode, if enabled */
   7307 			if ((cfg.filtermode || filterset()) && !refresh) {
   7308 				presel = FILTER;
   7309 				goto nochange;
   7310 			}
   7311 
   7312 			/* Save current */
   7313 			copycurname();
   7314 			/* Repopulate as directory content may have changed */
   7315 			cd = FALSE;
   7316 			goto begin;
   7317 		}
   7318 		case SEL_SEL:
   7319 			if (!ndents)
   7320 				goto nochange;
   7321 
   7322 			startselection();
   7323 			if (g_state.rangesel)
   7324 				g_state.rangesel = 0;
   7325 
   7326 			/* Toggle selection status */
   7327 			pdents[cur].flags ^= FILE_SELECTED;
   7328 
   7329 			if (pdents[cur].flags & FILE_SELECTED) {
   7330 				++nselected;
   7331 				appendfpath(newpath, mkpath(path, pdents[cur].name, newpath));
   7332 				writesel(pselbuf, selbufpos - 1); /* Truncate NULL from end */
   7333 			} else {
   7334 				--nselected;
   7335 				rmfromselbuf(mkpath(path, pdents[cur].name, g_sel));
   7336 			}
   7337 
   7338 #ifndef NOX11
   7339 			if (cfg.x11)
   7340 				plugscript(utils[UTIL_CBCP], F_NOWAIT | F_NOTRACE);
   7341 #endif
   7342 #ifndef NOMOUSE
   7343 			if (rightclicksel)
   7344 				rightclicksel = 0;
   7345 			else
   7346 #endif
   7347 				/* move cursor to the next entry if this is not the last entry */
   7348 				if (!g_state.stayonsel && (cur != ndents - 1))
   7349 					move_cursor((cur + 1) % ndents, 0);
   7350 			break;
   7351 		case SEL_SELMUL:
   7352 			if (!ndents)
   7353 				goto nochange;
   7354 
   7355 			startselection();
   7356 			g_state.rangesel ^= 1;
   7357 
   7358 			if (stat(path, &sb) == -1) {
   7359 				printwarn(&presel);
   7360 				goto nochange;
   7361 			}
   7362 
   7363 			if (g_state.rangesel) { /* Range selection started */
   7364 				inode = sb.st_ino;
   7365 				selstartid = cur;
   7366 				continue;
   7367 			}
   7368 
   7369 			if (inode != sb.st_ino) {
   7370 				printwait(messages[MSG_DIR_CHANGED], &presel);
   7371 				goto nochange;
   7372 			}
   7373 
   7374 			if (cur < selstartid) {
   7375 				selendid = selstartid;
   7376 				selstartid = cur;
   7377 			} else
   7378 				selendid = cur;
   7379 
   7380 			/* Clear selection on repeat on same file */
   7381 			if (selstartid == selendid) {
   7382 				resetselind();
   7383 				clearselection();
   7384 				break;
   7385 			} // fallthrough
   7386 		case SEL_SELALL: // fallthrough
   7387 		case SEL_SELINV:
   7388 			if (sel == SEL_SELALL || sel == SEL_SELINV) {
   7389 				if (!ndents)
   7390 					goto nochange;
   7391 
   7392 				startselection();
   7393 				if (g_state.rangesel)
   7394 					g_state.rangesel = 0;
   7395 
   7396 				selstartid = 0;
   7397 				selendid = ndents - 1;
   7398 			}
   7399 
   7400 			if ((nselected > LARGESEL) || (nselected && (ndents > LARGESEL))) {
   7401 				printmsg("processing...");
   7402 				refresh();
   7403 			}
   7404 
   7405 			r = scanselforpath(path, TRUE); /* Get path length suffixed by '/' */
   7406 			((sel == SEL_SELINV) && findselpos)
   7407 				? invertselbuf(r) : addtoselbuf(r, selstartid, selendid);
   7408 
   7409 #ifndef NOX11
   7410 			if (cfg.x11)
   7411 				plugscript(utils[UTIL_CBCP], F_NOWAIT | F_NOTRACE);
   7412 #endif
   7413 			continue;
   7414 		case SEL_SELEDIT:
   7415 			r = editselection();
   7416 			if (r <= 0) {
   7417 				r = !r ? MSG_0_SELECTED : MSG_FAILED;
   7418 				printwait(messages[r], &presel);
   7419 			} else {
   7420 #ifndef NOX11
   7421 				if (cfg.x11)
   7422 					plugscript(utils[UTIL_CBCP], F_NOWAIT | F_NOTRACE);
   7423 #endif
   7424 				cfg.filtermode ?  presel = FILTER : statusbar(path);
   7425 			}
   7426 			goto nochange;
   7427 		case SEL_CP: // fallthrough
   7428 		case SEL_MV: // fallthrough
   7429 		case SEL_CPMVAS: // fallthrough
   7430 		case SEL_RM:
   7431 		{
   7432 			if (sel == SEL_RM) {
   7433 				r = get_cur_or_sel();
   7434 				if (!r) {
   7435 					statusbar(path);
   7436 					goto nochange;
   7437 				}
   7438 
   7439 				if (r == 'c') {
   7440 					tmp = (listpath && xstrcmp(path, listpath) == 0)
   7441 					      ? listroot : path;
   7442 					mkpath(tmp, pdents[cur].name, newpath);
   7443 					if (!xrm(newpath))
   7444 						continue;
   7445 
   7446 					xrmfromsel(tmp, newpath);
   7447 
   7448 					copynextname(lastname);
   7449 
   7450 					if (cfg.filtermode || filterset())
   7451 						presel = FILTER;
   7452 					cd = FALSE;
   7453 					goto begin;
   7454 				}
   7455 			}
   7456 
   7457 			(nselected == 1 && (sel == SEL_CP || sel == SEL_MV))
   7458 				? mkpath(path, xbasename(pselbuf), newpath)
   7459 				: (newpath[0] = '\0');
   7460 
   7461 			endselection(TRUE);
   7462 
   7463 			if (!cpmvrm_selection(sel, path)) {
   7464 				presel = MSGWAIT;
   7465 				goto nochange;
   7466 			}
   7467 
   7468 			if (cfg.filtermode)
   7469 				presel = FILTER;
   7470 			clearfilter();
   7471 
   7472 #ifndef NOX11
   7473 			/* Show notification on operation complete */
   7474 			if (cfg.x11)
   7475 				plugscript(utils[UTIL_NTFY], F_NOWAIT | F_NOTRACE);
   7476 #endif
   7477 
   7478 			if (newpath[0] && !access(newpath, F_OK))
   7479 				xstrsncpy(lastname, xbasename(newpath), NAME_MAX+1);
   7480 			else
   7481 				copycurname();
   7482 			cd = FALSE;
   7483 			goto begin;
   7484 		}
   7485 		case SEL_ARCHIVE: // fallthrough
   7486 		case SEL_OPENWITH: // fallthrough
   7487 		case SEL_NEW: // fallthrough
   7488 		case SEL_RENAME:
   7489 		{
   7490 			int fd, ret = 'n';
   7491 
   7492 			if (!ndents && (sel == SEL_OPENWITH || sel == SEL_RENAME))
   7493 				break;
   7494 
   7495 			if (sel != SEL_OPENWITH)
   7496 				endselection(TRUE);
   7497 
   7498 			switch (sel) {
   7499 			case SEL_ARCHIVE:
   7500 				r = get_cur_or_sel();
   7501 				if (!r) {
   7502 					statusbar(path);
   7503 					goto nochange;
   7504 				}
   7505 
   7506 				if (r == 's') {
   7507 					if (!selsafe()) {
   7508 						presel = MSGWAIT;
   7509 						goto nochange;
   7510 					}
   7511 
   7512 					tmp = NULL;
   7513 				} else
   7514 					tmp = pdents[cur].name;
   7515 
   7516 				tmp = xreadline(tmp, messages[MSG_ARCHIVE_NAME]);
   7517 				break;
   7518 			case SEL_OPENWITH:
   7519 #ifndef NORL
   7520 				if (g_state.picker) {
   7521 #endif
   7522 					tmp = xreadline(NULL, messages[MSG_OPEN_WITH]);
   7523 #ifndef NORL
   7524 				} else
   7525 					tmp = getreadline(messages[MSG_OPEN_WITH]);
   7526 #endif
   7527 				break;
   7528 			case SEL_NEW:
   7529 				r = get_input(messages[MSG_NEW_OPTS]);
   7530 				if (r == 'f' || r == 'd')
   7531 					tmp = xreadline(NULL, messages[MSG_NEW_PATH]);
   7532 				else if (r == 's' || r == 'h')
   7533 					tmp = xreadline(NULL,
   7534 						messages[nselected <= 1?MSG_NEW_PATH:MSG_LINK_PREFIX]);
   7535 				else
   7536 					tmp = NULL;
   7537 				break;
   7538 			default: /* SEL_RENAME */
   7539 				tmp = xreadline(pdents[cur].name, "");
   7540 				break;
   7541 			}
   7542 
   7543 			if (!tmp || !*tmp)
   7544 				break;
   7545 
   7546 			switch (sel) {
   7547 			case SEL_ARCHIVE:
   7548 				if (r == 'c' && strcmp(tmp, pdents[cur].name) == 0)
   7549 					goto nochange;
   7550 
   7551 				mkpath(path, tmp, newpath);
   7552 				if (access(newpath, F_OK) == 0) {
   7553 					if (!xconfirm(get_input(messages[MSG_OVERWRITE]))) {
   7554 						statusbar(path);
   7555 						goto nochange;
   7556 					}
   7557 				}
   7558 				get_archive_cmd(newpath, tmp);
   7559 				(r == 's') ? archive_selection(newpath, tmp, path)
   7560 					   : spawn(newpath, tmp, pdents[cur].name,
   7561 						   NULL, F_CLI | F_CONFIRM);
   7562 
   7563 				mkpath(path, tmp, newpath);
   7564 				if (access(newpath, F_OK) == 0) { /* File created */
   7565 					xstrsncpy(lastname, tmp, NAME_MAX + 1);
   7566 					clearfilter(); /* Archive name may not match */
   7567 					clearselection(); /* Archive operation complete */
   7568 					cd = FALSE;
   7569 					goto begin;
   7570 				}
   7571 				continue;
   7572 			case SEL_OPENWITH:
   7573 				handle_openwith(path, pdents[cur].name, newpath, tmp);
   7574 
   7575 				cfg.filtermode ?  presel = FILTER : statusbar(path);
   7576 				copycurname();
   7577 				goto nochange;
   7578 			case SEL_RENAME:
   7579 				/* Skip renaming to same name */
   7580 				if (strcmp(tmp, pdents[cur].name) == 0) {
   7581 					tmp = xreadline(pdents[cur].name, messages[MSG_COPY_NAME]);
   7582 					if (!tmp || !tmp[0] || !strcmp(tmp, pdents[cur].name)) {
   7583 						cfg.filtermode ?  presel = FILTER : statusbar(path);
   7584 						copycurname();
   7585 						goto nochange;
   7586 					}
   7587 					ret = 'd';
   7588 				}
   7589 				break;
   7590 			default: /* SEL_NEW */
   7591 				break;
   7592 			}
   7593 
   7594 			/* Open the descriptor to currently open directory */
   7595 #ifdef O_DIRECTORY
   7596 			fd = open(path, O_RDONLY | O_DIRECTORY);
   7597 #else
   7598 			fd = open(path, O_RDONLY);
   7599 #endif
   7600 			if (fd == -1) {
   7601 				printwarn(&presel);
   7602 				goto nochange;
   7603 			}
   7604 
   7605 			/* Check if another file with same name exists */
   7606 			if (fstatat(fd, tmp, &sb, AT_SYMLINK_NOFOLLOW) == 0) {
   7607 				if (sel == SEL_RENAME) {
   7608 					/* Overwrite file with same name? */
   7609 					if (!xconfirm(get_input(messages[MSG_OVERWRITE]))) {
   7610 						close(fd);
   7611 						break;
   7612 					}
   7613 				} else {
   7614 					/* Do nothing in case of NEW */
   7615 					close(fd);
   7616 					printwait(messages[MSG_EXISTS], &presel);
   7617 					goto nochange;
   7618 				}
   7619 			}
   7620 
   7621 			if (sel == SEL_RENAME) {
   7622 				/* Rename the file */
   7623 				if (ret == 'd')
   7624 					spawn("cp -rp", pdents[cur].name, tmp, NULL, F_SILENT);
   7625 				else if (renameat(fd, pdents[cur].name, fd, tmp) != 0) {
   7626 					close(fd);
   7627 					printwarn(&presel);
   7628 					goto nochange;
   7629 				}
   7630 				close(fd);
   7631 				xstrsncpy(lastname, tmp, NAME_MAX + 1);
   7632 			} else { /* SEL_NEW */
   7633 				close(fd);
   7634 				presel = 0;
   7635 
   7636 				/* Check if it's a dir or file */
   7637 				if (r == 'f' || r == 'd') {
   7638 					mkpath(path, tmp, newpath);
   7639 					ret = xmktree(newpath, r == 'f' ? FALSE : TRUE);
   7640 				} else if (r == 's' || r == 'h') {
   7641 					if (tmp[0] == '@' && tmp[1] == '\0')
   7642 						tmp[0] = '\0';
   7643 					ret = xlink(tmp, path, (ndents ? pdents[cur].name : NULL),
   7644 						  newpath, &presel, r);
   7645 				}
   7646 
   7647 				if (!ret)
   7648 					printwait(messages[MSG_FAILED], &presel);
   7649 
   7650 				if (ret <= 0)
   7651 					goto nochange;
   7652 
   7653 				if (r == 'f' || r == 'd')
   7654 					xstrsncpy(lastname, tmp, NAME_MAX + 1);
   7655 				else if (ndents) {
   7656 					if (cfg.filtermode)
   7657 						presel = FILTER;
   7658 					copycurname();
   7659 				}
   7660 				clearfilter();
   7661 			}
   7662 
   7663 			cd = FALSE;
   7664 			goto begin;
   7665 		}
   7666 		case SEL_PLUGIN:
   7667 			/* Check if directory is accessible */
   7668 			if (!xdiraccess(plgpath)) {
   7669 				printwarn(&presel);
   7670 				goto nochange;
   7671 			}
   7672 
   7673 			if (!pkey) {
   7674 				r = xstrsncpy(g_buf, messages[MSG_KEYS], CMD_LEN_MAX);
   7675 				printkeys(plug, g_buf + r - 1, maxplug);
   7676 				printmsg(g_buf);
   7677 				r = get_input(NULL);
   7678 			} else {
   7679 				r = pkey;
   7680 				pkey = '\0';
   7681 			}
   7682 
   7683 			if (r != '\r') {
   7684 				endselection(FALSE);
   7685 				tmp = get_kv_val(plug, NULL, r, maxplug, NNN_PLUG);
   7686 				if (!tmp) {
   7687 					printwait(messages[MSG_INVALID_KEY], &presel);
   7688 					goto nochange;
   7689 				}
   7690 
   7691 				if (tmp[0] == '-' && tmp[1]) {
   7692 					++tmp;
   7693 					r = FALSE; /* Do not refresh dir after completion */
   7694 				} else
   7695 					r = TRUE;
   7696 
   7697 				if (!run_plugin(&path, tmp, (ndents ? pdents[cur].name : NULL),
   7698 							 &lastname, &lastdir)) {
   7699 					printwait(messages[MSG_FAILED], &presel);
   7700 					goto nochange;
   7701 				}
   7702 
   7703 				if (g_state.picked)
   7704 					return EXIT_SUCCESS;
   7705 
   7706 				copycurname();
   7707 
   7708 				if (!r) {
   7709 					cfg.filtermode ? presel = FILTER : statusbar(path);
   7710 					goto nochange;
   7711 				}
   7712 			} else { /* 'Return/Enter' enters the plugin directory */
   7713 				g_state.runplugin ^= 1;
   7714 				if (!g_state.runplugin && rundir[0]) {
   7715 					/*
   7716 					 * If toggled, and still in the plugin dir,
   7717 					 * switch to original directory
   7718 					 */
   7719 					if (strcmp(path, plgpath) == 0) {
   7720 						xstrsncpy(path, rundir, PATH_MAX);
   7721 						xstrsncpy(lastname, runfile, NAME_MAX + 1);
   7722 						rundir[0] = runfile[0] = '\0';
   7723 						setdirwatch();
   7724 						goto begin;
   7725 					}
   7726 
   7727 					/* Otherwise, initiate choosing plugin again */
   7728 					g_state.runplugin = 1;
   7729 				}
   7730 
   7731 				xstrsncpy(lastdir, path, PATH_MAX);
   7732 				xstrsncpy(rundir, path, PATH_MAX);
   7733 				xstrsncpy(path, plgpath, PATH_MAX);
   7734 				if (ndents)
   7735 					xstrsncpy(runfile, pdents[cur].name, NAME_MAX);
   7736 				g_state.runctx = cfg.curctx;
   7737 				lastname[0] = '\0';
   7738 			}
   7739 			setdirwatch();
   7740 			clearfilter();
   7741 			goto begin;
   7742 		case SEL_SHELL: // fallthrough
   7743 		case SEL_LAUNCH: // fallthrough
   7744 		case SEL_PROMPT:
   7745 			r = handle_cmd(sel, newpath);
   7746 
   7747 			/* Continue in type-to-nav mode, if enabled */
   7748 			if (cfg.filtermode)
   7749 				presel = FILTER;
   7750 
   7751 			/* Save current */
   7752 			copycurname();
   7753 
   7754 			if (!r)
   7755 				goto nochange;
   7756 
   7757 			/* Repopulate as directory content may have changed */
   7758 			cd = FALSE;
   7759 			goto begin;
   7760 		case SEL_UMOUNT:
   7761 			presel = MSG_ZERO;
   7762 			if (!unmount((ndents ? pdents[cur].name : NULL), newpath, &presel, path)) {
   7763 				if (presel == MSG_ZERO)
   7764 					statusbar(path);
   7765 				goto nochange;
   7766 			}
   7767 
   7768 			/* Dir removed, go to next entry */
   7769 			copynextname(lastname);
   7770 			cd = FALSE;
   7771 			goto begin;
   7772 #ifndef NOSSN
   7773 		case SEL_SESSIONS:
   7774 			r = get_input(messages[MSG_SSN_OPTS]);
   7775 
   7776 			if (r == 's') {
   7777 				tmp = xreadline(NULL, messages[MSG_SSN_NAME]);
   7778 				if (tmp && *tmp)
   7779 					save_session(tmp, &presel);
   7780 			} else if (r == 'l' || r == 'r') {
   7781 				if (load_session(NULL, &path, &lastdir, &lastname, r == 'r')) {
   7782 					setdirwatch();
   7783 					goto begin;
   7784 				}
   7785 			}
   7786 
   7787 			statusbar(path);
   7788 			goto nochange;
   7789 #endif
   7790 		case SEL_EXPORT:
   7791 			export_file_list();
   7792 			cfg.filtermode ?  presel = FILTER : statusbar(path);
   7793 			goto nochange;
   7794 		case SEL_TIMETYPE:
   7795 			if (!set_time_type(&presel))
   7796 				goto nochange;
   7797 			cd = FALSE;
   7798 			goto begin;
   7799 		case SEL_QUITCTX: // fallthrough
   7800 		case SEL_QUITCD: // fallthrough
   7801 		case SEL_QUIT:
   7802 		case SEL_QUITERR:
   7803 			if (sel == SEL_QUITCTX) {
   7804 				int ctx = cfg.curctx;
   7805 
   7806 				for (r = (ctx - 1) & (CTX_MAX - 1);
   7807 				     (r != ctx) && !g_ctx[r].c_cfg.ctxactive;
   7808 				     r = ((r - 1) & (CTX_MAX - 1))) {
   7809 				};
   7810 
   7811 				if (r != ctx) {
   7812 					g_ctx[ctx].c_cfg.ctxactive = 0;
   7813 
   7814 					/* Switch to next active context */
   7815 					path = g_ctx[r].c_path;
   7816 					lastdir = g_ctx[r].c_last;
   7817 					lastname = g_ctx[r].c_name;
   7818 
   7819 					cfg = g_ctx[r].c_cfg;
   7820 
   7821 					cfg.curctx = r;
   7822 					setdirwatch();
   7823 					goto begin;
   7824 				}
   7825 			} else if (!g_state.forcequit) {
   7826 				for (r = 0; r < CTX_MAX; ++r)
   7827 					if (r != cfg.curctx && g_ctx[r].c_cfg.ctxactive) {
   7828 						r = get_input(messages[MSG_QUIT_ALL]);
   7829 						break;
   7830 					}
   7831 
   7832 				if (!(r == CTX_MAX || xconfirm(r)))
   7833 					break; // fallthrough
   7834 			}
   7835 
   7836 			/* CD on Quit */
   7837 			tmp = getenv("NNN_TMPFILE");
   7838 			if ((sel == SEL_QUITCD) || tmp) {
   7839 				write_lastdir(path, tmp);
   7840 				/* ^G is a way to quit picker mode without picking anything */
   7841 				if ((sel == SEL_QUITCD) && g_state.picker)
   7842 					selbufpos = 0;
   7843 			}
   7844 
   7845 			if (sel != SEL_QUITERR)
   7846 				return EXIT_SUCCESS;
   7847 
   7848 			if (selbufpos && !g_state.picker) {
   7849 				/* Pick files to stdout and exit */
   7850 				g_state.picker = 1;
   7851 				free(selpath);
   7852 				selpath = NULL;
   7853 				return EXIT_SUCCESS;
   7854 			}
   7855 
   7856 			return EXIT_FAILURE;
   7857 		default:
   7858 			if (xlines != LINES || xcols != COLS)
   7859 				continue;
   7860 
   7861 			if (idletimeout && idle == idletimeout) {
   7862 				lock_terminal(); /* Locker */
   7863 				idle = 0;
   7864 			}
   7865 
   7866 			copycurname();
   7867 			goto nochange;
   7868 		} /* switch (sel) */
   7869 	}
   7870 }
   7871 
   7872 static char *make_tmp_tree(char **paths, ssize_t entries, const char *prefix)
   7873 {
   7874 	/* tmpdir holds the full path */
   7875 	/* tmp holds the path without the tmp dir prefix */
   7876 	int err;
   7877 	struct stat sb;
   7878 	char *slash, *tmp;
   7879 	ssize_t len = xstrlen(prefix);
   7880 	char *tmpdir = malloc(PATH_MAX);
   7881 
   7882 	if (!tmpdir) {
   7883 		DPRINTF_S(strerror(errno));
   7884 		return NULL;
   7885 	}
   7886 
   7887 	tmp = tmpdir + tmpfplen - 1;
   7888 	xstrsncpy(tmpdir, g_tmpfpath, tmpfplen);
   7889 	xstrsncpy(tmp, "/nnnXXXXXX", 11);
   7890 
   7891 	/* Points right after the base tmp dir */
   7892 	tmp += 10;
   7893 
   7894 	/* handle the case where files are directly under / */
   7895 	if (!prefix[1] && (prefix[0] == '/'))
   7896 		len = 0;
   7897 
   7898 	if (!mkdtemp(tmpdir)) {
   7899 		free(tmpdir);
   7900 
   7901 		DPRINTF_S(strerror(errno));
   7902 		return NULL;
   7903 	}
   7904 
   7905 	listpath = tmpdir;
   7906 
   7907 	for (ssize_t i = 0; i < entries; ++i) {
   7908 		if (!paths[i])
   7909 			continue;
   7910 
   7911 		err = stat(paths[i], &sb);
   7912 		if (err && errno == ENOENT)
   7913 			continue;
   7914 
   7915 		/* Don't copy the common prefix */
   7916 		xstrsncpy(tmp, paths[i] + len, xstrlen(paths[i]) - len + 1);
   7917 
   7918 		/* Get the dir containing the path */
   7919 		slash = xmemrchr((uchar_t *)tmp, '/', xstrlen(paths[i]) - len);
   7920 		if (slash)
   7921 			*slash = '\0';
   7922 
   7923 		xmktree(tmpdir, TRUE);
   7924 
   7925 		if (slash)
   7926 			*slash = '/';
   7927 
   7928 		if (symlink(paths[i], tmpdir)) {
   7929 			DPRINTF_S(paths[i]);
   7930 			DPRINTF_S(strerror(errno));
   7931 		}
   7932 	}
   7933 
   7934 	/* Get the dir in which to start */
   7935 	*tmp = '\0';
   7936 	return tmpdir;
   7937 }
   7938 
   7939 static char *load_input(int fd, const char *path)
   7940 {
   7941 	ssize_t i, chunk_count = 1, chunk = (ssize_t)(512 * 1024) /* 512 KiB chunk size */, entries = 0;
   7942 	char *input = malloc(sizeof(char) * chunk), *tmpdir = NULL;
   7943 	char cwd[PATH_MAX], *next;
   7944 	size_t offsets[LIST_FILES_MAX];
   7945 	char **paths = NULL;
   7946 	ssize_t input_read, total_read = 0, off = 0;
   7947 	int msgnum = 0;
   7948 
   7949 	if (!input) {
   7950 		DPRINTF_S(strerror(errno));
   7951 		return NULL;
   7952 	}
   7953 
   7954 	if (!path) {
   7955 		if (!getcwd(cwd, PATH_MAX)) {
   7956 			free(input);
   7957 			return NULL;
   7958 		}
   7959 	} else
   7960 		xstrsncpy(cwd, path, PATH_MAX);
   7961 
   7962 	while (chunk_count < 512) {
   7963 		input_read = read(fd, input + total_read, chunk);
   7964 		if (input_read < 0) {
   7965 			DPRINTF_S(strerror(errno));
   7966 			goto malloc_1;
   7967 		}
   7968 
   7969 		if (input_read == 0)
   7970 			break;
   7971 
   7972 		total_read += input_read;
   7973 		++chunk_count;
   7974 
   7975 		while (off < total_read) {
   7976 			if ((next = memchr(input + off, '\0', total_read - off)) == NULL)
   7977 				break;
   7978 			++next;
   7979 
   7980 			if (next - input == off + 1) {
   7981 				off = next - input;
   7982 				continue;
   7983 			}
   7984 
   7985 			if (entries == LIST_FILES_MAX) {
   7986 				msgnum = MSG_LIMIT;
   7987 				goto malloc_1;
   7988 			}
   7989 
   7990 			offsets[entries++] = off;
   7991 			off = next - input;
   7992 		}
   7993 
   7994 		if (chunk_count == 512) {
   7995 			msgnum = MSG_LIMIT;
   7996 			goto malloc_1;
   7997 		}
   7998 
   7999 		/* We don't need to allocate another chunk */
   8000 		if (chunk_count == (total_read - input_read) / chunk)
   8001 			continue;
   8002 
   8003 		chunk_count = total_read / chunk;
   8004 		if (total_read % chunk)
   8005 			++chunk_count;
   8006 
   8007 		input = xrealloc(input, (chunk_count + 1) * chunk);
   8008 		if (!input)
   8009 			return NULL;
   8010 	}
   8011 
   8012 	if (off != total_read) {
   8013 		if (entries == LIST_FILES_MAX) {
   8014 			msgnum = MSG_LIMIT;
   8015 			goto malloc_1;
   8016 		}
   8017 
   8018 		offsets[entries++] = off;
   8019 	}
   8020 
   8021 	DPRINTF_D(entries);
   8022 	DPRINTF_D(total_read);
   8023 	DPRINTF_D(chunk_count);
   8024 
   8025 	if (!entries) {
   8026 		msgnum = MSG_0_ENTRIES;
   8027 		goto malloc_1;
   8028 	}
   8029 
   8030 	input[total_read] = '\0';
   8031 
   8032 	paths = malloc(entries * sizeof(char *));
   8033 	if (!paths)
   8034 		goto malloc_1;
   8035 
   8036 	for (i = 0; i < entries; ++i)
   8037 		paths[i] = input + offsets[i];
   8038 
   8039 	listroot = malloc(sizeof(char) * PATH_MAX);
   8040 	if (!listroot)
   8041 		goto malloc_1;
   8042 	listroot[0] = '\0';
   8043 
   8044 	DPRINTF_S(paths[0]);
   8045 
   8046 	for (i = 0; i < entries; ++i) {
   8047 		if (paths[i][0] == '\n' || selforparent(paths[i])) {
   8048 			paths[i] = NULL;
   8049 			continue;
   8050 		}
   8051 
   8052 		paths[i] = abspath(paths[i], cwd, NULL);
   8053 		if (!paths[i]) {
   8054 			entries = i; // free from the previous entry
   8055 			goto malloc_2;
   8056 
   8057 		}
   8058 
   8059 		DPRINTF_S(paths[i]);
   8060 
   8061 		xstrsncpy(g_buf, paths[i], PATH_MAX);
   8062 		if (!common_prefix(xdirname(g_buf), listroot)) {
   8063 			entries = i + 1; // free from the current entry
   8064 			goto malloc_2;
   8065 		}
   8066 
   8067 		DPRINTF_S(listroot);
   8068 	}
   8069 
   8070 	DPRINTF_S(listroot);
   8071 
   8072 	if (listroot[0])
   8073 		tmpdir = make_tmp_tree(paths, entries, listroot);
   8074 
   8075 malloc_2:
   8076 	for (i = entries - 1; i >= 0; --i)
   8077 		free(paths[i]);
   8078 malloc_1:
   8079 	if (msgnum) {
   8080 		if (home) { /* We are past init stage */
   8081 			printmsg(messages[msgnum]);
   8082 			xdelay(XDELAY_INTERVAL_MS);
   8083 		} else
   8084 			msg(messages[msgnum]);
   8085 	}
   8086 	free(input);
   8087 	free(paths);
   8088 	return tmpdir;
   8089 }
   8090 
   8091 static void check_key_collision(void)
   8092 {
   8093 	int key;
   8094 	bool bitmap[KEY_MAX] = {FALSE};
   8095 
   8096 	for (ullong_t i = 0; i < sizeof(bindings) / sizeof(struct key); ++i) {
   8097 		key = bindings[i].sym;
   8098 
   8099 		if (bitmap[key])
   8100 			dprintf(STDERR_FILENO, "key collision! [%s]\n", keyname(key));
   8101 		else
   8102 			bitmap[key] = TRUE;
   8103 	}
   8104 }
   8105 
   8106 static void usage(void)
   8107 {
   8108 	dprintf(STDERR_FILENO,
   8109 		"%s: nnn [OPTIONS] [PATH]\n\n"
   8110 		"The unorthodox terminal file manager.\n\n"
   8111 		"positional args:\n"
   8112 		"  PATH   start dir/file [default: .]\n\n"
   8113 		"optional args:\n"
   8114 #ifndef NOFIFO
   8115 		" -a      auto NNN_FIFO\n"
   8116 #endif
   8117 		" -A      no dir auto-enter in type-to-nav\n"
   8118 		" -b key  open bookmark key (trumps -s/S)\n"
   8119 		" -c      cli-only NNN_OPENER (trumps -e)\n"
   8120 		" -C      8-color scheme\n"
   8121 		" -d      detail mode\n"
   8122 		" -D      dirs in context color\n"
   8123 		" -e      text in $VISUAL/$EDITOR/vi\n"
   8124 		" -E      internal edits in EDITOR\n"
   8125 #ifndef NORL
   8126 		" -f      use readline history file\n"
   8127 #endif
   8128 #ifndef NOFIFO
   8129 		" -F val  fifo mode [0:preview 1:explore]\n"
   8130 #endif
   8131 		" -g      regex filters\n"
   8132 		" -H      show hidden files\n"
   8133 		" -i      show current file info\n"
   8134 		" -J      no auto-proceed on selection\n"
   8135 		" -K      detect key collision\n"
   8136 		" -l val  set scroll lines\n"
   8137 		" -n      type-to-nav mode\n"
   8138 		" -o      open files only on Enter\n"
   8139 		" -p file selection file [-:stdout]\n"
   8140 		" -P key  run plugin key\n"
   8141 		" -Q      no quit confirmation\n"
   8142 		" -r      use advcpmv patched cp, mv\n"
   8143 		" -R      no rollover at edges\n"
   8144 #ifndef NOSSN
   8145 		" -s name load session by name\n"
   8146 		" -S      persistent session\n"
   8147 #endif
   8148 		" -t secs timeout to lock\n"
   8149 		" -T key  sort order [a/d/e/r/s/t/v]\n"
   8150 		" -u      use selection (no prompt)\n"
   8151 #ifndef NOUG
   8152 		" -U      show user and group\n"
   8153 #endif
   8154 		" -V      show version\n"
   8155 #ifndef NOX11
   8156 		" -x      notis, selection sync, xterm title\n"
   8157 #endif
   8158 		" -h      show help\n\n"
   8159 		"v%s\n%s\n", __func__, VERSION, GENERAL_INFO);
   8160 }
   8161 
   8162 static bool setup_config(void)
   8163 {
   8164 	size_t r, len;
   8165 	char *xdgcfg = getenv("XDG_CONFIG_HOME");
   8166 	bool xdg = FALSE;
   8167 
   8168 	/* Set up configuration file paths */
   8169 	if (xdgcfg && xdgcfg[0]) {
   8170 		DPRINTF_S(xdgcfg);
   8171 		if (xdgcfg[0] == '~') {
   8172 			r = xstrsncpy(g_buf, home, PATH_MAX);
   8173 			xstrsncpy(g_buf + r - 1, xdgcfg + 1, PATH_MAX);
   8174 			xdgcfg = g_buf;
   8175 			DPRINTF_S(xdgcfg);
   8176 		}
   8177 
   8178 		if (!xdiraccess(xdgcfg)) {
   8179 			xerror();
   8180 			return FALSE;
   8181 		}
   8182 
   8183 		len = xstrlen(xdgcfg) + xstrlen("/nnn/bookmarks") + 1;
   8184 		xdg = TRUE;
   8185 	}
   8186 
   8187 	if (!xdg)
   8188 		len = xstrlen(home) + xstrlen("/.config/nnn/bookmarks") + 1;
   8189 
   8190 	cfgpath = (char *)malloc(len);
   8191 	plgpath = (char *)malloc(len);
   8192 	if (!cfgpath || !plgpath) {
   8193 		xerror();
   8194 		return FALSE;
   8195 	}
   8196 
   8197 	if (xdg) {
   8198 		xstrsncpy(cfgpath, xdgcfg, len);
   8199 		r = len - xstrlen("/nnn/bookmarks");
   8200 	} else {
   8201 		r = xstrsncpy(cfgpath, home, len);
   8202 
   8203 		/* Create ~/.config */
   8204 		xstrsncpy(cfgpath + r - 1, "/.config", len - r);
   8205 		DPRINTF_S(cfgpath);
   8206 		r += 8; /* length of "/.config" */
   8207 	}
   8208 
   8209 	/* Create ~/.config/nnn */
   8210 	xstrsncpy(cfgpath + r - 1, "/nnn", len - r);
   8211 	DPRINTF_S(cfgpath);
   8212 
   8213 	/* Create bookmarks, sessions, mounts and plugins directories */
   8214 	for (r = 0; r < ELEMENTS(toks); ++r) {
   8215 		mkpath(cfgpath, toks[r], plgpath);
   8216 		if (!xmktree(plgpath, TRUE)) {
   8217 			DPRINTF_S(toks[r]);
   8218 			xerror();
   8219 			return FALSE;
   8220 		}
   8221 	}
   8222 
   8223 	/* Set selection file path */
   8224 	if (!g_state.picker) {
   8225 		char *env_sel = xgetenv(env_cfg[NNN_SEL], NULL);
   8226 
   8227 		selpath = env_sel ? xstrdup(env_sel)
   8228 				  : (char *)malloc(len + 3); /* Length of "/.config/nnn/.selection" */
   8229 
   8230 		if (!selpath) {
   8231 			xerror();
   8232 			return FALSE;
   8233 		}
   8234 
   8235 		if (!env_sel) {
   8236 			r = xstrsncpy(selpath, cfgpath, len + 3);
   8237 			xstrsncpy(selpath + r - 1, "/.selection", 12);
   8238 			DPRINTF_S(selpath);
   8239 		}
   8240 	}
   8241 
   8242 	return TRUE;
   8243 }
   8244 
   8245 static bool set_tmp_path(void)
   8246 {
   8247 	char *tmp = "/tmp";
   8248 	char *path = xdiraccess(tmp) ? tmp : getenv("TMPDIR");
   8249 
   8250 	if (!path) {
   8251 		msg("set TMPDIR");
   8252 		return FALSE;
   8253 	}
   8254 
   8255 	tmpfplen = (uchar_t)xstrsncpy(g_tmpfpath, path, TMP_LEN_MAX);
   8256 	DPRINTF_S(g_tmpfpath);
   8257 	DPRINTF_U(tmpfplen);
   8258 
   8259 	return TRUE;
   8260 }
   8261 
   8262 static void cleanup(void)
   8263 {
   8264 #ifndef NOX11
   8265 	if (cfg.x11 && !g_state.picker) {
   8266 		printf("\033[23;0t"); /* reset terminal window title */
   8267 		fflush(stdout);
   8268 
   8269 		free(hostname);
   8270 	}
   8271 #endif
   8272 	free(selpath);
   8273 	free(plgpath);
   8274 	free(cfgpath);
   8275 	free(initpath);
   8276 	free(bmstr);
   8277 	free(pluginstr);
   8278 	free(listroot);
   8279 	free(ihashbmp);
   8280 	free(bookmark);
   8281 	free(plug);
   8282 	free(lastcmd);
   8283 #ifndef NOFIFO
   8284 	if (g_state.autofifo)
   8285 		unlink(fifopath);
   8286 #endif
   8287 	if (g_state.pluginit)
   8288 		unlink(g_pipepath);
   8289 #ifdef DEBUG
   8290 	disabledbg();
   8291 #endif
   8292 }
   8293 
   8294 int main(int argc, char *argv[])
   8295 {
   8296 	char *arg = NULL;
   8297 	char *session = NULL;
   8298 	int fd, opt, sort = 0, pkey = '\0'; /* Plugin key */
   8299 #ifndef NOMOUSE
   8300 	mmask_t mask;
   8301 	char *middle_click_env = xgetenv(env_cfg[NNN_MCLICK], "\0");
   8302 
   8303 	middle_click_key = (middle_click_env[0] == '^' && middle_click_env[1])
   8304 			    ? CONTROL(middle_click_env[1])
   8305 			    : (uchar_t)middle_click_env[0];
   8306 #endif
   8307 
   8308 	const char * const env_opts = xgetenv(env_cfg[NNN_OPTS], NULL);
   8309 	int env_opts_id = env_opts ? (int)xstrlen(env_opts) : -1;
   8310 #ifndef NORL
   8311 	bool rlhist = FALSE;
   8312 #endif
   8313 
   8314 	while ((opt = (env_opts_id > 0
   8315 		       ? env_opts[--env_opts_id]
   8316 		       : getopt(argc, argv, "aAb:cCdDeEfF:gHiJKl:nop:P:QrRs:St:T:uUVxh"))) != -1) {
   8317 		switch (opt) {
   8318 #ifndef NOFIFO
   8319 		case 'a':
   8320 			g_state.autofifo = 1;
   8321 			break;
   8322 #endif
   8323 		case 'A':
   8324 			cfg.autoenter = 0;
   8325 			break;
   8326 		case 'b':
   8327 			if (env_opts_id < 0)
   8328 				arg = optarg;
   8329 			break;
   8330 		case 'c':
   8331 			cfg.cliopener = 1;
   8332 			break;
   8333 		case 'C':
   8334 			g_state.oldcolor = 1;
   8335 			break;
   8336 		case 'd':
   8337 			cfg.showdetail = 1;
   8338 			break;
   8339 		case 'D':
   8340 			g_state.dirctx = 1;
   8341 			break;
   8342 		case 'e':
   8343 			cfg.useeditor = 1;
   8344 			break;
   8345 		case 'E':
   8346 			cfg.waitedit = 1;
   8347 			break;
   8348 		case 'f':
   8349 #ifndef NORL
   8350 			rlhist = TRUE;
   8351 #endif
   8352 			break;
   8353 #ifndef NOFIFO
   8354 		case 'F':
   8355 			if (env_opts_id < 0) {
   8356 				fd = atoi(optarg);
   8357 				if ((fd < 0) || (fd > 1))
   8358 					return EXIT_FAILURE;
   8359 				g_state.fifomode = fd;
   8360 			}
   8361 			break;
   8362 #endif
   8363 		case 'g':
   8364 			cfg.regex = 1;
   8365 			filterfn = &visible_re;
   8366 			break;
   8367 		case 'H':
   8368 			cfg.showhidden = 1;
   8369 			break;
   8370 		case 'i':
   8371 			cfg.fileinfo = 1;
   8372 			break;
   8373 		case 'J':
   8374 			g_state.stayonsel = 1;
   8375 			break;
   8376 		case 'K':
   8377 			check_key_collision();
   8378 			return EXIT_SUCCESS;
   8379 		case 'l':
   8380 			if (env_opts_id < 0)
   8381 				scroll_lines = atoi(optarg);
   8382 			break;
   8383 		case 'n':
   8384 			cfg.filtermode = 1;
   8385 			break;
   8386 		case 'o':
   8387 			cfg.nonavopen = 1;
   8388 			break;
   8389 		case 'p':
   8390 			if (env_opts_id >= 0)
   8391 				break;
   8392 
   8393 			g_state.picker = 1;
   8394 			if (!(optarg[0] == '-' && optarg[1] == '\0')) {
   8395 				fd = open(optarg, O_WRONLY | O_CREAT, 0600);
   8396 				if (fd == -1) {
   8397 					xerror();
   8398 					return EXIT_FAILURE;
   8399 				}
   8400 
   8401 				close(fd);
   8402 				selpath = abspath(optarg, NULL, NULL);
   8403 				unlink(selpath);
   8404 			}
   8405 			break;
   8406 		case 'P':
   8407 			if (env_opts_id < 0 && !optarg[1])
   8408 				pkey = (uchar_t)optarg[0];
   8409 			break;
   8410 		case 'Q':
   8411 			g_state.forcequit = 1;
   8412 			break;
   8413 		case 'r':
   8414 #ifdef __linux__
   8415 			cp[2] = cp[5] = mv[2] = mv[5] = 'g'; /* cp -iRp -> cpg -giRp */
   8416 			cp[4] = mv[4] = '-';
   8417 #endif
   8418 			break;
   8419 		case 'R':
   8420 			cfg.rollover = 0;
   8421 			break;
   8422 #ifndef NOSSN
   8423 		case 's':
   8424 			if (env_opts_id < 0)
   8425 				session = optarg;
   8426 			break;
   8427 		case 'S':
   8428 			g_state.prstssn = 1;
   8429 			if (!session) /* Support named persistent sessions */
   8430 				session = "@";
   8431 			break;
   8432 #endif
   8433 		case 't':
   8434 			if (env_opts_id < 0)
   8435 				idletimeout = atoi(optarg);
   8436 			break;
   8437 		case 'T':
   8438 			if (env_opts_id < 0)
   8439 				sort = (uchar_t)optarg[0];
   8440 			break;
   8441 		case 'u':
   8442 			cfg.prefersel = 1;
   8443 			break;
   8444 		case 'U':
   8445 			g_state.uidgid = 1;
   8446 			break;
   8447 		case 'V':
   8448 			dprintf(STDOUT_FILENO, "%s\n", VERSION);
   8449 			return EXIT_SUCCESS;
   8450 		case 'x':
   8451 			cfg.x11 = 1;
   8452 			break;
   8453 		case 'h':
   8454 			usage();
   8455 			return EXIT_SUCCESS;
   8456 		default:
   8457 			usage();
   8458 			return EXIT_FAILURE;
   8459 		}
   8460 		if (env_opts_id == 0)
   8461 			env_opts_id = -1;
   8462 	}
   8463 
   8464 #ifdef DEBUG
   8465 	enabledbg();
   8466 	DPRINTF_S(VERSION);
   8467 #endif
   8468 
   8469 	/* Prefix for temporary files */
   8470 	if (!set_tmp_path())
   8471 		return EXIT_FAILURE;
   8472 
   8473 	atexit(cleanup);
   8474 
   8475 	/* Check if we are in path list mode */
   8476 	if (!isatty(STDIN_FILENO)) {
   8477 		/* This is the same as listpath */
   8478 		initpath = load_input(STDIN_FILENO, NULL);
   8479 		if (!initpath)
   8480 			return EXIT_FAILURE;
   8481 
   8482 		/* We return to tty */
   8483 		if (!isatty(STDOUT_FILENO)) {
   8484 			fd = open(ctermid(NULL), O_RDONLY, 0400);
   8485 			dup2(fd, STDIN_FILENO);
   8486 			close(fd);
   8487 		} else
   8488 			dup2(STDOUT_FILENO, STDIN_FILENO);
   8489 
   8490 		if (session)
   8491 			session = NULL;
   8492 	}
   8493 
   8494 	home = getenv("HOME");
   8495 	if (!home) {
   8496 		msg("set HOME");
   8497 		return EXIT_FAILURE;
   8498 	}
   8499 	DPRINTF_S(home);
   8500 	homelen = (uchar_t)xstrlen(home);
   8501 
   8502 	if (!setup_config())
   8503 		return EXIT_FAILURE;
   8504 
   8505 	/* Get custom opener, if set */
   8506 	opener = xgetenv(env_cfg[NNN_OPENER], utils[UTIL_OPENER]);
   8507 	DPRINTF_S(opener);
   8508 
   8509 	/* Parse bookmarks string */
   8510 	if (!parsekvpair(&bookmark, &bmstr, NNN_BMS, &maxbm)) {
   8511 		msg(env_cfg[NNN_BMS]);
   8512 		return EXIT_FAILURE;
   8513 	}
   8514 
   8515 	/* Parse plugins string */
   8516 	if (!parsekvpair(&plug, &pluginstr, NNN_PLUG, &maxplug)) {
   8517 		msg(env_cfg[NNN_PLUG]);
   8518 		return EXIT_FAILURE;
   8519 	}
   8520 
   8521 	/* Parse order string */
   8522 	if (!parsekvpair(&order, &orderstr, NNN_ORDER, &maxorder)) {
   8523 		msg(env_cfg[NNN_ORDER]);
   8524 		return EXIT_FAILURE;
   8525 	}
   8526 
   8527 	if (!initpath) {
   8528 		if (arg) { /* Open a bookmark directly */
   8529 			if (!arg[1]) /* Bookmarks keys are single char */
   8530 				initpath = get_kv_val(bookmark, NULL, *arg, maxbm, NNN_BMS);
   8531 
   8532 			if (!initpath) {
   8533 				msg(messages[MSG_INVALID_KEY]);
   8534 				return EXIT_FAILURE;
   8535 			}
   8536 
   8537 			if (session)
   8538 				session = NULL;
   8539 		} else if (argc == optind) {
   8540 			/* Start in the current directory */
   8541 			char *startpath = getenv("PWD");
   8542 
   8543 			initpath = startpath ? xstrdup(startpath) : getcwd(NULL, 0);
   8544 			if (!initpath)
   8545 				initpath = "/";
   8546 		} else {
   8547 			arg = argv[optind];
   8548 			DPRINTF_S(arg);
   8549 			if (xstrlen(arg) > 7 && is_prefix(arg, "file://", 7))
   8550 				arg = arg + 7;
   8551 			initpath = abspath(arg, NULL, NULL);
   8552 			DPRINTF_S(initpath);
   8553 			if (!initpath) {
   8554 				xerror();
   8555 				return EXIT_FAILURE;
   8556 			}
   8557 
   8558 			/*
   8559 			 * If nnn is set as the file manager, applications may try to open
   8560 			 * files by invoking nnn. In that case pass the file path to the
   8561 			 * desktop opener and exit.
   8562 			 */
   8563 			struct stat sb;
   8564 
   8565 			if (stat(initpath, &sb) == -1) {
   8566 				xerror();
   8567 				return EXIT_FAILURE;
   8568 			}
   8569 
   8570 			if (!S_ISDIR(sb.st_mode))
   8571 				g_state.initfile = 1;
   8572 
   8573 			if (session)
   8574 				session = NULL;
   8575 		}
   8576 	}
   8577 
   8578 	/* Set archive handling (enveditor used as tmp var) */
   8579 	enveditor = getenv(env_cfg[NNN_ARCHIVE]);
   8580 #ifdef PCRE
   8581 	if (setfilter(&archive_pcre, (enveditor ? enveditor : patterns[P_ARCHIVE]))) {
   8582 #else
   8583 	if (setfilter(&archive_re, (enveditor ? enveditor : patterns[P_ARCHIVE]))) {
   8584 #endif
   8585 		msg(messages[MSG_INVALID_REG]);
   8586 		return EXIT_FAILURE;
   8587 	}
   8588 
   8589 	/* An all-CLI opener overrides option -e) */
   8590 	if (cfg.cliopener)
   8591 		cfg.useeditor = 0;
   8592 
   8593 	/* Get VISUAL/EDITOR */
   8594 	enveditor = xgetenv(envs[ENV_EDITOR], utils[UTIL_VI]);
   8595 	editor = xgetenv(envs[ENV_VISUAL], enveditor);
   8596 	DPRINTF_S(getenv(envs[ENV_VISUAL]));
   8597 	DPRINTF_S(getenv(envs[ENV_EDITOR]));
   8598 	DPRINTF_S(editor);
   8599 
   8600 	/* Get PAGER */
   8601 	pager = xgetenv(envs[ENV_PAGER], utils[UTIL_LESS]);
   8602 	DPRINTF_S(pager);
   8603 
   8604 	/* Get SHELL */
   8605 	shell = xgetenv(envs[ENV_SHELL], utils[UTIL_SH]);
   8606 	DPRINTF_S(shell);
   8607 
   8608 	DPRINTF_S(getenv("PWD"));
   8609 
   8610 #ifndef NOFIFO
   8611 	/* Create fifo */
   8612 	if (g_state.autofifo) {
   8613 		g_tmpfpath[tmpfplen - 1] = '\0';
   8614 
   8615 		size_t r = mkpath(g_tmpfpath, "nnn-fifo.", g_buf);
   8616 
   8617 		xstrsncpy(g_buf + r - 1, xitoa(getpid()), PATH_MAX - r);
   8618 		setenv("NNN_FIFO", g_buf, TRUE);
   8619 	}
   8620 
   8621 	fifopath = xgetenv("NNN_FIFO", NULL);
   8622 	if (fifopath) {
   8623 		if (mkfifo(fifopath, 0600) != 0 && !(errno == EEXIST && access(fifopath, W_OK) == 0)) {
   8624 			xerror();
   8625 			return EXIT_FAILURE;
   8626 		}
   8627 
   8628 		sigaction(SIGPIPE, &(struct sigaction){.sa_handler = SIG_IGN}, NULL);
   8629 	}
   8630 #endif
   8631 
   8632 #ifdef LINUX_INOTIFY
   8633 	/* Initialize inotify */
   8634 	inotify_fd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC);
   8635 	if (inotify_fd < 0) {
   8636 		xerror();
   8637 		return EXIT_FAILURE;
   8638 	}
   8639 #elif defined(BSD_KQUEUE)
   8640 	kq = kqueue();
   8641 	if (kq < 0) {
   8642 		xerror();
   8643 		return EXIT_FAILURE;
   8644 	}
   8645 #elif defined(HAIKU_NM)
   8646 	haiku_hnd = haiku_init_nm();
   8647 	if (!haiku_hnd) {
   8648 		xerror();
   8649 		return EXIT_FAILURE;
   8650 	}
   8651 #endif
   8652 
   8653 	/* Configure trash preference */
   8654 	opt = xgetenv_val(env_cfg[NNN_TRASH]);
   8655 	if (opt && opt <= 2)
   8656 		g_state.trash = opt;
   8657 
   8658 	/* Ignore/handle certain signals */
   8659 	struct sigaction act = {.sa_handler = sigint_handler};
   8660 
   8661 	if (sigaction(SIGINT, &act, NULL) < 0) {
   8662 		xerror();
   8663 		return EXIT_FAILURE;
   8664 	}
   8665 
   8666 	act.sa_handler = clean_exit_sighandler;
   8667 
   8668 	if (sigaction(SIGTERM, &act, NULL) < 0 || sigaction(SIGHUP, &act, NULL) < 0) {
   8669 		xerror();
   8670 		return EXIT_FAILURE;
   8671 	}
   8672 
   8673 	act.sa_handler = SIG_IGN;
   8674 
   8675 	if (sigaction(SIGQUIT, &act, NULL) < 0) {
   8676 		xerror();
   8677 		return EXIT_FAILURE;
   8678 	}
   8679 
   8680 #ifndef NOLC
   8681 	/* Set locale */
   8682 	setlocale(LC_ALL, "");
   8683 #ifdef PCRE
   8684 	tables = pcre_maketables();
   8685 #endif
   8686 #endif
   8687 
   8688 #ifndef NORL
   8689 #if RL_READLINE_VERSION >= 0x0603
   8690 	/* readline would overwrite the WINCH signal hook */
   8691 	rl_change_environment = 0;
   8692 #endif
   8693 	/* Bind TAB to cycling */
   8694 	rl_variable_bind("completion-ignore-case", "on");
   8695 #ifdef __linux__
   8696 	rl_bind_key('\t', rl_menu_complete);
   8697 #else
   8698 	rl_bind_key('\t', rl_complete);
   8699 #endif
   8700 	if (rlhist) {
   8701 		mkpath(cfgpath, ".history", g_buf);
   8702 		read_history(g_buf);
   8703 	}
   8704 #endif
   8705 
   8706 #ifndef NOX11
   8707 	if (cfg.x11 && !g_state.picker) {
   8708 		/* Save terminal window title */
   8709 		printf("\033[22;0t");
   8710 		fflush(stdout);
   8711 
   8712 		hostname = malloc(_POSIX_HOST_NAME_MAX + 1);
   8713 		if (!hostname) {
   8714 			xerror();
   8715 			return EXIT_FAILURE;
   8716 		}
   8717 		gethostname(hostname, _POSIX_HOST_NAME_MAX);
   8718 		hostname[_POSIX_HOST_NAME_MAX] = '\0';
   8719 	}
   8720 #endif
   8721 
   8722 #ifndef NOMOUSE
   8723 	if (!initcurses(&mask))
   8724 #else
   8725 	if (!initcurses(NULL))
   8726 #endif
   8727 		return EXIT_FAILURE;
   8728 
   8729 	if (sort)
   8730 		set_sort_flags(sort);
   8731 
   8732 	opt = browse(initpath, session, pkey);
   8733 
   8734 #ifndef NOSSN
   8735 	if (session && g_state.prstssn)
   8736 		save_session(session, NULL);
   8737 #endif
   8738 
   8739 #ifndef NOMOUSE
   8740 	mousemask(mask, NULL);
   8741 #endif
   8742 
   8743 	exitcurses();
   8744 
   8745 #ifndef NORL
   8746 	if (rlhist) {
   8747 		mkpath(cfgpath, ".history", g_buf);
   8748 		write_history(g_buf);
   8749 	}
   8750 #endif
   8751 
   8752 	if (g_state.picker) {
   8753 		if (selbufpos) {
   8754 			fd = selpath ? open(selpath, O_WRONLY | O_CREAT | O_TRUNC, 0600) : STDOUT_FILENO;
   8755 			if ((fd == -1) || (seltofile(fd, NULL) != (size_t)(selbufpos)))
   8756 				xerror();
   8757 
   8758 			if (fd > 1)
   8759 				close(fd);
   8760 		}
   8761 	} else if (selpath)
   8762 		unlink(selpath);
   8763 
   8764 	/* Remove tmp dir in list mode */
   8765 	rmlistpath();
   8766 
   8767 	/* Free the regex */
   8768 #ifdef PCRE
   8769 	pcre_free(archive_pcre);
   8770 #else
   8771 	regfree(&archive_re);
   8772 #endif
   8773 
   8774 	/* Free the selection buffer */
   8775 	free(pselbuf);
   8776 
   8777 #ifdef LINUX_INOTIFY
   8778 	/* Shutdown inotify */
   8779 	if (inotify_wd >= 0)
   8780 		inotify_rm_watch(inotify_fd, inotify_wd);
   8781 	close(inotify_fd);
   8782 #elif defined(BSD_KQUEUE)
   8783 	if (event_fd >= 0)
   8784 		close(event_fd);
   8785 	close(kq);
   8786 #elif defined(HAIKU_NM)
   8787 	haiku_close_nm(haiku_hnd);
   8788 #endif
   8789 
   8790 #ifndef NOFIFO
   8791 	if (!g_state.fifomode)
   8792 		notify_fifo(FALSE);
   8793 	if (fifofd != -1)
   8794 		close(fifofd);
   8795 #endif
   8796 
   8797 	return opt;
   8798 }