pax_global_header00006660000000000000000000000064136206560570014524gustar00rootroot0000000000000052 comment=bc7a81921ed974a408d4de2cbfb537fb9b45908e nnn-3.0/000077500000000000000000000000001362065605700121575ustar00rootroot00000000000000nnn-3.0/.circleci/000077500000000000000000000000001362065605700140125ustar00rootroot00000000000000nnn-3.0/.circleci/config.yml000066400000000000000000000063541362065605700160120ustar00rootroot00000000000000version: 2 jobs: compile: docker: - image: ubuntu:18.04 working_directory: ~/nnn environment: CI_FORCE_TEST: 1 steps: - run: command: | apt update -qq apt install -y --no-install-recommends software-properties-common wget gpg-agent shellcheck apt-add-repository -y ppa:jonathonf/gcc-9.1 wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key|apt-key add - apt-add-repository -y "deb http://apt.llvm.org/bionic/ llvm-toolchain-bionic-8 main" apt update -qq apt install -y --no-install-recommends git gcc gcc-8 gcc-9 clang clang-8 clang-tidy-8 make pkg-config libncursesw5-dev libreadline-dev - checkout - run: command: | export CFLAGS=-Werror make clean echo echo "########## gcc ##########" CC=gcc make strip ls -l nnn make clean echo echo "########## gcc-8 ##########" CC=gcc-8 make strip ls -l nnn make clean echo echo "########## gcc-9 ##########" CC=gcc-9 make strip ls -l nnn make clean echo echo "########## clang ##########" CC=clang make strip ls -l nnn make clean echo echo "########## clang-8 ##########" CC=clang-8 make strip ls -l nnn make clean echo echo "########## clang-tidy-8 ##########" clang-tidy-8 src/* -- -I/usr/include -I/usr/include/ncursesw echo "########## checllcheck ##########" find plugins/ -type f -not -name "*.md" -exec shellcheck -e SC1090 {} + package-and-publish: machine: true working_directory: ~/nnn steps: - checkout - run: name: "package with packagecore" command: | # Clean up rm -rf ./dist/* # Pack source git archive -o ../${CIRCLE_PROJECT_REPONAME}-${CIRCLE_TAG}.tar.gz --format tar.gz --prefix=${CIRCLE_PROJECT_REPONAME}-${CIRCLE_TAG#v}/ ${CIRCLE_TAG} # Use latest installed python3 from pyenv export PYENV_VERSION="$(pyenv versions | grep -Po '\b3\.\d+\.\d+' | tail -1)" pip install packagecore packagecore -c misc/packagecore/packagecore.yaml -o ./dist/ ${CIRCLE_TAG#v} # Move source pack to dist mv ../${CIRCLE_PROJECT_REPONAME}-${CIRCLE_TAG}.tar.gz dist/ - run: name: "publish to GitHub" command: | go get github.com/tcnksm/ghr ghr -t ${GITHUB_API_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} -replace ${CIRCLE_TAG} ./dist/ workflows: version: 2 test: jobs: &all-tests - compile nightly: triggers: - schedule: cron: "0 0 * * 6" filters: branches: only: - master jobs: *all-tests publish-github-release: jobs: - package-and-publish: filters: tags: only: /^v.*/ branches: ignore: /.*/ nnn-3.0/.github/000077500000000000000000000000001362065605700135175ustar00rootroot00000000000000nnn-3.0/.github/FUNDING.yml000066400000000000000000000002121362065605700153270ustar00rootroot00000000000000# These are supported funding model platforms custom: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=RMLTQ76JSXJ4Q nnn-3.0/.github/ISSUE_TEMPLATE.md000066400000000000000000000024151362065605700162260ustar00rootroot00000000000000#### BUG REPORTS If it looks like a local environment issue, please try to debug yourself. Debugging local setup issues is not our top priority. Before opening an issue, please try to reproduce on latest master. The bug you noticed might have already been fixed. Useful links: - compile `nnn` from source - https://github.com/jarun/nnn#from-source - debugging `nnn` - https://github.com/jarun/nnn/wiki/debugging-nnn If the issue can be reproduced on master, log it. Please provide the environment details. **If that's missing, the issue will be closed without any cited reason.** If we need more information and there is no communication from the bug reporter within 7 days from the date of request, we will close the issue. If you have relevant information, resume discussion any time. #### FEATURE REQUESTS Please consider contributing the feature back to `nnn` yourself. Feel free to discuss in the ToDo list thread. We are more than happy to help. --- PLEASE DELETE THIS LINE AND EVERYTHING ABOVE --- #### Environment details (Put `x` in the checkbox along with the information) [ ] Operating System: [ ] Desktop Environment: [ ] Terminal Emulator: [ ] Shell: [ ] Custom desktop opener (if applicable): [ ] Issue exists on `nnn` master #### Exact steps to reproduce the issue nnn-3.0/.gitignore000066400000000000000000000000101362065605700141360ustar00rootroot00000000000000*.o nnn nnn-3.0/.travis.yml000066400000000000000000000037431362065605700142770ustar00rootroot00000000000000language: c sudo: required services: - docker env: global: - REPO=nnn matrix: include: # Access more recent gcc and clang via a Xenial image #- os: linux # dist: trusty # compiler: gcc #- os: linux # dist: trusty # compiler: clang - os: osx compiler: gcc - os: osx compiler: clang install: - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then if [[ "$CC" == "clang" ]]; then brew update; brew install llvm; export PATH="/usr/local/opt/llvm/bin:$PATH"; fi; fi script: - export CFLAGS=-Werror; - make clean; make; - make clean; - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then if [[ "$CC" == "clang" ]]; then clang-tidy src/* -- -I/usr/include; fi; fi before_deploy: - cd .. - rm -rf $REPO/.git - tar -czf $REPO-${TRAVIS_TAG}.tar.gz $REPO - cd $REPO - unset CFLAGS - sudo apt-get update -qy - sudo apt-get install -qy python3 python3-pip - sudo python3 -m pip install --upgrade pip - sudo python3 -m pip install --upgrade packagecore setuptools - packagecore -o dist/ "${TRAVIS_TAG#v}" - mv ../$REPO-${TRAVIS_TAG}.tar.gz dist/ deploy: provider: releases api-key: secure: bdw73zBeBEZtDZGEyFpAksnpwLHTBaC7VP1pONmnsXv4qmMcynASz0LfE4krEdAsRnvxQvqPZXviy/SZ3bmaLpVQmJIC1uRWIyOBE6K/7ddf+wfLy+1uO9EPd/zDF/D28Xy8QJLDGDZI08BY5Fist4XowyvtdW3GRwwDL7TwyZyigr0fkqLJwYcqCpojsHsjXjhkpGZqB5XRztaJ4kIEVz8du03ZU1l5kA9lI8Pdk9+mntOOt6emlaJl+Wr81QkwA8TvGPlZ1VP/+h9YCtlRy/4OuiU+bg5/Atxjh8M6rsq+WpZ3ZdYbR6x5vP78p9f6TsJa4ymGhhW6MhYrKPwdT4oITzZcOXJl6AEoIvrWHQWLY7K3WaEfRbT+DODnXks0ToFKls7wyWfi/AHI0ejeDD5Na5/XhY0jdVYOVvovoY2n8LBaqRTFJDYKoCbJ1S+6myUvvmFVwiZWcgOv9gVX1aaIl4wh+XuzUtPDJTcJtUOE8Q2MWl0bdYVtEkHVRznPXN5u3odrDaeTD3vo+pEaEpshLoSKhCyyWvslSzN7T98ez3aw3KFXoFhXPEg5MEJWy7u359MaVwJIsOGUDAFhy/Y7h83LjQYnu8cmX2wuVrQIRIlGVB0f10GYZzPffZz98I/T0xv75NzpyRs31/wMxvdXz35c8m/yTup4kAkG/1s= file_glob: true file: - dist/* skip_cleanup: true on: condition: - "$TRAVIS_OS_NAME == linux" - "$CC == gcc" tags: true repo: jarun/nnn nnn-3.0/CHANGELOG000066400000000000000000000623241362065605700134000ustar00rootroot00000000000000nnn v3.0 2020-02-12 - take list of files as input and show - option `-e` replaces `NNN_USE_EDITOR` - option `-t` replaces `NNN_IDLE_TIMEOUT` - PCRE support - more readline bindings for native prompts - run GUI app as plugin - attempt lazy unmount when regular unmount fails - fix unmount on macOS: use `umount` - detect `sshfs` and `rclone` to prompt intelligently - auto-proceed on file open (toggle key +) - quit with error code on Q - additional key F5 to toggle hidden - key e to edit in EDITOR (back on multiple user requests) - option to edit list of files in selection is changed to E - do not end selection on redraw - `nuke`: [`glow`](https://github.com/charmbracelet/glow) as Markdown viewer - `nuke`: refactor, handle some common video types by extension - file name removed from status bar - static Makefile target - generate, upload static package on release - fix crash on entering empty dir, then Down - fix keypresses lost when showing message - fix #227: `nnn` creates xdg-open zombies ------------------------------------------------------------------------------- nnn v2.9 2020-01-15 - all keybinds and options reviewed by the team and frozen (see #422) - reduced number of keybinds - greatly improved help screen readability - `nuke`: sample opener (CLI-only by default) and plugin - fast line redraws instead of full screen refresh (thanks @annagrram) - auto archive handling by extension (see config `NNN_ARCHIVE`) - Lead key simplified to bookmark key (b or ^/) - single key to toggle order (t or ^T) - plugins - `.cbcp`: copy selection to system clipboard (internal, program option `-x`) - `.ntfy`: show noti on cp, mv, rm completion (internal, program option `-x`) - `autojump`: navigate using autojump - `upload`: paste text files to http://ix.io, upload rest to https://file.io - all fuzzy plugins modified to support both `fzf` and `fzy` - more control on plugins - prefix `-` to skip directory refresh after running (cmd as) plugin - suffix `*` to skip confirmation after running cmd as plugin - indicate range selection mode with `*` - list keys at bookmark and plugin key prompts - visit to pinned dir like bookmarks (Bookmark key followed by ,) - toggle executable (key *) - show mime along with file details - more special keys at empty filter prompt: - apply the last filter (^L) - toggle between string and regex (/) - toggle case-sensitivity (:) - retain filter on Esc, Up, Down - show filter details when filter is on - remove option to run filter as cmd on prompt key (can be disruptive) - program options - option `-x`: enable notis and copy selection to system clipboard - option `-g`: regex filters (string filter is default now) - option `-Q`: quit program without confirmation - option `-s`: load session - option `-n`: start in nav-as-you-type mode - option `-v`: version sort - option `-V`: show program version - option `-A`: disable dir auto-select - ISO 8601 compliant date in status bar - ported to Haiku OS (thanks @annagrram) - sort only filtered entries (to avoid directory refresh) - fix `getplugs` to install hidden files - fix several selection issues (see #400) - fix detail mode not restored on loading session - fix symlink to directory not auto-selected - fix regex error on partial regex patterns - fix symlink not shown if `stat(2)` on target fails - fix flags when spawning a CLI opener as default FM - fix issue with stat flag on Sun (no support for `dirent.d_type`) - fix current file in current context not saved correctly in session - signed source distribution on release - simplified debugging with line numbers in logs ------------------------------------------------------------------------------- nnn v2.8.1 2019-12-05 - Fix always archiving current file - More elaborate docs on selection changes ------------------------------------------------------------------------------- nnn v2.8 2019-12-04 - sessions (thanks @annagrram) - `rclone` support for remote access (mount _any_ cloud storage!!!) - toggle selection with Space or ^J - ignore events during selection so the `+` symbol is not lost - run custom (non-shell-interpreted) commands like plugins - configure _cd-on-quit_ as the default behaviour - create parent dirs for new files and dirs, duplicate a file/dir anywhere - _copy/move as_ workflow (thanks @KlzXS) - edit , flush selection buffer (thanks @KlzXS) - support xargs with minimal options (as in BusyBox) (thanks @KlzXS) - changed the key to size sort to z - additional key ] to show command prompt - mount archives using `archivemount` - smoother double click handling - program option `-R` to disable rollover at edges - keybind collision checker (for custom keybind config) (thanks @annagrram) - show size of file in bytes in status bar in disk usage mode - pass unresolved path as second argument (`$2`) to plugin - mechanism for plugins to control active directory - all binary questions are confirmed by y or Y - plugins - some plugins renamed - integrated `shellcheck` in CI, POSIX-compliance fixes (thanks @koalaman) - `getplugs` - detect modifications in exiting plugin file (thanks @KlzXS) - `drag-file` & `drop-file`: drag & drop files using dragon - `gutenread`: browse, download and read from Project Gutenberg - `suedit` - edit file with superuser permissions - `fzhist` - fuzzy select commands from history, edit and run - `fzcd` - change to a fuzzy-searched directory - `rename` - batch rename directory or selection using qmv or vidir - `pskill` - fuzzy list a process or zombies by name and kill - `exetoggle` - toggle executable status of hovered file - `treeview` - informative tree output with file permissions and size - `chksum` - recursively calculate checksum for files in hovered directory - `fzopen` renamed to `fzopen` - `imgsxiv` instructions added to browse and rename images - create link to current file - additional key ; to execute plugin - more explicit force removal message - force non-detachable internal edits in $EDITOR (option `-E`) - export current file as `$nnn` (instead of `$NN`) - fix file open failure from browser when configured as default FM ------------------------------------------------------------------------------- nnn v2.7 2019-10-06 - plugins for image preview, image and video thumbnails - redesigned selection workflow - drop path prefix for files in current dir for selection based archives - custom direct keybinds for plugins - libreadline `.history` file moved to `nnn` config directory - export current entry as `$NN` at command prompt - more informative status bar in light/detail modes - auto-proceed to next file on single file select - path clipping for long paths - completely revamped wiki - new program options: - `-a` to use file access time throughout the program - `-c` to indicate cli-only opener - `-f` to run filter as command on ^P - `-o` replaces config `NNN_RESTRICT_NAV_OPEN` - `-t` replaces config `NNN_NO_AUTOSELECT` - `-r` replaces config `NNN_OPS_PROG` - plugin changes: - `vidthumb` - show video thumbnails in terminal - `mediainf` - show media info (decoupled as a plugin) - `notes` - open a quick notes file/dir in `$EDITOR` (decoupled as a plugin) - `dups` - list duplicate files in the current directory - `oldbigfile` - list large files by access time - `moclyrics` - show lyrics of the track currently playing in MOC - `uidgid` list uid and gid of files in directory - `mocplay` - now detects if a track is playing or not - `organize` - categorize files and move to respective directories - `pastebin` - now uses ix.io paste service - `fzy-edit` - merged into `fzy-open` - `viuimg` - fix directory view - `checksum` - fixed POSIX compliance issues - `boom` - play music in MOC - keybind changes: - select entry: Space and ^J - select range (or clear selection): m and ^K - select all in dir: a - list selection: M - ^N replaces ^T to toggle _nav-as-you-type_ - Shift TAB to reverse context cycle - ' to jump to first file in dir - S for du, A for apparent du - additional key : to run plugin - additional key F2 to rename file - additional key F5 to redraw - quit context key Leadq is removed - Leader key combinations: - Lead' to jump to first file in dir - Lead] go to next active context - Lead[ go to prev active context - Lead. toggle show hidden files - improved duplicate file workflow - improved batch rename workflow when a selection exists - removed the wild load option (`-w`) - removed quick notes (added plugin `notes`) - fix #225 (thanks @KlzXS) - fix `tar`/`bsdtar` always creating tar archives (and not by suffix) - fix single mouse click to select file not working - fix symlink to dir removed on batch rename - fix detail mode not set with program option `-S` ------------------------------------------------------------------------------- nnn v2.6 2019-08-06 - new plugins - view image or browse a directory of images in terminal - show image thumbnails - PDF and text file reader - calculate and verify checksum of selection or file - append (and play) selection/dir/file music in MOC - variable bitrate mp3 ringtone generator - split current file or join selection - better experience on Termux (and touch based devices) - mouse scrolling support (with ncursesw6.0 and above) - tap/left click to visit parent, toggle nav-as-you-type mode - light mode set as default - show status bar and use reverse video in light mode - changed program options - `-d`: detail mode - `-H`: show hidden files - `-l` is retired - support `XDG_CONFIG_HOME` - support / as an additional Leader key when filter is on - sort by file extension - use zip/unzip/tar if atool/bsdtar not found - support duplicate file (key ^R, same as rename file) - new config option `NNN_SSHFS_OPTS` to specify `sshfs` options - restrict opening 0 byte files (`NNN_RESTRICT_0B` is obsolete) - critical defects fixed - fix #276 - crash with variable length inotify event handling - fix #285 - hang after deleting/moving current directory - fix #274 - a broken prompt on empty input with libreadline - fix #304 - list selection from another instance - `cmatrix` as locker fallback - wait for user input after running a command from prompt - scrolloff set to 3 from 5 ------------------------------------------------------------------------------- nnn v2.5 2019-05-27 - mouse support - new location for config files - `~/.config/nnn` - plugin dir location: `~/.config/nnn/plugins` - selection file `.nnncp` is now `~/.config/nnn/.selection` - plugins: - pdfview: view a PDF in pager - nmount: (un)mount a storage device - ndiff: file and directory diff for selection - hexview: view a file in hex - imgresize: batch resize images to desktop resolution - ipinfo: check your IP address and whois information - transfer: upload a file to transfer.in - pastebin: paste the contents of a text file to paste.ubuntu.com - boom: play random music from a directory - nwal: set an image as wallpaper using nitrogen - pywal: set selected image as wallpaper, change terminal color scheme - getplugs: update plugins - SSHFS support - support `bsdtar`, simplify `patool` integration - native batch rename support (`vidir` dependency dropped) - changes to support [configuration](https://github.com/jarun/nnn/wiki/nnn-as-default-file-manager) as the default file manager - per-context detail/light mode - case-insensitive version compare - shortcut to visit `/` - ` (backtick) - vim-like scrolloff support - ^D & ^U: scroll half page, PgDn & PdUp: scroll full page - fix selection across contexts - recognize Home and End keys at prompt for editing - fix broken program option `-b` - POSIX-compliant user-scripts (wherever possible) - `NNN_SCRIPT` is retired (replaced by plugins) ------------------------------------------------------------------------------- nnn v2.4 2019-03-19 - FreeDesktop.org compliant trashing - mark selected entries with `+` - _wild_ mode (option `-w`, key ^W) for _nav-as-you-type_ - POSIX-compliant GUI app launcher with drop-down menu (key =) - new scripts: - upload image to imgur - send selection to Android using kdeconnect-cli - show permissions in detail mode - cp, mv progress bar for Linux (needs advcpmv) [BSD, macOS shows on ^T] - make libreadline an optional dep (reduces memory usage) - minimize the number of redraws - handle screen resize gracefully - option `-d` to show hidden files (`NNN_SHOW_HIDDEN` is removed) - additional key K to toggle selection - change visit start dir key to @ - option `-C` to disable colors removed - per-context initial directory replaced by program start dir - marker msg when spawning new shell removed - rename debug file to `nnndbg` ------------------------------------------------------------------------------- nnn v2.3 2019-02-19 - file picker mode - repo of user-contributed scripts - substring search for filters (option `-s`) - version sort (option `-n`) - disk usage calculation abort with ^C - create sym/hard link(s) to files in selection - archiving of selection - show dir symlinks along with dirs in top - fixed CJK character handling at prompts - key `N` (1 <= N <= 4) to switch to context N - bring back `NNN_OPENER` to specify file opener - env var `NNN_NOTE` and keybind ^N for quick notes - handle multiple arguments in VISUAL/EDITOR - show the current directory being scanned in `du` mode - select all files (Y) - show command prompt (^P) - key , replaces ` as alternative Leader Key - keybind for visit pinned directory is now ^B - additional key ^V to run or select custom script - use libreadline for command prompt - reduce delay on Esc press - config option to avoid unexpected behaviour on 0-byte file open (see #187) - rename config option `DISABLE_FILE_OPEN_ON_NAV` to `NNN_RESTRICT_NAV_OPEN` - keys removed - $, ^, Backspace, ^H, ^P, ^M, ^W, ` ------------------------------------------------------------------------------- nnn v2.2 2019-01-01 What's in? - (neo)vim plugin [nnn.vim](https://github.com/mcchrish/nnn.vim) - macOS fixes - Fix issues with file copy, move, remove - Handle 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 (key 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-3.0/LICENSE000066400000000000000000000027001362065605700131630ustar00rootroot00000000000000BSD 2-Clause License Copyright (c) 2014-2016, Lazaros Koromilas Copyright (c) 2014-2016, Dimitris Papastamos Copyright (c) 2016-2020, 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-3.0/Makefile000066400000000000000000000064521362065605700136260ustar00rootroot00000000000000VERSION = $(shell grep -m1 VERSION $(SRC) | cut -f 2 -d'"') PREFIX ?= /usr/local MANPREFIX ?= $(PREFIX)/share/man STRIP ?= strip PKG_CONFIG ?= pkg-config INSTALL ?= install CP ?= cp CFLAGS_OPTIMIZATION ?= -O3 O_DEBUG := 0 O_NORL := 0 # no readline support O_NOLOC := 0 # no locale support # convert targets to flags for backwards compatibility ifneq ($(filter debug,$(MAKECMDGOALS)),) O_DEBUG := 1 endif ifneq ($(filter norl,$(MAKECMDGOALS)),) O_NORL := 1 endif ifneq ($(filter noloc,$(MAKECMDGOALS)),) O_NORL := 1 O_NOLOC := 1 endif ifeq ($(O_DEBUG),1) CPPFLAGS += -DDBGMODE CFLAGS += -g endif ifeq ($(O_NORL),1) CPPFLAGS += -DNORL else ifeq ($(O_STATIC),1) CPPFLAGS += -DNORL else LDLIBS += -lreadline endif ifeq ($(O_PCRE),1) CPPFLAGS += -DPCRE LDLIBS += -lpcre endif ifeq ($(O_NOLOC),1) CPPFLAGS += -DNOLOCALE endif 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 CFLAGS += $(CFLAGS_OPTIMIZATION) CFLAGS += $(CFLAGS_CURSES) LDLIBS += $(LDLIBS_CURSES) # static compilation needs libgpm development package ifeq ($(O_STATIC),1) LDFLAGS += -static LDLIBS += -lgpm endif DISTFILES = src nnn.1 Makefile README.md LICENSE SRC = src/nnn.c HEADERS = src/nnn.h BIN = nnn all: $(BIN) $(BIN): $(SRC) $(HEADERS) $(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) -o $@ $< $(LDLIBS) # targets for backwards compatibility debug: $(BIN) norl: $(BIN) noloc: $(BIN) 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) $^ static: make O_STATIC=1 strip mv $(BIN) $(BIN)-static dist: mkdir -p nnn-$(VERSION) $(CP) -r $(DISTFILES) nnn-$(VERSION) tar -cf - nnn-$(VERSION) | gzip > nnn-$(VERSION).tar.gz $(RM) -r nnn-$(VERSION) sign: git archive -o nnn-$(VERSION).tar.gz --format tar.gz --prefix=nnn-$(VERSION)/ v$(VERSION) gpg --detach-sign --yes nnn-$(VERSION).tar.gz rm -f nnn-$(VERSION).tar.gz upload-local: sign static $(eval ID=$(shell curl -s 'https://api.github.com/repos/jarun/nnn/releases/tags/v$(VERSION)' | jq .id)) curl -XPOST 'https://uploads.github.com/repos/jarun/nnn/releases/$(ID)/assets?name=nnn-$(VERSION).tar.gz.sig' \ -H 'Authorization: token $(NNN_SIG_UPLOAD_TOKEN)' -H 'Content-Type: application/pgp-signature' \ --upload-file nnn-$(VERSION).tar.gz.sig tar -cf $(BIN)-static-$(VERSION).x86-64.tar.gz $(BIN)-static curl -XPOST 'https://uploads.github.com/repos/jarun/nnn/releases/$(ID)/assets?name=nnn-$(VERSION)-static' \ -H 'Authorization: token $(NNN_SIG_UPLOAD_TOKEN)' -H 'Content-Type: application/x-sharedlib' \ --upload-file $(BIN)-static-$(VERSION).x86-64.tar.gz clean: $(RM) -f $(BIN) nnn-$(VERSION).tar.gz *.sig \ $(BIN)-static $(BIN)-static-$(VERSION).x86-64.tar.gz skip: ; .PHONY: all install uninstall strip static dist sign upload-local clean nnn-3.0/README.md000066400000000000000000000225501362065605700134420ustar00rootroot00000000000000

nnn - supercharge your productivity!

Latest release Availability Travis Status CircleCI Status Privacy Awareness License

navigate-as-you-type & du (click to see demo video)

## Introduction `nnn` is a full-featured terminal file manager. It's tiny and nearly 0-config with an [incredible performance](https://github.com/jarun/nnn/wiki/Performance). `nnn` is also a du analyzer, an app launcher, a batch renamer and a file picker. The [plugin repository](https://github.com/jarun/nnn/tree/master/plugins#nnn-plugins) has tons of plugins and documentation to extend the capabilities further. You can _plug_ new functionality _and play_ with a hotkey. There's an independent [(neo)vim plugin](https://github.com/mcchrish/nnn.vim). It runs smoothly on the Pi, [Termux](https://www.youtube.com/watch?v=AbaauM7gUJw), Linux, macOS, BSD, Haiku, Cygwin, WSL, across DEs and GUI utilities or a strictly CLI environment. [**Wiki**](https://github.com/jarun/nnn/wiki).

Donate via PayPal!

## Features - Resource sensitive - Typically needs less than 3.5MB resident memory - Works with 8-bit colors - Disk-IO sensitive (few disk reads and writes) - No FPU usage (all integer maths, even for file size) - Minimizes screen refresh with fast line redraws - Tiny binary (typically less than 100KB) - Portable - Language-agnostic plugins - Minimal library deps, easily compilable, tiny binary - No config file, minimal config with sensible defaults - Widely available on many packagers - Unicode support - Quality - Privacy-aware (no unconfirmed user data collection) - POSIX-compliant, follows Linux kernel coding style - Highly optimized, static analysis integrated code - Modes - Light (default), detail - Disk usage analyzer (block/apparent) - File picker, (neo)vim plugin - Navigation - *Navigate-as-you-type* with dir auto-select - Contexts (_aka_ tabs/workspaces) with custom colors - Sessions, bookmarks with hotkeys; pin and visit a dir - Remote mounts (needs sshfs, rclone) - Familiar shortcuts (arrows, ~, -, @), quick reference - CD on quit (*easy* shell integration) - Auto-proceed on opening files - Search - Instant filtering with *search-as-you-type* - Regex (POSIX/PCRE) and string (default) filters - Subtree search plugin to open or edit files - Sort - Ordered pure numeric names by default (visit _/proc_) - Case-insensitive version (_aka_ natural) sort - By file name, modification/access time, size, extension - Reverse sort - Mimes - Open with desktop opener or specify a custom app - Create, list, extract, mount (FUSE based) archives - Option to open all text files in EDITOR - Information - Detailed file information - Media information plugin - Convenience - Run plugins and custom commands with hotkeys - FreeDesktop compliant trash (needs trash-cli) - Cross-dir file/all/range selection - Batch renamer (feature-limited) for selection or dir - Display a list of files from stdin - Copy (as), move (as), delete, archive, link selection - Dir updates, notification on cp, mv, rm completion - Copy file paths to system clipboard on select - Create (with parents), rename, duplicate (anywhere) files and dirs - Launch GUI apps, run commands, spawn a shell, toggle executable - Hovered file set as `$nnn` at prompt and spawned shell - Lock terminal after configurable idle timeout ## Quickstart 1. Install the [utilities you may need](https://github.com/jarun/nnn#utility-dependencies) based on your regular workflows. 2. Configure [cd on quit](https://github.com/jarun/nnn/wiki/Basic-use-cases#configure-cd-on-quit). 3. To open text files in `$VISUAL` (else `$EDITOR`, fallback vi) add program option `-e` in your alias. 4. For additional functionality [install plugins](https://github.com/jarun/nnn/tree/master/plugins#installing-plugins). 5. To copy selected file paths to system clipboard and show notis on cp, mv, rm completion use option `-x`. 6. For a strictly CLI environment, customize and use plugin [`nuke`](https://github.com/jarun/nnn/blob/master/plugins/nuke). Don't memorize! Arrows (or h j k l), /, q suffice. Tab creates, cycles contexts. ? lists shortcuts. ## Installation #### Library dependencies A curses library with wide char support (e.g. ncursesw), libreadline (optional) and standard libc. #### Utility dependencies | Dependency | Installation | Operation | | --- | --- | --- | | xdg-open (Linux), open(1) (macOS), cygstart
(Cygwin), open (Haiku) | base | desktop opener | | file, coreutils (cp, mv, rm), xargs | base | file type, copy, move and remove | | tar, (un)zip [atool/bsdtar for more formats] | base | create, list, extract bzip2, (g)zip, tar | | archivemount, fusermount(3) | optional | mount, unmount archives | | sshfs, [rclone](https://rclone.org/), fusermount(3) | optional | mount, unmount remotes | | trash-cli | optional | trash files (default action: rm) | | vlock (Linux), bashlock (macOS), lock(1) (BSD),
peaclock (Haiku) | optional | terminal locker (fallback: [cmatrix](https://github.com/abishekvashok/cmatrix)) | | advcpmv (Linux) ([integration](https://github.com/jarun/nnn/wiki/Advanced-use-cases#show-cp-mv-progress)) | optional | copy, move progress | | `$VISUAL` (else `$EDITOR`), `$PAGER`, `$SHELL` | optional | fallback vi, less, sh | #### From a package manager Install `nnn` from your package manager. If the version available is dated try an alternative installation method.
Packaging status (expand)


Packaging status

Unlisted packagers:


● CentOS (yum --enablerepo=epel install nnn)
Milis Linux (mps kur nnn)
NuTyX (cards install nnn)
Source Mage (cast nnn)

#### Release packages Packages for Arch Linux, CentOS, Debian, Fedora and Ubuntu are auto-generated with the [latest stable release](https://github.com/jarun/nnn/releases/latest). #### From source Download the latest stable release or clone this repository (*risky*), install deps and compile. On Ubuntu 18.04: $ sudo apt-get install pkg-config libncursesw5-dev libreadline-dev $ sudo make strip install `PREFIX` is supported, in case you want to install to a different location. See the [developer guides](https://github.com/jarun/nnn/wiki/Developer-guides) for source verification, compilation notes on the Pi, Cygwin and other tips. #### Shell completion Completion scripts for Bash, Fish and Zsh are [available](misc/auto-completion). Refer to your shell's manual for installation instructions. ## Elsewhere - [Wikipedia](https://en.wikipedia.org/wiki/Nnn_(file_manager)) - [ArchWiki](https://wiki.archlinux.org/index.php/Nnn) - [FOSSMint](https://www.fossmint.com/nnn-linux-terminal-file-browser/) - [gHacks Tech News](https://www.ghacks.net/2019/11/01/nnn-is-an-excellent-command-line-based-file-manager-for-linux-macos-and-bsds/) - Hacker News [[1](https://news.ycombinator.com/item?id=18520898)] [[2](https://news.ycombinator.com/item?id=19850656)] - [It's FOSS](https://itsfoss.com/nnn-file-browser-linux/) - LinuxLinks [[1](https://www.linuxlinks.com/nnn-fast-and-flexible-file-manager/)] [[2](https://www.linuxlinks.com/bestconsolefilemanagers/)] - [Suckless Rocks](https://suckless.org/rocks/) - [Ubuntu Full Circle Magazine - Issue 135](https://fullcirclemagazine.org/issue-135/) ## Developers - [Arun Prakash Jana](https://github.com/jarun) (Copyright © 2016-2020) - [0xACE](https://github.com/0xACE) - [Anna Arad](https://github.com/annagrram) - [KlzXS](https://github.com/KlzXS) - [Maxim Baz](https://github.com/maximbaz) - and other contributors `nnn` is actively developed. Visit the to the [ToDo list](https://github.com/jarun/nnn/issues/472) to contribute or see the features in progress. nnn-3.0/misc/000077500000000000000000000000001362065605700131125ustar00rootroot00000000000000nnn-3.0/misc/auto-completion/000077500000000000000000000000001362065605700162315ustar00rootroot00000000000000nnn-3.0/misc/auto-completion/bash/000077500000000000000000000000001362065605700171465ustar00rootroot00000000000000nnn-3.0/misc/auto-completion/bash/nnn-completion.bash000066400000000000000000000021641362065605700227500ustar00rootroot00000000000000# # Rudimentary Bash completion definition for nnn. # # Author: # Arun Prakash Jana # _nnn () { COMPREPLY=() local IFS=$'\n' local cur=$2 prev=$3 local -a opts opts=( -a -A -b -c -d -e -E -g -H -K -n -o -p -Q -r -R -s -S -t -v -V -x -h ) if [[ $prev == -b ]]; then local bookmarks=$(echo $NNN_BMS | awk -F: -v RS=\; '{print $1}') COMPREPLY=( $(compgen -W "$bookmarks" -- "$cur") ) elif [[ $prev == -p ]]; then COMPREPLY=( $(compgen -f -d -- "$cur") ) elif [[ $prev == -s ]]; then local sessions_dir=${XDG_CONFIG_HOME:-$HOME/.config}/nnn/sessions COMPREPLY=( $(cd "$sessions_dir" && compgen -f -d -- "$cur") ) elif [[ $prev == -t ]]; then return 1 elif [[ $cur == -* ]]; then COMPREPLY=( $(compgen -W "${opts[*]}" -- "$cur") ) else COMPREPLY=( $(compgen -f -d -- "$cur") ) fi } complete -o filenames -F _nnn nnn nnn-3.0/misc/auto-completion/fish/000077500000000000000000000000001362065605700171625ustar00rootroot00000000000000nnn-3.0/misc/auto-completion/fish/nnn.fish000066400000000000000000000031311362065605700206240ustar00rootroot00000000000000# # Fish completion definition for nnn. # # Author: # Arun Prakash Jana # if test -n "$XDG_CONFIG_HOME" set sessions_dir $XDG_CONFIG_HOME/.config/nnn/sessions else set sessions_dir $HOME/.config/nnn/sessions end complete -c nnn -s a -d 'use access time' complete -c nnn -s A -d 'disable dir auto-select' complete -c nnn -s b -r -d 'bookmark key to open' -x -a '(echo $NNN_BMS | awk -F: -v RS=\; \'{print $1"\t"$2}\')' complete -c nnn -s c -d 'cli-only opener' complete -c nnn -s d -d 'start in detail mode' complete -c nnn -s e -d 'open text files in $VISUAL/$EDITOR/vi' complete -c nnn -s E -d 'use EDITOR for undetached edits' complete -c nnn -s g -d 'regex filters' complete -c nnn -s H -d 'show hidden files' complete -c nnn -s K -d 'detect key collision' complete -c nnn -s n -d 'start in navigate-as-you-type mode' complete -c nnn -s o -d 'open files only on Enter' complete -c nnn -s p -r -d 'copy selection to file' -a '-\tstdout' complete -c nnn -s Q -d 'disable quit confirmation' complete -c nnn -s r -d 'show cp, mv progress (Linux-only)' complete -c nnn -s R -d 'disable rollover at edges' complete -c nnn -s s -r -d 'load session by name' -x -a '@\t"last session" (ls $sessions_dir)' complete -c nnn -s S -d 'start in disk usage analyzer mode' complete -c nnn -s t -r -d 'timeout in seconds to lock' complete -c nnn -s v -d 'use version compare to sort files' complete -c nnn -s V -d 'show program version and exit' complete -c nnn -s x -d 'notis, sel to system clipboard' complete -c nnn -s h -d 'show program help' nnn-3.0/misc/auto-completion/zsh/000077500000000000000000000000001362065605700170355ustar00rootroot00000000000000nnn-3.0/misc/auto-completion/zsh/_nnn000066400000000000000000000022021362065605700177040ustar00rootroot00000000000000#compdef nnn # # Completion definition for nnn. # # Author: # Arun Prakash Jana # setopt localoptions noshwordsplit noksharrays local -a args args=( '(-a)-a[use access time]' '(-A)-A[disable dir auto-select]' '(-b)-b[bookmark key to open]:key char' '(-c)-c[cli-only opener]' '(-d)-d[start in detail mode]' '(-e)-e[open text files in $VISUAL/$EDITOR/vi]' '(-E)-E[use EDITOR for undetached edits]' '(-g)-g[regex filters]' '(-H)-H[show hidden files]' '(-K)-K[detect key collision]' '(-n)-n[start in navigate-as-you-type mode]' '(-o)-o[open files only on Enter]' '(-p)-p[copy selection to file]:file name' '(-Q)-Q[disable quit confirmation]' '(-r)-r[show cp, mv progress (Linux-only)]' '(-R)-R[disable rollover at edges]' '(-s)-s[load session]:session name' '(-S)-S[start in disk usage analyzer mode]' '(-t)-t[timeout to lock]:seconds' '(-v)-v[use version compare to sort files]' '(-V)-V[show program version and exit]' '(-x)-x[notis, sel to system clipboard]' '(-h)-h[show program help]' '*:filename:_files' ) _arguments -S -s $args nnn-3.0/misc/haiku/000077500000000000000000000000001362065605700142135ustar00rootroot00000000000000nnn-3.0/misc/haiku/Makefile000066400000000000000000000065661362065605700156700ustar00rootroot00000000000000VERSION = $(shell grep -m1 VERSION $(SRC) | cut -f 2 -d'"') PREFIX ?= /boot/system/non-packaged MANPREFIX ?= $(PREFIX)/documentation/man STRIP ?= strip PKG_CONFIG ?= pkg-config INSTALL ?= install CP ?= cp CFLAGS_OPTIMIZATION ?= -O3 O_DEBUG := 0 O_NORL := 0 # no readline support O_NOLOC := 0 # no locale support # convert targets to flags for backwards compatibility ifneq ($(filter debug,$(MAKECMDGOALS)),) O_DEBUG := 1 endif ifneq ($(filter norl,$(MAKECMDGOALS)),) O_NORL := 1 endif ifneq ($(filter noloc,$(MAKECMDGOALS)),) O_NORL := 1 O_NOLOC := 1 endif ifeq ($(O_DEBUG),1) CPPFLAGS += -DDBGMODE CFLAGS += -g LDLIBS += -lrt endif ifeq ($(O_NORL),1) CPPFLAGS += -DNORL else LDLIBS += -lreadline endif ifeq ($(O_NOLOC),1) CPPFLAGS += -DNOLOCALE endif 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 ifeq ($(shell uname -s), Haiku) LDLIBS_HAIKU ?= -lstdc++ -lbe SRC_HAIKU ?= misc/haiku/nm.cpp OBJS_HAIKU ?= misc/haiku/nm.o endif CFLAGS += -Wall -Wextra CFLAGS += $(CFLAGS_OPTIMIZATION) CFLAGS += $(CFLAGS_CURSES) LDLIBS += $(LDLIBS_CURSES) $(LDLIBS_HAIKU) DISTFILES = src nnn.1 Makefile README.md LICENSE SRC = src/nnn.c HEADERS = src/nnn.h BIN = nnn OBJS := nnn.o $(OBJS_HAIKU) all: $(BIN) ifeq ($(shell uname -s), Haiku) $(OBJS_HAIKU): $(SRC_HAIKU) $(CC) -c $(CPPFLAGS) $(CFLAGS) -o $@ $< endif nnn.o: $(SRC) $(HEADERS) $(CC) -c $(CPPFLAGS) $(CFLAGS) -o $@ $< $(BIN): $(OBJS) $(CC) $(LDFLAGS) -o $@ $^ $(LDLIBS) # targets for backwards compatibility debug: $(BIN) norl: $(BIN) noloc: $(BIN) 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) $^ static: make O_STATIC=1 strip mv $(BIN) $(BIN)-static dist: mkdir -p nnn-$(VERSION) $(CP) -r $(DISTFILES) nnn-$(VERSION) mkdir -p nnn-$(VERSION)/misc $(CP) -r misc/haiku nnn-$(VERSION)/misc tar -cf - nnn-$(VERSION) | gzip > nnn-$(VERSION).tar.gz $(RM) -r nnn-$(VERSION) sign: git archive -o nnn-$(VERSION).tar.gz --format tar.gz --prefix=nnn-$(VERSION)/ v$(VERSION) gpg --detach-sign --yes nnn-$(VERSION).tar.gz rm -f nnn-$(VERSION).tar.gz upload-local: sign static $(eval ID=$(shell curl -s 'https://api.github.com/repos/jarun/nnn/releases/tags/v$(VERSION)' | jq .id)) curl -XPOST 'https://uploads.github.com/repos/jarun/nnn/releases/$(ID)/assets?name=nnn-$(VERSION).tar.gz.sig' \ -H 'Authorization: token $(NNN_SIG_UPLOAD_TOKEN)' -H 'Content-Type: application/pgp-signature' \ --upload-file nnn-$(VERSION).tar.gz.sig curl -XPOST 'https://uploads.github.com/repos/jarun/nnn/releases/$(ID)/assets?name=nnn-$(VERSION)-static' \ -H 'Authorization: token $(NNN_SIG_UPLOAD_TOKEN)' -H 'Content-Type: application/x-sharedlib' \ --upload-file $(BIN)-static clean: $(RM) -f $(BIN) $(BIN)-static $(OBJS) nnn-$(VERSION).tar.gz *.sig skip: ; .PHONY: all install uninstall strip static dist sign upload-local clean nnn-3.0/misc/haiku/haiku_interop.h000066400000000000000000000004661362065605700172330ustar00rootroot00000000000000#ifdef __cplusplus extern "C" { #endif typedef struct haiku_nm_t *haiku_nm_h; haiku_nm_h haiku_init_nm(); void haiku_close_nm(haiku_nm_h hnd); int haiku_watch_dir(haiku_nm_h hnd, const char *path); int haiku_stop_watch(haiku_nm_h hnd); int haiku_is_update_needed(haiku_nm_h hnd); #ifdef __cplusplus } #endif nnn-3.0/misc/haiku/nm.cpp000066400000000000000000000031501362065605700153300ustar00rootroot00000000000000#include #include #include #include #include "haiku_interop.h" filter_result dir_mon_flt(BMessage *message, BHandler **hnd, BMessageFilter *fltr) { (void) hnd; (void) fltr; if (message->what == B_NODE_MONITOR) { int32 val; message->FindInt32("opcode", &val); switch (val) { case B_ENTRY_CREATED: case B_ENTRY_MOVED: case B_ENTRY_REMOVED: return B_DISPATCH_MESSAGE; } } return B_SKIP_MESSAGE; } class DirectoryListener : public BLooper { public: bool recv_reset() { Lock(); bool val = _ev_on; _ev_on = false; Unlock(); return val; } private: void MessageReceived(BMessage * message) override { Lock(); _ev_on = true; Unlock(); BLooper::MessageReceived(message); } bool _ev_on = false; }; struct haiku_nm_t { haiku_nm_t() { dl = new DirectoryListener(); flt = new BMessageFilter(B_PROGRAMMED_DELIVERY, B_LOCAL_SOURCE, dir_mon_flt); dl->AddCommonFilter(flt); dl->Run(); } DirectoryListener *dl; BMessageFilter *flt; node_ref nr; }; haiku_nm_h haiku_init_nm() { return new haiku_nm_t(); } void haiku_close_nm(haiku_nm_h hnd) { delete hnd->flt; // This is the way of deleting a BLooper hnd->dl->PostMessage(B_QUIT_REQUESTED); delete hnd; } int haiku_watch_dir(haiku_nm_h hnd, const char *path) { BDirectory dir(path); dir.GetNodeRef(&(hnd->nr)); return watch_node(&(hnd->nr), B_WATCH_DIRECTORY, nullptr, hnd->dl); } int haiku_stop_watch(haiku_nm_h hnd) { return watch_node(&(hnd->nr), B_STOP_WATCHING, nullptr, hnd->dl); } int haiku_is_update_needed(haiku_nm_h hnd) { return hnd->dl->recv_reset(); } nnn-3.0/misc/haiku/nnn-master.recipe000066400000000000000000000030011362065605700174600ustar00rootroot00000000000000SUMMARY="The missing terminal file manager for X" DESCRIPTION="nnn is a full-featured terminal file manager. It's tiny and \ nearly 0-config with an incredible performance. nnn is also a du analyzer, an app launcher, a batch renamer and a file picker. \ The plugin repository has tons of plugins and documentation to extend the \ capabilities further. You can plug new functionality and play with a \ custom keybind instantly. There's an independent (neo)vim plugin. It runs smoothly on the Raspberry Pi, Termux on Android, Linux, macOS, BSD, \ Cygwin, WSL and works seamlessly with DEs and GUI utilities. Visit the Wiki for concepts, program usage, how-tos and troubleshooting." HOMEPAGE="https://github.com/jarun/nnn" COPYRIGHT="2016-2020 Arun Prakash Jana" LICENSE="BSD (2-clause)" REVISION="1" SOURCE_URI="git://github.com/jarun/nnn.git" ARCHITECTURES="x86_64" SECONDARY_ARCHITECTURES="x86" PROVIDES=" nnn$secondaryArchSuffix = $portVersion cmd:nnn = $portVersion " REQUIRES=" haiku$secondaryArchSuffix file$secondaryArchSuffix lib:libncurses$secondaryArchSuffix lib:libreadline$secondaryArchSuffix " BUILD_REQUIRES=" haiku${secondaryArchSuffix}_devel devel:libncurses$secondaryArchSuffix devel:libreadline$secondaryArchSuffix " BUILD_PREREQUIRES=" cmd:g++$secondaryArchSuffix cmd:gcc$secondaryArchSuffix cmd:install cmd:ld$secondaryArchSuffix cmd:make cmd:pkg_config$secondaryArchSuffix " BUILD() { make -f misc/haiku/Makefile $jobArgs } INSTALL() { make -f misc/haiku/Makefile install PREFIX=$prefix } nnn-3.0/misc/natool/000077500000000000000000000000001362065605700144065ustar00rootroot00000000000000nnn-3.0/misc/natool/natool000077500000000000000000000025751362065605700156410ustar00rootroot00000000000000#!/usr/bin/env python3 # ############################################################################# # natool: a wrapper script to patool to list, extract and create archives # # usage: natool [-a] [-l] [-x] [archive] [file/dir] # # Examples: # - create archive : natool -a archive.7z archive_dir # - list archive : natool -l archive.7z # - extract archive: natool -x archive.7z # # Brief: # natool is written to integrate patool (instead of the default atool) with nnn # A copies of this file should be dropped somewhere in $PATH as atool # # 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 [-a] [-l] [-x] [archive] [file/dir]') sys.exit(0) if sys.argv[1] == '-a': cmd = ['patool', '--non-interactive', 'create', sys.argv[2]] cmd.extend(sys.argv[3:]) elif sys.argv[1] == '-l': cmd = ['patool', '--non-interactive', 'list'] cmd.extend(sys.argv[2:]) elif sys.argv[1] == '-x': cmd = ['patool', '--non-interactive', 'extract'] cmd.extend(sys.argv[2:]) else: sys.exit(0) pipe = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) out, err = pipe.communicate() print(out.decode()) print(err.decode()) nnn-3.0/misc/packagecore/000077500000000000000000000000001362065605700153565ustar00rootroot00000000000000nnn-3.0/misc/packagecore/packagecore.yaml000066400000000000000000000045661362065605700205210ustar00rootroot00000000000000name: 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 - readline container: "archlinux/base" centos7.5: builddeps: - make - gcc - pkgconfig - ncurses-devel - readline-devel deps: - ncurses - readline commands: pre: - yum install epel-release centos7.6: builddeps: - make - gcc - pkgconfig - ncurses-devel - readline-devel deps: - ncurses - readline commands: pre: - yum install epel-release centos8.0: builddeps: - make - gcc - pkgconfig - ncurses-devel - readline-devel deps: - ncurses - readline commands: pre: - yum install epel-release debian9: builddeps: - make - gcc - pkg-config - libncursesw5-dev - libreadline-dev deps: - libncursesw5 - readline-common debian10: builddeps: - make - gcc - pkg-config - libncursesw5-dev - libreadline-dev deps: - libncursesw5 - readline-common fedora29: builddeps: - make - gcc - pkg-config - ncurses-devel - readline-devel deps: - ncurses - readline fedora30: builddeps: - make - gcc - pkg-config - ncurses-devel - readline-devel deps: - ncurses - readline fedora31: builddeps: - make - gcc - pkg-config - ncurses-devel - readline-devel deps: - ncurses - readline opensuse15.1: builddeps: - make - gcc - pkg-config - readline-devel - ncurses-devel deps: - libncurses6 - libreadline7 ubuntu16.04: builddeps: - make - gcc - pkg-config - libncursesw5-dev - libreadline6-dev deps: - libncursesw5 - libreadline6 ubuntu18.04: builddeps: - make - gcc - pkg-config - libncursesw5-dev - libreadline-dev deps: - libncursesw5 - libreadline7 nnn-3.0/misc/quitcd/000077500000000000000000000000001362065605700144035ustar00rootroot00000000000000nnn-3.0/misc/quitcd/quitcd.bash000066400000000000000000000014311362065605700165320ustar00rootroot00000000000000n () { # Block nesting of nnn in subshells if [ -n $NNNLVL ] && [ "${NNNLVL:-0}" -ge 1 ]; then echo "nnn is already running" return fi # The default behaviour is to cd on quit (nnn checks if NNN_TMPFILE is set) # To cd on quit only on ^G, remove the "export" as in: # NNN_TMPFILE="${XDG_CONFIG_HOME:-$HOME/.config}/nnn/.lastd" # NOTE: NNN_TMPFILE is fixed, should not be modified export NNN_TMPFILE="${XDG_CONFIG_HOME:-$HOME/.config}/nnn/.lastd" # Unmask ^Q (, ^V etc.) (if required, see `stty -a`) to Quit nnn # stty start undef # stty stop undef # stty lwrap undef # stty lnext undef nnn "$@" if [ -f "$NNN_TMPFILE" ]; then . "$NNN_TMPFILE" rm -f "$NNN_TMPFILE" > /dev/null fi } nnn-3.0/misc/quitcd/quitcd.csh000066400000000000000000000007711362065605700164000ustar00rootroot00000000000000# NOTE: set NNN_TMPFILE correctly if you use 'XDG_CONFIG_HOME' # The default behaviour is to cd on quit (nnn checks if NNN_TMPFILE is set) # To cd on quit only on ^G, export NNN_TMPFILE after the call to nnn # NOTE: NNN_TMPFILE is fixed, should not be modified set NNN_TMPFILE=~/.config/nnn/.lastd # Unmask ^Q (, ^V etc.) (if required, see `stty -a`) to Quit nnn # stty start undef # stty stop undef # stty lwrap undef # stty lnext undef alias n 'nnn -fis; source "$NNN_TMPFILE"; rm -f "$NNN_TMPFILE"' nnn-3.0/misc/quitcd/quitcd.fish000066400000000000000000000020531362065605700165470ustar00rootroot00000000000000# Rename this file to match the name of the function # e.g. ~/.config/fish/functions/n.fish # or, add the lines to the 'config.fish' file. function n --description 'support nnn quit and change directory' # Block nesting of nnn in subshells if test -n NNNLVL if [ (expr $NNNLVL + 0) -ge 1 ] echo "nnn is already running" return end end # The default behaviour is to cd on quit (nnn checks if NNN_TMPFILE is set) # To cd on quit only on ^G, remove the "-x" as in: # set NNN_TMPFILE "$XDG_CONFIG_HOME/nnn/.lastd" # NOTE: NNN_TMPFILE is fixed, should not be modified if test -n "$XDG_CONFIG_HOME" set -x NNN_TMPFILE "$XDG_CONFIG_HOME/nnn/.lastd" else set -x NNN_TMPFILE "$HOME/.config/nnn/.lastd" end # Unmask ^Q (, ^V etc.) (if required, see `stty -a`) to Quit nnn # stty start undef # stty stop undef # stty lwrap undef # stty lnext undef nnn $argv if test -e $NNN_TMPFILE source $NNN_TMPFILE rm $NNN_TMPFILE end end nnn-3.0/misc/quitcd/quitcd.zsh000066400000000000000000000014311362065605700164210ustar00rootroot00000000000000n () { # Block nesting of nnn in subshells if [ -n $NNNLVL ] && [ "${NNNLVL:-0}" -ge 1 ]; then echo "nnn is already running" return fi # The default behaviour is to cd on quit (nnn checks if NNN_TMPFILE is set) # To cd on quit only on ^G, remove the "export" as in: # NNN_TMPFILE="${XDG_CONFIG_HOME:-$HOME/.config}/nnn/.lastd" # NOTE: NNN_TMPFILE is fixed, should not be modified export NNN_TMPFILE="${XDG_CONFIG_HOME:-$HOME/.config}/nnn/.lastd" # Unmask ^Q (, ^V etc.) (if required, see `stty -a`) to Quit nnn # stty start undef # stty stop undef # stty lwrap undef # stty lnext undef nnn "$@" if [ -f "$NNN_TMPFILE" ]; then . "$NNN_TMPFILE" rm -f "$NNN_TMPFILE" > /dev/null fi } nnn-3.0/misc/test/000077500000000000000000000000001362065605700140715ustar00rootroot00000000000000nnn-3.0/misc/test/mktest.sh000077500000000000000000000071531362065605700157450ustar00rootroot00000000000000#!/bin/sh # Create test files and directories test -e outdir && { echo "Remove 'outdir' and try again" exit 1 } mkdir -p outdir && cd outdir || exit 1 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 korean touch 'korean/[ENG sub] PRODUCE48 울림ㅣ김채원ㅣ행복 나눠주는 천사소녀 @자기소개_1분 PR 180615 EP.0-Cgnmr6Fd82' touch 'korean/[ENG sub] PRODUCE48 [48스페셜] 윙크요정, 내꺼야!ㅣ김채원(울림) 180615 EP.0-K7ulTiuJZK8.mp4' touch 'korean/[FULL ENG SUB] 181008 SALEWA x IZ_ONE Long Padding Photoshoot Behind Film-[오늘의 시구] 아이즈원 (IZONE) 장원영&미야와키 사쿠라! 시구 시타! (10.06)-VmDl5eBJ3x0.mkv' touch 'korean/IZ_ONE (아이즈원) - 1st Mini Album [COLOR_IZ] Highlight Medley-w9V2xFrYIgk.web' touch 'korean/IZ_ONE (아이즈원) - 1st Mini Album [COLOR_IZ] MV TEASER 1-uhnJLBNBNto.mkv' touch 'korean/IZ_ONE CHU [1회] ′순도 100%′ 우리즈원 숙소 생활 ★최초 공개★ 181025 EP.1-pcutrQN1Sbg.mkv' touch 'korean/IZ_ONE CHU [1회_예고] 아이즈원 데뷔 준비 과정 ★독점 공개★ 아이즈원 츄 이번주 (목) 밤 11시 첫방송 181025' touch 'korean/IZ_ONE CHU [1회] 도치기현 1호 이모 팬과의 만남! 181025 EP.1-5kYoReT5x44.mp4' touch 'korean/IZ_ONE CHU [1회] ′12명 소녀들의 새로운 시작′ 앞으로 아이즈원 잘 부탁해♥ 181025 EP.1-RVNvgbdLQLQ' touch 'korean/IZ_ONE CHU [1회] ′앗..그것만은!′ 자비없는 합숙생활 폭로전 181025 EP.1-AmP5KzpoI38.mkv' touch 'korean/IZ_ONE CHU [1회] 휴게소 간식 내기 노래 맞히기 게임 181025 EP.1-LyNDKflpWYE.mp4' touch 'korean/IZ_ONE CHU [1회] 2018 아이즈원 걸크러시능력시험 (feat. 치타쌤) 181025 EP.1-9qHWpbo0eB8.mp4' touch 'korean/IZ_ONE CHU [1회] ′돼지요′ 아니죠, ′되지요′ 맞습니다! (feat. 꾸라먹방) 181025EP.1-WDLFqMWiKn' touch 'korean/IZ_ONE CHU [1회] ′두근두근′ 첫 MT를 앞둔 비글력 만렙의 아이즈원 181025 EP.1' 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-3.0/nnn.1000066400000000000000000000257511362065605700130440ustar00rootroot00000000000000.Dd Feb 12, 2020 .Dt NNN 1 .Os .Sh NAME .Nm nnn .Nd the missing terminal file manager for X .Sh SYNOPSIS .Nm .Op Ar -a .Op Ar -A .Op Ar -b key .Op Ar -c .Op Ar -d .Op Ar -e .Op Ar -E .Op Ar -g .Op Ar -H .Op Ar -K .Op Ar -n .Op Ar -p file .Op Ar -Q .Op Ar -r .Op Ar -R .Op Ar -s name .Op Ar -S .Op Ar -t secs .Op Ar -v .Op Ar -V .Op Ar -x .Op Ar -h .Op Ar PATH .Sh DESCRIPTION .Nm (Nnn's 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 Press \fB?\fR in .Nm to see the list of keybinds. .Sh OPTIONS .Pp .Nm supports the following options: .Pp .Fl a use access time for all operations (default: modification time) .Pp .Fl A disable directory auto-select in navigate-as-you-type mode .Pp .Fl "b key" specify bookmark key to open .Pp .Fl c opener opens files in cli utilities only (overrides -e) .Pp .Fl d detail mode .Pp .Fl e open text files in $VISUAL (else $EDITOR, fallback vi) [preferably CLI] .Pp .Fl E use $EDITOR for internal undetached edits .Pp .Fl g use regex filters instead of substring match .Pp .Fl H show hidden files .Pp .Fl K test for keybind collision .Pp .Fl n start in navigate-as-you-type mode .Pp .Fl o open files only on Enter key .Pp .Fl "p file" copy (or \fIpick\fR) selection to file, or stdout if file='-' .Pp .Fl Q disable confirmation on quit with multiple contexts active .Pp .Fl r show cp, mv progress (Linux-only, needs advcpmv; '^T' shows the progress on BSD/macOS) .Pp .Fl R disable rollover at edges .Pp .Fl "s name" load a session by name .Pp .Fl S start in disk usage analyzer mode .Pp .Fl "t secs" idle timeout in seconds to lock terminal .Pp .Fl v use case-insensitive version compare to sort files .Pp .Fl V show version and exit .Pp .Fl x show notis on selection cp, mv, rm completion copy path to system clipboard on select .Pp .Fl h show program help and exit .Sh CONFIGURATION There is no configuration file. Associated files are at .Pp \fB${XDG_CONFIG_HOME:-$HOME/.config}/nnn/\fR .Pp Configuration is done using a few optional (set if you need) environment variables. See ENVIRONMENT section. .Pp .Nm uses \fIxdg-open\fR (on Linux), \fIopen(1)\fR (on macOS), \fIcygstart\fR on (Cygwin) and \fIopen\fR on (Haiku) as the desktop opener. It's also possible to specify a custom opener. See ENVIRONMENT section. .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 video .br - other active contexts are underlined .br - rest are inactive .Pp On context creation, the state of the previous context is copied. Each context remembers its last visited directory. .Pp Each context can have its own directory color specified. See ENVIRONMENT section. .Sh SESSIONS Sessions are a way to save and restore states of work. A session stores the settings and contexts. .Pp Sessions can be loaded dynamically from within a running .Nm instance, or with a program option. .Pp When a session is loaded dynamically, the last working session is saved automatically to a dedicated -- "last session" -- session file. .Pp All the session files are located by session name in the directory .Pp \fB${XDG_CONFIG_HOME:-$HOME/.config}/nnn/sessions\fR .Pp "@" is the "last session" file. .Sh FILTERS Filters are strings to find matching entries in the current directory instantly (\fIsearch-as-you-type\fR). There is a program option to switch to regex filters. Matches are case-insensitive by default. In each context the last filter is persisted at runtime or in saved sessions. .Pp Special keys at empty filter prompt: .Pp - toggle between string and regex: '/' .br - toggle case sensitivity: ':' .br - apply the last filter (or clear filter if non-empty): '^L' .br - show help and config screen: '?' .br - show command prompt: ']' .br - launch an application: '=' .br - run a plugin by its key: ';' .br - pin current directory: ',' .Pp Other noteworthy keys: .Pp - '^char': usual keybind functionality .br - 'Esc': exit filter prompt but skip dir refresh .Pp Common regex 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 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 When there's a unique match and it's a directory, .Nm auto selects the directory and enters it in this mode. Use the relevant program option to disable this behaviour. .Sh SELECTION .Nm allows file selection across directories and contexts! .Pp There are 3 groups of keybinds to add files to selection: .Pp (1) hovered file selection toggle (deselects if '+' is visible before the entry, else adds to selection) .br (2) add a range of files to selection (repeat the range key on the same entry twice to clear selection completely) .br (3) add all files in the current directory to selection .Pp A selection can be edited, copied, moved, removed, archived or linked. .Pp Absolute paths of the selected files are copied to \fB.selection\fR file in the config directory. .Pp To edit the selection use the _edit selection_ key. Use this key to remove a file from selection after you navigate away from its directory. Editing doesn't end the selection mode. You can add more files to the selection and edit the list again. If no file is selected in the current session, this option attempts to list the selection file. .Sh LIST FILES .Nm can receive a list of files as input. The paths should be NUL-separated ('\\0') but doesn't need to be NUL-terminated. Paths and can be relative to the current directory or absolute. .Pp Input is limited by 65,536 paths or 256 MiB of input. .Pp Start .Nm in this mode by writing to its standard input. So the output of another command can be piped to it. For example, to list files in current directory larger than 1M: .Bd -literal find -maxdepth 1 -size +1M -print0 | nnn .Ed .Pp or you can redirect a list from a file: .Bd -literal nnn < files.txt .Ed .Pp A temporary directory will be created containing symlinks to the given paths. Any action performed on these symlinks will be performed only on their targets, after which they might become invalid. .Pp Though the term "files" is used, any input is valid as long as it's a valid path. \fBInvalid paths are ignored.\fR .Sh UNITS The minimum file size unit is byte (B). The rest are K, M, G, T, P, E, Z, Y (powers of 1024), same as the default units in \fIls\fR. .Sh ENVIRONMENT The SHELL, EDITOR (VISUAL, if defined) and PAGER environment variables are used. A single combination of arguments is supported for SHELL and PAGER. .Pp \fBNNN_OPENER:\fR specify a custom file opener. .Bd -literal export NNN_OPENER=nuke NOTE: `nuke` is a file opener available in plugin repository .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: To go to a bookmark, press the Lead key followed by the bookmark key. .Ed .Pp \fBNNN_PLUG:\fR directly executable plugins as \fIkey_char:location\fR pairs (max 15) separated by \fI;\fR: .Bd -literal export NNN_PLUG='o:fzopen;p:mocplay;d:diffs;m:nmount;t:imgthumb' NOTES: 1. To run a plugin directly, press \fI;\fR followed by the plugin key 2. To skip directory refresh after running a plugin, prefix with \fB-\fR export NNN_PLUG='m:-mediainfo' .Ed .Pp To assign keys to arbitrary non-background non-shell-interpreted cli commands and invoke like plugins, add \fI_\fR (underscore) before the command. .Bd -literal export NNN_PLUG='x:_chmod +x $nnn;g:_git log;s:_smplayer $nnn;o:fzopen' NOTES: 1. Use single quotes for $NNN_PLUG so $nnn is not interpreted 2. $nnn should be the last argument (IF used) 3. (Again) add \fB_\fR before the command 4. To disable directory refresh after running a \fIcommand as plugin\fR, prefix with \fB-_\fR 5. To skip user confirmation after command execution, suffix with \fB*\fR export NNN_PLUG='y:-_sync*' 6. To run a \fIGUI app as plugin\fR, add a \fB|\fR after \fB_\fR export NNN_PLUG='m:-_|mousepad $nnn' EXAMPLES: ----------------------------------- + ------------------------------------------------- Key:Command | Description ----------------------------------- + ------------------------------------------------- k:-_fuser -kiv $nnn* | Interactively kill process(es) using hovered file l:_git log | Show git log n:-_vi /home/user/Dropbox/dir/note* | Take quick notes in a synced file/dir of notes p:-_less -iR $nnn* | Page through hovered file in less s:-_|smplayer -minigui $nnn | Play hovered media file, even unfinished download x:_chmod +x $nnn | Make the hovered file executable y:-_sync* | Flush cached writes ----------------------------------- + ------------------------------------------------- .Ed .Pp \fBNNN_COLORS:\fR string of color codes for each context, e.g.: .Bd -literal export NNN_COLORS='1234' codes: 0-black, 1-red, 2-green, 3-yellow, 4-blue (default), 5-magenta, 6-cyan, 7-white .Ed .Pp \fBNNN_SSHFS:\fR pass additional options to sshfs command: .Bd -literal export NNN_SSHFS='sshfs -o reconnect,idmap=user,cache_timeout=3600' NOTE: The options must be preceded by `sshfs` and comma-separated without any space between them. .Ed .Pp \fBNNN_RCLONE:\fR pass additional options to rclone command: .Bd -literal export NNN_RCLONE='rclone mount --read-only --no-checksum' NOTE: The options must be preceded by `rclone` and max 5 flags are supported. .Ed .Pp \fBNNN_TRASH:\fR trash (instead of \fIdelete\fR) files to desktop Trash. .Bd -literal export NNN_TRASH=1 .Ed .Pp \fBnnn:\fR this is a special variable set to the hovered entry before executing a command from the command prompt or spawning a shell. .Sh KNOWN ISSUES .Nm may not handle keypresses correctly when used with tmux (see issue #104 for more details). Set \fBTERM=xterm-256color\fR to address it. .Sh AUTHORS .An Arun Prakash Jana Aq Mt engineerarun@gmail.com , .An Lazaros Koromilas Aq Mt lostd@2f30.org , .An Dimitris Papastamos Aq Mt sin@2f30.org . .Sh HOME .Em https://github.com/jarun/nnn nnn-3.0/plugins/000077500000000000000000000000001362065605700136405ustar00rootroot00000000000000nnn-3.0/plugins/.cbcp000077500000000000000000000024231362065605700145540ustar00rootroot00000000000000#!/usr/bin/env sh # Description: Copy selection to system clipboard as newline-separated entries # Requires: tr and # xclip/xsel (Linux) # pbcopy (macOS) # termux-clipboard-set (Termux) # clip.exe (WSL) # clip (Cygwin) # wl-copy (Wayland) # # LIMITATION: breaks if a filename has newline in it # # Note: For a space-separated list: # xargs -0 < "$SELECTION" # # Shell: POSIX compliant # Author: Arun Prakash Jana IFS="$(printf '%b_' '\n')"; IFS="${IFS%_}" # protect trailing \n SELECTION=${XDG_CONFIG_HOME:-$HOME/.config}/nnn/.selection if which xsel >/dev/null 2>&1; then # Linux tr '\0' '\n' < "$SELECTION" | xsel -bi elif which xclip >/dev/null 2>&1; then # Linux tr '\0' '\n' < "$SELECTION" | xclip -sel clip elif which pbcopy >/dev/null 2>&1; then # macOS tr '\0' '\n' < "$SELECTION" | pbcopy elif which termux-clipboard-set >/dev/null 2>&1; then # Termux tr '\0' '\n' < "$SELECTION" | termux-clipboard-set elif which clip.exe >/dev/null 2>&1; then # WSL tr '\0' '\n' < "$SELECTION" | clip.exe elif which clip >/dev/null 2>&1; then # Cygwin tr '\0' '\n' < "$SELECTION" | clip elif which wl-copy >/dev/null 2>&1; then # Wayland tr '\0' '\n' < "$SELECTION" | wl-copy fi nnn-3.0/plugins/.nnn-plugin-helper000066400000000000000000000012271362065605700172050ustar00rootroot00000000000000#!/usr/bin/env sh # Description: Helper script for plugins # # Shell: POSIX compliant # Author: Anna Arad selection=${XDG_CONFIG_HOME:-$HOME/.config}/nnn/.selection export selection ## Ask nnn to switch to directory $1 in context $2. ## If $2 is not provided, the function asks explicitly. nnn_cd () { dir=$1 if [ -z "$NNN_PIPE" ]; then echo "No pipe file found" 1>&2 return fi if [ -n "$2" ]; then context=$2 else printf "Choose context 1-4 (blank for current): " read -r context fi printf "%s" "${context:-0}$dir" > "$NNN_PIPE" } cmd_exists () { which "$1" > /dev/null 2>&1 echo $? } nnn-3.0/plugins/.ntfy000077500000000000000000000010371362065605700146250ustar00rootroot00000000000000#!/usr/bin/env sh # Description: Show a notification # # Details: nnn invokes this plugin to show notification when a cp/mv/rm operation is complete. # # Requires: notify-send (Ubuntu)/ntfy (https://github.com/dschep/ntfy)/osascript (macOS) # # Shell: POSIX compliant # Author: Anna Arad OS="$(uname)" if which notify-send >/dev/null 2>&1; then notify-send nnn "Done!" elif [ "$OS" = "Darwin" ]; then osascript -e 'display notification "Done!" with title "nnn"' elif which ntfy >/dev/null 2>&1; then ntfy -t nnn send "Done!" fi nnn-3.0/plugins/README.md000066400000000000000000000240571362065605700151270ustar00rootroot00000000000000

nnn plugins

read ebooks with plugin gutenread (Android)

image preview with plugin imgthumb

## Introduction Plugins extend the capabilities of `nnn`. They are _executable_ scripts (or binaries) which `nnn` can communicate with and trigger. This mechanism fits perfectly with the fundamental design to keep the core file manager lean and fast, by delegating repetitive (but not necessarily file manager-specific) tasks to the plugins. `nnn` is language-agnostic when it comes to plugins. You can write a plugin in any (scripting) language you are comfortable in! ## Installing plugins The following command installs or updates (after backup) all plugins: curl -Ls https://raw.githubusercontent.com/jarun/nnn/master/plugins/getplugs | sh Plugins are installed to `${XDG_CONFIG_HOME:-$HOME/.config}/nnn/plugins`. ## List of plugins | Plugin (a-z) | Description | Lang | Deps | | --- | --- | --- | --- | | autojump | Navigate to dir/path (**autojump stores navigation patterns**) | sh | autojump | | boom | Play random music from dir | sh | [moc](http://moc.daper.net/) | | dups | List non-empty duplicate files in current dir | sh | find, md5sum,
sort uniq xargs | | chksum | Create and verify checksums | sh | md5sum,
sha256sum | | diffs | Diff for selection (limited to 2 for directories) | sh | vimdiff | | dragdrop | Drag/drop files from/into nnn | sh | [dragon](https://github.com/mwh/dragon) | | fzcd | Change to the directory of a fuzzy-selected file/dir | sh | fzf/fzy
fd/fdfind/find | | fzhist | Fuzzy-select a cmd from history, edit in `$EDITOR` and run | sh | fzf/fzy | | fzopen | Fuzzy find a file in dir subtree and edit or open | sh | fzf/fzy, xdg-open | | getplugs | Update plugins | sh | curl | | gutenread | Browse, download, read from Project Gutenberg | sh | curl, unzip, w3m
[epr](https://github.com/wustho/epr) (optional) | | hexview | View a file in hex in `$PAGER` | sh | xxd | | imgresize | Resize images in dir to screen resolution | sh | [imgp](https://github.com/jarun/imgp) | | imgthumb | View thumbnail of an image or dir of images | sh | [lsix](https://github.com/hackerb9/lsix) | | imgur | Upload an image to imgur (from [imgur-screenshot](https://github.com/jomo/imgur-screenshot)) | bash | - | | imgview | Browse images, set wallpaper, copy path ([config](https://wiki.archlinux.org/index.php/Sxiv#Assigning_keyboard_shortcuts)), [rename](https://github.com/jarun/nnn/wiki/Basic-use-cases#browse-rename-images)| sh | sxiv/[viu](https://github.com/atanunq/viu), less| | ipinfo | Fetch external IP address and whois information | sh | curl, whois | | kdeconnect | Send selected files to an Android device | sh | kdeconnect-cli | | launch | GUI application launcher | sh | fzf/fzy | | mediainf | Show media information | sh | mediainfo | | moclyrics | Show lyrics of the track playing in moc | sh | [ddgr](https://github.com/jarun/ddgr), [moc](http://moc.daper.net/) | | mocplay | Append (and/or play) selection/dir/file in moc | sh | [moc](http://moc.daper.net/) | | nmount | Toggle mount status of a device as normal user | sh | pmount, udisks2 | | nuke | Sample file opener (CLI-only by default) | sh | various | | oldbigfile | List large files by access time | sh | find, sort | | organize | Auto-organize files in directories by file type | sh | file | | pdfread | Read a PDF or text file aloud | sh | pdftotext, mpv,
pico2wave | | pdfview | View PDF file in `$PAGER` | sh | pdftotext/
mupdf-tools | | picker | Pick files and list one per line (to pipe) | sh | nnn | | pskill | Fuzzy list by name and kill process or zombie | sh | fzf/fzy, ps,
sudo/doas | | renamer | Batch rename selection or files in dir | sh | [qmv](https://www.nongnu.org/renameutils/)/[vidir](https://joeyh.name/code/moreutils/) | | ringtone | Create a variable bitrate mp3 ringtone from file | sh | date, ffmpeg | | splitjoin | Split file or join selection | sh | split, cat | | suedit | Edit file using superuser permissions | sh | sudoedit/sudo/doas | | treeview | Informative tree output in `$EDITOR` | sh | tree | | uidgid | List user and group of all files in dir | sh | ls, less | | upgrade | Upgrade nnn manually on Debian 9 Stretch | sh | curl | | upload | Paste text to ix.io, upload binary to file.io | sh | curl, jq, tr | | vidthumb | Show video thumbnails in terminal | sh | [ffmpegthumbnailer](https://github.com/dirkvdb/ffmpegthumbnailer),
[lsix](https://github.com/hackerb9/lsix) | | wall | Set wallpaper or change colorscheme | sh | nitrogen/pywal | ## Invoking a plugin Use the plugin shortcut (;key or ^Skey) to list the defined plugin keys and press the required key. E.g., with the below config: export NNN_PLUG='o:fzopen;p:mocplay;d:diffs;m:nmount;n:notes;v:imgviu;t:imgthumb' Plugin `fzopen` can be run with the keybind ;o, `mocplay` can be run with ;p and so on... The key vs. plugin pairs are shown in the help and config screen. A maximum of 15 keys can be defined. To select and invoke a plugin from the plugin directory, press Enter (to _enter_ the plugin dir) after the plugin shortcut. #### Skip directory refresh after running a plugin `nnn` refreshes the directory after running a plugin to reflect any changes by the plugin. To disable this (say while running the `mediainfo` plugin on some filtered files), add a `-` before the plugin name: export NNN_PLUG='m:-mediainfo' Now `nnn` will not refresh the directory after running the `mediainfo` plugin. ## Running commands as plugin To assign keys to arbitrary non-background, non-shell-interpreted cli commands and invoke like plugins, add `_` (underscore) before the command. For example: export NNN_PLUG='x:_chmod +x $nnn;g:_git log;s:_smplayer $nnn;o:fzopen' Now ;x can be used to make a file executable, ;g can be used to the git log of a git project directory, ;s can be used to preview a partially downloaded media file. #### Skip user confirmation after command execution `nnn` waits for user confirmation (the prompt `Press Enter to continue`) after it executes a command as plugin (unlike plugins which can add a `read` to wait). To skip this, add a `*` after the command. For example: export NNN_PLUG='s:_smplayer $nnn*;n:-_vim /home/vaio/Dropbox/Public/synced_note*' Now there will be no prompt after ;s and ;n. #### Run GUI app as plugin To run a GUI app as plugin, add a `|` after `_`. For example: export NNN_PLUG='m:-_|mousepad $nnn' Notes: 1. Use single quotes for `$NNN_PLUG` so `$nnn` is not interpreted 2. `$nnn` should be the last argument (IF used) 3. (_Again_) add `_` before the command 4. To disable directory refresh after running a _command as plugin_, prefix with `-_` #### Some useful key-command examples | Key:Command | Description | |---|---| | `k:-_fuser -kiv $nnn*` | Interactively kill process(es) using hovered file | | `l:_git log` | Show git log | | `n:-_vi /home/user/Dropbox/dir/note*` | Take quick notes in a synced file/dir of notes | | `p:-_less -iR $nnn*` | Page through hovered file in less | | `s:-_\|smplayer -minigui $nnn` | Play hovered media file, even unfinished download | | `x:_chmod +x $nnn` | Make the hovered file executable | | `y:-_sync*` | Flush cached writes | ## Access level of plugins When `nnn` executes a plugin, it does the following: - Changes to the directory where the plugin is to be run (`$PWD` pointing to the active directory) - Passes two arguments to the script: 1. The hovered file's name. 2. The working directory (might differ from `$PWD` in case of symlinked paths; non-canonical). - Sets the environment variable `NNN_PIPE` used to control `nnn` active directory. Plugins can also read the `.selection` file in the config directory. ## Create your own plugins Plugins can be written in any scripting language. However, POSIX-compliant shell scripts runnable in `sh` are preferred. Drop the plugin in `${XDG_CONFIG_HOME:-$HOME/.config}/nnn/plugins` and make it executable. Optionally add a hotkey in `$NNN_PLUG` for frequent usage. #### Controlling `nnn`'s active directory `nnn` provides a mechanism for plugins to control its active directory. The way to do so is by writing to the pipe pointed by the environment variable `NNN_PIPE`. The plugin should write a single string in the format `` without a newline at the end. For example, `1/etc`. The number indicates the context to change the active directory of (0 is used to indicate the current context). For convenience, we provided a helper script named `.nnn-plugin-helper` and a function named `nnn_cd` to ease this process. `nnn_cd` receives the path to change to as the first argument, and the context as an optional second argument. If a context is not provided, it is asked for explicitly. Usage examples can be found in the Examples section below. #### Examples There are many plugins provided by `nnn` which can be used as examples. Here are a few simple selected examples. - Show the git log of changes to the particular file along with the code for a quick and easy review. ```sh #!/usr/bin/env sh git log -p -- "$1" ``` - Change to directory in clipboard using helper script ```sh #!/usr/bin/env sh . $(dirname $0)/.nnn-plugin-helper nnn_cd "$(xsel -ob)" ``` - Change directory to the location of a link using helper script with specific context (current) ```sh #!/usr/bin/env sh . $(dirname $0)/.nnn-plugin-helper nnn_cd "$(dirname $(readlink -fn $1))" 0 ``` - Change to arbitrary directory without helper script ```sh #!/usr/bin/env sh printf "cd to: " read -r dir printf "%s" "0$dir" > "$NNN_PIPE" ``` ## Contributing plugins 1. Add informative sections like _Description_, _Notes_, _Dependencies_, _Shell_, _Author_ etc. in the plugin. 2. Add an entry in the table above. 3. Keep non-portable commands (like `notify-send`) commented so users from any other OS/DE aren't surprised. 4. The plugin file should be executable. nnn-3.0/plugins/autojump000077500000000000000000000006541362065605700154370ustar00rootroot00000000000000#!/usr/bin/env sh # Description: Navigate to directory using autojump # # Requires: autojump - https://github.com/wting/autojump # # Note: autojump STORES NAVIGATION PATTERNS # # Shell: POSIX compliant # Author: Marty Buchaus if which autojump >/dev/null 2>&1; then printf "jump to: " read -r dir odir="$(autojump "$dir")" printf "%s" "0$odir" > "$NNN_PIPE" else printf "autojump missing" read -r _ fi nnn-3.0/plugins/boom000077500000000000000000000016401362065605700145230ustar00rootroot00000000000000#!/usr/bin/env sh # Description: Play random music from current directory. Identifies MP3, FLAC, M4A, WEBM, WMA. # You may want to set GUIPLAYER. # # Shell: POSIX compliant # Author: Arun Prakash Jana #GUIPLAYER=smplayer NUMTRACKS=100 if [ ! -z "$GUIPLAYER" ]; then PLAYER="$GUIPLAYER" find . -type f \( -iname "*.mp3" -o -iname "*.flac" -o -iname "*.m4a" -o -iname "*.webm" -o -iname "*.wma" \) | shuf | head -n $NUMTRACKS | xargs -d "\n" "$PLAYER" > /dev/null 2>&1 & # detach the player sleep 1 elif which mocp >/dev/null 2>&1; then # start MOC server mocp -S # clear MOC playlist mocp -c # add up to 100 random audio files find . -type f \( -iname "*.mp3" -o -iname "*.flac" -o -iname "*.m4a" -o -iname "*.webm" -o -iname "*.wma" \) | shuf | head -n $NUMTRACKS | xargs -d "\n" mocp -a # start playing mocp -p else printf "moc missing" read -r _ fi nnn-3.0/plugins/chksum000077500000000000000000000041531362065605700150630ustar00rootroot00000000000000#!/usr/bin/env sh # Description: Create and verify checksums # # For selection: it will generate one file containing the checksums with file names # [and with paths if they are in another directory] # the output checksum filename will be checksum_timestamp.checksum_type # For file: if the file is a checksum, the plugin does the verification # if the file is not a checksum, checksum will be generated for it # the output checksum filename will be filename.checksum_type # For directory: recursively calculates checksum for all the files in the directory # the output checksum filename will be directory.checksum_type # # Shell: POSIX compliant # Author: ath3, Arun Prakash Jana selection=${XDG_CONFIG_HOME:-$HOME/.config}/nnn/.selection resp=f chsum=md5 checksum_type() { echo "possible checksums: md5, sha1, sha224, sha256, sha384, sha512" printf "create md5 (m), sha256 (s), sha512 (S) (or type one of the above checksums) [default=m]: " read -r chsum_resp for chks in md5 sha1 sha224 sha256 sha384 sha512 do if [ "$chsum_resp" = "$chks" ]; then chsum=$chsum_resp return fi done if [ "$chsum_resp" = "s" ]; then chsum=sha256 elif [ "$chsum_resp" = "S" ]; then chsum=sha512 fi } if [ -s "$selection" ]; then printf "work with selection (s) or current file (f) [default=f]: " read -r resp fi if [ "$resp" = "s" ]; then checksum_type sed 's|'"$PWD/"'||g' < "$selection" | xargs -0 -I{} ${chsum}sum {} > "checksum_$(date '+%Y%m%d%H%M').$chsum" elif [ -n "$1" ]; then if [ -f "$1" ]; then for chks in md5 sha1 sha224 sha256 sha384 sha512 do if echo "$1" | grep -q \.${chks}$; then ${chks}sum -c < "$1" read -r _ return fi done checksum_type file=$(basename "$1").$chsum ${chsum}sum "$1" > "$file" elif [ -d "$1" ]; then checksum_type file=$(basename "$1").$chsum find "$1" -type f -exec ${chsum}sum "{}" + > "$file" fi fi nnn-3.0/plugins/diffs000077500000000000000000000023121362065605700146570ustar00rootroot00000000000000#!/usr/bin/env sh # Description: Show diff of 2 directories or multiple files in vimdiff # # Note: vim may show the warning: 'Vim: Warning: Input is not from a terminal' # press 'Enter' to ignore and proceed. # # Shell: POSIX compliant # Authors: Arun Prakash Jana, ath3 selection=${XDG_CONFIG_HOME:-$HOME/.config}/nnn/.selection if [ -s "$selection" ]; then arr=$(tr '\0' '\n' < "$selection") if [ "$(echo "$arr" | wc -l)" -gt 1 ]; then f1="$(echo "$arr" | sed -n '1p')" f2="$(echo "$arr" | sed -n '2p')" if [ -d "$f1" ] && [ -d "$f2" ]; then dir1=$(mktemp "${TMPDIR:-/tmp}"/nnn-"$(basename "$f1")".XXXXXXXX) dir2=$(mktemp "${TMPDIR:-/tmp}"/nnn-"$(basename "$f2")".XXXXXXXX) ls -A1 "$f1" > "$dir1" ls -A1 "$f2" > "$dir2" vimdiff "$dir1" "$dir2" rm "$dir1" "$dir2" else # If xargs supports the -o option, use it to get rid of: # Vim: Warning: Input is not from a terminal # xargs -0 -o vimdiff < $selection xargs -0 vimdiff +0 < "$selection" fi else echo "needs at least 2 files or directories selected for comparison" fi fi nnn-3.0/plugins/dragdrop000077500000000000000000000034411362065605700153720ustar00rootroot00000000000000#!/usr/bin/env sh # Description: Open a Drag and drop window, to drop files onto other programs. # Also provides drag and drop window for files. # # Files that are dropped will be added to nnn's selection # Some webbased files will be downloaded to current directory with curl # and it may overwrite some existing files # # The user has to mm to clear nnn's selection first # # Dependency: https://github.com/mwh/dragon # Shell: POSIX compliant # Author: 0xACE selection=${XDG_CONFIG_HOME:-$HOME/.config}/nnn/.selection resp=f all= if which dragon-drag-and-drop >/dev/null 2>&1; then dnd="dragon-drag-and-drop" else dnd="dragon" fi add_file () { printf '%s\0' "$@" >> "$selection" } use_all () { printf "mark --all (a) [default=none]: " read -r resp if [ "$resp" = "a" ]; then all="--all" else all="" fi } if [ -s "$selection" ]; then printf "Drop file (r). Drag selection (s), Drag current directory (d) or drag current file (f) [default=f]: " read -r resp else printf "Drop file (r). Drag current directory (d) or drag current file (f) [default=f]: " read -r resp if [ "$resp" = "s" ]; then resp=f fi fi if [ "$resp" = "s" ]; then use_all sed -z 's|'"$PWD/"'||g' < "$selection" | xargs -0 "$dnd" "$all" & elif [ "$resp" = "d" ]; then use_all "$dnd" "$all" "$PWD/"* & elif [ "$resp" = "r" ]; then true > "$selection" "$dnd" --print-path --target | while read -r f do if printf "%s" "$f" | grep '^\(https\?\|ftps\?\|s\?ftp\):\/\/' ; then curl -LJO "$f" add_file "$PWD/$(basename "$f")" elif [ -e "$f" ]; then add_file "$f" fi done & else if [ -n "$1" ] && [ -e "$1" ]; then "$dnd" "$1" & fi fi nnn-3.0/plugins/dups000077500000000000000000000010331362065605700145360ustar00rootroot00000000000000#!/usr/bin/env sh # Description: List non-empty duplicate files in the current directory (based on size followed by MD5) # # Source: https://www.commandlinefu.com/commands/view/3555/find-duplicate-files-based-on-size-first-then-md5-hash # # Requires: find md5sum sort uniq xargs # # Shell: POSIX compliant # Author: syssyphus find . -size +0 -type f -printf "%s\n" | sort -rn | uniq -d | xargs -I{} -n1 find -type f -size {}c -print0 | xargs -0 md5sum | sort | uniq -w32 --all-repeated=separate printf "Press any key to exit" read -r _ nnn-3.0/plugins/fzcd000077500000000000000000000012251362065605700145140ustar00rootroot00000000000000#!/usr/bin/env sh # Description: Run fzf/fzy, fd/fdfind/find and go to the directory of the file selected # # Shell: POSIX compliant # Author: Anna Arad . "$(dirname "$0")"/.nnn-plugin-helper if [ "$(cmd_exists fzy)" -eq "0" ]; then if [ "$(cmd_exists fd)" -eq "0" ]; then fd=fd elif [ "$(cmd_exists fdfind)" -eq "0" ]; then fd=fdfind else fd=find fi sel=$($fd | fzy) elif [ "$(cmd_exists fzf)" -eq "0" ]; then sel=$(fzf) else exit 1 fi if [ -n "$sel" ]; then if ! [ -d "$sel" ]; then sel=$(dirname "$sel") elif [ "$sel" = "." ]; then exit 0 fi # Remove "./" prefix if it exists sel="${sel#./}" nnn_cd "$PWD/$sel" fi nnn-3.0/plugins/fzhist000077500000000000000000000016001362065605700150720ustar00rootroot00000000000000#!/usr/bin/env sh # Description: Fuzzy find a command from history, edit in $EDITOR and run as a command # Currently supports only bash and fish history # # Shell: POSIX compliant # Author: Arun Prakash Jana if which fzf >/dev/null 2>&1; then fuzzy=fzf elif which fzy >/dev/null 2>&1; then fuzzy=fzy else exit 1 fi shellname="$(basename "$SHELL")" if [ "$shellname" = "bash" ]; then hist_file="$HOME/.bash_history" entry="$("$fuzzy" < "$hist_file")" elif [ "$shellname" = "fish" ]; then hist_file="$HOME/.config/fish/fish_history" entry="$(grep "\- cmd: " "$hist_file" | cut -c 8- | "$fuzzy")" fi if ! [ -z "$entry" ]; then tmpfile=$(mktemp) echo "$entry" >> "$tmpfile" $EDITOR "$tmpfile" if [ -s "$tmpfile" ]; then $SHELL -c "$(cat "$tmpfile")" fi rm "$tmpfile" printf "Press any key to exit" read -r _ fi nnn-3.0/plugins/fzopen000077500000000000000000000011141362065605700150640ustar00rootroot00000000000000#!/usr/bin/env sh # Description: Fuzzy find a file in directory subtree with fzy # Opens in $VISUAL or $EDITOR if text # Opens other type of files with xdg-open # # Requires: fzf/fzy, xdg-open # # Shell: POSIX compliant # Author: Arun Prakash Jana if which fzf >/dev/null 2>&1; then fuzzy=fzf elif which fzy >/dev/null 2>&1; then fuzzy=fzy else exit 1 fi entry="$(find . -type f 2>/dev/null | "$fuzzy")" case "$(file -biL "$entry")" in *text*) "${VISUAL:-$EDITOR}" "$entry" ;; *) xdg-open "$entry" >/dev/null 2>&1 ;; esac nnn-3.0/plugins/getplugs000077500000000000000000000026731362065605700154300ustar00rootroot00000000000000#!/usr/bin/env sh # Description: Update nnn plugins # # Shell: POSIX compliant # Author: Arun Prakash Jana, KlzXS CONFIG_DIR=${XDG_CONFIG_HOME:-$HOME/.config}/nnn/ PLUGIN_DIR=${XDG_CONFIG_HOME:-$HOME/.config}/nnn/plugins is_cmd_exists () { which "$1" > /dev/null 2>&1 echo $? } merge () { vimdiff +0 "$1" "$2" } prompt () { printf "%s" "Plugin $1 already exists and is different.\n" printf "Keep (k), merge (m), overwrite (o) [default: k]? " read -r operation if [ "$operation" = "m" ]; then op="merge" elif [ "$operation" = "o" ]; then op="cp -vRf" else op="true" fi } # if [ "$(is_cmd_exists sudo)" -eq "0" ]; then # sucmd=sudo # elif [ "$(is_cmd_exists doas)" -eq "0" ]; then # sucmd=doas # else # sucmd=: # noop # fi # backup any earlier plugins if [ -d "$PLUGIN_DIR" ]; then tar -C "$CONFIG_DIR" -czf "$CONFIG_DIR""plugins-$(date '+%Y%m%d%H%M').tar.gz" plugins/ fi mkdir -p "$PLUGIN_DIR" cd "$CONFIG_DIR" || exit 1 curl -Ls -O https://github.com/jarun/nnn/archive/master.tar.gz tar -zxf master.tar.gz cd nnn-master/plugins || exit 1 # shellcheck disable=SC2044 # We do not use obnoxious names for plugins for f in $(find . -maxdepth 1 \( ! -iname "." ! -iname "*.md" \)); do if [ -f ../../plugins/"$f" ]; then if [ "$(diff --brief "$f" ../../plugins/"$f")" ]; then prompt "$f" $op "$f" ../../plugins/ fi else cp -vRf "$f" ../../plugins/ fi done cd ../.. || exit 1 rm -rf nnn-master/ master.tar.gz nnn-3.0/plugins/gutenread000077500000000000000000000030531362065605700155450ustar00rootroot00000000000000#!/usr/bin/env sh # Description: Browse Project Gutenberg catalogue by popularity, then download # and read a book of your choice. # # Details: Set the variable EBOOK_ID to download in html format and read in w3m. # Clear EBOOK_ID to browse available ebooks by popularity and set it to # the ID once you find an interesting one. # To dowload and read in epub format set READER to an epub reader like # epr: https://github.com/wustho/epr # # More on EBOOK_ID: # Wuthering Heights by Emily Brontë is at https://www.gutenberg.org/ebooks/768 # So EBOOK_ID would be 768 # # Downloaded ebooks are at ${XDG_CACHE_HOME:-$HOME/.cache}/nnn/gutenbooks/ # # Shell: POSIX compliant # Author: Arun Prakash Jana EBOOK_ID= DIR="${XDG_CACHE_HOME:-$HOME/.cache}/nnn/gutenbooks/$EBOOK_ID" BROWSE_LINK="http://www.gutenberg.org/ebooks/search/?sort_order=downloads" BROWSER=w3m READER= if [ -n "$EBOOK_ID" ]; then if [ ! -e "$DIR" ]; then mkdir -p "$DIR" cd "$DIR" || exit 1 if [ -z "$READER" ]; then curl -L -O "https://www.gutenberg.org/files/$EBOOK_ID/$EBOOK_ID-h.zip" unzip "$EBOOK_ID"-h.zip else curl -L -o "$EBOOK_ID".epub "http://www.gutenberg.org/ebooks/$EBOOK_ID.epub.noimages" fi fi if [ -d "$DIR" ]; then if [ -z "$READER" ]; then "$BROWSER" "$DIR/$EBOOK_ID-h/$EBOOK_ID-h.htm" else "$READER" "$DIR/$EBOOK_ID.epub" fi fi else "$BROWSER" "$BROWSE_LINK" fi nnn-3.0/plugins/hexview000077500000000000000000000002701362065605700152440ustar00rootroot00000000000000#!/usr/bin/env sh # Description: View a file in hex # Requires: xxd and $PAGER # # Shell: POSIX compliant # Author: Arun Prakash Jana if ! [ -z "$1" ]; then xxd "$1" | $PAGER fi nnn-3.0/plugins/imgresize000077500000000000000000000014031362065605700155620ustar00rootroot00000000000000#!/usr/bin/env sh # Description: Resize images in a directory to screen resolution with imgp # imgp homepage: https://github.com/jarun/imgp # # Notes: # 1. Set res if you don't want to be prompted for desktop resolution every time # 2. minsize is set to 1MB by default, adjust it if you want # 3. imgp options used: # a - adaptive mode # c - convert PNG to JPG # k - skip images matching specified hres/vres # # Shell: POSIX compliant # Author: Arun Prakash Jana # set resolution (e.g. 1920x1080) res= # set minimum image size (in bytes) to resize (default: 1MB) minsize=1048576 if [ -z "$res" ]; then printf "desktop resolution (hxv): " read -r res fi if ! [ -z "$res" ] && ! [ -z "$minsize" ]; then imgp -ackx "$res" -s "$minsize" fi nnn-3.0/plugins/imgthumb000077500000000000000000000004661362065605700154100ustar00rootroot00000000000000#!/usr/bin/env sh # Description: View thumbnail of an image or a directory of images with lsix # # Shell: POSIX compliant # Author: Arun Prakash Jana if ! [ -z "$1" ]; then if [ -d "$1" ]; then lsix "$1"/* else lsix "$1" fi printf "Press any key to exit..." read -r _ fi nnn-3.0/plugins/imgur000077500000000000000000000504231362065605700147150ustar00rootroot00000000000000#!/usr/bin/env bash ########################################################################## # The MIT License # # Copyright (c) jomo # # Permission is hereby granted, free of charge, # to any person obtaining a copy of this software and # associated documentation files (the "Software"), to # deal in the Software without restriction, including # without limitation the rights to use, copy, modify, # merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom # the Software is furnished to do so, # subject to the following conditions: # # The above copyright notice and this permission notice # shall be included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ########################################################################## # https://github.com/jomo/imgur-screenshot # https://imgur.com/tools # # Slightly modified for `nnn` integration # # Shell: bash # Description: Upload an image file to imgur if [ "${1}" = "--debug" ]; then echo "########################################" echo "Enabling debug mode" echo "Please remove credentials before pasting" echo "########################################" echo "" uname -a for arg in ${0} "${@}"; do echo -n "'${arg}' " done echo -e "\n" shift set -x fi current_version="v1.7.4" function is_mac() { uname | grep -q "Darwin" } ### IMGUR-SCREENSHOT DEFAULT CONFIG #### # You can override the config in ~/.config/imgur-screenshot/settings.conf imgur_anon_id="ea6c0ef2987808e" imgur_icon_path="${HOME}/Pictures/imgur.png" imgur_acct_key="" imgur_secret="" login="false" album_title="" album_id="" credentials_file="${HOME}/.config/imgur-screenshot/credentials.conf" file_name_format="imgur-%Y_%m_%d-%H:%M:%S.png" # when using scrot, must end with .png! file_dir="${HOME}/Pictures" upload_connect_timeout="5" upload_timeout="120" upload_retries="1" # shellcheck disable=SC2034 if is_mac; then screenshot_select_command="screencapture -i %img" screenshot_window_command="screencapture -iWa %img" screenshot_full_command="screencapture %img" open_command="open %url" else screenshot_select_command="scrot -s %img" screenshot_window_command="scrot %img" screenshot_full_command="scrot %img" open_command="xdg-open %url" fi open="true" mode="select" edit_command="gimp %img" edit="false" exit_on_album_creation_fail="true" log_file="${HOME}/.imgur-screenshot.log" auto_delete="" copy_url="true" keep_file="true" check_update="true" # NOTICE: if you make changes here, also edit the docs at # https://github.com/jomo/imgur-screenshot/wiki/Config # You can override the config in ~/.config/imgur-screenshot/settings.conf ############## END CONFIG ############## settings_path="${HOME}/.config/imgur-screenshot/settings.conf" if [ -f "${settings_path}" ]; then source "${settings_path}" fi # dependency check if [ "${1}" = "--check" ]; then (which grep &>/dev/null && echo "OK: found grep") || echo "ERROR: grep not found" if is_mac; then if which growlnotify &>/dev/null; then echo "OK: found growlnotify" elif which terminal-notifier &>/dev/null; then echo "OK: found terminal-notifier" else echo "ERROR: growlnotify nor terminal-notifier found" fi (which screencapture &>/dev/null && echo "OK: found screencapture") || echo "ERROR: screencapture not found" (which pbcopy &>/dev/null && echo "OK: found pbcopy") || echo "ERROR: pbcopy not found" else (which notify-send &>/dev/null && echo "OK: found notify-send") || echo "ERROR: notify-send (from libnotify-bin) not found" (which scrot &>/dev/null && echo "OK: found scrot") || echo "ERROR: scrot not found" (which xclip &>/dev/null && echo "OK: found xclip") || echo "ERROR: xclip not found" fi (which curl &>/dev/null && echo "OK: found curl") || echo "ERROR: curl not found" exit 0 fi # notify <'ok'|'error'> <text> function notify() { if is_mac; then if which growlnotify &>/dev/null; then growlnotify --icon "${imgur_icon_path}" --iconpath "${imgur_icon_path}" --title "${2}" --message "${3}" else terminal-notifier -appIcon "${imgur_icon_path}" -contentImage "${imgur_icon_path}" -title "imgur: ${2}" -message "${3}" fi else if [ "${1}" = "error" ]; then notify-send -a ImgurScreenshot -u critical -c "im.error" -i "${imgur_icon_path}" -t 500 "imgur: ${2}" "${3}" else notify-send -a ImgurScreenshot -u low -c "transfer.complete" -i "${imgur_icon_path}" -t 500 "imgur: ${2}" "${3}" fi fi } function take_screenshot() { echo "Please select area" is_mac || sleep 0.1 # https://bbs.archlinux.org/viewtopic.php?pid=1246173#p1246173 cmd="screenshot_${mode}_command" cmd=${!cmd//\%img/${1}} if ! shot_err="$(${cmd} &>/dev/null)"; then #takes a screenshot with selection echo "Failed to take screenshot '${1}': '${shot_err}'. For more information visit https://github.com/jomo/imgur-screenshot/wiki/Troubleshooting" | tee -a "${log_file}" notify error "Something went wrong :(" "Information has been logged" exit 1 fi } function check_for_update() { # exit non-zero on HTTP error, output only the body (no stats) but output errors, follow redirects, output everything to stdout remote_version="$(curl --compressed -fsSL --stderr - "https://api.github.com/repos/jomo/imgur-screenshot/releases" | grep -Em 1 --color 'tag_name":\s*".*"' | cut -d '"' -f 4)" if ! [ -z "$remote_version" ]; then if [ ! "${current_version}" = "${remote_version}" ] && [ ! -z "${current_version}" ] && [ ! -z "${remote_version}" ]; then echo "Update found!" echo "Version ${remote_version} is available (You have ${current_version})" notify ok "Update found" "Version ${remote_version} is available (You have ${current_version}). https://github.com/jomo/imgur-screenshot" echo "Check https://github.com/jomo/imgur-screenshot/releases/${remote_version} for more info." elif [ -z "${current_version}" ] || [ -z "${remote_version}" ]; then echo "Invalid empty version string" echo "Current (local) version: '${current_version}'" echo "Latest (remote) version: '${remote_version}'" else echo "Version ${current_version} is up to date." fi else echo "Failed to check for latest version: ${remote_version}" fi } function check_oauth2_client_secrets() { if [ -z "${imgur_acct_key}" ] || [ -z "${imgur_secret}" ]; then echo "In order to upload to your account, register a new application at:" echo "https://api.imgur.com/oauth2/addclient" echo "Select 'OAuth 2 authorization without a callback URL'" echo "Then, set the imgur_acct_key (Client ID) and imgur_secret in your config." exit 1 fi } function load_access_token() { token_expire_time=0 # check for saved access_token and its expiration date if [ -f "${credentials_file}" ]; then source "${credentials_file}" fi current_time="$(date +%s)" preemptive_refresh_time="$((10*60))" expired="$((current_time > (token_expire_time - preemptive_refresh_time)))" if [ ! -z "${refresh_token}" ]; then # token already set if [ "${expired}" -eq "0" ]; then # token expired refresh_access_token "${credentials_file}" fi else acquire_access_token "${credentials_file}" fi } function acquire_access_token() { check_oauth2_client_secrets # prompt for a PIN authorize_url="https://api.imgur.com/oauth2/authorize?client_id=${imgur_acct_key}&response_type=pin" echo "Go to" echo "${authorize_url}" echo "and grant access to this application." read -rp "Enter the PIN: " imgur_pin if [ -z "${imgur_pin}" ]; then echo "PIN not entered, exiting" exit 1 fi # exchange the PIN for access token and refresh token response="$(curl --compressed -fsSL --stderr - \ -F "client_id=${imgur_acct_key}" \ -F "client_secret=${imgur_secret}" \ -F "grant_type=pin" \ -F "pin=${imgur_pin}" \ https://api.imgur.com/oauth2/token)" save_access_token "${response}" "${1}" } function refresh_access_token() { check_oauth2_client_secrets token_url="https://api.imgur.com/oauth2/token" # exchange the refresh token for access_token and refresh_token if ! response="$(curl --compressed -fsSL --stderr - \ -F "client_id=${imgur_acct_key}" \ -F "client_secret=${imgur_secret}" \ -F "grant_type=refresh_token" \ -F "refresh_token=${refresh_token}" \ "${token_url}" )"; then # curl failed handle_upload_error "${response}" "${token_url}" exit 1 fi save_access_token "${response}" "${1}" } function save_access_token() { if ! grep -q "access_token" <<<"${1}"; then # server did not send access_token echo "Error: Something is wrong with your credentials:" echo "${1}" exit 1 fi access_token="$(grep -Eo 'access_token":".*"' <<<"${1}" | cut -d '"' -f 3)" refresh_token="$(grep -Eo 'refresh_token":".*"' <<<"${1}" | cut -d '"' -f 3)" expires_in="$(grep -Eo 'expires_in":[0-9]*' <<<"${1}" | cut -d ':' -f 2)" token_expire_time="$(( $(date +%s) + expires_in ))" # create dir if not exist mkdir -p "$(dirname "${2}")" 2>/dev/null touch "${2}" && chmod 600 "${2}" cat <<EOF > "${2}" access_token="${access_token}" refresh_token="${refresh_token}" token_expire_time="${token_expire_time}" EOF } function fetch_account_info() { response="$(curl --compressed --connect-timeout "${upload_connect_timeout}" -m "${upload_timeout}" --retry "${upload_retries}" -fsSL --stderr - -H "Authorization: Bearer ${access_token}" https://api.imgur.com/3/account/me)" if grep -Eq '"success":\s*true' <<<"${response}"; then username="$(grep -Eo '"url":\s*"[^"]+"' <<<"${response}" | cut -d "\"" -f 4)" echo "Logged in as ${username}." echo "https://${username}.imgur.com" else echo "Failed to fetch info: ${response}" fi } function delete_image() { response="$(curl --compressed -X DELETE -fsSL --stderr - -H "Authorization: Client-ID ${1}" "https://api.imgur.com/3/image/${2}")" if grep -Eq '"success":\s*true' <<<"${response}"; then echo "Image successfully deleted (delete hash: ${2})." >> "${3}" else echo "The Image could not be deleted: ${response}." >> "${3}" fi } function upload_authenticated_image() { echo "Uploading '${1}'..." title="$(echo "${1}" | rev | cut -d "/" -f 1 | cut -d "." -f 2- | rev)" if [ -n "${album_id}" ]; then response="$(curl --compressed --connect-timeout "${upload_connect_timeout}" -m "${upload_timeout}" --retry "${upload_retries}" -fsSL --stderr - -F "title=${title}" -F "image=@\"${1}\"" -F "album=${album_id}" -H "Authorization: Bearer ${access_token}" https://api.imgur.com/3/image)" else response="$(curl --compressed --connect-timeout "${upload_connect_timeout}" -m "${upload_timeout}" --retry "${upload_retries}" -fsSL --stderr - -F "title=${title}" -F "image=@\"${1}\"" -H "Authorization: Bearer ${access_token}" https://api.imgur.com/3/image)" fi # JSON parser premium edition (not really) if grep -Eq '"success":\s*true' <<<"${response}"; then img_id="$(grep -Eo '"id":\s*"[^"]+"' <<<"${response}" | cut -d "\"" -f 4)" img_ext="$(grep -Eo '"link":\s*"[^"]+"' <<<"${response}" | cut -d "\"" -f 4 | rev | cut -d "." -f 1 | rev)" # "link" itself has ugly '\/' escaping and no https! del_id="$(grep -Eo '"deletehash":\s*"[^"]+"' <<<"${response}" | cut -d "\"" -f 4)" if [ ! -z "${auto_delete}" ]; then export -f delete_image echo "Deleting image in ${auto_delete} seconds." nohup /bin/bash -c "sleep ${auto_delete} && delete_image ${imgur_anon_id} ${del_id} ${log_file}" & fi handle_upload_success "https://i.imgur.com/${img_id}.${img_ext}" "https://imgur.com/delete/${del_id}" "${1}" else # upload failed err_msg="$(grep -Eo '"error":\s*"[^"]+"' <<<"${response}" | cut -d "\"" -f 4)" test -z "${err_msg}" && err_msg="${response}" handle_upload_error "${err_msg}" "${1}" fi } function upload_anonymous_image() { echo "Uploading '${1}'..." title="$(echo "${1}" | rev | cut -d "/" -f 1 | cut -d "." -f 2- | rev)" if [ -n "${album_id}" ]; then response="$(curl --compressed --connect-timeout "${upload_connect_timeout}" -m "${upload_timeout}" --retry "${upload_retries}" -fsSL --stderr - -H "Authorization: Client-ID ${imgur_anon_id}" -F "title=${title}" -F "image=@\"${1}\"" -F "album=${album_id}" https://api.imgur.com/3/image)" else response="$(curl --compressed --connect-timeout "${upload_connect_timeout}" -m "${upload_timeout}" --retry "${upload_retries}" -fsSL --stderr - -H "Authorization: Client-ID ${imgur_anon_id}" -F "title=${title}" -F "image=@\"${1}\"" https://api.imgur.com/3/image)" fi # JSON parser premium edition (not really) if grep -Eq '"success":\s*true' <<<"${response}"; then img_id="$(grep -Eo '"id":\s*"[^"]+"' <<<"${response}" | cut -d "\"" -f 4)" img_ext="$(grep -Eo '"link":\s*"[^"]+"' <<<"${response}" | cut -d "\"" -f 4 | rev | cut -d "." -f 1 | rev)" # "link" itself has ugly '\/' escaping and no https! del_id="$(grep -Eo '"deletehash":\s*"[^"]+"' <<<"${response}" | cut -d "\"" -f 4)" if [ ! -z "${auto_delete}" ]; then export -f delete_image echo "Deleting image in ${auto_delete} seconds." nohup /bin/bash -c "sleep ${auto_delete} && delete_image ${imgur_anon_id} ${del_id} ${log_file}" & fi handle_upload_success "https://i.imgur.com/${img_id}.${img_ext}" "https://imgur.com/delete/${del_id}" "${1}" else # upload failed err_msg="$(grep -Eo '"error":\s*"[^"]+"' <<<"${response}" | cut -d "\"" -f 4)" test -z "${err_msg}" && err_msg="${response}" handle_upload_error "${err_msg}" "${1}" fi } function handle_upload_success() { echo "" echo "image link: ${1}" echo "delete link: ${2}" if [ "${copy_url}" = "true" ] && [ -z "${album_title}" ]; then if is_mac; then echo -n "${1}" | pbcopy else echo -n "${1}" | xclip -selection clipboard fi echo "URL copied to clipboard" fi # print to log file: image link, image location, delete link echo -e "${1}\t${3}\t${2}" >> "${log_file}" notify ok "Upload done!" "${1}" # if [ ! -z "${open_command}" ] && [ "${open}" = "true" ]; then # open_cmd=${open_command//\%url/${1}} # open_cmd=${open_cmd//\%img/${2}} # echo "Opening '${open_cmd}'" # eval "${open_cmd}" # fi } function handle_upload_error() { error="Upload failed: \"${1}\"" echo "${error}" echo -e "Error\t${2}\t${error}" >> "${log_file}" notify error "Upload failed :(" "${1}" } function handle_album_creation_success() { echo "" echo "Album link: ${1}" echo "Delete hash: ${2}" echo "" notify ok "Album created!" "${1}" if [ "${copy_url}" = "true" ]; then if is_mac; then echo -n "${1}" | pbcopy else echo -n "${1}" | xclip -selection clipboard fi echo "URL copied to clipboard" fi # print to log file: album link, album title, delete hash echo -e "${1}\t\"${3}\"\t${2}" >> "${log_file}" } function handle_album_creation_error() { error="Album creation failed: \"${1}\"" echo -e "Error\t${2}\t${error}" >> "${log_file}" notify error "Album creation failed :(" "${1}" if [ ${exit_on_album_creation_fail} ]; then exit 1 fi } while [ ${#} != 0 ]; do case "${1}" in -h | --help) echo "usage: ${0} [--debug] [-c | --check | -v | -h | -u]" echo " ${0} [--debug] [option]... [file]..." echo "" echo " --debug Enable debugging, must be first option" echo " -h, --help Show this help, exit" echo " -v, --version Show current version, exit" echo " --check Check if all dependencies are installed, exit" echo " -c, --connect Show connected imgur account, exit" echo " -o, --open <true|false> Override 'open' config" echo " -e, --edit <true|false> Override 'edit' config" echo " -i, --edit-command <command> Override 'edit_command' config (include '%img'), sets --edit 'true'" echo " -l, --login <true|false> Override 'login' config" echo " -a, --album <album_title> Create new album and upload there" echo " -A, --album-id <album_id> Override 'album_id' config" echo " -k, --keep-file <true|false> Override 'keep_file' config" echo " -d, --auto-delete <s> Automatically delete image after <s> seconds" echo " -u, --update Check for updates, exit" echo " file Upload file instead of taking a screenshot" exit 0;; -v | --version) echo "${current_version}" exit 0;; -s | --select) mode="select" shift;; -w | --window) mode="window" shift;; -f | --full) mode="full" shift;; -o | --open) # shellcheck disable=SC2034 open="${2}" shift 2;; -e | --edit) edit="${2}" shift 2;; -i | --edit-command) edit_command="${2}" edit="true" shift 2;; -l | --login) login="${2}" shift 2;; -c | --connect) load_access_token fetch_account_info exit 0;; -a | --album) album_title="${2}" shift 2;; -A | --album-id) album_id="${2}" shift 2;; -k | --keep-file) keep_file="${2}" shift 2;; -d | --auto-delete) auto_delete="${2}" shift 2;; -u | --update) check_for_update exit 0;; *) upload_files=("${@}") break;; esac done if [ "${login}" = "true" ]; then # load before changing directory load_access_token fi if [ -n "${album_title}" ]; then if [ "${login}" = "true" ]; then response="$(curl -fsSL --stderr - \ -F "title=${album_title}" \ -H "Authorization: Bearer ${access_token}" \ https://api.imgur.com/3/album)" else response="$(curl -fsSL --stderr - \ -F "title=${album_title}" \ -H "Authorization: Client-ID ${imgur_anon_id}" \ https://api.imgur.com/3/album)" fi if grep -Eq '"success":\s*true' <<<"${response}"; then # Album creation successful echo "Album '${album_title}' successfully created" album_id="$(grep -Eo '"id":\s*"[^"]+"' <<<"${response}" | cut -d "\"" -f 4)" del_id="$(grep -Eo '"deletehash":\s*"[^"]+"' <<<"${response}" | cut -d "\"" -f 4)" handle_album_creation_success "http://imgur.com/a/${album_id}" "${del_id}" "${album_title}" if [ "${login}" = "false" ]; then album_id="${del_id}" fi else # Album creation failed err_msg="$(grep -Eo '"error":\s*"[^"]+"' <<<"${response}" | cut -d "\"" -f 4)" test -z "${err_msg}" && err_msg="${response}" handle_album_creation_error "${err_msg}" "${album_title}" fi fi if [ -z "${upload_files[*]}" ]; then upload_files[0]="" fi for upload_file in "${upload_files[@]}"; do if [ -z "${upload_file}" ]; then cd "${file_dir}" || exit 1 # new filename with date img_file="$(date +"${file_name_format}")" take_screenshot "${img_file}" else # upload file instead of screenshot img_file="${upload_file}" fi # get full path #cd "$(dirname "$(realpath "${img_file}")")" #img_file="$(realpath "${img_file}")" # check if file exists if ! [ -f "${img_file}" ]; then echo "file '${img_file}' doesn't exist !" read -r _ exit 1 fi # open image in editor if configured if [ "${edit}" = "true" ]; then edit_cmd=${edit_command//\%img/${img_file}} echo "Opening editor '${edit_cmd}'" if ! (eval "${edit_cmd}"); then echo "Error for image '${img_file}': command '${edit_cmd}' failed, not uploading. For more information visit https://github.com/jomo/imgur-screenshot/wiki/Troubleshooting" | tee -a "${log_file}" notify error "Something went wrong :(" "Information has been logged" exit 1 fi fi if [ "${login}" = "true" ]; then upload_authenticated_image "${img_file}" else upload_anonymous_image "${img_file}" fi # delete file if configured if [ "${keep_file}" = "false" ] && [ -z "${1}" ]; then echo "Deleting temp file ${file_dir}/${img_file}" rm -rf "${img_file}" fi echo "" done if [ "${check_update}" = "true" ]; then check_for_update fi read -r _ ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������nnn-3.0/plugins/imgview�����������������������������������������������������������������������������0000775�0000000�0000000�00000001153�13620656057�0015235�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env sh # Description: Open images in hovered directory and thumbnails # open hovered image in sxiv or viu and browse other images in the directory # # Shell: POSIX compliant # Author: Arun Prakash Jana if [ -z "$1" ] || ! [ -s "$1" ]; then printf "empty file" read -r _ exit 1 fi if command -v sxiv >/dev/null 2>&1; then if [ -f "$1" ]; then sxiv -q "$1" "$PWD" elif [ -d "$1" ] || [ -h "$1" ]; then sxiv -qt "$1" fi elif command -v viu >/dev/null 2>&1; then viu -n "$1" | less -R else printf "install sxiv or viu" read -r _ exit 2 fi ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������nnn-3.0/plugins/ipinfo������������������������������������������������������������������������������0000775�0000000�0000000�00000000372�13620656057�0015054�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env sh # Description: Shows the external IP address and whois information. Useful over VPNs. # # Shell: POSIX compliant # Author: Arun Prakash Jana IP=$(curl -s ifconfig.me) whois "$IP" echo your external IP address is "$IP" read -r _ ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������nnn-3.0/plugins/kdeconnect��������������������������������������������������������������������������0000775�0000000�0000000�00000001175�13620656057�0015707�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env sh # Description: Send the selected files to your Android device using kdeconnect-cli. You must have installed and configured kdeconnect both on the Android device and on the PC. # # Shell: POSIX compliant # Author: juacq97 SELECTION=${XDG_CONFIG_HOME:-$HOME/.config}/nnn/.selection id=$(kdeconnect-cli -a --id-only | awk '{print $1}') if [ "$(find "$SELECTION")" ]; then kdeconnect-cli -d "$id" --share "$(cat "$SELECTION")" # If you want a system notification, uncomment the next 3 lines. # notify-send -a "Kdeconnect" "Sending $(cat "$SELECTION")" #else # notify-send -a "Kdeconnect" "No file selected" fi ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������nnn-3.0/plugins/launch������������������������������������������������������������������������������0000775�0000000�0000000�00000002174�13620656057�0015044�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env sh # Description: Independent POSIX-compliant GUI application launcher. # Fuzzy find executables in $PATH and launch an application. # stdin, stdout, stderr are suppressed so CLI tools exit silently. # # To configure launch as an independent app launcher add a keybind # to open launch in a terminal e.g., # # xfce4-terminal -e "${XDG_CONFIG_HOME:-$HOME/.config}/nnn/plugins/launch # # Requires: fzf/fzy # # Usage: launch [delay] # delay is in seconds, if omitted launch waits for 1 sec # # Integration with nnn: launch is installed with other plugins, nnn picks it up. # # Shell: POSIX compliant # Author: Arun Prakash Jana # shellcheck disable=SC2086 IFS=':' get_selection() { if which fzf >/dev/null 2>&1; then { IFS=':'; ls -H $PATH; } | sort | fzf elif which fzy >/dev/null 2>&1; then { IFS=':'; ls -H $PATH; } | sort | fzy else exit 1 fi } if selection=$( get_selection ); then setsid "$selection" 2>/dev/null 1>/dev/null & if ! [ -z "$1" ]; then sleep "$1" else sleep 1 fi fi ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������nnn-3.0/plugins/mediainf����������������������������������������������������������������������������0000775�0000000�0000000�00000000376�13620656057�0015350�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env sh # Description: Show media information of a file in pager # # Requires: mediainfo # # Shell: POSIX compliant # Author: Arun Prakash Jana if ! [ -z "$1" ] && [ -f "$1" ]; then mediainfo "$1" | $PAGER # exiftool "$1" | $PAGER fi ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������nnn-3.0/plugins/moclyrics���������������������������������������������������������������������������0000775�0000000�0000000�00000002064�13620656057�0015574�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env sh # Description: Fetches the lyrics of the track currently playing in MOC # Requires ddgr (https://github.com/jarun/ddgr) # # Shell: POSIX compliant # Author: Arun Prakash Jana # Check if MOC server is running cmd=$(pgrep -x mocp 2>/dev/null) ret=$cmd if [ -z "$ret" ]; then exit fi # Grab the output out="$(mocp -i)" # Check if anything is playing state=$(echo "$out" | grep "State:" | cut -d' ' -f2) if ! [ "$state" = 'PLAY' ]; then exit fi # Try by Artist and Song Title first ARTIST="$(echo "$out" | grep 'Artist:' | cut -d':' -f2 | sed 's/^[[:blank:]]*//;s/[[:blank:]]*$//')" TITLE="$(echo "$out" | grep 'SongTitle:' | cut -d':' -f2 | sed 's/^[[:blank:]]*//;s/[[:blank:]]*$//')" if ! [ -z "$ARTIST" ] && ! [ -z "$TITLE" ]; then ddgr -w azlyrics.com --ducky "$ARTIST" "$TITLE" else # Try by file name FILENAME="$(basename "$(echo "$out" | grep 'File:' | cut -d':' -f2)")" FILENAME="$(echo "${FILENAME%%.*}" | tr -d -)" if ! [ -z "$FILENAME" ]; then ddgr -w azlyrics.com --ducky "$FILENAME" fi fi ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������nnn-3.0/plugins/mocplay�����������������������������������������������������������������������������0000775�0000000�0000000�00000004151�13620656057�0015233�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env sh # Description: Appends and optionally plays music in MOC # # Notes: # - if selection is available, plays it, else plays the current file or directory # - appends tracks and exits is MOC is running, else clears playlist and adds tracks # - to randomize the order of files appended to the playlist, set SHUFFLE=1 # if you add a directory with many files when SHUFFLE=1 is set, it might take a very long time to finish! # # Shell: POSIX compliant # Author: Arun Prakash Jana, ath3 IFS="$(printf '\n\r')" selection=${XDG_CONFIG_HOME:-$HOME/.config}/nnn/.selection cmd=$(pgrep -x mocp 2>/dev/null) ret=$cmd SHUFFLE=0 mocp_add () { if [ $SHUFFLE = 1 ]; then if [ "$resp" = "y" ]; then arr=$(tr '\0' '\n' < "$selection") elif [ -n "$1" ]; then arr="$1" fi for entry in $arr do if [ -d "$entry" ]; then arr2=$arr2$(find "$entry" -type f) else arr2=$(printf "%s\n%s" "$entry" "$arr2") fi done arr2=$(echo "$arr2" | awk 'BEGIN{srand();}{print rand()"\t"$0}' | sort -k1 -n | cut -f2-) for entry in $arr2 do if [ -f "$entry" ] && echo "$entry" | grep -qv '\.m3u$\|\.pls$' ; then mocp -a "$entry" fi done else if [ "$resp" = "y" ]; then xargs < "$selection" -0 mocp -a else mocp -a "$1" fi fi } if [ ! -s "$selection" ] && [ -z "$1" ]; then exit fi if [ -s "$selection" ]; then printf "Work with selection? Enter 'y' to confirm: " read -r resp fi if [ -z "$ret" ]; then # mocp not running mocp -S else # mocp running, check if it's playing state=$(mocp -i | grep "State:" | cut -d' ' -f2) if [ "$state" = 'PLAY' ]; then # add to playlist and exit mocp_add "$1" # uncomment the line below to show mocp interface after appending # mocp exit fi fi # clear selection and play mocp -c mocp_add "$1" "$resp" mocp -p # uncomment the line below to show mocp interface after appending # mocp �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������nnn-3.0/plugins/nmount������������������������������������������������������������������������������0000775�0000000�0000000�00000002652�13620656057�0015113�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env sh # Description: Toggle mount status of a device using pmount # If the device is not mounted, it will be mounted. # If the device is mounted, it will be unmounted and powered down. # # Runs `lsblk` if 'l' is entered, exits on 'Return`. # # Note: # - The script uses Linux-specific lsblk to list block devices. Alternatives: # macOS: "diskutil list" # BSD: "geom disk list" # - The script uses udisksctl (from udisks2) to pwoer down devices. This is also Linux-specific. # Users on non-Linux platforms can comment it and use an alterntive to power-down disks. # # Shell: POSIX compliant # Author: Arun Prakash Jana prompt="device name ['l' lists]: " lsblk printf "\nEnsure you aren't still in the mounted device.\n" printf "%s" "$prompt" read -r dev while ! [ -z "$dev" ] do if [ "$dev" = "l" ]; then lsblk elif [ "$dev" = "q" ]; then exit else if grep -qs "$dev " /proc/mounts; then sync if pumount "$dev" then echo "$dev" unmounted. if udisksctl power-off -b /dev/"$dev" then echo "$dev" ejected. fi fi else pmount "$dev" echo "$dev" mounted to "$(lsblk -n /dev/"$dev" | rev | cut -d' ' -f1 | rev)". fi fi echo printf "%s" "$prompt" read -r dev done ��������������������������������������������������������������������������������������nnn-3.0/plugins/nuke��������������������������������������������������������������������������������0000775�0000000�0000000�00000036370�13620656057�0014541�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env sh # ############################################################################# # Description: Sample script to play files in apps by file type or mime # # Shell: POSIX compliant # Usage: nuke filepath # # Integration with nnn: # 1. Export the required config: # export NNN_OPENER=/absolute/path/to/nuke # # Otherwise, if nuke is in $PATH # # export NNN_OPENER=nuke # 2. Run nnn with the program option to indicate a CLI opener # nnn -c # # The -c program option overrides option -e # 3. nuke can use nnn plugins (e.g. mocplay is used for audio), $PATH is updated. # # Details: # Inspired by ranger's scope.sh, modified for usage with nnn. # # Guards against accidentally opening mime types like executables, shared libs etc. # # Tries to play 'file' (1st argument) in the following order: # i. by extension # ii. by mime (image, video, audio, pdf) # iii. by mime (other file types) # # Modification tips: # 1. Invokes CLI utilities by default. Set GUI to 1 to enable GUI apps. # 2. PAGER is "less -R". # 3. Start GUI apps in bg to unblock. Redirect stdout and strerr if required. # 4. Some CLI utilities are piped to the $PAGER, to wait and quit uniformly. # 5. If the output cannot be paged use "read -r _" to wait for user input. # 6. On a DE, try 'xdg-open' in handle_fallback() as last resort. # # Feel free to change the utilities to your favourites and add more mimes. # # Defaults: # By extension (only the enbaled ones): # most archives: list with atool, bsdtar # rar: list with unrar # 7-zip: list with 7z # pdf: zathura (GUI), pdftotext, mutool, exiftool # audio: mocplay (nnn plugin using MOC), mpv, mediainfo, exiftool # avi|dat|mkv|mp4: smplayer, mpv (GUI), ffmpegthumbnailer, mediainfo, exiftool # torrent: rtorrent, transmission-show # odt|ods|odp|sxw: odt2txt # md: glow (https://github.com/charmbracelet/glow) # htm|html|xhtml: w3m, lynx, elinks # json: jq, python (json.tool module) # Multimedia by mime: # image/*: sxiv (GUI), viu, img2txt, exiftool # video/*: smplayer, mpv (GUI), ffmpegthumbnailer, mediainfo, exiftool # audio/*: mocplay (nnn plugin using MOC), mpv, mediainfo, exiftool # application/pdf: zathura (GUI), pdftotext, mutool, exiftool # Other mimes: # text/troff: man -l # text/* | */xml: vi # image/vnd.djvu): djvutxt, exiftool # # ToDo: # 1. Adapt, test and enable all mimes # 2. Clean-up unnecessary the exit codes # ############################################################################# # set to 1 to enable GUI apps GUI=0 set -euf -o noclobber -o noglob -o nounset IFS="$(printf '%b_' '\n')"; IFS="${IFS%_}" # protect trailing \n PATH=$PATH:"${XDG_CONFIG_HOME:-$HOME/.config}/nnn/plugins" IMAGE_CACHE_PATH="$(dirname "$1")"/.thumbs FPATH="$1" FNAME=$(basename "$1") ext="${FNAME##*.}" if ! [ -z "$ext" ]; then ext="$(printf "%s" "${ext}" | tr '[:upper:]' '[:lower:]')" fi handle_pdf() { if [ $GUI -ne 0 ] && which zathura >/dev/null 2>&1; then zathura "${FPATH}" >/dev/null 2>&1 & exit 0 elif which pdftotext >/dev/null 2>&1; then ## Preview as text conversion pdftotext -l 10 -nopgbrk -q -- "${FPATH}" - | less -R exit 0 elif which mutool >/dev/null 2>&1; then mutool draw -F txt -i -- "${FPATH}" 1-10 exit 0 elif which exiftool >/dev/null 2>&1; then exiftool "${FPATH}" | less -R exit 0 fi } handle_audio() { if which mocp >/dev/null 2>&1; then mocplay "${FPATH}" >/dev/null 2>&1 exit 0 elif which mpv >/dev/null 2>&1; then mpv "${FPATH}" >/dev/null 2>&1 & exit 0 elif which mediainfo >/dev/null 2>&1; then mediainfo "${FPATH}" | less -R exit 0 elif which exiftool >/dev/null 2>&1; then exiftool "${FPATH}"| less -R exit 0 fi } handle_video() { if [ $GUI -ne 0 ] && which smplayer >/dev/null 2>&1; then smplayer "${FPATH}" >/dev/null 2>&1 & exit 0 elif [ $GUI -ne 0 ] && which mpv >/dev/null 2>&1; then mpv "${FPATH}" >/dev/null 2>&1 & exit 0 elif which ffmpegthumbnailer >/dev/null 2>&1; then # Thumbnail [ -d "${IMAGE_CACHE_PATH}" ] || mkdir "${IMAGE_CACHE_PATH}" ffmpegthumbnailer -i "${FPATH}" -o "${IMAGE_CACHE_PATH}/${FNAME}.jpg" -s 0 viu -n "${IMAGE_CACHE_PATH}/${FNAME}.jpg" | less -R exit 0 elif which mediainfo >/dev/null 2>&1; then mediainfo "${FPATH}" | less -R exit 0 elif which exiftool >/dev/null 2>&1; then exiftool "${FPATH}"| less -R exit 0 fi } # handle this extension and exit handle_extension() { case "${ext}" in ## Archive a|ace|alz|arc|arj|bz|bz2|cab|cpio|deb|gz|jar|lha|lz|lzh|lzma|lzo|\ rpm|rz|t7z|tar|tbz|tbz2|tgz|tlz|txz|tZ|tzo|war|xpi|xz|Z|zip) if which atool >/dev/null 2>&1; then atool --list -- "${FPATH}" | less -R exit 0 elif which bsdtar >/dev/null 2>&1; then bsdtar --list --file "${FPATH}" | less -R exit 0 fi exit 1;; rar) if which unrar >/dev/null 2>&1; then ## Avoid password prompt by providing empty password unrar lt -p- -- "${FPATH}" | less -R fi exit 1;; 7z) if which 7z >/dev/null 2>&1; then ## Avoid password prompt by providing empty password 7z l -p -- "${FPATH}" | less -R exit 0 fi exit 1;; ## PDF pdf) handle_pdf exit 1;; ## Audio aac|flac|m4a|mid|midi|mpa|mp2|mp3|ogg|wav|wma) handle_audio exit 1;; ## Video avi|dat|mkv|mp4) handle_video exit 1;; ## BitTorrent torrent) if which rtorrent >/dev/null 2>&1; then rtorrent "${FPATH}" exit 0 elif which transmission-show >/dev/null 2>&1; then transmission-show -- "${FPATH}" exit 0 fi exit 1;; ## OpenDocument odt|ods|odp|sxw) if which odt2txt >/dev/null 2>&1; then ## Preview as text conversion odt2txt "${FPATH}" | less -R exit 0 fi exit 1;; ## Markdown md) if which glow >/dev/null 2>&1; then glow -sdark "${FPATH}" | less -R exit 0 fi ;; ## HTML htm|html|xhtml) ## Preview as text conversion if which w3m >/dev/null 2>&1; then w3m -dump "${FPATH}" | less -R exit 0 elif which lynx >/dev/null 2>&1; then lynx -dump -- "${FPATH}" | less -R exit 0 elif which elinks >/dev/null 2>&1; then elinks -dump "${FPATH}" | less -R exit 0 fi ;; ## JSON json) if which jq >/dev/null 2>&1; then jq --color-output . "${FPATH}" | less -R exit 0 elif which python >/dev/null 2>&1; then python -m json.tool -- "${FPATH}" | less -R exit 0 fi ;; esac } handle_multimedia() { ## Size of the preview if there are multiple options or it has to be ## rendered from vector graphics. If the conversion program allows ## specifying only one dimension while keeping the aspect ratio, the width ## will be used. # local DEFAULT_SIZE="1920x1080" mimetype="${1}" case "${mimetype}" in ## SVG # image/svg+xml|image/svg) # convert -- "${FPATH}" "${IMAGE_CACHE_PATH}" && exit 6 # exit 1;; ## DjVu # image/vnd.djvu) # ddjvu -format=tiff -quality=90 -page=1 -size="${DEFAULT_SIZE}" \ # - "${IMAGE_CACHE_PATH}" < "${FPATH}" \ # && exit 6 || exit 1;; ## Image image/*) if [ $GUI -ne 0 ] && which sxiv >/dev/null 2>&1; then sxiv -q "${FPATH}" & exit 0 elif which viu >/dev/null 2>&1; then viu -n "${FPATH}" | less -R exit 0 elif which img2txt >/dev/null 2>&1; then img2txt --gamma=0.6 -- "${FPATH}" | less -R exit 0 elif which exiftool >/dev/null 2>&1; then exiftool "${FPATH}" | less -R exit 0 fi # local orientation # orientation="$( identify -format '%[EXIF:Orientation]\n' -- "${FPATH}" )" ## If orientation data is present and the image actually ## needs rotating ("1" means no rotation)... # if [[ -n "$orientation" && "$orientation" != 1 ]]; then ## ...auto-rotate the image according to the EXIF data. # convert -- "${FPATH}" -auto-orient "${IMAGE_CACHE_PATH}" && exit 6 # fi ## `w3mimgdisplay` will be called for all images (unless overriden ## as above), but might fail for unsupported types. exit 7;; ## PDF application/pdf) handle_pdf exit 1;; ## Audio audio/*) handle_audio exit 1;; ## Video video/*) handle_video exit 1;; # pdftoppm -f 1 -l 1 \ # -scale-to-x "${DEFAULT_SIZE%x*}" \ # -scale-to-y -1 \ # -singlefile \ # -jpeg -tiffcompression jpeg \ # -- "${FPATH}" "${IMAGE_CACHE_PATH%.*}" \ # && exit 6 || exit 1;; ## ePub, MOBI, FB2 (using Calibre) # application/epub+zip|application/x-mobipocket-ebook|\ # application/x-fictionbook+xml) # # ePub (using https://github.com/marianosimone/epub-thumbnailer) # epub-thumbnailer "${FPATH}" "${IMAGE_CACHE_PATH}" \ # "${DEFAULT_SIZE%x*}" && exit 6 # ebook-meta --get-cover="${IMAGE_CACHE_PATH}" -- "${FPATH}" \ # >/dev/null && exit 6 # exit 1;; ## Font # application/font*|application/*opentype) # preview_png="/tmp/$(basename "${IMAGE_CACHE_PATH%.*}").png" # if fontimage -o "${preview_png}" \ # --pixelsize "120" \ # --fontname \ # --pixelsize "80" \ # --text " ABCDEFGHIJKLMNOPQRSTUVWXYZ " \ # --text " abcdefghijklmnopqrstuvwxyz " \ # --text " 0123456789.:,;(*!?') ff fl fi ffi ffl " \ # --text " The quick brown fox jumps over the lazy dog. " \ # "${FPATH}"; # then # convert -- "${preview_png}" "${IMAGE_CACHE_PATH}" \ # && rm "${preview_png}" \ # && exit 6 # else # exit 1 # fi # ;; ## Preview archives using the first image inside. ## (Very useful for comic book collections for example.) # application/zip|application/x-rar|application/x-7z-compressed|\ # application/x-xz|application/x-bzip2|application/x-gzip|application/x-tar) # local fn=""; local fe="" # local zip=""; local rar=""; local tar=""; local bsd="" # case "${mimetype}" in # application/zip) zip=1 ;; # application/x-rar) rar=1 ;; # application/x-7z-compressed) ;; # *) tar=1 ;; # esac # { [ "$tar" ] && fn=$(tar --list --file "${FPATH}"); } || \ # { fn=$(bsdtar --list --file "${FPATH}") && bsd=1 && tar=""; } || \ # { [ "$rar" ] && fn=$(unrar lb -p- -- "${FPATH}"); } || \ # { [ "$zip" ] && fn=$(zipinfo -1 -- "${FPATH}"); } || return # # fn=$(echo "$fn" | python -c "import sys; import mimetypes as m; \ # [ print(l, end='') for l in sys.stdin if \ # (m.guess_type(l[:-1])[0] or '').startswith('image/') ]" |\ # sort -V | head -n 1) # [ "$fn" = "" ] && return # [ "$bsd" ] && fn=$(printf '%b' "$fn") # # [ "$tar" ] && tar --extract --to-stdout \ # --file "${FPATH}" -- "$fn" > "${IMAGE_CACHE_PATH}" && exit 6 # fe=$(echo -n "$fn" | sed 's/[][*?\]/\\\0/g') # [ "$bsd" ] && bsdtar --extract --to-stdout \ # --file "${FPATH}" -- "$fe" > "${IMAGE_CACHE_PATH}" && exit 6 # [ "$bsd" ] || [ "$tar" ] && rm -- "${IMAGE_CACHE_PATH}" # [ "$rar" ] && unrar p -p- -inul -- "${FPATH}" "$fn" > \ # "${IMAGE_CACHE_PATH}" && exit 6 # [ "$zip" ] && unzip -pP "" -- "${FPATH}" "$fe" > \ # "${IMAGE_CACHE_PATH}" && exit 6 # [ "$rar" ] || [ "$zip" ] && rm -- "${IMAGE_CACHE_PATH}" # ;; esac } handle_mime() { mimetype="${1}" case "${mimetype}" in ## Manpages text/troff) man -l "${FPATH}" exit 0;; ## Text text/* | */xml) vi "${FPATH}" exit 0;; ## Syntax highlight # if [[ "$( stat --printf='%s' -- "${FPATH}" )" -gt "${HIGHLIGHT_SIZE_MAX}" ]]; then # exit 2 # fi # if [[ "$( tput colors )" -ge 256 ]]; then # local pygmentize_format='terminal256' # local highlight_format='xterm256' # else # local pygmentize_format='terminal' # local highlight_format='ansi' # fi # env HIGHLIGHT_OPTIONS="${HIGHLIGHT_OPTIONS}" highlight \ # --out-format="${highlight_format}" \ # --force -- "${FPATH}" && exit 5 # pygmentize -f "${pygmentize_format}" -O "style=${PYGMENTIZE_STYLE}"\ # -- "${FPATH}" && exit 5 # exit 2;; ## DjVu image/vnd.djvu) if which djvutxt >/dev/null 2>&1; then ## Preview as text conversion (requires djvulibre) djvutxt "${FPATH}" | less -R exit 0 elif which exiftool >/dev/null 2>&1; then exiftool "${FPATH}" | less -R exit 0 fi exit 1;; esac } handle_fallback() { if [ $GUI -ne 0 ]; then xdg-open "${FPATH}" >/dev/null 2>&1 & exit 0 fi echo '----- File details -----' && file --dereference --brief -- "${FPATH}" exit 1 } handle_blocked() { case "${MIMETYPE}" in application/x-sharedlib) exit 0;; application/x-shared-library-la) exit 0;; application/x-executable) exit 0;; application/x-shellscript) exit 0;; esac } MIMETYPE="$( file --dereference --brief --mime-type -- "${FPATH}" )" handle_blocked "${MIMETYPE}" handle_extension handle_multimedia "${MIMETYPE}" handle_mime "${MIMETYPE}" handle_fallback exit 1 ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������nnn-3.0/plugins/oldbigfile��������������������������������������������������������������������������0000775�0000000�0000000�00000000476�13620656057�0015675�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env sh # Description: List files bigger than input size by ascending access date. # # Requires: find sort # # Shell: POSIX compliant # Author: Arun Prakash Jana printf "Min file size (MB): " read -r size find . -size +"$size"M -type f -printf '%A+ %s %p\n' | sort echo "Press any key to exit" read -r _ ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������nnn-3.0/plugins/organize����������������������������������������������������������������������������0000775�0000000�0000000�00000002565�13620656057�0015414�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env sh # Description: Organize files in directories by category # # Shell: POSIX compliant # Author: th3lusive organize() { case "$(file -biL "$1")" in *video*) [ ! -d "Videos" ] && mkdir "Videos" mv "$1" "Videos/$1" printf "Moved %s to Videos\n" "$1" ;; *audio*) [ ! -d "Audio" ] && mkdir "Audio" mv "$1" "Audio/$1" printf "Moved %s to Audio\n" "$1" ;; *image*) [ ! -d "Images" ] && mkdir "Images" mv "$1" "Images/$1" printf "Moved %s to Images\n" "$1" ;; *pdf*|*document*|*epub*|*djvu*|*cb*) [ ! -d "Documents" ] && mkdir "Documents" mv "$1" "Documents/$1" printf "Moved %s to Documents\n" "$1" ;; *text*) [ ! -d "Plaintext" ] && mkdir "Plaintext" mv "$1" "Plaintext/$1" printf "Moved %s to Plaintext\n" "$1" ;; *tar*|*xz*|*compress*|*7z*|*rar*|*zip*) [ ! -d "Archives" ] && mkdir "Archives" mv "$1" "Archives/$1" printf "Moved %s to Archives\n" "$1" ;; *binary*) [ ! -d "Binaries" ] && mkdir "Binaries" mv "$1" "Binaries/$1" printf "Moved %s to Binaries\n" "$1" ;; esac } main() { for file in * do [ -f "$file" ] && organize "$file" done } main "$@" �������������������������������������������������������������������������������������������������������������������������������������������nnn-3.0/plugins/pdfread�����������������������������������������������������������������������������0000775�0000000�0000000�00000001335�13620656057�0015175�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env sh # Description: Read a text or PDF file in British English # # Shell: POSIX compliant # Author: Arun Prakash Jana if ! [ -z "$1" ]; then tmpf="$(basename "$1")" tmpf="${TMPDIR:-/tmp}"/"${tmpf%.*}" if [ "$(head -c 4 "$1")" = "%PDF" ]; then # Convert using pdftotext pdftotext -nopgbrk -layout "$1" - | sed 's/\xe2\x80\x8b//g' > "$tmpf".txt pico2wave -w "$tmpf".wav -l en-GB "$(tr '\n' ' ' < "$tmpf".txt)" rm "$tmpf".txt else pico2wave -w "$tmpf".wav -l en-GB "$(tr '\n' ' ' < "$1")" fi # to jump around and note the time mpv "$tmpf".wav # flat read but better quality # play -qV0 "$tmpf".wav treble 2 gain -l 2 rm "$tmpf".wav fi ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������nnn-3.0/plugins/pdfview�����������������������������������������������������������������������������0000775�0000000�0000000�00000001147�13620656057�0015235�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env sh # Description: View a PDF file in pager # # Notes: # - $PAGER must be 'less -R' or 'most' # - To use mutool, uncomment the relevant lines and comment the pdftotext line # # Shell: POSIX compliant # Author: Arun Prakash Jana if ! [ -z "$1" ]; then if [ "$(head -c 4 "$1")" = "%PDF" ]; then # Convert using pdftotext pdftotext -nopgbrk -layout "$1" - | sed 's/\xe2\x80\x8b//g' | $PAGER # Convert using mutool # file=`basename "$1"` # txt=/tmp/"$file".txt # mutool convert -o "$txt" "$1" # eval $PAGER $txt # rm "$txt" fi fi �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������nnn-3.0/plugins/picker������������������������������������������������������������������������������0000775�0000000�0000000�00000001136�13620656057�0015044�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env sh # Description: Pick files and pipe the newline-separated list to another utility # # Shell: POSIX compliant # Author: Arun Prakash Jana # # Usage: # Copy this file in your $PATH, make it executable and preferably name it to picker. # Run commands like: # ls -l `picker` # cd `picker` # vimdiff `picker` # or, in fish shell: # ls -l (picker) # cd (picker) # vimdiff (picker) # # NOTE: This use case is limited to picking files, other functionality may not work as expected. nnn -p /tmp/picked if [ -f /tmp/picked ]; then tr '\0' '\n' < /tmp/picked rm /tmp/picked fi ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������nnn-3.0/plugins/pskill������������������������������������������������������������������������������0000775�0000000�0000000�00000001402�13620656057�0015061�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env sh # Description: Fuzzy list and kill a (zombie) process by name # # Requires: fzf or fzy, ps # # Note: To kill a zombie process enter "zombie" # # Shell: POSIX compliant # Author: Arun Prakash Jana printf "Enter process name ['defunct' for zombies]: " read -r psname # shellcheck disable=SC2009 if ! [ -z "$psname" ]; then if which sudo >/dev/null 2>&1; then sucmd=sudo elif which doas >/dev/null 2>&1; then sucmd=doas else sucmd=: # noop fi if which fzf >/dev/null 2>&1; then fuzzy=fzf elif which fzy >/dev/null 2>&1; then fuzzy=fzy else exit 1 fi cmd="$(ps -ax | grep -iw "$psname" | "$fuzzy" | sed -e 's/^[ \t]*//' | cut -d' ' -f1)" $sucmd kill -9 "$cmd" fi ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������nnn-3.0/plugins/renamer�����������������������������������������������������������������������������0000775�0000000�0000000�00000002017�13620656057�0015217�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env sh # Description: Batch rename selection or current directory with qmv # # Notes: # - Try to mimic current batch rename functionality but with correct # handling of edge cases by qmv or vidir. # Qmv opens with hidden files if no selection is used. Selected # directories are shown. # Vidir don't show directories nor hidden files. # # Shell: POSIX compliant # Author: José Neder selection=${XDG_CONFIG_HOME:-$HOME/.config}/nnn/.selection if command -v qmv >/dev/null 2>&1; then batchrenamesel="qmv -fdo -da" batchrename="qmv -fdo -a" elif command -v vidir >/dev/null 2>&1; then batchrenamesel="vidir" batchrename="vidir" else printf "there is not batchrename program installed." exit fi if [ -s "$selection" ]; then printf "rename selection? " read -r resp fi if [ "$resp" = "y" ]; then # -o flag is necessary for interactive editors xargs -o -0 $batchrenamesel < "$selection" elif [ ! "$(LC_ALL=C ls -a)" = ". .." ]; then # On older systems that don't have ls -A $batchrename fi �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������nnn-3.0/plugins/ringtone����������������������������������������������������������������������������0000775�0000000�0000000�00000001656�13620656057�0015423�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env sh # Description: Create an mp3 ringtone out of an audio file in any format # Needs user to provide start and end where to cut the file # Input file audio.ext results in audio_ringtone.mp3 # # Tip: To convert a complete media file, set start as 0 and # the runtime of the file as end. # # Requires: date, ffmpeg # # Shell: POSIX compliant # Author: Arun Prakash Jana if [ -n "$1" ]; then printf "start (hh:mm:ss): " read -r start st=$(date -d "$start" +%s) || exit 1 printf "end (hh:mm:ss): " read -r end et=$(date -d "$end" +%s) || exit 1 if [ "$st" -ge "$et" ]; then printf "error: start >= end " read -r _ exit 1 fi interval=$(( et - st )) outfile=$(basename "$1") outfile="${outfile%.*}"_ringtone.mp3 ffmpeg -i "$1" -ss "$start" -t "$interval" -vn -sn -acodec libmp3lame -q:a 2 "$outfile" fi ����������������������������������������������������������������������������������nnn-3.0/plugins/splitjoin���������������������������������������������������������������������������0000775�0000000�0000000�00000002417�13620656057�0015605�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env sh # Description: Splits the file passed as argument or joins selection # # Note: Adds numeric suffix to split files # Adds '.out suffix to the first file to be joined and saves as output file for join # # Shell: POSIX compliant # Authors: Arun Prakash Jana, ath3 selection=${XDG_CONFIG_HOME:-$HOME/.config}/nnn/.selection resp=s if [ -s "$selection" ]; then printf "press 's' (split current file) or 'j' (join selection): " read -r resp fi if [ "$resp" = "j" ]; then if [ -s "$selection" ]; then arr=$(tr '\0' '\n' < "$selection") if [ "$(echo "$arr" | wc -l)" -lt 2 ]; then echo "joining needs at least 2 files" exit fi for entry in $arr do if [ -d "$entry" ]; then echo "cant join directories" exit fi done file="$(basename "$(echo "$arr" | sed -n '1p' | sed -e 's/[0-9][0-9]$//')")" sort -z < "$selection" | xargs -0 -I{} cat {} > "${file}.out" fi elif [ "$resp" = "s" ]; then if [ -n "$1" ] && [ -f "$1" ]; then # a single file is passed printf "split size in MB: " read -r size if [ -n "$size" ]; then split -d -b "$size"M "$1" "$1" fi fi fi �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������nnn-3.0/plugins/suedit������������������������������������������������������������������������������0000775�0000000�0000000�00000000630�13620656057�0015062�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env sh # Description: Edit file as superuser # # Shell: POSIX compliant # Author: Anna Arad EDITOR="${EDITOR:-vim}" is_cmd_exists () { which "$1" > /dev/null 2>&1 echo $? } if [ "$(is_cmd_exists sudo)" -eq "0" ]; then sudo "$EDITOR" "$1" elif [ "$(is_cmd_exists sudoedit)" -eq "0" ]; then sudoedit "$1" elif [ "$(is_cmd_exists doas)" -eq "0" ]; then doas "$EDITOR" "$1" fi ��������������������������������������������������������������������������������������������������������nnn-3.0/plugins/treeview����������������������������������������������������������������������������0000775�0000000�0000000�00000000213�13620656057�0015414�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env sh # Description: Show tree output in $EDITOR # # Shell: POSIX compliant # Author: Arun Prakash Jana tree -ps | $EDITOR - �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������nnn-3.0/plugins/uidgid������������������������������������������������������������������������������0000775�0000000�0000000�00000000320�13620656057�0015026�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env sh # Description: list uid and gid of files # # Shell: POSIX compliant # Author: Arun Prakash Jana, superDuperCyberTechno # shellcheck disable=SC2012 ls -lah --group-directories-first | less ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������nnn-3.0/plugins/upgrade�����������������������������������������������������������������������������0000775�0000000�0000000�00000001222�13620656057�0015212�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env sh # Description: Check and update to latest version of nnn manually on Debian 9 Stretch # # Shell: POSIX-compliant # Author: Arun Prakash Jana # NOTE: This script installs a package, should be issued with admin privilege cur="$(nnn -v)" new="$(curl -s "https://github.com/jarun/nnn/releases/latest" | grep -Eo "[0-9]+\.[0-9]+")" if [ "$cur" = "$new" ]; then echo 'Already at latest version' exit 0 fi # get the package curl -Ls -O "https://github.com/jarun/nnn/releases/download/v$new/nnn_$new-1_debian9.amd64.deb" # install it sudo dpkg -i nnn_"$new"-1_debian9.amd64.deb # remove the file rm -rf nnn_"$new"-1_debian9.amd64.deb ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������nnn-3.0/plugins/upload������������������������������������������������������������������������������0000775�0000000�0000000�00000001414�13620656057�0015052�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env sh # Description: Paste contents of a text a file http://ix.io # Upload a binary file to file.io # Requires: curl, jq, tr # Note: Binary file set to expire after a week # # Shell: POSIX compliant # Author: Arun Prakash Jana if ! [ -z "$1" ] && [ -s "$1" ]; then if [ "$(mimetype --output-format %m "$1" | awk -F '/' '{print $1}')" = "text" ]; then curl -F "f:1=@$1" ix.io else # Upload the file, show the download link and wait till user presses any key curl -s -F "file=@$1" https://file.io/?expires=1w | jq '.link' | tr -d '"' # To write download link to "$1".loc and exit # curl -s -F "file=@$1" https://file.io/?expires=1w -o `basename "$1"`.loc fi else printf "empty file!" fi read -r _ ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������nnn-3.0/plugins/vidthumb����������������������������������������������������������������������������0000775�0000000�0000000�00000001057�13620656057�0015413�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env sh # Description: Generate video thumbnails and view them # # Requires: # ffmpegthumbnailer: https://github.com/dirkvdb/ffmpegthumbnailer # lsix: https://github.com/hackerb9/lsix # # Shell: POSIX compliant # Author: Arun Prakash Jana mkdir .nthumbs > /dev/null 2>&1 for file in *; do if [ -f "$file" ]; then ffmpegthumbnailer -i "$file" -o .nthumbs/"${file%%.*}".jpg 2> /dev/null fi done # render thumbnails in lsix lsix .nthumbs/* # remove the thumbnails rm -rf .nthumbs printf "Press any key to exit..." read -r _ ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������nnn-3.0/plugins/wall��������������������������������������������������������������������������������0000775�0000000�0000000�00000001503�13620656057�0014524�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env sh # Description: Set the selected image as wallpaper using nitrogen or pywal. # Usage: Hover on an image and run the script to set it as wallpaper. # # Shell: POSIX Compliant # Author: juacq97 cmd_exists () { which "$1" > /dev/null 2>&1 echo $? } if ! [ -z "$1" ]; then if [ "$(mimetype --output-format %m "$1" | awk -F '/' '{print $1}')" = "image" ]; then if [ "$(cmd_exists nitrogen)" -eq "0" ]; then nitrogen --set-zoom-fill --save "$1" elif [ "$(cmd_exists wal)" -eq "0" ]; then wal -i "$1" else printf "nitrogen ir pywal missing" read -r _ fi # If you want a system notification, uncomment the next 3 lines. # notify-send -a "nnn" "Wallpaper changed!" # else # notify-send -a "nnn" "No image selected" fi fi ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������nnn-3.0/src/����������������������������������������������������������������������������������������0000775�0000000�0000000�00000000000�13620656057�0012746�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������nnn-3.0/src/.clang-tidy�����������������������������������������������������������������������������0000664�0000000�0000000�00000001617�13620656057�0015007�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- 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,-readability-isolate-declaration,-bugprone-narrowing-conversions' WarningsAsErrors: '*' HeaderFilterRegex: '.*(?<!lookup3.c)$' FormatStyle: 'file' CheckOptions: - key: readability-braces-around-statements.ShortStatementLines value: '1' - key: google-runtime-int.TypeSufix value: '_t' - key: fuchsia-restrict-system-includes.Includes value: '*,-stdint.h,-stdbool.h' - key: readability-function-size.StatementThreshold value: '900' ... �����������������������������������������������������������������������������������������������������������������nnn-3.0/src/dbg.h�����������������������������������������������������������������������������������0000664�0000000�0000000�00000005241�13620656057�0013655�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* * BSD 2-Clause License * * Copyright (C) 2014-2016, Lazaros Koromilas <lostd@2f30.org> * Copyright (C) 2014-2016, Dimitris Papastamos <sin@2f30.org> * Copyright (C) 2016-2020, Arun Prakash Jana <engineerarun@gmail.com> * 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 #ifdef DBGMODE 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 && (unsigned int)r < sizeof(buf)) r = write(fd, buf, r); va_end(ap); return r; } static int enabledbg(void) { FILE *fp = fopen("/tmp/nnndbg", "w"); if (!fp) { perror("dbg(1)"); fp = fopen("./nnndbg", "w"); if (!fp) { perror("dbg(2)"); return -1; } } DEBUG_FD = dup(fileno(fp)); fclose(fp); if (DEBUG_FD == -1) { perror("dbg(3)"); return -1; } return 0; } static void disabledbg(void) { close(DEBUG_FD); } #define STRINGIFY(x) #x #define TOSTRING(x) STRINGIFY(x) #define DPRINTF_D(x) xprintf(DEBUG_FD, "ln " TOSTRING(__LINE__) ": " #x "=%d\n", x) #define DPRINTF_U(x) xprintf(DEBUG_FD, "ln " TOSTRING(__LINE__) ": " #x "=%u\n", x) #define DPRINTF_S(x) xprintf(DEBUG_FD, "ln " TOSTRING(__LINE__) ": " #x "=%s\n", x) #define DPRINTF_P(x) xprintf(DEBUG_FD, "ln " TOSTRING(__LINE__) ": " #x "=%p\n", x) #else #define DPRINTF_D(x) #define DPRINTF_U(x) #define DPRINTF_S(x) #define DPRINTF_P(x) #endif /* DBGMODE */ ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������nnn-3.0/src/nnn.c�����������������������������������������������������������������������������������0000664�0000000�0000000�00000447443�13620656057�0013723�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* * BSD 2-Clause License * * Copyright (C) 2014-2016, Lazaros Koromilas <lostd@2f30.org> * Copyright (C) 2014-2016, Dimitris Papastamos <sin@2f30.org> * Copyright (C) 2016-2020, Arun Prakash Jana <engineerarun@gmail.com> * 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__ #ifndef _GNU_SOURCE #define _GNU_SOURCE #endif #if defined(__arm__) || defined(__i386__) #define _FILE_OFFSET_BITS 64 /* Support large files on 32-bit */ #endif #include <sys/inotify.h> #define LINUX_INOTIFY #if !defined(__GLIBC__) #include <sys/types.h> #endif #endif #include <sys/resource.h> #include <sys/stat.h> #include <sys/statvfs.h> #if defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__) #include <sys/types.h> #include <sys/event.h> #include <sys/time.h> #define BSD_KQUEUE #elif defined(__HAIKU__) #include "../misc/haiku/haiku_interop.h" #define HAIKU_NM #else #include <sys/sysmacros.h> #endif #include <sys/wait.h> #ifdef __linux__ /* Fix failure due to mvaddnwstr() */ #ifndef NCURSES_WIDECHAR #define NCURSES_WIDECHAR 1 #endif #elif defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__) || defined(__sun) #ifndef _XOPEN_SOURCE_EXTENDED #define _XOPEN_SOURCE_EXTENDED #endif #endif #ifndef __USE_XOPEN /* Fix wcswidth() failure, ncursesw/curses.h includes whcar.h on Ubuntu 14.04 */ #define __USE_XOPEN #endif #include <dirent.h> #include <errno.h> #include <fcntl.h> #include <libgen.h> #include <limits.h> #ifndef NOLOCALE #include <locale.h> #endif #include <stdio.h> #ifndef NORL #include <readline/history.h> #include <readline/readline.h> #endif #ifdef PCRE #include <pcre.h> #else #include <regex.h> #endif #include <signal.h> #include <stdarg.h> #include <stdlib.h> #ifdef __sun #include <alloca.h> #endif #include <string.h> #include <strings.h> #include <time.h> #include <unistd.h> #ifndef __USE_XOPEN_EXTENDED #define __USE_XOPEN_EXTENDED 1 #endif #include <ftw.h> #include <wchar.h> #include "nnn.h" #include "dbg.h" /* Macro definitions */ #define VERSION "3.0" #define GENERAL_INFO "BSD 2-Clause\nhttps://github.com/jarun/nnn" #define SESSIONS_VERSION 1 #ifndef S_BLKSIZE #define S_BLKSIZE 512 /* S_BLKSIZE is missing on Android NDK (Termux) */ #endif /* * NAME_MAX and PATH_MAX may not exist, e.g. with dirent.c_name being a * flexible array on Illumos. Use somewhat accomodating fallback values. */ #ifndef NAME_MAX #define NAME_MAX 255 #endif #ifndef PATH_MAX #define PATH_MAX 4096 #endif #define _ABSSUB(N, M) (((N) <= (M)) ? ((M) - (N)) : ((N) - (M))) #define DOUBLECLICK_INTERVAL_NS (400000000) #define XDELAY_INTERVAL_MS (350000) /* 350 ms delay */ #define ELEMENTS(x) (sizeof(x) / sizeof(*(x))) #undef MIN #define MIN(x, y) ((x) < (y) ? (x) : (y)) #undef MAX #define MAX(x, y) ((x) > (y) ? (x) : (y)) #define ISODD(x) ((x) & 1) #define ISBLANK(x) ((x) == ' ' || (x) == '\t') #define TOUPPER(ch) (((ch) >= 'a' && (ch) <= 'z') ? ((ch) - 'a' + 'A') : (ch)) #define CMD_LEN_MAX (PATH_MAX + ((NAME_MAX + 1) << 1)) #define READLINE_MAX 256 #define FILTER '/' #define RFILTER '\\' #define CASE ':' #define MSGWAIT '$' #define REGEX_MAX 48 #define BM_MAX 10 #define PLUGIN_MAX 15 #define ENTRY_INCR 64 /* Number of dir 'entry' structures to allocate per shot */ #define NAMEBUF_INCR 0x800 /* 64 dir entries at once, avg. 32 chars per filename = 64*32B = 2KB */ #define DESCRIPTOR_LEN 32 #define _ALIGNMENT 0x10 /* 16-byte alignment */ #define _ALIGNMENT_MASK 0xF #define TMP_LEN_MAX 64 #define CTX_MAX 4 #define DOT_FILTER_LEN 7 #define ASCII_MAX 128 #define EXEC_ARGS_MAX 8 #define LIST_FILES_MAX (1 << 16) #define SCROLLOFF 3 #define MIN_DISPLAY_COLS 10 #define LONG_SIZE sizeof(ulong) #define ARCHIVE_CMD_LEN 16 #define BLK_SHIFT_512 9 /* Program return codes */ #define _SUCCESS 0 #define _FAILURE !_SUCCESS /* Entry flags */ #define DIR_OR_LINK_TO_DIR 0x1 #define FILE_SELECTED 0x10 /* Macros to define process spawn behaviour as flags */ #define F_NONE 0x00 /* no flag set */ #define F_MULTI 0x01 /* first arg can be combination of args; to be used with F_NORMAL */ #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_NORMAL 0x08 /* spawn child process in non-curses regular CLI mode */ #define F_CONFIRM 0x10 /* run command - show results before exit (must have F_NORMAL) */ #define F_CLI (F_NORMAL | F_MULTI) #define F_SILENT (F_CLI | F_NOTRACE) /* Version compare macros */ /* * states: S_N: normal, S_I: comparing integral part, S_F: comparing * fractional parts, S_Z: idem but with leading Zeroes only */ #define S_N 0x0 #define S_I 0x3 #define S_F 0x6 #define S_Z 0x9 /* result_type: VCMP: return diff; VLEN: compare using len_diff/diff */ #define VCMP 2 #define VLEN 3 /* Volume info */ #define FREE 0 #define CAPACITY 1 /* TYPE DEFINITIONS */ typedef unsigned long ulong; typedef unsigned int uint; typedef unsigned char uchar; typedef unsigned short ushort; typedef long long ll; /* 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 */ } *pEntry; /* Key-value pairs from env */ typedef struct { int key; char *val; } kv; typedef struct { #ifdef PCRE const pcre *pcrex; #else const regex_t *regex; #endif const char *str; } fltrexp_t; /* * Settings * NOTE: update default values if changing order */ 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 extnorder : 1; /* Order by extension */ uint showhidden : 1; /* Set to show hidden files */ uint selmode : 1; /* Set when selecting files */ uint showdetail : 1; /* Clear to show fewer file info */ uint ctxactive : 1; /* Context active or not */ uint reserved : 2; /* The following settings are global */ uint forcequit : 1; /* Do not confirm when quitting program */ uint curctx : 2; /* Current context number */ uint dircolor : 1; /* Current status of dir color */ 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 autoselect : 1; /* Auto-select dir in nav-as-you-type mode */ uint metaviewer : 1; /* Index of metadata viewer in utils[] */ uint useeditor : 1; /* Use VISUAL to open text files */ uint runplugin : 1; /* Choose plugin mode */ uint runctx : 2; /* The context in which plugin is to be run */ uint regex : 1; /* Use regex filters */ uint x11 : 1; /* Copy to system clipboard and show notis */ uint trash : 1; /* Move removed files to trash */ uint mtime : 1; /* Use modification time (else access time) */ uint cliopener : 1; /* All-CLI app opener */ uint waitedit : 1; /* For ops that can't be detached, used EDITOR */ uint rollover : 1; /* Roll over at edges */ } settings; /* Contexts or workspaces */ typedef struct { char c_path[PATH_MAX]; /* Current dir */ char c_last[PATH_MAX]; /* Last visited dir */ char c_name[NAME_MAX + 1]; /* Current file name */ char c_fltr[REGEX_MAX]; /* Current filter */ settings c_cfg; /* Current configuration */ uint color; /* Color code for directories */ } context; typedef struct { size_t ver; size_t pathln[CTX_MAX]; size_t lastln[CTX_MAX]; size_t nameln[CTX_MAX]; size_t fltrln[CTX_MAX]; } session_header_t; /* GLOBALS */ /* Configuration, contexts */ static settings cfg = { 0, /* filtermode */ 0, /* mtimeorder */ 0, /* sizeorder */ 0, /* apparentsz */ 0, /* blkorder */ 0, /* extnorder */ 0, /* showhidden */ 0, /* selmode */ 0, /* showdetail */ 1, /* ctxactive */ 0, /* reserved */ 0, /* forcequit */ 0, /* curctx */ 0, /* dircolor */ 0, /* picker */ 0, /* pickraw */ 0, /* nonavopen */ 1, /* autoselect */ 0, /* metaviewer */ 0, /* useeditor */ 0, /* runplugin */ 0, /* runctx */ 0, /* regex */ 0, /* x11 */ 0, /* trash */ 1, /* mtime */ 0, /* cliopener */ 0, /* waitedit */ 1, /* rollover */ }; static context g_ctx[CTX_MAX] __attribute__ ((aligned)); static int ndents, cur, last, curscroll, last_curscroll, total_dents = ENTRY_INCR; static int xlines, xcols; static int nselected; static uint idle; static uint idletimeout, selbufpos, lastappendpos, selbuflen; static char *bmstr; static char *pluginstr; static char *opener; static char *editor; static char *enveditor; static char *pager; static char *shell; static char *home; static char *initpath; static char *cfgdir; static char *g_selpath; static char *g_listpath; static char *g_prefixpath; static char *plugindir; static char *sessiondir; static char *pnamebuf, *pselbuf; static struct entry *dents; static blkcnt_t ent_blocks; static blkcnt_t dir_blocks; static ulong num_files; static kv bookmark[BM_MAX]; static kv plug[PLUGIN_MAX]; static uchar g_tmpfplen; static uchar blk_shift = BLK_SHIFT_512; static const uint _WSHIFT = (LONG_SIZE == 8) ? 3 : 2; #ifdef PCRE static pcre *archive_pcre; #else static regex_t archive_re; #endif /* Retain old signal handlers */ #ifdef __linux__ static sighandler_t oldsighup; /* old value of hangup signal */ static sighandler_t oldsigtstp; /* old value of SIGTSTP */ #else /* note: no sig_t on Solaris-derivs */ static void (*oldsighup)(int); static void (*oldsigtstp)(int); #endif /* For use in functions which are isolated and don't return the buffer */ static char g_buf[CMD_LEN_MAX] __attribute__ ((aligned)); /* Buffer to store tmp file path to show selection, file stats and help */ static char g_tmpfpath[TMP_LEN_MAX] __attribute__ ((aligned)); /* Buffer to store plugins control pipe location */ static char g_pipepath[TMP_LEN_MAX] __attribute__ ((aligned)); /* MISC NON-PERSISTENT INTERNAL BINARY STATES */ /* Plugin control initialization status */ #define STATE_PLUGIN_INIT 0x1 #define STATE_INTERRUPTED 0x2 #define STATE_RANGESEL 0x4 #define STATE_MOVE_OP 0x8 #define STATE_AUTONEXT 0x10 #define STATE_MSG 0x20 static uchar g_states; /* Options to identify file mime */ #if defined(__APPLE__) #define FILE_MIME_OPTS "-bIL" #elif !defined(__sun) /* no mime option for 'file' */ #define FILE_MIME_OPTS "-biL" #endif /* Macros for utilities */ #define UTIL_OPENER 0 #define UTIL_ATOOL 1 #define UTIL_BSDTAR 2 #define UTIL_UNZIP 3 #define UTIL_TAR 4 #define UTIL_LOCKER 5 #define UTIL_CMATRIX 6 #define UTIL_LAUNCH 7 #define UTIL_SH_EXEC 8 #define UTIL_ARCHIVEMOUNT 9 #define UTIL_SSHFS 10 #define UTIL_RCLONE 11 #define UTIL_VI 12 #define UTIL_LESS 13 #define UTIL_SH 14 #define UTIL_FZF 15 #define UTIL_FZY 16 #define UTIL_NTFY 17 #define UTIL_CBCP 18 /* Utilities to open files, run actions */ static char * const utils[] = { #ifdef __APPLE__ "/usr/bin/open", #elif defined __CYGWIN__ "cygstart", #elif defined __HAIKU__ "open", #else "xdg-open", #endif "atool", "bsdtar", "unzip", "tar", #ifdef __APPLE__ "bashlock", #elif defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) "lock", #elif defined __HAIKU__ "peaclock", #else "vlock", #endif "cmatrix", "launch", "sh -c", "archivemount", "sshfs", "rclone", "vi", "less", "sh", "fzf", "fzy", ".ntfy", ".cbcp", }; /* Common strings */ #define MSG_NO_TRAVERSAL 0 #define MSG_INVALID_KEY 1 #define STR_TMPFILE 2 #define MSG_0_SELECTED 3 #define MSG_UTIL_MISSING 4 #define MSG_FAILED 5 #define MSG_SSN_NAME 6 #define MSG_CP_MV_AS 7 #define MSG_CUR_SEL_OPTS 8 #define MSG_FORCE_RM 9 #define MSG_CREATE_CTX 10 #define MSG_NEW_OPTS 11 #define MSG_CLI_MODE 12 #define MSG_OVERWRITE 13 #define MSG_SSN_OPTS 14 #define MSG_QUIT_ALL 15 #define MSG_HOSTNAME 16 #define MSG_ARCHIVE_NAME 17 #define MSG_OPEN_WITH 18 #define MSG_REL_PATH 19 #define MSG_LINK_PREFIX 20 #define MSG_COPY_NAME 21 #define MSG_CONTINUE 22 #define MSG_SEL_MISSING 23 #define MSG_ACCESS 24 #define MSG_EMPTY_FILE 25 #define MSG_UNSUPPORTED 26 #define MSG_NOT_SET 27 #define MSG_EXISTS 28 #define MSG_FEW_COLUMNS 29 #define MSG_REMOTE_OPTS 30 #define MSG_RCLONE_DELAY 31 #define MSG_APP_NAME 32 #define MSG_ARCHIVE_OPTS 33 #define MSG_PLUGIN_KEYS 34 #define MSG_BOOKMARK_KEYS 35 #define MSG_INVALID_REG 36 #define MSG_ORDER 37 #define MSG_LAZY 38 #define MSG_IGNORED 39 #ifndef DIR_LIMITED_SELECTION #define MSG_DIR_CHANGED 40 /* Must be the last entry */ #endif static const char * const messages[] = { "no traversal", "invalid key", "/.nnnXXXXXX", "0 selected", "missing util", "failed!", "session name: ", "'c'p / 'm'v as?", "'c'urrent / 's'el?", "forcibly remove %s file%s (unrecoverable)?", "create context %d?", "'f'ile / 'd'ir / 's'ym / 'h'ard?", "'c'li / 'g'ui?", "overwrite?", "'s'ave / 'l'oad / 'r'estore?", "Quit all contexts?", "remote name: ", "archive name: ", "open with: ", "relative path: ", "link prefix [@ for none]: ", "copy name: ", "\nPress Enter to continue", "open failed", "dir inaccessible", "empty: edit or open with", "unsupported file", "not set", "entry exists", "too few columns!", "'s'shfs / 'r'clone?", "may take a while, try refresh", "app name: ", "'d'efault / e'x'tract / 'l'ist / 'm'ount?", "plugin keys:", "bookmark keys:", "invalid regex", "toggle 'a'u / 'd'u / 'e'xtn / 'r'everse / 's'ize / 't'ime / 'v'ersion?", "unmount failed! try lazy?", "ignoring invalid paths...", #ifndef DIR_LIMITED_SELECTION "dir changed, range sel off", /* Must be the last entry */ #endif }; /* Supported configuration environment variables */ #define NNN_BMS 0 #define NNN_PLUG 1 #define NNN_OPENER 2 #define NNN_COLORS 3 #define NNNLVL 4 #define NNN_PIPE 5 #define NNN_ARCHIVE 6 /* strings end here */ #define NNN_TRASH 7 /* flags begin here */ static const char * const env_cfg[] = { "NNN_BMS", "NNN_PLUG", "NNN_OPENER", "NNN_COLORS", "NNNLVL", "NNN_PIPE", "NNN_ARCHIVE", "NNN_TRASH", }; /* Required environment variables */ #define ENV_SHELL 0 #define ENV_VISUAL 1 #define ENV_EDITOR 2 #define ENV_PAGER 3 #define ENV_NCUR 4 static const char * const envs[] = { "SHELL", "VISUAL", "EDITOR", "PAGER", "nnn", }; #ifdef __linux__ static char cp[] = "cp -iRp"; static char mv[] = "mv -i"; #else static char cp[] = "cp -iRp"; static char mv[] = "mv -i"; #endif static const char cpmvformatcmd[] = "sed -i 's|^\\(\\(.*/\\)\\(.*\\)$\\)|#\\1\\n\\3|' %s"; static const char cpmvrenamecmd[] = "sed 's|^\\([^#][^/]\\?.*\\)$|%s/\\1|;s|^#\\(/.*\\)$|\\1|' " "%s | tr '\\n' '\\0' | xargs -0 -n2 sh -c '%s \"$0\" \"$@\" " "< /dev/tty'"; static const char batchrenamecmd[] = "paste -d'\n' %s %s | sed 'N; /^\\(.*\\)\\n\\1$/!p;d' | " "tr '\n' '\\0' | xargs -0 -n2 mv 2>/dev/null"; static const char archive_regex[] = "\\.(bz|bz2|gz|tar|taz|tbz|tbz2|tgz|z|zip)$"; static const char replaceprefixcmd[] = "sed -i 's|^%s\\(.*\\)$|%s\\1|' %s"; /* Event handling */ #ifdef LINUX_INOTIFY #define NUM_EVENT_SLOTS 8 /* Make room for 8 events */ #define EVENT_SIZE (sizeof(struct inotify_event)) #define EVENT_BUF_LEN (EVENT_SIZE * NUM_EVENT_SLOTS) 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) #define NUM_EVENT_SLOTS 1 #define NUM_EVENT_FDS 1 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; #elif defined(HAIKU_NM) static bool haiku_nm_active = FALSE; static haiku_nm_h haiku_hnd; #endif /* Function macros */ #define tolastln() move(xlines - 1, 0) #define exitcurses() endwin() #define clearprompt() printmsg("") #define printwarn(presel) printwait(strerror(errno), presel) #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)) /* We don't care about the return value from strcmp() */ #define xstrcmp(a, b) (*(a) != *(b) ? -1 : strcmp((a), (b))) /* A faster version of xisdigit */ #define xisdigit(c) ((unsigned int) (c) - '0' <= 9) #define xerror() perror(xitoa(__LINE__)) /* Forward declarations */ static void redraw(char *path); static int spawn(char *file, char *arg1, char *arg2, const char *dir, uchar flag); static int (*nftw_fn)(const char *fpath, const struct stat *sb, int typeflag, struct FTW *ftwbuf); static int dentfind(const char *fname, int n); static void move_cursor(int target, int ignore_scrolloff); static inline bool getutil(char *util); static size_t mkpath(const char *dir, const char *name, char *out); static char *xgetenv(const char *name, char *fallback); static void plugscript(const char *plugin, char *newpath, uchar flags); /* Functions */ static void sigint_handler(int sig) { (void) sig; g_states |= STATE_INTERRUPTED; } static uint xatoi(const char *str) { int val = 0; if (!str) return 0; while (xisdigit(*str)) { val = val * 10 + (*str - '0'); ++str; } return val; } static char *xitoa(uint val) { static char ascbuf[32] = {0}; int i = 30; uint rem; if (!val) return "0"; while (val && i) { rem = val / 10; ascbuf[i] = '0' + (val - (rem * 10)); val = rem; --i; } return &ascbuf[++i]; } static void clearinfoln(void) { move(xlines - 2, 0); addch('\n'); } #ifdef KEY_RESIZE /* Clear the old prompt */ static void clearoldprompt(void) { clearinfoln(); tolastln(); addch('\n'); } #endif /* Messages show up at the bottom */ static void printmsg(const char *msg) { tolastln(); addstr(msg); addch('\n'); } static void printwait(const char *msg, int *presel) { printmsg(msg); if (presel) *presel = MSGWAIT; } /* Kill curses and display error before exiting */ static void printerr(int linenum) { exitcurses(); perror(xitoa(linenum)); if (!cfg.picker && g_selpath) unlink(g_selpath); free(pselbuf); exit(1); } /* Print prompt on the last line */ static void printprompt(const char *str) { clearprompt(); addstr(str); } static void printinfoln(const char *str) { clearinfoln(); mvaddstr(xlines - 2, xcols - strlen(str), str); } static int get_input(const char *prompt) { int r; if (prompt) printprompt(prompt); cleartimeout(); #ifdef KEY_RESIZE do { r = getch(); if (r == KEY_RESIZE && prompt) { clearoldprompt(); xlines = LINES; printprompt(prompt); } } while (r == KEY_RESIZE); #else r = getch(); #endif settimeout(); return r; } static int get_cur_or_sel(void) { if (selbufpos && ndents) { int choice = get_input(messages[MSG_CUR_SEL_OPTS]); return ((choice == 'c' || choice == 's') ? choice : 0); } if (selbufpos) return 's'; if (ndents) return 'c'; return 0; } static void xdelay(useconds_t delay) { refresh(); usleep(delay); } static char confirm_force(bool selection) { char str[64]; int r; snprintf(str, 64, messages[MSG_FORCE_RM], (selection ? xitoa(nselected) : "current"), (selection ? "(s)" : "")); r = get_input(str); 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(void) { 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) { void *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) { if (!src || !dest || !n) return 0; ulong *s, *d; size_t len = strlen(src) + 1, blocks; 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: https://www.felixcloutier.com/x86/MOVDQA:VMOVDQA32:VMOVDQA64 */ if ((n >= LONG_SIZE) && (((ulong)src & _ALIGNMENT_MASK) == 0 && ((ulong)dest & _ALIGNMENT_MASK) == 0)) { s = (ulong *)src; d = (ulong *)dest; blocks = n >> _WSHIFT; n &= LONG_SIZE - 1; while (blocks) { *d = *s; // NOLINT ++d, ++s; --blocks; } if (!n) { dest = (char *)d; *--dest = '\0'; return len; } src = (char *)s; dest = (char *)d; } while (--n && (*dest = *src)) // NOLINT ++dest, ++src; if (!n) *dest = '\0'; return len; } static bool is_suffix(const char *str, const char *suffix) { if (!str || !suffix) return FALSE; size_t lenstr = strlen(str); size_t lensuffix = strlen(suffix); if (lensuffix > lenstr) return FALSE; return (xstrcmp(str + (lenstr - lensuffix), suffix) == 0); } /* * 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) { if (!s || !n) return NULL; uchar *ptr = s + n; do { --ptr; if (*ptr == ch) return ptr; } while (s != ptr); return NULL; } /* Assumes both the paths passed are directories */ static char *common_prefix(const char *path, char *prefix) { const char *x = path, *y = prefix; char *sep; if (!path || !*path || !prefix) return NULL; if (!*prefix) { xstrlcpy(prefix, path, PATH_MAX); return prefix; } while (*x && *y && (*x == *y)) ++x, ++y; /* Strings are same */ if (!*x && !*y) return prefix; /* Path is shorter */ if (!*x && *y == '/') { xstrlcpy(prefix, path, y - path); return prefix; } /* Prefix is shorter */ if (!*y && *x == '/') return prefix; /* Shorten prefix */ prefix[y - prefix] = '\0'; sep = xmemrchr((uchar *)prefix, '/', y - prefix); if (sep != prefix) *sep = '\0'; else /* Just '/' */ prefix[1] = '\0'; return prefix; } /* * The library function realpath() resolves symlinks. * If there's a symlink in file list we want to show the symlink not what it's points to. */ static char *abspath(const char *path, const char *cwd) { if (!path || !cwd) return NULL; size_t dst_size = 0, src_size = strlen(path), cwd_size = strlen(cwd); const char *src, *next; char *dst; char *resolved_path = malloc(src_size + (*path == '/' ? 0 : cwd_size) + 1); if (!resolved_path) return NULL; /* Turn relative paths into absolute */ if (path[0] != '/') dst_size = xstrlcpy(resolved_path, cwd, cwd_size + 1) - 1; else resolved_path[0] = '\0'; src = path; dst = resolved_path + dst_size; for (next = NULL; next != path + src_size;) { next = strchr(src, '/'); if (!next) next = path + src_size; if (next - src == 2 && src[0] == '.' && src[1] == '.') { if (dst - resolved_path) { dst = xmemrchr((uchar *)resolved_path, '/', dst - resolved_path); *dst = '\0'; } } else if (next - src == 1 && src[0] == '.') { /* NOP */ } else if (next - src) { *(dst++) = '/'; xstrlcpy(dst, src, next - src + 1); dst += next - src; } src = next + 1; } if (*resolved_path == '\0') { resolved_path[0] = '/'; resolved_path[1] = '\0'; } return resolved_path; } static char *xbasename(char *path) { char *base = xmemrchr((uchar *)path, '/', strlen(path)); // NOLINT return base ? base + 1 : path; } static int create_tmp_file(void) { xstrlcpy(g_tmpfpath + g_tmpfplen - 1, messages[STR_TMPFILE], TMP_LEN_MAX - g_tmpfplen); int fd = mkstemp(g_tmpfpath); if (fd == -1) { DPRINTF_S(strerror(errno)); } return fd; } /* Writes buflen char(s) from buf to a file */ static void writesel(const char *buf, const size_t buflen) { if (cfg.pickraw || !g_selpath) return; FILE *fp = fopen(g_selpath, "w"); if (fp) { if (fwrite(buf, 1, buflen, fp) != buflen) printwarn(NULL); fclose(fp); } else printwarn(NULL); } static void appendfpath(const char *path, const size_t len) { if ((selbufpos >= selbuflen) || ((len + 3) > (selbuflen - selbufpos))) { selbuflen += PATH_MAX; pselbuf = xrealloc(pselbuf, selbuflen); if (!pselbuf) errexit(); } selbufpos += xstrlcpy(pselbuf + selbufpos, path, len); } /* Write selected file paths to fd, linefeed separated */ static size_t seltofile(int fd, uint *pcount) { uint lastpos, count = 0; char *pbuf = pselbuf; size_t pos = 0, len; ssize_t r; if (pcount) *pcount = 0; if (!selbufpos) return 0; lastpos = selbufpos - 1; while (pos <= lastpos) { len = strlen(pbuf); pos += len; r = write(fd, pbuf, len); if (r != (ssize_t)len) return pos; if (pos <= lastpos) { if (write(fd, "\n", 1) != 1) return pos; pbuf += len + 1; } ++pos; ++count; } if (pcount) *pcount = count; return pos; } /* List selection from selection file (another instance) */ static bool listselfile(void) { struct stat sb; if (stat(g_selpath, &sb) == -1) return FALSE; /* Nothing selected if file size is 0 */ if (!sb.st_size) return FALSE; snprintf(g_buf, CMD_LEN_MAX, "tr \'\\0\' \'\\n\' < %s", g_selpath); spawn(utils[UTIL_SH_EXEC], g_buf, NULL, NULL, F_CLI | F_CONFIRM); return TRUE; } /* Reset selection indicators */ static void resetselind(void) { int r = 0; for (; r < ndents; ++r) if (dents[r].flags & FILE_SELECTED) dents[r].flags &= ~FILE_SELECTED; } static void startselection(void) { if (!cfg.selmode) { cfg.selmode = 1; nselected = 0; if (selbufpos) { resetselind(); writesel(NULL, 0); selbufpos = 0; } lastappendpos = 0; } } static void updateselbuf(const char *path, char *newpath) { int i = 0; size_t r; for (; i < ndents; ++i) if (dents[i].flags & FILE_SELECTED) { r = mkpath(path, dents[i].name, newpath); appendfpath(newpath, r); } } /* Finish selection procedure before an operation */ static void endselection(void) { int fd; ssize_t count; char buf[sizeof(replaceprefixcmd) + PATH_MAX + (TMP_LEN_MAX << 1)]; if (cfg.selmode) cfg.selmode = 0; if (!g_listpath || !selbufpos) return; fd = create_tmp_file(); if (fd == -1) { DPRINTF_S("couldn't create tmp file"); return; } seltofile(fd, NULL); if (close(fd)) { DPRINTF_S(strerror(errno)); printwarn(NULL); return; } snprintf(buf, sizeof(buf), replaceprefixcmd, g_listpath, g_prefixpath, g_tmpfpath); spawn(utils[UTIL_SH_EXEC], buf, NULL, NULL, F_CLI); fd = open(g_tmpfpath, O_RDONLY); if (fd == -1) { DPRINTF_S(strerror(errno)); printwarn(NULL); if (unlink(g_tmpfpath)) { DPRINTF_S(strerror(errno)); printwarn(NULL); } return; } count = read(fd, pselbuf, selbuflen); if (count < 0) { DPRINTF_S(strerror(errno)); printwarn(NULL); if (close(fd) || unlink(g_tmpfpath)) { DPRINTF_S(strerror(errno)); } return; } if (close(fd) || unlink(g_tmpfpath)) { DPRINTF_S(strerror(errno)); printwarn(NULL); return; } selbufpos = count; pselbuf[--count] = '\0'; for (--count; count > 0; --count) if (pselbuf[count] == '\n' && pselbuf[count+1] == '/') pselbuf[count] = '\0'; writesel(pselbuf, selbufpos - 1); } static void clearselection(void) { nselected = 0; selbufpos = 0; cfg.selmode = 0; writesel(NULL, 0); } /* Returns: 1 - success, 0 - none selected, -1 - other failure */ static int editselection(void) { int ret = -1; int fd, lines = 0; ssize_t count; struct stat sb; time_t mtime; if (!selbufpos) return listselfile(); fd = create_tmp_file(); if (fd == -1) { DPRINTF_S("couldn't create tmp file"); return -1; } seltofile(fd, NULL); if (close(fd)) { DPRINTF_S(strerror(errno)); return -1; } /* Save the last modification time */ if (stat(g_tmpfpath, &sb)) { DPRINTF_S(strerror(errno)); unlink(g_tmpfpath); return -1; } mtime = sb.st_mtime; spawn((cfg.waitedit ? enveditor : editor), g_tmpfpath, NULL, NULL, F_CLI); fd = open(g_tmpfpath, O_RDONLY); if (fd == -1) { DPRINTF_S(strerror(errno)); unlink(g_tmpfpath); return -1; } fstat(fd, &sb); if (mtime == sb.st_mtime) { DPRINTF_S("selection is not modified"); unlink(g_tmpfpath); return 1; } if (sb.st_size > selbufpos) { DPRINTF_S("edited buffer larger than previous"); unlink(g_tmpfpath); goto emptyedit; } count = read(fd, pselbuf, selbuflen); if (count < 0) { DPRINTF_S(strerror(errno)); printwarn(NULL); if (close(fd) || unlink(g_tmpfpath)) { DPRINTF_S(strerror(errno)); printwarn(NULL); } goto emptyedit; } if (close(fd) || unlink(g_tmpfpath)) { DPRINTF_S(strerror(errno)); printwarn(NULL); goto emptyedit; } if (!count) { ret = 1; goto emptyedit; } resetselind(); selbufpos = count; /* The last character should be '\n' */ pselbuf[--count] = '\0'; for (--count; count > 0; --count) { /* Replace every '\n' that separates two paths */ if (pselbuf[count] == '\n' && pselbuf[count + 1] == '/') { ++lines; pselbuf[count] = '\0'; } } /* Add a line for the last file */ ++lines; if (lines > nselected) { DPRINTF_S("files added to selection"); goto emptyedit; } nselected = lines; writesel(pselbuf, selbufpos - 1); return 1; emptyedit: resetselind(); clearselection(); return ret; } static bool selsafe(void) { /* Fail if selection file path not generated */ if (!g_selpath) { printmsg(messages[MSG_SEL_MISSING]); return FALSE; } /* Fail if selection file path isn't accessible */ if (access(g_selpath, R_OK | W_OK) == -1) { errno == ENOENT ? printmsg(messages[MSG_0_SELECTED]) : printwarn(NULL); return FALSE; } return TRUE; } /* Initialize curses mode */ static bool initcurses(mmask_t *oldmask) { short i; char *colors = xgetenv(env_cfg[NNN_COLORS], "4444"); if (cfg.picker) { if (!newterm(NULL, stderr, stdin)) { fprintf(stderr, "newterm!\n"); return FALSE; } } else if (!initscr()) { fprintf(stderr, "initscr!\n"); DPRINTF_S(getenv("TERM")); return FALSE; } cbreak(); noecho(); nonl(); //intrflush(stdscr, FALSE); keypad(stdscr, TRUE); #if NCURSES_MOUSE_VERSION <= 1 mousemask(BUTTON1_PRESSED | BUTTON1_DOUBLE_CLICKED, oldmask); #else mousemask(BUTTON1_PRESSED | BUTTON4_PRESSED | BUTTON5_PRESSED, oldmask); #endif mouseinterval(0); curs_set(FALSE); /* Hide cursor */ start_color(); use_default_colors(); /* Get and set the context colors */ for (i = 0; i < CTX_MAX; ++i) { if (*colors) { g_ctx[i].color = (*colors < '0' || *colors > '7') ? 4 : *colors - '0'; ++colors; } else g_ctx[i].color = 4; init_pair(i + 1, g_ctx[i].color, -1); g_ctx[i].c_fltr[REGEX_MAX - 1] = '\0'; } settimeout(); /* One second */ set_escdelay(25); return TRUE; } /* No NULL check here as spawn() guards against it */ static int parseargs(char *line, char **argv) { int count = 0; argv[count++] = line; while (*line) { // NOLINT if (ISBLANK(*line)) { *line++ = '\0'; if (!*line) // NOLINT return count; argv[count++] = line; if (count == EXEC_ARGS_MAX) return -1; } ++line; } return count; } static pid_t xfork(uchar flag) { int status; pid_t p = fork(); if (p > 0) { /* the parent ignores the interrupt, quit and hangup signals */ oldsighup = signal(SIGHUP, SIG_IGN); oldsigtstp = signal(SIGTSTP, SIG_DFL); } else if (p == 0) { /* We create a grandchild to detach */ if (flag & F_NOWAIT) { p = fork(); if (p > 0) _exit(0); else if (p == 0) { signal(SIGHUP, SIG_DFL); signal(SIGINT, SIG_DFL); signal(SIGQUIT, SIG_DFL); signal(SIGTSTP, SIG_DFL); setsid(); return p; } perror("fork"); _exit(0); } /* so they can be used to stop the child */ signal(SIGHUP, SIG_DFL); signal(SIGINT, SIG_DFL); signal(SIGQUIT, SIG_DFL); signal(SIGTSTP, SIG_DFL); } /* This is the parent waiting for the child to create grandchild*/ if (flag & F_NOWAIT) waitpid(p, &status, 0); if (p == -1) perror("fork"); return p; } static int join(pid_t p, uchar flag) { int status = 0xFFFF; if (!(flag & F_NOWAIT)) { /* wait for the child to exit */ do { } while (waitpid(p, &status, 0) == -1); if (WIFEXITED(status)) { status = WEXITSTATUS(status); DPRINTF_D(status); } } /* restore parent's signal handling */ signal(SIGHUP, oldsighup); signal(SIGTSTP, oldsigtstp); return status; } /* * Spawns a child process. Behaviour can be controlled using flag. * Limited to 2 arguments to a program, flag works on bit set. */ static int spawn(char *file, char *arg1, char *arg2, const char *dir, uchar flag) { pid_t pid; int status, retstatus = 0xFFFF; char *argv[EXEC_ARGS_MAX] = {0}; char *cmd = NULL; if (!file || !*file) return retstatus; /* Swap args if the first arg is NULL and second isn't */ if (!arg1 && arg2) { arg1 = arg2; arg2 = NULL; } if (flag & F_MULTI) { size_t len = strlen(file) + 1; cmd = (char *)malloc(len); if (!cmd) { DPRINTF_S("malloc()!"); return retstatus; } xstrlcpy(cmd, file, len); status = parseargs(cmd, argv); if (status == -1 || status > (EXEC_ARGS_MAX - 3)) { /* arg1, arg2 and last NULL */ free(cmd); DPRINTF_S("NULL or too many args"); return retstatus; } argv[status++] = arg1; argv[status] = arg2; } else { argv[0] = file; argv[1] = arg1; argv[2] = arg2; } if (flag & F_NORMAL) exitcurses(); pid = xfork(flag); if (pid == 0) { if (dir && chdir(dir) == -1) _exit(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); } execvp(*argv, argv); _exit(1); } else { retstatus = join(pid, flag); DPRINTF_D(pid); if (flag & F_NORMAL) { if (flag & F_CONFIRM) { printf("%s", messages[MSG_CONTINUE]); while (getchar() != '\n'); } refresh(); } free(cmd); } return retstatus; } static void prompt_run(char *cmd, const char *cur, const char *path) { setenv(envs[ENV_NCUR], cur, 1); spawn(shell, "-c", cmd, path, F_CLI | F_CONFIRM); } /* Get program name from env var, else return fallback program */ static char *xgetenv(const char * const name, char *fallback) { char *value = getenv(name); return value && value[0] ? value : fallback; } /* Checks if an env variable is set to 1 */ static bool xgetenv_set(const char *name) { char *value = getenv(name); if (value && value[0] == '1' && !value[1]) return TRUE; return FALSE; } /* Check if a dir exists, IS a dir and is readable */ static bool xdiraccess(const char *path) { DIR *dirp = opendir(path); if (!dirp) { printwarn(NULL); return FALSE; } closedir(dirp); return TRUE; } static void opstr(char *buf, char *op) { snprintf(buf, CMD_LEN_MAX, "xargs -0 sh -c '%s \"$0\" \"$@\" . < /dev/tty' < %s", op, g_selpath); } static void rmmulstr(char *buf) { if (cfg.trash) snprintf(buf, CMD_LEN_MAX, "xargs -0 trash-put < %s", g_selpath); else snprintf(buf, CMD_LEN_MAX, "xargs -0 sh -c 'rm -%cr \"$0\" \"$@\" < /dev/tty' < %s", confirm_force(TRUE), g_selpath); } static void xrm(char *path) { if (cfg.trash) spawn("trash-put", path, NULL, NULL, F_NORMAL); else { char rm_opts[] = "-ir"; rm_opts[1] = confirm_force(FALSE); spawn("rm", rm_opts, path, NULL, F_NORMAL); } } static uint lines_in_file(int fd, char *buf, size_t buflen) { ssize_t len; uint count = 0; while ((len = read(fd, buf, buflen)) > 0) while (len) count += (buf[--len] == '\n'); /* For all use cases 0 linecount is considered as error */ return ((len < 0) ? 0 : count); } static bool cpmv_rename(int choice, const char *path) { int fd; uint count = 0, lines = 0; bool ret = FALSE; char *cmd = (choice == 'c' ? cp : mv); char buf[sizeof(cpmvrenamecmd) + sizeof(cmd) + (PATH_MAX << 1)]; fd = create_tmp_file(); if (fd == -1) return ret; /* selsafe() returned TRUE for this to be called */ if (!selbufpos) { snprintf(buf, sizeof(buf), "tr '\\0' '\\n' < %s > %s", g_selpath, g_tmpfpath); spawn(utils[UTIL_SH_EXEC], buf, NULL, NULL, F_CLI); count = lines_in_file(fd, buf, sizeof(buf)); if (!count) goto finish; } else seltofile(fd, &count); close(fd); snprintf(buf, sizeof(buf), cpmvformatcmd, g_tmpfpath); spawn(utils[UTIL_SH_EXEC], buf, NULL, path, F_CLI); spawn((cfg.waitedit ? enveditor : editor), g_tmpfpath, NULL, path, F_CLI); fd = open(g_tmpfpath, O_RDONLY); if (fd == -1) goto finish; lines = lines_in_file(fd, buf, sizeof(buf)); DPRINTF_U(count); DPRINTF_U(lines); if (!lines || (2 * count != lines)) { DPRINTF_S("num mismatch"); goto finish; } snprintf(buf, sizeof(buf), cpmvrenamecmd, path, g_tmpfpath, cmd); spawn(utils[UTIL_SH_EXEC], buf, NULL, path, F_CLI); ret = TRUE; finish: if (fd >= 0) close(fd); return ret; } static bool cpmvrm_selection(enum action sel, char *path, int *presel) { int r; if (!selsafe()) { *presel = MSGWAIT; return FALSE; } switch (sel) { case SEL_CP: opstr(g_buf, cp); break; case SEL_MV: opstr(g_buf, mv); break; case SEL_CPMVAS: r = get_input(messages[MSG_CP_MV_AS]); if (r != 'c' && r != 'm') { printwait(messages[MSG_INVALID_KEY], presel); return FALSE; } if (!cpmv_rename(r, path)) { printwait(messages[MSG_FAILED], presel); return FALSE; } break; default: /* SEL_RM */ rmmulstr(g_buf); break; } if (sel != SEL_CPMVAS) spawn(utils[UTIL_SH_EXEC], g_buf, NULL, path, F_CLI); /* Clear selection on move or delete */ if (sel != SEL_CP) clearselection(); if (cfg.filtermode) *presel = FILTER; return TRUE; } static bool batch_rename(const char *path) { int fd1, fd2, i; uint count = 0, lines = 0; bool dir = FALSE, ret = FALSE; char foriginal[TMP_LEN_MAX] = {0}; char buf[sizeof(batchrenamecmd) + (PATH_MAX << 1)]; i = get_cur_or_sel(); if (!i) return ret; if (i == 'c') { /* Rename entries in current dir */ selbufpos = 0; dir = TRUE; } fd1 = create_tmp_file(); if (fd1 == -1) return ret; xstrlcpy(foriginal, g_tmpfpath, strlen(g_tmpfpath)+1); fd2 = create_tmp_file(); if (fd2 == -1) { unlink(foriginal); close(fd1); return ret; } if (dir) for (i = 0; i < ndents; ++i) appendfpath(dents[i].name, NAME_MAX); seltofile(fd1, &count); seltofile(fd2, NULL); close(fd2); if (dir) /* Don't retain dir entries in selection */ selbufpos = 0; spawn((cfg.waitedit ? enveditor : editor), g_tmpfpath, NULL, path, F_CLI); /* Reopen file descriptor to get updated contents */ fd2 = open(g_tmpfpath, O_RDONLY); if (fd2 == -1) goto finish; lines = lines_in_file(fd2, buf, sizeof(buf)); DPRINTF_U(count); DPRINTF_U(lines); if (!lines || (count != lines)) { DPRINTF_S("cannot delete files"); goto finish; } snprintf(buf, sizeof(buf), batchrenamecmd, foriginal, g_tmpfpath); spawn(utils[UTIL_SH_EXEC], buf, NULL, path, F_CLI); ret = TRUE; finish: if (fd1 >= 0) close(fd1); unlink(foriginal); if (fd2 >= 0) close(fd2); unlink(g_tmpfpath); return ret; } static void get_archive_cmd(char *cmd, char *archive) { if (getutil(utils[UTIL_ATOOL])) xstrlcpy(cmd, "atool -a", ARCHIVE_CMD_LEN); else if (getutil(utils[UTIL_BSDTAR])) xstrlcpy(cmd, "bsdtar -acvf", ARCHIVE_CMD_LEN); else if (is_suffix(archive, ".zip")) xstrlcpy(cmd, "zip -r", ARCHIVE_CMD_LEN); else xstrlcpy(cmd, "tar -acvf", ARCHIVE_CMD_LEN); } static void archive_selection(const char *cmd, const char *archive, const char *curpath) { /* The 70 comes from the string below */ char *buf = (char *)malloc((70 + strlen(cmd) + strlen(archive) + strlen(curpath) + strlen(g_selpath)) * sizeof(char)); if (!buf) { DPRINTF_S(strerror(errno)); printwarn(NULL); return; } snprintf(buf, CMD_LEN_MAX, #ifdef __linux__ "sed -ze 's|^%s/||' '%s' | xargs -0 %s %s", curpath, g_selpath, cmd, archive); #else "tr '\\0' '\n' < '%s' | sed -e 's|^%s/||' | tr '\n' '\\0' | xargs -0 %s %s", g_selpath, curpath, cmd, archive); #endif spawn(utils[UTIL_SH_EXEC], buf, NULL, curpath, F_CLI); free(buf); } static bool write_lastdir(const char *curpath) { bool ret = TRUE; size_t len = strlen(cfgdir); xstrlcpy(cfgdir + len, "/.lastd", 8); DPRINTF_S(cfgdir); FILE *fp = fopen(cfgdir, "w"); if (fp) { if (fprintf(fp, "cd \"%s\"", curpath) < 0) ret = FALSE; fclose(fp); } else ret = FALSE; return ret; } /* * 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) { char *p1, *p2; ll v1 = strtoll(s1, &p1, 10); ll v2 = strtoll(s2, &p2, 10); /* Check if at least 1 string is numeric */ if (s1 != p1 || s2 != p2) { /* Handle both pure numeric */ if (s1 != p1 && s2 != p2) { if (v2 > v1) return -1; if (v1 > v2) return 1; } /* Only first string non-numeric */ if (s1 == p1) return 1; /* Only second string non-numeric */ if (s2 == p2) return -1; } /* Handle 1. all non-numeric and 2. both same numeric value cases */ #ifndef NOLOCALE return strcoll(s1, s2); #else return strcasecmp(s1, s2); #endif } /* * Version comparison * * The code for version compare is a modified version of the GLIBC * and uClibc implementation of strverscmp(). The source is here: * https://elixir.bootlin.com/uclibc-ng/latest/source/libc/string/strverscmp.c */ /* * Compare S1 and S2 as strings holding indices/version numbers, * returning less than, equal to or greater than zero if S1 is less than, * equal to or greater than S2 (for more info, see the texinfo doc). * * Ignores case. */ static int xstrverscasecmp(const char * const s1, const char * const s2) { const uchar *p1 = (const uchar *)s1; const uchar *p2 = (const uchar *)s2; int state, diff; uchar c1, c2; /* * Symbol(s) 0 [1-9] others * Transition (10) 0 (01) d (00) x */ static const uint8_t next_state[] = { /* state x d 0 */ /* S_N */ S_N, S_I, S_Z, /* S_I */ S_N, S_I, S_I, /* S_F */ S_N, S_F, S_F, /* S_Z */ S_N, S_F, S_Z }; static const int8_t result_type[] __attribute__ ((aligned)) = { /* state x/x x/d x/0 d/x d/d d/0 0/x 0/d 0/0 */ /* S_N */ VCMP, VCMP, VCMP, VCMP, VLEN, VCMP, VCMP, VCMP, VCMP, /* S_I */ VCMP, -1, -1, 1, VLEN, VLEN, 1, VLEN, VLEN, /* S_F */ VCMP, VCMP, VCMP, VCMP, VCMP, VCMP, VCMP, VCMP, VCMP, /* S_Z */ VCMP, 1, 1, -1, VCMP, VCMP, -1, VCMP, VCMP }; if (p1 == p2) return 0; c1 = TOUPPER(*p1); ++p1; c2 = TOUPPER(*p2); ++p2; /* Hint: '0' is a digit too. */ state = S_N + ((c1 == '0') + (xisdigit(c1) != 0)); while ((diff = c1 - c2) == 0) { if (c1 == '\0') return diff; state = next_state[state]; c1 = TOUPPER(*p1); ++p1; c2 = TOUPPER(*p2); ++p2; state += (c1 == '0') + (xisdigit(c1) != 0); } state = result_type[state * 3 + (((c2 == '0') + (xisdigit(c2) != 0)))]; switch (state) { case VCMP: return diff; case VLEN: while (xisdigit(*p1++)) if (!xisdigit(*p2++)) return 1; return xisdigit(*p2) ? -1 : diff; default: return state; } } static int (*namecmpfn)(const char * const s1, const char * const s2) = &xstricmp; /* Return the integer value of a char representing HEX */ static char xchartohex(char c) { if (xisdigit(c)) return c - '0'; if (c >= 'a' && c <= 'f') return c - 'a' + 10; if (c >= 'A' && c <= 'F') return c - 'A' + 10; return c; } static char * (*fnstrstr)(const char *haystack, const char *needle) = &strcasestr; #ifdef PCRE static const unsigned char *tables; static int pcreflags = PCRE_NO_AUTO_CAPTURE | PCRE_EXTENDED | PCRE_CASELESS; #else static int regflags = REG_NOSUB | REG_EXTENDED | REG_ICASE; #endif #ifdef PCRE static int setfilter(pcre **pcrex, const char *filter) { const char *errstr = NULL; int erroffset = 0; *pcrex = pcre_compile(filter, pcreflags, &errstr, &erroffset, tables); return errstr ? -1 : 0; } #else static int setfilter(regex_t *regex, const char *filter) { return regcomp(regex, filter, regflags); } #endif static int visible_re(const fltrexp_t *fltrexp, const char *fname) { #ifdef PCRE return pcre_exec(fltrexp->pcrex, NULL, fname, strlen(fname), 0, 0, NULL, 0) == 0; #else return regexec(fltrexp->regex, fname, 0, NULL, 0) == 0; #endif } static int visible_str(const fltrexp_t *fltrexp, const char *fname) { return fnstrstr(fname, fltrexp->str) != NULL; } static int (*filterfn)(const fltrexp_t *fltr, const char *fname) = &visible_str; static void clearfilter(void) { char *fltr = g_ctx[cfg.curctx].c_fltr; if (fltr[1]) { fltr[REGEX_MAX - 1] = fltr[1]; fltr[1] = '\0'; } } static int entrycmp(const void *va, const void *vb) { const struct entry *pa = (pEntry)va; const struct entry *pb = (pEntry)vb; if ((pb->flags & DIR_OR_LINK_TO_DIR) != (pa->flags & DIR_OR_LINK_TO_DIR)) { if (pb->flags & DIR_OR_LINK_TO_DIR) return 1; return -1; } /* Sort based on specified order */ if (cfg.mtimeorder) { if (pb->t > pa->t) return 1; if (pb->t < pa->t) return -1; } else if (cfg.sizeorder) { if (pb->size > pa->size) return 1; if (pb->size < pa->size) return -1; } else if (cfg.blkorder) { if (pb->blocks > pa->blocks) return 1; if (pb->blocks < pa->blocks) return -1; } else if (cfg.extnorder && !(pb->flags & DIR_OR_LINK_TO_DIR)) { char *extna = xmemrchr((uchar *)pa->name, '.', pa->nlen - 1); char *extnb = xmemrchr((uchar *)pb->name, '.', pb->nlen - 1); if (extna || extnb) { if (!extna) return -1; if (!extnb) return 1; int ret = strcasecmp(extna, extnb); if (ret) return ret; } } return namecmpfn(pa->name, pb->name); } static int reventrycmp(const void *va, const void *vb) { if ((((pEntry)vb)->flags & DIR_OR_LINK_TO_DIR) != (((pEntry)va)->flags & DIR_OR_LINK_TO_DIR)) { if (((pEntry)vb)->flags & DIR_OR_LINK_TO_DIR) return 1; return -1; } return -entrycmp(va, vb); } static int (*entrycmpfn)(const void *va, const void *vb) = &entrycmp; /* * 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) { int c = presel; uint i; #ifdef LINUX_INOTIFY struct inotify_event *event; char inotify_buf[EVENT_BUF_LEN]; memset((void *)inotify_buf, 0x0, EVENT_BUF_LEN); #elif defined(BSD_KQUEUE) struct kevent event_data[NUM_EVENT_SLOTS]; memset((void *)event_data, 0x0, sizeof(struct kevent) * NUM_EVENT_SLOTS); #elif defined(HAIKU_NM) // TODO: Do some Haiku declarations #endif if (c == 0 || c == MSGWAIT) { c = getch(); //DPRINTF_D(c); //DPRINTF_S(keyname(c)); if (c == ERR && presel == MSGWAIT) c = (cfg.filtermode) ? FILTER : CONTROL('L'); else if (c == FILTER || c == CONTROL('L')) /* Clear previous filter when manually starting */ clearfilter(); } 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.selmode && !cfg.blkorder && inotify_wd >= 0 && (idle & 1)) { i = read(inotify_fd, inotify_buf, EVENT_BUF_LEN); if (i > 0) { char *ptr; for (ptr = inotify_buf; ptr + ((struct inotify_event *)ptr)->len < inotify_buf + i; ptr += sizeof(struct inotify_event) + event->len) { event = (struct inotify_event *) ptr; DPRINTF_D(event->wd); DPRINTF_D(event->mask); if (!event->wd) break; if (event->mask & INOTIFY_MASK) { c = CONTROL('L'); DPRINTF_S("issue refresh"); break; } } DPRINTF_S("inotify read done"); } } #elif defined(BSD_KQUEUE) if (!cfg.selmode && !cfg.blkorder && event_fd >= 0 && idle & 1 && kevent(kq, events_to_monitor, NUM_EVENT_SLOTS, event_data, NUM_EVENT_FDS, >imeout) > 0) c = CONTROL('L'); #elif defined(HAIKU_NM) if (!cfg.selmode && !cfg.blkorder && haiku_nm_active && idle & 1 && haiku_is_update_needed(haiku_hnd)) c = CONTROL('L'); #endif } else idle = 0; for (i = 0; i < (int)ELEMENTS(bindings); ++i) if (c == bindings[i].sym) return bindings[i].act; return 0; } static int getorderstr(char *sort) { int i = 0; if (cfg.mtimeorder) sort[0] = cfg.mtime ? 'T' : 'A'; else if (cfg.sizeorder) sort[0] = 'S'; else if (cfg.extnorder) sort[0] = 'E'; if (sort[i]) ++i; if (entrycmpfn == &reventrycmp) { sort[i] = 'R'; ++i; } if (namecmpfn == &xstrverscasecmp) { sort[i] = 'V'; ++i; } if (i) sort[i] = ' '; return i; } static void showfilterinfo(void) { int i = 0; char info[REGEX_MAX] = "\0\0\0\0"; i = getorderstr(info); snprintf(info + i, REGEX_MAX - i - 1, " %s [/], %s [:]", (cfg.regex ? "regex" : "str"), ((fnstrstr == &strcasestr) ? "ic" : "noic")); printinfoln(info); } static void showfilter(char *str) { showfilterinfo(); printprompt(str); } static inline void swap_ent(int id1, int id2) { struct entry _dent, *pdent1 = &dents[id1], *pdent2 = &dents[id2]; *(&_dent) = *pdent1; *pdent1 = *pdent2; *pdent2 = *(&_dent); } #ifdef PCRE static int fill(const char *fltr, pcre *pcrex) #else static int fill(const char *fltr, regex_t *re) #endif { int count = 0; #ifdef PCRE fltrexp_t fltrexp = { .pcrex = pcrex, .str = fltr }; #else fltrexp_t fltrexp = { .regex = re, .str = fltr }; #endif for (; count < ndents; ++count) { if (filterfn(&fltrexp, dents[count].name) == 0) { if (count != --ndents) { swap_ent(count, ndents); --count; } continue; } } return ndents; } static int matches(const char *fltr) { #ifdef PCRE pcre *pcrex = NULL; /* Search filter */ if (cfg.regex && setfilter(&pcrex, fltr)) return -1; ndents = fill(fltr, pcrex); if (cfg.regex) pcre_free(pcrex); #else regex_t re; /* Search filter */ if (cfg.regex && setfilter(&re, fltr)) return -1; ndents = fill(fltr, &re); if (cfg.regex) regfree(&re); #endif qsort(dents, ndents, sizeof(*dents), entrycmpfn); return ndents; } static int filterentries(char *path, char *lastname) { wchar_t *wln = (wchar_t *)alloca(sizeof(wchar_t) * REGEX_MAX); char *ln = g_ctx[cfg.curctx].c_fltr; wint_t ch[2] = {0}; int r, total = ndents, len; char *pln = g_ctx[cfg.curctx].c_fltr + 1; if (ndents && (ln[0] == FILTER || ln[0] == RFILTER) && *pln) { if (matches(pln) != -1) { move_cursor(dentfind(lastname, ndents), 0); redraw(path); } len = mbstowcs(wln, ln, REGEX_MAX); } else { ln[0] = wln[0] = cfg.regex ? RFILTER : FILTER; ln[1] = wln[1] = '\0'; len = 1; } cleartimeout(); curs_set(TRUE); showfilter(ln); while ((r = get_wch(ch)) != ERR) { //DPRINTF_D(*ch); //DPRINTF_S(keyname(*ch)); switch (*ch) { #ifdef KEY_RESIZE case KEY_RESIZE: clearoldprompt(); redraw(path); showfilter(ln); continue; #endif case KEY_DC: // fallthrough case KEY_BACKSPACE: // fallthrough case '\b': // fallthrough case 127: /* handle DEL */ if (len != 1) { wln[--len] = '\0'; wcstombs(ln, wln, REGEX_MAX); ndents = total; } else *ch = CONTROL('L'); // fallthrough case CONTROL('L'): if (*ch == CONTROL('L')) { if (wln[1]) { ln[REGEX_MAX - 1] = ln[1]; ln[1] = wln[1] = '\0'; len = 1; ndents = total; } else if (ln[REGEX_MAX - 1]) { /* Show the previous filter */ ln[1] = ln[REGEX_MAX - 1]; ln[REGEX_MAX - 1] = '\0'; len = mbstowcs(wln, ln, REGEX_MAX); } } /* Go to the top, we don't know if the hovered file will match the filter */ cur = 0; if (matches(pln) != -1) redraw(path); showfilter(ln); continue; case KEY_MOUSE: // fallthrough case 27: /* Exit filter mode on Escape */ goto end; } if (r != OK) /* Handle Fn keys in main loop */ break; /* Handle all control chars in main loop */ if (*ch < ASCII_MAX && keyname(*ch)[0] == '^' && *ch != '^') goto end; if (len == 1) { switch (*ch) { case '=': // fallthrough /* Launch app */ case ']': // fallthorugh /*Prompt key */ case ';': // fallthrough /* Run plugin key */ case ',': // fallthrough /* Pin CWD */ case '?': /* Help and config key, '?' is an invalid regex */ goto end; } /* Toggle case-sensitivity */ if (*ch == CASE) { fnstrstr = (fnstrstr == &strcasestr) ? &strstr : &strcasestr; #ifdef PCRE pcreflags ^= PCRE_CASELESS; #else regflags ^= REG_ICASE; #endif showfilter(ln); continue; } /* toggle string or regex filter */ if (*ch == FILTER) { wln[0] = ln[0] = (ln[0] == FILTER) ? RFILTER : FILTER; cfg.regex ^= 1; filterfn = (filterfn == &visible_str) ? &visible_re : &visible_str; showfilter(ln); continue; } } /* 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) { showfilter(ln); continue; } /* If the only match is a dir, auto-select and cd into it */ if (ndents == 1 && cfg.filtermode && cfg.autoselect && (dents[0].flags & DIR_OR_LINK_TO_DIR)) { *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); showfilter(ln); } end: clearinfoln(); /* Save last working filter in-filter */ if (ln[1]) ln[REGEX_MAX - 1] = ln[1]; if (*ch != 27 && *ch != '\t' && *ch != KEY_UP && *ch != KEY_DOWN && *ch != CONTROL('T')) { ln[0] = ln[1] = '\0'; move_cursor(cur, 0); } /* Save current */ if (ndents) copycurname(); curs_set(FALSE); settimeout(); /* Return keys for navigation etc. */ return *ch; } /* Show a prompt with input string and return the changes */ static char *xreadline(const char *prefill, const char *prompt) { size_t len, pos; int x, r; const int WCHAR_T_WIDTH = sizeof(wchar_t); wint_t ch[2] = {0}; wchar_t * const buf = malloc(sizeof(wchar_t) * READLINE_MAX); if (!buf) errexit(); cleartimeout(); printprompt(prompt); if (prefill) { DPRINTF_S(prefill); len = pos = mbstowcs(buf, prefill, READLINE_MAX); } else len = (size_t)-1; if (len == (size_t)-1) { buf[0] = '\0'; len = pos = 0; } x = getcurx(stdscr); curs_set(TRUE); while (1) { buf[len] = ' '; mvaddnwstr(xlines - 1, x, buf, len + 1); move(xlines - 1, x + wcswidth(buf, pos)); r = get_wch(ch); if (r == ERR) continue; if (r == OK) { switch (*ch) { case KEY_ENTER: // fallthrough case '\n': // fallthrough case '\r': goto END; case CONTROL('D'): if (pos < len) ++pos; else if (!(pos || len)) { /* Exit on ^D at empty prompt */ len = 0; goto END; } else continue; // fallthrough case 127: // fallthrough case '\b': /* rhel25 sends '\b' for backspace */ if (pos > 0) { memmove(buf + pos - 1, buf + pos, (len - pos) * WCHAR_T_WIDTH); --len, --pos; } // fallthrough case '\t': /* TAB breaks cursor position, ignore it */ continue; case CONTROL('F'): if (pos < len) ++pos; continue; case CONTROL('B'): if (pos > 0) --pos; continue; case CONTROL('W'): printprompt(prompt); do { if (pos == 0) break; memmove(buf + pos - 1, buf + pos, (len - pos) * WCHAR_T_WIDTH); --pos, --len; } while (buf[pos - 1] != ' ' && buf[pos - 1] != '/'); // NOLINT continue; case CONTROL('K'): printprompt(prompt); len = pos; continue; case CONTROL('L'): printprompt(prompt); len = pos = 0; continue; case CONTROL('A'): pos = 0; continue; case CONTROL('E'): pos = len; continue; case CONTROL('U'): printprompt(prompt); memmove(buf, buf + pos, (len - pos) * WCHAR_T_WIDTH); len -= pos; pos = 0; continue; case 27: /* Exit prompt on Escape */ len = 0; goto END; } /* Filter out all other control chars */ if (*ch < ASCII_MAX && keyname(*ch)[0] == '^') continue; if (pos < READLINE_MAX - 1) { memmove(buf + pos + 1, buf + pos, (len - pos) * WCHAR_T_WIDTH); buf[pos] = *ch; ++len, ++pos; continue; } } else { switch (*ch) { #ifdef KEY_RESIZE case KEY_RESIZE: clearoldprompt(); xlines = LINES; printprompt(prompt); break; #endif 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) * WCHAR_T_WIDTH); --len, --pos; } break; case KEY_DC: if (pos < len) { memmove(buf + pos, buf + pos + 1, (len - pos - 1) * WCHAR_T_WIDTH); --len; } break; case KEY_END: pos = len; break; case KEY_HOME: pos = 0; break; default: break; } } } END: curs_set(FALSE); settimeout(); clearprompt(); buf[len] = '\0'; pos = wcstombs(g_buf, buf, READLINE_MAX - 1); if (pos >= READLINE_MAX - 1) g_buf[READLINE_MAX - 1] = '\0'; free(buf); return g_buf; } #ifndef NORL /* * Caller should check the value of presel to confirm if it needs to wait to show warning */ static char *getreadline(const char *prompt, char *path, char *curpath, int *presel) { /* Switch to current path for readline(3) */ if (chdir(path) == -1) { printwarn(presel); return NULL; } exitcurses(); char *input = readline(prompt); refresh(); if (chdir(curpath) == -1) printwarn(presel); else if (input && input[0]) { add_history(input); xstrlcpy(g_buf, input, CMD_LEN_MAX); free(input); return g_buf; } free(input); return NULL; } #endif /* * Updates out with "dir/name or "/name" * Returns the number of bytes copied including the terminating NULL byte */ static size_t mkpath(const char *dir, const char *name, char *out) { size_t len; /* Handle absolute path */ if (name[0] == '/') return xstrlcpy(out, name, PATH_MAX); /* Handle root case */ if (istopdir(dir)) len = 1; else len = xstrlcpy(out, dir, PATH_MAX); out[len - 1] = '/'; // NOLINT return (xstrlcpy(out + len, name, PATH_MAX - len) + len); } /* * Create symbolic/hard link(s) to file(s) in selection list * Returns the number of links created, -1 on error */ static int xlink(char *prefix, char *path, char *curfname, char *buf, int *presel, int type) { int count = 0, choice; char *psel = pselbuf, *fname; size_t pos = 0, len, r; int (*link_fn)(const char *, const char *) = NULL; char lnpath[PATH_MAX]; choice = get_cur_or_sel(); if (!choice) return -1; if (type == 's') /* symbolic link */ link_fn = &symlink; else /* hard link */ link_fn = &link; if (choice == 'c') { r = xstrlcpy(buf, prefix, NAME_MAX + 1); /* Copy prefix */ xstrlcpy(buf + r - 1, curfname, NAME_MAX - r); /* Suffix target file name */ mkpath(path, buf, lnpath); /* Generate link path */ mkpath(path, curfname, buf); /* Generate target file path */ if (!link_fn(buf, lnpath)) return 1; /* One link created */ printwarn(presel); return -1; } while (pos < selbufpos) { len = strlen(psel); fname = xbasename(psel); r = xstrlcpy(buf, prefix, NAME_MAX + 1); /* Copy prefix */ xstrlcpy(buf + r - 1, fname, NAME_MAX - r); /* Suffix target file name */ mkpath(path, buf, lnpath); /* Generate link path */ if (!link_fn(psel, lnpath)) ++count; pos += len + 1; psel += len + 1; } return count; } static bool parsekvpair(kv *kvarr, char **envcpy, const char *cfgstr, uchar maxitems, size_t maxlen) { int i = 0; char *nextkey; char *ptr = getenv(cfgstr); if (!ptr || !*ptr) return TRUE; *envcpy = strdup(ptr); ptr = *envcpy; nextkey = ptr; while (*ptr && i < maxitems) { if (ptr == nextkey) { kvarr[i].key = *ptr; if (*++ptr != ':') return FALSE; if (*++ptr == '\0') return FALSE; kvarr[i].val = ptr; ++i; } if (*ptr == ';') { /* Remove trailing space */ if (i > 0 && *(ptr - 1) == '/') *(ptr - 1) = '\0'; *ptr = '\0'; nextkey = ptr + 1; } ++ptr; } if (i < maxitems) { if (*kvarr[i - 1].val == '\0') return FALSE; kvarr[i].key = '\0'; } for (i = 0; i < maxitems && kvarr[i].key; ++i) if (strlen(kvarr[i].val) >= maxlen) return FALSE; return TRUE; } /* * Get the value corresponding to a key * * 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_kv_val(kv *kvarr, char *buf, int key, uchar max, bool path) { int r = 0; for (; kvarr[r].key && r < max; ++r) { if (kvarr[r].key == key) { if (!path) return kvarr[r].val; if (kvarr[r].val[0] == '~') { ssize_t len = strlen(home); ssize_t loclen = strlen(kvarr[r].val); if (!buf) { buf = (char *)malloc(len + loclen); if (!buf) { DPRINTF_S(strerror(errno)); return NULL; } } xstrlcpy(buf, home, len + 1); xstrlcpy(buf + len, kvarr[r].val + 1, loclen); return buf; } return realpath(kvarr[r].val, buf); } } DPRINTF_S("Invalid key"); return NULL; } static void resetdircolor(int flags) { if (cfg.dircolor && !(flags & DIR_OR_LINK_TO_DIR)) { 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; * Max supported str length: NAME_MAX; * * 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, wchar_t **wstr) { static wchar_t wbuf[NAME_MAX + 1] __attribute__ ((aligned)); wchar_t *buf = wbuf; size_t lencount = 0; #ifdef NOLOCALE memset(wbuf, 0, sizeof(wbuf)); #endif /* Convert multi-byte to wide char */ size_t len = mbstowcs(wbuf, str, NAME_MAX); while (*buf && lencount <= maxcols) { if (*buf <= '\x1f' || *buf == '\x7f') *buf = '\?'; ++buf; ++lencount; } len = lencount = wcswidth(wbuf, len); /* Reduce number of wide chars to max columns */ if (len > maxcols) { lencount = maxcols + 1; /* Reduce wide chars one by one till it fits */ while (len > maxcols) len = wcswidth(wbuf, --lencount); wbuf[lencount] = L'\0'; } if (wstr) { *wstr = wbuf; return NULL; } /* Convert wide char to multi-byte */ wcstombs(g_buf, wbuf, NAME_MAX); return g_buf; } static char *coolsize(off_t size) { const char * const U = "BKMGTPEZY"; static char size_buf[12]; /* Buffer to hold human readable size */ off_t rem = 0; size_t ret; int i = 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 && rem) { ret = xstrlcpy(size_buf, xitoa(size), 12); size_buf[ret - 1] = '.'; char *frac = xitoa(rem); size_t toprint = i > 3 ? 3 : i; size_t len = strlen(frac); if (len < toprint) { size_buf[ret] = size_buf[ret + 1] = size_buf[ret + 2] = '0'; xstrlcpy(size_buf + ret + (toprint - len), frac, len + 1); } else xstrlcpy(size_buf + ret, frac, toprint + 1); ret += toprint; } else { ret = xstrlcpy(size_buf, size ? xitoa(size) : "0", 12); --ret; } size_buf[ret] = U[i]; size_buf[ret + 1] = '\0'; return size_buf; } /* Convert a mode field into "ls -l" type perms field. */ static char *get_lsperms(mode_t mode) { static const char * const rwx[] = {"---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx"}; static char bits[11] = {'\0'}; switch (mode & S_IFMT) { case S_IFREG: bits[0] = '-'; break; case S_IFDIR: bits[0] = 'd'; break; case S_IFLNK: bits[0] = 'l'; break; case S_IFSOCK: bits[0] = 's'; break; case S_IFIFO: bits[0] = 'p'; break; case S_IFBLK: bits[0] = 'b'; break; case S_IFCHR: bits[0] = 'c'; break; default: bits[0] = '?'; break; } 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; } static void printent(const struct entry *ent, uint namecols, bool sel) { wchar_t *wstr; char ind = '\0'; switch (ent->mode & S_IFMT) { case S_IFREG: if (ent->mode & 0100) ind = '*'; break; case S_IFDIR: ind = '/'; break; case S_IFLNK: ind = '@'; break; case S_IFSOCK: ind = '='; break; case S_IFIFO: ind = '|'; break; case S_IFBLK: // fallthrough case S_IFCHR: break; default: ind = '?'; break; } if (!ind) ++namecols; unescape(ent->name, namecols, &wstr); /* Directories are always shown on top */ resetdircolor(ent->flags); if (sel) attron(A_REVERSE); addch((ent->flags & FILE_SELECTED) ? '+' : ' '); addwstr(wstr); if (ind) addch(ind); addch('\n'); if (sel) attroff(A_REVERSE); } static void printent_long(const struct entry *ent, uint namecols, bool sel) { char timebuf[24], permbuf[4], ind1 = '\0', ind2[] = "\0\0"; const char cp = (ent->flags & FILE_SELECTED) ? '+' : ' '; /* Timestamp */ strftime(timebuf, sizeof(timebuf), "%F %R", localtime(&ent->t)); timebuf[sizeof(timebuf)-1] = '\0'; /* Permissions */ permbuf[0] = '0' + ((ent->mode >> 6) & 7); permbuf[1] = '0' + ((ent->mode >> 3) & 7); permbuf[2] = '0' + (ent->mode & 7); permbuf[3] = '\0'; /* Add a column if no indicator is needed */ if (S_ISREG(ent->mode) && !(ent->mode & 0100)) ++namecols; /* Trim escape chars from name */ const char *pname = unescape(ent->name, namecols, NULL); /* Directories are always shown on top */ resetdircolor(ent->flags); if (sel) attron(A_REVERSE); switch (ent->mode & S_IFMT) { case S_IFREG: if (ent->mode & 0100) printw("%c%-16.16s %s %8.8s* %s*\n", cp, timebuf, permbuf, coolsize(cfg.blkorder ? ent->blocks << blk_shift : ent->size), pname); else printw("%c%-16.16s %s %8.8s %s\n", cp, timebuf, permbuf, coolsize(cfg.blkorder ? ent->blocks << blk_shift : ent->size), pname); break; case S_IFDIR: printw("%c%-16.16s %s %8.8s %s/\n", cp, timebuf, permbuf, coolsize(cfg.blkorder ? ent->blocks << blk_shift : ent->size), pname); break; case S_IFLNK: printw("%c%-16.16s %s @ %s@\n", cp, timebuf, permbuf, pname); break; case S_IFSOCK: ind1 = ind2[0] = '='; // fallthrough case S_IFIFO: if (!ind1) ind1 = ind2[0] = '|'; // fallthrough case S_IFBLK: if (!ind1) ind1 = 'b'; // fallthrough case S_IFCHR: if (!ind1) ind1 = 'c'; // fallthrough default: if (!ind1) ind1 = ind2[0] = '?'; printw("%c%-16.16s %s %c %s%s\n", cp, timebuf, permbuf, ind1, pname, ind2); break; } if (sel) attroff(A_REVERSE); } static void (*printptr)(const struct entry *ent, uint namecols, bool sel) = &printent; static void savecurctx(settings *curcfg, char *path, char *curname, int r /* next context num */) { settings cfg = *curcfg; bool selmode = cfg.selmode ? TRUE : FALSE; /* Save current context */ xstrlcpy(g_ctx[cfg.curctx].c_name, curname, NAME_MAX + 1); g_ctx[cfg.curctx].c_cfg = cfg; if (g_ctx[r].c_cfg.ctxactive) { /* Switch to saved context */ /* Switch light/detail mode */ if (cfg.showdetail != g_ctx[r].c_cfg.showdetail) /* set the reverse */ printptr = cfg.showdetail ? &printent : &printent_long; 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); g_ctx[r].c_last[0] = '\0'; g_ctx[r].c_name[0] = '\0'; g_ctx[r].c_fltr[0] = g_ctx[r].c_fltr[1] = '\0'; g_ctx[r].c_cfg = cfg; g_ctx[r].c_cfg.runplugin = 0; } /* Continue selection mode */ cfg.selmode = selmode; cfg.curctx = r; *curcfg = cfg; } static void save_session(bool last_session, int *presel) { char spath[PATH_MAX]; int i; session_header_t header; FILE *fsession; char *sname; bool status = FALSE; header.ver = SESSIONS_VERSION; for (i = 0; i < CTX_MAX; ++i) { if (!g_ctx[i].c_cfg.ctxactive) { header.pathln[i] = header.nameln[i] = header.lastln[i] = header.fltrln[i] = 0; } else { if (cfg.curctx == i && ndents) /* Update current file name, arrows don't update it */ xstrlcpy(g_ctx[i].c_name, dents[cur].name, NAME_MAX + 1); header.pathln[i] = strnlen(g_ctx[i].c_path, PATH_MAX) + 1; header.lastln[i] = strnlen(g_ctx[i].c_last, PATH_MAX) + 1; header.nameln[i] = strnlen(g_ctx[i].c_name, NAME_MAX) + 1; header.fltrln[i] = strnlen(g_ctx[i].c_fltr, REGEX_MAX) + 1; } } sname = !last_session ? xreadline(NULL, messages[MSG_SSN_NAME]) : "@"; if (!sname[0]) return; mkpath(sessiondir, sname, spath); fsession = fopen(spath, "wb"); if (!fsession) { printwait(messages[MSG_ACCESS], presel); return; } if ((fwrite(&header, sizeof(header), 1, fsession) != 1) || (fwrite(&cfg, sizeof(cfg), 1, fsession) != 1)) goto END; for (i = 0; i < CTX_MAX; ++i) if ((fwrite(&g_ctx[i].c_cfg, sizeof(settings), 1, fsession) != 1) || (fwrite(&g_ctx[i].color, sizeof(uint), 1, fsession) != 1) || (header.nameln[i] > 0 && fwrite(g_ctx[i].c_name, header.nameln[i], 1, fsession) != 1) || (header.lastln[i] > 0 && fwrite(g_ctx[i].c_last, header.lastln[i], 1, fsession) != 1) || (header.fltrln[i] > 0 && fwrite(g_ctx[i].c_fltr, header.fltrln[i], 1, fsession) != 1) || (header.pathln[i] > 0 && fwrite(g_ctx[i].c_path, header.pathln[i], 1, fsession) != 1)) goto END; status = TRUE; END: fclose(fsession); if (!status) printwait(messages[MSG_FAILED], presel); } static bool load_session(const char *sname, char **path, char **lastdir, char **lastname, bool restore) { char spath[PATH_MAX]; int i = 0; session_header_t header; FILE *fsession; bool has_loaded_dynamically = !(sname || restore); bool status = FALSE; if (!restore) { sname = sname ? sname : xreadline(NULL, messages[MSG_SSN_NAME]); if (!sname[0]) return FALSE; mkpath(sessiondir, sname, spath); } else mkpath(sessiondir, "@", spath); if (has_loaded_dynamically) save_session(TRUE, NULL); fsession = fopen(spath, "rb"); if (!fsession) { printmsg(messages[MSG_ACCESS]); xdelay(XDELAY_INTERVAL_MS); return FALSE; } if ((fread(&header, sizeof(header), 1, fsession) != 1) || (header.ver != SESSIONS_VERSION) || (fread(&cfg, sizeof(cfg), 1, fsession) != 1)) goto END; g_ctx[cfg.curctx].c_name[0] = g_ctx[cfg.curctx].c_last[0] = g_ctx[cfg.curctx].c_fltr[0] = g_ctx[cfg.curctx].c_fltr[1] = '\0'; for (; i < CTX_MAX; ++i) if ((fread(&g_ctx[i].c_cfg, sizeof(settings), 1, fsession) != 1) || (fread(&g_ctx[i].color, sizeof(uint), 1, fsession) != 1) || (header.nameln[i] > 0 && fread(g_ctx[i].c_name, header.nameln[i], 1, fsession) != 1) || (header.lastln[i] > 0 && fread(g_ctx[i].c_last, header.lastln[i], 1, fsession) != 1) || (header.fltrln[i] > 0 && fread(g_ctx[i].c_fltr, header.fltrln[i], 1, fsession) != 1) || (header.pathln[i] > 0 && fread(g_ctx[i].c_path, header.pathln[i], 1, fsession) != 1)) goto END; *path = g_ctx[cfg.curctx].c_path; *lastdir = g_ctx[cfg.curctx].c_last; *lastname = g_ctx[cfg.curctx].c_name; printptr = cfg.showdetail ? &printent_long : &printent; status = TRUE; END: fclose(fsession); if (!status) { printmsg(messages[MSG_FAILED]); xdelay(XDELAY_INTERVAL_MS); } else if (restore) unlink(spath); return status; } /* * 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, const size_t bytes, const char *file, const char *arg1, const char *arg2, const 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]); spawn(pager, NULL, NULL, NULL, F_CLI); _exit(1); } /* In parent */ waitpid(pid, &tmp, 0); close(pipefd[0]); return NULL; } static inline bool getutil(char *util) { return spawn("which", util, NULL, NULL, F_NORMAL | F_NOTRACE) == 0; } static void pipetof(char *cmd, FILE *fout) { FILE *fin = popen(cmd, "r"); if (fin) { while (fgets(g_buf, CMD_LEN_MAX - 1, fin)) fprintf(fout, "%s", g_buf); pclose(fin); } } /* * Follows the stat(1) output closely */ static bool show_stats(const char *fpath, const struct stat *sb) { int fd; FILE *fp; char *p, *begin = g_buf; size_t r; fd = create_tmp_file(); if (fd == -1) return FALSE; r = xstrlcpy(g_buf, "stat \"", PATH_MAX); r += xstrlcpy(g_buf + r - 1, fpath, PATH_MAX); g_buf[r - 2] = '\"'; g_buf[r - 1] = '\0'; DPRINTF_S(g_buf); fp = fdopen(fd, "w"); if (!fp) { close(fd); return FALSE; } pipetof(g_buf, fp); if (S_ISREG(sb->st_mode)) { /* Show file(1) output */ p = get_output(g_buf, CMD_LEN_MAX, "file", "-b", fpath, FALSE); if (p) { fprintf(fp, "\n\n "); while (*p) { if (*p == ',') { *p = '\0'; fprintf(fp, " %s\n", begin); begin = p + 1; } ++p; } fprintf(fp, " %s\n ", begin); /* Show the file mime type */ get_output(g_buf, CMD_LEN_MAX, "file", FILE_MIME_OPTS, fpath, FALSE); fprintf(fp, "%s", g_buf); } } fprintf(fp, "\n"); fclose(fp); close(fd); spawn(pager, g_tmpfpath, NULL, NULL, F_CLI); unlink(g_tmpfpath); return TRUE; } static bool xchmod(const char *fpath, mode_t mode) { /* (Un)set (S_IXUSR | S_IXGRP | S_IXOTH) */ (0100 & mode) ? (mode &= ~0111) : (mode |= 0111); return (chmod(fpath, mode) == 0); } static size_t get_fs_info(const char *path, bool type) { struct statvfs svb; if (statvfs(path, &svb) == -1) return 0; if (type == CAPACITY) return svb.f_blocks << ffs((int)(svb.f_frsize >> 1)); return svb.f_bavail << ffs((int)(svb.f_frsize >> 1)); } /* List or extract archive */ static void handle_archive(char *fpath, const char *dir, char op) { char arg[] = "-tvf"; /* options for tar/bsdtar to list files */ char *util; if (getutil(utils[UTIL_ATOOL])) { util = utils[UTIL_ATOOL]; arg[1] = op; arg[2] = '\0'; } else if (getutil(utils[UTIL_BSDTAR])) { util = utils[UTIL_BSDTAR]; if (op == 'x') arg[1] = op; } else if (is_suffix(fpath, ".zip")) { util = utils[UTIL_UNZIP]; arg[1] = (op == 'l') ? 'v' /* verbose listing */ : '\0'; arg[2] = '\0'; } else { util = utils[UTIL_TAR]; if (op == 'x') arg[1] = op; } if (op == 'x') { /* extract */ spawn(util, arg, fpath, dir, F_NORMAL); } else { /* list */ exitcurses(); get_output(NULL, 0, util, arg, fpath, TRUE); refresh(); } } static char *visit_parent(char *path, char *newpath, int *presel) { char *dir; /* There is no going back */ if (istopdir(path)) { /* Continue in navigate-as-you-type mode, if enabled */ if (cfg.filtermode) *presel = FILTER; return NULL; } /* Use a copy as dirname() may change the string passed */ xstrlcpy(newpath, path, PATH_MAX); dir = dirname(newpath); if (access(dir, R_OK) == -1) { printwarn(presel); return NULL; } return dir; } static void find_accessible_parent(char *path, char *newpath, char *lastname, int *presel) { char *dir; /* Save history */ xstrlcpy(lastname, xbasename(path), NAME_MAX + 1); xstrlcpy(newpath, path, PATH_MAX); while (true) { dir = visit_parent(path, newpath, presel); if (istopdir(path) || istopdir(newpath)) { if (!dir) dir = dirname(newpath); break; } if (!dir) { xstrlcpy(path, newpath, PATH_MAX); continue; } break; } xstrlcpy(path, dir, PATH_MAX); printmsg(messages[MSG_ACCESS]); xdelay(XDELAY_INTERVAL_MS); } /* Create non-existent parents and a file or dir */ static bool xmktree(char *path, bool dir) { char *p = path; char *slash = path; if (!p || !*p) return FALSE; /* Skip the first '/' */ ++p; while (*p != '\0') { if (*p == '/') { slash = p; *p = '\0'; } else { ++p; continue; } /* Create folder from path to '\0' inserted at p */ if (mkdir(path, 0777) == -1 && errno != EEXIST) { #ifdef __HAIKU__ // XDG_CONFIG_HOME contains a directory // that is read-only, but the full path // is writeable. // Try to continue and see what happens. // TODO: Find a more robust solution. if (errno == B_READ_ONLY_DEVICE) goto next; #endif DPRINTF_S("mkdir1!"); DPRINTF_S(strerror(errno)); *slash = '/'; return FALSE; } #ifdef __HAIKU__ next: #endif /* Restore path */ *slash = '/'; ++p; } if (dir) { if (mkdir(path, 0777) == -1 && errno != EEXIST) { DPRINTF_S("mkdir2!"); DPRINTF_S(strerror(errno)); return FALSE; } } else { int fd = open(path, O_CREAT, 0666); if (fd == -1 && errno != EEXIST) { DPRINTF_S("open!"); DPRINTF_S(strerror(errno)); return FALSE; } close(fd); } return TRUE; } static bool archive_mount(char *name, char *path, char *newpath, int *presel) { char *dir, *cmd = utils[UTIL_ARCHIVEMOUNT]; size_t len; if (!getutil(cmd)) { printwait(messages[MSG_UTIL_MISSING], presel); return FALSE; } dir = strdup(name); if (!dir) return FALSE; len = strlen(dir); while (len > 1) if (dir[--len] == '.') { dir[len] = '\0'; break; } DPRINTF_S(dir); /* Create the mount point */ mkpath(cfgdir, dir, newpath); free(dir); if (!xmktree(newpath, TRUE)) { printwarn(presel); return FALSE; } /* Mount archive */ DPRINTF_S(name); DPRINTF_S(newpath); if (spawn(cmd, name, newpath, path, F_NORMAL)) { printwait(messages[MSG_FAILED], presel); return FALSE; } return TRUE; } static bool remote_mount(char *newpath, int *presel) { uchar flag = F_CLI; int opt; char *tmp, *env, *cmd; bool r, s; r = getutil(utils[UTIL_RCLONE]); s = getutil(utils[UTIL_SSHFS]); if (!(r || s)) return FALSE; if (r && s) opt = get_input(messages[MSG_REMOTE_OPTS]); else opt = (!s) ? 'r' : 's'; if (opt == 's') { cmd = utils[UTIL_SSHFS]; env = xgetenv("NNN_SSHFS", cmd); } else if (opt == 'r') { flag |= F_NOWAIT | F_NOTRACE; cmd = utils[UTIL_RCLONE]; env = xgetenv("NNN_RCLONE", "rclone mount"); } else { printwait(messages[MSG_INVALID_KEY], presel); return FALSE; } if (!getutil(cmd)) { printwait(messages[MSG_UTIL_MISSING], presel); return FALSE; } tmp = xreadline(NULL, messages[MSG_HOSTNAME]); if (!tmp[0]) return FALSE; /* Create the mount point */ mkpath(cfgdir, tmp, newpath); if (!xmktree(newpath, TRUE)) { printwarn(presel); return FALSE; } /* Convert "Host" to "Host:" */ size_t len = strlen(tmp); if (tmp[len - 1] != ':') { /* Append ':' if missing */ tmp[len] = ':'; tmp[len + 1] = '\0'; } /* Connect to remote */ if (opt == 's') { if (spawn(env, tmp, newpath, NULL, flag)) { printwait(messages[MSG_FAILED], presel); return FALSE; } } else { spawn(env, tmp, newpath, NULL, flag); printmsg(messages[MSG_RCLONE_DELAY]); xdelay(XDELAY_INTERVAL_MS << 2); /* Set 4 times the usual delay */ } return TRUE; } /* * Unmounts if the directory represented by name is a mount point. * Otherwise, asks for hostname */ static bool unmount(char *name, char *newpath, int *presel, char *currentpath) { #ifdef __APPLE__ static char cmd[] = "umount"; #else static char cmd[] = "fusermount3"; /* Arch Linux utility */ static bool found = FALSE; #endif char *tmp = name; struct stat sb, psb; bool child = FALSE; bool parent = FALSE; #ifndef __APPLE__ /* On Ubuntu it's fusermount */ if (!found && !getutil(cmd)) { cmd[10] = '\0'; found = TRUE; } #endif if (tmp && strcmp(cfgdir, currentpath) == 0) { mkpath(cfgdir, tmp, newpath); child = lstat(newpath, &sb) != -1; parent = lstat(dirname(newpath), &psb) != -1; if (!child && !parent) { *presel = MSGWAIT; return FALSE; } } if (!tmp || !child || !S_ISDIR(sb.st_mode) || (child && parent && sb.st_dev == psb.st_dev)) { tmp = xreadline(NULL, messages[MSG_HOSTNAME]); if (!tmp[0]) return FALSE; } /* Create the mount point */ mkpath(cfgdir, tmp, newpath); if (!xdiraccess(newpath)) { *presel = MSGWAIT; return FALSE; } #ifdef __APPLE__ if (spawn(cmd, newpath, NULL, NULL, F_NORMAL)) { #else if (spawn(cmd, "-u", newpath, NULL, F_NORMAL)) { #endif int r = get_input(messages[MSG_LAZY]); if (r != 'y' && r != 'Y') return FALSE; #ifdef __APPLE__ if (spawn(cmd, "-l", newpath, NULL, F_NORMAL)) { #else if (spawn(cmd, "-uz", newpath, NULL, F_NORMAL)) { #endif printwait(messages[MSG_FAILED], presel); return FALSE; } } return TRUE; } static void lock_terminal(void) { char *tmp = utils[UTIL_LOCKER]; if (!getutil(tmp)) tmp = utils[UTIL_CMATRIX]; spawn(tmp, NULL, NULL, NULL, F_NORMAL); } static void printkv(kv *kvarr, FILE *fp, uchar max) { uchar i = 0; for (; i < max && kvarr[i].key; ++i) fprintf(fp, " %c: %s\n", (char)kvarr[i].key, kvarr[i].val); } static void printkeys(kv *kvarr, char *buf, uchar max) { uchar i = 0; for (; i < max && kvarr[i].key; ++i) { buf[i << 1] = ' '; buf[(i << 1) + 1] = kvarr[i].key; } buf[i << 1] = '\0'; } /* * 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 void show_help(const char *path) { int i, fd; FILE *fp; const char *start, *end; const char helpstr[] = { "0\n" "1NAVIGATION\n" "9Up k Up%-16cPgUp ^U Scroll up\n" "9Dn j Down%-14cPgDn ^D Scroll down\n" "9Lt h Parent%-12c~ ` @ - HOME, /, start, last\n" "5Ret Rt l Open%-20c' First file\n" "9g ^A Top%-18c. F5 Toggle hidden\n" "9G ^E End%-21c0 Lock terminal\n" "9b ^/ Bookmark key%-12c, Pin CWD\n" "a1-4 Context 1-4%-8c(B)Tab Cycle context\n" "c/ Filter%-17c^N Nav-as-you-type toggle\n" "aEsc Exit prompt%-12c^L Redraw/clear prompt\n" "c? Help, conf%-14c+ Toggle proceed on open\n" "cq Quit context%-11c^G QuitCD\n" "b^Q Quit%-20cQ Quit with err\n" "1FILES\n" "9o ^O Open with...%-12cn Create new/link\n" "9f ^F File details%-12cd Detail view toggle\n" "b^R Rename/dup%-14cr Batch rename\n" "cz Archive%-17ce Edit in EDITOR\n" "5Space ^J (Un)select%-11cm ^K Mark range/clear\n" "9p ^P Copy sel here%-11ca Select all\n" "9v ^V Move sel here%-8cw ^W Copy/move sel as\n" "9x ^X Delete%-18cE Edit sel\n" "c* Toggle exe%-0c\n" "1MISC\n" "9; ^S Select plugin%-11c= Launch app\n" "9! ^] Shell%-19c] Cmd prompt\n" "cc Connect remote%-10cu Unmount\n" "9t ^T Sort toggles%-12cs Manage session\n" }; fd = create_tmp_file(); if (fd == -1) return; fp = fdopen(fd, "w"); if (!fp) { close(fd); return; } start = end = helpstr; while (*end) { if (*end == '\n') { snprintf(g_buf, CMD_LEN_MAX, "%*c%.*s", xchartohex(*start), ' ', (int)(end - start), start + 1); fprintf(fp, g_buf, ' '); start = end + 1; } ++end; } fprintf(fp, "\nVOLUME: %s of ", coolsize(get_fs_info(path, FREE))); fprintf(fp, "%s free\n\n", coolsize(get_fs_info(path, CAPACITY))); if (bookmark[0].val) { fprintf(fp, "BOOKMARKS\n"); printkv(bookmark, fp, BM_MAX); fprintf(fp, "\n"); } if (plug[0].val) { fprintf(fp, "PLUGIN KEYS\n"); printkv(plug, fp, PLUGIN_MAX); fprintf(fp, "\n"); } for (i = NNN_OPENER; i <= NNN_TRASH; ++i) { start = getenv(env_cfg[i]); if (start) fprintf(fp, "%s: %s\n", env_cfg[i], start); } if (g_selpath) fprintf(fp, "SELECTION FILE: %s\n", g_selpath); fprintf(fp, "\nv%s\n%s\n", VERSION, GENERAL_INFO); fclose(fp); close(fd); spawn(pager, g_tmpfpath, NULL, NULL, F_CLI); unlink(g_tmpfpath); } static bool run_cmd_as_plugin(const char *path, const char *file, char *newpath, char *runfile) { uchar flags = F_CLI | F_CONFIRM; size_t len; /* Get rid of preceding _ */ ++file; if (!*file) return FALSE; /* Check if GUI flags are to be used */ if (*file == '|') { flags = F_NOTRACE | F_NOWAIT; ++file; if (!*file) return FALSE; } xstrlcpy(newpath, file, PATH_MAX); len = strlen(newpath); if (len > 1 && newpath[len - 1] == '*') { flags &= ~F_CONFIRM; /* Skip user confirmation */ newpath[len - 1] = '\0'; /* Get rid of trailing no confirmation symbol */ --len; } if (is_suffix(newpath, " $nnn")) { /* Set `\0` to clear ' $nnn' suffix */ newpath[len - 5] = '\0'; } else runfile = NULL; spawn(newpath, runfile, NULL, path, flags); return TRUE; } static bool plctrl_init(void) { snprintf(g_buf, CMD_LEN_MAX, "nnn-pipe.%d", getpid()); /* g_tmpfpath is used to generate tmp file names */ g_tmpfpath[g_tmpfplen - 1] = '\0'; mkpath(g_tmpfpath, g_buf, g_pipepath); unlink(g_pipepath); if (mkfifo(g_pipepath, 0600) != 0) return _FAILURE; setenv(env_cfg[NNN_PIPE], g_pipepath, TRUE); return _SUCCESS; } static bool run_selected_plugin(char **path, const char *file, char *newpath, char *runfile, char **lastname, char **lastdir) { int fd; size_t len; if (*file == '_') return run_cmd_as_plugin(*path, file, newpath, runfile); if (!(g_states & STATE_PLUGIN_INIT)) { plctrl_init(); g_states |= STATE_PLUGIN_INIT; } fd = open(g_pipepath, O_RDONLY | O_NONBLOCK); if (fd == -1) return FALSE; /* Generate absolute path to plugin */ mkpath(plugindir, file, newpath); if (runfile && runfile[0]) { xstrlcpy(*lastname, runfile, NAME_MAX); spawn(newpath, *lastname, *path, *path, F_NORMAL); } else spawn(newpath, NULL, *path, *path, F_NORMAL); len = read(fd, g_buf, PATH_MAX); g_buf[len] = '\0'; close(fd); if (len > 1) { int ctx = g_buf[0] - '0'; if (ctx == 0 || ctx == cfg.curctx + 1) { xstrlcpy(*lastdir, *path, PATH_MAX); xstrlcpy(*path, g_buf + 1, PATH_MAX); } else if (ctx >= 1 && ctx <= CTX_MAX) { int r = ctx - 1; g_ctx[r].c_cfg.ctxactive = 0; savecurctx(&cfg, g_buf + 1, dents[cur].name, r); *path = g_ctx[r].c_path; *lastdir = g_ctx[r].c_last; *lastname = g_ctx[r].c_name; } } return TRUE; } static void plugscript(const char *plugin, char *newpath, uchar flags) { mkpath(plugindir, plugin, newpath); if (!access(newpath, X_OK)) spawn(newpath, NULL, NULL, NULL, flags); } static void launch_app(const char *path, char *newpath) { int r = F_NORMAL; char *tmp = newpath; mkpath(plugindir, utils[UTIL_LAUNCH], newpath); if (!(getutil(utils[UTIL_FZF]) || getutil(utils[UTIL_FZY])) || access(newpath, X_OK) < 0) { tmp = xreadline(NULL, messages[MSG_APP_NAME]); r = F_NOWAIT | F_NOTRACE | F_MULTI; } if (tmp && *tmp) // NOLINT spawn(tmp, "0", NULL, path, r); } static int sum_bsize(const char *fpath, const struct stat *sb, int typeflag, struct FTW *ftwbuf) { (void) fpath; (void) ftwbuf; if (sb->st_blocks && (typeflag == FTW_F || typeflag == FTW_D)) ent_blocks += sb->st_blocks; ++num_files; return 0; } static int sum_asize(const char *fpath, const struct stat *sb, int typeflag, struct FTW *ftwbuf) { (void) fpath; (void) ftwbuf; if (sb->st_size && (typeflag == FTW_F || typeflag == FTW_D)) ent_blocks += sb->st_size; ++num_files; return 0; } static void dentfree(void) { free(pnamebuf); free(dents); } static blkcnt_t dirwalk(char *path, struct stat *psb) { static uint open_max; /* Increase current open file descriptor limit */ if (!open_max) open_max = max_openfds(); ent_blocks = 0; tolastln(); addstr(xbasename(path)); addstr(" [^C aborts]\n"); refresh(); if (nftw(path, nftw_fn, open_max, FTW_MOUNT | FTW_PHYS) < 0) { DPRINTF_S("nftw failed"); return cfg.apparentsz ? psb->st_size : psb->st_blocks; } return ent_blocks; } /* Skip self and parent */ static bool selforparent(const char *path) { return path[0] == '.' && (path[1] == '\0' || (path[1] == '.' && path[2] == '\0')); } static int dentfill(char *path, struct entry **dents) { int n = 0, count, flags = 0; ulong num_saved; struct dirent *dp; char *namep, *pnb, *buf = NULL; struct entry *dentp; size_t off = 0, namebuflen = NAMEBUF_INCR; struct stat sb_path, sb; DIR *dirp = opendir(path); if (!dirp) return 0; int fd = dirfd(dirp); if (cfg.blkorder) { num_files = 0; dir_blocks = 0; buf = (char *)alloca(strlen(path) + NAME_MAX + 2); if (fstatat(fd, path, &sb_path, 0) == -1) { closedir(dirp); printwarn(NULL); return 0; } } #if _POSIX_C_SOURCE >= 200112L posix_fadvise(fd, 0, 0, POSIX_FADV_SEQUENTIAL); #endif dp = readdir(dirp); if (!dp) goto exit; #if defined(__sun) || defined(__HAIKU__) flags = AT_SYMLINK_NOFOLLOW; /* no d_type */ #else if (cfg.blkorder || dp->d_type == DT_UNKNOWN) { /* * Optimization added for filesystems which support dirent.d_type * see readdir(3) * Known drawbacks: * - the symlink size is set to 0 * - the modification time of the symlink is set to that of the target file */ flags = AT_SYMLINK_NOFOLLOW; } #endif do { namep = dp->d_name; if (selforparent(namep)) 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) { mkpath(path, namep, buf); dir_blocks += dirwalk(buf, &sb); if (g_states & STATE_INTERRUPTED) { closedir(dirp); return n; } } } else { dir_blocks += (cfg.apparentsz ? sb.st_size : sb.st_blocks); ++num_files; } continue; } if (fstatat(fd, namep, &sb, flags) == -1) { /* List a symlink with target missing */ if (flags || (!flags && fstatat(fd, namep, &sb, AT_SYMLINK_NOFOLLOW) == -1)) { DPRINTF_U(flags); if (!flags) { DPRINTF_S(namep); DPRINTF_S(strerror(errno)); } continue; } } if (n == total_dents) { total_dents += ENTRY_INCR; *dents = xrealloc(*dents, total_dents * sizeof(**dents)); if (!*dents) { free(pnamebuf); closedir(dirp); errexit(); } DPRINTF_P(*dents); } /* If 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) { free(*dents); closedir(dirp); 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; /* Selection 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->t = cfg.mtime ? sb.st_mtime : sb.st_atime; #if !(defined(__sun) || defined(__HAIKU__)) if (!flags && dp->d_type == DT_LNK) { /* Do not add sizes for links */ dentp->mode = (sb.st_mode & ~S_IFMT) | S_IFLNK; dentp->size = g_listpath ? sb.st_size : 0; } else { dentp->mode = sb.st_mode; dentp->size = sb.st_size; } #else dentp->mode = sb.st_mode; dentp->size = sb.st_size; #endif dentp->flags = 0; if (cfg.blkorder) { if (S_ISDIR(sb.st_mode)) { num_saved = num_files + 1; mkpath(path, namep, buf); /* Need to show the disk usage of this dir */ dentp->blocks = dirwalk(buf, &sb); if (sb_path.st_dev == sb.st_dev) // NOLINT dir_blocks += dentp->blocks; else num_files = num_saved; if (g_states & STATE_INTERRUPTED) { closedir(dirp); return n; } } else { dentp->blocks = (cfg.apparentsz ? sb.st_size : sb.st_blocks); dir_blocks += dentp->blocks; ++num_files; } } if (flags) { /* Flag if this is a dir or symlink to a dir */ if (S_ISLNK(sb.st_mode)) { sb.st_mode = 0; fstatat(fd, namep, &sb, 0); } if (S_ISDIR(sb.st_mode)) dentp->flags |= DIR_OR_LINK_TO_DIR; #if !(defined(__sun) || defined(__HAIKU__)) /* no d_type */ } else if (dp->d_type == DT_DIR || (dp->d_type == DT_LNK && S_ISDIR(sb.st_mode))) { dentp->flags |= DIR_OR_LINK_TO_DIR; #endif } ++n; } while ((dp = readdir(dirp))); exit: /* Should never be null */ if (closedir(dirp) == -1) errexit(); return n; } /* * Return the position of the matching entry or 0 otherwise * Note there's no NULL check for fname */ static int dentfind(const char *fname, int n) { int i = 0; for (; i < n; ++i) if (xstrcmp(fname, dents[i].name) == 0) return i; return 0; } static void populate(char *path, char *lastname) { #ifdef DBGMODE struct timespec ts1, ts2; clock_gettime(CLOCK_REALTIME, &ts1); /* Use CLOCK_MONOTONIC on FreeBSD */ #endif ndents = dentfill(path, &dents); if (!ndents) return; qsort(dents, ndents, sizeof(*dents), entrycmpfn); #ifdef DBGMODE clock_gettime(CLOCK_REALTIME, &ts2); DPRINTF_U(ts2.tv_nsec - ts1.tv_nsec); #endif /* Find cur from history */ /* No NULL check for lastname, always points to an array */ if (!*lastname) move_cursor(0, 0); else move_cursor(dentfind(lastname, ndents), 0); // Force full redraw last_curscroll = -1; } static void move_cursor(int target, int ignore_scrolloff) { int delta, scrolloff, onscreen = xlines - 4; last_curscroll = curscroll; target = MAX(0, MIN(ndents - 1, target)); delta = target - cur; last = cur; cur = target; if (!ignore_scrolloff) { scrolloff = MIN(SCROLLOFF, onscreen >> 1); /* * When ignore_scrolloff is 1, the cursor can jump into the scrolloff * margin area, but when ignore_scrolloff is 0, act like a boa * constrictor and squeeze the cursor towards the middle region of the * screen by allowing it to move inward and disallowing it to move * outward (deeper into the scrolloff margin area). */ if (((cur < (curscroll + scrolloff)) && delta < 0) || ((cur > (curscroll + onscreen - scrolloff - 1)) && delta > 0)) curscroll += delta; } curscroll = MIN(curscroll, MIN(cur, ndents - onscreen)); curscroll = MAX(curscroll, MAX(cur - (onscreen - 1), 0)); } static void handle_screen_move(enum action sel) { int onscreen; switch (sel) { case SEL_NEXT: if (ndents && (cfg.rollover || (cur != ndents - 1))) move_cursor((cur + 1) % ndents, 0); break; case SEL_PREV: if (ndents && (cfg.rollover || cur)) move_cursor((cur + ndents - 1) % ndents, 0); break; case SEL_PGDN: onscreen = xlines - 4; move_cursor(curscroll + (onscreen - 1), 1); curscroll += onscreen - 1; break; case SEL_CTRL_D: onscreen = xlines - 4; move_cursor(curscroll + (onscreen - 1), 1); curscroll += onscreen >> 1; break; case SEL_PGUP: // fallthrough onscreen = xlines - 4; move_cursor(curscroll, 1); curscroll -= onscreen - 1; break; case SEL_CTRL_U: onscreen = xlines - 4; move_cursor(curscroll, 1); curscroll -= onscreen >> 1; break; case SEL_HOME: move_cursor(0, 1); break; case SEL_END: move_cursor(ndents - 1, 1); break; default: /* case SEL_FIRST */ { int r = 0; for (; r < ndents; ++r) { if (!(dents[r].flags & DIR_OR_LINK_TO_DIR)) { move_cursor((r) % ndents, 0); break; } } break; } } } static int handle_context_switch(enum action sel, char *newpath) { int r = -1, input; switch (sel) { case SEL_CYCLE: // fallthrough case SEL_CYCLER: /* visit next and previous contexts */ r = cfg.curctx; if (sel == SEL_CYCLE) do r = (r + 1) & ~CTX_MAX; while (!g_ctx[r].c_cfg.ctxactive); else do r = (r + (CTX_MAX - 1)) & (CTX_MAX - 1); while (!g_ctx[r].c_cfg.ctxactive); // fallthrough default: /* SEL_CTXN */ if (sel >= SEL_CTX1) /* CYCLE keys are lesser in value */ r = sel - SEL_CTX1; /* Save the next context id */ if (cfg.curctx == r) { if (sel != SEL_CYCLE) return -1; (r == CTX_MAX - 1) ? (r = 0) : ++r; snprintf(newpath, PATH_MAX, messages[MSG_CREATE_CTX], r + 1); input = get_input(newpath); if (input != 'y' && input != 'Y') return -1; } if (cfg.selmode) lastappendpos = selbufpos; } return r; } static bool set_sort_flags(void) { int r = get_input(messages[MSG_ORDER]); if ((r == 'a' || r == 'd' || r == 'e' || r == 's' || r == 't') && (entrycmpfn == &reventrycmp)) entrycmpfn = &entrycmp; switch (r) { case 'a': /* Apparent du */ cfg.apparentsz ^= 1; if (cfg.apparentsz) { nftw_fn = &sum_asize; cfg.blkorder = 1; blk_shift = 0; } else cfg.blkorder = 0; // fallthrough case 'd': /* Disk usage */ if (r == 'd') { if (!cfg.apparentsz) cfg.blkorder ^= 1; nftw_fn = &sum_bsize; 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.extnorder = 0; clearfilter(); /* Reload directory */ endselection(); /* We are going to reload dir */ break; case 'e': /* File extension */ cfg.extnorder ^= 1; cfg.sizeorder = 0; cfg.mtimeorder = 0; cfg.apparentsz = 0; cfg.blkorder = 0; break; case 'r': /* Reverse sort */ entrycmpfn = (entrycmpfn == &entrycmp) ? &reventrycmp : &entrycmp; break; case 's': /* File size */ cfg.sizeorder ^= 1; cfg.mtimeorder = 0; cfg.apparentsz = 0; cfg.blkorder = 0; cfg.extnorder = 0; break; case 't': /* Modification or access time */ cfg.mtimeorder ^= 1; cfg.sizeorder = 0; cfg.apparentsz = 0; cfg.blkorder = 0; cfg.extnorder = 0; break; case 'v': /* Version */ namecmpfn = (namecmpfn == &xstrverscasecmp) ? &xstricmp : &xstrverscasecmp; break; default: return FALSE; } return TRUE; } static void statusbar(char *path) { int i = 0, extnlen = 0; char *ptr; char buf[24]; pEntry pent = &dents[cur]; if (!ndents) { printmsg("0/0"); return; } /* Get the file extension for regular files */ if (S_ISREG(pent->mode)) { i = (int)(pent->nlen - 1); ptr = xmemrchr((uchar *)pent->name, '.', i); if (ptr) { extnlen = ptr - pent->name; extnlen = i - extnlen; } if (!ptr || extnlen > 5 || extnlen < 2) ptr = "\b"; } else ptr = "\b"; if (cfg.blkorder) { /* du mode */ xstrlcpy(buf, coolsize(dir_blocks << blk_shift), 12); mvprintw(xlines - 1, 0, "%d/%d [%d:%s] %cu:%s free:%s files:%lu %lldB %s", cur + 1, ndents, cfg.selmode, ((g_states & STATE_RANGESEL) ? "*" : (nselected ? xitoa(nselected) : "")), (cfg.apparentsz ? 'a' : 'd'), buf, coolsize(get_fs_info(path, FREE)), num_files, (ll)pent->blocks << blk_shift, ptr); } else { /* light or detail mode */ char sort[] = "\0\0\0\0"; getorderstr(sort); /* Timestamp */ strftime(buf, sizeof(buf), "%F %R", localtime(&pent->t)); buf[sizeof(buf)-1] = '\0'; mvprintw(xlines - 1, 0, "%d/%d [%d:%s] %s%s %s %s %s", cur + 1, ndents, cfg.selmode, ((g_states & STATE_RANGESEL) ? "*" : (nselected ? xitoa(nselected) : "")), sort, buf, get_lsperms(pent->mode), coolsize(pent->size), ptr); } addch('\n'); } static int adjust_cols(int ncols) { /* Calculate the number of cols available to print entry name */ if (cfg.showdetail) { /* Fallback to light mode if less than 35 columns */ if (ncols < 36) { cfg.showdetail ^= 1; printptr = &printent; ncols -= 3; /* Preceding space, indicator, newline */ } else ncols -= 35; } else ncols -= 3; /* Preceding space, indicator, newline */ return ncols; } static void draw_line(char *path, int ncols) { bool dir = FALSE; ncols = adjust_cols(ncols); if (dents[last].flags & DIR_OR_LINK_TO_DIR) { attron(COLOR_PAIR(cfg.curctx + 1) | A_BOLD); dir = TRUE; } move(2 + last - curscroll, 0); printptr(&dents[last], ncols, false); if (dents[cur].flags & DIR_OR_LINK_TO_DIR) { if (!dir) {/* First file is not a directory */ attron(COLOR_PAIR(cfg.curctx + 1) | A_BOLD); dir = TRUE; } } else if (dir) { /* Second file is not a directory */ attroff(COLOR_PAIR(cfg.curctx + 1) | A_BOLD); dir = FALSE; } move(2 + cur - curscroll, 0); printptr(&dents[cur], ncols, true); /* Must reset e.g. no files in dir */ if (dir) attroff(COLOR_PAIR(cfg.curctx + 1) | A_BOLD); statusbar(path); } static void redraw(char *path) { xlines = LINES; xcols = COLS; int ncols = (xcols <= PATH_MAX) ? xcols : PATH_MAX; int onscreen = xlines - 4; int i; char *ptr = path; // Fast redraw if (g_states & STATE_MOVE_OP) { g_states &= ~STATE_MOVE_OP; if (ndents && (last_curscroll == curscroll)) return draw_line(path, ncols); } /* Clear screen */ erase(); /* Enforce scroll/cursor invariants */ move_cursor(cur, 1); /* Fail redraw if < than 10 columns, context info prints 10 chars */ if (ncols < MIN_DISPLAY_COLS) { printmsg(messages[MSG_FEW_COLUMNS]); return; } //DPRINTF_D(cur); DPRINTF_S(path); addch('['); for (i = 0; i < CTX_MAX; ++i) { if (!g_ctx[i].c_cfg.ctxactive) { addch(i + '1'); } else { int attrs = (cfg.curctx != i) ? (COLOR_PAIR(i + 1) | A_BOLD | A_UNDERLINE) /* Active */ : (COLOR_PAIR(i + 1) | A_BOLD | A_REVERSE); /* Current */ attron(attrs); addch(i + '1'); attroff(attrs); } addch(' '); } addstr("\b] "); /* 10 chars printed for contexts - "[1 2 3 4] " */ attron(A_UNDERLINE); /* Print path */ i = (int)strlen(path); if ((i + MIN_DISPLAY_COLS) <= ncols) addnstr(path, ncols - MIN_DISPLAY_COLS); else { char *base = xbasename(path); if ((base - ptr) <= 1) addnstr(path, ncols - MIN_DISPLAY_COLS); else { i = 0; --base; while (ptr < base) { if (*ptr == '/') { i += 2; /* 2 characters added */ if (ncols < i + MIN_DISPLAY_COLS) { base = NULL; /* Can't print more characters */ break; } addch(*ptr); addch(*(++ptr)); } ++ptr; } addnstr(base, ncols - (MIN_DISPLAY_COLS + i)); } } /* Go to first entry */ move(2, 0); attroff(A_UNDERLINE); ncols = adjust_cols(ncols); attron(COLOR_PAIR(cfg.curctx + 1) | A_BOLD); cfg.dircolor = 1; /* Print listing */ for (i = curscroll; i < ndents && i < curscroll + onscreen; ++i) printptr(&dents[i], ncols, i == cur); /* Must reset e.g. no files in dir */ if (cfg.dircolor) { attroff(COLOR_PAIR(cfg.curctx + 1) | A_BOLD); cfg.dircolor = 0; } statusbar(path); } static bool browse(char *ipath, const char *session) { char newpath[PATH_MAX] __attribute__ ((aligned)); char rundir[PATH_MAX] __attribute__ ((aligned)); char runfile[NAME_MAX + 1] __attribute__ ((aligned)); char *path, *lastdir, *lastname, *dir, *tmp, *mark = NULL; enum action sel; struct stat sb; MEVENT event; struct timespec mousetimings[2] = {{.tv_sec = 0, .tv_nsec = 0}, {.tv_sec = 0, .tv_nsec = 0} }; int r = -1, fd, presel, selstartid = 0, selendid = 0; const uchar opener_flags = (cfg.cliopener ? F_CLI : (F_NOTRACE | F_NOWAIT)); bool currentmouse = 1, dir_changed = FALSE; #ifndef DIR_LIMITED_SELECTION ino_t inode = 0; #endif atexit(dentfree); /* setup first context */ if (!session || !load_session(session, &path, &lastdir, &lastname, FALSE)) { xstrlcpy(g_ctx[0].c_path, ipath, PATH_MAX); /* current directory */ path = g_ctx[0].c_path; g_ctx[0].c_last[0] = g_ctx[0].c_name[0] = '\0'; lastdir = g_ctx[0].c_last; /* last visited directory */ lastname = g_ctx[0].c_name; /* last visited filename */ g_ctx[0].c_fltr[0] = g_ctx[0].c_fltr[1] = '\0'; g_ctx[0].c_cfg = cfg; /* current configuration */ } newpath[0] = rundir[0] = runfile[0] = '\0'; presel = cfg.filtermode ? FILTER : 0; dents = xrealloc(dents, total_dents * sizeof(struct entry)); if (!dents) errexit(); /* Allocate buffer to hold names */ pnamebuf = (char *)xrealloc(pnamebuf, NAMEBUF_INCR); if (!pnamebuf) errexit(); begin: /* 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) { DPRINTF_S("directory inaccessible"); find_accessible_parent(path, newpath, lastname, &presel); setdirwatch(); } if (cfg.selmode && lastdir[0]) lastappendpos = selbufpos; #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; } #elif defined(HAIKU_NM) if ((presel == FILTER || dir_changed) && haiku_hnd != NULL) { haiku_stop_watch(haiku_hnd); haiku_nm_active = FALSE; dir_changed = FALSE; } #endif populate(path, lastname); if (g_states & STATE_INTERRUPTED) { g_states &= ~STATE_INTERRUPTED; cfg.apparentsz = 0; cfg.blkorder = 0; blk_shift = BLK_SHIFT_512; presel = CONTROL('L'); } #ifdef LINUX_INOTIFY if (presel != FILTER && inotify_wd == -1) inotify_wd = inotify_add_watch(inotify_fd, path, INOTIFY_MASK); #elif defined(BSD_KQUEUE) if (presel != FILTER && 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); } #elif defined(HAIKU_NM) haiku_nm_active = haiku_watch_dir(haiku_hnd, path) == _SUCCESS; #endif while (1) { redraw(path); /* Display a one-time message */ if (g_listpath && (g_states & STATE_MSG)) { g_states &= ~STATE_MSG; printwait(messages[MSG_IGNORED], &presel); } nochange: /* Exit if parent has exited */ if (getppid() == 1) { free(mark); _exit(0); } /* If CWD is deleted or moved or perms changed, find an accessible parent */ if (access(path, F_OK)) goto begin; /* If STDIN is no longer a tty (closed) we should exit */ if (!isatty(STDIN_FILENO) && !cfg.picker) { free(mark); return _FAILURE; } sel = nextsel(presel); if (presel) presel = 0; switch (sel) { case SEL_CLICK: if (getmouse(&event) != OK) goto nochange; // fallthrough case SEL_BACK: /* Handle clicking on a context at the top */ if (sel == SEL_CLICK && event.bstate == BUTTON1_PRESSED && event.y == 0) { /* Get context from: "[1 2 3 4]..." */ r = event.x >> 1; /* If clicked after contexts, go to parent */ if (r >= CTX_MAX) sel = SEL_BACK; else if (r >= 0 && r < CTX_MAX && r != cfg.curctx) { if (cfg.selmode) lastappendpos = selbufpos; savecurctx(&cfg, path, dents[cur].name, r); /* Reset the pointers */ path = g_ctx[r].c_path; lastdir = g_ctx[r].c_last; lastname = g_ctx[r].c_name; setdirwatch(); goto begin; } } if (sel == SEL_BACK) { dir = visit_parent(path, newpath, &presel); if (!dir) goto nochange; /* Save last working directory */ xstrlcpy(lastdir, path, PATH_MAX); /* Save history */ xstrlcpy(lastname, xbasename(path), NAME_MAX + 1); clearfilter(); xstrlcpy(path, dir, PATH_MAX); setdirwatch(); goto begin; } #if NCURSES_MOUSE_VERSION > 1 /* Scroll up */ if (event.bstate == BUTTON4_PRESSED && ndents && (cfg.rollover || cur)) { move_cursor((cur + ndents - 1) % ndents, 0); break; } /* Scroll down */ if (event.bstate == BUTTON5_PRESSED && ndents && (cfg.rollover || (cur != ndents - 1))) { move_cursor((cur + 1) % ndents, 0); break; } #endif /* Toggle filter mode on left click on last 2 lines */ if (event.y >= xlines - 2 && event.bstate == BUTTON1_PRESSED) { clearfilter(); cfg.filtermode ^= 1; if (cfg.filtermode) { presel = FILTER; goto nochange; } /* Start watching the directory */ dir_changed = TRUE; if (ndents) copycurname(); goto begin; } /* Handle clicking on a file */ if (event.y >= 2 && event.y <= ndents + 1 && event.bstate == BUTTON1_PRESSED) { r = curscroll + (event.y - 2); move_cursor(r, 1); currentmouse ^= 1; clock_gettime( #if defined(CLOCK_MONOTONIC_RAW) CLOCK_MONOTONIC_RAW, #elif defined(CLOCK_MONOTONIC) CLOCK_MONOTONIC, #else CLOCK_REALTIME, #endif &mousetimings[currentmouse]); /*Single click just selects, double click also opens */ if (((_ABSSUB(mousetimings[0].tv_sec, mousetimings[1].tv_sec) << 30) + (mousetimings[0].tv_nsec - mousetimings[1].tv_nsec)) > DOUBLECLICK_INTERVAL_NS) break; mousetimings[currentmouse].tv_sec = 0; } else { if (cfg.filtermode) presel = FILTER; goto nochange; // fallthrough } case SEL_NAV_IN: // fallthrough case SEL_GOIN: /* Cannot descend in empty directories */ if (!ndents) goto begin; mkpath(path, dents[cur].name, newpath); DPRINTF_S(newpath); /* Cannot use stale data in entry, file may be missing by now */ if (stat(newpath, &sb) == -1) { printwarn(&presel); goto nochange; } DPRINTF_U(sb.st_mode); switch (sb.st_mode & S_IFMT) { case S_IFDIR: if (access(newpath, R_OK) == -1) { printwarn(&presel); goto nochange; } /* Save last working directory */ xstrlcpy(lastdir, path, PATH_MAX); xstrlcpy(path, newpath, PATH_MAX); lastname[0] = '\0'; clearfilter(); setdirwatch(); goto begin; case S_IFREG: { /* If opened as vim plugin and Enter/^M pressed, pick */ if (cfg.picker && sel == SEL_GOIN) { appendfpath(newpath, mkpath(path, dents[cur].name, newpath)); writesel(pselbuf, selbufpos - 1); free(mark); return _SUCCESS; } /* If open file is disabled on right arrow or `l`, return */ if (cfg.nonavopen && sel == SEL_NAV_IN) goto nochange; /* Handle plugin selection mode */ if (cfg.runplugin) { cfg.runplugin = 0; /* Must be in plugin dir and same context to select plugin */ if ((cfg.runctx == cfg.curctx) && !strcmp(path, plugindir)) { endselection(); /* Copy path so we can return back to earlier dir */ xstrlcpy(path, rundir, PATH_MAX); rundir[0] = '\0'; if (!run_selected_plugin(&path, dents[cur].name, newpath, runfile, &lastname, &lastdir)) { DPRINTF_S("plugin failed!"); } if (runfile[0]) runfile[0] = '\0'; clearfilter(); setdirwatch(); goto begin; } } if (cfg.useeditor && (!sb.st_size || #ifdef FILE_MIME_OPTS (get_output(g_buf, CMD_LEN_MAX, "file", FILE_MIME_OPTS, newpath, FALSE) && !strncmp(g_buf, "text/", 5)))) { #else /* no mime option; guess from description instead */ (get_output(g_buf, CMD_LEN_MAX, "file", "-b", newpath, FALSE) && strstr(g_buf, "text")))) { #endif spawn(editor, newpath, NULL, path, F_CLI); continue; } if (!sb.st_size) { printwait(messages[MSG_EMPTY_FILE], &presel); goto nochange; } #ifdef PCRE if (!pcre_exec(archive_pcre, NULL, dents[cur].name, strlen(dents[cur].name), 0, 0, NULL, 0)) { #else if (!regexec(&archive_re, dents[cur].name, 0, NULL, 0)) { #endif r = get_input(messages[MSG_ARCHIVE_OPTS]); if (r == 'l' || r == 'x') { mkpath(path, dents[cur].name, newpath); handle_archive(newpath, path, r); copycurname(); goto begin; } if (r == 'm') { if (archive_mount(dents[cur].name, path, newpath, &presel)) { lastname[0] = '\0'; /* Save last working directory */ xstrlcpy(lastdir, path, PATH_MAX); /* Switch to mount point */ xstrlcpy(path, newpath, PATH_MAX); setdirwatch(); goto begin; } else { printwait(messages[MSG_FAILED], &presel); goto nochange; } } if (r != 'd') { printwait(messages[MSG_INVALID_KEY], &presel); goto nochange; } } /* Invoke desktop opener as last resort */ spawn(opener, newpath, NULL, NULL, opener_flags); /* Move cursor to the next entry if not the last entry */ if ((g_states & STATE_AUTONEXT) && cur != ndents - 1) move_cursor((cur + 1) % ndents, 0); continue; } default: printwait(messages[MSG_UNSUPPORTED], &presel); goto nochange; } case SEL_NEXT: // fallthrough case SEL_PREV: // fallthrough case SEL_PGDN: // fallthrough case SEL_CTRL_D: // fallthrough case SEL_PGUP: // fallthrough case SEL_CTRL_U: // fallthrough case SEL_HOME: // fallthrough case SEL_END: // fallthrough case SEL_FIRST: g_states |= STATE_MOVE_OP; handle_screen_move(sel); break; case SEL_CDHOME: // fallthrough case SEL_CDBEGIN: // fallthrough case SEL_CDLAST: // fallthrough case SEL_CDROOT: switch (sel) { case SEL_CDHOME: dir = home; break; case SEL_CDBEGIN: dir = ipath; break; case SEL_CDLAST: dir = lastdir; break; default: /* SEL_CDROOT */ dir = "/"; break; } if (!dir || !*dir) { printwait(messages[MSG_NOT_SET], &presel); goto nochange; } if (!xdiraccess(dir)) { presel = MSGWAIT; goto nochange; } if (strcmp(path, dir) == 0) goto nochange; /* SEL_CDLAST: dir pointing to lastdir */ xstrlcpy(newpath, dir, PATH_MAX); /* Save last working directory */ xstrlcpy(lastdir, path, PATH_MAX); xstrlcpy(path, newpath, PATH_MAX); lastname[0] = '\0'; clearfilter(); DPRINTF_S(path); setdirwatch(); goto begin; case SEL_BOOKMARK: r = xstrlcpy(g_buf, messages[MSG_BOOKMARK_KEYS], CMD_LEN_MAX); if (mark) { /* There is a pinned directory */ g_buf[--r] = ' '; g_buf[++r] = ','; g_buf[++r] = '\0'; ++r; } printkeys(bookmark, g_buf + r - 1, BM_MAX); printprompt(g_buf); fd = get_input(NULL); r = FALSE; if (fd == ',') /* Visit pinned directory */ mark ? xstrlcpy(newpath, mark, PATH_MAX) : (r = MSG_NOT_SET); else if (!get_kv_val(bookmark, newpath, fd, BM_MAX, TRUE)) r = MSG_INVALID_KEY; if (!r && !xdiraccess(newpath)) r = MSG_ACCESS; if (r) { printwait(messages[r], &presel); goto nochange; } if (strcmp(path, newpath) == 0) break; lastname[0] = '\0'; clearfilter(); /* 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_CYCLE: // fallthrough case SEL_CYCLER: // fallthrough case SEL_CTX1: // fallthrough case SEL_CTX2: // fallthrough case SEL_CTX3: // fallthrough case SEL_CTX4: r = handle_context_switch(sel, newpath); if (r < 0) continue; savecurctx(&cfg, path, dents[cur].name, r); /* Reset the pointers */ path = g_ctx[r].c_path; lastdir = g_ctx[r].c_last; lastname = g_ctx[r].c_name; setdirwatch(); goto begin; case SEL_PIN: free(mark); mark = strdup(path); printwait(mark, &presel); goto nochange; case SEL_FLTR: /* Unwatch dir if we are still in a filtered view */ #ifdef LINUX_INOTIFY if (inotify_wd >= 0) { inotify_rm_watch(inotify_fd, inotify_wd); inotify_wd = -1; } #elif defined(BSD_KQUEUE) if (event_fd >= 0) { close(event_fd); event_fd = -1; } #elif defined(HAIKU_NM) if (haiku_nm_active) { haiku_stop_watch(haiku_hnd); haiku_nm_active = FALSE; } #endif presel = filterentries(path, lastname); if (presel == 27) { presel = 0; break; } goto nochange; case SEL_MFLTR: // fallthrough case SEL_HIDDEN: // fallthrough case SEL_DETAIL: // fallthrough case SEL_SORT: switch (sel) { case SEL_MFLTR: cfg.filtermode ^= 1; if (cfg.filtermode) { presel = FILTER; clearfilter(); goto nochange; } /* Start watching the directory */ dir_changed = TRUE; break; case SEL_HIDDEN: cfg.showhidden ^= 1; if (ndents) copycurname(); if (cfg.filtermode) presel = FILTER; clearfilter(); goto begin; case SEL_DETAIL: cfg.showdetail ^= 1; cfg.showdetail ? (printptr = &printent_long) : (printptr = &printent); cfg.blkorder = 0; continue; default: /* SEL_SORT */ if (!set_sort_flags()) { if (cfg.filtermode) presel = FILTER; printwait(messages[MSG_INVALID_KEY], &presel); goto nochange; } } if (cfg.filtermode) presel = FILTER; /* Save current */ if (ndents) copycurname(); /* If there's no filter, reload the directory */ if (!g_ctx[cfg.curctx].c_fltr[1]) goto begin; presel = FILTER; /* If there's a filter, apply it */ break; case SEL_STATS: // fallthrough case SEL_CHMODX: if (ndents) { tmp = (g_listpath && xstrcmp(path, g_listpath) == 0) ? g_prefixpath : path; mkpath(tmp, dents[cur].name, newpath); if (lstat(newpath, &sb) == -1 || (sel == SEL_STATS && !show_stats(newpath, &sb)) || (sel == SEL_CHMODX && !xchmod(newpath, sb.st_mode))) { printwarn(&presel); goto nochange; } if (sel == SEL_CHMODX) dents[cur].mode ^= 0111; } break; case SEL_REDRAW: // fallthrough case SEL_RENAMEMUL: // fallthrough case SEL_HELP: // fallthrough case SEL_EDIT: // fallthrough case SEL_LOCK: { bool refresh = FALSE; if (ndents) mkpath(path, dents[cur].name, newpath); switch (sel) { case SEL_REDRAW: refresh = TRUE; break; case SEL_RENAMEMUL: endselection(); if (!batch_rename(path)) { printwait(messages[MSG_FAILED], &presel); goto nochange; } refresh = TRUE; break; case SEL_HELP: show_help(path); if (cfg.filtermode) presel = FILTER; continue; case SEL_EDIT: spawn(editor, dents[cur].name, NULL, path, F_CLI); continue; default: /* SEL_LOCK */ lock_terminal(); break; } /* In case of successful operation, reload contents */ /* Continue in navigate-as-you-type mode, if enabled */ if (cfg.filtermode && !refresh) break; /* Save current */ if (ndents) copycurname(); /* Repopulate as directory content may have changed */ goto begin; } case SEL_SEL: if (!ndents) goto nochange; startselection(); if (g_states & STATE_RANGESEL) g_states &= ~STATE_RANGESEL; /* Toggle selection status */ dents[cur].flags ^= FILE_SELECTED; if (dents[cur].flags & FILE_SELECTED) { ++nselected; appendfpath(newpath, mkpath(path, dents[cur].name, newpath)); writesel(pselbuf, selbufpos - 1); /* Truncate NULL from end */ } else { selbufpos = lastappendpos; if (--nselected) { updateselbuf(path, newpath); writesel(pselbuf, selbufpos - 1); /* Truncate NULL from end */ } else writesel(NULL, 0); } if (cfg.x11) plugscript(utils[UTIL_CBCP], newpath, F_NOWAIT | F_NOTRACE); if (!nselected) unlink(g_selpath); /* move cursor to the next entry if this is not the last entry */ if (!cfg.picker && cur != ndents - 1) move_cursor((cur + 1) % ndents, 0); break; case SEL_SELMUL: if (!ndents) goto nochange; startselection(); g_states ^= STATE_RANGESEL; if (stat(path, &sb) == -1) { printwarn(&presel); goto nochange; } if (g_states & STATE_RANGESEL) { /* Range selection started */ #ifndef DIR_LIMITED_SELECTION inode = sb.st_ino; #endif selstartid = cur; continue; } #ifndef DIR_LIMITED_SELECTION if (inode != sb.st_ino) { printwait(messages[MSG_DIR_CHANGED], &presel); goto nochange; } #endif if (cur < selstartid) { selendid = selstartid; selstartid = cur; } else selendid = cur; /* Clear selection on repeat on same file */ if (selstartid == selendid) { resetselind(); clearselection(); break; } // fallthrough case SEL_SELALL: if (sel == SEL_SELALL) { if (!ndents) goto nochange; startselection(); if (g_states & STATE_RANGESEL) g_states &= ~STATE_RANGESEL; selstartid = 0; selendid = ndents - 1; } /* Remember current selection buffer position */ for (r = selstartid; r <= selendid; ++r) if (!(dents[r].flags & FILE_SELECTED)) { /* Write the path to selection file to avoid flush */ appendfpath(newpath, mkpath(path, dents[r].name, newpath)); dents[r].flags |= FILE_SELECTED; ++nselected; } writesel(pselbuf, selbufpos - 1); /* Truncate NULL from end */ if (cfg.x11) plugscript(utils[UTIL_CBCP], newpath, F_NOWAIT | F_NOTRACE); continue; case SEL_SELEDIT: r = editselection(); if (r <= 0) { r = !r ? MSG_0_SELECTED : MSG_FAILED; printwait(messages[r], &presel); } else { if (cfg.x11) plugscript(utils[UTIL_CBCP], newpath, F_NOWAIT | F_NOTRACE); cfg.filtermode ? presel = FILTER : statusbar(path); } goto nochange; case SEL_CP: // fallthrough case SEL_MV: // fallthrough case SEL_CPMVAS: // fallthrough case SEL_RM: { if (sel == SEL_RM) { r = get_cur_or_sel(); if (!r) { statusbar(path); goto nochange; } if (r == 'c') { tmp = (g_listpath && xstrcmp(path, g_listpath) == 0) ? g_prefixpath : path; mkpath(tmp, dents[cur].name, newpath); xrm(newpath); if (cur && access(newpath, F_OK) == -1) { move_cursor(cur - 1, 0); copycurname(); } else lastname[0] = '\0'; if (cfg.filtermode) presel = FILTER; goto begin; } } endselection(); if (!cpmvrm_selection(sel, path, &presel)) goto nochange; clearfilter(); /* Show notification on operation complete */ if (cfg.x11) plugscript(utils[UTIL_NTFY], newpath, F_NOWAIT | F_NOTRACE); if (ndents) copycurname(); goto begin; } case SEL_ARCHIVE: // fallthrough case SEL_OPENWITH: // fallthrough case SEL_NEW: // fallthrough case SEL_RENAME: { int dup = 'n'; if (!ndents && (sel == SEL_OPENWITH || sel == SEL_RENAME)) break; if (sel != SEL_OPENWITH) endselection(); switch (sel) { case SEL_ARCHIVE: r = get_cur_or_sel(); if (!r) { statusbar(path); goto nochange; } if (r == 's') { if (!selsafe()) { presel = MSGWAIT; goto nochange; } tmp = NULL; } else tmp = dents[cur].name; tmp = xreadline(tmp, messages[MSG_ARCHIVE_NAME]); break; case SEL_OPENWITH: #ifdef NORL tmp = xreadline(NULL, messages[MSG_OPEN_WITH]); #else presel = 0; tmp = getreadline(messages[MSG_OPEN_WITH], path, ipath, &presel); if (presel == MSGWAIT) goto nochange; #endif break; case SEL_NEW: r = get_input(messages[MSG_NEW_OPTS]); if (r == 'f' || r == 'd') tmp = xreadline(NULL, messages[MSG_REL_PATH]); else if (r == 's' || r == 'h') tmp = xreadline(NULL, messages[MSG_LINK_PREFIX]); else tmp = NULL; break; default: /* SEL_RENAME */ tmp = xreadline(dents[cur].name, ""); break; } if (!tmp || !*tmp) break; /* Allow only relative, same dir paths */ if (tmp[0] == '/' || ((r != 'f' && r != 'd') && (xstrcmp(xbasename(tmp), tmp) != 0))) { printwait(messages[MSG_NO_TRAVERSAL], &presel); goto nochange; } switch (sel) { case SEL_ARCHIVE: if (r == 'c' && strcmp(tmp, dents[cur].name) == 0) goto nochange; mkpath(path, tmp, newpath); if (access(newpath, F_OK) == 0) { fd = get_input(messages[MSG_OVERWRITE]); if (fd != 'y' && fd != 'Y') { statusbar(path); goto nochange; } } get_archive_cmd(newpath, tmp); (r == 's') ? archive_selection(newpath, tmp, path) : spawn(newpath, tmp, dents[cur].name, path, F_NORMAL | F_MULTI); // fallthrough case SEL_OPENWITH: if (sel == SEL_OPENWITH) { /* Confirm if app is CLI or GUI */ r = get_input(messages[MSG_CLI_MODE]); r = (r == 'c' ? F_CLI : (r == 'g' ? F_NOWAIT | F_NOTRACE | F_MULTI : 0)); if (!r) { cfg.filtermode ? presel = FILTER : statusbar(path); goto nochange; } mkpath(path, dents[cur].name, newpath); spawn(tmp, newpath, NULL, path, r); } if (cfg.filtermode) presel = FILTER; copycurname(); goto begin; case SEL_RENAME: /* Skip renaming to same name */ if (strcmp(tmp, dents[cur].name) == 0) { tmp = xreadline(dents[cur].name, messages[MSG_COPY_NAME]); if (strcmp(tmp, dents[cur].name) == 0) goto nochange; dup = 'd'; } break; default: /* SEL_NEW */ break; } /* Open the descriptor to currently open directory */ #ifdef O_DIRECTORY fd = open(path, O_RDONLY | O_DIRECTORY); #else fd = open(path, O_RDONLY); #endif if (fd == -1) { printwarn(&presel); 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? */ r = get_input(messages[MSG_OVERWRITE]); if (r != 'y' && r != 'Y') { close(fd); break; } } else { /* Do nothing in case of NEW */ close(fd); printwait(messages[MSG_EXISTS], &presel); goto nochange; } } if (sel == SEL_RENAME) { /* Rename the file */ if (dup == 'd') spawn("cp -rp", dents[cur].name, tmp, path, F_SILENT); else if (renameat(fd, dents[cur].name, fd, tmp) != 0) { close(fd); printwarn(&presel); goto nochange; } close(fd); xstrlcpy(lastname, tmp, NAME_MAX + 1); } else { /* SEL_NEW */ close(fd); /* Use fd as tmp var */ presel = 0; /* Check if it's a dir or file */ if (r == 'f') { mkpath(path, tmp, newpath); fd = xmktree(newpath, FALSE); } else if (r == 'd') { mkpath(path, tmp, newpath); fd = xmktree(newpath, TRUE); } else if (r == 's' || r == 'h') { if (tmp[0] == '@' && tmp[1] == '\0') tmp[0] = '\0'; fd = xlink(tmp, path, (ndents ? dents[cur].name : NULL), newpath, &presel, r); } if (!fd) printwait(messages[MSG_FAILED], &presel); if (fd <= 0) goto nochange; if (r == 'f' || r == 'd') xstrlcpy(lastname, tmp, NAME_MAX + 1); else if (ndents) { if (cfg.filtermode) presel = FILTER; copycurname(); } } goto begin; } case SEL_PLUGIN: /* Check if directory is accessible */ if (!xdiraccess(plugindir)) { printwarn(&presel); goto nochange; } r = xstrlcpy(g_buf, messages[MSG_PLUGIN_KEYS], CMD_LEN_MAX); printkeys(plug, g_buf + r - 1, PLUGIN_MAX); printprompt(g_buf); r = get_input(NULL); if (r != '\r') { endselection(); tmp = get_kv_val(plug, NULL, r, PLUGIN_MAX, FALSE); if (!tmp) { printwait(messages[MSG_INVALID_KEY], &presel); goto nochange; } if (tmp[0] == '-' && tmp[1]) { ++tmp; r = FALSE; /* Do not refresh dir after completion */ } else r = TRUE; if (!run_selected_plugin(&path, tmp, newpath, (ndents ? dents[cur].name : NULL), &lastname, &lastdir)) { printwait(messages[MSG_FAILED], &presel); goto nochange; } if (!r) { cfg.filtermode ? presel = FILTER : statusbar(path); goto nochange; } if (ndents) copycurname(); } else { /* 'Return/Enter' enters the plugin directory */ cfg.runplugin ^= 1; if (!cfg.runplugin && rundir[0]) { /* * If toggled, and still in the plugin dir, * switch to original directory */ if (strcmp(path, plugindir) == 0) { xstrlcpy(path, rundir, PATH_MAX); xstrlcpy(lastname, runfile, NAME_MAX); rundir[0] = runfile[0] = '\0'; setdirwatch(); goto begin; } /* Otherwise, initiate choosing plugin again */ cfg.runplugin = 1; } xstrlcpy(rundir, path, PATH_MAX); xstrlcpy(path, plugindir, PATH_MAX); if (ndents) xstrlcpy(runfile, dents[cur].name, NAME_MAX); cfg.runctx = cfg.curctx; lastname[0] = '\0'; } setdirwatch(); clearfilter(); goto begin; case SEL_SHELL: // fallthrough case SEL_LAUNCH: // fallthrough case SEL_RUNCMD: endselection(); switch (sel) { case SEL_SHELL: setenv(envs[ENV_NCUR], (ndents ? dents[cur].name : ""), 1); spawn(shell, NULL, NULL, path, F_CLI); break; case SEL_LAUNCH: launch_app(path, newpath); if (cfg.filtermode) presel = FILTER; goto nochange; default: /* SEL_RUNCMD */ #ifndef NORL if (cfg.picker) { #endif tmp = xreadline(NULL, ">>> "); #ifndef NORL } else { presel = 0; tmp = getreadline(">>> ", path, ipath, &presel); if (presel == MSGWAIT) goto nochange; } #endif if (tmp && *tmp) // NOLINT prompt_run(tmp, (ndents ? dents[cur].name : ""), path); else goto nochange; } /* Continue in navigate-as-you-type mode, if enabled */ if (cfg.filtermode) presel = FILTER; /* Save current */ if (ndents) copycurname(); /* Repopulate as directory content may have changed */ goto begin; case SEL_REMOTE: if (sel == SEL_REMOTE && !remote_mount(newpath, &presel)) goto nochange; lastname[0] = '\0'; /* Save last working directory */ xstrlcpy(lastdir, path, PATH_MAX); /* Switch to mount point */ xstrlcpy(path, newpath, PATH_MAX); setdirwatch(); goto begin; case SEL_UMOUNT: tmp = ndents ? dents[cur].name : NULL; unmount(tmp, newpath, &presel, path); goto nochange; case SEL_SESSIONS: r = get_input(messages[MSG_SSN_OPTS]); if (r == 's') save_session(FALSE, &presel); else if (r == 'l' || r == 'r') { if (load_session(NULL, &path, &lastdir, &lastname, r == 'r')) { setdirwatch(); goto begin; } } statusbar(path); goto nochange; case SEL_AUTONEXT: g_states ^= STATE_AUTONEXT; goto nochange; case SEL_QUITCTX: // fallthrough case SEL_QUITCD: // fallthrough case SEL_QUIT: case SEL_QUITFAIL: if (sel == SEL_QUITCTX) { fd = cfg.curctx; /* fd used as tmp var */ for (r = (fd + 1) & ~CTX_MAX; (r != fd) && !g_ctx[r].c_cfg.ctxactive; r = ((r + 1) & ~CTX_MAX)) { }; if (r != fd) { bool selmode = cfg.selmode ? TRUE : FALSE; g_ctx[fd].c_cfg.ctxactive = 0; /* Switch to next active context */ path = g_ctx[r].c_path; lastdir = g_ctx[r].c_last; lastname = g_ctx[r].c_name; /* Switch light/detail mode */ if (cfg.showdetail != g_ctx[r].c_cfg.showdetail) /* Set the reverse */ printptr = cfg.showdetail ? &printent : &printent_long; cfg = g_ctx[r].c_cfg; /* Continue selection mode */ cfg.selmode = selmode; cfg.curctx = r; setdirwatch(); goto begin; } } else if (!cfg.forcequit) { for (r = 0; r < CTX_MAX; ++r) if (r != cfg.curctx && g_ctx[r].c_cfg.ctxactive) { r = get_input(messages[MSG_QUIT_ALL]); break; } if (!(r == CTX_MAX || r == 'y' || r == 'Y')) break; // fallthrough } /* CD on Quit */ /* In vim picker mode, clear selection and exit */ /* Picker mode: reset buffer or clear file */ if (sel == SEL_QUITCD || getenv("NNN_TMPFILE")) cfg.picker ? selbufpos = 0 : write_lastdir(path); free(mark); return sel == SEL_QUITFAIL ? _FAILURE : _SUCCESS; default: if (xlines != LINES || xcols != COLS) setdirwatch(); /* Terminal resized */ else if (idletimeout && idle == idletimeout) lock_terminal(); /* Locker */ else goto nochange; idle = 0; if (ndents) copycurname(); goto begin; } /* switch (sel) */ } } static char *make_tmp_tree(char **paths, ssize_t entries, const char *prefix) { /* tmpdir holds the full path */ /* tmp holds the path without the tmp dir prefix */ int err, ignore = 0; struct stat sb; char *slash, *tmp; ssize_t i, len = strlen(prefix); char *tmpdir = malloc(sizeof(char) * (PATH_MAX + TMP_LEN_MAX)); if (!tmpdir) { DPRINTF_S(strerror(errno)); return NULL; } tmp = tmpdir + g_tmpfplen - 1; xstrlcpy(tmpdir, g_tmpfpath, g_tmpfplen); xstrlcpy(tmp, "/nnnXXXXXX", 11); /* Points right after the base tmp dir */ tmp += 10; if (!mkdtemp(tmpdir)) { free(tmpdir); DPRINTF_S(strerror(errno)); return NULL; } g_listpath = tmpdir; for (i = 0; i < entries; ++i) { if (!paths[i]) continue; err = stat(paths[i], &sb); if (err && errno == ENOENT) { ignore = 1; continue; } /* Don't copy the common prefix */ xstrlcpy(tmp, paths[i] + len, strlen(paths[i]) - len + 1); /* Get the dir containing the path */ slash = xmemrchr((uchar *)tmp, '/', strlen(paths[i]) - len); if (slash) *slash = '\0'; xmktree(tmpdir, TRUE); if (slash) *slash = '/'; if (symlink(paths[i], tmpdir)) { DPRINTF_S(paths[i]); DPRINTF_S(strerror(errno)); } } if (ignore) g_states |= STATE_MSG; /* Get the dir in which to start */ *tmp = '\0'; return tmpdir; } static char *load_input() { /* 512 KiB chunk size */ ssize_t i, chunk_count = 1, chunk = 512 * 1024, entries = 0; char *input = malloc(sizeof(char) * chunk), *tmpdir = NULL; char cwd[PATH_MAX], *next, *tmp; size_t offsets[LIST_FILES_MAX]; char **paths = NULL; ssize_t input_read, total_read = 0, off = 0; if (!input) { DPRINTF_S(strerror(errno)); return NULL; } if (!getcwd(cwd, PATH_MAX)) { free(input); return NULL; } while (chunk_count < 512) { input_read = read(STDIN_FILENO, input + total_read, chunk); if (input_read < 0) { DPRINTF_S(strerror(errno)); goto malloc_1; } if (input_read == 0) break; total_read += input_read; ++chunk_count; while (off < total_read) { next = memchr(input + off, '\0', total_read - off) + 1; if (next == (void *)1) break; if (next - input == off + 1) { off = next - input; continue; } if (entries == LIST_FILES_MAX) goto malloc_1; offsets[entries++] = off; off = next - input; } if (chunk_count == 512) goto malloc_1; /* We don't need to allocate another chunk */ if (chunk_count == (total_read - input_read) / chunk) continue; chunk_count = total_read / chunk; if (total_read % chunk) ++chunk_count; if (!(input = xrealloc(input, (chunk_count + 1) * chunk))) return NULL; } if (off != total_read) { if (entries == LIST_FILES_MAX) goto malloc_1; offsets[entries++] = off; } DPRINTF_D(entries); DPRINTF_D(total_read); DPRINTF_D(chunk_count); if (!entries) goto malloc_1; input[total_read] = '\0'; paths = malloc(entries * sizeof(char *)); if (!paths) goto malloc_1; for (i = 0; i < entries; ++i) paths[i] = input + offsets[i]; g_prefixpath = malloc(sizeof(char) * PATH_MAX); if (!g_prefixpath) goto malloc_1; g_prefixpath[0] = '\0'; DPRINTF_S(paths[0]); for (i = 0; i < entries; ++i) { if (paths[i][0] == '\n' || selforparent(paths[i])) { paths[i] = NULL; continue; } if (!(paths[i] = abspath(paths[i], cwd))) { entries = i; // free from the previous entry goto malloc_2; } DPRINTF_S(paths[i]); xstrlcpy(g_buf, paths[i], PATH_MAX); if (!common_prefix(dirname(g_buf), g_prefixpath)) { entries = i + 1; // free from the current entry goto malloc_2; } DPRINTF_S(g_prefixpath); } DPRINTF_S(g_prefixpath); if (g_prefixpath[0]) { if (entries == 1) { tmp = xmemrchr((uchar *)g_prefixpath, '/', strlen(g_prefixpath)); if (!tmp) goto malloc_2; *(tmp != g_prefixpath ? tmp : tmp + 1) = '\0'; } tmpdir = make_tmp_tree(paths, entries, g_prefixpath); } malloc_2: for (i = entries - 1; i >= 0; --i) free(paths[i]); malloc_1: free(input); free(paths); return tmpdir; } static void check_key_collision(void) { int key; ulong i = 0; bool bitmap[KEY_MAX] = {FALSE}; for (; i < sizeof(bindings) / sizeof(struct key); ++i) { key = bindings[i].sym; if (bitmap[key]) fprintf(stdout, "key collision! [%s]\n", keyname(key)); else bitmap[key] = TRUE; } } static void usage(void) { fprintf(stdout, "%s: nnn [OPTIONS] [PATH]\n\n" "The missing terminal file manager for X.\n\n" "positional args:\n" " PATH start dir [default: .]\n\n" "optional args:\n" " -a use access time\n" " -A no dir auto-select\n" " -b key open bookmark key\n" " -c cli-only opener (overrides -e)\n" " -d detail mode\n" " -e text in $VISUAL ($EDITOR/vi)\n" " -E use EDITOR for undetached edits\n" " -g regex filters [default: string]\n" " -H show hidden files\n" " -K detect key collision\n" " -n nav-as-you-type mode\n" " -o open files only on Enter\n" " -p file selection file [stdout if '-']\n" " -Q no quit confirmation\n" " -r use advcpmv patched cp, mv\n" " -R no rollover at edges\n" " -s name load session by name\n" " -S du mode\n" " -t secs timeout to lock\n" " -v version sort\n" " -V show version\n" " -x notis, sel to system clipboard\n" " -h show help\n\n" "v%s\n%s\n", __func__, VERSION, GENERAL_INFO); } static bool setup_config(void) { size_t r, len; char *xdgcfg = getenv("XDG_CONFIG_HOME"); bool xdg = FALSE; /* Set up configuration file paths */ if (xdgcfg && xdgcfg[0]) { DPRINTF_S(xdgcfg); if (xdgcfg[0] == '~') { r = xstrlcpy(g_buf, home, PATH_MAX); xstrlcpy(g_buf + r - 1, xdgcfg + 1, PATH_MAX); xdgcfg = g_buf; DPRINTF_S(xdgcfg); } if (!xdiraccess(xdgcfg)) { xerror(); return FALSE; } len = strlen(xdgcfg) + 1 + 13; /* add length of "/nnn/sessions" */ xdg = TRUE; } if (!xdg) len = strlen(home) + 1 + 21; /* add length of "/.config/nnn/sessions" */ cfgdir = (char *)malloc(len); plugindir = (char *)malloc(len); sessiondir = (char *)malloc(len); if (!cfgdir || !plugindir || !sessiondir) { xerror(); return FALSE; } if (xdg) { xstrlcpy(cfgdir, xdgcfg, len); r = len - 13; /* subtract length of "/nnn/sessions" */ } else { r = xstrlcpy(cfgdir, home, len); /* Create ~/.config */ xstrlcpy(cfgdir + r - 1, "/.config", len - r); DPRINTF_S(cfgdir); r += 8; /* length of "/.config" */ } /* Create ~/.config/nnn */ xstrlcpy(cfgdir + r - 1, "/nnn", len - r); DPRINTF_S(cfgdir); /* Create ~/.config/nnn/plugins */ xstrlcpy(cfgdir + r + 4 - 1, "/plugins", 9); /* subtract length of "/nnn" (4) */ DPRINTF_S(cfgdir); xstrlcpy(plugindir, cfgdir, len); DPRINTF_S(plugindir); if (access(plugindir, F_OK) && !xmktree(plugindir, TRUE)) { xerror(); return FALSE; } /* Create ~/.config/nnn/sessions */ xstrlcpy(cfgdir + r + 4 - 1, "/sessions", 10); /* subtract length of "/nnn" (4) */ DPRINTF_S(cfgdir); xstrlcpy(sessiondir, cfgdir, len); DPRINTF_S(sessiondir); if (access(sessiondir, F_OK) && !xmktree(sessiondir, TRUE)) { xerror(); return FALSE; } /* Reset to config path */ cfgdir[r + 3] = '\0'; DPRINTF_S(cfgdir); /* Set selection file path */ if (!cfg.picker) { /* Length of "/.config/nnn/.selection" */ g_selpath = (char *)malloc(len + 3); if (!g_selpath) { xerror(); return FALSE; } r = xstrlcpy(g_selpath, cfgdir, len + 3); xstrlcpy(g_selpath + r - 1, "/.selection", 12); DPRINTF_S(g_selpath); } return TRUE; } static bool set_tmp_path(void) { char *tmp = "/tmp"; char *path = xdiraccess(tmp) ? tmp : getenv("TMPDIR"); if (!path) { fprintf(stderr, "set TMPDIR\n"); return FALSE; } g_tmpfplen = (uchar)xstrlcpy(g_tmpfpath, path, TMP_LEN_MAX); return TRUE; } static void cleanup(void) { free(g_selpath); free(plugindir); free(sessiondir); free(cfgdir); free(initpath); free(bmstr); free(pluginstr); free(g_prefixpath); unlink(g_pipepath); #ifdef DBGMODE disabledbg(); #endif } int main(int argc, char *argv[]) { mmask_t mask; char *arg = NULL; char *session = NULL; int opt; while ((opt = getopt(argc, argv, "aAb:cdeEgHKnop:QrRs:St:vVxh")) != -1) { switch (opt) { case 'a': cfg.mtime = 0; break; case 'A': cfg.autoselect = 0; break; case 'b': arg = optarg; break; case 'c': cfg.cliopener = 1; break; case 'S': cfg.blkorder = 1; nftw_fn = sum_bsize; blk_shift = ffs(S_BLKSIZE) - 1; // fallthrough case 'd': cfg.showdetail = 1; printptr = &printent_long; break; case 'e': cfg.useeditor = 1; break; case 'E': cfg.waitedit = 1; break; case 'g': cfg.regex = 1; filterfn = &visible_re; break; case 'H': cfg.showhidden = 1; break; case 'K': check_key_collision(); return _SUCCESS; case 'n': cfg.filtermode = 1; break; case 'o': cfg.nonavopen = 1; break; case 'p': cfg.picker = 1; if (optarg[0] == '-' && optarg[1] == '\0') cfg.pickraw = 1; else { int fd = open(optarg, O_WRONLY | O_CREAT, 0600); if (fd == -1) { xerror(); return _FAILURE; } close(fd); g_selpath = realpath(optarg, NULL); unlink(g_selpath); } break; case 'Q': cfg.forcequit = 1; break; case 'r': #ifdef __linux__ cp[2] = cp[5] = mv[2] = mv[5] = 'g'; /* cp -iRp -> cpg -giRp */ cp[4] = mv[4] = '-'; #endif break; case 'R': cfg.rollover = 0; break; case 's': session = optarg; break; case 't': idletimeout = xatoi(optarg); break; case 'v': namecmpfn = &xstrverscasecmp; break; case 'V': fprintf(stdout, "%s\n", VERSION); return _SUCCESS; case 'x': cfg.x11 = 1; break; case 'h': usage(); return _SUCCESS; default: usage(); return _FAILURE; } } #ifdef DBGMODE enabledbg(); DPRINTF_S(VERSION); #endif /* Prefix for temporary files */ if (!set_tmp_path()) return _FAILURE; atexit(cleanup); if (!cfg.picker) { /* Confirm we are in a terminal */ if (!isatty(STDOUT_FILENO)) exit(1); /* Now we are in path list mode */ if (!isatty(STDIN_FILENO)) { /* This is the same as g_listpath */ initpath = load_input(); if (!initpath) exit(1); /* We return to tty */ dup2(STDOUT_FILENO, STDIN_FILENO); } } home = getenv("HOME"); if (!home) { fprintf(stderr, "set HOME\n"); return _FAILURE; } DPRINTF_S(home); if (!setup_config()) return _FAILURE; /* Get custom opener, if set */ opener = xgetenv(env_cfg[NNN_OPENER], utils[UTIL_OPENER]); DPRINTF_S(opener); /* Parse bookmarks string */ if (!parsekvpair(bookmark, &bmstr, env_cfg[NNN_BMS], BM_MAX, PATH_MAX)) { fprintf(stderr, "%s\n", env_cfg[NNN_BMS]); return _FAILURE; } /* Parse plugins string */ if (!parsekvpair(plug, &pluginstr, env_cfg[NNN_PLUG], PLUGIN_MAX, PATH_MAX)) { fprintf(stderr, "%s\n", env_cfg[NNN_PLUG]); return _FAILURE; } if (initpath) { /* NOP */ } else if (arg) { /* Open a bookmark directly */ if (!arg[1]) /* Bookmarks keys are single char */ initpath = get_kv_val(bookmark, NULL, *arg, BM_MAX, TRUE); if (!initpath) { fprintf(stderr, "%s\n", messages[MSG_INVALID_KEY]); return _FAILURE; } } else if (argc == optind) { /* Start in the current directory */ initpath = getcwd(NULL, PATH_MAX); if (!initpath) initpath = "/"; } else { arg = argv[optind]; DPRINTF_S(arg); if (strlen(arg) > 7 && !strncmp(arg, "file://", 7)) arg = arg + 7; initpath = realpath(arg, NULL); DPRINTF_S(initpath); if (!initpath) { xerror(); return _FAILURE; } /* * If nnn is set as the file manager, applications may try to open * files by invoking nnn. In that case pass the file path to the * desktop opener and exit. */ struct stat sb; if (stat(initpath, &sb) == -1) { xerror(); return _FAILURE; } if (S_ISREG(sb.st_mode)) { spawn(opener, arg, NULL, NULL, cfg.cliopener ? F_CLI : F_NOTRACE | F_NOWAIT); return _SUCCESS; } } /* Set archive handling (enveditor used as tmp var) */ enveditor = getenv(env_cfg[NNN_ARCHIVE]); #ifdef PCRE if (setfilter(&archive_pcre, (enveditor ? enveditor : archive_regex))) { #else if (setfilter(&archive_re, (enveditor ? enveditor : archive_regex))) { #endif fprintf(stderr, "%s\n", messages[MSG_INVALID_REG]); return _FAILURE; } /* An all-CLI opener overrides option -e) */ if (cfg.cliopener) cfg.useeditor = 0; /* Get VISUAL/EDITOR */ enveditor = xgetenv(envs[ENV_EDITOR], utils[UTIL_VI]); editor = xgetenv(envs[ENV_VISUAL], enveditor); DPRINTF_S(getenv(envs[ENV_VISUAL])); DPRINTF_S(getenv(envs[ENV_EDITOR])); DPRINTF_S(editor); /* Get PAGER */ pager = xgetenv(envs[ENV_PAGER], utils[UTIL_LESS]); DPRINTF_S(pager); /* Get SHELL */ shell = xgetenv(envs[ENV_SHELL], utils[UTIL_SH]); DPRINTF_S(shell); DPRINTF_S(getenv("PWD")); #ifdef LINUX_INOTIFY /* Initialize inotify */ inotify_fd = inotify_init1(IN_NONBLOCK); if (inotify_fd < 0) { xerror(); return _FAILURE; } #elif defined(BSD_KQUEUE) kq = kqueue(); if (kq < 0) { xerror(); return _FAILURE; } #elif defined(HAIKU_NM) haiku_hnd = haiku_init_nm(); if (!haiku_hnd) { xerror(); return _FAILURE; } #endif /* Set nnn nesting level */ setenv(env_cfg[NNNLVL], xitoa(xatoi(getenv(env_cfg[NNNLVL])) + 1), 1); if (xgetenv_set(env_cfg[NNN_TRASH])) cfg.trash = 1; /* Ignore/handle certain signals */ struct sigaction act = {.sa_handler = sigint_handler}; if (sigaction(SIGINT, &act, NULL) < 0) { xerror(); return _FAILURE; } signal(SIGQUIT, SIG_IGN); #ifndef NOLOCALE /* Set locale */ setlocale(LC_ALL, ""); #ifdef PCRE tables = pcre_maketables(); #endif #endif #ifndef NORL #if RL_READLINE_VERSION >= 0x0603 /* readline would overwrite the WINCH signal hook */ rl_change_environment = 0; #endif /* Bind TAB to cycling */ rl_variable_bind("completion-ignore-case", "on"); #ifdef __linux__ rl_bind_key('\t', rl_menu_complete); #else rl_bind_key('\t', rl_complete); #endif mkpath(cfgdir, ".history", g_buf); read_history(g_buf); #endif if (!initcurses(&mask)) return _FAILURE; opt = browse(initpath, session); mousemask(mask, NULL); if (g_listpath) spawn("rm -rf", initpath, NULL, NULL, F_SILENT); exitcurses(); #ifndef NORL mkpath(cfgdir, ".history", g_buf); write_history(g_buf); #endif if (cfg.pickraw) { if (selbufpos && (seltofile(1, NULL) != (size_t)(selbufpos))) xerror(); } else if (cfg.picker) { if (selbufpos) writesel(pselbuf, selbufpos - 1); } else if (g_selpath) unlink(g_selpath); /* Free the regex */ #ifdef PCRE pcre_free(archive_pcre); #else regfree(&archive_re); #endif /* Free the selection buffer */ free(pselbuf); #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); #elif defined(HAIKU_NM) haiku_close_nm(haiku_hnd); #endif return opt; } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������nnn-3.0/src/nnn.h�����������������������������������������������������������������������������������0000664�0000000�0000000�00000015006�13620656057�0013712�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* * BSD 2-Clause License * * Copyright (C) 2014-2016, Lazaros Koromilas <lostd@2f30.org> * Copyright (C) 2014-2016, Dimitris Papastamos <sin@2f30.org> * Copyright (C) 2016-2020, Arun Prakash Jana <engineerarun@gmail.com> * 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 <curses.h> #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_CTRL_D, SEL_CTRL_U, SEL_HOME, SEL_END, SEL_FIRST, SEL_CDHOME, SEL_CDBEGIN, SEL_CDLAST, SEL_CDROOT, SEL_BOOKMARK, SEL_CYCLE, SEL_CYCLER, SEL_CTX1, SEL_CTX2, SEL_CTX3, SEL_CTX4, SEL_PIN, SEL_FLTR, SEL_MFLTR, SEL_HIDDEN, SEL_DETAIL, SEL_STATS, SEL_CHMODX, SEL_ARCHIVE, SEL_SORT, SEL_REDRAW, SEL_SEL, SEL_SELMUL, SEL_SELALL, SEL_SELEDIT, SEL_CP, SEL_MV, SEL_CPMVAS, SEL_RM, SEL_OPENWITH, SEL_NEW, SEL_RENAME, SEL_RENAMEMUL, SEL_REMOTE, SEL_UMOUNT, SEL_HELP, SEL_EDIT, SEL_PLUGIN, SEL_SHELL, SEL_LAUNCH, SEL_RUNCMD, SEL_LOCK, SEL_SESSIONS, SEL_AUTONEXT, SEL_QUITCTX, SEL_QUITCD, SEL_QUIT, SEL_QUITFAIL, SEL_CLICK, }; /* Associate a pressed key to an action */ struct key { int sym; /* Key pressed */ enum action act; /* Action */ }; static struct key bindings[] = { /* Back */ { KEY_LEFT, SEL_BACK }, { '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 }, /* Previous */ { 'k', SEL_PREV }, { KEY_UP, SEL_PREV }, /* Page down */ { KEY_NPAGE, SEL_PGDN }, /* Page up */ { KEY_PPAGE, SEL_PGUP }, /* Ctrl+D */ { CONTROL('D'), SEL_CTRL_D }, /* Ctrl+U */ { CONTROL('U'), SEL_CTRL_U }, /* First entry */ { KEY_HOME, SEL_HOME }, { 'g', SEL_HOME }, { CONTROL('A'), SEL_HOME }, /* Last entry */ { KEY_END, SEL_END }, { 'G', SEL_END }, { CONTROL('E'), SEL_END }, /* Go to first file */ { '\'', SEL_FIRST }, /* HOME */ { '~', SEL_CDHOME }, /* Initial directory */ { '@', SEL_CDBEGIN }, /* Last visited dir */ { '-', SEL_CDLAST }, /* Go to / */ { '`', SEL_CDROOT }, /* Leader key */ { 'b', SEL_BOOKMARK }, { CONTROL('_'), SEL_BOOKMARK }, /* Cycle contexts in forward direction */ { '\t', SEL_CYCLE }, /* Cycle contexts in reverse direction */ { KEY_BTAB, SEL_CYCLER }, /* Go to/create context N */ { '1', SEL_CTX1 }, { '2', SEL_CTX2 }, { '3', SEL_CTX3 }, { '4', SEL_CTX4 }, /* Mark a path to visit later */ { ',', SEL_PIN }, /* Filter */ { '/', SEL_FLTR }, /* Toggle filter mode */ { CONTROL('N'), SEL_MFLTR }, /* Toggle hide .dot files */ { '.', SEL_HIDDEN }, { KEY_F(5), SEL_HIDDEN }, /* Detailed listing */ { 'd', SEL_DETAIL }, /* File details */ { 'f', SEL_STATS }, { CONTROL('F'), SEL_STATS }, /* Toggle executable status */ { '*', SEL_CHMODX }, /* Create archive */ { 'z', SEL_ARCHIVE }, /* Sort toggles */ { 't', SEL_SORT }, { CONTROL('T'), SEL_SORT }, /* Redraw window */ { CONTROL('L'), SEL_REDRAW }, /* Select current file path */ { CONTROL('J'), SEL_SEL }, { ' ', SEL_SEL }, /* Toggle select multiple files */ { 'm', SEL_SELMUL }, { CONTROL('K'), SEL_SELMUL }, /* Select all files in current dir */ { 'a', SEL_SELALL }, /* List, edit selection */ { 'E', SEL_SELEDIT }, /* Copy from selection buffer */ { 'p', SEL_CP }, { CONTROL('P'), SEL_CP }, /* Move from selection buffer */ { 'v', SEL_MV }, { CONTROL('V'), SEL_MV }, /* Copy/move from selection buffer and rename */ { 'w', SEL_CPMVAS }, { CONTROL('W'), SEL_CPMVAS }, /* Delete from selection buffer */ { 'x', SEL_RM }, { CONTROL('X'), SEL_RM }, /* Open in a custom application */ { 'o', SEL_OPENWITH }, { 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_RENAMEMUL }, /* Connect to server over SSHFS */ { 'c', SEL_REMOTE }, /* Disconnect a SSHFS mount point */ { 'u', SEL_UMOUNT }, /* Show help */ { '?', SEL_HELP }, /* Edit in EDITOR */ { 'e', SEL_EDIT }, /* Run a plugin */ { ';', SEL_PLUGIN }, { CONTROL('S'), SEL_PLUGIN }, /* Run command */ { '!', SEL_SHELL }, { CONTROL(']'), SEL_SHELL }, /* Launcher */ { '=', SEL_LAUNCH }, /* Run a command */ { ']', SEL_RUNCMD }, /* Lock screen */ { '0', SEL_LOCK }, /* Manage sessions */ { 's', SEL_SESSIONS }, /* Quit a context */ { '+', SEL_AUTONEXT }, /* Quit a context */ { 'q', SEL_QUITCTX }, /* Change dir on quit */ { CONTROL('G'), SEL_QUITCD }, /* Quit */ { CONTROL('Q'), SEL_QUIT }, /* Quit with an error code */ { 'Q', SEL_QUITFAIL }, { KEY_MOUSE, SEL_CLICK }, }; ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������