pax_global_header00006660000000000000000000000064134125506540014517gustar00rootroot0000000000000052 comment=673e3b988f25e82db225a86848db927c390bf74c nnn-2.2/000077500000000000000000000000001341255065400121535ustar00rootroot00000000000000nnn-2.2/.clang-tidy000066400000000000000000000013621341255065400142110ustar00rootroot00000000000000--- Checks: 'clang-diagnostic-*,clang-analyzer-*,readability-*,modernize-*,bugprone-*,misc-*,-misc-unused-parameters,google-runtime-int,-llvm-header-guard,fuchsia-restrict-system-includes,-clang-analyzer-valist.Uninitialized,-clang-analyzer-security.insecureAPI.rand,-clang-analyzer-alpha.*,-readability-magic-numbers,-readability-braces-around-statements' WarningsAsErrors: '*' HeaderFilterRegex: '.*(?Del in rename prompt - Pass correct `file` option to identify mime - Support selection across directories and contexts - Offer option `force` before file remove - Keys Tab, ^I to go to next active context - Per-context directory color specified by `$NNN_CONTEXT_COLORS` - Option `-c` is removed - Option `-C` to disable colors - Choose script to run from a script directory - Run a command (or launch an application) - Run file as executable C - Documentation on lftp integration for remote file transfers - Support a _combined_ set of arguments to `$EDITOR`, `$PAGER` and `$SHELL` - Handle > 2 GB files on 32-bit ARM - Env var `$DISABLE_FILE_OPEN_ON_NAV` to disable file open on Right or l - `NUL`-terminated file paths in selection list instead of `LF` - Better support for Termux and Cygwin environments - Remapped keys - ^I - go to next active context - ^T - toggle _navigate-as-you-type_ ------------------------------------------------------------------------------- nnn v2.1 2018-11-23 What's in? - Inclusion in several distros including Arch Linux official repo - Multiple contexts (_aka_ tabs _aka_ workspaces) [max 4] - Copy, move, remove selected files, remove current file - [Leader key](https://github.com/jarun/nnn#leader-key) (like vim) - In-built GUI app launcher with up to 2 arguments (key o) - List copy selection (key y) - Env var `NNN_NO_AUTOSELECT` to disable dir auto-select - Key Esc exits prompt, ^L clears prompt - Program runtime help revamped - Static code analysis integration - gcc-8 warnings fixed - Remapped keys: - ^W - go to pinned dir - ^X - delete current entry - ^Q - quit program - `nlay` is retired (functionality built into `nnn`) - `chdir` prompt is retired - Env var `NNN_NO_X` retired, selection now works out of the box - Only single-char bookmark keys (to work with Leader key) ------------------------------------------------------------------------------- nnn v2.0 2018-10-19 What's in? - Mode to show apparent size (key `S`) - Script to integrate `patool` instead of `atool` - Support `bashlock` (OS X) and `lock` (BSD) as terminal locker - Symbol `@/` for symlink to dir - Dependency on `libreadline` removed ------------------------------------------------------------------------------- nnn v1.9 2018-08-10 What's in? - Support unlimited number of scripts - Pass currently selected filename as first argument to custom scripts - Support directory auto-select in _navigate-as-you-type_ mode - Show selection name in archive name prompt - Support Cygwin opener - Better support on RHEL 25 with earlier version on curses - Sample script for `fzy` integration - Now available on OpenBSD - Disabled package generation for Ubuntu 17.10 ------------------------------------------------------------------------------- nnn v1.8 2018-05-02 What's in? - Run a custom script - Archive selected file/directory - Show number of cherry-picked files in multi-copy mode - Env var `NNN_SHOW_HIDDEN` to show hidden files by default - Additional information in help screen - Give preference to env var VISUAL, if defined, over EDITOR - New/changed/remapped shortcuts - ^] - spawn a new shell in current directory - r - edit directory entries in vidir - R - run a custom script - ^I - toggle navigate-as-you-type mode - L - lock the current terminal (Linux-only) - All Ctrl shortcuts enabled in navigate-as-you-type mode - Fix: GUI programs closing when parent terminal is closed - Recognize `~`, `-` and `&` at bookmark prompt - Recognize ruby (.rb) files as text files - Efficient integer-only file size calculation - Official inclusion on openSUSE and Fedora - Package generation for Ubuntu 18.04 ------------------------------------------------------------------------------- nnn v1.7 2018-02-28 What's in? - Batch rename/move/delete files in vidir from [moreutils](https://joeyh.name/code/moreutils/) - Copy multiple file paths - Copy file paths when X is unavailable - Optionally quote individual file paths with single quotes on copy - Use ISO 8601 date format in file details - New/changed/remapped shortcuts: - ^B - show bookmark prompt (replaces b) - b - pin current dir (replaces ^B) - ^J - toggle du mode - R - batch rename files in vidir - ^F - extract archive (replaces ^X) - ^G - quit nnn and change dir - ^X - quit nnn (replaces ^Q) - Extra shortcuts enabled in nav-as-you-type mode: - ^K, ^Y (file path copy) - ^T (toggles quoted file path copy) - ^R (rename) - ^O (open with...) - ^B (show bookmark prompt) - ^V (visit pinned dir) - ^J (toggle du mode) - ^/ (open desktop opener) - ^F (extract archive) - ^L (refresh) - ^G (quit nnn and change dir) - ^X (quit nnn) ------------------------------------------------------------------------------- nnn v1.6 2017-12-25 What's in? - Shortcut `^O` to open file with custom application - Option `-b` to open bookmarks directly at start - Huge performance improvements around file name storing and handling - Several large static buffers removed or reduced - Several internal algorithms fine tuned for performance/resource usage ------------------------------------------------------------------------------- nnn v1.5 2017-10-05 What's in? - File and directory creation (`n`) - Env variable `NNN_NOWAIT` to unblock nnn when opening files (DE-specific) - Show current entry number in status bar - Support archive listing (`F`) and extraction (`Ctrl-X`) [using `atool`] - Show correct file size on i386 for large files (> 2GB) ------------------------------------------------------------------------------- nnn v1.4 2017-09-04 What's in? - Monitor directory changes - In-place file rename - Pin (`Ctrl-B`) a directory and visit (`Ctrl-V`) it anytime - Auto-completion scripts - Show volume capacity and free in help - Auto-fallback to light mode if too few columns (< 35) - PackageCore integration - Unsupported Function keys (they never work universally): - `F2` (rename), use `Ctrl-R` - `F5` (refresh), use `Ctrl-L` ------------------------------------------------------------------------------- nnn v1.3 2017-07-26 What's in? - Show directories in custom color (default: enabled in blue) - Option `-e` to use exiftool instead of mediainfo - Fixed #34: nftw(3) broken with too many open descriptors - More concise help screen ------------------------------------------------------------------------------- nnn v1.2 2017-06-29 What's in? - Use the desktop opener (xdg-open on Linux, open(1) on OS X) to open files - Option `NNN_USE_EDITOR` to open text files in EDITOR (fallback vi) - Bookmark support (maximum 10, key `b`) - *Navigate-as-you-type* mode (key `Insert` or option `-i`) - Subtree search: gnome-search-tool, fallback catfish (key `^/`) (customizable) - Show current directory content size and file count in disk usage mode - Add detail view mode as default, use `-l` to start in light mode - Shortcuts `F2` and `^L` to refresh and unfilter Note: if filter is empty, `Enter` *opens* the currently selected file now - Help screen shows bookmarks and configuration - Show a message when calculating disk usage - Show the spawned shell level - Linux only: use vlock as the locker on timeout (set using `NNN_IDLE_TIMEOUT`) ------------------------------------------------------------------------------- nnn v1.1 2017-05-12 News - Introducing nlay - a highly customizable bash script to handle media type - nnn is on [Homebrew](http://braumeister.org/formula/nnn) now - RPM packages for CentOS 7 and Fedora 24 generated on release What's in? - *Search-as-you-type* - Unicode support - Option `-S` to start in disk usage analyzer mode - Show media information (using mediainfo) - Use readline at change directory prompt - Jump to prev directories using `cd .....` (with `.` as PWD) - Jump to initial directory using `&` - Show help, mediainfo and file info in PAGER - Several optimizations ------------------------------------------------------------------------------- nnn v1.0 2017-04-13 Modifications - Behaviour and navigation - Detail view (default: disabled) with: - file type (directory, regular, symlink etc.) - modification time - human-readable file size - current item in reverse video - number of items in current directory - full name of currently selected file in 'bar' - Show details of the currently selected file (stat, file) - Disk usage analyzer mode (within the same fs, doesn't follow symlinks) - Directories first (even with sorting) - Sort numeric names in numeric order - Case-insensitive alphabetic content listing instead of upper case first - Key `-` to jump to last visited directory - Roll over at the first and last entries of a directory (with Up/Down keys) - Removed navigation restriction with relative paths (and let permissions handle it) - Sort entries by file size (largest to smallest) - Shortcut to invoke file name copier (set using environment variable `NNN_COPIER`) - File association - Set `NNN_OPENER` to let a desktop opener handle it all. E.g.: export NNN_OPENER=xdg-open export NNN_OPENER=gnome-open export NNN_OPENER=gvfs-open - Selective file associations (ignored if `NNN_OPENER` is set): - Associate plain text files (determined using file) with vi - Associate common audio and video mimes with mpv - Associate PDF files with [zathura](https://pwmt.org/projects/zathura/) - Removed `less` as default file opener (there is no universal standalone opener utility) - You can customize further (see [how to change file associations](#change-file-associations)) - `NNN_FALLBACK_OPENER` is the last line of defense: - If the executable in static file association is missing - If a file type was not handled in static file association - This may be the best option to set your desktop opener to - To enable the desktop file manager key, set `NNN_DE_FILE_MANAGER`. E.g.: export NNN_DE_FILE_MANAGER=thunar - Optimization - All redundant buffer removal - All frequently used local chunks now static - Removed some redundant string allocation and manipulation - Simplified some roundabout procedures - Compiler warnings fixed - strip the final binary ------------------------------------------------------------------------------- nnn-2.2/LICENSE000066400000000000000000000027001341255065400131570ustar00rootroot00000000000000BSD 2-Clause License Copyright (c) 2014-2016, Lazaros Koromilas Copyright (c) 2014-2016, Dimitris Papastamos Copyright (c) 2016-2019, Arun Prakash Jana All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. nnn-2.2/Makefile000066400000000000000000000030501341255065400136110ustar00rootroot00000000000000VERSION = 2.2 PREFIX ?= /usr/local MANPREFIX ?= $(PREFIX)/share/man STRIP ?= strip PKG_CONFIG ?= pkg-config INSTALL ?= install CFLAGS_OPTIMIZATION ?= -O3 ifeq ($(shell $(PKG_CONFIG) ncursesw && echo 1),1) CFLAGS_CURSES ?= $(shell $(PKG_CONFIG) --cflags ncursesw) LDLIBS_CURSES ?= $(shell $(PKG_CONFIG) --libs ncursesw) else ifeq ($(shell $(PKG_CONFIG) ncurses && echo 1),1) CFLAGS_CURSES ?= $(shell $(PKG_CONFIG) --cflags ncurses) LDLIBS_CURSES ?= $(shell $(PKG_CONFIG) --libs ncurses) else LDLIBS_CURSES ?= -lncurses endif CFLAGS += -Wall -Wextra -Wno-unused-parameter CFLAGS += $(CFLAGS_OPTIMIZATION) CFLAGS += $(CFLAGS_CURSES) LDLIBS += $(LDLIBS_CURSES) DISTFILES = src nnn.1 Makefile README.md LICENSE SRC = src/nnn.c BIN = nnn all: $(BIN) $(SRC): src/nnn.h $(BIN): $(SRC) $(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) -o $@ $^ $(LDLIBS) debug: $(SRC) $(CC) -DDEBUGMODE -g $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) -o $(BIN) $^ $(LDLIBS) install: all $(INSTALL) -m 0755 -d $(DESTDIR)$(PREFIX)/bin $(INSTALL) -m 0755 $(BIN) $(DESTDIR)$(PREFIX)/bin $(INSTALL) -m 0755 -d $(DESTDIR)$(MANPREFIX)/man1 $(INSTALL) -m 0644 $(BIN).1 $(DESTDIR)$(MANPREFIX)/man1 uninstall: $(RM) $(DESTDIR)$(PREFIX)/bin/$(BIN) $(RM) $(DESTDIR)$(MANPREFIX)/man1/$(BIN).1 strip: $(BIN) $(STRIP) $^ dist: mkdir -p nnn-$(VERSION) $(CP) -r $(DISTFILES) nnn-$(VERSION) tar -cf nnn-$(VERSION).tar nnn-$(VERSION) gzip nnn-$(VERSION).tar $(RM) -r nnn-$(VERSION) clean: $(RM) -f $(BIN) nnn-$(VERSION).tar.gz skip: ; .PHONY: $(BIN) $(SRC) all debug install uninstall strip dist clean nnn-2.2/README.md000066400000000000000000000556541341255065400134510ustar00rootroot00000000000000## nnn Noice is Not Noice, a noicer fork...

Latest release Homebrew Arch Linux Debian Buster+ Fedora 27+ openSUSE Leap 15.0+ Ubuntu Artful+

Build Status License

[![nnn video](https://i.imgur.com/pDyq5oa.jpg)](https://www.youtube.com/watch?v=U2n5aGqou9E "Click to see nnn in action!")

nnn in action! (Thanks Luke Smith for the video!)

`nnn` is probably the [fastest and most resource-sensitive](#comparison) file manager you have ever used. It integrates seamlessly with your DE and favourite GUI utilities, has a unique [navigate-as-you-type](#navigate-as-you-type-mode) mode with auto-select, disk usage analyzer mode, bookmarks, contexts, application launcher, familiar navigation shortcuts, subshell spawning and much more. [Integrate utilities](https://github.com/jarun/nnn#sample-scripts) like sxiv or fzy easily, transfer selected files using lftp or use it as a [(neo)vim plugin](https://github.com/jarun/nnn#neovim-plugin); `nnn` supports as many scripts as you need! It runs on Linux, macOS, Raspberry Pi, BSD, Cygwin, Linux subsystem for Windows and Termux. [Quickstart](#quickstart) and see how `nnn` simplifies those long desktop sessions. We need contributors. Please visit the ToDo list. *Love smart and efficient utilities? Explore [my repositories](https://github.com/jarun?tab=repositories). Buy me a cup of coffee if they help you.*

Donate via PayPal!

#### TABLE OF CONTENTS - [Features](#features) - [Comparison](#comparison) - [Installation](#installation) - [Dependencies](#dependencies) - [From a package manager](#from-a-package-manager) - [Release packages](#release-packages) - [From source](#from-source) - [Shell completion](#shell-completion) - [Usage](#usage) - [Cmdline options](#cmdline-options) - [Keyboard shortcuts](#keyboard-shortcuts) - [Leader key](#leader-key) - [Contexts](#contexts) - [Directory color](#directory-color) - [Filters](#filters) - [Navigate-as-you-type mode](#navigate-as-you-type-mode) - [File indicators](#file-indicators) - [Utility dependencies](#utility-dependencies) - [Help](#help) - [Quickstart](#quickstart) - [How to](#how-to) - [add bookmarks](#add-bookmarks) - [copy file paths](#copy-file-paths) - [selection](#selection) - [to clipboard](#to-clipboard) - [get selection manually](#get-selection-manually) - [cd on quit](#cd-on-quit) - [(neo)vim plugin](#neovim-plugin) - [run custom scripts](#run-custom-scripts) - [sample scripts](#sample-scripts) - [launch applications](#launch-applications) - [change dir color](#change-dir-color) - [integrate patool](#integrate-patool) - [lftp transfers](#lftp-transfers) - [work faster at rename prompt](#work-faster-at-rename-prompt) - [set idle timeout](#set-idle-timeout) - [show hot plugged drives](#show-hot-plugged-drives) - [tmux configuration](#tmux-configuration) - [BSD terminal issue](#bsd-terminal-issue) - [disable file open on navigation](#disable-file-open-on-navigation) - [Why fork?](#why-fork) - [Mentions](#mentions) - [Developers](#developers) #### FEATURES - Modes - Basic, detail (default), disk usage analyzer (du) - Vim (or neovim) file picker (as a plugin) - Navigation - Familiar, easy shortcuts (arrows, `~`, `-`, `&`) - *Navigate-as-you-type* with auto-select directory - Contexts (_aka_ tabs _aka_ workspaces) - Bookmarks - Pin and visit a directory - Sorting - Directories always listed on top - Sort by file name, modification time, size - Numeric order for numeric names (visit _/proc_) - Search - Instant filtering with *search-as-you-type* - Mimes - Open with desktop opener or specify a custom app - Create, list, extract archives (needs (p)atool) - Open all text files in EDITOR (optional) - Information - Detailed stat-like file information - Media information (needs mediainfo/exiftool) - Convenience - Create, rename files and directories - Select files across directories - Copy, move, delete selection - Transfer files using lftp - Batch rename/move/delete (needs vidir) - Show directories in custom color (default: blue) - Spawn a subshell in the current directory - Run a command, launch applications - Run custom scripts in the current directory - Run current file as executable - Change directory at exit (*easy* shell integration) - Edit file in EDITOR or open in PAGER - Terminal locker integration - Unicode support - Highly optimized, static analysis integrated code - Available on many distros #### COMPARISON Stripped binary (or script) size and memory usage of `nnn` and some other similar utilities while viewing a directory with 13.5K files (0 directories), sorted by size/du:
BINSZ    VIRT    RES    SHR S  %MEM   COMMAND
 650K  139720  91220   8460 S   1.1   ranger
   1M   50496  15328   4076 S   0.2   vifm
   1M   72152  12468   7336 S   0.2   mc
  70K   16068   4620   2408 S   0.1   ncdu
  52K   15712   4368   2512 S   0.1   nnn -S
Intrigued? Find out [HOW](https://github.com/jarun/nnn/wiki/performance-factors). #### INSTALLATION #### Dependencies `nnn` needs a curses library with wide character support (like ncursesw) and standard libc. #### From a package manager - [Arch Linux](https://www.archlinux.org/packages/community/x86_64/nnn/) (`pacman -S nnn`) - [Debian](https://packages.debian.org/search?keywords=nnn&searchon=names&exact=1) (`apt-get install nnn`) - [Fedora](https://apps.fedoraproject.org/packages/nnn) (`dnf install nnn`) - [FreeBSD](https://www.freshports.org/misc/nnn) (`pkg install nnn`) - [Gentoo](https://packages.gentoo.org/packages/app-misc/nnn) (`emerge nnn`) - [macOS/Homebrew](http://formulae.brew.sh/formula/nnn) (`brew install nnn`) - [NixOS](https://github.com/NixOS/nixpkgs/tree/master/pkgs/applications/misc/nnn) (`nix-env -i nnn`) - [OpenBSD](https://cvsweb.openbsd.org/cgi-bin/cvsweb/ports/sysutils/nnn/) (`pkg_add nnn`) - [openSUSE](https://software.opensuse.org/package/nnn) (and packages for several other distros) (`zypper in nnn`) - [pkgrsc](http://pkgsrc.se/sysutils/nnn) (`pkg_add nnn`) - [Raspbian Testing](https://archive.raspbian.org/raspbian/pool/main/n/nnn/) (`apt-get install nnn`) - [Slackware](http://slackbuilds.org/repository/14.2/system/nnn/) (`slackpkg install nnn`) - [Solus](https://packages.getsol.us/shannon/n/nnn/) (`eopkg install nnn`) - [Source Mage](http://codex.sourcemage.org/test/shell-term-fm/nnn/) (`cast nnn`) - [Termux](https://github.com/termux/termux-packages/tree/master/packages/nnn) (`pkg in nnn`) - [Ubuntu](https://packages.ubuntu.com/search?keywords=nnn&searchon=names&exact=1) (`apt-get install nnn`) - [Ubuntu PPA](https://launchpad.net/~twodopeshaggy/+archive/ubuntu/jarun/) (`apt-get install nnn`) - [Void Linux](https://github.com/void-linux/void-packages/tree/master/srcpkgs/nnn) (`xbps-install -S nnn`) #### Release packages Packages for Arch Linux, CentOS, Debian, Fedora, Solus, and Ubuntu are available with the [latest stable release](https://github.com/jarun/nnn/releases/latest). #### From source To cook yourself, download the [latest stable release](https://github.com/jarun/nnn/releases/latest) or clone this repository (*risky*). Then install the dependencies and compile (e.g. on Ubuntu 16.04): $ sudo apt-get install pkg-config libncursesw5-dev $ make $ sudo make install `PREFIX` is supported, in case you want to install to a different location. Instructions for [Cygwin](https://github.com/jarun/nnn/wiki/Cygwin-instructions). #### Shell completion Search keyword and option completion scripts for Bash, Fish and Zsh can be found in respective subdirectories of [`scripts/auto-completion/`](scripts/auto-completion). Please refer to your shell's manual for installation instructions. #### USAGE #### Cmdline options ``` usage: nnn [-b key] [-C] [-e] [-i] [-l] [-p file] [-S] [-v] [-h] [PATH] The missing terminal file manager for X. positional args: PATH start dir [default: current dir] optional args: -b key bookmark key to open -C disable directory color -e use exiftool instead of mediainfo -i start in navigate-as-you-type mode -l start in light mode -p file copy selection to file (stdout if '-') -S start in disk usage analyser mode -v show program version -h show this help ``` `>` indicates the currently selected entry in `nnn`. #### Keyboard shortcuts Press ? in `nnn` to see the list anytime. ``` NAVIGATION ↑, k, ^P Up PgUp, ^U Scroll up ↓, j, ^N Down PgDn, ^D Scroll down Home, g, ^, ^A First entry ~ Go HOME End, G, $, ^E Last entry & Start dir ←, Bksp, h, ^H Parent dir - Last visited dir →, ↵, l, ^M Open file/dir . Toggle show hidden / Filter Ins, ^T Toggle nav-as-you-type b Pin current dir ^W Go to pinned dir Tab, ^I Next context d Toggle detail view `, ^/ Leader key LeaderN Go to/create context N Esc Exit prompt ^L Redraw/clear prompt ^G Quit and cd q Quit context Q, ^Q Quit ? Help, config FILES ^O Open with... n Create new D File details ^R Rename entry ⎵, ^K Copy entry path r Open dir in vidir Y, ^Y Toggle selection y List selection P Copy selection X Delete selection V Move selection ^X Delete entry f Archive entry F List archive ^F Extract archive m, M Brief/full media info e Edit in EDITOR p Open in PAGER ORDER TOGGLES ^J Disk usage S Apparent du t Modification time s Size MISC !, ^] Spawn SHELL in dir C Execute entry R Run custom script L Lock terminal ^S Run a command ``` Help & settings, file details, media info and archive listing are shown in the PAGER. Please use the PAGER-specific keys in these screens. #### Leader key The Leader key (` or ^/) provides a powerful multi-functional navigation mechanism. It is case-sensitive and understands contexts, bookmarks and handy location shortcuts. | Key | Function | |:---:| --- | | 1-4 | Go to/create selected context | | >, . | Go to next active context | | <, , | Go to previous active context | | key | Go to bookmarked location | | ~ | Go to HOME directory | | - | Go to last visited directory | | & | Go to start directory | | q | Quit context | #### Contexts Contexts serve the purpose of exploring multiple directories simultaneously. 4 contexts are available. The status of the contexts are shown in the top left corner: - the current context is in reverse - other used contexts are underlined - rest are unused To switch to a context press the Leader key followed by the context number (1-4). The first time a context is entered, it copies the state of the last visited context. Each context remembers its start directory and last visited directory. When a context is quit, the next active context is selected. If the last active context is quit, the program quits. #### Directory color Each context can have its own color for directories specified: export NNN_CONTEXT_COLORS="1234" colors: 0-black, 1-red, 2-green, 3-yellow, 4-blue (default), 5-magenta, 6-cyan, 7-white #### Filters Filters support regexes to instantly (search-as-you-type) list the matching entries in the current directory. Ways to exit filter prompt: - press ^L to clear filter followed by Bksp (to clear the filter symbol, like vi) - at other prompts ^L followed by Enter discards all changes and exits prompt - run a search with no matches and press Enter Common use cases: - to list all matches starting with the filter expression, start the expression with a `^` (caret) symbol - type `\.mkv` to list all MKV files - use `.*` to match any character (_sort of_ fuzzy search) If `nnn` is invoked as root or the environment variable `NNN_SHOW_HIDDEN` is set the default filter will also match hidden files. #### Navigate-as-you-type mode In this mode directories are opened in filter mode, allowing continuous navigation. Works best with the **arrow keys**. In case of only one match and it's a directory, `nnn` auto selects the directory and enters it in this mode. To disable this behaviour, export NNN_NO_AUTOSELECT=1 #### File indicators The following indicators are used in the detail view: | Indicator | File Type | |:---:| --- | | `/` | Directory | | `*` | Executable | | | | Fifo | | `=` | Socket | | `@` | Symbolic Link | | `@/` | Symbolic Link to directory | | `b` | Block Device | | `c` | Character Device | #### Utility dependencies | External dependency | Operation | | --- | --- | | xdg-open (Linux), open(1) (macOS), cygstart (Cygwin) | desktop opener | | file | determine file type | | cp, mv, rm, xargs (from findutils on Linux) | copy, move and remove files | | mediainfo, exiftool | multimedia file details | | atool, patool ([integration](#integrate-patool)) | create, list and extract archives | | vidir (from moreutils) | batch rename, move, delete dir entries | | vlock (Linux), bashlock (macOS), lock(1) (BSD) | terminal locker | | $EDITOR (overridden by $VISUAL, if defined) | edit files (fallback vi) | | $PAGER | page through files (fallback less) | | $SHELL | spawn a shell, run script (fallback sh) | To edit all text files in EDITOR (preferably CLI, fallback vi): export NNN_USE_EDITOR=1 Arguments to the `$EDITOR`, `$PAGER` and `$SHELL` should be combined together, e.g., export EDITOR='vim -xR' The option `open with` takes 1 combined argument. #### Help $ nnn -h $ man nnn To lookup keyboard shortcuts at runtime, press ?. #### QUICKSTART 1. Install the [utilities required](#file-handling) for your regular activities. 2. Configure [cd on quit](#cd-on-quit). 3. Optionally open all text files in EDITOR (fallback vi): export NNN_USE_EDITOR=1 4. Run `n`. 5. Press ? for help on keyboard shortcuts anytime. 6. For additional functionality [setup custom scripts](#run-custom-scripts). #### HOW TO #### add bookmarks Set environment variable `NNN_BMS` as a string of `key_char:location` pairs (max 10) separated by semicolons (`;`): export NNN_BMS='d:~/Documents;u:/home/user/Cam Uploads;D:~/Downloads/' NOTE: Bookmark keys should be single-character to use them in combination with the Leader key. #### copy file paths ##### selection Use ^K to copy the absolute path of the file under the cursor. To copy multiple absolute file paths: - press ^Y (or Y) to enter selection mode. In this mode it's possible to - cherry-pick individual files one by one by pressing ^K on each entry (works across directories and contexts); or, - navigate to another file in the same directory to select a range of files - press ^Y (or Y) _again_ to copy the paths and exit the selection mode The files in the list can now be copied (P), moved (V) or removed (X). To list the file paths copied to memory press y. File paths are copied to the temporary file `DIR/.nnncp`, where `DIR` (by priority) is: $HOME or, $TMPDIR or, /tmp The path is shown in the help and configuration screen.. To use the copied paths from the cmdline, use command substitution. For example, if `DIR` above is `/home/user`: # bash/zsh ls -ltr `cat /home/user/.nnncp` ls -ltr $(cat /home/user/.nnncp) # fish ls -ltr (cat /home/user/.nnncp) An alias may be handy, e.g. when you want to copy selection at the _run a command_ prompt: alias ncp='cat /home/user/.nnncp' so you can easily handle files together: # bash/zsh ls -ltr `ncp` ls -ltr $(ncp) # fish ls -ltr (ncp) ##### to clipboard Along with default copy, `nnn` can pipe the absolute path of the current file or multiple files to a copier script. For example, you can use `xsel` on Linux or `pbcopy` on macOS. Here's a sample [copier script](https://github.com/jarun/nnn/tree/master/scripts/copier). To inform `nnn` of the executable copier script location: export NNN_COPIER="/path/to/copier.sh" ##### get selection manually To get a space-separated list of the file paths in selection, say at the command-prompt: cat ~/.nnncp | xargs -0 echo Set an easy to remember alias: alias ncp="cat ~/.nnncp | xargs -0 echo" To get the list in a file: ncp > out.txt #### cd on quit To quit `nnn` and switch to the directory last opened follow the instructions below. Pick the appropriate file for your shell from [`scripts/quitcd`](scripts/quitcd) and add the contents to your shell's rc file. You'll need to spawn a new shell for the change to take effect. You should start `nnn` as `n` (or modify the function name to something else). To change directory on quit press `^G` while exiting. As you might notice, `nnn` uses the environment variable `NNN_TMPFILE` to write the last visited directory path. You can change it. #### (neo)vim plugin `nnn` can be used as a file picker/chooser within vim or neovim. Find the plugin [here](https://github.com/mcchrish/nnn.vim). #### run custom scripts `nnn` can invoke custom scripts with the currently selected file name as argument 1. Export the absolute path to the directory with your scripts or a single script: export NNN_SCRIPT=/home/user/scripts OR export NNN_SCRIPT=/usr/local/bin/nscript.sh Press R to run the script in the current directory. You can also use this key to cancel choosing a script from the script directory. ##### sample scripts - Open image files in current dir in **sxiv**: #!/usr/bin/env sh sxiv -q * >/dev/null 2>&1 - Fuzzy find files in **fzy** and open with xdg-open: #!/usr/bin/env sh xdg-open $(find -type f | fzy) >/dev/null 2>&1 #### launch applications Applications can be launched from the _run a command_ prompt. Use `&` to launch GUI applications in the background. #### change dir color The default color for directories is blue. Option `-c` accepts color codes from 0 to 7 to use a different color: 0-black, 1-red, 2-green, 3-yellow, 4-blue, 5-magenta, 6-cyan, 7-white Any other value disables colored directories. #### integrate patool On systems where `atool` is not available but `patool` is, drop two copies of the Python3 script [natool](https://github.com/jarun/nnn/tree/master/scripts/natool) as `atool` and `apack` somewhere in `$PATH`. #### lftp transfers lftp can be used from `nnn` for automated remote transfers or copying a selection to a server. Visit the [wiki page](https://github.com/jarun/nnn/wiki/simplify-remote-transfers-with-lftp) for more details on the integration. #### work faster at rename prompt The rename prompt supports some bash-like command-line shortcuts - ^A, ^E, ^U. ^L clears the name. #### set idle timeout The terminal locker is disabled by default. To set the wait time in seconds, use environment variable `NNN_IDLE_TIMEOUT`. #### show hot plugged drives Enable volume management in your DE file manager and set removable drives or media to be auto-mounted when inserted. Then visit the usual mount point location (`/mnt` or `/media/user`) in `nnn`. #### tmux configuration `nnn` might not handle keypresses correctly when used with tmux (see issue #104 for more details). Set `TERM=xterm-256color` to address it. #### BSD terminal issue By default in OpenBSD & FreeBSD, `stty` maps ^Y to `DSUSP`. This means that typing ^Y will suspend `nnn` as if you typed ^Z (you can bring `nnn` back to the foreground by issuing `fg`) instead of entering multi-copy mode. You can check this with `stty -a`. If it includes the text `dsusp = ^Y`, issuing `stty dsusp undef` will disable this `DSUSP` and let `nnn` receive the ^Y instead. #### Disable file open on navigation In order to disable opening files on accidental navigation key ( or l) press: export DISABLE_FILE_OPEN_ON_NAV=1 To open files with this setting, press Enter. #### WHY FORK? `nnn` was initially forked from [noice](http://git.2f30.org/noice/) but is significantly [different](https://github.com/jarun/nnn/wiki/nnn-vs.-noice) today. I chose to fork because: - one can argue my approach deviates from the goal of the original project - keep the utility `suckless`. `noice` was rudimentary. In my opinion evolution is the taste of time. - I would like to have a bit of control on what features are added in the name of desktop integration. A feature-bloat is the last thing in my mind. Check out [nnn design considerations](https://github.com/jarun/nnn/wiki/nnn-design-considerations) for more details. #### MENTIONS - [FOSSMint](https://www.fossmint.com/nnn-linux-terminal-file-browser/) - [Hacker News](https://news.ycombinator.com/item?id=18520898) - [It's FOSS](https://itsfoss.com/nnn-file-browser-linux/) - [LinuxLinks](https://www.linuxlinks.com/nnn-fast-and-flexible-file-manager/) - [Suckless Rocks](suckless.org/rocks/) - [Ubuntu Full Circle Magazine - Issue 135](https://fullcirclemagazine.org/issue-135/) #### DEVELOPERS 1. Copyright © 2014-2016 Lazaros Koromilas 2. Copyright © 2014-2016 Dimitris Papastamos 3. Copyright © 2016-2019 [Arun Prakash Jana](https://github.com/jarun) nnn-2.2/nnn.1000066400000000000000000000216041341255065400130310ustar00rootroot00000000000000.Dd Jan 01, 2019 .Dt NNN 1 .Os .Sh NAME .Nm nnn .Nd the missing terminal file manager for X .Sh SYNOPSIS .Nm .Op Ar -b key .Op Ar -C .Op Ar -e .Op Ar -i .Op Ar -l .Op Ar -p file .Op Ar -S .Op Ar -v .Op Ar -h .Op Ar PATH .Sh DESCRIPTION .Nm (Noice is Not Noice) is a performance-optimized, feature-packed fork of noice (http://git.2f30.org/noice/) with seamless desktop integration, simplified navigation, \fInavigate-as-you-type\fR mode with auto select, disk usage analyzer mode, bookmarks, contexts, application launcher, familiar navigation shortcuts, subshell spawning and much more. It remains a simple and efficient file manager that stays out of your way. .Pp .Nm opens the current working directory by default if .Ar PATH is not specified. .Sh KEYBINDS .Pp .Nm supports both vi-like and emacs-like key bindings as listed below. .Pp NAVIGATION .Pp .Bl -tag -width "l, [Right], [Return] or C-mXXXX" -offset indent -compact .It Ic [Up], k, ^P Move to previous entry .It Ic [Down], j, ^N Move to next entry .It Ic [PgUp], ^U Scroll up half a page .It Ic [PgDn], ^D Scroll down half a page .It Ic [Home], g, ^, ^A Move to the first entry .It Ic [End], G, $, ^E Move to the last entry .It Ic [Left], [Backspace], h, ^H Go to parent directory .It Ic [Right], [Enter], l, ^M Open file or enter directory .It Ic ~ Change to the HOME directory .It Ic & Change to initial directory .It Ic \- Change to the last visited directory .It Ic \&. Toggle show hidden . (dot) files .It Ic / Change filter (more information below) .It Ic [Insert], ^T Toggle navigate-as-you-type mode .It Ic b Pin current directory .It Ic ^W Visit pinned directory .It Ic d Toggle detail view .It Ic Tab, ^I Next context, ask to create if none .It Ic `, ^/ Leader key .It Ic LeaderN Switch to context N .It Ic Esc Exit prompt .It Ic ^L Force a redraw, clear prompt .It Ic q Quit the current context .It Ic ^G Quit and change directory .It Ic Q, ^Q Quit .It Ic \&? Help and configuration screen .El .Pp FILES .Pp .Bl -tag -width "l, [Right], [Return] or C-mXXXX" -offset indent -compact .It Ic ^O Open with an application (takes 1 combined argument) .It Ic n Create a new file or directory .It Ic D Show entry details .It Ic ^R Rename selected entry .It Ic r Open directory in vidir .It Ic Y, ^Y Toggle selection mode .It Ic Space, ^K Copy entry absolute path .It Ic y Show selection list .It Ic P Copy files from selection .It Ic V Move files from selection .It Ic X Delete files from selection .It Ic ^X Delete entry .It Ic f Archive entry .It Ic F List files in archive .It Ic ^F Extract archive in current directory .It Ic m, M Show brief/full media info .It Ic e Open entry in EDITOR (fallback vi) .It Ic p Open entry in PAGER (fallback less) .El .Pp ORDER TOGGLES .Pp .Bl -tag -width "l, [Right], [Return] or C-mXXXX" -offset indent -compact .It Ic ^J Toggle disk usage analyzer mode .It Ic S Toggle sort by apparent size .It Ic t Toggle sort by time modified .It Ic s Toggle sort by file size .El .Pp MISC .Pp .Bl -tag -width "l, [Right], [Return] or C-mXXXX" -offset indent -compact .It Ic \&!, ^] Spawn SHELL in current directory (fallback sh) .It Ic C Execute entry .It Ic R Run or choose a custom script .It Ic L Lock terminal .It Ic ^S Run a command .El .Pp Backing up one directory level will set the cursor position at the directory you came out of. .Pp Help & settings, file details, media info and archive listing are shown in the PAGER. Please use the PAGER-specific keys in these screens. .Sh OPTIONS .Pp .Nm supports the following options: .Pp .Fl "b key" specify bookmark key to open .Pp .Fl C disable directory color .Pp .Fl e use exiftool instead of mediainfo .Pp .Fl i start in navigate-as-you-type mode .Pp .Fl l start in light mode (fewer details) .Pp .Fl "p file" copy (or \fIpick\fR) selection to file, or stdout if file='-' .Pp .Fl S start in disk usage analyzer mode .Pp .Fl v show version and exit .Pp .Fl h show program help and exit .Sh CONFIGURATION .Nm uses \fIxdg-open\fR (on Linux) and \fIopen(1)\fR (on macOS) as the desktop opener. .Pp There is no configuration file. Settings work on environment variables. Please refer to the ENVIRONMENT section below. .Pp Configuring .Nm to change to the last visited directory on quit requires shell integration in a few easy steps. Please visit the project page (linked below) for the instructions. .Sh CONTEXTS Contexts serve the purpose of exploring multiple directories simultaneously. 4 contexts are available. The status of the contexts are shown in the top left corner: .Pp - the current context is in reverse .br - other used contexts are underlined .br - rest are unused .Pp To switch to a context press the Leader key followed by the context number (1-4). .Pp The first time a context is entered, it copies the state of the last visited context. Each context remembers its start directory and last visited directory. .Pp When a context is quit, the next active context is selected. If the last active context is quit, the program quits. .Sh FILTERS Filters support regexes to instantly (search-as-you-type) list the matching entries in the current directory. .Pp Ways to exit filter prompt: .Pp (1) press \fI^L\fR to clear filter followed by \fIBksp\fR (to clear the filter symbol, like vi) .br - at other prompts \fI^L\fR followed by \fIEnter\fR discards all changes and exits prompt .br (2) run a search with no matches and press \fIEnter\fR .Pp Common use cases: .Pp (1) To list all matches starting with the filter expression, start the expression with a '^' (caret) symbol. .br (2) Type '\\.mkv' to list all MKV files. .br (3) Use '.*' to match any character (\fIsort of\fR fuzzy search). .Pp If .Nm is invoked as root or the environment variable \fBNNN_SHOW_HIDDEN\fR is set the default filter will also match hidden files. .Pp In the \fInavigate-as-you-type\fR mode directories are opened in filter mode, allowing continuous navigation. Works best with the \fBarrow keys\fR. .br In case of only one match and it's a directory, `nnn` auto selects the directory and enters it in this mode. .Sh SELECTION MODE The absolute path of a single file can be copied to clipboard by pressing \fI^K\fR if NNN_COPIER is set (see ENVIRONMENT section below). .Pp To copy multiple file paths the selection mode should be enabled using \fI^Y\fR. In this mode it's possible to .Pp (1) cherry-pick individual files one by one by pressing ^K on each entry (works across directories and contexts); or, .br (2) navigate to another file in the same directory to select a range of files. .Pp Pressing \fI^Y\fR again copies the paths and exits the selection mode. The files in the list can now be copied, moved or removed using respective keyboard shortcuts. .Pp To list the file paths copied to memory press \fIy\fR. .Sh ENVIRONMENT The SHELL, EDITOR (VISUAL, if defined) and PAGER environment variables take precedence when dealing with the !, e and p commands respectively. A single combination to arguments is supported, e.g.: .Bd -literal export EDITOR='vim -xR' .Ed .Pp \fBNNN_BMS:\fR bookmark string as \fIkey_char:location\fR pairs (max 10) separated by \fI;\fR: .Bd -literal export NNN_BMS='d:~/Documents;u:/home/user/Cam Uploads;D:~/Downloads/' NOTE: Bookmark keys should be single-character to use them in combination with the Leader key. .Ed .Pp \fBNNN_USE_EDITOR:\fR use EDITOR (preferably CLI, fallback vi) to handle text files. .Bd -literal export NNN_USE_EDITOR=1 .Ed .Pp \fBNNN_CONTEXT_COLORS:\fR string of color codes for each context, e.g.: .Bd -literal export NNN_CONTEXT_COLORS="1234" codes: 0-black, 1-red, 2-green, 3-yellow, 4-blue (default), 5-magenta, 6-cyan, 7-white .Ed .Pp \fBNNN_IDLE_TIMEOUT:\fR set idle timeout (in seconds) to invoke terminal locker (default: disabled). .Pp \fBNNN_COPIER:\fR set to a clipboard copier script. .Bd -literal NOTE: By default file paths are copied to the tmp file \fBDIR/.nnncp\fR, where 'DIR' (by priority) is: \fI$HOME\fR or, \fI$TMPDIR\fR or, \fI/tmp\fR. The path is shown in the help and configuration screen. .Ed .Pp \fBNNN_SCRIPT:\fR absolute path to a directory to select a script from or a single script to invoke with currently selected file name as argument 1. .Bd -literal export NNN_SCRIPT=/home/user/scripts OR export NNN_SCRIPT=/usr/local/bin/nscript.sh .Ed .Pp \fBNNN_SHOW_HIDDEN:\fR show hidden files. .Bd -literal export NNN_SHOW_HIDDEN=1 .Ed .Pp \fBNNN_NO_AUTOSELECT:\fR disable directory auto-selection in \fInavigate-as-you-type\fR mode. .Bd -literal export NNN_NO_AUTOSELECT=1 .Ed .Pp \fBDISABLE_FILE_OPEN_ON_NAV:\fR disable file open on \fBRight\fR or \fBl\fR keys (\fBEnter\fR opens files). .Bd -literal export DISABLE_FILE_OPEN_ON_NAV=1 .Ed .Sh KNOWN ISSUES If you are using urxvt you might have to set backspace key to DEC. .Sh AUTHORS .An Lazaros Koromilas Aq Mt lostd@2f30.org , .An Dimitris Papastamos Aq Mt sin@2f30.org , .An Arun Prakash Jana Aq Mt engineerarun@gmail.com . .Sh HOME .Em https://github.com/jarun/nnn nnn-2.2/packagecore.yaml000066400000000000000000000040001341255065400152750ustar00rootroot00000000000000name: nnn maintainer: Arun Prakash Jana license: BSD 2-Clause summary: The missing terminal file manager for X. homepage: https://github.com/jarun/nnn commands: install: - make PREFIX="/usr" strip install DESTDIR="${BP_DESTDIR}" packages: archlinux: builddeps: - make - gcc - pkg-config deps: - ncurses container: "base/archlinux" centos7.2: builddeps: - make - gcc - pkgconfig - ncurses-devel deps: - ncurses commands: pre: - yum install epel-release centos7.3: builddeps: - make - gcc - pkgconfig - ncurses-devel deps: - ncurses commands: pre: - yum install epel-release centos7.4: builddeps: - make - gcc - pkgconfig - ncurses-devel deps: - ncurses commands: pre: - yum install epel-release debian9: builddeps: - make - gcc - pkg-config - libncursesw5-dev deps: - libncursesw5 fedora25: builddeps: - make - gcc - pkgconfig - ncurses-devel deps: - ncurses fedora26: builddeps: - make - gcc - pkg-config - ncurses-devel deps: - ncurses fedora27: builddeps: - make - gcc - pkg-config - ncurses-devel deps: - ncurses fedora28: builddeps: - make - gcc - pkg-config - ncurses-devel deps: - ncurses fedora29: builddeps: - make - gcc - pkg-config - ncurses-devel deps: - ncurses # opensuse42.3: # builddeps: # - make # - gcc # - pkg-config # - ncurses-devel # deps: # - ncurses ubuntu16.04: builddeps: - make - gcc - pkg-config - libncursesw5-dev deps: - libncursesw5 ubuntu18.04: builddeps: - make - gcc - pkg-config - libncursesw5-dev deps: - libncursesw5 nnn-2.2/scripts/000077500000000000000000000000001341255065400136425ustar00rootroot00000000000000nnn-2.2/scripts/auto-completion/000077500000000000000000000000001341255065400167615ustar00rootroot00000000000000nnn-2.2/scripts/auto-completion/bash/000077500000000000000000000000001341255065400176765ustar00rootroot00000000000000nnn-2.2/scripts/auto-completion/bash/nnn-completion.bash000066400000000000000000000013541341255065400235000ustar00rootroot00000000000000# # Rudimentary Bash completion definition for nnn. # # Author: # Arun Prakash Jana # _nnn () { COMPREPLY=() local IFS=$' \n' local cur=$2 prev=$3 local -a opts opts_with_args opts=( -b -C -e -h -i -l -p -S -v ) opts_with_arg=( -b -p ) # Do not complete non option names [[ $cur == -* ]] || return 1 # Do not complete when the previous arg is an option expecting an argument for opt in "${opts_with_arg[@]}"; do [[ $opt == $prev ]] && return 1 done # Complete option names COMPREPLY=( $(compgen -W "${opts[*]}" -- "$cur") ) return 0 } complete -F _nnn nnn nnn-2.2/scripts/auto-completion/fish/000077500000000000000000000000001341255065400177125ustar00rootroot00000000000000nnn-2.2/scripts/auto-completion/fish/nnn.fish000066400000000000000000000011601341255065400213540ustar00rootroot00000000000000# # Fish completion definition for nnn. # # Author: # Arun Prakash Jana # complete -c nnn -s b -r -d 'bookmark key to open' complete -c nnn -s C -d 'disable directory color' complete -c nnn -s e -d 'use exiftool instead of mediainfo' complete -c nnn -s h -d 'show this help and exit' complete -c nnn -s i -d 'start in navigate-as-you-type mode' complete -c nnn -s l -d 'start in light mode (fewer details)' complete -c nnn -s p -r -d 'copy selection to file' complete -c nnn -s S -d 'start in disk usage analyzer mode' complete -c nnn -s v -d 'show program version and exit' nnn-2.2/scripts/auto-completion/zsh/000077500000000000000000000000001341255065400175655ustar00rootroot00000000000000nnn-2.2/scripts/auto-completion/zsh/_nnn000066400000000000000000000011701341255065400204370ustar00rootroot00000000000000#compdef nnn # # Completion definition for nnn. # # Author: # Arun Prakash Jana # setopt localoptions noshwordsplit noksharrays local -a args args=( '(-b)-b[bookmark key to open]:key char' '(-C)-C[disable directory color]' '(-e)-e[use exiftool instead of mediainfo]' '(-h)-h[show this help and exit]' '(-i)-i[start in navigate-as-you-type mode]' '(-l)-l[start in light mode (fewer details)]' '(-p)-p[copy selection to file]:file name' '(-S)-S[start in disk usage analyzer mode]' '(-v)-v[show program version and exit]' '*:filename:_files' ) _arguments -S -s $args nnn-2.2/scripts/copier/000077500000000000000000000000001341255065400151235ustar00rootroot00000000000000nnn-2.2/scripts/copier/copier.sh000077500000000000000000000003471341255065400167470ustar00rootroot00000000000000#!/bin/sh # Linux cat ~/.nnncp | xargs -0 | xsel -bi # macOS # cat ~/.nnncp | xargs -0 | pbcopy # Termux # cat /data/data/com.termux/files/home/.nnncp | xargs -0 | termux-clipboard-set # Cygwin # cat ~/.nnncp | xargs -0 | clip nnn-2.2/scripts/natool/000077500000000000000000000000001341255065400151365ustar00rootroot00000000000000nnn-2.2/scripts/natool/natool000077500000000000000000000024371341255065400163660ustar00rootroot00000000000000#!/usr/bin/env python3 # ############################################################################# # natool: a wrapper script to patool to list, extract and create archives # # usage: natool [-l] [-x] [archive] [file/dir] # # Examples: # - list archive : natool -l archive.7z # - extract archive: natool -x archive.7z # - create archive : natool archive.7z archive_dir # # Brief: # natool is written to integrate patool (instead of the default atool) with nnn # Two copies of this file should be dropped somewhere in $PATH - atool, apack # # Author: Arun Prakash Jana # Email: engineerarun@gmail.com # Homepage: https://github.com/jarun/nnn # Copyright © 2019 Arun Prakash Jana # ############################################################################# import sys from subprocess import Popen, PIPE, DEVNULL if len(sys.argv) != 3: print('usage: natool [-l] [-x] [archive] [file/dir]') sys.exit(0) if sys.argv[1] == '-x': cmd = ['patool', '--non-interactive', 'extract', sys.argv[2]] elif sys.argv[1] == '-l': cmd = ['patool', '--non-interactive', 'list', sys.argv[2]] else: cmd = ['patool', '--non-interactive', 'create', sys.argv[1], sys.argv[2]] pipe = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) out, err = pipe.communicate() print(out.decode()) print(err.decode()) nnn-2.2/scripts/nlay/000077500000000000000000000000001341255065400146055ustar00rootroot00000000000000nnn-2.2/scripts/nlay/nlay000077500000000000000000000053411341255065400155010ustar00rootroot00000000000000#!/usr/bin/env bash # ############################################################################# # nlay: a customizable script to play files in different apps by file type # # usage: nlay file/path type/action # # MUST READ: # # 1. Feel free to change the default apps to your favourite ones. # If you change the app for a group you may also need to modify the opts and # bg settings. If bg is set the app is detached and started in the background # in silent mode. # # The bg setting depends on personal preferences and type of utility, e.g., I # would start vi (CLI) in the foreground but Sublime Text (GUI) in background. # # Check (and TOGGLE as you wish) the default bg settings. # # 2. Detached apps are not killed when nnn exits. Use kill(1) or killall(1) to # stop console based background apps. # # 3. nlay is OVERWRITTEN during nnn upgrade. You can store your custom nlay in a # location other than the default and have an alias with nnn option '-p' to # invoke it. Remember it might break or lack new capabilities added to nlay # in future releases. Check the file diff once in a while. # # Author: Arun Prakash Jana # Email: engineerarun@gmail.com # Homepage: https://github.com/jarun/nnn # Copyright © 2016-2019 Arun Prakash Jana # ############################################################################# # Enable the lines below to handle file by extension # This is provided for using a custom player for specific files # $ext holds the extension </dev/null 2>&1 &" ">/dev/null 2>&1 &") #------------------ LOCKER ------------------ elif [ "$2" == "locker" ]; then app=("vlock" "bashlock" "lock") for index in ${!app[@]} do type -P ${app[$index]} &>/dev/null && eval ${app[$index]} && exit 0 done #------------------ SCRIPT ------------------ elif [ "$2" == "script" ]; then # add commands or a custom script below # echo "my commands or custom script" # sh "path_to_script.sh" $SHELL "$1" exit 0 fi #----------------- RUN APP ------------------ for index in ${!app[@]} do type -P ${app[$index]} &>/dev/null && eval ${app[$index]} ${opts[$index]} "\"$1\"" ${bg[$index]} && break done nnn-2.2/scripts/nlay/nlay.1000066400000000000000000000023221341255065400156310ustar00rootroot00000000000000.Dd Mar 14, 2018 .Dt NLAY 1 .Os .Sh NAME .Nm nlay .Nd a bash script to play files in different apps by file type or run some actions. .Sh SYNOPSIS .Nm file/path type/action .Sh DESCRIPTION .Nm is shipped with \fInnn\fR to deliver a level of flexibility to users to choose their own apps when running some actions, run some commands or custom scripts. It has provisions to handle text files too. However, the capability is not used in the latest releases. Now .Nm is invoked to run a desktop search (\fIgnome-search-tool\fR or \fIcatfish\fR) or screen locker (\fIvlock\fR or \fIbashlock\fR or \fIlock\fR) utility. However, .Nm can run independently and can be highly customized for personal usage. .Pp .Nm supports the following options: .Pp "file/path" The first argument can be the file or path to pass as an argument to the app. It can also be an empty string e.g., while locking the terminal. .Pp "type/action" This can be any of the strings \fItext\fR, \fIsearch\fR, \fIscript\fR or \fIlocker\fR. .Sh USAGE .Pp .Bd -literal $ nlay info.txt text $ nlay . search $ nlay ~/script.sh script $ nlay "" locker .Ed .Sh AUTHOR .An Arun Prakash Jana Aq Mt engineerarun@gmail.com . .Sh HOME .Em https://github.com/jarun/nnn nnn-2.2/scripts/quitcd/000077500000000000000000000000001341255065400151335ustar00rootroot00000000000000nnn-2.2/scripts/quitcd/quitcd.bash000066400000000000000000000002671341255065400172700ustar00rootroot00000000000000export NNN_TMPFILE="/tmp/nnn" n() { nnn "$@" if [ -f $NNN_TMPFILE ]; then . $NNN_TMPFILE rm -f $NNN_TMPFILE > /dev/null fi } nnn-2.2/scripts/quitcd/quitcd.csh000066400000000000000000000001251341255065400171210ustar00rootroot00000000000000setenv NNN_TMPFILE /tmp/nnn alias n 'nnn; source "$NNN_TMPFILE"; rm "$NNN_TMPFILE"' nnn-2.2/scripts/quitcd/quitcd.fish000066400000000000000000000003471341255065400173030ustar00rootroot00000000000000export NNN_TMPFILE="/tmp/nnn" function n --description 'support nnn quit and change directory' nnn $argv if test -e $NNN_TMPFILE source $NNN_TMPFILE rm $NNN_TMPFILE end end nnn-2.2/scripts/quitcd/quitcd.zsh000066400000000000000000000002501341255065400171470ustar00rootroot00000000000000export NNN_TMPFILE="/tmp/nnn" n() { nnn "$@" if [ -f $NNN_TMPFILE ]; then . $NNN_TMPFILE rm $NNN_TMPFILE fi } nnn-2.2/scripts/test/000077500000000000000000000000001341255065400146215ustar00rootroot00000000000000nnn-2.2/scripts/test/mktest.sh000077500000000000000000000035451341255065400164760ustar00rootroot00000000000000#!/bin/sh # Create test files and directories test -e outdir && { echo "Remove 'outdir' and try again" exit 1 } mkdir -p outdir && cd outdir echo 'It works!' > normal.txt echo 'Με δουλέβει;' > 'κοινό.txt' ln -sf normal.txt ln-normal.txt ln -sf normal.txt ln-normal mkdir -p normal-dir ln -sf normal-dir ln-normal-dir ln -sf nowhere ln-nowhere mkfifo mk-fifo touch no-access && chmod 000 no-access mkdir -p no-access-dir && chmod 000 no-access-dir ln -sf ../normal.txt normal-dir/ln-normal.txt ln -sf ../normal.txt normal-dir/ln-normal echo 'int main(void) { *((char *)0) = 0; }' > ill.c make ill > /dev/null echo 'test/ill' > ill.sh mkdir -p empty-dir mkdir -p cage echo 'chmod 000 test/cage' > cage/lock.sh echo 'chmod 755 test/cage' > cage-unlock.sh mkdir -p cage/lion echo 'chmod 000 test/cage' > cage/lion/lock.sh mkdir -p unicode touch 'unicode/Malgudi Days - मालगुडी डेज - E05. Swami and Friends - स्वामी और उसके दोस्त (Part 1)' touch 'unicode/Malgudi Days - मालगुडी डेज - E05. Swami and Friends - स्वामी और उसके दोस्त (Part 2)' touch 'unicode/Malgudi Days - मालगुडी डेज - E05. Swami and Friends - स्वामी और उसके दोस्त (Part 3)' chmod +x 'unicode/Malgudi Days - मालगुडी डेज - E05. Swami and Friends - स्वामी और उसके दोस्त (Part 2)' touch 'unicode/Führer' touch 'unicode/Eso eso aamar ghare eso ♫ এসো এসো আমার ঘরে এসো ♫ Swagatalakshmi Dasgupta' touch 'max_chars_filename_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' nnn-2.2/src/000077500000000000000000000000001341255065400127425ustar00rootroot00000000000000nnn-2.2/src/nnn.c000066400000000000000000002533361341255065400137130ustar00rootroot00000000000000/* * BSD 2-Clause License * * Copyright (c) 2014-2016, Lazaros Koromilas * Copyright (c) 2014-2016, Dimitris Papastamos * Copyright (c) 2016-2019, Arun Prakash Jana * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifdef __linux__ #if defined(__arm__) || defined(__i386__) #define _FILE_OFFSET_BITS 64 /* Support large files on 32-bit */ #endif #include #define LINUX_INOTIFY #if !defined(__GLIBC__) #include #endif #endif #include #include #include #if defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__) #include #include #include #define BSD_KQUEUE #else #include #endif #include #include #ifdef __linux__ /* Fix failure due to mvaddnwstr() */ #ifndef NCURSES_WIDECHAR #define NCURSES_WIDECHAR 1 #endif #elif defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__) #ifndef _XOPEN_SOURCE_EXTENDED #define _XOPEN_SOURCE_EXTENDED #endif #endif #ifndef __USE_XOPEN /* Fix failure due to wcswidth(), ncursesw/curses.h includes whcar.h on Ubuntu 14.04 */ #define __USE_XOPEN #endif #include #include #include #include #include #include #ifdef __gnu_hurd__ #define PATH_MAX 4096 #endif #include #include #include #include #include #include #include #include #include #include #include #ifndef __USE_XOPEN_EXTENDED #define __USE_XOPEN_EXTENDED 1 #endif #include #include #ifndef S_BLKSIZE #define S_BLKSIZE 512 /* S_BLKSIZE is missing on Android NDK (Termux) */ #endif #include "nnn.h" #ifdef DEBUGMODE static int DEBUG_FD; static int xprintf(int fd, const char *fmt, ...) { char buf[BUFSIZ]; int r; va_list ap; va_start(ap, fmt); r = vsnprintf(buf, sizeof(buf), fmt, ap); if (r > 0) r = write(fd, buf, r); va_end(ap); return r; } static int enabledbg() { FILE *fp = fopen("/tmp/nnn_debug", "w"); if (!fp) { fprintf(stderr, "debug: open failed! (1)\n"); fp = fopen("./nnn_debug", "w"); if (!fp) { fprintf(stderr, "debug: open failed! (2)\n"); return -1; } } DEBUG_FD = fileno(fp); if (DEBUG_FD == -1) { fprintf(stderr, "debug: open fd failed!\n"); return -1; } return 0; } static void disabledbg() { close(DEBUG_FD); } #define DPRINTF_D(x) xprintf(DEBUG_FD, #x "=%d\n", x) #define DPRINTF_U(x) xprintf(DEBUG_FD, #x "=%u\n", x) #define DPRINTF_S(x) xprintf(DEBUG_FD, #x "=%s\n", x) #define DPRINTF_P(x) xprintf(DEBUG_FD, #x "=%p\n", x) #else #define DPRINTF_D(x) #define DPRINTF_U(x) #define DPRINTF_S(x) #define DPRINTF_P(x) #endif /* DEBUGMODE */ /* Macro definitions */ #define VERSION "2.2" #define GENERAL_INFO "License: BSD 2-Clause\nWebpage: https://github.com/jarun/nnn" #define LEN(x) (sizeof(x) / sizeof(*(x))) #undef MIN #define MIN(x, y) ((x) < (y) ? (x) : (y)) #define ISODD(x) ((x) & 1) #define TOUPPER(ch) \ (((ch) >= 'a' && (ch) <= 'z') ? ((ch) - 'a' + 'A') : (ch)) #define CMD_LEN_MAX (PATH_MAX + ((NAME_MAX + 1) << 1)) #define CURSR " > " #define EMPTY " " #define CURSYM(flag) ((flag) ? CURSR : EMPTY) #define FILTER '/' #define REGEX_MAX 128 #define BM_MAX 10 #define ENTRY_INCR 64 /* Number of dir 'entry' structures to allocate per shot */ #define NAMEBUF_INCR 0x1000 /* 64 dir entries at a time, avg. 64 chars per filename = 64*64B = 4KB */ #define DESCRIPTOR_LEN 32 #define _ALIGNMENT 0x10 /* 16-byte alignment */ #define _ALIGNMENT_MASK 0xF #define SYMLINK_TO_DIR 0x1 #define HOME_LEN_MAX 64 #define CTX_MAX 4 #define DOT_FILTER_LEN 7 /* Macros to define process spawn behaviour as flags */ #define F_NONE 0x00 /* no flag set */ #define F_MARKER 0x01 /* draw marker to indicate nnn spawned (e.g. shell) */ #define F_NOWAIT 0x02 /* don't wait for child process (e.g. file manager) */ #define F_NOTRACE 0x04 /* suppress stdout and strerr (no traces) */ #define F_SIGINT 0x08 /* restore default SIGINT handler */ #define F_NORMAL 0x80 /* spawn child process in non-curses regular CLI mode */ /* CRC8 macros */ #define WIDTH (sizeof(unsigned char) << 3) #define TOPBIT (1 << (WIDTH - 1)) #define POLYNOMIAL 0xD8 /* 11011 followed by 0's */ #define CRC8_TABLE_LEN 256 /* Volume info */ #define FREE 0 #define CAPACITY 1 /* Function macros */ #define exitcurses() endwin() #define clearprompt() printmsg("") #define printwarn() printmsg(strerror(errno)) #define istopdir(path) ((path)[1] == '\0' && (path)[0] == '/') #define copycurname() xstrlcpy(lastname, dents[cur].name, NAME_MAX + 1) #define settimeout() timeout(1000) #define cleartimeout() timeout(-1) #define errexit() printerr(__LINE__) #define setdirwatch() (cfg.filtermode ? (presel = FILTER) : (dir_changed = TRUE)) #ifdef LINUX_INOTIFY #define EVENT_SIZE (sizeof(struct inotify_event)) #define EVENT_BUF_LEN (1024 * (EVENT_SIZE + 16)) #elif defined(BSD_KQUEUE) #define NUM_EVENT_SLOTS 1 #define NUM_EVENT_FDS 1 #endif /* TYPE DEFINITIONS */ typedef unsigned long ulong; typedef unsigned int uint; typedef unsigned char uchar; typedef unsigned short ushort; /* STRUCTURES */ /* Directory entry */ typedef struct entry { char *name; time_t t; off_t size; blkcnt_t blocks; /* number of 512B blocks allocated */ mode_t mode; ushort nlen; /* Length of file name; can be uchar (< NAME_MAX + 1) */ uchar flags; /* Flags specific to the file */ } __attribute__ ((packed, aligned(_ALIGNMENT))) *pEntry; /* Bookmark */ typedef struct { int key; char *loc; } bm; /* Settings */ typedef struct { uint filtermode : 1; /* Set to enter filter mode */ uint mtimeorder : 1; /* Set to sort by time modified */ uint sizeorder : 1; /* Set to sort by file size */ uint apparentsz : 1; /* Set to sort by apparent size (disk usage) */ uint blkorder : 1; /* Set to sort by blocks used (disk usage) */ uint showhidden : 1; /* Set to show hidden files */ uint copymode : 1; /* Set when copying files */ uint autoselect : 1; /* Auto-select dir in nav-as-you-type mode */ uint showdetail : 1; /* Clear to show fewer file info */ uint showcolor : 1; /* Set to show dirs in blue */ uint dircolor : 1; /* Current status of dir color */ uint metaviewer : 1; /* Index of metadata viewer in utils[] */ uint ctxactive : 1; /* Context active or not */ uint reserved : 10; /* The following settings are global */ uint curctx : 2; /* Current context number */ uint picker : 1; /* Write selection to user-specified file */ uint pickraw : 1; /* Write selection to sdtout before exit */ uint nonavopen : 1; /* Open file on right arrow or `l` */ uint useeditor : 1; /* Use VISUAL to open text files */ uint runscript : 1; /* Choose script to run mode */ uint runctx : 2; /* The context in which script is to be run */ } settings; /* Contexts or workspaces */ typedef struct { char c_path[PATH_MAX]; /* Current dir */ char c_init[PATH_MAX]; /* Initial dir */ char c_last[PATH_MAX]; /* Last visited dir */ char c_name[NAME_MAX + 1]; /* Current file name */ settings c_cfg; /* Current configuration */ uint color; /* Color code for directories */ } context; /* GLOBALS */ /* Configuration, contexts */ static settings cfg = {0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0}; static context g_ctx[CTX_MAX] __attribute__ ((aligned)); static struct entry *dents; static char *pnamebuf, *pcopybuf; static int ndents, cur, total_dents = ENTRY_INCR; static uint idle; static uint idletimeout, copybufpos, copybuflen; static char *copier; static char *editor, *editor_arg; static char *pager, *pager_arg; static char *shell, *shell_arg; static char *runpath; static blkcnt_t ent_blocks; static blkcnt_t dir_blocks; static ulong num_files; static uint open_max; static bm bookmark[BM_MAX]; static size_t g_tmpfplen; /* path to tmp files for copy without X, keybind help and file stats */ static uchar g_crc; static uchar BLK_SHIFT = 9; /* CRC data */ static uchar crc8table[CRC8_TABLE_LEN] __attribute__ ((aligned)); /* For use in functions which are isolated and don't return the buffer */ static char g_buf[CMD_LEN_MAX] __attribute__ ((aligned)); /* Buffer for file path copy file */ static char g_cppath[PATH_MAX] __attribute__ ((aligned)); /* Buffer to store tmp file path */ static char g_tmpfpath[HOME_LEN_MAX] __attribute__ ((aligned)); #ifdef LINUX_INOTIFY static int inotify_fd, inotify_wd = -1; static uint INOTIFY_MASK = IN_ATTRIB | IN_CREATE | IN_DELETE | IN_DELETE_SELF | IN_MODIFY | IN_MOVE_SELF | IN_MOVED_FROM | IN_MOVED_TO; #elif defined(BSD_KQUEUE) static int kq, event_fd = -1; static struct kevent events_to_monitor[NUM_EVENT_FDS]; static uint KQUEUE_FFLAGS = NOTE_DELETE | NOTE_EXTEND | NOTE_LINK | NOTE_RENAME | NOTE_REVOKE | NOTE_WRITE; static struct timespec gtimeout; #endif /* Replace-str for xargs on different platforms */ #if defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__) #define REPLACE_STR 'J' #elif defined(__linux__) || defined(__CYGWIN__) #define REPLACE_STR 'I' #else #define REPLACE_STR 'I' #endif /* Options to identify file mime */ #ifdef __APPLE__ #define FILE_OPTS "-bI" #else #define FILE_OPTS "-bi" #endif /* Macros for utilities */ #define MEDIAINFO 0 #define EXIFTOOL 1 #define OPENER 2 #define ATOOL 3 #define APACK 4 #define VIDIR 5 #define LOCKER 6 #define UNKNOWN 7 /* Utilities to open files, run actions */ static char * const utils[] = { "mediainfo", "exiftool", #ifdef __APPLE__ "/usr/bin/open", #elif defined __CYGWIN__ "cygstart", #else "xdg-open", #endif "atool", "apack", "vidir", #ifdef __APPLE__ "bashlock", #elif defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) "lock", #else "vlock", #endif "UNKNOWN" }; /* Common strings */ #define STR_NFTWFAIL_ID 0 #define STR_NOHOME_ID 1 #define STR_INPUT_ID 2 #define STR_INVBM_KEY 3 #define STR_COPY_ID 4 #define STR_DATE_ID 5 static const char messages[][16] = { "nftw failed", "HOME not set", "no traversal", "invalid key", "copy not set", "%F %T %z", }; /* Forward declarations */ static void redraw(char *path); static void spawn(const char *file, const char *arg1, const char *arg2, const char *dir, uchar flag); int (*nftw_fn) (const char *fpath, const struct stat *sb, int typeflag, struct FTW *ftwbuf); /* Functions */ /* * CRC8 source: * https://barrgroup.com/Embedded-Systems/How-To/CRC-Calculation-C-Code */ static void crc8init() { uchar remainder, bit; uint dividend; /* Compute the remainder of each possible dividend */ for (dividend = 0; dividend < CRC8_TABLE_LEN; ++dividend) { /* Start with the dividend followed by zeros */ remainder = dividend << (WIDTH - 8); /* Perform modulo-2 division, a bit at a time */ for (bit = 8; bit > 0; --bit) { /* Try to divide the current data bit */ if (remainder & TOPBIT) remainder = (remainder << 1) ^ POLYNOMIAL; else remainder = (remainder << 1); } /* Store the result into the table */ crc8table[dividend] = remainder; } } static uchar crc8fast(uchar const message[], size_t n) { static uchar data, remainder; static size_t byte; /* Divide the message by the polynomial, a byte at a time */ for (remainder = byte = 0; byte < n; ++byte) { data = message[byte] ^ (remainder >> (WIDTH - 8)); remainder = crc8table[data] ^ (remainder << 8); } /* The final remainder is the CRC */ return remainder; } /* Messages show up at the bottom */ static void printmsg(const char *msg) { mvprintw(LINES - 1, 0, "%s\n", msg); } /* Kill curses and display error before exiting */ static void printerr(int linenum) { exitcurses(); fprintf(stderr, "line %d: (%d) %s\n", linenum, errno, strerror(errno)); if (!cfg.picker && g_cppath[0]) unlink(g_cppath); free(pcopybuf); exit(1); } /* Print prompt on the last line */ static void printprompt(const char *str) { clearprompt(); printw(str); } static int get_input(const char *prompt) { if (prompt) printprompt(prompt); cleartimeout(); int r = getch(); settimeout(); return r; } static char confirm_force() { int r = get_input("use force? ('y/Y' confirms, else interactive)"); if (r == 'y' || r == 'Y') return 'f'; /* forceful */ return 'i'; /* interactive */ } /* Increase the limit on open file descriptors, if possible */ static rlim_t max_openfds() { struct rlimit rl; rlim_t limit = getrlimit(RLIMIT_NOFILE, &rl); if (limit != 0) return 32; limit = rl.rlim_cur; rl.rlim_cur = rl.rlim_max; /* Return ~75% of max possible */ if (setrlimit(RLIMIT_NOFILE, &rl) == 0) { limit = rl.rlim_max - (rl.rlim_max >> 2); /* * 20K is arbitrary. If the limit is set to max possible * value, the memory usage increases to more than double. */ return limit > 20480 ? 20480 : limit; } return limit; } /* * Wrapper to realloc() * Frees current memory if realloc() fails and returns NULL. * * As per the docs, the *alloc() family is supposed to be memory aligned: * Ubuntu: http://manpages.ubuntu.com/manpages/xenial/man3/malloc.3.html * macOS: https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man3/malloc.3.html */ static void *xrealloc(void *pcur, size_t len) { static void *pmem; pmem = realloc(pcur, len); if (!pmem) free(pcur); return pmem; } /* * Just a safe strncpy(3) * Always null ('\0') terminates if both src and dest are valid pointers. * Returns the number of bytes copied including terminating null byte. */ static size_t xstrlcpy(char *dest, const char *src, size_t n) { static ulong *s, *d; static size_t len, blocks; static const uint lsize = sizeof(ulong); static const uint _WSHIFT = (sizeof(ulong) == 8) ? 3 : 2; if (!src || !dest || !n) return 0; len = strlen(src) + 1; if (n > len) n = len; else if (len > n) /* Save total number of bytes to copy in len */ len = n; /* * To enable -O3 ensure src and dest are 16-byte aligned * More info: http://www.felixcloutier.com/x86/MOVDQA.html */ if ((n >= lsize) && (((ulong)src & _ALIGNMENT_MASK) == 0 && ((ulong)dest & _ALIGNMENT_MASK) == 0)) { s = (ulong *)src; d = (ulong *)dest; blocks = n >> _WSHIFT; n &= lsize - 1; while (blocks) { *d = *s; ++d, ++s; --blocks; } if (!n) { dest = (char *)d; *--dest = '\0'; return len; } src = (char *)s; dest = (char *)d; } while (--n && (*dest = *src)) ++dest, ++src; if (!n) *dest = '\0'; return len; } /* * The poor man's implementation of memrchr(3). * We are only looking for '/' in this program. * And we are NOT expecting a '/' at the end. * Ideally 0 < n <= strlen(s). */ static void *xmemrchr(uchar *s, uchar ch, size_t n) { static uchar *ptr; if (!s || !n) return NULL; ptr = s + n; do { --ptr; if (*ptr == ch) return ptr; } while (s != ptr); return NULL; } /* * The following dirname(3) implementation does not * modify the input. We use a copy of the original. * * Modified from the glibc (GNU LGPL) version. */ static char *xdirname(const char *path) { static char * const buf = g_buf, *last_slash, *runp; xstrlcpy(buf, path, PATH_MAX); /* Find last '/'. */ last_slash = xmemrchr((uchar *)buf, '/', strlen(buf)); if (last_slash != NULL && last_slash != buf && last_slash[1] == '\0') { /* Determine whether all remaining characters are slashes. */ for (runp = last_slash; runp != buf; --runp) if (runp[-1] != '/') break; /* The '/' is the last character, we have to look further. */ if (runp != buf) last_slash = xmemrchr((uchar *)buf, '/', runp - buf); } if (last_slash != NULL) { /* Determine whether all remaining characters are slashes. */ for (runp = last_slash; runp != buf; --runp) if (runp[-1] != '/') break; /* Terminate the buffer. */ if (runp == buf) { /* The last slash is the first character in the string. * We have to return "/". As a special case we have to * return "//" if there are exactly two slashes at the * beginning of the string. See XBD 4.10 Path Name * Resolution for more information. */ if (last_slash == buf + 1) ++last_slash; else last_slash = buf + 1; } else last_slash = runp; last_slash[0] = '\0'; } else { /* This assignment is ill-designed but the XPG specs require to * return a string containing "." in any case no directory part * is found and so a static and constant string is required. */ buf[0] = '.'; buf[1] = '\0'; } return buf; } static char *xbasename(char *path) { static char *base; base = xmemrchr((uchar *)path, '/', strlen(path)); return base ? base + 1 : path; } /* Writes buflen char(s) from buf to a file */ static void writecp(const char *buf, const size_t buflen) { if (cfg.pickraw) return; if (!g_cppath[0]) { printmsg(messages[STR_COPY_ID]); return; } FILE *fp = fopen(g_cppath, "w"); if (fp) { fwrite(buf, 1, buflen, fp); fclose(fp); } else printwarn(); } static bool appendfpath(const char *path, const size_t len) { if ((copybufpos >= copybuflen) || ((len + 3) > (copybuflen - copybufpos))) { copybuflen += PATH_MAX; pcopybuf = xrealloc(pcopybuf, copybuflen); if (!pcopybuf) { printmsg("no memory!"); return FALSE; } } /* Enabling the following will miss files with newlines */ /* if (copybufpos) pcopybuf[copybufpos - 1] = '\n'; */ copybufpos += xstrlcpy(pcopybuf + copybufpos, path, len); return TRUE; } /* Write selected file paths to fd, linefeed separated */ static ssize_t selectiontofd(int fd) { char *pbuf = pcopybuf; ssize_t pos = 0, len, r, lastpos = copybufpos - 1; while (pos < copybufpos) { while(pcopybuf[pos]) ++pos; len = strlen(pbuf); r = write(fd, pbuf, len); if (r != len) return pos; if (pos != lastpos) { if (write(fd, "\n", 1) != 1) return pos; pbuf = pbuf + len + 1; } ++pos; } return pos; } static bool showcplist() { int fd; ssize_t pos; if (!copybufpos) return FALSE; if (g_tmpfpath[0]) xstrlcpy(g_tmpfpath + g_tmpfplen - 1, "/.nnnXXXXXX", HOME_LEN_MAX - g_tmpfplen); else { printmsg(messages[STR_NOHOME_ID]); return -1; } fd = mkstemp(g_tmpfpath); if (fd == -1) return FALSE; pos = selectiontofd(fd); close(fd); if (pos && pos == copybufpos) spawn(pager, pager_arg, g_tmpfpath, NULL, F_NORMAL); unlink(g_tmpfpath); return TRUE; } /* Initialize curses mode */ static bool initcurses(void) { if (initscr() == NULL) { char *term = getenv("TERM"); if (term != NULL) fprintf(stderr, "error opening TERM: %s\n", term); else fprintf(stderr, "initscr() failed\n"); return FALSE; } cbreak(); noecho(); nonl(); intrflush(stdscr, FALSE); keypad(stdscr, TRUE); curs_set(FALSE); /* Hide cursor */ start_color(); use_default_colors(); if (cfg.showcolor) { init_pair(1, g_ctx[0].color, -1); init_pair(2, g_ctx[1].color, -1); init_pair(3, g_ctx[2].color, -1); init_pair(4, g_ctx[3].color, -1); } settimeout(); /* One second */ return TRUE; } /* * Spawns a child process. Behaviour can be controlled using flag. * Limited to 2 arguments to a program, flag works on bit set. */ static void spawn(const char *file, const char *arg1, const char *arg2, const char *dir, uchar flag) { static const char *shlvl; static pid_t pid; static int status; /* Swap args if the first arg is NULL and second isn't */ if (!arg1 && arg2) { shlvl = arg1; arg1 = arg2; arg2 = shlvl; } if (flag & F_NORMAL) exitcurses(); pid = fork(); if (pid == 0) { if (dir != NULL) status = chdir(dir); shlvl = getenv("SHLVL"); /* Show a marker (to indicate nnn spawned shell) */ if (flag & F_MARKER && shlvl != NULL) { fprintf(stdout, "\n +-++-++-+\n | n n n |\n +-++-++-+\n\n"); fprintf(stdout, "Next shell level: %d\n", atoi(shlvl) + 1); } /* Suppress stdout and stderr */ if (flag & F_NOTRACE) { int fd = open("/dev/null", O_WRONLY, 0200); dup2(fd, 1); dup2(fd, 2); close(fd); } if (flag & F_NOWAIT) { signal(SIGHUP, SIG_IGN); signal(SIGPIPE, SIG_IGN); setsid(); } if (flag & F_SIGINT) signal(SIGINT, SIG_DFL); execlp(file, file, arg1, arg2, NULL); _exit(1); } else { if (!(flag & F_NOWAIT)) /* Ignore interruptions */ while (waitpid(pid, &status, 0) == -1) DPRINTF_D(status); DPRINTF_D(pid); if (flag & F_NORMAL) refresh(); } } /* Get program name from env var, else return fallback program */ static char *xgetenv(const char *name, char *fallback) { static char *value; if (name == NULL) return fallback; value = getenv(name); return value && value[0] ? value : fallback; } /* * Parse a string to get program and argument * NOTE: original string may be modified */ static void getprogarg(char *prog, char **arg) { char *argptr; while (*prog && !isblank(*prog)) ++prog; if (*prog) { *prog = '\0'; *arg = ++prog; argptr = *arg; /* Make sure there are no more args */ while (*argptr) { if (isblank(*argptr)) { fprintf(stderr, "Too many args [%s]\n", prog); exit(1); } ++argptr; } } } /* Check if a dir exists, IS a dir and is readable */ static bool xdiraccess(const char *path) { static DIR *dirp; dirp = opendir(path); if (dirp == NULL) { printwarn(); return FALSE; } closedir(dirp); return TRUE; } /* * We assume none of the strings are NULL. * * Let's have the logic to sort numeric names in numeric order. * E.g., the order '1, 10, 2' doesn't make sense to human eyes. * * If the absolute numeric values are same, we fallback to alphasort. */ static int xstricmp(const char * const s1, const char * const s2) { static const char *c1, *c2; c1 = s1; while (isspace(*c1)) ++c1; c2 = s2; while (isspace(*c2)) ++c2; if (*c1 == '-' || *c1 == '+') ++c1; if (*c2 == '-' || *c2 == '+') ++c2; if (isdigit(*c1) && isdigit(*c2)) { while (*c1 >= '0' && *c1 <= '9') ++c1; while (isspace(*c1)) ++c1; while (*c2 >= '0' && *c2 <= '9') ++c2; while (isspace(*c2)) ++c2; } if (!*c1 && !*c2) { static long long num1, num2; num1 = strtoll(s1, NULL, 10); num2 = strtoll(s2, NULL, 10); if (num1 != num2) { if (num1 > num2) return 1; return -1; } } return strcoll(s1, s2); } /* Return the integer value of a char representing HEX */ static char xchartohex(char c) { if (c >= '0' && c <= '9') return c - '0'; c = TOUPPER(c); if (c >= 'A' && c <= 'F') return c - 'A' + 10; return c; } static int setfilter(regex_t *regex, char *filter) { static size_t len; static int r; r = regcomp(regex, filter, REG_NOSUB | REG_EXTENDED | REG_ICASE); if (r != 0 && filter && filter[0] != '\0') { len = COLS; if (len > NAME_MAX) len = NAME_MAX; regerror(r, regex, g_buf, len); printmsg(g_buf); } return r; } static int visible(regex_t *regex, char *file) { return regexec(regex, file, 0, NULL, 0) == 0; } static int entrycmp(const void *va, const void *vb) { static pEntry pa, pb; pa = (pEntry)va; pb = (pEntry)vb; /* Sort directories first */ if (S_ISDIR(pb->mode) && !S_ISDIR(pa->mode)) return 1; if (S_ISDIR(pa->mode) && !S_ISDIR(pb->mode)) return -1; /* Do the actual sorting */ if (cfg.mtimeorder) return pb->t - pa->t; if (cfg.sizeorder) { if (pb->size > pa->size) return 1; if (pb->size < pa->size) return -1; } if (cfg.blkorder) { if (pb->blocks > pa->blocks) return 1; if (pb->blocks < pa->blocks) return -1; } return xstricmp(pa->name, pb->name); } /* * Returns SEL_* if key is bound and 0 otherwise. * Also modifies the run and env pointers (used on SEL_{RUN,RUNARG}). * The next keyboard input can be simulated by presel. */ static int nextsel(int *presel) { static int c; static uint i; static const uint len = LEN(bindings); #ifdef LINUX_INOTIFY static char inotify_buf[EVENT_BUF_LEN]; #elif defined(BSD_KQUEUE) static struct kevent event_data[NUM_EVENT_SLOTS]; #endif c = *presel; if (c == 0) { c = getch(); DPRINTF_D(c); } else { /* Unwatch dir if we are still in a filtered view */ #ifdef LINUX_INOTIFY if (*presel == FILTER && inotify_wd >= 0) { inotify_rm_watch(inotify_fd, inotify_wd); inotify_wd = -1; } #elif defined(BSD_KQUEUE) if (*presel == FILTER && event_fd >= 0) { close(event_fd); event_fd = -1; } #endif *presel = 0; } if (c == -1) { ++idle; /* * Do not check for directory changes in du mode. A redraw forces du calculation. * Check for changes every odd second. */ #ifdef LINUX_INOTIFY if (!cfg.blkorder && inotify_wd >= 0 && idle & 1 && read(inotify_fd, inotify_buf, EVENT_BUF_LEN) > 0) #elif defined(BSD_KQUEUE) if (!cfg.blkorder && event_fd >= 0 && idle & 1 && kevent(kq, events_to_monitor, NUM_EVENT_SLOTS, event_data, NUM_EVENT_FDS, >imeout) > 0) #endif c = CONTROL('L'); } else idle = 0; for (i = 0; i < len; ++i) if (c == bindings[i].sym) { return bindings[i].act; } return 0; } /* * Move non-matching entries to the end */ static int fill(struct entry **dents, int (*filter)(regex_t *, char *), regex_t *re) { static int count; static struct entry _dent, *pdent1, *pdent2; for (count = 0; count < ndents; ++count) { if (filter(re, (*dents)[count].name) == 0) { if (count != --ndents) { pdent1 = &(*dents)[count]; pdent2 = &(*dents)[ndents]; *(&_dent) = *pdent1; *pdent1 = *pdent2; *pdent2 = *(&_dent); --count; } continue; } } return ndents; } static int matches(char *fltr) { static regex_t re; /* Search filter */ if (setfilter(&re, fltr) != 0) return -1; ndents = fill(&dents, visible, &re); regfree(&re); if (!ndents) return 0; qsort(dents, ndents, sizeof(*dents), entrycmp); return 0; } static int filterentries(char *path) { static char ln[REGEX_MAX] __attribute__ ((aligned)); static wchar_t wln[REGEX_MAX] __attribute__ ((aligned)); static wint_t ch[2] = {0}; int r, total = ndents, oldcur = cur, len = 1; char *pln = ln + 1; ln[0] = wln[0] = FILTER; ln[1] = wln[1] = '\0'; cur = 0; cleartimeout(); curs_set(TRUE); printprompt(ln); while ((r = get_wch(ch)) != ERR) { switch (*ch) { case KEY_DC: // fallthrough case KEY_BACKSPACE: // fallthrough case '\b': // fallthrough case CONTROL('L'): // fallthrough case 127: /* handle DEL */ if (len == 1 && *ch != CONTROL('L')) { cur = oldcur; *ch = CONTROL('L'); goto end; } if (*ch == CONTROL('L')) while (len > 1) wln[--len] = '\0'; else wln[--len] = '\0'; if (len == 1) cur = oldcur; wcstombs(ln, wln, REGEX_MAX); ndents = total; if (matches(pln) != -1) redraw(path); printprompt(ln); continue; case 27: /* Exit filter mode on Escape */ cur = oldcur; *ch = CONTROL('L'); goto end; } if (r == OK) { /* Handle all control chars in main loop */ if (keyname(*ch)[0] == '^' && *ch != '^') { if (len == 1) cur = oldcur; goto end; } switch (*ch) { case '\r': // with nonl(), this is ENTER key value if (len == 1) { cur = oldcur; goto end; } if (matches(pln) == -1) goto end; redraw(path); goto end; case '?': // '?' is an invalid regex, show help instead if (len == 1) { cur = oldcur; goto end; } // fallthrough default: /* Reset cur in case it's a repeat search */ if (len == 1) cur = 0; if (len == REGEX_MAX - 1) break; wln[len] = (wchar_t)*ch; wln[++len] = '\0'; wcstombs(ln, wln, REGEX_MAX); /* Forward-filtering optimization: * - new matches can only be a subset of current matches. */ /* ndents = total; */ if (matches(pln) == -1) continue; /* If the only match is a dir, auto-select and cd into it */ if (ndents == 1 && cfg.filtermode && cfg.autoselect && S_ISDIR(dents[0].mode)) { *ch = KEY_ENTER; cur = 0; goto end; } /* * redraw() should be above the auto-select optimization, for * the case where there's an issue with dir auto-select, say, * due to a permission problem. The transition is _jumpy_ in * case of such an error. However, we optimize for successful * cases where the dir has permissions. This skips a redraw(). */ redraw(path); printprompt(ln); } } else { if (len == 1) cur = oldcur; goto end; } } end: curs_set(FALSE); settimeout(); /* Return keys for navigation etc. */ return *ch; } /* Show a prompt with input string and return the changes */ static char *xreadline(char *prefill, char *prompt) { size_t len, pos; int x, y, r; wint_t ch[2] = {0}; static wchar_t * const buf = (wchar_t *)g_buf; cleartimeout(); printprompt(prompt); if (prefill) { DPRINTF_S(prefill); len = pos = mbstowcs(buf, prefill, NAME_MAX); } else len = (size_t)-1; if (len == (size_t)-1) { buf[0] = '\0'; len = pos = 0; } getyx(stdscr, y, x); curs_set(TRUE); while (1) { buf[len] = ' '; mvaddnwstr(y, x, buf, len + 1); move(y, x + wcswidth(buf, pos)); r = get_wch(ch); if (r != ERR) { if (r == OK) { switch (*ch) { case KEY_ENTER: // fallthrough case '\n': // fallthrough case '\r': goto END; case 127: /* Handle DEL */ // fallthrough case '\b': /* some old curses (e.g. rhel25) still send '\b' for backspace */ if (pos > 0) { memmove(buf + pos - 1, buf + pos, (len - pos) << 2); --len, --pos; } // fallthrough case '\t': /* TAB breaks cursor position, ignore it */ continue; case CONTROL('L'): clearprompt(); printprompt(prompt); len = pos = 0; continue; case CONTROL('A'): pos = 0; continue; case CONTROL('E'): pos = len; continue; case CONTROL('U'): clearprompt(); printprompt(prompt); memmove(buf, buf + pos, (len - pos) << 2); len -= pos; pos = 0; continue; case 27: /* Exit prompt on Escape */ len = 0; goto END; } /* Filter out all other control chars */ if (keyname(*ch)[0] == '^') continue; if (pos < NAME_MAX - 1) { memmove(buf + pos + 1, buf + pos, (len - pos) << 2); buf[pos] = *ch; ++len, ++pos; continue; } } else { switch (*ch) { case KEY_LEFT: if (pos > 0) --pos; break; case KEY_RIGHT: if (pos < len) ++pos; break; case KEY_BACKSPACE: if (pos > 0) { memmove(buf + pos - 1, buf + pos, (len - pos) << 2); --len, --pos; } break; case KEY_DC: if (pos < len) { memmove(buf + pos, buf + pos + 1, (len - pos - 1) << 2); --len; } break; default: break; } } } } END: curs_set(FALSE); settimeout(); clearprompt(); buf[len] = '\0'; DPRINTF_S(buf); wcstombs(g_buf + ((NAME_MAX + 1) << 2), buf, NAME_MAX); return g_buf + ((NAME_MAX + 1) << 2); } /* * Updates out with "dir/name or "/name" * Returns the number of bytes copied including the terminating NULL byte */ static size_t mkpath(char *dir, char *name, char *out, size_t n) { static size_t len; /* Handle absolute path */ if (name[0] == '/') return xstrlcpy(out, name, n); /* Handle root case */ if (istopdir(dir)) len = 1; else len = xstrlcpy(out, dir, n); out[len - 1] = '/'; return (xstrlcpy(out + len, name, n - len) + len); } static bool parsebmstr() { int i = 0; char *bms = getenv("NNN_BMS"); if (!bms) return TRUE; while (*bms && i < BM_MAX) { bookmark[i].key = *bms; if (!*++bms) { bookmark[i].key = '\0'; break; } if (*bms != ':') return FALSE; /* We support single char keys only */ bookmark[i].loc = ++bms; if (bookmark[i].loc[0] == '\0' || bookmark[i].loc[0] == ';') { bookmark[i].key = '\0'; break; } while (*bms && *bms != ';') ++bms; if (*bms) *bms = '\0'; else break; ++bms; ++i; } return TRUE; } /* * Get the real path to a bookmark * * NULL is returned in case of no match, path resolution failure etc. * buf would be modified, so check return value before access */ static char *get_bm_loc(int key, char *buf) { int r; for (r = 0; bookmark[r].key && r < BM_MAX; ++r) { if (bookmark[r].key == key) { if (bookmark[r].loc[0] == '~') { char *home = getenv("HOME"); if (!home) { DPRINTF_S(messages[STR_NOHOME_ID]); return NULL; } snprintf(buf, PATH_MAX, "%s%s", home, bookmark[r].loc + 1); } else xstrlcpy(buf, bookmark[r].loc, PATH_MAX); return buf; } } DPRINTF_S("Invalid key"); return NULL; } static void resetdircolor(mode_t mode) { if (cfg.dircolor && !S_ISDIR(mode)) { attroff(COLOR_PAIR(cfg.curctx + 1) | A_BOLD); cfg.dircolor = 0; } } /* * Replace escape characters in a string with '?' * Adjust string length to maxcols if > 0; * * Interestingly, note that unescape() uses g_buf. What happens if * str also points to g_buf? In this case we assume that the caller * acknowledges that it's OK to lose the data in g_buf after this * call to unescape(). * The API, on its part, first converts str to multibyte (after which * it doesn't touch str anymore). Only after that it starts modifying * g_buf. This is a phased operation. */ static char *unescape(const char *str, uint maxcols) { static wchar_t wbuf[PATH_MAX] __attribute__ ((aligned)); static wchar_t *buf; static size_t len; /* Convert multi-byte to wide char */ len = mbstowcs(wbuf, str, PATH_MAX); g_buf[0] = '\0'; buf = wbuf; if (maxcols && len > maxcols) { len = wcswidth(wbuf, len); if (len > maxcols) wbuf[maxcols] = 0; } while (*buf) { if (*buf <= '\x1f' || *buf == '\x7f') *buf = '\?'; ++buf; } /* Convert wide char to multi-byte */ wcstombs(g_buf, wbuf, PATH_MAX); return g_buf; } static char *coolsize(off_t size) { static const char * const U = "BKMGTPEZY"; static char size_buf[12]; /* Buffer to hold human readable size */ static off_t rem; static int i; i = 0; rem = 0; while (size > 1024) { rem = size & (0x3FF); /* 1024 - 1 = 0x3FF */ size >>= 10; ++i; } if (i == 1) { rem = (rem * 1000) >> 10; rem /= 10; if (rem % 10 >= 5) { rem = (rem / 10) + 1; if (rem == 10) { ++size; rem = 0; } } else rem /= 10; } else if (i == 2) { rem = (rem * 1000) >> 10; if (rem % 10 >= 5) { rem = (rem / 10) + 1; if (rem == 100) { ++size; rem = 0; } } else rem /= 10; } else if (i > 0) { rem = (rem * 10000) >> 10; if (rem % 10 >= 5) { rem = (rem / 10) + 1; if (rem == 1000) { ++size; rem = 0; } } else rem /= 10; } if (i > 0 && i < 6) snprintf(size_buf, 12, "%lu.%0*lu%c", (ulong)size, i, (ulong)rem, U[i]); else snprintf(size_buf, 12, "%lu%c", (ulong)size, U[i]); return size_buf; } static char *get_file_sym(mode_t mode) { static char ind[2] = "\0\0"; if (S_ISDIR(mode)) ind[0] = '/'; else if (S_ISLNK(mode)) ind[0] = '@'; else if (S_ISSOCK(mode)) ind[0] = '='; else if (S_ISFIFO(mode)) ind[0] = '|'; else if (mode & 0100) ind[0] = '*'; else ind[0] = '\0'; return ind; } static void printent(struct entry *ent, int sel, uint namecols) { static char *pname; pname = unescape(ent->name, namecols); /* Directories are always shown on top */ resetdircolor(ent->mode); printw("%s%s%s\n", CURSYM(sel), pname, get_file_sym(ent->mode)); } static void printent_long(struct entry *ent, int sel, uint namecols) { static char buf[18], *pname; strftime(buf, 18, "%F %R", localtime(&ent->t)); pname = unescape(ent->name, namecols); /* Directories are always shown on top */ resetdircolor(ent->mode); if (sel) attron(A_REVERSE); if (S_ISDIR(ent->mode)) { if (cfg.blkorder) printw("%s%-16.16s %8.8s/ %s/\n", CURSYM(sel), buf, coolsize(ent->blocks << BLK_SHIFT), pname); else printw("%s%-16.16s / %s/\n", CURSYM(sel), buf, pname); } else if (S_ISLNK(ent->mode)) { if (ent->flags & SYMLINK_TO_DIR) printw("%s%-16.16s @/ %s@\n", CURSYM(sel), buf, pname); else printw("%s%-16.16s @ %s@\n", CURSYM(sel), buf, pname); } else if (S_ISSOCK(ent->mode)) printw("%s%-16.16s = %s=\n", CURSYM(sel), buf, pname); else if (S_ISFIFO(ent->mode)) printw("%s%-16.16s | %s|\n", CURSYM(sel), buf, pname); else if (S_ISBLK(ent->mode)) printw("%s%-16.16s b %s\n", CURSYM(sel), buf, pname); else if (S_ISCHR(ent->mode)) printw("%s%-16.16s c %s\n", CURSYM(sel), buf, pname); else if (ent->mode & 0100) { if (cfg.blkorder) printw("%s%-16.16s %8.8s* %s*\n", CURSYM(sel), buf, coolsize(ent->blocks << BLK_SHIFT), pname); else printw("%s%-16.16s %8.8s* %s*\n", CURSYM(sel), buf, coolsize(ent->size), pname); } else { if (cfg.blkorder) printw("%s%-16.16s %8.8s %s\n", CURSYM(sel), buf, coolsize(ent->blocks << BLK_SHIFT), pname); else printw("%s%-16.16s %8.8s %s\n", CURSYM(sel), buf, coolsize(ent->size), pname); } if (sel) attroff(A_REVERSE); } static void (*printptr)(struct entry *ent, int sel, uint namecols) = &printent_long; static char get_fileind(mode_t mode, char *desc) { static char c; if (S_ISREG(mode)) { c = '-'; xstrlcpy(desc, "regular file", DESCRIPTOR_LEN); if (mode & 0100) xstrlcpy(desc + 12, ", executable", DESCRIPTOR_LEN - 12); /* Length of string "regular file" is 12 */ } else if (S_ISDIR(mode)) { c = 'd'; xstrlcpy(desc, "directory", DESCRIPTOR_LEN); } else if (S_ISBLK(mode)) { c = 'b'; xstrlcpy(desc, "block special device", DESCRIPTOR_LEN); } else if (S_ISCHR(mode)) { c = 'c'; xstrlcpy(desc, "character special device", DESCRIPTOR_LEN); #ifdef S_ISFIFO } else if (S_ISFIFO(mode)) { c = 'p'; xstrlcpy(desc, "FIFO", DESCRIPTOR_LEN); #endif /* S_ISFIFO */ #ifdef S_ISLNK } else if (S_ISLNK(mode)) { c = 'l'; xstrlcpy(desc, "symbolic link", DESCRIPTOR_LEN); #endif /* S_ISLNK */ #ifdef S_ISSOCK } else if (S_ISSOCK(mode)) { c = 's'; xstrlcpy(desc, "socket", DESCRIPTOR_LEN); #endif /* S_ISSOCK */ #ifdef S_ISDOOR /* Solaris 2.6, etc. */ } else if (S_ISDOOR(mode)) { c = 'D'; desc[0] = '\0'; #endif /* S_ISDOOR */ } else { /* Unknown type -- possibly a regular file? */ c = '?'; desc[0] = '\0'; } return c; } /* Convert a mode field into "ls -l" type perms field. */ static char *get_lsperms(mode_t mode, char *desc) { static const char * const rwx[] = {"---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx"}; static char bits[11] = {'\0'}; bits[0] = get_fileind(mode, desc); xstrlcpy(&bits[1], rwx[(mode >> 6) & 7], 4); xstrlcpy(&bits[4], rwx[(mode >> 3) & 7], 4); xstrlcpy(&bits[7], rwx[(mode & 7)], 4); if (mode & S_ISUID) bits[3] = (mode & 0100) ? 's' : 'S'; /* user executable */ if (mode & S_ISGID) bits[6] = (mode & 0010) ? 's' : 'l'; /* group executable */ if (mode & S_ISVTX) bits[9] = (mode & 0001) ? 't' : 'T'; /* others executable */ return bits; } /* * Gets only a single line (that's what we need * for now) or shows full command output in pager. * * If page is valid, returns NULL */ static char *get_output(char *buf, size_t bytes, char *file, char *arg1, char *arg2, bool page) { pid_t pid; int pipefd[2]; FILE *pf; int tmp, flags; char *ret = NULL; if (pipe(pipefd) == -1) errexit(); for (tmp = 0; tmp < 2; ++tmp) { /* Get previous flags */ flags = fcntl(pipefd[tmp], F_GETFL, 0); /* Set bit for non-blocking flag */ flags |= O_NONBLOCK; /* Change flags on fd */ fcntl(pipefd[tmp], F_SETFL, flags); } pid = fork(); if (pid == 0) { /* In child */ close(pipefd[0]); dup2(pipefd[1], STDOUT_FILENO); dup2(pipefd[1], STDERR_FILENO); close(pipefd[1]); execlp(file, file, arg1, arg2, NULL); _exit(1); } /* In parent */ waitpid(pid, &tmp, 0); close(pipefd[1]); if (!page) { pf = fdopen(pipefd[0], "r"); if (pf) { ret = fgets(buf, bytes, pf); close(pipefd[0]); } return ret; } pid = fork(); if (pid == 0) { /* Show in pager in child */ dup2(pipefd[0], STDIN_FILENO); close(pipefd[0]); execlp(pager, pager, NULL); _exit(1); } /* In parent */ waitpid(pid, &tmp, 0); close(pipefd[0]); return NULL; } static bool getutil(char *util) { if (!get_output(g_buf, CMD_LEN_MAX, "which", util, NULL, FALSE)) return FALSE; return TRUE; } static char *xgetpwuid(uid_t uid) { struct passwd *pwd = getpwuid(uid); if (!pwd) return utils[UNKNOWN]; return pwd->pw_name; } static char *xgetgrgid(gid_t gid) { struct group *grp = getgrgid(gid); if (!grp) return utils[UNKNOWN]; return grp->gr_name; } /* * Follows the stat(1) output closely */ static bool show_stats(char *fpath, char *fname, struct stat *sb) { char desc[DESCRIPTOR_LEN]; char *perms = get_lsperms(sb->st_mode, desc); char *p, *begin = g_buf; if (g_tmpfpath[0]) xstrlcpy(g_tmpfpath + g_tmpfplen - 1, "/.nnnXXXXXX", HOME_LEN_MAX - g_tmpfplen); else { printmsg(messages[STR_NOHOME_ID]); return FALSE; } int fd = mkstemp(g_tmpfpath); if (fd == -1) return FALSE; dprintf(fd, " File: '%s'", unescape(fname, 0)); /* Show file name or 'symlink' -> 'target' */ if (perms[0] == 'l') { /* Note that CMD_LEN_MAX > PATH_MAX */ ssize_t len = readlink(fpath, g_buf, CMD_LEN_MAX); if (len != -1) { struct stat tgtsb; if (!stat(fpath, &tgtsb) && S_ISDIR(tgtsb.st_mode)) g_buf[len++] = '/'; g_buf[len] = '\0'; /* * We pass g_buf but unescape() operates on g_buf too! * Read the API notes for information on how this works. */ dprintf(fd, " -> '%s'", unescape(g_buf, 0)); } } /* Show size, blocks, file type */ #if defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__) dprintf(fd, "\n Size: %-15lld Blocks: %-10lld IO Block: %-6d %s", (long long)sb->st_size, (long long)sb->st_blocks, sb->st_blksize, desc); #else dprintf(fd, "\n Size: %-15ld Blocks: %-10ld IO Block: %-6ld %s", sb->st_size, sb->st_blocks, (long)sb->st_blksize, desc); #endif /* Show containing device, inode, hardlink count */ snprintf(g_buf, 32, "%lxh/%lud", (ulong)sb->st_dev, (ulong)sb->st_dev); #if defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__) dprintf(fd, "\n Device: %-15s Inode: %-11llu Links: %-9hu", g_buf, (unsigned long long)sb->st_ino, sb->st_nlink); #else dprintf(fd, "\n Device: %-15s Inode: %-11lu Links: %-9lu", g_buf, sb->st_ino, (ulong)sb->st_nlink); #endif /* Show major, minor number for block or char device */ if (perms[0] == 'b' || perms[0] == 'c') dprintf(fd, " Device type: %x,%x", major(sb->st_rdev), minor(sb->st_rdev)); /* Show permissions, owner, group */ dprintf(fd, "\n Access: 0%d%d%d/%s Uid: (%u/%s) Gid: (%u/%s)", (sb->st_mode >> 6) & 7, (sb->st_mode >> 3) & 7, sb->st_mode & 7, perms, sb->st_uid, xgetpwuid(sb->st_uid), sb->st_gid, xgetgrgid(sb->st_gid)); /* Show last access time */ strftime(g_buf, 40, messages[STR_DATE_ID], localtime(&sb->st_atime)); dprintf(fd, "\n\n Access: %s", g_buf); /* Show last modification time */ strftime(g_buf, 40, messages[STR_DATE_ID], localtime(&sb->st_mtime)); dprintf(fd, "\n Modify: %s", g_buf); /* Show last status change time */ strftime(g_buf, 40, messages[STR_DATE_ID], localtime(&sb->st_ctime)); dprintf(fd, "\n Change: %s", g_buf); if (S_ISREG(sb->st_mode)) { /* Show file(1) output */ p = get_output(g_buf, CMD_LEN_MAX, "file", "-b", fpath, FALSE); if (p) { dprintf(fd, "\n\n "); while (*p) { if (*p == ',') { *p = '\0'; dprintf(fd, " %s\n", begin); begin = p + 1; } ++p; } dprintf(fd, " %s", begin); } dprintf(fd, "\n\n"); } else dprintf(fd, "\n\n\n"); close(fd); spawn(pager, pager_arg, g_tmpfpath, NULL, F_NORMAL); unlink(g_tmpfpath); return TRUE; } static size_t get_fs_info(const char *path, bool type) { static struct statvfs svb; if (statvfs(path, &svb) == -1) return 0; if (type == CAPACITY) return svb.f_blocks << ffs(svb.f_bsize >> 1); return svb.f_bavail << ffs(svb.f_frsize >> 1); } static bool show_mediainfo(char *fpath, char *arg) { if (!getutil(utils[cfg.metaviewer])) return FALSE; exitcurses(); get_output(NULL, 0, utils[cfg.metaviewer], fpath, arg, TRUE); refresh(); return TRUE; } static bool handle_archive(char *fpath, char *arg, char *dir) { if (!getutil(utils[ATOOL])) return FALSE; if (arg[1] == 'x') spawn(utils[ATOOL], arg, fpath, dir, F_NORMAL); else { exitcurses(); get_output(NULL, 0, utils[ATOOL], arg, fpath, TRUE); refresh(); } return TRUE; } /* * The help string tokens (each line) start with a HEX value * which indicates the number of spaces to print before the * particular token. This method was chosen instead of a flat * string because the number of bytes in help was increasing * the binary size by around a hundred bytes. This would only * have increased as we keep adding new options. */ static bool show_help(char *path) { if (g_tmpfpath[0]) xstrlcpy(g_tmpfpath + g_tmpfplen - 1, "/.nnnXXXXXX", HOME_LEN_MAX - g_tmpfplen); else { printmsg(messages[STR_NOHOME_ID]); return FALSE; } int i = 0, fd = mkstemp(g_tmpfpath); char *start, *end; static char helpstr[] = { "0\n" "1NAVIGATION\n" "7↑, k, ^P Up PgUp, ^U Scroll up\n" "7↓, j, ^N Down PgDn, ^D Scroll down\n" "1Home, g, ^, ^A First entry ~ Go HOME\n" "2End, G, $, ^E Last entry & Start dir\n" "1←, Bksp, h, ^H Parent dir - Last visited dir\n" "4→, ↵, l, ^M Open file/dir . Toggle show hidden\n" "e/ Filter Ins, ^T Toggle nav-as-you-type\n" "eb Pin current dir ^W Go to pinned dir\n" "8Tab, ^I Next context d Toggle detail view\n" "a`, ^/ Leader key LeaderN Go to/create context N\n" "cEsc Exit prompt ^L Redraw/clear prompt\n" "d^G Quit and cd q Quit context\n" "aQ, ^Q Quit ? Help, config\n" "1FILES\n" "d^O Open with... n Create new\n" "eD File details ^R Rename entry\n" "a⎵, ^K Copy entry path r Open dir in vidir\n" "aY, ^Y Toggle selection y List selection\n" "eP Copy selection X Delete selection\n" "eV Move selection ^X Delete entry\n" "ef Archive entry F List archive\n" "d^F Extract archive m, M Brief/full media info\n" "ee Edit in EDITOR p Open in PAGER\n" "1ORDER TOGGLES\n" "d^J Disk usage S Apparent du\n" "et Modification time s Size\n" "1MISC\n" "a!, ^] Spawn SHELL in dir C Execute entry\n" "eR Run custom script L Lock terminal\n" "d^S Run a command\n"}; if (fd == -1) return FALSE; start = end = helpstr; while (*end) { while (*end != '\n') ++end; if (start == end) { ++end; continue; } dprintf(fd, "%*c%.*s", xchartohex(*start), ' ', (int)(end - start), start + 1); start = ++end; } dprintf(fd, "\nVOLUME: %s of ", coolsize(get_fs_info(path, FREE))); dprintf(fd, "%s free\n\n", coolsize(get_fs_info(path, CAPACITY))); if (getenv("NNN_BMS")) { dprintf(fd, "BOOKMARKS\n"); for (; i < BM_MAX; ++i) if (bookmark[i].key) dprintf(fd, " %c: %s\n", (char)bookmark[i].key, bookmark[i].loc); else break; dprintf(fd, "\n"); } if (cfg.useeditor) dprintf(fd, "NNN_USE_EDITOR: 1\n"); if (getenv("NNN_CONTEXT_COLORS")) dprintf(fd, "NNN_CONTEXT_COLORS: %s\n", getenv("NNN_CONTEXT_COLORS")); if (idletimeout) dprintf(fd, "NNN_IDLE_TIMEOUT: %d secs\n", idletimeout); if (copier) dprintf(fd, "NNN_COPIER: %s\n", copier); else if (g_cppath[0]) dprintf(fd, "copy file: %s\n", g_cppath); if (runpath) dprintf(fd, "NNN_SCRIPT: %s\n", runpath); if (getenv("NNN_SHOW_HIDDEN")) dprintf(fd, "NNN_SHOW_HIDDEN: 1\n"); if (getenv("NNN_NO_AUTOSELECT")) dprintf(fd, "NNN_NO_AUTOSELECT: 1\n"); if (getenv("DISABLE_FILE_OPEN_ON_NAV")) dprintf(fd, "DISABLE_FILE_OPEN_ON_NAV: 1\n"); dprintf(fd, "\n"); if (getenv("PWD")) dprintf(fd, "PWD: %s\n", getenv("PWD")); if (getenv("SHELL")) dprintf(fd, "SHELL: %s %s\n", shell, shell_arg); if (getenv("SHLVL")) dprintf(fd, "SHLVL: %s\n", getenv("SHLVL")); if (getenv("VISUAL")) dprintf(fd, "VISUAL: %s %s\n", editor, editor_arg); else if (getenv("EDITOR")) dprintf(fd, "EDITOR: %s %s\n", editor, editor_arg); if (getenv("PAGER")) dprintf(fd, "PAGER: %s %s\n", pager, pager_arg); dprintf(fd, "\nVersion: %s\n%s\n", VERSION, GENERAL_INFO); close(fd); spawn(pager, pager_arg, g_tmpfpath, NULL, F_NORMAL); unlink(g_tmpfpath); return TRUE; } static int sum_bsizes(const char *fpath, const struct stat *sb, int typeflag, struct FTW *ftwbuf) { if (sb->st_blocks && (typeflag == FTW_F || typeflag == FTW_D)) ent_blocks += sb->st_blocks; ++num_files; return 0; } static int sum_sizes(const char *fpath, const struct stat *sb, int typeflag, struct FTW *ftwbuf) { if (sb->st_size && (typeflag == FTW_F || typeflag == FTW_D)) ent_blocks += sb->st_size; ++num_files; return 0; } static void dentfree(struct entry *dents) { free(pnamebuf); free(dents); } static int dentfill(char *path, struct entry **dents) { static DIR *dirp; static struct dirent *dp; static char *namep, *pnb; static struct entry *dentp; static size_t off, namebuflen = NAMEBUF_INCR; static ulong num_saved; static int fd, n, count; static struct stat sb_path, sb; off = 0; dirp = opendir(path); if (dirp == NULL) return 0; fd = dirfd(dirp); n = 0; if (cfg.blkorder) { num_files = 0; dir_blocks = 0; if (fstatat(fd, ".", &sb_path, 0) == -1) { printwarn(); return 0; } } while ((dp = readdir(dirp)) != NULL) { namep = dp->d_name; /* Skip self and parent */ if ((namep[0] == '.' && (namep[1] == '\0' || (namep[1] == '.' && namep[2] == '\0')))) continue; if (!cfg.showhidden && namep[0] == '.') { if (!cfg.blkorder) continue; if (fstatat(fd, namep, &sb, AT_SYMLINK_NOFOLLOW) == -1) continue; if (S_ISDIR(sb.st_mode)) { if (sb_path.st_dev == sb.st_dev) { ent_blocks = 0; mkpath(path, namep, g_buf, PATH_MAX); if (nftw(g_buf, nftw_fn, open_max, FTW_MOUNT | FTW_PHYS) == -1) { printmsg(messages[STR_NFTWFAIL_ID]); dir_blocks += (cfg.apparentsz ? sb.st_size : sb.st_blocks); } else dir_blocks += ent_blocks; } } else { dir_blocks += (cfg.apparentsz ? sb.st_size : sb.st_blocks); ++num_files; } continue; } if (fstatat(fd, namep, &sb, AT_SYMLINK_NOFOLLOW) == -1) { DPRINTF_S(namep); continue; } if (n == total_dents) { total_dents += ENTRY_INCR; *dents = xrealloc(*dents, total_dents * sizeof(**dents)); if (*dents == NULL) { free(pnamebuf); errexit(); } DPRINTF_P(*dents); } /* If there's not enough bytes left to copy a file name of length NAME_MAX, re-allocate */ if (namebuflen - off < NAME_MAX + 1) { namebuflen += NAMEBUF_INCR; pnb = pnamebuf; pnamebuf = (char *)xrealloc(pnamebuf, namebuflen); if (pnamebuf == NULL) { free(*dents); errexit(); } DPRINTF_P(pnamebuf); /* realloc() may result in memory move, we must re-adjust if that happens */ if (pnb != pnamebuf) { dentp = *dents; dentp->name = pnamebuf; for (count = 1; count < n; ++dentp, ++count) /* Current filename starts at last filename start + length */ (dentp + 1)->name = (char *)((size_t)dentp->name + dentp->nlen); } } dentp = *dents + n; /* Copy file name */ dentp->name = (char *)((size_t)pnamebuf + off); dentp->nlen = xstrlcpy(dentp->name, namep, NAME_MAX + 1); off += dentp->nlen; /* Copy other fields */ dentp->mode = sb.st_mode; dentp->t = sb.st_mtime; dentp->size = sb.st_size; if (cfg.blkorder) { if (S_ISDIR(sb.st_mode)) { ent_blocks = 0; num_saved = num_files + 1; mkpath(path, namep, g_buf, PATH_MAX); if (nftw(g_buf, nftw_fn, open_max, FTW_MOUNT | FTW_PHYS) == -1) { printmsg(messages[STR_NFTWFAIL_ID]); dentp->blocks = (cfg.apparentsz ? sb.st_size : sb.st_blocks); } else dentp->blocks = ent_blocks; if (sb_path.st_dev == sb.st_dev) dir_blocks += dentp->blocks; else num_files = num_saved; } else { dentp->blocks = (cfg.apparentsz ? sb.st_size : sb.st_blocks); dir_blocks += dentp->blocks; ++num_files; } } /* Flag if this is a symlink to a dir */ if (S_ISLNK(sb.st_mode)) if (!fstatat(fd, namep, &sb, 0)) { if (S_ISDIR(sb.st_mode)) dentp->flags |= SYMLINK_TO_DIR; else dentp->flags &= ~SYMLINK_TO_DIR; } ++n; } /* Should never be null */ if (closedir(dirp) == -1) { dentfree(*dents); errexit(); } return n; } /* Return the position of the matching entry or 0 otherwise */ static int dentfind(struct entry *dents, const char *fname, int n) { static int i; if (!fname) return 0; DPRINTF_S(fname); for (i = 0; i < n; ++i) if (strcmp(fname, dents[i].name) == 0) return i; return 0; } static bool populate(char *path, char *lastname) { /* Can fail when permissions change while browsing. * It's assumed that path IS a directory when we are here. */ if (access(path, R_OK) == -1) return FALSE; if (cfg.blkorder) { printmsg("calculating..."); refresh(); } #ifdef DEBUGMODE struct timespec ts1, ts2; clock_gettime(CLOCK_REALTIME, &ts1); /* Use CLOCK_MONOTONIC on FreeBSD */ #endif ndents = dentfill(path, &dents); if (!ndents) return TRUE; qsort(dents, ndents, sizeof(*dents), entrycmp); #ifdef DEBUGMODE clock_gettime(CLOCK_REALTIME, &ts2); DPRINTF_U(ts2.tv_nsec - ts1.tv_nsec); #endif /* Find cur from history */ cur = dentfind(dents, lastname, ndents); return TRUE; } static void redraw(char *path) { static char buf[NAME_MAX + 65] __attribute__ ((aligned)); static size_t ncols; static int nlines, i, attrs; static bool mode_changed; mode_changed = FALSE; nlines = MIN(LINES - 4, ndents); /* Clear screen */ erase(); #ifdef DIR_LIMITED_COPY if (cfg.copymode) if (g_crc != crc8fast((uchar *)dents, ndents * sizeof(struct entry))) { cfg.copymode = 0; DPRINTF_S("selection off"); } #endif /* Fail redraw if < than 11 columns, context info prints 10 chars */ if (COLS < 11) { printmsg("too few columns!"); return; } /* Strip trailing slashes */ for (i = strlen(path) - 1; i > 0; --i) if (path[i] == '/') path[i] = '\0'; else break; DPRINTF_D(cur); DPRINTF_S(path); if (!realpath(path, g_buf)) { printwarn(); return; } ncols = COLS; if (ncols > PATH_MAX) ncols = PATH_MAX; printw("["); for (i = 0; i < CTX_MAX; ++i) { /* Print current context in reverse */ if (cfg.curctx == i) { if (cfg.showcolor) attrs = COLOR_PAIR(i + 1) | A_BOLD | A_REVERSE; else attrs = A_REVERSE; attron(attrs); printw("%d", i + 1); attroff(attrs); printw(" "); } else if (g_ctx[i].c_cfg.ctxactive) { if (cfg.showcolor) attrs = COLOR_PAIR(i + 1) | A_BOLD | A_UNDERLINE; else attrs = A_UNDERLINE; attron(attrs); printw("%d", i + 1); attroff(attrs); printw(" "); } else printw("%d ", i + 1); } printw("\b] "); /* 10 chars printed in total for contexts - "[1 2 3 4] " */ attron(A_UNDERLINE); /* No text wrapping in cwd line */ g_buf[ncols - 11] = '\0'; printw("%s\n\n", g_buf); attroff(A_UNDERLINE); /* Fallback to light mode if less than 35 columns */ if (ncols < 35 && cfg.showdetail) { cfg.showdetail ^= 1; printptr = &printent; mode_changed = TRUE; } /* Calculate the number of cols available to print entry name */ if (cfg.showdetail) ncols -= 32; else ncols -= 5; if (cfg.showcolor) { attron(COLOR_PAIR(cfg.curctx + 1) | A_BOLD); cfg.dircolor = 1; } /* Print listing */ if (cur < (nlines >> 1)) { for (i = 0; i < nlines; ++i) printptr(&dents[i], i == cur, ncols); } else if (cur >= ndents - (nlines >> 1)) { for (i = ndents - nlines; i < ndents; ++i) printptr(&dents[i], i == cur, ncols); } else { static int odd; odd = ISODD(nlines); nlines >>= 1; for (i = cur - nlines; i < cur + nlines + odd; ++i) printptr(&dents[i], i == cur, ncols); } /* Must reset e.g. no files in dir */ if (cfg.dircolor) { attroff(COLOR_PAIR(cfg.curctx + 1) | A_BOLD); cfg.dircolor = 0; } if (cfg.showdetail) { if (ndents) { static char sort[9]; if (cfg.mtimeorder) xstrlcpy(sort, "by time ", 9); else if (cfg.sizeorder) xstrlcpy(sort, "by size ", 9); else sort[0] = '\0'; /* We need to show filename as it may be truncated in directory listing */ if (!cfg.blkorder) snprintf(buf, NAME_MAX + 65, "%d/%d %s[%s%s]", cur + 1, ndents, sort, unescape(dents[cur].name, NAME_MAX), get_file_sym(dents[cur].mode)); else { i = snprintf(buf, 64, "%d/%d ", cur + 1, ndents); if (cfg.apparentsz) buf[i++] = 'a'; else buf[i++] = 'd'; i += snprintf(buf + i, 64, "u: %s (%lu files) ", coolsize(dir_blocks << BLK_SHIFT), num_files); snprintf(buf + i, NAME_MAX, "vol: %s free [%s%s]", coolsize(get_fs_info(path, FREE)), unescape(dents[cur].name, NAME_MAX), get_file_sym(dents[cur].mode)); } printmsg(buf); } else printmsg("0 items"); } if (mode_changed) { cfg.showdetail ^= 1; printptr = &printent_long; } } static void browse(char *ipath) { char newpath[PATH_MAX] __attribute__ ((aligned)); char mark[PATH_MAX] __attribute__ ((aligned)); char rundir[PATH_MAX] __attribute__ ((aligned)); char runfile[NAME_MAX + 1] __attribute__ ((aligned)); char *path, *lastdir, *lastname; char *dir, *tmp; struct stat sb; int r = -1, fd, presel, ncp = 0, copystartid = 0, copyendid = 0; enum action sel; bool dir_changed = FALSE; /* setup first context */ xstrlcpy(g_ctx[0].c_path, ipath, PATH_MAX); /* current directory */ path = g_ctx[0].c_path; xstrlcpy(g_ctx[0].c_init, ipath, PATH_MAX); /* start directory */ g_ctx[0].c_last[0] = g_ctx[0].c_name[0] = newpath[0] = mark[0] = '\0'; rundir[0] = runfile[0] = '\0'; lastdir = g_ctx[0].c_last; /* last visited directory */ lastname = g_ctx[0].c_name; /* last visited filename */ g_ctx[0].c_cfg = cfg; /* current configuration */ if (cfg.filtermode) presel = FILTER; else presel = 0; dents = xrealloc(dents, total_dents * sizeof(struct entry)); if (dents == NULL) errexit(); DPRINTF_P(dents); /* Allocate buffer to hold names */ pnamebuf = (char *)xrealloc(pnamebuf, NAMEBUF_INCR); if (pnamebuf == NULL) { free(dents); errexit(); } DPRINTF_P(pnamebuf); begin: #ifdef LINUX_INOTIFY if ((presel == FILTER || dir_changed) && inotify_wd >= 0) { inotify_rm_watch(inotify_fd, inotify_wd); inotify_wd = -1; dir_changed = FALSE; } #elif defined(BSD_KQUEUE) if ((presel == FILTER || dir_changed) && event_fd >= 0) { close(event_fd); event_fd = -1; dir_changed = FALSE; } #endif if (!populate(path, lastname)) { printwarn(); goto nochange; } #ifdef LINUX_INOTIFY if (inotify_wd == -1) inotify_wd = inotify_add_watch(inotify_fd, path, INOTIFY_MASK); #elif defined(BSD_KQUEUE) if (event_fd == -1) { #if defined(O_EVTONLY) event_fd = open(path, O_EVTONLY); #else event_fd = open(path, O_RDONLY); #endif if (event_fd >= 0) EV_SET(&events_to_monitor[0], event_fd, EVFILT_VNODE, EV_ADD | EV_CLEAR, KQUEUE_FFLAGS, 0, path); } #endif while (1) { redraw(path); nochange: /* Exit if parent has exited */ if (getppid() == 1) _exit(0); sel = nextsel(&presel); switch (sel) { case SEL_BACK: /* There is no going back */ if (istopdir(path)) { /* Continue in navigate-as-you-type mode, if enabled */ if (cfg.filtermode) presel = FILTER; goto nochange; } dir = xdirname(path); if (access(dir, R_OK) == -1) { printwarn(); goto nochange; } /* Save history */ xstrlcpy(lastname, xbasename(path), NAME_MAX + 1); /* Save last working directory */ xstrlcpy(lastdir, path, PATH_MAX); xstrlcpy(path, dir, PATH_MAX); setdirwatch(); goto begin; case SEL_NAV_IN: // fallthrough case SEL_GOIN: /* Cannot descend in empty directories */ if (!ndents) goto begin; mkpath(path, dents[cur].name, newpath, PATH_MAX); DPRINTF_S(newpath); /* Cannot use stale data in entry, file may be missing by now */ if (stat(newpath, &sb) == -1) { printwarn(); goto nochange; } DPRINTF_U(sb.st_mode); switch (sb.st_mode & S_IFMT) { case S_IFDIR: if (access(newpath, R_OK) == -1) { printwarn(); goto nochange; } /* Save last working directory */ xstrlcpy(lastdir, path, PATH_MAX); xstrlcpy(path, newpath, PATH_MAX); lastname[0] = '\0'; setdirwatch(); goto begin; case S_IFREG: { /* If opened as vim plugin and Enter/^M pressed, pick */ if (cfg.picker && sel == SEL_GOIN) { r = mkpath(path, dents[cur].name, newpath, PATH_MAX); appendfpath(newpath, r); writecp(pcopybuf, copybufpos - 1); dentfree(dents); return; } /* If open file is disabled on right arrow or `l`, return */ if (cfg.nonavopen && sel == SEL_NAV_IN) continue; /* Handle script selection mode */ if (cfg.runscript) { if (cfg.runctx != cfg.curctx) continue; /* Must be in script directory to select script */ if (strcmp(path, runpath) != 0) continue; mkpath(path, dents[cur].name, newpath, PATH_MAX); xstrlcpy(path, rundir, PATH_MAX); if (runfile[0]) { xstrlcpy(lastname, runfile, NAME_MAX); spawn(shell, newpath, lastname, path, F_NORMAL | F_SIGINT); runfile[0] = '\0'; } else spawn(shell, newpath, NULL, path, F_NORMAL | F_SIGINT); rundir[0] = '\0'; cfg.runscript = 0; setdirwatch(); goto begin; } /* If NNN_USE_EDITOR is set, open text in EDITOR */ if (cfg.useeditor && get_output(g_buf, CMD_LEN_MAX, "file", FILE_OPTS, newpath, FALSE) && strstr(g_buf, "text/") == g_buf) { spawn(editor, editor_arg, newpath, path, F_NORMAL); continue; } /* Invoke desktop opener as last resort */ spawn(utils[OPENER], newpath, NULL, NULL, F_NOWAIT | F_NOTRACE); continue; } default: printmsg("unsupported file"); goto nochange; } case SEL_NEXT: if (cur < ndents - 1) ++cur; else if (ndents) /* Roll over, set cursor to first entry */ cur = 0; break; case SEL_PREV: if (cur > 0) --cur; else if (ndents) /* Roll over, set cursor to last entry */ cur = ndents - 1; break; case SEL_PGDN: if (cur < ndents - 1) cur += MIN((LINES - 4) / 2, ndents - 1 - cur); break; case SEL_PGUP: if (cur > 0) cur -= MIN((LINES - 4) / 2, cur); break; case SEL_HOME: cur = 0; break; case SEL_END: cur = ndents - 1; break; case SEL_CDHOME: dir = getenv("HOME"); if (dir == NULL) { clearprompt(); goto nochange; } // fallthrough case SEL_CDBEGIN: if (sel == SEL_CDBEGIN) dir = ipath; if (!xdiraccess(dir)) goto nochange; if (strcmp(path, dir) == 0) break; /* Save last working directory */ xstrlcpy(lastdir, path, PATH_MAX); xstrlcpy(path, dir, PATH_MAX); lastname[0] = '\0'; DPRINTF_S(path); setdirwatch(); goto begin; case SEL_CDLAST: // fallthrough case SEL_VISIT: if (sel == SEL_VISIT) { if (strcmp(mark, path) == 0) break; tmp = mark; } else tmp = lastdir; if (tmp[0] == '\0') { printmsg("not set"); goto nochange; } if (!xdiraccess(tmp)) goto nochange; xstrlcpy(newpath, tmp, PATH_MAX); xstrlcpy(lastdir, path, PATH_MAX); xstrlcpy(path, newpath, PATH_MAX); lastname[0] = '\0'; DPRINTF_S(path); setdirwatch(); goto begin; case SEL_LEADER: // fallthrough case SEL_CYCLE: if (sel == SEL_CYCLE) fd = '>'; else fd = get_input(NULL); switch (fd) { case 'q': // fallthrough case '~': // fallthrough case '-': // fallthrough case '&': presel = fd; goto nochange; case '>': case '.': case '<': case ',': r = cfg.curctx; if (fd == '>' || fd == '.') do (r == CTX_MAX - 1) ? (r = 0) : ++r; while (!g_ctx[r].c_cfg.ctxactive); else do (r == 0) ? (r = CTX_MAX - 1) : --r; while (!g_ctx[r].c_cfg.ctxactive); // fallthrough fd = '1' + r; // fallthrough case '1': // fallthrough case '2': // fallthrough case '3': // fallthrough case '4': r = fd - '1'; /* Save the next context id */ if (cfg.curctx == r) { if (sel == SEL_CYCLE) { (r == CTX_MAX - 1) ? (r = 0) : ++r; snprintf(newpath, PATH_MAX, "Create context %d? ('Enter' confirms)", r + 1); fd = get_input(newpath); if (fd != '\r') continue; } else continue; } #ifdef DIR_LIMITED_COPY g_crc = 0; #endif /* Save current context */ xstrlcpy(g_ctx[cfg.curctx].c_name, dents[cur].name, NAME_MAX + 1); g_ctx[cfg.curctx].c_cfg = cfg; if (g_ctx[r].c_cfg.ctxactive) /* Switch to saved context */ cfg = g_ctx[r].c_cfg; else { /* Setup a new context from current context */ g_ctx[r].c_cfg.ctxactive = 1; xstrlcpy(g_ctx[r].c_path, path, PATH_MAX); xstrlcpy(g_ctx[r].c_init, path, PATH_MAX); g_ctx[r].c_last[0] = '\0'; xstrlcpy(g_ctx[r].c_name, dents[cur].name, NAME_MAX + 1); g_ctx[r].c_cfg = cfg; g_ctx[r].c_cfg.runscript = 0; } /* Reset the pointers */ path = g_ctx[r].c_path; ipath = g_ctx[r].c_init; lastdir = g_ctx[r].c_last; lastname = g_ctx[r].c_name; cfg.curctx = r; setdirwatch(); goto begin; } if (get_bm_loc(fd, newpath) == NULL) { printmsg(messages[STR_INVBM_KEY]); goto nochange; } if (!xdiraccess(newpath)) goto nochange; if (strcmp(path, newpath) == 0) break; lastname[0] = '\0'; /* Save last working directory */ xstrlcpy(lastdir, path, PATH_MAX); /* Save the newly opted dir in path */ xstrlcpy(path, newpath, PATH_MAX); DPRINTF_S(path); setdirwatch(); goto begin; case SEL_PIN: xstrlcpy(mark, path, PATH_MAX); printmsg(mark); goto nochange; case SEL_FLTR: presel = filterentries(path); /* Save current */ if (ndents) copycurname(); goto nochange; case SEL_MFLTR: // fallthrough case SEL_TOGGLEDOT: // fallthrough case SEL_DETAIL: // fallthrough case SEL_FSIZE: // fallthrough case SEL_BSIZE: // fallthrough case SEL_MTIME: switch (sel) { case SEL_MFLTR: cfg.filtermode ^= 1; if (cfg.filtermode) { presel = FILTER; goto nochange; } /* Start watching the directory */ dir_changed = TRUE; break; case SEL_TOGGLEDOT: cfg.showhidden ^= 1; break; case SEL_DETAIL: cfg.showdetail ^= 1; cfg.showdetail ? (printptr = &printent_long) : (printptr = &printent); break; case SEL_FSIZE: cfg.sizeorder ^= 1; cfg.mtimeorder = 0; cfg.apparentsz = 0; cfg.blkorder = 0; cfg.copymode = 0; break; case SEL_BSIZE: if (sel == SEL_BSIZE) { if (!cfg.apparentsz) cfg.blkorder ^= 1; nftw_fn = &sum_bsizes; cfg.apparentsz = 0; BLK_SHIFT = ffs(S_BLKSIZE) - 1; } if (cfg.blkorder) { cfg.showdetail = 1; printptr = &printent_long; } cfg.mtimeorder = 0; cfg.sizeorder = 0; cfg.copymode = 0; break; default: /* SEL_MTIME */ cfg.mtimeorder ^= 1; cfg.sizeorder = 0; cfg.apparentsz = 0; cfg.blkorder = 0; cfg.copymode = 0; break; } /* Save current */ if (ndents) copycurname(); goto begin; case SEL_STATS: if (!ndents) break; mkpath(path, dents[cur].name, newpath, PATH_MAX); if (lstat(newpath, &sb) == -1 || !show_stats(newpath, dents[cur].name, &sb)) { printwarn(); goto nochange; } break; case SEL_MEDIA: // fallthrough case SEL_FMEDIA: // fallthrough case SEL_ARCHIVELS: // fallthrough case SEL_EXTRACT: // fallthrough case SEL_RENAMEALL: // fallthrough case SEL_RUNEDIT: // fallthrough case SEL_RUNPAGE: if (!ndents) break; // fallthrough case SEL_REDRAW: // fallthrough case SEL_HELP: // fallthrough case SEL_LOCK: { mkpath(path, dents[cur].name, newpath, PATH_MAX); switch (sel) { case SEL_MEDIA: r = show_mediainfo(newpath, NULL); break; case SEL_FMEDIA: r = show_mediainfo(newpath, "-f"); break; case SEL_ARCHIVELS: r = handle_archive(newpath, "-l", path); break; case SEL_EXTRACT: r = handle_archive(newpath, "-x", path); break; case SEL_REDRAW: if (ndents) copycurname(); goto begin; case SEL_RENAMEALL: if ((r = getutil(utils[VIDIR]))) spawn(utils[VIDIR], ".", NULL, path, F_NORMAL); break; case SEL_HELP: r = show_help(path); break; case SEL_RUNEDIT: r = TRUE; spawn(editor, editor_arg, dents[cur].name, path, F_NORMAL); break; case SEL_RUNPAGE: r = TRUE; spawn(pager, pager_arg, dents[cur].name, path, F_NORMAL); break; default: /* SEL_LOCK */ r = TRUE; spawn(utils[LOCKER], NULL, NULL, NULL, F_NORMAL | F_SIGINT); break; } if (!r) { printmsg("required utility missing"); goto nochange; } /* In case of successful operation, reload contents */ /* Continue in navigate-as-you-type mode, if enabled */ if (cfg.filtermode) presel = FILTER; /* Save current */ copycurname(); /* Repopulate as directory content may have changed */ goto begin; } case SEL_ASIZE: cfg.apparentsz ^= 1; if (cfg.apparentsz) { nftw_fn = &sum_sizes; cfg.blkorder = 1; BLK_SHIFT = 0; } else cfg.blkorder = 0; // fallthrough case SEL_COPY: if (!ndents) goto nochange; if (cfg.copymode) { /* * Clear the tmp copy path file on first copy. * * This ensures that when the first file path is * copied into memory (but not written to tmp file * yet to save on writes), the tmp file is cleared. * The user may be in the middle of selection mode op * and issue a cp, mv of multi-rm assuming the files * in the copy list would be affected. However, these * ops read the source file paths from the tmp file. */ if (!ncp) writecp(NULL, 0); r = mkpath(path, dents[cur].name, newpath, PATH_MAX); if (!appendfpath(newpath, r)) goto nochange; ++ncp; } else { r = mkpath(path, dents[cur].name, newpath, PATH_MAX); /* Keep the copy buf in sync */ copybufpos = 0; appendfpath(newpath, r); writecp(newpath, r - 1); /* Truncate NULL from end */ if (copier) spawn(copier, NULL, NULL, NULL, F_NOTRACE); } printmsg(newpath); goto nochange; case SEL_COPYMUL: cfg.copymode ^= 1; if (cfg.copymode) { g_crc = crc8fast((uchar *)dents, ndents * sizeof(struct entry)); copystartid = cur; copybufpos = 0; ncp = 0; printmsg("selection on"); DPRINTF_S("selection on"); goto nochange; } if (!ncp) { /* Handle range selection */ #ifndef DIR_LIMITED_COPY if (g_crc != crc8fast((uchar *)dents, ndents * sizeof(struct entry))) { cfg.copymode = 0; printmsg("range error: dir/content changed"); DPRINTF_S("range error: dir/content changed"); goto nochange; } #endif if (cur < copystartid) { copyendid = copystartid; copystartid = cur; } else copyendid = cur; if (copystartid < copyendid) { for (r = copystartid; r <= copyendid; ++r) if (!appendfpath(newpath, mkpath(path, dents[r].name, newpath, PATH_MAX))) goto nochange; snprintf(newpath, PATH_MAX, "%d files copied", copyendid - copystartid + 1); printmsg(newpath); } } if (copybufpos) { /* File path(s) written to the buffer */ writecp(pcopybuf, copybufpos - 1); /* Truncate NULL from end */ if (copier) spawn(copier, NULL, NULL, NULL, F_NOTRACE); if (ncp) { /* Some files cherry picked */ snprintf(newpath, PATH_MAX, "%d files copied", ncp); printmsg(newpath); } } else printmsg("selection off"); goto nochange; case SEL_COPYLIST: if (copybufpos) showcplist(); else printmsg("none selected"); goto nochange; case SEL_CP: case SEL_MV: case SEL_RMMUL: { /* Fail if copy file path not generated */ if (!g_cppath[0]) { printmsg("copy file not found"); goto nochange; } /* Warn if selection not completed */ if (cfg.copymode) { printmsg("finish selection first"); goto nochange; } /* Fail if copy file path isn't accessible */ if (access(g_cppath, R_OK) == -1) { printmsg("empty selection list"); goto nochange; } if (sel == SEL_CP) { snprintf(g_buf, CMD_LEN_MAX, #ifdef __linux__ "xargs -0 -a %s -%c src cp -iRp src .", #else "cat %s | xargs -0 -o -%c src cp -iRp src .", #endif g_cppath, REPLACE_STR); } else if (sel == SEL_MV) { snprintf(g_buf, CMD_LEN_MAX, #ifdef __linux__ "xargs -0 -a %s -%c src mv -i src .", #else "cat %s | xargs -0 -o -%c src mv -i src .", #endif g_cppath, REPLACE_STR); } else { /* SEL_RMMUL */ snprintf(g_buf, CMD_LEN_MAX, #ifdef __linux__ "xargs -0 -a %s rm -%cr", #else "cat %s | xargs -0 -o rm -%cr", #endif g_cppath, confirm_force()); } spawn("sh", "-c", g_buf, path, F_NORMAL | F_SIGINT); copycurname(); if (cfg.filtermode) presel = FILTER; goto begin; } case SEL_RM: { if (!ndents) break; char rm_opts[] = "-ir"; rm_opts[1] = confirm_force(); mkpath(path, dents[cur].name, newpath, PATH_MAX); spawn("rm", rm_opts, newpath, NULL, F_NORMAL | F_SIGINT); if (cur && access(newpath, F_OK) == -1) --cur; copycurname(); if (cfg.filtermode) presel = FILTER; goto begin; } case SEL_ARCHIVE: // fallthrough case SEL_OPENWITH: // fallthrough case SEL_RENAME: if (!ndents) break; // fallthrough case SEL_NEW: { switch (sel) { case SEL_ARCHIVE: tmp = xreadline(dents[cur].name, "name: "); break; case SEL_OPENWITH: tmp = xreadline(NULL, "open with: "); break; case SEL_NEW: tmp = xreadline(NULL, "name: "); break; default: /* SEL_RENAME */ tmp = xreadline(dents[cur].name, ""); break; } if (tmp == NULL || tmp[0] == '\0') break; /* Allow only relative, same dir paths */ if (tmp[0] == '/' || strcmp(xbasename(tmp), tmp) != 0) { printmsg(messages[STR_INPUT_ID]); goto nochange; } /* Confirm if app is CLI or GUI */ if (sel == SEL_OPENWITH) { r = get_input("press 'c' for cli mode"); (r == 'c') ? (r = F_NORMAL) : (r = F_NOWAIT | F_NOTRACE); } switch (sel) { case SEL_ARCHIVE: /* newpath is used as temporary buffer */ if (!getutil(utils[APACK])) { printmsg("utility missing"); continue; } spawn(utils[APACK], tmp, dents[cur].name, path, F_NORMAL); break; case SEL_OPENWITH: dir = NULL; getprogarg(tmp, &dir); /* dir used as tmp var */ mkpath(path, dents[cur].name, newpath, PATH_MAX); spawn(tmp, dir, newpath, path, r); break; case SEL_RENAME: /* Skip renaming to same name */ if (strcmp(tmp, dents[cur].name) == 0) goto nochange; break; default: break; } /* Complete OPEN, LAUNCH, ARCHIVE operations */ if (sel != SEL_NEW && sel != SEL_RENAME) { /* Continue in navigate-as-you-type mode, if enabled */ if (cfg.filtermode) presel = FILTER; /* Save current */ copycurname(); /* Repopulate as directory content may have changed */ goto begin; } /* Open the descriptor to currently open directory */ fd = open(path, O_RDONLY | O_DIRECTORY); if (fd == -1) { printwarn(); goto nochange; } /* Check if another file with same name exists */ if (faccessat(fd, tmp, F_OK, AT_SYMLINK_NOFOLLOW) != -1) { if (sel == SEL_RENAME) { /* Overwrite file with same name? */ if (get_input("press 'y' to overwrite") != 'y') { close(fd); break; } } else { /* Do nothing in case of NEW */ close(fd); printmsg("entry exists"); goto nochange; } } if (sel == SEL_RENAME) { /* Rename the file */ if (renameat(fd, dents[cur].name, fd, tmp) != 0) { close(fd); printwarn(); goto nochange; } } else { /* Check if it's a dir or file */ r = get_input("press 'f'(ile) or 'd'(ir)"); if (r == 'f') { r = openat(fd, tmp, O_CREAT, 0666); close(r); } else if (r == 'd') { r = mkdirat(fd, tmp, 0777); } else { close(fd); break; } /* Check if file creation failed */ if (r == -1) { close(fd); printwarn(); goto nochange; } } close(fd); xstrlcpy(lastname, tmp, NAME_MAX + 1); goto begin; } case SEL_EXEC: // fallthrough case SEL_SHELL: // fallthrough case SEL_SCRIPT: // fallthrough case SEL_RUNCMD: switch (sel) { case SEL_EXEC: if (!ndents) goto nochange; /* Check if this is a directory */ if (!S_ISREG(dents[cur].mode)) { printmsg("not regular file"); goto nochange; } /* Check if file is executable */ if (!(dents[cur].mode & 0100)) { printmsg("permission denied"); goto nochange; } mkpath(path, dents[cur].name, newpath, PATH_MAX); spawn(newpath, NULL, NULL, path, F_NORMAL | F_SIGINT); break; case SEL_SHELL: spawn(shell, shell_arg, NULL, path, F_NORMAL | F_MARKER); break; case SEL_SCRIPT: if (!runpath) { printmsg("set NNN_SCRIPT"); goto nochange; } if (stat(runpath, &sb) == -1) { printwarn(); goto nochange; } if (S_ISDIR(sb.st_mode)) { cfg.runscript ^= 1; if (!cfg.runscript && rundir[0]) { /* If toggled, and still in the script dir, switch to original directory */ if (strcmp(path, runpath) == 0) { xstrlcpy(path, rundir, PATH_MAX); xstrlcpy(lastname, runfile, NAME_MAX); rundir[0] = runfile[0] = '\0'; setdirwatch(); goto begin; } break; } /* Check if directory is accessible */ if (!xdiraccess(runpath)) goto nochange; xstrlcpy(rundir, path, PATH_MAX); xstrlcpy(path, runpath, PATH_MAX); if (ndents) xstrlcpy(runfile, dents[cur].name, NAME_MAX); cfg.runctx = cfg.curctx; lastname[0] = '\0'; setdirwatch(); goto begin; } if (S_ISREG(sb.st_mode)) { if (ndents) tmp = dents[cur].name; else tmp = NULL; spawn(shell, runpath, tmp, path, F_NORMAL | F_SIGINT); } break; default: /* SEL_RUNCMD */ tmp = xreadline(NULL, "> "); if (tmp && tmp[0]) spawn(shell, "-c", tmp, path, F_NORMAL | F_SIGINT); } /* Continue in navigate-as-you-type mode, if enabled */ if (cfg.filtermode) presel = FILTER; /* Save current */ if (ndents) copycurname(); /* Re-populate as directory content may have changed */ goto begin; case SEL_QUITCD: // fallthrough case SEL_QUIT: for (r = 0; r < CTX_MAX; ++r) if (r != cfg.curctx && g_ctx[r].c_cfg.ctxactive) { r = get_input("Quit all contexts? ('Enter' confirms)"); break; } if (!(r == CTX_MAX || r == '\r')) break; if (sel == SEL_QUITCD) { /* In vim picker mode, clear selection and exit */ if (cfg.picker) { if (copybufpos) { if (cfg.pickraw) /* Reset for for raw pick */ copybufpos = 0; else /* Clear the picker file */ writecp(NULL, 0); } dentfree(dents); return; } tmp = getenv("NNN_TMPFILE"); if (!tmp) { printmsg("set NNN_TMPFILE"); goto nochange; } FILE *fp = fopen(tmp, "w"); if (fp) { fprintf(fp, "cd \"%s\"", path); fclose(fp); } } // fallthrough case SEL_QUITCTX: if (sel == SEL_QUITCTX) { uint iter = 1; r = cfg.curctx; while (iter < CTX_MAX) { (r == CTX_MAX - 1) ? (r = 0) : ++r; if (g_ctx[r].c_cfg.ctxactive) { g_ctx[cfg.curctx].c_cfg.ctxactive = 0; /* Switch to next active context */ path = g_ctx[r].c_path; ipath = g_ctx[r].c_init; lastdir = g_ctx[r].c_last; lastname = g_ctx[r].c_name; cfg = g_ctx[r].c_cfg; cfg.curctx = r; setdirwatch(); goto begin; } ++iter; } } dentfree(dents); return; } /* switch (sel) */ /* Locker */ if (idletimeout != 0 && idle == idletimeout) { idle = 0; spawn(utils[LOCKER], NULL, NULL, NULL, F_NORMAL | F_SIGINT); } } } static void usage(void) { fprintf(stdout, "usage: nnn [-b key] [-C] [-e] [-i] [-l]\n" " [-p file] [-S] [-v] [-h] [PATH]\n\n" "The missing terminal file manager for X.\n\n" "positional args:\n" " PATH start dir [default: current dir]\n\n" "optional args:\n" " -b key bookmark key to open\n" " -C disable directory color\n" " -e use exiftool instead of mediainfo\n" " -i start in navigate-as-you-type mode\n" " -l start in light mode\n" " -p file copy selection to file (stdout if '-')\n" " -S start in disk usage analyser mode\n" " -v show program version\n" " -h show this help\n\n" "Version: %s\n%s\n", VERSION, GENERAL_INFO); } int main(int argc, char *argv[]) { char cwd[PATH_MAX] __attribute__ ((aligned)); char *ipath = NULL; int opt; while ((opt = getopt(argc, argv, "Slib:Cep:vh")) != -1) { switch (opt) { case 'S': cfg.blkorder = 1; nftw_fn = sum_bsizes; BLK_SHIFT = ffs(S_BLKSIZE) - 1; break; case 'l': cfg.showdetail = 0; printptr = &printent; break; case 'i': cfg.filtermode = 1; break; case 'b': ipath = optarg; break; case 'C': cfg.showcolor = 0; break; case 'e': cfg.metaviewer = EXIFTOOL; break; case 'p': cfg.picker = 1; if (optarg[0] == '-' && optarg[1] == '\0') cfg.pickraw = 1; else { /* copier used as tmp var */ copier = realpath(optarg, g_cppath); if (!g_cppath[0]) { fprintf(stderr, "%s\n", strerror(errno)); return 1; } } break; case 'v': fprintf(stdout, "%s\n", VERSION); return 0; case 'h': usage(); return 0; default: usage(); return 1; } } /* Confirm we are in a terminal */ if (!isatty(0) || !isatty(1)) { fprintf(stderr, "stdin or stdout is not a tty\n"); return 1; } /* Get the context colors; copier used as tmp var */ if (cfg.showcolor) { copier = getenv("NNN_CONTEXT_COLORS"); if (copier) { opt = 0; while (*copier && opt < CTX_MAX) { if (*copier < '0' || *copier > '7') { fprintf(stderr, "invalid color code\n"); return 1; } g_ctx[opt].color = *copier - '0'; ++copier; ++opt; } while (opt != CTX_MAX) { g_ctx[opt].color = 4; ++opt; } } else for (opt = 0; opt < CTX_MAX; ++opt) g_ctx[opt].color = 4; /* Default color is blue */ } /* Parse bookmarks string */ if (!parsebmstr()) { fprintf(stderr, "NNN_BMS: single-char keys only\n"); return 1; } if (ipath) { /* Open a bookmark directly */ if (ipath[1] || get_bm_loc(*ipath, cwd) == NULL) { fprintf(stderr, "%s\n", messages[STR_INVBM_KEY]); return 1; } ipath = cwd; } else if (argc == optind) { /* Start in the current directory */ ipath = getcwd(cwd, PATH_MAX); if (ipath == NULL) ipath = "/"; } else { ipath = realpath(argv[optind], cwd); if (!ipath) { fprintf(stderr, "%s: no such dir\n", argv[optind]); return 1; } } /* Increase current open file descriptor limit */ open_max = max_openfds(); if (getuid() == 0 || getenv("NNN_SHOW_HIDDEN")) cfg.showhidden = 1; /* Edit text in EDITOR, if opted */ if (getenv("NNN_USE_EDITOR")) cfg.useeditor = 1; /* Get VISUAL/EDITOR */ editor = xgetenv("VISUAL", xgetenv("EDITOR", "vi")); getprogarg(editor, &editor_arg); /* Get PAGER */ pager = xgetenv("PAGER", "less"); getprogarg(pager, &pager_arg); /* Get SHELL */ shell = xgetenv("SHELL", "sh"); getprogarg(shell, &shell_arg); /* Setup script execution */ runpath = getenv("NNN_SCRIPT"); #ifdef LINUX_INOTIFY /* Initialize inotify */ inotify_fd = inotify_init1(IN_NONBLOCK); if (inotify_fd < 0) { fprintf(stderr, "inotify init! %s\n", strerror(errno)); return 1; } #elif defined(BSD_KQUEUE) kq = kqueue(); if (kq < 0) { fprintf(stderr, "kqueue init! %s\n", strerror(errno)); return 1; } #endif /* Get locker wait time, if set; copier used as tmp var */ copier = getenv("NNN_IDLE_TIMEOUT"); if (copier) { opt = atoi(copier); idletimeout = opt * ((opt > 0) - (opt < 0)); } /* Get the clipboard copier, if set */ copier = getenv("NNN_COPIER"); if (getenv("HOME")) g_tmpfplen = xstrlcpy(g_tmpfpath, getenv("HOME"), HOME_LEN_MAX); else if (getenv("TMPDIR")) g_tmpfplen = xstrlcpy(g_tmpfpath, getenv("TMPDIR"), HOME_LEN_MAX); else if (xdiraccess("/tmp")) g_tmpfplen = xstrlcpy(g_tmpfpath, "/tmp", HOME_LEN_MAX); if (!cfg.picker && g_tmpfplen) { xstrlcpy(g_cppath, g_tmpfpath, HOME_LEN_MAX); xstrlcpy(g_cppath + g_tmpfplen - 1, "/.nnncp", HOME_LEN_MAX - g_tmpfplen); } /* Disable auto-select if opted */ if (getenv("NNN_NO_AUTOSELECT")) cfg.autoselect = 0; /* Disable opening files on right arrow and `l` */ if (getenv("DISABLE_FILE_OPEN_ON_NAV")) cfg.nonavopen = 1; signal(SIGINT, SIG_IGN); signal(SIGQUIT, SIG_IGN); /* Test initial path */ if (!xdiraccess(ipath)) { fprintf(stderr, "%s: %s\n", ipath, strerror(errno)); return 1; } /* Set locale */ setlocale(LC_ALL, ""); crc8init(); #ifdef DEBUGMODE enabledbg(); #endif if (!initcurses()) return 1; browse(ipath); exitcurses(); if (cfg.pickraw) { if (copybufpos) { opt = selectiontofd(1); if (opt != (int)(copybufpos)) fprintf(stderr, "%s\n", strerror(errno)); } } else if (!cfg.picker && g_cppath[0]) unlink(g_cppath); /* Free the copy buffer */ free(pcopybuf); #ifdef LINUX_INOTIFY /* Shutdown inotify */ if (inotify_wd >= 0) inotify_rm_watch(inotify_fd, inotify_wd); close(inotify_fd); #elif defined(BSD_KQUEUE) if (event_fd >= 0) close(event_fd); close(kq); #endif #ifdef DEBUGMODE disabledbg(); #endif return 0; } nnn-2.2/src/nnn.h000066400000000000000000000144421341255065400137110ustar00rootroot00000000000000/* * BSD 2-Clause License * * Copyright (c) 2014-2016, Lazaros Koromilas * Copyright (c) 2014-2016, Dimitris Papastamos * Copyright (c) 2016-2019, Arun Prakash Jana * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #pragma once #include #define CONTROL(c) ((c) ^ 0x40) /* Supported actions */ enum action { SEL_BACK = 1, SEL_GOIN, SEL_NAV_IN, SEL_NEXT, SEL_PREV, SEL_PGDN, SEL_PGUP, SEL_HOME, SEL_END, SEL_CDHOME, SEL_CDBEGIN, SEL_CDLAST, SEL_LEADER, SEL_CYCLE, SEL_PIN, SEL_VISIT, SEL_FLTR, SEL_MFLTR, SEL_TOGGLEDOT, SEL_DETAIL, SEL_STATS, SEL_MEDIA, SEL_FMEDIA, SEL_ARCHIVE, SEL_ARCHIVELS, SEL_EXTRACT, SEL_FSIZE, /* file size */ SEL_ASIZE, /* apparent size */ SEL_BSIZE, /* block size */ SEL_MTIME, SEL_REDRAW, SEL_COPY, SEL_COPYMUL, SEL_COPYLIST, SEL_CP, SEL_MV, SEL_RMMUL, SEL_RM, SEL_OPENWITH, SEL_NEW, SEL_RENAME, SEL_RENAMEALL, SEL_HELP, SEL_EXEC, SEL_SHELL, SEL_SCRIPT, SEL_RUNCMD, SEL_RUNEDIT, SEL_RUNPAGE, SEL_LOCK, SEL_QUITCTX, SEL_QUITCD, SEL_QUIT, }; /* Associate a pressed key to an action */ struct key { int sym; /* Key pressed */ enum action act; /* Action */ }; static struct key bindings[] = { /* Back */ { KEY_BACKSPACE, SEL_BACK }, { '\b' /* BS */, SEL_BACK }, { 127 /* DEL */, SEL_BACK }, { KEY_LEFT, SEL_BACK }, { 'h', SEL_BACK }, { CONTROL('H'), SEL_BACK }, /* Inside or select */ { KEY_ENTER, SEL_GOIN }, { '\r', SEL_GOIN }, /* Pure navigate inside */ { KEY_RIGHT, SEL_NAV_IN }, { 'l', SEL_NAV_IN }, /* Next */ { 'j', SEL_NEXT }, { KEY_DOWN, SEL_NEXT }, { CONTROL('N'), SEL_NEXT }, /* Previous */ { 'k', SEL_PREV }, { KEY_UP, SEL_PREV }, { CONTROL('P'), SEL_PREV }, /* Page down */ { KEY_NPAGE, SEL_PGDN }, { CONTROL('D'), SEL_PGDN }, /* Page up */ { KEY_PPAGE, SEL_PGUP }, { CONTROL('U'), SEL_PGUP }, /* First entry */ { KEY_HOME, SEL_HOME }, { 'g', SEL_HOME }, { CONTROL('A'), SEL_HOME }, { '^', SEL_HOME }, /* Last entry */ { KEY_END, SEL_END }, { 'G', SEL_END }, { CONTROL('E'), SEL_END }, { '$', SEL_END }, /* HOME */ { '~', SEL_CDHOME }, /* Initial directory */ { '&', SEL_CDBEGIN }, /* Last visited dir */ { '-', SEL_CDLAST }, /* Leader key */ { CONTROL('_'), SEL_LEADER }, { '`', SEL_LEADER }, /* Cycle contexts in forward direction */ { '\t', SEL_CYCLE }, { CONTROL('I'), SEL_CYCLE }, /* Mark a path to visit later */ { 'b', SEL_PIN }, /* Visit marked directory */ { CONTROL('W'), SEL_VISIT }, /* Filter */ { '/', SEL_FLTR }, /* Toggle filter mode */ { KEY_IC, SEL_MFLTR }, { CONTROL('T'), SEL_MFLTR }, /* Toggle hide .dot files */ { '.', SEL_TOGGLEDOT }, /* Detailed listing */ { 'd', SEL_DETAIL }, /* File details */ { 'D', SEL_STATS }, /* Show media info short, run is hacked */ { 'm', SEL_MEDIA }, /* Show media info full, run is hacked */ { 'M', SEL_FMEDIA }, /* Create archive */ { 'f', SEL_ARCHIVE }, /* List archive */ { 'F', SEL_ARCHIVELS }, /* Extract archive */ { CONTROL('F'), SEL_EXTRACT }, /* Toggle sort by size */ { 's', SEL_FSIZE }, /* Sort by apparent size including dir contents */ { 'S', SEL_ASIZE }, /* Sort by total block count including dir contents */ { CONTROL('J'), SEL_BSIZE }, /* Toggle sort by time */ { 't', SEL_MTIME }, /* Redraw window */ { CONTROL('L'), SEL_REDRAW }, /* Copy currently selected file path */ { CONTROL('K'), SEL_COPY }, { ' ', SEL_COPY }, /* Toggle copy multiple file paths */ { CONTROL('Y'), SEL_COPYMUL }, { 'Y', SEL_COPYMUL }, /* Show list of copied files */ { 'y', SEL_COPYLIST }, /* Copy from copy buffer */ { 'P', SEL_CP }, /* Move from copy buffer */ { 'V', SEL_MV }, /* Delete from copy buffer */ { 'X', SEL_RMMUL }, /* Delete currently selected */ { CONTROL('X'), SEL_RM }, /* Open in a custom application */ { CONTROL('O'), SEL_OPENWITH }, /* Create a new file */ { 'n', SEL_NEW }, /* Show rename prompt */ { CONTROL('R'), SEL_RENAME }, /* Rename contents of current dir */ { 'r', SEL_RENAMEALL }, /* Show help */ { '?', SEL_HELP }, /* Execute file */ { 'C', SEL_EXEC }, /* Run command */ { '!', SEL_SHELL }, { CONTROL(']'), SEL_SHELL }, /* Run a custom script */ { 'R', SEL_SCRIPT }, /* Run a command */ { CONTROL('S'), SEL_RUNCMD }, /* Open in EDITOR or PAGER */ { 'e', SEL_RUNEDIT }, { 'p', SEL_RUNPAGE }, /* Lock screen */ { 'L', SEL_LOCK }, /* Quit a context */ { 'q', SEL_QUITCTX }, /* Change dir on quit */ { CONTROL('G'), SEL_QUITCD }, /* Quit */ { 'Q', SEL_QUIT }, { CONTROL('Q'), SEL_QUIT }, };