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