diff options
| author | Dylan Araps <dylan.araps@gmail.com> | 2026-02-27 13:41:56 +0200 |
|---|---|---|
| committer | Dylan Araps <dylan.araps@gmail.com> | 2026-02-27 13:41:56 +0200 |
| commit | da28548905dab7c60c3fcec4975ccfa23e315909 (patch) | |
| tree | 9cef34a718563969a6bcda5cfeff033eeaf6b1a0 | |
0.99.0
| -rw-r--r-- | LICENSE | 19 | ||||
| -rw-r--r-- | Makefile.in | 47 | ||||
| -rw-r--r-- | README.txt | 463 | ||||
| -rwxr-xr-x | bin/dpp | 76 | ||||
| -rwxr-xr-x | bin/u8 | 34 | ||||
| -rw-r--r-- | config.h.in | 169 | ||||
| -rw-r--r-- | config_cmd.h.in | 136 | ||||
| -rw-r--r-- | config_key.h.in | 190 | ||||
| -rwxr-xr-x | configure | 141 | ||||
| -rw-r--r-- | dfm.c | 3502 | ||||
| -rwxr-xr-x | example/opener_ext | 27 | ||||
| -rwxr-xr-x | example/opener_mime | 33 | ||||
| -rw-r--r-- | lib/arg.h | 77 | ||||
| -rw-r--r-- | lib/bitset.h | 152 | ||||
| -rw-r--r-- | lib/date.h | 139 | ||||
| -rw-r--r-- | lib/readline.h | 529 | ||||
| -rw-r--r-- | lib/str.h | 183 | ||||
| -rw-r--r-- | lib/term.h | 206 | ||||
| -rw-r--r-- | lib/term_key.h | 292 | ||||
| -rw-r--r-- | lib/utf8.h | 185 | ||||
| -rw-r--r-- | lib/util.h | 345 | ||||
| -rw-r--r-- | lib/vt.h | 177 | ||||
| -rw-r--r-- | platform/linux.h | 109 | ||||
| -rw-r--r-- | platform/posix.h | 61 |
24 files changed, 7292 insertions, 0 deletions
@@ -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 + @@ -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 + @@ -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 + @@ -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, <)) return 0; + if (!gmtime_r(&n, >)) return 0; + time_t lo = mktime(<); + time_t gm = mktime(>); + 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 + |