diff options
Diffstat (limited to 'dfm.1')
| -rw-r--r-- | dfm.1 | 568 |
1 files changed, 568 insertions, 0 deletions
@@ -0,0 +1,568 @@ +.\" -*- mode: troff; coding: utf-8 -*- +.Dd March 14, 2026 +.Dt DFM 1 +.Os +.Sh dfm (Dylan\(cqs File Manager) +A powerful, simple and snappy terminal file manager with minimal resource usage. +.Pp +.Pp +Initial Announcement: +.Lk https://dylan.gr/1772192922 +.Pp +.Bl -bullet -compact +.It +Tiny (\f(CRCONFIG_SMALL\fR: \(ti90KiB, \f(CRCONFIG_TINY\fR: \(ti40KiB, \f(CRCONFIG_TINY\fR+ \f(CR-static\fR: \(ti150KiB) +.It +Fast (should only be limited by IO) +.It +No dynamic memory allocation (\(ti1.5MiB static) +.It +Does nothing unless a key is pressed +.It +No dependencies outside of POSIX/libc +.It +Manually implemented TUI +.It +Manually implemented interactive line editor +.It +Efficient low-bandwidth partial rendering +.It +UTF8 support (minus grapheme clusters and other unruly things) +.It +Inline image viewing (sixel, kitty) +.It +Multiple view modes (name, size, permissions, mtime, \[u2026]) +.It +Multiple sort modes (name, extension, size, mtime, reverse, \[u2026]) +.It +Ranger-style bulk rename +.It +Incremental as-you-type search +.It +Bookmarks +.It +Vim-like keybindings +.It +Customizable keybindings +.It +Command system +.It +Multi-entry marking +.It +Basic operations (open, copy, move, remove, link, etc) +.It +Watches filesystem for changes +.It +CD on exit +.It +And more\[u2026] +.El +.Ss Table of Contents +.Bl -bullet -compact +.It +.Lk #dependencies Dependencies +.It +.Lk #building Building +.It +.Lk #configuration Configuration +.Bl -bullet -compact +.It +.Lk #dpp-dylans-preprocessor DPP (Dylan\(cqs Preprocessor) +.It +.Lk #command-line Command-line +.It +.Lk #environment Environment +.It +.Lk #cd-on-exit CD On Exit +.El +.It +.Lk #usage Usage +.Bl -bullet -compact +.It +.Lk #statusline Statusline +.It +.Lk #view-modes View Modes +.It +.Lk #sort-modes Sort Modes +.It +.Lk #prompt Prompt +.It +.Lk #images Images +.It +.Lk #searching Searching +.It +.Lk #marking Marking +.It +.Lk #commands Commands +.It +.Lk #privilege-escalation Privilege Escalation +.It +.Lk #bound-commands Bound Commands +.El +.It +.Lk #design-considerations Design Considerations +.It +.Lk #conclusion Conclusion +.El +.Ss Dependencies +Required: +.Pp +.Bl -bullet -compact +.It +POSIX \f(CRcat\fR, \f(CRcp\fR, \f(CRdate\fR, \f(CRmkdir\fR, \f(CRprintf\fR, \f(CRrm\fR, \f(CRsh\fR +.It +POSIX \f(CRmake\fR +.It +POSIX libc +.It +C99 compiler +.El +.Pp +Optional: +.Pp +.Bl -bullet -compact +.It +\f(CRstrip\fR (for \f(CRCONFIG_SMALL\fR and \f(CRCONFIG_TINY\fR) +.It +\f(CRclang\fR (for \f(CRCONFIG_TINY\fR) +.It +\f(CRchafa\fR (for image view using \f(CRsixel\fR) +.It +\f(CRkitty\fR (for image view \f(CRkitty\fR) +.El +.Ss Building +.Bd -literal -offset indent +$ ./configure --prefix=/usr +$ make +$ make DESTDIR=\(dq\(dq install +.Ed +.Pp +The configure script takes three forms of arguments. +.Pp +.Bl -enum -compact +.It +Long-opts: \f(CR--prefix=/usr\fR, \f(CR--help\fR +.It +Variables: \f(CRCC=/bin/cc\fR, \f(CRCFLAGS=\(dq-O3\(dq\fR, \f(CRLDFLAGS=\(dq \(dq\fR +.It +C macro definitions: \f(CR-DMACRO\fR, \f(CR-DMACRO=VALUE\fR, \f(CR-UMACRO\fR +.El +.Pp +There are three different build configurations. +.Pp +.Bl -enum -compact +.It +Default: \f(CR-O2\fR +.It +\f(CRCONFIG_SMALL\fR: \f(CR-Os\fR + aggressive compiler flags +.It +\f(CRCONFIG_TINY\fR: \f(CR-Oz\fR + \f(CRCONFIG_SMALL\fR + (you must set \f(CRCC=clang\fR) +.El +.Pp +.Bl -bullet -compact +.It +To produce a static binary, pass \f(CR-static\fR via \f(CRCFLAGS\fR. +.It +To enable LTO, pass \f(CR-flto\fR via \f(CRCFLAGS\fR. +.El +.Pp +Everything contained within \f(CR./configure\fR, \f(CRMakefile.in\fR, \f(CRconfig.h.in\fR, +\f(CRconfig_cmd.h.in\fR and \f(CRconfig_key.h.in\fR can be configured on the command-line +via \f(CR./configure\fR. See \f(CR./configure --help\fR and also refer to these files for +more information. +.Pp +Bonus example: +.Bd -literal -offset indent +\&./configure \e + --prefix=/usr \e + -DCONFIG_TINY=1 \e + CC=clang \e + CFLAGS=\(dq$CFLAGS -flto -static\(dq \e + -DDFM_NO_COLOR \e + -DDFM_COL_NAV=\(dqVT_SGR(34,7)\(dq +.Ed +.Pp +NOTE: If you are building for an environment without support for the XTerm +alternate screen, add \f(CR-DDFM_CLEAR_EDIT\fR to your configure flags. +.Ss Configuration +\f(CRdfm\fR is configured at compile-time via its config files. +.Pp +.Bl -bullet -compact +.It +\f(CR./configure\fR: Build system, compilation and installation. +.It +\f(CRconfig.h.in\fR: Default settings, colors, etc. +.It +\f(CRconfig_key.h.in\fR: Keybindings. +.It +\f(CRconfig_cmd.h.in\fR: Commands. +.El +.Pp +Refer to these files for more information. +.Ss DPP (Dylan\(cqs Preprocessor) +The \f(CRconfig*.in\fR files are processed by \f(CRdpp\fR (see \f(CRbin/dpp\fR) so POSIX shell +code can be used within them. Everything defined by \f(CR./configure\fR is also +accessible within these files as variables. +.Pp +See +.Lk https://github.com/dylanaraps/dpp +for more information. +.Ss Command-line +.Bd -literal -offset indent +usage: dfm [options] [path] + +options: +-H | +H toggle hidden files (-H off, +H on) +-p picker mode (print selected path to stdout and exit) +-o <opener> program to use when opening files (default: xdg-open) +-c <name> position cursor over 'name' instead of first entry +-q <query> start in search results (\(dq*query\(dq for substring) +-s <mode> change default sort + n name + N name reverse + e extension + s size + S size reverse + d date + D date reverse +-v <mode> change default view + n name only + s size + p permissions + t time + a all + +--help show this help +--version show version + +path: +directory to open (default: \(dq.\(dq) +.Ed +.Ss Environment +A few things can be set at runtime via environment variables. If unset in the +environment, default values are derived from the \f(CRconfig.h.in\fR file. +.Bd -literal -offset indent +- DFM_COPYER (The clipboard tool to use when copying PWD or file + contents. The tool is fed the data via <stdin>) + +- DFM_BOOKMARK_[0-9] (Directory bookmarks. set DFM_BOOKMARK_[0-9] and then + bind act_cd_bookmark_[0-9] to the keys of your choosing) + +- DFM_OPENER (Opener script to use when opening files. This could be + xdg-open or a custom script (see the script/ directory)) + +- DFM_TRASH (Program to use when trashing files) + +- DFM_TRASH_DIR (Path to trash directory) + +- DFM_IMG_MODE (Image mode to use: 'chafa' (default), 'kitty') + +- DFM_SU (Privilege escalation tool to use: 'sudo' (default)) +.Ed +.Ss CD On Exit +There are two ways to exit \f(CRdfm\fR. +.Bd -literal -offset indent +1) act_quit (default 'q') +2) act_quit_print_pwd (default 'Q') +.Ed +.Pp +Exiting with 2) will make \f(CRdfm\fR output the absolute path to the directory it was +in. This output can be passed to \f(CRcd\fR to change directory automatically on exit. +.Bd -literal -offset indent +$ cd \(dq$(dfm)\(dq +$ var=$(dfm) +$ dfm > file +.Ed +.Ss Usage +\f(CRdfm\fR is a single column file-manager with VIM like keybindings. Its basic usage +is pretty straightforward and anything non-obvious can be divined by looking +at the actions each key is bound to. +.Ss Statusline +The statusline is as follows: +.Bd -literal -offset indent +1 1/1 [RnHE] [1+] \(ti0B /path/to/current/directory/<query> + + 1 - Shows nest level of dfm. Only shown if > 0. + 1/1 - The entry number under the cursor and the total visible entries. + + [RnHE] - Indicators. + + R - Shown when dfm is running as root. + n - Current sort mode: [n]ame, [N]ame reverse, [s]ize, + [S]ize reverse, [d]ate modified, [D]ate modified reverse, + [e]xtension. If the current directory is too large, in place + of sort mode, [T] is shown. + H - Shown when hidden files are enabled. + E - Shown when a command fails. This indicates that the user must + check the alternate buffer (bound to 'z' by default) to see + the error messages left by the command failure. + + [1+] - Number of marked files, hidden when 0. + + \(ti0B - Approximate size of directory (shallow, excludes sub-directories). + + /path/to - The current directory. + /<query> - The search query if the list was filtered. +.Ed +.Ss View Modes +There are five view modes: Normal, Size, Permissions, Date Modified and All. +The view mode can be cycled by pressing \f(CR<Tab>\fR by default. +.Pp +All is the sum of the other view modes and gives an idea of what is shown: +.Bd -literal -offset indent +-rwxr-xr-x 16m 4.0K .git/ +-rwxr-xr-x 2h 4.0K bin/ +-rwxr-xr-x 4d 4.0K script/ +-rwxr-xr-x 32m 4.0K lib/ +-rwxr-xr-x 16h 4.0K platform/ +-rw-r--r-- 16m 0B .config_macro.h +-rw-r--r-- 16m 62B .gitignore +-rw-r--r-- 4d 1.0K LICENSE.md +-rw-r--r-- 16m 1.8K Makefile +-rw-r--r-- 8h 1.8K Makefile.in +-rw-r--r-- 32s 6.6K README.txt +-rw-r--r-- 16m 4.0K config.h +-rw-r--r-- 32m 4.0K config.h.in +-rw-r--r-- 32m 6.5K config_cmd.h +-rw-r--r-- 32m 6.5K config_cmd.h.in +-rw-r--r-- 16m 6.5K config_key.h +-rw-r--r-- 32m 6.5K config_key.h.in +-rwxr-xr-x 16m 3.5K configure* +-rwxr-xr-x 16m 130K dfm* +-rw-r--r-- 32m 72K dfm.c + + 2/20 [nH] \(ti268K /home/dylan/kiss/fork/dfm +.Ed +.Ss Sort Modes +There are seven sort modes: \f(CRname\fR, \f(CRname reverse\fR, \f(CRsize\fR, \f(CRsize reverse\fR, +\f(CRdate modified\fR, \f(CRdate modified reverse\fR, \f(CRextension\fR. The sort mode can be +cycled by pressing \(oq\(ga\(cq (backtick) by default. +.Pp +The \f(CRname\fR sort performs a natural/human sort and puts directories before files. +.Ss Prompt +The area where searches and commands are inputted is a complete interactive line +editor supporting all the usual actions (left/right scroll, insert, +bracketed clipboard paste, backspace, delete, prev/next word, etc). +The default keybindings match what is found in readline and POSIXy shells. +.Pp +As of now there is no \f(CR<Tab>\fR complete or up/down arrow history cycling. +.Pp +NOTE: The prompt is implemented as a gap buffer. There are two buffers, cursor +left and cursor right with the cursor sitting inbetween both buffers. When it +comes time to commit the input it is simply joined together. Make not of this +detail as it is necessary to know it when creating your own bound commands. +.Ss Images +Images can be viewed inside of \f(CRdfm\fR by pressing \f(CRi\fR by default. This will +display the image and wait for a keypress before returning to the directory +listing. Two backends are supported: \f(CRsixel\fR (via \f(CRchafa\fR) and \f(CRkitty\fR. +.Pp +The mode can be set in \f(CRconfig.h.in\fR or at runtime via an environment variable. +.Pp +\fBImage viewer\fR (Image: \fIhttps://dylan.gr/img/neofetch.png\fR) +.Ss Searching +There are two search modes: \f(CRstartswith\fR (default \f(CR/\fR) and \f(CRsubstring\fR +(default \f(CR?\fR). They each perform a case-sensitive and incremental as-you-type +search on the current directory\(cqs entries. +.Pp +Pressing \f(CR<Enter>\fR confirms the search and the results become navigable. If +there is only one match, pressing \f(CR<Enter>\fR will open the entry in a single +press. +.Ss Marking +Files can be marked and unmarked (\f(CR<spacebar>\fR by default). There are also +shortcuts to navigate between marks, select all, clear all and to invert the +selection. +.Pp +The marks can be operated on in three ways. +.Pp +.Bl -enum -compact +.It +Foreach: A command is executed once per mark. +.It +Bulk: A command is executed once and given the list of marks as its argv. +.It +Shell: A shell command is executed (\f(CRsh -euc \(dq<cmd>\(dq <marks argv>\fR) +.El +.Pp +.Bl -bullet -compact +.It +NOTE: All three can also be executed in the background. +.It +NOTE: If nothing is marked, the entry under the cursor is operated on. +.El +.Pp +These operations are defined as \(lqcommands\(rq which can be typed or bound to keys. +To avoid copying data, only the basenames of marks are passed to commands and +the commands are exec\(cqd in the directory containing them. +.Pp +Example: +.Bd -literal -offset indent +cp -f %m %d -> PWD=/path/to/mark_dir cp -f a b c /path/to/pwd +.Ed +.Ss Commands +Commands are simply strings which are minimally transformed into argvs and +executed. Modifiers control how the string will be transformed and executed. +.Bd -literal -offset indent +:echo hello -> echo hello +:echo %f world -> foreach entry: echo <entry> world +:echo %m world -> echo <entry_1> <entry_2> ... world'. +:<waycopy -> foreach entry: (stdin) waycopy +.Ed +.Pp +In addition to these modifiers are the following: +.Bd -literal -offset indent +%p -> Path to PWD. +$WORD -> Expand environment variable. +& -> Run in background (must be last word).. +.Ed +.Pp +NOTE: None of the above transformations pass through or incur the cost of +running within a shell. They are merely pointer arrays passed to \f(CRexec()\fR. +.Pp +NOTE: \f(CR%m\fR and \f(CR%f\fR cannot be combined and only the first occurrence of \f(CR%m\fR or +\f(CR%f\fR is evaluated. Also, \f(CR%m\fR and \f(CR%f\fR must appear on their own. +.Pp +If these are too limiting, prepending a \f(CR!\fR bypasses \f(CRdfm\fR\(cqs internal command mode +and sends it all to the shell. +.Bd -literal -offset indent +:!echo \(dq$@\(dq -> sh -euc 'echo \(dq$@\(dq' <entry_1> <entry_2> ... +:!echo \(dq$1\(dq \(dq$2\(dq -> sh -euc 'echo \(dq$1\(dq \(dq$2\(dq' <entry_1> <entry_2> ... +.Ed +.Ss Privilege Escalation +Commands can be run as root by prepending \f(CRsudo\fR or a similar tool on the +command-line. For more complex situations, pressing \f(CRZ\fR by default will use +\f(CRDFM_SU\fR (default \f(CRsudo\fR) to spawn another \f(CRdfm\fR as \f(CRroot\fR. The statusline will +be a different color, show the nest level and display an \f(CRR\fR indicator to make +the escalation obvious. Pressing \f(CRZ\fR again inside of this escalated mode quits +and returns to the original \f(CRdfm\fR. +.Pp +This can be configured at runtime using the environment variable \f(CRDFM_SU\fR and +at compile time via the \f(CRconfig.h.in\fR file. +.Ss Bound Commands +Commands can be bound to keys. When a command is bound it can either run +straight away or open the interactive prompt with pre-filled information. +Flags can also be set to better integrate the command into \f(CRdfm\fR. +.Pp +Move is defined as follows: +.Bd -literal -offset indent +FM_CMD(cmd_move, +\&.prompt = CUT(\(dq:\(dq), - The prompt. +\&.left = CUT(\(dqecho mv -f %m %d\(dq), - Text left of cursor. +\&.enter = fm_cmd_run, - Callback. +\&.config = CMD_NOT_MARK_DIR | - Forbid running in mark directory. + CMD_MUT | - Command may mutate directory. + CMD_EXEC_MARK | - Skip interactive mode if marks. + CMD_CONFLICT, - Prompt on conflicts. +) +.Ed +.Pp +Chown is defined as follows: +.Bd -literal -offset indent +FM_CMD(cmd_chown, +\&.prompt = CUT(\(dq:\(dq), +\&.left = CUT(\(dqchown\(dq), +\&.right = CUT(\(dq %m\(dq), - Text right of cursor. +\&.enter = fm_cmd_run, +\&.config = CMD_MUT, +) +.Ed +.Pp +This opens the interactive prompt and puts the cursor between \f(CRchown\fR and \f(CR%m\fR +so the user can add additional information. +.Bd -literal -offset indent +:chown | %a +.Ed +.Pp +In addition to \f(CRfm_cmd_run\fR, \f(CRfm_cmd_run_sh\fR can be set to bypass \f(CRdfm\fR\(cqs +internal command mode to run the command in the shell. +.Pp +See the \f(CRconfig_key.h.in\fR and \f(CRconfig_cmd.h.in\fR files for more information. +.Ss Design Considerations +.Bl -bullet +.It +I employed many tricks in order to keep memory usage low whilst still allowing +for fast operations and relatively large directory trees. +.It +When a directory too large for \f(CRdfm\fR is entered the statusline sort indicator +is replaced with \f(CR[T]\fR to signify truncation, sorting is disabled and the +statusline colored red. Truncation occurs when memory in the name storage or +entry list is exhausted, whichever comes first. The limits are reasonable and +unlikely to be reached outside of synthetic directory trees so this isn\(cqt +really a problem. +.It +File operations using coreutils commands work well but aren\(cqt as nice as +having fully integrated internal operations. I was working on it but it ended +up being a massive pain in the ass so I abandoned the idea. It\(cqs not enough to +use the POSIX functions as you will be left fighting \f(CRTOCTOU\fR race conditions, +control flow hell, error handling madness and other crap. A solution is to +conditionally use each OS\(cqs extension functions (ie, Linux\(cqs \f(CRcopy_file()\fR, +\f(CRrenameat2()\fR, \f(CRO_TMPFILE\fR, \f(CRAT_EMPTY_PATH\fR, etc) but then you end up stuck in +preprocessor \f(CR#ifdef\fR soup. +.It +UTF8 support intentionally excludes grapheme clusters, emojis and other +complicated things. Everything else should work just fine though. +.It +\f(CRdfm\fR will do partial rendering wherever possible and also tries to do as +little display IO as it can (this is what I mean by low-bandwidth in the +feature list). +.It +The TUI is manually implemented using VT100 escape sequences and a few +optional modern ones (bracketed paste, XTerm alt screen, synchronized +updates). Look at \f(CRlib/term.h\fR, \f(CRlib/term_key.h\fR, \f(CRlib/vt.h\fR and scan \f(CRdfm.c\fR +for \f(CRVT_.*\fR to see how it works. +.Pp +NOTE: \f(CRdfm\fR works in pretty much every terminal emulator in wide use but since +it intentionally doesn\(cqt use terminfo it may not display correctly in some +environments (notably the TTY console in some BSDs). I don\(cqt think there\(cqs +anything I can do to remedy this unfortunately. +.It +The number of marks is bounded only when it comes to materializing them. For +1000 marks \f(CRdfm\fR needs the space to construct an \f(CRargv\fR to accommodate them. +This is not all, if a \f(CRcd\fR is performed, space is also needed to store the mark +entry names as the new directory will overwrite them. Marks are stored on the +end of the directory storage growing towards its middle. In other words, +materialized marks are stored in the free space not taken up by directory +entries. This creates two scenarios. +.Bl -enum +.It +Inside the same directory as the marks \f(CRdfm\fR can mark and operate on all of +the entries without needing any extra memory as the marks are virtual. +However, if \f(CR%m\fR is used inside the mark directory, \f(CRdfm\fR must materialize +them and the number is bounded by whatever unused memory is available. This +doesn\(cqt limit operation on files as \f(CRdfm\fR will process the marks in chunks. +.Pp +.Bl -bullet -compact +.It +\f(CR%f\fR: 900 marks -> n/a -> cmd x 900 +.It +\f(CR%m\fR: 900 marks -> 300 slots -> cmd x 3 +.El +.It +Outside of the directory \f(CRdfm\fR needs space to materialize the marks so +mark that travel are bounded. +.Pp +In short: +.Pp +.Bl -bullet -compact +.It +in mark dir + \f(CR%f\fR == boundless mark operations. +.It +in mark dir + \f(CR%m\fR == boundless mark operations (chunked). +.It +outside mark dir + \f(CR%f\fR == bounded mark operations. +.It +outside mark dir + \f(CR%m\fR == bounded mark operations. +.El +.El +.El +.Ss Conclusion +I had a lot of fun writing this. +Thank you for reading. +.Pp +.Bl -bullet -compact +.It +Also check out \f(CRdpp\fR: +.Lk https://github.com/dylanaraps/dpp +.It +And my blog: +.Lk https://dylan.gr +.El |