pax_global_header00006660000000000000000000000064150204410340014503gustar00rootroot0000000000000052 comment=c208a2ef11b11d0180811e91a7335857f04175fd sff/000077500000000000000000000000001502044103400116255ustar00rootroot00000000000000sff/.gitignore000066400000000000000000000000371502044103400136150ustar00rootroot00000000000000backup/ bin-pack/ sff sff.1.gz sff/CHANGELOG.md000066400000000000000000000043341502044103400134420ustar00rootroot00000000000000# Changelog ## 1.1 (2025-06-05) ### Added * Add plugins support * New preview plugin ([#3][3]) [3]: https://codeberg.org/sylphenix/sff/issues/3 ### Changed * Changed sff-extfunc installation path from libexec/ to libexec/sff/ * Simplified the control sequence sent from sff-extfunc to sff * 'Search via fzf' now provided as a plugin * 'Extract and create archives' now provided as a plugin * Optimized and simplified prompt text for extension functions ### Fixed * Fixed SIGWINCH/SIGTSTP signal interference during the extension script execution * Fixed handling of paths with special characters (e.g. `\` `&`) during file operations ## 1.0 (2025-04-05) ### Changed * Set the precision of the file size value to one decimal place * Rewrote the `xstrverscmp` function for better performance * Simplified the implementation of reading pipe data * Now shares selections with extension script via a pipe instead of a regular file * Select range feature now selects files in start-to-end order * Tab switching no longer falls back to home directory on chdir failure ### Fixed * Fixed issue where the executable file extension was not showing in the status bar * Fixed 'Illegal instruction' error on Chimera Linux ([#1][1]) * Fixed go to root directory key binding issue * Unset `LESS` in the extension script to ensure proper pager behavior ([#2][2]) [1]: https://codeberg.org/sylphenix/sff/issues/1 [2]: https://codeberg.org/sylphenix/sff/issues/2 ## 0.9 (2025-03-04) ### Added * New filter feature * New quick find feature ### Changed * Replaced `histstat` and `histpath` linked lists with arrays * Moved config directory check/creation to extension function call * Centralized memory allocation for `cfgpath`, `extfunc`, `selpath` and `pipepath` * Extension script now gets config directory from arguments instead of environment variables * Extension script initializes buffers and sets their permissions when needed * Simplified extension script `sff_duplicate` function * Optimized extension script `sff_paste` function for better performance ### Fixed * Fixed issue where sudo mode and normal mode did not share buffers * Fixed error when checking files starting with '-' during file creation sff/LICENSE000066400000000000000000000024311502044103400126320ustar00rootroot00000000000000BSD 2-Clause License Copyright (c) 2023-2025 Shi Yanling All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. 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 AUTHOR 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 AUTHOR 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. sff/Makefile000066400000000000000000000031171502044103400132670ustar00rootroot00000000000000# sff - simple file finder VERSION = 1.1 # paths PREFIX = /usr/local MANPREFIX = ${PREFIX}/share/man EXTFNNAME = sff-extfunc EXTFNPREFIX = ${PREFIX}/libexec/sff # includes and libs INCS = LIBS = -lncursesw # flags CPPFLAGS = -D_DEFAULT_SOURCE -DVERSION=\"${VERSION}\" -DEXTFNNAME=\"${EXTFNNAME}\" -DEXTFNPREFIX=\"${EXTFNPREFIX}\" -I/usr/include/ncursesw CFLAGS = -std=c11 -pedantic -Wall -Wextra -Wshadow -Wno-deprecated-declarations -Os ${CPPFLAGS} LDFLAGS = ${LIBS} -s # compiler and linker CC = cc #==================================== SRC = sff.c OBJ = ${SRC:.c=.o} all: options sff options: @echo sff build options: @echo "CFLAGS = ${CFLAGS}" @echo "LDFLAGS = ${LDFLAGS}" @echo "CC = ${CC}" .c.o: ${CC} -c ${CFLAGS} $< ${OBJ}: config.h config.h: cp config.def.h $@ sff: ${OBJ} ${CC} -o $@ ${OBJ} ${LDFLAGS} clean: rm -f sff ${OBJ} sff.1.gz sff-${VERSION}.tar.gz dist: mkdir -p sff-${VERSION} cp -R LICENSE Makefile README.md ${SRC} ${EXTFNNAME} plugins config.h sff.1 sff-${VERSION}/ tar -cf sff-${VERSION}.tar sff-${VERSION} gzip sff-${VERSION}.tar rm -rf sff-${VERSION} install: all mkdir -p ${DESTDIR}${PREFIX}/bin install -m 755 sff ${DESTDIR}${PREFIX}/bin/ mkdir -p ${DESTDIR}${EXTFNPREFIX} cp -fR ${EXTFNNAME} plugins ${DESTDIR}${EXTFNPREFIX}/ chmod -R 755 ${DESTDIR}${EXTFNPREFIX} mkdir -p ${DESTDIR}${MANPREFIX}/man1 gzip -fk sff.1 install -m 644 sff.1.gz ${DESTDIR}${MANPREFIX}/man1/ uninstall: rm -rf ${DESTDIR}${PREFIX}/bin/sff \ ${DESTDIR}${EXTFNPREFIX} \ ${DESTDIR}${MANPREFIX}/man1/sff.1.gz .PHONY: all options clean dist install uninstall sff/README.md000066400000000000000000000122221502044103400131030ustar00rootroot00000000000000# sff sff (simple file finder) is a simple, fast, and feature-rich terminal file manager inspired by nnn and guided by the suckless philosophy. It aims to provide a reliable, efficient, and user-friendly file management experience with high extensibility. sff is fully compatible with POSIX-compliant systems. It has been extensively tested on GNU/Linux and FreeBSD. ## Features - POSIX-compliant and highly optimized - Fast startup and low memory footprint - Extensible with shell scripts - Customizable detail columns - Type-to-navigate - Advanced search via 'find' - Fast file search via 'fzf' - Convenient temporary sudo mode - Undo/Redo for the last file operation - Batch file and directory creation - Batch rename - Multi-tab support, cross-directory selection - Extract, list, create archives - ... and more! ## Installation ### Dependencies | Library/Package | Install? | Notes | |-------------------------------------------------|-----------|-------------------------------------------| | libc, curses (wide character support) | Required* | Essential runtime libraries | | coreutils (Linux), findutils (Linux), sed, file | Required* | File operations | | vi/vim | Required* | Default text editor | | sudo | Optional* | Sudo mode | | xdg-utils | Optional* | File opening via default applications | | tar, gzip, bzip2, xz, 7zip | Optional | Archive handling (archive plugin) | | fzf | Optional | File search via fzf (fzf-find plugin) | | chafa | Optional | Image preview (preview plugin) | | poppler-utils | Optional | PDF preview (preview plugin) | | ffmpegthumbnailer | Optional | Video thumbnail preview (preview plugin) | _* These dependencies are part of the base system in most environments and generally don't require manual installation._ You can install all dependencies using the following commands: - Debian/Ubuntu: ``` sudo apt install 7zip fzf chafa poppler-utils ffmpegthumbnailer ``` - Arch Linux: ``` sudo pacman -S 7zip fzf chafa poppler ffmpegthumbnailer ``` - Fedora: ``` sudo dnf install p7zip fzf chafa poppler-utils ffmpegthumbnailer ``` - FreeBSD: ``` sudo pkg install 7-zip fzf chafa poppler-utils ffmpegthumbnailer ``` ### Install from binary packages 1. Download the appropriate package for your system from [OpenBuildService](https://software.opensuse.org//download.html?project=home%3Asylphenix%3Asff&package=sff). 2. Install the package using the package manager specific to your system. - Debian/Ubuntu: ``` sudo apt install /PATH/TO/sff_VERSION_amd64.deb ``` - Arch Linux: ``` sudo pacman -U /PATH/TO/sff-VERSION-x86_64.pkg.tar.zst ``` - Fedora: ``` sudo dnf install /PATH/TO/sff-VERSION.x86_64.rpm ``` ### Build and install from source 0. For Linux users, ensure that a C compiler, make utility, and the ncurses headers are installed. You can install them using the following commands: - Debian/Ubuntu: ``` sudo apt install gcc make libncurses-dev ``` - Arch Linux: ``` sudo pacman -S gcc make ncurses ``` - Fedora: ``` sudo dnf install gcc make ncurses-devel ``` 1. [Download](https://codeberg.org/sylphenix/sff/releases) and extract the latest release, or clone the repository to get the development version. 2. Change to the root directory of the project. 3. Build and install sff. - To install under `/usr` (recommended for Linux): ``` sudo make install PREFIX=/usr ``` - To install under `/usr/local` (recommended for FreeBSD): ``` sudo make install ``` ## Usage Simply run `sff` to start the application from the current directory. While sff is running: - Press `?` or `F1` to see the list of key bindings for built-in functions. - Press `alt`+`/` to see the list of key bindings for extension functions and plugins. - Press `Q` to quit sff. For more details, run `man sff` to see the documentation, or visit the [wiki](https://codeberg.org/sylphenix/sff/wiki/Home) for useful tips and tricks. ## Philosophy sff is built on the belief that simplicity ensures reliability. It follows a minimalist design, divided into two parts: the core program and the extension script. The core program is a lightweight file browser and selector, sticking to features that are simple, necessary, and straightforward to implement. The extension script, a POSIX-compliant shell script, handles file operations such as copying, moving, and deleting. This modular design allows users to easily customize or extend functionality while keeping the core simple and efficient. ## License sff is released under the 2-Clause BSD License. See the LICENSE file for more details. ## Acknowledgements Special thanks to [nnn](https://github.com/jarun/nnn) and [suckless.org](https://suckless.org). sff/config.h000066400000000000000000000145671502044103400132600ustar00rootroot00000000000000/* Default settings */ #define OPENER "xdg-open" // Default opener #define EDITOR "vi" // Default editor #define SUDOER "sudo" // Default sudo utility static Settings gcfg = { .showhidden = 0, // Show hidden files .dirontop = 1, // Sort directories on the top .sortby = 0, // (0: name, 1: size, 2: time, 3: extension) .caseinsen = 1, // Case insensitive .natural = 1, // Natural numeric sorting .reverse = 0, // Reverse sort .showtime = 1, // Show time info .showowner = 0, // Show owner:group info .showperm = 0, // Show permissions info .showsize = 1, // Show size info .timetype = 1, // (0: access, 1: modify, 2: change) }; /* Key definitions */ #define CTRL(c) ((c) & 0x1f) #define ESC 27 #define CTRL_UP 601 #define CTRL_DOWN 602 #define CTRL_RIGHT 603 #define CTRL_LEFT 604 #define SHIFT_UP 605 #define SHIFT_DOWN 606 static const Key keys[] = { // key1 key2 function argument comment(Up to 39 characters) { KEY_UP, 'k', movecursor, -1, " Up, k Move up" }, { KEY_DOWN, 'j', movecursor, 1, " Down, j Move down" }, { KEY_LEFT, 'h', gotoparent, 0, " Left, h Go to parent dir" }, { KEY_RIGHT, 'l', enterdir, 0, " Right, l Enter dir" }, { CTRL_UP, CTRL('K'), movequarterpage, -1, " C-Up, ^K Quarter page up" }, { CTRL_DOWN, CTRL('J'), movequarterpage, 1, "C-Down, ^J Quarter page down" }, { KEY_PPAGE, CTRL('B'), scrollpage, -1, " PgUp, ^B Scroll page up" }, { KEY_NPAGE, CTRL('F'), scrollpage, 1, "PgDown, ^F Scroll page down" }, { 'B', 0, scrolleighth, -1, " B Scroll eighth up" }, { 'F', 0, scrolleighth, 1, " F Scroll eighth down" }, { KEY_HOME, 'g', movetoedge, -1, " Home, g Move to top" }, { KEY_END, 'G', movetoedge, 1, " End, G Move to bottom" }, { 'e', 0, openfile, 1, " e Edit file" }, { '\r', KEY_ENTER, openfile, 2, " Enter Open file" }, { 'r', KEY_F(5), refreshview, 1, " F5, r Refresh" }, { '`', 0, gotohome, 1, " ` Go to home dir" }, { '~', 0, gotohome, 2, " ~ Go to root dir" }, { CTRL_LEFT, CTRL('H'), switchhistpath, 0, "C-Left, ^H Toggle previous path" }, { '1', 0, switchtab, 0, " 1 Tab 1" }, { '2', 0, switchtab, 1, " 2 Tab 2" }, { '3', 0, switchtab, 2, " 3 Tab 3" }, { '4', 0, switchtab, 3, " 4 Tab 4" }, { '5', 0, switchtab, 4, " 5 Search result tab" }, { 'q', 0, closetab, 0, " q Close tab" }, { ' ', 0, toggleselection, 0, " Space (Un)select current" }, { SHIFT_UP, 'K', toggleselection, -1, " Sh-Up, K (Un)select and move up" }, { SHIFT_DOWN, 'J', toggleselection, 1, "Sh-Down, J (Un)select and move down" }, { 'a', 0, selectall, 0, " a Select all" }, { 'A', 0, invertselection, 0, " A Invert selection" }, { CTRL('A'), ESC, clearselection, 0, " Esc, ^A Clear selection" }, { 'm', 0, selectrange, 1, " m Select range" }, { 'M', 0, selectrange, -1, " M Deselect range" }, { '/', 0, setfilter, 1, " / (Un)filter" }, { 'f', 0, quickfind, 0, " f Quick find" }, { 'n', 0, qfindnext, 1, " n Find next" }, { 'N', 0, qfindnext, -1, " N Find previous" }, { 'T', 0, togglemode, 3, " T Toggle browse mode" }, { CTRL('T'), 0, togglemode, 1, " ^T Toggle sudo mode" }, { 'o', 0, viewoptions, 0, " o View options" }, { '?', KEY_F(1), showhelp, 0, " F1, ? Show this help" }, { 'Q', 0, quitsff, 0, " Q Quit" }, }; /* Color definitions for 256 color*/ static void setcolorpair256(void) { // type fg color bg color (default color: -1) init_pair( F_REG, -1, -1 ); // Regular file, Default init_pair( F_DIR, 39, -1 ); // Directory, DeepSkyBlue1 init_pair( F_LNK, 51, -1 ); // Symbolic link, Cyan1 init_pair( F_CHR, 226, -1 ); // Char device, Yellow1 init_pair( F_BLK, 193, -1 ); // Block device, DarkSeaGreen1 init_pair( F_IFO, 214, -1 ); // FIFO, Orange1 init_pair( F_SOCK, 171, -1 ); // Socket, MediumOrchid1 init_pair( F_HLNK, 96, -1 ); // Hard link, Plum4 init_pair( F_EXEC, 46, -1 ); // Executable, Green1 init_pair( C_DETAIL, 246, -1 ); // Detail info, Grey62 init_pair( C_TABTAG, 226, -1 ); // Tabs tag, Yellow1 init_pair( C_PATHBAR, 214, -1 ); // Path bar, Orange1 init_pair( C_STATBAR, 214, -1 ); // Status bar, Orange1 init_pair( C_WARN, 196, -1 ); // Warning, Red1 init_pair( C_NEWFILE, 168, -1 ); // New file, DeepPink1 } /* Color definitions for 8 color*/ static void setcolorpair8(void) { // type fg color bg color (default color: -1) init_pair( F_REG, -1, -1 ); // Regular file, Default init_pair( F_DIR, 4, -1 ); // Directory, Blue init_pair( F_LNK, 5, -1 ); // Symbolic link, Magenta init_pair( F_CHR, 3, -1 ); // Char device, Yellow init_pair( F_BLK, 3, -1 ); // Block device, Yellow init_pair( F_IFO, 5, -1 ); // FIFO, Magenta init_pair( F_SOCK, 5, -1 ); // Socket, Magenta init_pair( F_HLNK, -1, -1 ); // Hard link, Default init_pair( F_EXEC, 2, -1 ); // Executable, Green init_pair( C_DETAIL, -1, -1 ); // Detail info, Default init_pair( C_TABTAG, 3, -1 ); // Tabs tag, Yellow init_pair( C_PATHBAR, 3, -1 ); // Path bar, Yellow init_pair( C_STATBAR, 3, -1 ); // Status bar, Yellow init_pair( C_WARN, 1, -1 ); // Warning, Red init_pair( C_NEWFILE, 5, -1 ); // New file, Magenta } sff/plugins/000077500000000000000000000000001502044103400133065ustar00rootroot00000000000000sff/plugins/archive000077500000000000000000000112411502044103400146540ustar00rootroot00000000000000#!/usr/bin/env sh # Description: Extract and create archives # # Supported creation: # gz, bz2, xz, tar (with gz, bz2, xz), zip, 7z # # Supported extraction: # ar, arj, bz2, cab, chm, cpio, cramfs, deb, dmg, gz, iso, lzh, lzma, # msi, nsis, qcow2, rpm, squashfs, tar (with gz, bz2, xz), wim, xar, # xz, zip, Z, 7z # # Dependencies: # - tar, gzip, bzip2, xz # - 7zip # # Usage: # Extracting archives: # You can select multiple archives for batch extraction or content preview. # # Creating archives: # Enter a filename with extension (e.g., foo.tar.gz, foo.txz, foo.zip) to # archive and compress selected files together. # # For tar-based formats: # Files will be archived using relative paths only if all selected files # are located within the current directory or its subdirectories; # otherwise, absolute paths will be used. # # For zip or 7z formats: # The directory hierarchy of the selected files will be ignored, and all # files will be packed directly into the archive without preserving paths. # # If you enter just .gz, .bz2, or .xz as the filename, each selected file will # be individually compressed in the specified format. # # Shell: POSIX compliant # # Author: Shi Yanling sffpipe=$1 sffdir=${sffpipe%/*} tmpdir=${TMPDIR:-/tmp} [ ! -w "$tmpdir" ] && tmpdir=$sffdir uid=$(id -u) tsel="${tmpdir}/sff-tmpsel-$uid" tbuf1="${tmpdir}/sff-tmpbuf2-$uid" type 7zz >/dev/null 2>&1 && _7z='7zz' || _7z='7z' sffpipe_refresh() { [ "$1" = '-c' ] && _x='.' || _x='' printf "*%s" "$_x" >"$sffpipe" } sffpipe_sel_file() { printf "@%s\0" "$1" >"$sffpipe" } sffpipe_get_sel() { printf "%s" "$$" >"$sffpipe" sel=$sffpipe } extract_archive() { sffpipe_get_sel tr '\n\0' '\035\n' <"$sel" >"$tsel" [ ! -s "$tsel" ] && return 0 printf "\n(l)ist contents / (e)xtract / (c)ancel [c]: "; read -r _x _selfile='' while IFS='' read -r _path <&3; do _path=$(printf "%s" "$_path" | tr '\035' '\n') _wdir=${_path%/*} _name=${_path##*/} _bname=${_name%.?*} _bname=${_bname%.tar} case "$_name" in *?.?*) [ -d "$_path" ] && continue;; *) continue;; esac case "$_x" in 'e') if [ -e "${_wdir}/$_bname" ]; then printf "\n'%s' exists. Overwrite? (y/n) [n]: " "$_bname"; read -r _x2 [ "$_x2" != 'y' ] && continue fi _tdir=$(mktemp -d "${_wdir}/tmp.XXXXXX") [ -e "${_wdir}/$_bname" ] && rm -rf "${_wdir}/$_bname" echo "Extracting..." case "$(printf "%s" "$_name" | tr 'A-Z' 'a-z')" in *?.tar.?*|*?.t?*) cd "$_tdir" && tar -xvf "$_path";; *?.gz) gunzip -k "$_path";; *?.bz2) bunzip2 -k "$_path";; *?.xz) unxz -k "$_path";; *?.?*) cd "$_tdir" && "$_7z" x -aoa "$_path";; esac && _selfile="$_bname" || { printf "Press Enter to continue "; read -r _x2; } _num=$(find "$_tdir"/ -mindepth 1 -maxdepth 1 ! -name . ! -name .. -print0 | tr '\n\0' '\035\n' | wc -l) if [ -d "${_tdir}/$_bname" ] && [ "$_num" -eq 1 ]; then mv "${_tdir}/$_bname" "$_wdir"/ elif [ ! -e "${_wdir}/$_bname" ] && [ "$_num" -gt 0 ]; then mv "$_tdir" "${_wdir}/$_bname" fi [ -e "$_tdir" ] && rm -rf "$_tdir" ;; 'l') echo "" case "$(printf "%s" "$_name" | tr 'A-Z' 'a-z')" in *?.tar.?*|*?.t?*) tar -tvf "$_path";; *?.gz) zcat "$_path";; *?.bz2) bzcat "$_path";; *?.xz) xzcat "$_path";; *?.?*) "$_7z" l "$_path";; esac printf "Press Enter to continue "; read -r _x2 ;; *) return 0 ;; esac done 3<"$tsel" if [ "$_selfile" ]; then sffpipe_sel_file "$_selfile" else sffpipe_refresh fi } create_archive() { sffpipe_get_sel tr '\n\0' '\035\n' <"$sel" >"$tsel" [ ! -s "$tsel" ] && return 0 while true; do printf "\nArchive name (empty to cancel): "; read -r _name [ -z "$_name" ] && return 0 if [ -e "$_name" ]; then printf "\n'%s' exists. Overwrite? (y/n) [n]: " "$_name"; read -r _x [ "$_x" = 'y' ] && break else break fi done _pwd=$(printf '%s' "$PWD" | tr '\n' '\035' | sed -e 's/[^^]/[&]/g' -e 's/\^/\\^/g') if [ "$(sed "/^$_pwd\//d" "$tsel")" ]; then tr '\n\035' '\0\n' <"$tsel" >"$tbuf1" else sed "s|^$_pwd/||" "$tsel" | tr '\n\035' '\0\n' >"$tbuf1" fi [ -e "$_name" ] && rm -f "$_name" case "$(printf "%s" "$_name" | tr 'A-Z' 'a-z')" in *?.tar.?*|*?.t?*) xargs -0 tar -cavf "$_name" -- <"$tbuf1";; *.gz) xargs -0 gzip -fk -- <"$tbuf1";; *.bz2) xargs -0 bzip2 -fk -- <"$tbuf1";; *.xz) xargs -0 xz -fk -- <"$tbuf1";; *?.zip|*?.7z) xargs -0 "$_7z" a "$_name" -- <"$tbuf1";; *) printf "\nUnsupported archive format\n"; false;; esac if [ "$?" -eq 0 ]; then sffpipe_sel_file "$_name" else sffpipe_refresh printf "Press Enter to continue "; read -r _x fi } case "$2" in 'e') extract_archive;; 'c') create_archive;; esac rm -f "$tsel" "$tbuf1" sff/plugins/fzf-find000077500000000000000000000005251502044103400147410ustar00rootroot00000000000000#!/usr/bin/env sh # Description: Search file with fzf # # Dependencies: fzf # # Shell: POSIX compliant # # Author: Shi Yanling sffpipe=$1 sffpipe_enter() { printf ">%s\0" "$1" >"$sffpipe" } if ! fzf --version >/dev/null; then printf "Press Enter to continue "; read -r _x exit 0 fi _path=$(fzf) [ "$_path" ] && sffpipe_enter "$_path" sff/plugins/preview000077500000000000000000000061631502044103400147230ustar00rootroot00000000000000#!/usr/bin/env sh # Description: File preview # # Dependencies: # - Image preview: chafa # - PDF preview: poppler-utils # - Video thumbnail preview: ffmpegthumbnailertar # # Usage: # When sff is running inside tmux: # Enabling preview will split a new pane for display. # # When sff is running outside tmux: # If SFF_PV_TERM is set, enabling preview will launch the terminal emulator # specified by this variable as the previewer. # # If SFF_PV_TERM is not set, enabling preview will prompt you to enter # the name of a terminal emulator to use temporarily as the previewer. # # The SFF_PV_TERM environment variable defines the default terminal emulator # to be used as the previewer. You can set SFF_PV_TERM like this: # $ export SFF_PV_TERM=xterm # # Note: This plugin cannot handle paths containing line breaks. # # Shell: POSIX compliant # # Author: Shi Yanling sffpipe=$1 sffdir=${sffpipe%/*} pvfifo="${sffpipe}.pv" tmpdir=${TMPDIR:-/tmp} [ ! -w "$tmpdir" ] && tmpdir=$sffdir tmppvfile="${tmpdir}/sff-tmppv-$(id -u)" preview_loop_tui() { mkfifo "$pvfifo" || exit 0 while IFS='' read -r _path; do while _x=$(timeout 0.005 sh -c 'IFS="" read -r _x && printf "%s" "$_x"'); do _path=$_x done clear _dest='' _mime=$(file -bL --mime-type "$_path") case "$_mime" in image/*) _dest=$_path ;; application/pdf) _dest=$tmppvfile pdftoppm -jpeg -f 1 -singlefile -scale-to 800 "$_path" "$_dest" >/dev/null 2>&1 \ && _dest="${_dest}.jpg" || _dest='' ;; video/*) _dest="${tmppvfile}.jpg" ffmpegthumbnailer -m -s 0 -i "$_path" -o "$_dest" >/dev/null 2>&1 \ || _dest='' ;; *) _rows=$(tput lines); _cols=$(tput cols) case "$_mime" in text/*) { printf " --- %s ---\n" "$_mime"; head -n "$_rows" "$_path"; } | less -SX +gq continue ;; inode/directory) { printf " --- %s --- (%s files)\n" "$_mime" \ $(find "$_path"/ -mindepth 1 -maxdepth 1 ! -name . ! -name .. 2>/dev/null | wc -l); \ ls -1Ap "$_path"; } | head -n "$((_rows - 1))" | cut -c 1-"$_cols" continue ;; esac ;; esac if [ "$_dest" ]; then chafa "$_dest" tput civis else printf " --- %s ---" "$_mime" fi done <"$pvfifo" } check_fifo() { if [ -p "$pvfifo" ]; then printf "#q" >"$sffpipe" rm -f "$pvfifo" exit 0 fi } wait_for_fifo() { _i=1 while [ ! -p "$pvfifo" ]; do sleep 0.1 _i=$((_i + 1)) [ "$_i" -gt 20 ] && exit 0 done } case "$2" in 'tui') check_fifo if [ "$TMUX" ]; then [ $(($(tput lines) * 2)) -gt "$(tput cols)" ] && _layout='v' || _layout='h' tmux split-window -d"$_layout" "$0" "$sffpipe" 'loop' else if [ -z "$SFF_PV_TERM" ]; then printf "\nSFF_PV_TERM: not set\n" printf "Terminal emulator for preview (empty to cancel): "; read -r _x [ -z "$_x" ] && exit 0 SFF_PV_TERM=$_x fi case "$SFF_PV_TERM" in 'kitty'|'gnome-terminal') "$SFF_PV_TERM" -- "$0" "$sffpipe" 'loop' & ;; 'xfce4-terminal'|'terminator') "$SFF_PV_TERM" -e "\"$0\" \"$sffpipe\" loop" & ;; *) "$SFF_PV_TERM" -e "$0" "$sffpipe" 'loop' & ;; esac fi wait_for_fifo printf "#p" >"$sffpipe" ;; 'gui') : ;; 'loop') tput civis preview_loop_tui ;; esac sff/sff-extfunc000077500000000000000000000321061502044103400140050ustar00rootroot00000000000000#!/usr/bin/env sh # BSD 2-Clause License # # Copyright (c) 2023-2025 Shi Yanling # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. 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 AUTHOR 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 AUTHOR 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. EDITOR=${EDITOR:-vi} PWD=${PWD:-$(pwd)} sffpipe=$1 sffdir=${sffpipe%/*} tmpdir=${TMPDIR:-/tmp} [ ! -w "$tmpdir" ] && tmpdir=$sffdir uid=$(id -u) exbuf1="${sffdir}/.exec-buf1" exbuf2="${sffdir}/.exec-buf2" lastop="${sffdir}/.last-operation" cpbuf="${sffdir}/.copy-buf" tsel="${tmpdir}/sff-tmpsel-$uid" tbuf1="${tmpdir}/sff-tmpbuf1-$uid" tbuf2="${tmpdir}/sff-tmpbuf2-$uid" unset LESS [ ! -p "$sffpipe" ] && exit 0 # === sff extension functions === sffpipe_clear_sel() { printf "." >"$sffpipe" } sffpipe_refresh() { [ "$1" = '-c' ] && _x='.' || _x='' printf "*%s" "$_x" >"$sffpipe" } sffpipe_sel_file() { printf "@%s\0" "$1" >"$sffpipe" } sffpipe_enter() { printf ">%s\0" "$1" >"$sffpipe" } sffpipe_get_sel() { printf "%s" "$$" >"$sffpipe" sel=$sffpipe } sff_init_bufs() { [ -e "$exbuf1" ] && [ -e "$exbuf2" ] && [ -e "$lastop" ] && [ -e "$cpbuf" ] && return 0 touch -a "$exbuf1" "$exbuf2" "$lastop" "$cpbuf" chmod 600 "$exbuf1" "$exbuf2" "$lastop" "$cpbuf" [ "$uid" -eq 0 ] && ls -nd "$sffpipe" | { read -r _ _ _x _; chown "$_x" "$exbuf1" "$exbuf2" "$lastop" "$cpbuf"; } } sff_abort() { rm -f "$tsel" "$tbuf1" "$tbuf2" [ "$1" ] && rm -f "$1" exit 0 } sff_pwd_perm() { if [ ! -w "$PWD" ]; then printf "\n%s: Permission denied\n" "$PWD" printf "Press Enter to continue "; read -r _x exit 0 fi } sff_new() { sff_pwd_perm sff_init_bufs _tlist="${tmpdir}/sff-create-files-$uid" : >"$_tlist" while true; do "$EDITOR" "$_tlist" [ -e "$_tlist" ] && sed -e 's|^/*||' -e 's|[ \t]*$||' -e '/^$/d' "$_tlist" >"$tbuf1" [ ! -s "$tbuf1" ] && sff_abort "$_tlist" sed 's|/.*$||' "$tbuf1" | sort -u >"$tbuf2" _existf=$(tr '\n' '\0' <"$tbuf2" | xargs -0 ls -1d -- 2>/dev/null | head -n 80) [ -z "$_existf" ] && break printf "\n%s\n" "$_existf" echo "^^^ file exists" printf "(e)dit list / (c)ancel [e]: "; read -r _x [ "$_x" = 'c' ] && sff_abort "$_tlist" done _pwd=$(printf '%s' "$PWD" | tr '\n' '\035' | sed 's/[\|&]/\\&/g') sed "s|^|$_pwd/|" "$tbuf2" | tr '\n\035' '\0\n' >"$exbuf2" sed -e '/\//!d' -e 's|/[^/]*$||' -e "s|^|$_pwd/|" "$tbuf1" | sort -u | tr '\n' '\0' >"$exbuf1" printf "\037" >>"$exbuf1" sed -e '/\/$/d' -e "s|^|$_pwd/|" "$tbuf1" | tr '\n' '\0' >>"$exbuf1" rm -f "$_tlist" "$tbuf1" "$tbuf2" sff_do_new } sff_do_new() { printf "new" >"$lastop" sffpipe_sel_file "$(tr '\n\0' '\035\n' <"$exbuf2" | head -n 1 | tr -d '\n' | tr '\035' '\n')" _err='' sed 's/.*$//' "$exbuf1" | tr '\035' '\n' | xargs -r -0 mkdir -p || _err=1 sed 's/^.*//' "$exbuf1" | tr '\035' '\n' | xargs -r -0 touch || _err=1 [ "$_err" ] && { printf "Press Enter to continue "; read -r _x; } } sff_undo_new() { [ ! -s "$exbuf2" ] && exit 0 _op=$(cat "$lastop") echo "" tr '\n\0' '\035\n' <"$exbuf2" | head -n 160 echo "^^^" $(tr '\n\0' '\035\n' <"$exbuf2" | wc -l) "files will be deleted" printf "Undo '%s'? (y/n) [n]: " "$_op"; read -r _x [ "$_x" != 'y' ] && exit 0 printf "un%s" "$_op" >"$lastop" sffpipe_refresh xargs -0 rm -rf <"$exbuf2" \ || { printf "Press Enter to continue "; read -r _x; } } sff_write_cbuf() { sff_init_bufs sffpipe_get_sel tr '\n\0' '\035\n' <"$sel" >"$cpbuf" [ "$1" = 'mv' ] && chmod u+s "$cpbuf" sffpipe_clear_sel } sff_view_cbuf() { echo "" if [ -s "$cpbuf" ]; then head -n 160 "$cpbuf" echo $(wc -l <"$cpbuf") "file(s) in buffer" else echo "Buffer is empty" fi printf "Press Enter to continue "; read -r _x } sff_clear_cbuf() { [ -s "$cpbuf" ] && : >"$cpbuf" } sff_paste() { [ ! -s "$cpbuf" ] || [ "$(find "$cpbuf" -mmin +30)" ] && exit 0 sff_pwd_perm _x=''; _op='copy' [ -u "$cpbuf" ] && { _op='move'; chmod u-s "$cpbuf"; } _existf=$(sed 's|^.*/||' "$cpbuf" | tr '\n\035' '\0\n' | xargs -0 ls -1d -- 2>/dev/null | head -n 80) if [ "$_existf" ]; then printf "\n%s\n" "$_existf" echo "^^^ files exist" printf "(s)kip all / (i)nteractive / (o)verwrite all / (c)ancel [c]: "; read -r _x [ "${_x%[sio]}" -o -z "$_x" ] && sff_abort fi _pwd=$(printf '%s' "$PWD" | tr '\n' '\035' | sed 's/[\|&]/\\&/g') sed "s|$|\n$_pwd/|" "$cpbuf" | tr '\n\035' '\0\n' >"$exbuf1" case "$_op" in 'copy') sed "s|^.*/|$_pwd/|" "$cpbuf" | tr '\n\035' '\0\n' >"$exbuf2" [ "$1" ] && : >"$cpbuf";; 'move') sed "s|^.*/|$_pwd/|" "$cpbuf" | paste -d '' - "$cpbuf" | tr '\037\n\035' '\0\0\n' >"$exbuf2" : >"$cpbuf";; esac _x=${_x:-'w'} sff_do_paste "$_op" "$_x" } sff_do_paste() { printf "%s" "$1" >"$lastop" [ "$2" != 'w' ] && touch -mt 202310011200.00 "$lastop" sffpipe_sel_file "$(tr '\n\0' '\035\n' <"$exbuf2" | head -n 1 | tr -d '\n' | tr '\035' '\n')" case "$1" in 'copy') printf "\nCopying...\n" case "$2" in 'o'|'w') xargs -0 -n 2 cp -afv <"$exbuf1";; 's') xargs -0 -n 2 cp -anv <"$exbuf1";; 'i') xargs -0 -n 2 -o cp -aiv <"$exbuf1";; esac ;; 'move') printf "\nMoving...\n" case "$2" in 'o'|'w') xargs -0 -n 2 mv -fv <"$exbuf1";; 's') xargs -0 -n 2 mv -nv <"$exbuf1";; 'i') xargs -0 -n 2 -o mv -iv <"$exbuf1";; esac ;; esac || { printf "Press Enter to continue "; read -r _x; } } sff_rename() { sff_init_bufs sffpipe_get_sel tr '\n\0' '\035\n' <"$sel" >"$tsel" [ ! -s "$tsel" ] && exit 0 sed 's|/[^/]*$||' "$tsel" >"$tbuf1" _tlist="${tmpdir}/sff-rename-$uid" if [ "$(sort -u "$tbuf1" | wc -l)" -eq 1 ]; then sed 's|^.*/||' "$tsel" >"$_tlist" else cat "$tsel" >"$_tlist" fi while true; do "$EDITOR" "$_tlist" sed -e 's|^.*/||' -e 's|^[ \t]*$||' "$_tlist" | paste -d '/' "$tsel" "$tbuf1" - \ | sed -e '/^\(.*\)\1$/d' -e '/^/d' -e '/\/$/d' >"$tbuf2" [ ! -s "$tbuf2" ] && sff_abort "$_tlist" _dupnames=$(cut -d '' -f 2 "$tbuf2" | sort | uniq -d | head -n 80) [ "$_dupnames" ] && printf "\n%s\n^^^ duplicate names\n" "$_dupnames" _existf=$(cut -d '' -f 2 "$tbuf2" | tr '\n\035' '\0\n' | xargs -0 ls -1d -- 2>/dev/null | head -n 80) [ "$_existf" ] && printf "\n%s\n^^^ file exists\n" "$_existf" [ -z "$_dupnames" ] && [ -z "$_existf" ] && break printf "(e)dit list / (c)ancel [e]: "; read -r _x [ "$_x" = 'c' ] && sff_abort "$_tlist" done tr '\037\n\035' '\0\0\n' <"$tbuf2" >"$exbuf1" sed 's/\(.*\)\(.*\)/\2\1/' "$tbuf2" | tr '\037\n\035' '\0\0\n' >"$exbuf2" rm -f "$tsel" "$_tlist" "$tbuf1" "$tbuf2" sff_do_rename } sff_do_rename() { printf "rename" >"$lastop" sffpipe_sel_file "$(tr '\n\0' '\035\n' <"$exbuf2" | head -n 1 | tr -d '\n' | tr '\035' '\n')" xargs -0 -n 2 mv -nv <"$exbuf1" \ || { printf "Press Enter to continue "; read -r _x; } } sff_undo_move() { [ ! -s "$exbuf2" ] && exit 0 _op=$(cat "$lastop") echo "" xargs -0 -n 2 printf "%s -> %s\n" <"$exbuf2" | head -n 160 printf "Undo '%s'? (y/n) [n]: " "$_op"; read -r _x [ "$_x" != 'y' ] && exit 0 printf "un%s" "$_op" >"$lastop" sffpipe_sel_file "$(tr '\n\0' '\035\n' <"$exbuf1" | head -n 1 | tr -d '\n' | tr '\035' '\n')" xargs -0 -n 2 mv -n <"$exbuf2" \ || { printf "Press Enter to continue "; read -r _x; } } sff_duplicate() { sff_init_bufs sffpipe_get_sel tr '\n\0' '\035\n' <"$sel" >"$tsel" [ ! -s "$tsel" ] && exit 0 printf "\nNumber of copies / (c)ancel [1]: "; read -r _x _x=$(printf "%s" "${_x:-'1'}" | tr -cd '0-9') _x=${_x#"${_x%%[!0]*}"} [ -z "$_x" ] && exit 0 : >"$exbuf1"; : >"$exbuf2" while IFS='' read -r _path; do _num=1 _path=$(printf "%s" "$_path" | tr '\035' '\n') for _ in $(seq "$_x"); do _npath="${_path}_$_num" _i=$_num while [ -e "$_npath" ]; do _i=$((_i + 1)) _npath="${_path}_$_i" done _num=$((_i + 1)) printf "%s\0%s\0" "$_path" "$_npath" >>"$exbuf1" printf "%s\0" "$_npath" >>"$exbuf2" done done <"$tsel" rm -f "$tsel" [ ! -s "$exbuf1" ] && sff_abort sff_do_duplicate } sff_do_duplicate() { printf "duplicate" >"$lastop" sffpipe_sel_file "$(tr '\n\0' '\035\n' <"$exbuf2" | head -n 1 | tr -d '\n' | tr '\035' '\n')" echo "Duplicating..." xargs -0 -n 2 cp -an <"$exbuf1" \ || { printf "Press Enter to continue "; read -r _x; } } sff_delete() { sffpipe_get_sel tr '\n\0' '\035\n' <"$sel" >"$tsel" [ ! -s "$tsel" ] && sff_abort echo "" head -n 160 "$tsel" printf "Permanently delete %s files? (y/n) [n]: " $(wc -l <"$tsel"); read -r _x [ "$_x" != 'y' ] && exit 0 sffpipe_refresh -c tr '\n\035' '\0\n' <"$tsel" | xargs -0 rm -rf \ || { printf "Press Enter to continue "; read -r _x; } rm -f "$tsel" } sff_chmod_chown() { printf "\nPermission or user:group (e.g., 644, -R a+x, root:wheel)\n" printf "(empty to cancel): "; read -r _x [ -z "$_x" ] && exit 0 sffpipe_get_sel case "$_x" in *:*) xargs -r -0 chown $_x <"$sel";; *) xargs -r -0 chmod $_x <"$sel";; esac || { printf "Press Enter to continue "; read -r _x; } sffpipe_refresh } sff_find() { printf "\nSearch filename (e.g., *.jpg, lib???, *)\n" printf "(empty to cancel): "; read -r _x [ -z "$_x" ] && exit 0 printf "More options (optional): "; read -r _x2 echo "Searching in $PWD/ ..." { printf "?"; find ./ $_x2 -name "$_x" -print0 2>/dev/null | tr '\n\0' '\035\n' \ | sed -e 's|^\./||' -e '/^[./]$/d' -e '/^\.\.$/d' -e '/^$/d' | tr '\n\035' '\0\n'; } >"$sffpipe" } sff_file_stat() { sffpipe_get_sel tr '\n\0' '\035\n' <"$sel" >"$tsel" [ ! -s "$tsel" ] && sff_abort while IFS='' read -r _path; do echo "" _path=$(printf "%s" "$_path" | tr '\035' '\n') if stat --version >/dev/null 2>&1; then stat "$_path" else stat -x "$_path" fi file -bi "$_path" file -b "$_path" done <"$tsel" rm -f "$tsel" printf "Press Enter to continue "; read -r _x } sff_disk_usage() { sffpipe_get_sel echo "" xargs -r -0 du -shc <"$sel" | sort -h echo "" df -h "$PWD" printf "Press Enter to continue "; read -r _x } sff_undo() { [ ! -s "$lastop" ] || [ "$(find "$lastop" -mmin +360)" ] && exit 0 case "$(cat "$lastop")" in 'new'|'copy'|'duplicate') sff_undo_new;; 'move'|'rename') sff_undo_move;; esac } sff_redo() { [ ! -s "$lastop" ] && exit 0 case "$(cat "$lastop")" in 'unnew') sff_do_new;; 'uncopy') sff_do_paste 'copy' 'w';; 'unmove') sff_do_paste 'move' 'w';; 'unrename') sff_do_rename;; 'unduplicate') sff_do_duplicate;; esac } sff_help() { sed -n '/[ ][#][?][>][ ]/p' "$0" | sed 's/^.*[#][?][>]//' | less } run_pl() { _plugin="${sffdir}/plugins/$1" [ ! -e "$_plugin" ] && _plugin="${0%/*}/plugins/$1" [ ! -e "$_plugin" ] && _plugin="/usr/libexec/sff/plugins/$1" [ ! -e "$_plugin" ] && _plugin="/usr/local/libexec/sff/plugins/$1" if [ ! -e "$_plugin" ]; then printf "\nPlugin '%s' not found\n" "$1" printf "Press Enter to continue "; read -r _x else "$_plugin" "$sffpipe" "$2" fi } # === custom functions === # === key bindings === case "$2" in #?> Extension functions: 'n') sff_new;; #?> Alt-n Create new files (ends with '/' for dirs) 'd') sff_delete;; #?> Alt-d Delete 'y') sff_write_cbuf 'cp';; #?> Alt-y Copy 'x') sff_write_cbuf 'mv';; #?> Alt-x Cut 'v') sff_view_cbuf;; #?> Alt-v View copy/cut buffer 'V') sff_clear_cbuf;; #?> Alt-V Clear copy/cut buffer 'p') sff_paste 'd';; #?> Alt-p Paste 'P') sff_paste;; #?> Alt-P Paste and keep copy buffer 'r') sff_rename;; #?> Alt-r Rename 'Y') sff_duplicate;; #?> Alt-Y Duplicate 'm') sff_chmod_chown;; #?> Alt-m Change permissions or owner 'f') sff_find;; #?> Alt-f Advanced search via 'find' 'i') sff_file_stat;; #?> Alt-i Show file status 'I') sff_disk_usage;; #?> Alt-I Show disk usage 'u') sff_undo;; #?> Alt-u Undo last operation 'U') sff_redo;; #?> Alt-U Redu last operation '/') sff_help;; #?> Alt-/ Show this help #?> Plugins: 'F') run_pl 'fzf-find';; #?> Alt-F Search via 'fzf' '=') run_pl 'preview' 'tui';; #?> Alt-= Toggle preview 'z') run_pl 'archive' 'e';; #?> Alt-z Extract archive 'Z') run_pl 'archive' 'c';; #?> Alt-Z Create archive esac #?> Press 'q' to leave this page. sff/sff.1000066400000000000000000000226751502044103400125010ustar00rootroot00000000000000.Dd 2025-5-22 .Dt SFF 1 .Os .Sh NAME .Nm sff .Nd simple and fast terminal file manager .Sh SYNOPSIS .Nm .Op Fl bcHmvh .Op Fl d Ar keys .Op Ar path .Sh DESCRIPTION .Nm (simple file finder) is a simple, fast, and feature-rich terminal file manager inspired by \fBnnn\fR and guided by the suckless philosophy. It consists of two parts: a core program and an extension script. The core program is designed as a pure file browser and selector with minimal built-in functionality. All file operations, such as copying, moving, and deleting, are implemented by the extension script. For more details, see the \fIEXTENSION SCRIPT\fR section. .Pp .Nm opens the current working directory if .Ar path is not specified. .Sh OPTIONS The following options are available: .Bl -tag -width indent .It Fl b Force the program to run in browse mode. For more details, see the \fIRUNNING MODE\fR section. .It Fl c Enable case sensitivity when sorting by filename. .It Fl d Ar keys Specify the details to show by default. Valid keys include: .Pp - \fBt\fR: time - \fBo\fR: owner & group - \fBp\fR: permissions - \fBs\fR: size - \fBn\fR: none .It Fl H Show hidden files. .It Fl h Display program help and exit. .It Fl m Mix directories and files when sorting. .It Fl v Print version information and exit. .Sh KEY BINDINGS Press '?' or 'F1' in .Nm to see the list of key bindings for built-in functions. .Pp Press Alt+'/' in .Nm to see the list of key bindings for extension functions and plugins. .Sh CONFIGURATION .Nm does not use a runtime configuration file. To customize .Nm , you need to edit \fBconfig.h\fR and recompile the program. \fBconfig.h\fR is a source code file which is included by \fBsff.c\fR (the main source code module). It is a C language header file, and serves as the configuration file for default settings, key bindings, and colors. .Sh EXTENSION SCRIPT The extension functions are provided by a POSIX-compliant shell script named \fBsff-extfunc\fR, which is installed by default in .Pa /usr/libexec/sff/ or .Pa /usr/local/libexec/sff/ . .Pp You can easily customize key bindings for extension functions and plugins, modify existing functions, or add your own functions by editing this file. It is generally not recommended to directly modify the system-wide \fBsff-extfunc\fR. Instead, copy it to the user's config directory and make modifications there. .Pp The user's config directory is required for executing extension functions. This directory is either .Pa $XDG_CONFIG_HOME/sff or .Pa ~/.config/sff , whichever is encountered first. If this directory does not exist, the program will attempt to create it when calling an extension function. .Pp During initialization, .Nm determines the location of \fBsff-extfunc\fR by checking the following directories in order, and uses the first occurrence found: .Pp 1. The user's config directory 2. The directory where the .Nm executable resides 3. /usr/libexec/sff/ or /usr/local/libexec/sff/ .Sh TABS The tab status is displayed in the top-left corner of the screen. Five tab indicators are shown, with the current tab highlighted in reverse video. Tabs 1 through 4 are regular tabs and are indicated by '*' when inactive. When switching to an inactive tab, the new tab will be activated and use the current directory path as its starting path. The fifth tab is a special tab, indicated by '#', dedicated to handling search results. .Sh SELECTION The names of the selected files are highlighted in reverse video. By default, the file under the cursor is automatically selected. When a file selection operation is performed by the user, the program enters manual selection mode, and the file under the cursor will no longer be automatically selected. Clearing all selections causes the program to exit manual selection mode and return to the default state. .Pp .Nm allows file selection across directories. Each tab maintains its own independent selection state. The second set of numbers in the bottom status bar (highlighted in reverse video) indicates the total number of selected files in the current tab. .Pp When an extension function is executed, if it sends a request, the absolute paths of the selected files are delivered to the extension function via a FIFO. .Sh FILTERS Filters are strings used to dynamically list matching files in the current directory. When a filter is enabled, it appears above the bottom status bar, and the program enters input mode. In this mode, you can perform the following actions: .Pp - Enter a filter matching string (matching is case-insensitive). .Pp - Use the Up and Down Arrow keys to move the cursor. .Pp - Press Enter or Esc to exit input mode while keeping the filter active. .Pp - Press '/' to disable the filter. .Pp The filter only applies to the current directory. When navigating away from the current directory, the filter is automatically disabled. .Sh QUICK FIND Quick Find is used to quickly locate a file within the current directory. When Quick Find is enabled, it appears above the bottom status bar, and the program enters input mode. In this mode, you can perform the following actions: .Pp - Enter a search string to match filenames. .Pp - Enter '/' as the first character to navigate to the root directory. .Pp - Press Tab or '/' to enter the directory under the cursor and clear the search string. .Pp - Press the Left Arrow key to go to the parent directory. .Pp - Use the Up and Down Arrow keys to move the cursor. .Pp - Press Enter or Esc to exit Quick Find. .Pp Matching is case-insensitive and prioritizes matches at the beginning of filenames. If no filename starts with the search string, it matches filenames containing the string. Upon a match, the cursor jumps to the first matching file. .Sh ADVANCED SEARCH Advanced Search is an extension function based on the \fBfind\fR command. It requires two inputs: .Pp 1. Filename pattern: This is passed to the -name option of the \fBfind\fR command. So remember to use wildcards when necessary. For example, to search for files containing 'lib' in their name, enter '*lib*' instead of just 'lib'. If you do not want to search by filename, enter '*' to match all files. .Pp 2. Additional search options: Here, you can provide more options for the \fBfind\fR command, such as '-size +4k' to search for files larger than 4KB. If no additional options are needed, leave this field blank and press Enter. .Pp After both inputs are provided, the executed command will be: .Pp find ./ input2 -name "input1" .Pp The search results are sent back to .Nm and listed in the fifth tab, where you can further process them. .Sh UNDO AND REDO .Nm supports undoing or redoing the last file operation. Supported operations include: .Pp - Create new files - Copy-paste (when none of the pasted files already exist) - Cut-paste (when none of the pasted files already exist) - Rename - Duplicate .Pp Undo/redo actions apply across different tabs and even different .Nm instances. A file operation performed in one .Nm instance can be undone or redone in another instance. .Sh RUNNING MODE .Pp \fBBrowse Mode\fR: .br A green reversed 'B' is displayed in the bottom-left corner of the screen as an indicator. .Pp This can be considered a safe mode. In this mode, extension functions are disabled, and .Nm does not make any changes to the file system. .Pp The program is forced into browse mode and cannot exit this mode until termination under the following conditions: .Pp - When the -b option is used while running .Nm - During initialization, if certain non-fatal errors occur (e.g., the \fBsff-extfunc\fR file is missing). .Pp \fBSudo Mode\fR: .br A red reversed 'S' is displayed in the bottom-left corner of the screen as an indicator. .Pp When .Nm is run as a regular user and switched to sudo mode, the following operations are executed with superuser privileges: .Pp - All extension functions - File editing .Pp However, all other operations are still performed by the current user. .Pp When .Nm is run as the superuser, the program will always run in sudo mode until termination or can be switched to browse mode. All operations are performed by the superuser. .Sh PLUGINS Plugins are shell scripts used to extend functionality. They are invoked by the extension script, which also sets their keybindings. By default, plugins are installed in .Pa /usr/libexec/sff/plugins/ or .Pa /usr/local/libexec/sff/plugins/ . .Pp Detailed information about a plugin should usually be provided as comments at the beginning of the script. To view this information, refer directly to the plugin file. .Pp If you wish to modify or add your own plugins, it is recommended to do so in the \fBplugins\fR directory within the user's config directory. For details on the user's config directory, see the \fIEXTENSION SCRIPT\fR section. When the extension script invokes a plugin, it searches for the plugin in the following locations in order and uses the first match found: .Pp 1. The \fBplugins\fR directory in the user's config directory 2. The \fBplugins\fR directory where the currently running extension script resides 3. /usr/libexec/sff/plugins/ or /usr/local/libexec/sff/plugins/ .Pp .Sh ENVIRONMENT \fBEDITOR\fR: The default text editor used in the program. If not set, 'vi' is used. .Pp \fBHOME\fR: The home directory used by the program. If not set, '/' is used. .Sh AUTHORS .An Shi Yanling Aq Mt sylphenix@outlook.com .Sh HOMEPAGE .Em https://codeberg.org/sylphenix/sff sff/sff.c000066400000000000000000001620001502044103400125460ustar00rootroot00000000000000/* * BSD 2-Clause License * * Copyright (c) 2023-2025 Shi Yanling * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. 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 AUTHOR 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 AUTHOR 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. */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifndef _XOPEN_SOURCE_EXTENDED #define _XOPEN_SOURCE_EXTENDED #endif #include #ifndef VERSION #define VERSION "1.1" #endif #ifndef EXTFNNAME #define EXTFNNAME "sff-extfunc" #endif #ifndef EXTFNPREFIX #define EXTFNPREFIX "/usr/local/libexec/sff" #endif #ifndef PATH_MAX #define PATH_MAX 4096 #endif #ifndef NAME_MAX #define NAME_MAX 255 #endif #define TABS_MAX 4 // Number of tabs, the range of acceptable values is 1-7 #define ENTRY_INCR 128 // Number of Entry structures to allocate per shot #define NAME_INCR 4096 // 128 entries * avg. 32 chars per name = 4KB #define FILT_MAX 128 // Maximum length of filter string #define HSTAT_INCR 16 // Number of Histstat structures to allocate each time #define LENGTH(X) (sizeof X / sizeof X[0]) #define MIN(x, y) ((x) < (y) ? (x) : (y)) #define MAX(x, y) ((x) > (y) ? (x) : (y)) enum entryflag { E_REG_FILE = 0x01, E_DIR_DIRLNK = 0x02, E_SEL = 0x04, E_SEL_SCANED = 0x08, E_NEW = 0x10 }; enum filetypes { F_REG = 0, F_DIR, F_CHR, F_BLK, F_IFO, F_LNK, F_SOCK, F_HLNK, F_EXEC, F_EMPT, F_ORPH, F_MISS, F_UNKN }; enum colorflag { C_DETAIL = F_UNKN + 1, C_TABTAG, C_PATHBAR, C_STATBAR, C_WARN, C_NEWFILE }; enum histstatflag { S_UNVIS = 0, S_VIS, S_ROOT, S_SUBROOT }; enum procctrl { GO_NONE = 0, GO_FASTDRAW, GO_STATBAR, GO_REDRAW, GO_SORT, GO_RELOAD, GO_QUIT }; typedef struct { char *name; // 8 bytes off_t size; // 8 bytes time_t sec; // 8 bytes unsigned int nsec; // 4 bytes mode_t mode; // 4 bytes uid_t uid; // 4 bytes gid_t gid; // 4 bytes unsigned short type; // 2 bytes unsigned short flag; // 2 bytes unsigned short nlen; // 2 bytes unsigned short misc; // 2 bytes } Entry; typedef struct { char name[NAME_MAX + 1]; int cur; int scrl; int flag; } Histstat; typedef struct { char path[PATH_MAX]; Histstat *hs; Histstat *stat; unsigned int nhs; unsigned int ths; } Histpath; struct selstat { char path[PATH_MAX]; struct selstat *prev; struct selstat *next; char *nbuf; char *endp; unsigned int buflen; }; typedef struct { unsigned int enabled : 1; unsigned int showhidden : 1; // Show hidden files unsigned int dirontop : 1; // Sort directories on the top unsigned int sortby : 3; // (0: name, 1: size, 2: time, 3: extension) unsigned int caseinsen : 1; // Case insensitive unsigned int natural : 1; // Natural numeric sorting unsigned int reverse : 1; // Reverse sort unsigned int showtime : 1; // Show time info unsigned int showowner : 1; // Show owner:group info unsigned int showperm : 1; // Show permissions info unsigned int showsize : 1; // Show size info unsigned int timetype : 2; // (0: access, 1: modify, 2: change) unsigned int havesel : 1; // (0: no selection in current path, 1: have selection) unsigned int selmode : 1; // (0: normal mode, 1: selection mode) // global settings unsigned int ct : 3; // Current tab unsigned int lt : 3; // Last tab unsigned int mode : 3; // (0: normal, 1: sudo, 2: permanent sudo, 3: browse, 4: permanent browse) unsigned int newent : 1; // (0: do not mark new entry, 1: mark new entry) } Settings; typedef struct { Histpath *hp; struct selstat *ss; char filt[FILT_MAX]; char find[FILT_MAX]; int ftlen; int fdlen; int nde; int nsel; Settings cfg; } Tabs; typedef struct { int keysym1; int keysym2; int (*func)(int); int arg; char cmnt[40]; } Key; /*** Global Variables ***/ static int ndents = 0, cursel = 0, lastsel = -1, curscroll = 0, lastscroll = -1; static int markent = -1, errline = 0, errnum = 0; static int xlines, xcols, onscr, ncols; static unsigned int tdents = 0, namebuflen = 0; static char *home, *editor; static char *cfgpath = NULL, *extfunc = NULL, *pipepath = NULL, *pvfifo = NULL; static char *pnamebuf = NULL, *pfindbuf = NULL, *pfindend = NULL, *findname = NULL; static Entry *pdents = NULL; static Tabs *ptab = NULL; alignas(max_align_t) static char gpbuf[PATH_MAX * sizeof(wchar_t)] = {0}; alignas(max_align_t) static char gmbuf[PATH_MAX * sizeof(wchar_t)] = {0}; alignas(max_align_t) static Tabs gtab[TABS_MAX + 1] = {0}; alignas(max_align_t) static Histpath ghpath[(TABS_MAX + 1) * 2] = {0}; /****** Generic Functions ******/ #ifdef DEBUG static void dbgprint(char *vn, char *str, int n) { FILE *fp = fopen("/tmp/sffdbg", "a"); if (!fp) { perror("dbg"); return; } fprintf(fp, "--- %s: %s %d\n", vn, str, n); fclose(fp); } #endif /* Get directory portion of pathname. Source would be modified!!! */ static char *xdirname(char *path) { char *p = memrchr(path, '/', strlen(path)); if (p == path) path[1] = '\0'; else if (p) *p = '\0'; return path; } /* Get filename portion of pathname. Source would be untouched. */ static char *xbasename(char *path) { char *p = memrchr(path, '/', strlen(path)); return p ? p + 1 : path; } /* Make path/name in buf. Ensure buf size is not less than PATH_MAX. */ static int makepath(const char *path, const char *name, char *buf) { char *p; if (!path || !path[0] || !buf) return 0; if (path == buf) p = memchr(buf, '\0', PATH_MAX - 2); else if ((p = memccpy(buf, path, '\0', PATH_MAX - 2))) --p; if (p) { if (*(p - 1) != '/') *p++ = '/'; p = memccpy(p, name, '\0', PATH_MAX - (p - buf) - 1); } return p ? p - buf : 0; } /* Get file extension. Extensions longer than 8 chars will be ignored. */ static char *getextension(char *name, int len) { char *p; if (len > 3) { p = name + len - 2; len = (len > 11) ? 9 : len - 2; while (--len > 0) if (*--p == '.') return p; } return NULL; } /* Get the absolute pathname without resolving symlinks. Neither path nor buf can be NULL. Ensure buf size is not less than PATH_MAX. */ static char *abspath(const char *path, char *buf) { const char *src; char *dst; size_t len = 0; if (!path || !buf) return NULL; if (path[0] != '/') { if (!getcwd(buf, PATH_MAX)) return NULL; len = strlen(buf); } else ++path; if (len + strlen(path) + 2 > PATH_MAX) { errno = ENAMETOOLONG; return NULL; } src = path; dst = buf + len; *dst++ = '/'; while (*src) { if (src[0] == '/' && (dst[-1] == '/' || src[1] == '/' || src[1] == '\0')) { ++src; continue; } else if (dst[-1] == '/' && src[0] == '.' && (src[1] == '/' || src[1] == '\0')) { src = (src[1] == '\0') ? src + 1 : src + 2; continue; } else if (dst[-1] == '/' && src[0] == '.' && src[1] == '.' && (src[2] == '/' || src[2] == '\0')) { dst = (char *)memrchr(buf, '/', MAX(1, dst - buf - 1)) + 1; src = (src[2] == '\0') ? src + 2 : src + 3; continue; } *dst++ = *src++; } if (dst - 1 > buf && dst[-1] == '/') dst[-1] = '\0'; else dst[0] = '\0'; return buf; } /* Convert unsigned integer to string. The maximum value it can handle is 4,294,967,295 This is a modified version of xitoa() from nnn. https://github.com/jarun/nnn */ static char *xitoa(unsigned int val) { static char dst[16] = {0}; static const char digits[204] = "0001020304050607080910111213141516171819" "2021222324252627282930313233343536373839" "4041424344454647484950515253545556575859" "6061626364656667686970717273747576777879" "8081828384858687888990919293949596979899"; unsigned int i, j, quo; for (i = 14; val >= 100; --i) { // Fill digits backward from dst[14] quo = val / 100; j = (val - (quo * 100)) << 1; val = quo; dst[i] = digits[j + 1]; dst[--i] = digits[j]; } if (val >= 10) { j = val << 1; dst[i] = digits[j + 1]; dst[--i] = digits[j]; } else dst[i] = '0' + val; return &dst[i]; } /* Convert integer size to string like 6.2K 25.0M 198.3G etc. */ static char *tohumansize(off_t size) { static char sbuf[12] = {0}; static const char unit[12] = "BKMGTPEZY"; char *sp; int i, numint, frac = 0; for (i = 0; size >= 1024000; ++i) size >>= 10; if (i > 0 || size >= 1024) { size += 51; // round frac by (x + 51) / 100 numint = size >> 10; frac = (size & 1023) * 10 >> 10; // by simplifying (size % 1024) * 1000 / 1024 / 100 ++i; } else numint = size; sp = (char *)memccpy(sbuf, xitoa(numint), '\0', 6) - 1; if (i > 0) { *sp++ = '.'; *sp++ = '0' + frac; } *sp = unit[i]; *++sp = '\0'; return sbuf; } /* Convert inode permission info into a symbolic string, except the inode type. */ static char *strperms(mode_t mode) { static char str[12] = {0}; str[0] = mode & S_IRUSR ? 'r' : '-'; str[1] = mode & S_IWUSR ? 'w' : '-'; str[2] = (mode & S_ISUID ? (mode & S_IXUSR ? 's' : 'S') : (mode & S_IXUSR ? 'x' : '-')); str[3] = mode & S_IRGRP ? 'r' : '-'; str[4] = mode & S_IWGRP ? 'w' : '-'; str[5] = (mode & S_ISGID ? (mode & S_IXGRP ? 's' : 'S') : (mode & S_IXGRP ? 'x' : '-')); str[6] = mode & S_IROTH ? 'r' : '-'; str[7] = mode & S_IWOTH ? 'w' : '-'; str[8] = (mode & S_ISVTX ? (mode & S_IXOTH ? 't' : 'T') : (mode & S_IXOTH ? 'x' : '-')); return str; } /* Returns the cached user name if the provided uid is the same as the previous uid. */ static char *getpwname(uid_t uid) { static char *unamecache = NULL; static uid_t uidcache = (uid_t)-1; if (uid != uidcache) { struct passwd *pw = getpwuid(uid); unamecache = pw ? pw->pw_name : NULL; uidcache = uid; } return unamecache ? unamecache : xitoa(uid); } /* Returns the cached group name if the provided gid is the same as the previous gid. */ static char *getgrname(gid_t gid) { static char *gnamecache = NULL; static gid_t gidcache = (gid_t)-1; if (gid != gidcache) { struct group *gr = getgrgid(gid); gnamecache = gr ? gr->gr_name : NULL; gidcache = gid; } return gnamecache ? gnamecache : xitoa(gid); } static int seterrnum(int line, int err) { errline = line; errnum = err; return TRUE; } static void *irealloc(void *ptr, unsigned int *len, int incr, size_t size) { void *p = realloc(ptr, (*len += incr) * size); if (p == NULL && seterrnum(__LINE__, errno)) *len -= incr; return p; } /****** Key Functions ******/ static int movecursor(int n); static int movequarterpage(int n); static int scrollpage(int n); static int scrolleighth(int n); static int movetoedge(int n); static int switchhistpath(int n); static int enterdir(int n); static int gotoparent(int n); static int gotohome(int n); static int refreshview(int n); static int openfile(int n); static int toggleselection(int n); static int selectall(int n); static int invertselection(int n); static int selectrange(int n); static int clearselection(int n); static int setfilter(int n); static int quickfind(int n); static int qfindnext(int n); static int switchtab(int n); static int closetab(int n); static int togglemode(int n); static int viewoptions(int n); static int showhelp(int n); static int quitsff(int n); #include "config.h" // Configuration static void spawn(char *arg0, char *arg1, char *arg2, int detach, int sudo) { pid_t pid; char *args[5] = {SUDOER, arg0, arg1, arg2, NULL}; char **argv = sudo ? &args[0] : &args[1]; struct sigaction oldsigtstp, oldsigwinch; struct sigaction act = {.sa_handler = SIG_IGN}; pid = fork(); if (pid > 0) { sigaction(SIGTSTP, &act, &oldsigtstp); sigaction(SIGWINCH, &act, &oldsigwinch); waitpid(pid, NULL, 0); sigaction(SIGTSTP, &oldsigtstp, NULL); sigaction(SIGWINCH, &oldsigwinch, NULL); } else if (pid == 0) { if (detach) { pid = fork(); // Fork a grandchild to detach if (pid != 0) _exit(EXIT_SUCCESS); setsid(); // Suppress stdout and stderr int fd = open("/dev/null", O_WRONLY, 0200); if (fd != -1) { dup2(fd, STDOUT_FILENO); dup2(fd, STDERR_FILENO); close(fd); } } sigaction(SIGTSTP, &act, NULL); act.sa_handler = SIG_DFL; sigaction(SIGINT, &act, NULL); sigaction(SIGPIPE, &act, NULL); execvp(*argv, argv); _exit(EXIT_SUCCESS); } else seterrnum(__LINE__, errno); } static int shiftcursor(int step, int scrl) { lastsel = cursel; lastscroll = curscroll; cursel = MAX(0, MIN(ndents - 1, cursel + step)); if ((step == 1 || step == -1) && scrl == 0) { if ((cursel < curscroll + ((onscr + 2) / 4) && step < 0) || (cursel >= curscroll + onscr - ((onscr + 2) / 4) && step > 0)) curscroll += step; } else curscroll += scrl; curscroll = MIN(curscroll, MIN(cursel, ndents - onscr)); curscroll = MAX(curscroll, MAX(cursel - (onscr - 1), 0)); if (lastsel == cursel && lastscroll == curscroll) return GO_FASTDRAW; return GO_REDRAW; } static int movecursor(int n) { return shiftcursor(n, 0); } static int movequarterpage(int n) { return shiftcursor(onscr / 4 * n, 0); } static int scrollpage(int n) { int step = (xlines - 5) * n; return shiftcursor(step, step); } static int scrolleighth(int n) { int step = ((ndents + 3) / 8) * n; return shiftcursor(step, step); } static int movetoedge(int n) { return shiftcursor(ndents * n, 0); } static inline void savehiststat(Histstat *hs) { if (ndents > 0) { strncpy(hs->name, pdents[cursel].name, NAME_MAX); hs->cur = cursel; hs->scrl = curscroll; } } static Histpath *inithistpath(Histpath *hp, char *path, int check) { char *name = NULL; struct stat sb; Histstat *tmphs; if (check) { if (lstat(path, &sb) == -1 && seterrnum(__LINE__, errno)) { return NULL; } else if (!S_ISDIR(sb.st_mode)) { name = xbasename(path); xdirname(path); } if (chdir(path) == -1 && seterrnum(__LINE__, errno)) return NULL; } // Each level of path corresponds to a histstat. Add one more for current level hp->nhs = 0; for (char *p = path, *p2 = hp->path; ; ++p, ++p2) { if (*p == '/' || (*p == '\0' && path[1] != '\0')) { if (hp->nhs == hp->ths) { if (!(tmphs = irealloc(hp->hs, &hp->ths, HSTAT_INCR, sizeof(Histstat)))) { free(hp->hs); memset(hp, 0, sizeof(Histpath)); return NULL; } hp->hs = tmphs; } memset((hp->hs + hp->nhs++), 0, sizeof(Histstat)); } *p2 = *p; if (*p == '\0') break; } hp->stat = hp->hs + hp->nhs - 1; hp->stat->flag = S_VIS; if (name) { findname = strncpy(hp->stat->name, name, NAME_MAX); if (*name == '.') gcfg.showhidden = 1; } return hp; } static int newhistpath(char *path) { Histpath *hp = ptab->hp; Histpath *hp2 = ((hp - ghpath) & 1) ? hp - 1 : hp + 1; if (strcmp(hp->path, path) == 0) return GO_NONE; if (!inithistpath(hp2, path, TRUE)) return GO_STATBAR; if (hp->stat->flag == S_ROOT) hp2->stat->flag = S_SUBROOT; savehiststat(hp->stat); ptab->hp = hp2; return GO_RELOAD; } static int switchhistpath(int n) { Histpath *hp = ptab->hp; Histpath *hp2 = ((hp - ghpath) & 1) ? hp - 1 : hp + 1; if ((gcfg.ct == TABS_MAX && n == 0) || !hp2->path[0] || chdir(hp2->path) == -1) return GO_NONE; savehiststat(hp->stat); findname = hp2->stat->name; ptab->hp = hp2; return GO_RELOAD; } static int enterdir(int n __attribute__((unused))) { Histpath *hp = ptab->hp; Histpath *hp2 = ((hp - ghpath) & 1) ? hp - 1 : hp + 1; Histstat *tmphs, *hs = hp->stat; unsigned int nhs = hs - hp->hs + 1; char *newpath = gpbuf; Entry *ent = &pdents[cursel]; if (ndents == 0 || !(ent->flag & E_DIR_DIRLNK)) return GO_NONE; makepath(hp->path, ent->name, newpath); if (hs->flag == S_ROOT) { if (strcmp(newpath, hp2->path) == 0) return switchhistpath(1); else return newhistpath(newpath); } if (chdir(newpath) == -1 && seterrnum(__LINE__, errno)) return GO_STATBAR; if (nhs == hp->ths) { if (!(tmphs = irealloc(hp->hs, &hp->ths, HSTAT_INCR, sizeof(Histstat)))) { chdir(hp->path); return GO_STATBAR; } hp->hs = tmphs; } else if (nhs < hp->nhs) { if (strcmp(ent->name, hs->name) != 0) { if ((strcmp(hp->path, hp2->path) == 0 && strcmp(ent->name, hp2->stat->name) == 0) || (gcfg.ct < TABS_MAX && inithistpath(hp2, hp->path, FALSE))) hp = hp2; else hp->nhs = nhs; } findname = (hp->stat + 1)->name; } hp->stat = hp->hs + nhs; if (nhs == hp->nhs) { memset(hp->stat, 0, sizeof(Histstat)); hp->stat->flag = S_VIS; ++hp->nhs; } savehiststat(hp->stat - 1); strncpy(hp->path, newpath, PATH_MAX - 1); ptab->hp = hp; return GO_RELOAD; } static int gotoparent(int n __attribute__((unused))) { char *path = gtab[gcfg.ct].hp->path; Histstat *hs = gtab[gcfg.ct].hp->stat; if ((path[0] == '/' && path[1] == '\0') || hs->flag == S_ROOT) return GO_NONE; if (hs->flag == S_SUBROOT) return switchhistpath(1); savehiststat(hs); do { --hs; if (hs->flag == S_UNVIS) { strncpy(hs->name, xbasename(path), NAME_MAX); hs->flag = S_VIS; } } while (chdir(xdirname(path)) == -1 && path[1] != '\0' && hs->flag != S_SUBROOT); findname = hs->name; ptab->hp->stat = hs; return GO_RELOAD; } static int gotohome(int n) { if (gcfg.ct == TABS_MAX) return GO_NONE; return newhistpath((n == 1 && home) ? home : "/"); } static int refreshview(int n) { Histstat *hs = ptab->hp->stat; if (ndents > 0) { savehiststat(hs); findname = hs->name; } if (n == 1) gcfg.newent ^= 1; if (n == 2) return GO_SORT; return GO_RELOAD; } static int openfile(int n) { Entry *ent = &pdents[cursel]; if (ndents == 0) return GO_NONE; makepath(ptab->hp->path, ent->name, gpbuf); switch (n) { case 1: if (!(ent->flag & E_REG_FILE)) return GO_NONE; endwin(); spawn(editor, gpbuf, NULL, FALSE, gcfg.mode == 1); refresh(); return refreshview(0); default : if (ent->flag & E_DIR_DIRLNK) return enterdir(0); spawn(OPENER, gpbuf, NULL, TRUE, FALSE); } return GO_STATBAR; } static struct selstat *addselstat(struct selstat *ss, char *path) { struct selstat *n = malloc(sizeof(struct selstat)); if (!n && seterrnum(__LINE__, errno)) return NULL; n->nbuf = calloc(NAME_INCR, 1); if (!n->nbuf && seterrnum(__LINE__, errno)) { free(n); return NULL; } if (ss) { while (ss->next) ss = ss->next; ss->next = n; } n->prev = ss; n->next = NULL; strncpy(n->path, path, PATH_MAX - 1); n->endp = n->nbuf; n->buflen = NAME_INCR; return n; } static void deleteselstat(struct selstat *ss) { if (!ss) return; gtab[gcfg.ct].ss = NULL; if (ss->prev) { ss->prev->next = ss->next; gtab[gcfg.ct].ss = ss->prev; } if (ss->next) { ss->next->prev = ss->prev; gtab[gcfg.ct].ss = ss->next; } free(ss->nbuf); free(ss); ptab->cfg.havesel = 0; if (!ptab->ss) ptab->cfg.selmode = 0; } static void deleteallselstat(struct selstat *ss) { struct selstat *tmp; if (!ss) return; while (ss->next) ss = ss->next; while (ss) { tmp = ss->prev; free(ss->nbuf); free(ss); ss = tmp; } } static struct selstat *getselstat(void) { struct selstat *ss = ptab->ss; if (ndents == 0) return NULL; if (ptab->cfg.havesel == 0) { ss = addselstat(ss, ptab->hp->path); if (ss) ptab->cfg.havesel = 1; ptab->ss = ss; } return ss; } static int appendselection(Entry *ent) { size_t len; struct selstat *ss = getselstat(); char *tmp; if (!ss) return FALSE; len = ss->endp - ss->nbuf; if (ent->nlen >= ss->buflen - len) { if (!(tmp = irealloc(ss->nbuf, &ss->buflen, NAME_INCR, 1))) return FALSE; ss->nbuf = tmp; ss->endp = len + ss->nbuf; } strncpy(ss->endp, ent->name, ent->nlen); ss->endp += ent->nlen; ent->flag |= E_SEL; ++gtab[gcfg.ct].nsel; if (!ptab->cfg.selmode) ptab->cfg.selmode = 1; return TRUE; } static char *findinbuf(char *buf, size_t len, char *name, int nlen) { char *dst, *src = buf; for (;;) { dst = memmem(src, len, name, nlen); if (!dst) return NULL; if (dst == buf || dst[-1] == '\0') return dst; src += nlen; // found name as a substring of another name, move forward if (src >= buf + len) return NULL; } } static void removeselection(Entry *ent) { char *dst, *src; struct selstat *ss = ptab->ss; if (!ss || !ptab->cfg.havesel) return; dst = findinbuf(ss->nbuf, ss->endp - ss->nbuf, ent->name, ent->nlen); if (!dst) return; src = dst + ent->nlen; memmove(dst, src, ss->endp - src); ss->endp -= ent->nlen; if (ss->endp <= ss->nbuf) deleteselstat(ss); ent->flag &= ~E_SEL; --gtab[gcfg.ct].nsel; } static int toggleselection(int n) { if (pdents[cursel].flag & E_SEL) removeselection(&pdents[cursel]); else appendselection(&pdents[cursel]); return shiftcursor(n, 0); } static int selectall(int n __attribute__((unused))) { for (int i = 0; i < ndents; ++i) if (!(pdents[i].flag & E_SEL)) appendselection(&pdents[i]); return GO_REDRAW; } static int invertselection(int n __attribute__((unused))) { struct selstat *ss = getselstat(); if (!ss) return GO_STATBAR; ss->endp = ss->nbuf; for (int i = 0; i < ndents; ++i) { if (pdents[i].flag & E_SEL) { pdents[i].flag &= ~E_SEL; --gtab[gcfg.ct].nsel; } else appendselection(&pdents[i]); } if (ss->endp == ss->nbuf) deleteselstat(ss); return GO_REDRAW; } static int selectrange(int n) { int step = (cursel >= markent) ? 1 : -1; if (ndents == 0) return GO_NONE; if (markent == -1) { markent = cursel; ptab->cfg.selmode = 1; return shiftcursor(0, 0); } if (n > 0) { for (int i = markent; (step > 0) ? i <= cursel : i >= cursel; i += step) if (!(pdents[i].flag & E_SEL)) appendselection(&pdents[i]); } else { for (int i = markent; (step > 0) ? i <= cursel : i >= cursel; i += step) if (pdents[i].flag & E_SEL) removeselection(&pdents[i]); } markent = -1; if (!ptab->ss) ptab->cfg.selmode = 0; return GO_REDRAW; } static int clearselection(int n __attribute__((unused))) { deleteallselstat(ptab->ss); ptab->ss = NULL; ptab->nsel = 0; ptab->cfg.havesel = 0; ptab->cfg.selmode = 0; markent = -1; for (int i = 0; i < ptab->nde; ++i) if (pdents[i].flag & E_SEL) pdents[i].flag &= ~E_SEL; return GO_REDRAW; } static int setfilter(int n) { static Histstat *hs = NULL; switch (n) { case 1: // turn on or activate filter if (ptab->ftlen == 0) { // ftlen=0 no filter, ftlen<0 inactive, ftlen>0 active ptab->ftlen = 1; ptab->filt[0] = '\0'; hs = ptab->hp->stat; } else if (ptab->ftlen < 0) ptab->ftlen = -ptab->ftlen; break; case 2: // turn off filter when path changed if (hs == ptab->hp->stat) return GO_NONE; // fallthrough case 0: // turn off filter ptab->ftlen = 0; ndents = ptab->nde; } return GO_REDRAW; } static int quickfind(int n __attribute__((unused))) { if (ptab->fdlen <= 0) { // fdlen=0 no quick find, fdlen<0 invisible, fdlen>0 active ptab->find[0] = '\0'; ptab->fdlen = 1; } return GO_REDRAW; } static int qfindnext(int n) { int sta = (n == 0) ? 0 : cursel + n; if (ptab->fdlen == 0 || ptab->find[0] == '\0') return GO_NONE; n = (n == 0) ? 1 : n; for (int i = sta; i >= 0 && i < ndents; i += n) { if (strcasestr(pdents[i].name, ptab->find)) { cursel = i; curscroll = MAX(i - (onscr * 3 >> 2), MIN(i - (onscr >> 2), curscroll)); return GO_REDRAW; } } return GO_REDRAW; } static int inittab(char *path, int n) { deleteallselstat(gtab[n].ss); gtab[n].ss = NULL; gtab[n].hp = inithistpath(&ghpath[n * 2], path, TRUE); if (!gtab[n].hp) return FALSE; if (n == TABS_MAX) gtab[n].hp->stat->flag = S_ROOT; gtab[n].ftlen = gtab[n].fdlen = 0; gtab[n].nde = gtab[n].nsel = 0; gtab[n].cfg = gcfg; gtab[n].cfg.enabled = 1; return TRUE; } static int switchtab(int n) { Histpath *hp = ptab->hp; if (n == gcfg.ct) return GO_NONE; if (gtab[n].cfg.enabled == 0 && !inittab(hp->path, n) && !inittab(home ? home : "/", n)) return GO_STATBAR; hp->stat->cur = cursel; hp->stat->scrl = curscroll; if (gcfg.ct < TABS_MAX) gcfg.lt = gcfg.ct; gcfg.ct = n; chdir(gtab[n].hp->path); return GO_RELOAD; } static int closetab(int n __attribute__((unused))) { int ct = gcfg.ct, lt = -1; for (int i = 0; i < TABS_MAX; ++i) if (i != ct && gtab[i].cfg.enabled == 1) lt = i; if (gcfg.lt != ct && gtab[gcfg.lt].cfg.enabled == 1) lt = gcfg.lt; if (lt == -1) { if (ct == 0) return GO_NONE; else if (!inittab(home ? home : "/", 0)) return GO_STATBAR; gcfg.ct = 0; } else { chdir(gtab[lt].hp->path); gcfg.ct = lt; } if (ct == TABS_MAX) { free(pfindbuf); pfindbuf = pfindend = NULL; } else gcfg.lt = ct; deleteallselstat(gtab[ct].ss); gtab[ct].ss = NULL; gtab[ct].cfg.enabled = 0; return GO_RELOAD; } static int togglemode(int n) { int def = (getuid() == 0) ? 2 : 0; if (gcfg.mode == 4 || (def == 2 && n == 1)) return GO_STATBAR; gcfg.mode = (gcfg.mode == n) ? def : n; return GO_REDRAW; } static int getinput(WINDOW *w) { int c, i, tmp; c = wgetch(w); if (c == ESC) { // alt+key or esc wtimeout(w, 0); for (i = 0; (tmp = wgetch(w)) != ERR; ++i) c = tmp; wtimeout(w, -1); if (i == 1 && c > 31 && c < 127) c = -c; else if (i > 0) // when i=0, keep c=ESC c = 0; } return (c == ERR) ? 0 : c; } static int viewoptions(int n __attribute__((unused))) { int i, c = 0; int h = MIN(20, xlines), w = MIN(50, xcols); Settings *cfg = >ab[gcfg.ct].cfg; WINDOW *dpo = newpad(h, w); werase(dpo); box(dpo, 0, 0); mvwaddstr(dpo, i = 0, 6, " View options "); mvwaddstr(dpo, i += 2, 2, "[.]"); wattron(dpo, cfg->showhidden ? A_REVERSE : 0); waddstr(dpo, "show hidden"); wattroff(dpo, A_REVERSE); waddstr(dpo, " [/]"); wattron(dpo, cfg->dirontop ? A_REVERSE : 0); waddstr(dpo, "dirs on top"); wattroff(dpo, A_REVERSE); mvwaddstr(dpo, i += 2, 2, "Sort by:"); mvwaddstr(dpo, ++i, 2, " (n)"); wattron(dpo, (cfg->sortby == 0) ? A_REVERSE : 0); waddstr(dpo, "name"); wattroff(dpo, A_REVERSE); waddstr(dpo, " (s)"); wattron(dpo, (cfg->sortby == 1) ? A_REVERSE : 0); waddstr(dpo, "size"); wattroff(dpo, A_REVERSE); waddstr(dpo, " (t)"); wattron(dpo, (cfg->sortby == 2) ? A_REVERSE : 0); waddstr(dpo, "time"); wattroff(dpo, A_REVERSE); waddstr(dpo, " (e)"); wattron(dpo, (cfg->sortby == 3) ? A_REVERSE : 0); waddstr(dpo, "extension"); wattroff(dpo, A_REVERSE); mvwaddstr(dpo, i += 2, 2, " [c]"); wattron(dpo, !cfg->caseinsen ? A_REVERSE : 0); waddstr(dpo, "case-sensitive"); wattroff(dpo, A_REVERSE); waddstr(dpo, " [v]"); wattron(dpo, cfg->natural ? A_REVERSE : 0); waddstr(dpo, "natural"); wattroff(dpo, A_REVERSE); waddstr(dpo, " [r]"); wattron(dpo, cfg->reverse ? A_REVERSE : 0); waddstr(dpo, "reverse"); wattroff(dpo, A_REVERSE); mvwaddstr(dpo, i += 2, 2, "Detail info:"); mvwaddstr(dpo, ++i, 2, " [i]"); wattron(dpo, cfg->showtime ? A_REVERSE : 0); waddstr(dpo, "time"); wattroff(dpo, A_REVERSE); waddstr(dpo, " [u]"); wattron(dpo, cfg->showowner ? A_REVERSE : 0); waddstr(dpo, "owner"); wattroff(dpo, A_REVERSE); waddstr(dpo, " [p]"); wattron(dpo, cfg->showperm ? A_REVERSE : 0); waddstr(dpo, "permissions"); wattroff(dpo, A_REVERSE); waddstr(dpo, " [z]"); wattron(dpo, cfg->showsize ? A_REVERSE : 0); waddstr(dpo, "size"); wattroff(dpo, A_REVERSE); mvwaddstr(dpo, i += 2, 2, " (d)default (x)none"); mvwaddstr(dpo, i += 2, 2, "Time type:"); mvwaddstr(dpo, ++i, 2, " (a)"); wattron(dpo, (cfg->timetype == 0) ? A_REVERSE : 0); waddstr(dpo, "access"); wattroff(dpo, A_REVERSE); waddstr(dpo, " (m)"); wattron(dpo, (cfg->timetype == 1) ? A_REVERSE : 0); waddstr(dpo, "modify"); wattroff(dpo, A_REVERSE); waddstr(dpo, " (h)"); wattron(dpo, (cfg->timetype == 2) ? A_REVERSE : 0); waddstr(dpo, "change"); wattroff(dpo, A_REVERSE); mvwaddstr(dpo, i += 2, 2, "Press 'o' or Esc to close"); while (c == 0) { prefresh(dpo, 0, 0, (xlines - h) / 2, (xcols - w) / 2, (xlines - h) / 2 + h, (xcols - w) / 2 + w); c = getinput(dpo); switch (c) { case '.': cfg->showhidden ^= 1; break; case '/': cfg->dirontop ^= 1; break; case 'n': cfg->sortby = 0; break; case 's': cfg->sortby = 1; break; case 't': cfg->sortby = 2; break; case 'e': cfg->sortby = 3; break; case 'c': cfg->caseinsen ^= 1; break; case 'v': cfg->natural ^= 1; break; case 'r': cfg->reverse ^= 1; break; case 'i': cfg->showtime ^= 1; break; case 'u': cfg->showowner ^= 1; break; case 'p': cfg->showperm ^= 1; break; case 'z': cfg->showsize ^= 1; break; case 'd': cfg->showtime = gcfg.showtime; cfg->showowner = gcfg.showowner; cfg->showperm = gcfg.showperm; cfg->showsize = gcfg.showsize; break; case 'x': cfg->showtime = cfg->showowner = cfg->showperm = cfg->showsize = 0; break; case 'a': cfg->timetype = 0; break; case 'm': cfg->timetype = 1; break; case 'h': cfg->timetype = 2; break; case 'o': break; case ESC: break; default: c = 0; } } delwin(dpo); if (c == ESC || strchr("oiupzdx", c)) return GO_REDRAW; return refreshview(strchr(".amh", c) ? 0 : 2); } static int showhelp(int n __attribute__((unused))) { int klines = (int)LENGTH(keys), plines = klines + 8; WINDOW *help = newpad(plines, 80); keypad(help, TRUE); erase(); refresh(); waddstr(help, "sff "VERSION"\n\n" " Builtin functions:\n"); for (int i = 0; i < klines; ++i) wprintw(help, " %s\n", keys[i].cmnt); waddstr(help, "\nNote: All file operations are implemented by extension functions.\n" "To get help for extension functions, press Alt-/ in the main view.\n" "Press 'q' or Esc to leave this page."); for (int c = 0, start = 0; c != ESC && c != 'q'; ) { start = MAX(0, MIN(start, plines - xlines)); prefresh(help, start, 0, 0, 0, xlines - 1, xcols - 1); c = getinput(help); if (c == KEY_UP || c == 'k') --start; else if (c == KEY_DOWN || c == 'j') ++start; else if (c == KEY_PPAGE || c == CTRL('B')) start -= xlines - 1; else if (c == KEY_NPAGE || c == CTRL('F')) start += xlines - 1; } delwin(help); return GO_REDRAW; } static int quitsff(int n __attribute__((unused))) { return GO_QUIT; } /****** Core Functions ******/ static void usage(void) { printf("Usage: sff [OPTIONS] [PATH]\n\n" "Option Meaning\n" " -b Run in browse mode\n" " -c Sort with case sensitivity\n" " -d keys Show details: 't'ime, 'o'wner, 'p'erm, 's'ize, 'n'one\n" " -H Show hidden files\n" " -m Mix dirs and files when sorting\n" " -v Print version and exit\n" " -h Print this help and exit\n"); } static int xstrverscmp (const char *s1, const char *s2, int ci) { const unsigned char *p1 = (const unsigned char *)s1; const unsigned char *p2 = (const unsigned char *)s2; int diff = 0, indig = 0; if (p1 == p2) return 0; for (unsigned int c1, c2; diff == 0 || indig; ++p1, ++p2) { c1 = *p1; c2 = *p2; if (indig) { if (c1 - '0' <= 9) { if (c2 - '0' <= 9) { // c1 and c2 are digit if (diff) continue; } else // c1 is digit and c2 is not return 1; } else { if (c2 - '0' <= 9) // c1 is not digit and c2 is return -1; else { // c1 and c2 are not digit if (diff) return diff; indig = 0; } } } else if (c1 == '\0' || c2 == '\0') { return c1 - c2; } else if (c1 - '1' <= 8 && c2 - '1' <= 8) { // c1 and c2 are 1-9 indig = 1; } else if (ci) { if (c1 <= 'Z' && c1 >= 'A') c1 += 32; if (c2 <= 'Z' && c2 >= 'A') c2 += 32; } diff = c1 - c2; } return diff; } static int xstrcasecmp (const char *s1, const char *s2) { const unsigned char *p1 = (const unsigned char *)s1; const unsigned char *p2 = (const unsigned char *)s2; int diff = 0; if (p1 == p2) return 0; for (unsigned int c1, c2; diff == 0; ++p1, ++p2) { if ((c1 = *p1) == '\0') return -1; if ((c2 = *p2) == '\0') return 1; if (c1 <= 'Z' && c1 >= 'A') c1 += 32; if (c2 <= 'Z' && c2 >= 'A') c2 += 32; diff = c1 - c2; } return diff; } static int entrycmp(const void *va, const void *vb) { const Entry *pa = (Entry *)va, *pb = (Entry *)vb; int fa = pa->flag & E_DIR_DIRLNK, fb = pb->flag & E_DIR_DIRLNK; char *exta, *extb; if (ptab->cfg.dirontop && fa != fb) { // Dirs on top if (fb) return 1; return -1; } switch (ptab->cfg.sortby) { case 1: // Sort by size if (pb->size > pa->size) return 1; if (pb->size < pa->size) return -1; break; case 2: // Sort by time if (pb->sec > pa->sec) return 1; if (pb->sec < pa->sec) return -1; if (pb->nsec > pa->nsec) return 1; if (pb->nsec < pa->nsec) return -1; break; case 3: // Sort by extension exta = fa ? NULL : getextension(pa->name, pa->nlen); extb = fb ? NULL : getextension(pb->name, pb->nlen); if (exta || extb) { if (!extb) return 1; if (!exta) return -1; int res = xstrcasecmp(++exta, ++extb); if (res) return res; } } if (ptab->cfg.natural) return xstrverscmp(pa->name, pb->name, ptab->cfg.caseinsen); if (ptab->cfg.caseinsen) return xstrcasecmp(pa->name, pb->name); return strcoll(pa->name, pb->name); } static int reventrycmp(const void *va, const void *vb) { const Entry *pa = (Entry *)va, *pb = (Entry *)vb; int fa = pa->flag & E_DIR_DIRLNK, fb = pb->flag & E_DIR_DIRLNK; if (gtab[gcfg.ct].cfg.dirontop && fa != fb) { // Dirs on top if (fb) return 1; return -1; } return -entrycmp(va, vb); } static void setpreview(int op) { static int fd = -1; switch (op) { case 0: // open preview if (fd == -1) { fd = open(pvfifo, O_WRONLY|O_NONBLOCK|O_CLOEXEC); if (fd == -1 && seterrnum(__LINE__, errno)) return; } // fallthrough case 1: // send file path if (fd == -1 || ndents == 0) return; int len = makepath(ptab->hp->path, pdents[cursel].name, gpbuf); gpbuf[len - 1] = '\n'; if (write(fd, gpbuf, len) == len) return; seterrnum(__LINE__, errno); // fallthrough case 2: // close preview if (fd != -1) { close(fd); fd = -1; } } } static int writeselection(int fd) { ssize_t len; struct selstat *ss; int selcur = ptab->cfg.selmode == 0 && ndents > 0; if (selcur && !appendselection(&pdents[cursel])) return FALSE; ss = ptab->ss; while (ss && ss->prev) ss = ss->prev; while (ss && errline == 0) { for (char *pos = ss->nbuf, *end; pos < ss->endp && (end = memchr(pos, '\0', PATH_MAX)); pos = end + 1) { len = makepath(ss->path, pos, gpbuf); if (write(fd, gpbuf, len) != len && seterrnum(__LINE__, errno)) break; } ss = ss->next; } if (selcur) clearselection(0); return (errline == 0) ? TRUE : FALSE; } static int readfindresult(int fd) { ssize_t len = 1; unsigned int buflen = 0, reslen = 0; char *tmp; while (len > 0) { if (buflen - reslen < NAME_INCR) { if (!(tmp = irealloc(pfindbuf, &buflen, NAME_INCR, 1))) { len = -1; break; } pfindbuf = tmp; } len = read(fd, pfindbuf + reslen, NAME_INCR); reslen += len; } if (len == -1 && seterrnum(__LINE__, errno)) { free(pfindbuf); pfindbuf = NULL; return FALSE; } pfindend = pfindbuf + reslen; *pfindend = '\0'; return TRUE; } static int handlepipedata(int fd, int op) { if (op == 0) read(fd, &op, 1); switch (op) { case '.': // clear selection return clearselection(0); case '*': // refresh if (read(fd, &op, 1) == 1) clearselection(0); return refreshview(0); case '@': // select specified file if (read(fd, gpbuf, PATH_MAX) == -1 && seterrnum(__LINE__, errno)) return GO_STATBAR; strncpy(ptab->hp->stat->name, xbasename(gpbuf), NAME_MAX); findname = ptab->hp->stat->name; gcfg.newent = 1; clearselection(0); return GO_RELOAD; case '>': // enter specified path if (read(fd, gpbuf, PATH_MAX) == -1 && seterrnum(__LINE__, errno)) return GO_STATBAR; if (gcfg.ct < TABS_MAX && abspath(gpbuf, gmbuf)) return newhistpath(gmbuf); break; case '?': // load search result if (!readfindresult(fd)) return GO_STATBAR; if (!inittab(ptab->hp->path, TABS_MAX)) return GO_STATBAR; switchtab(TABS_MAX); return GO_RELOAD; case '#': // set preview read(fd, &op, 1); if (op == 'p') setpreview(0); else if (op == 'q') setpreview(2); return GO_FASTDRAW; } return GO_REDRAW; } static int callextfunc(int c) { pid_t pid, gpid = 0; int rfd, wfd, len, ctl = GO_STATBAR; struct sigaction oldsigtstp, oldsigwinch; struct sigaction act = {.sa_handler = SIG_IGN}; if (access(cfgpath, F_OK) == -1 && mkdir(cfgpath, 0700) == -1 && seterrnum(__LINE__, errno)) return GO_STATBAR; if (mkfifo(pipepath, 0600) == -1 && seterrnum(__LINE__, errno)) return GO_STATBAR; endwin(); pid = fork(); if (pid > 0) { sigaction(SIGTSTP, &act, &oldsigtstp); sigaction(SIGWINCH, &act, &oldsigwinch); if ((rfd = open(pipepath, O_RDONLY)) != -1) { len = read(rfd, gpbuf, 1); if (isdigit(gpbuf[0])) { len = read(rfd, &gpbuf[1], 8); gpbuf[len + 1] = '\0'; gpid = (pid_t)strtol(gpbuf, NULL, 10); } else if (len == 1) ctl = handlepipedata(rfd, gpbuf[0]); close(rfd); if (gpid > 9 && (wfd = open(pipepath, O_WRONLY)) != -1) { if (!writeselection(wfd)) kill(gpid, SIGTERM); close(wfd); } if (gpid > 9 && (rfd = open(pipepath, O_RDONLY)) != -1) { ctl = handlepipedata(rfd, 0); close(rfd); } } else seterrnum(__LINE__, errno); waitpid(pid, NULL, 0); sigaction(SIGTSTP, &oldsigtstp, NULL); sigaction(SIGWINCH, &oldsigwinch, NULL); } else if (pid == 0) { spawn(extfunc, pipepath, (char [2]){c, '\0'}, FALSE, gcfg.mode == 1); if ((wfd = open(pipepath, O_WRONLY | O_NONBLOCK)) != -1) { write(wfd, "/", 1); close(wfd); } _exit(EXIT_SUCCESS); } else seterrnum(__LINE__, errno); refresh(); unlink(pipepath); return ctl; } static inline void fillentry(int fd, Entry *ent, struct stat sb, time_t curtime) { switch (ptab->cfg.timetype) { case 0: ent->sec = sb.st_atime; ent->nsec = (unsigned int)sb.st_atim.tv_nsec; break; case 1: ent->sec = sb.st_mtime; ent->nsec = (unsigned int)sb.st_mtim.tv_nsec; break; case 2: ent->sec = sb.st_ctime; ent->nsec = (unsigned int)sb.st_ctim.tv_nsec; } ent->size = sb.st_size; ent->mode = sb.st_mode; ent->uid = sb.st_uid; ent->gid = sb.st_gid; ent->flag = 0; switch (ent->mode & S_IFMT) { case S_IFREG: ent->type = F_REG; if (sb.st_nlink > 1) ent->type = F_HLNK; if (sb.st_mode & S_IXUSR) ent->type = F_EXEC; ent->flag |= E_REG_FILE; break; case S_IFDIR: ent->type = F_DIR; ent->flag |= E_DIR_DIRLNK; break; case S_IFLNK: ent->type = F_LNK; fstatat(fd, ent->name, &sb, 0); if (S_ISDIR(sb.st_mode)) ent->flag |= E_DIR_DIRLNK; break; case S_IFCHR: ent->type = F_CHR; break; case S_IFBLK: ent->type = F_BLK; break; case S_IFIFO: ent->type = F_IFO; break; case S_IFSOCK: ent->type = F_SOCK; break; default: ent->type = F_UNKN; } if (gcfg.newent && (curtime - sb.st_ctime <= 180)) ent->flag |= E_NEW; } static void loaddirentry(DIR *dirp, int fd) { char *name, *tmp; size_t off = 0; struct dirent *dp; struct stat sb; time_t curtime = time(NULL); Entry *ent,*tmpent; while ((dp = readdir(dirp))) { name = dp->d_name; if (name[0] == '.' && (name[1] == '\0' || (name[1] == '.' && name[2] == '\0'))) continue; // Skip self and parent if (name[0] == '.' && !ptab->cfg.showhidden) continue; if (fstatat(fd, name, &sb, AT_SYMLINK_NOFOLLOW) == -1) continue; if ((unsigned int)ndents == tdents) { if (!(tmpent = irealloc(pdents, &tdents, ENTRY_INCR, sizeof(Entry)))) return; pdents = tmpent; } if (namebuflen - off <= NAME_MAX) { if (!(tmp = irealloc(pnamebuf, &namebuflen, NAME_INCR, 1))) return; // Reset entry names if realloc() causes memory move if (pnamebuf != tmp) { pnamebuf = tmp; for (int i = 0; i < ndents; tmp += pdents[i].nlen, ++i) pdents[i].name = tmp; } } ent = pdents + ndents; ent->name = pnamebuf + off; tmp = memccpy(ent->name, name, '\0', NAME_MAX + 1); ent->nlen = tmp - ent->name; // include terminational '\0' off += ent->nlen; fillentry(fd, ent, sb, curtime); ++ndents; } } static void loadsrchentry(int fd) { struct stat sb; time_t curtime = time(NULL); Entry *ent, *tmpent; for (char *name = pfindbuf, *end; name < pfindend && (end = memchr(name, '\0', PATH_MAX)); name = end + 1) { if (fstatat(fd, name, &sb, AT_SYMLINK_NOFOLLOW) == -1) continue; if ((unsigned int)ndents == tdents) { if (!(tmpent = irealloc(pdents, &tdents, ENTRY_INCR, sizeof(Entry)))) return; pdents = tmpent; } ent = pdents + ndents; ent->name = name; ent->nlen = end - name + 1; fillentry(fd, ent, sb, curtime); ++ndents; } } static void loadentries(char *path) { int fd; DIR *dirp = opendir(path); ndents = 0; if (!dirp && seterrnum(__LINE__, errno)) return; fd = dirfd(dirp); if (ptab->hp->stat->flag != S_ROOT) loaddirentry(dirp, fd); // Load dir entry else if (pfindbuf) loadsrchentry(fd); // Load search result closedir(dirp); ptab->nde = ndents; } static void setcurrentstat(Histpath *hp, struct selstat *ss) { Histstat *hs = hp->stat; // Find current entry, and set cursel if (findname) { if (hs->cur >= ndents || strcmp(findname, pdents[hs->cur].name) != 0) { for (int i = 0; i < ndents; ++i) { if (strcmp(findname, pdents[i].name) == 0) { hs->cur = i; hs->scrl = MAX(i - (onscr * 3 >> 2), MIN(i - (onscr >> 2), hs->scrl)); break; } } } findname = NULL; } cursel = hs->cur; curscroll = hs->scrl; // Find corresponding selstat, and set selection status ptab->cfg.havesel = 0; markent = -1; if (!ss) return; while (ss->next) ss = ss->next; do { if (strcmp(ss->path, hp->path) == 0) { ptab->cfg.havesel = 1; ptab->ss = ss; break; } } while ((ss = ss->prev)); } static wchar_t *fitnamecols(const char *str, int maxcols) { wchar_t *wbuf = (wchar_t *)gpbuf; int i, len = mbstowcs(wbuf, str, maxcols + 1); // Convert multi-byte to wide char for (i = 0; i < len; ++i) if (wbuf[i] <= L'\x1f') wbuf[i] = L'?'; // Replace escape chars with '?' if (len > maxcols >> 1) { for (i = len; wcswidth(wbuf, i) > maxcols; --i) // Reduce wide chars to fit room ; if (i < len) wbuf[i - 1] = L'~'; wbuf[i] = L'\0'; } return wbuf; } static wchar_t *fitpathcols(const char *path, int maxcols) { static int homelen = 0; wchar_t *tbuf = (wchar_t *)gmbuf, *wbuf = (wchar_t *)gpbuf; wchar_t *tbp = tbuf, *wbp = wbuf, *slash = NULL; int i, len, fold = 0; if (homelen == 0 && home) homelen = strlen(home); // Replace home path with '~' if (home && strncmp(home, path, homelen) == 0) { path += homelen; *wbp++ = L'~'; } len = mbstowcs(tbp, path, PATH_MAX - 1); if (wcswidth(tbuf, len) + (wbp - wbuf) > maxcols) { fold = 1; *wbp++ = *tbp++; // If fold path, keep the first level } for (; *tbp; ++tbp, ++wbp) { if (fold && *tbp == L'/' && slash) slash = wbp = slash + 2; if (fold && *tbp == L'/' && !slash) slash = wbp; *wbp = (*tbp > L'\x1f') ? *tbp : L'?'; // Replace escape chars with '?' } len = wbp - wbuf; for (i = len; wcswidth(wbuf, i) > maxcols; --i) // Reduce wide chars to fit room ; if (i < len) wbuf[i - 1] = L'~'; wbuf[i] = L'\0'; return wbuf; } static char *filetypechar(int type) { switch (type) { case F_DIR: return ""; case F_CHR: return ""; case F_BLK: return ""; case F_IFO: return "

"; case F_LNK: return ""; case F_SOCK: return ""; case F_UNKN: return ""; } return "<->"; } static inline void printenttime(const time_t *timep) { struct tm t; localtime_r(timep, &t); printw("%s-%02d-%02d %02d:%02d ", xitoa(t.tm_year + 1900), t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min); } static inline void printentname(const Entry *ent, int sel) { int attr = COLOR_PAIR(ent->type) | (ent->flag & E_DIR_DIRLNK ? A_BOLD : 0) | ((ent->flag & E_SEL) || (sel && !ptab->cfg.selmode) ? A_REVERSE : 0) | ((sel && ptab->cfg.selmode) ? A_UNDERLINE : 0); attron(attr); if (ptab->hp->stat->flag != S_ROOT) addwstr(fitnamecols(ent->name, ncols)); else addwstr(fitpathcols(ent->name, ncols)); attroff(attr); } static void printent(const Entry *ent, int sel, int mark) { int attr = COLOR_PAIR(C_DETAIL) | (mark || (sel && ptab->cfg.selmode) ? A_REVERSE : 0); if (sel) addch('>' | A_BOLD); else addch(' '); attron(attr); if (ptab->cfg.showtime) printenttime(&ent->sec); if (ptab->cfg.showowner) printw("%7.6s:%-7.6s", getpwname(ent->uid), getgrname(ent->gid)); if (ptab->cfg.showperm) printw(" %c%c%c ", '0' + ((ent->mode >> 6) & 7), '0' + ((ent->mode >> 3) & 7), '0' + (ent->mode & 7)); if (ptab->cfg.showsize) printw("%7s ", (ent->flag & E_REG_FILE) ? tohumansize(ent->size) : filetypechar(ent->type)); attron(gcfg.newent && (ent->flag & E_NEW) ? (COLOR_PAIR(C_NEWFILE) | A_REVERSE) : 0); if (sel) addch('>' | A_BOLD); else addch(' '); attroff(gcfg.newent && (ent->flag & E_NEW) ? (COLOR_PAIR(C_NEWFILE) | A_REVERSE) : 0); attroff(attr); if (ncols > 0) printentname(ent, sel); } static void redraw(char *path) { getmaxyx(stdscr, xlines, xcols); int pcols = xcols - (TABS_MAX + 1) * 2; int dcols = (ptab->cfg.showtime ? 17 : 0) + (ptab->cfg.showowner ? 15 : 0) + (ptab->cfg.showperm ? 5 : 0) + (ptab->cfg.showsize ? 8 : 0) + 2; int btm, j = 0; struct selstat *ss = ptab->ss; erase(); shiftcursor(0, 0); onscr = xlines - 4; ncols = xcols - dcols - 1; // Print tabs tag for (int i = 0; i <= TABS_MAX; ++i) { if (gtab[i].cfg.enabled == 1) addch((i < TABS_MAX ? i + '1' : '#') | (COLOR_PAIR(C_TABTAG) | (gcfg.ct == i ? A_REVERSE : 0) | A_BOLD)); else addch(i < TABS_MAX ? '*' : '#'); addch(' '); } // Print path attron(COLOR_PAIR(C_PATHBAR) | A_UNDERLINE); if (pcols > 0) addwstr(fitpathcols(path, pcols)); attroff(COLOR_PAIR(C_PATHBAR) | A_UNDERLINE); // Print entries move(++j, dcols); if (curscroll > 0 && ncols > 0) addstr("<<"); btm = MIN(onscr + curscroll, ndents); for (int i = curscroll; i < btm; ++i) { if (ptab->cfg.havesel && !(pdents[i].flag & E_SEL_SCANED)) { if (findinbuf(ss->nbuf, ss->endp - ss->nbuf, pdents[i].name, pdents[i].nlen)) pdents[i].flag |= E_SEL; pdents[i].flag |= E_SEL_SCANED; } move(++j, 0); printent(&pdents[i], i == cursel, i == markent); } move(++j, dcols); if (btm < ndents && ncols > 0) addstr(">>"); // Print filter if (ptab->ftlen != 0) { move(xlines - 2, 0); attron(COLOR_PAIR(F_SOCK)); addstr("Filter: "); mbstowcs((wchar_t *)gmbuf, ptab->filt, xcols - 12); addwstr((wchar_t *)gmbuf); attroff(COLOR_PAIR(F_SOCK)); addch(' ' | (ptab->ftlen > 0 ? A_REVERSE : 0)); } // Print quick find if (ptab->fdlen > 0) { move(xlines - 2, 0); attron(COLOR_PAIR(F_CHR)); addstr("Quick find: "); mbstowcs((wchar_t *)gmbuf, ptab->find, xcols - 12); addwstr((wchar_t *)gmbuf); attroff(COLOR_PAIR(F_CHR)); addch(' ' | A_REVERSE); } // Draw scroll indicator j = (ndents > 0) ? ndents : 1; btm = (j <= onscr) ? onscr : MAX(1, ((onscr * onscr << 1) / j + 1) >> 1); // indicator height, round a/b by (a*2/b+1)/2 j = (curscroll == 0 || j <= onscr) ? 2 : 2 + (((curscroll * (onscr - btm) << 1) / (j - onscr) + 1) >> 1); // starting row to drawing attron(COLOR_PAIR(C_DETAIL)); mvaddch(1, xcols -1, '='); for (int i = 0; i < btm; ++i, ++j) mvaddch(j, xcols - 1, ' ' | A_REVERSE); mvaddch(xlines - 2, xcols - 1 , '='); attroff(COLOR_PAIR(C_DETAIL)); xcols = -xcols; } static void fastredraw(void) { if (xcols < 0) { // skip fastredraw if redraw() has already done xcols = -xcols; return; } else if (ndents == 0) return; move(2 + lastsel - curscroll, 0); printent(&pdents[lastsel], FALSE, lastsel == markent); move(2 + cursel - curscroll, 0); printent(&pdents[cursel], TRUE, cursel == markent); } static void statusbar(void) { int n, x; move(xlines - 1, 0); clrtoeol(); if (gcfg.mode == 1 || gcfg.mode == 2) { attron(COLOR_PAIR(C_WARN) | A_REVERSE | A_BOLD); addstr(" S "); attroff(COLOR_PAIR(C_WARN) | A_REVERSE | A_BOLD); addch(' '); } else if (gcfg.mode > 2) { attron(COLOR_PAIR(F_EXEC) | A_REVERSE | A_BOLD); addstr(" B "); attroff(COLOR_PAIR(F_EXEC) | A_REVERSE | A_BOLD); addch(' '); } if (errline != 0) { attron(COLOR_PAIR(C_WARN)); printw("Failed (%s): %s", xitoa(errline), strerror(errnum)); attroff(COLOR_PAIR(C_WARN)); errline = 0; return; } attron(COLOR_PAIR(C_STATBAR)); printw("%d/%d ", ndents > 0 ? cursel + 1 : 0, ndents); attron(A_REVERSE); printw(" %d ", (ndents > 0 && !ptab->cfg.selmode) ? 1 : ptab->nsel); attroff(A_REVERSE); addch(' '); if (ndents > 0) { Entry *ent = &pdents[cursel]; addch(filetypechar(ent->type)[1]); addstr(strperms(ent->mode)); addch(' '); addstr(getpwname(ent->uid)); addch(':'); addstr(getgrname(ent->gid)); printw(" %s ", tohumansize(ent->size)); printenttime(&ent->sec); getyx(stdscr, n, x); n = xcols - x; if (ent->type == F_LNK && n > 1) { if ((x = readlink(ent->name, gpbuf, PATH_MAX)) > 1) { gpbuf[x] = '\0'; addstr("->"); addwstr(fitpathcols(gpbuf, n - 2)); // Show symlink target } } else if ((ent->flag & E_REG_FILE) && n > 1) { char *p = getextension(ent->name, ent->nlen); if (p) addnstr(p , n); // Show file extension } } getyx(stdscr, n, x); if (xcols - x >= 8) mvaddstr(n, xcols - 8, " [?]help"); attroff(COLOR_PAIR(C_STATBAR)); } static void filterentry(void) { Entry tmpent; if (ptab->ftlen == 0 || setfilter(2) == GO_REDRAW) return; for (int i = 0; i < ndents; ++i) { if (!strcasestr(pdents[i].name, ptab->filt) && i != --ndents) { tmpent = pdents[i]; pdents[i] = pdents[ndents]; pdents[ndents] = tmpent; --i; } } } static int filterinput(int c) { wchar_t *wbuf = (wchar_t *)gmbuf; if (ptab->ftlen <= 0) // ftlen=0 no filter, ftlen<0 inactive, ftlen>0 active return GO_NONE; if (c == '/') { // turn off filter setfilter(0); return refreshview(2); } else if (c == '\r' || c == ESC){ // set to inactive ptab->ftlen = (ptab->filt[0] == '\0') ? 0 : -ptab->ftlen; return GO_REDRAW; } else if (c == KEY_BACKSPACE || c == KEY_DC) { if (ptab->ftlen <= 1) return GO_REDRAW; size_t len = mbstowcs(wbuf, ptab->filt, ptab->ftlen); wbuf[len - 1] = L'\0'; ptab->ftlen = wcstombs(ptab->filt, wbuf, ptab->ftlen) + 1; ptab->filt[ptab->ftlen - 1] = '\0'; ndents = ptab->nde; } else if (c > 31 && c < 256) { ptab->filt[ptab->ftlen - 1] = c; ptab->filt[ptab->ftlen == FILT_MAX - 1 ? ptab->ftlen : ++ptab->ftlen - 1] = '\0'; } else return GO_NONE; return refreshview(2); } static int qfindinput(int c) { int cd = FALSE; wchar_t *wbuf = (wchar_t *)gmbuf; if (ptab->fdlen <= 0) // fdlen=0 no quick find, fdlen<0 invisible, fdlen>0 active return GO_NONE; if (c == '\r' || c == ESC) { // turn of or set to invisible ptab->fdlen = (ptab->find[0] == '\0') ? 0 : -ptab->fdlen; return GO_REDRAW; } else if (c == '/' && ptab->find[0] == '\0') { // go to root dir ptab->find[0] = '\0'; ptab->fdlen = 1; newhistpath("/"); return GO_RELOAD; } else if (c == '\t' || c == '/') { // enter dir ptab->find[0] = '\0'; ptab->fdlen = 1; cd = TRUE; } else if (c == KEY_BACKSPACE || c == KEY_DC) { if (ptab->fdlen <= 1) return GO_REDRAW; size_t len = mbstowcs(wbuf, ptab->find, ptab->fdlen); wbuf[len - 1] = L'\0'; ptab->fdlen = wcstombs(ptab->find, wbuf, ptab->fdlen) + 1; ptab->find[ptab->fdlen - 1] = '\0'; } else if (c > 31 && c < 256) { ptab->find[ptab->fdlen - 1] = c; ptab->find[ptab->fdlen == FILT_MAX - 1 ? ptab->fdlen : ++ptab->fdlen - 1] = '\0'; } else return GO_NONE; if (cd) { if (enterdir(0) == GO_NONE) return GO_REDRAW; return GO_RELOAD; } if (ptab->find[0] == '\0') return GO_REDRAW; for (int i = 0; i < ndents; ++i) { if (strncasecmp(pdents[i].name, ptab->find, ptab->fdlen - 1) == 0) { cursel = i; curscroll = MAX(i - (onscr * 3 >> 2), MIN(i - (onscr >> 2), curscroll)); return GO_REDRAW; } } return qfindnext(0); } static void browse(void) { int c, ctl = GO_RELOAD; for (;;) { switch (ctl) { case GO_RELOAD: ptab = >ab[gcfg.ct]; loadentries(ptab->hp->path); // fallthrough case GO_SORT: filterentry(); qsort(pdents, ndents, sizeof(*pdents), ptab->cfg.reverse ? &reventrycmp : &entrycmp); setcurrentstat(ptab->hp, ptab->ss); // fallthrough case GO_REDRAW: redraw(ptab->hp->path); // fallthrough case GO_FASTDRAW: fastredraw(); setpreview(1); // fallthrough case GO_STATBAR: statusbar(); // fallthrough case GO_NONE: c = getinput(stdscr); ctl = GO_NONE; if (c == KEY_RESIZE) { ctl = GO_REDRAW; break; } else if (c == 0) break; if ((ctl = filterinput(c)) != GO_NONE) break; if ((ctl = qfindinput(c)) != GO_NONE) break; for (int i = 0; i < (int)LENGTH(keys); ++i) if ((c == keys[i].keysym1 || c == keys[i].keysym2) && keys[i].func) ctl = keys[i].func(keys[i].arg); if (c < 0 && gcfg.mode < 3) ctl = callextfunc(-c); break; case GO_QUIT: return; } } } static void exitsighandler(int sig __attribute__((unused))) { endwin(); exit(EXIT_SUCCESS); } static int initsff(char *arg0, char *argx) { char *path, *xdgcfg = getenv("XDG_CONFIG_HOME"); struct sigaction act = {.sa_handler = exitsighandler}; // Handle/ignore certain signals sigaction(SIGHUP, &act, NULL); sigaction(SIGTERM, &act, NULL); act.sa_handler = SIG_IGN; sigaction(SIGINT, &act, NULL); sigaction(SIGQUIT, &act, NULL); sigaction(SIGPIPE, &act, NULL); // Reset standard input if (!freopen("/dev/null", "r", stdin) || !freopen("/dev/tty", "r", stdin)) { perror(xitoa(__LINE__)); return FALSE; } // Get environment variables home = getenv("HOME"); if (!home || !home[0] || access(home, R_OK | W_OK | X_OK) == -1) home = NULL; editor = getenv("EDITOR"); if (!editor || !editor[0]) editor = EDITOR; // Set config path: xdgcfg+"/sff" or home+"/.config/sff" if ((xdgcfg && xdgcfg[0] && makepath(xdgcfg, "sff", gpbuf)) || (home && makepath(home, ".config/sff", gpbuf))) cfgpath = gpbuf; // Set extfunc path, and check sff-extfunc file if (cfgpath && makepath(cfgpath, EXTFNNAME, gmbuf) && access(gmbuf, R_OK | X_OK) == 0) // check it in config path extfunc = gmbuf; if (!extfunc && realpath(arg0, gmbuf) && makepath(xdirname(gmbuf), EXTFNNAME, gmbuf) && access(gmbuf, R_OK | X_OK) == 0) // check it in where sff is located extfunc = gmbuf; if (!extfunc && makepath(EXTFNPREFIX, EXTFNNAME, gmbuf) && access(gmbuf, R_OK | X_OK) == 0) // check it in EXTFNPREFIX extfunc = gmbuf; // Allocate memory for cfgpath + extfunc + pipepath and set these paths if (cfgpath && extfunc && (cfgpath = malloc(strlen(gpbuf) * 3 + strlen(gmbuf) + 64))) { extfunc = memccpy(cfgpath, gpbuf, '\0', PATH_MAX); pipepath = memccpy(extfunc, gmbuf, '\0', PATH_MAX); strcat(strcat(gpbuf, "/.sff-pipe."), xitoa(getpid())); pvfifo = memccpy(pipepath, gpbuf, '\0', PATH_MAX); strcpy(pvfifo, gpbuf); strcat(pvfifo, ".pv"); } else { cfgpath = NULL; seterrnum(__LINE__, errno); } // Initialize first tab path = argx ? abspath(argx, gpbuf) : getcwd(gpbuf, PATH_MAX); if (!path || !inittab(path, 0)) { perror(xitoa(__LINE__)); return FALSE; } if (getuid() == 0) gcfg.mode = 2; if (!cfgpath) gcfg.mode = 4; return TRUE; } static void setupcurses(void) { cbreak(); noecho(); nonl(); curs_set(FALSE); keypad(stdscr, TRUE); set_escdelay(50); define_key("\033[1;5A", CTRL_UP); define_key("\033[1;5B", CTRL_DOWN); define_key("\033[1;5C", CTRL_RIGHT); define_key("\033[1;5D", CTRL_LEFT); define_key("\033[1;2A", SHIFT_UP); define_key("\033[1;2B", SHIFT_DOWN); start_color(); use_default_colors(); if (COLORS >= 256) setcolorpair256(); else setcolorpair8(); getmaxyx(stdscr, xlines, xcols); onscr = xlines - 4; } static void cleanup(void) { setpreview(2); for (int i = 0; i <= TABS_MAX; ++i) { free(ghpath[i * 2].hs); free(ghpath[i * 2 + 1].hs); deleteallselstat(gtab[i].ss); } free(cfgpath); free(pdents); free(pnamebuf); free(pfindbuf); } int main(int argc, char *argv[]) { int opt; while ((opt = getopt(argc, argv, "bcd:Hmvh")) != -1) { switch (opt) { case 'b': gcfg.mode = 4; break; case 'c': gcfg.caseinsen = 0; break; case 'd': gcfg.showtime = gcfg.showowner = gcfg.showperm = gcfg.showsize = 0; for (int i = 0; optarg[i]; ++i) { if (optarg[i] == 't') gcfg.showtime = 1; if (optarg[i] == 'o') gcfg.showowner = 1; if (optarg[i] == 'p') gcfg.showperm = 1; if (optarg[i] == 's') gcfg.showsize = 1; } break; case 'H': gcfg.showhidden = 1; break; case 'm': gcfg.dirontop = 0; break; case 'v': printf("sff "VERSION"\n"); return EXIT_SUCCESS; case 'h': usage(); return EXIT_SUCCESS; default: dprintf(STDOUT_FILENO, "Try 'sff -h' for available options.\n"); return EXIT_FAILURE; } } atexit(cleanup); if (!initsff(argv[0], argc == optind ? NULL : argv[optind])) return EXIT_FAILURE; if (!initscr()) return EXIT_FAILURE; setupcurses(); setlocale(LC_ALL, ""); browse(); endwin(); return EXIT_SUCCESS; }