aboutsummaryrefslogtreecommitdiff
path: root/dfm.1
diff options
context:
space:
mode:
authortwells46 <tom@wellsth.com>2026-04-16 20:24:28 -0500
committertwells46 <tom@wellsth.com>2026-04-16 20:24:28 -0500
commitd1d271cb51d5322fa24ec325a3065debf4c4e79b (patch)
treead9970c12bb663b8d59aa199c8806205b444d142 /dfm.1
parent3cc0f63c18e30cd3da3106eedca4dcd463ce5bfe (diff)
My changesHEADmain
Diffstat (limited to 'dfm.1')
-rw-r--r--dfm.1568
1 files changed, 568 insertions, 0 deletions
diff --git a/dfm.1 b/dfm.1
new file mode 100644
index 0000000..cd90a3b
--- /dev/null
+++ b/dfm.1
@@ -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