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