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