"Fossies" - the Fresh Open Source Software Archive

Member "nnn-4.4/src/nnn.c" (23 Nov 2021, 198773 Bytes) of package /linux/misc/nnn-v4.4.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) C and C++ source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. For more information about "nnn.c" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: v4.3_vs_v4.4.

    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 #ifdef MACOS_BELOW_1012
  112 #include "../misc/macos-legacy/mach_gettime.h"
  113 #endif
  114 
  115 #if !defined(alloca) && defined(__GNUC__)
  116 /*
  117  * GCC doesn't expand alloca() to __builtin_alloca() in standards mode
  118  * (-std=...) and not all standard libraries do or supply it, e.g.
  119  * NetBSD/arm64 so explicitly use the builtin.
  120  */
  121 #define alloca(size) __builtin_alloca(size)
  122 #endif
  123 
  124 #include "nnn.h"
  125 #include "dbg.h"
  126 
  127 #if defined(ICONS) || defined(NERD)
  128 #include "icons.h"
  129 #define ICONS_ENABLED
  130 #endif
  131 
  132 #ifdef TOURBIN_QSORT
  133 #include "qsort.h"
  134 #endif
  135 
  136 /* Macro definitions */
  137 #define VERSION      "4.4"
  138 #define GENERAL_INFO "BSD 2-Clause\nhttps://github.com/jarun/nnn"
  139 
  140 #ifndef NOSSN
  141 #define SESSIONS_VERSION 1
  142 #endif
  143 
  144 #ifndef S_BLKSIZE
  145 #define S_BLKSIZE 512 /* S_BLKSIZE is missing on Android NDK (Termux) */
  146 #endif
  147 
  148 /*
  149  * NAME_MAX and PATH_MAX may not exist, e.g. with dirent.c_name being a
  150  * flexible array on Illumos. Use somewhat accommodating fallback values.
  151  */
  152 #ifndef NAME_MAX
  153 #define NAME_MAX 255
  154 #endif
  155 
  156 #ifndef PATH_MAX
  157 #define PATH_MAX 4096
  158 #endif
  159 
  160 #define _ABSSUB(N, M)   (((N) <= (M)) ? ((M) - (N)) : ((N) - (M)))
  161 #define ELEMENTS(x)     (sizeof(x) / sizeof(*(x)))
  162 #undef MIN
  163 #define MIN(x, y)       ((x) < (y) ? (x) : (y))
  164 #undef MAX
  165 #define MAX(x, y)       ((x) > (y) ? (x) : (y))
  166 #define ISODD(x)        ((x) & 1)
  167 #define ISBLANK(x)      ((x) == ' ' || (x) == '\t')
  168 #define TOUPPER(ch)     (((ch) >= 'a' && (ch) <= 'z') ? ((ch) - 'a' + 'A') : (ch))
  169 #define TOLOWER(ch)     (((ch) >= 'A' && (ch) <= 'Z') ? ((ch) - 'A' + 'a') : (ch))
  170 #define ISUPPER_(ch)    ((ch) >= 'A' && (ch) <= 'Z')
  171 #define ISLOWER_(ch)    ((ch) >= 'a' && (ch) <= 'z')
  172 #define CMD_LEN_MAX     (PATH_MAX + ((NAME_MAX + 1) << 1))
  173 #define ALIGN_UP(x, A)  ((((x) + (A) - 1) / (A)) * (A))
  174 #define READLINE_MAX    256
  175 #define FILTER          '/'
  176 #define RFILTER         '\\'
  177 #define CASE            ':'
  178 #define MSGWAIT         '$'
  179 #define SELECT          ' '
  180 #define PROMPT          ">>> "
  181 #define REGEX_MAX       48
  182 #define ENTRY_INCR      64 /* Number of dir 'entry' structures to allocate per shot */
  183 #define NAMEBUF_INCR    0x800 /* 64 dir entries at once, avg. 32 chars per file name = 64*32B = 2KB */
  184 #define DESCRIPTOR_LEN  32
  185 #define _ALIGNMENT      0x10 /* 16-byte alignment */
  186 #define _ALIGNMENT_MASK 0xF
  187 #define TMP_LEN_MAX     64
  188 #define DOT_FILTER_LEN  7
  189 #define ASCII_MAX       128
  190 #define EXEC_ARGS_MAX   10
  191 #define LIST_FILES_MAX  (1 << 16)
  192 #define SCROLLOFF       3
  193 #define COLOR_256       256
  194 
  195 /* Time intervals */
  196 #define DBLCLK_INTERVAL_NS (400000000)
  197 #define XDELAY_INTERVAL_MS (350000) /* 350 ms delay */
  198 
  199 #ifndef CTX8
  200 #define CTX_MAX 4
  201 #else
  202 #define CTX_MAX 8
  203 #endif
  204 
  205 /* BSDs or Solaris or SunOS */
  206 #if defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__) || defined(sun) || defined(__sun)
  207 #define SED "gsed"
  208 #else
  209 #define SED "sed"
  210 #endif
  211 
  212 /* Large selection threshold */
  213 #ifndef LARGESEL
  214 #define LARGESEL 1000
  215 #endif
  216 
  217 #define MIN_DISPLAY_COL (CTX_MAX * 2)
  218 #define ARCHIVE_CMD_LEN 16
  219 #define BLK_SHIFT_512   9
  220 
  221 /* Detect hardlinks in du */
  222 #define HASH_BITS   (0xFFFFFF)
  223 #define HASH_OCTETS (HASH_BITS >> 6) /* 2^6 = 64 */
  224 
  225 /* Entry flags */
  226 #define DIR_OR_DIRLNK 0x01
  227 #define HARD_LINK     0x02
  228 #define SYM_ORPHAN    0x04
  229 #define FILE_MISSING  0x08
  230 #define FILE_SELECTED 0x10
  231 #define FILE_SCANNED  0x20
  232 
  233 /* Macros to define process spawn behaviour as flags */
  234 #define F_NONE    0x00  /* no flag set */
  235 #define F_MULTI   0x01  /* first arg can be combination of args; to be used with F_NORMAL */
  236 #define F_NOWAIT  0x02  /* don't wait for child process (e.g. file manager) */
  237 #define F_NOTRACE 0x04  /* suppress stdout and stderr (no traces) */
  238 #define F_NORMAL  0x08  /* spawn child process in non-curses regular CLI mode */
  239 #define F_CONFIRM 0x10  /* run command - show results before exit (must have F_NORMAL) */
  240 #define F_CHKRTN  0x20  /* wait for user prompt if cmd returns failure status */
  241 #define F_NOSTDIN 0x40  /* suppress stdin */
  242 #define F_PAGE    0x80  /* page output in run-cmd-as-plugin mode */
  243 #define F_TTY     0x100 /* Force stdout to go to tty if redirected to a non-tty */
  244 #define F_CLI     (F_NORMAL | F_MULTI)
  245 #define F_SILENT  (F_CLI | F_NOTRACE)
  246 
  247 /* Version compare macros */
  248 /*
  249  * states: S_N: normal, S_I: comparing integral part, S_F: comparing
  250  *         fractional parts, S_Z: idem but with leading Zeroes only
  251  */
  252 #define S_N 0x0
  253 #define S_I 0x3
  254 #define S_F 0x6
  255 #define S_Z 0x9
  256 
  257 /* result_type: VCMP: return diff; VLEN: compare using len_diff/diff */
  258 #define VCMP 2
  259 #define VLEN 3
  260 
  261 /* Volume info */
  262 #define FREE     0
  263 #define CAPACITY 1
  264 
  265 /* TYPE DEFINITIONS */
  266 typedef unsigned int uint_t;
  267 typedef unsigned char uchar_t;
  268 typedef unsigned short ushort_t;
  269 typedef unsigned long long ullong_t;
  270 
  271 /* STRUCTURES */
  272 
  273 /* Directory entry */
  274 typedef struct entry {
  275     char *name;  /* 8 bytes */
  276     time_t sec;  /* 8 bytes */
  277     uint_t nsec; /* 4 bytes (enough to store nanosec) */
  278     mode_t mode; /* 4 bytes */
  279     off_t size;  /* 8 bytes */
  280     struct {
  281         ullong_t blocks : 40; /* 5 bytes (enough for 512 TiB in 512B blocks allocated) */
  282         ullong_t nlen   : 16; /* 2 bytes (length of file name) */
  283         ullong_t flags  : 8;  /* 1 byte (flags specific to the file) */
  284     };
  285 #ifndef NOUG
  286     uid_t uid; /* 4 bytes */
  287     gid_t gid; /* 4 bytes */
  288 #endif
  289 } *pEntry;
  290 
  291 /* Selection marker */
  292 typedef struct {
  293     char *startpos;
  294     size_t len;
  295 } selmark;
  296 
  297 /* Key-value pairs from env */
  298 typedef struct {
  299     int key;
  300     int off;
  301 } kv;
  302 
  303 typedef struct {
  304 #ifdef PCRE
  305     const pcre *pcrex;
  306 #else
  307     const regex_t *regex;
  308 #endif
  309     const char *str;
  310 } fltrexp_t;
  311 
  312 /*
  313  * Settings
  314  * NOTE: update default values if changing order
  315  */
  316 typedef struct {
  317     uint_t filtermode : 1;  /* Set to enter filter mode */
  318     uint_t timeorder  : 1;  /* Set to sort by time */
  319     uint_t sizeorder  : 1;  /* Set to sort by file size */
  320     uint_t apparentsz : 1;  /* Set to sort by apparent size (disk usage) */
  321     uint_t blkorder   : 1;  /* Set to sort by blocks used (disk usage) */
  322     uint_t extnorder  : 1;  /* Order by extension */
  323     uint_t showhidden : 1;  /* Set to show hidden files */
  324     uint_t reserved0  : 1;
  325     uint_t showdetail : 1;  /* Clear to show lesser file info */
  326     uint_t ctxactive  : 1;  /* Context active or not */
  327     uint_t reverse    : 1;  /* Reverse sort */
  328     uint_t version    : 1;  /* Version sort */
  329     uint_t reserved1  : 1;
  330     /* The following settings are global */
  331     uint_t curctx     : 3;  /* Current context number */
  332     uint_t prefersel  : 1;  /* Prefer selection over current, if exists */
  333     uint_t fileinfo   : 1;  /* Show file information on hover */
  334     uint_t nonavopen  : 1;  /* Open file on right arrow or `l` */
  335     uint_t autoselect : 1;  /* Auto-select dir in type-to-nav mode */
  336     uint_t reserved2  : 1;
  337     uint_t useeditor  : 1;  /* Use VISUAL to open text files */
  338     uint_t reserved3  : 3;
  339     uint_t regex      : 1;  /* Use regex filters */
  340     uint_t x11        : 1;  /* Copy to system clipboard, show notis, xterm title */
  341     uint_t timetype   : 2;  /* Time sort type (0: access, 1: change, 2: modification) */
  342     uint_t cliopener  : 1;  /* All-CLI app opener */
  343     uint_t waitedit   : 1;  /* For ops that can't be detached, used EDITOR */
  344     uint_t rollover   : 1;  /* Roll over at edges */
  345 } settings;
  346 
  347 /* Non-persistent program-internal states (alphabeical order) */
  348 typedef struct {
  349     uint_t autofifo   : 1;  /* Auto-create NNN_FIFO */
  350     uint_t autonext   : 1;  /* Auto-proceed on open */
  351     uint_t dircolor   : 1;  /* Current status of dir color */
  352     uint_t dirctx     : 1;  /* Show dirs in context color */
  353     uint_t duinit     : 1;  /* Initialize disk usage */
  354     uint_t fifomode   : 1;  /* FIFO notify mode: 0: preview, 1: explore */
  355     uint_t forcequit  : 1;  /* Do not prompt on quit */
  356     uint_t initfile   : 1;  /* Positional arg is a file */
  357     uint_t interrupt  : 1;  /* Program received an interrupt */
  358     uint_t move       : 1;  /* Move operation */
  359     uint_t oldcolor   : 1;  /* Use older colorscheme */
  360     uint_t picked     : 1;  /* Plugin has picked files */
  361     uint_t picker     : 1;  /* Write selection to user-specified file */
  362     uint_t pluginit   : 1;  /* Plugin framework initialized */
  363     uint_t prstssn    : 1;  /* Persistent session */
  364     uint_t rangesel   : 1;  /* Range selection on */
  365     uint_t runctx     : 3;  /* The context in which plugin is to be run */
  366     uint_t runplugin  : 1;  /* Choose plugin mode */
  367     uint_t selmode    : 1;  /* Set when selecting files */
  368     uint_t stayonsel  : 1;  /* Disable auto-proceed on select */
  369     uint_t trash      : 2;  /* Trash method 0: rm -rf, 1: trash-cli, 2: gio trash */
  370     uint_t uidgid     : 1;  /* Show owner and group info */
  371     uint_t reserved   : 7;  /* Adjust when adding/removing a field */
  372 } runstate;
  373 
  374 /* Contexts or workspaces */
  375 typedef struct {
  376     char c_path[PATH_MAX];     /* Current dir */
  377     char c_last[PATH_MAX];     /* Last visited dir */
  378     char c_name[NAME_MAX + 1]; /* Current file name */
  379     char c_fltr[REGEX_MAX];    /* Current filter */
  380     settings c_cfg;            /* Current configuration */
  381     uint_t color;              /* Color code for directories */
  382 } context;
  383 
  384 #ifndef NOSSN
  385 typedef struct {
  386     size_t ver;
  387     size_t pathln[CTX_MAX];
  388     size_t lastln[CTX_MAX];
  389     size_t nameln[CTX_MAX];
  390     size_t fltrln[CTX_MAX];
  391 } session_header_t;
  392 #endif
  393 
  394 /* GLOBALS */
  395 
  396 /* Configuration, contexts */
  397 static settings cfg = {
  398     0, /* filtermode */
  399     0, /* timeorder */
  400     0, /* sizeorder */
  401     0, /* apparentsz */
  402     0, /* blkorder */
  403     0, /* extnorder */
  404     0, /* showhidden */
  405     0, /* reserved0 */
  406     0, /* showdetail */
  407     1, /* ctxactive */
  408     0, /* reverse */
  409     0, /* version */
  410     0, /* reserved1 */
  411     0, /* curctx */
  412     0, /* prefersel */
  413     0, /* fileinfo */
  414     0, /* nonavopen */
  415     1, /* autoselect */
  416     0, /* reserved2 */
  417     0, /* useeditor */
  418     0, /* reserved3 */
  419     0, /* regex */
  420     0, /* x11 */
  421     2, /* timetype (T_MOD) */
  422     0, /* cliopener */
  423     0, /* waitedit */
  424     1, /* rollover */
  425 };
  426 
  427 static context g_ctx[CTX_MAX] __attribute__ ((aligned));
  428 
  429 static int ndents, cur, last, curscroll, last_curscroll, total_dents = ENTRY_INCR, scroll_lines = 1;
  430 static int nselected;
  431 #ifndef NOFIFO
  432 static int fifofd = -1;
  433 #endif
  434 static uint_t idletimeout, selbufpos, selbuflen;
  435 static ushort_t xlines, xcols;
  436 static ushort_t idle;
  437 static uchar_t maxbm, maxplug, maxorder;
  438 static uchar_t cfgsort[CTX_MAX + 1];
  439 static char *bmstr;
  440 static char *pluginstr;
  441 static char *orderstr;
  442 static char *opener;
  443 static char *editor;
  444 static char *enveditor;
  445 static char *pager;
  446 static char *shell;
  447 static char *home;
  448 static char *initpath;
  449 static char *cfgpath;
  450 static char *selpath;
  451 static char *listpath;
  452 static char *listroot;
  453 static char *plgpath;
  454 static char *pnamebuf, *pselbuf, *findselpos;
  455 static char *mark;
  456 #ifndef NOX11
  457 static char *hostname;
  458 #endif
  459 #ifndef NOFIFO
  460 static char *fifopath;
  461 #endif
  462 static char *lastcmd;
  463 static ullong_t *ihashbmp;
  464 static struct entry *pdents;
  465 static blkcnt_t dir_blocks;
  466 static kv *bookmark;
  467 static kv *plug;
  468 static kv *order;
  469 static uchar_t tmpfplen, homelen;
  470 static uchar_t blk_shift = BLK_SHIFT_512;
  471 #ifndef NOMOUSE
  472 static int middle_click_key;
  473 #endif
  474 #ifdef PCRE
  475 static pcre *archive_pcre;
  476 #else
  477 static regex_t archive_re;
  478 #endif
  479 
  480 /* pthread related */
  481 #define NUM_DU_THREADS (4) /* Can use sysconf(_SC_NPROCESSORS_ONLN) */
  482 #define DU_TEST (((node->fts_info & FTS_F) && \
  483         (sb->st_nlink <= 1 || test_set_bit((uint_t)sb->st_ino))) || node->fts_info & FTS_DP)
  484 
  485 static int threadbmp = -1; /* Has 1 in the bit position for idle threads */
  486 static volatile int active_threads;
  487 static pthread_mutex_t running_mutex = PTHREAD_MUTEX_INITIALIZER;
  488 static pthread_mutex_t hardlink_mutex = PTHREAD_MUTEX_INITIALIZER;
  489 static ullong_t *core_files;
  490 static blkcnt_t *core_blocks;
  491 static ullong_t num_files;
  492 
  493 typedef struct {
  494     char path[PATH_MAX];
  495     int entnum;
  496     ushort_t core;
  497     bool mntpoint;
  498 } thread_data;
  499 
  500 static thread_data *core_data;
  501 
  502 /* Retain old signal handlers */
  503 static struct sigaction oldsighup;
  504 static struct sigaction oldsigtstp;
  505 static struct sigaction oldsigwinch;
  506 
  507 /* For use in functions which are isolated and don't return the buffer */
  508 static char g_buf[CMD_LEN_MAX] __attribute__ ((aligned));
  509 
  510 /* For use as a scratch buffer in selection manipulation */
  511 static char g_sel[PATH_MAX] __attribute__ ((aligned));
  512 
  513 /* Buffer to store tmp file path to show selection, file stats and help */
  514 static char g_tmpfpath[TMP_LEN_MAX] __attribute__ ((aligned));
  515 
  516 /* Buffer to store plugins control pipe location */
  517 static char g_pipepath[TMP_LEN_MAX] __attribute__ ((aligned));
  518 
  519 /* Non-persistent runtime states */
  520 static runstate g_state;
  521 
  522 /* Options to identify file MIME */
  523 #if defined(__APPLE__)
  524 #define FILE_MIME_OPTS "-bIL"
  525 #elif !defined(__sun) /* no MIME option for 'file' */
  526 #define FILE_MIME_OPTS "-biL"
  527 #endif
  528 
  529 /* Macros for utilities */
  530 #define UTIL_OPENER    0
  531 #define UTIL_ATOOL     1
  532 #define UTIL_BSDTAR    2
  533 #define UTIL_UNZIP     3
  534 #define UTIL_TAR       4
  535 #define UTIL_LOCKER    5
  536 #define UTIL_LAUNCH    6
  537 #define UTIL_SH_EXEC   7
  538 #define UTIL_BASH      8
  539 #define UTIL_SSHFS     9
  540 #define UTIL_RCLONE    10
  541 #define UTIL_VI        11
  542 #define UTIL_LESS      12
  543 #define UTIL_SH        13
  544 #define UTIL_FZF       14
  545 #define UTIL_NTFY      15
  546 #define UTIL_CBCP      16
  547 #define UTIL_NMV       17
  548 #define UTIL_TRASH_CLI 18
  549 #define UTIL_GIO_TRASH 19
  550 #define UTIL_RM_RF     20
  551 
  552 /* Utilities to open files, run actions */
  553 static char * const utils[] = {
  554 #ifdef __APPLE__
  555     "/usr/bin/open",
  556 #elif defined __CYGWIN__
  557     "cygstart",
  558 #elif defined __HAIKU__
  559     "open",
  560 #else
  561     "xdg-open",
  562 #endif
  563     "atool",
  564     "bsdtar",
  565     "unzip",
  566     "tar",
  567 #ifdef __APPLE__
  568     "bashlock",
  569 #elif defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__)
  570     "lock",
  571 #elif defined __HAIKU__
  572     "peaclock",
  573 #else
  574     "vlock",
  575 #endif
  576     "launch",
  577     "sh -c",
  578     "bash",
  579     "sshfs",
  580     "rclone",
  581     "vi",
  582     "less",
  583     "sh",
  584     "fzf",
  585     ".ntfy",
  586     ".cbcp",
  587     ".nmv",
  588     "trash-put",
  589     "gio trash",
  590     "rm -rf",
  591 };
  592 
  593 /* Common strings */
  594 #define MSG_ZERO         0 /* Unused */
  595 #define MSG_0_ENTRIES    1
  596 #define STR_TMPFILE      2
  597 #define MSG_0_SELECTED   3
  598 #define MSG_CANCEL       4
  599 #define MSG_FAILED       5
  600 #define MSG_SSN_NAME     6
  601 #define MSG_CP_MV_AS     7
  602 #define MSG_CUR_SEL_OPTS 8
  603 #define MSG_FORCE_RM     9
  604 #define MSG_LIMIT        10
  605 #define MSG_NEW_OPTS     11
  606 #define MSG_CLI_MODE     12
  607 #define MSG_OVERWRITE    13
  608 #define MSG_SSN_OPTS     14
  609 #define MSG_QUIT_ALL     15
  610 #define MSG_HOSTNAME     16
  611 #define MSG_ARCHIVE_NAME 17
  612 #define MSG_OPEN_WITH    18
  613 #define MSG_NEW_PATH     19
  614 #define MSG_LINK_PREFIX  20
  615 #define MSG_COPY_NAME    21
  616 #define MSG_ENTER        22
  617 #define MSG_SEL_MISSING  23
  618 #define MSG_ACCESS       24
  619 #define MSG_EMPTY_FILE   25
  620 #define MSG_UNSUPPORTED  26
  621 #define MSG_NOT_SET      27
  622 #define MSG_EXISTS       28
  623 #define MSG_FEW_COLUMNS  29
  624 #define MSG_REMOTE_OPTS  30
  625 #define MSG_RCLONE_DELAY 31
  626 #define MSG_APP_NAME     32
  627 #define MSG_ARCHIVE_OPTS 33
  628 #define MSG_KEYS         34
  629 #define MSG_INVALID_REG  35
  630 #define MSG_ORDER        36
  631 #define MSG_LAZY         37
  632 #define MSG_FIRST        38
  633 #define MSG_RM_TMP       39
  634 #define MSG_INVALID_KEY  40
  635 #define MSG_NOCHANGE     41
  636 #define MSG_DIR_CHANGED  42
  637 
  638 static const char * const messages[] = {
  639     "",
  640     "0 entries",
  641     "/.nnnXXXXXX",
  642     "0 selected",
  643     "cancelled",
  644     "failed!",
  645     "session name: ",
  646     "'c'p / 'm'v as?",
  647     "'c'urrent / 's'el?",
  648     "%s %s? [Esc cancels]",
  649     "limit exceeded",
  650     "'f'ile / 'd'ir / 's'ym / 'h'ard?",
  651     "'c'li / 'g'ui?",
  652     "overwrite?",
  653     "'s'ave / 'l'oad / 'r'estore?",
  654     "Quit all contexts?",
  655     "remote name (- for hovered): ",
  656     "archive [path/]name: ",
  657     "open with: ",
  658     "[path/]name: ",
  659     "link prefix [@ for none]: ",
  660     "copy [path/]name: ",
  661     "\n'Enter' to continue",
  662     "open failed",
  663     "dir inaccessible",
  664     "empty! edit/open with",
  665     "?",
  666     "not set",
  667     "entry exists",
  668     "too few cols!",
  669     "'s'shfs / 'r'clone?",
  670     "refresh if slow",
  671     "app name: ",
  672     "'o'pen / e'x'tract / 'l's / 'm'nt?",
  673     "keys:",
  674     "invalid regex",
  675     "'a'u / 'd'u / 'e'xt / 'r'ev / 's'z / 't'm / 'v'er / 'c'lr / '^T'?",
  676     "unmount failed! try lazy?",
  677     "first file (\')/char?",
  678     "remove tmp file?",
  679     "invalid key",
  680     "unchanged",
  681     "dir changed, range sel off",
  682 };
  683 
  684 /* Supported configuration environment variables */
  685 #define NNN_OPTS    0
  686 #define NNN_BMS     1
  687 #define NNN_PLUG    2
  688 #define NNN_OPENER  3
  689 #define NNN_COLORS  4
  690 #define NNN_FCOLORS 5
  691 #define NNNLVL      6
  692 #define NNN_PIPE    7
  693 #define NNN_MCLICK  8
  694 #define NNN_SEL     9
  695 #define NNN_ARCHIVE 10
  696 #define NNN_ORDER   11
  697 #define NNN_HELP    12 /* strings end here */
  698 #define NNN_TRASH   13 /* flags begin here */
  699 
  700 static const char * const env_cfg[] = {
  701     "NNN_OPTS",
  702     "NNN_BMS",
  703     "NNN_PLUG",
  704     "NNN_OPENER",
  705     "NNN_COLORS",
  706     "NNN_FCOLORS",
  707     "NNNLVL",
  708     "NNN_PIPE",
  709     "NNN_MCLICK",
  710     "NNN_SEL",
  711     "NNN_ARCHIVE",
  712     "NNN_ORDER",
  713     "NNN_HELP",
  714     "NNN_TRASH",
  715 };
  716 
  717 /* Required environment variables */
  718 #define ENV_SHELL  0
  719 #define ENV_VISUAL 1
  720 #define ENV_EDITOR 2
  721 #define ENV_PAGER  3
  722 #define ENV_NCUR   4
  723 
  724 static const char * const envs[] = {
  725     "SHELL",
  726     "VISUAL",
  727     "EDITOR",
  728     "PAGER",
  729     "nnn",
  730 };
  731 
  732 /* Time type used */
  733 #define T_ACCESS 0
  734 #define T_CHANGE 1
  735 #define T_MOD    2
  736 
  737 #ifdef __linux__
  738 static char cp[] = "cp   -iRp";
  739 static char mv[] = "mv   -i";
  740 #else
  741 static char cp[] = "cp -iRp";
  742 static char mv[] = "mv -i";
  743 #endif
  744 
  745 /* Archive commands */
  746 static const char * const archive_cmd[] = {"atool -a", "bsdtar -acvf", "zip -r", "tar -acvf"};
  747 
  748 /* Tokens used for path creation */
  749 #define TOK_BM  0
  750 #define TOK_SSN 1
  751 #define TOK_MNT 2
  752 #define TOK_PLG 3
  753 
  754 static const char * const toks[] = {
  755     "bookmarks",
  756     "sessions",
  757     "mounts",
  758     "plugins", /* must be the last entry */
  759 };
  760 
  761 /* Patterns */
  762 #define P_CPMVFMT 0
  763 #define P_CPMVRNM 1
  764 #define P_ARCHIVE 2
  765 #define P_REPLACE 3
  766 
  767 static const char * const patterns[] = {
  768     SED" -i 's|^\\(\\(.*/\\)\\(.*\\)$\\)|#\\1\\n\\3|' %s",
  769     SED" 's|^\\([^#/][^/]\\?.*\\)$|%s/\\1|;s|^#\\(/.*\\)$|\\1|' "
  770         "%s | tr '\\n' '\\0' | xargs -0 -n2 sh -c '%s \"$0\" \"$@\" < /dev/tty'",
  771     "\\.(bz|bz2|gz|tar|taz|tbz|tbz2|tgz|z|zip)$",
  772     SED" -i 's|^%s\\(.*\\)$|%s\\1|' %s",
  773 };
  774 
  775 /* Colors */
  776 #define C_BLK (CTX_MAX + 1) /* Block device: DarkSeaGreen1 */
  777 #define C_CHR (C_BLK + 1)   /* Character device: Yellow1 */
  778 #define C_DIR (C_CHR + 1)   /* Directory: DeepSkyBlue1 */
  779 #define C_EXE (C_DIR + 1)   /* Executable file: Green1 */
  780 #define C_FIL (C_EXE + 1)   /* Regular file: Normal */
  781 #define C_HRD (C_FIL + 1)   /* Hard link: Plum4 */
  782 #define C_LNK (C_HRD + 1)   /* Symbolic link: Cyan1 */
  783 #define C_MIS (C_LNK + 1)   /* Missing file OR file details: Grey62 */
  784 #define C_ORP (C_MIS + 1)   /* Orphaned symlink: DeepPink1 */
  785 #define C_PIP (C_ORP + 1)   /* Named pipe (FIFO): Orange1 */
  786 #define C_SOC (C_PIP + 1)   /* Socket: MediumOrchid1 */
  787 #define C_UND (C_SOC + 1)   /* Unknown OR 0B regular/exe file: Red1 */
  788 
  789 #ifdef ICONS_ENABLED
  790 /* 0-9, A-Z, OTHER = 36. */
  791 static ushort_t icon_positions[37];
  792 #endif
  793 
  794 static char gcolors[] = "c1e2272e006033f7c6d6abc4";
  795 static uint_t fcolors[C_UND + 1] = {0};
  796 
  797 /* Event handling */
  798 #ifdef LINUX_INOTIFY
  799 #define NUM_EVENT_SLOTS 32 /* Make room for 32 events */
  800 #define EVENT_SIZE (sizeof(struct inotify_event))
  801 #define EVENT_BUF_LEN (EVENT_SIZE * NUM_EVENT_SLOTS)
  802 static int inotify_fd, inotify_wd = -1;
  803 static uint_t INOTIFY_MASK = /* IN_ATTRIB | */ IN_CREATE | IN_DELETE | IN_DELETE_SELF
  804                | IN_MODIFY | IN_MOVE_SELF | IN_MOVED_FROM | IN_MOVED_TO;
  805 #elif defined(BSD_KQUEUE)
  806 #define NUM_EVENT_SLOTS 1
  807 #define NUM_EVENT_FDS 1
  808 static int kq, event_fd = -1;
  809 static struct kevent events_to_monitor[NUM_EVENT_FDS];
  810 static uint_t KQUEUE_FFLAGS = NOTE_DELETE | NOTE_EXTEND | NOTE_LINK
  811                 | NOTE_RENAME | NOTE_REVOKE | NOTE_WRITE;
  812 static struct timespec gtimeout;
  813 #elif defined(HAIKU_NM)
  814 static bool haiku_nm_active = FALSE;
  815 static haiku_nm_h haiku_hnd;
  816 #endif
  817 
  818 /* Function macros */
  819 #define tolastln() move(xlines - 1, 0)
  820 #define tocursor() move(cur + 2 - curscroll, 0)
  821 #define exitcurses() endwin()
  822 #define printwarn(presel) printwait(strerror(errno), presel)
  823 #define istopdir(path) ((path)[1] == '\0' && (path)[0] == '/')
  824 #define copycurname() xstrsncpy(lastname, ndents ? pdents[cur].name : "\0", NAME_MAX + 1)
  825 #define settimeout() timeout(1000)
  826 #define cleartimeout() timeout(-1)
  827 #define errexit() printerr(__LINE__)
  828 #define setdirwatch() (cfg.filtermode ? (presel = FILTER) : (watch = TRUE))
  829 #define filterset() (g_ctx[cfg.curctx].c_fltr[1])
  830 /* We don't care about the return value from strcmp() */
  831 #define xstrcmp(a, b)  (*(a) != *(b) ? -1 : strcmp((a), (b)))
  832 /* A faster version of xisdigit */
  833 #define xisdigit(c) ((unsigned int) (c) - '0' <= 9)
  834 #define xerror() perror(xitoa(__LINE__))
  835 
  836 #ifdef TOURBIN_QSORT
  837 #define ENTLESS(i, j) (entrycmpfn(pdents + (i), pdents + (j)) < 0)
  838 #define ENTSWAP(i, j) (swap_ent((i), (j)))
  839 #define ENTSORT(pdents, ndents, entrycmpfn) QSORT((ndents), ENTLESS, ENTSWAP)
  840 #else
  841 #define ENTSORT(pdents, ndents, entrycmpfn) qsort((pdents), (ndents), sizeof(*(pdents)), (entrycmpfn))
  842 #endif
  843 
  844 /* Forward declarations */
  845 static void redraw(char *path);
  846 static int spawn(char *file, char *arg1, char *arg2, char *arg3, ushort_t flag);
  847 static void move_cursor(int target, int ignore_scrolloff);
  848 static char *load_input(int fd, const char *path);
  849 static int set_sort_flags(int r);
  850 static void statusbar(char *path);
  851 static bool get_output(char *file, char *arg1, char *arg2, int fdout, bool multi, bool page);
  852 #ifndef NOFIFO
  853 static void notify_fifo(bool force);
  854 #endif
  855 
  856 /* Functions */
  857 
  858 static void sigint_handler(int sig)
  859 {
  860     (void) sig;
  861     g_state.interrupt = 1;
  862 }
  863 
  864 static void clean_exit_sighandler(int sig)
  865 {
  866     (void) sig;
  867     exitcurses();
  868     /* This triggers cleanup() thanks to atexit() */
  869     exit(EXIT_SUCCESS);
  870 }
  871 
  872 static char *xitoa(uint_t val)
  873 {
  874     static char dst[32] = {'\0'};
  875     static const char digits[201] =
  876         "0001020304050607080910111213141516171819"
  877         "2021222324252627282930313233343536373839"
  878         "4041424344454647484950515253545556575859"
  879         "6061626364656667686970717273747576777879"
  880         "8081828384858687888990919293949596979899";
  881     uint_t next = 30, quo, i;
  882 
  883     while (val >= 100) {
  884         quo = val / 100;
  885         i = (val - (quo * 100)) * 2;
  886         val = quo;
  887         dst[next] = digits[i + 1];
  888         dst[--next] = digits[i];
  889         --next;
  890     }
  891 
  892     /* Handle last 1-2 digits */
  893     if (val < 10)
  894         dst[next] = '0' + val;
  895     else {
  896         i = val * 2;
  897         dst[next] = digits[i + 1];
  898         dst[--next] = digits[i];
  899     }
  900 
  901     return &dst[next];
  902 }
  903 
  904 /* Return the integer value of a char representing HEX */
  905 static uchar_t xchartohex(uchar_t c)
  906 {
  907     if (xisdigit(c))
  908         return c - '0';
  909 
  910     if (c >= 'a' && c <= 'f')
  911         return c - 'a' + 10;
  912 
  913     if (c >= 'A' && c <= 'F')
  914         return c - 'A' + 10;
  915 
  916     return c;
  917 }
  918 
  919 /*
  920  * Source: https://elixir.bootlin.com/linux/latest/source/arch/alpha/include/asm/bitops.h
  921  */
  922 static bool test_set_bit(uint_t nr)
  923 {
  924     nr &= HASH_BITS;
  925 
  926     pthread_mutex_lock(&hardlink_mutex);
  927     ullong_t *m = ((ullong_t *)ihashbmp) + (nr >> 6);
  928 
  929     if (*m & (1 << (nr & 63))) {
  930         pthread_mutex_unlock(&hardlink_mutex);
  931         return FALSE;
  932     }
  933 
  934     *m |= 1 << (nr & 63);
  935     pthread_mutex_unlock(&hardlink_mutex);
  936 
  937     return TRUE;
  938 }
  939 
  940 #ifndef __APPLE__
  941 /* Increase the limit on open file descriptors, if possible */
  942 static void max_openfds(void)
  943 {
  944     struct rlimit rl;
  945 
  946     if (!getrlimit(RLIMIT_NOFILE, &rl))
  947         if (rl.rlim_cur < rl.rlim_max) {
  948             rl.rlim_cur = rl.rlim_max;
  949             setrlimit(RLIMIT_NOFILE, &rl);
  950         }
  951 }
  952 #endif
  953 
  954 /*
  955  * Wrapper to realloc()
  956  * Frees current memory if realloc() fails and returns NULL.
  957  *
  958  * The *alloc() family returns aligned address: https://man7.org/linux/man-pages/man3/malloc.3.html
  959  */
  960 static void *xrealloc(void *pcur, size_t len)
  961 {
  962     void *pmem = realloc(pcur, len);
  963 
  964     if (!pmem)
  965         free(pcur);
  966 
  967     return pmem;
  968 }
  969 
  970 /*
  971  * Just a safe strncpy(3)
  972  * Always null ('\0') terminates if both src and dest are valid pointers.
  973  * Returns the number of bytes copied including terminating null byte.
  974  */
  975 static size_t xstrsncpy(char *restrict dst, const char *restrict src, size_t n)
  976 {
  977     char *end = memccpy(dst, src, '\0', n);
  978 
  979     if (!end) {
  980         dst[n - 1] = '\0'; // NOLINT
  981         end = dst + n; /* If we return n here, binary size increases due to auto-inlining */
  982     }
  983 
  984     return end - dst;
  985 }
  986 
  987 static inline size_t xstrlen(const char *restrict s)
  988 {
  989 #if !defined(__GLIBC__)
  990     return strlen(s); // NOLINT
  991 #else
  992     return (char *)rawmemchr(s, '\0') - s; // NOLINT
  993 #endif
  994 }
  995 
  996 static char *xstrdup(const char *restrict s)
  997 {
  998     size_t len = xstrlen(s) + 1;
  999     char *ptr = malloc(len);
 1000 
 1001     if (ptr)
 1002         xstrsncpy(ptr, s, len);
 1003     return ptr;
 1004 }
 1005 
 1006 static bool is_suffix(const char *restrict str, const char *restrict suffix)
 1007 {
 1008     if (!str || !suffix)
 1009         return FALSE;
 1010 
 1011     size_t lenstr = xstrlen(str);
 1012     size_t lensuffix = xstrlen(suffix);
 1013 
 1014     if (lensuffix > lenstr)
 1015         return FALSE;
 1016 
 1017     return (xstrcmp(str + (lenstr - lensuffix), suffix) == 0);
 1018 }
 1019 
 1020 static inline bool is_prefix(const char *restrict str, const char *restrict prefix, size_t len)
 1021 {
 1022     return !strncmp(str, prefix, len);
 1023 }
 1024 
 1025 /*
 1026  * The poor man's implementation of memrchr(3).
 1027  * We are only looking for '/' in this program.
 1028  * And we are NOT expecting a '/' at the end.
 1029  * Ideally 0 < n <= xstrlen(s).
 1030  */
 1031 static void *xmemrchr(uchar_t *restrict s, uchar_t ch, size_t n)
 1032 {
 1033 #if defined(__GLIBC__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__)
 1034     return memrchr(s, ch, n);
 1035 #else
 1036 
 1037     if (!s || !n)
 1038         return NULL;
 1039 
 1040     uchar_t *ptr = s + n;
 1041 
 1042     do {
 1043         if (*--ptr == ch)
 1044             return ptr;
 1045     } while (s != ptr);
 1046 
 1047     return NULL;
 1048 #endif
 1049 }
 1050 
 1051 /* A very simplified implementation, changes path */
 1052 static char *xdirname(char *path)
 1053 {
 1054     char *base = xmemrchr((uchar_t *)path, '/', xstrlen(path));
 1055 
 1056     if (base == path)
 1057         path[1] = '\0';
 1058     else
 1059         *base = '\0';
 1060 
 1061     return path;
 1062 }
 1063 
 1064 static char *xbasename(char *path)
 1065 {
 1066     char *base = xmemrchr((uchar_t *)path, '/', xstrlen(path)); // NOLINT
 1067 
 1068     return base ? base + 1 : path;
 1069 }
 1070 
 1071 static inline char *xextension(const char *fname, size_t len)
 1072 {
 1073     return xmemrchr((uchar_t *)fname, '.', len);
 1074 }
 1075 
 1076 #ifndef NOUG
 1077 /*
 1078  * One-shot cache for getpwuid/getgrgid. Returns the cached name if the
 1079  * provided uid is the same as the previous uid. Returns xitoa(guid) if
 1080  * the guid is not found in the password database.
 1081  */
 1082 static char *getpwname(uid_t uid)
 1083 {
 1084     static uint_t uidcache = UINT_MAX;
 1085     static char *namecache;
 1086 
 1087     if (uidcache != uid) {
 1088         struct passwd *pw = getpwuid(uid);
 1089 
 1090         uidcache = uid;
 1091         namecache = pw ? pw->pw_name : NULL;
 1092     }
 1093 
 1094     return namecache ? namecache : xitoa(uid);
 1095 }
 1096 
 1097 static char *getgrname(gid_t gid)
 1098 {
 1099     static uint_t gidcache = UINT_MAX;
 1100     static char *grpcache;
 1101 
 1102     if (gidcache != gid) {
 1103         struct group *gr = getgrgid(gid);
 1104 
 1105         gidcache = gid;
 1106         grpcache = gr ? gr->gr_name : NULL;
 1107     }
 1108 
 1109     return grpcache ? grpcache : xitoa(gid);
 1110 }
 1111 #endif
 1112 
 1113 static inline bool getutil(char *util)
 1114 {
 1115     return spawn("which", util, NULL, NULL, F_NORMAL | F_NOTRACE) == 0;
 1116 }
 1117 
 1118 /*
 1119  * Updates out with "dir/name or "/name"
 1120  * Returns the number of bytes copied including the terminating NULL byte
 1121  *
 1122  * Note: dir and out must be PATH_MAX in length to avoid macOS fault
 1123  */
 1124 static size_t mkpath(const char *dir, const char *name, char *out)
 1125 {
 1126     size_t len = 0;
 1127 
 1128     if (name[0] != '/') { // NOLINT
 1129         /* Handle root case */
 1130         if (istopdir(dir))
 1131             len = 1;
 1132         else
 1133             len = xstrsncpy(out, dir, PATH_MAX);
 1134 
 1135         out[len - 1] = '/'; // NOLINT
 1136     }
 1137     return (xstrsncpy(out + len, name, PATH_MAX - len) + len);
 1138 }
 1139 
 1140 /* Assumes both the paths passed are directories */
 1141 static char *common_prefix(const char *path, char *prefix)
 1142 {
 1143     const char *x = path, *y = prefix;
 1144     char *sep;
 1145 
 1146     if (!path || !*path || !prefix)
 1147         return NULL;
 1148 
 1149     if (!*prefix) {
 1150         xstrsncpy(prefix, path, PATH_MAX);
 1151         return prefix;
 1152     }
 1153 
 1154     while (*x && *y && (*x == *y))
 1155         ++x, ++y;
 1156 
 1157     /* Strings are same */
 1158     if (!*x && !*y)
 1159         return prefix;
 1160 
 1161     /* Path is shorter */
 1162     if (!*x && *y == '/') {
 1163         xstrsncpy(prefix, path, y - path);
 1164         return prefix;
 1165     }
 1166 
 1167     /* Prefix is shorter */
 1168     if (!*y && *x == '/')
 1169         return prefix;
 1170 
 1171     /* Shorten prefix */
 1172     prefix[y - prefix] = '\0';
 1173 
 1174     sep = xmemrchr((uchar_t *)prefix, '/', y - prefix);
 1175     if (sep != prefix)
 1176         *sep = '\0';
 1177     else /* Just '/' */
 1178         prefix[1] = '\0';
 1179 
 1180     return prefix;
 1181 }
 1182 
 1183 /*
 1184  * The library function realpath() resolves symlinks.
 1185  * If there's a symlink in file list we want to show the symlink not what it's points to.
 1186  * Resolves ./../~ in path
 1187  */
 1188 static char *abspath(const char *path, const char *cwd, char *buf)
 1189 {
 1190     if (!path)
 1191         return NULL;
 1192 
 1193     if (path[0] == '~')
 1194         cwd = home;
 1195     else if ((path[0] != '/') && !cwd)
 1196         cwd = getcwd(NULL, 0);
 1197 
 1198     size_t dst_size = 0, src_size = xstrlen(path), cwd_size = cwd ? xstrlen(cwd) : 0;
 1199     size_t len = src_size;
 1200     const char *src;
 1201     char *dst;
 1202     /*
 1203      * We need to add 2 chars at the end as relative paths may start with:
 1204      * ./ (find .)
 1205      * no separator (fd .): this needs an additional char for '/'
 1206      */
 1207     char *resolved_path = buf ? buf : malloc(src_size + cwd_size + 2);
 1208 
 1209     if (!resolved_path)
 1210         return NULL;
 1211 
 1212     /* Turn relative paths into absolute */
 1213     if (path[0] != '/') {
 1214         if (!cwd) {
 1215             if (!buf)
 1216                 free(resolved_path);
 1217             return NULL;
 1218         }
 1219         dst_size = xstrsncpy(resolved_path, cwd, cwd_size + 1) - 1;
 1220     } else
 1221         resolved_path[0] = '\0';
 1222 
 1223     src = path;
 1224     dst = resolved_path + dst_size;
 1225     for (const char *next = NULL; next != path + src_size;) {
 1226         next = memchr(src, '/', len);
 1227         if (!next)
 1228             next = path + src_size;
 1229 
 1230         if (next - src == 2 && src[0] == '.' && src[1] == '.') {
 1231             if (dst - resolved_path) {
 1232                 dst = xmemrchr((uchar_t *)resolved_path, '/', dst - resolved_path);
 1233                 *dst = '\0';
 1234             }
 1235         } else if (next - src == 1 && src[0] == '.') {
 1236             /* NOP */
 1237         } else if (next - src) {
 1238             *(dst++) = '/';
 1239             xstrsncpy(dst, src, next - src + 1);
 1240             dst += next - src;
 1241         }
 1242 
 1243         src = next + 1;
 1244         len = src_size - (src - path);
 1245     }
 1246 
 1247     if (*resolved_path == '\0') {
 1248         resolved_path[0] = '/';
 1249         resolved_path[1] = '\0';
 1250     }
 1251 
 1252     return resolved_path;
 1253 }
 1254 
 1255 static bool set_tilde_in_path(char *path)
 1256 {
 1257     if (is_prefix(path, home, homelen)) {
 1258         home[homelen] = path[homelen - 1];
 1259         path[homelen - 1] = '~';
 1260         return TRUE;
 1261     }
 1262 
 1263     return FALSE;
 1264 }
 1265 
 1266 static void reset_tilde_in_path(char *path)
 1267 {
 1268     path[homelen - 1] = home[homelen];
 1269     home[homelen] = '\0';
 1270 }
 1271 
 1272 #ifndef NOX11
 1273 static void xterm_cfg(char *path)
 1274 {
 1275     if (cfg.x11 && !g_state.picker) {
 1276         /* Signal CWD change to terminal */
 1277         printf("\033]7;file://%s%s\033\\", hostname, path);
 1278 
 1279         /* Set terminal window title */
 1280         bool r = set_tilde_in_path(path);
 1281 
 1282         printf("\033]2;%s\007", r ? &path[homelen - 1] : path);
 1283         fflush(stdout);
 1284 
 1285         if (r)
 1286             reset_tilde_in_path(path);
 1287     }
 1288 }
 1289 #endif
 1290 
 1291 static void convert_tilde(const char *path, char *buf)
 1292 {
 1293     if (path[0] == '~') {
 1294         ssize_t len = xstrlen(home);
 1295         ssize_t loclen = xstrlen(path);
 1296 
 1297         xstrsncpy(buf, home, len + 1);
 1298         xstrsncpy(buf + len, path + 1, loclen);
 1299     }
 1300 }
 1301 
 1302 static int create_tmp_file(void)
 1303 {
 1304     xstrsncpy(g_tmpfpath + tmpfplen - 1, messages[STR_TMPFILE], TMP_LEN_MAX - tmpfplen);
 1305 
 1306     int fd = mkstemp(g_tmpfpath);
 1307 
 1308     if (fd == -1) {
 1309         DPRINTF_S(strerror(errno));
 1310     }
 1311 
 1312     return fd;
 1313 }
 1314 
 1315 static void msg(const char *message)
 1316 {
 1317     dprintf(STDERR_FILENO, "%s\n", message);
 1318 }
 1319 
 1320 #ifdef KEY_RESIZE
 1321 static void handle_key_resize()
 1322 {
 1323     endwin();
 1324     refresh();
 1325 }
 1326 
 1327 /* Clear the old prompt */
 1328 static void clearoldprompt(void)
 1329 {
 1330     // clear info line
 1331     move(xlines - 2, 0);
 1332     clrtoeol();
 1333 
 1334     tolastln();
 1335     clrtoeol();
 1336     handle_key_resize();
 1337 }
 1338 #endif
 1339 
 1340 /* Messages show up at the bottom */
 1341 static inline void printmsg_nc(const char *msg)
 1342 {
 1343     tolastln();
 1344     addstr(msg);
 1345     clrtoeol();
 1346 }
 1347 
 1348 static void printmsg(const char *msg)
 1349 {
 1350     attron(COLOR_PAIR(cfg.curctx + 1));
 1351     printmsg_nc(msg);
 1352     attroff(COLOR_PAIR(cfg.curctx + 1));
 1353 }
 1354 
 1355 static void printwait(const char *msg, int *presel)
 1356 {
 1357     printmsg(msg);
 1358     if (presel) {
 1359         *presel = MSGWAIT;
 1360         if (ndents)
 1361             xstrsncpy(g_ctx[cfg.curctx].c_name, pdents[cur].name, NAME_MAX + 1);
 1362     }
 1363 }
 1364 
 1365 /* Kill curses and display error before exiting */
 1366 static void printerr(int linenum)
 1367 {
 1368     exitcurses();
 1369     perror(xitoa(linenum));
 1370     if (!g_state.picker && selpath)
 1371         unlink(selpath);
 1372     free(pselbuf);
 1373     exit(1);
 1374 }
 1375 
 1376 static inline bool xconfirm(int c)
 1377 {
 1378     return (c == 'y' || c == 'Y');
 1379 }
 1380 
 1381 static int get_input(const char *prompt)
 1382 {
 1383     if (prompt)
 1384         printmsg(prompt);
 1385     cleartimeout();
 1386 
 1387     int r = getch();
 1388 
 1389 #ifdef KEY_RESIZE
 1390     while (r == KEY_RESIZE) {
 1391         if (prompt) {
 1392             clearoldprompt();
 1393             xlines = LINES;
 1394             printmsg(prompt);
 1395         }
 1396 
 1397         r = getch();
 1398     }
 1399 #endif
 1400     settimeout();
 1401     return r;
 1402 }
 1403 
 1404 static bool isselfileempty(void)
 1405 {
 1406     struct stat sb;
 1407 
 1408     return (stat(selpath, &sb) == -1) || (!sb.st_size);
 1409 }
 1410 
 1411 static int get_cur_or_sel(void)
 1412 {
 1413     bool sel = (selbufpos || !isselfileempty());
 1414 
 1415     /* Check both local buffer and selection file for external selection */
 1416     if (sel && ndents) {
 1417         /* If selection is preferred and we have a local selection, return selection.
 1418          * Always show the prompt in case of an external selection.
 1419          */
 1420         if (cfg.prefersel && selbufpos)
 1421             return 's';
 1422 
 1423         int choice = get_input(messages[MSG_CUR_SEL_OPTS]);
 1424 
 1425         return ((choice == 'c' || choice == 's') ? choice : 0);
 1426     }
 1427 
 1428     if (sel)
 1429         return 's';
 1430 
 1431     if (ndents)
 1432         return 'c';
 1433 
 1434     return 0;
 1435 }
 1436 
 1437 static void xdelay(useconds_t delay)
 1438 {
 1439     refresh();
 1440     usleep(delay);
 1441 }
 1442 
 1443 static char confirm_force(bool selection)
 1444 {
 1445     char str[64];
 1446 
 1447     snprintf(str, 64, messages[MSG_FORCE_RM],
 1448          g_state.trash ? utils[UTIL_GIO_TRASH] + 4 : utils[UTIL_RM_RF],
 1449          (selection ? "selection" : "hovered"));
 1450 
 1451     int r = get_input(str);
 1452 
 1453     if (r == ESC)
 1454         return '\0'; /* cancel */
 1455     if (r == 'y' || r == 'Y')
 1456         return 'f'; /* forceful for rm */
 1457     return (g_state.trash ? '\0' : 'i'); /* interactive for rm */
 1458 }
 1459 
 1460 /* Writes buflen char(s) from buf to a file */
 1461 static void writesel(const char *buf, const size_t buflen)
 1462 {
 1463     if (!selpath)
 1464         return;
 1465 
 1466     int fd = open(selpath, O_CREAT | O_WRONLY | O_TRUNC, 0666);
 1467 
 1468     if (fd != -1) {
 1469         if (write(fd, buf, buflen) != (ssize_t)buflen)
 1470             printwarn(NULL);
 1471         close(fd);
 1472     } else
 1473         printwarn(NULL);
 1474 }
 1475 
 1476 static void appendfpath(const char *path, const size_t len)
 1477 {
 1478     if ((selbufpos >= selbuflen) || ((len + 3) > (selbuflen - selbufpos))) {
 1479         selbuflen += PATH_MAX;
 1480         pselbuf = xrealloc(pselbuf, selbuflen);
 1481         if (!pselbuf)
 1482             errexit();
 1483     }
 1484 
 1485     selbufpos += xstrsncpy(pselbuf + selbufpos, path, len);
 1486 }
 1487 
 1488 static void selbufrealloc(const size_t alloclen)
 1489 {
 1490     if ((selbufpos + alloclen) > selbuflen) {
 1491         selbuflen = ALIGN_UP(selbufpos + alloclen, PATH_MAX);
 1492         pselbuf = xrealloc(pselbuf, selbuflen);
 1493         if (!pselbuf)
 1494             errexit();
 1495     }
 1496 }
 1497 
 1498 /* Write selected file paths to fd, linefeed separated */
 1499 static size_t seltofile(int fd, uint_t *pcount)
 1500 {
 1501     uint_t lastpos, count = 0;
 1502     char *pbuf = pselbuf;
 1503     size_t pos = 0;
 1504     ssize_t len, prefixlen = 0, initlen = 0;
 1505 
 1506     if (pcount)
 1507         *pcount = 0;
 1508 
 1509     if (!selbufpos)
 1510         return 0;
 1511 
 1512     lastpos = selbufpos - 1;
 1513 
 1514     if (listpath) {
 1515         prefixlen = (ssize_t)xstrlen(listroot);
 1516         initlen = (ssize_t)xstrlen(listpath);
 1517     }
 1518 
 1519     while (pos <= lastpos) {
 1520         DPRINTF_S(pbuf);
 1521         len = (ssize_t)xstrlen(pbuf);
 1522 
 1523         if (!listpath || !is_prefix(pbuf, listpath, initlen)) {
 1524             if (write(fd, pbuf, len) != len)
 1525                 return pos;
 1526         } else {
 1527             if (write(fd, listroot, prefixlen) != prefixlen)
 1528                 return pos;
 1529             if (write(fd, pbuf + initlen, len - initlen) != (len - initlen))
 1530                 return pos;
 1531         }
 1532 
 1533         pos += len;
 1534         if (pos <= lastpos) {
 1535             if (write(fd, "\n", 1) != 1)
 1536                 return pos;
 1537             pbuf += len + 1;
 1538         }
 1539         ++pos;
 1540         ++count;
 1541     }
 1542 
 1543     if (pcount)
 1544         *pcount = count;
 1545 
 1546     return pos;
 1547 }
 1548 
 1549 /* List selection from selection file (another instance) */
 1550 static bool listselfile(void)
 1551 {
 1552     if (isselfileempty())
 1553         return FALSE;
 1554 
 1555     snprintf(g_buf, CMD_LEN_MAX, "tr \'\\0\' \'\\n\' < %s", selpath);
 1556     spawn(utils[UTIL_SH_EXEC], g_buf, NULL, NULL, F_CLI | F_CONFIRM);
 1557 
 1558     return TRUE;
 1559 }
 1560 
 1561 /* Reset selection indicators */
 1562 static void resetselind(void)
 1563 {
 1564     for (int r = 0; r < ndents; ++r)
 1565         if (pdents[r].flags & FILE_SELECTED)
 1566             pdents[r].flags &= ~FILE_SELECTED;
 1567 }
 1568 
 1569 static void startselection(void)
 1570 {
 1571     if (!g_state.selmode) {
 1572         g_state.selmode = 1;
 1573         nselected = 0;
 1574 
 1575         if (selbufpos) {
 1576             resetselind();
 1577             writesel(NULL, 0);
 1578             selbufpos = 0;
 1579         }
 1580     }
 1581 }
 1582 
 1583 static void clearselection(void)
 1584 {
 1585     nselected = 0;
 1586     selbufpos = 0;
 1587     g_state.selmode = 0;
 1588     writesel(NULL, 0);
 1589 }
 1590 
 1591 static char *findinsel(char *startpos, int len)
 1592 {
 1593     if (!selbufpos)
 1594         return FALSE;
 1595 
 1596     if (!startpos)
 1597         startpos = pselbuf;
 1598 
 1599     char *found = startpos;
 1600     size_t buflen = selbufpos - (startpos - pselbuf);
 1601 
 1602     while (1) {
 1603         /* memmem(3): not specified in POSIX.1, but present on a number of other systems. */
 1604         found = memmem(found, buflen - (found - startpos), g_sel, len);
 1605         if (!found)
 1606             return NULL;
 1607         if (found == startpos || *(found - 1) == '\0')
 1608             return found;
 1609         found += len; /* We found g_sel as a substring of a path, move forward */
 1610         if (found >= startpos + buflen)
 1611             return NULL;
 1612     }
 1613 }
 1614 
 1615 static int markcmp(const void *va, const void *vb)
 1616 {
 1617     const selmark *ma = (selmark*)va;
 1618     const selmark *mb = (selmark*)vb;
 1619 
 1620     return ma->startpos - mb->startpos;
 1621 }
 1622 
 1623 /* scanselforpath() must be called before calling this */
 1624 static inline void findmarkentry(size_t len, struct entry *dentp)
 1625 {
 1626     if (!(dentp->flags & FILE_SCANNED)) {
 1627         if (findinsel(findselpos, len + xstrsncpy(g_sel + len, dentp->name, dentp->nlen)))
 1628             dentp->flags |= FILE_SELECTED;
 1629         dentp->flags |= FILE_SCANNED;
 1630     }
 1631 }
 1632 
 1633 /*
 1634  * scanselforpath() must be called before calling this
 1635  * pathlen = length of path + 1 (+1 for trailing slash)
 1636  */
 1637 static void invertselbuf(const int pathlen)
 1638 {
 1639     size_t len, endpos, shrinklen = 0, alloclen = 0;
 1640     char * const pbuf = g_sel + pathlen;
 1641     char *found;
 1642     int i, nmarked = 0, prev = 0;
 1643     struct entry *dentp;
 1644     selmark *marked = malloc(nselected * sizeof(selmark));
 1645     bool scan = FALSE;
 1646 
 1647     /* First pass: inversion */
 1648     for (i = 0; i < ndents; ++i) {
 1649         dentp = &pdents[i];
 1650 
 1651         if (dentp->flags & FILE_SCANNED) {
 1652             if (dentp->flags & FILE_SELECTED) {
 1653                 dentp->flags ^= FILE_SELECTED; /* Clear selection status */
 1654                 scan = TRUE;
 1655             } else {
 1656                 dentp->flags |= FILE_SELECTED;
 1657                 alloclen += pathlen + dentp->nlen;
 1658             }
 1659         } else {
 1660             dentp->flags |= FILE_SCANNED;
 1661             scan = TRUE;
 1662         }
 1663 
 1664         if (scan) {
 1665             len = pathlen + xstrsncpy(pbuf, dentp->name, NAME_MAX);
 1666             found = findinsel(findselpos, len);
 1667             if (found) {
 1668                 if (findselpos == found)
 1669                     findselpos += len;
 1670 
 1671                 if (nmarked && (found
 1672                     == (marked[nmarked - 1].startpos + marked[nmarked - 1].len)))
 1673                     marked[nmarked - 1].len += len;
 1674                 else {
 1675                     marked[nmarked].startpos = found;
 1676                     marked[nmarked].len = len;
 1677                     ++nmarked;
 1678                 }
 1679 
 1680                 --nselected;
 1681                 shrinklen += len; /* buffer size adjustment */
 1682             } else {
 1683                 dentp->flags |= FILE_SELECTED;
 1684                 alloclen += pathlen + dentp->nlen;
 1685             }
 1686             scan = FALSE;
 1687         }
 1688     }
 1689 
 1690     /*
 1691      * Files marked for deselection could be found in arbitrary order.
 1692      * Sort by appearance in selection buffer.
 1693      * With entries sorted we can merge adjacent ones allowing us to
 1694      * move them in a single go.
 1695      */
 1696     qsort(marked, nmarked, sizeof(selmark), &markcmp);
 1697 
 1698     /* Some files might be adjacent. Merge them into a single entry */
 1699     for (i = 1; i < nmarked; ++i) {
 1700         if (marked[i].startpos == marked[prev].startpos + marked[prev].len)
 1701             marked[prev].len += marked[i].len;
 1702         else {
 1703             ++prev;
 1704             marked[prev].startpos = marked[i].startpos;
 1705             marked[prev].len = marked[i].len;
 1706         }
 1707     }
 1708 
 1709     /*
 1710      * Number of entries is increased by encountering a non-adjacent entry
 1711      * After we finish the loop we should increment it once more.
 1712      */
 1713 
 1714     if (nmarked) /* Make sure there is something to deselect */
 1715         nmarked = prev + 1;
 1716 
 1717     /* Using merged entries remove unselected chunks from selection buffer */
 1718     for (i = 0; i < nmarked; ++i) {
 1719         /*
 1720          * found: points to where the current block starts
 1721          *        variable is recycled from previous for readability
 1722          * endpos: points to where the the next block starts
 1723          *         area between the end of current block (found + len)
 1724          *         and endpos is selected entries. This is what we are
 1725          *         moving back.
 1726          */
 1727         found = marked[i].startpos;
 1728         endpos = (i + 1 == nmarked ? selbufpos : marked[i + 1].startpos - pselbuf);
 1729         len = marked[i].len;
 1730 
 1731         /* Move back only selected entries. No selected memory is moved twice */
 1732         memmove(found, found + len, endpos - (found + len - pselbuf));
 1733     }
 1734 
 1735     free(marked);
 1736 
 1737     /* Buffer size adjustment */
 1738     selbufpos -= shrinklen;
 1739 
 1740     selbufrealloc(alloclen);
 1741 
 1742     /* Second pass: append newly selected to buffer */
 1743     for (i = 0; i < ndents; ++i) {
 1744         if (pdents[i].flags & FILE_SELECTED) {
 1745             len = pathlen + xstrsncpy(pbuf, pdents[i].name, NAME_MAX);
 1746             appendfpath(g_sel, len);
 1747             ++nselected;
 1748         }
 1749     }
 1750 
 1751     nselected ? writesel(pselbuf, selbufpos - 1) : clearselection();
 1752 }
 1753 
 1754 /*
 1755  * scanselforpath() must be called before calling this
 1756  * pathlen = length of path + 1 (+1 for trailing slash)
 1757  */
 1758 static void addtoselbuf(const int pathlen, int startid, int endid)
 1759 {
 1760     int i;
 1761     size_t len, alloclen = 0;
 1762     struct entry *dentp;
 1763     char *found;
 1764     char * const pbuf = g_sel + pathlen;
 1765 
 1766     /* Remember current selection buffer position */
 1767     for (i = startid; i <= endid; ++i) {
 1768         dentp = &pdents[i];
 1769 
 1770         if (findselpos) {
 1771             len = pathlen + xstrsncpy(pbuf, dentp->name, NAME_MAX);
 1772             found = findinsel(findselpos, len);
 1773             if (found) {
 1774                 dentp->flags |= (FILE_SCANNED | FILE_SELECTED);
 1775                 if (found == findselpos) {
 1776                     findselpos += len;
 1777                     if (findselpos == (pselbuf + selbufpos))
 1778                         findselpos = NULL;
 1779                 }
 1780             } else
 1781                 alloclen += pathlen + dentp->nlen;
 1782         } else
 1783             alloclen += pathlen + dentp->nlen;
 1784     }
 1785 
 1786     selbufrealloc(alloclen);
 1787 
 1788     for (i = startid; i <= endid; ++i) {
 1789         if (!(pdents[i].flags & FILE_SELECTED)) {
 1790             len = pathlen + xstrsncpy(pbuf, pdents[i].name, NAME_MAX);
 1791             appendfpath(g_sel, len);
 1792             ++nselected;
 1793             pdents[i].flags |= (FILE_SCANNED | FILE_SELECTED);
 1794         }
 1795     }
 1796 
 1797     writesel(pselbuf, selbufpos - 1); /* Truncate NULL from end */
 1798 }
 1799 
 1800 /* Removes g_sel from selbuf */
 1801 static void rmfromselbuf(size_t len)
 1802 {
 1803     char *found = findinsel(findselpos, len);
 1804     if (!found)
 1805         return;
 1806 
 1807     memmove(found, found + len, selbufpos - (found + len - pselbuf));
 1808     selbufpos -= len;
 1809 
 1810     nselected ? writesel(pselbuf, selbufpos - 1) : clearselection();
 1811 }
 1812 
 1813 static int scanselforpath(const char *path, bool getsize)
 1814 {
 1815     if (!path[1]) { /* path should always be at least two bytes (including NULL) */
 1816         g_sel[0] = '/';
 1817         findselpos = pselbuf;
 1818         return 1; /* Length of '/' is 1 */
 1819     }
 1820 
 1821     size_t off = xstrsncpy(g_sel, path, PATH_MAX);
 1822 
 1823     g_sel[off - 1] = '/';
 1824     /*
 1825      * We set findselpos only here. Directories can be listed in arbitrary order.
 1826      * This is the best best we can do for remembering position.
 1827      */
 1828     findselpos = findinsel(NULL, off);
 1829 
 1830     if (getsize)
 1831         return off;
 1832     return (findselpos ? off : 0);
 1833 }
 1834 
 1835 /* Finish selection procedure before an operation */
 1836 static void endselection(bool endselmode)
 1837 {
 1838     int fd;
 1839     ssize_t count;
 1840     char buf[sizeof(patterns[P_REPLACE]) + PATH_MAX + (TMP_LEN_MAX << 1)];
 1841 
 1842     if (endselmode && g_state.selmode)
 1843         g_state.selmode = 0;
 1844 
 1845     /* The code below is only for listing mode */
 1846     if (!listpath || !selbufpos)
 1847         return;
 1848 
 1849     fd = create_tmp_file();
 1850     if (fd == -1) {
 1851         DPRINTF_S("couldn't create tmp file");
 1852         return;
 1853     }
 1854 
 1855     seltofile(fd, NULL);
 1856     if (close(fd)) {
 1857         DPRINTF_S(strerror(errno));
 1858         printwarn(NULL);
 1859         return;
 1860     }
 1861 
 1862     snprintf(buf, sizeof(buf), patterns[P_REPLACE], listpath, listroot, g_tmpfpath);
 1863     spawn(utils[UTIL_SH_EXEC], buf, NULL, NULL, F_CLI);
 1864 
 1865     fd = open(g_tmpfpath, O_RDONLY);
 1866     if (fd == -1) {
 1867         DPRINTF_S(strerror(errno));
 1868         printwarn(NULL);
 1869         if (unlink(g_tmpfpath)) {
 1870             DPRINTF_S(strerror(errno));
 1871             printwarn(NULL);
 1872         }
 1873         return;
 1874     }
 1875 
 1876     count = read(fd, pselbuf, selbuflen);
 1877     if (count < 0) {
 1878         DPRINTF_S(strerror(errno));
 1879         printwarn(NULL);
 1880         if (close(fd) || unlink(g_tmpfpath)) {
 1881             DPRINTF_S(strerror(errno));
 1882         }
 1883         return;
 1884     }
 1885 
 1886     if (close(fd) || unlink(g_tmpfpath)) {
 1887         DPRINTF_S(strerror(errno));
 1888         printwarn(NULL);
 1889         return;
 1890     }
 1891 
 1892     selbufpos = count;
 1893     pselbuf[--count] = '\0';
 1894     for (--count; count > 0; --count)
 1895         if (pselbuf[count] == '\n' && pselbuf[count+1] == '/')
 1896             pselbuf[count] = '\0';
 1897 
 1898     writesel(pselbuf, selbufpos - 1);
 1899 }
 1900 
 1901 /* Returns: 1 - success, 0 - none selected, -1 - other failure */
 1902 static int editselection(void)
 1903 {
 1904     int ret = -1;
 1905     int fd, lines = 0;
 1906     ssize_t count;
 1907     struct stat sb;
 1908     time_t mtime;
 1909 
 1910     if (!selbufpos) /* External selection is only editable at source */
 1911         return listselfile();
 1912 
 1913     fd = create_tmp_file();
 1914     if (fd == -1) {
 1915         DPRINTF_S("couldn't create tmp file");
 1916         return -1;
 1917     }
 1918 
 1919     seltofile(fd, NULL);
 1920     if (close(fd)) {
 1921         DPRINTF_S(strerror(errno));
 1922         return -1;
 1923     }
 1924 
 1925     /* Save the last modification time */
 1926     if (stat(g_tmpfpath, &sb)) {
 1927         DPRINTF_S(strerror(errno));
 1928         unlink(g_tmpfpath);
 1929         return -1;
 1930     }
 1931     mtime = sb.st_mtime;
 1932 
 1933     spawn((cfg.waitedit ? enveditor : editor), g_tmpfpath, NULL, NULL, F_CLI);
 1934 
 1935     fd = open(g_tmpfpath, O_RDONLY);
 1936     if (fd == -1) {
 1937         DPRINTF_S(strerror(errno));
 1938         unlink(g_tmpfpath);
 1939         return -1;
 1940     }
 1941 
 1942     fstat(fd, &sb);
 1943 
 1944     if (mtime == sb.st_mtime) {
 1945         DPRINTF_S("selection is not modified");
 1946         unlink(g_tmpfpath);
 1947         return 1;
 1948     }
 1949 
 1950     if (sb.st_size > selbufpos) {
 1951         DPRINTF_S("edited buffer larger than previous");
 1952         unlink(g_tmpfpath);
 1953         goto emptyedit;
 1954     }
 1955 
 1956     count = read(fd, pselbuf, selbuflen);
 1957     if (count < 0) {
 1958         DPRINTF_S(strerror(errno));
 1959         printwarn(NULL);
 1960         if (close(fd) || unlink(g_tmpfpath)) {
 1961             DPRINTF_S(strerror(errno));
 1962             printwarn(NULL);
 1963         }
 1964         goto emptyedit;
 1965     }
 1966 
 1967     if (close(fd) || unlink(g_tmpfpath)) {
 1968         DPRINTF_S(strerror(errno));
 1969         printwarn(NULL);
 1970         goto emptyedit;
 1971     }
 1972 
 1973     if (!count) {
 1974         ret = 1;
 1975         goto emptyedit;
 1976     }
 1977 
 1978     resetselind();
 1979     selbufpos = count;
 1980     /* The last character should be '\n' */
 1981     pselbuf[--count] = '\0';
 1982     for (--count; count > 0; --count) {
 1983         /* Replace every '\n' that separates two paths */
 1984         if (pselbuf[count] == '\n' && pselbuf[count + 1] == '/') {
 1985             ++lines;
 1986             pselbuf[count] = '\0';
 1987         }
 1988     }
 1989 
 1990     /* Add a line for the last file */
 1991     ++lines;
 1992 
 1993     if (lines > nselected) {
 1994         DPRINTF_S("files added to selection");
 1995         goto emptyedit;
 1996     }
 1997 
 1998     nselected = lines;
 1999     writesel(pselbuf, selbufpos - 1);
 2000 
 2001     return 1;
 2002 
 2003 emptyedit:
 2004     resetselind();
 2005     clearselection();
 2006     return ret;
 2007 }
 2008 
 2009 static bool selsafe(void)
 2010 {
 2011     /* Fail if selection file path not generated */
 2012     if (!selpath) {
 2013         printmsg(messages[MSG_SEL_MISSING]);
 2014         return FALSE;
 2015     }
 2016 
 2017     /* Fail if selection file path isn't accessible */
 2018     if (access(selpath, R_OK | W_OK) == -1) {
 2019         errno == ENOENT ? printmsg(messages[MSG_0_SELECTED]) : printwarn(NULL);
 2020         return FALSE;
 2021     }
 2022 
 2023     return TRUE;
 2024 }
 2025 
 2026 static void export_file_list(void)
 2027 {
 2028     if (!ndents)
 2029         return;
 2030 
 2031     struct entry *pdent = pdents;
 2032     int fd = create_tmp_file();
 2033 
 2034     if (fd == -1) {
 2035         DPRINTF_S(strerror(errno));
 2036         return;
 2037     }
 2038 
 2039     for (int r = 0; r < ndents; ++pdent, ++r) {
 2040         if (write(fd, pdent->name, pdent->nlen - 1) != (pdent->nlen - 1))
 2041             break;
 2042 
 2043         if ((r != ndents - 1) && (write(fd, "\n", 1) != 1))
 2044             break;
 2045     }
 2046 
 2047     if (close(fd)) {
 2048         DPRINTF_S(strerror(errno));
 2049     }
 2050 
 2051     spawn(editor, g_tmpfpath, NULL, NULL, F_CLI);
 2052 
 2053     if (xconfirm(get_input(messages[MSG_RM_TMP])))
 2054         unlink(g_tmpfpath);
 2055 }
 2056 
 2057 static bool init_fcolors(void)
 2058 {
 2059     char *f_colors = getenv(env_cfg[NNN_FCOLORS]);
 2060 
 2061     if (!f_colors || !*f_colors)
 2062         f_colors = gcolors;
 2063 
 2064     for (uchar_t id = C_BLK; *f_colors && id <= C_UND; ++id) {
 2065         fcolors[id] = xchartohex(*f_colors) << 4;
 2066         if (*++f_colors) {
 2067             fcolors[id] += xchartohex(*f_colors);
 2068             if (fcolors[id])
 2069                 init_pair(id, fcolors[id], -1);
 2070         } else
 2071             return FALSE;
 2072         ++f_colors;
 2073     }
 2074 
 2075     return TRUE;
 2076 }
 2077 
 2078 /* Initialize curses mode */
 2079 static bool initcurses(void *oldmask)
 2080 {
 2081 #ifdef NOMOUSE
 2082     (void) oldmask;
 2083 #endif
 2084 
 2085     if (g_state.picker) {
 2086         if (!newterm(NULL, stderr, stdin)) {
 2087             msg("newterm!");
 2088             return FALSE;
 2089         }
 2090     } else if (!initscr()) {
 2091         msg("initscr!");
 2092         DPRINTF_S(getenv("TERM"));
 2093         return FALSE;
 2094     }
 2095 
 2096     cbreak();
 2097     noecho();
 2098     nonl();
 2099     //intrflush(stdscr, FALSE);
 2100     keypad(stdscr, TRUE);
 2101 #ifndef NOMOUSE
 2102 #if NCURSES_MOUSE_VERSION <= 1
 2103     mousemask(BUTTON1_PRESSED | BUTTON1_DOUBLE_CLICKED | BUTTON2_PRESSED | BUTTON3_PRESSED,
 2104           (mmask_t *)oldmask);
 2105 #else
 2106     mousemask(BUTTON1_PRESSED | BUTTON2_PRESSED | BUTTON3_PRESSED | BUTTON4_PRESSED
 2107           | BUTTON5_PRESSED, (mmask_t *)oldmask);
 2108 #endif
 2109     mouseinterval(0);
 2110 #endif
 2111     curs_set(FALSE); /* Hide cursor */
 2112 
 2113     char *colors = getenv(env_cfg[NNN_COLORS]);
 2114 
 2115     if (colors || !getenv("NO_COLOR")) {
 2116         uint_t *pcode;
 2117         bool ext = FALSE;
 2118 
 2119         start_color();
 2120         use_default_colors();
 2121 
 2122         /* Initialize file colors */
 2123         if (COLORS >= COLOR_256) {
 2124             if (!(g_state.oldcolor || init_fcolors())) {
 2125                 exitcurses();
 2126                 msg(env_cfg[NNN_FCOLORS]);
 2127                 return FALSE;
 2128             }
 2129         } else
 2130             g_state.oldcolor = 1;
 2131 
 2132         DPRINTF_D(COLORS);
 2133         DPRINTF_D(COLOR_PAIRS);
 2134 
 2135         if (colors && *colors == '#') {
 2136             char *sep = strchr(colors, ';');
 2137 
 2138             if (!g_state.oldcolor && COLORS >= COLOR_256) {
 2139                 ++colors;
 2140                 ext = TRUE;
 2141 
 2142                 /*
 2143                  * If fallback colors are specified, set the separator
 2144                  * to NULL so we don't interpret separator and fallback
 2145                  * if fewer than CTX_MAX xterm 256 colors are specified.
 2146                  */
 2147                 if (sep)
 2148                     *sep = '\0';
 2149             } else {
 2150                 colors = sep; /* Detect if 8 colors fallback is appended */
 2151                 if (colors)
 2152                     ++colors;
 2153             }
 2154         }
 2155 
 2156         /* Get and set the context colors */
 2157         for (uchar_t i = 0; i <  CTX_MAX; ++i) {
 2158             pcode = &g_ctx[i].color;
 2159 
 2160             if (colors && *colors) {
 2161                 if (ext) {
 2162                     *pcode = xchartohex(*colors) << 4;
 2163                     if (*++colors)
 2164                         fcolors[i + 1] = *pcode += xchartohex(*colors);
 2165                     else { /* Each color code must be 2 hex symbols */
 2166                         exitcurses();
 2167                         msg(env_cfg[NNN_COLORS]);
 2168                         return FALSE;
 2169                     }
 2170                 } else
 2171                     *pcode = (*colors < '0' || *colors > '7') ? 4 : *colors - '0';
 2172                 ++colors;
 2173             } else
 2174                 *pcode = 4;
 2175 
 2176             init_pair(i + 1, *pcode, -1);
 2177         }
 2178     }
 2179 
 2180 #ifdef ICONS_ENABLED
 2181     if (!g_state.oldcolor) {
 2182         uchar_t icolors[COLOR_256] = {0};
 2183         char c;
 2184 
 2185         memset(icon_positions, 0x7f, sizeof(icon_positions));
 2186 
 2187         for (uint_t i = 0; i < sizeof(icons_ext)/sizeof(struct icon_pair); ++i) {
 2188             c = TOUPPER(icons_ext[i].match[0]);
 2189             if (c >= 'A' && c <= 'Z') {
 2190                 if (icon_positions[c - 'A' + 10] == 0x7f7f)
 2191                     icon_positions[c - 'A' + 10] = i;
 2192             } else if (c >= '0' && c <= '9') {
 2193                 if (icon_positions[c - '0'] == 0x7f7f)
 2194                     icon_positions[c - '0'] = i;
 2195             } else if (icon_positions[36] == 0x7f7f)
 2196                 icon_positions[36] = i;
 2197 
 2198             if (icons_ext[i].color && !icolors[icons_ext[i].color]) {
 2199                 init_pair(C_UND + 1 + icons_ext[i].color, icons_ext[i].color, -1);
 2200                 icolors[icons_ext[i].color] = 1;
 2201             }
 2202         }
 2203     }
 2204 #endif
 2205 
 2206     settimeout(); /* One second */
 2207     set_escdelay(25);
 2208     return TRUE;
 2209 }
 2210 
 2211 /* No NULL check here as spawn() guards against it */
 2212 static char *parseargs(char *cmd, char **argv, int *pindex)
 2213 {
 2214     int count = 0;
 2215     size_t len = xstrlen(cmd) + 1;
 2216     char *line = (char *)malloc(len);
 2217 
 2218     if (!line) {
 2219         DPRINTF_S("malloc()!");
 2220         return NULL;
 2221     }
 2222 
 2223     xstrsncpy(line, cmd, len);
 2224     argv[count++] = line;
 2225     cmd = line;
 2226 
 2227     while (*line) { // NOLINT
 2228         if (ISBLANK(*line)) {
 2229             *line++ = '\0';
 2230 
 2231             if (!*line) // NOLINT
 2232                 break;
 2233 
 2234             argv[count++] = line;
 2235             if (count == EXEC_ARGS_MAX) {
 2236                 count = -1;
 2237                 break;
 2238             }
 2239         }
 2240 
 2241         ++line;
 2242     }
 2243 
 2244     if (count == -1 || count > (EXEC_ARGS_MAX - 4)) { /* 3 args and last NULL */
 2245         free(cmd);
 2246         cmd = NULL;
 2247         DPRINTF_S("NULL or too many args");
 2248     }
 2249 
 2250     *pindex = count;
 2251     return cmd;
 2252 }
 2253 
 2254 static void enable_signals(void)
 2255 {
 2256     struct sigaction dfl_act = {.sa_handler = SIG_DFL};
 2257 
 2258     sigaction(SIGHUP, &dfl_act, NULL);
 2259     sigaction(SIGINT, &dfl_act, NULL);
 2260     sigaction(SIGQUIT, &dfl_act, NULL);
 2261     sigaction(SIGTSTP, &dfl_act, NULL);
 2262     sigaction(SIGWINCH, &dfl_act, NULL);
 2263 }
 2264 
 2265 static pid_t xfork(uchar_t flag)
 2266 {
 2267     pid_t p = fork();
 2268 
 2269     if (p > 0) {
 2270         /* the parent ignores the interrupt, quit and hangup signals */
 2271         sigaction(SIGHUP, &(struct sigaction){.sa_handler = SIG_IGN}, &oldsighup);
 2272         sigaction(SIGTSTP, &(struct sigaction){.sa_handler = SIG_DFL}, &oldsigtstp);
 2273         sigaction(SIGWINCH, &(struct sigaction){.sa_handler = SIG_IGN}, &oldsigwinch);
 2274     } else if (p == 0) {
 2275         /* We create a grandchild to detach */
 2276         if (flag & F_NOWAIT) {
 2277             p = fork();
 2278 
 2279             if (p > 0)
 2280                 _exit(EXIT_SUCCESS);
 2281             else if (p == 0) {
 2282                 enable_signals();
 2283                 setsid();
 2284                 return p;
 2285             }
 2286 
 2287             perror("fork");
 2288             _exit(EXIT_FAILURE);
 2289         }
 2290 
 2291         /* So they can be used to stop the child */
 2292         enable_signals();
 2293     }
 2294 
 2295     /* This is the parent waiting for the child to create grandchild */
 2296     if (flag & F_NOWAIT)
 2297         waitpid(p, NULL, 0);
 2298 
 2299     if (p == -1)
 2300         perror("fork");
 2301     return p;
 2302 }
 2303 
 2304 static int join(pid_t p, uchar_t flag)
 2305 {
 2306     int status = 0xFFFF;
 2307 
 2308     if (!(flag & F_NOWAIT)) {
 2309         /* wait for the child to exit */
 2310         do {
 2311         } while (waitpid(p, &status, 0) == -1);
 2312 
 2313         if (WIFEXITED(status)) {
 2314             status = WEXITSTATUS(status);
 2315             DPRINTF_D(status);
 2316         }
 2317     }
 2318 
 2319     /* restore parent's signal handling */
 2320     sigaction(SIGHUP, &oldsighup, NULL);
 2321     sigaction(SIGTSTP, &oldsigtstp, NULL);
 2322     sigaction(SIGWINCH, &oldsigwinch, NULL);
 2323 
 2324     return status;
 2325 }
 2326 
 2327 /*
 2328  * Spawns a child process. Behaviour can be controlled using flag.
 2329  * Limited to 3 arguments to a program, flag works on bit set.
 2330  */
 2331 static int spawn(char *file, char *arg1, char *arg2, char *arg3, ushort_t flag)
 2332 {
 2333     pid_t pid;
 2334     int status = 0, retstatus = 0xFFFF;
 2335     char *argv[EXEC_ARGS_MAX] = {0};
 2336     char *cmd = NULL;
 2337 
 2338     if (!file || !*file)
 2339         return retstatus;
 2340 
 2341     /* Swap args if the first arg is NULL and the other 2 aren't */
 2342     if (!arg1 && arg2) {
 2343         arg1 = arg2;
 2344         if (arg3) {
 2345             arg2 = arg3;
 2346             arg3 = NULL;
 2347         } else
 2348             arg2 = NULL;
 2349     }
 2350 
 2351     if (flag & F_MULTI) {
 2352         cmd = parseargs(file, argv, &status);
 2353         if (!cmd)
 2354             return -1;
 2355     } else
 2356         argv[status++] = file;
 2357 
 2358     argv[status] = arg1;
 2359     argv[++status] = arg2;
 2360     argv[++status] = arg3;
 2361 
 2362     if (flag & F_NORMAL)
 2363         exitcurses();
 2364 
 2365     pid = xfork(flag);
 2366     if (pid == 0) {
 2367         /* Suppress stdout and stderr */
 2368         if (flag & F_NOTRACE) {
 2369             int fd = open("/dev/null", O_WRONLY, 0200);
 2370 
 2371             if (flag & F_NOSTDIN)
 2372                 dup2(fd, STDIN_FILENO);
 2373             dup2(fd, STDOUT_FILENO);
 2374             dup2(fd, STDERR_FILENO);
 2375             close(fd);
 2376         } else if (flag & F_TTY) {
 2377             /* If stdout has been redirected to a non-tty, force output to tty */
 2378             if (!isatty(STDOUT_FILENO)) {
 2379                 int fd = open(ctermid(NULL), O_WRONLY, 0200);
 2380                 dup2(fd, STDOUT_FILENO);
 2381                 close(fd);
 2382             }
 2383         }
 2384 
 2385         execvp(*argv, argv);
 2386         _exit(EXIT_SUCCESS);
 2387     } else {
 2388         retstatus = join(pid, flag);
 2389         DPRINTF_D(pid);
 2390 
 2391         if ((flag & F_CONFIRM) || ((flag & F_CHKRTN) && retstatus)) {
 2392             status = write(STDOUT_FILENO, messages[MSG_ENTER], xstrlen(messages[MSG_ENTER]));
 2393             (void)status;
 2394             while ((read(STDIN_FILENO, &status, 1) > 0) && (status != '\n'));
 2395         }
 2396 
 2397         if (flag & F_NORMAL)
 2398             refresh();
 2399 
 2400         free(cmd);
 2401     }
 2402 
 2403     return retstatus;
 2404 }
 2405 
 2406 /* Get program name from env var, else return fallback program */
 2407 static char *xgetenv(const char * const name, char *fallback)
 2408 {
 2409     char *value = getenv(name);
 2410 
 2411     return value && value[0] ? value : fallback;
 2412 }
 2413 
 2414 /* Checks if an env variable is set to 1 */
 2415 static inline uint_t xgetenv_val(const char *name)
 2416 {
 2417     char *str = getenv(name);
 2418 
 2419     if (str && str[0])
 2420         return atoi(str);
 2421 
 2422     return 0;
 2423 }
 2424 
 2425 /* Check if a dir exists, IS a dir, and is readable */
 2426 static bool xdiraccess(const char *path)
 2427 {
 2428     DIR *dirp = opendir(path);
 2429 
 2430     if (!dirp) {
 2431         printwarn(NULL);
 2432         return FALSE;
 2433     }
 2434 
 2435     closedir(dirp);
 2436     return TRUE;
 2437 }
 2438 
 2439 static bool plugscript(const char *plugin, uchar_t flags)
 2440 {
 2441     mkpath(plgpath, plugin, g_buf);
 2442     if (!access(g_buf, X_OK)) {
 2443         spawn(g_buf, NULL, NULL, NULL, flags);
 2444         return TRUE;
 2445     }
 2446 
 2447     return FALSE;
 2448 }
 2449 
 2450 static void opstr(char *buf, char *op)
 2451 {
 2452     snprintf(buf, CMD_LEN_MAX, "xargs -0 sh -c '%s \"$0\" \"$@\" . < /dev/tty' < %s",
 2453          op, selpath);
 2454 }
 2455 
 2456 static bool rmmulstr(char *buf)
 2457 {
 2458     char r = confirm_force(TRUE);
 2459     if (!r)
 2460         return FALSE;
 2461 
 2462     if (!g_state.trash)
 2463         snprintf(buf, CMD_LEN_MAX, "xargs -0 sh -c 'rm -%cr \"$0\" \"$@\" < /dev/tty' < %s",
 2464              r, selpath);
 2465     else
 2466         snprintf(buf, CMD_LEN_MAX, "xargs -0 %s < %s",
 2467              utils[(g_state.trash == 1) ? UTIL_TRASH_CLI : UTIL_GIO_TRASH], selpath);
 2468 
 2469     return TRUE;
 2470 }
 2471 
 2472 /* Returns TRUE if file is removed, else FALSE */
 2473 static bool xrm(char * const fpath)
 2474 {
 2475     char r = confirm_force(FALSE);
 2476     if (!r)
 2477         return FALSE;
 2478 
 2479     if (!g_state.trash) {
 2480         char rm_opts[] = "-ir";
 2481 
 2482         rm_opts[1] = r;
 2483         spawn("rm", rm_opts, fpath, NULL, F_NORMAL | F_CHKRTN);
 2484     } else
 2485         spawn(utils[(g_state.trash == 1) ? UTIL_TRASH_CLI : UTIL_GIO_TRASH],
 2486               fpath, NULL, NULL, F_NORMAL | F_MULTI);
 2487 
 2488     return (access(fpath, F_OK) == -1); /* File is removed */
 2489 }
 2490 
 2491 static void xrmfromsel(char *path, char *fpath)
 2492 {
 2493 #ifndef NOX11
 2494     bool selected = TRUE;
 2495 #endif
 2496 
 2497     if ((pdents[cur].flags & DIR_OR_DIRLNK) && scanselforpath(fpath, FALSE))
 2498         clearselection();
 2499     else if (pdents[cur].flags & FILE_SELECTED) {
 2500         --nselected;
 2501         rmfromselbuf(mkpath(path, pdents[cur].name, g_sel));
 2502     }
 2503 #ifndef NOX11
 2504     else
 2505         selected = FALSE;
 2506 
 2507     if (selected && cfg.x11)
 2508         plugscript(utils[UTIL_CBCP], F_NOWAIT | F_NOTRACE);
 2509 #endif
 2510 }
 2511 
 2512 static uint_t lines_in_file(int fd, char *buf, size_t buflen)
 2513 {
 2514     ssize_t len;
 2515     uint_t count = 0;
 2516 
 2517     while ((len = read(fd, buf, buflen)) > 0)
 2518         while (len)
 2519             count += (buf[--len] == '\n');
 2520 
 2521     /* For all use cases 0 linecount is considered as error */
 2522     return ((len < 0) ? 0 : count);
 2523 }
 2524 
 2525 static bool cpmv_rename(int choice, const char *path)
 2526 {
 2527     int fd;
 2528     uint_t count = 0, lines = 0;
 2529     bool ret = FALSE;
 2530     char *cmd = (choice == 'c' ? cp : mv);
 2531     char buf[sizeof(patterns[P_CPMVRNM]) + sizeof(cmd) + (PATH_MAX << 1)];
 2532 
 2533     fd = create_tmp_file();
 2534     if (fd == -1)
 2535         return ret;
 2536 
 2537     /* selsafe() returned TRUE for this to be called */
 2538     if (!selbufpos) {
 2539         snprintf(buf, sizeof(buf), "tr '\\0' '\\n' < %s > %s", selpath, g_tmpfpath);
 2540         spawn(utils[UTIL_SH_EXEC], buf, NULL, NULL, F_CLI);
 2541 
 2542         count = lines_in_file(fd, buf, sizeof(buf));
 2543         if (!count)
 2544             goto finish;
 2545     } else
 2546         seltofile(fd, &count);
 2547 
 2548     close(fd);
 2549 
 2550     snprintf(buf, sizeof(buf), patterns[P_CPMVFMT], g_tmpfpath);
 2551     spawn(utils[UTIL_SH_EXEC], buf, NULL, NULL, F_CLI);
 2552 
 2553     spawn((cfg.waitedit ? enveditor : editor), g_tmpfpath, NULL, NULL, F_CLI);
 2554 
 2555     fd = open(g_tmpfpath, O_RDONLY);
 2556     if (fd == -1)
 2557         goto finish;
 2558 
 2559     lines = lines_in_file(fd, buf, sizeof(buf));
 2560     DPRINTF_U(count);
 2561     DPRINTF_U(lines);
 2562     if (!lines || (2 * count != lines)) {
 2563         DPRINTF_S("num mismatch");
 2564         goto finish;
 2565     }
 2566 
 2567     snprintf(buf, sizeof(buf), patterns[P_CPMVRNM], path, g_tmpfpath, cmd);
 2568     if (!spawn(utils[UTIL_SH_EXEC], buf, NULL, NULL, F_CLI | F_CHKRTN))
 2569         ret = TRUE;
 2570 finish:
 2571     if (fd >= 0)
 2572         close(fd);
 2573 
 2574     return ret;
 2575 }
 2576 
 2577 static bool cpmvrm_selection(enum action sel, char *path)
 2578 {
 2579     int r;
 2580 
 2581     if (isselfileempty()) {
 2582         if (nselected)
 2583             clearselection();
 2584         printmsg(messages[MSG_0_SELECTED]);
 2585         return FALSE;
 2586     }
 2587 
 2588     if (!selsafe())
 2589         return FALSE;
 2590 
 2591     switch (sel) {
 2592     case SEL_CP:
 2593         opstr(g_buf, cp);
 2594         break;
 2595     case SEL_MV:
 2596         opstr(g_buf, mv);
 2597         break;
 2598     case SEL_CPMVAS:
 2599         r = get_input(messages[MSG_CP_MV_AS]);
 2600         if (r != 'c' && r != 'm') {
 2601             printmsg(messages[MSG_INVALID_KEY]);
 2602             return FALSE;
 2603         }
 2604 
 2605         if (!cpmv_rename(r, path)) {
 2606             printmsg(messages[MSG_FAILED]);
 2607             return FALSE;
 2608         }
 2609         break;
 2610     default: /* SEL_RM */
 2611         if (!rmmulstr(g_buf)) {
 2612             printmsg(messages[MSG_CANCEL]);
 2613             return FALSE;
 2614         }
 2615     }
 2616 
 2617     if (sel != SEL_CPMVAS && spawn(utils[UTIL_SH_EXEC], g_buf, NULL, NULL, F_CLI | F_CHKRTN)) {
 2618         printmsg(messages[MSG_FAILED]);
 2619         return FALSE;
 2620     }
 2621 
 2622     /* Clear selection */
 2623     clearselection();
 2624 
 2625     return TRUE;
 2626 }
 2627 
 2628 #ifndef NOBATCH
 2629 static bool batch_rename(void)
 2630 {
 2631     int fd1, fd2;
 2632     uint_t count = 0, lines = 0;
 2633     bool dir = FALSE, ret = FALSE;
 2634     char foriginal[TMP_LEN_MAX] = {0};
 2635     static const char batchrenamecmd[] = "paste -d'\n' %s %s | "SED" 'N; /^\\(.*\\)\\n\\1$/!p;d' | "
 2636                          "tr '\n' '\\0' | xargs -0 -n2 sh -c 'mv -i \"$0\" \"$@\" <"
 2637                          " /dev/tty'";
 2638     char buf[sizeof(batchrenamecmd) + (PATH_MAX << 1)];
 2639     int i = get_cur_or_sel();
 2640 
 2641     if (!i)
 2642         return ret;
 2643 
 2644     if (i == 'c') { /* Rename entries in current dir */
 2645         selbufpos = 0;
 2646         dir = TRUE;
 2647     }
 2648 
 2649     fd1 = create_tmp_file();
 2650     if (fd1 == -1)
 2651         return ret;
 2652 
 2653     xstrsncpy(foriginal, g_tmpfpath, xstrlen(g_tmpfpath) + 1);
 2654 
 2655     fd2 = create_tmp_file();
 2656     if (fd2 == -1) {
 2657         unlink(foriginal);
 2658         close(fd1);
 2659         return ret;
 2660     }
 2661 
 2662     if (dir)
 2663         for (i = 0; i < ndents; ++i)
 2664             appendfpath(pdents[i].name, NAME_MAX);
 2665 
 2666     seltofile(fd1, &count);
 2667     seltofile(fd2, NULL);
 2668     close(fd2);
 2669 
 2670     if (dir) /* Don't retain dir entries in selection */
 2671         selbufpos = 0;
 2672 
 2673     spawn((cfg.waitedit ? enveditor : editor), g_tmpfpath, NULL, NULL, F_CLI);
 2674 
 2675     /* Reopen file descriptor to get updated contents */
 2676     fd2 = open(g_tmpfpath, O_RDONLY);
 2677     if (fd2 == -1)
 2678         goto finish;
 2679 
 2680     lines = lines_in_file(fd2, buf, sizeof(buf));
 2681     DPRINTF_U(count);
 2682     DPRINTF_U(lines);
 2683     if (!lines || (count != lines)) {
 2684         DPRINTF_S("cannot delete files");
 2685         goto finish;
 2686     }
 2687 
 2688     snprintf(buf, sizeof(buf), batchrenamecmd, foriginal, g_tmpfpath);
 2689     spawn(utils[UTIL_SH_EXEC], buf, NULL, NULL, F_CLI);
 2690     ret = TRUE;
 2691 
 2692 finish:
 2693     if (fd1 >= 0)
 2694         close(fd1);
 2695     unlink(foriginal);
 2696 
 2697     if (fd2 >= 0)
 2698         close(fd2);
 2699     unlink(g_tmpfpath);
 2700 
 2701     return ret;
 2702 }
 2703 #endif
 2704 
 2705 static void get_archive_cmd(char *cmd, const char *archive)
 2706 {
 2707     uchar_t i = 3;
 2708 
 2709     if (getutil(utils[UTIL_ATOOL]))
 2710         i = 0;
 2711     else if (getutil(utils[UTIL_BSDTAR]))
 2712         i = 1;
 2713     else if (is_suffix(archive, ".zip"))
 2714         i = 2;
 2715     // else tar
 2716 
 2717     xstrsncpy(cmd, archive_cmd[i], ARCHIVE_CMD_LEN);
 2718 }
 2719 
 2720 static void archive_selection(const char *cmd, const char *archive, const char *curpath)
 2721 {
 2722     /* The 70 comes from the string below */
 2723     char *buf = (char *)malloc((70 + xstrlen(cmd) + xstrlen(archive)
 2724                        + xstrlen(curpath) + xstrlen(selpath)) * sizeof(char));
 2725     if (!buf) {
 2726         DPRINTF_S(strerror(errno));
 2727         printwarn(NULL);
 2728         return;
 2729     }
 2730 
 2731     snprintf(buf, CMD_LEN_MAX,
 2732 #ifdef __linux__
 2733         SED" -ze 's|^%s/||' '%s' | xargs -0 %s %s", curpath, selpath, cmd, archive
 2734 #else
 2735         "tr '\\0' '\n' < '%s' | "SED" -e 's|^%s/||' | tr '\n' '\\0' | xargs -0 %s %s",
 2736         selpath, curpath, cmd, archive
 2737 #endif
 2738         );
 2739     spawn(utils[UTIL_SH_EXEC], buf, NULL, NULL, F_CLI | F_CONFIRM);
 2740     free(buf);
 2741 }
 2742 
 2743 static void write_lastdir(const char *curpath, const char *outfile)
 2744 {
 2745     if (!outfile)
 2746         xstrsncpy(cfgpath + xstrlen(cfgpath), "/.lastd", 8);
 2747     else
 2748         convert_tilde(outfile, g_buf);
 2749 
 2750     int fd = open(outfile
 2751             ? (outfile[0] == '~' ? g_buf : outfile)
 2752             : cfgpath, O_CREAT | O_WRONLY | O_TRUNC, 0666);
 2753 
 2754     if (fd != -1) {
 2755         dprintf(fd, "cd \"%s\"", curpath);
 2756         close(fd);
 2757     }
 2758 }
 2759 
 2760 /*
 2761  * We assume none of the strings are NULL.
 2762  *
 2763  * Let's have the logic to sort numeric names in numeric order.
 2764  * E.g., the order '1, 10, 2' doesn't make sense to human eyes.
 2765  *
 2766  * If the absolute numeric values are same, we fallback to alphasort.
 2767  */
 2768 static int xstricmp(const char * const s1, const char * const s2)
 2769 {
 2770     char *p1, *p2;
 2771 
 2772     long long v1 = strtoll(s1, &p1, 10);
 2773     long long v2 = strtoll(s2, &p2, 10);
 2774 
 2775     /* Check if at least 1 string is numeric */
 2776     if (s1 != p1 || s2 != p2) {
 2777         /* Handle both pure numeric */
 2778         if (s1 != p1 && s2 != p2) {
 2779             if (v2 > v1)
 2780                 return -1;
 2781 
 2782             if (v1 > v2)
 2783                 return 1;
 2784         }
 2785 
 2786         /* Only first string non-numeric */
 2787         if (s1 == p1)
 2788             return 1;
 2789 
 2790         /* Only second string non-numeric */
 2791         if (s2 == p2)
 2792             return -1;
 2793     }
 2794 
 2795     /* Handle 1. all non-numeric and 2. both same numeric value cases */
 2796 #ifndef NOLC
 2797     return strcoll(s1, s2);
 2798 #else
 2799     return strcasecmp(s1, s2);
 2800 #endif
 2801 }
 2802 
 2803 /*
 2804  * Version comparison
 2805  *
 2806  * The code for version compare is a modified version of the GLIBC
 2807  * and uClibc implementation of strverscmp(). The source is here:
 2808  * https://elixir.bootlin.com/uclibc-ng/latest/source/libc/string/strverscmp.c
 2809  */
 2810 
 2811 /*
 2812  * Compare S1 and S2 as strings holding indices/version numbers,
 2813  * returning less than, equal to or greater than zero if S1 is less than,
 2814  * equal to or greater than S2 (for more info, see the texinfo doc).
 2815  *
 2816  * Ignores case.
 2817  */
 2818 static int xstrverscasecmp(const char * const s1, const char * const s2)
 2819 {
 2820     const uchar_t *p1 = (const uchar_t *)s1;
 2821     const uchar_t *p2 = (const uchar_t *)s2;
 2822     int state, diff;
 2823     uchar_t c1, c2;
 2824 
 2825     /*
 2826      * Symbol(s)    0       [1-9]   others
 2827      * Transition   (10) 0  (01) d  (00) x
 2828      */
 2829     static const uint8_t next_state[] = {
 2830         /* state    x    d    0  */
 2831         /* S_N */  S_N, S_I, S_Z,
 2832         /* S_I */  S_N, S_I, S_I,
 2833         /* S_F */  S_N, S_F, S_F,
 2834         /* S_Z */  S_N, S_F, S_Z
 2835     };
 2836 
 2837     static const int8_t result_type[] __attribute__ ((aligned)) = {
 2838         /* state   x/x  x/d  x/0  d/x  d/d  d/0  0/x  0/d  0/0  */
 2839 
 2840         /* S_N */  VCMP, VCMP, VCMP, VCMP, VLEN, VCMP, VCMP, VCMP, VCMP,
 2841         /* S_I */  VCMP,   -1,   -1,    1, VLEN, VLEN,    1, VLEN, VLEN,
 2842         /* S_F */  VCMP, VCMP, VCMP, VCMP, VCMP, VCMP, VCMP, VCMP, VCMP,
 2843         /* S_Z */  VCMP,    1,    1,   -1, VCMP, VCMP,   -1, VCMP, VCMP
 2844     };
 2845 
 2846     if (p1 == p2)
 2847         return 0;
 2848 
 2849     c1 = TOUPPER(*p1);
 2850     ++p1;
 2851     c2 = TOUPPER(*p2);
 2852     ++p2;
 2853 
 2854     /* Hint: '0' is a digit too.  */
 2855     state = S_N + ((c1 == '0') + (xisdigit(c1) != 0));
 2856 
 2857     while ((diff = c1 - c2) == 0) {
 2858         if (c1 == '\0')
 2859             return diff;
 2860 
 2861         state = next_state[state];
 2862         c1 = TOUPPER(*p1);
 2863         ++p1;
 2864         c2 = TOUPPER(*p2);
 2865         ++p2;
 2866         state += (c1 == '0') + (xisdigit(c1) != 0);
 2867     }
 2868 
 2869     state = result_type[state * 3 + (((c2 == '0') + (xisdigit(c2) != 0)))]; // NOLINT
 2870 
 2871     switch (state) {
 2872     case VCMP:
 2873         return diff;
 2874     case VLEN:
 2875         while (xisdigit(*p1++))
 2876             if (!xisdigit(*p2++))
 2877                 return 1;
 2878         return xisdigit(*p2) ? -1 : diff;
 2879     default:
 2880         return state;
 2881     }
 2882 }
 2883 
 2884 static int (*namecmpfn)(const char * const s1, const char * const s2) = &xstricmp;
 2885 
 2886 static char * (*fnstrstr)(const char *haystack, const char *needle) = &strcasestr;
 2887 #ifdef PCRE
 2888 static const unsigned char *tables;
 2889 static int pcreflags = PCRE_NO_AUTO_CAPTURE | PCRE_EXTENDED | PCRE_CASELESS | PCRE_UTF8;
 2890 #else
 2891 static int regflags = REG_NOSUB | REG_EXTENDED | REG_ICASE;
 2892 #endif
 2893 
 2894 #ifdef PCRE
 2895 static int setfilter(pcre **pcrex, const char *filter)
 2896 {
 2897     const char *errstr = NULL;
 2898     int erroffset = 0;
 2899 
 2900     *pcrex = pcre_compile(filter, pcreflags, &errstr, &erroffset, tables);
 2901 
 2902     return errstr ? -1 : 0;
 2903 }
 2904 #else
 2905 static int setfilter(regex_t *regex, const char *filter)
 2906 {
 2907     return regcomp(regex, filter, regflags);
 2908 }
 2909 #endif
 2910 
 2911 static int visible_re(const fltrexp_t *fltrexp, const char *fname)
 2912 {
 2913 #ifdef PCRE
 2914     return pcre_exec(fltrexp->pcrex, NULL, fname, xstrlen(fname), 0, 0, NULL, 0) == 0;
 2915 #else
 2916     return regexec(fltrexp->regex, fname, 0, NULL, 0) == 0;
 2917 #endif
 2918 }
 2919 
 2920 static int visible_str(const fltrexp_t *fltrexp, const char *fname)
 2921 {
 2922     return fnstrstr(fname, fltrexp->str) != NULL;
 2923 }
 2924 
 2925 static int (*filterfn)(const fltrexp_t *fltr, const char *fname) = &visible_str;
 2926 
 2927 static void clearfilter(void)
 2928 {
 2929     char *fltr = g_ctx[cfg.curctx].c_fltr;
 2930 
 2931     if (fltr[1]) {
 2932         fltr[REGEX_MAX - 1] = fltr[1];
 2933         fltr[1] = '\0';
 2934     }
 2935 }
 2936 
 2937 static int entrycmp(const void *va, const void *vb)
 2938 {
 2939     const struct entry *pa = (pEntry)va;
 2940     const struct entry *pb = (pEntry)vb;
 2941 
 2942     if ((pb->flags & DIR_OR_DIRLNK) != (pa->flags & DIR_OR_DIRLNK)) {
 2943         if (pb->flags & DIR_OR_DIRLNK)
 2944             return 1;
 2945         return -1;
 2946     }
 2947 
 2948     /* Sort based on specified order */
 2949     if (cfg.timeorder) {
 2950         if (pb->sec > pa->sec)
 2951             return 1;
 2952         if (pb->sec < pa->sec)
 2953             return -1;
 2954         /* If sec matches, comare nsec */
 2955         if (pb->nsec > pa->nsec)
 2956             return 1;
 2957         if (pb->nsec < pa->nsec)
 2958             return -1;
 2959     } else if (cfg.sizeorder) {
 2960         if (pb->size > pa->size)
 2961             return 1;
 2962         if (pb->size < pa->size)
 2963             return -1;
 2964     } else if (cfg.blkorder) {
 2965         if (pb->blocks > pa->blocks)
 2966             return 1;
 2967         if (pb->blocks < pa->blocks)
 2968             return -1;
 2969     } else if (cfg.extnorder && !(pb->flags & DIR_OR_DIRLNK)) {
 2970         char *extna = xextension(pa->name, pa->nlen - 1);
 2971         char *extnb = xextension(pb->name, pb->nlen - 1);
 2972 
 2973         if (extna || extnb) {
 2974             if (!extna)
 2975                 return -1;
 2976 
 2977             if (!extnb)
 2978                 return 1;
 2979 
 2980             int ret = strcasecmp(extna, extnb);
 2981 
 2982             if (ret)
 2983                 return ret;
 2984         }
 2985     }
 2986 
 2987     return namecmpfn(pa->name, pb->name);
 2988 }
 2989 
 2990 static int reventrycmp(const void *va, const void *vb)
 2991 {
 2992     if ((((pEntry)vb)->flags & DIR_OR_DIRLNK)
 2993         != (((pEntry)va)->flags & DIR_OR_DIRLNK)) {
 2994         if (((pEntry)vb)->flags & DIR_OR_DIRLNK)
 2995             return 1;
 2996         return -1;
 2997     }
 2998 
 2999     return -entrycmp(va, vb);
 3000 }
 3001 
 3002 static int (*entrycmpfn)(const void *va, const void *vb) = &entrycmp;
 3003 
 3004 /* In case of an error, resets *wch to Esc */
 3005 static int handle_alt_key(wint_t *wch)
 3006 {
 3007     timeout(0);
 3008 
 3009     int r = get_wch(wch);
 3010 
 3011     if (r == ERR)
 3012         *wch = ESC;
 3013     cleartimeout();
 3014 
 3015     return r;
 3016 }
 3017 
 3018 static inline int handle_event(void)
 3019 {
 3020     if (nselected && isselfileempty())
 3021         clearselection();
 3022     return CONTROL('L');
 3023 }
 3024 
 3025 /*
 3026  * Returns SEL_* if key is bound and 0 otherwise.
 3027  * Also modifies the run and env pointers (used on SEL_{RUN,RUNARG}).
 3028  * The next keyboard input can be simulated by presel.
 3029  */
 3030 static int nextsel(int presel)
 3031 {
 3032 #ifdef BENCH
 3033     return SEL_QUIT;
 3034 #endif
 3035     int c = presel;
 3036     uint_t i;
 3037     bool escaped = FALSE;
 3038 
 3039     if (c == 0 || c == MSGWAIT) {
 3040 try_quit:
 3041         c = getch();
 3042         //DPRINTF_D(c);
 3043         //DPRINTF_S(keyname(c));
 3044 
 3045 #ifdef KEY_RESIZE
 3046         if (c == KEY_RESIZE)
 3047             handle_key_resize();
 3048 #endif
 3049 
 3050         /* Handle Alt+key */
 3051         if (c == ESC) {
 3052             timeout(0);
 3053             c = getch();
 3054             if (c != ERR) {
 3055                 if (c == ESC)
 3056                     c = CONTROL('L');
 3057                 else {
 3058                     ungetch(c);
 3059                     c = ';';
 3060                 }
 3061                 settimeout();
 3062             } else if (escaped) {
 3063                 settimeout();
 3064                 c = CONTROL('Q');
 3065             } else {
 3066 #ifndef NOFIFO
 3067                 if (!g_state.fifomode)
 3068                     notify_fifo(TRUE); /* Send hovered path to NNN_FIFO */
 3069 #endif
 3070                 escaped = TRUE;
 3071                 settimeout();
 3072                 goto try_quit;
 3073             }
 3074         }
 3075 
 3076         if (c == ERR && presel == MSGWAIT)
 3077             c = (cfg.filtermode || filterset()) ? FILTER : CONTROL('L');
 3078         else if (c == FILTER || c == CONTROL('L'))
 3079             /* Clear previous filter when manually starting */
 3080             clearfilter();
 3081     }
 3082 
 3083     if (c == -1) {
 3084         ++idle;
 3085 
 3086         /*
 3087          * Do not check for directory changes in du mode.
 3088          * A redraw forces du calculation.
 3089          * Check for changes every odd second.
 3090          */
 3091 #ifdef LINUX_INOTIFY
 3092         if (!cfg.blkorder && inotify_wd >= 0 && (idle & 1)) {
 3093             struct inotify_event *event;
 3094             char inotify_buf[EVENT_BUF_LEN];
 3095 
 3096             memset((void *)inotify_buf, 0x0, EVENT_BUF_LEN);
 3097             i = read(inotify_fd, inotify_buf, EVENT_BUF_LEN);
 3098             if (i > 0) {
 3099                 for (char *ptr = inotify_buf;
 3100                      ptr + ((struct inotify_event *)ptr)->len < inotify_buf + i;
 3101                      ptr += sizeof(struct inotify_event) + event->len) {
 3102                     event = (struct inotify_event *)ptr;
 3103                     DPRINTF_D(event->wd);
 3104                     DPRINTF_D(event->mask);
 3105                     if (!event->wd)
 3106                         break;
 3107 
 3108                     if (event->mask & INOTIFY_MASK) {
 3109                         c = handle_event();
 3110                         break;
 3111                     }
 3112                 }
 3113                 DPRINTF_S("inotify read done");
 3114             }
 3115         }
 3116 #elif defined(BSD_KQUEUE)
 3117         if (!cfg.blkorder && event_fd >= 0 && (idle & 1)) {
 3118             struct kevent event_data[NUM_EVENT_SLOTS];
 3119 
 3120             memset((void *)event_data, 0x0, sizeof(struct kevent) * NUM_EVENT_SLOTS);
 3121             if (kevent(kq, events_to_monitor, NUM_EVENT_SLOTS,
 3122                    event_data, NUM_EVENT_FDS, &gtimeout) > 0)
 3123                 c = handle_event();
 3124         }
 3125 #elif defined(HAIKU_NM)
 3126         if (!cfg.blkorder && haiku_nm_active && (idle & 1) && haiku_is_update_needed(haiku_hnd))
 3127             c = handle_event();
 3128 #endif
 3129     } else
 3130         idle = 0;
 3131 
 3132     for (i = 0; i < (int)ELEMENTS(bindings); ++i)
 3133         if (c == bindings[i].sym)
 3134             return bindings[i].act;
 3135 
 3136     return 0;
 3137 }
 3138 
 3139 static int getorderstr(char *sort)
 3140 {
 3141     int i = 0;
 3142 
 3143     if (cfg.showhidden)
 3144         sort[i++] = 'H';
 3145 
 3146     if (cfg.timeorder)
 3147         sort[i++] = (cfg.timetype == T_MOD) ? 'M' : ((cfg.timetype == T_ACCESS) ? 'A' : 'C');
 3148     else if (cfg.sizeorder)
 3149         sort[i++] = 'S';
 3150     else if (cfg.extnorder)
 3151         sort[i++] = 'E';
 3152 
 3153     if (entrycmpfn == &reventrycmp)
 3154         sort[i++] = 'R';
 3155 
 3156     if (namecmpfn == &xstrverscasecmp)
 3157         sort[i++] = 'V';
 3158 
 3159     if (i)
 3160         sort[i] = ' ';
 3161 
 3162     return i;
 3163 }
 3164 
 3165 static void showfilterinfo(void)
 3166 {
 3167     int i = 0;
 3168     char info[REGEX_MAX] = "\0\0\0\0\0";
 3169 
 3170     i = getorderstr(info);
 3171 
 3172     snprintf(info + i, REGEX_MAX - i - 1, "  %s [/], %s [:]",
 3173          (cfg.regex ? "reg" : "str"),
 3174          ((fnstrstr == &strcasestr) ? "ic" : "noic"));
 3175 
 3176     if (cfg.fileinfo && ndents && get_output("file", "-b", pdents[cur].name, -1, FALSE, FALSE))
 3177         mvaddstr(xlines - 2, 2, g_buf);
 3178 
 3179     mvaddstr(xlines - 2, xcols - xstrlen(info), info);
 3180 }
 3181 
 3182 static void showfilter(char *str)
 3183 {
 3184     attron(COLOR_PAIR(cfg.curctx + 1));
 3185     showfilterinfo();
 3186     printmsg(str);
 3187     // printmsg calls attroff()
 3188 }
 3189 
 3190 static inline void swap_ent(int id1, int id2)
 3191 {
 3192     struct entry _dent, *pdent1 = &pdents[id1], *pdent2 =  &pdents[id2];
 3193 
 3194     *(&_dent) = *pdent1;
 3195     *pdent1 = *pdent2;
 3196     *pdent2 = *(&_dent);
 3197 }
 3198 
 3199 #ifdef PCRE
 3200 static int fill(const char *fltr, pcre *pcrex)
 3201 #else
 3202 static int fill(const char *fltr, regex_t *re)
 3203 #endif
 3204 {
 3205 #ifdef PCRE
 3206     fltrexp_t fltrexp = { .pcrex = pcrex, .str = fltr };
 3207 #else
 3208     fltrexp_t fltrexp = { .regex = re, .str = fltr };
 3209 #endif
 3210 
 3211     for (int count = 0; count < ndents; ++count) {
 3212         if (filterfn(&fltrexp, pdents[count].name) == 0) {
 3213             if (count != --ndents) {
 3214                 swap_ent(count, ndents);
 3215                 --count;
 3216             }
 3217 
 3218             continue;
 3219         }
 3220     }
 3221 
 3222     return ndents;
 3223 }
 3224 
 3225 static int matches(const char *fltr)
 3226 {
 3227 #ifdef PCRE
 3228     pcre *pcrex = NULL;
 3229 
 3230     /* Search filter */
 3231     if (cfg.regex && setfilter(&pcrex, fltr))
 3232         return -1;
 3233 
 3234     ndents = fill(fltr, pcrex);
 3235 
 3236     if (cfg.regex)
 3237         pcre_free(pcrex);
 3238 #else
 3239     regex_t re;
 3240 
 3241     /* Search filter */
 3242     if (cfg.regex && setfilter(&re, fltr))
 3243         return -1;
 3244 
 3245     ndents = fill(fltr, &re);
 3246 
 3247     if (cfg.regex)
 3248         regfree(&re);
 3249 #endif
 3250 
 3251     ENTSORT(pdents, ndents, entrycmpfn);
 3252 
 3253     return ndents;
 3254 }
 3255 
 3256 /*
 3257  * Return the position of the matching entry or 0 otherwise
 3258  * Note there's no NULL check for fname
 3259  */
 3260 static int dentfind(const char *fname, int n)
 3261 {
 3262     for (int i = 0; i < n; ++i)
 3263         if (xstrcmp(fname, pdents[i].name) == 0)
 3264             return i;
 3265 
 3266     return 0;
 3267 }
 3268 
 3269 static int filterentries(char *path, char *lastname)
 3270 {
 3271     wchar_t *wln = (wchar_t *)alloca(sizeof(wchar_t) * REGEX_MAX);
 3272     char *ln = g_ctx[cfg.curctx].c_fltr;
 3273     wint_t ch[2] = {0};
 3274     int r, total = ndents, len;
 3275     char *pln = g_ctx[cfg.curctx].c_fltr + 1;
 3276 
 3277     DPRINTF_S(__func__);
 3278 
 3279     if (ndents && (ln[0] == FILTER || ln[0] == RFILTER) && *pln) {
 3280         if (matches(pln) != -1) {
 3281             move_cursor(dentfind(lastname, ndents), 0);
 3282             redraw(path);
 3283         }
 3284 
 3285         if (!cfg.filtermode) {
 3286             statusbar(path);
 3287             return 0;
 3288         }
 3289 
 3290         len = mbstowcs(wln, ln, REGEX_MAX);
 3291     } else {
 3292         ln[0] = wln[0] = cfg.regex ? RFILTER : FILTER;
 3293         ln[1] = wln[1] = '\0';
 3294         len = 1;
 3295     }
 3296 
 3297     cleartimeout();
 3298     curs_set(TRUE);
 3299     showfilter(ln);
 3300 
 3301     while ((r = get_wch(ch)) != ERR) {
 3302         //DPRINTF_D(*ch);
 3303         //DPRINTF_S(keyname(*ch));
 3304 
 3305         switch (*ch) {
 3306 #ifdef KEY_RESIZE
 3307         case 0: // fallthrough
 3308         case KEY_RESIZE:
 3309             clearoldprompt();
 3310             redraw(path);
 3311             showfilter(ln);
 3312             continue;
 3313 #endif
 3314         case KEY_DC: // fallthrough
 3315         case KEY_BACKSPACE: // fallthrough
 3316         case '\b': // fallthrough
 3317         case DEL: /* handle DEL */
 3318             if (len != 1) {
 3319                 wln[--len] = '\0';
 3320                 wcstombs(ln, wln, REGEX_MAX);
 3321                 ndents = total;
 3322             } else {
 3323                 *ch = FILTER;
 3324                 goto end;
 3325             }
 3326             // fallthrough
 3327         case CONTROL('L'):
 3328             if (*ch == CONTROL('L')) {
 3329                 if (wln[1]) {
 3330                     ln[REGEX_MAX - 1] = ln[1];
 3331                     ln[1] = wln[1] = '\0';
 3332                     len = 1;
 3333                     ndents = total;
 3334                 } else if (ln[REGEX_MAX - 1]) { /* Show the previous filter */
 3335                     ln[1] = ln[REGEX_MAX - 1];
 3336                     ln[REGEX_MAX - 1] = '\0';
 3337                     len = mbstowcs(wln, ln, REGEX_MAX);
 3338                 } else
 3339                     goto end;
 3340             }
 3341 
 3342             /* Go to the top, we don't know if the hovered file will match the filter */
 3343             cur = 0;
 3344 
 3345             if (matches(pln) != -1)
 3346                 redraw(path);
 3347 
 3348             showfilter(ln);
 3349             continue;
 3350 #ifndef NOMOUSE
 3351         case KEY_MOUSE:
 3352             goto end;
 3353 #endif
 3354         case ESC:
 3355             if (handle_alt_key(ch) != ERR) {
 3356                 if (*ch == ESC) /* Handle Alt+Esc */
 3357                     *ch = 'q'; /* Quit context */
 3358                 else {
 3359                     unget_wch(*ch);
 3360                     *ch = ';'; /* Run plugin */
 3361                 }
 3362             }
 3363             goto end;
 3364         }
 3365 
 3366         if (r != OK) /* Handle Fn keys in main loop */
 3367             break;
 3368 
 3369         /* Handle all control chars in main loop */
 3370         if (*ch < ASCII_MAX && keyname(*ch)[0] == '^' && *ch != '^')
 3371             goto end;
 3372 
 3373         if (len == 1) {
 3374             if (*ch == '?') /* Help and config key, '?' is an invalid regex */
 3375                 goto end;
 3376 
 3377             if (cfg.filtermode) {
 3378                 switch (*ch) {
 3379                 case '\'': // fallthrough /* Go to first non-dir file */
 3380                 case '+': // fallthrough /* Toggle auto-advance */
 3381                 case ',': // fallthrough /* Mark CWD */
 3382                 case '-': // fallthrough /* Visit last visited dir */
 3383                 case '.': // fallthrough /* Show hidden files */
 3384                 case ';': // fallthrough /* Run plugin key */
 3385                 case '=': // fallthrough /* Launch app */
 3386                 case '>': // fallthrough /* Export file list */
 3387                 case '@': // fallthrough /* Visit start dir */
 3388                 case ']': // fallthorugh /* Prompt key */
 3389                 case '`': // fallthrough /* Visit / */
 3390                 case '~': /* Go HOME */
 3391                     goto end;
 3392                 }
 3393             }
 3394 
 3395             /* Toggle case-sensitivity */
 3396             if (*ch == CASE) {
 3397                 fnstrstr = (fnstrstr == &strcasestr) ? &strstr : &strcasestr;
 3398 #ifdef PCRE
 3399                 pcreflags ^= PCRE_CASELESS;
 3400 #else
 3401                 regflags ^= REG_ICASE;
 3402 #endif
 3403                 showfilter(ln);
 3404                 continue;
 3405             }
 3406 
 3407             /* Toggle string or regex filter */
 3408             if (*ch == FILTER) {
 3409                 ln[0] = (ln[0] == FILTER) ? RFILTER : FILTER;
 3410                 wln[0] = (uchar_t)ln[0];
 3411                 cfg.regex ^= 1;
 3412                 filterfn = cfg.regex ? &visible_re : &visible_str;
 3413                 showfilter(ln);
 3414                 continue;
 3415             }
 3416 
 3417             /* Reset cur in case it's a repeat search */
 3418             cur = 0;
 3419         } else if (len == REGEX_MAX - 1)
 3420             continue;
 3421 
 3422         wln[len] = (wchar_t)*ch;
 3423         wln[++len] = '\0';
 3424         wcstombs(ln, wln, REGEX_MAX);
 3425 
 3426         /* Forward-filtering optimization:
 3427          * - new matches can only be a subset of current matches.
 3428          */
 3429         /* ndents = total; */
 3430 #ifdef MATCHFLTR
 3431         r = matches(pln);
 3432         if (r <= 0) {
 3433             !r ? unget_wch(KEY_BACKSPACE) : showfilter(ln);
 3434 #else
 3435         if (matches(pln) == -1) {
 3436             showfilter(ln);
 3437 #endif
 3438             continue;
 3439         }
 3440 
 3441         /* If the only match is a dir, auto-select and cd into it */
 3442         if (ndents == 1 && cfg.filtermode
 3443             && cfg.autoselect && (pdents[0].flags & DIR_OR_DIRLNK)) {
 3444             *ch = KEY_ENTER;
 3445             cur = 0;
 3446             goto end;
 3447         }
 3448 
 3449         /*
 3450          * redraw() should be above the auto-select optimization, for
 3451          * the case where there's an issue with dir auto-select, say,
 3452          * due to a permission problem. The transition is _jumpy_ in
 3453          * case of such an error. However, we optimize for successful
 3454          * cases where the dir has permissions. This skips a redraw().
 3455          */
 3456         redraw(path);
 3457         showfilter(ln);
 3458     }
 3459 end:
 3460 
 3461     /* Save last working filter in-filter */
 3462     if (ln[1])
 3463         ln[REGEX_MAX - 1] = ln[1];
 3464 
 3465     /* Save current */
 3466     copycurname();
 3467 
 3468     curs_set(FALSE);
 3469     settimeout();
 3470 
 3471     /* Return keys for navigation etc. */
 3472     return *ch;
 3473 }
 3474 
 3475 /* Show a prompt with input string and return the changes */
 3476 static char *xreadline(const char *prefill, const char *prompt)
 3477 {
 3478     size_t len, pos;
 3479     int x, r;
 3480     const int WCHAR_T_WIDTH = sizeof(wchar_t);
 3481     wint_t ch[2] = {0};
 3482     wchar_t * const buf = malloc(sizeof(wchar_t) * READLINE_MAX);
 3483 
 3484     if (!buf)
 3485         errexit();
 3486 
 3487     cleartimeout();
 3488     printmsg(prompt);
 3489 
 3490     if (prefill) {
 3491         DPRINTF_S(prefill);
 3492         len = pos = mbstowcs(buf, prefill, READLINE_MAX);
 3493     } else
 3494         len = (size_t)-1;
 3495 
 3496     if (len == (size_t)-1) {
 3497         buf[0] = '\0';
 3498         len = pos = 0;
 3499     }
 3500 
 3501     x = getcurx(stdscr);
 3502     curs_set(TRUE);
 3503 
 3504     while (1) {
 3505         buf[len] = ' ';
 3506         attron(COLOR_PAIR(cfg.curctx + 1));
 3507         if (pos > (size_t)(xcols - x)) {
 3508             mvaddnwstr(xlines - 1, x, buf + (pos - (xcols - x) + 1), xcols - x);
 3509             move(xlines - 1, xcols - 1);
 3510         } else {
 3511             mvaddnwstr(xlines - 1, x, buf, len + 1);
 3512             move(xlines - 1, x + wcswidth(buf, pos));
 3513         }
 3514         attroff(COLOR_PAIR(cfg.curctx + 1));
 3515 
 3516         r = get_wch(ch);
 3517         if (r == ERR)
 3518             continue;
 3519 
 3520         if (r == OK) {
 3521             switch (*ch) {
 3522             case KEY_ENTER: // fallthrough
 3523             case '\n': // fallthrough
 3524             case '\r':
 3525                 goto END;
 3526             case CONTROL('D'):
 3527                 if (pos < len)
 3528                     ++pos;
 3529                 else if (!(pos || len)) { /* Exit on ^D at empty prompt */
 3530                     len = 0;
 3531                     goto END;
 3532                 } else
 3533                     continue;
 3534                 // fallthrough
 3535             case DEL: // fallthrough
 3536             case '\b': /* rhel25 sends '\b' for backspace */
 3537                 if (pos > 0) {
 3538                     memmove(buf + pos - 1, buf + pos,
 3539                         (len - pos) * WCHAR_T_WIDTH);
 3540                     --len, --pos;
 3541                 }
 3542                 continue;
 3543             case '\t':
 3544                 if (!(len || pos) && ndents)
 3545                     len = pos = mbstowcs(buf, pdents[cur].name, READLINE_MAX);
 3546                 continue;
 3547             case CONTROL('F'):
 3548                 if (pos < len)
 3549                     ++pos;
 3550                 continue;
 3551             case CONTROL('B'):
 3552                 if (pos > 0)
 3553                     --pos;
 3554                 continue;
 3555             case CONTROL('W'):
 3556                 printmsg(prompt);
 3557                 do {
 3558                     if (pos == 0)
 3559                         break;
 3560                     memmove(buf + pos - 1, buf + pos,
 3561                         (len - pos) * WCHAR_T_WIDTH);
 3562                     --pos, --len;
 3563                 } while (buf[pos - 1] != ' ' && buf[pos - 1] != '/'); // NOLINT
 3564                 continue;
 3565             case CONTROL('K'):
 3566                 printmsg(prompt);
 3567                 len = pos;
 3568                 continue;
 3569             case CONTROL('L'):
 3570                 printmsg(prompt);
 3571                 len = pos = 0;
 3572                 continue;
 3573             case CONTROL('A'):
 3574                 pos = 0;
 3575                 continue;
 3576             case CONTROL('E'):
 3577                 pos = len;
 3578                 continue;
 3579             case CONTROL('U'):
 3580                 printmsg(prompt);
 3581                 memmove(buf, buf + pos, (len - pos) * WCHAR_T_WIDTH);
 3582                 len -= pos;
 3583                 pos = 0;
 3584                 continue;
 3585             case ESC: /* Exit prompt on Esc, but just filter out Alt+key */
 3586                 if (handle_alt_key(ch) != ERR)
 3587                     continue;
 3588 
 3589                 len = 0;
 3590                 goto END;
 3591             }
 3592 
 3593             /* Filter out all other control chars */
 3594             if (*ch < ASCII_MAX && keyname(*ch)[0] == '^')
 3595                 continue;
 3596 
 3597             if (pos < READLINE_MAX - 1) {
 3598                 memmove(buf + pos + 1, buf + pos,
 3599                     (len - pos) * WCHAR_T_WIDTH);
 3600                 buf[pos] = *ch;
 3601                 ++len, ++pos;
 3602                 continue;
 3603             }
 3604         } else {
 3605             switch (*ch) {
 3606 #ifdef KEY_RESIZE
 3607             case KEY_RESIZE:
 3608                 clearoldprompt();
 3609                 xlines = LINES;
 3610                 printmsg(prompt);
 3611                 break;
 3612 #endif
 3613             case KEY_LEFT:
 3614                 if (pos > 0)
 3615                     --pos;
 3616                 break;
 3617             case KEY_RIGHT:
 3618                 if (pos < len)
 3619                     ++pos;
 3620                 break;
 3621             case KEY_BACKSPACE:
 3622                 if (pos > 0) {
 3623                     memmove(buf + pos - 1, buf + pos,
 3624                         (len - pos) * WCHAR_T_WIDTH);
 3625                     --len, --pos;
 3626                 }
 3627                 break;
 3628             case KEY_DC:
 3629                 if (pos < len) {
 3630                     memmove(buf + pos, buf + pos + 1,
 3631                         (len - pos - 1) * WCHAR_T_WIDTH);
 3632                     --len;
 3633                 }
 3634                 break;
 3635             case KEY_END:
 3636                 pos = len;
 3637                 break;
 3638             case KEY_HOME:
 3639                 pos = 0;
 3640                 break;
 3641             case KEY_UP: // fallthrough
 3642             case KEY_DOWN:
 3643                 if (prompt && lastcmd && (xstrcmp(prompt, PROMPT) == 0)) {
 3644                     printmsg(prompt);
 3645                     len = pos = mbstowcs(buf, lastcmd, READLINE_MAX); // fallthrough
 3646                 }
 3647             default:
 3648                 break;
 3649             }
 3650         }
 3651     }
 3652 
 3653 END:
 3654     curs_set(FALSE);
 3655     settimeout();
 3656     printmsg("");
 3657 
 3658     buf[len] = '\0';
 3659 
 3660     pos = wcstombs(g_buf, buf, READLINE_MAX - 1);
 3661     if (pos >= READLINE_MAX - 1)
 3662         g_buf[READLINE_MAX - 1] = '\0';
 3663 
 3664     free(buf);
 3665     return g_buf;
 3666 }
 3667 
 3668 #ifndef NORL
 3669 /*
 3670  * Caller should check the value of presel to confirm if it needs to wait to show warning
 3671  */
 3672 static char *getreadline(const char *prompt)
 3673 {
 3674     exitcurses();
 3675 
 3676     char *input = readline(prompt);
 3677 
 3678     refresh();
 3679 
 3680     if (input && input[0]) {
 3681         add_history(input);
 3682         xstrsncpy(g_buf, input, CMD_LEN_MAX);
 3683         free(input);
 3684         return g_buf;
 3685     }
 3686 
 3687     free(input);
 3688     return NULL;
 3689 }
 3690 #endif
 3691 
 3692 /*
 3693  * Create symbolic/hard link(s) to file(s) in selection list
 3694  * Returns the number of links created, -1 on error
 3695  */
 3696 static int xlink(char *prefix, char *path, char *curfname, char *buf, int *presel, int type)
 3697 {
 3698     int count = 0, choice;
 3699     char *psel = pselbuf, *fname;
 3700     size_t pos = 0, len, r;
 3701     int (*link_fn)(const char *, const char *) = NULL;
 3702     char lnpath[PATH_MAX];
 3703 
 3704     choice = get_cur_or_sel();
 3705     if (!choice)
 3706         return -1;
 3707 
 3708     if (type == 's') /* symbolic link */
 3709         link_fn = &symlink;
 3710     else /* hard link */
 3711         link_fn = &link;
 3712 
 3713     if (choice == 'c' || (nselected == 1)) {
 3714         mkpath(path, prefix, lnpath); /* Generate link path */
 3715         mkpath(path, (choice == 'c') ? curfname : pselbuf, buf); /* Generate target file path */
 3716 
 3717         if (!link_fn(buf, lnpath)) {
 3718             if (choice == 's')
 3719                 clearselection();
 3720             return 1; /* One link created */
 3721         }
 3722 
 3723         printwarn(presel);
 3724         return -1;
 3725     }
 3726 
 3727     while (pos < selbufpos) {
 3728         len = xstrlen(psel);
 3729         fname = xbasename(psel);
 3730 
 3731         r = xstrsncpy(buf, prefix, NAME_MAX + 1); /* Copy prefix */
 3732         xstrsncpy(buf + r - 1, fname, NAME_MAX - r); /* Suffix target file name */
 3733         mkpath(path, buf, lnpath); /* Generate link path */
 3734 
 3735         if (!link_fn(psel, lnpath))
 3736             ++count;
 3737 
 3738         pos += len + 1;
 3739         psel += len + 1;
 3740     }
 3741 
 3742     clearselection();
 3743     return count;
 3744 }
 3745 
 3746 static bool parsekvpair(kv **arr, char **envcpy, const uchar_t id, uchar_t *items)
 3747 {
 3748     bool new = TRUE;
 3749     const uchar_t INCR = 8;
 3750     uint_t i = 0;
 3751     kv *kvarr = NULL;
 3752     char *ptr = getenv(env_cfg[id]);
 3753 
 3754     if (!ptr || !*ptr)
 3755         return TRUE;
 3756 
 3757     *envcpy = xstrdup(ptr);
 3758     if (!*envcpy) {
 3759         xerror();
 3760         return FALSE;
 3761     }
 3762 
 3763     ptr = *envcpy;
 3764 
 3765     while (*ptr && i < 100) {
 3766         if (new) {
 3767             if (!(i & (INCR - 1))) {
 3768                 kvarr = xrealloc(kvarr, sizeof(kv) * (i + INCR));
 3769                 *arr = kvarr;
 3770                 if (!kvarr) {
 3771                     xerror();
 3772                     return FALSE;
 3773                 }
 3774                 memset(kvarr + i, 0, sizeof(kv) * INCR);
 3775             }
 3776             kvarr[i].key = (uchar_t)*ptr;
 3777             if (*++ptr != ':' || *++ptr == '\0' || *ptr == ';')
 3778                 return FALSE;
 3779             kvarr[i].off = ptr - *envcpy;
 3780             ++i;
 3781 
 3782             new = FALSE;
 3783         }
 3784 
 3785         if (*ptr == ';') {
 3786             *ptr = '\0';
 3787             new = TRUE;
 3788         }
 3789 
 3790         ++ptr;
 3791     }
 3792 
 3793     *items = i;
 3794     return (i != 0);
 3795 }
 3796 
 3797 /*
 3798  * Get the value corresponding to a key
 3799  *
 3800  * NULL is returned in case of no match, path resolution failure etc.
 3801  * buf would be modified, so check return value before access
 3802  */
 3803 static char *get_kv_val(kv *kvarr, char *buf, int key, uchar_t max, uchar_t id)
 3804 {
 3805     char *val;
 3806 
 3807     if (!kvarr)
 3808         return NULL;
 3809 
 3810     for (int r = 0; kvarr[r].key && r < max; ++r) {
 3811         if (kvarr[r].key == key) {
 3812             /* Do not allocate new memory for plugin */
 3813             if (id == NNN_PLUG)
 3814                 return pluginstr + kvarr[r].off;
 3815 
 3816             val = bmstr + kvarr[r].off;
 3817             convert_tilde(val, g_buf);
 3818             return abspath(((val[0] == '~') ? g_buf : val), NULL, buf);
 3819         }
 3820     }
 3821 
 3822     DPRINTF_S("Invalid key");
 3823     return NULL;
 3824 }
 3825 
 3826 static int get_kv_key(kv *kvarr, char *val, uchar_t max, uchar_t id)
 3827 {
 3828     if (!kvarr)
 3829         return -1;
 3830 
 3831     if (id != NNN_ORDER) /* For now this function supports only order string */
 3832         return -1;
 3833 
 3834     for (int r = 0; kvarr[r].key && r < max; ++r) {
 3835         if (xstrcmp((orderstr + kvarr[r].off), val) == 0)
 3836             return kvarr[r].key;
 3837     }
 3838 
 3839     return -1;
 3840 }
 3841 
 3842 static void resetdircolor(int flags)
 3843 {
 3844     /* Directories are always shown on top, clear the color when moving to first file */
 3845     if (g_state.dircolor && !(flags & DIR_OR_DIRLNK)) {
 3846         attroff(COLOR_PAIR(cfg.curctx + 1) | A_BOLD);
 3847         g_state.dircolor = 0;
 3848     }
 3849 }
 3850 
 3851 /*
 3852  * Replace escape characters in a string with '?'
 3853  * Adjust string length to maxcols if > 0;
 3854  * Max supported str length: NAME_MAX;
 3855  */
 3856 #ifdef NOLC
 3857 static char *unescape(const char *str, uint_t maxcols)
 3858 {
 3859     char * const wbuf = g_buf;
 3860     char *buf = wbuf;
 3861 
 3862     xstrsncpy(wbuf, str, maxcols);
 3863 #else
 3864 static wchar_t *unescape(const char *str, uint_t maxcols)
 3865 {
 3866     wchar_t * const wbuf = (wchar_t *)g_buf;
 3867     wchar_t *buf = wbuf;
 3868     size_t len = mbstowcs(wbuf, str, maxcols); /* Convert multi-byte to wide char */
 3869 
 3870     len = wcswidth(wbuf, len);
 3871 
 3872     if (len >= maxcols) {
 3873         size_t lencount = maxcols;
 3874 
 3875         while (len > maxcols) /* Reduce wide chars one by one till it fits */
 3876             len = wcswidth(wbuf, --lencount);
 3877 
 3878         wbuf[lencount] = L'\0';
 3879     }
 3880 #endif
 3881 
 3882     while (*buf) {
 3883         if (*buf <= '\x1f' || *buf == '\x7f')
 3884             *buf = '\?';
 3885 
 3886         ++buf;
 3887     }
 3888 
 3889     return wbuf;
 3890 }
 3891 
 3892 static off_t get_size(off_t size, off_t *pval, int comp)
 3893 {
 3894     off_t rem = *pval;
 3895     off_t quo = rem / 10;
 3896 
 3897     if ((rem - (quo * 10)) >= 5) {
 3898         rem = quo + 1;
 3899         if (rem == comp) {
 3900             ++size;
 3901             rem = 0;
 3902         }
 3903     } else
 3904         rem = quo;
 3905 
 3906     *pval = rem;
 3907     return size;
 3908 }
 3909 
 3910 static char *coolsize(off_t size)
 3911 {
 3912     const char * const U = "BKMGTPEZY";
 3913     static char size_buf[12]; /* Buffer to hold human readable size */
 3914     off_t rem = 0;
 3915     size_t ret;
 3916     int i = 0;
 3917 
 3918     while (size >= 1024) {
 3919         rem = size & (0x3FF); /* 1024 - 1 = 0x3FF */
 3920         size >>= 10;
 3921         ++i;
 3922     }
 3923 
 3924     if (i == 1) {
 3925         rem = (rem * 1000) >> 10;
 3926         rem /= 10;
 3927         size = get_size(size, &rem, 10);
 3928     } else if (i == 2) {
 3929         rem = (rem * 1000) >> 10;
 3930         size = get_size(size, &rem, 100);
 3931     } else if (i > 2) {
 3932         rem = (rem * 10000) >> 10;
 3933         size = get_size(size, &rem, 1000);
 3934     }
 3935 
 3936     if (i > 0 && i < 6 && rem) {
 3937         ret = xstrsncpy(size_buf, xitoa(size), 12);
 3938         size_buf[ret - 1] = '.';
 3939 
 3940         char *frac = xitoa(rem);
 3941         size_t toprint = i > 3 ? 3 : i;
 3942         size_t len = xstrlen(frac);
 3943 
 3944         if (len < toprint) {
 3945             size_buf[ret] = size_buf[ret + 1] = size_buf[ret + 2] = '0';
 3946             xstrsncpy(size_buf + ret + (toprint - len), frac, len + 1);
 3947         } else
 3948             xstrsncpy(size_buf + ret, frac, toprint + 1);
 3949 
 3950         ret += toprint;
 3951     } else {
 3952         ret = xstrsncpy(size_buf, size ? xitoa(size) : "0", 12);
 3953         --ret;
 3954     }
 3955 
 3956     size_buf[ret] = U[i];
 3957     size_buf[ret + 1] = '\0';
 3958 
 3959     return size_buf;
 3960 }
 3961 
 3962 /* Convert a mode field into "ls -l" type perms field. */
 3963 static char *get_lsperms(mode_t mode)
 3964 {
 3965     static const char * const rwx[] = {"---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx"};
 3966     static char bits[11] = {'\0'};
 3967 
 3968     switch (mode & S_IFMT) {
 3969     case S_IFREG:
 3970         bits[0] = '-';
 3971         break;
 3972     case S_IFDIR:
 3973         bits[0] = 'd';
 3974         break;
 3975     case S_IFLNK:
 3976         bits[0] = 'l';
 3977         break;
 3978     case S_IFSOCK:
 3979         bits[0] = 's';
 3980         break;
 3981     case S_IFIFO:
 3982         bits[0] = 'p';
 3983         break;
 3984     case S_IFBLK:
 3985         bits[0] = 'b';
 3986         break;
 3987     case S_IFCHR:
 3988         bits[0] = 'c';
 3989         break;
 3990     default:
 3991         bits[0] = '?';
 3992         break;
 3993     }
 3994 
 3995     xstrsncpy(&bits[1], rwx[(mode >> 6) & 7], 4);
 3996     xstrsncpy(&bits[4], rwx[(mode >> 3) & 7], 4);
 3997     xstrsncpy(&bits[7], rwx[(mode & 7)], 4);
 3998 
 3999     if (mode & S_ISUID)
 4000         bits[3] = (mode & 0100) ? 's' : 'S';  /* user executable */
 4001     if (mode & S_ISGID)
 4002         bits[6] = (mode & 0010) ? 's' : 'l';  /* group executable */
 4003     if (mode & S_ISVTX)
 4004         bits[9] = (mode & 0001) ? 't' : 'T';  /* others executable */
 4005 
 4006     return bits;
 4007 }
 4008 
 4009 #ifdef ICONS_ENABLED
 4010 static const struct icon_pair *get_icon(const struct entry *ent)
 4011 {
 4012     ushort_t i = 0;
 4013 
 4014     for (; i < sizeof(icons_name)/sizeof(struct icon_pair); ++i)
 4015         if (strcasecmp(ent->name, icons_name[i].match) == 0)
 4016             return &icons_name[i];
 4017 
 4018     if (ent->flags & DIR_OR_DIRLNK)
 4019         return &dir_icon;
 4020 
 4021     char *tmp = xextension(ent->name, ent->nlen);
 4022 
 4023     if (!tmp) {
 4024         if (ent->mode & 0100)
 4025             return &exec_icon;
 4026 
 4027         return &file_icon;
 4028     }
 4029 
 4030     /* Skip the . */
 4031     ++tmp;
 4032 
 4033     if (*tmp >= '0' && *tmp <= '9')
 4034         i = *tmp - '0'; /* NUMBER 0-9 */
 4035     else if (TOUPPER(*tmp) >= 'A' && TOUPPER(*tmp) <= 'Z')
 4036         i = TOUPPER(*tmp) - 'A' + 10; /* LETTER A-Z */
 4037     else
 4038         i = 36; /* OTHER */
 4039 
 4040     for (ushort_t j = icon_positions[i]; j < sizeof(icons_ext)/sizeof(struct icon_pair) &&
 4041             icons_ext[j].match[0] == icons_ext[icon_positions[i]].match[0]; ++j)
 4042         if (strcasecmp(tmp, icons_ext[j].match) == 0)
 4043             return &icons_ext[j];
 4044 
 4045     /* If there's no match and the file is executable, icon that */
 4046     if (ent->mode & 0100)
 4047         return &exec_icon;
 4048 
 4049     return &file_icon;
 4050 }
 4051 
 4052 static void print_icon(const struct entry *ent, const int attrs)
 4053 {
 4054     const struct icon_pair *picon = get_icon(ent);
 4055 
 4056     addstr(ICON_PADDING_LEFT);
 4057     if (picon->color)
 4058         attron(COLOR_PAIR(C_UND + 1 + picon->color));
 4059     else if (attrs)
 4060         attron(attrs);
 4061     addstr(picon->icon);
 4062     if (picon->color)
 4063         attroff(COLOR_PAIR(C_UND + 1 + picon->color));
 4064     else if (attrs)
 4065         attroff(attrs);
 4066     addstr(ICON_PADDING_RIGHT);
 4067 }
 4068 #endif
 4069 
 4070 static void print_time(const time_t *timep)
 4071 {
 4072     struct tm t;
 4073 
 4074     localtime_r(timep, &t);
 4075     printw("%s-%02d-%02d %02d:%02d",
 4076         xitoa(t.tm_year + 1900), t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min);
 4077 }
 4078 
 4079 static char get_detail_ind(const mode_t mode)
 4080 {
 4081     switch (mode & S_IFMT) {
 4082     case S_IFDIR:  // fallthrough
 4083     case S_IFREG:  return ' ';
 4084     case S_IFLNK:  return '@';
 4085     case S_IFSOCK: return '=';
 4086     case S_IFIFO:  return '|';
 4087     case S_IFBLK:  return 'b';
 4088     case S_IFCHR:  return 'c';
 4089     }
 4090     return '?';
 4091 }
 4092 
 4093 /* Note: attribute and indicator values must be initialized to 0 */
 4094 static uchar_t get_color_pair_name_ind(const struct entry *ent, char *pind, int *pattr)
 4095 {
 4096     switch (ent->mode & S_IFMT) {
 4097     case S_IFREG:
 4098         if (!ent->size) {
 4099             if (ent->mode & 0100)
 4100                 *pind = '*';
 4101             return C_UND;
 4102         }
 4103         if (ent->flags & HARD_LINK) {
 4104             if (ent->mode & 0100)
 4105                 *pind = '*';
 4106             return C_HRD;
 4107         }
 4108         if (ent->mode & 0100) {
 4109             *pind = '*';
 4110             return C_EXE;
 4111         }
 4112         return C_FIL;
 4113     case S_IFDIR:
 4114         *pind = '/';
 4115         if (g_state.oldcolor)
 4116             return C_DIR;
 4117         *pattr |= A_BOLD;
 4118         return g_state.dirctx ? cfg.curctx + 1 : C_DIR;
 4119     case S_IFLNK:
 4120         if (ent->flags & DIR_OR_DIRLNK) {
 4121             *pind = '/';
 4122             *pattr |= g_state.oldcolor ? A_DIM : A_BOLD;
 4123         } else {
 4124             *pind = '@';
 4125             if (g_state.oldcolor)
 4126                 *pattr |= A_DIM;
 4127         }
 4128         if (!g_state.oldcolor || cfg.showdetail)
 4129             return (ent->flags & SYM_ORPHAN) ? C_ORP : C_LNK;
 4130         return 0;
 4131     case S_IFSOCK:
 4132         *pind = '=';
 4133         return C_SOC;
 4134     case S_IFIFO:
 4135         *pind = '|';
 4136         return C_PIP;
 4137     case S_IFBLK:
 4138         return C_BLK;
 4139     case S_IFCHR:
 4140         return C_CHR;
 4141     }
 4142 
 4143     *pind = '?';
 4144     return C_UND;
 4145 }
 4146 
 4147 static void printent(const struct entry *ent, uint_t namecols, bool sel)
 4148 {
 4149     char ind = '\0';
 4150     int attrs;
 4151 
 4152     if (cfg.showdetail) {
 4153         int type = ent->mode & S_IFMT;
 4154         char perms[6] = {' ', ' ', (char)('0' + ((ent->mode >> 6) & 7)),
 4155                 (char)('0' + ((ent->mode >> 3) & 7)),
 4156                 (char)('0' + (ent->mode & 7)), '\0'};
 4157 
 4158         addch(' ');
 4159         attrs = g_state.oldcolor ? (resetdircolor(ent->flags), A_DIM)
 4160                      : (fcolors[C_MIS] ? COLOR_PAIR(C_MIS) : 0);
 4161         if (attrs)
 4162             attron(attrs);
 4163 
 4164         /* Print details */
 4165         print_time(&ent->sec);
 4166 
 4167         printw("%s%9s ", perms, (type == S_IFREG || type == S_IFDIR)
 4168             ? coolsize(cfg.blkorder ? (blkcnt_t)ent->blocks << blk_shift : ent->size)
 4169             : (type = (uchar_t)get_detail_ind(ent->mode), (char *)&type));
 4170 
 4171         if (attrs)
 4172             attroff(attrs);
 4173     }
 4174 
 4175     attrs = 0;
 4176 
 4177     uchar_t color_pair = get_color_pair_name_ind(ent, &ind, &attrs);
 4178 
 4179     addch((ent->flags & FILE_SELECTED) ? '+' | A_REVERSE | A_BOLD : ' ');
 4180 
 4181     if (g_state.oldcolor)
 4182         resetdircolor(ent->flags);
 4183     else {
 4184         if (ent->flags & FILE_MISSING)
 4185             color_pair = C_MIS;
 4186         if (color_pair && fcolors[color_pair])
 4187             attrs |= COLOR_PAIR(color_pair);
 4188 #ifdef ICONS_ENABLED
 4189         print_icon(ent, attrs);
 4190 #endif
 4191     }
 4192 
 4193     if (sel)
 4194         attrs |= A_REVERSE;
 4195     if (attrs)
 4196         attron(attrs);
 4197     if (!ind)
 4198         ++namecols;
 4199 
 4200 #ifndef NOLC
 4201     addwstr(unescape(ent->name, namecols));
 4202 #else
 4203     addstr(unescape(ent->name, MIN(namecols, ent->nlen) + 1));
 4204 #endif
 4205 
 4206     if (attrs)
 4207         attroff(attrs);
 4208     if (ind)
 4209         addch(ind);
 4210 }
 4211 
 4212 static void savecurctx(char *path, char *curname, int nextctx)
 4213 {
 4214     settings tmpcfg = cfg;
 4215     context *ctxr = &g_ctx[nextctx];
 4216 
 4217     /* Save current context */
 4218     if (curname)
 4219         xstrsncpy(g_ctx[tmpcfg.curctx].c_name, curname, NAME_MAX + 1);
 4220     else
 4221         g_ctx[tmpcfg.curctx].c_name[0] = '\0';
 4222 
 4223     g_ctx[tmpcfg.curctx].c_cfg = tmpcfg;
 4224 
 4225     if (ctxr->c_cfg.ctxactive) { /* Switch to saved context */
 4226         tmpcfg = ctxr->c_cfg;
 4227         /* Skip ordering an open context */
 4228         if (order) {
 4229             cfgsort[CTX_MAX] = cfgsort[nextctx];
 4230             cfgsort[nextctx] = '0';
 4231         }
 4232     } else { /* Set up a new context from current context */
 4233         ctxr->c_cfg.ctxactive = 1;
 4234         xstrsncpy(ctxr->c_path, path, PATH_MAX);
 4235         ctxr->c_last[0] = ctxr->c_name[0] = ctxr->c_fltr[0] = ctxr->c_fltr[1] = '\0';
 4236         ctxr->c_cfg = tmpcfg;
 4237         /* If already in an ordered dir, clear ordering for the new context and let it order */
 4238         if (cfgsort[cfg.curctx] == 'z')
 4239             cfgsort[nextctx] = 'z';
 4240     }
 4241 
 4242     tmpcfg.curctx = nextctx;
 4243     cfg = tmpcfg;
 4244 }
 4245 
 4246 #ifndef NOSSN
 4247 static void save_session(const char *sname, int *presel)
 4248 {
 4249     int fd, i;
 4250     session_header_t header;
 4251     bool status = FALSE;
 4252     char ssnpath[PATH_MAX];
 4253     char spath[PATH_MAX];
 4254 
 4255     memset(&header, 0, sizeof(session_header_t));
 4256 
 4257     header.ver = SESSIONS_VERSION;
 4258 
 4259     for (i = 0; i < CTX_MAX; ++i) {
 4260         if (g_ctx[i].c_cfg.ctxactive) {
 4261             if (cfg.curctx == i && ndents)
 4262                 /* Update current file name, arrows don't update it */
 4263                 xstrsncpy(g_ctx[i].c_name, pdents[cur].name, NAME_MAX + 1);
 4264             header.pathln[i] = strnlen(g_ctx[i].c_path, PATH_MAX) + 1;
 4265             header.lastln[i] = strnlen(g_ctx[i].c_last, PATH_MAX) + 1;
 4266             header.nameln[i] = strnlen(g_ctx[i].c_name, NAME_MAX) + 1;
 4267             header.fltrln[i] = REGEX_MAX;
 4268         }
 4269     }
 4270 
 4271     mkpath(cfgpath, toks[TOK_SSN], ssnpath);
 4272     mkpath(ssnpath, sname, spath);
 4273 
 4274     fd = open(spath, O_CREAT | O_WRONLY | O_TRUNC, 0666);
 4275     if (fd == -1) {
 4276         printwait(messages[MSG_SEL_MISSING], presel);
 4277         return;
 4278     }
 4279 
 4280     if ((write(fd, &header, sizeof(header)) != (ssize_t)sizeof(header))
 4281         || (write(fd, &cfg, sizeof(cfg)) != (ssize_t)sizeof(cfg)))
 4282         goto END;
 4283 
 4284     for (i = 0; i < CTX_MAX; ++i)
 4285         if ((write(fd, &g_ctx[i].c_cfg, sizeof(settings)) != (ssize_t)sizeof(settings))
 4286             || (write(fd, &g_ctx[i].color, sizeof(uint_t)) != (ssize_t)sizeof(uint_t))
 4287             || (header.nameln[i] > 0
 4288                 && write(fd, g_ctx[i].c_name, header.nameln[i]) != (ssize_t)header.nameln[i])
 4289             || (header.lastln[i] > 0
 4290                 && write(fd, g_ctx[i].c_last, header.lastln[i]) != (ssize_t)header.lastln[i])
 4291             || (header.fltrln[i] > 0
 4292                 && write(fd, g_ctx[i].c_fltr, header.fltrln[i]) != (ssize_t)header.fltrln[i])
 4293             || (header.pathln[i] > 0
 4294                 && write(fd, g_ctx[i].c_path, header.pathln[i]) != (ssize_t)header.pathln[i]))
 4295             goto END;
 4296 
 4297     status = TRUE;
 4298 
 4299 END:
 4300     close(fd);
 4301 
 4302     if (!status)
 4303         printwait(messages[MSG_FAILED], presel);
 4304 }
 4305 
 4306 static bool load_session(const char *sname, char **path, char **lastdir, char **lastname, bool restore)
 4307 {
 4308     int fd, i = 0;
 4309     session_header_t header;
 4310     bool has_loaded_dynamically = !(sname || restore);
 4311     bool status = (sname && g_state.picker); /* Picker mode with session program option */
 4312     char ssnpath[PATH_MAX];
 4313     char spath[PATH_MAX];
 4314 
 4315     mkpath(cfgpath, toks[TOK_SSN], ssnpath);
 4316 
 4317     if (!restore) {
 4318         sname = sname ? sname : xreadline(NULL, messages[MSG_SSN_NAME]);
 4319         if (!sname[0])
 4320             return FALSE;
 4321 
 4322         mkpath(ssnpath, sname, spath);
 4323 
 4324         /* If user is explicitly loading the "last session", skip auto-save */
 4325         if ((sname[0] == '@') && !sname[1])
 4326             has_loaded_dynamically = FALSE;
 4327     } else
 4328         mkpath(ssnpath, "@", spath);
 4329 
 4330     if (has_loaded_dynamically)
 4331         save_session("@", NULL);
 4332 
 4333     fd = open(spath, O_RDONLY, 0666);
 4334     if (fd == -1) {
 4335         if (!status) {
 4336             printmsg(messages[MSG_SEL_MISSING]);
 4337             xdelay(XDELAY_INTERVAL_MS);
 4338         }
 4339         return FALSE;
 4340     }
 4341 
 4342     status = FALSE;
 4343 
 4344     if ((read(fd, &header, sizeof(header)) != (ssize_t)sizeof(header))
 4345         || (header.ver != SESSIONS_VERSION)
 4346         || (read(fd, &cfg, sizeof(cfg)) != (ssize_t)sizeof(cfg)))
 4347         goto END;
 4348 
 4349     g_ctx[cfg.curctx].c_name[0] = g_ctx[cfg.curctx].c_last[0]
 4350         = g_ctx[cfg.curctx].c_fltr[0] = g_ctx[cfg.curctx].c_fltr[1] = '\0';
 4351 
 4352     for (; i < CTX_MAX; ++i)
 4353         if ((read(fd, &g_ctx[i].c_cfg, sizeof(settings)) != (ssize_t)sizeof(settings))
 4354             || (read(fd, &g_ctx[i].color, sizeof(uint_t)) != (ssize_t)sizeof(uint_t))
 4355             || (header.nameln[i] > 0
 4356                 && read(fd, g_ctx[i].c_name, header.nameln[i]) != (ssize_t)header.nameln[i])
 4357             || (header.lastln[i] > 0
 4358                 && read(fd, g_ctx[i].c_last, header.lastln[i]) != (ssize_t)header.lastln[i])
 4359             || (header.fltrln[i] > 0
 4360                 && read(fd, g_ctx[i].c_fltr, header.fltrln[i]) != (ssize_t)header.fltrln[i])
 4361             || (header.pathln[i] > 0
 4362                 && read(fd, g_ctx[i].c_path, header.pathln[i]) != (ssize_t)header.pathln[i]))
 4363             goto END;
 4364 
 4365     *path = g_ctx[cfg.curctx].c_path;
 4366     *lastdir = g_ctx[cfg.curctx].c_last;
 4367     *lastname = g_ctx[cfg.curctx].c_name;
 4368     set_sort_flags('\0'); /* Set correct sort options */
 4369     status = TRUE;
 4370 
 4371 END:
 4372     close(fd);
 4373 
 4374     if (!status) {
 4375         printmsg(messages[MSG_FAILED]);
 4376         xdelay(XDELAY_INTERVAL_MS);
 4377     } else if (restore)
 4378         unlink(spath);
 4379 
 4380     return status;
 4381 }
 4382 #endif
 4383 
 4384 static uchar_t get_free_ctx(void)
 4385 {
 4386     uchar_t r = cfg.curctx;
 4387 
 4388     do
 4389         r = (r + 1) & ~CTX_MAX;
 4390     while (g_ctx[r].c_cfg.ctxactive && (r != cfg.curctx));
 4391 
 4392     return r;
 4393 }
 4394 
 4395 /* ctx is absolute: 1 to 4, + for smart context */
 4396 static void set_smart_ctx(int ctx, char *nextpath, char **path, char *file, char **lastname, char **lastdir)
 4397 {
 4398     if (ctx == '+') /* Get smart context */
 4399         ctx = (int)(get_free_ctx() + 1);
 4400 
 4401     if (ctx == 0 || ctx == cfg.curctx + 1) { /* Same context */
 4402         xstrsncpy(*lastdir, *path, PATH_MAX);
 4403         xstrsncpy(*path, nextpath, PATH_MAX);
 4404     } else { /* New context */
 4405         --ctx;
 4406         /* Deactivate the new context and build from scratch */
 4407         g_ctx[ctx].c_cfg.ctxactive = 0;
 4408         DPRINTF_S(nextpath);
 4409         savecurctx(nextpath, file, ctx);
 4410         *path = g_ctx[ctx].c_path;
 4411         *lastdir = g_ctx[ctx].c_last;
 4412         *lastname = g_ctx[ctx].c_name;
 4413     }
 4414 }
 4415 
 4416 /*
 4417  * Gets only a single line (that's what we need for now) or shows full command output in pager.
 4418  * Uses g_buf internally.
 4419  */
 4420 static bool get_output(char *file, char *arg1, char *arg2, int fdout, bool multi, bool page)
 4421 {
 4422     pid_t pid;
 4423     int pipefd[2];
 4424     int index = 0, flags;
 4425     bool ret = FALSE;
 4426     bool tmpfile = ((fdout == -1) && page);
 4427     char *argv[EXEC_ARGS_MAX] = {0};
 4428     char *cmd = NULL;
 4429     int fd = -1;
 4430     ssize_t len;
 4431 
 4432     if (tmpfile) {
 4433         fdout = create_tmp_file();
 4434         if (fdout == -1)
 4435             return FALSE;
 4436     }
 4437 
 4438     if (multi) {
 4439         cmd = parseargs(file, argv, &index);
 4440         if (!cmd)
 4441             return FALSE;
 4442     } else
 4443         argv[index++] = file;
 4444 
 4445     argv[index] = arg1;
 4446     argv[++index] = arg2;
 4447 
 4448     if (pipe(pipefd) == -1) {
 4449         free(cmd);
 4450         errexit();
 4451     }
 4452 
 4453     for (index = 0; index < 2; ++index) {
 4454         /* Get previous flags */
 4455         flags = fcntl(pipefd[index], F_GETFL, 0);
 4456 
 4457         /* Set bit for non-blocking flag */
 4458         flags |= O_NONBLOCK;
 4459 
 4460         /* Change flags on fd */
 4461         fcntl(pipefd[index], F_SETFL, flags);
 4462     }
 4463 
 4464     pid = fork();
 4465     if (pid == 0) {
 4466         /* In child */
 4467         close(pipefd[0]);
 4468         dup2(pipefd[1], STDOUT_FILENO);
 4469         dup2(pipefd[1], STDERR_FILENO);
 4470         close(pipefd[1]);
 4471         execvp(*argv, argv);
 4472         _exit(EXIT_SUCCESS);
 4473     }
 4474 
 4475     /* In parent */
 4476     waitpid(pid, NULL, 0);
 4477     close(pipefd[1]);
 4478     free(cmd);
 4479 
 4480     while ((len = read(pipefd[0], g_buf, CMD_LEN_MAX - 1)) > 0) {
 4481         ret = TRUE;
 4482         if (fdout == -1) /* Read only the first line of output to buffer */
 4483             break;
 4484         if (write(fdout, g_buf, len) != len)
 4485             break;
 4486     }
 4487 
 4488     close(pipefd[0]);
 4489     if (!page)
 4490         return ret;
 4491 
 4492     if (tmpfile) {
 4493         close(fdout);
 4494         close(fd);
 4495     }
 4496 
 4497     spawn(pager, g_tmpfpath, NULL, NULL, F_CLI | F_TTY);
 4498 
 4499     if (tmpfile)
 4500         unlink(g_tmpfpath);
 4501 
 4502     return TRUE;
 4503 }
 4504 
 4505 /*
 4506  * Follows the stat(1) output closely
 4507  */
 4508 static bool show_stats(char *fpath)
 4509 {
 4510     static char * const cmds[] = {
 4511 #ifdef FILE_MIME_OPTS
 4512         ("file " FILE_MIME_OPTS),
 4513 #endif
 4514         "file -b",
 4515         "stat",
 4516     };
 4517 
 4518     size_t r = ELEMENTS(cmds);
 4519     int fd = create_tmp_file();
 4520     if (fd == -1)
 4521         return FALSE;
 4522 
 4523     while (r)
 4524         get_output(cmds[--r], fpath, NULL, fd, TRUE, FALSE);
 4525 
 4526     close(fd);
 4527 
 4528     spawn(pager, g_tmpfpath, NULL, NULL, F_CLI | F_TTY);
 4529     unlink(g_tmpfpath);
 4530     return TRUE;
 4531 }
 4532 
 4533 static bool xchmod(const char *fpath, mode_t mode)
 4534 {
 4535     /* (Un)set (S_IXUSR | S_IXGRP | S_IXOTH) */
 4536     (0100 & mode) ? (mode &= ~0111) : (mode |= 0111);
 4537 
 4538     return (chmod(fpath, mode) == 0);
 4539 }
 4540 
 4541 static size_t get_fs_info(const char *path, bool type)
 4542 {
 4543     struct statvfs svb;
 4544 
 4545     if (statvfs(path, &svb) == -1)
 4546         return 0;
 4547 
 4548     if (type == CAPACITY)
 4549         return (size_t)svb.f_blocks << ffs((int)(svb.f_frsize >> 1));
 4550 
 4551     return (size_t)svb.f_bavail << ffs((int)(svb.f_frsize >> 1));
 4552 }
 4553 
 4554 /* Create non-existent parents and a file or dir */
 4555 static bool xmktree(char *path, bool dir)
 4556 {
 4557     char *p = path;
 4558     char *slash = path;
 4559 
 4560     if (!p || !*p)
 4561         return FALSE;
 4562 
 4563     /* Skip the first '/' */
 4564     ++p;
 4565 
 4566     while (*p != '\0') {
 4567         if (*p == '/') {
 4568             slash = p;
 4569             *p = '\0';
 4570         } else {
 4571             ++p;
 4572             continue;
 4573         }
 4574 
 4575         /* Create folder from path to '\0' inserted at p */
 4576         if (mkdir(path, 0777) == -1 && errno != EEXIST) {
 4577 #ifdef __HAIKU__
 4578             // XDG_CONFIG_HOME contains a directory
 4579             // that is read-only, but the full path
 4580             // is writeable.
 4581             // Try to continue and see what happens.
 4582             // TODO: Find a more robust solution.
 4583             if (errno == B_READ_ONLY_DEVICE)
 4584                 goto next;
 4585 #endif
 4586             DPRINTF_S("mkdir1!");
 4587             DPRINTF_S(strerror(errno));
 4588             *slash = '/';
 4589             return FALSE;
 4590         }
 4591 
 4592 #ifdef __HAIKU__
 4593 next:
 4594 #endif
 4595         /* Restore path */
 4596         *slash = '/';
 4597         ++p;
 4598     }
 4599 
 4600     if (dir) {
 4601         if (mkdir(path, 0777) == -1 && errno != EEXIST) {
 4602             DPRINTF_S("mkdir2!");
 4603             DPRINTF_S(strerror(errno));
 4604             return FALSE;
 4605         }
 4606     } else {
 4607         int fd = open(path, O_CREAT, 0666);
 4608 
 4609         if (fd == -1 && errno != EEXIST) {
 4610             DPRINTF_S("open!");
 4611             DPRINTF_S(strerror(errno));
 4612             return FALSE;
 4613         }
 4614 
 4615         close(fd);
 4616     }
 4617 
 4618     return TRUE;
 4619 }
 4620 
 4621 /* List or extract archive */
 4622 static bool handle_archive(char *fpath /* in-out param */, char op)
 4623 {
 4624     char arg[] = "-tvf"; /* options for tar/bsdtar to list files */
 4625     char *util, *outdir = NULL;
 4626     bool x_to = FALSE;
 4627     bool is_atool = getutil(utils[UTIL_ATOOL]);
 4628 
 4629     if (op == 'x') {
 4630         outdir = xreadline(is_atool ? "." : xbasename(fpath), messages[MSG_NEW_PATH]);
 4631         if (!outdir || !*outdir) { /* Cancelled */
 4632             printwait(messages[MSG_CANCEL], NULL);
 4633             return FALSE;
 4634         }
 4635         /* Do not create smart context for current dir */
 4636         if (!(*outdir == '.' && outdir[1] == '\0')) {
 4637             if (!xmktree(outdir, TRUE) || (chdir(outdir) == -1)) {
 4638                 printwarn(NULL);
 4639                 return FALSE;
 4640             }
 4641             /* Copy the new dir path to open it in smart context */
 4642             outdir = getcwd(NULL, 0);
 4643             x_to = TRUE;
 4644         }
 4645     }
 4646 
 4647     if (is_atool) {
 4648         util = utils[UTIL_ATOOL];
 4649         arg[1] = op;
 4650         arg[2] = '\0';
 4651     } else if (getutil(utils[UTIL_BSDTAR])) {
 4652         util = utils[UTIL_BSDTAR];
 4653         if (op == 'x')
 4654             arg[1] = op;
 4655     } else if (is_suffix(fpath, ".zip")) {
 4656         util = utils[UTIL_UNZIP];
 4657         arg[1] = (op == 'l') ? 'v' /* verbose listing */ : '\0';
 4658         arg[2] = '\0';
 4659     } else {
 4660         util = utils[UTIL_TAR];
 4661         if (op == 'x')
 4662             arg[1] = op;
 4663     }
 4664 
 4665     if (op == 'x') /* extract */
 4666         spawn(util, arg, fpath, NULL, F_NORMAL | F_MULTI);
 4667     else /* list */
 4668         get_output(util, arg, fpath, -1, TRUE, TRUE);
 4669 
 4670     if (x_to) {
 4671         if (chdir(xdirname(fpath)) == -1) {
 4672             printwarn(NULL);
 4673             free(outdir);
 4674             return FALSE;
 4675         }
 4676         xstrsncpy(fpath, outdir, PATH_MAX);
 4677         free(outdir);
 4678     } else if (op == 'x')
 4679         fpath[0] = '\0';
 4680 
 4681     return TRUE;
 4682 }
 4683 
 4684 static char *visit_parent(char *path, char *newpath, int *presel)
 4685 {
 4686     char *dir;
 4687 
 4688     /* There is no going back */
 4689     if (istopdir(path)) {
 4690         /* Continue in type-to-nav mode, if enabled */
 4691         if (cfg.filtermode && presel)
 4692             *presel = FILTER;
 4693         return NULL;
 4694     }
 4695 
 4696     /* Use a copy as xdirname() may change the string passed */
 4697     if (newpath)
 4698         xstrsncpy(newpath, path, PATH_MAX);
 4699     else
 4700         newpath = path;
 4701 
 4702     dir = xdirname(newpath);
 4703     if (chdir(dir) == -1) {
 4704         printwarn(presel);
 4705         return NULL;
 4706     }
 4707 
 4708     return dir;
 4709 }
 4710 
 4711 static void valid_parent(char *path, char *lastname)
 4712 {
 4713     /* Save history */
 4714     xstrsncpy(lastname, xbasename(path), NAME_MAX + 1);
 4715 
 4716     while (!istopdir(path))
 4717         if (visit_parent(path, NULL, NULL))
 4718             break;
 4719 
 4720     printwarn(NULL);
 4721     xdelay(XDELAY_INTERVAL_MS);
 4722 }
 4723 
 4724 static bool archive_mount(char *newpath)
 4725 {
 4726     char *str = "install archivemount";
 4727     char *dir, *cmd = str + 8; /* Start of "archivemount" */
 4728     char *name = pdents[cur].name;
 4729     size_t len = pdents[cur].nlen;
 4730     char mntpath[PATH_MAX];
 4731 
 4732     if (!getutil(cmd)) {
 4733         printmsg(str);
 4734         return FALSE;
 4735     }
 4736 
 4737     dir = xstrdup(name);
 4738     if (!dir) {
 4739         printmsg(messages[MSG_FAILED]);
 4740         return FALSE;
 4741     }
 4742 
 4743     while (len > 1)
 4744         if (dir[--len] == '.') {
 4745             dir[len] = '\0';
 4746             break;
 4747         }
 4748 
 4749     DPRINTF_S(dir);
 4750 
 4751     /* Create the mount point */
 4752     mkpath(cfgpath, toks[TOK_MNT], mntpath);
 4753     mkpath(mntpath, dir, newpath);
 4754     free(dir);
 4755 
 4756     if (!xmktree(newpath, TRUE)) {
 4757         printwarn(NULL);
 4758         return FALSE;
 4759     }
 4760 
 4761     /* Mount archive */
 4762     DPRINTF_S(name);
 4763     DPRINTF_S(newpath);
 4764     if (spawn(cmd, name, newpath, NULL, F_NORMAL)) {
 4765         printmsg(messages[MSG_FAILED]);
 4766         return FALSE;
 4767     }
 4768 
 4769     return TRUE;
 4770 }
 4771 
 4772 static bool remote_mount(char *newpath)
 4773 {
 4774     uchar_t flag = F_CLI;
 4775     int opt;
 4776     char *tmp, *env;
 4777     bool r = getutil(utils[UTIL_RCLONE]), s = getutil(utils[UTIL_SSHFS]);
 4778     char mntpath[PATH_MAX];
 4779 
 4780     if (!(r || s)) {
 4781         printmsg("install sshfs/rclone");
 4782         return FALSE;
 4783     }
 4784 
 4785     if (r && s)
 4786         opt = get_input(messages[MSG_REMOTE_OPTS]);
 4787     else
 4788         opt = (!s) ? 'r' : 's';
 4789 
 4790     if (opt == 's')
 4791         env = xgetenv("NNN_SSHFS", utils[UTIL_SSHFS]);
 4792     else if (opt == 'r') {
 4793         flag |= F_NOWAIT | F_NOTRACE;
 4794         env = xgetenv("NNN_RCLONE", "rclone mount");
 4795     } else {
 4796         printmsg(messages[MSG_INVALID_KEY]);
 4797         return FALSE;
 4798     }
 4799 
 4800     tmp = xreadline(NULL, "host[:dir] > ");
 4801     if (!tmp[0]) {
 4802         printmsg(messages[MSG_CANCEL]);
 4803         return FALSE;
 4804     }
 4805 
 4806     char *div = strchr(tmp, ':');
 4807 
 4808     if (div)
 4809         *div = '\0';
 4810 
 4811     /* Create the mount point */
 4812     mkpath(cfgpath, toks[TOK_MNT], mntpath);
 4813     mkpath(mntpath, tmp, newpath);
 4814     if (!xmktree(newpath, TRUE)) {
 4815         printwarn(NULL);
 4816         return FALSE;
 4817     }
 4818 
 4819     if (!div) { /* Convert "host" to "host:" */
 4820         size_t len = xstrlen(tmp);
 4821 
 4822         tmp[len] = ':';
 4823         tmp[len + 1] = '\0';
 4824     } else
 4825         *div = ':';
 4826 
 4827     /* Connect to remote */
 4828     if (opt == 's') {
 4829         if (spawn(env, tmp, newpath, NULL, flag)) {
 4830             printmsg(messages[MSG_FAILED]);
 4831             return FALSE;
 4832         }
 4833     } else {
 4834         spawn(env, tmp, newpath, NULL, flag);
 4835         printmsg(messages[MSG_RCLONE_DELAY]);
 4836         xdelay(XDELAY_INTERVAL_MS << 2); /* Set 4 times the usual delay */
 4837     }
 4838 
 4839     return TRUE;
 4840 }
 4841 
 4842 /*
 4843  * Unmounts if the directory represented by name is a mount point.
 4844  * Otherwise, asks for hostname
 4845  * Returns TRUE if directory needs to be refreshed *.
 4846  */
 4847 static bool unmount(char *name, char *newpath, int *presel, char *currentpath)
 4848 {
 4849 #if defined(__APPLE__) || defined(__FreeBSD__)
 4850     static char cmd[] = "umount";
 4851 #else
 4852     static char cmd[] = "fusermount3"; /* Arch Linux utility */
 4853     static bool found = FALSE;
 4854 #endif
 4855     char *tmp = name;
 4856     struct stat sb, psb;
 4857     bool child = FALSE;
 4858     bool parent = FALSE;
 4859     bool hovered = FALSE;
 4860     char mntpath[PATH_MAX];
 4861 
 4862 #if !defined(__APPLE__) && !defined(__FreeBSD__)
 4863     /* On Ubuntu it's fusermount */
 4864     if (!found && !getutil(cmd)) {
 4865         cmd[10] = '\0';
 4866         found = TRUE;
 4867     }
 4868 #endif
 4869 
 4870     mkpath(cfgpath, toks[TOK_MNT], mntpath);
 4871 
 4872     if (tmp && strcmp(mntpath, currentpath) == 0) {
 4873         mkpath(mntpath, tmp, newpath);
 4874         child = lstat(newpath, &sb) != -1;
 4875         parent = lstat(xdirname(newpath), &psb) != -1;
 4876         if (!child && !parent) {
 4877             *presel = MSGWAIT;
 4878             return FALSE;
 4879         }
 4880     }
 4881 
 4882     if (!tmp || !child || !S_ISDIR(sb.st_mode) || (child && parent && sb.st_dev == psb.st_dev)) {
 4883         tmp = xreadline(NULL, messages[MSG_HOSTNAME]);
 4884         if (!tmp[0])
 4885             return FALSE;
 4886         if (name && (tmp[0] == '-') && (tmp[1] == '\0')) {
 4887             mkpath(currentpath, name, newpath);
 4888             hovered = TRUE;
 4889         }
 4890     }
 4891 
 4892     if (!hovered)
 4893         mkpath(mntpath, tmp, newpath);
 4894 
 4895     if (!xdiraccess(newpath)) {
 4896         *presel = MSGWAIT;
 4897         return FALSE;
 4898     }
 4899 
 4900 #if defined(__APPLE__) || defined(__FreeBSD__)
 4901     if (spawn(cmd, newpath, NULL, NULL, F_NORMAL)) {
 4902 #else
 4903     if (spawn(cmd, "-qu", newpath, NULL, F_NORMAL)) {
 4904 #endif
 4905         if (!xconfirm(get_input(messages[MSG_LAZY])))
 4906             return FALSE;
 4907 
 4908 #ifdef __APPLE__
 4909         if (spawn(cmd, "-l", newpath, NULL, F_NORMAL)) {
 4910 #elif defined(__FreeBSD__)
 4911         if (spawn(cmd, "-f", newpath, NULL, F_NORMAL)) {
 4912 #else
 4913         if (spawn(cmd, "-quz", newpath, NULL, F_NORMAL)) {
 4914 #endif
 4915             printwait(messages[MSG_FAILED], presel);
 4916             return FALSE;
 4917         }
 4918     }
 4919 
 4920     if (rmdir(newpath) == -1) {
 4921         printwarn(presel);
 4922         return FALSE;
 4923     }
 4924 
 4925     return TRUE;
 4926 }
 4927 
 4928 static void lock_terminal(void)
 4929 {
 4930     spawn(xgetenv("NNN_LOCKER", utils[UTIL_LOCKER]), NULL, NULL, NULL, F_CLI);
 4931 }
 4932 
 4933 static void printkv(kv *kvarr, int fd, uchar_t max, uchar_t id)
 4934 {
 4935     char *val = (id == NNN_BMS) ? bmstr : pluginstr;
 4936 
 4937     for (uchar_t i = 0; i < max && kvarr[i].key; ++i)
 4938         dprintf(fd, " %c: %s\n", (char)kvarr[i].key, val + kvarr[i].off);
 4939 }
 4940 
 4941 static void printkeys(kv *kvarr, char *buf, uchar_t max)
 4942 {
 4943     uchar_t i = 0;
 4944 
 4945     for (; i < max && kvarr[i].key; ++i) {
 4946         buf[i << 1] = ' ';
 4947         buf[(i << 1) + 1] = kvarr[i].key;
 4948     }
 4949 
 4950     buf[i << 1] = '\0';
 4951 }
 4952 
 4953 static size_t handle_bookmark(const char *bmark, char *newpath)
 4954 {
 4955     int fd = '\r';
 4956     size_t r;
 4957 
 4958     if (maxbm || bmark) {
 4959         r = xstrsncpy(g_buf, messages[MSG_KEYS], CMD_LEN_MAX);
 4960 
 4961         if (bmark) { /* There is a marked directory */
 4962             g_buf[--r] = ' ';
 4963             g_buf[++r] = ',';
 4964             g_buf[++r] = '\0';
 4965             ++r;
 4966         }
 4967         printkeys(bookmark, g_buf + r - 1, maxbm);
 4968         printmsg(g_buf);
 4969         fd = get_input(NULL);
 4970     }
 4971 
 4972     r = FALSE;
 4973     if (fd == ',') /* Visit marked directory */
 4974         bmark ? xstrsncpy(newpath, bmark, PATH_MAX) : (r = MSG_NOT_SET);
 4975     else if (fd == '\r') /* Visit bookmarks directory */
 4976         mkpath(cfgpath, toks[TOK_BM], newpath);
 4977     else if (!get_kv_val(bookmark, newpath, fd, maxbm, NNN_BMS))
 4978         r = MSG_INVALID_KEY;
 4979 
 4980     if (!r && chdir(newpath) == -1)
 4981         r = MSG_ACCESS;
 4982 
 4983     return r;
 4984 }
 4985 
 4986 static void add_bookmark(char *path, char *newpath, int *presel)
 4987 {
 4988     char *dir = xbasename(path);
 4989 
 4990     dir = xreadline(dir[0] ? dir : NULL, "name: ");
 4991     if (dir && *dir) {
 4992         size_t r = mkpath(cfgpath, toks[TOK_BM], newpath);
 4993 
 4994         newpath[r - 1] = '/';
 4995         xstrsncpy(newpath + r, dir, PATH_MAX - r);
 4996         printwait((symlink(path, newpath) == -1) ? strerror(errno) : newpath, presel);
 4997     } else
 4998         printwait(messages[MSG_CANCEL], presel);
 4999 }
 5000 
 5001 /*
 5002  * The help string tokens (each line) start with a HEX value
 5003  * which indicates the number of spaces to print before the
 5004  * particular token. This method was chosen instead of a flat
 5005  * string because the number of bytes in help was increasing
 5006  * the binary size by around a hundred bytes. This would only
 5007  * have increased as we keep adding new options.
 5008  */
 5009 static void show_help(const char *path)
 5010 {
 5011     const char *start, *end;
 5012     const char helpstr[] = {
 5013     "0\n"
 5014     "1NAVIGATION\n"
 5015            "9Up k  Up%-16cPgUp ^U  Page up\n"
 5016            "9Dn j  Down%-14cPgDn ^D  Page down\n"
 5017            "9Lt h  Parent%-12c~ ` @ -  ~, /, start, prev\n"
 5018        "5Ret Rt l  Open%-20c'  First file/match\n"
 5019            "9g ^A  Top%-21c.  Toggle hidden\n"
 5020            "9G ^E  End%-21c+  Toggle auto-advance\n"
 5021           "8B (,)  Book(mark)%-11cb ^/  Select bookmark\n"
 5022         "a1-4  Context%-11c(Sh)Tab  Cycle/new context\n"
 5023         "62Esc ^Q  Quit%-20cq  Quit context\n"
 5024          "b^G  QuitCD%-18cQ  Pick/err, quit\n"
 5025     "0\n"
 5026     "1FILTER & PROMPT\n"
 5027           "c/  Filter%-17c^N  Toggle type-to-nav\n"
 5028         "aEsc  Exit prompt%-12c^L  Toggle last filter\n"
 5029             "d%-20cAlt+Esc  Unfilter, quit context\n"
 5030     "0\n"
 5031     "1FILES\n"
 5032            "9o ^O  Open with%-15cn  Create new/link\n"
 5033            "9f ^F  File stats%-14cd  Detail mode toggle\n"
 5034          "b^R  Rename/dup%-14cr  Batch rename\n"
 5035           "cz  Archive%-17ce  Edit file\n"
 5036           "c*  Toggle exe%-14c>  Export list\n"
 5037        "5Space ^J  (Un)select%-12cm-m  Select range/clear\n"
 5038               "ca  Select all%-14cA  Invert sel\n"
 5039            "9p ^P  Copy here%-12cw ^W  Cp/mv sel as\n"
 5040            "9v ^V  Move here%-15cE  Edit sel list\n"
 5041            "9x ^X  Delete%-16cEsc  Send to FIFO\n"
 5042     "0\n"
 5043     "1MISC\n"
 5044           "8Alt ;  Select plugin%-11c=  Launch app\n"
 5045            "9! ^]  Shell%-19c]  Cmd prompt\n"
 5046           "cc  Connect remote%-10cu  Unmount remote/archive\n"
 5047            "9t ^T  Sort toggles%-12cs  Manage session\n"
 5048           "cT  Set time type%-11c0  Lock\n"
 5049          "b^L  Redraw%-18c?  Help, conf\n"
 5050     };
 5051 
 5052     int fd = create_tmp_file();
 5053     if (fd == -1)
 5054         return;
 5055 
 5056     dprintf(fd, "  |V\\_\n"
 5057             "  /. \\\\\n"
 5058             " (;^; ||\n"
 5059             "   /___3\n"
 5060             "  (___n))\n");
 5061 
 5062     char *prog = xgetenv(env_cfg[NNN_HELP], NULL);
 5063     if (prog)
 5064         get_output(prog, NULL, NULL, fd, TRUE, FALSE);
 5065 
 5066     start = end = helpstr;
 5067     while (*end) {
 5068         if (*end == '\n') {
 5069             snprintf(g_buf, CMD_LEN_MAX, "%*c%.*s",
 5070                  xchartohex(*start), ' ', (int)(end - start), start + 1);
 5071             dprintf(fd, g_buf, ' ');
 5072             start = end + 1;
 5073         }
 5074 
 5075         ++end;
 5076     }
 5077 
 5078     dprintf(fd, "\nLOCATIONS:\n");
 5079     for (uchar_t i = 0; i < CTX_MAX; ++i)
 5080         if (g_ctx[i].c_cfg.ctxactive)
 5081             dprintf(fd, " %u: %s\n", i + 1, g_ctx[i].c_path);
 5082 
 5083     dprintf(fd, "\nVOLUME: %s of ", coolsize(get_fs_info(path, FREE)));
 5084     dprintf(fd, "%s free\n\n", coolsize(get_fs_info(path, CAPACITY)));
 5085 
 5086     if (bookmark) {
 5087         dprintf(fd, "BOOKMARKS\n");
 5088         printkv(bookmark, fd, maxbm, NNN_BMS);
 5089         dprintf(fd, "\n");
 5090     }
 5091 
 5092     if (plug) {
 5093         dprintf(fd, "PLUGIN KEYS\n");
 5094         printkv(plug, fd, maxplug, NNN_PLUG);
 5095         dprintf(fd, "\n");
 5096     }
 5097 
 5098     for (uchar_t i = NNN_OPENER; i <= NNN_TRASH; ++i) {
 5099         start = getenv(env_cfg[i]);
 5100         if (start)
 5101             dprintf(fd, "%s: %s\n", env_cfg[i], start);
 5102     }
 5103 
 5104     if (selpath)
 5105         dprintf(fd, "SELECTION FILE: %s\n", selpath);
 5106 
 5107     dprintf(fd, "\nv%s\n%s\n", VERSION, GENERAL_INFO);
 5108     close(fd);
 5109 
 5110     spawn(pager, g_tmpfpath, NULL, NULL, F_CLI | F_TTY);
 5111     unlink(g_tmpfpath);
 5112 }
 5113 
 5114 static void setexports(void)
 5115 {
 5116     char dvar[] = "d0";
 5117     char fvar[] = "f0";
 5118 
 5119     if (ndents) {
 5120         setenv(envs[ENV_NCUR], pdents[cur].name, 1);
 5121         xstrsncpy(g_ctx[cfg.curctx].c_name, pdents[cur].name, NAME_MAX + 1);
 5122     } else if (g_ctx[cfg.curctx].c_name[0])
 5123         g_ctx[cfg.curctx].c_name[0] = '\0';
 5124 
 5125     for (uchar_t i = 0; i < CTX_MAX; ++i) {
 5126         if (g_ctx[i].c_cfg.ctxactive) {
 5127             dvar[1] = fvar[1] = '1' + i;
 5128             setenv(dvar, g_ctx[i].c_path, 1);
 5129 
 5130             if (g_ctx[i].c_name[0]) {
 5131                 mkpath(g_ctx[i].c_path, g_ctx[i].c_name, g_buf);
 5132                 setenv(fvar, g_buf, 1);
 5133             }
 5134         }
 5135     }
 5136 }
 5137 
 5138 static bool run_cmd_as_plugin(const char *file, char *runfile, uchar_t flags)
 5139 {
 5140     size_t len;
 5141 
 5142     xstrsncpy(g_buf, file, PATH_MAX);
 5143 
 5144     len = xstrlen(g_buf);
 5145     if (len > 1 && g_buf[len - 1] == '*') {
 5146         flags &= ~F_CONFIRM; /* Skip user confirmation */
 5147         g_buf[len - 1] = '\0'; /* Get rid of trailing no confirmation symbol */
 5148         --len;
 5149     }
 5150 
 5151     if (flags & F_PAGE)
 5152         get_output(g_buf, NULL, NULL, -1, TRUE, TRUE);
 5153     else if (flags & F_NOTRACE) {
 5154         if (is_suffix(g_buf, " $nnn"))
 5155             g_buf[len - 5] = '\0';
 5156         else
 5157             runfile = NULL;
 5158         spawn(g_buf, runfile, NULL, NULL, flags);
 5159     } else
 5160         spawn(utils[UTIL_SH_EXEC], g_buf, NULL, NULL, flags);
 5161 
 5162     return TRUE;
 5163 }
 5164 
 5165 static bool plctrl_init(void)
 5166 {
 5167     size_t len;
 5168 
 5169     /* g_tmpfpath is used to generate tmp file names */
 5170     g_tmpfpath[tmpfplen - 1] = '\0';
 5171     len = xstrsncpy(g_pipepath, g_tmpfpath, TMP_LEN_MAX);
 5172     g_pipepath[len - 1] = '/';
 5173     len = xstrsncpy(g_pipepath + len, "nnn-pipe.", TMP_LEN_MAX - len) + len;
 5174     xstrsncpy(g_pipepath + len - 1, xitoa(getpid()), TMP_LEN_MAX - len);
 5175     setenv(env_cfg[NNN_PIPE], g_pipepath, TRUE);
 5176 
 5177     return EXIT_SUCCESS;
 5178 }
 5179 
 5180 static void rmlistpath(void)
 5181 {
 5182     if (listpath) {
 5183         DPRINTF_S(__func__);
 5184         DPRINTF_S(listpath);
 5185         spawn(utils[UTIL_RM_RF], listpath, NULL, NULL, F_NOTRACE | F_MULTI);
 5186         /* Do not free if program was started in list mode */
 5187         if (listpath != initpath)
 5188             free(listpath);
 5189         listpath = NULL;
 5190     }
 5191 }
 5192 
 5193 static ssize_t read_nointr(int fd, void *buf, size_t count)
 5194 {
 5195     ssize_t len;
 5196 
 5197     do
 5198         len = read(fd, buf, count);
 5199     while (len == -1 && errno == EINTR);
 5200 
 5201     return len;
 5202 }
 5203 
 5204 static char *readpipe(int fd, char *ctxnum, char **path)
 5205 {
 5206     char ctx, *nextpath = NULL;
 5207 
 5208     if (read_nointr(fd, g_buf, 1) != 1)
 5209         return NULL;
 5210 
 5211     if (g_buf[0] == '-') { /* Clear selection on '-' */
 5212         clearselection();
 5213         if (read_nointr(fd, g_buf, 1) != 1)
 5214             return NULL;
 5215     }
 5216 
 5217     if (g_buf[0] == '+')
 5218         ctx = (char)(get_free_ctx() + 1);
 5219     else if (g_buf[0] < '0')
 5220         return NULL;
 5221     else {
 5222         ctx = g_buf[0] - '0';
 5223         if (ctx > CTX_MAX)
 5224             return NULL;
 5225     }
 5226 
 5227     if (read_nointr(fd, g_buf, 1) != 1)
 5228         return NULL;
 5229 
 5230     char op = g_buf[0];
 5231 
 5232     if (op == 'c') {
 5233         ssize_t len = read_nointr(fd, g_buf, PATH_MAX);
 5234 
 5235         if (len <= 0)
 5236             return NULL;
 5237 
 5238         g_buf[len] = '\0'; /* Terminate the path read */
 5239         if (g_buf[0] == '/') {
 5240             nextpath = g_buf;
 5241             len = xstrlen(g_buf);
 5242             while (--len && (g_buf[len] == '/')) /* Trim all trailing '/' */
 5243                 g_buf[len] = '\0';
 5244         }
 5245     } else if (op == 'l') {
 5246         rmlistpath(); /* Remove last list mode path, if any */
 5247         nextpath = load_input(fd, *path);
 5248     } else if (op == 'p') {
 5249         free(selpath);
 5250         selpath = NULL;
 5251         clearselection();
 5252         g_state.picker = 0;
 5253         g_state.picked = 1;
 5254     }
 5255 
 5256     *ctxnum = ctx;
 5257 
 5258     return nextpath;
 5259 }
 5260 
 5261 static bool run_plugin(char **path, const char *file, char *runfile, char **lastname, char **lastdir)
 5262 {
 5263     pid_t p;
 5264     char ctx = 0;
 5265     uchar_t flags = 0;
 5266     bool cmd_as_plugin = FALSE;
 5267     char *nextpath;
 5268 
 5269     if (!g_state.pluginit) {
 5270         plctrl_init();
 5271         g_state.pluginit = 1;
 5272     }
 5273 
 5274     setexports();
 5275 
 5276     /* Check for run-cmd-as-plugin mode */
 5277     if (*file == '!') {
 5278         flags = F_MULTI | F_CONFIRM;
 5279         ++file;
 5280 
 5281         if (*file == '|') { /* Check if output should be paged */
 5282             flags |= F_PAGE;
 5283             ++file;
 5284         } else if (*file == '&') { /* Check if GUI flags are to be used */
 5285             flags = F_NOTRACE | F_NOWAIT;
 5286             ++file;
 5287         }
 5288 
 5289         if (!*file)
 5290             return FALSE;
 5291 
 5292         if ((flags & F_NOTRACE) || (flags & F_PAGE))
 5293             return run_cmd_as_plugin(file, runfile, flags);
 5294 
 5295         cmd_as_plugin = TRUE;
 5296     }
 5297 
 5298     if (mkfifo(g_pipepath, 0600) != 0)
 5299         return FALSE;
 5300 
 5301     exitcurses();
 5302 
 5303     p = fork();
 5304 
 5305     if (!p) { // In child
 5306         int wfd = open(g_pipepath, O_WRONLY | O_CLOEXEC);
 5307 
 5308         if (wfd == -1)
 5309             _exit(EXIT_FAILURE);
 5310 
 5311         if (!cmd_as_plugin) {
 5312             char *sel = NULL;
 5313             char std[2] = "-";
 5314 
 5315             /* Generate absolute path to plugin */
 5316             mkpath(plgpath, file, g_buf);
 5317 
 5318             if (g_state.picker)
 5319                 sel = selpath ? selpath : std;
 5320 
 5321             if (runfile && runfile[0]) {
 5322                 xstrsncpy(*lastname, runfile, NAME_MAX);
 5323                 spawn(g_buf, *lastname, *path, sel, 0);
 5324             } else
 5325                 spawn(g_buf, NULL, *path, sel, 0);
 5326         } else
 5327             run_cmd_as_plugin(file, NULL, flags);
 5328 
 5329         close(wfd);
 5330         _exit(EXIT_SUCCESS);
 5331     }
 5332 
 5333     int rfd;
 5334 
 5335     do
 5336         rfd = open(g_pipepath, O_RDONLY);
 5337     while (rfd == -1 && errno == EINTR);
 5338 
 5339     nextpath = readpipe(rfd, &ctx, path);
 5340     if (nextpath)
 5341         set_smart_ctx(ctx, nextpath, path, runfile, lastname, lastdir);
 5342 
 5343     close(rfd);
 5344 
 5345     /* wait for the child to finish. no zombies allowed */
 5346     waitpid(p, NULL, 0);
 5347 
 5348     refresh();
 5349 
 5350     unlink(g_pipepath);
 5351 
 5352     return TRUE;
 5353 }
 5354 
 5355 static bool launch_app(char *newpath)
 5356 {
 5357     int r = F_NORMAL;
 5358     char *tmp = newpath;
 5359 
 5360     mkpath(plgpath, utils[UTIL_LAUNCH], newpath);
 5361 
 5362     if (!getutil(utils[UTIL_FZF]) || access(newpath, X_OK) < 0) {
 5363         tmp = xreadline(NULL, messages[MSG_APP_NAME]);
 5364         r = F_NOWAIT | F_NOTRACE | F_MULTI;
 5365     }
 5366 
 5367     if (tmp && *tmp) // NOLINT
 5368         spawn(tmp, (r == F_NORMAL) ? "0" : NULL, NULL, NULL, r);
 5369 
 5370     return FALSE;
 5371 }
 5372 
 5373 /* Returns TRUE if at least one command was run */
 5374 static bool prompt_run(void)
 5375 {
 5376     bool ret = FALSE;
 5377     char *cmdline, *next;
 5378     int cnt_j, cnt_J;
 5379     size_t len;
 5380 
 5381     const char *xargs_j = "xargs -0 -I{} %s < %s";
 5382     const char *xargs_J = "xargs -0 %s < %s";
 5383     char cmd[CMD_LEN_MAX + 32]; // 32 for xargs format strings
 5384 
 5385     while (1) {
 5386 #ifndef NORL
 5387         if (g_state.picker) {
 5388 #endif
 5389             cmdline = xreadline(NULL, PROMPT);
 5390 #ifndef NORL
 5391         } else
 5392             cmdline = getreadline("\n"PROMPT);
 5393 #endif
 5394         // Check for an empty command
 5395         if (!cmdline || !cmdline[0])
 5396             break;
 5397 
 5398         free(lastcmd);
 5399         lastcmd = xstrdup(cmdline);
 5400         ret = TRUE;
 5401 
 5402         len = xstrlen(cmdline);
 5403 
 5404         cnt_j = 0;
 5405         next = cmdline;
 5406         while ((next = strstr(next, "%j"))) {
 5407             ++cnt_j;
 5408 
 5409             // replace %j with {} for xargs later
 5410             next[0] = '{';
 5411             next[1] = '}';
 5412 
 5413             ++next;
 5414         }
 5415 
 5416         cnt_J = 0;
 5417         next = cmdline;
 5418         while ((next = strstr(next, "%J"))) {
 5419             ++cnt_J;
 5420 
 5421             // %J should be the last thing in the command
 5422             if (next == cmdline + len - 2) {
 5423                 cmdline[len - 2] = '\0';
 5424             }
 5425 
 5426             ++next;
 5427         }
 5428 
 5429         // We can't handle both %j and %J in a single command
 5430         if (cnt_j && cnt_J)
 5431             break;
 5432 
 5433         if (cnt_j)
 5434             snprintf(cmd, CMD_LEN_MAX + 32, xargs_j, cmdline, selpath);