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