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>
9 * Redistribution and use in source and binary forms, with or without
10 * modification, are permitted provided that the following conditions are met:
12 * * Redistributions of source code must retain the above copyright notice, this
13 * list of conditions and the following disclaimer.
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.
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.
31 #if defined(__linux__) || defined(MINGW) || defined(__MINGW32__) || defined(__MINGW64__) || defined(__CYGWIN__)
35 #if defined(__arm__) || defined(__i386__)
36 #define _FILE_OFFSET_BITS 64 /* Support large files on 32-bit */
38 #if defined(__linux__)
39 #include <sys/inotify.h>
42 #if !defined(__GLIBC__)
43 #include <sys/types.h>
46 #include <sys/resource.h>
48 #include <sys/statvfs.h>
49 #if defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__)
50 #include <sys/types.h>
51 #include <sys/event.h>
54 #elif defined(__HAIKU__)
55 #include "../misc/haiku/haiku_interop.h"
58 #include <sys/sysmacros.h>
62 #ifdef __linux__ /* Fix failure due to mvaddnwstr() */
63 #ifndef NCURSES_WIDECHAR
64 #define NCURSES_WIDECHAR 1
66 #elif defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__) || defined(__sun)
67 #ifndef _XOPEN_SOURCE_EXTENDED
68 #define _XOPEN_SOURCE_EXTENDED
71 #ifndef __USE_XOPEN /* Fix wcswidth() failure, ncursesw/curses.h includes whcar.h on Ubuntu 14.04 */
84 #include <readline/history.h>
85 #include <readline/readline.h>
99 #ifndef __USE_XOPEN_EXTENDED
100 #define __USE_XOPEN_EXTENDED 1
107 #if !defined(alloca) && defined(__GNUC__)
109 * GCC doesn't expand alloca() to __builtin_alloca() in standards mode
110 * (-std=...) and not all standard libraries do or supply it, e.g.
111 * NetBSD/arm64 so explicitly use the builtin.
113 #define alloca(size) __builtin_alloca(size)
119 #if defined(ICONS) || defined(NERD)
121 #define ICONS_ENABLED
128 /* Macro definitions */
129 #define VERSION "3.7"
130 #define GENERAL_INFO "BSD 2-Clause\nhttps://github.com/jarun/nnn"
133 #define SESSIONS_VERSION 1
137 #define S_BLKSIZE 512 /* S_BLKSIZE is missing on Android NDK (Termux) */
141 * NAME_MAX and PATH_MAX may not exist, e.g. with dirent.c_name being a
142 * flexible array on Illumos. Use somewhat accommodating fallback values.
149 #define PATH_MAX 4096
152 #define _ABSSUB(N, M) (((N) <= (M)) ? ((M) - (N)) : ((N) - (M)))
153 #define DOUBLECLICK_INTERVAL_NS (400000000)
154 #define XDELAY_INTERVAL_MS (350000) /* 350 ms delay */
155 #define ELEMENTS(x) (sizeof(x) / sizeof(*(x)))
157 #define MIN(x, y) ((x) < (y) ? (x) : (y))
159 #define MAX(x, y) ((x) > (y) ? (x) : (y))
160 #define ISODD(x) ((x) & 1)
161 #define ISBLANK(x) ((x) == ' ' || (x) == '\t')
162 #define TOUPPER(ch) (((ch) >= 'a' && (ch) <= 'z') ? ((ch) - 'a' + 'A') : (ch))
163 #define CMD_LEN_MAX (PATH_MAX + ((NAME_MAX + 1) << 1))
164 #define READLINE_MAX 256
171 #define ENTRY_INCR 64 /* Number of dir 'entry' structures to allocate per shot */
172 #define NAMEBUF_INCR 0x800 /* 64 dir entries at once, avg. 32 chars per file name = 64*32B = 2KB */
173 #define DESCRIPTOR_LEN 32
174 #define _ALIGNMENT 0x10 /* 16-byte alignment */
175 #define _ALIGNMENT_MASK 0xF
176 #define TMP_LEN_MAX 64
177 #define DOT_FILTER_LEN 7
178 #define ASCII_MAX 128
179 #define EXEC_ARGS_MAX 8
180 #define LIST_FILES_MAX (1 << 16)
195 #define MIN_DISPLAY_COLS ((CTX_MAX * 2) + 2) /* Two chars for [ and ] */
196 #define ARCHIVE_CMD_LEN 16
197 #define BLK_SHIFT_512 9
199 /* Detect hardlinks in du */
200 #define HASH_BITS (0xFFFFFF)
201 #define HASH_OCTETS (HASH_BITS >> 6) /* 2^6 = 64 */
204 #define DIR_OR_LINK_TO_DIR 0x01
205 #define HARD_LINK 0x02
206 #define SYM_ORPHAN 0x04
207 #define FILE_MISSING 0x08
208 #define FILE_SELECTED 0x10
210 /* Macros to define process spawn behaviour as flags */
211 #define F_NONE 0x00 /* no flag set */
212 #define F_MULTI 0x01 /* first arg can be combination of args; to be used with F_NORMAL */
213 #define F_NOWAIT 0x02 /* don't wait for child process (e.g. file manager) */
214 #define F_NOTRACE 0x04 /* suppress stdout and strerr (no traces) */
215 #define F_NORMAL 0x08 /* spawn child process in non-curses regular CLI mode */
216 #define F_CONFIRM 0x10 /* run command - show results before exit (must have F_NORMAL) */
217 #define F_CHKRTN 0x20 /* wait for user prompt if cmd returns failure status */
218 #define F_NOSTDIN 0x40 /* suppress stdin */
219 #define F_CLI (F_NORMAL | F_MULTI)
220 #define F_SILENT (F_CLI | F_NOTRACE)
222 /* Version compare macros */
224 * states: S_N: normal, S_I: comparing integral part, S_F: comparing
225 * fractional parts, S_Z: idem but with leading Zeroes only
232 /* result_type: VCMP: return diff; VLEN: compare using len_diff/diff */
240 /* TYPE DEFINITIONS */
241 typedef unsigned int uint_t;
242 typedef unsigned char uchar_t;
243 typedef unsigned short ushort_t;
244 typedef unsigned long long ulong_t;
248 /* Directory entry */
249 typedef struct entry {
253 blkcnt_t blocks; /* number of 512B blocks allocated */
259 ushort_t nlen; /* Length of file name */
260 uchar_t flags; /* Flags specific to the file */
263 /* Key-value pairs from env */
273 const regex_t *regex;
280 * NOTE: update default values if changing order
283 uint_t filtermode : 1; /* Set to enter filter mode */
284 uint_t timeorder : 1; /* Set to sort by time */
285 uint_t sizeorder : 1; /* Set to sort by file size */
286 uint_t apparentsz : 1; /* Set to sort by apparent size (disk usage) */
287 uint_t blkorder : 1; /* Set to sort by blocks used (disk usage) */
288 uint_t extnorder : 1; /* Order by extension */
289 uint_t showhidden : 1; /* Set to show hidden files */
290 uint_t reserved0 : 1;
291 uint_t showdetail : 1; /* Clear to show lesser file info */
292 uint_t ctxactive : 1; /* Context active or not */
293 uint_t reverse : 1; /* Reverse sort */
294 uint_t version : 1; /* Version sort */
295 uint_t reserved1 : 1;
296 /* The following settings are global */
297 uint_t curctx : 3; /* Current context number */
298 uint_t prefersel : 1; /* Prefer selection over current, if exists */
299 uint_t reserved2 : 1;
300 uint_t nonavopen : 1; /* Open file on right arrow or `l` */
301 uint_t autoselect : 1; /* Auto-select dir in type-to-nav mode */
302 uint_t cursormode : 1; /* Move hardware cursor with selection */
303 uint_t useeditor : 1; /* Use VISUAL to open text files */
304 uint_t reserved3 : 3;
305 uint_t regex : 1; /* Use regex filters */
306 uint_t x11 : 1; /* Copy to system clipboard and show notis */
307 uint_t timetype : 2; /* Time sort type (0: access, 1: change, 2: modification) */
308 uint_t cliopener : 1; /* All-CLI app opener */
309 uint_t waitedit : 1; /* For ops that can't be detached, used EDITOR */
310 uint_t rollover : 1; /* Roll over at edges */
313 /* Non-persistent program-internal states */
315 uint_t pluginit : 1; /* Plugin framework initialized */
316 uint_t interrupt : 1; /* Program received an interrupt */
317 uint_t rangesel : 1; /* Range selection on */
318 uint_t move : 1; /* Move operation */
319 uint_t autonext : 1; /* Auto-proceed on open */
320 uint_t fortune : 1; /* Show fortune messages in help */
321 uint_t trash : 2; /* Use trash to delete files 1: trash-cli, 2: gio trash */
322 uint_t forcequit : 1; /* Do not prompt on quit */
323 uint_t autofifo : 1; /* Auto-create NNN_FIFO */
324 uint_t initfile : 1; /* Positional arg is a file */
325 uint_t dircolor : 1; /* Current status of dir color */
326 uint_t picker : 1; /* Write selection to user-specified file */
327 uint_t pickraw : 1; /* Write selection to stdout before exit */
328 uint_t runplugin : 1; /* Choose plugin mode */
329 uint_t runctx : 3; /* The context in which plugin is to be run */
330 uint_t selmode : 1; /* Set when selecting files */
331 uint_t oldcolor : 1; /* Use older colorscheme */
332 uint_t stayonsel : 1; /* Disable auto-proceed on select */
333 uint_t dirctx : 1; /* Show dirs in context color */
334 uint_t uidgid : 1; /* Show owner and group info */
335 uint_t reserved : 9; /* Adjust when adding/removing a field */
338 /* Contexts or workspaces */
340 char c_path[PATH_MAX]; /* Current dir */
341 char c_last[PATH_MAX]; /* Last visited dir */
342 char c_name[NAME_MAX + 1]; /* Current file name */
343 char c_fltr[REGEX_MAX]; /* Current filter */
344 settings c_cfg; /* Current configuration */
345 uint_t color; /* Color code for directories */
351 size_t pathln[CTX_MAX];
352 size_t lastln[CTX_MAX];
353 size_t nameln[CTX_MAX];
354 size_t fltrln[CTX_MAX];
360 /* Configuration, contexts */
361 static settings cfg = {
385 2, /* timetype (T_MOD) */
391 static context g_ctx[CTX_MAX] __attribute__ ((aligned));
393 static int ndents, cur, last, curscroll, last_curscroll, total_dents = ENTRY_INCR, scroll_lines = 1;
394 static int nselected;
396 static int fifofd = -1;
398 static uint_t idletimeout, selbufpos, lastappendpos, selbuflen;
399 static ushort_t xlines, xcols;
400 static ushort_t idle;
401 static uchar_t maxbm, maxplug;
403 static char *pluginstr;
406 static char *enveditor;
410 static char *initpath;
411 static char *cfgpath;
412 static char *selpath;
413 static char *listpath;
414 static char *listroot;
415 static char *plgpath;
416 static char *pnamebuf, *pselbuf;
419 static char *fifopath;
421 static unsigned long long *ihashbmp;
422 static struct entry *pdents;
423 static blkcnt_t ent_blocks;
424 static blkcnt_t dir_blocks;
425 static ulong_t num_files;
428 static uchar_t tmpfplen, homelen;
429 static uchar_t blk_shift = BLK_SHIFT_512;
431 static int middle_click_key;
434 static pcre *archive_pcre;
436 static regex_t archive_re;
439 /* Retain old signal handlers */
440 static struct sigaction oldsighup;
441 static struct sigaction oldsigtstp;
443 /* For use in functions which are isolated and don't return the buffer */
444 static char g_buf[CMD_LEN_MAX] __attribute__ ((aligned));
446 /* Buffer to store tmp file path to show selection, file stats and help */
447 static char g_tmpfpath[TMP_LEN_MAX] __attribute__ ((aligned));
449 /* Buffer to store plugins control pipe location */
450 static char g_pipepath[TMP_LEN_MAX] __attribute__ ((aligned));
452 /* Non-persistent runtime states */
453 static runstate g_state;
455 /* Options to identify file MIME */
456 #if defined(__APPLE__)
457 #define FILE_MIME_OPTS "-bIL"
458 #elif !defined(__sun) /* no MIME option for 'file' */
459 #define FILE_MIME_OPTS "-biL"
462 /* Macros for utilities */
463 #define UTIL_OPENER 0
465 #define UTIL_BSDTAR 2
468 #define UTIL_LOCKER 5
469 #define UTIL_LAUNCH 6
470 #define UTIL_SH_EXEC 7
473 #define UTIL_RCLONE 10
482 /* Utilities to open files, run actions */
483 static char * const utils[] = {
486 #elif defined __CYGWIN__
488 #elif defined __HAIKU__
499 #elif defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__)
501 #elif defined __HAIKU__
521 #define MSG_ZERO 0 /* Unused */
522 #define MSG_0_ENTRIES 1
523 #define STR_TMPFILE 2
524 #define MSG_0_SELECTED 3
527 #define MSG_SSN_NAME 6
528 #define MSG_CP_MV_AS 7
529 #define MSG_CUR_SEL_OPTS 8
530 #define MSG_FORCE_RM 9
532 #define MSG_NEW_OPTS 11
533 #define MSG_CLI_MODE 12
534 #define MSG_OVERWRITE 13
535 #define MSG_SSN_OPTS 14
536 #define MSG_QUIT_ALL 15
537 #define MSG_HOSTNAME 16
538 #define MSG_ARCHIVE_NAME 17
539 #define MSG_OPEN_WITH 18
540 #define MSG_NEW_PATH 19
541 #define MSG_LINK_PREFIX 20
542 #define MSG_COPY_NAME 21
543 #define MSG_CONTINUE 22
544 #define MSG_SEL_MISSING 23
545 #define MSG_ACCESS 24
546 #define MSG_EMPTY_FILE 25
547 #define MSG_UNSUPPORTED 26
548 #define MSG_NOT_SET 27
549 #define MSG_EXISTS 28
550 #define MSG_FEW_COLUMNS 29
551 #define MSG_REMOTE_OPTS 30
552 #define MSG_RCLONE_DELAY 31
553 #define MSG_APP_NAME 32
554 #define MSG_ARCHIVE_OPTS 33
555 #define MSG_PLUGIN_KEYS 34
556 #define MSG_BOOKMARK_KEYS 35
557 #define MSG_INVALID_REG 36
561 #define MSG_RM_TMP 40
562 #define MSG_INVALID_KEY 41
563 #define MSG_NOCHANGE 42
564 #ifndef DIR_LIMITED_SELECTION
565 #define MSG_DIR_CHANGED 43 /* Must be the last entry */
568 static const char * const messages[] = {
577 "'c'urrent / 's'el?",
578 "rm -rf %s file%s? [Esc cancels]",
580 "'f'ile / 'd'ir / 's'ym / 'h'ard?",
583 "'s'ave / 'l'oad / 'r'estore?",
584 "Quit all contexts?",
585 "remote name ('-' for hovered): ",
586 "archive [path/]name: ",
589 "link prefix [@ for none]: ",
590 "copy [path/]name: ",
591 "\n'Enter' to continue",
594 "empty: edit/open with",
599 "'s'shfs / 'r'clone?",
602 "'d'efault / e'x'tract / 'l'ist / 'm'ount?",
606 "'a'u / 'd'u / 'e'xtn / 'r'ev / 's'ize / 't'ime / 'v'er / 'c'lr / '^T' (cycle)?",
607 "unmount failed! try lazy?",
608 "first file (\')/char?",
612 #ifndef DIR_LIMITED_SELECTION
613 "dir changed, range sel off", /* Must be the last entry */
617 /* Supported configuration environment variables */
627 #define NNN_ARCHIVE 9 /* strings end here */
628 #define NNN_TRASH 10 /* flags begin here */
630 static const char * const env_cfg[] = {
644 /* Required environment variables */
651 static const char * const envs[] = {
665 static char cp[] = "cp -iRp";
666 static char mv[] = "mv -i";
668 static char cp[] = "cp -iRp";
669 static char mv[] = "mv -i";
672 /* Archive commands */
673 const char *archive_cmd[] = {"atool -a", "bsdtar -acvf", "zip -r", "tar -acvf"};
675 /* Tokens used for path creation */
680 static const char * const toks[] = {
683 "plugins", /* must be the last entry */
692 static const char * const patterns[] = {
693 SED" -i 's|^\\(\\(.*/\\)\\(.*\\)$\\)|#\\1\\n\\3|' %s",
694 SED" 's|^\\([^#/][^/]\\?.*\\)$|%s/\\1|;s|^#\\(/.*\\)$|\\1|' "
695 "%s | tr '\\n' '\\0' | xargs -0 -n2 sh -c '%s \"$0\" \"$@\" < /dev/tty'",
696 "\\.(bz|bz2|gz|tar|taz|tbz|tbz2|tgz|z|zip)$",
697 SED" -i 's|^%s\\(.*\\)$|%s\\1|' %s",
701 #define C_BLK (CTX_MAX + 1) /* Block device: DarkSeaGreen1 */
702 #define C_CHR (C_BLK + 1) /* Character device: Yellow1 */
703 #define C_DIR (C_CHR + 1) /* Directory: DeepSkyBlue1 */
704 #define C_EXE (C_DIR + 1) /* Executable file: Green1 */
705 #define C_FIL (C_EXE + 1) /* Regular file: Normal */
706 #define C_HRD (C_FIL + 1) /* Hard link: Plum4 */
707 #define C_LNK (C_HRD + 1) /* Symbolic link: Cyan1 */
708 #define C_MIS (C_LNK + 1) /* Missing file OR file details: Grey62 */
709 #define C_ORP (C_MIS + 1) /* Orphaned symlink: DeepPink1 */
710 #define C_PIP (C_ORP + 1) /* Named pipe (FIFO): Orange1 */
711 #define C_SOC (C_PIP + 1) /* Socket: MediumOrchid1 */
712 #define C_UND (C_SOC + 1) /* Unknown OR 0B regular/exe file: Red1 */
715 /* 0-9, A-Z, OTHER = 36. */
716 static ushort_t icon_positions[37];
719 static char gcolors[] = "c1e2272e006033f7c6d6abc4";
720 static uint_t fcolors[C_UND + 1] = {0};
724 #define NUM_EVENT_SLOTS 32 /* Make room for 32 events */
725 #define EVENT_SIZE (sizeof(struct inotify_event))
726 #define EVENT_BUF_LEN (EVENT_SIZE * NUM_EVENT_SLOTS)
727 static int inotify_fd, inotify_wd = -1;
728 static uint_t INOTIFY_MASK = /* IN_ATTRIB | */ IN_CREATE | IN_DELETE | IN_DELETE_SELF
729 | IN_MODIFY | IN_MOVE_SELF | IN_MOVED_FROM | IN_MOVED_TO;
730 #elif defined(BSD_KQUEUE)
731 #define NUM_EVENT_SLOTS 1
732 #define NUM_EVENT_FDS 1
733 static int kq, event_fd = -1;
734 static struct kevent events_to_monitor[NUM_EVENT_FDS];
735 static uint_t KQUEUE_FFLAGS = NOTE_DELETE | NOTE_EXTEND | NOTE_LINK
736 | NOTE_RENAME | NOTE_REVOKE | NOTE_WRITE;
737 static struct timespec gtimeout;
738 #elif defined(HAIKU_NM)
739 static bool haiku_nm_active = FALSE;
740 static haiku_nm_h haiku_hnd;
743 /* Function macros */
744 #define tolastln() move(xlines - 1, 0)
745 #define tocursor() move(cur + 2 - curscroll, 0)
746 #define exitcurses() endwin()
747 #define printwarn(presel) printwait(strerror(errno), presel)
748 #define istopdir(path) ((path)[1] == '\0' && (path)[0] == '/')
749 #define copycurname() xstrsncpy(lastname, pdents[cur].name, NAME_MAX + 1)
750 #define settimeout() timeout(1000)
751 #define cleartimeout() timeout(-1)
752 #define errexit() printerr(__LINE__)
753 #define setdirwatch() (cfg.filtermode ? (presel = FILTER) : (watch = TRUE))
754 #define filterset() (g_ctx[cfg.curctx].c_fltr[1])
755 /* We don't care about the return value from strcmp() */
756 #define xstrcmp(a, b) (*(a) != *(b) ? -1 : strcmp((a), (b)))
757 /* A faster version of xisdigit */
758 #define xisdigit(c) ((unsigned int) (c) - '0' <= 9)
759 #define xerror() perror(xitoa(__LINE__))
762 #define ENTLESS(i, j) (entrycmpfn(pdents + (i), pdents + (j)) < 0)
763 #define ENTSWAP(i, j) (swap_ent((i), (j)))
764 #define ENTSORT(pdents, ndents, entrycmpfn) QSORT((ndents), ENTLESS, ENTSWAP)
766 #define ENTSORT(pdents, ndents, entrycmpfn) qsort((pdents), (ndents), sizeof(*(pdents)), (entrycmpfn))
770 #define UNUSED(x) UNUSED_##x __attribute__((__unused__))
772 #define UNUSED(x) UNUSED_##x
773 #endif /* __GNUC__ */
775 /* Forward declarations */
776 static void redraw(char *path);
777 static int spawn(char *file, char *arg1, char *arg2, uchar_t flag);
778 static int (*nftw_fn)(const char *fpath, const struct stat *sb, int typeflag, struct FTW *ftwbuf);
779 static void move_cursor(int target, int ignore_scrolloff);
780 static char *load_input(int fd, const char *path);
781 static int set_sort_flags(int r);
782 static void (*printptr)(const struct entry *ent, uint_t namecols, bool sel);
783 static void printent_long(const struct entry *ent, uint_t namecols, bool sel);
785 static void notify_fifo(bool force);
790 static void sigint_handler(int UNUSED(sig))
792 g_state.interrupt = 1;
795 static void clean_exit_sighandler(int UNUSED(sig))
798 /* This triggers cleanup() thanks to atexit() */
802 static char *xitoa(uint_t val)
804 static char dst[32] = {'\0'};
805 static const char digits[201] =
806 "0001020304050607080910111213141516171819"
807 "2021222324252627282930313233343536373839"
808 "4041424344454647484950515253545556575859"
809 "6061626364656667686970717273747576777879"
810 "8081828384858687888990919293949596979899";
811 uint_t next = 30, quo, i;
815 i = (val - (quo * 100)) * 2;
817 dst[next] = digits[i + 1];
818 dst[--next] = digits[i];
822 /* Handle last 1-2 digits */
824 dst[next] = '0' + val;
827 dst[next] = digits[i + 1];
828 dst[--next] = digits[i];
834 /* Return the integer value of a char representing HEX */
835 static uchar_t xchartohex(uchar_t c)
840 if (c >= 'a' && c <= 'f')
843 if (c >= 'A' && c <= 'F')
850 * Source: https://elixir.bootlin.com/linux/latest/source/arch/alpha/include/asm/bitops.h
852 static bool test_set_bit(uint_t nr)
856 unsigned long long *m = ((unsigned long long *)ihashbmp) + (nr >> 6);
858 if (*m & (1 << (nr & 63)))
861 *m |= 1 << (nr & 63);
867 /* Increase the limit on open file descriptors, if possible */
868 static rlim_t max_openfds(void)
871 rlim_t limit = getrlimit(RLIMIT_NOFILE, &rl);
875 rl.rlim_cur = rl.rlim_max;
877 /* Return ~75% of max possible */
878 if (setrlimit(RLIMIT_NOFILE, &rl) == 0) {
879 limit = rl.rlim_max - (rl.rlim_max >> 2);
881 * 20K is arbitrary. If the limit is set to max possible
882 * value, the memory usage increases to more than double.
895 * Wrapper to realloc()
896 * Frees current memory if realloc() fails and returns NULL.
898 * As per the docs, the *alloc() family is supposed to be memory aligned:
899 * Ubuntu: https://manpages.ubuntu.com/manpages/xenial/man3/malloc.3.html
900 * macOS: https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man3/malloc.3.html
902 static void *xrealloc(void *pcur, size_t len)
904 void *pmem = realloc(pcur, len);
913 * Just a safe strncpy(3)
914 * Always null ('\0') terminates if both src and dest are valid pointers.
915 * Returns the number of bytes copied including terminating null byte.
917 static size_t xstrsncpy(char *restrict dst, const char *restrict src, size_t n)
919 char *end = memccpy(dst, src, '\0', n);
922 dst[n - 1] = '\0'; // NOLINT
923 end = dst + n; /* If we return n here, binary size increases due to auto-inlining */
929 static inline size_t xstrlen(const char *restrict s)
931 #if !defined(__GLIBC__)
932 return strlen(s); // NOLINT
934 return (char *)rawmemchr(s, '\0') - s; // NOLINT
938 static char *xstrdup(const char *restrict s)
940 size_t len = xstrlen(s) + 1;
941 char *ptr = malloc(len);
944 xstrsncpy(ptr, s, len);
948 static bool is_suffix(const char *restrict str, const char *restrict suffix)
953 size_t lenstr = xstrlen(str);
954 size_t lensuffix = xstrlen(suffix);
956 if (lensuffix > lenstr)
959 return (xstrcmp(str + (lenstr - lensuffix), suffix) == 0);
962 static bool is_prefix(const char *restrict str, const char *restrict prefix, size_t len)
964 return !strncmp(str, prefix, len);
968 * The poor man's implementation of memrchr(3).
969 * We are only looking for '/' in this program.
970 * And we are NOT expecting a '/' at the end.
971 * Ideally 0 < n <= xstrlen(s).
973 static void *xmemrchr(uchar_t *restrict s, uchar_t ch, size_t n)
975 #if defined(__GLIBC__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__)
976 return memrchr(s, ch, n);
982 uchar_t *ptr = s + n;
993 /* A very simplified implementation, changes path */
994 static char *xdirname(char *path)
996 char *base = xmemrchr((uchar_t *)path, '/', xstrlen(path));
1006 static char *xbasename(char *path)
1008 char *base = xmemrchr((uchar_t *)path, '/', xstrlen(path)); // NOLINT
1010 return base ? base + 1 : path;
1013 static char *xextension(const char *fname, size_t len)
1015 return xmemrchr((uchar_t *)fname, '.', len);
1018 static inline bool getutil(char *util)
1020 return spawn("which", util, NULL, F_NORMAL | F_NOTRACE) == 0;
1024 * Updates out with "dir/name or "/name"
1025 * Returns the number of bytes copied including the terminating NULL byte
1027 * Note: dir and out must be PATH_MAX in length to avoid macOS fault
1029 static size_t mkpath(const char *dir, const char *name, char *out)
1033 /* Handle absolute path */
1034 if (name[0] == '/') // NOLINT
1035 return xstrsncpy(out, name, PATH_MAX);
1037 /* Handle root case */
1041 len = xstrsncpy(out, dir, PATH_MAX);
1043 out[len - 1] = '/'; // NOLINT
1044 return (xstrsncpy(out + len, name, PATH_MAX - len) + len);
1047 /* Assumes both the paths passed are directories */
1048 static char *common_prefix(const char *path, char *prefix)
1050 const char *x = path, *y = prefix;
1053 if (!path || !*path || !prefix)
1057 xstrsncpy(prefix, path, PATH_MAX);
1061 while (*x && *y && (*x == *y))
1064 /* Strings are same */
1068 /* Path is shorter */
1069 if (!*x && *y == '/') {
1070 xstrsncpy(prefix, path, y - path);
1074 /* Prefix is shorter */
1075 if (!*y && *x == '/')
1078 /* Shorten prefix */
1079 prefix[y - prefix] = '\0';
1081 sep = xmemrchr((uchar_t *)prefix, '/', y - prefix);
1091 * The library function realpath() resolves symlinks.
1092 * If there's a symlink in file list we want to show the symlink not what it's points to.
1094 static char *abspath(const char *path, const char *cwd)
1099 size_t dst_size = 0, src_size = xstrlen(path), cwd_size = xstrlen(cwd);
1100 size_t len = src_size;
1104 * We need to add 2 chars at the end as relative paths may start with:
1106 * no separator (fd .): this needs an additional char for '/'
1108 char *resolved_path = malloc(src_size + (*path == '/' ? 0 : cwd_size) + 2);
1113 /* Turn relative paths into absolute */
1115 dst_size = xstrsncpy(resolved_path, cwd, cwd_size + 1) - 1;
1117 resolved_path[0] = '\0';
1120 dst = resolved_path + dst_size;
1121 for (const char *next = NULL; next != path + src_size;) {
1122 next = memchr(src, '/', len);
1124 next = path + src_size;
1126 if (next - src == 2 && src[0] == '.' && src[1] == '.') {
1127 if (dst - resolved_path) {
1128 dst = xmemrchr((uchar_t *)resolved_path, '/', dst - resolved_path);
1131 } else if (next - src == 1 && src[0] == '.') {
1133 } else if (next - src) {
1135 xstrsncpy(dst, src, next - src + 1);
1140 len = src_size - (src - path);
1143 if (*resolved_path == '\0') {
1144 resolved_path[0] = '/';
1145 resolved_path[1] = '\0';
1148 return resolved_path;
1151 static bool set_tilde_in_path(char *path)
1153 if (is_prefix(path, home, homelen)) {
1154 home[homelen] = path[homelen - 1];
1155 path[homelen - 1] = '~';
1162 static void reset_tilde_in_path(char *path)
1164 path[homelen - 1] = home[homelen];
1165 home[homelen] = '\0';
1168 static int create_tmp_file(void)
1170 xstrsncpy(g_tmpfpath + tmpfplen - 1, messages[STR_TMPFILE], TMP_LEN_MAX - tmpfplen);
1172 int fd = mkstemp(g_tmpfpath);
1175 DPRINTF_S(strerror(errno));
1181 static void clearinfoln(void)
1183 move(xlines - 2, 0);
1188 /* Clear the old prompt */
1189 static void clearoldprompt(void)
1197 /* Messages show up at the bottom */
1198 static inline void printmsg_nc(const char *msg)
1205 static void printmsg(const char *msg)
1207 attron(COLOR_PAIR(cfg.curctx + 1));
1209 attroff(COLOR_PAIR(cfg.curctx + 1));
1212 static void printwait(const char *msg, int *presel)
1218 xstrsncpy(g_ctx[cfg.curctx].c_name, pdents[cur].name, NAME_MAX + 1);
1222 /* Kill curses and display error before exiting */
1223 static void printerr(int linenum)
1226 perror(xitoa(linenum));
1227 if (!g_state.picker && selpath)
1233 static inline bool xconfirm(int c)
1235 return (c == 'y' || c == 'Y');
1238 static int get_input(const char *prompt)
1247 while (r == KEY_RESIZE) {
1261 static int get_cur_or_sel(void)
1263 if (selbufpos && ndents) {
1267 int choice = get_input(messages[MSG_CUR_SEL_OPTS]);
1269 return ((choice == 'c' || choice == 's') ? choice : 0);
1281 static void xdelay(useconds_t delay)
1287 static char confirm_force(bool selection)
1291 snprintf(str, 64, messages[MSG_FORCE_RM],
1292 (selection ? xitoa(nselected) : "current"), (selection ? "(s)" : ""));
1294 int r = get_input(str);
1297 return '\0'; /* cancel */
1298 if (r == 'y' || r == 'Y')
1299 return 'f'; /* forceful */
1300 return 'i'; /* interactive */
1303 /* Writes buflen char(s) from buf to a file */
1304 static void writesel(const char *buf, const size_t buflen)
1306 if (g_state.pickraw || !selpath)
1309 FILE *fp = fopen(selpath, "w");
1312 if (fwrite(buf, 1, buflen, fp) != buflen)
1319 static void appendfpath(const char *path, const size_t len)
1321 if ((selbufpos >= selbuflen) || ((len + 3) > (selbuflen - selbufpos))) {
1322 selbuflen += PATH_MAX;
1323 pselbuf = xrealloc(pselbuf, selbuflen);
1328 selbufpos += xstrsncpy(pselbuf + selbufpos, path, len);
1331 /* Write selected file paths to fd, linefeed separated */
1332 static size_t seltofile(int fd, uint_t *pcount)
1334 uint_t lastpos, count = 0;
1335 char *pbuf = pselbuf;
1337 ssize_t len, prefixlen = 0, initlen = 0;
1345 lastpos = selbufpos - 1;
1348 prefixlen = (ssize_t)xstrlen(listroot);
1349 initlen = (ssize_t)xstrlen(listpath);
1352 while (pos <= lastpos) {
1354 len = (ssize_t)xstrlen(pbuf);
1356 if (!listpath || !is_prefix(pbuf, listpath, initlen)) {
1357 if (write(fd, pbuf, len) != len)
1360 if (write(fd, listroot, prefixlen) != prefixlen)
1362 if (write(fd, pbuf + initlen, len - initlen) != (len - initlen))
1367 if (pos <= lastpos) {
1368 if (write(fd, "\n", 1) != 1)
1382 static bool isselfileempty(void)
1386 return (stat(selpath, &sb) == -1) || (!sb.st_size);
1389 /* List selection from selection file (another instance) */
1390 static bool listselfile(void)
1392 if (isselfileempty())
1395 snprintf(g_buf, CMD_LEN_MAX, "tr \'\\0\' \'\\n\' < %s", selpath);
1396 spawn(utils[UTIL_SH_EXEC], g_buf, NULL, F_CLI | F_CONFIRM);
1401 /* Reset selection indicators */
1402 static void resetselind(void)
1404 for (int r = 0; r < ndents; ++r)
1405 if (pdents[r].flags & FILE_SELECTED)
1406 pdents[r].flags &= ~FILE_SELECTED;
1409 static void startselection(void)
1411 if (!g_state.selmode) {
1412 g_state.selmode = 1;
1425 static void updateselbuf(const char *path, char *newpath)
1429 for (int i = 0; i < ndents; ++i)
1430 if (pdents[i].flags & FILE_SELECTED) {
1431 r = mkpath(path, pdents[i].name, newpath);
1432 appendfpath(newpath, r);
1436 /* Finish selection procedure before an operation */
1437 static void endselection(void)
1441 char buf[sizeof(patterns[P_REPLACE]) + PATH_MAX + (TMP_LEN_MAX << 1)];
1443 if (g_state.selmode)
1444 g_state.selmode = 0;
1446 if (!listpath || !selbufpos)
1449 fd = create_tmp_file();
1451 DPRINTF_S("couldn't create tmp file");
1455 seltofile(fd, NULL);
1457 DPRINTF_S(strerror(errno));
1462 snprintf(buf, sizeof(buf), patterns[P_REPLACE], listpath, listroot, g_tmpfpath);
1463 spawn(utils[UTIL_SH_EXEC], buf, NULL, F_CLI);
1465 fd = open(g_tmpfpath, O_RDONLY);
1467 DPRINTF_S(strerror(errno));
1469 if (unlink(g_tmpfpath)) {
1470 DPRINTF_S(strerror(errno));
1476 count = read(fd, pselbuf, selbuflen);
1478 DPRINTF_S(strerror(errno));
1480 if (close(fd) || unlink(g_tmpfpath)) {
1481 DPRINTF_S(strerror(errno));
1486 if (close(fd) || unlink(g_tmpfpath)) {
1487 DPRINTF_S(strerror(errno));
1493 pselbuf[--count] = '\0';
1494 for (--count; count > 0; --count)
1495 if (pselbuf[count] == '\n' && pselbuf[count+1] == '/')
1496 pselbuf[count] = '\0';
1498 writesel(pselbuf, selbufpos - 1);
1501 static void clearselection(void)
1505 g_state.selmode = 0;
1509 /* Returns: 1 - success, 0 - none selected, -1 - other failure */
1510 static int editselection(void)
1519 return listselfile();
1521 fd = create_tmp_file();
1523 DPRINTF_S("couldn't create tmp file");
1527 seltofile(fd, NULL);
1529 DPRINTF_S(strerror(errno));
1533 /* Save the last modification time */
1534 if (stat(g_tmpfpath, &sb)) {
1535 DPRINTF_S(strerror(errno));
1539 mtime = sb.st_mtime;
1541 spawn((cfg.waitedit ? enveditor : editor), g_tmpfpath, NULL, F_CLI);
1543 fd = open(g_tmpfpath, O_RDONLY);
1545 DPRINTF_S(strerror(errno));
1552 if (mtime == sb.st_mtime) {
1553 DPRINTF_S("selection is not modified");
1558 if (sb.st_size > selbufpos) {
1559 DPRINTF_S("edited buffer larger than previous");
1564 count = read(fd, pselbuf, selbuflen);
1566 DPRINTF_S(strerror(errno));
1568 if (close(fd) || unlink(g_tmpfpath)) {
1569 DPRINTF_S(strerror(errno));
1575 if (close(fd) || unlink(g_tmpfpath)) {
1576 DPRINTF_S(strerror(errno));
1588 /* The last character should be '\n' */
1589 pselbuf[--count] = '\0';
1590 for (--count; count > 0; --count) {
1591 /* Replace every '\n' that separates two paths */
1592 if (pselbuf[count] == '\n' && pselbuf[count + 1] == '/') {
1594 pselbuf[count] = '\0';
1598 /* Add a line for the last file */
1601 if (lines > nselected) {
1602 DPRINTF_S("files added to selection");
1607 writesel(pselbuf, selbufpos - 1);
1617 static bool selsafe(void)
1619 /* Fail if selection file path not generated */
1621 printmsg(messages[MSG_SEL_MISSING]);
1625 /* Fail if selection file path isn't accessible */
1626 if (access(selpath, R_OK | W_OK) == -1) {
1627 errno == ENOENT ? printmsg(messages[MSG_0_SELECTED]) : printwarn(NULL);
1634 static void export_file_list(void)
1639 struct entry *pdent = pdents;
1640 int fd = create_tmp_file();
1643 DPRINTF_S(strerror(errno));
1647 for (int r = 0; r < ndents; ++pdent, ++r) {
1648 if (write(fd, pdent->name, pdent->nlen - 1) != (pdent->nlen - 1))
1651 if ((r != ndents - 1) && (write(fd, "\n", 1) != 1))
1656 DPRINTF_S(strerror(errno));
1659 spawn(editor, g_tmpfpath, NULL, F_CLI);
1661 if (xconfirm(get_input(messages[MSG_RM_TMP])))
1665 static bool init_fcolors(void)
1667 char *f_colors = getenv("NNN_FCOLORS");
1669 if (!f_colors || !*f_colors)
1672 for (uchar_t id = C_BLK; *f_colors && id <= C_UND; ++id) {
1673 fcolors[id] = xchartohex(*f_colors) << 4;
1675 fcolors[id] += xchartohex(*f_colors);
1677 init_pair(id, fcolors[id], -1);
1686 /* Initialize curses mode */
1687 static bool initcurses(void *oldmask)
1693 if (g_state.picker) {
1694 if (!newterm(NULL, stderr, stdin)) {
1695 fprintf(stderr, "newterm!\n");
1698 } else if (!initscr()) {
1699 fprintf(stderr, "initscr!\n");
1700 DPRINTF_S(getenv("TERM"));
1707 //intrflush(stdscr, FALSE);
1708 keypad(stdscr, TRUE);
1710 #if NCURSES_MOUSE_VERSION <= 1
1711 mousemask(BUTTON1_PRESSED | BUTTON1_DOUBLE_CLICKED | BUTTON2_PRESSED | BUTTON3_PRESSED,
1712 (mmask_t *)oldmask);
1714 mousemask(BUTTON1_PRESSED | BUTTON2_PRESSED | BUTTON3_PRESSED | BUTTON4_PRESSED | BUTTON5_PRESSED,
1715 (mmask_t *)oldmask);
1719 curs_set(FALSE); /* Hide cursor */
1721 char *colors = getenv(env_cfg[NNN_COLORS]);
1723 if (colors || !getenv("NO_COLOR")) {
1728 use_default_colors();
1730 /* Initialize file colors */
1731 if (COLORS >= 256) {
1732 if (!(g_state.oldcolor || init_fcolors())) {
1734 fprintf(stderr, "NNN_FCOLORS!\n");
1738 g_state.oldcolor = 1;
1741 DPRINTF_D(COLOR_PAIRS);
1743 if (colors && *colors == '#') {
1744 char *sep = strchr(colors, ';');
1746 if (!g_state.oldcolor && COLORS >= 256) {
1751 * If fallback colors are specified, set the separator
1752 * to NULL so we don't interpret separator and fallback
1753 * if fewer than CTX_MAX xterm 256 colors are specified.
1758 colors = sep; /* Detect if 8 colors fallback is appended */
1764 /* Get and set the context colors */
1765 for (uchar_t i = 0; i < CTX_MAX; ++i) {
1766 pcode = &g_ctx[i].color;
1768 if (colors && *colors) {
1770 *pcode = xchartohex(*colors) << 4;
1772 fcolors[i + 1] = *pcode += xchartohex(*colors);
1773 else { /* Each color code must be 2 hex symbols */
1775 fprintf(stderr, "NNN_COLORS!\n");
1779 *pcode = (*colors < '0' || *colors > '7') ? 4 : *colors - '0';
1784 init_pair(i + 1, *pcode, -1);
1788 #ifdef ICONS_ENABLED
1789 if (!g_state.oldcolor) {
1790 uchar_t icolors[256] = {0};
1793 memset(icon_positions, 0x7f, sizeof(icon_positions));
1795 for (uint_t i = 0; i < sizeof(icons_ext)/sizeof(struct icon_pair); ++i) {
1796 c = TOUPPER(icons_ext[i].match[0]);
1797 if (c >= 'A' && c <= 'Z') {
1798 if (icon_positions[c - 'A' + 10] == 0x7f7f)
1799 icon_positions[c - 'A' + 10] = i;
1800 } else if (c >= '0' && c <= '9') {
1801 if (icon_positions[c - '0'] == 0x7f7f)
1802 icon_positions[c - '0'] = i;
1803 } else if (icon_positions[36] == 0x7f7f)
1804 icon_positions[36] = i;
1806 if (icons_ext[i].color && !icolors[icons_ext[i].color]) {
1807 init_pair(C_UND + 1 + icons_ext[i].color, icons_ext[i].color, -1);
1808 icolors[icons_ext[i].color] = 1;
1814 settimeout(); /* One second */
1819 /* No NULL check here as spawn() guards against it */
1820 static int parseargs(char *line, char **argv)
1824 argv[count++] = line;
1826 while (*line) { // NOLINT
1827 if (ISBLANK(*line)) {
1830 if (!*line) // NOLINT
1833 argv[count++] = line;
1834 if (count == EXEC_ARGS_MAX)
1844 static pid_t xfork(uchar_t flag)
1848 struct sigaction dfl_act = {.sa_handler = SIG_DFL};
1851 /* the parent ignores the interrupt, quit and hangup signals */
1852 sigaction(SIGHUP, &(struct sigaction){.sa_handler = SIG_IGN}, &oldsighup);
1853 sigaction(SIGTSTP, &dfl_act, &oldsigtstp);
1854 } else if (p == 0) {
1855 /* We create a grandchild to detach */
1856 if (flag & F_NOWAIT) {
1860 _exit(EXIT_SUCCESS);
1862 sigaction(SIGHUP, &dfl_act, NULL);
1863 sigaction(SIGINT, &dfl_act, NULL);
1864 sigaction(SIGQUIT, &dfl_act, NULL);
1865 sigaction(SIGTSTP, &dfl_act, NULL);
1872 _exit(EXIT_FAILURE);
1875 /* so they can be used to stop the child */
1876 sigaction(SIGHUP, &dfl_act, NULL);
1877 sigaction(SIGINT, &dfl_act, NULL);
1878 sigaction(SIGQUIT, &dfl_act, NULL);
1879 sigaction(SIGTSTP, &dfl_act, NULL);
1882 /* This is the parent waiting for the child to create grandchild */
1883 if (flag & F_NOWAIT)
1884 waitpid(p, &status, 0);
1891 static int join(pid_t p, uchar_t flag)
1893 int status = 0xFFFF;
1895 if (!(flag & F_NOWAIT)) {
1896 /* wait for the child to exit */
1898 } while (waitpid(p, &status, 0) == -1);
1900 if (WIFEXITED(status)) {
1901 status = WEXITSTATUS(status);
1906 /* restore parent's signal handling */
1907 sigaction(SIGHUP, &oldsighup, NULL);
1908 sigaction(SIGTSTP, &oldsigtstp, NULL);
1914 * Spawns a child process. Behaviour can be controlled using flag.
1915 * Limited to 2 arguments to a program, flag works on bit set.
1917 static int spawn(char *file, char *arg1, char *arg2, uchar_t flag)
1920 int status = 0, retstatus = 0xFFFF;
1921 char *argv[EXEC_ARGS_MAX] = {0};
1924 if (!file || !*file)
1927 /* Swap args if the first arg is NULL and second isn't */
1928 if (!arg1 && arg2) {
1933 if (flag & F_MULTI) {
1934 size_t len = xstrlen(file) + 1;
1936 cmd = (char *)malloc(len);
1938 DPRINTF_S("malloc()!");
1942 xstrsncpy(cmd, file, len);
1943 status = parseargs(cmd, argv);
1944 if (status == -1 || status > (EXEC_ARGS_MAX - 3)) { /* arg1, arg2 and last NULL */
1946 DPRINTF_S("NULL or too many args");
1950 argv[status++] = file;
1952 argv[status] = arg1;
1953 argv[++status] = arg2;
1955 if (flag & F_NORMAL)
1960 /* Suppress stdout and stderr */
1961 if (flag & F_NOTRACE) {
1962 int fd = open("/dev/null", O_WRONLY, 0200);
1964 if (flag & F_NOSTDIN)
1971 execvp(*argv, argv);
1972 _exit(EXIT_SUCCESS);
1974 retstatus = join(pid, flag);
1978 if ((flag & F_CONFIRM) || ((flag & F_CHKRTN) && retstatus)) {
1979 printf("%s", messages[MSG_CONTINUE]);
1983 while (getchar() != '\n') {};
1986 if (flag & F_NORMAL)
1995 /* Get program name from env var, else return fallback program */
1996 static char *xgetenv(const char * const name, char *fallback)
1998 char *value = getenv(name);
2000 return value && value[0] ? value : fallback;
2003 /* Checks if an env variable is set to 1 */
2004 static inline uint_t xgetenv_val(const char *name)
2006 char *str = getenv(name);
2014 /* Check if a dir exists, IS a dir, and is readable */
2015 static bool xdiraccess(const char *path)
2017 DIR *dirp = opendir(path);
2028 static void opstr(char *buf, char *op)
2030 snprintf(buf, CMD_LEN_MAX, "xargs -0 sh -c '%s \"$0\" \"$@\" . < /dev/tty' < %s",
2034 static bool rmmulstr(char *buf)
2036 if (!g_state.trash) {
2037 char r = confirm_force(TRUE);
2042 snprintf(buf, CMD_LEN_MAX, "xargs -0 sh -c 'rm -%cr \"$0\" \"$@\" < /dev/tty' < %s",
2044 } else if (g_state.trash == 1)
2045 snprintf(buf, CMD_LEN_MAX, "xargs -0 trash-put < %s", selpath);
2047 snprintf(buf, CMD_LEN_MAX, "xargs -0 gio trash < %s", selpath);
2052 /* Returns TRUE if file is removed, else FALSE */
2053 static bool xrm(char *fpath)
2055 if (!g_state.trash) {
2056 char rm_opts[] = "-ir";
2058 rm_opts[1] = confirm_force(FALSE);
2062 spawn("rm", rm_opts, fpath, F_NORMAL | F_CHKRTN);
2063 } else if (g_state.trash == 1)
2064 spawn("trash-put", fpath, NULL, F_NORMAL);
2066 spawn("gio trash", fpath, NULL, F_NORMAL | F_MULTI);
2068 return (access(fpath, F_OK) == -1); /* File is removed */
2071 static uint_t lines_in_file(int fd, char *buf, size_t buflen)
2076 while ((len = read(fd, buf, buflen)) > 0)
2078 count += (buf[--len] == '\n');
2080 /* For all use cases 0 linecount is considered as error */
2081 return ((len < 0) ? 0 : count);
2084 static bool cpmv_rename(int choice, const char *path)
2087 uint_t count = 0, lines = 0;
2089 char *cmd = (choice == 'c' ? cp : mv);
2090 char buf[sizeof(patterns[P_CPMVRNM]) + sizeof(cmd) + (PATH_MAX << 1)];
2092 fd = create_tmp_file();
2096 /* selsafe() returned TRUE for this to be called */
2098 snprintf(buf, sizeof(buf), "tr '\\0' '\\n' < %s > %s", selpath, g_tmpfpath);
2099 spawn(utils[UTIL_SH_EXEC], buf, NULL, F_CLI);
2101 count = lines_in_file(fd, buf, sizeof(buf));
2105 seltofile(fd, &count);
2109 snprintf(buf, sizeof(buf), patterns[P_CPMVFMT], g_tmpfpath);
2110 spawn(utils[UTIL_SH_EXEC], buf, NULL, F_CLI);
2112 spawn((cfg.waitedit ? enveditor : editor), g_tmpfpath, NULL, F_CLI);
2114 fd = open(g_tmpfpath, O_RDONLY);
2118 lines = lines_in_file(fd, buf, sizeof(buf));
2121 if (!lines || (2 * count != lines)) {
2122 DPRINTF_S("num mismatch");
2126 snprintf(buf, sizeof(buf), patterns[P_CPMVRNM], path, g_tmpfpath, cmd);
2127 if (!spawn(utils[UTIL_SH_EXEC], buf, NULL, F_CLI | F_CHKRTN))
2136 static bool cpmvrm_selection(enum action sel, char *path)
2140 if (!selbufpos && isselfileempty()) {
2141 printmsg(messages[MSG_0_SELECTED]);
2156 r = get_input(messages[MSG_CP_MV_AS]);
2157 if (r != 'c' && r != 'm') {
2158 printmsg(messages[MSG_INVALID_KEY]);
2162 if (!cpmv_rename(r, path)) {
2163 printmsg(messages[MSG_FAILED]);
2167 default: /* SEL_RM */
2168 if (!rmmulstr(g_buf)) {
2169 printmsg(messages[MSG_CANCEL]);
2174 if (sel != SEL_CPMVAS && spawn(utils[UTIL_SH_EXEC], g_buf, NULL, F_CLI | F_CHKRTN)) {
2175 printmsg(messages[MSG_FAILED]);
2179 /* Clear selection */
2186 static bool batch_rename(void)
2189 uint_t count = 0, lines = 0;
2190 bool dir = FALSE, ret = FALSE;
2191 char foriginal[TMP_LEN_MAX] = {0};
2192 static const char batchrenamecmd[] = "paste -d'\n' %s %s | "SED" 'N; /^\\(.*\\)\\n\\1$/!p;d' | "
2193 "tr '\n' '\\0' | xargs -0 -n2 mv 2>/dev/null";
2194 char buf[sizeof(batchrenamecmd) + (PATH_MAX << 1)];
2195 int i = get_cur_or_sel();
2200 if (i == 'c') { /* Rename entries in current dir */
2205 fd1 = create_tmp_file();
2209 xstrsncpy(foriginal, g_tmpfpath, xstrlen(g_tmpfpath) + 1);
2211 fd2 = create_tmp_file();
2219 for (i = 0; i < ndents; ++i)
2220 appendfpath(pdents[i].name, NAME_MAX);
2222 seltofile(fd1, &count);
2223 seltofile(fd2, NULL);
2226 if (dir) /* Don't retain dir entries in selection */
2229 spawn((cfg.waitedit ? enveditor : editor), g_tmpfpath, NULL, F_CLI);
2231 /* Reopen file descriptor to get updated contents */
2232 fd2 = open(g_tmpfpath, O_RDONLY);
2236 lines = lines_in_file(fd2, buf, sizeof(buf));
2239 if (!lines || (count != lines)) {
2240 DPRINTF_S("cannot delete files");
2244 snprintf(buf, sizeof(buf), batchrenamecmd, foriginal, g_tmpfpath);
2245 spawn(utils[UTIL_SH_EXEC], buf, NULL, F_CLI);
2261 static void get_archive_cmd(char *cmd, const char *archive)
2265 if (getutil(utils[UTIL_ATOOL]))
2267 else if (getutil(utils[UTIL_BSDTAR]))
2269 else if (is_suffix(archive, ".zip"))
2273 xstrsncpy(cmd, archive_cmd[i], ARCHIVE_CMD_LEN);
2276 static void archive_selection(const char *cmd, const char *archive, const char *curpath)
2278 /* The 70 comes from the string below */
2279 char *buf = (char *)malloc((70 + xstrlen(cmd) + xstrlen(archive)
2280 + xstrlen(curpath) + xstrlen(selpath)) * sizeof(char));
2282 DPRINTF_S(strerror(errno));
2287 snprintf(buf, CMD_LEN_MAX,
2289 SED" -ze 's|^%s/||' '%s' | xargs -0 %s %s", curpath, selpath, cmd, archive
2291 "tr '\\0' '\n' < '%s' | "SED" -e 's|^%s/||' | tr '\n' '\\0' | xargs -0 %s %s",
2292 selpath, curpath, cmd, archive
2295 spawn(utils[UTIL_SH_EXEC], buf, NULL, F_CLI | F_CONFIRM);
2299 static bool write_lastdir(const char *curpath)
2302 size_t len = xstrlen(cfgpath);
2304 xstrsncpy(cfgpath + len, "/.lastd", 8);
2307 FILE *fp = fopen(cfgpath, "w");
2310 if (fprintf(fp, "cd \"%s\"", curpath) < 0)
2321 * We assume none of the strings are NULL.
2323 * Let's have the logic to sort numeric names in numeric order.
2324 * E.g., the order '1, 10, 2' doesn't make sense to human eyes.
2326 * If the absolute numeric values are same, we fallback to alphasort.
2328 static int xstricmp(const char * const s1, const char * const s2)
2332 long long v1 = strtoll(s1, &p1, 10);
2333 long long v2 = strtoll(s2, &p2, 10);
2335 /* Check if at least 1 string is numeric */
2336 if (s1 != p1 || s2 != p2) {
2337 /* Handle both pure numeric */
2338 if (s1 != p1 && s2 != p2) {
2346 /* Only first string non-numeric */
2350 /* Only second string non-numeric */
2355 /* Handle 1. all non-numeric and 2. both same numeric value cases */
2357 return strcoll(s1, s2);
2359 return strcasecmp(s1, s2);
2364 * Version comparison
2366 * The code for version compare is a modified version of the GLIBC
2367 * and uClibc implementation of strverscmp(). The source is here:
2368 * https://elixir.bootlin.com/uclibc-ng/latest/source/libc/string/strverscmp.c
2372 * Compare S1 and S2 as strings holding indices/version numbers,
2373 * returning less than, equal to or greater than zero if S1 is less than,
2374 * equal to or greater than S2 (for more info, see the texinfo doc).
2378 static int xstrverscasecmp(const char * const s1, const char * const s2)
2380 const uchar_t *p1 = (const uchar_t *)s1;
2381 const uchar_t *p2 = (const uchar_t *)s2;
2386 * Symbol(s) 0 [1-9] others
2387 * Transition (10) 0 (01) d (00) x
2389 static const uint8_t next_state[] = {
2391 /* S_N */ S_N, S_I, S_Z,
2392 /* S_I */ S_N, S_I, S_I,
2393 /* S_F */ S_N, S_F, S_F,
2394 /* S_Z */ S_N, S_F, S_Z
2397 static const int8_t result_type[] __attribute__ ((aligned)) = {
2398 /* state x/x x/d x/0 d/x d/d d/0 0/x 0/d 0/0 */
2400 /* S_N */ VCMP, VCMP, VCMP, VCMP, VLEN, VCMP, VCMP, VCMP, VCMP,
2401 /* S_I */ VCMP, -1, -1, 1, VLEN, VLEN, 1, VLEN, VLEN,
2402 /* S_F */ VCMP, VCMP, VCMP, VCMP, VCMP, VCMP, VCMP, VCMP, VCMP,
2403 /* S_Z */ VCMP, 1, 1, -1, VCMP, VCMP, -1, VCMP, VCMP
2414 /* Hint: '0' is a digit too. */
2415 state = S_N + ((c1 == '0') + (xisdigit(c1) != 0));
2417 while ((diff = c1 - c2) == 0) {
2421 state = next_state[state];
2426 state += (c1 == '0') + (xisdigit(c1) != 0);
2429 state = result_type[state * 3 + (((c2 == '0') + (xisdigit(c2) != 0)))]; // NOLINT
2435 while (xisdigit(*p1++))
2436 if (!xisdigit(*p2++))
2438 return xisdigit(*p2) ? -1 : diff;
2444 static int (*namecmpfn)(const char * const s1, const char * const s2) = &xstricmp;
2446 static char * (*fnstrstr)(const char *haystack, const char *needle) = &strcasestr;
2448 static const unsigned char *tables;
2449 static int pcreflags = PCRE_NO_AUTO_CAPTURE | PCRE_EXTENDED | PCRE_CASELESS | PCRE_UTF8;
2451 static int regflags = REG_NOSUB | REG_EXTENDED | REG_ICASE;
2455 static int setfilter(pcre **pcrex, const char *filter)
2457 const char *errstr = NULL;
2460 *pcrex = pcre_compile(filter, pcreflags, &errstr, &erroffset, tables);
2462 return errstr ? -1 : 0;
2465 static int setfilter(regex_t *regex, const char *filter)
2467 return regcomp(regex, filter, regflags);
2471 static int visible_re(const fltrexp_t *fltrexp, const char *fname)
2474 return pcre_exec(fltrexp->pcrex, NULL, fname, xstrlen(fname), 0, 0, NULL, 0) == 0;
2476 return regexec(fltrexp->regex, fname, 0, NULL, 0) == 0;
2480 static int visible_str(const fltrexp_t *fltrexp, const char *fname)
2482 return fnstrstr(fname, fltrexp->str) != NULL;
2485 static int (*filterfn)(const fltrexp_t *fltr, const char *fname) = &visible_str;
2487 static void clearfilter(void)
2489 char *fltr = g_ctx[cfg.curctx].c_fltr;
2492 fltr[REGEX_MAX - 1] = fltr[1];
2497 static int entrycmp(const void *va, const void *vb)
2499 const struct entry *pa = (pEntry)va;
2500 const struct entry *pb = (pEntry)vb;
2502 if ((pb->flags & DIR_OR_LINK_TO_DIR) != (pa->flags & DIR_OR_LINK_TO_DIR)) {
2503 if (pb->flags & DIR_OR_LINK_TO_DIR)
2508 /* Sort based on specified order */
2509 if (cfg.timeorder) {
2514 } else if (cfg.sizeorder) {
2515 if (pb->size > pa->size)
2517 if (pb->size < pa->size)
2519 } else if (cfg.blkorder) {
2520 if (pb->blocks > pa->blocks)
2522 if (pb->blocks < pa->blocks)
2524 } else if (cfg.extnorder && !(pb->flags & DIR_OR_LINK_TO_DIR)) {
2525 char *extna = xextension(pa->name, pa->nlen - 1);
2526 char *extnb = xextension(pb->name, pb->nlen - 1);
2528 if (extna || extnb) {
2535 int ret = strcasecmp(extna, extnb);
2542 return namecmpfn(pa->name, pb->name);
2545 static int reventrycmp(const void *va, const void *vb)
2547 if ((((pEntry)vb)->flags & DIR_OR_LINK_TO_DIR)
2548 != (((pEntry)va)->flags & DIR_OR_LINK_TO_DIR)) {
2549 if (((pEntry)vb)->flags & DIR_OR_LINK_TO_DIR)
2554 return -entrycmp(va, vb);
2557 static int (*entrycmpfn)(const void *va, const void *vb) = &entrycmp;
2559 /* In case of an error, resets *wch to Esc */
2560 static int handle_alt_key(wint_t *wch)
2564 int r = get_wch(wch);
2574 * Returns SEL_* if key is bound and 0 otherwise.
2575 * Also modifies the run and env pointers (used on SEL_{RUN,RUNARG}).
2576 * The next keyboard input can be simulated by presel.
2578 static int nextsel(int presel)
2585 bool escaped = FALSE;
2587 if (c == 0 || c == MSGWAIT) {
2591 //DPRINTF_S(keyname(c));
2593 /* Handle Alt+key */
2605 } else if (escaped) {
2610 /* Send hovered path to NNN_FIFO */
2619 if (c == ERR && presel == MSGWAIT)
2620 c = (cfg.filtermode || filterset()) ? FILTER : CONTROL('L');
2621 else if (c == FILTER || c == CONTROL('L'))
2622 /* Clear previous filter when manually starting */
2630 * Do not check for directory changes in du mode.
2631 * A redraw forces du calculation.
2632 * Check for changes every odd second.
2634 #ifdef LINUX_INOTIFY
2635 if (!g_state.selmode && !cfg.blkorder && inotify_wd >= 0 && (idle & 1)) {
2636 struct inotify_event *event;
2637 char inotify_buf[EVENT_BUF_LEN];
2639 memset((void *)inotify_buf, 0x0, EVENT_BUF_LEN);
2640 i = read(inotify_fd, inotify_buf, EVENT_BUF_LEN);
2642 for (char *ptr = inotify_buf;
2643 ptr + ((struct inotify_event *)ptr)->len < inotify_buf + i;
2644 ptr += sizeof(struct inotify_event) + event->len) {
2645 event = (struct inotify_event *)ptr;
2646 DPRINTF_D(event->wd);
2647 DPRINTF_D(event->mask);
2651 if (event->mask & INOTIFY_MASK) {
2653 DPRINTF_S("issue refresh");
2657 DPRINTF_S("inotify read done");
2660 #elif defined(BSD_KQUEUE)
2661 if (!g_state.selmode && !cfg.blkorder && event_fd >= 0 && idle & 1) {
2662 struct kevent event_data[NUM_EVENT_SLOTS];
2664 memset((void *)event_data, 0x0, sizeof(struct kevent) * NUM_EVENT_SLOTS);
2665 if (kevent(kq, events_to_monitor, NUM_EVENT_SLOTS, event_data, NUM_EVENT_FDS, >imeout) > 0)
2668 #elif defined(HAIKU_NM)
2669 if (!g_state.selmode && !cfg.blkorder && haiku_nm_active && idle & 1 && haiku_is_update_needed(haiku_hnd))
2675 for (i = 0; i < (int)ELEMENTS(bindings); ++i)
2676 if (c == bindings[i].sym)
2677 return bindings[i].act;
2682 static int getorderstr(char *sort)
2690 sort[i++] = (cfg.timetype == T_MOD) ? 'M' : ((cfg.timetype == T_ACCESS) ? 'A' : 'C');
2691 else if (cfg.sizeorder)
2693 else if (cfg.extnorder)
2696 if (entrycmpfn == &reventrycmp)
2699 if (namecmpfn == &xstrverscasecmp)
2708 static void showfilterinfo(void)
2711 char info[REGEX_MAX] = "\0\0\0\0\0";
2713 i = getorderstr(info);
2715 snprintf(info + i, REGEX_MAX - i - 1, " %s [/], %s [:]",
2716 (cfg.regex ? "regex" : "str"),
2717 ((fnstrstr == &strcasestr) ? "ic" : "noic"));
2720 mvaddstr(xlines - 2, xcols - xstrlen(info), info);
2723 static void showfilter(char *str)
2725 attron(COLOR_PAIR(cfg.curctx + 1));
2728 // printmsg calls attroff()
2731 static inline void swap_ent(int id1, int id2)
2733 struct entry _dent, *pdent1 = &pdents[id1], *pdent2 = &pdents[id2];
2735 *(&_dent) = *pdent1;
2737 *pdent2 = *(&_dent);
2741 static int fill(const char *fltr, pcre *pcrex)
2743 static int fill(const char *fltr, regex_t *re)
2747 fltrexp_t fltrexp = { .pcrex = pcrex, .str = fltr };
2749 fltrexp_t fltrexp = { .regex = re, .str = fltr };
2752 for (int count = 0; count < ndents; ++count) {
2753 if (filterfn(&fltrexp, pdents[count].name) == 0) {
2754 if (count != --ndents) {
2755 swap_ent(count, ndents);
2766 static int matches(const char *fltr)
2772 if (cfg.regex && setfilter(&pcrex, fltr))
2775 ndents = fill(fltr, pcrex);
2783 if (cfg.regex && setfilter(&re, fltr))
2786 ndents = fill(fltr, &re);
2792 ENTSORT(pdents, ndents, entrycmpfn);
2798 * Return the position of the matching entry or 0 otherwise
2799 * Note there's no NULL check for fname
2801 static int dentfind(const char *fname, int n)
2803 for (int i = 0; i < n; ++i)
2804 if (xstrcmp(fname, pdents[i].name) == 0)
2810 static int filterentries(char *path, char *lastname)
2812 wchar_t *wln = (wchar_t *)alloca(sizeof(wchar_t) * REGEX_MAX);
2813 char *ln = g_ctx[cfg.curctx].c_fltr;
2815 int r, total = ndents, len;
2816 char *pln = g_ctx[cfg.curctx].c_fltr + 1;
2818 DPRINTF_S(__func__);
2820 if (ndents && (ln[0] == FILTER || ln[0] == RFILTER) && *pln) {
2821 if (matches(pln) != -1) {
2822 move_cursor(dentfind(lastname, ndents), 0);
2826 if (!cfg.filtermode)
2829 len = mbstowcs(wln, ln, REGEX_MAX);
2831 ln[0] = wln[0] = cfg.regex ? RFILTER : FILTER;
2832 ln[1] = wln[1] = '\0';
2840 while ((r = get_wch(ch)) != ERR) {
2842 //DPRINTF_S(keyname(*ch));
2852 case KEY_DC: // fallthrough
2853 case KEY_BACKSPACE: // fallthrough
2854 case '\b': // fallthrough
2855 case DEL: /* handle DEL */
2858 wcstombs(ln, wln, REGEX_MAX);
2864 if (*ch == CONTROL('L')) {
2866 ln[REGEX_MAX - 1] = ln[1];
2867 ln[1] = wln[1] = '\0';
2870 } else if (ln[REGEX_MAX - 1]) { /* Show the previous filter */
2871 ln[1] = ln[REGEX_MAX - 1];
2872 ln[REGEX_MAX - 1] = '\0';
2873 len = mbstowcs(wln, ln, REGEX_MAX);
2878 /* Go to the top, we don't know if the hovered file will match the filter */
2881 if (matches(pln) != -1)
2890 case ESC: /* Exit filter mode on Esc and Alt+key */
2891 if (handle_alt_key(ch) != ERR) {
2892 if (*ch == ESC) { /* Handle Alt+Esc */
2894 ln[REGEX_MAX - 1] = ln[1];
2895 ln[1] = wln[1] = '\0';
2907 if (r != OK) /* Handle Fn keys in main loop */
2910 /* Handle all control chars in main loop */
2911 if (*ch < ASCII_MAX && keyname(*ch)[0] == '^' && *ch != '^') {
2912 if (keyname(*ch)[1] == '@')
2918 if (*ch == '?') /* Help and config key, '?' is an invalid regex */
2921 if (cfg.filtermode) {
2923 case '\'': // fallthrough /* Go to first non-dir file */
2924 case '+': // fallthrough /* Toggle auto-advance */
2925 case ',': // fallthrough /* Mark CWD */
2926 case '-': // fallthrough /* Visit last visited dir */
2927 case '.': // fallthrough /* Show hidden files */
2928 case ';': // fallthrough /* Run plugin key */
2929 case '=': // fallthrough /* Launch app */
2930 case '>': // fallthrough /* Export file list */
2931 case '@': // fallthrough /* Visit start dir */
2932 case ']': // fallthorugh /* Prompt key */
2933 case '`': // fallthrough /* Visit / */
2934 case '~': /* Go HOME */
2939 /* Toggle case-sensitivity */
2941 fnstrstr = (fnstrstr == &strcasestr) ? &strstr : &strcasestr;
2943 pcreflags ^= PCRE_CASELESS;
2945 regflags ^= REG_ICASE;
2951 /* Toggle string or regex filter */
2952 if (*ch == FILTER) {
2953 ln[0] = (ln[0] == FILTER) ? RFILTER : FILTER;
2954 wln[0] = (uchar_t)ln[0];
2956 filterfn = cfg.regex ? &visible_re : &visible_str;
2961 /* Reset cur in case it's a repeat search */
2963 } else if (len == REGEX_MAX - 1)
2966 wln[len] = (wchar_t)*ch;
2968 wcstombs(ln, wln, REGEX_MAX);
2970 /* Forward-filtering optimization:
2971 * - new matches can only be a subset of current matches.
2973 /* ndents = total; */
2975 if (matches(pln) == -1) {
2980 /* If the only match is a dir, auto-select and cd into it */
2981 if (ndents == 1 && cfg.filtermode
2982 && cfg.autoselect && (pdents[0].flags & DIR_OR_LINK_TO_DIR)) {
2989 * redraw() should be above the auto-select optimization, for
2990 * the case where there's an issue with dir auto-select, say,
2991 * due to a permission problem. The transition is _jumpy_ in
2992 * case of such an error. However, we optimize for successful
2993 * cases where the dir has permissions. This skips a redraw().
3001 /* Save last working filter in-filter */
3003 ln[REGEX_MAX - 1] = ln[1];
3012 /* Return keys for navigation etc. */
3016 /* Show a prompt with input string and return the changes */
3017 static char *xreadline(const char *prefill, const char *prompt)
3021 const int WCHAR_T_WIDTH = sizeof(wchar_t);
3023 wchar_t * const buf = malloc(sizeof(wchar_t) * READLINE_MAX);
3033 len = pos = mbstowcs(buf, prefill, READLINE_MAX);
3037 if (len == (size_t)-1) {
3042 x = getcurx(stdscr);
3047 attron(COLOR_PAIR(cfg.curctx + 1));
3048 mvaddnwstr(xlines - 1, x, buf, len + 1);
3049 move(xlines - 1, x + wcswidth(buf, pos));
3050 attroff(COLOR_PAIR(cfg.curctx + 1));
3058 case KEY_ENTER: // fallthrough
3059 case '\n': // fallthrough
3065 else if (!(pos || len)) { /* Exit on ^D at empty prompt */
3071 case DEL: // fallthrough
3072 case '\b': /* rhel25 sends '\b' for backspace */
3074 memmove(buf + pos - 1, buf + pos,
3075 (len - pos) * WCHAR_T_WIDTH);
3078 case '\t': /* Tab breaks cursor position, ignore it */
3093 memmove(buf + pos - 1, buf + pos,
3094 (len - pos) * WCHAR_T_WIDTH);
3096 } while (buf[pos - 1] != ' ' && buf[pos - 1] != '/'); // NOLINT
3114 memmove(buf, buf + pos, (len - pos) * WCHAR_T_WIDTH);
3118 case ESC: /* Exit prompt on Esc, but just filter out Alt+key */
3119 if (handle_alt_key(ch) != ERR)
3126 /* Filter out all other control chars */
3127 if (*ch < ASCII_MAX && keyname(*ch)[0] == '^')
3130 if (pos < READLINE_MAX - 1) {
3131 memmove(buf + pos + 1, buf + pos,
3132 (len - pos) * WCHAR_T_WIDTH);
3156 memmove(buf + pos - 1, buf + pos,
3157 (len - pos) * WCHAR_T_WIDTH);
3163 memmove(buf + pos, buf + pos + 1,
3164 (len - pos - 1) * WCHAR_T_WIDTH);
3187 pos = wcstombs(g_buf, buf, READLINE_MAX - 1);
3188 if (pos >= READLINE_MAX - 1)
3189 g_buf[READLINE_MAX - 1] = '\0';
3197 * Caller should check the value of presel to confirm if it needs to wait to show warning
3199 static char *getreadline(const char *prompt)
3203 char *input = readline(prompt);
3207 if (input && input[0]) {
3209 xstrsncpy(g_buf, input, CMD_LEN_MAX);
3220 * Create symbolic/hard link(s) to file(s) in selection list
3221 * Returns the number of links created, -1 on error
3223 static int xlink(char *prefix, char *path, char *curfname, char *buf, int *presel, int type)
3225 int count = 0, choice;
3226 char *psel = pselbuf, *fname;
3227 size_t pos = 0, len, r;
3228 int (*link_fn)(const char *, const char *) = NULL;
3229 char lnpath[PATH_MAX];
3231 choice = get_cur_or_sel();
3235 if (type == 's') /* symbolic link */
3237 else /* hard link */
3240 if (choice == 'c') {
3241 r = xstrsncpy(buf, prefix, NAME_MAX + 1); /* Copy prefix */
3242 xstrsncpy(buf + r - 1, curfname, NAME_MAX - r); /* Suffix target file name */
3243 mkpath(path, buf, lnpath); /* Generate link path */
3244 mkpath(path, curfname, buf); /* Generate target file path */
3246 if (!link_fn(buf, lnpath))
3247 return 1; /* One link created */
3253 while (pos < selbufpos) {
3254 len = xstrlen(psel);
3255 fname = xbasename(psel);
3257 r = xstrsncpy(buf, prefix, NAME_MAX + 1); /* Copy prefix */
3258 xstrsncpy(buf + r - 1, fname, NAME_MAX - r); /* Suffix target file name */
3259 mkpath(path, buf, lnpath); /* Generate link path */
3261 if (!link_fn(psel, lnpath))
3272 static bool parsekvpair(kv **arr, char **envcpy, const uchar_t id, uchar_t *items)
3275 const uchar_t INCR = 8;
3278 char *ptr = getenv(env_cfg[id]);
3283 *envcpy = xstrdup(ptr);
3291 while (*ptr && i < 100) {
3293 if (!(i & (INCR - 1))) {
3294 kvarr = xrealloc(kvarr, sizeof(kv) * (i + INCR));
3300 memset(kvarr + i, 0, sizeof(kv) * INCR);
3302 kvarr[i].key = (uchar_t)*ptr;
3303 if (*++ptr != ':' || *++ptr == '\0' || *ptr == ';')
3305 kvarr[i].off = ptr - *envcpy;
3324 * Get the value corresponding to a key
3326 * NULL is returned in case of no match, path resolution failure etc.
3327 * buf would be modified, so check return value before access
3329 static char *get_kv_val(kv *kvarr, char *buf, int key, uchar_t max, uchar_t id)
3336 for (int r = 0; kvarr[r].key && r < max; ++r) {
3337 if (kvarr[r].key == key) {
3338 /* Do not allocate new memory for plugin */
3340 return pluginstr + kvarr[r].off;
3342 val = bmstr + kvarr[r].off;
3344 if (val[0] == '~') {
3345 ssize_t len = xstrlen(home);
3346 ssize_t loclen = xstrlen(val);
3348 xstrsncpy(g_buf, home, len + 1);
3349 xstrsncpy(g_buf + len, val + 1, loclen);
3352 return realpath(((val[0] == '~') ? g_buf : val), buf);
3356 DPRINTF_S("Invalid key");
3360 static void resetdircolor(int flags)
3362 if (g_state.dircolor && !(flags & DIR_OR_LINK_TO_DIR)) {
3363 attroff(COLOR_PAIR(cfg.curctx + 1) | A_BOLD);
3364 g_state.dircolor = 0;
3369 * Replace escape characters in a string with '?'
3370 * Adjust string length to maxcols if > 0;
3371 * Max supported str length: NAME_MAX;
3374 static wchar_t *unescape(const char *str, uint_t maxcols)
3376 wchar_t * const wbuf = (wchar_t *)g_buf;
3377 wchar_t *buf = wbuf;
3378 size_t lencount = 0;
3380 /* Convert multi-byte to wide char */
3381 size_t len = mbstowcs(wbuf, str, NAME_MAX);
3383 len = wcswidth(wbuf, len);
3385 /* Reduce number of wide chars to max columns */
3386 if (len > maxcols) {
3387 while (*buf && lencount <= maxcols) {
3388 if (*buf <= '\x1f' || *buf == '\x7f')
3395 lencount = maxcols + 1;
3397 /* Reduce wide chars one by one till it fits */
3399 len = wcswidth(wbuf, --lencount);
3400 while (len > maxcols);
3402 wbuf[lencount] = L'\0';
3404 do { /* We do not expect a NULL string */
3405 if (*buf <= '\x1f' || *buf == '\x7f')
3413 static char *unescape(const char *str, uint_t maxcols)
3415 ssize_t len = (ssize_t)xstrsncpy(g_buf, str, maxcols);
3419 if (g_buf[len] <= '\x1f' || g_buf[len] == '\x7f')
3426 static off_t get_size(off_t size, off_t *pval, uint_t comp)
3429 off_t quo = rem / 10;
3431 if ((rem - (quo * 10)) >= 5) {
3444 static char *coolsize(off_t size)
3446 const char * const U = "BKMGTPEZY";
3447 static char size_buf[12]; /* Buffer to hold human readable size */
3452 while (size >= 1024) {
3453 rem = size & (0x3FF); /* 1024 - 1 = 0x3FF */
3459 rem = (rem * 1000) >> 10;
3461 size = get_size(size, &rem, 10);
3462 } else if (i == 2) {
3463 rem = (rem * 1000) >> 10;
3464 size = get_size(size, &rem, 100);
3466 rem = (rem * 10000) >> 10;
3467 size = get_size(size, &rem, 1000);
3470 if (i > 0 && i < 6 && rem) {
3471 ret = xstrsncpy(size_buf, xitoa(size), 12);
3472 size_buf[ret - 1] = '.';
3474 char *frac = xitoa(rem);
3475 size_t toprint = i > 3 ? 3 : i;
3476 size_t len = xstrlen(frac);
3478 if (len < toprint) {
3479 size_buf[ret] = size_buf[ret + 1] = size_buf[ret + 2] = '0';
3480 xstrsncpy(size_buf + ret + (toprint - len), frac, len + 1);
3482 xstrsncpy(size_buf + ret, frac, toprint + 1);
3486 ret = xstrsncpy(size_buf, size ? xitoa(size) : "0", 12);
3490 size_buf[ret] = U[i];
3491 size_buf[ret + 1] = '\0';
3496 /* Convert a mode field into "ls -l" type perms field. */
3497 static char *get_lsperms(mode_t mode)
3499 static const char * const rwx[] = {"---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx"};
3500 static char bits[11] = {'\0'};
3502 switch (mode & S_IFMT) {
3529 xstrsncpy(&bits[1], rwx[(mode >> 6) & 7], 4);
3530 xstrsncpy(&bits[4], rwx[(mode >> 3) & 7], 4);
3531 xstrsncpy(&bits[7], rwx[(mode & 7)], 4);
3534 bits[3] = (mode & 0100) ? 's' : 'S'; /* user executable */
3536 bits[6] = (mode & 0010) ? 's' : 'l'; /* group executable */
3538 bits[9] = (mode & 0001) ? 't' : 'T'; /* others executable */
3543 #ifdef ICONS_ENABLED
3544 static const struct icon_pair *get_icon(const struct entry *ent)
3548 for (; i < sizeof(icons_name)/sizeof(struct icon_pair); ++i)
3549 if (strcasecmp(ent->name, icons_name[i].match) == 0)
3550 return &icons_name[i];
3552 if (ent->flags & DIR_OR_LINK_TO_DIR)
3555 char *tmp = xextension(ent->name, ent->nlen);
3558 if (ent->mode & 0100)
3567 if (*tmp >= '0' && *tmp <= '9')
3568 i = *tmp - '0'; /* NUMBER 0-9 */
3569 else if (TOUPPER(*tmp) >= 'A' && TOUPPER(*tmp) <= 'Z')
3570 i = TOUPPER(*tmp) - 'A' + 10; /* LETTER A-Z */
3574 for (ushort_t j = icon_positions[i]; j < sizeof(icons_ext)/sizeof(struct icon_pair) &&
3575 icons_ext[j].match[0] == icons_ext[icon_positions[i]].match[0]; ++j)
3576 if (strcasecmp(tmp, icons_ext[j].match) == 0)
3577 return &icons_ext[j];
3579 /* If there's no match and the file is executable, icon that */
3580 if (ent->mode & 0100)
3586 static void print_icon(const struct entry *ent, const int attrs)
3588 const struct icon_pair *picon = get_icon(ent);
3590 addstr(ICON_PADDING_LEFT);
3592 attron(COLOR_PAIR(C_UND + 1 + picon->color));
3595 addstr(picon->icon);
3597 attroff(COLOR_PAIR(C_UND + 1 + picon->color));
3600 addstr(ICON_PADDING_RIGHT);
3604 static void print_time(const time_t *timep)
3606 struct tm *t = localtime(timep);
3608 printw("%s-%02d-%02d %02d:%02d",
3609 xitoa(t->tm_year + 1900), t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min);
3612 static char get_detail_ind(const mode_t mode)
3614 switch (mode & S_IFMT) {
3615 case S_IFDIR: // fallthrough
3616 case S_IFREG: return ' ';
3617 case S_IFLNK: return '@';
3618 case S_IFSOCK: return '=';
3619 case S_IFIFO: return '|';
3620 case S_IFBLK: return 'b';
3621 case S_IFCHR: return 'c';
3626 static uchar_t get_color_pair_name_ind(const struct entry *ent, char *pind, bool detailed)
3628 switch (ent->mode & S_IFMT) {
3630 *pind = (ent->mode & 0100) ? '*' : '\0';
3633 if (ent->flags & HARD_LINK)
3635 if (ent->mode & 0100)
3640 return (!g_state.oldcolor && g_state.dirctx) ? cfg.curctx + 1 : C_DIR;
3642 *pind = (ent->flags & DIR_OR_LINK_TO_DIR) ? '/' : '@';
3643 if (!g_state.oldcolor || detailed)
3644 return (ent->flags & SYM_ORPHAN) ? C_ORP : C_LNK;
3664 static void printent(const struct entry *ent, uint_t namecols, bool sel)
3667 uchar_t color_pair = get_color_pair_name_ind(ent, &ind, (printptr == &printent_long));
3668 int attrs = 0, entry_type = ent->mode & S_IFMT;
3670 addch((ent->flags & FILE_SELECTED) ? '+' : ' ');
3672 /* Directories are always shown on top */
3673 resetdircolor(ent->flags);
3675 if (entry_type == S_IFDIR) {
3676 if (!g_state.oldcolor)
3678 } else if (entry_type == S_IFLNK) {
3679 if (!g_state.oldcolor && (ent->flags & DIR_OR_LINK_TO_DIR))
3681 else if (g_state.oldcolor)
3685 if (!g_state.oldcolor) {
3686 if (ent->flags & FILE_MISSING)
3688 if (color_pair && fcolors[color_pair])
3689 attrs |= COLOR_PAIR(color_pair);
3690 #ifdef ICONS_ENABLED
3691 print_icon(ent, attrs);
3703 addwstr(unescape(ent->name, namecols));
3705 addstr(unescape(ent->name, MIN(namecols, ent->nlen) + 1));
3715 static void print_details(const struct entry *ent)
3717 int entry_type = ent->mode & S_IFMT;
3718 char perms[6] = {' ', ' ', (char)('0' + ((ent->mode >> 6) & 7)),
3719 (char)('0' + ((ent->mode >> 3) & 7)), (char)('0' + (ent->mode & 7)), '\0'};
3721 /* Directories are always shown on top */
3722 resetdircolor(ent->flags);
3724 print_time(&ent->t);
3727 if (entry_type == S_IFREG || entry_type == S_IFDIR) {
3728 char *size = coolsize(cfg.blkorder ? ent->blocks << blk_shift : ent->size);
3730 printw("%*c%s", 9 - (uint_t)xstrlen(size), ' ', size);
3732 printw("%*c%c", 8, ' ', get_detail_ind(ent->mode));
3735 static void printent_long(const struct entry *ent, uint_t namecols, bool sel)
3737 int attrs1 = g_state.oldcolor ? A_DIM : COLOR_PAIR(C_MIS);
3740 addch(sel ? ACS_CKBOARD : ' ');
3742 addch(sel ? '>' : ' ');
3749 printent(ent, namecols, sel);
3752 static void (*printptr)(const struct entry *ent, uint_t namecols, bool sel) = &printent;
3754 static void savecurctx(settings *curcfg, char *path, char *curname, int nextctx)
3756 settings tmpcfg = *curcfg;
3757 context *ctxr = &g_ctx[nextctx];
3759 /* Save current context */
3761 xstrsncpy(g_ctx[tmpcfg.curctx].c_name, curname, NAME_MAX + 1);
3763 g_ctx[tmpcfg.curctx].c_name[0] = '\0';
3765 g_ctx[tmpcfg.curctx].c_cfg = tmpcfg;
3767 if (ctxr->c_cfg.ctxactive) { /* Switch to saved context */
3768 /* Switch light/detail mode */
3769 if (tmpcfg.showdetail != ctxr->c_cfg.showdetail)
3770 /* set the reverse */
3771 printptr = tmpcfg.showdetail ? &printent : &printent_long;
3773 tmpcfg = ctxr->c_cfg;
3774 } else { /* Set up a new context from current context */
3775 ctxr->c_cfg.ctxactive = 1;
3776 xstrsncpy(ctxr->c_path, path, PATH_MAX);
3777 ctxr->c_last[0] = ctxr->c_name[0] = ctxr->c_fltr[0] = ctxr->c_fltr[1] = '\0';
3778 ctxr->c_cfg = tmpcfg;
3781 tmpcfg.curctx = nextctx;
3786 static void save_session(bool last_session, int *presel)
3789 session_header_t header;
3792 bool status = FALSE;
3793 char ssnpath[PATH_MAX];
3794 char spath[PATH_MAX];
3796 memset(&header, 0, sizeof(session_header_t));
3798 header.ver = SESSIONS_VERSION;
3800 for (i = 0; i < CTX_MAX; ++i) {
3801 if (g_ctx[i].c_cfg.ctxactive) {
3802 if (cfg.curctx == i && ndents)
3803 /* Update current file name, arrows don't update it */
3804 xstrsncpy(g_ctx[i].c_name, pdents[cur].name, NAME_MAX + 1);
3805 header.pathln[i] = strnlen(g_ctx[i].c_path, PATH_MAX) + 1;
3806 header.lastln[i] = strnlen(g_ctx[i].c_last, PATH_MAX) + 1;
3807 header.nameln[i] = strnlen(g_ctx[i].c_name, NAME_MAX) + 1;
3808 header.fltrln[i] = strnlen(g_ctx[i].c_fltr, REGEX_MAX) + 1;
3812 sname = !last_session ? xreadline(NULL, messages[MSG_SSN_NAME]) : "@";
3816 mkpath(cfgpath, toks[TOK_SSN], ssnpath);
3817 mkpath(ssnpath, sname, spath);
3819 fsession = fopen(spath, "wb");
3821 printwait(messages[MSG_SEL_MISSING], presel);
3825 if ((fwrite(&header, sizeof(header), 1, fsession) != 1)
3826 || (fwrite(&cfg, sizeof(cfg), 1, fsession) != 1))
3829 for (i = 0; i < CTX_MAX; ++i)
3830 if ((fwrite(&g_ctx[i].c_cfg, sizeof(settings), 1, fsession) != 1)
3831 || (fwrite(&g_ctx[i].color, sizeof(uint_t), 1, fsession) != 1)
3832 || (header.nameln[i] > 0
3833 && fwrite(g_ctx[i].c_name, header.nameln[i], 1, fsession) != 1)
3834 || (header.lastln[i] > 0
3835 && fwrite(g_ctx[i].c_last, header.lastln[i], 1, fsession) != 1)
3836 || (header.fltrln[i] > 0
3837 && fwrite(g_ctx[i].c_fltr, header.fltrln[i], 1, fsession) != 1)
3838 || (header.pathln[i] > 0
3839 && fwrite(g_ctx[i].c_path, header.pathln[i], 1, fsession) != 1))
3848 printwait(messages[MSG_FAILED], presel);
3851 static bool load_session(const char *sname, char **path, char **lastdir, char **lastname, bool restore)
3854 session_header_t header;
3856 bool has_loaded_dynamically = !(sname || restore);
3857 bool status = FALSE;
3858 char ssnpath[PATH_MAX];
3859 char spath[PATH_MAX];
3861 mkpath(cfgpath, toks[TOK_SSN], ssnpath);
3864 sname = sname ? sname : xreadline(NULL, messages[MSG_SSN_NAME]);
3868 mkpath(ssnpath, sname, spath);
3870 /* If user is explicitly loading the "last session", skip auto-save */
3871 if ((sname[0] == '@') && !sname[1])
3872 has_loaded_dynamically = FALSE;
3874 mkpath(ssnpath, "@", spath);
3876 if (has_loaded_dynamically)
3877 save_session(TRUE, NULL);
3879 fsession = fopen(spath, "rb");
3881 printmsg(messages[MSG_SEL_MISSING]);
3882 xdelay(XDELAY_INTERVAL_MS);
3886 if ((fread(&header, sizeof(header), 1, fsession) != 1)
3887 || (header.ver != SESSIONS_VERSION)
3888 || (fread(&cfg, sizeof(cfg), 1, fsession) != 1))
3891 g_ctx[cfg.curctx].c_name[0] = g_ctx[cfg.curctx].c_last[0]
3892 = g_ctx[cfg.curctx].c_fltr[0] = g_ctx[cfg.curctx].c_fltr[1] = '\0';
3894 for (; i < CTX_MAX; ++i)
3895 if ((fread(&g_ctx[i].c_cfg, sizeof(settings), 1, fsession) != 1)
3896 || (fread(&g_ctx[i].color, sizeof(uint_t), 1, fsession) != 1)
3897 || (header.nameln[i] > 0
3898 && fread(g_ctx[i].c_name, header.nameln[i], 1, fsession) != 1)
3899 || (header.lastln[i] > 0
3900 && fread(g_ctx[i].c_last, header.lastln[i], 1, fsession) != 1)
3901 || (header.fltrln[i] > 0
3902 && fread(g_ctx[i].c_fltr, header.fltrln[i], 1, fsession) != 1)
3903 || (header.pathln[i] > 0
3904 && fread(g_ctx[i].c_path, header.pathln[i], 1, fsession) != 1))
3907 *path = g_ctx[cfg.curctx].c_path;
3908 *lastdir = g_ctx[cfg.curctx].c_last;
3909 *lastname = g_ctx[cfg.curctx].c_name;
3910 printptr = cfg.showdetail ? &printent_long : &printent;
3911 set_sort_flags('\0'); /* Set correct sort options */
3918 printmsg(messages[MSG_FAILED]);
3919 xdelay(XDELAY_INTERVAL_MS);
3927 static uchar_t get_free_ctx(void)
3929 uchar_t r = cfg.curctx;
3932 r = (r + 1) & ~CTX_MAX;
3933 while (g_ctx[r].c_cfg.ctxactive && (r != cfg.curctx));
3939 * Gets only a single line (that's what we need
3940 * for now) or shows full command output in pager.
3942 * If page is valid, returns NULL
3944 static char *get_output(char *buf, const size_t bytes, const char *file,
3945 const char *arg1, const char *arg2, const bool page)
3953 if (pipe(pipefd) == -1)
3956 for (tmp = 0; tmp < 2; ++tmp) {
3957 /* Get previous flags */
3958 flags = fcntl(pipefd[tmp], F_GETFL, 0);
3960 /* Set bit for non-blocking flag */
3961 flags |= O_NONBLOCK;
3963 /* Change flags on fd */
3964 fcntl(pipefd[tmp], F_SETFL, flags);
3971 dup2(pipefd[1], STDOUT_FILENO);
3972 dup2(pipefd[1], STDERR_FILENO);
3974 execlp(file, file, arg1, arg2, NULL);
3975 _exit(EXIT_SUCCESS);
3979 waitpid(pid, &tmp, 0);
3983 pf = fdopen(pipefd[0], "r");
3985 ret = fgets(buf, bytes, pf);
3995 /* Show in pager in child */
3996 dup2(pipefd[0], STDIN_FILENO);
3998 spawn(pager, NULL, NULL, F_CLI);
3999 _exit(EXIT_SUCCESS);
4003 waitpid(pid, &tmp, 0);
4009 static void pipetof(char *cmd, FILE *fout)
4011 FILE *fin = popen(cmd, "r");
4014 while (fgets(g_buf, CMD_LEN_MAX - 1, fin))
4015 fprintf(fout, "%s", g_buf);
4021 * Follows the stat(1) output closely
4023 static bool show_stats(const char *fpath, const struct stat *sb)
4027 char *p, *begin = g_buf;
4030 fd = create_tmp_file();
4034 r = xstrsncpy(g_buf, "stat \"", PATH_MAX);
4035 r += xstrsncpy(g_buf + r - 1, fpath, PATH_MAX);
4036 g_buf[r - 2] = '\"';
4037 g_buf[r - 1] = '\0';
4040 fp = fdopen(fd, "w");
4048 if (S_ISREG(sb->st_mode)) {
4049 /* Show file(1) output */
4050 p = get_output(g_buf, CMD_LEN_MAX, "file", "-b", fpath, FALSE);
4052 fprintf(fp, "\n\n ");
4056 fprintf(fp, " %s\n", begin);
4062 fprintf(fp, " %s\n ", begin);
4064 #ifdef FILE_MIME_OPTS
4065 /* Show the file MIME type */
4066 get_output(g_buf, CMD_LEN_MAX, "file", FILE_MIME_OPTS, fpath, FALSE);
4067 fprintf(fp, "%s", g_buf);
4076 spawn(pager, g_tmpfpath, NULL, F_CLI);
4081 static bool xchmod(const char *fpath, mode_t mode)
4083 /* (Un)set (S_IXUSR | S_IXGRP | S_IXOTH) */
4084 (0100 & mode) ? (mode &= ~0111) : (mode |= 0111);
4086 return (chmod(fpath, mode) == 0);
4089 static size_t get_fs_info(const char *path, bool type)
4093 if (statvfs(path, &svb) == -1)
4096 if (type == CAPACITY)
4097 return (size_t)svb.f_blocks << ffs((int)(svb.f_frsize >> 1));
4099 return (size_t)svb.f_bavail << ffs((int)(svb.f_frsize >> 1));
4102 /* List or extract archive */
4103 static void handle_archive(char *fpath, char op)
4105 char arg[] = "-tvf"; /* options for tar/bsdtar to list files */
4108 if (getutil(utils[UTIL_ATOOL])) {
4109 util = utils[UTIL_ATOOL];
4112 } else if (getutil(utils[UTIL_BSDTAR])) {
4113 util = utils[UTIL_BSDTAR];
4116 } else if (is_suffix(fpath, ".zip")) {
4117 util = utils[UTIL_UNZIP];
4118 arg[1] = (op == 'l') ? 'v' /* verbose listing */ : '\0';
4121 util = utils[UTIL_TAR];
4126 if (op == 'x') /* extract */
4127 spawn(util, arg, fpath, F_NORMAL);
4129 get_output(NULL, 0, util, arg, fpath, TRUE);
4132 static char *visit_parent(char *path, char *newpath, int *presel)
4136 /* There is no going back */
4137 if (istopdir(path)) {
4138 /* Continue in type-to-nav mode, if enabled */
4139 if (cfg.filtermode && presel)
4144 /* Use a copy as xdirname() may change the string passed */
4146 xstrsncpy(newpath, path, PATH_MAX);
4150 dir = xdirname(newpath);
4151 if (chdir(dir) == -1) {
4159 static void valid_parent(char *path, char *lastname)
4162 xstrsncpy(lastname, xbasename(path), NAME_MAX + 1);
4164 while (!istopdir(path))
4165 if (visit_parent(path, NULL, NULL))
4169 xdelay(XDELAY_INTERVAL_MS);
4172 /* Create non-existent parents and a file or dir */
4173 static bool xmktree(char *path, bool dir)
4181 /* Skip the first '/' */
4184 while (*p != '\0') {
4193 /* Create folder from path to '\0' inserted at p */
4194 if (mkdir(path, 0777) == -1 && errno != EEXIST) {
4196 // XDG_CONFIG_HOME contains a directory
4197 // that is read-only, but the full path
4199 // Try to continue and see what happens.
4200 // TODO: Find a more robust solution.
4201 if (errno == B_READ_ONLY_DEVICE)
4204 DPRINTF_S("mkdir1!");
4205 DPRINTF_S(strerror(errno));
4219 if (mkdir(path, 0777) == -1 && errno != EEXIST) {
4220 DPRINTF_S("mkdir2!");
4221 DPRINTF_S(strerror(errno));
4225 int fd = open(path, O_CREAT, 0666);
4227 if (fd == -1 && errno != EEXIST) {
4229 DPRINTF_S(strerror(errno));
4239 static bool archive_mount(char *newpath)
4241 char *str = "install archivemount";
4242 char *dir, *cmd = str + 8; /* Start of "archivemount" */
4243 char *name = pdents[cur].name;
4244 size_t len = pdents[cur].nlen;
4245 char mntpath[PATH_MAX];
4247 if (!getutil(cmd)) {
4252 dir = xstrdup(name);
4254 printmsg(messages[MSG_FAILED]);
4259 if (dir[--len] == '.') {
4266 /* Create the mount point */
4267 mkpath(cfgpath, toks[TOK_MNT], mntpath);
4268 mkpath(mntpath, dir, newpath);
4271 if (!xmktree(newpath, TRUE)) {
4279 if (spawn(cmd, name, newpath, F_NORMAL)) {
4280 printmsg(messages[MSG_FAILED]);
4287 static bool remote_mount(char *newpath)
4289 uchar_t flag = F_CLI;
4292 bool r = getutil(utils[UTIL_RCLONE]), s = getutil(utils[UTIL_SSHFS]);
4293 char mntpath[PATH_MAX];
4296 printmsg("install sshfs/rclone");
4301 opt = get_input(messages[MSG_REMOTE_OPTS]);
4303 opt = (!s) ? 'r' : 's';
4306 env = xgetenv("NNN_SSHFS", utils[UTIL_SSHFS]);
4307 else if (opt == 'r') {
4308 flag |= F_NOWAIT | F_NOTRACE;
4309 env = xgetenv("NNN_RCLONE", "rclone mount");
4311 printmsg(messages[MSG_INVALID_KEY]);
4315 tmp = xreadline(NULL, "host[:dir] > ");
4317 printmsg(messages[MSG_CANCEL]);
4321 char *div = strchr(tmp, ':');
4326 /* Create the mount point */
4327 mkpath(cfgpath, toks[TOK_MNT], mntpath);
4328 mkpath(mntpath, tmp, newpath);
4329 if (!xmktree(newpath, TRUE)) {
4334 if (!div) { /* Convert "host" to "host:" */
4335 size_t len = xstrlen(tmp);
4338 tmp[len + 1] = '\0';
4342 /* Connect to remote */
4344 if (spawn(env, tmp, newpath, flag)) {
4345 printmsg(messages[MSG_FAILED]);
4349 spawn(env, tmp, newpath, flag);
4350 printmsg(messages[MSG_RCLONE_DELAY]);
4351 xdelay(XDELAY_INTERVAL_MS << 2); /* Set 4 times the usual delay */
4358 * Unmounts if the directory represented by name is a mount point.
4359 * Otherwise, asks for hostname
4360 * Returns TRUE if directory needs to be refreshed *.
4362 static bool unmount(char *name, char *newpath, int *presel, char *currentpath)
4364 #if defined (__APPLE__) || defined (__FreeBSD__)
4365 static char cmd[] = "umount";
4367 static char cmd[] = "fusermount3"; /* Arch Linux utility */
4368 static bool found = FALSE;
4371 struct stat sb, psb;
4373 bool parent = FALSE;
4374 bool hovered = TRUE;
4375 char mntpath[PATH_MAX];
4377 #if !defined ( __APPLE__) && !defined (__FreeBSD__)
4378 /* On Ubuntu it's fusermount */
4379 if (!found && !getutil(cmd)) {
4385 mkpath(cfgpath, toks[TOK_MNT], mntpath);
4387 if (tmp && strcmp(mntpath, currentpath) == 0) {
4388 mkpath(mntpath, tmp, newpath);
4389 child = lstat(newpath, &sb) != -1;
4390 parent = lstat(xdirname(newpath), &psb) != -1;
4391 if (!child && !parent) {
4397 if (!tmp || !child || !S_ISDIR(sb.st_mode) || (child && parent && sb.st_dev == psb.st_dev)) {
4398 tmp = xreadline(NULL, messages[MSG_HOSTNAME]);
4404 /* Create the mount point */
4405 mkpath(mntpath, tmp, newpath);
4406 if (!xdiraccess(newpath)) {
4411 #if defined (__APPLE__) || defined (__FreeBSD__)
4412 if (spawn(cmd, newpath, NULL, F_NORMAL)) {
4414 if (spawn(cmd, "-u", newpath, F_NORMAL)) {
4416 if (!xconfirm(get_input(messages[MSG_LAZY])))
4420 if (spawn(cmd, "-l", newpath, F_NORMAL)) {
4421 #elif defined (__FreeBSD__)
4422 if (spawn(cmd, "-f", newpath, F_NORMAL)) {
4424 if (spawn(cmd, "-uz", newpath, F_NORMAL)) {
4426 printwait(messages[MSG_FAILED], presel);
4431 if (rmdir(newpath) == -1) {
4439 static void lock_terminal(void)
4441 spawn(xgetenv("NNN_LOCKER", utils[UTIL_LOCKER]), NULL, NULL, F_CLI);
4444 static void printkv(kv *kvarr, FILE *fp, uchar_t max, uchar_t id)
4446 char *val = (id == NNN_BMS) ? bmstr : pluginstr;
4448 for (uchar_t i = 0; i < max && kvarr[i].key; ++i)
4449 fprintf(fp, " %c: %s\n", (char)kvarr[i].key, val + kvarr[i].off);
4452 static void printkeys(kv *kvarr, char *buf, uchar_t max)
4456 for (; i < max && kvarr[i].key; ++i) {
4458 buf[(i << 1) + 1] = kvarr[i].key;
4464 static size_t handle_bookmark(const char *bmark, char *newpath)
4467 size_t r = xstrsncpy(g_buf, messages[MSG_BOOKMARK_KEYS], CMD_LEN_MAX);
4469 if (bmark) { /* There is a marked directory */
4475 printkeys(bookmark, g_buf + r - 1, maxbm);
4479 fd = get_input(NULL);
4480 if (fd == ',') /* Visit marked directory */
4481 bmark ? xstrsncpy(newpath, bmark, PATH_MAX) : (r = MSG_NOT_SET);
4482 else if (!get_kv_val(bookmark, newpath, fd, maxbm, NNN_BMS))
4483 r = MSG_INVALID_KEY;
4485 if (!r && chdir(newpath) == -1)
4492 * The help string tokens (each line) start with a HEX value
4493 * which indicates the number of spaces to print before the
4494 * particular token. This method was chosen instead of a flat
4495 * string because the number of bytes in help was increasing
4496 * the binary size by around a hundred bytes. This would only
4497 * have increased as we keep adding new options.
4499 static void show_help(const char *path)
4503 const char *start, *end;
4504 const char helpstr[] = {
4507 "9Up k Up%-16cPgUp ^U Scroll up\n"
4508 "9Dn j Down%-14cPgDn ^D Scroll down\n"
4509 "9Lt h Parent%-12c~ ` @ - HOME, /, start, last\n"
4510 "5Ret Rt l Open%-20c' First file/match\n"
4511 "9g ^A Top%-21c. Toggle hidden\n"
4512 "9G ^E End%-21c+ Toggle auto-advance\n"
4513 "9b ^/ Bookmark key%-12c, Mark CWD\n"
4514 "a1-4 Context 1-4%-7c(Sh)Tab Cycle context\n"
4515 "aEsc Send to FIFO%-11c^L Redraw\n"
4516 "cQ Pick/err, quit%-9c^G QuitCD\n"
4517 "cq Quit context%-6c2Esc ^Q Quit\n"
4519 "1FILTER & PROMPT\n"
4520 "c/ Filter%-12cAlt+Esc Clear filter & redraw\n"
4521 "aEsc Exit prompt%-12c^L Clear prompt/last filter\n"
4522 "b^N Toggle type-to-nav%-0c\n"
4524 "9o ^O Open with...%-12cn Create new/link\n"
4525 "9f ^F File details%-12cd Detail mode toggle\n"
4526 "b^R Rename/dup%-14cr Batch rename\n"
4527 "cz Archive%-17ce Edit file\n"
4528 "c* Toggle exe%-14c> Export list\n"
4529 "5Space ^J (Un)select%-7cm ^Space Mark range/clear sel\n"
4530 "ca Select all%-14cA Invert sel\n"
4531 "9p ^P Copy sel here%-8cw ^W Cp/mv sel as\n"
4532 "9v ^V Move sel here%-11cE Edit sel\n"
4535 "8Alt ; Select plugin%-11c= Launch app\n"
4536 "9! ^] Shell%-19c] Cmd prompt\n"
4537 "cc Connect remote%-10cu Unmount remote/archive\n"
4538 "9t ^T Sort toggles%-12cs Manage session\n"
4539 "cT Set time type%-11c0 Lock\n"
4542 fd = create_tmp_file();
4546 fp = fdopen(fd, "w");
4552 if (g_state.fortune && getutil("fortune"))
4554 pipetof("fortune -s", fp);
4556 pipetof("fortune", fp);
4559 start = end = helpstr;
4562 snprintf(g_buf, CMD_LEN_MAX, "%*c%.*s",
4563 xchartohex(*start), ' ', (int)(end - start), start + 1);
4564 fprintf(fp, g_buf, ' ');
4571 fprintf(fp, "\nVOLUME: %s of ", coolsize(get_fs_info(path, FREE)));
4572 fprintf(fp, "%s free\n\n", coolsize(get_fs_info(path, CAPACITY)));
4575 fprintf(fp, "BOOKMARKS\n");
4576 printkv(bookmark, fp, maxbm, NNN_BMS);
4581 fprintf(fp, "PLUGIN KEYS\n");
4582 printkv(plug, fp, maxplug, NNN_PLUG);
4586 for (uchar_t i = NNN_OPENER; i <= NNN_TRASH; ++i) {
4587 start = getenv(env_cfg[i]);
4589 fprintf(fp, "%s: %s\n", env_cfg[i], start);
4593 fprintf(fp, "SELECTION FILE: %s\n", selpath);
4595 fprintf(fp, "\nv%s\n%s\n", VERSION, GENERAL_INFO);
4599 spawn(pager, g_tmpfpath, NULL, F_CLI);
4603 static bool run_cmd_as_plugin(const char *file, char *runfile, uchar_t flags)
4607 xstrsncpy(g_buf, file, PATH_MAX);
4609 len = xstrlen(g_buf);
4610 if (len > 1 && g_buf[len - 1] == '*') {
4611 flags &= ~F_CONFIRM; /* Skip user confirmation */
4612 g_buf[len - 1] = '\0'; /* Get rid of trailing no confirmation symbol */
4616 if (is_suffix(g_buf, " $nnn"))
4617 g_buf[len - 5] = '\0'; /* Set `\0` to clear ' $nnn' suffix */
4621 spawn(g_buf, runfile, NULL, flags);
4625 static bool plctrl_init(void)
4629 /* g_tmpfpath is used to generate tmp file names */
4630 g_tmpfpath[tmpfplen - 1] = '\0';
4631 len = xstrsncpy(g_pipepath, g_tmpfpath, TMP_LEN_MAX);
4632 g_pipepath[len - 1] = '/';
4633 len = xstrsncpy(g_pipepath + len, "nnn-pipe.", TMP_LEN_MAX - len) + len;
4634 xstrsncpy(g_pipepath + len - 1, xitoa(getpid()), TMP_LEN_MAX - len);
4635 setenv(env_cfg[NNN_PIPE], g_pipepath, TRUE);
4637 return EXIT_SUCCESS;
4640 static void rmlistpath(void)
4643 DPRINTF_S(__func__);
4644 DPRINTF_S(listpath);
4645 spawn("rm -rf", listpath, NULL, F_NOTRACE | F_MULTI);
4646 /* Do not free if program was started in list mode */
4647 if (listpath != initpath)
4653 static ssize_t read_nointr(int fd, void *buf, size_t count)
4658 len = read(fd, buf, count);
4659 while (len == -1 && errno == EINTR);
4664 static void readpipe(int fd, char **path, char **lastname, char **lastdir)
4667 char ctx, *nextpath = NULL;
4668 ssize_t len = read_nointr(fd, g_buf, 1);
4673 if (g_buf[0] == '+')
4674 ctx = (char)(get_free_ctx() + 1);
4675 else if (g_buf[0] == '-') { /* Clear selection on '-' */
4678 } else if (g_buf[0] < '0')
4681 ctx = g_buf[0] - '0';
4686 len = read_nointr(fd, g_buf, 1);
4693 len = read_nointr(fd, g_buf, PATH_MAX);
4697 /* Terminate the path read */
4700 } else if (op == 'l') {
4701 /* Remove last list mode path, if any */
4704 nextpath = load_input(fd, *path);
4708 if (ctx == 0 || ctx == cfg.curctx + 1) { /* Same context */
4709 xstrsncpy(*lastdir, *path, PATH_MAX);
4710 xstrsncpy(*path, nextpath, PATH_MAX);
4712 } else { /* New context */
4714 /* Deactivate the new context and build from scratch */
4715 g_ctx[r].c_cfg.ctxactive = 0;
4716 savecurctx(&cfg, nextpath, pdents[cur].name, r);
4717 *path = g_ctx[r].c_path;
4718 *lastdir = g_ctx[r].c_last;
4719 *lastname = g_ctx[r].c_name;
4724 static bool run_selected_plugin(char **path, const char *file, char *runfile, char **lastname, char **lastdir)
4726 bool cmd_as_plugin = FALSE;
4729 if (!g_state.pluginit) {
4731 g_state.pluginit = 1;
4735 flags = F_MULTI | F_CONFIRM;
4737 /* Get rid of preceding _ */
4742 /* Check if GUI flags are to be used */
4744 flags = F_NOTRACE | F_NOWAIT;
4750 run_cmd_as_plugin(file, runfile, flags);
4754 cmd_as_plugin = TRUE;
4757 if (mkfifo(g_pipepath, 0600) != 0)
4758 return EXIT_FAILURE;
4762 if (fork() == 0) { // In child
4763 int wfd = open(g_pipepath, O_WRONLY | O_CLOEXEC);
4766 _exit(EXIT_FAILURE);
4768 if (!cmd_as_plugin) {
4769 /* Generate absolute path to plugin */
4770 mkpath(plgpath, file, g_buf);
4772 if (runfile && runfile[0]) {
4773 xstrsncpy(*lastname, runfile, NAME_MAX);
4774 spawn(g_buf, *lastname, *path, 0);
4776 spawn(g_buf, NULL, *path, 0);
4778 run_cmd_as_plugin(file, runfile, flags);
4781 _exit(EXIT_SUCCESS);
4787 rfd = open(g_pipepath, O_RDONLY);
4788 while (rfd == -1 && errno == EINTR);
4790 readpipe(rfd, path, lastname, lastdir);
4800 static bool plugscript(const char *plugin, uchar_t flags)
4802 mkpath(plgpath, plugin, g_buf);
4803 if (!access(g_buf, X_OK)) {
4804 spawn(g_buf, NULL, NULL, flags);
4811 static bool launch_app(char *newpath)
4814 char *tmp = newpath;
4816 mkpath(plgpath, utils[UTIL_LAUNCH], newpath);
4818 if (!getutil(utils[UTIL_FZF]) || access(newpath, X_OK) < 0) {
4819 tmp = xreadline(NULL, messages[MSG_APP_NAME]);
4820 r = F_NOWAIT | F_NOTRACE | F_MULTI;
4823 if (tmp && *tmp) // NOLINT
4824 spawn(tmp, (r == F_NORMAL) ? "0" : NULL, NULL, r);
4829 /* Returns TRUE if at least command was run */
4830 static bool prompt_run(const char *current)
4835 setenv(envs[ENV_NCUR], current, 1);
4839 if (g_state.picker) {
4841 tmp = xreadline(NULL, ">>> ");
4844 tmp = getreadline("\n>>> ");
4846 if (tmp && *tmp) { // NOLINT
4848 spawn(shell, "-c", tmp, F_CLI | F_CONFIRM);
4856 static bool handle_cmd(enum action sel, const char *current, char *newpath)
4860 if (sel == SEL_RUNCMD)
4861 return prompt_run(current);
4863 if (sel == SEL_LAUNCH)
4864 return launch_app(newpath);
4866 /* Set nnn nesting level */
4867 char *tmp = getenv(env_cfg[NNNLVL]);
4868 int r = tmp ? atoi(tmp) : 0;
4870 setenv(env_cfg[NNNLVL], xitoa(r + 1), 1);
4871 setenv(envs[ENV_NCUR], current, 1);
4872 spawn(shell, NULL, NULL, F_CLI);
4873 setenv(env_cfg[NNNLVL], xitoa(r), 1);
4877 static int sum_bsize(const char *UNUSED(fpath), const struct stat *sb, int typeflag, struct FTW *UNUSED(ftwbuf))
4880 && ((typeflag == FTW_F && (sb->st_nlink <= 1 || test_set_bit((uint_t)sb->st_ino)))
4881 || typeflag == FTW_D))
4882 ent_blocks += sb->st_blocks;
4888 static int sum_asize(const char *UNUSED(fpath), const struct stat *sb, int typeflag, struct FTW *UNUSED(ftwbuf))
4891 && ((typeflag == FTW_F && (sb->st_nlink <= 1 || test_set_bit((uint_t)sb->st_ino)))
4892 || typeflag == FTW_D))
4893 ent_blocks += sb->st_size;
4899 static void dentfree(void)
4906 static blkcnt_t dirwalk(char *path, struct stat *psb)
4909 static uint_t open_max;
4911 /* Increase current open file descriptor limit */
4913 open_max = max_openfds();
4918 addstr(xbasename(path));
4919 addstr(" [^C aborts]\n");
4923 if (nftw(path, nftw_fn, open_max, FTW_MOUNT | FTW_PHYS) < 0) {
4925 if (nftw(path, nftw_fn, OPEN_MAX, FTW_MOUNT | FTW_PHYS) < 0) {
4927 DPRINTF_S("nftw failed");
4928 return cfg.apparentsz ? psb->st_size : psb->st_blocks;
4934 /* Skip self and parent */
4935 static bool selforparent(const char *path)
4937 return path[0] == '.' && (path[1] == '\0' || (path[1] == '.' && path[2] == '\0'));
4940 static int dentfill(char *path, struct entry **ppdents)
4942 uchar_t entflags = 0;
4943 int n = 0, flags = 0;
4946 char *namep, *pnb, *buf = NULL;
4947 struct entry *dentp;
4948 size_t off = 0, namebuflen = NAMEBUF_INCR;
4949 struct stat sb_path, sb;
4950 DIR *dirp = opendir(path);
4952 DPRINTF_S(__func__);
4957 int fd = dirfd(dirp);
4962 buf = (char *)alloca(xstrlen(path) + NAME_MAX + 2);
4966 if (fstatat(fd, path, &sb_path, 0) == -1)
4970 ihashbmp = calloc(1, HASH_OCTETS << 3);
4974 memset(ihashbmp, 0, HASH_OCTETS << 3);
4976 attron(COLOR_PAIR(cfg.curctx + 1));
4979 #if _POSIX_C_SOURCE >= 200112L
4980 posix_fadvise(fd, 0, 0, POSIX_FADV_SEQUENTIAL);
4987 #if defined(__sun) || defined(__HAIKU__)
4988 flags = AT_SYMLINK_NOFOLLOW; /* no d_type */
4990 if (cfg.blkorder || dp->d_type == DT_UNKNOWN) {
4992 * Optimization added for filesystems which support dirent.d_type
4995 * - the symlink size is set to 0
4996 * - the modification time of the symlink is set to that of the target file
4998 flags = AT_SYMLINK_NOFOLLOW;
5005 if (selforparent(namep))
5008 if (!cfg.showhidden && namep[0] == '.') {
5012 if (fstatat(fd, namep, &sb, AT_SYMLINK_NOFOLLOW) == -1)
5015 if (S_ISDIR(sb.st_mode)) {
5016 if (sb_path.st_dev == sb.st_dev) { // NOLINT
5017 mkpath(path, namep, buf);
5019 dir_blocks += dirwalk(buf, &sb);
5021 if (g_state.interrupt)
5025 /* Do not recount hard links */
5026 if (sb.st_nlink <= 1 || test_set_bit((uint_t)sb.st_ino))
5027 dir_blocks += (cfg.apparentsz ? sb.st_size : sb.st_blocks);
5034 if (fstatat(fd, namep, &sb, flags) == -1) {
5035 if (flags || (fstatat(fd, namep, &sb, AT_SYMLINK_NOFOLLOW) == -1)) {
5040 DPRINTF_S(strerror(errno));
5043 entflags = FILE_MISSING;
5044 memset(&sb, 0, sizeof(struct stat));
5045 } else /* Orphaned symlink */
5046 entflags = SYM_ORPHAN;
5049 if (n == total_dents) {
5050 total_dents += ENTRY_INCR;
5051 *ppdents = xrealloc(*ppdents, total_dents * sizeof(**ppdents));
5057 DPRINTF_P(*ppdents);
5060 /* If not enough bytes left to copy a file name of length NAME_MAX, re-allocate */
5061 if (namebuflen - off < NAME_MAX + 1) {
5062 namebuflen += NAMEBUF_INCR;
5065 pnamebuf = (char *)xrealloc(pnamebuf, namebuflen);
5071 DPRINTF_P(pnamebuf);
5073 /* realloc() may result in memory move, we must re-adjust if that happens */
5074 if (pnb != pnamebuf) {
5076 dentp->name = pnamebuf;
5078 for (int count = 1; count < n; ++dentp, ++count)
5079 /* Current file name starts at last file name start + length */
5080 (dentp + 1)->name = (char *)((size_t)dentp->name + dentp->nlen);
5084 dentp = *ppdents + n;
5086 /* Selection file name */
5087 dentp->name = (char *)((size_t)pnamebuf + off);
5088 dentp->nlen = xstrsncpy(dentp->name, namep, NAME_MAX + 1);
5091 /* Copy other fields */
5092 dentp->t = ((cfg.timetype == T_MOD)
5094 : ((cfg.timetype == T_ACCESS) ? sb.st_atime : sb.st_ctime));
5095 #if !(defined(__sun) || defined(__HAIKU__))
5096 if (!flags && dp->d_type == DT_LNK) {
5097 /* Do not add sizes for links */
5098 dentp->mode = (sb.st_mode & ~S_IFMT) | S_IFLNK;
5099 dentp->size = listpath ? sb.st_size : 0;
5101 dentp->mode = sb.st_mode;
5102 dentp->size = sb.st_size;
5105 dentp->mode = sb.st_mode;
5106 dentp->size = sb.st_size;
5110 dentp->uid = sb.st_uid;
5111 dentp->gid = sb.st_gid;
5114 dentp->flags = S_ISDIR(sb.st_mode) ? 0 : ((sb.st_nlink > 1) ? HARD_LINK : 0);
5116 dentp->flags |= entflags;
5121 if (S_ISDIR(sb.st_mode)) {
5122 num_saved = num_files + 1;
5123 mkpath(path, namep, buf);
5125 /* Need to show the disk usage of this dir */
5126 dentp->blocks = dirwalk(buf, &sb);
5128 if (sb_path.st_dev == sb.st_dev) // NOLINT
5129 dir_blocks += dentp->blocks;
5131 num_files = num_saved;
5133 if (g_state.interrupt)
5136 dentp->blocks = (cfg.apparentsz ? sb.st_size : sb.st_blocks);
5137 /* Do not recount hard links */
5138 if (sb.st_nlink <= 1 || test_set_bit((uint_t)sb.st_ino))
5139 dir_blocks += dentp->blocks;
5145 /* Flag if this is a dir or symlink to a dir */
5146 if (S_ISLNK(sb.st_mode)) {
5148 fstatat(fd, namep, &sb, 0);
5151 if (S_ISDIR(sb.st_mode))
5152 dentp->flags |= DIR_OR_LINK_TO_DIR;
5153 #if !(defined(__sun) || defined(__HAIKU__)) /* no d_type */
5154 } else if (dp->d_type == DT_DIR || ((dp->d_type == DT_LNK || dp->d_type == DT_UNKNOWN) && S_ISDIR(sb.st_mode))) {
5155 dentp->flags |= DIR_OR_LINK_TO_DIR;
5160 } while ((dp = readdir(dirp)));
5164 attroff(COLOR_PAIR(cfg.curctx + 1));
5166 /* Should never be null */
5167 if (closedir(dirp) == -1)
5173 static void populate(char *path, char *lastname)
5176 struct timespec ts1, ts2;
5178 clock_gettime(CLOCK_REALTIME, &ts1); /* Use CLOCK_MONOTONIC on FreeBSD */
5181 ndents = dentfill(path, &pdents);
5185 ENTSORT(pdents, ndents, entrycmpfn);
5188 clock_gettime(CLOCK_REALTIME, &ts2);
5189 DPRINTF_U(ts2.tv_nsec - ts1.tv_nsec);
5192 /* Find cur from history */
5193 /* No NULL check for lastname, always points to an array */
5194 move_cursor(*lastname ? dentfind(lastname, ndents) : 0, 0);
5196 // Force full redraw
5197 last_curscroll = -1;
5201 static void notify_fifo(bool force)
5207 fifofd = open(fifopath, O_WRONLY|O_NONBLOCK|O_CLOEXEC);
5210 /* Unexpected error, the FIFO file might have been removed */
5211 /* We give up FIFO notification */
5217 static struct entry lastentry;
5219 if (!force && !memcmp(&lastentry, &pdents[cur], sizeof(struct entry)))
5222 lastentry = pdents[cur];
5224 char path[PATH_MAX];
5225 size_t len = mkpath(g_ctx[cfg.curctx].c_path, ndents ? pdents[cur].name : "", path);
5227 path[len - 1] = '\n';
5229 ssize_t ret = write(fifofd, path, len);
5231 if (ret != (ssize_t)len && !(ret == -1 && (errno == EAGAIN || errno == EPIPE))) {
5232 DPRINTF_S(strerror(errno));
5237 static void move_cursor(int target, int ignore_scrolloff)
5239 int onscreen = xlines - 4; /* Leave top 2 and bottom 2 lines */
5241 target = MAX(0, MIN(ndents - 1, target));
5242 last_curscroll = curscroll;
5246 if (!ignore_scrolloff) {
5247 int delta = target - last;
5248 int scrolloff = MIN(SCROLLOFF, onscreen >> 1);
5251 * When ignore_scrolloff is 1, the cursor can jump into the scrolloff
5252 * margin area, but when ignore_scrolloff is 0, act like a boa
5253 * constrictor and squeeze the cursor towards the middle region of the
5254 * screen by allowing it to move inward and disallowing it to move
5255 * outward (deeper into the scrolloff margin area).
5257 if (((cur < (curscroll + scrolloff)) && delta < 0)
5258 || ((cur > (curscroll + onscreen - scrolloff - 1)) && delta > 0))
5261 curscroll = MIN(curscroll, MIN(cur, ndents - onscreen));
5262 curscroll = MAX(curscroll, MAX(cur - (onscreen - 1), 0));
5269 static void handle_screen_move(enum action sel)
5275 if (ndents && (cfg.rollover || (cur != ndents - 1)))
5276 move_cursor((cur + 1) % ndents, 0);
5279 if (ndents && (cfg.rollover || cur))
5280 move_cursor((cur + ndents - 1) % ndents, 0);
5283 onscreen = xlines - 4;
5284 move_cursor(curscroll + (onscreen - 1), 1);
5285 curscroll += onscreen - 1;
5288 onscreen = xlines - 4;
5289 move_cursor(curscroll + (onscreen - 1), 1);
5290 curscroll += onscreen >> 1;
5292 case SEL_PGUP: // fallthrough
5293 onscreen = xlines - 4;
5294 move_cursor(curscroll, 1);
5295 curscroll -= onscreen - 1;
5298 onscreen = xlines - 4;
5299 move_cursor(curscroll, 1);
5300 curscroll -= onscreen >> 1;
5306 move_cursor(ndents - 1, 1);
5308 default: /* case SEL_FIRST */
5310 int c = get_input(messages[MSG_FIRST]);
5317 int r = (c == TOUPPER(*pdents[cur].name)) ? (cur + 1) : 0;
5319 for (; r < ndents; ++r) {
5320 if (((c == '\'') && !(pdents[r].flags & DIR_OR_LINK_TO_DIR))
5321 || (c == TOUPPER(*pdents[r].name))) {
5322 move_cursor((r) % ndents, 0);
5331 static void copynextname(char *lastname)
5334 cur += (cur != (ndents - 1)) ? 1 : -1;
5340 static int handle_context_switch(enum action sel)
5345 case SEL_CYCLE: // fallthrough
5347 /* visit next and previous contexts */
5349 if (sel == SEL_CYCLE)
5351 r = (r + 1) & ~CTX_MAX;
5352 while (!g_ctx[r].c_cfg.ctxactive);
5355 r = (r + (CTX_MAX - 1)) & (CTX_MAX - 1);
5356 while (!g_ctx[r].c_cfg.ctxactive);
5358 default: /* SEL_CTXN */
5359 if (sel >= SEL_CTX1) /* CYCLE keys are lesser in value */
5360 r = sel - SEL_CTX1; /* Save the next context id */
5362 if (cfg.curctx == r) {
5363 if (sel == SEL_CYCLE)
5364 (r == CTX_MAX - 1) ? (r = 0) : ++r;
5365 else if (sel == SEL_CYCLER)
5366 (r == 0) ? (r = CTX_MAX - 1) : --r;
5371 if (g_state.selmode)
5372 lastappendpos = selbufpos;
5378 static int set_sort_flags(int r)
5382 /* Set the correct input in case of a session load */
5384 if (cfg.apparentsz) {
5387 } else if (cfg.blkorder) {
5393 namecmpfn = &xstrverscasecmp;
5396 entrycmpfn = &reventrycmp;
5397 } else if (r == CONTROL('T')) {
5398 /* Cycling order: clear -> size -> time -> clear */
5401 else if (cfg.sizeorder)
5408 case 'a': /* Apparent du */
5409 cfg.apparentsz ^= 1;
5410 if (cfg.apparentsz) {
5411 nftw_fn = &sum_asize;
5417 case 'd': /* Disk usage */
5419 if (!cfg.apparentsz)
5421 nftw_fn = &sum_bsize;
5423 blk_shift = ffs(S_BLKSIZE) - 1;
5428 printptr = &printent_long;
5435 entrycmpfn = &entrycmp;
5437 endselection(); /* We are going to reload dir */
5447 entrycmpfn = &entrycmp;
5448 namecmpfn = &xstricmp;
5450 case 'e': /* File extension */
5457 entrycmpfn = &entrycmp;
5459 case 'r': /* Reverse sort */
5461 entrycmpfn = cfg.reverse ? &reventrycmp : &entrycmp;
5463 case 's': /* File size */
5470 entrycmpfn = &entrycmp;
5472 case 't': /* Time */
5479 entrycmpfn = &entrycmp;
5481 case 'v': /* Version */
5483 namecmpfn = cfg.version ? &xstrverscasecmp : &xstricmp;
5497 static bool set_time_type(int *presel)
5500 char buf[] = "'a'ccess / 'c'hange / 'm'od [ ]";
5502 buf[sizeof(buf) - 3] = cfg.timetype == T_MOD ? 'm' : (cfg.timetype == T_ACCESS ? 'a' : 'c');
5504 int r = get_input(buf);
5506 if (r == 'a' || r == 'c' || r == 'm') {
5507 r = (r == 'm') ? T_MOD : ((r == 'a') ? T_ACCESS : T_CHANGE);
5508 if (cfg.timetype != r) {
5511 if (cfg.filtermode || g_ctx[cfg.curctx].c_fltr[1])
5518 r = MSG_INVALID_KEY;
5521 printwait(messages[r], presel);
5526 static void statusbar(char *path)
5528 int i = 0, extnlen = 0;
5530 pEntry pent = &pdents[cur];
5537 /* Get the file extension for regular files */
5538 if (S_ISREG(pent->mode)) {
5539 i = (int)(pent->nlen - 1);
5540 ptr = xextension(pent->name, i);
5542 extnlen = i - (ptr - pent->name);
5543 if (!ptr || extnlen > 5 || extnlen < 2)
5549 attron(COLOR_PAIR(cfg.curctx + 1));
5551 printw("%d/%s ", cur + 1, xitoa(ndents));
5553 if (g_state.selmode) {
5556 if (g_state.rangesel)
5559 addstr(xitoa(nselected));
5567 if (cfg.blkorder) { /* du mode */
5570 xstrsncpy(buf, coolsize(dir_blocks << blk_shift), 12);
5572 printw("%cu:%s free:%s files:%lu %lldB %s\n",
5573 (cfg.apparentsz ? 'a' : 'd'), buf, coolsize(get_fs_info(path, FREE)),
5574 num_files, (long long)pent->blocks << blk_shift, ptr);
5575 } else { /* light or detail mode */
5576 char sort[] = "\0\0\0\0\0";
5578 if (getorderstr(sort))
5582 print_time(&pent->t);
5585 addstr(get_lsperms(pent->mode));
5588 if (g_state.uidgid) {
5589 struct passwd *pw = getpwuid(pent->uid);
5590 struct group *gr = getgrgid(pent->gid);
5592 addstr(pw ? pw->pw_name : xitoa(pent->uid));
5594 addstr(gr ? gr->gr_name : xitoa(pent->gid));
5598 if (S_ISLNK(pent->mode))
5600 i = readlink(pent->name, g_buf, PATH_MAX);
5602 addstr(coolsize(i >= 0 ? i : pent->size)); /* Show symlink size */
5604 if (i > 1) { /* Show symlink target */
5610 addstr(coolsize(pent->size));
5613 if (pent->flags & HARD_LINK)
5617 if (stat(pent->name, &sb) != -1) {
5619 addstr(xitoa((int)sb.st_nlink)); /* Show number of links */
5621 addstr(xitoa((int)sb.st_ino)); /* Show inode number */
5629 attroff(COLOR_PAIR(cfg.curctx + 1));
5635 static int adjust_cols(int n)
5637 /* Calculate the number of cols available to print entry name */
5638 if (cfg.showdetail) {
5639 /* Fallback to light mode if less than 35 columns */
5641 cfg.showdetail ^= 1;
5642 printptr = &printent;
5644 /* 3 more accounted for below */
5649 /* 3 = Preceding space, indicator, newline */
5650 #ifdef ICONS_ENABLED
5651 return (n - (g_state.oldcolor ? 3
5652 : 3 + xstrlen(ICON_PADDING_LEFT) + xstrlen(ICON_PADDING_RIGHT) + 1));
5658 static void draw_line(char *path, int ncols)
5662 ncols = adjust_cols(ncols);
5664 if (g_state.oldcolor && (pdents[last].flags & DIR_OR_LINK_TO_DIR)) {
5665 attron(COLOR_PAIR(cfg.curctx + 1) | A_BOLD);
5669 move(2 + last - curscroll, 0);
5670 printptr(&pdents[last], ncols, FALSE);
5672 if (g_state.oldcolor && (pdents[cur].flags & DIR_OR_LINK_TO_DIR)) {
5673 if (!dir) {/* First file is not a directory */
5674 attron(COLOR_PAIR(cfg.curctx + 1) | A_BOLD);
5677 } else if (dir) { /* Second file is not a directory */
5678 attroff(COLOR_PAIR(cfg.curctx + 1) | A_BOLD);
5682 move(2 + cur - curscroll, 0);
5683 printptr(&pdents[cur], ncols, TRUE);
5685 /* Must reset e.g. no files in dir */
5687 attroff(COLOR_PAIR(cfg.curctx + 1) | A_BOLD);
5692 static void redraw(char *path)
5697 int ncols = (xcols <= PATH_MAX) ? xcols : PATH_MAX;
5698 int onscreen = xlines - 4;
5705 if (ndents && (last_curscroll == curscroll))
5706 return draw_line(path, ncols);
5709 DPRINTF_S(__func__);
5714 /* Enforce scroll/cursor invariants */
5715 move_cursor(cur, 1);
5717 /* Fail redraw if < than 10 columns, context info prints 10 chars */
5718 if (ncols < MIN_DISPLAY_COLS) {
5719 printmsg(messages[MSG_FEW_COLUMNS]);
5727 for (i = 0; i < CTX_MAX; ++i) {
5728 if (!g_ctx[i].c_cfg.ctxactive)
5731 addch((i + '1') | (COLOR_PAIR(i + 1) | A_BOLD
5732 /* active: underline, current: reverse */
5733 | ((cfg.curctx != i) ? A_UNDERLINE : A_REVERSE)));
5735 if (i != CTX_MAX - 1)
5738 addstr("] "); /* 10 chars printed for contexts - "[1 2 3 4] " */
5740 attron(A_UNDERLINE | COLOR_PAIR(cfg.curctx + 1));
5743 bool in_home = set_tilde_in_path(path);
5744 char *ptr = in_home ? &path[homelen - 1] : path;
5746 i = (int)xstrlen(ptr);
5747 if ((i + MIN_DISPLAY_COLS) <= ncols)
5748 addnstr(ptr, ncols - MIN_DISPLAY_COLS);
5750 char *base = xmemrchr((uchar_t *)ptr, '/', i);
5759 if (ptr && (base != ptr)) {
5760 while (ptr < base) {
5762 i += 2; /* 2 characters added */
5763 if (ncols < i + MIN_DISPLAY_COLS) {
5764 base = NULL; /* Can't print more characters */
5776 addnstr(base, ncols - (MIN_DISPLAY_COLS + i));
5780 reset_tilde_in_path(path);
5782 attroff(A_UNDERLINE | COLOR_PAIR(cfg.curctx + 1));
5784 ncols = adjust_cols(ncols);
5786 /* Go to first entry */
5787 if (curscroll > 0) {
5794 if (g_state.oldcolor) {
5795 attron(COLOR_PAIR(cfg.curctx + 1) | A_BOLD);
5796 g_state.dircolor = 1;
5800 for (i = curscroll; i < ndents && i < curscroll + onscreen; ++i)
5801 printptr(&pdents[i], ncols, i == cur);
5803 /* Must reset e.g. no files in dir */
5804 if (g_state.dircolor) {
5805 attroff(COLOR_PAIR(cfg.curctx + 1) | A_BOLD);
5806 g_state.dircolor = 0;
5809 /* Go to first entry */
5811 move(xlines - 2, 0);
5818 static bool cdprep(char *lastdir, char *lastname, char *path, char *newpath)
5823 /* Save last working directory */
5824 xstrsncpy(lastdir, path, PATH_MAX);
5826 /* Save the newly opted dir in path */
5827 xstrsncpy(path, newpath, PATH_MAX);
5831 return cfg.filtermode;
5834 static bool browse(char *ipath, const char *session, int pkey)
5836 char newpath[PATH_MAX] __attribute__ ((aligned)),
5837 rundir[PATH_MAX] __attribute__ ((aligned)),
5838 runfile[NAME_MAX + 1] __attribute__ ((aligned));
5839 char *path, *lastdir, *lastname, *dir, *tmp;
5843 int r = -1, presel, selstartid = 0, selendid = 0;
5844 const uchar_t opener_flags = (cfg.cliopener ? F_CLI : (F_NOTRACE | F_NOSTDIN | F_NOWAIT));
5849 struct timespec mousetimings[2] = {{.tv_sec = 0, .tv_nsec = 0}, {.tv_sec = 0, .tv_nsec = 0} };
5850 int mousedent[2] = {-1, -1};
5851 bool currentmouse = 1, rightclicksel = 0;
5854 #ifndef DIR_LIMITED_SELECTION
5864 /* set-up first context */
5865 if (!session || !load_session(session, &path, &lastdir, &lastname, FALSE)) {
5869 g_ctx[0].c_last[0] = '\0';
5870 lastdir = g_ctx[0].c_last; /* last visited directory */
5872 if (g_state.initfile) {
5873 xstrsncpy(g_ctx[0].c_name, xbasename(ipath), sizeof(g_ctx[0].c_name));
5876 g_ctx[0].c_name[0] = '\0';
5878 lastname = g_ctx[0].c_name; /* last visited file name */
5880 xstrsncpy(g_ctx[0].c_path, ipath, PATH_MAX);
5881 /* If the initial path is a file, retain a way to return to start dir */
5882 if (g_state.initfile) {
5884 initpath = ipath = getcwd(NULL, 0);
5886 path = g_ctx[0].c_path; /* current directory */
5888 g_ctx[0].c_fltr[0] = g_ctx[0].c_fltr[1] = '\0';
5889 g_ctx[0].c_cfg = cfg; /* current configuration */
5894 newpath[0] = rundir[0] = runfile[0] = '\0';
5896 presel = pkey ? ';' : (cfg.filtermode ? FILTER : 0);
5898 pdents = xrealloc(pdents, total_dents * sizeof(struct entry));
5902 /* Allocate buffer to hold names */
5903 pnamebuf = (char *)xrealloc(pnamebuf, NAMEBUF_INCR);
5908 /* Can fail when permissions change while browsing.
5909 * It's assumed that path IS a directory when we are here.
5911 if (chdir(path) == -1) {
5912 DPRINTF_S("directory inaccessible");
5913 valid_parent(path, lastname);
5917 if (!g_state.picker) {
5918 /* Set terminal window title */
5919 r = set_tilde_in_path(path);
5921 printf("\033]2;%s\007", r ? &path[homelen - 1] : path);
5925 reset_tilde_in_path(path);
5928 if (g_state.selmode && lastdir[0])
5929 lastappendpos = selbufpos;
5931 #ifdef LINUX_INOTIFY
5932 if ((presel == FILTER || watch) && inotify_wd >= 0) {
5933 inotify_rm_watch(inotify_fd, inotify_wd);
5937 #elif defined(BSD_KQUEUE)
5938 if ((presel == FILTER || watch) && event_fd >= 0) {
5943 #elif defined(HAIKU_NM)
5944 if ((presel == FILTER || watch) && haiku_hnd != NULL) {
5945 haiku_stop_watch(haiku_hnd);
5946 haiku_nm_active = FALSE;
5951 populate(path, lastname);
5952 if (g_state.interrupt) {
5953 g_state.interrupt = cfg.apparentsz = cfg.blkorder = 0;
5954 blk_shift = BLK_SHIFT_512;
5955 presel = CONTROL('L');
5958 #ifdef LINUX_INOTIFY
5959 if (presel != FILTER && inotify_wd == -1)
5960 inotify_wd = inotify_add_watch(inotify_fd, path, INOTIFY_MASK);
5961 #elif defined(BSD_KQUEUE)
5962 if (presel != FILTER && event_fd == -1) {
5963 #if defined(O_EVTONLY)
5964 event_fd = open(path, O_EVTONLY);
5966 event_fd = open(path, O_RDONLY);
5969 EV_SET(&events_to_monitor[0], event_fd, EVFILT_VNODE,
5970 EV_ADD | EV_CLEAR, KQUEUE_FFLAGS, 0, path);
5972 #elif defined(HAIKU_NM)
5973 haiku_nm_active = haiku_watch_dir(haiku_hnd, path) == EXIT_SUCCESS;
5977 /* Do not do a double redraw in filterentries */
5978 if ((presel != FILTER) || !filterset())
5982 /* Exit if parent has exited */
5984 _exit(EXIT_FAILURE);
5986 /* If CWD is deleted or moved or perms changed, find an accessible parent */
5987 if (chdir(path) == -1)
5990 /* If STDIN is no longer a tty (closed) we should exit */
5991 if (!isatty(STDIN_FILENO) && !g_state.picker)
5992 return EXIT_FAILURE;
5994 sel = nextsel(presel);
6001 if (getmouse(&event) != OK)
6004 /* Handle clicking on a context at the top */
6005 if (event.bstate == BUTTON1_PRESSED && event.y == 0) {
6006 /* Get context from: "[1 2 3 4]..." */
6009 /* If clicked after contexts, go to parent */
6012 else if (r >= 0 && r != cfg.curctx) {
6013 if (g_state.selmode)
6014 lastappendpos = selbufpos;
6016 savecurctx(&cfg, path, pdents[cur].name, r);
6018 /* Reset the pointers */
6019 path = g_ctx[r].c_path;
6020 lastdir = g_ctx[r].c_last;
6021 lastname = g_ctx[r].c_name;
6031 if (sel == SEL_BACK) {
6033 dir = visit_parent(path, newpath, &presel);
6038 xstrsncpy(lastname, xbasename(path), NAME_MAX + 1);
6040 cdprep(lastdir, NULL, path, dir) ? (presel = FILTER) : (watch = TRUE);
6047 /* Middle click action */
6048 if (event.bstate == BUTTON2_PRESSED) {
6049 presel = middle_click_key;
6052 #if NCURSES_MOUSE_VERSION > 1
6054 if (event.bstate == BUTTON4_PRESSED && ndents && (cfg.rollover || cur)) {
6055 if (!cfg.rollover && cur < scroll_lines)
6058 move_cursor((cur + ndents - scroll_lines) % ndents, 0);
6063 if (event.bstate == BUTTON5_PRESSED && ndents
6064 && (cfg.rollover || (cur != ndents - 1))) {
6065 if (!cfg.rollover && cur >= ndents - scroll_lines)
6066 move_cursor(ndents-1, 0);
6068 move_cursor((cur + scroll_lines) % ndents, 0);
6073 /* Toggle filter mode on left click on last 2 lines */
6074 if (event.y >= xlines - 2 && event.bstate == BUTTON1_PRESSED) {
6076 cfg.filtermode ^= 1;
6077 if (cfg.filtermode) {
6082 /* Start watching the directory */
6090 /* Handle clicking on a file */
6091 if (event.y >= 2 && event.y <= ndents + 1 &&
6092 (event.bstate == BUTTON1_PRESSED ||
6093 event.bstate == BUTTON3_PRESSED)) {
6094 r = curscroll + (event.y - 2);
6098 else if (event.bstate == BUTTON1_PRESSED)
6101 /* Handle right click selection */
6102 if (event.bstate == BUTTON3_PRESSED) {
6110 #if defined(CLOCK_MONOTONIC_RAW)
6111 CLOCK_MONOTONIC_RAW,
6112 #elif defined(CLOCK_MONOTONIC)
6117 &mousetimings[currentmouse]);
6118 mousedent[currentmouse] = cur;
6120 /* Single click just selects, double click falls through to SEL_OPEN */
6121 if ((mousedent[0] != mousedent[1]) ||
6122 (((_ABSSUB(mousetimings[0].tv_sec, mousetimings[1].tv_sec) << 30)
6123 + (_ABSSUB(mousetimings[0].tv_nsec, mousetimings[1].tv_nsec)))
6124 > DOUBLECLICK_INTERVAL_NS))
6126 mousetimings[currentmouse].tv_sec = 0;
6127 mousedent[currentmouse] = -1;
6129 if (cfg.filtermode || filterset())
6137 case SEL_NAV_IN: // fallthrough
6139 /* Cannot descend in empty directories */
6143 pent = &pdents[cur];
6144 mkpath(path, pent->name, newpath);
6147 /* Visit directory */
6148 if (pent->flags & DIR_OR_LINK_TO_DIR) {
6149 if (chdir(newpath) == -1) {
6154 cdprep(lastdir, lastname, path, newpath) ? (presel = FILTER) : (watch = TRUE);
6158 /* Cannot use stale data in entry, file may be missing by now */
6159 if (stat(newpath, &sb) == -1) {
6163 DPRINTF_U(sb.st_mode);
6165 /* Do not open non-regular files */
6166 if (!S_ISREG(sb.st_mode)) {
6167 printwait(messages[MSG_UNSUPPORTED], &presel);
6171 /* If opened as vim plugin and Enter/^M pressed, pick */
6172 if (g_state.picker && sel == SEL_OPEN) {
6173 appendfpath(newpath, mkpath(path, pent->name, newpath));
6174 writesel(pselbuf, selbufpos - 1);
6175 return EXIT_SUCCESS;
6178 if (sel == SEL_NAV_IN) {
6179 /* If in listing dir, go to target on `l` or Right on symlink */
6180 if (listpath && S_ISLNK(pent->mode)
6181 && is_prefix(path, listpath, xstrlen(listpath))) {
6182 if (!realpath(pent->name, newpath)) {
6189 if (chdir(newpath) == -1) {
6194 /* Mark current directory */
6196 mark = xstrdup(path);
6198 cdprep(lastdir, NULL, path, newpath)
6199 ? (presel = FILTER) : (watch = TRUE);
6200 xstrsncpy(lastname, pent->name, NAME_MAX + 1);
6204 /* Open file disabled on right arrow or `l` */
6209 /* Handle plugin selection mode */
6210 if (g_state.runplugin) {
6211 g_state.runplugin = 0;
6212 /* Must be in plugin dir and same context to select plugin */
6213 if ((g_state.runctx == cfg.curctx) && !strcmp(path, plgpath)) {
6215 /* Copy path so we can return back to earlier dir */
6216 xstrsncpy(path, rundir, PATH_MAX);
6219 if (chdir(path) == -1
6220 || !run_selected_plugin(&path, pent->name,
6221 runfile, &lastname, &lastdir)) {
6222 DPRINTF_S("plugin failed!");
6234 printwait(messages[MSG_EMPTY_FILE], &presel);
6239 #ifdef FILE_MIME_OPTS
6240 && get_output(g_buf, CMD_LEN_MAX, "file", FILE_MIME_OPTS, newpath, FALSE)
6241 && is_prefix(g_buf, "text/", 5)
6243 /* no MIME option; guess from description instead */
6244 && get_output(g_buf, CMD_LEN_MAX, "file", "-bL", newpath, FALSE)
6245 && strstr(g_buf, "text")
6248 spawn(editor, newpath, NULL, F_CLI);
6252 /* Get the extension for regext match */
6253 tmp = xextension(pent->name, pent->nlen - 1);
6255 if (tmp && !pcre_exec(archive_pcre, NULL, tmp,
6256 pent->nlen - (tmp - pent->name) - 1, 0, 0, NULL, 0)) {
6258 if (tmp && !regexec(&archive_re, tmp, 0, NULL, 0)) {
6260 r = get_input(messages[MSG_ARCHIVE_OPTS]);
6261 if (r == 'l' || r == 'x') {
6262 mkpath(path, pent->name, newpath);
6263 handle_archive(newpath, r);
6274 if (!archive_mount(newpath)) {
6279 /* Mark current directory */
6281 mark = xstrdup(path);
6283 cdprep(lastdir, lastname, path, newpath)
6284 ? (presel = FILTER) : (watch = TRUE);
6289 printwait(messages[MSG_INVALID_KEY], &presel);
6294 /* Invoke desktop opener as last resort */
6295 spawn(opener, newpath, NULL, opener_flags);
6297 /* Move cursor to the next entry if not the last entry */
6298 if (g_state.autonext && cur != ndents - 1)
6299 move_cursor((cur + 1) % ndents, 0);
6301 case SEL_NEXT: // fallthrough
6302 case SEL_PREV: // fallthrough
6303 case SEL_PGDN: // fallthrough
6304 case SEL_CTRL_D: // fallthrough
6305 case SEL_PGUP: // fallthrough
6306 case SEL_CTRL_U: // fallthrough
6307 case SEL_HOME: // fallthrough
6308 case SEL_END: // fallthrough
6312 handle_screen_move(sel);
6315 case SEL_CDHOME: // fallthrough
6316 case SEL_CDBEGIN: // fallthrough
6317 case SEL_CDLAST: // fallthrough
6319 dir = (sel == SEL_CDHOME) ? home
6320 : ((sel == SEL_CDBEGIN) ? ipath
6321 : ((sel == SEL_CDLAST) ? lastdir
6322 : "/" /* SEL_CDROOT */));
6324 if (!dir || !*dir) {
6325 printwait(messages[MSG_NOT_SET], &presel);
6329 if (strcmp(path, dir) == 0) {
6335 if (chdir(dir) == -1) {
6340 /* SEL_CDLAST: dir pointing to lastdir */
6341 xstrsncpy(newpath, dir, PATH_MAX); // fallthrough
6343 if (sel == SEL_BOOKMARK) {
6344 r = (int)handle_bookmark(mark, newpath);
6346 printwait(messages[r], &presel);
6350 if (strcmp(path, newpath) == 0)
6354 if (sel == SEL_REMOTE && !remote_mount(newpath)) {
6359 /* Mark current directory */
6361 mark = xstrdup(path);
6363 /* In list mode, retain the last file name to highlight it, if possible */
6364 cdprep(lastdir, listpath && sel == SEL_CDLAST ? NULL : lastname, path, newpath)
6365 ? (presel = FILTER) : (watch = TRUE);
6367 case SEL_CYCLE: // fallthrough
6368 case SEL_CYCLER: // fallthrough
6369 case SEL_CTX1: // fallthrough
6370 case SEL_CTX2: // fallthrough
6371 case SEL_CTX3: // fallthrough
6379 r = handle_context_switch(sel);
6382 savecurctx(&cfg, path, pdents[cur].name, r);
6384 /* Reset the pointers */
6385 path = g_ctx[r].c_path;
6386 lastdir = g_ctx[r].c_last;
6387 lastname = g_ctx[r].c_name;
6388 tmp = g_ctx[r].c_fltr;
6390 if (cfg.filtermode || ((tmp[0] == FILTER || tmp[0] == RFILTER) && tmp[1]))
6398 mark = xstrdup(path);
6399 printwait(mark, &presel);
6402 /* Unwatch dir if we are still in a filtered view */
6403 #ifdef LINUX_INOTIFY
6404 if (inotify_wd >= 0) {
6405 inotify_rm_watch(inotify_fd, inotify_wd);
6408 #elif defined(BSD_KQUEUE)
6409 if (event_fd >= 0) {
6413 #elif defined(HAIKU_NM)
6414 if (haiku_nm_active) {
6415 haiku_stop_watch(haiku_hnd);
6416 haiku_nm_active = FALSE;
6419 presel = filterentries(path, lastname);
6421 if (presel == ESC) {
6426 case SEL_MFLTR: // fallthrough
6427 case SEL_HIDDEN: // fallthrough
6428 case SEL_DETAIL: // fallthrough
6432 cfg.filtermode ^= 1;
6433 if (cfg.filtermode) {
6439 watch = TRUE; // fallthrough
6441 if (sel == SEL_HIDDEN) {
6442 cfg.showhidden ^= 1;
6451 cfg.showdetail ^= 1;
6452 cfg.showdetail ? (printptr = &printent_long) : (printptr = &printent);
6455 default: /* SEL_SORT */
6456 r = set_sort_flags(get_input(messages[MSG_ORDER]));
6458 printwait(messages[MSG_INVALID_KEY], &presel);
6463 if (cfg.filtermode || filterset())
6469 if (r == 'd' || r == 'a')
6472 ENTSORT(pdents, ndents, entrycmpfn);
6473 move_cursor(ndents ? dentfind(lastname, ndents) : 0, 0);
6476 case SEL_STATS: // fallthrough
6479 tmp = (listpath && xstrcmp(path, listpath) == 0) ? listroot : path;
6480 mkpath(tmp, pdents[cur].name, newpath);
6482 if (lstat(newpath, &sb) == -1
6483 || (sel == SEL_STATS && !show_stats(newpath, &sb))
6484 || (sel == SEL_CHMODX && !xchmod(newpath, sb.st_mode))) {
6489 if (sel == SEL_CHMODX)
6490 pdents[cur].mode ^= 0111;
6493 case SEL_REDRAW: // fallthrough
6494 case SEL_RENAMEMUL: // fallthrough
6495 case SEL_HELP: // fallthrough
6496 case SEL_AUTONEXT: // fallthrough
6497 case SEL_EDIT: // fallthrough
6500 bool refresh = FALSE;
6503 mkpath(path, pdents[cur].name, newpath);
6504 else if (sel == SEL_EDIT) /* Avoid trying to edit a non-existing file */
6514 if (!(getutil(utils[UTIL_BASH])
6515 && plugscript(utils[UTIL_NMV], F_CLI))
6520 printwait(messages[MSG_FAILED], &presel);
6527 show_help(path); // fallthrough
6529 if (sel == SEL_AUTONEXT)
6530 g_state.autonext ^= 1;
6537 spawn(editor, newpath, NULL, F_CLI);
6539 default: /* SEL_LOCK */
6544 /* In case of successful operation, reload contents */
6546 /* Continue in type-to-nav mode, if enabled */
6547 if ((cfg.filtermode || filterset()) && !refresh) {
6555 /* Repopulate as directory content may have changed */
6563 if (g_state.rangesel)
6564 g_state.rangesel = 0;
6566 /* Toggle selection status */
6567 pdents[cur].flags ^= FILE_SELECTED;
6569 if (pdents[cur].flags & FILE_SELECTED) {
6571 appendfpath(newpath, mkpath(path, pdents[cur].name, newpath));
6572 writesel(pselbuf, selbufpos - 1); /* Truncate NULL from end */
6574 selbufpos = lastappendpos;
6576 updateselbuf(path, newpath);
6577 writesel(pselbuf, selbufpos - 1); /* Truncate NULL from end */
6583 plugscript(utils[UTIL_CBCP], F_NOWAIT | F_NOTRACE);
6592 /* move cursor to the next entry if this is not the last entry */
6593 if (!g_state.stayonsel && !g_state.picker && cur != ndents - 1)
6594 move_cursor((cur + 1) % ndents, 0);
6601 g_state.rangesel ^= 1;
6603 if (stat(path, &sb) == -1) {
6608 if (g_state.rangesel) { /* Range selection started */
6609 #ifndef DIR_LIMITED_SELECTION
6616 #ifndef DIR_LIMITED_SELECTION
6617 if (inode != sb.st_ino) {
6618 printwait(messages[MSG_DIR_CHANGED], &presel);
6622 if (cur < selstartid) {
6623 selendid = selstartid;
6628 /* Clear selection on repeat on same file */
6629 if (selstartid == selendid) {
6634 case SEL_SELALL: // fallthrough
6636 if (sel == SEL_SELALL || sel == SEL_SELINV) {
6641 if (g_state.rangesel)
6642 g_state.rangesel = 0;
6645 selendid = ndents - 1;
6648 if (sel == SEL_SELINV) {
6649 /* Toggle selection status */
6650 for (r = selstartid; r <= selendid; ++r) {
6651 pdents[r].flags ^= FILE_SELECTED;
6652 pdents[r].flags & FILE_SELECTED ? ++nselected : --nselected;
6655 selbufpos = lastappendpos;
6657 updateselbuf(path, newpath);
6658 writesel(pselbuf, selbufpos - 1); /* Truncate NULL from end */
6662 /* Remember current selection buffer position */
6663 for (r = selstartid; r <= selendid; ++r) {
6664 if (!(pdents[r].flags & FILE_SELECTED)) {
6665 /* Write the path to selection file to avoid flush */
6666 appendfpath(newpath, mkpath(path, pdents[r].name, newpath));
6668 pdents[r].flags |= FILE_SELECTED;
6673 writesel(pselbuf, selbufpos - 1); /* Truncate NULL from end */
6677 plugscript(utils[UTIL_CBCP], F_NOWAIT | F_NOTRACE);
6680 r = editselection();
6682 r = !r ? MSG_0_SELECTED : MSG_FAILED;
6683 printwait(messages[r], &presel);
6686 plugscript(utils[UTIL_CBCP], F_NOWAIT | F_NOTRACE);
6687 cfg.filtermode ? presel = FILTER : statusbar(path);
6690 case SEL_CP: // fallthrough
6691 case SEL_MV: // fallthrough
6692 case SEL_CPMVAS: // fallthrough
6695 if (sel == SEL_RM) {
6696 r = get_cur_or_sel();
6703 tmp = (listpath && xstrcmp(path, listpath) == 0)
6705 mkpath(tmp, pdents[cur].name, newpath);
6709 copynextname(lastname);
6711 if (cfg.filtermode || filterset())
6717 if (nselected == 1 && (sel == SEL_CP || sel == SEL_MV))
6718 mkpath(path, xbasename(pselbuf), newpath);
6724 if (!cpmvrm_selection(sel, path)) {
6733 /* Show notification on operation complete */
6735 plugscript(utils[UTIL_NTFY], F_NOWAIT | F_NOTRACE);
6737 if (newpath[0] && !access(newpath, F_OK))
6738 xstrsncpy(lastname, xbasename(newpath), NAME_MAX+1);
6743 case SEL_ARCHIVE: // fallthrough
6744 case SEL_OPENWITH: // fallthrough
6745 case SEL_NEW: // fallthrough
6750 if (!ndents && (sel == SEL_OPENWITH || sel == SEL_RENAME))
6753 if (sel != SEL_OPENWITH)
6758 r = get_cur_or_sel();
6772 tmp = pdents[cur].name;
6774 tmp = xreadline(tmp, messages[MSG_ARCHIVE_NAME]);
6778 tmp = xreadline(NULL, messages[MSG_OPEN_WITH]);
6780 tmp = getreadline(messages[MSG_OPEN_WITH]);
6784 r = get_input(messages[MSG_NEW_OPTS]);
6785 if (r == 'f' || r == 'd')
6786 tmp = xreadline(NULL, messages[MSG_NEW_PATH]);
6787 else if (r == 's' || r == 'h')
6788 tmp = xreadline(NULL, messages[MSG_LINK_PREFIX]);
6792 default: /* SEL_RENAME */
6793 tmp = xreadline(pdents[cur].name, "");
6802 if (r == 'c' && strcmp(tmp, pdents[cur].name) == 0)
6805 mkpath(path, tmp, newpath);
6806 if (access(newpath, F_OK) == 0) {
6807 if (!xconfirm(get_input(messages[MSG_OVERWRITE]))) {
6812 get_archive_cmd(newpath, tmp);
6813 (r == 's') ? archive_selection(newpath, tmp, path)
6814 : spawn(newpath, tmp, pdents[cur].name, F_CLI | F_CONFIRM);
6816 mkpath(path, tmp, newpath);
6817 if (access(newpath, F_OK) == 0) { /* File created */
6818 xstrsncpy(lastname, tmp, NAME_MAX + 1);
6819 clearfilter(); /* Archive name may not match */
6820 clearselection(); /* Archive operation complete */
6825 /* Confirm if app is CLI or GUI */
6826 r = get_input(messages[MSG_CLI_MODE]);
6827 r = (r == 'c' ? F_CLI :
6828 (r == 'g' ? F_NOWAIT | F_NOTRACE | F_MULTI : 0));
6830 mkpath(path, pdents[cur].name, newpath);
6831 spawn(tmp, newpath, NULL, r);
6834 cfg.filtermode ? presel = FILTER : statusbar(path);
6838 /* Skip renaming to same name */
6839 if (strcmp(tmp, pdents[cur].name) == 0) {
6840 tmp = xreadline(pdents[cur].name, messages[MSG_COPY_NAME]);
6841 if (!tmp || !tmp[0] || !strcmp(tmp, pdents[cur].name)) {
6842 cfg.filtermode ? presel = FILTER : statusbar(path);
6849 default: /* SEL_NEW */
6853 /* Open the descriptor to currently open directory */
6855 fd = open(path, O_RDONLY | O_DIRECTORY);
6857 fd = open(path, O_RDONLY);
6864 /* Check if another file with same name exists */
6865 if (faccessat(fd, tmp, F_OK, AT_SYMLINK_NOFOLLOW) != -1) {
6866 if (sel == SEL_RENAME) {
6867 /* Overwrite file with same name? */
6868 if (!xconfirm(get_input(messages[MSG_OVERWRITE]))) {
6873 /* Do nothing in case of NEW */
6875 printwait(messages[MSG_EXISTS], &presel);
6880 if (sel == SEL_RENAME) {
6881 /* Rename the file */
6883 spawn("cp -rp", pdents[cur].name, tmp, F_SILENT);
6884 else if (renameat(fd, pdents[cur].name, fd, tmp) != 0) {
6890 xstrsncpy(lastname, tmp, NAME_MAX + 1);
6891 } else { /* SEL_NEW */
6895 /* Check if it's a dir or file */
6896 if (r == 'f' || r == 'd') {
6897 mkpath(path, tmp, newpath);
6898 ret = xmktree(newpath, r == 'f' ? FALSE : TRUE);
6899 } else if (r == 's' || r == 'h') {
6900 if (tmp[0] == '@' && tmp[1] == '\0')
6902 ret = xlink(tmp, path, (ndents ? pdents[cur].name : NULL),
6903 newpath, &presel, r);
6907 printwait(messages[MSG_FAILED], &presel);
6912 if (r == 'f' || r == 'd')
6913 xstrsncpy(lastname, tmp, NAME_MAX + 1);
6925 /* Check if directory is accessible */
6926 if (!xdiraccess(plgpath)) {
6932 r = xstrsncpy(g_buf, messages[MSG_PLUGIN_KEYS], CMD_LEN_MAX);
6933 printkeys(plug, g_buf + r - 1, maxplug);
6935 r = get_input(NULL);
6943 tmp = get_kv_val(plug, NULL, r, maxplug, NNN_PLUG);
6945 printwait(messages[MSG_INVALID_KEY], &presel);
6949 if (tmp[0] == '-' && tmp[1]) {
6951 r = FALSE; /* Do not refresh dir after completion */
6955 if (!run_selected_plugin(&path, tmp, (ndents ? pdents[cur].name : NULL),
6956 &lastname, &lastdir)) {
6957 printwait(messages[MSG_FAILED], &presel);
6965 cfg.filtermode ? presel = FILTER : statusbar(path);
6968 } else { /* 'Return/Enter' enters the plugin directory */
6969 g_state.runplugin ^= 1;
6970 if (!g_state.runplugin && rundir[0]) {
6972 * If toggled, and still in the plugin dir,
6973 * switch to original directory
6975 if (strcmp(path, plgpath) == 0) {
6976 xstrsncpy(path, rundir, PATH_MAX);
6977 xstrsncpy(lastname, runfile, NAME_MAX + 1);
6978 rundir[0] = runfile[0] = '\0';
6983 /* Otherwise, initiate choosing plugin again */
6984 g_state.runplugin = 1;
6987 xstrsncpy(rundir, path, PATH_MAX);
6988 xstrsncpy(path, plgpath, PATH_MAX);
6990 xstrsncpy(runfile, pdents[cur].name, NAME_MAX);
6991 g_state.runctx = cfg.curctx;
6997 case SEL_SHELL: // fallthrough
6998 case SEL_LAUNCH: // fallthrough
7000 r = handle_cmd(sel, (ndents ? pdents[cur].name : ""), newpath);
7002 /* Continue in type-to-nav mode, if enabled */
7013 /* Repopulate as directory content may have changed */
7016 if (!unmount((ndents ? pdents[cur].name : NULL), newpath, &presel, path))
7019 /* Dir removed, go to next entry */
7020 copynextname(lastname);
7024 r = get_input(messages[MSG_SSN_OPTS]);
7027 save_session(FALSE, &presel);
7028 else if (r == 'l' || r == 'r') {
7029 if (load_session(NULL, &path, &lastdir, &lastname, r == 'r')) {
7040 cfg.filtermode ? presel = FILTER : statusbar(path);
7043 if (!set_time_type(&presel))
7046 case SEL_QUITCTX: // fallthrough
7047 case SEL_QUITCD: // fallthrough
7050 if (sel == SEL_QUITCTX) {
7051 int ctx = cfg.curctx;
7053 for (r = (ctx + 1) & ~CTX_MAX;
7054 (r != ctx) && !g_ctx[r].c_cfg.ctxactive;
7055 r = ((r + 1) & ~CTX_MAX)) {
7059 g_ctx[ctx].c_cfg.ctxactive = 0;
7061 /* Switch to next active context */
7062 path = g_ctx[r].c_path;
7063 lastdir = g_ctx[r].c_last;
7064 lastname = g_ctx[r].c_name;
7066 /* Switch light/detail mode */
7067 if (cfg.showdetail != g_ctx[r].c_cfg.showdetail)
7068 /* Set the reverse */
7069 printptr = cfg.showdetail ?
7070 &printent : &printent_long;
7072 cfg = g_ctx[r].c_cfg;
7078 } else if (!g_state.forcequit) {
7079 for (r = 0; r < CTX_MAX; ++r)
7080 if (r != cfg.curctx && g_ctx[r].c_cfg.ctxactive) {
7081 r = get_input(messages[MSG_QUIT_ALL]);
7085 if (!(r == CTX_MAX || xconfirm(r)))
7086 break; // fallthrough
7090 if (session && *session == '@' && !session[1])
7091 save_session(TRUE, NULL);
7095 if (sel == SEL_QUITCD || getenv("NNN_TMPFILE")) {
7096 write_lastdir(path);
7101 if (sel != SEL_QUITERR)
7102 return EXIT_SUCCESS;
7104 if (selbufpos && !g_state.picker) {
7105 g_state.pickraw = 1;
7106 return EXIT_SUCCESS;
7109 return EXIT_FAILURE;
7111 if (xlines != LINES || xcols != COLS)
7114 if (idletimeout && idle == idletimeout) {
7115 lock_terminal(); /* Locker */
7123 } /* switch (sel) */
7127 static char *make_tmp_tree(char **paths, ssize_t entries, const char *prefix)
7129 /* tmpdir holds the full path */
7130 /* tmp holds the path without the tmp dir prefix */
7134 ssize_t len = xstrlen(prefix);
7135 char *tmpdir = malloc(PATH_MAX);
7138 DPRINTF_S(strerror(errno));
7142 tmp = tmpdir + tmpfplen - 1;
7143 xstrsncpy(tmpdir, g_tmpfpath, tmpfplen);
7144 xstrsncpy(tmp, "/nnnXXXXXX", 11);
7146 /* Points right after the base tmp dir */
7149 /* handle the case where files are directly under / */
7150 if (!prefix[1] && (prefix[0] == '/'))
7153 if (!mkdtemp(tmpdir)) {
7156 DPRINTF_S(strerror(errno));
7162 for (ssize_t i = 0; i < entries; ++i) {
7166 err = stat(paths[i], &sb);
7167 if (err && errno == ENOENT)
7170 /* Don't copy the common prefix */
7171 xstrsncpy(tmp, paths[i] + len, xstrlen(paths[i]) - len + 1);
7173 /* Get the dir containing the path */
7174 slash = xmemrchr((uchar_t *)tmp, '/', xstrlen(paths[i]) - len);
7178 xmktree(tmpdir, TRUE);
7183 if (symlink(paths[i], tmpdir)) {
7184 DPRINTF_S(paths[i]);
7185 DPRINTF_S(strerror(errno));
7189 /* Get the dir in which to start */
7194 static char *load_input(int fd, const char *path)
7196 ssize_t i, chunk_count = 1, chunk = 512 * 1024 /* 512 KiB chunk size */, entries = 0;
7197 char *input = malloc(sizeof(char) * chunk), *tmpdir = NULL;
7198 char cwd[PATH_MAX], *next;
7199 size_t offsets[LIST_FILES_MAX];
7200 char **paths = NULL;
7201 ssize_t input_read, total_read = 0, off = 0;
7205 DPRINTF_S(strerror(errno));
7210 if (!getcwd(cwd, PATH_MAX)) {
7215 xstrsncpy(cwd, path, PATH_MAX);
7217 while (chunk_count < 512) {
7218 input_read = read(fd, input + total_read, chunk);
7219 if (input_read < 0) {
7220 DPRINTF_S(strerror(errno));
7224 if (input_read == 0)
7227 total_read += input_read;
7230 while (off < total_read) {
7231 next = memchr(input + off, '\0', total_read - off) + 1;
7232 if (next == (void *)1)
7235 if (next - input == off + 1) {
7240 if (entries == LIST_FILES_MAX) {
7245 offsets[entries++] = off;
7249 if (chunk_count == 512) {
7254 /* We don't need to allocate another chunk */
7255 if (chunk_count == (total_read - input_read) / chunk)
7258 chunk_count = total_read / chunk;
7259 if (total_read % chunk)
7262 input = xrealloc(input, (chunk_count + 1) * chunk);
7267 if (off != total_read) {
7268 if (entries == LIST_FILES_MAX) {
7273 offsets[entries++] = off;
7277 DPRINTF_D(total_read);
7278 DPRINTF_D(chunk_count);
7281 msgnum = MSG_0_ENTRIES;
7285 input[total_read] = '\0';
7287 paths = malloc(entries * sizeof(char *));
7291 for (i = 0; i < entries; ++i)
7292 paths[i] = input + offsets[i];
7294 listroot = malloc(sizeof(char) * PATH_MAX);
7299 DPRINTF_S(paths[0]);
7301 for (i = 0; i < entries; ++i) {
7302 if (paths[i][0] == '\n' || selforparent(paths[i])) {
7307 paths[i] = abspath(paths[i], cwd);
7309 entries = i; // free from the previous entry
7314 DPRINTF_S(paths[i]);
7316 xstrsncpy(g_buf, paths[i], PATH_MAX);
7317 if (!common_prefix(xdirname(g_buf), listroot)) {
7318 entries = i + 1; // free from the current entry
7322 DPRINTF_S(listroot);
7325 DPRINTF_S(listroot);
7328 tmpdir = make_tmp_tree(paths, entries, listroot);
7331 for (i = entries - 1; i >= 0; --i)
7335 if (home) { /* We are past init stage */
7336 printmsg(messages[msgnum]);
7337 xdelay(XDELAY_INTERVAL_MS);
7339 fprintf(stderr, "%s\n", messages[msgnum]);
7346 static void check_key_collision(void)
7349 bool bitmap[KEY_MAX] = {FALSE};
7351 for (ulong_t i = 0; i < sizeof(bindings) / sizeof(struct key); ++i) {
7352 key = bindings[i].sym;
7355 fprintf(stdout, "key collision! [%s]\n", keyname(key));
7361 static void usage(void)
7364 "%s: nnn [OPTIONS] [PATH]\n\n"
7365 "The unorthodox terminal file manager.\n\n"
7366 "positional args:\n"
7367 " PATH start dir/file [default: .]\n\n"
7370 " -a auto NNN_FIFO\n"
7372 " -A no dir auto-select\n"
7373 " -b key open bookmark key (trumps -s/S)\n"
7374 " -c cli-only NNN_OPENER (trumps -e)\n"
7375 " -C earlier colorscheme\n"
7377 " -D dirs in context color\n"
7378 " -e text in $VISUAL/$EDITOR/vi\n"
7379 " -E use EDITOR for undetached edits\n"
7381 " -f use readline history file\n"
7383 " -F show fortune\n"
7384 " -g regex filters [default: string]\n"
7385 " -H show hidden files\n"
7386 " -J no auto-proceed on select\n"
7387 " -K detect key collision\n"
7388 " -l val set scroll lines\n"
7389 " -n type-to-nav mode\n"
7390 " -o open files only on Enter\n"
7391 " -p file selection file [stdout if '-']\n"
7392 " -P key run plugin key\n"
7393 " -Q no quit confirmation\n"
7394 " -r use advcpmv patched cp, mv\n"
7395 " -R no rollover at edges\n"
7397 " -s name load session by name\n"
7398 " -S persistent session\n"
7400 " -t secs timeout to lock\n"
7401 " -T key sort order [a/d/e/r/s/t/v]\n"
7402 " -u use selection (no prompt)\n"
7404 " -U show user and group\n"
7406 " -V show version\n"
7407 " -w place HW cursor on hovered\n"
7408 " -x notis, sel to system clipboard\n"
7410 "v%s\n%s\n", __func__, VERSION, GENERAL_INFO);
7413 static bool setup_config(void)
7416 char *xdgcfg = getenv("XDG_CONFIG_HOME");
7419 /* Set up configuration file paths */
7420 if (xdgcfg && xdgcfg[0]) {
7422 if (xdgcfg[0] == '~') {
7423 r = xstrsncpy(g_buf, home, PATH_MAX);
7424 xstrsncpy(g_buf + r - 1, xdgcfg + 1, PATH_MAX);
7429 if (!xdiraccess(xdgcfg)) {
7434 len = xstrlen(xdgcfg) + 1 + 13; /* add length of "/nnn/sessions" */
7439 len = xstrlen(home) + 1 + 21; /* add length of "/.config/nnn/sessions" */
7441 cfgpath = (char *)malloc(len);
7442 plgpath = (char *)malloc(len);
7443 if (!cfgpath || !plgpath) {
7449 xstrsncpy(cfgpath, xdgcfg, len);
7450 r = len - 13; /* subtract length of "/nnn/sessions" */
7452 r = xstrsncpy(cfgpath, home, len);
7454 /* Create ~/.config */
7455 xstrsncpy(cfgpath + r - 1, "/.config", len - r);
7457 r += 8; /* length of "/.config" */
7460 /* Create ~/.config/nnn */
7461 xstrsncpy(cfgpath + r - 1, "/nnn", len - r);
7464 /* Create sessions, mounts and plugins directories */
7465 for (r = 0; r < ELEMENTS(toks); ++r) {
7466 mkpath(cfgpath, toks[r], plgpath);
7467 if (!xmktree(plgpath, TRUE)) {
7474 /* Set selection file path */
7475 if (!g_state.picker) {
7476 char *env_sel = xgetenv(env_cfg[NNN_SEL], NULL);
7478 selpath = env_sel ? xstrdup(env_sel)
7479 : (char *)malloc(len + 3); /* Length of "/.config/nnn/.selection" */
7487 r = xstrsncpy(selpath, cfgpath, len + 3);
7488 xstrsncpy(selpath + r - 1, "/.selection", 12);
7496 static bool set_tmp_path(void)
7499 char *path = xdiraccess(tmp) ? tmp : getenv("TMPDIR");
7502 fprintf(stderr, "set TMPDIR\n");
7506 tmpfplen = (uchar_t)xstrsncpy(g_tmpfpath, path, TMP_LEN_MAX);
7507 DPRINTF_S(g_tmpfpath);
7508 DPRINTF_U(tmpfplen);
7513 static void cleanup(void)
7515 if (!g_state.picker) {
7516 printf("\033[23;0t"); /* reset terminal window title */
7530 if (g_state.autofifo)
7533 if (g_state.pluginit)
7540 int main(int argc, char *argv[])
7543 char *session = NULL;
7544 int fd, opt, sort = 0, pkey = '\0'; /* Plugin key */
7547 char *middle_click_env = xgetenv(env_cfg[NNN_MCLICK], "\0");
7549 if (middle_click_env[0] == '^' && middle_click_env[1])
7550 middle_click_key = CONTROL(middle_click_env[1]);
7552 middle_click_key = (uchar_t)middle_click_env[0];
7555 const char * const env_opts = xgetenv(env_cfg[NNN_OPTS], NULL);
7556 int env_opts_id = env_opts ? (int)xstrlen(env_opts) : -1;
7558 bool rlhist = FALSE;
7561 while ((opt = (env_opts_id > 0
7562 ? env_opts[--env_opts_id]
7563 : getopt(argc, argv, "aAb:cCdDeEfFgHJKl:nop:P:QrRs:St:T:uUVwxh"))) != -1) {
7567 g_state.autofifo = 1;
7574 if (env_opts_id < 0)
7581 g_state.oldcolor = 1;
7585 printptr = &printent_long;
7602 g_state.fortune = 1;
7606 filterfn = &visible_re;
7612 g_state.stayonsel = 1;
7615 check_key_collision();
7616 return EXIT_SUCCESS;
7618 if (env_opts_id < 0)
7619 scroll_lines = atoi(optarg);
7628 if (env_opts_id >= 0)
7632 if (optarg[0] == '-' && optarg[1] == '\0')
7633 g_state.pickraw = 1;
7635 fd = open(optarg, O_WRONLY | O_CREAT, 0600);
7638 return EXIT_FAILURE;
7642 selpath = realpath(optarg, NULL);
7647 if (env_opts_id < 0 && !optarg[1])
7648 pkey = (uchar_t)optarg[0];
7651 g_state.forcequit = 1;
7655 cp[2] = cp[5] = mv[2] = mv[5] = 'g'; /* cp -iRp -> cpg -giRp */
7656 cp[4] = mv[4] = '-';
7664 if (env_opts_id < 0)
7672 if (env_opts_id < 0)
7673 idletimeout = atoi(optarg);
7676 if (env_opts_id < 0)
7677 sort = (uchar_t)optarg[0];
7686 fprintf(stdout, "%s\n", VERSION);
7687 return EXIT_SUCCESS;
7696 return EXIT_SUCCESS;
7699 return EXIT_FAILURE;
7701 if (env_opts_id == 0)
7710 /* Prefix for temporary files */
7711 if (!set_tmp_path())
7712 return EXIT_FAILURE;
7716 /* Check if we are in path list mode */
7717 if (!isatty(STDIN_FILENO)) {
7718 /* This is the same as listpath */
7719 initpath = load_input(STDIN_FILENO, NULL);
7721 return EXIT_FAILURE;
7723 /* We return to tty */
7724 dup2(STDOUT_FILENO, STDIN_FILENO);
7730 home = getenv("HOME");
7732 fprintf(stderr, "set HOME\n");
7733 return EXIT_FAILURE;
7736 homelen = (uchar_t)xstrlen(home);
7738 if (!setup_config())
7739 return EXIT_FAILURE;
7741 /* Get custom opener, if set */
7742 opener = xgetenv(env_cfg[NNN_OPENER], utils[UTIL_OPENER]);
7745 /* Parse bookmarks string */
7746 if (!parsekvpair(&bookmark, &bmstr, NNN_BMS, &maxbm)) {
7747 fprintf(stderr, "%s\n", env_cfg[NNN_BMS]);
7748 return EXIT_FAILURE;
7751 /* Parse plugins string */
7752 if (!parsekvpair(&plug, &pluginstr, NNN_PLUG, &maxplug)) {
7753 fprintf(stderr, "%s\n", env_cfg[NNN_PLUG]);
7754 return EXIT_FAILURE;
7758 if (arg) { /* Open a bookmark directly */
7759 if (!arg[1]) /* Bookmarks keys are single char */
7760 initpath = get_kv_val(bookmark, NULL, *arg, maxbm, NNN_BMS);
7763 fprintf(stderr, "%s\n", messages[MSG_INVALID_KEY]);
7764 return EXIT_FAILURE;
7769 } else if (argc == optind) {
7770 /* Start in the current directory */
7771 initpath = getcwd(NULL, 0);
7777 if (xstrlen(arg) > 7 && is_prefix(arg, "file://", 7))
7779 initpath = realpath(arg, NULL);
7780 DPRINTF_S(initpath);
7783 return EXIT_FAILURE;
7787 * If nnn is set as the file manager, applications may try to open
7788 * files by invoking nnn. In that case pass the file path to the
7789 * desktop opener and exit.
7793 if (stat(initpath, &sb) == -1) {
7795 return EXIT_FAILURE;
7798 if (!S_ISDIR(sb.st_mode))
7799 g_state.initfile = 1;
7806 /* Set archive handling (enveditor used as tmp var) */
7807 enveditor = getenv(env_cfg[NNN_ARCHIVE]);
7809 if (setfilter(&archive_pcre, (enveditor ? enveditor : patterns[P_ARCHIVE]))) {
7811 if (setfilter(&archive_re, (enveditor ? enveditor : patterns[P_ARCHIVE]))) {
7813 fprintf(stderr, "%s\n", messages[MSG_INVALID_REG]);
7814 return EXIT_FAILURE;
7817 /* An all-CLI opener overrides option -e) */
7821 /* Get VISUAL/EDITOR */
7822 enveditor = xgetenv(envs[ENV_EDITOR], utils[UTIL_VI]);
7823 editor = xgetenv(envs[ENV_VISUAL], enveditor);
7824 DPRINTF_S(getenv(envs[ENV_VISUAL]));
7825 DPRINTF_S(getenv(envs[ENV_EDITOR]));
7829 pager = xgetenv(envs[ENV_PAGER], utils[UTIL_LESS]);
7833 shell = xgetenv(envs[ENV_SHELL], utils[UTIL_SH]);
7836 DPRINTF_S(getenv("PWD"));
7840 if (g_state.autofifo) {
7841 g_tmpfpath[tmpfplen - 1] = '\0';
7843 size_t r = mkpath(g_tmpfpath, "nnn-fifo.", g_buf);
7845 xstrsncpy(g_buf + r - 1, xitoa(getpid()), PATH_MAX - r);
7846 setenv("NNN_FIFO", g_buf, TRUE);
7849 fifopath = xgetenv("NNN_FIFO", NULL);
7851 if (mkfifo(fifopath, 0600) != 0 && !(errno == EEXIST && access(fifopath, W_OK) == 0)) {
7853 return EXIT_FAILURE;
7856 sigaction(SIGPIPE, &(struct sigaction){.sa_handler = SIG_IGN}, NULL);
7860 #ifdef LINUX_INOTIFY
7861 /* Initialize inotify */
7862 inotify_fd = inotify_init1(IN_NONBLOCK);
7863 if (inotify_fd < 0) {
7865 return EXIT_FAILURE;
7867 #elif defined(BSD_KQUEUE)
7871 return EXIT_FAILURE;
7873 #elif defined(HAIKU_NM)
7874 haiku_hnd = haiku_init_nm();
7877 return EXIT_FAILURE;
7881 /* Configure trash preference */
7882 opt = xgetenv_val(env_cfg[NNN_TRASH]);
7883 if (opt && opt <= 2)
7884 g_state.trash = opt;
7886 /* Ignore/handle certain signals */
7887 struct sigaction act = {.sa_handler = sigint_handler};
7889 if (sigaction(SIGINT, &act, NULL) < 0) {
7891 return EXIT_FAILURE;
7894 act.sa_handler = clean_exit_sighandler;
7896 if (sigaction(SIGTERM, &act, NULL) < 0 || sigaction(SIGHUP, &act, NULL) < 0) {
7898 return EXIT_FAILURE;
7901 act.sa_handler = SIG_IGN;
7903 if (sigaction(SIGQUIT, &act, NULL) < 0) {
7905 return EXIT_FAILURE;
7910 setlocale(LC_ALL, "");
7912 tables = pcre_maketables();
7917 #if RL_READLINE_VERSION >= 0x0603
7918 /* readline would overwrite the WINCH signal hook */
7919 rl_change_environment = 0;
7921 /* Bind TAB to cycling */
7922 rl_variable_bind("completion-ignore-case", "on");
7924 rl_bind_key('\t', rl_menu_complete);
7926 rl_bind_key('\t', rl_complete);
7929 mkpath(cfgpath, ".history", g_buf);
7930 read_history(g_buf);
7934 if (!g_state.picker) {
7935 /* Save terminal window title */
7936 printf("\033[22;0t");
7941 if (!initcurses(&mask))
7943 if (!initcurses(NULL))
7945 return EXIT_FAILURE;
7948 set_sort_flags(sort);
7950 opt = browse(initpath, session, pkey);
7953 mousemask(mask, NULL);
7960 mkpath(cfgpath, ".history", g_buf);
7961 write_history(g_buf);
7965 if (g_state.pickraw || g_state.picker) {
7967 fd = g_state.pickraw ? STDOUT_FILENO : open(selpath, O_WRONLY | O_CREAT, 0600);
7968 if ((fd == -1) || (seltofile(fd, NULL) != (size_t)(selbufpos)))
7977 /* Remove tmp dir in list mode */
7980 /* Free the regex */
7982 pcre_free(archive_pcre);
7984 regfree(&archive_re);
7987 /* Free the selection buffer */
7990 #ifdef LINUX_INOTIFY
7991 /* Shutdown inotify */
7992 if (inotify_wd >= 0)
7993 inotify_rm_watch(inotify_fd, inotify_wd);
7995 #elif defined(BSD_KQUEUE)
7999 #elif defined(HAIKU_NM)
8000 haiku_close_nm(haiku_hnd);