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