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