aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDylan Araps <dylan.araps@gmail.com>2026-02-27 13:41:56 +0200
committerDylan Araps <dylan.araps@gmail.com>2026-02-27 13:41:56 +0200
commitda28548905dab7c60c3fcec4975ccfa23e315909 (patch)
tree9cef34a718563969a6bcda5cfeff033eeaf6b1a0
0.99.0
-rw-r--r--LICENSE19
-rw-r--r--Makefile.in47
-rw-r--r--README.txt463
-rwxr-xr-xbin/dpp76
-rwxr-xr-xbin/u834
-rw-r--r--config.h.in169
-rw-r--r--config_cmd.h.in136
-rw-r--r--config_key.h.in190
-rwxr-xr-xconfigure141
-rw-r--r--dfm.c3502
-rwxr-xr-xexample/opener_ext27
-rwxr-xr-xexample/opener_mime33
-rw-r--r--lib/arg.h77
-rw-r--r--lib/bitset.h152
-rw-r--r--lib/date.h139
-rw-r--r--lib/readline.h529
-rw-r--r--lib/str.h183
-rw-r--r--lib/term.h206
-rw-r--r--lib/term_key.h292
-rw-r--r--lib/utf8.h185
-rw-r--r--lib/util.h345
-rw-r--r--lib/vt.h177
-rw-r--r--platform/linux.h109
-rw-r--r--platform/posix.h61
24 files changed, 7292 insertions, 0 deletions
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..8878cb6
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,19 @@
+Copyright 2026 - Dylan Araps
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
diff --git a/Makefile.in b/Makefile.in
new file mode 100644
index 0000000..eb6f9f2
--- /dev/null
+++ b/Makefile.in
@@ -0,0 +1,47 @@
+#
+# Copyright (c) 2026 Dylan Araps
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+#
+.POSIX:
+
+all: $CFG_NAME
+
+$CFG_NAME: $CFG_NAME.c $(echo $CFG_MAKE_DEP) $CFG_BUILD $CFG_INPUT $CFG_COMMAND
+ DPP_INCLUDE=$CFG_DPP_INCLUDE $CFG_DPP_CMD $@ < $CFG_BUILD > $CFG_BUILD_GEN
+ DPP_INCLUDE=$CFG_DPP_INCLUDE $CFG_DPP_CMD $@ < $CFG_INPUT > $CFG_INPUT_GEN
+ DPP_INCLUDE=$CFG_DPP_INCLUDE $CFG_DPP_CMD $@ < $CFG_COMMAND > $CFG_COMMAND_GEN
+ $CC $cc_flags ${CPPFLAGS:-} ${CFLAGS:-} ${LDFLAGS:-} -o \$@ \$<
+!!
+case ${CONFIG_SMALL:=${CONFIG_TINY:=0}} in 1)
+ echo " $STRIP $strip_flags $CFG_NAME"
+esac
+!!
+
+clean:
+ $RM -f $(while read -r l _; do case $l in [!\#]*)
+ v=${v:-}$l\ ;
+ esac done < "$CFG_IGNORE"; echo "$v")
+
+install: $CFG_NAME
+ $MKDIR -p "$prefix/bin"
+ $CP -f "\${DESTDIR}$prefix/bin/$CFG_NAME"
+
+.PHONY: all clean install
+
diff --git a/README.txt b/README.txt
new file mode 100644
index 0000000..2657d49
--- /dev/null
+++ b/README.txt
@@ -0,0 +1,463 @@
+2026/02 0.99.0
+
+
+
+ oooooooooo. oooooooooooo ooo ooooo
+ `888' `Y8b `888' `8 `88. .888'
+ 888 888 888 888b d'888
+ 888 888 888oooo8 8 Y88. .P 888
+ 888 888 888 " 8 `888' 888
+ 888 d88' 888 8 Y 888
+ o888bood8P' o888o o8o o888o
+
+ Dylan's File Manager
+
+
+
+* Tiny (CONFIG_SMALL: ~90KiB, CONFIG_TINY: ~40KiB, CONFIG_STATIC: ~150KiB)
+* Fast (should only be limited by IO)
+* No dynamic memory allocation (~1.5MiB static)
+* Does nothing unless a key is pressed
+* No dependencies outside of POSIX/libc
+* Manually implemented TUI
+* Manually implemented interactive line editor
+* Efficient low-bandwidth partial rendering
+* UTF8 support (minus grapheme clusters and other unruly things)
+* Multiple view modes (name, size, permissions, mtime, ...)
+* Multiple sort modes (name, extension, size, mtime, reverse, ...)
+* No temporary file usage
+* Incremental as-you-type search
+* Bookmarks
+* Vim-like keybindings
+* Customizable keybindings
+* Command system
+* Multi-entry marking
+* Basic operations (open, copy, move, remove, link, etc)
+* Watches filesystem for changes
+* CD on exit
+* And more...
+
+
+INTRODUCTION
+________________________________________________________________________________
+
+Inbetween the annual pruning of olive trees I had some free time this month to
+write this little file manager. This is the third file manager I have written
+after fff and shfm.
+
+For my needs this is essentially complete and outside of bug fixes or cool ideas
+proposed by people its development is finished.
+
+I use this everyday and find it immensely useful and I hope you do too useful.
+
+ Dylan
+
+P.S. I haven't tested it outside of x86_64 KISS Linux so I'm sure there will be
+portability issues/oversights... and... I rushed a few features in before
+release. Let me know if anything is broken. I numbered the first release as
+0.99.0 to reflect this. Once things settle down I'll do a 1.0.0.
+
+
+DEPENDENCIES
+________________________________________________________________________________
+
+Required:
+
+- POSIX shell
+- POSIX cat, cp, date, mkdir, printf, rm
+- POSIX make
+- POSIX libc
+- C99 compiler
+
+Optional:
+
+- strip (for CONFIG_SMALL and CONFIG_TINY)
+- clang (for CONFIG_TINY)
+
+
+BUILDING
+________________________________________________________________________________
+
+$ ./configure --prefix=/usr
+$ make
+$ make DESTDIR="" install
+
+The configure script takes three forms of arguments.
+
+1) Long-opts: --prefix=/usr --help
+2) Variables: CC=/bin/cc CFLAGS="-O3" CONFIG_TINY=1 LDFLAGS=" "
+3) C macro definitions: -DMACRO -DMACRO=VALUE -UMACRO
+
+There are three different user-centric build configurations.
+
+1) Default: -O2
+2) CONFIG_SMALL: -Os + aggressive compiler flags
+3) CONFIG_TINY: -Oz + CONFIG_SMALL + (you must set CC to clang)
+
+To produce a static binary, pass -static via CFLAGS.
+To enable LTO, pass -flto via CFLAGS.
+
+Everything contained within ./configure, Makefile.in, config.h.in,
+config_cmd.h.in and config_key.h.in can be configured on the command-line via
+./configure. See './configure --help' and also refer to these files for more
+information.
+
+Bonus example:
+
+ ./configure \
+ --prefix=/usr \
+ CONFIG_TINY=1 \
+ CC=clang \
+ CFLAGS="$CFLAGS -flto -static" \
+ -DDFM_NO_COLOR \
+ -DDFM_COL_NAV="VT_SGR(34,7)"
+
+
+CONFIGURATION
+________________________________________________________________________________
+
+DFM is mostly configured at compile-time via its config files.
+
+* ./configure: Build system, compilation and installation.
+* config.h.in: Default settings, colors, etc.
+* config_key.h.in: Keybindings.
+* config_cmd.h.in: Commands.
+
+Refer to these files for more information.
+
+
+--[DPP]-------------------------------------------------------------------------
+
+The config .in files are processed by https://github.com/dylanaraps/dpp
+(see bin/dpp) so POSIX shell code can be used within them. Everything defined
+by ./configure is accessible within these files.
+
+See https://github.com/dylanaraps/dpp for more information.
+
+
+--[Command-line]----------------------------------------------------------------
+
+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)
+-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: ".")
+
+
+--[Environment]-----------------------------------------------------------------
+
+A few things can be set at runtime via environment variables. If unset in the
+environment, default values are derived from the config.h.in file.
+
+- 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 examples/ directory))
+
+
+--[CD On Exit]------------------------------------------------------------------
+
+There are two ways to exit DFM.
+
+1) act_quit (default 'q')
+2) act_quit_print_pwd (default 'Q')
+
+Exiting with 2) will make DFM output the absolute path to the directory it was
+in. This output can be passed to 'cd' to change directory automatically on exit.
+
+$ cd "$(dfm)"
+$ var=$(dfm)
+$ dfm > file
+
+
+USAGE
+________________________________________________________________________________
+
+DFM 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.
+
+
+--[Statusline]------------------------------------------------------------------
+
+The statusline is as follows:
+
+ 1/1 [RnHE] [1 marked] ~0B /path/to/current/directory/<query>
+
+ 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 marked] - Number of marked files, hidden when 0.
+
+ ~0B - Approximate size of directory (shallow, excludes sub-directories).
+
+ /path/to - The current directory.
+ /<query> - The search query if the list was filtered.
+
+
+--[View Modes]------------------------------------------------------------------
+
+There are five view modes: Normal, Size, Permissions, Date Modified and All.
+The view mode can be cycled by pressing <Tab> by default.
+
+All is the sum of the other view modes and gives an idea of what is shown:
+
+-rwxr-xr-x 16m 4.0K .git/
+-rwxr-xr-x 2h 4.0K bin/
+-rwxr-xr-x 4d 4.0K example/
+-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] ~268K /home/dylan/kiss/fork/dfm
+
+
+--[Sort Modes]------------------------------------------------------------------
+
+There are seven sort modes: Name, Name reverse, Size, Size reverse,
+Date modified, Date modified reverse, Extension. The sort mode can be cycled by
+pressing '`' (backtick) by default.
+
+The "Name" sort performs a natural/human sort and puts directories before files.
+
+
+--[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.
+
+As of now there is no <Tab> complete or up/down arrow history cycling.
+
+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.
+
+
+--[Searching]-------------------------------------------------------------------
+
+There are two search modes: Startswith and Substring. Startswith is bound to '/'
+by default and Substring to '?'. They each perform a case-sensitive and
+incremental as-you-type search on the current directory's entries.
+
+Pressing <Enter> confirms the search and the results become navigable. If there
+is only one match, pressing <Enter> will open the entry in a single press.
+
+
+--[Marking]---------------------------------------------------------------------
+
+Files can be marked and unmarked (spacebar by default). There are also shortcuts
+to navigate between marks, select all, clear all and to invert the selection.
+
+The marks can be operated on in three ways.
+
+1) Foreach: A command is executed once per mark.
+2) Bulk: A command is executed once and given the list of marks as its argv.
+3) Shell: A shell command is executed (sh -euc "<cmd>" <marks argv>)
+
+NOTE: All three can also be executed in the background.
+NOTE: If nothing is marked, the entry under the cursor is operated on.
+
+These operations are defined as "commands" 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'd in the directory containing them.
+
+Example:
+
+ 'cp -f %m %d' -> PWD=/path/to/mark_dir cp -f a b c /path/to/pwd
+
+
+--[Commands]--------------------------------------------------------------------
+
+Commands are simply strings which are minimally transformed into argvs and
+executed. Modifiers control how the string will be transformed and executed.
+
+: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
+
+In addition to these modifiers are the following:
+
+%p -> Path to PWD.
+$WORD -> Expand environment variable.
+& -> Run in background (must be last word)..
+
+NOTE: None of the above transformations pass through or incur the cost of
+running within a shell. They are merely pointer arrays passed to exec().
+
+NOTE: %m and %f cannot be combined and only the first occurrence of %m or %f is
+evaluated. Also, %m and %f must appear on their own.
+
+If these are too limiting, prepending a '!' bypasses DFM's internal command mode
+and sends it all to the shell.
+
+:!echo "$@" -> sh -euc 'echo "$@"' <entry_1> <entry_2> ...
+:!echo "$1" "$2" -> sh -euc 'echo "$1" "$2"' <entry_1> <entry_2> ...
+
+
+--[Bound Commands]--------------------------------------------------------------
+
+Commands can be bound to keys. When a command is bound it can either run
+straightaway or open the interactive prompt with pre-filled information.
+Flas can also be set to better integrate the command into DFM.
+
+Move is defined as follows:
+
+ FM_CMD(cmd_move,
+ .prompt = CUT(":"), - The prompt.
+ .left = CUT("echo mv -f %m %d"), - 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.
+ )
+
+Chown is defined as follows:
+
+ FM_CMD(cmd_chown,
+ .prompt = CUT(":"),
+ .left = CUT("chown"),
+ .right = CUT(" %m"), - Text right of cursor.
+ .enter = fm_cmd_run,
+ .config = CMD_MUT,
+ )
+
+This opens the interactive prompt and puts the cursor between chown and %m so
+the user can add additional information.
+
+ :chown | %a
+
+In addition to fm_cmd_run, fm_cmd_run_sh can be set to bypass DFM's internal
+command mode to run the command in the shell.
+
+See the config_key.h.in and config_cmd.h.in files for more information.
+
+
+DESIGN CONSIDERATIONS
+________________________________________________________________________________
+
+* I wanted DFM to do absolutely nothing when idle so SIGWINCH (resize handling)
+ will not automatically perform a size adjustment and redraw until the next
+ keypress.
+
+* I employed many tricks in order to keep memory usage low whilst still allowing
+ for fast operations and relatively large directory trees.
+
+* When a directory too large for DFM is entered the statusline sort indicator is
+ replaced with [T] to signify truncation and the statusline colored red.
+ Truncation occurs when 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't really a problem.
+
+* File operations using coreutils commands work but aren't 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's not enough to
+ use the POSIX functions as you will be left fighting TOCTOU race conditions,
+ control flow hell, error handling madness and other crap. A solution is to
+ conditionally use each OS's extension functions (ie, Linux's copy_file,
+ renameat2, O_TMPFILE, AT_EMPTY_PATH, etc) but then you end up stuck in
+ preprocessor ifdef soup.
+
+* UTF8 support intentionally excludes grapheme clusters, emojis and other
+ complicated things. Everything else should work just fine though.
+
+* DFM 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).
+
+* The TUI is manually implemented using VT100 escape sequences and a few
+ optional modern ones (bracketed paste, XTerm alt screen,
+ synchronized updates). Look at lib/term.h, lib/term_key.h, lib/vt.h and scan
+ dfm.c for VT_.* to see how it works.
+
+* The number of marks is bounded only when it comes to materializing them. For
+ 1000 marks dfm needs the space to construct an argv to accommodate them. This
+ is not all, if a 'cd' 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.
+
+ 1) Inside the same directory as the marks dfm can mark and operate on all of
+ the entries without needing any extra memory as the marks are virtual.
+ However, if %m is used inside the mark directory, dfm must materialize them
+ and the number is bounded by whatever unused memory is available. This
+ doesnt limit operation on files as dfm will process the marks in chunks.
+
+ %f: 900 marks -> n/a -> cmd <arg> x 900
+ %m: 900 marks -> 300 slots -> cmd <args> x 3
+
+ 2) Outside of the directory dfm needs space to materialize the marks so marks
+ that travel are bounded.
+
+ In short:
+
+ - in mark dir + %f == boundless mark operations.
+ - in mark dir + %m == boundless mark operations (chunked).
+ - outside mark dir + %f == bounded mark operations.
+ - outside mark dir + %m == bounded mark operations.
+
+
+CONCLUSION
+________________________________________________________________________________
+
+I had a lot of fun writing this.
+Thank you for reading.
+
+Also check out dpp: https://github.com/dylanaraps/dpp
+And my blog: https://dylan.gr
+
diff --git a/bin/dpp b/bin/dpp
new file mode 100755
index 0000000..f25c092
--- /dev/null
+++ b/bin/dpp
@@ -0,0 +1,76 @@
+#!/bin/sh -euf
+#
+# DPP - Bonus shell implementation.
+#
+# Copyright (c) 2026 Dylan Araps
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+#
+
+: "${DPP_EOF_MARKER:=___DPPEOFMARKER___}"
+: "${DPP_SHELL:=/bin/sh}"
+
+{
+ echo "#!$DPP_SHELL"
+ echo "# This file has been generated by dylanaraps/dpp."
+ echo "(set -euo pipefail 2>/dev/null) && set -eo pipefail || set -eu"
+ echo "export DPP_VERSION=1.0.1"
+ echo "export DPP_LEVEL=\$((DPP_LEVEL + 1))"
+ echo "set -- $*"
+ echo "${DPP_INCLUDE:+. \""$DPP_INCLUDE"\"}"
+
+ while IFS= read -r l; do case $l in
+ "${DPP_BLOCK:=!!}"*)
+ case ${b:-0} in 2)
+ echo "$DPP_EOF_MARKER"
+ b=0
+ esac
+
+ l=${l#"$DPP_BLOCK"}
+
+ case $l in
+ '') b=$((!b)) ;;
+ " "*) b=0 l=${l#?} ;;
+ *) b=0 ;;
+ esac
+
+ echo "$l"
+ ;;
+
+ *)
+ case $l in *\\) l=$l\\; esac
+
+ case ${b:-0} in 0)
+ echo "\${DPP_CAT:=cat} << $DPP_EOF_MARKER"
+ b=2
+ esac
+
+ echo "$l"
+ esac done
+
+ case $b in
+ 1) echo "error: DPP_BLOCK not closed" >&2; exit 1 ;;
+ 2) echo "$DPP_EOF_MARKER" ;;
+ esac
+
+} | case ${0##*/} in
+ dpp-compile) cat ;;
+ *) "$DPP_SHELL" ;;
+esac
+
diff --git a/bin/u8 b/bin/u8
new file mode 100755
index 0000000..c022900
--- /dev/null
+++ b/bin/u8
@@ -0,0 +1,34 @@
+#!/bin/sh -euf
+#
+# Convert input to 32bit hex encoded utf8.
+#
+# Copyright (c) 2026 Dylan Araps
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+#
+
+LC_ALL=C
+a=$1 b=${a#?} c=${b#?} d=${c#?}
+a=${a%"$b"} b=${b%"$c"} c=${c%"$d"} d=${d%"${1#????}"}
+export $(printf 'a=%d b=%d c=%d d=%d' "'$a" "'$b" "'$c" "'$d")
+printf '0x%08X\n' "$((a < 128 ? a : a < 224 ?
+(a & 0x1F) << 6 | (b & 0x3F) : a < 240 ?
+(a & 0x0F) << 12 | (b & 0x3F) << 6 | (c & 0x3F) :
+(a & 0x07) << 18 | (b & 0x3F) << 12 | (c & 0x3F) << 6 | (d & 0x3f)))"
+
diff --git a/config.h.in b/config.h.in
new file mode 100644
index 0000000..c330b0f
--- /dev/null
+++ b/config.h.in
@@ -0,0 +1,169 @@
+//
+// DFM - Dylan's File Manager - Configuration file.
+//
+#ifndef DFM_CONFIG_H
+#define DFM_CONFIG_H
+
+//
+// Name of the program.
+//
+#define CFG_NAME "$CFG_NAME"
+
+//
+// Default DFM_OPENER to use when unset in environment.
+//
+#define DFM_OPENER "xdg-open"
+
+//
+// Default DFM_COPYER to use when unset in environment.
+//
+#define DFM_COPYER "waycopy"
+
+//
+// Default DFM_BOOKMARK_[0-9] values when unset in environment.
+//
+#define DFM_BOOKMARK_0 ""
+#define DFM_BOOKMARK_1 ""
+#define DFM_BOOKMARK_2 ""
+#define DFM_BOOKMARK_3 ""
+#define DFM_BOOKMARK_4 ""
+#define DFM_BOOKMARK_5 ""
+#define DFM_BOOKMARK_6 ""
+#define DFM_BOOKMARK_7 ""
+#define DFM_BOOKMARK_8 ""
+#define DFM_BOOKMARK_9 ""
+
+//
+// Default sort mode.
+// See: fm_sort_fn()
+//
+#define DFM_DEFAULT_SORT 'n'
+
+//
+// Default view mode.
+// See: fm_draw_ent()
+//
+#define DFM_DEFAULT_VIEW 'n'
+
+//
+// Show hidden files by default.
+//
+#define DFM_SHOW_HIDDEN 0
+
+//
+// Disable colors.
+// Uncomment to disable all colors.
+//
+// #define DFM_NO_COLOR
+
+//
+// Default colors.
+// Value becomes: '\033[A;B;Cm'.
+//
+#ifndef DFM_NO_COLOR
+#define DFM_COL_CURSOR VT_SGR(7,1)
+#define DFM_COL_DIR VT_SGR(32,1)
+#define DFM_COL_FIFO VT_SGR(33)
+#define DFM_COL_LNK VT_SGR(36)
+#define DFM_COL_LNK_BRK VT_SGR(31,7)
+#define DFM_COL_LNK_DIR VT_SGR(34,1)
+#define DFM_COL_MARK VT_SGR(31)
+#define DFM_COL_REG VT_SGR(37)
+#define DFM_COL_REG_EXEC VT_SGR(36)
+#define DFM_COL_SOCK VT_SGR(35)
+#define DFM_COL_SPEC VT_SGR(33,1)
+#define DFM_COL_UNKNOWN VT_SGR(31,7)
+
+#define DFM_COL_NAV VT_SGR(7)
+#define DFM_COL_NAV_ERR VT_SGR(7,31)
+#define DFM_COL_NAV_MSG VT_SGR(7,32)
+#define DFM_COL_NAV_CMD VT_SGR(7,36)
+#define DFM_COL_NAV_ROOT VT_SGR(7,33)
+#define DFM_COL_NAV_CURSOR VT_SGR(7,0)
+#define DFM_COL_NAV_MARK VT_SGR(7,35,1)
+
+#else
+#define DFM_COL_CURSOR VT_SGR(7,1)
+#define DFM_COL_DIR ""
+#define DFM_COL_FIFO ""
+#define DFM_COL_LNK ""
+#define DFM_COL_LNK_BRK VT_SGR(7)
+#define DFM_COL_LNK_DIR VT_SGR(1)
+#define DFM_COL_MARK ""
+#define DFM_COL_REG ""
+#define DFM_COL_REG_EXEC ""
+#define DFM_COL_SOCK ""
+#define DFM_COL_SPEC VT_SGR(1)
+#define DFM_COL_UNKNOWN ""
+
+#define DFM_COL_NAV VT_SGR(7)
+#define DFM_COL_NAV_ERR VT_SGR(7)
+#define DFM_COL_NAV_MSG VT_SGR(7)
+#define DFM_COL_NAV_CMD VT_SGR(7)
+#define DFM_COL_NAV_ROOT VT_SGR(7)
+#define DFM_COL_NAV_CURSOR VT_SGR(7)
+#define DFM_COL_NAV_MARK VT_SGR(7,1)
+#endif
+
+//
+// Maximum buffer sizes.
+//
+#define DFM_IO_MAX (1 << 13)
+#define DFM_NAME_MAX (1 << 8)
+#define DFM_PATH_MAX (1 << 12)
+#define DFM_ENT_MAX (1 << 20)
+#define DFM_DIR_MAX (1 << 15)
+
+//
+// Size of hash table for directory entries (load factor ~0.66).
+// NOTE: Must be a power of 2.
+//
+#define DFM_DIR_HT_CAP (DFM_DIR_MAX << 1)
+
+//
+// Line editing buffer size.
+//
+#define RL_MAX (1 << 13)
+
+//
+// Reserve room before and after marks for command and extra arguments.
+// Marks are stored as an array of (char *) with the following layout:
+//
+// [DFM_MARK_CMD_PRE...][MARKS...][DFM_MARK_CMD_POST...][NULL]
+//
+// In order to run a command on marks dfm will simply write the caller's argv
+// into the free slots in DFM_MARK_CMD_PRE. Similarly, there is space after the
+// marks (DFM_MARK_CMD_PRE) where the caller can put arguments. Right now this
+// POST location is only used to append a path to some commands.
+//
+// After writing the caller's argv, the marks array is simply passed as-is to
+// exec. In other words, to call `rm -rf`, only two ptrs are copied
+//
+#define DFM_MARK_CMD_PRE 32
+#define DFM_MARK_CMD_POST 16
+
+//
+// What shell options to enable when spawning a shell via '!' or 'act_cmd_sh'.
+//
+#define DFM_SHELL_OPTS "-euc"
+
+//
+// Make information about the build available.
+//
+#define CC_DATE "${CFG_DATE:-"$(date '+%Y-%m-%d %H:%M')"}"
+#define CC_BRANCH "$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo n/a)"
+#define CC_COMMIT "$(git rev-parse HEAD 2>/dev/null || echo n/a)"
+#define CFG_VERSION "$CFG_VERSION"
+
+//
+// Space to leave for statusline.
+//
+#define DFM_MARGIN 3
+
+//
+// Configuration overrides from ./configure.
+//
+#include "$CFG_MACRO_GEN"
+
+#endif // DFM_CONFIG_H
+
diff --git a/config_cmd.h.in b/config_cmd.h.in
new file mode 100644
index 0000000..bd12f0e
--- /dev/null
+++ b/config_cmd.h.in
@@ -0,0 +1,136 @@
+//
+// DFM - Dylan's File Manager - Configuration file.
+//
+// Commands can be created using the FM_CMD macro which declares a function and
+// fills in a struct fm_cmd.
+//
+// struct fm_cmd {
+// cut prompt; // Prompt text.
+// cut left; // Text left of cursor.
+// cut right; // Text right of cursor.
+// fm_key_press press; // Callback on press.
+// fm_key_enter enter; // Callback on enter.
+// u32 config; // Configuration.
+// };
+//
+// The config field supports the following options:
+//
+// CMD_BG - Run the command in the background.
+// CMD_CONFLICT - Prompt on file conflicts.
+// CMD_MUT - Hint that the command might mutate directory entries.
+// CMD_EXEC - Skip the interactive prompt and execute the command.
+// CMD_MARK_DIR - Command must run in the mark directory.
+// CMD_NOT_MARK_DIR - Command must not run in the mark directory.
+// CMD_STDIN = Feed file under cursor to command via stdin.
+// CMD_FILE_CURSOR = Ignore marks and add the name under the cursor to input.
+// CMD_EXEC_MARK = Skip interactive prompt only if marks exist..
+// CMD_EXEC_ROOT = Skip interactive prompt even if root.
+//
+
+FM_CMD(cmd_exec,
+ .prompt = CUT(":"),
+ .enter = fm_cmd_run,
+ .config = CMD_MUT,
+)
+
+FM_CMD(cmd_exec_sh,
+ .prompt = CUT(":!"),
+ .enter = fm_cmd_run_sh,
+ .config = CMD_MUT | CMD_STDIN,
+)
+
+FM_CMD(cmd_exec_stdin,
+ .prompt = CUT(":<"),
+ .enter = fm_cmd_run,
+ .config = CMD_MUT | CMD_STDIN,
+)
+
+FM_CMD(cmd_exec_open,
+ .prompt = CUT(":"),
+ .right = CUT(" %m"),
+ .enter = fm_cmd_run,
+)
+
+FM_CMD(cmd_exec_open_bg,
+ .prompt = CUT(":"),
+ .right = CUT(" %m &"),
+ .enter = fm_cmd_run
+)
+
+FM_CMD(cmd_link,
+ .prompt = CUT(":"),
+ .left = CUT("ln -sf %m %d"),
+ .enter = fm_cmd_run,
+ .config = CMD_NOT_MARK_DIR | CMD_MUT | CMD_EXEC_MARK | CMD_CONFLICT,
+)
+
+FM_CMD(cmd_remove,
+ .prompt = CUT(":"),
+ .left = CUT("rm -rf %m"),
+ .enter = fm_cmd_run,
+ .config = CMD_MARK_DIR | CMD_MUT | CMD_EXEC_MARK,
+)
+
+FM_CMD(cmd_copy,
+ .prompt = CUT(":"),
+ .left = CUT("cp -Rf %m %d"),
+ .enter = fm_cmd_run,
+ .config = CMD_NOT_MARK_DIR | CMD_MUT | CMD_EXEC_MARK | CMD_CONFLICT,
+)
+
+FM_CMD(cmd_move,
+ .prompt = CUT(":"),
+ .left = CUT("echo mv -f %m %d"),
+ .enter = fm_cmd_run,
+ .config = CMD_NOT_MARK_DIR | CMD_MUT | CMD_EXEC_MARK | CMD_CONFLICT,
+)
+
+FM_CMD(cmd_rename,
+ .prompt = CUT(":"),
+ .left = CUT("mv -f %f "),
+ .enter = fm_cmd_run,
+ .config = CMD_FILE_CURSOR | CMD_MUT,
+)
+
+FM_CMD(cmd_chmod,
+ .prompt = CUT(":"),
+ .left = CUT("chmod"),
+ .right = CUT(" %m"),
+ .enter = fm_cmd_run,
+ .config = CMD_MUT,
+)
+
+FM_CMD(cmd_chown,
+ .prompt = CUT(":"),
+ .left = CUT("chown"),
+ .right = CUT(" %m"),
+ .enter = fm_cmd_run,
+ .config = CMD_MUT,
+)
+
+FM_CMD(cmd_copy_clipboard,
+ .prompt = CUT(":cd "),
+ .left = get_env("DFM_COPYER", DFM_COPYER),
+ .enter = fm_cmd_run,
+ .config = CMD_EXEC | CMD_STDIN,
+)
+
+FM_CMD(cmd_cd,
+ .prompt = CUT(":cd "),
+ .enter = fm_cmd_cd,
+)
+
+FM_CMD(cmd_touch,
+ .prompt = CUT(":"),
+ .left = CUT("touch "),
+ .enter = fm_cmd_run,
+ .config = CMD_MUT,
+)
+
+FM_CMD(cmd_mkdir,
+ .prompt = CUT(":"),
+ .left = CUT("mkdir -p "),
+ .enter = fm_cmd_run,
+ .config = CMD_MUT,
+)
+
diff --git a/config_key.h.in b/config_key.h.in
new file mode 100644
index 0000000..392dfac
--- /dev/null
+++ b/config_key.h.in
@@ -0,0 +1,190 @@
+//
+// DFM - Dylan's File Manager - Keybinding Configuration file.
+//
+// There are three key types.
+//
+// 1) Simple ASCII: 'a', 'B', etc.
+// 2) UTF8: u8("ρ") u8("Σ")
+// 3) Symbolic: KEY_LEFT, KEY_DOWN
+//
+// Key types can be joined to modifiers using the K() macro.
+//
+// K(MOD_CTRL, 'a')
+// K(MOD_SHIFT, u8("φ"))
+// K(MOD_ALT, KEY_TAB)
+//
+// Everything above expands at compile-time to unique 32bit integers.
+// For the full list of symbolic keys, look at lib/term_key.h
+//
+
+//
+// There are two function types that can be bound.
+//
+// 1) act_.* - Internal file manager actions (dfm.c)
+// 2) cmd_.* - External configurable commands (config_cmd.h)
+//
+// See the config_cmd.h file for more information.
+//
+static inline void (*fm_key(u32 cp))(struct fm *)
+{
+ switch (cp) {
+ case 'h': return act_cd_up;
+ case KEY_LEFT: return act_cd_up;
+ case KEY_BACKSPACE: return act_cd_up;
+
+ case 'j': return act_scroll_down;
+ case KEY_DOWN: return act_scroll_down;
+
+ case 'k': return act_scroll_up;
+ case KEY_UP: return act_scroll_up;
+
+ case 'l': return act_open;
+ case KEY_RIGHT: return act_open;
+ case KEY_ENTER: return act_open;
+
+ case 'g': return act_scroll_top;
+ case 'G': return act_scroll_bottom;
+ case KEY_HOME: return act_scroll_top;
+ case KEY_END: return act_scroll_bottom;
+
+ case KEY_PAGE_DOWN: return act_page_down;
+ case KEY_PAGE_UP: return act_page_up;
+
+ case KEY_TAB: return act_view_next;
+ case '\`': return act_sort_next;
+
+ case '!': return act_shell;
+ case '-': return act_cd_last;
+ case '.': return act_toggle_hidden;
+ case '/': return act_search_startswith;
+ case '?': return act_search_substring;
+ case ';': return cmd_cd;
+ case ':': return cmd_exec;
+ case '\'': return cmd_exec_sh;
+ case '<': return cmd_exec_stdin;
+
+ case '0': return act_cd_bookmark_0;
+ case '1': return act_cd_bookmark_1;
+ case '2': return act_cd_bookmark_2;
+ case '3': return act_cd_bookmark_3;
+ case '4': return act_cd_bookmark_4;
+ case '5': return act_cd_bookmark_5;
+ case '6': return act_cd_bookmark_6;
+ case '7': return act_cd_bookmark_7;
+ case '8': return act_cd_bookmark_8;
+ case '9': return act_cd_bookmark_9;
+
+ case 'R': return act_refresh;
+ case K(MOD_CTRL, 'l'): return act_redraw;
+ case 'q': return act_quit;
+ case 'Q': return act_quit_print_pwd;
+
+ case 'o': return cmd_exec_open;
+ case 'O': return cmd_exec_open_bg;
+
+ case 'f': return cmd_touch;
+ case 'n': return cmd_mkdir;
+ case 'r': return cmd_rename;
+ case 'x': return act_stat;
+ case 'z': return act_alt_buffer;
+ case 'p': return cmd_chmod;
+ case 'P': return cmd_chown;
+ case '~': return act_cd_home;
+
+ case 'Y': return cmd_copy_clipboard;
+ case K(MOD_CTRL, 'y'): return act_copy_pwd;
+
+ case 'M': return act_cd_mark_directory;
+ case ' ': return act_mark_toggle;
+ case K(MOD_CTRL, 'a'): return act_mark_toggle_all;
+ case 'I': return act_mark_invert;
+ case 'C': return act_mark_clear;
+ case '[': return act_mark_prev;
+ case ']': return act_mark_next;
+
+ case 'd': return cmd_remove;
+ case 'y': return cmd_copy;
+ case 'm': return cmd_move;
+ case 's': return cmd_link;
+
+#ifdef DFM_KEY_GREEK
+ // Map Greek to Latin keys.
+ // This fixes a pet peeve of mine where I cant use non-insert modes in vim
+ // and other TUIs without changing layout back to English.
+ case $(u8 λ): return act_open;
+ case $(u8 η): return act_cd_up;
+ case $(u8 ξ): return act_scroll_down;
+ case $(u8 κ): return act_scroll_up;
+ case $(u8 γ): return act_scroll_top;
+ case $(u8 Γ): return act_scroll_bottom;
+ case $(u8 Ρ): return act_refresh;
+ case $(u8 Φ): return cmd_touch;
+ case $(u8 ν): return cmd_mkdir;
+ case $(u8 ρ): return cmd_rename;
+ case $(u8 χ): return act_stat;
+ case $(u8 ζ): return act_alt_buffer;
+ case $(u8 π): return cmd_chmod;
+ case $(u8 Π): return cmd_chown;
+ case $(u8 Ι): return act_mark_invert;
+ case $(u8 Ψ): return act_mark_clear;
+ case $(u8 δ): return cmd_remove;
+ case $(u8 υ): return cmd_copy;
+ case $(u8 μ): return cmd_move;
+ case $(u8 σ): return cmd_link;
+ case $(u8 ´): return cmd_cd;
+ case $(u8 ¨): return cmd_exec;
+#endif // DFM_KEY_GREEK
+
+ //
+ // Debugging.
+ //
+ // case K(MOD_CTRL, 'c'): return act_crash;
+
+ default: return input_disabled;
+ }
+}
+
+//
+// For the full list of line editing functions, search dfm.c for "^input_.*".
+//
+static inline void (*fm_key_input(u32 cp))(struct fm *)
+{
+ switch (cp) {
+ case KEY_HOME: return input_move_beginning;
+ case KEY_END: return input_move_end;
+
+ case K(MOD_CTRL, 'a'): return input_move_beginning;
+ case K(MOD_CTRL, 'e'): return input_move_end;
+
+ case KEY_LEFT: return input_move_left;
+ case KEY_RIGHT: return input_move_right;
+
+ case KEY_ENTER: return input_submit;
+ case KEY_ESCAPE: return input_cancel;
+
+ case KEY_DELETE: return input_delete;
+ case K(MOD_CTRL, 'd'): return input_delete;
+
+ case K(MOD_CTRL, 'k'): return input_delete_to_end;
+ case K(MOD_CTRL, 'u'): return input_delete_to_beginning;
+ case K(MOD_CTRL, 'h'): return input_backspace;
+ case KEY_BACKSPACE: return input_backspace;
+
+ case K(MOD_ALT, 'b'): return input_move_word_left;
+ case K(MOD_ALT, 'f'): return input_move_word_right;
+
+ case K(MOD_CTRL, 'w'): return input_delete_word_left;
+ case K(MOD_ALT, 'a'): return input_delete_word_right;
+
+ case KEY_PASTE: return input_insert_paste;
+
+ //
+ // Don't send modifier keys, symbolic keys or unprintable characters to
+ // input_insert.
+ //
+ default:
+ return KEY_GET_MOD(cp) || KEY_IS_SYM(cp) || cp < 32
+ ? input_disabled : input_insert;
+ }
+}
+
diff --git a/configure b/configure
new file mode 100755
index 0000000..0d2e6da
--- /dev/null
+++ b/configure
@@ -0,0 +1,141 @@
+#!/bin/sh -eu
+#
+# DFM configure script.
+#
+# Copyright (c) 2026 Dylan Araps - MIT License
+#
+# The configure script takes three forms of arguments.
+#
+# 1) Long-opts: --prefix=/usr --help
+# 2) Variables: CC=/bin/cc CFLAGS="-O3" CONFIG_TINY=1
+# 3) C macro definitions: -DMACRO -DMACRO=VALUE -UMACRO
+#
+# Everything below, as well as in the Makefile.in and config.h.in files can be
+# configured via this script. Alternatively, you can edit the files directly,
+# refer to them for more information.
+#
+
+#
+# Program configuration.
+#
+export CFG_NAME=dfm
+export CFG_VERSION=0.99.0
+export c_version=c99
+
+#
+# DPP configuration.
+#
+export PATH="$PWD/bin:$PATH"
+export CFG_DPP_INCLUDE=./configure
+export CFG_DPP_CMD=bin/dpp
+
+#
+# Make configuration.
+#
+export CFG_MAKE_FILE=Makefile
+export CFG_MAKE_CONFIG=Makefile.in
+export CFG_MANUAL=README.txt
+export CFG_BUILD=config.h.in
+export CFG_BUILD_GEN=config.h
+export CFG_INPUT=config_key.h.in
+export CFG_INPUT_GEN=config_key.h
+export CFG_COMMAND_GEN=config_cmd.h
+export CFG_COMMAND=config_cmd.h.in
+export CFG_MACRO_GEN=.config_macro.h
+export CFG_MAKE_DEP="*/*.[ch] *.[ch] $CFG_MAKE_FILE $CFG_MAKE_CONFIG $CFG_DPP_INCLUDE"
+export CFG_IGNORE=.gitignore
+
+#
+# Installation
+#
+export prefix=/usr/local
+
+#
+# Build commands.
+#
+export RM="${RM:-rm}"
+export MKDIR="${MKDIR:-mkdir}"
+export CP="${CP:-cp}"
+export CC="${CC:-cc}"
+export STRIP="${STRIP:-strip}"
+
+#
+# Compiler flags.
+#
+export cc_flags="-std=$c_version -O2 -pipe"
+export cc_flags="$cc_flags -D_POSIX_C_SOURCE=200809L"
+export cc_flags="$cc_flags -D_BSD_SOURCE -D_XOPEN_SOURCE=500"
+export cc_flags="$cc_flags -Wall -Wextra -pedantic -Wshadow"
+
+
+#///////////////////////////////////////////////////////////////////////////////
+#
+# NOTE: Do not edit below this line.
+#
+case ${DPP_LEVEL:-} in '')
+ #
+ # Handle command-line arguments.
+ #
+ # '--prefix=/usr' -> 'export prefix=/usr'
+ # 'prefix=/usr' -> 'export prefix=/usr'
+ # '-DWORD' -> 'export WORD' 'CFLAGS+=-DWORD'
+ # '-DWORD=val' -> 'export WORD' '#undef WORD\n#define WORD val'
+ # '-UWORD' -> 'unset WORD' 'CFLAGS+=-UWORD'
+ #
+ for a do case $a in
+ --help) cat "$0"; exit 0 ;;
+ -D*=*) mo="${mo:-}$a
+" _a=${a#-D}; export "${_a%%=*}=" ;;
+ -D*) cc_flags="$cc_flags $a"; export "${a#-D}=" ;;
+ -U*) cc_flags="$cc_flags $a"; unset "${a#-U}" ;;
+ *?=?*) export "${a#--}" ;;
+ esac done
+
+ #
+ # Build configurations.
+ #
+ case ${CONFIG_SMALL:=${CONFIG_TINY:=0}} in 1)
+ cc_flags="$cc_flags -Os -DNDEBUG"
+ cc_flags="$cc_flags -fno-asynchronous-unwind-tables -fno-unwind-tables"
+ cc_flags="$cc_flags -Wl,-z,norelro"
+ cc_flags="$cc_flags -no-pie"
+ cc_flags="$cc_flags -fno-plt"
+ strip_flags="${strip_flags:-} -s -R .comment -R .note"
+ strip_flags="$strip_flags --remove-section=.eh_frame"
+ strip_flags="$strip_flags --remove-section=.eh_frame_hdr"
+ export strip_flags
+
+ case ${CONFIG_TINY:=0} in 1)
+ cc_flags="$cc_flags -Oz"
+ esac
+ esac
+
+ #
+ # Generate macro overrides.
+ #
+ while IFS== read -r k v; do
+ echo "${k:+#undef ${k#-D}
+#define ${k#-D} $v}"
+ done <<EOF > "$CFG_MACRO_GEN"
+${mo:-}
+EOF
+
+ #
+ # Generate Makefile
+ #
+ "$CFG_DPP_CMD" < "$CFG_MAKE_CONFIG" > "$CFG_MAKE_FILE"
+
+ #
+ # Generate .gitignore.
+ #
+ cat <<EOF > "$CFG_IGNORE"
+$CFG_IGNORE
+$CFG_NAME
+$CFG_MACRO_GEN
+$CFG_BUILD_GEN
+$CFG_COMMAND_GEN
+$CFG_INPUT_GEN
+$CFG_MAKE_FILE
+EOF
+esac
+
diff --git a/dfm.c b/dfm.c
new file mode 100644
index 0000000..566eeb3
--- /dev/null
+++ b/dfm.c
@@ -0,0 +1,3502 @@
+/*
+ * vim: foldmethod=marker
+ *
+ *
+ * oooooooooo. oooooooooooo ooo ooooo
+ * `888' `Y8b `888' `8 `88. .888'
+ * 888 888 888 888b d'888
+ * 888 888 888oooo8 8 Y88. .P 888
+ * 888 888 888 " 8 `888' 888
+ * 888 d88' 888 8 Y 888
+ * o888bood8P' o888o o8o o888o
+ *
+ * Dylan's File Manager
+ *
+ *
+ * Copyright (c) 2026 Dylan Araps
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <ftw.h>
+#include <limits.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <termios.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+
+#include "config.h"
+
+#include "lib/arg.h"
+#include "lib/bitset.h"
+#include "lib/date.h"
+#include "lib/readline.h"
+#include "lib/str.h"
+#include "lib/term.h"
+#include "lib/term_key.h"
+#include "lib/vt.h"
+
+#ifdef __linux__
+#include "platform/linux.h"
+#else
+#include "platform/posix.h"
+#endif
+
+static const char DFM_HELP[] =
+ "usage: " CFG_NAME " [options] [path]\n\n"
+ "options:\n"
+ "-H | +H toggle hidden files (-H off, +H on)\n"
+ "-p picker mode (print selected path to stdout and exit)\n"
+ "-o <opener> program to use when opening files (default: xdg-open)\n"
+ "-s <mode> change default sort\n"
+ " n name\n"
+ " N name reverse\n"
+ " e extension\n"
+ " s size\n"
+ " S size reverse\n"
+ " d date\n"
+ " D date reverse\n"
+ "-v <mode> change default view\n"
+ " n name only\n"
+ " s size\n"
+ " p permissions\n"
+ " t time\n"
+ " a all\n\n"
+ "--help show this help\n"
+ "--version show version\n\n"
+ "path:\n"
+ "directory to open (default: \".\")\n\n"
+ "environment:\n"
+ "DFM_OPENER program used to open files (overridden by -o)\n"
+ "DFM_BOOKMARK_[0-9] bookmark directories\n"
+ "DFM_COPYER program used to copy PWD and file contents.\n"
+;
+
+enum fm_opt {
+ FM_ERROR = 1 << 0,
+ FM_ROOT = 1 << 1,
+
+ FM_REDRAW_DIR = 1 << 2,
+ FM_REDRAW_NAV = 1 << 3,
+ FM_REDRAW_CMD = 1 << 4,
+ FM_REDRAW_FLUSH = 1 << 5,
+ FM_REDRAW = FM_REDRAW_DIR|FM_REDRAW_NAV|FM_REDRAW_CMD|FM_REDRAW_FLUSH,
+
+ FM_DIRTY = 1 << 6,
+ FM_DIRTY_WITHIN = 1 << 7,
+ FM_HIDDEN = 1 << 8,
+ FM_TRUNC = 1 << 9,
+ FM_MARK_PWD = 1 << 10,
+ FM_MSG = 1 << 11,
+ FM_MSG_ERR = 1 << 12,
+ FM_PICKER = 1 << 13,
+ FM_PRINT_PWD = 1 << 14,
+ FM_SEARCH = 1 << 15,
+};
+
+struct fm;
+typedef void (*fm_key_press)(struct fm *, int k, cut, cut);
+typedef int (*fm_key_enter)(struct fm *, str *);
+typedef int (*fm_filter)(struct fm *, usize, cut, cut);
+
+struct fm {
+ struct term t;
+ struct term_key k;
+ struct platform p;
+ struct readline r;
+
+ int dfd;
+ str pwd;
+ str ppwd;
+ str mpwd;
+
+ str io;
+
+ usize ml;
+ usize mp;
+
+ char de[DFM_ENT_MAX];
+ usize del;
+ usize dec;
+
+ union {
+ align_max _a;
+ unsigned char d[DFM_DIR_MAX * sizeof(u32)];
+ } d;
+
+ usize dl;
+ u8 dv;
+ u8 ds;
+ u32 du;
+
+ u64 v[BITSET_W(DFM_DIR_MAX)];
+ u16 vp[BITSET_W(DFM_DIR_MAX)];
+ usize vl;
+ char vq[DFM_NAME_MAX];
+ usize vql;
+
+ u64 vm[BITSET_W(DFM_DIR_MAX)];
+ usize vml;
+
+ u32 ht[DFM_DIR_HT_CAP];
+
+ usize y;
+ usize o;
+ usize c;
+ u32 st;
+
+ u16 row;
+ u16 col;
+
+ u32 f;
+ u32 cf;
+
+ cut opener;
+
+ fm_key_press kp;
+ fm_key_enter kd;
+ fm_filter sf;
+
+ s64 tz;
+};
+
+// Entry Virtual {{{
+
+#define ENT_V_OFF 0, 20
+#define ENT_V_CHAR 20, 8
+#define ENT_V_TOMB 28, 1
+#define ENT_V_MARK 29, 1
+#define ENT_V_VIS 30, 1
+#define ENT_V_DOT 31, 1
+
+#define ent_v_get(e, o) bitfield_get32((e), ENT_V_##o)
+#define ent_v_set(e, o, v) bitfield_set32((e), (v), ENT_V_##o)
+#define ent_v_geto(p, i, o) ent_v_get(ent_v_load((p), (i)), o)
+
+static inline unsigned char *
+ent_v_ptr(struct fm *p, usize i)
+{
+ return p->d.d + (i * sizeof(u32));
+}
+
+static inline const unsigned char *
+ent_v_ptr_const(const struct fm *p, usize i)
+{
+ return p->d.d + (i * sizeof(u32));
+}
+
+static inline u32
+ent_v_load(const struct fm *p, usize i)
+{
+ u32 v;
+ memcpy(&v, ent_v_ptr_const(p, i), sizeof(v));
+ return v;
+}
+
+static inline void
+ent_v_store(struct fm *p, usize i, u32 v)
+{
+ memcpy(ent_v_ptr(p, i), &v, sizeof(v));
+}
+
+// }}}
+
+// Entry Physical {{{
+
+enum {
+ ENT_DIR = 0,
+ ENT_LNK_DIR = 1,
+ ENT_LNK = 3,
+ ENT_LNK_BRK = 5,
+ ENT_UNKNOWN = 4,
+ ENT_FIFO = 6,
+ ENT_SOCK = 8,
+ ENT_SPEC = 10,
+ ENT_REG = 12,
+ ENT_REG_EXEC = 14,
+ ENT_TYPE_MAX = 16,
+};
+
+#define ENT_IS_LNK(t) ((t) & 1)
+#define ENT_IS_DIR(t) ((t) <= ENT_LNK_DIR)
+
+#define ENT_UTF8 0, 1
+#define ENT_WIDE 1, 1
+#define ENT_LOC 2, 16
+#define ENT_LEN 18, 8
+#define ENT_SIZE 26, 12
+#define ENT_TYPE 38, 4
+#define ENT_PERM 42, 12
+#define ENT_TIME 54, 5
+#define ENT_HASH 59, 5
+
+#define ent_get(e, o) bitfield_get64((u64)(e), ENT_##o)
+#define ent_set(e, o, v) bitfield_set64((e), (v), ENT_##o)
+#define lnk_set(l, o, v) bitfield_set8((l), (v), ENT_##o)
+
+static inline u64
+ent_load(const struct fm *p, usize i)
+{
+ u64 m;
+ memcpy(&m, p->de + ent_v_geto(p, i, OFF) - sizeof(m), sizeof(m));
+ return m;
+}
+
+static inline void
+ent_store(struct fm *p, usize i, u64 m)
+{
+ memcpy(p->de + ent_v_geto(p, i, OFF) - sizeof(m), &m, 8);
+}
+
+static inline u64
+ent_load_off(const struct fm *p, u32 o)
+{
+ u64 m;
+ memcpy(&m, p->de + o - sizeof(m), sizeof(m));
+ return m;
+}
+
+static inline void
+ent_perm_decode(str *s, mode_t m, u8 t)
+{
+ char b[11];
+ int d = t ? t == ENT_DIR : S_ISDIR(m);
+ b[0] = d ? 'd' : '-';
+ b[1] = (m & S_IRUSR) ? 'r' : '-';
+ b[2] = (m & S_IWUSR) ? 'w' : '-';
+ b[3] = (m & S_ISUID) ? (m & S_IXUSR) ? 's' : 'S' : (m & S_IXUSR) ? 'x' : '-';
+ b[4] = (m & S_IRGRP) ? 'r' : '-';
+ b[5] = (m & S_IWGRP) ? 'w' : '-';
+ b[6] = (m & S_ISGID) ? (m & S_IXGRP) ? 's' : 'S' : (m & S_IXGRP) ? 'x' : '-';
+ b[7] = (m & S_IROTH) ? 'r' : '-';
+ b[8] = (m & S_IWOTH) ? 'w' : '-';
+ b[9] = (m & S_ISVTX) ? (m & S_IXOTH) ? 't' : 'T' : (m & S_IXOTH) ? 'x' : '-';
+ b[10] = ' ';
+ str_push(s, b, sizeof(b));
+}
+
+static inline u32
+ent_size_encode(off_t s)
+{
+ if (s <= 0) return 0;
+ u64 v = (u64)s;
+ u32 e = 63 - u64_clz(v);
+ u64 b = 1ULL << e;
+ u32 f = 0;
+ if (e) {
+ u64 d = v - b;
+ f = (u32)((d << 6) >> e);
+ if (f > 63) f = 63;
+ }
+ return (e << 6) | f;
+}
+
+static inline u64
+ent_size_bytes(u32 v, u8 t)
+{
+ if (ENT_IS_LNK(t)) return v;
+ if (!v) return 0;
+ u32 e = v >> 6;
+ u32 f = v & 63;
+ u64 b = 1ULL << e;
+ return b + ((b * f) >> 6);
+}
+
+static inline u32
+ent_size_add(u32 e, u64 a)
+{
+ if (!e) return ent_size_encode(a);
+ if (!a) return e;
+ u64 c = ent_size_bytes(e, ENT_TYPE_MAX);
+ return ent_size_encode(c + a);
+}
+
+static inline u32
+ent_size_sub(u32 e, u64 s)
+{
+ if (!e) return 0;
+ u64 c = ent_size_bytes(e, ENT_TYPE_MAX);
+ if (s >= c) return 0;
+ return ent_size_encode(c - s);
+}
+
+static inline void
+ent_size_decode(str *s, u32 v, usize p, u8 t)
+{
+ if (ENT_IS_LNK(t) || !v) {
+ str_push_u32_p(s, v, ' ', p ? p - 1 : 0);
+ str_push_c(s, 'B');
+ if (p) str_push_c(s, ' ');
+ return;
+ }
+ u32 e = v >> 6;
+ u32 f = v & 63;
+ u32 u = e / 10;
+ if (u > 6) u = 6;
+ u64 b = 1ULL << (e - u * 10);
+ u64 ip = b + ((b * f) >> 6);
+ u32 d = ((f * 10) + 32) >> 6;
+ if (d == 10) { ip++; d = 0; }
+ int sd = (u && ip < 10);
+ usize su = 1 + (sd ? 2 : 0);
+ usize pa = p > su ? p - su : 0;
+ str_push_u32_p(s, (u32)ip, ' ', pa);
+ if (sd) {
+ str_push_c(s, '.');
+ str_push_u32(s, d);
+ }
+ str_push_c(s, "BKMGTPE"[u]);
+ if (p) str_push_c(s, ' ');
+}
+
+static inline u32
+ent_time_encode(time_t t)
+{
+ time_t d = time(NULL) - t;
+ return d <= 0 ? 0 : (u32)(63 - u64_clz(d));
+}
+
+static inline void
+ent_time_decode(str *s, u32 v)
+{
+ static const char *u[] = {
+ "s ", "s ", "s ", "s ", "s ", "s ",
+ "m ", "m ", "m ", "m ", "m ", "m ",
+ "h ", "h ", "h ", "h ", "h ",
+ "d ", "d ", "d ", "d ", "d ",
+ "w ", "w ", "w ", "w ",
+ "mo", "mo", "mo", "mo", "mo"
+ };
+ if (v > 31) v = 31;
+ str_push(s, v == 31 ? ">= " : " ", 3);
+ str_push_u32_p(s, v == 31 ? 1u << 5 : 1u << (v % 6), ' ', 2);
+ str_push(s, u[v], 2);
+ str_push_c(s, ' ');
+}
+
+static inline void
+ent_map_stat(u64 *e, const struct stat *s, u8 t)
+{
+ if (t != ENT_TYPE_MAX) goto e;
+ if (S_ISDIR(s->st_mode)) t = ENT_DIR;
+ else if (S_ISLNK(s->st_mode)) t = ENT_LNK;
+ else if (S_ISREG(s->st_mode) && (s->st_mode & (S_IXUSR|S_IXGRP|S_IXOTH)))
+ t = ENT_REG_EXEC;
+ else if (S_ISREG(s->st_mode)) t = ENT_REG;
+ else if (S_ISFIFO(s->st_mode)) t = ENT_FIFO;
+ else if (S_ISSOCK(s->st_mode)) t = ENT_SOCK;
+ else if (S_ISCHR(s->st_mode) || S_ISBLK(s->st_mode))
+ t = ENT_SPEC;
+ else t = ENT_UNKNOWN;
+e:
+ ent_set(e, TYPE, t);
+ ent_set(e, PERM, s->st_mode & 07777);
+ ent_set(e, TIME, ent_time_encode(s->st_mtime));
+}
+
+static inline void
+ent_map_stat_size(u64 *e, const struct stat *s)
+{
+ ent_set(e, SIZE, S_ISLNK(s->st_mode)
+ ? s->st_size : ent_size_encode(s->st_size));
+}
+
+static inline usize
+ent_name_len(const char *s, u8 *utf8, u8 *wide)
+{
+ const unsigned char *m = (const unsigned char *)s;
+ const unsigned char *p = m;
+ *utf8 = 0;
+ *wide = 0;
+#ifdef __GNUC__
+ typedef size_t __attribute__((__may_alias__)) W;
+ #define ONES ((size_t)-1 / UCHAR_MAX)
+ #define HIGHS (ONES * (UCHAR_MAX / 2 + 1))
+ for (; (uintptr_t)p % sizeof(W); p++) {
+ if (!*p) return (size)(p - m);
+ if (*p & 0x80) { *utf8 = 1; goto check_wide; }
+ }
+ for (const W *w = (const void *)p;; w++) {
+ W v = *w;
+ if ((v & HIGHS) | ((v - ONES) & ~v & HIGHS)) {
+ p = (const unsigned char *)w;
+ for (;; p++) {
+ if (!*p) return (size)(p - m);
+ if (*p & 0x80) { *utf8 = 1; goto check_wide; }
+ }
+ }
+ }
+#endif
+ for (;;) {
+ unsigned char b = *p;
+ if (!b) return (size)(p - m);
+ if (!(b & 0x80)) { p++; continue; }
+ *utf8 = 1;
+check_wide:;
+ unsigned char b2 = *p;
+ if ((b2 & 0xF8) == 0xF0) { *wide = 1; break; }
+ if ((b2 & 0xF0) == 0xE0) {
+ u32 cp;
+ utf8_decode((void *)p, &cp);
+ if (utf8_width(cp) > 1) { *wide = 1; break; }
+ }
+ usize n = utf8_expected(b2);
+ p += n ? n : 1;
+ }
+ for (p++;;) {
+#ifdef __GNUC__
+ for (; (uintptr_t)p % sizeof(W); p++) if (!*p) return (size)(p - m);
+ for (const W *w = (const void *)p;; w++) {
+ if ((*w - ONES) & ~*w & HIGHS) {
+ p = (const unsigned char *)w;
+ for (; *p; p++);
+ return (size)(p - m);
+ }
+ }
+#endif
+ if (!*p) return (size)(p - m);
+ p++;
+ }
+}
+
+static inline usize
+ent_next(struct fm *p, usize i)
+{
+ return bitset_next_set(p->v, i, p->dl);
+}
+
+static inline usize
+ent_prev(struct fm *p, usize i)
+{
+ return bitset_prev_set(p->v, i, p->dl);
+}
+
+// }}}
+
+// Sorting {{{
+
+typedef int (*ent_sort_cb)(struct fm *, u32, u32);
+
+static inline int
+fm_ent_cmp_name(struct fm *p, u32 a, u32 b)
+{
+ static const unsigned char t[256] = {
+ ['0']=1,['1']=1,['2']=1,['3']=1,['4']=1,
+ ['5']=1,['6']=1,['7']=1,['8']=1,['9']=1
+ };
+
+ u32 oa = ent_v_get(a, OFF);
+ u32 ob = ent_v_get(b, OFF);
+ u64 ma = ent_load_off(p, oa);
+ u64 mb = ent_load_off(p, ob);
+
+ int r = ENT_IS_DIR(ent_get(mb, TYPE)) - ENT_IS_DIR(ent_get(ma, TYPE));
+ if (unlikely(r)) return r;
+
+ u8 fa = ent_v_get(a, CHAR);
+ u8 fb = ent_v_get(b, CHAR);
+
+ int da = (unsigned)(fa - '0') < 10;
+ int db = (unsigned)(fb - '0') < 10;
+ if (da ^ db)
+ return da ? -1 : 1;
+
+ if (fa != fb && !(t[fa] & t[fb]))
+ return fa < fb ? -1 : 1;
+
+ const char *pa = p->de + oa;
+ const char *pb = p->de + ob;
+ usize la = ent_get(ma, LEN);
+ usize lb = ent_get(mb, LEN);
+ usize i = 0;
+ usize j = 0;
+
+ while (i < la && j < lb) {
+ unsigned char ca = (unsigned char)pa[i];
+ unsigned char cb = (unsigned char)pb[j];
+
+ if (unlikely(t[ca] & t[cb])) {
+ usize ia = i;
+ usize ja = j;
+ for (; ia < la && pa[ia] == '0'; ia++);
+ for (; ja < lb && pb[ja] == '0'; ja++);
+ usize ea = ia;
+ usize eb = ja;
+ for (; ea < la && t[(unsigned char)pa[ea]]; ea++);
+ for (; eb < lb && t[(unsigned char)pb[eb]]; eb++);
+ usize na = ea - ia;
+ usize nb = eb - ja;
+ if (na != nb) return na < nb ? -1 : 1;
+ int cmp = memcmp(pa + ia, pb + ja, na);
+ if (cmp) return cmp;
+ usize za = ia - i;
+ usize zb = ja - j;
+ if (za != zb) return za < zb ? -1 : 1;
+ i = ea;
+ j = eb;
+ continue;
+ }
+
+ if (ca != cb) return ca < cb ? -1 : 1;
+ i++;
+ j++;
+ }
+
+ return (i < la) - (j < lb);
+}
+
+static inline int
+fm_ent_cmp_name_rev(struct fm *p, u32 a, u32 b)
+{
+ return fm_ent_cmp_name(p, a, b) * -1;
+}
+
+static inline int
+fm_ent_cmp_size(struct fm *p, u32 a, u32 b)
+{
+ u64 ma = ent_load_off(p, ent_v_get(a, OFF));
+ u64 mb = ent_load_off(p, ent_v_get(b, OFF));
+ u32 sa = ent_size_bytes(ent_get(ma, SIZE), ent_get(ma, TYPE));
+ u32 sb = ent_size_bytes(ent_get(mb, SIZE), ent_get(mb, TYPE));
+ return (size)sa - (size)sb;
+}
+
+static inline int
+fm_ent_cmp_date(struct fm *p, u32 a, u32 b)
+{
+ u64 ma = ent_load_off(p, ent_v_get(a, OFF));
+ u64 mb = ent_load_off(p, ent_v_get(b, OFF));
+ return (size)ent_get(ma, TIME) - (size)ent_get(mb, TIME);
+}
+
+static inline int
+fm_ent_cmp_size_rev(struct fm *p, u32 a, u32 b)
+{
+ return fm_ent_cmp_size(p, b, a);
+}
+
+static inline int
+fm_ent_cmp_date_rev(struct fm *p, u32 a, u32 b)
+{
+ return fm_ent_cmp_date(p, b, a);
+}
+
+static inline int
+fm_ent_cmp_fext(struct fm *p, u32 a, u32 b)
+{
+ u32 oa = ent_v_get(a, OFF);
+ u32 ob = ent_v_get(b, OFF);
+ u64 ma = ent_load_off(p, oa);
+ u64 mb = ent_load_off(p, ob);
+ cut ca = { p->de + oa, ent_get(ma, LEN) };
+ cut cb = { p->de + ob, ent_get(mb, LEN) };
+ const char *pa = ca.d + ca.l;
+ const char *pb = cb.d + cb.l;
+ for (; pa > ca.d && pa[-1] != '.'; pa--);
+ for (; pb > cb.d && pb[-1] != '.'; pb--);
+ if (pa == ca.d && pb != cb.d) return 1;
+ if (pb == cb.d && pa != ca.d) return -1;
+ if (pa == ca.d && pb == cb.d) return 0;
+ usize la = (usize)(ca.d + ca.l - pa);
+ usize lb = (usize)(cb.d + cb.l - pb);
+ int r = memcmp(pa, pb, la < lb ? la : lb);
+ return r ? r : (int)(la < lb) - (int)(la > lb);
+}
+
+static inline void
+fm_ent_isort(struct fm *p, ent_sort_cb f, usize lo, usize hi)
+{
+ for (usize i = lo + 1; i < hi; i++) {
+ u32 x = ent_v_load(p, i);
+ usize j = i;
+ for (; j > lo && f(p, ent_v_load(p, j - 1), x) > 0; j--)
+ ent_v_store(p, j, ent_v_load(p, j - 1));
+ ent_v_store(p, j, x);
+ }
+}
+
+static inline void
+fm_ent_qsort(struct fm *p, ent_sort_cb f, usize lo, usize hi, int d)
+{
+ while (hi - lo > 16) {
+ if (!d--) break;
+ usize mid = lo + ((hi - lo) >> 1);
+
+ u32 a = ent_v_load(p, lo);
+ u32 b = ent_v_load(p, mid);
+ u32 c = ent_v_load(p, hi - 1);
+ u32 pivot = (f(p, a, b) < 0
+ ? (f(p, b, c) < 0 ? b : (f(p, a, c) < 0 ? c : a))
+ : (f(p, a, c) < 0 ? a : (f(p, b, c) < 0 ? c : b)));
+
+ usize i = lo;
+ usize j = hi - 1;
+
+ for (;; i++, j--) {
+ for (; f(p, ent_v_load(p, i), pivot) < 0; i++);
+ for (; f(p, pivot, ent_v_load(p, j)) < 0; j--);
+ if (i >= j) break;
+ u32 t = ent_v_load(p, i);
+ ent_v_store(p, i, ent_v_load(p, j));
+ ent_v_store(p, j, t);
+ }
+
+ if (j - lo < hi - (j + 1)) {
+ fm_ent_qsort(p, f, lo, j + 1, d);
+ lo = j + 1;
+ } else {
+ fm_ent_qsort(p, f, j + 1, hi, d);
+ hi = j + 1;
+ }
+ }
+
+ fm_ent_isort(p, f, lo, hi);
+}
+
+static inline ent_sort_cb
+fm_sort_fn(u8 s)
+{
+ switch (s) {
+ case 'n': return fm_ent_cmp_name;
+ case 'N': return fm_ent_cmp_name_rev;
+ case 'e': return fm_ent_cmp_fext;
+ case 's': return fm_ent_cmp_size;
+ case 'S': return fm_ent_cmp_size_rev;
+ case 'd': return fm_ent_cmp_date;
+ case 'D': return fm_ent_cmp_date_rev;
+ default: return 0;
+ }
+}
+
+// }}}
+
+// Util {{{
+
+static inline cut
+fm_ent(const struct fm *p, usize i)
+{
+ u32 o = ent_v_geto(p, i, OFF);
+ u64 m = ent_load_off(p, o);
+ return (cut) {p->de + o, ent_get(m, LEN) };
+}
+
+static inline cut
+fm_file_type(mode_t m)
+{
+ if (S_ISREG(m) && m & (S_IXUSR|S_IXGRP|S_IXOTH))
+ return (cut){ S("executable file") };
+ if (S_ISREG(m)) return (cut){ S("regular file") };
+ if (S_ISDIR(m)) return (cut){ S("directory") };
+ if (S_ISLNK(m)) return (cut){ S("symlink") };
+ if (S_ISCHR(m)) return (cut){ S("char device") };
+ if (S_ISBLK(m)) return (cut){ S("block device") };
+ if (S_ISFIFO(m)) return (cut){ S("fifo") };
+ if (S_ISSOCK(m)) return (cut){ S("socket") };
+ return (cut){ S("unknown") };
+}
+
+static inline void
+str_push_time(str *s, s64 tz, const struct timespec *ts)
+{
+ s32 y; u32 m; u32 d;
+ u32 H; u32 M; u32 S;
+ ut_to_date_time(tz, ts->tv_sec, &y, &m, &d, &H, &M, &S);
+ str_push_u32_p(s, y, '0', 2);
+ str_push_c(s, '-');
+ str_push_u32_p(s, m, '0', 2);
+ str_push_c(s, '-');
+ str_push_u32_p(s, d, '0', 2);
+ str_push_c(s, ' ');
+ str_push_u32_p(s, H, '0', 2);
+ str_push_c(s, ':');
+ str_push_u32_p(s, M, '0', 2);
+ str_push_c(s, ':');
+ str_push_u32_p(s, S, '0', 2);
+}
+
+static inline int
+next_tok(const char *s, usize l, usize *c, cut *o)
+{
+ usize p = *c;
+ for (; p < l && (s[p] == ' ' || !s[p]); p++);
+ if (p >= l) {
+ *c = p;
+ *o = (cut){ s, 0 };
+ return 0;
+ }
+ usize t = p;
+ for (; p < l && s[p] != ' ' && s[p]; p++);
+ *c = p;
+ *o = (cut){ &s[t], p - t };
+ return 1;
+}
+
+// }}}
+
+// Visibility {{{
+
+static inline void
+fm_v_clr(struct fm *p, usize i)
+{
+ if (!ent_v_geto(p, i, VIS)) return;
+ u32 e = ent_v_load(p, i);
+ ent_v_set(&e, VIS, 0);
+ ent_v_store(p, i, e);
+}
+
+static inline void
+fm_v_assign(struct fm *p, usize i, u8 v)
+{
+ if (ent_v_geto(p, i, VIS) == v)
+ return;
+ u32 e = ent_v_load(p, i);
+ ent_v_set(&e, VIS, v);
+ ent_v_store(p, i, e);
+}
+
+static inline void
+fm_v_rebuild(struct fm *p)
+{
+ p->vl = 0;
+ u16 s = 0;
+ for (usize b = 0, c = BITSET_W(p->dl); b < c; b++) {
+ u64 w = 0;
+ for (usize j = 0; j < 64; j++) {
+ usize i = (b << 6) + j;
+ if (i >= p->dl) break;
+ if (ent_v_geto(p, i, VIS))
+ w |= 1ULL << j;
+ }
+ p->v[b] = w;
+ p->vp[b] = s;
+ s += u64_popcount(w);
+ }
+ p->vl = s;
+}
+
+// }}}
+
+// Filtering {{{
+
+static inline usize
+fm_filter_pct_rank(struct fm *p, usize idx)
+{
+ usize b = idx >> 6;
+ usize o = idx & 63;
+ u64 m = o ? ((1ULL << o) - 1) : 0ULL;
+ return (usize)p->vp[b] + u64_popcount(p->v[b] & m);
+}
+
+static inline void
+fm_filter_apply(struct fm *p, fm_filter f, cut cl, cut cr)
+{
+ for (usize i = 0; i < p->dl; i++)
+ if (ent_v_geto(p, i, TOMB))
+ fm_v_clr(p, i);
+ else
+ fm_v_assign(p, i, f(p, i, cl, cr));
+ fm_v_rebuild(p);
+ p->f |= FM_REDRAW_DIR|FM_REDRAW_NAV;
+}
+
+static inline void
+fm_filter_apply_inc(struct fm *p, fm_filter f, cut cl, cut cr)
+{
+ for (usize i = ent_next(p, 0); i != SIZE_MAX; i = ent_next(p, i + 1))
+ if (ent_v_geto(p, i, TOMB) || !f(p, i, cl, cr))
+ fm_v_clr(p, i);
+ fm_v_rebuild(p);
+ p->f |= FM_REDRAW_DIR|FM_REDRAW_NAV;
+}
+
+static int
+fm_filter_hidden(struct fm *p, usize i, cut cl, cut cr)
+{
+ (void)cl;
+ (void)cr;
+ if (ent_v_geto(p, i, TOMB))
+ return 0;
+ if (p->f & FM_HIDDEN)
+ return 1;
+ return !ent_v_geto(p, i, DOT);
+}
+
+static inline int
+fm_filter_startswith(struct fm *p, usize i, cut cl, cut cr)
+{
+ usize al = cl.l;
+ usize bl = cr.l;
+ const char *am = cl.d;
+ const char *bm = cr.d;
+ u64 m = ent_load(p, i);
+ u32 o = ent_v_geto(p, i, OFF);
+ cut n = { p->de + o, ent_get(m, LEN) };
+ usize w = al + bl;
+ if (w > n.l) return 0;
+ if (al && (*n.d != *am || (al > 1 && memcmp(n.d + 1, am + 1, al - 1))))
+ return 0;
+ return !(bl && memcmp(n.d + al, bm, bl));
+}
+
+static int
+fm_filter_substr(struct fm *p, usize i, cut cl, cut cr)
+{
+ usize al = cl.l;
+ usize bl = cr.l;
+ const char *am = cl.d;
+ const char *bm = cr.d;
+ usize w = al + bl;
+ if (!w) return 1;
+ u64 m = ent_load(p, i);
+ u32 o = ent_v_geto(p, i, OFF);
+ cut n = { p->de + o, ent_get(m, LEN) };
+ if (w > n.l) return 0;
+ for (usize j = 0, x = n.l - w; j <= x; j++) {
+ if (al && memcmp(n.d + j, am, al)) continue;
+ if (bl && memcmp(n.d + j + al, bm, bl)) continue;
+ return 1;
+ }
+ return 0;
+}
+
+//
+// TODO: Make this incremental.
+//
+static inline void
+fm_filter_save(struct fm *p, cut cl, cut cr)
+{
+ usize c = sizeof(p->vq);
+ usize i = 0;
+ if (cl.l) {
+ usize n = MIN(cl.l, c - 1);
+ memcpy(p->vq, cl.d, n);
+ i += n;
+ }
+ if (cr.l && i < c - 1) {
+ usize n = MIN(cr.l, c - 1 - i);
+ memcpy(p->vq + i, cr.d, n);
+ i += n;
+ }
+ p->vq[i] = 0;
+ p->vql = i;
+}
+
+static inline void
+fm_filter_clear(struct fm *p)
+{
+ fm_filter_apply(p, fm_filter_hidden, CUT_NULL, CUT_NULL);
+ p->vql = 0;
+ p->f &= ~FM_SEARCH;
+}
+
+static inline usize
+fm_visible_select(struct fm *p, usize k)
+{
+ if (k >= p->vl) return SIZE_MAX;
+ usize lo = 0;
+ usize hi = BITSET_W(p->dl);
+ if (!hi) return SIZE_MAX;
+ while (lo + 1 < hi) {
+ usize mi = lo + ((hi - lo) >> 1);
+ if (p->vp[mi] <= k) lo = mi;
+ else hi = mi;
+ }
+ u64 w = p->v[lo];
+ usize rank = k - p->vp[lo];
+ for (; rank--; w &= w - 1);
+ return (lo << 6) + u64_ctz(w);
+}
+
+// }}}
+
+// UTF8 Truncation Cache {{{
+
+#define DFM_HT_OCC 0x800u
+#define DFM_HT_CACHE 0x40000000u
+#define CACHE_HASH(x) ((u32)((x) & 0x0003F7FFu))
+#define CACHE_LEN(x) ((u16)(((x) >> 18) & 0x0FFFu))
+#define CACHE_IS(x) (((x) & (DFM_HT_CACHE | DFM_HT_OCC)) == DFM_HT_CACHE)
+#define CACHE_PACK(h,l) (DFM_HT_CACHE | \
+ ((h) & 0x0003F7FFu) | (((u32)(l) & 0x0FFFu) << 18))
+
+static inline u16
+fm_cache_hash(const struct fm *p, const char *n, usize l)
+{
+ u32 h = hash_fnv1a32(n, l);
+ u32 m = h;
+ m ^= (u32)p->col * 0x9E3779B1u;
+ m ^= (u32)p->dv * 0x85EBCA6Bu;
+ m ^= m >> 16;
+ return (u16)m;
+}
+
+static inline usize
+fm_cache_slot(u16 h)
+{
+ return (h & 0xF7FFu) & (DFM_DIR_HT_CAP - 1);
+}
+
+static inline void
+fm_dir_ht_clear_cache(struct fm *p)
+{
+ for (usize i = 0; i < DFM_DIR_HT_CAP; i++)
+ if (CACHE_IS(p->ht[i])) p->ht[i] = 0;
+}
+
+// }}}
+
+// Directory Lookup {{{
+
+#define DFM_HT_TOMB 0x7FFu
+#define DFM_HT_IS_FREE(x) (!((x) & DFM_HT_OCC))
+
+static inline void
+fm_dir_ht_hash_split(u32 h, u16 *a, u8 *b)
+{
+ u32 m = h ^ (h >> 16);
+ u16 x = m & 0x07FF;
+ *a = x ? x : 1;
+ *b = (m >> 11) & 0x1F;
+}
+
+static inline usize
+fm_dir_ht_find(struct fm *p, cut c, u16 *o)
+{
+ u32 h = hash_fnv1a32(c.d, c.l);
+ u16 a;
+ u8 b;
+ fm_dir_ht_hash_split(h, &a, &b);
+ usize i = h & (DFM_DIR_HT_CAP - 1);
+ for (;;) {
+ u32 s = p->ht[i];
+ if (!s) {
+ *o = 0xFFFF;
+ return i;
+ }
+ if ((s & DFM_HT_OCC) && !CACHE_IS(s) && (s & 0x07FF) == a) {
+ u64 m = ent_load_off(p, s >> 12);
+ if (ent_get(m, HASH) == b) {
+ u16 j = (u16)ent_get(m, LOC);
+ if (!ent_v_geto(p, j, TOMB) && cut_cmp(fm_ent(p, j), c)) {
+ *o = j;
+ return i;
+ }
+ }
+ }
+ i = (i + 1) & (DFM_DIR_HT_CAP - 1);
+ }
+}
+
+static inline int
+fm_dir_exists(struct fm *p, cut c)
+{
+ u16 i;
+ fm_dir_ht_find(p, c, &i);
+ return i != 0xFFFF;
+}
+
+static inline usize
+fm_dir_ht_find_insert(struct fm *p, u32 h)
+{
+ usize i = h & (DFM_DIR_HT_CAP - 1);
+ usize ft = SIZE_MAX;
+ for (;;) {
+ u32 s = p->ht[i];
+ if (s == DFM_HT_TOMB) {
+ if (ft == SIZE_MAX) ft = i;
+ } else if (DFM_HT_IS_FREE(s) || CACHE_IS(s))
+ return ft != SIZE_MAX ? ft : i;
+ i = (i + 1) & (DFM_DIR_HT_CAP - 1);
+ }
+}
+
+static inline void
+fm_dir_ht_insert(struct fm *p, cut c, u16 o, u64 *m)
+{
+ u32 h = hash_fnv1a32(c.d, c.l);
+ u16 a;
+ u8 b;
+ fm_dir_ht_hash_split(h, &a, &b);
+ ent_set(m, HASH, b);
+ usize i = fm_dir_ht_find_insert(p, h);
+ p->ht[i] = (ent_v_geto(p, o, OFF) << 12) | DFM_HT_OCC | a;
+}
+
+static inline void
+fm_dir_ht_remove(struct fm *p, usize i)
+{
+ p->ht[i] = DFM_HT_TOMB;
+}
+
+static inline void
+fm_dir_ht_clear(struct fm *p)
+{
+ memset(p->ht, 0, sizeof(p->ht));
+}
+
+// }}}
+
+// Draw {{{
+
+static inline void
+fm_draw_flush(struct fm *p)
+{
+ STR_PUSH(&p->io, VT_ESU);
+ p->io.f(&p->io, p, 0);
+ STR_PUSH(&p->io, VT_BSU);
+}
+
+static inline usize
+fm_draw_trunc_name(struct fm *p, u64 m, const char *n, usize l, usize c)
+{
+ if (!c) return 0;
+ int w = ent_get(m, WIDE);
+ if (l < c) return l;
+ int u = ent_get(m, UTF8);
+ if (!u) return MIN(l, c);
+ if (!w) return utf8_trunc_narrow(n, l, c);
+ u16 h = fm_cache_hash(p, n, l);
+ usize i = fm_cache_slot(h);
+ for (usize j = 0; j < 4; j++) {
+ usize s = (i + j) & (DFM_DIR_HT_CAP - 1);
+ u32 v = p->ht[s];
+ if (CACHE_IS(v) && CACHE_HASH(v) == (h & 0xF7FFu))
+ return CACHE_LEN(v) < l ? CACHE_LEN(v) : l;
+ }
+ usize tl = utf8_trunc_wide(n, l, c);
+ for (usize j = 0; j < 4; j++) {
+ usize s = (i + j) & (DFM_DIR_HT_CAP - 1);
+ u32 v = p->ht[s];
+ if (CACHE_IS(v) || !(v & DFM_HT_OCC)) {
+ p->ht[s] = CACHE_PACK(h, (u16)tl);
+ break;
+ }
+ }
+ return tl;
+}
+
+static inline void
+fm_draw_ent(struct fm *p, usize n)
+{
+ u64 e = ent_load(p, n);
+ u32 o = ent_v_geto(p, n, OFF);
+ u32 t = ent_get(e, TYPE);
+ s32 vw = p->col;
+
+ switch (p->dv) {
+ case 's': vw -= 7; ent_size_decode(&p->io, ent_get(e, SIZE), 6, t); break;
+ case 'p': vw -= 11; ent_perm_decode(&p->io, ent_get(e, PERM), t); break;
+ case 't': vw -= 8; ent_time_decode(&p->io, ent_get(e, TIME)); break;
+ case 'a':
+ vw -= 26;
+ ent_perm_decode(&p->io, ent_get(e, PERM), t);
+ ent_time_decode(&p->io, ent_get(e, TIME));
+ ent_size_decode(&p->io, ent_get(e, SIZE), 6, t);
+ break;
+ }
+
+ switch (t) {
+ case ENT_DIR: STR_PUSH(&p->io, DFM_COL_DIR); vw--; break;
+ case ENT_FIFO: STR_PUSH(&p->io, DFM_COL_FIFO); break;
+ case ENT_LNK: STR_PUSH(&p->io, DFM_COL_LNK); break;
+ case ENT_LNK_BRK: STR_PUSH(&p->io, DFM_COL_LNK_BRK); break;
+ case ENT_LNK_DIR: STR_PUSH(&p->io, DFM_COL_LNK_DIR); break;
+ case ENT_REG_EXEC: STR_PUSH(&p->io, DFM_COL_REG_EXEC); vw--; break;
+ case ENT_SOCK: STR_PUSH(&p->io, DFM_COL_SOCK); break;
+ case ENT_SPEC: STR_PUSH(&p->io, DFM_COL_SPEC); break;
+ case ENT_UNKNOWN: STR_PUSH(&p->io, DFM_COL_UNKNOWN); break;
+ }
+
+ int m = p->f & FM_MARK_PWD && p->vml && ent_v_geto(p, n, MARK);
+ if (m) {
+ STR_PUSH(&p->io, DFM_COL_MARK " ");
+ vw -= 2;
+ }
+ if (p->c == n) STR_PUSH(&p->io, DFM_COL_CURSOR);
+ usize l = ent_get(e, LEN);
+ const char *dn = &p->de[o];
+ usize c = fm_draw_trunc_name(p, e, dn, l, vw < 0 ? 0 : vw);
+ str_push_sanitize(&p->io, dn, c);
+
+ switch (t) {
+ case ENT_LNK_DIR:
+ case ENT_DIR: str_push_c(&p->io, '/'); break;
+ case ENT_REG_EXEC: str_push_c(&p->io, '*'); break;
+ }
+
+ if (m) str_push_c(&p->io, '*');
+
+ if (ENT_IS_LNK(t)) {
+ u8 sl = ent_get(e, SIZE);
+ vw -= c + 4;
+ if (vw <= 0) goto e;
+ STR_PUSH(&p->io, VT_SGR0 " -> ");
+ if (sl) {
+ dn = &p->de[o + l + 2];
+ c = fm_draw_trunc_name(p, dn[-1], dn, sl, vw < 0 ? 0 : vw);
+ str_push_sanitize(&p->io, dn, c);
+ } else
+ str_push_c(&p->io, '?');
+ }
+
+e:
+ STR_PUSH(&p->io, VT_SGR0 VT_EL0 VT_CR);
+}
+
+static inline void
+fm_draw_dir(struct fm *p)
+{
+ usize s = p->y >= p->o ? p->y - p->o : 0;
+ usize m = p->vl - s;
+ usize d = MIN(m, p->row);
+ usize c = fm_visible_select(p, s);
+ STR_PUSH(&p->io, VT_CUP1);
+
+ for (usize i = 0; i < d && c != SIZE_MAX; i++) {
+ fm_draw_ent(p, c);
+ STR_PUSH(&p->io, VT_CUD1);
+ c = ent_next(p, c + 1);
+ }
+
+ for (usize i = d; i < p->row; i++)
+ STR_PUSH(&p->io, VT_EL2 VT_CUD1);
+}
+
+static inline void
+fm_draw_nav_begin(struct fm *p, cut c)
+{
+ vt_cup(&p->io, 0, p->row + (DFM_MARGIN - 1));
+ str_push(&p->io, c.d, c.l);
+ str_memset(&p->io, ' ', p->col);
+ STR_PUSH(&p->io, VT_CR);
+}
+
+static inline void
+fm_draw_nav_end(struct fm *p)
+{
+ STR_PUSH(&p->io, VT_SGR0);
+}
+
+static inline void
+fm_draw_inf(struct fm *p)
+{
+ cut c = p->f & (FM_TRUNC|FM_ERROR) ? CUT(DFM_COL_NAV_ERR) :
+ p->f & FM_ROOT ? CUT(DFM_COL_NAV_ROOT) : CUT(DFM_COL_NAV);
+ fm_draw_nav_begin(p, c);
+ str_push_c(&p->io, ' ');
+ str_push_u32(&p->io, p->y + !!p->vl);
+ str_push_c(&p->io, '/');
+ str_push_u32(&p->io, p->vl);
+ STR_PUSH(&p->io, " ");
+
+ str_push_c(&p->io, '[');
+ if (unlikely(p->f & FM_ROOT)) str_push_c(&p->io, 'R');
+ if (likely(!(p->f & FM_TRUNC))) str_push_c(&p->io, p->ds);
+ else str_push_c(&p->io, 'T');
+ if (unlikely(p->f & FM_ERROR)) str_push_c(&p->io, 'E');
+ if (unlikely(p->f & FM_HIDDEN)) str_push_c(&p->io, 'H');
+ STR_PUSH(&p->io, "] ");
+
+ if (p->vml) {
+ STR_PUSH(&p->io, DFM_COL_NAV_MARK " ");
+ str_push_u32(&p->io, p->vml);
+ STR_PUSH(&p->io, " marked " VT_SGR0);
+ str_push(&p->io, c.d, c.l);
+ str_push_c(&p->io, ' ');
+ }
+
+ if (likely(!(p->f & FM_TRUNC))) {
+ STR_PUSH(&p->io, "~");
+ ent_size_decode(&p->io, p->du, 0, ENT_TYPE_MAX);
+ STR_PUSH(&p->io, " ");
+ }
+
+ str_push_sanitize(&p->io, p->pwd.m, MIN(p->pwd.l, p->col));
+
+ if (p->f & FM_SEARCH) {
+ STR_PUSH(&p->io, "/" VT_SGR(1));
+ if (p->sf == fm_filter_substr) str_push_c(&p->io, '*');
+ str_push(&p->io, p->vq, p->vql);
+ STR_PUSH(&p->io, "*" VT_SGR0);
+ }
+
+ fm_draw_nav_end(p);
+}
+
+static inline void
+fm_draw_msg(struct fm *p, const char *s, usize l)
+{
+ p->f |= FM_MSG|FM_REDRAW_NAV;
+ rl_clear(&p->r);
+ str_push(&p->r.cl, s, l);
+}
+
+static inline void
+fm_draw_err(struct fm *p, const char *s, usize l, int e)
+{
+ p->f |= FM_MSG_ERR|FM_REDRAW_NAV;
+ rl_clear(&p->r);
+ STR_PUSH(&p->r.cl, " error: ");
+ str_push(&p->r.cl, s, l);
+ if (!e) return;
+ STR_PUSH(&p->r.cl, ": ");
+ str_push_s(&p->r.cl, strerror(e));
+}
+
+static inline void
+fm_draw_cmd(struct fm *p)
+{
+ vt_cup(&p->io, 0, p->row + DFM_MARGIN);
+ rl_write_visible(&p->r, &p->io);
+ STR_PUSH(&p->io, VT_EL0);
+}
+
+static inline void
+fm_draw_buf(struct fm *p, cut c)
+{
+ fm_draw_nav_begin(p, c);
+ str_push(&p->io, p->r.cl.m, p->r.cl.l);
+ fm_draw_nav_end(p);
+}
+
+static inline void
+fm_draw_nav(struct fm *p)
+{
+ if (p->f & (FM_MSG|FM_MSG_ERR)) {
+ fm_draw_buf(p, p->f & FM_MSG ? CUT(DFM_COL_NAV_MSG) : CUT(DFM_COL_NAV_ERR));
+ rl_clear(&p->r);
+ p->f &= ~(FM_MSG|FM_MSG_ERR);
+ } else
+ fm_draw_inf(p);
+}
+
+// }}}
+
+// Cursor {{{
+
+static inline void
+fm_cursor_set(struct fm *p, usize y, usize o)
+{
+ if (!p->vl || !p->row) {
+ p->y = 0;
+ p->o = 0;
+ p->c = ent_next(p, 0);
+ return;
+ }
+ if (y >= p->vl) y = p->vl - 1;
+ if (o >= p->row) o = p->row - 1;
+ if (o > y) o = y;
+ p->y = y;
+ p->o = o;
+ p->c = fm_visible_select(p, y);
+}
+
+static inline void
+fm_scroll_to(struct fm *p, cut d)
+{
+ if (!p->vl) goto e;
+ u16 i;
+ fm_dir_ht_find(p, d, &i);
+ if (i == 0xFFFF || !ent_v_geto(p, i, VIS))
+ goto e;
+ usize r = fm_filter_pct_rank(p, i);
+ usize ms = p->vl > p->row ? p->vl - p->row : 0;
+ usize h = p->row >> 1;
+ usize s = r <= p->row - 2 ? 0 : r >= ms ? ms : r > h ? r - h : 0;
+ if (s > ms) s = ms;
+ fm_cursor_set(p, r, r - s);
+ return;
+e:
+ fm_cursor_set(p, 0, 0);
+}
+
+static inline size
+fm_scroll_to_rank(struct fm *p, usize r)
+{
+ size dy = (size)r - (size)p->y;
+ if (!dy || !p->vl) return 0;
+ if (dy > (size)p->row || dy < -(size)p->row) {
+ size h = (size)p->row >> 1;
+ size j = (size)r - (dy > 0 ? h : -h);
+ if (j < 0) j = 0;
+ if (j >= (size)p->vl) j = (size)p->vl - 1;
+ fm_cursor_set(p, (usize)j, 0);
+ p->f |= FM_REDRAW_DIR|FM_REDRAW_NAV;
+ dy = (size)r - (size)p->y;
+ }
+ return dy;
+}
+
+static inline void
+fm_cursor_sync(struct fm *p)
+{
+ if (!p->vl || !p->row) {
+ p->y = 0;
+ p->o = 0;
+ p->c = SIZE_MAX;
+ return;
+ }
+ if (p->y >= p->vl) p->y = p->vl - 1;
+ if (p->o >= p->row) p->o = p->row - 1;
+ if (p->o > p->y) p->o = p->y;
+ p->c = fm_visible_select(p, p->y);
+}
+
+// }}}
+
+// Terminal {{{
+
+static inline int
+fm_term_resize(struct fm *p)
+{
+ if (term_size_update(&p->t, &p->row, &p->col) < 0)
+ return -1;
+ p->row = p->row > DFM_MARGIN ? p->row - DFM_MARGIN : 1;
+ rl_vw_set(&p->r, p->col);
+ vt_decstbm(&p->io, 1, p->row);
+ fm_cursor_set(p, p->y, p->o);
+ p->f |= FM_REDRAW;
+ return 0;
+}
+
+static inline int
+fm_term_raw(struct fm *p)
+{
+ STR_PUSH(&p->io,
+ VT_ALT_SCREEN_Y VT_DECTCEM_N VT_DECAWM_N VT_BPASTE_ON VT_ED2 VT_CUP1);
+ return term_raw(&p->t) < 0 ? -1 : fm_term_resize(p);
+}
+
+static inline int
+fm_term_cooked(struct fm *p)
+{
+ vt_decstbm(&p->io, 1, p->row + DFM_MARGIN);
+ STR_PUSH(&p->io,
+ VT_SGR0 VT_BPASTE_OFF VT_DECAWM_Y VT_DECTCEM_Y VT_ALT_SCREEN_N);
+ fm_draw_flush(p);
+ return term_cooked(&p->t);
+}
+
+static inline int
+fm_term_init(struct fm *p)
+{
+ int r = term_init(&p->t);
+ return fm_term_raw(p) < 0 ? -1 : r < 0 ? -1 : fm_term_resize(p);
+}
+
+static inline int
+fm_term_free(struct fm *p)
+{
+ int r = fm_term_cooked(p);
+ term_destroy(&p->t);
+ return r;
+}
+
+// }}}
+
+// Entry Marking {{{
+
+static inline void *
+fm_mark_slot(struct fm *p, usize i)
+{
+ return p->d.d + i * sizeof(char *);
+}
+
+static inline char *
+fm_mark_load(struct fm *p, usize i)
+{
+ char *v;
+ memcpy(&v, fm_mark_slot(p, p->mp + i), sizeof(v));
+ return v;
+}
+
+static inline void
+fm_mark_store(struct fm *p, usize i, char *v)
+{
+ memcpy(fm_mark_slot(p, p->mp + i), &v, sizeof(v));
+}
+
+static inline int
+fm_mark_has_room(const struct fm *p)
+{
+ return p->mp * sizeof(char *) >
+ (p->dl + DFM_MARK_CMD_PRE) * sizeof(u32) + sizeof(char *);
+}
+
+static inline u8
+fm_mark_len(const char *p)
+{
+ return (u8)p[-1];
+}
+
+static inline cut
+fm_mark_at(struct fm *p, usize i)
+{
+ char *m = fm_mark_load(p, i);
+ return (cut) { m, fm_mark_len(m) };
+}
+
+static inline void
+fm_mark_terminate(struct fm *p)
+{
+ fm_mark_store(p, p->ml, NULL);
+}
+
+static inline void
+fm_mark_write_at(struct fm *p, usize i, char *v)
+{
+ fm_mark_store(p, i, v);
+}
+
+static inline void
+fm_mark_write_newest(struct fm *p, char *v)
+{
+ p->mp--;
+ p->ml++;
+ fm_mark_store(p, 0, v);
+ fm_mark_terminate(p);
+}
+
+static inline void
+fm_mark_clear_ptr(struct fm *p)
+{
+#define DIR_PTR_CAP (DFM_DIR_MAX / (sizeof(char *) / sizeof(u32)))
+ p->mp = DIR_PTR_CAP - DFM_MARK_CMD_PRE - DFM_MARK_CMD_POST;
+}
+
+static inline void
+fm_mark_clear_range(struct fm *p, usize lo, usize hi)
+{
+ if (hi <= lo) return;
+ usize b0 = lo >> 6;
+ usize b1 = (hi - 1) >> 6;
+ for (usize b = b0; b <= b1; b++) {
+ u64 m = ~0ULL;
+ if (b == b0) {
+ u64 m0 = (lo & 63) ? ((1ULL << (lo & 63)) - 1ULL) : 0ULL;
+ m &= ~m0;
+ }
+ if (b == b1) {
+ u64 end = ((hi - 1) & 63);
+ u64 m1 = (end == 63) ? ~0ULL : ((1ULL << (end + 1)) - 1ULL);
+ m &= m1;
+ }
+ u64 tc = p->vm[b] & p->v[b] & m;
+ if (!tc) continue;
+ u64 w = tc;
+ while (w) {
+ usize i = (b << 6) + u64_ctz(w);
+ w &= w - 1;
+ if (i >= p->dl) break;
+ u32 x = ent_v_load(p, i);
+ ent_v_set(&x, MARK, 0);
+ ent_v_store(p, i, x);
+ }
+ p->vm[b] &= ~m;
+ p->vml -= u64_popcount(tc);
+ }
+}
+
+static inline void
+fm_mark_clear_all(struct fm *p)
+{
+ p->ml = 0;
+ p->vml = 0;
+ memset(p->vm, 0, sizeof(p->vm));
+ fm_mark_clear_ptr(p);
+ fm_mark_terminate(p);
+ for (usize i = 0; i < p->dl; i++) {
+ u32 e = ent_v_load(p, i);
+ ent_v_set(&e, MARK, 0);
+ ent_v_store(p, i, e);
+ }
+ p->dec = sizeof(p->de);
+}
+
+static inline int
+fm_mark_push(struct fm *p, cut c)
+{
+ usize n = c.l + 4;
+ if (unlikely(!fm_mark_has_room(p) || p->dec < p->del + n))
+ return 0;
+ p->dec -= n;
+ char *b = p->de + p->dec;
+ u16 h = (u16) hash_fnv1a32(c.d, c.l);
+ b[0] = (unsigned char)(h & 0xff);
+ b[1] = (unsigned char)(h >> 8);
+ b[2] = (unsigned char)c.l;
+ memcpy(b + 3, c.d, c.l);
+ b[c.l + 3] = 0;
+ fm_mark_write_newest(p, b + 3);
+ p->f |= FM_MARK_PWD;
+ return 1;
+}
+
+static inline void
+fm_mark_drop_idx(struct fm *p, usize i)
+{
+ if (!p->ml) return;
+ if (i != p->ml - 1)
+ fm_mark_write_at(p, i, fm_mark_load(p, p->ml - 1));
+ p->ml--;
+ fm_mark_terminate(p);
+}
+
+static inline usize
+fm_mark_find(struct fm *p, usize c, int d)
+{
+ usize n = SIZE_MAX;
+ usize nw = BITSET_W(p->dl);
+ for (usize b = 0; b < nw; b++) {
+ for (u64 w = p->vm[b] & p->v[b]; w; ) {
+ usize j = (b << 6) + u64_ctz(w);
+ w &= w - 1;
+ if (d > 0) {
+ if (j > c && (n == SIZE_MAX || j < n))
+ n = j;
+ } else {
+ if (j < c && (n == SIZE_MAX || j > n))
+ n = j;
+ }
+ }
+ }
+ return n;
+}
+
+static inline void
+fm_mark_apply_bitset(struct fm *p, const u64 *s)
+{
+ usize nw = BITSET_W(p->dl);
+ for (usize b = 0; b < nw; b++) {
+ u64 w = s[b];
+ while (w) {
+ usize i = (b << 6) + u64_ctz(w);
+ w &= w - 1;
+ if (i >= p->dl) break;
+ u32 x = ent_v_load(p, i);
+ ent_v_set(&x, MARK, 1);
+ ent_v_store(p, i, x);
+ }
+ }
+}
+
+static inline void
+fm_mark_invalidate(struct fm *p)
+{
+ p->ml = 0;
+ fm_mark_clear_ptr(p);
+ fm_mark_terminate(p);
+ p->dec = sizeof(p->de);
+}
+
+static inline usize
+fm_mark_materialize_range(struct fm *p, usize *x)
+{
+ if (!p->vml) return 0;
+ if (!p->mpwd.l) return 0;
+ usize n = 0;
+ usize i = *x;
+ usize nw = BITSET_W(p->dl);
+ for (usize b = i >> 6; b < nw; b++) {
+ u64 w = p->vm[b] & p->v[b];
+ if (b == (i >> 6))
+ w &= ~((1ULL << (i & 63)) - 1ULL);
+ while (w) {
+ usize bit = (b << 6) + u64_ctz(w);
+ w &= w - 1;
+ if (bit >= p->dl) continue;
+ cut c = fm_ent(p, bit);
+ usize cl = c.l + 4;
+ if (!fm_mark_has_room(p) || p->dec < p->del + cl) {
+ *x = n ? bit : i;
+ return n;
+ }
+ if (!fm_mark_push(p, c)) {
+ *x = n ? bit : i;
+ return n;
+ }
+ n++;
+ *x = bit + 1;
+ }
+ }
+ *x = p->dl;
+ return n;
+}
+
+static inline int
+fm_mark_materialize(struct fm *p)
+{
+ if (!p->vml) return 0;
+ if (p->ml) return 0;
+ if (!p->mpwd.l) return 0;
+ if (!str_cmp(&p->mpwd, &p->pwd)) return 0;
+ fm_mark_invalidate(p);
+ usize oml = p->ml;
+ usize omp = p->mp;
+ usize odec = p->dec;
+ usize i = 0;
+ usize n = fm_mark_materialize_range(p, &i);
+ if (n != p->vml) {
+ p->ml = oml;
+ p->mp = omp;
+ p->dec = odec;
+ return -1;
+ }
+ return 0;
+}
+
+static inline void
+fm_mark_clear_idx(struct fm *p, usize i)
+{
+ if (!ent_v_geto(p, i, MARK))
+ return;
+ u32 x = ent_v_load(p, i);
+ ent_v_set(&x, MARK, 0);
+ ent_v_store(p, i, x);
+ usize b = i >> 6;
+ u64 bit = 1ULL << (i & 63);
+ p->vm[b] &= ~bit;
+ p->vml--;
+}
+
+static inline void
+fm_mark_pop_first(struct fm *p)
+{
+ if (!p->ml) return;
+ cut m = fm_mark_at(p, 0);
+ u16 j;
+ fm_dir_ht_find(p, m, &j);
+ if (j != 0xFFFF)
+ fm_mark_clear_idx(p, j);
+ fm_mark_drop_idx(p, 0);
+}
+
+static inline void
+fm_mark_clear(struct fm *p)
+{
+ fm_mark_clear_all(p);
+ p->mpwd.l = 0;
+ p->f &= ~FM_MARK_PWD;
+}
+
+static inline void
+fm_mark_init(struct fm *p)
+{
+ p->mpwd.l = 0;
+ str_push(&p->mpwd, p->pwd.m, p->pwd.l);
+ str_terminate(&p->mpwd);
+ p->f |= FM_MARK_PWD;
+}
+
+static inline u32
+fm_mark_toggle_idx(struct fm *p, usize i)
+{
+ u8 s = ent_v_geto(p, i, MARK);
+ u32 x = ent_v_load(p, i);
+ ent_v_set(&x, MARK, !s);
+ ent_v_store(p, i, x);
+ usize b = i >> 6;
+ u64 bit = 1ULL << (i & 63);
+ if (s) {
+ p->vm[b] &= ~bit;
+ p->vml--;
+ } else {
+ p->vm[b] |= bit;
+ p->vml++;
+ }
+ if (p->ml) fm_mark_invalidate(p);
+ return 1;
+}
+
+// }}}
+
+// Filesystem {{{
+
+static inline int
+fm_dir_has_room(const struct fm *p, usize e)
+{
+ return (p->dl + e) * sizeof(u32) <=
+ p->mp * sizeof(char *) - DFM_MARK_CMD_PRE * sizeof(char *);
+}
+
+static inline void
+fm_dir_rebuild_loc(struct fm *p)
+{
+ for (usize i = 0; i < p->dl; i++) {
+ u64 m = ent_load(p, i);
+ ent_set(&m, LOC, (u16)i);
+ ent_store(p, i, m);
+ }
+}
+
+static inline void
+fm_dir_sort(struct fm *p)
+{
+ if (likely(!(p->f & FM_TRUNC))) {
+ fm_ent_qsort(p, fm_sort_fn(p->ds), 0, p->dl, 32);
+ fm_dir_rebuild_loc(p);
+ }
+ fm_filter f = rl_empty(&p->r) ? fm_filter_hidden : p->sf;
+ fm_filter_apply(p, f, rl_cl_get(&p->r), rl_cr_get(&p->r));
+ fm_cursor_set(p, p->y, p->o);
+}
+
+static inline void
+fm_dir_mark_rebuild(struct fm *p)
+{
+ if (!p->ml || !(p->f & FM_MARK_PWD))
+ return;
+ memset(p->vm, 0, sizeof(p->vm));
+ p->vml = 0;
+ for (usize i = 0; i < p->dl; i++) {
+ u32 x = ent_v_load(p, i);
+ ent_v_set(&x, MARK, 0);
+ ent_v_store(p, i, x);
+ }
+ for (usize i = 0; i < p->ml; i++) {
+ cut m = fm_mark_at(p, i);
+ u16 j;
+ fm_dir_ht_find(p, m, &j);
+ if (j != 0xFFFF) {
+ u32 x = ent_v_load(p, j);
+ ent_v_set(&x, MARK, 1);
+ ent_v_store(p, j, x);
+ p->vm[j >> 6] |= 1ULL << (j & 63);
+ p->vml++;
+ u64 md = ent_load(p, j);
+ ent_store(p, j, md);
+ }
+ }
+}
+
+static inline void
+fm_dir_clear(struct fm *p)
+{
+ p->y = 0;
+ p->o = 0;
+ p->c = 0;
+ p->f &= ~FM_TRUNC;
+ rl_clear(&p->r);
+ p->del = 0;
+ p->dl = 0;
+ p->du = 0;
+ p->st = 0;
+ fm_dir_ht_clear(p);
+}
+
+static inline int
+fm_dir_load_ent(struct fm *p, const char *s)
+{
+ if (s[0] == '.' && (s[1] == '\0' || (s[1] == '.' && s[2] == '\0')))
+ return 0;
+ if (unlikely(!fm_dir_has_room(p, 1)))
+ return -1;
+
+ u8 utf8;
+ u8 wide;
+ u8 l = (u8)ent_name_len(s, &utf8, &wide);
+
+ if (unlikely(p->del + sizeof(u64) + l + 1 >= p->dec))
+ return -1;
+
+ u64 m = 0;
+ u32 o = p->del;
+ u32 x = 0;
+ ent_v_set(&x, OFF, o + sizeof(m));
+ ent_v_set(&x, CHAR, s[0]);
+ ent_v_set(&x, DOT, s[0] == '.');
+ ent_v_store(p, p->dl, x);
+ ent_set(&m, LEN, l);
+ ent_set(&m, LOC, (u16)p->dl);
+ ent_set(&m, UTF8, utf8);
+ ent_set(&m, WIDE, wide);
+
+ memcpy(p->de + p->del + sizeof(m), s, l + 1);
+ p->del += sizeof(m) + l + 1;
+ p->dl++;
+
+ struct stat st;
+ if (unlikely(fstatat(p->dfd, s, &st, AT_SYMLINK_NOFOLLOW) == -1)) {
+ ent_set(&m, TYPE, ENT_UNKNOWN);
+ ent_set(&m, SIZE, 0);
+ ent_set(&m, TIME, 0);
+ ent_set(&m, PERM, 0);
+ goto t;
+ }
+
+ if (S_ISLNK(st.st_mode)) {
+ struct stat ts;
+ if (fstatat(p->dfd, s, &ts, 0) == -1)
+ ent_map_stat(&m, &st, ENT_LNK_BRK);
+ else
+ ent_map_stat(&m, &ts, S_ISDIR(ts.st_mode) ? ENT_LNK_DIR : ENT_LNK);
+
+ usize ll = (usize)st.st_size;
+ if (p->del + ll + 2 < p->dec) {
+ char *lm = p->de + p->del + 1;
+ ssize_t r = readlinkat(p->dfd, s, lm, st.st_size);
+ if (likely(r != -1)) {
+ u8 lu;
+ u8 lw;
+ ent_name_len(lm, &lu, &lw);
+ u8 f = 0;
+ lnk_set(&f, UTF8, lu);
+ lnk_set(&f, WIDE, lw);
+ lm[-1] = f;
+ lm[ll] = 0;
+ p->del += ll + 2;
+ } else {
+ ent_set(&m, SIZE, 0);
+ goto t;
+ }
+ }
+ goto w;
+ }
+
+ ent_map_stat(&m, &st, ENT_TYPE_MAX);
+w:
+ ent_map_stat_size(&m, &st);
+ u64 sz = ent_size_bytes(ent_get(m, SIZE), ent_get(m, TYPE));
+ p->du = ent_size_add(p->du, sz);
+t:
+ fm_dir_ht_insert(p, (cut){ s, l }, (u16)(p->dl - 1), &m);
+ memcpy(p->de + o, &m, sizeof(m));
+ return 0;
+}
+
+static inline int
+fm_dir_load(struct fm *p)
+{
+ int d = openat(p->dfd, ".", O_RDONLY|O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW);
+ if (d < 0) return 0;
+ DIR *n = fdopendir(d);
+ if (unlikely(!n)) { close(d); return 0; }
+ fm_dir_clear(p);
+
+ for (struct dirent *e; (e = readdir(n)); )
+ if (fm_dir_load_ent(p, e->d_name) == -1) {
+ p->f |= FM_TRUNC;
+ break;
+ }
+
+ closedir(n);
+ fm_dir_sort(p);
+ fm_dir_mark_rebuild(p);
+ fs_watch(&p->p, ".");
+ return 1;
+}
+
+static inline int
+fm_dir_add(struct fm *p, cut c)
+{
+ if (fm_dir_exists(p, c))
+ return 0;
+ if (fm_dir_load_ent(p, c.d) == -1)
+ return -1;
+ int h = !(p->f & FM_HIDDEN) && *c.d == '.';
+ fm_v_assign(p, p->dl - 1, !h);
+ p->f |= FM_DIRTY;
+ p->st = ent_v_geto(p, p->dl - 1, OFF);
+ return 0;
+}
+
+static inline int
+fm_dir_del(struct fm *p, cut c)
+{
+ u16 f;
+ usize s = fm_dir_ht_find(p, c, &f);
+ if (f == 0xFFFF) return -1;
+
+ u64 m = ent_load(p, f);
+ u64 sz = ent_size_bytes(ent_get(m, SIZE), ent_get(m, TYPE));
+ p->du = ent_size_sub(p->du, sz);
+
+ u32 x = ent_v_load(p, f);
+ ent_v_set(&x, TOMB, 1);
+ ent_v_set(&x, MARK, 0);
+ ent_v_store(p, f, x);
+
+ fm_dir_ht_remove(p, s);
+ p->f |= FM_DIRTY;
+ return 0;
+}
+
+static inline void
+fm_dir_refresh(struct fm *p)
+{
+ cut o = p->c == SIZE_MAX ? CUT_NULL : fm_ent(p, p->c);
+ fm_dir_load(p);
+ fm_scroll_to(p, o);
+ fm_cursor_sync(p);
+ p->f |= FM_DIRTY;
+}
+
+// }}}
+
+// Core {{{
+
+static inline int
+fm_path_change(struct fm *p)
+{
+ fm_filter_clear(p);
+ if (fm_mark_materialize(p) < 0) {
+ fm_draw_err(p,
+ S("Not enough memory to materialize marks, unmark to cd"), 0);
+ return 0;
+ }
+ return 1;
+}
+
+static inline int
+fm_path_open(struct fm *p)
+{
+ int fd = open(p->pwd.m, O_DIRECTORY|O_CLOEXEC);
+ if (fd == -1) return 0;
+ if (p->dfd != AT_FDCWD) close(p->dfd);
+ p->dfd = fd;
+ p->f ^= (-(p->mpwd.l && str_cmp(&p->mpwd, &p->pwd)) ^ p->f) & FM_MARK_PWD;
+ return fchdir(fd) != -1;
+}
+
+static inline void
+fm_path_save(struct fm *p)
+{
+ p->ppwd.l = 0;
+ str_push(&p->ppwd, p->pwd.m, p->pwd.l);
+}
+
+static inline void
+fm_path_load(struct fm *p)
+{
+ p->pwd.l = 0;
+ str_push(&p->pwd, p->ppwd.m, p->ppwd.l);
+}
+
+static inline int
+fm_path_cd(struct fm *p, const char *d, usize l)
+{
+ if (!fm_path_change(p)) return 0;
+ fm_path_save(p);
+ p->pwd.l = 0;
+ str_push(&p->pwd, d, l);
+ str_terminate(&p->pwd);
+ usize nl = fm_path_resolve(p->pwd.m, p->pwd.l);
+ p->pwd.l = nl;
+ int r = fm_path_open(p);
+ if (!r) {
+ fm_path_load(p);
+ fm_draw_err(p, S("cd"), errno);
+ }
+ return r && fm_dir_load(p);
+}
+
+static inline int
+fm_path_chdir(struct fm *p, const char *d)
+{
+ if (!fm_path_change(p)) return 0;
+ fm_path_save(p);
+ p->pwd.l = 0;
+ str_push(&p->pwd, d, strlen(d));
+ str_terminate(&p->pwd);
+ int r = fm_path_open(p);
+ if (!r || !fm_dir_load(p)) {
+ fm_path_load(p);
+ fm_draw_err(p, S("cd"), errno);
+ return 0;
+ }
+ fm_path_save(p);
+ if (!getcwd(p->pwd.m, p->pwd.c)) {
+ fm_path_load(p);
+ fm_draw_err(p, S("cd"), errno);
+ return 0;
+ }
+ p->pwd.l = strlen(p->pwd.m);
+ str_terminate(&p->pwd);
+ return 1;
+}
+
+static inline int
+fm_path_cd_relative(struct fm *p, const char *d, u8 l)
+{
+ if (!fm_path_change(p)) return 0;
+ fm_path_save(p);
+ if (p->pwd.l > 1) str_push_c(&p->pwd, '/');
+ str_push(&p->pwd, d, l);
+ str_terminate(&p->pwd);
+ usize nl = fm_path_resolve(p->pwd.m, p->pwd.l);
+ p->pwd.l = nl;
+ int r = fm_path_open(p);
+ if (!r) {
+ fm_path_load(p);
+ fm_draw_err(p, S("cd"), errno);
+ }
+ return r && fm_dir_load(p);
+}
+
+static inline cut
+fm_path_cd_up(struct fm *p)
+{
+ if (!fm_path_change(p)) return CUT_NULL;
+ fm_path_save(p);
+ usize l = p->pwd.l;
+ usize i = l;
+ for (; i > 1 && p->pwd.m[i - 1] != '/'; i--);
+ usize n = (i > 1) ? i - 1 : 1;
+ char s = p->pwd.m[n];
+ p->pwd.m[n] = 0;
+ p->pwd.l = n;
+ int r = fm_path_open(p);
+ if (!r) {
+ p->pwd.m[n] = s;
+ p->pwd.l = l;
+ fm_draw_err(p, S("cd"), errno);
+ }
+ return r && fm_dir_load(p) ? (cut){ p->ppwd.m + i, l - i } : CUT_NULL;
+}
+
+static inline int
+fm_exec(struct fm *p, int in, const char *d, const char *const a[], bool bg, bool tf)
+{
+ if (tf) fm_term_cooked(p);
+ int r = run_cmd(bg ? p->t.null : p->t.fd, in, d, a, bg);
+ if (tf) fm_term_raw(p);
+ if (r == -1) {
+ fm_draw_err(p, S("exec"), errno);
+ return -1;
+ }
+ if (WIFEXITED(r)) {
+ int ec = WEXITSTATUS(r);
+ if (ec == 127) {
+ fm_draw_err(p, S("exec: command not found"), 0);
+ return -1;
+ } else if (ec) {
+ fm_draw_err(p, S("exec: exited non-zero"), 0);
+ return -1;
+ }
+ }
+ if (WIFSIGNALED(r)) {
+ fm_draw_err(p, S("exec: killed by signal"), 0);
+ return -1;
+ }
+ return 0;
+}
+
+static inline void
+fm_open(struct fm *p)
+{
+ if (p->c == SIZE_MAX) return;
+ cut c = fm_ent(p, p->c);
+ if (!c.l) return;
+ u64 m = ent_load(p, p->c);
+ if (ENT_IS_DIR(ent_get(m, TYPE)))
+ fm_path_cd_relative(p, c.d, c.l);
+ else if (unlikely(p->f & FM_PICKER)) {
+ str_push_c(&p->pwd, '/');
+ str_push(&p->pwd, c.d, c.l);
+ term_set_dead(&p->t, 1);
+ } else {
+ const char *const a[] = { p->opener.d, c.d, NULL };
+ fm_exec(p, -1, NULL, a, 0, 1);
+ }
+}
+
+// }}}
+
+// Command {{{
+
+struct fm_cmd {
+ cut prompt;
+ cut left;
+ cut right;
+ fm_key_press press;
+ fm_key_enter enter;
+ u32 config;
+};
+
+#define FM_CMD(C, ...) \
+static inline void \
+C(struct fm *p) \
+{ \
+ fm_cmd(p, &(struct fm_cmd){__VA_ARGS__}); \
+}
+
+enum {
+ CMD_BG = 1 << 0,
+ CMD_CONFLICT = 1 << 1,
+ CMD_MUT = 1 << 2,
+ CMD_EXEC = 1 << 3,
+ CMD_MARK_DIR = 1 << 4,
+ CMD_NOT_MARK_DIR = 1 << 5,
+ CMD_STDIN = 1 << 6,
+ CMD_FILE_CURSOR = 1 << 7,
+ CMD_EXEC_MARK = 1 << 8,
+ CMD_EXEC_ROOT = 1 << 9,
+
+ CMD_MODE_EACH = 0,
+ CMD_MODE_VIRTUAL,
+ CMD_MODE_CHUNK,
+ CMD_MODE_BULK,
+ CMD_MODE_SINGLE,
+};
+
+static inline void
+fm_cmd_exec(struct fm *p)
+{
+ if (p->kd && p->kd(p, &p->r.cl) >= 0)
+ rl_clear(&p->r);
+ p->r.vx = 0;
+ p->r.pr.l = 0;
+ p->kp = 0;
+ p->kd = 0;
+}
+
+static inline void
+fm_cmd(struct fm *p, struct fm_cmd *c)
+{
+ if (!c->press && !c->enter) {
+ fm_draw_err(p, S("no callbacks defined"), 0);
+ return;
+ }
+ rl_clear(&p->r);
+ rl_pr_set(&p->r, c->prompt);
+ if (c->left.l) {
+ str_push(&p->r.cl, c->left.d, c->left.l);
+ str_terminate(&p->r.cl);
+ }
+ if (c->right.l) rl_cr_set(&p->r, c->right);
+ if (c->config & CMD_FILE_CURSOR) {
+ if (p->c == SIZE_MAX) return;
+ cut e = fm_ent(p, p->c);
+ str_push(&p->r.cl, e.d, e.l);
+ }
+ rl_cl_sync(&p->r);
+ p->cf = c->config;
+ p->kp = c->press;
+ p->kd = c->enter;
+ p->f |= FM_REDRAW_CMD;
+ if (p->f & FM_ROOT && !(p->cf & CMD_EXEC_ROOT))
+ return;
+ if ((p->cf & CMD_EXEC_MARK && p->vml) || p->cf & CMD_EXEC)
+ fm_cmd_exec(p);
+}
+
+static inline void
+fm_cmd_search_press(struct fm *p, int k, cut cl, cut cr)
+{
+ if (cl.l > 1 && k != KEY_BACKSPACE && p->vl != p->dl && !cr.l) {
+ fm_filter_apply_inc(p, p->sf, cl, cr);
+ } else
+ fm_filter_apply(p, p->sf, cl, cr);
+ fm_filter_save(p, cl, cr);
+ fm_cursor_set(p, 0, 0);
+}
+
+static inline int
+fm_cmd_search(struct fm *p, str *s)
+{
+ if (p->vl == 1) fm_open(p);
+ else {
+ if (s->l) {
+ cut q = (cut){ s->m, s->l };
+ fm_filter_apply(p, p->sf, q, CUT_NULL);
+ fm_filter_save(p, q, CUT_NULL);
+ }
+ else
+ fm_filter_apply(p, fm_filter_hidden, (cut){ s->m, s->l }, CUT_NULL);
+ fm_cursor_set(p, 0, 0);
+ }
+ return -1;
+}
+
+static inline int
+fm_cmd_cd(struct fm *p, str *s)
+{
+ int r = 0;
+ if (s->m[0] == '/')
+ r = fm_path_cd(p, s->m, s->l);
+ else
+ r = fm_path_cd_relative(p, s->m, s->l);
+ return r ? 0 : -1;
+}
+
+static u8
+fm_prompt_conflict(struct fm *p, cut d)
+{
+ fm_draw_nav_begin(p, CUT(DFM_COL_NAV_ERR));
+ STR_PUSH(&p->io, "conflict: '");
+ str_push(&p->io, d.d, d.l);
+ STR_PUSH(&p->io, "': try overwrite?");
+ STR_PUSH(&p->io, " [a]bort [y]es [Y]es all [n]o [N]o all");
+ fm_draw_nav_end(p);
+ fm_draw_flush(p);
+ for (;;) {
+ if (!term_key_read(p->t.fd, &p->k))
+ return 'a';
+ switch (p->k.b[0]) {
+ case 'a': case 'y': case 'Y': case 'n': case 'N':
+ return p->k.b[0];
+ }
+ }
+}
+
+static inline int
+fm_prepare_marks_conflict(struct fm *p)
+{
+ usize i = 0;
+ cut m = CUT_NULL;
+ int om = 0;
+ if (!p->ml) {
+ m = fm_ent(p, p->c);
+ goto c;
+ }
+ for (; i < p->ml; ) {
+ m = fm_mark_at(p, i);
+c:
+ if (!fm_dir_exists(p, m)) goto s;
+ if (om != 'Y' && om != 'N')
+ om = fm_prompt_conflict(p, m);
+ switch (om) {
+ case 'a': return -1;
+ case 'y': case 'Y': goto s;
+ case 'n': fm_mark_drop_idx(p, i); om = -2; continue;
+ case 'N': p->ml = 0; return -1;
+ }
+s:
+ i++;
+ }
+ return om;
+}
+
+static inline int
+fm_cmd_build_bulk_exec(struct fm *p, cut s, usize ti, usize tc, u32 f)
+{
+ usize omi = ti;
+ usize prn = ti;
+ usize pon = (tc - omi - 1);
+ usize tt = prn + p->ml + pon;
+ usize pri = p->mp - prn;
+ usize poi = p->mp + p->ml;
+ char **m = (char **)(void *)p->d.d;
+ cut c;
+ for (usize i = 0, n = 0; next_tok(s.d, s.l, &n, &c); i++) {
+ if (i == omi) {
+ if (p->ml) continue;
+ cut e = fm_ent(p, p->c);
+ c = e;
+ } else if (c.l == 2 && c.d[0] == '%' && c.d[1] == 'd')
+ c = (cut) { p->pwd.m, p->pwd.l };
+ else if (c.l > 1 && c.d[0] == '$') {
+ c.d = getenv(c.d + 1);
+ if (!c.d) return -2;
+ }
+ m[i < ti ? pri++ : poi++] = (char *)c.d;
+ }
+ char **a = &m[p->mp - prn];
+ a[tt] = NULL;
+ const char *wd = p->mpwd.m;
+ return fm_exec(p, -1, wd, (const char *const *)a, f & CMD_BG, !(f & CMD_BG));
+}
+
+static inline int
+fm_cmd_build_bulk_chunk(struct fm *p, cut s, usize ti, usize tc, u32 f)
+{
+ int r = 0;
+ assert(!p->ml);
+ usize b = 0;
+ while (b < p->dl && p->vml) {
+ fm_mark_invalidate(p);
+ usize pb = b;
+ usize n = fm_mark_materialize_range(p, &b);
+ if (!n) break;
+ r = fm_cmd_build_bulk_exec(p, s, ti, tc, f);
+ if (r < 0) return r;
+ fm_mark_clear_range(p, pb, b);
+ }
+ if (!p->vml) fm_mark_clear_all(p);
+ return r;
+}
+
+static inline int
+fm_cmd_build_bulk(struct fm *p, cut s, usize ti, usize tc, u32 f)
+{
+ if (fm_mark_materialize(p) < 0) {
+ fm_draw_err(p, S("Not enough memory to materialize marks"), 0);
+ return -1;
+ }
+ if (fm_cmd_build_bulk_exec(p, s, ti, tc, f) < 0)
+ return -1;
+ fm_mark_clear_all(p);
+ return 0;
+}
+
+static inline int
+fm_cmd_build(struct fm *p, cut s, usize ti, usize tc, u32 f, usize mp, cut mk, bool t)
+{
+ char **m = (char **)(void *)p->d.d;
+ usize pri = mp;
+ cut c;
+ for (usize j = 0, n = 0; next_tok(s.d, s.l, &n, &c); j++)
+ if (j == ti)
+ m[pri++] = (char *)mk.d;
+ else if (c.l == 2 && c.d[0] == '%' && c.d[1] == 'd')
+ m[pri++] = p->pwd.m;
+ else if (c.l > 1 && c.d[0] == '$') {
+ char *e = getenv(c.d + 1);
+ if (!e) return -2;
+ m[pri++] = e;
+ } else
+ m[pri++] = (char *)c.d;
+ u32 lf = f;
+ m[mp + tc] = NULL;
+ int fd = -1;
+ if (lf & CMD_STDIN) {
+ fd = open(mk.d, O_RDONLY|O_CLOEXEC);
+ if (fd < 0) return -1;
+ }
+ int r = fm_exec(p, fd, p->pwd.m, (const char *const *)&m[mp], lf & CMD_BG, t);
+ if (fd >= 0) close(fd);
+ return r;
+}
+
+static inline int
+fm_cmd_build_each_virtual(struct fm *p, cut s, usize ti, usize tc, u32 f)
+{
+ cut mk = CUT_NULL;
+ usize mp = p->mp - (tc + 1);
+ if (!p->vml) {
+ if (p->c == SIZE_MAX) return 0;
+ mk = fm_ent(p, p->c);
+ return fm_cmd_build(p, s, ti, tc, f, mp, mk, 1);
+ }
+ if (!(f & CMD_BG)) fm_term_cooked(p);
+ int r = 0;
+ usize nw = BITSET_W(p->dl);
+ for (usize b = 0; b < nw; b++) {
+ for (u64 w = p->vm[b] & p->v[b]; w; ) {
+ usize i = (b << 6) + u64_ctz(w);
+ w &= w - 1;
+ if (i >= p->dl) break;
+ mk = fm_ent(p, i);
+ if (fm_cmd_build(p, s, ti, tc, f, mp, mk, 0) < 0) {
+ r = -1;
+ goto e;
+ }
+ fm_mark_clear_idx(p, i);
+ if (!p->vml) goto e;
+ }
+ }
+e:
+ if (!(f & CMD_BG)) fm_term_raw(p);
+ return r;
+}
+
+static inline int
+fm_cmd_build_each(struct fm *p, cut s, usize ti, usize tc, u32 f)
+{
+ cut mk = CUT_NULL;
+ usize mp = p->mp - (tc + 1);
+ if (!p->vml) {
+ if (p->c == SIZE_MAX) return 0;
+ mk = fm_ent(p, p->c);
+ return fm_cmd_build(p, s, ti, tc, f, mp, mk, 1);
+ }
+ if (!(f & CMD_BG)) fm_term_cooked(p);
+ for (; p->ml; ) {
+ mk = fm_mark_at(p, 0);
+ str fp = p->mpwd;
+ str_push_c(&fp, '/');
+ str_push(&fp, mk.d, mk.l);
+ str_terminate(&fp);
+ mk = (cut) { fp.m, fp.l };
+ if (fm_cmd_build(p, s, ti, tc, f, mp, mk, 0) < 0) {
+ if (!(f & CMD_BG)) fm_term_raw(p);
+ return -1;
+ }
+ fm_mark_pop_first(p);
+ if (p->vml) p->vml--;
+ }
+ if (!(f & CMD_BG)) fm_term_raw(p);
+ return 0;
+}
+
+static inline int
+fm_cmd_sh(struct fm *p, cut c)
+{
+ if (!c.l) return 0;
+ cut f = !p->ml && p->c != SIZE_MAX ? fm_ent(p, p->c) : CUT_NULL;
+ cut sh = get_env("SHELL", "/bin/sh");
+ const char *cmd[] = { sh.d, DFM_SHELL_OPTS, c.d, CFG_NAME, f.d, NULL };
+ STATIC_ASSERT(ARR_SIZE(cmd) < DFM_MARK_CMD_PRE, "");
+ usize l = ARR_SIZE(cmd) - (!f.l * 2);
+ char **m = (char **)(void *)p->d.d;
+ char **a = &m[p->mp - l];
+ memcpy(a, cmd, sizeof(*cmd) * l);
+ if (!f.l) a[l + p->ml] = NULL;
+ int r =
+ fm_exec(p, -1, f.l ? p->pwd.m : p->mpwd.m, (const char *const *)a, 0, 1);
+ if (!f.l) fm_mark_clear_all(p);
+ if (r < 0) p->f |= FM_ERROR;
+ return r;
+}
+
+static inline int
+fm_cmd_run_sh(struct fm *p, str *s)
+{
+ if (fm_mark_materialize(p) < 0) {
+ fm_draw_err(p, S("Not enough memory to materialize marks"), 0);
+ return -1;
+ }
+ usize e = s->m[0] == '!';
+ return fm_cmd_sh(p, (cut){ s->m + e, s->l - e });
+}
+
+static inline cut
+fm_cmd_parse(struct fm *p, str *s, usize *oti, usize *ott, usize *otc)
+{
+ cut c;
+ usize tc = 0;
+ usize ti = SIZE_MAX;
+ usize tt = 0;
+ usize li = 0;
+ cut lt = CUT_NULL;
+ for (usize i = 0, n = 0; next_tok(s->m, s->l, &n, &c); i++, tc++) {
+ ((char *)c.d)[c.l] = 0;
+ lt = c;
+ li = i;
+ if (ti == SIZE_MAX && c.l == 2 && c.d[0] == '%') {
+ if (c.d[1] == 'm' || c.d[1] == 'f' ) {
+ ti = i;
+ tt = c.d[1];
+ }
+ }
+ }
+ if (tc && lt.l == 1 && lt.d[0] == '&') {
+ p->cf |= CMD_BG;
+ tc--;
+ if (ti != SIZE_MAX && li < ti) ti--;
+ }
+ *oti = ti;
+ *ott = tt;
+ *otc = tc;
+ c = (cut){ s->m, s->l };
+ switch (s->m[0]) {
+ case '<': c.d++; c.l--; p->cf |= CMD_STDIN; break;
+ }
+ return c;
+}
+
+static inline int
+fm_cmd_run(struct fm *p, str *s)
+{
+ int m = CMD_MODE_SINGLE;
+ int r = 0;
+ if (!s->l) return 0;
+ if (p->cf & CMD_MARK_DIR && !(p->f & FM_MARK_PWD) && p->vml) {
+ fm_draw_err(p, S("Not in mark directory"), 0);
+ return -1;
+ }
+ if (p->cf & CMD_NOT_MARK_DIR && p->f & FM_MARK_PWD) {
+ fm_draw_err(p, S("In mark directory"), 0);
+ return -1;
+ }
+ if (s->m[0] == '!')
+ return fm_cmd_run_sh(p, s);
+ usize ti;
+ usize tt;
+ usize tc;
+ cut a = fm_cmd_parse(p, s, &ti, &tt, &tc);
+ if ((tt && (!p->vml && !p->vl))) {
+ fm_draw_err(p, S("nothing to operate on"), 0);
+ return -1;
+ }
+ if (p->cf & (CMD_STDIN|CMD_FILE_CURSOR))
+ m = CMD_MODE_SINGLE;
+ else if (tt == 'm') {
+ m = p->f & FM_MARK_PWD ? CMD_MODE_CHUNK : CMD_MODE_BULK;
+ m = p->vml ? m : CMD_MODE_EACH;
+ } else if (tt == 'f')
+ m = p->f & FM_MARK_PWD ? CMD_MODE_VIRTUAL : CMD_MODE_EACH;
+ switch (m) {
+ case CMD_MODE_SINGLE:
+ case CMD_MODE_EACH:
+ case CMD_MODE_VIRTUAL:
+ if (tc > DFM_MARK_CMD_PRE) {
+ fm_draw_err(p, S("argv too large"), 0);
+ return -1;
+ }
+ break;
+ case CMD_MODE_BULK:
+ case CMD_MODE_CHUNK:
+ if (ti > DFM_MARK_CMD_PRE || tc - ti - 1 > DFM_MARK_CMD_POST) {
+ fm_draw_err(p, S("argv too large"), 0);
+ return -1;
+ }
+ }
+ switch (m) {
+ case CMD_MODE_EACH:
+ case CMD_MODE_BULK:
+ if (p->cf & CMD_CONFLICT) {
+ r = fm_prepare_marks_conflict(p);
+ if (r < 0) p->f |= FM_REDRAW_NAV;
+ if (r == -1) return 0;
+ if (r == -2 && !p->ml) return 0;
+ }
+ break;
+ }
+ cut mk = CUT_NULL;
+ switch (m) {
+ case CMD_MODE_SINGLE:
+ if (p->c != SIZE_MAX)
+ mk = fm_ent(p, p->c);
+ r = fm_cmd_build(p, a, ti, tc, p->cf, p->mp - (tc + 1), mk, 1);
+ break;
+ case CMD_MODE_EACH:
+ r = fm_cmd_build_each(p, a, ti, tc, p->cf);
+ break;
+ case CMD_MODE_VIRTUAL:
+ r = fm_cmd_build_each_virtual(p, a, ti, tc, p->cf);
+ break;
+ case CMD_MODE_BULK:
+ r = fm_cmd_build_bulk(p, a, ti, tc, p->cf);
+ break;
+ case CMD_MODE_CHUNK:
+ r = fm_cmd_build_bulk_chunk(p, a, ti, tc, p->cf);
+ break;
+ }
+#ifdef FS_WATCH
+ if (r != -1 && p->cf & CMD_MUT)
+ p->f |= FM_DIRTY_WITHIN;
+#else
+ if (r != -1 && (p->cf & (CMD_MUT)))
+ fm_dir_refresh(p);
+#endif
+ if (r == -2)
+ fm_draw_err(p, S("environment variable unset"), 0);
+ if (r < 0) p->f |= FM_ERROR;
+ return r;
+}
+
+// }}}
+
+// Action {{{
+
+static inline void
+act_quit(struct fm *p)
+{
+ term_set_dead(&p->t, 1);
+}
+
+// static inline void
+// act_crash(struct fm *p)
+// {
+// (void) p;
+// *(volatile int *)0 = 1;
+// }
+
+static inline void
+act_quit_print_pwd(struct fm *p)
+{
+ p->f |= FM_PRINT_PWD;
+ act_quit(p);
+}
+
+static inline void
+act_cd_home(struct fm *p)
+{
+ cut h = get_env("HOME", "");
+ if (!h.l) return;
+ fm_path_cd(p, h.d, h.l);
+}
+
+static inline void
+act_cd_mark_directory(struct fm *p)
+{
+ if (!p->vml) return;
+ fm_path_cd(p, p->mpwd.m, p->mpwd.l);
+}
+
+static inline void
+act_cd_last(struct fm *p)
+{
+ fm_path_cd(p, p->ppwd.m, p->ppwd.l);
+}
+
+static inline void
+act_copy_pwd(struct fm *p)
+{
+ int fd = fd_from_buf(p->pwd.m, p->pwd.l);
+ if (fd < 0)
+ fm_draw_err(p, S("PWD too large"), errno);
+ else {
+ const char *const a[] = { get_env("DFM_COPYER", DFM_COPYER).d, NULL };
+ fm_exec(p, fd, NULL, a, 1, 0);
+ close(fd);
+ fm_draw_msg(p, S("Copied PWD to clipboard"));
+ }
+}
+
+#define ACT_CD_BOOKMARK(N) \
+static inline void \
+act_cd_bookmark_##N(struct fm *p) \
+{ \
+ cut e = get_env("DFM_BOOKMARK_" #N, DFM_BOOKMARK_##N); \
+ if (e.l) fm_path_cd(p, e.d, e.l); \
+ else fm_draw_err(p, S("DFM_BOOKMARK_" #N " not set"), 0); \
+}
+ACT_CD_BOOKMARK(0) ACT_CD_BOOKMARK(1) ACT_CD_BOOKMARK(2) ACT_CD_BOOKMARK(3)
+ACT_CD_BOOKMARK(4) ACT_CD_BOOKMARK(5) ACT_CD_BOOKMARK(6) ACT_CD_BOOKMARK(7)
+ACT_CD_BOOKMARK(8) ACT_CD_BOOKMARK(9)
+
+static inline void
+act_cd_up(struct fm *p)
+{
+ if (p->f & FM_SEARCH) {
+ rl_clear(&p->r);
+ fm_filter_clear(p);
+ if (p->c == SIZE_MAX) {
+ fm_cursor_set(p, 0, 0);
+ return;
+ }
+ usize o = ent_next(p, 0);
+ if (o == SIZE_MAX) return;
+ cut c = fm_ent(p, o);
+ fm_scroll_to(p, c);
+ p->c = o;
+ return;
+ }
+
+ cut b = fm_path_cd_up(p);
+ if (!b.l) return;
+ fm_scroll_to(p, b);
+ fm_cursor_sync(p);
+}
+
+static inline void
+act_stat(struct fm *p)
+{
+ if (p->c == SIZE_MAX) return;
+ cut e = fm_ent(p, p->c);
+
+ struct stat st;
+ if (fstatat(p->dfd, e.d, &st, AT_SYMLINK_NOFOLLOW) == -1) {
+ fm_draw_err(p, S("stat"), errno);
+ return;
+ }
+
+ STR_PUSH(&p->io, VT_ED2 VT_CUP1);
+
+ STR_PUSH(&p->io, "Name: ");
+ str_push(&p->io, e.d, e.l);
+ STR_PUSH(&p->io, VT_CR VT_LF);
+
+ STR_PUSH(&p->io, "Type: ");
+ cut t = fm_file_type(st.st_mode);
+ str_push(&p->io, t.d, t.l);
+ STR_PUSH(&p->io, VT_CR VT_LF);
+
+ if (S_ISLNK(st.st_mode)) {
+ char b[PATH_MAX];
+ ssize_t r = readlinkat(p->dfd, e.d, b, sizeof(b) - 1);
+ if (r >= 0) {
+ b[r] = 0;
+ STR_PUSH(&p->io, "Target: ");
+ str_push_s(&p->io, b);
+ STR_PUSH(&p->io, VT_CR VT_LF);
+ }
+ }
+
+ STR_PUSH(&p->io, "Size: ");
+ str_push_u64(&p->io, (u64)st.st_size);
+ STR_PUSH(&p->io, VT_CR VT_LF);
+
+ STR_PUSH(&p->io, "Mode: 0");
+ str_push_u32_b(&p->io, st.st_mode & 07777, 8, 0, 0);
+ STR_PUSH(&p->io, ", ");
+ ent_perm_decode(&p->io, st.st_mode, 0);
+ STR_PUSH(&p->io, VT_CR VT_LF);
+
+ STR_PUSH(&p->io, "UID: ");
+ str_push_u32(&p->io, (u32)st.st_uid);
+ STR_PUSH(&p->io, VT_CR VT_LF);
+
+ STR_PUSH(&p->io, "GID: ");
+ str_push_u32(&p->io, (u32)st.st_gid);
+ STR_PUSH(&p->io, VT_CR VT_LF);
+
+ STR_PUSH(&p->io, "Links: ");
+ str_push_u64(&p->io, (u64)st.st_nlink);
+ STR_PUSH(&p->io, VT_CR VT_LF);
+
+ STR_PUSH(&p->io, "Blocks: ");
+ str_push_u64(&p->io, (u64)st.st_blocks);
+ STR_PUSH(&p->io, VT_CR VT_LF);
+
+ STR_PUSH(&p->io, "Inode: ");
+ str_push_u64(&p->io, (u64)st.st_ino);
+ STR_PUSH(&p->io, VT_CR VT_LF);
+
+ STR_PUSH(&p->io, "Device: ");
+ str_push_u64(&p->io, (u64)st.st_dev);
+ STR_PUSH(&p->io, VT_CR VT_LF);
+
+ STR_PUSH(&p->io, "Access: ");
+ str_push_time(&p->io, p->tz, &st.st_atim);
+ STR_PUSH(&p->io, VT_CR VT_LF);
+
+ STR_PUSH(&p->io, "Modify: ");
+ str_push_time(&p->io, p->tz, &st.st_mtim);
+ STR_PUSH(&p->io, VT_CR VT_LF);
+
+ STR_PUSH(&p->io, "Change: ");
+ str_push_time(&p->io, p->tz, &st.st_ctim);
+ STR_PUSH(&p->io, VT_CR VT_LF);
+
+ STR_PUSH(&p->io, VT_CR VT_LF "Press any key...");
+
+ fm_draw_flush(p);
+ term_key_read(p->t.fd, &p->k);
+ p->f |= FM_REDRAW;
+}
+
+static inline void
+act_open(struct fm *p)
+{
+ fm_open(p);
+}
+
+static inline void
+act_view_next(struct fm *p)
+{
+ switch (p->dv) {
+ default: p->dv = 's'; break;
+ case 's': p->dv = 'p'; break;
+ case 'p': p->dv = 't'; break;
+ case 't': p->dv = 'a'; break;
+ case 'a': p->dv = 'n'; break;
+ }
+ p->f |= FM_REDRAW_DIR|FM_REDRAW_NAV;
+}
+
+static inline void
+act_sort_next(struct fm *p)
+{
+ switch (p->ds) {
+ default: p->ds = 'N'; break;
+ case 'N': p->ds = 's'; break;
+ case 's': p->ds = 'S'; break;
+ case 'S': p->ds = 'd'; break;
+ case 'd': p->ds = 'D'; break;
+ case 'D': p->ds = 'e'; break;
+ case 'e': p->ds = 'n'; break;
+ }
+ fm_dir_sort(p);
+}
+
+static inline void
+act_redraw(struct fm *p)
+{
+ p->f |= FM_REDRAW;
+}
+
+static inline void
+act_refresh(struct fm *p)
+{
+ fm_dir_refresh(p);
+}
+
+static inline void
+act_scroll_top(struct fm *p)
+{
+ fm_cursor_set(p, 0, 0);
+ p->f |= FM_REDRAW_DIR|FM_REDRAW_NAV;
+}
+
+static inline void
+act_scroll_bottom(struct fm *p)
+{
+ fm_cursor_set(p, p->vl - !!p->vl, p->row -1);
+ p->f |= FM_REDRAW_DIR|FM_REDRAW_NAV;
+}
+
+static inline void
+act_page_down(struct fm *p)
+{
+ if (!p->vl) return;
+ usize ny = p->y + p->row;
+ if (ny >= p->vl) ny = p->vl - 1;
+ fm_cursor_set(p, ny, p->row - 1);
+ p->f |= FM_REDRAW_DIR|FM_REDRAW_NAV;
+}
+
+static inline void
+act_page_up(struct fm *p)
+{
+ if (!p->vl) return;
+ usize ny = (p->y > p->row) ? (p->y - p->row) : 0;
+ fm_cursor_set(p, ny, 0);
+ p->f |= FM_REDRAW_DIR|FM_REDRAW_NAV;
+}
+
+static inline void
+act_scroll_down(struct fm *p)
+{
+ if (unlikely(p->y + 1 >= p->vl)) return;
+ usize l = p->c;
+ p->y++;
+ p->o += p->o < p->row - 1;
+ usize n = ent_next(p, p->c + 1);
+ if (n == SIZE_MAX) return;
+ p->c = n;
+ fm_draw_ent(p, l);
+ STR_PUSH(&p->io, VT_LF);
+ fm_draw_ent(p, p->c);
+ p->f |= FM_REDRAW_NAV;
+}
+
+static inline void
+act_scroll_up(struct fm *p)
+{
+ if (unlikely(!p->y)) return;
+ usize l = p->c;
+ p->y--;
+ usize n = ent_prev(p, p->c - 1);
+ if (n == SIZE_MAX) return;
+ p->c = n;
+ fm_draw_ent(p, l);
+
+ if (!p->o) {
+ STR_PUSH(&p->io, VT_IL0);
+ } else {
+ p->o -= !!p->o;
+ STR_PUSH(&p->io, VT_CUU1);
+ }
+
+ fm_draw_ent(p, p->c);
+ p->f |= FM_REDRAW_NAV;
+}
+
+static inline void
+act_toggle_hidden(struct fm *p)
+{
+ if (p->c == SIZE_MAX) return;
+ cut c = fm_ent(p, p->c);
+ if (!c.l) return;
+ p->f ^= FM_HIDDEN;
+ fm_filter_clear(p);
+ fm_scroll_to(p, c);
+ fm_cursor_sync(p);
+}
+
+static inline void
+act_search_startswith(struct fm *p)
+{
+ p->sf = fm_filter_startswith;
+ fm_filter_clear(p);
+ p->f |= FM_SEARCH;
+ fm_cursor_set(p, 0, 0);
+ fm_cmd(p, &(struct fm_cmd){
+ .prompt = CUT("/"),
+ .press = fm_cmd_search_press,
+ .enter = fm_cmd_search,
+ });
+}
+
+static inline void
+act_search_substring(struct fm *p)
+{
+ p->sf = fm_filter_substr;
+ fm_filter_clear(p);
+ p->f |= FM_SEARCH;
+ fm_cursor_set(p, 0, 0);
+ fm_cmd(p, &(struct fm_cmd){
+ .prompt = CUT("/*"),
+ .press = fm_cmd_search_press,
+ .enter = fm_cmd_search,
+ });
+}
+
+static inline void
+act_shell(struct fm *p)
+{
+ cut sh = get_env("SHELL", "/bin/sh");
+ const char *const a[] = { sh.d, NULL };
+ fm_exec(p, -1, NULL, a, 0, 1);
+}
+
+static inline void
+act_alt_buffer(struct fm *p)
+{
+ STR_PUSH(&p->io, VT_ALT_SCREEN_N);
+ fm_draw_flush(p);
+ term_key_read(p->t.fd, &p->k);
+ STR_PUSH(&p->io, VT_ALT_SCREEN_Y);
+ fm_draw_flush(p);
+ p->f &= ~FM_ERROR;
+ p->f |= FM_REDRAW;
+}
+
+static inline void
+act_mark_toggle(struct fm *p)
+{
+ if (p->c == SIZE_MAX) return;
+ if (!(p->f & FM_MARK_PWD)) fm_mark_clear(p);
+ fm_mark_init(p);
+ if (!fm_mark_toggle_idx(p, p->c)) {
+ fm_draw_err(p, S("Not enough memory to mark"), 0);
+ return;
+ }
+ fm_mark_invalidate(p);
+ fm_draw_ent(p, p->c);
+ p->f |= FM_REDRAW_NAV;
+}
+
+static inline void
+act_mark_toggle_all(struct fm *p)
+{
+ usize i = ent_next(p, 0);
+ if (i == SIZE_MAX) return;
+ int pr = ent_v_geto(p, i, MARK);
+ fm_mark_clear(p);
+ if (pr) goto e;
+ fm_mark_init(p);
+ p->vml = 0;
+ usize nw = BITSET_W(p->dl);
+ for (usize b = 0; b < nw; b++) {
+ p->vm[b] = p->v[b];
+ p->vml += u64_popcount(p->vm[b]);
+ }
+ fm_mark_apply_bitset(p, p->vm);
+ p->ml = 0;
+e:
+ p->f |= FM_REDRAW_DIR|FM_REDRAW_NAV;
+}
+
+static inline void
+act_mark_clear(struct fm *p)
+{
+ fm_mark_clear(p);
+ p->f |= FM_REDRAW_DIR|FM_REDRAW_NAV;
+}
+
+static inline void
+act_mark_next(struct fm *p)
+{
+ if (!p->vml || p->c == SIZE_MAX) return;
+ usize b = fm_mark_find(p, p->c, 1);
+ if (b == SIZE_MAX) return;
+ usize r = fm_filter_pct_rank(p, b);
+ size y = fm_scroll_to_rank(p, r);
+ while (y-- > 0) act_scroll_down(p);
+}
+
+static inline void
+act_mark_prev(struct fm *p)
+{
+ if (!p->vml || p->c == SIZE_MAX) return;
+ usize b = fm_mark_find(p, p->c, -1);
+ if (b == SIZE_MAX) return;
+ usize r = fm_filter_pct_rank(p, b);
+ size y = fm_scroll_to_rank(p, r);
+ while (y++ < 0) act_scroll_up(p);
+}
+
+static inline void
+act_mark_invert(struct fm *p)
+{
+ if (!p->vl) return;
+ if (!(p->f & FM_MARK_PWD)) {
+ fm_mark_clear(p);
+ fm_mark_init(p);
+ }
+ p->vml = 0;
+ usize nw = BITSET_W(p->dl);
+ for (usize b = 0; b < nw; b++) {
+ p->vm[b] = p->v[b] & ~p->vm[b];
+ p->vml += u64_popcount(p->vm[b]);
+ }
+ fm_mark_apply_bitset(p, p->vm);
+ for (usize b = 0; b < nw; b++) {
+ u64 cl = p->v[b] & ~p->vm[b];
+ while (cl) {
+ usize i = (b << 6) + u64_ctz(cl);
+ cl &= cl - 1;
+ if (i >= p->dl) break;
+ u32 x = ent_v_load(p, i);
+ ent_v_set(&x, MARK, 0);
+ ent_v_store(p, i, x);
+ }
+ }
+ fm_mark_invalidate(p);
+ p->f |= FM_REDRAW_DIR|FM_REDRAW_NAV;
+}
+
+// }}}
+
+// Input {{{
+
+static inline void
+input_disabled(struct fm *p)
+{
+ (void) p;
+}
+
+static inline void
+input_move_beginning(struct fm *p)
+{
+ switch (rl_home(&p->r)) {
+ case RL_FULL:
+ p->f |= FM_REDRAW_CMD;
+ break;
+ case RL_PARTIAL:
+ STR_PUSH(&p->io, VT_CR);
+ p->f |= FM_REDRAW_FLUSH;
+ break;
+ }
+}
+
+static inline void
+input_move_end(struct fm *p)
+{
+ switch (rl_end(&p->r)) {
+ case RL_FULL:
+ p->f |= FM_REDRAW_CMD;
+ break;
+ case RL_PARTIAL:
+ STR_PUSH(&p->io, VT_CR);
+ vt_cuf(&p->io, p->r.vx);
+ p->f |= FM_REDRAW_FLUSH;
+ break;
+ }
+}
+
+static inline void
+input_move_left(struct fm *p)
+{
+ usize n;
+ switch (rl_left(&p->r, &n)) {
+ case RL_FULL:
+ p->f |= FM_REDRAW_CMD;
+ break;
+ case RL_PARTIAL:
+ vt_cub(&p->io, n);
+ p->f |= FM_REDRAW_FLUSH;
+ break;
+ }
+}
+
+static inline void
+input_move_word_left(struct fm *p)
+{
+ if (rl_word_left(&p->r) != -1)
+ p->f |= FM_REDRAW_CMD;
+}
+
+static inline void
+input_move_word_right(struct fm *p)
+{
+ if (rl_word_right(&p->r) != -1)
+ p->f |= FM_REDRAW_CMD;
+}
+
+static inline void
+input_move_right(struct fm *p)
+{
+ usize n;
+ switch (rl_right(&p->r, &n)) {
+ case RL_FULL:
+ p->f |= FM_REDRAW_CMD;
+ break;
+ case RL_PARTIAL:
+ vt_cuf(&p->io, n);
+ p->f |= FM_REDRAW_FLUSH;
+ break;
+ }
+}
+
+static inline void
+input_delete_to_end(struct fm *p)
+{
+ int r = rl_delete_right(&p->r);
+ if (r == RL_NONE) return;
+ STR_PUSH(&p->io, VT_EL0);
+ p->f |= FM_REDRAW_FLUSH;
+ if (p->kp) p->kp(p, 0, rl_cl_get(&p->r), rl_cr_get(&p->r));
+}
+
+static inline void
+input_delete_to_beginning(struct fm *p)
+{
+ int r = rl_delete_left(&p->r);
+ if (r == RL_NONE) return;
+ p->f |= FM_REDRAW_CMD;
+ if (p->kp) p->kp(p, 0, rl_cl_get(&p->r), rl_cr_get(&p->r));
+}
+
+static inline void
+input_delete(struct fm *p)
+{
+ usize n;
+ switch (rl_delete(&p->r, &n)) {
+ case RL_FULL:
+ p->f |= FM_REDRAW_CMD;
+ break;
+ case RL_PARTIAL:
+ vt_dch(&p->io, n);
+ p->f |= FM_REDRAW_FLUSH;
+ break;
+ case RL_NONE: return;
+ }
+ if (p->kp) p->kp(p, 0, rl_cl_get(&p->r), rl_cr_get(&p->r));
+}
+
+static inline void
+input_delete_word_left(struct fm *p)
+{
+ int r = rl_delete_word_prev(&p->r);
+ if (r == RL_NONE) return;
+ p->f |= FM_REDRAW_CMD;
+ if (p->kp) p->kp(p, 0, rl_cl_get(&p->r), rl_cr_get(&p->r));
+}
+
+static inline void
+input_delete_word_right(struct fm *p)
+{
+ int r = rl_delete_word_right(&p->r);
+ if (r == RL_NONE) return;
+ p->f |= FM_REDRAW_CMD;
+ if (p->kp) p->kp(p, 0, rl_cl_get(&p->r), rl_cr_get(&p->r));
+}
+
+static inline void
+input_backspace(struct fm *p)
+{
+ usize n;
+ switch (rl_backspace(&p->r, &n)) {
+ case RL_FULL:
+ p->f |= FM_REDRAW_CMD;
+ break;
+ case RL_PARTIAL:
+ vt_cub(&p->io, n);
+ vt_dch(&p->io, n);
+ p->f |= FM_REDRAW_FLUSH;
+ break;
+ case RL_NONE: return;
+ }
+ if (p->kp) p->kp(p, 0, rl_cl_get(&p->r), rl_cr_get(&p->r));
+}
+
+static inline void
+input_cancel(struct fm *p)
+{
+ rl_clear(&p->r);
+ p->kp = 0;
+ p->kd = 0;
+ STR_PUSH(&p->io, VT_EL2);
+ p->f |= FM_REDRAW_NAV;
+}
+
+static inline void
+input_submit(struct fm *p)
+{
+ rl_join(&p->r);
+ fm_cmd_exec(p);
+ p->r.vx = 0;
+ STR_PUSH(&p->io, VT_EL2);
+ p->f |= FM_REDRAW_NAV;
+}
+
+static inline void
+input_insert(struct fm *p)
+{
+ assert(!(p->k.c & KEY_TAG));
+ usize n;
+ switch (rl_insert(&p->r, p->k.c, p->k.b, p->k.l, &n)) {
+ case RL_FULL:
+ p->f |= FM_REDRAW_CMD;
+ break;
+ case RL_PARTIAL:
+ vt_ich(&p->io, n);
+ str_push(&p->io, (char *) p->k.b, p->k.l);
+ p->f |= FM_REDRAW_FLUSH;
+ break;
+ case RL_NONE: return;
+ }
+ if (p->kp) p->kp(p, 0, rl_cl_get(&p->r), rl_cr_get(&p->r));
+}
+
+static inline void
+input_insert_paste(struct fm *p)
+{
+ for (bool s = false;;) {
+ if (!term_key_read(p->t.fd, &p->k)) return;
+ if (p->k.c == KEY_PASTE_END) return;
+ if (p->k.b[0] == '\r' || p->k.b[0] == '\n') {
+ if (!s) p->k.c = p->k.b[0] = ' ';
+ s = true;
+ } else
+ s = false;
+ if (KEY_GET_MOD(p->k.c) || KEY_IS_SYM(p->k.c) || p->k.c < 32)
+ continue;
+ input_insert(p);
+ }
+}
+
+// }}}
+
+#include "config_cmd.h"
+#include "config_key.h"
+
+// Init {{{
+
+static inline usize
+fm_io_flush(str *s, void *ctx, usize n)
+{
+ (void) n;
+ struct fm *p = ctx;
+ write_all(p->t.fd, s->m, s->l);
+ s->l = 0;
+ return 0;
+}
+
+static inline int
+fm_init(struct fm *p)
+{
+ if (fs_watch_init(&p->p) == -1)
+ return -1;
+ p->opener = get_env("DFM_OPENER", DFM_OPENER);
+ p->dfd = AT_FDCWD;
+ p->ds = DFM_DEFAULT_SORT;
+ p->dv = DFM_DEFAULT_VIEW;
+ p->sf = fm_filter_startswith;
+ p->dec = sizeof(p->de);
+ p->tz = tz_offset();
+#if DFM_SHOW_HIDDEN
+ p->f |= FM_HIDDEN;
+#endif
+ if (!geteuid()) p->f |= FM_ROOT;
+ fm_mark_clear_all(p);
+ STR_INIT(&p->pwd, DFM_PATH_MAX, 0, 0);
+ STR_INIT(&p->ppwd, DFM_PATH_MAX, 0, 0);
+ STR_INIT(&p->mpwd, DFM_PATH_MAX, 0, 0);
+ STR_INIT(&p->io, DFM_IO_MAX, fm_io_flush, p);
+ return 0;
+}
+
+static inline void
+fm_free(struct fm *p)
+{
+ fs_watch_free(&p->p);
+ close(p->dfd);
+ int fd = term_dead(&p->t) ? STDOUT_FILENO : STDERR_FILENO;
+ if (!p->pwd.l) return;
+ write_all(fd, p->pwd.m, p->pwd.l);
+ write_all(fd, S("\n"));
+}
+
+// }}}
+
+// Main {{{
+
+static inline void
+fm_watch_handle(struct fm *p)
+{
+ for (cut n = {0};;) {
+ switch (fs_watch_pump(&p->p, &n.d, &n.l)) {
+ case '!': fm_dir_refresh(p); return;
+ case '+': fm_dir_add(p, n); break;
+ case '-': fm_dir_del(p, n); break;
+ case '~': fm_dir_del(p, n); fm_dir_add(p, n); break;
+ default: return;
+ }
+ }
+}
+
+static inline void
+fm_update(struct fm *p)
+{
+ term_reap();
+ if (unlikely(term_resize(&p->t)))
+ if (fm_term_resize(p) < 0)
+ fm_draw_err(p, S("resize failed"), errno);
+ fm_watch_handle(p);
+ if (!(p->f & FM_DIRTY)) return;
+ p->f &= ~FM_DIRTY;
+ p->f |= FM_REDRAW_DIR|FM_REDRAW_NAV;
+ fm_dir_sort(p);
+ fm_cursor_sync(p);
+ if (p->f & FM_DIRTY_WITHIN && p->st) {
+ u64 m = ent_load_off(p, p->st);
+ cut st = { p->de + p->st, ent_get(m, LEN) };
+ fm_scroll_to(p, st);
+ p->st = 0;
+ p->f &= ~FM_DIRTY_WITHIN;
+ }
+}
+
+static inline void
+fm_draw(struct fm *p)
+{
+ if ((p->f & FM_REDRAW) == FM_REDRAW) {
+ STR_PUSH(&p->io, VT_ED2);
+ fm_dir_ht_clear_cache(p);
+ }
+ if (p->f & FM_REDRAW_DIR)
+ fm_draw_dir(p);
+ if (p->f & FM_REDRAW_NAV)
+ fm_draw_nav(p);
+ if (p->f & FM_REDRAW_CMD)
+ fm_draw_cmd(p);
+ if (p->f & FM_REDRAW) {
+ if (p->kp || p->kd) {
+ vt_cup(&p->io, p->r.vx, p->row + DFM_MARGIN);
+ STR_PUSH(&p->io, VT_DECTCEM_Y);
+ } else {
+ vt_cup(&p->io, 0, p->o + 1);
+ STR_PUSH(&p->io, VT_DECTCEM_N);
+ }
+ fm_draw_flush(p);
+ }
+ p->f &= ~FM_REDRAW;
+}
+
+static inline void
+fm_input(struct fm *p)
+{
+ if (!term_key_read(p->t.fd, &p->k))
+ return;
+ if (p->r.pr.l) fm_key_input(p->k.c)(p);
+ else fm_key(p->k.c)(p);
+}
+
+static inline int
+fm_run(struct fm *p)
+{
+ if (fm_term_init(p) < 0) return -1;
+ rl_init(&p->r, p->col, CUT_NULL);
+ for (; likely(!term_dead(&p->t)); ) {
+ fm_update(p);
+ fm_draw(p);
+ fm_input(p);
+ }
+ fm_term_free(p);
+ if (!(p->f & FM_PRINT_PWD)) p->pwd.l = 0;
+ return 0;
+}
+
+int
+main(int argc, char *argv[])
+{
+ static struct fm p;
+ str *s = &p.pwd;
+
+ if (fm_init(&p) < 0) {
+ STR_PUSH(s, "error?: ");
+ str_push_s(s, strerror(errno));
+ goto e;
+ }
+
+ const char *pwd = ".";
+ struct argv A = arg_init(argc, argv);
+
+ for (struct arg a; (a = arg_next(&A)).sign != -1;) {
+ const char *n;
+ switch (a.name) {
+ case 'H':
+ p.f ^= (-(a.sign == '+') ^ p.f) & FM_HIDDEN;
+ continue;
+ case 'p':
+ p.f |= FM_PICKER;
+ continue;
+ case 'o':
+ n = arg_next_positional(&A);
+ if (!n) goto arg_no_val;
+ p.opener.d = n;
+ continue;
+ case 's':
+ n = arg_next_positional(&A);
+ if (!n) goto arg_no_val;
+ p.ds = fm_sort_fn(*n) ? *n : 'n';
+ continue;
+ case 'v':
+ n = arg_next_positional(&A);
+ if (!n) goto arg_no_val;
+ p.dv = *n;
+ continue;
+ case '-':
+ if (!strcmp(a.pos, "--help")) {
+ STR_PUSH(s, DFM_HELP);
+ term_set_dead(&p.t, 1);
+ goto e;
+ } else if (!strcmp(a.pos, "--version")) {
+ STR_PUSH(s, CFG_NAME " " CFG_VERSION " "
+ CC_COMMIT " (" CC_BRANCH ") " CC_DATE);
+ goto e;
+ }
+ STR_PUSH(s, "unknown arg ");
+ str_push_s(s, a.pos);
+ goto e;
+ default:
+ if (unlikely(a.name)) {
+ STR_PUSH(s, "unknown arg ");
+ str_push_c(s, a.sign);
+ str_push_c(s, a.name);
+ goto e;
+ }
+ pwd = a.pos;
+ continue;
+ }
+arg_no_val:
+ STR_PUSH(s, "arg ");
+ str_push_c(s, a.sign);
+ str_push_c(s, a.name);
+ STR_PUSH(s, " missing value");
+ goto e;
+ }
+
+ if (!fm_path_chdir(&p, pwd)) {
+ STR_PUSH(s, "cd: '");
+ str_push_s(s, pwd);
+ STR_PUSH(s, "': ");
+ str_push_s(s, strerror(errno));
+ goto e;
+ }
+
+ if (fm_run(&p) < 0) {
+ STR_PUSH(s, "term: '");
+ str_push_s(s, strerror(errno));
+ goto e;
+ }
+
+ fm_free(&p);
+ return EXIT_SUCCESS;
+e:
+ fm_free(&p);
+ return EXIT_FAILURE;
+}
+
+// }}}
+
diff --git a/example/opener_ext b/example/opener_ext
new file mode 100755
index 0000000..ce1699a
--- /dev/null
+++ b/example/opener_ext
@@ -0,0 +1,27 @@
+#!/bin/sh -feu
+#
+# Example DFM_OPENER script using file extension.
+#
+
+case $1 in
+ *.mkv | *.webm | *.mp4 | *.avi)
+ exec mpv -- "$1"
+ ;;
+
+ *.opus | *.mp3 | *.wav | *.flac | *.ogg)
+ exec mus --no-shuffle -- "$1"
+ ;;
+
+ *.jpg | *.jpeg | *.gif | *.png | *.CR2)
+ exec mpv --pause -- "$1"
+ ;;
+
+ *.svg)
+ exec inkscape "$1"
+ ;;
+
+ *?*)
+ exec "${EDITOR:-vim}" "$1"
+ ;;
+esac
+
diff --git a/example/opener_mime b/example/opener_mime
new file mode 100755
index 0000000..2f7bd52
--- /dev/null
+++ b/example/opener_mime
@@ -0,0 +1,33 @@
+#!/bin/sh -feu
+#
+# Example DFM_OPENER script using mimetype.
+#
+
+mime_type=$(file -bi "$1")
+
+case $mime_type in
+ audio/*)
+ exec mpv --no-video "$1"
+ ;;
+
+ video/*)
+ exec mpv "$1"
+ ;;
+
+ image/*)
+ exec gimp "$1"
+ ;;
+
+ text/html* | application/pdf*)
+ exec firefox "$1"
+ ;;
+
+ text/*)
+ exec "${EDITOR:=vi}" "$1"
+ ;;
+
+ *?*)
+ printf 'error: unhandled mime-type %s\n' "$mime_type" >&2
+ ;;
+esac
+
diff --git a/lib/arg.h b/lib/arg.h
new file mode 100644
index 0000000..06d3024
--- /dev/null
+++ b/lib/arg.h
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2026 Dylan Araps
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+#ifndef DYLAN_ARG
+#define DYLAN_ARG
+
+struct argv {
+ const char *const *argv;
+ int c;
+};
+
+struct arg {
+ const char *pos;
+ int sign;
+ int name;
+};
+
+static inline struct argv
+arg_init(int argc, char *a[])
+{
+ (void) argc;
+ return (struct argv) { .argv = (const char *const *) ++a };
+}
+
+static inline struct arg
+arg_next(struct argv *s)
+{
+ static const signed char t[256] = {
+ ['-'] = '-' - 1, ['+'] = '+' - 1,
+ };
+
+ struct arg a = {0};
+ a.pos = *s->argv;
+ a.sign = a.pos ? t[(unsigned char) **s->argv] + 1 : -1;
+
+ if (a.sign > 1) {
+ s->c |= !s->c;
+ a.name = (unsigned char) (a.pos[s->c] ? a.pos[s->c] : a.sign);
+ s->c += !!a.pos[s->c];
+ s->c *= !!a.pos[s->c];
+ }
+
+ s->argv += !s->c;
+ return a;
+}
+
+static inline const char *
+arg_next_positional(struct argv *s)
+{
+ const char *a = *s->argv;
+ if (!a) return a;
+ a += s->c;
+ s->c = 0;
+ s->argv++;
+ return a;
+}
+
+#endif // DYLAN_ARG
+
diff --git a/lib/bitset.h b/lib/bitset.h
new file mode 100644
index 0000000..d8979de
--- /dev/null
+++ b/lib/bitset.h
@@ -0,0 +1,152 @@
+/*
+ * Copyright (c) 2026 Dylan Araps
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+#ifndef DYLAN_BITSET_H
+#define DYLAN_BITSET_H
+
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+
+#include "util.h"
+
+enum {
+ BITSET_WORD_BITS = 64,
+ BITSET_WORD_SHIFT = 6,
+ BITSET_WORD_MASK = BITSET_WORD_BITS - 1,
+};
+
+#define BITSET_W(n) (((n) + BITSET_WORD_MASK) >> BITSET_WORD_SHIFT)
+
+static inline u8
+bitset_get(const u64 *b, usize i)
+{
+ return (b[i >> BITSET_WORD_SHIFT] >> (i & BITSET_WORD_MASK)) & 1ull;
+}
+
+static inline void
+bitset_set(u64 *b, usize i)
+{
+ b[i >> BITSET_WORD_SHIFT] |= 1ull << (i & BITSET_WORD_MASK);
+}
+
+static inline void
+bitset_clr(u64 *b, usize i)
+{
+ b[i >> BITSET_WORD_SHIFT] &= ~(1ull << (i & BITSET_WORD_MASK));
+}
+
+static inline void
+bitset_tog(u64 *b, usize i)
+{
+ b[i >> BITSET_WORD_SHIFT] ^= 1ull << (i & BITSET_WORD_MASK);
+}
+
+static inline void
+bitset_assign(u64 *b, usize i, int v)
+{
+ u64 *w = &b[i >> BITSET_WORD_SHIFT];
+ u64 m = 1ull << (i & BITSET_WORD_MASK);
+ *w = v ? (*w | m) : (*w & ~m);
+}
+
+static inline usize
+bitset_count(const u64 *b, usize l)
+{
+ usize c = 0;
+ for (usize i = 0, w = BITSET_W(l); i < w; i++)
+ c += u64_popcount(b[i]);
+ return c;
+}
+
+static inline void
+bitset_set_all(u64 *v, usize n)
+{
+ memset(v, 0xff, BITSET_W(n) * sizeof *v);
+}
+
+static inline void
+bitset_clr_all(u64 *v, usize n)
+{
+ memset(v, 0, BITSET_W(n) * sizeof *v);
+}
+
+static inline void
+bitset_invert(u64 *v, usize n)
+{
+ usize w = BITSET_W(n);
+ for (usize i = 0; i < w; i++) v[i] = ~v[i];
+ usize r = n & BITSET_WORD_MASK;
+ if (r) v[w - 1] &= (1ull << r) - 1;
+}
+
+static inline void
+bitset_swap(u64 *b, usize i, usize j)
+{
+ u8 bi = bitset_get(b, i);
+ u8 bj = bitset_get(b, j);
+ bitset_assign(b, i, bj);
+ bitset_assign(b, j, bi);
+}
+
+static inline usize
+bitset_next_set(const u64 *b, usize i, usize n)
+{
+ if (i >= n) return SIZE_MAX;
+ usize wi = i >> BITSET_WORD_SHIFT;
+ usize wN = BITSET_W(n);
+ u64 w = b[wi];
+ w &= (~0ull << (i & BITSET_WORD_MASK));
+
+ for (;;) {
+ if (w) {
+ usize j = (wi << BITSET_WORD_SHIFT) + u64_ctz(w);
+ return j < n ? j : SIZE_MAX;
+ }
+
+ if (++wi >= wN) break;
+ w = b[wi];
+ }
+
+ return SIZE_MAX;
+}
+
+static inline usize
+bitset_prev_set(const u64 *b, usize i, usize n)
+{
+ if (i >= n) i = n - 1;
+ usize wi = i >> BITSET_WORD_SHIFT;
+ u64 w = b[wi];
+ u64 r = (u64)(i & BITSET_WORD_MASK);
+ w &= (r == BITSET_WORD_MASK) ? ~0ull : ((1ull << (r + 1)) - 1ull);
+
+ for (;;) {
+ if (w)
+ return (wi << BITSET_WORD_SHIFT) + (BITSET_WORD_MASK - (usize)u64_clz(w));
+ if (!wi) break;
+ w = b[--wi];
+ }
+
+ return SIZE_MAX;
+}
+
+#endif // DYLAN_BITSET_H
+
diff --git a/lib/date.h b/lib/date.h
new file mode 100644
index 0000000..6d8b060
--- /dev/null
+++ b/lib/date.h
@@ -0,0 +1,139 @@
+/*
+ * Copyright (c) 2026 Dylan Araps
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+#ifndef DYLAN_DATE_H
+#define DYLAN_DATE_H
+
+//
+// Fast date algorithm, C implementation.
+// Source: https://www.benjoffe.com/fast-date-64
+//
+#include <stddef.h>
+#include <stdint.h>
+
+#include "util.h"
+
+#define C1 505054698555331ull // floor(2^64 * 4 / 146097)
+#define C2 50504432782230121ull // ceil(2^64 * 4 / 1461)
+#define C3 8619973866219416ull // floor(2^64 / 2140)
+
+#define SCALE 32u
+#define SHIFT0 (30556u * SCALE)
+#define SHIFT1 (5980u * SCALE)
+
+#if defined(__SIZEOF_INT128__)
+#define ERAS 4726498270ull
+#define D_SHIFT ((u64)(146097ull * ERAS - 719469ull))
+#define Y_SHIFT ((u64)(400ull * ERAS - 1ull))
+
+static inline u64
+mulhi(u64 a, u64 b)
+{
+ __uint128_t p = (__uint128_t)a * (__uint128_t)b;
+ return (u64)(p >> 64);
+}
+
+static inline void
+mul128(u64 a, u64 b, u64 *hi, u64 *lo)
+{
+ __uint128_t p = (__uint128_t)a * (__uint128_t)b;
+ *hi = (u64)(p >> 64);
+ *lo = (u64)p;
+}
+
+#else
+
+#define ERAS 14704u
+#define D_SHIFT ((u64)(146097ull * (u64)ERAS - 719469ull))
+#define Y_SHIFT ((u64)(400ull * (u64)ERAS - 1ull))
+
+static inline u64
+mulhi(u64 a, u64 b)
+{
+ u64 a0 = (u32)a;
+ u64 a1 = a >> 32;
+ u64 b0 = (u32)b;
+ u64 b1 = b >> 32;
+ u64 p00 = a0 * b0;
+ u64 p01 = a0 * b1;
+ u64 p10 = a1 * b0;
+ u64 p11 = a1 * b1;
+ u64 mid = (p00 >> 32) + (u32)p01 + (u32)p10;
+ return p11 + (p01 >> 32) + (p10 >> 32) + (mid >> 32);
+}
+
+static inline void
+mul128(u64 a, u64 b, u64 *hi, u64 *lo)
+{
+ u64 a0 = (u32)a;
+ u64 a1 = a >> 32;
+ u64 b0 = (u32)b;
+ u64 b1 = b >> 32;
+ u64 p00 = a0 * b0;
+ u64 p01 = a0 * b1;
+ u64 p10 = a1 * b0;
+ u64 p11 = a1 * b1;
+ u64 mid = (p00 >> 32) + (u32)p01 + (u32)p10;
+ *hi = p11 + (p01 >> 32) + (p10 >> 32) + (mid >> 32);
+ *lo = (mid << 32) | (u32)p00;
+}
+
+#endif // defined(__SIZEOF_INT128__)
+
+static inline void
+ut_to_date(s32 day, s32 *Y, u32 *M, u32 *D)
+{
+ u64 rev = (u64)D_SHIFT - (u64)(s64)day; // Reverse day count.
+ u64 cen = mulhi(C1, rev); // Divide 36524.25
+ u64 jul = rev - cen / 4u + cen; // Julian map.
+ u64 num_hi, num_lo;
+ mul128(C2, jul, &num_hi, &num_lo); // Divide 365.25
+ u64 yrs64 = (u64)Y_SHIFT - (u64)num_hi; // Forward year.
+ u64 yrs = (u32)yrs64;
+ u32 ypt = (u32)mulhi((u64)(24451u * SCALE), num_lo); // Year (backwards).
+ u32 bump = (ypt < (3952u * SCALE)); // Jan or Feb.
+ u32 shift = bump ? SHIFT1 : SHIFT0; // Month offset.
+ u32 N = (u32)((yrs % 4u) * (16u * SCALE) + shift - ypt); // Leap years.
+ u32 m = N / (2048u * SCALE);
+ u32 d = (u32)mulhi(C3, (u64)(N % (2048u * SCALE))); // Divide 2140
+ *Y = (s32)yrs + (s32)bump;
+ *M = m;
+ *D = d + 1u;
+}
+
+static inline void
+ut_to_date_time(s64 tz, s32 day, s32 *Y, u32 *M, u32 *D, u32 *h, u32 *m, u32 *s)
+{
+ s64 us = tz + day;
+ s32 days = (s32)(us / 86400);
+ s32 r = (s32)(us % 86400);
+ if (r < 0) { r += 86400; days -= 1; }
+ ut_to_date(days, Y, M, D);
+ u32 hr = r / 3600;
+ r -= hr * 3600;
+ *h = hr;
+ u32 mn = r / 60;
+ *m = mn;
+ *s = r - mn * 60;
+}
+
+#endif // DYLAN_DATE_H
+
diff --git a/lib/readline.h b/lib/readline.h
new file mode 100644
index 0000000..2c05341
--- /dev/null
+++ b/lib/readline.h
@@ -0,0 +1,529 @@
+/*
+ * Copyright (c) 2026 Dylan Araps
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+#ifndef DYLAN_READLINE
+#define DYLAN_READLINE
+
+#ifndef RL_MAX
+#error "RL_MAX not set"
+#endif
+
+#include <assert.h>
+#include <string.h>
+
+#include "str.h"
+#include "utf8.h"
+#include "util.h"
+
+enum {
+ RL_NONE,
+ RL_PARTIAL,
+ RL_FULL,
+ RL_CAP = (RL_MAX >> 1) - 3,
+};
+
+struct readline {
+ str cl;
+ str cr;
+ cut pr;
+
+ usize vx;
+ usize vw;
+
+ usize prw;
+ usize clw;
+ usize crw;
+};
+
+static inline int
+rl_is_ifs(int c)
+{
+ return c == ' ' || c == '\t';
+}
+
+static inline usize
+rl_prompt(const struct readline *r)
+{
+ return r->prw + !!r->prw;
+}
+
+static inline usize
+rl_cursor(const struct readline *r)
+{
+ return rl_prompt(r) + r->clw;
+}
+
+static inline usize
+rl_total(const struct readline *r)
+{
+ return rl_cursor(r) + r->crw;
+}
+
+static inline void
+rl_vw_set(struct readline *r, usize vw)
+{
+ assert(vw);
+ r->vw = vw;
+ if (r->vx >= vw) r->vx = vw - 1;
+}
+
+static inline void
+rl_pr_set(struct readline *r, cut pr)
+{
+ r->pr = pr;
+ usize lw;
+ r->prw = utf8_cols(pr.d, pr.l, &lw);
+}
+
+static inline void
+rl_cr_set(struct readline *r, cut c)
+{
+ assert(c.l <= RL_CAP);
+ memcpy(r->cr.m + (r->cr.c - c.l), c.d, c.l);
+ r->cr.l = c.l;
+ usize lw;
+ r->crw = utf8_cols(c.d, c.l, &lw);
+}
+
+static inline void
+rl_cl_sync(struct readline *r)
+{
+ usize lw;
+ r->clw = utf8_cols(r->cl.m, r->cl.l, &lw);
+ usize c = rl_cursor(r);
+ if (c < r->vw) r->vx = c;
+ else r->vx = r->vw - lw;
+}
+
+static inline void
+rl_init(struct readline *r, usize vw, cut pr)
+{
+ STR_INIT(&r->cl, RL_MAX, 0, 0);
+ STR_INIT(&r->cr, RL_MAX >> 1, 0, 0);
+ rl_vw_set(r, vw);
+ rl_pr_set(r, pr);
+ r->vx = rl_prompt(r);
+ r->clw = 0;
+ r->crw = 0;
+}
+
+static inline const char *
+rl_cr_ptr(const struct readline *r)
+{
+ return r->cr.m + (r->cr.c - r->cr.l);
+}
+
+static inline cut
+rl_cr_get(const struct readline *r)
+{
+ return (cut) { rl_cr_ptr(r), r->cr.l };
+}
+
+static inline cut
+rl_cl_get(const struct readline *r)
+{
+ return (cut) { r->cl.m, r->cl.l };
+}
+
+static inline usize
+rl_cl_last(const struct readline *r, u32 *cp, int *w)
+{
+ usize l = utf8_decode_rev((const unsigned char *)r->cl.m, r->cl.l, cp);
+ *w = utf8_width(*cp);
+ return l;
+}
+
+static inline usize
+rl_cr_first(const struct readline *r, u32 *cp, int *w)
+{
+ const char *p = rl_cr_ptr(r);
+ char *n = utf8_decode((void *)p, cp);
+ *w = utf8_width(*cp);
+ return (usize)(n - p);
+}
+
+static inline usize
+rl_offset(const struct readline *r)
+{
+ usize c = rl_cursor(r);
+ return c > r->vx ? c - r->vx : 0;
+}
+
+static inline int
+rl_empty(const struct readline *r)
+{
+ return !r->cl.l && !r->cr.l;
+}
+
+static inline void
+rl_clear(struct readline *r)
+{
+ rl_pr_set(r, CUT_NULL);
+ r->cl.l = 0;
+ r->cr.l = 0;
+ r->clw = 0;
+ r->crw = 0;
+ r->vx = 0;
+}
+
+static inline usize
+rl_consume_cl(struct readline *r)
+{
+ usize w = 0;
+ do {
+ u32 cp;
+ int cw;
+ usize l = rl_cl_last(r, &cp, &cw);
+ r->cl.l -= l;
+ r->clw -= cw;
+ w += (usize)cw;
+ if (cw != 0) break;
+ } while (r->cl.l);
+ return w;
+}
+
+static inline usize
+rl_consume_cr(struct readline *r)
+{
+ usize w = 0;
+ do {
+ u32 cp;
+ int cw;
+ usize l = rl_cr_first(r, &cp, &cw);
+ r->cr.l -= l;
+ r->crw -= cw;
+ w += (usize)cw;
+ if (cw != 0 && !r->cr.l) break;
+ if (cw != 0) {
+ u32 ncp;
+ int nw;
+ rl_cr_first(r, &ncp, &nw);
+ if (nw != 0) break;
+ }
+ } while (r->cr.l);
+ return w;
+}
+
+static inline int
+rl_take_left_to_right(struct readline *r, usize *wo)
+{
+ if (!r->cl.l) return 0;
+ usize tw = 0;
+ for (;;) {
+ u32 cp;
+ int w;
+ usize l = rl_cl_last(r, &cp, &w);
+ if (r->cr.l + l > RL_CAP)
+ return -1;
+ r->cl.l -= l;
+ r->clw -= w;
+ usize o = r->cr.c - r->cr.l - l;
+ memcpy(r->cr.m + o, r->cl.m + r->cl.l, l);
+ r->cr.l += l;
+ r->crw += w;
+ tw += (usize)w;
+ if (!r->cl.l || w != 0) break;
+ }
+ if (wo) *wo = tw;
+ return 1;
+}
+
+static inline int
+rl_take_right_to_left(struct readline *r, usize *wo)
+{
+ if (!r->cr.l) return 0;
+ usize tw = 0;
+ for (;;) {
+ u32 cp;
+ int w;
+ usize l = rl_cr_first(r, &cp, &w);
+ if (r->cl.l + l > RL_CAP)
+ return -1;
+ str_copy(&r->cl, rl_cr_ptr(r), l);
+ r->cr.l -= l;
+ r->clw += w;
+ r->crw -= w;
+ tw += (usize)w;
+ if (!r->cr.l || w != 0) break;
+ }
+ if (wo) *wo = tw;
+ return 1;
+}
+
+static inline int
+rl_insert(struct readline *r, u32 c, const u8 *b, usize l, usize *n)
+{
+ if (r->cl.l + l >= RL_CAP) return RL_NONE;
+ u8 w = utf8_width(c);
+ str_copy(&r->cl, (const char *)b, l);
+ r->clw += w;
+ if (n) *n = w;
+ if (r->vx + w < r->vw) {
+ r->vx += w;
+ return RL_PARTIAL;
+ } else {
+ r->vx = r->vw - w;
+ return RL_FULL;
+ }
+}
+
+static inline int
+rl_backspace(struct readline *r, usize *n)
+{
+ if (!r->cl.l) return RL_NONE;
+ usize w = rl_consume_cl(r);
+ if (n) *n = w;
+ usize c = rl_cursor(r);
+ if (rl_total(r) < r->vw && r->vx == c + w) {
+ r->vx = c;
+ return RL_PARTIAL;
+ }
+ if (r->vx > c) r->vx = c;
+ return RL_FULL;
+}
+
+static inline int
+rl_delete(struct readline *r, usize *n)
+{
+ if (n) *n = 0;
+ if (!r->cr.l) return RL_NONE;
+ usize w = rl_consume_cr(r);
+ if (n) *n = w;
+ return r->vx + r->crw + w < r->vw ? RL_PARTIAL : RL_FULL;
+}
+
+static inline int
+rl_delete_left(struct readline *r)
+{
+ if (!r->cl.l) return RL_NONE;
+ r->cl.l = 0;
+ r->clw = 0;
+ r->vx = rl_prompt(r);
+ return RL_FULL;
+}
+
+static inline int
+rl_delete_right(struct readline *r)
+{
+ if (!r->cr.l) return RL_NONE;
+ r->cr.l = 0;
+ r->crw = 0;
+ return RL_PARTIAL;
+}
+
+static inline int
+rl_left(struct readline *r, usize *n)
+{
+ if (n) *n = 0;
+ if (!r->cl.l) {
+ if (!rl_offset(r)) return RL_NONE;
+ r->vx = rl_cursor(r);
+ return RL_FULL;
+ }
+ usize w;
+ if (rl_take_left_to_right(r, &w) < 0) return RL_NONE;
+ if (n) *n = w;
+ if (r->vx >= w && r->vx - w > 0) {
+ r->vx -= w;
+ return RL_PARTIAL;
+ }
+ return RL_FULL;
+}
+
+static inline int
+rl_right(struct readline *r, usize *n)
+{
+ if (!r->cr.l) return RL_NONE;
+ usize w;
+ if (rl_take_right_to_left(r, &w) < 0) return RL_NONE;
+ if (n) *n = w;
+ if (r->vx + w + w <= r->vw) {
+ r->vx += w;
+ return RL_PARTIAL;
+ }
+ return RL_FULL;
+}
+
+static inline void
+rl_join(struct readline *r)
+{
+ str_copy(&r->cl, rl_cr_ptr(r), r->cr.l);
+ str_terminate(&r->cl);
+ r->clw += r->crw;
+ r->cr.l = 0;
+ r->crw = 0;
+}
+
+static inline int
+rl_home(struct readline *r)
+{
+ if (!r->cl.l) return RL_NONE;
+ if (r->cr.l + r->cl.l > RL_CAP) return RL_NONE;
+ usize s = rl_offset(r);
+ usize o = r->cr.c - r->cr.l - r->cl.l;
+ memcpy(r->cr.m + o, r->cl.m, r->cl.l);
+ r->cr.l += r->cl.l;
+ r->crw += r->clw;
+ r->cl.l = 0;
+ r->clw = 0;
+ r->vx = rl_prompt(r);
+ return s ? RL_FULL : RL_PARTIAL;
+}
+
+static inline int
+rl_end(struct readline *r)
+{
+ if (!r->cr.l) return RL_NONE;
+ if (r->cl.l + r->cr.l > RL_CAP) return RL_NONE;
+ str_copy(&r->cl, rl_cr_ptr(r), r->cr.l);
+ r->clw += r->crw;
+ r->cr.l = 0;
+ r->crw = 0;
+ usize c = rl_cursor(r);
+ if (c < r->vw) {
+ r->vx = c;
+ return RL_PARTIAL;
+ }
+ u32 cp;
+ int w;
+ rl_cl_last(r, &cp, &w);
+ r->vx = r->vw - w;
+ return RL_FULL;
+}
+
+static inline int
+rl_word_left(struct readline *r)
+{
+ if (!r->cl.l) return RL_NONE;
+ u32 cp;
+ int w;
+ while (r->cl.l) {
+ rl_cl_last(r, &cp, &w);
+ if (!rl_is_ifs(cp)) break;
+ if (rl_left(r, NULL) == RL_NONE) break;
+ }
+ while (r->cl.l) {
+ rl_cl_last(r, &cp, &w);
+ if (rl_is_ifs(cp)) break;
+ if (rl_left(r, NULL) == RL_NONE) break;
+ }
+ return RL_FULL;
+}
+
+static inline int
+rl_word_right(struct readline *r)
+{
+ if (!r->cr.l) return RL_NONE;
+ u32 cp;
+ int w;
+ while (r->cr.l) {
+ rl_cr_first(r, &cp, &w);
+ if (!rl_is_ifs(cp)) break;
+ if (rl_right(r, NULL) == RL_NONE) break;
+ }
+ while (r->cr.l) {
+ rl_cr_first(r, &cp, &w);
+ if (rl_is_ifs(cp)) break;
+ if (rl_right(r, NULL) == RL_NONE) break;
+ }
+ return RL_FULL;
+}
+
+static inline int
+rl_delete_word_prev(struct readline *r)
+{
+ if (!r->cl.l) return -1;
+ usize d = 0;
+ u32 cp;
+ int w;
+ for (; r->cl.l; d++) {
+ rl_cl_last(r, &cp, &w);
+ if (!rl_is_ifs(cp)) break;
+ if (rl_backspace(r, NULL) == RL_NONE) break;
+ }
+ for (; r->cl.l; d++) {
+ rl_cl_last(r, &cp, &w);
+ if (rl_is_ifs(cp)) break;
+ if (rl_backspace(r, NULL) == RL_NONE) break;
+ }
+ return d ? RL_FULL : RL_NONE;
+}
+
+static inline int
+rl_delete_word_right(struct readline *r)
+{
+ if (!r->cr.l) return RL_NONE;
+ usize d = 0;
+ u32 cp;
+ int w;
+ for (; r->cr.l; d++) {
+ rl_cr_first(r, &cp, &w);
+ if (!rl_is_ifs(cp)) break;
+ if (rl_delete(r, NULL) == RL_NONE) break;
+ }
+ for (; r->cr.l; d++) {
+ rl_cr_first(r, &cp, &w);
+ if (rl_is_ifs(cp)) break;
+ if (rl_delete(r, NULL) == RL_NONE) break;
+ }
+ return d ? RL_FULL : RL_NONE;
+}
+
+static inline usize
+rl_write_seg(str *s, const unsigned char *p, usize l, usize c, usize x, usize e)
+{
+ const unsigned char *pe = p + l;
+ u32 cp;
+ while (p < pe) {
+ const unsigned char *nc = utf8_decode((void *)p, &cp);
+ int w = utf8_width(cp);
+ if (c < x && c + w > x)
+ str_push_c(s, ' ');
+ else if (c + w > x && c + w <= e)
+ str_push(s, (const char *)p, (usize)(nc - p));
+ c += w;
+ if (c >= e) return c;
+ p = nc;
+ }
+ return c;
+}
+
+static inline void
+rl_write_range(const struct readline *r, str *s, usize x, usize n)
+{
+ usize c = 0;
+ usize e = x + n;
+ c = rl_write_seg(s, (const unsigned char *)r->pr.d, r->pr.l, c, x, e);
+ if (c >= e) return;
+ c = rl_write_seg(s, (const unsigned char *)r->cl.m, r->cl.l, c, x, e);
+ if (c >= e) return;
+ c = rl_write_seg(s, (const unsigned char *)rl_cr_ptr(r), r->cr.l, c, x, e);
+ if (c < e) str_memset(s, ' ', e - c);
+}
+
+static inline void
+rl_write_visible(const struct readline *r, str *s)
+{
+ rl_write_range(r, s, rl_offset(r), r->vw);
+}
+
+#endif // DYLAN_READLINE
+
diff --git a/lib/str.h b/lib/str.h
new file mode 100644
index 0000000..6464dcf
--- /dev/null
+++ b/lib/str.h
@@ -0,0 +1,183 @@
+/*
+ * Copyright (c) 2026 Dylan Araps
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+#ifndef DYLAN_STR_H
+#define DYLAN_STR_H
+
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+
+#include "util.h"
+
+struct str;
+typedef usize (str_err)(struct str *, void *, usize l);
+
+typedef struct str {
+ char *m;
+ usize l;
+ usize c;
+
+ str_err *f;
+ void *ctx;
+} str;
+
+#define STR_ERR ((usize) -1)
+#define STR_PUSH(s, p) str_push((s), (p), sizeof(p) - 1)
+#define STR_COPY(s, p) str_copy((s), (p), sizeof(p) - 1)
+
+#define STR_INIT(s, m, f, ctx) do { \
+ static char b[(m)]; \
+ str_init((s), (b), sizeof(b), (f), (ctx)); \
+} while (0)
+
+static inline void
+str_init(str *s, char *b, usize c, str_err *f, void *ctx)
+{
+ s->m = b;
+ s->l = -(!b || !c);
+ s->c = c;
+ s->f = f;
+ s->ctx = ctx;
+}
+
+static inline usize
+str_cb(str *s, usize l)
+{
+ return s->f ? s->f(s, s->ctx, l) : STR_ERR;
+}
+
+static inline int
+str_fit(str *s, usize l)
+{
+ if (s->l + l < s->c) return 1;
+ return str_cb(s, l) != STR_ERR;
+}
+
+static inline void
+str_copy(str *s, const char *p, usize l)
+{
+ memcpy(&s->m[s->l], p, l);
+ s->l += l;
+}
+
+static inline void
+str_copy_c(str *s, char c)
+{
+ s->m[s->l++] = c;
+}
+
+static inline cut
+str_push(str *s, const char *p, usize l)
+{
+ if (!str_fit(s, l)) return (cut) { 0, 0 };
+ str_copy(s, p, l);
+ return (cut){ &s->m[s->l - l], l };
+}
+
+static inline void
+str_push_c(str *s, char c)
+{
+ if (!str_fit(s, 1)) return;
+ str_copy_c(s, c);
+}
+
+static inline void
+str_push_s(str *s, const char *p)
+{
+ str_push(s, p, strlen(p));
+}
+
+static inline void
+str_memset(str *s, int c, usize n)
+{
+ if (!str_fit(s, n)) return;
+ memset(&s->m[s->l], c, n);
+ s->l += n;
+}
+
+static inline void
+str_push_u32_b(str *s, u32 v, u32 b, int c, usize l)
+{
+ static const char d[] = "0123456789abcdef";
+ char o[33];
+ char *p = &o[sizeof(o)];
+ assert(b >= 2 && b <= 16);
+ do {
+ *--p = d[v % b];
+ v /= b;
+ } while (v);
+ usize n = (usize)(&o[sizeof(o)] - p);
+ if (n < l) str_memset(s, c, l - n);
+ str_push(s, p, n);
+}
+
+static inline void
+str_push_u32_p(str *s, u32 v, int c, usize l)
+{
+ str_push_u32_b(s, v, 10, c, l);
+}
+
+static inline void
+str_push_u32(str *s, u32 v)
+{
+ str_push_u32_p(s, v, 0, 0);
+}
+
+static inline void
+str_push_u64(str *s, u64 v)
+{
+ char b[21];
+ char *p = &b[sizeof(b)];
+ do {
+ *--p = (char)('0' + (v % 10));
+ v /= 10;
+ } while (v);
+ str_push(s, p, (usize)(&b[sizeof(b)] - p));
+}
+
+static inline void
+str_push_sanitize(str *s, const char *p, usize l)
+{
+ if (!str_fit(s, l)) return;
+ char *d = s->m + s->l;
+ const unsigned char *b = (const unsigned char *) p;
+ for (usize i = 0; i < l; i++) {
+ unsigned char c = b[i];
+ d[i] = (char)(c >= 0x20 && c != 0x7F ? c : '?');
+ }
+ s->l += l;
+}
+
+static inline int
+str_cmp(str *a, str *b)
+{
+ return a->l == b->l && *a->m == *b->m && !memcmp(a->m, b->m, b->l);
+}
+
+static inline void
+str_terminate(str *s)
+{
+ if (str_fit(s, 1)) s->m[s->l] = 0;
+}
+
+#endif // DYLAN_STR_H
+
diff --git a/lib/term.h b/lib/term.h
new file mode 100644
index 0000000..b3b1440
--- /dev/null
+++ b/lib/term.h
@@ -0,0 +1,206 @@
+/*
+ * Copyright (c) 2026 Dylan Araps
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+#ifndef DYLAN_TERM_H
+#define DYLAN_TERM_H
+
+#include <fcntl.h>
+#include <signal.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <termios.h>
+#include <unistd.h>
+
+#include <sys/ioctl.h>
+#include <sys/select.h>
+#include <sys/time.h>
+#include <sys/wait.h>
+
+#include "util.h"
+#include "vt.h"
+
+enum {
+ TERM_LOADED = 1 << 0,
+ TERM_RESIZE = 1 << 1,
+};
+
+static struct term {
+ struct termios o;
+ int fd;
+ int null;
+ volatile sig_atomic_t flag;
+ volatile sig_atomic_t dead;
+} *TERM;
+
+static inline void
+term_set_dead(struct term *t, int s)
+{
+ t->dead = 128 + s;
+}
+
+static inline int
+term_dead(const struct term *t)
+{
+ return t->dead;
+}
+
+static inline int
+term_resize(const struct term *t)
+{
+ return t->flag & TERM_RESIZE;
+}
+
+static inline void
+term_restore_on_signal(int s)
+{
+ if (!TERM) return;
+ if (!(TERM->flag & TERM_LOADED)) return;
+ term_set_dead(TERM, s);
+ tcsetattr(TERM->fd, TCSAFLUSH, &TERM->o);
+
+ //
+ // TODO: Unhardcode this.
+ //
+#define TERM_COOKED \
+ S(VT_ED0 VT_BPASTE_OFF VT_DECAWM_Y VT_DECTCEM_Y VT_ALT_SCREEN_N)
+ write_all(TERM->fd, TERM_COOKED);
+ write_all(STDOUT_FILENO, TERM_COOKED);
+}
+
+static inline void
+term_signal_fatal(int s)
+{
+ term_restore_on_signal(s);
+ _exit(128 + s);
+}
+
+static inline void
+term_signal_crash(int s)
+{
+ term_restore_on_signal(s);
+ struct sigaction sa = {0};
+ sa.sa_handler = SIG_DFL;
+ sigemptyset(&sa.sa_mask);
+ sigaction(s, &sa, NULL);
+ kill(getpid(), s);
+}
+
+static inline void
+term_signal_sigwinch(int s)
+{
+ (void) s;
+ if (TERM) TERM->flag |= TERM_RESIZE;
+}
+
+static inline void
+term_signal_setup(void) {
+ struct sigaction sa;
+ sigemptyset(&sa.sa_mask);
+ sa.sa_flags = SA_RESTART;
+ sa.sa_handler = term_signal_fatal;
+ sigaction(SIGINT, &sa, NULL);
+ sigaction(SIGTERM, &sa, NULL);
+ sigaction(SIGQUIT, &sa, NULL);
+ sa.sa_handler = term_signal_crash;
+ sigaction(SIGSEGV, &sa, NULL);
+ sigaction(SIGABRT, &sa, NULL);
+ sigaction(SIGBUS, &sa, NULL);
+ sigaction(SIGFPE, &sa, NULL);
+ sigaction(SIGILL, &sa, NULL);
+ sa.sa_handler = term_signal_sigwinch;
+ sigaction(SIGWINCH, &sa, NULL);
+}
+
+static inline int
+term_size_update(struct term *t, u16 *row, u16 *col)
+{
+ struct winsize ws;
+ if (ioctl(t->fd, TIOCGWINSZ, &ws) < 0)
+ return -1;
+ t->flag &= ~TERM_RESIZE;
+ *row = ws.ws_row;
+ *col = ws.ws_col;
+ return 0;
+}
+
+static inline int
+term_init_io(struct term *t)
+{
+ if (isatty(STDIN_FILENO) && isatty(STDOUT_FILENO))
+ t->fd = STDIN_FILENO;
+ else if (isatty(STDIN_FILENO)) {
+ t->fd = open("/dev/tty", O_RDWR|O_CLOEXEC);
+ if (t->fd < 0) return -1;
+ } else {
+ t->fd = -1;
+ return -1;
+ }
+ t->null = open("/dev/null", O_WRONLY|O_CLOEXEC);
+ return t->null;
+}
+
+static inline int
+term_raw(const struct term *t)
+{
+ struct termios n = t->o;
+ n.c_iflag &= ~(BRKINT|ICRNL|INPCK|ISTRIP|IXON);
+ n.c_oflag &= ~(OPOST);
+ n.c_cflag |= (CS8);
+ n.c_lflag &= ~(ECHO|ICANON|IEXTEN|ISIG);
+ n.c_cc[VMIN] = 1;
+ n.c_cc[VTIME] = 0;
+ return tcsetattr(t->fd, TCSAFLUSH, &n);
+}
+
+static inline int
+term_cooked(struct term *t)
+{
+ assert(t->flag & TERM_LOADED);
+ return tcsetattr(t->fd, TCSAFLUSH, &t->o);
+}
+
+static inline int
+term_init(struct term *t)
+{
+ if (term_init_io(t) < 0) return -1;
+ if (tcgetattr(t->fd, &t->o) < 0) return -1;
+ TERM = t;
+ t->flag |= TERM_LOADED;
+ term_signal_setup();
+ return 0;
+}
+
+static inline void
+term_reap(void)
+{
+ for (int st; waitpid(-1, &st, WNOHANG) > 0; );
+}
+
+static inline void
+term_destroy(const struct term *t)
+{
+ if (t->fd >= 0) close(t->fd);
+ if (t->null >= 0) close(t->null);
+ TERM = NULL;
+}
+
+#endif // DYLAN_TERM_H
+
diff --git a/lib/term_key.h b/lib/term_key.h
new file mode 100644
index 0000000..f92ef80
--- /dev/null
+++ b/lib/term_key.h
@@ -0,0 +1,292 @@
+/*
+ * Copyright (c) 2026 Dylan Araps
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+#ifndef DYLAN_TERM_KEY_H
+#define DYLAN_TERM_KEY_H
+
+#include <fcntl.h>
+#include <signal.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <termios.h>
+#include <unistd.h>
+
+#include <sys/ioctl.h>
+#include <sys/select.h>
+#include <sys/time.h>
+
+#include "utf8.h"
+#include "util.h"
+
+//
+// Store encoded special keys alongside utf8 codepoints in an unused range.
+//
+#define KEY_TAG 0x80000000u
+#define KEY_SYM 0x40000000u
+#define KEY_MOD_SHIFT 27u
+#define KEY_MOD_MASK (0x7u << KEY_MOD_SHIFT)
+#define KEY_TXT_MASK 0x001FFFFFu
+#define KEY_SYM_MASK 0x000000FFu
+#define KEY_IS_SYM(k) ((((u32)(k)) & (KEY_TAG|KEY_SYM)) == (KEY_TAG|KEY_SYM))
+#define KEY_GET_MOD(k) ((((u32)(k)) & KEY_MOD_MASK) >> KEY_MOD_SHIFT)
+
+#define K(m, c) \
+((u32)(m) == 0u ? (u32)(c) : (KEY_IS_SYM(c) \
+? (((u32)(c) & ~KEY_MOD_MASK) | (((u32)(m) & 0x7u) << KEY_MOD_SHIFT)) \
+: (KEY_TAG | (((u32)(m) & 0x7u) << KEY_MOD_SHIFT) | ((u32)(c) & KEY_TXT_MASK))))
+
+#define KEY_REG(id) (KEY_TAG|KEY_SYM | ((u32)(id) & KEY_SYM_MASK))
+
+#define MOD_SHIFT (1u << 0)
+#define MOD_ALT (1u << 1)
+#define MOD_CTRL (1u << 2)
+
+#define KEY_ESCAPE 27
+#define KEY_BACKSPACE 127
+#define KEY_TAB K(MOD_CTRL, 'i')
+#define KEY_SHIFT_TAB K(MOD_SHIFT|MOD_CTRL, 'i')
+#define KEY_ENTER K(MOD_CTRL, 'm')
+#define KEY_UP KEY_REG(1)
+#define KEY_DOWN KEY_REG(2)
+#define KEY_LEFT KEY_REG(3)
+#define KEY_RIGHT KEY_REG(4)
+#define KEY_HOME KEY_REG(5)
+#define KEY_END KEY_REG(6)
+#define KEY_PAGE_UP KEY_REG(7)
+#define KEY_PAGE_DOWN KEY_REG(8)
+#define KEY_INSERT KEY_REG(9)
+#define KEY_DELETE KEY_REG(10)
+#define KEY_F1 KEY_REG(11)
+#define KEY_F2 KEY_REG(12)
+#define KEY_F3 KEY_REG(13)
+#define KEY_F4 KEY_REG(14)
+#define KEY_F5 KEY_REG(15)
+#define KEY_F6 KEY_REG(16)
+#define KEY_F7 KEY_REG(17)
+#define KEY_F8 KEY_REG(18)
+#define KEY_F9 KEY_REG(19)
+#define KEY_F10 KEY_REG(20)
+#define KEY_F11 KEY_REG(21)
+#define KEY_F12 KEY_REG(22)
+#define KEY_PASTE KEY_REG(23)
+#define KEY_PASTE_END KEY_REG(24)
+
+struct term_key {
+ u8 b[64];
+ u16 l;
+ u32 c;
+};
+
+static inline u32
+term_key_csi_tilde(u8 c, u32 m)
+{
+ switch (c) {
+ case 1: return K(m, KEY_HOME);
+ case 2: return K(m, KEY_INSERT);
+ case 3: return K(m, KEY_DELETE);
+ case 4: return K(m, KEY_END);
+ case 5: return K(m, KEY_PAGE_UP);
+ case 6: return K(m, KEY_PAGE_DOWN);
+ case 7: return K(m, KEY_HOME);
+ case 8: return K(m, KEY_END);
+ case 11: return K(m, KEY_F1);
+ case 12: return K(m, KEY_F2);
+ case 13: return K(m, KEY_F3);
+ case 14: return K(m, KEY_F4);
+ case 15: return K(m, KEY_F5);
+ case 17: return K(m, KEY_F6);
+ case 18: return K(m, KEY_F7);
+ case 19: return K(m, KEY_F8);
+ case 20: return K(m, KEY_F9);
+ case 21: return K(m, KEY_F10);
+ case 23: return K(m, KEY_F11);
+ case 24: return K(m, KEY_F12);
+ case 200: return KEY_PASTE;
+ case 201: return KEY_PASTE_END;
+ default: return 0;
+ }
+}
+
+static inline u32
+term_key_csi_final(u8 c, u32 m)
+{
+ switch (c) {
+ case 'A': return K(m, KEY_UP);
+ case 'B': return K(m, KEY_DOWN);
+ case 'C': return K(m, KEY_RIGHT);
+ case 'D': return K(m, KEY_LEFT);
+ case 'H': return K(m, KEY_HOME);
+ case 'F': return K(m, KEY_END);
+ case 'Z': return K(MOD_SHIFT|MOD_CTRL | m, 'i');
+ default: return 0;
+ }
+}
+
+static inline u32
+term_key_csi_ss3(u8 c)
+{
+ switch (c) {
+ case 'P': return K(0, KEY_F1);
+ case 'Q': return K(0, KEY_F2);
+ case 'R': return K(0, KEY_F3);
+ case 'S': return K(0, KEY_F4);
+ case 'A': return K(0, KEY_UP);
+ case 'B': return K(0, KEY_DOWN);
+ case 'C': return K(0, KEY_RIGHT);
+ case 'D': return K(0, KEY_LEFT);
+ case 'H': return K(0, KEY_HOME);
+ case 'F': return K(0, KEY_END);
+ default: return 0;
+ }
+}
+
+static inline s32
+term_key_csi_int(const u8 *b, usize l, usize *i)
+{
+ u32 v = 0;
+ usize j = *i;
+ if (j >= l || b[j] < '0' || b[j] > '9') return -1;
+ for (; j < l && b[j] >= '0' && b[j] <= '9'; j++)
+ v = v * 10u + (u32)(b[j] - '0');
+ *i = j;
+ return (s32) v;
+}
+
+static inline u32
+term_key_csi_xterm_mod(u32 x)
+{
+ u32 m = 0;
+ if (x < 2 || x > 8) return m;
+ if (x & 1u) m |= MOD_ALT;
+ if (x & 2u) m |= MOD_SHIFT;
+ if (x & 4u) m |= MOD_CTRL;
+ return m;
+}
+
+static inline bool
+term_key_csi_end(u8 c)
+{
+ return c >= 0x40 && c <= 0x7E;
+}
+
+static inline bool
+term_key_csi_read(int fd, struct term_key *k)
+{
+ for (;;) {
+ if (k->l >= sizeof(k->b))
+ return false;
+ if (read(fd, &k->b[k->l], 1) != 1)
+ return false;
+ u8 c = k->b[k->l++];
+ if (term_key_csi_end(c))
+ break;
+ }
+ return true;
+}
+
+static inline bool
+term_key_ss3(int fd, struct term_key *k)
+{
+ if (read(fd, &k->b[k->l], 1) != 1) return false;
+ k->l++;
+ k->c = term_key_csi_ss3(k->b[2]);
+ return !!k->c;
+}
+
+static inline bool
+term_key_csi(int fd, struct term_key *k)
+{
+ if (!term_key_csi_read(fd, k)) return false;
+ usize n = k->l;
+ if (n < 3) return false;
+ u8 f = k->b[n - 1];
+ usize i = 2;
+ if (i < n && (k->b[i] == '?' || k->b[i] == '>' || k->b[i] == '<'))
+ i++;
+ s32 p1 = term_key_csi_int(k->b, n, &i);
+ s32 p2 = -1;
+ if (p1 != -1 && i < n && k->b[i] == ';') {
+ i++;
+ p2 = term_key_csi_int(k->b, n, &i);
+ }
+ u32 m = p2 != -1 ? term_key_csi_xterm_mod(p2) : 0;
+ if (f == '~' && p1 != -1)
+ k->c = term_key_csi_tilde(p1, m);
+ else
+ k->c = term_key_csi_final(f, m);
+ return !!k->c;
+}
+
+static inline bool
+term_key_utf8(int fd, struct term_key *k, usize o, u32 m)
+{
+ u8 c = k->b[o];
+ if (c >= 1 && c <= 26) {
+ k->c = K(m | MOD_CTRL, 'a' + (c - 1));
+ k->l = o + 1;
+ return true;
+ }
+ if (c >= 0xC0) {
+ usize l = utf8_expected(c);
+ if (!l) return false;
+ for (usize i = 1; i < l; i++)
+ if (read(fd, &k->b[o + i], 1) != 1)
+ return false;
+ int e;
+ u32 cp;
+ utf8_decode_untrusted(&k->b[o], &cp, &e);
+ if (e) return false;
+ k->c = K(m, cp);
+ k->l = o + l;
+ return true;
+ }
+ k->c = K(m, c);
+ k->l = o + 1;
+ return true;
+}
+
+static inline bool
+term_key_read(int fd, struct term_key *k)
+{
+ if (read(fd, k->b, 1) != 1) return false;
+ k->l = 1;
+ if (unlikely(k->b[0] == '\033')) {
+ struct timeval tv = { .tv_sec = 0, .tv_usec = 30000 };
+ fd_set rfds;
+ FD_ZERO(&rfds);
+ FD_SET(fd, &rfds);
+ if (select(fd + 1, &rfds, NULL, NULL, &tv) <= 0) {
+ k->c = k->b[0];
+ return true;
+ }
+ if (read(fd, &k->b[1], 1) != 1) return false;
+ k->l = 2;
+ switch (k->b[1]) {
+ case '[': return term_key_csi(fd, k);
+ case 'O': return term_key_ss3(fd, k);
+ default: return term_key_utf8(fd, k, 1, MOD_ALT);
+ }
+ }
+ return term_key_utf8(fd, k, 0, 0);
+}
+
+#endif // DYLAN_TERM_KEY_H
+
diff --git a/lib/utf8.h b/lib/utf8.h
new file mode 100644
index 0000000..0104661
--- /dev/null
+++ b/lib/utf8.h
@@ -0,0 +1,185 @@
+/*
+ * Copyright (c) 2026 Dylan Araps
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+#ifndef DYLAN_UTF8_H
+#define DYLAN_UTF8_H
+
+#include "util.h"
+
+static inline usize
+utf8_expected(u8 b)
+{
+ static const u8 L[] = {
+ 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,2,2,2,2,3,3,4,0
+ };
+ return L[b >> 3];
+}
+
+static inline int
+utf8_width(u32 c)
+{
+ if (c == 0) return 0;
+
+ // Control.
+ if (c < 0x20) return 0;
+ if (c >= 0x7f && c < 0xa0) return 0;
+
+ // Zero width joiner.
+ if (c == 0x200d) return 0;
+
+ // Combining.
+ if ((c >= 0x0300 && c <= 0x036f) ||
+ (c >= 0x1ab0 && c <= 0x1aff) ||
+ (c >= 0x1dc0 && c <= 0x1dff) ||
+ (c >= 0x20d0 && c <= 0x20ff) ||
+ (c >= 0xfe20 && c <= 0xfe2f) ||
+ (c >= 0xe0100 && c <= 0xe01ef))
+ return 0;
+
+ // Variation selectors.
+ if ((c >= 0xfe00 && c <= 0xfe0f))
+ return 0;
+
+ // Emoji modifiers.
+ if (c >= 0x1f3fb && c <= 0x1f3ff)
+ return 0;
+
+ // East asian wide.
+ if ((c >= 0x1100 && c <= 0x115f) ||
+ c == 0x2329 || c == 0x232a ||
+ (c >= 0x2e80 && c <= 0xa4cf && c != 0x303f) ||
+ (c >= 0xac00 && c <= 0xd7a3) ||
+ (c >= 0xf900 && c <= 0xfaff) ||
+ (c >= 0xfe10 && c <= 0xfe19) ||
+ (c >= 0xfe30 && c <= 0xfe6f) ||
+ (c >= 0xff00 && c <= 0xff60) ||
+ (c >= 0xffe0 && c <= 0xffe6) ||
+ (c >= 0x20000 && c <= 0x2fffd) ||
+ (c >= 0x30000 && c <= 0x3fffd))
+ return 2;
+
+ // Emoji block.
+ if ((c >= 0x1f300 && c <= 0x1faff) ||
+ (c >= 0x2600 && c <= 0x27bf) ||
+ (c >= 0x2b50 && c <= 0x2b55))
+ return 2;
+
+ return 1;
+}
+
+//
+// Branchless UTF8 decoder by Skeeto.
+// Source: https://nullprogram.com/blog/2017/10/06/
+//
+static inline void *
+utf8_decode(void *b, u32 *c)
+{
+ unsigned char *s = (unsigned char *)b;
+ usize l = utf8_expected(s[0]);
+ static const int m[] = {0x00, 0x7f, 0x1f, 0x0f, 0x07};
+ static const int shc[] = {0, 18, 12, 6, 0};
+ *c = (u32)(s[0] & m[l]) << 18;
+ *c |= (u32)(s[1] & 0x3f) << 12;
+ *c |= (u32)(s[2] & 0x3f) << 6;
+ *c |= (u32)(s[3] & 0x3f);
+ *c >>= shc[l];
+ return s + l + !l;
+}
+
+static void *
+utf8_decode_untrusted(void *b, u32 *c, int *e)
+{
+ static const u32 mi[] = {4194304, 0, 128, 2048, 65536};
+ static const int she[] = {0, 6, 4, 2, 0};
+ unsigned char *s = (unsigned char *)b;
+ unsigned char *n = utf8_decode(b, c);
+ usize l = utf8_expected(s[0]);
+ *e = (*c < mi[l]) << 6; // Non-canonical encoding.
+ *e |= ((*c >> 11) == 0x1b) << 7; // Surrogate half?
+ *e |= (*c > 0x10FFFF) << 8; // Out of range?
+ *e |= (s[1] & 0xc0) >> 2;
+ *e |= (s[2] & 0xc0) >> 4;
+ *e |= (s[3]) >> 6;
+ *e ^= 0x2a; // Top two bits of each tail byte correct?
+ *e >>= she[l];
+ return n;
+}
+
+static inline usize
+utf8_decode_rev(const unsigned char *s, usize x, u32 *c)
+{
+ usize i = x;
+ while (i > 0 && (s[i - 1] & 0xc0) == 0x80) i--;
+ if (i > 0) i--;
+ usize l = x - i;
+ utf8_decode((void *)(s + i), c);
+ return l;
+}
+
+static inline usize
+utf8_cols(const void *s, usize l, usize *lw)
+{
+ usize w = 0;
+ const unsigned char *p = (const unsigned char *)s;
+ const unsigned char *e = p + l;
+ *lw = 0;
+ while (p < e) {
+ u32 cp;
+ p = utf8_decode((void *)p, &cp);
+ *lw = utf8_width(cp);
+ w += *lw;
+ }
+ return w;
+}
+
+static inline usize
+utf8_trunc_narrow(const char *s, usize l, usize c)
+{
+ const unsigned char *p = (const unsigned char *)s;
+ const unsigned char *e = p + l;
+ for (usize i = 0; p < e && i < c; i++) {
+ unsigned char b = *p++;
+ if (!(b & 0x80)) continue;
+ for (; p < e && ((*p & 0xC0) == 0x80); p++);
+ }
+ return (usize)(p - (const unsigned char *)s);
+}
+
+static inline usize
+utf8_trunc_wide(const char *s, usize l, usize c)
+{
+ const unsigned char *p = (const unsigned char *)s;
+ const unsigned char *e = p + l;
+ for (usize i = 0; p < e && i < c; ) {
+ u32 cp;
+ const unsigned char *n = (const unsigned char *)utf8_decode((void *)p, &cp);
+ usize a = (usize)(n - p);
+ if (!a) a = 1;
+ int w = utf8_width(cp);
+ if (i + w > c) break;
+ i += w;
+ p += a;
+ }
+ return (usize)(p - (const unsigned char *)s);
+}
+
+#endif // DYLAN_UTF8_H
+
diff --git a/lib/util.h b/lib/util.h
new file mode 100644
index 0000000..819e939
--- /dev/null
+++ b/lib/util.h
@@ -0,0 +1,345 @@
+/*
+ * Copyright (c) 2026 Dylan Araps
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+#ifndef DYLAN_UTIL_H
+#define DYLAN_UTIL_H
+
+#include <assert.h>
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <pwd.h>
+#include <spawn.h>
+#include <stdalign.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/wait.h>
+
+#define ARR_SIZE(a) ((intptr_t)(sizeof(a) / sizeof(*(a))))
+#define IS_POW2(x) ((x) > 0 && (((x) & ((x) - 1)) == 0))
+#define MIN(x, y) ((x) < (y) ? (x) : (y))
+#define MAX(x, y) ((x) > (y) ? (x) : (y))
+
+#if defined(__GNUC__) || defined(__clang__)
+#define likely(x) __builtin_expect(!!(x), 1)
+#define unlikely(x) __builtin_expect(!!(x), 0)
+#else
+#define likely(x) !!(x)
+#define unlikely(x) !!(x)
+#endif
+
+#if __STDC_VERSION__ >= 201112L
+#define STATIC_ASSERT _Static_assert
+#else
+#define SA_CAT_(a,b) a##b
+#define SA_CAT(a,b) SA_CAT_(a,b)
+#define STATIC_ASSERT(c, m) \
+ enum { SA_CAT(static_assert_line_, __LINE__) = 1 / (!!(c)) }
+#endif
+
+typedef union {
+ long long ll;
+ long double ld;
+ void *p;
+} align_max;
+
+typedef uint64_t u64;
+typedef uint32_t u32;
+typedef uint16_t u16;
+typedef uint8_t u8;
+typedef int64_t s64;
+typedef int32_t s32;
+typedef int16_t s16;
+typedef int8_t s8;
+typedef size_t usize;
+typedef ptrdiff_t size;
+
+typedef struct {
+ const char *d;
+ usize l;
+} cut;
+
+//
+// Add four bytes to the end of the CUT to allow the branchless utf8 decoder to
+// potentially overread the string.
+//
+#define CUT(s) (cut) { s "\0\0\0\0", sizeof(s) - 1 }
+#define CUT_NULL ((cut){0})
+#define STR_NULL (&(str){0})
+#define S(s) (s), (sizeof(s) - 1)
+
+static inline int
+cut_cmp(cut a, cut b)
+{
+ return a.l == b.l && *a.d == *b.d && !memcmp(a.d, b.d, b.l);
+}
+
+static inline u64
+bitfield_get64(u64 v, u8 s, u8 b)
+{
+ return (v >> s) & ((1ULL << b) - 1);
+}
+
+static inline void
+bitfield_set64(u64 *t, u64 v, u8 s, u8 b)
+{
+ u64 m = ((1ULL << b) - 1) << s;
+ *t = (*t & ~m) | ((v << s) & m);
+}
+
+static inline u32
+bitfield_get32(u32 v, int o, int l)
+{
+ return (v >> o) & ((1u << l) - 1);
+}
+
+static inline void
+bitfield_set32(u32 *v, u32 x, int o, int l)
+{
+ u32 m = ((1u << l) - 1) << o;
+ *v = (*v & ~m) | ((x << o) & m);
+}
+
+static inline void
+bitfield_set8(u8 *t, u8 v, u8 s, u8 b)
+{
+ u8 m = ((1ULL << b) - 1) << s;
+ *t = (*t & ~m) | ((v << s) & m);
+}
+
+static inline cut
+get_env(const char *e, const char *f)
+{
+ const char *p = getenv(e);
+ cut r;
+ r.d = p && *p ? p : f;
+ r.l = r.d ? strlen(r.d) : 0;
+ return r;
+}
+
+static inline int
+write_all(int fd, const char *b, usize l)
+{
+ for (usize o = 0; o < l; ) {
+ ssize_t r = write(fd, b + o, l - o);
+ if (r > 0) {
+ o += (usize) r;
+ continue;
+ }
+ if (r == -1 && errno == EINTR)
+ continue;
+ return -1;
+ }
+ return 0;
+}
+
+static inline int
+run_cmd(int tty, int in, const char *d, const char *const a[], bool bg)
+{
+ extern char **environ;
+ posix_spawn_file_actions_t fa;
+ pid_t pid;
+ int rc;
+ int st;
+ pid_t r;
+ rc = posix_spawn_file_actions_init(&fa);
+ if (rc) { errno = rc; return -1; }
+ if (in >= 0) {
+ rc = posix_spawn_file_actions_adddup2(&fa, in, 0);
+ if (rc) goto fail_fa;
+ }
+ if (tty >= 0) {
+ if ((rc = posix_spawn_file_actions_adddup2(&fa, tty, 1)) ||
+ (rc = posix_spawn_file_actions_adddup2(&fa, tty, 2)))
+ goto fail_fa;
+ }
+ if (d) {
+#if defined(_POSIX_VERSION) && _POSIX_VERSION >= 202405L
+ rc = posix_spawn_file_actions_addchdir(&fa, d);
+#elif defined(_GNU_SOURCE) || defined(_BSD_SOURCE)
+ rc = posix_spawn_file_actions_addchdir_np(&fa, d);
+#else
+ posix_spawn_file_actions_destroy(&fa);
+ pid = fork();
+ if (pid == -1) return -1;
+ if (!pid) {
+ if (in >= 0) {
+ if (dup2(in, 0) == -1) _exit(127);
+ if (in != 0) close(in);
+ }
+ if (tty >= 0) {
+ if (dup2(tty, 1) == -1) _exit(127);
+ if (dup2(tty, 2) == -1) _exit(127);
+ if (tty != 1 && tty != 2) close(tty);
+ }
+ if (d && chdir(d) == -1)
+ _exit(127);
+ execvp(a[0], (char *const *)a);
+ _exit(127);
+ }
+ goto end;
+#endif
+ if (rc) goto fail_fa;
+ }
+ rc = posix_spawnp(&pid, a[0], &fa, NULL, (char *const *)a, environ);
+ posix_spawn_file_actions_destroy(&fa);
+ if (rc) { errno = rc; return -1; }
+ goto end; // Silence compiler warning when ifdefs cause no jump.
+end:
+ if (bg) return 0;
+ do r = waitpid(pid, &st, 0);
+ while (r == -1 && errno == EINTR);
+ return r == -1 ? -1 : st;
+fail_fa:
+ posix_spawn_file_actions_destroy(&fa);
+ errno = rc;
+ return -1;
+}
+
+static inline int
+fd_from_buf(const char *b, usize l)
+{
+ int fd[2];
+ if (pipe(fd)) return -1;
+ if (l > PIPE_BUF) {
+#ifdef F_GETPIPE_SZ
+ int c = fcntl(fd[1], F_GETPIPE_SZ);
+ if (c < 0 || l > (usize)c) {
+ close(fd[0]);
+ close(fd[1]);
+ return -1;
+ }
+#else
+ close(fd[0]);
+ close(fd[1]);
+ return -1;
+#endif
+ }
+ ssize_t w = write(fd[1], b, l);
+ close(fd[1]);
+ if (w < 0 || (usize)w != l) {
+ close(fd[0]);
+ return -1;
+ }
+ return fd[0];
+}
+
+static inline usize
+u64_popcount(u64 x)
+{
+#if defined(__GNUC__) || defined(__clang__)
+ return (usize) __builtin_popcountll(x);
+#else
+ usize c = 0;
+ for (; x; x &= x - 1) c++;
+ return c;
+#endif
+}
+
+static inline u64
+u64_ctz(u64 x)
+{
+#if defined(__GNUC__) || defined(__clang__)
+ return (u64)__builtin_ctzll(x);
+#else
+ u64 i = 0;
+ while (!(x & 1ull)) { x >>= 1; i++; }
+ return i;
+#endif
+}
+
+static inline u64
+u64_clz(u64 x)
+{
+#if defined(__GNUC__) || defined(__clang__)
+ return (u64)__builtin_clzll(x);
+#else
+ u64 i = 0;
+ for (u64 m = 1ull << 63; !(x & m); m >>= 1) i++;
+ return i;
+#endif
+}
+
+static inline u32
+hash_fnv1a32(const char *d, usize l)
+{
+ u32 h = 2166136261u;
+ for (usize i = 0; i < l; i++)
+ h = (h ^ (unsigned char)d[i]) * 16777619u;
+ return h | 1;
+}
+
+static inline s64
+tz_offset(void)
+{
+ time_t n = time(NULL);
+ struct tm lt;
+ struct tm gt;
+ if (!localtime_r(&n, &lt)) return 0;
+ if (!gmtime_r(&n, &gt)) return 0;
+ time_t lo = mktime(&lt);
+ time_t gm = mktime(&gt);
+ if (lo == (time_t)-1 || gm == (time_t)-1) return 0;
+ return (s64)(lo - gm);
+}
+
+static inline usize
+fm_path_resolve(char *s, usize l)
+{
+ char *m = s;
+ usize i = 0, w = 0;
+ while (i < l) {
+ while (i < l && m[i] == '/') i++;
+ if (i >= l) break;
+ usize b = i;
+ while (i < l && m[i] != '/') i++;
+ usize n = i - b;
+ if (n == 1 && m[b] == '.')
+ continue;
+ if (n == 2 && m[b] == '.' && m[b + 1] == '.') {
+ if (w > 1) {
+ if (m[w - 1] == '/') w--;
+ while (w > 1 && m[w - 1] != '/') w--;
+ }
+ continue;
+ }
+ if (w == 0 || m[w - 1] != '/')
+ m[w++] = '/';
+ if (w != b) memmove(m + w, m + b, n);
+ w += n;
+ }
+ if (w > 1 && m[w - 1] == '/') w--;
+ if (!w) m[w++] = '/';
+ m[w] = 0;
+ return w;
+}
+
+#endif // DYLAN_UTIL_H
+
diff --git a/lib/vt.h b/lib/vt.h
new file mode 100644
index 0000000..4262154
--- /dev/null
+++ b/lib/vt.h
@@ -0,0 +1,177 @@
+/*
+ * Copyright (c) 2026 Dylan Araps
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+#ifndef DYLAN_VT_H
+#define DYLAN_VT_H
+
+#include <stdio.h>
+#include <unistd.h>
+
+#include "str.h"
+#include "util.h"
+
+#define VT_ESC "\x1b"
+#define VT_CR "\r"
+#define VT_LF "\n"
+#define VT_CUU1 VT_ESC "[A"
+#define VT_CUU(n) VT_ESC "[" #n "A"
+#define VT_CUD1 VT_ESC "[B"
+#define VT_CUD(n) VT_ESC "[" #n "B"
+#define VT_CUF1 VT_ESC "[C"
+#define VT_CUF(n) VT_ESC "[" #n "C"
+#define VT_CUB1 VT_ESC "[D"
+#define VT_CUB(n) VT_ESC "[" #n "D"
+#define VT_CUP(x, y) VT_ESC "[" #x ";" #y "H"
+#define VT_CUP1 VT_ESC "[H"
+#define VT_DECAWM_Y VT_ESC "[?7h"
+#define VT_DECAWM_N VT_ESC "[?7l"
+#define VT_DECSC VT_ESC "7"
+#define VT_DECRC VT_ESC "8"
+#define VT_DECSTBM(x, y) VT_ESC "[" #x ";" #y "r"
+#define VT_DECTCEM_Y VT_ESC "[?25h"
+#define VT_DECTCEM_N VT_ESC "[?25l"
+#define VT_ED0 VT_ESC "[J"
+#define VT_ED1 VT_ESC "[1J"
+#define VT_ED2 VT_ESC "[2J"
+#define VT_EL0 VT_ESC "[K"
+#define VT_EL1 VT_ESC "[1K"
+#define VT_EL2 VT_ESC "[2K"
+#define VT_IL0 VT_ESC "[L"
+#define VT_IL(n) VT_ESC "[" #n "L"
+#define VT_ICH1 VT_ESC "[@"
+#define VT_ICH(n) VT_ESC "[" #n "@"
+#define VT_DCH1 VT_ESC "[P"
+#define VT_DCH(n) VT_ESC "[" #n "P"
+#define VT_SGR0 VT_ESC "[m"
+
+//
+// XTerm Alternate Screen.
+// https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer
+//
+#define VT_ALT_SCREEN_Y VT_ESC "[?1049h"
+#define VT_ALT_SCREEN_N VT_ESC "[?1049l"
+
+//
+// Synchronized Updates.
+// https://gist.github.com/christianparpart/d8a62cc1ab659194337d73e399004036
+// https://github.com/contour-terminal/vt-extensions/blob/master/synchronized-output.md
+//
+#define VT_BSU VT_ESC "[?2026h"
+#define VT_ESU VT_ESC "[?2026l"
+
+//
+// Bracketed Paste.
+//
+#define VT_BPASTE_ON VT_ESC "[?2004h"
+#define VT_BPASTE_OFF VT_ESC "[?2004l"
+
+//
+// VT_SGR(...) macro supporting 16 arguments.
+// NOTE: A byte can be saved by using VT_SGR0 instead of VT_SGR(0).
+//
+#define VT_Fa(f, x) f(x)
+#define VT_Fb(f, x, ...) f(x) ";" VT_Fa(f, __VA_ARGS__)
+#define VT_Fc(f, x, ...) f(x) ";" VT_Fb(f, __VA_ARGS__)
+#define VT_Fd(f, x, ...) f(x) ";" VT_Fc(f, __VA_ARGS__)
+#define VT_Fe(f, x, ...) f(x) ";" VT_Fd(f, __VA_ARGS__)
+#define VT_Ff(f, x, ...) f(x) ";" VT_Fe(f, __VA_ARGS__)
+#define VT_Fg(f, x, ...) f(x) ";" VT_Ff(f, __VA_ARGS__)
+#define VT_Fh(f, x, ...) f(x) ";" VT_Fg(f, __VA_ARGS__)
+#define VT_Fi(f, x, ...) f(x) ";" VT_Fh(f, __VA_ARGS__)
+#define VT_Fj(f, x, ...) f(x) ";" VT_Fi(f, __VA_ARGS__)
+#define VT_Fk(f, x, ...) f(x) ";" VT_Fj(f, __VA_ARGS__)
+#define VT_Fl(f, x, ...) f(x) ";" VT_Fk(f, __VA_ARGS__)
+#define VT_Fm(f, x, ...) f(x) ";" VT_Fl(f, __VA_ARGS__)
+#define VT_Fn(f, x, ...) f(x) ";" VT_Fm(f, __VA_ARGS__)
+#define VT_Fo(f, x, ...) f(x) ";" VT_Fn(f, __VA_ARGS__)
+#define VT_Fp(f, x, ...) f(x) ";" VT_Fo(f, __VA_ARGS__)
+#define VT_GET_q(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,...) q
+#define VT_CNT(...) VT_GET_q(__VA_ARGS__,p,o,n,m,l,k,j,i,h,g,f,e,d,c,b,a,0)
+#define VT_STR(x) #x
+#define VT_CAT(a,b) a##b
+#define VT_FN(N, f, ...) VT_CAT(VT_F, N)(f, __VA_ARGS__)
+#define VT_JOIN(...) VT_FN(VT_CNT(__VA_ARGS__), VT_STR, __VA_ARGS__)
+#define VT_SGR(...) VT_ESC "[" VT_JOIN(__VA_ARGS__) "m"
+
+static inline void
+vt_cup(str *s, u32 x, u32 y)
+{
+ STR_PUSH(s, VT_ESC "[");
+ str_push_u32(s, y);
+ str_push_c(s, ';');
+ str_push_u32(s, x);
+ str_push_c(s, 'H');
+}
+
+static inline void
+vt_cuf(str *s, u32 n)
+{
+ STR_PUSH(s, VT_ESC "[");
+ str_push_u32(s, n);
+ str_push_c(s, 'C');
+}
+
+static inline void
+vt_cub(str *s, u32 n)
+{
+ STR_PUSH(s, VT_ESC "[");
+ str_push_u32(s, n);
+ str_push_c(s, 'D');
+}
+
+static inline void
+vt_ich(str *s, u32 n)
+{
+ STR_PUSH(s, VT_ESC "[");
+ str_push_u32(s, n);
+ str_push_c(s, '@');
+}
+
+static inline void
+vt_dch(str *s, u32 n)
+{
+ STR_PUSH(s, VT_ESC "[");
+ str_push_u32(s, n);
+ str_push_c(s, 'P');
+}
+
+static inline void
+vt_decstbm(str *s, u32 x, u32 y)
+{
+ STR_PUSH(s, VT_ESC "[");
+ str_push_u32(s, x);
+ str_push_c(s, ';');
+ str_push_u32(s, y);
+ str_push_c(s, 'r');
+}
+
+static inline void
+vt_sgr(str *s, u32 a, u32 b)
+{
+ STR_PUSH(s, VT_ESC "[");
+ str_push_u32(s, a);
+ str_push_c(s, ';');
+ str_push_u32(s, b);
+ str_push_c(s, 'm');
+}
+
+#endif // DYLAN_VT_H
+
diff --git a/platform/linux.h b/platform/linux.h
new file mode 100644
index 0000000..09b972d
--- /dev/null
+++ b/platform/linux.h
@@ -0,0 +1,109 @@
+/*
+ * Copyright (c) 2026 Dylan Araps
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+#ifndef DFM_PLATFORM_LINUX_H
+#define DFM_PLATFORM_LINUX_H
+
+#include <stdalign.h>
+#include <stddef.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <sys/inotify.h>
+
+#include "../lib/util.h"
+
+struct platform {
+ int inotify_wd;
+ int inotify_fd;
+ union {
+ align_max _a;
+ char b[4096];
+ } in;
+ ssize_t inl;
+ ssize_t ino;
+};
+
+static inline int
+fs_watch_init(struct platform *p)
+{
+ p->inotify_wd = -1;
+ p->inotify_fd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC);
+ return p->inotify_fd;
+}
+
+static inline void
+fs_watch(struct platform *p, const char *s)
+{
+ if (p->inotify_wd != -1)
+ inotify_rm_watch(p->inotify_fd, p->inotify_wd);
+ p->inotify_wd = inotify_add_watch(p->inotify_fd, s,
+ IN_CREATE|IN_DELETE|IN_MOVED_FROM|IN_MOVED_TO|IN_ATTRIB);
+}
+
+static inline int
+fs_watch_pump(struct platform *p, const char **s, size_t *l)
+{
+ *s = NULL;
+ if (p->inotify_fd == -1)
+ return 0;
+ if (p->ino >= p->inl) {
+ p->inl = read(p->inotify_fd, p->in.b, sizeof(p->in.b));
+ if (p->inl <= 0)
+ return 0;
+ p->ino = 0;
+ }
+ if (p->inl - p->ino < (ssize_t)sizeof(struct inotify_event))
+ return 0;
+ struct inotify_event *ev =
+ (struct inotify_event *)(void *)(p->in.b + p->ino);
+ ssize_t size = sizeof(struct inotify_event) + ev->len;
+ if (p->inl - p->ino < size)
+ return 0;
+ p->ino += size;
+ if (ev->mask & IN_Q_OVERFLOW)
+ return '!';
+ if (!ev->len)
+ return 0;
+ *s = ev->name;
+ *l = strlen(ev->name);
+ if (ev->mask & (IN_CREATE | IN_MOVED_TO))
+ return '+';
+ if (ev->mask & (IN_DELETE | IN_MOVED_FROM))
+ return '-';
+ if (ev->mask & IN_ATTRIB)
+ return '~';
+ return 0;
+}
+
+static inline void
+fs_watch_free(struct platform *p)
+{
+ if (p->inotify_wd != -1)
+ inotify_rm_watch(p->inotify_fd, p->inotify_wd);
+ if (p->inotify_fd != -1)
+ close(p->inotify_fd);
+}
+
+#define FS_WATCH 1
+
+#endif // DFM_PLATFORM_LINUX_H
+
diff --git a/platform/posix.h b/platform/posix.h
new file mode 100644
index 0000000..d098030
--- /dev/null
+++ b/platform/posix.h
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2026 Dylan Araps
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+#ifndef DFM_PLATFORM_POSIX_H
+#define DFM_PLATFORM_POSIX_H
+
+#include <stddef.h>
+
+struct platform {
+ void *_pad;
+};
+
+static inline int
+fs_watch_init(struct platform *p)
+{
+ (void) p;
+ return 0;
+}
+
+static inline void
+fs_watch(struct platform *p, const char *s)
+{
+ (void) p;
+ (void) s;
+}
+
+static inline int
+fs_watch_pump(struct platform *p, const char **s, size_t *l)
+{
+ (void) p;
+ (void) s;
+ (void) l;
+ return 0;
+}
+
+static inline void
+fs_watch_free(struct platform *p)
+{
+ (void) p;
+}
+
+#endif // DFM_PLATFORM_POSIX_H
+