Introduction
Note
NO AI of ANY sort was used at any time for
fzfm’s code or documentation.
Absolutely none.
Why?
A lot of people use fish as an interactive shell, but their scripts are in
bash. Or just sh. I found myself using fish for scripts also, and really
liking it – it’s a lot more sane than posix sh or bash anyday – so I decided
to see how far it could go.
Turns out it can go pretty far. (And that’s coming from a lifelong perl devotee). It hits the sweet spot between bash and perl for applications where glue and non-trivial logic are both equally important. Like a file manager :)
On the fzf part, I assume you already use fzf and know why it is so
powerful, especially what the man-page calls negation; i.e., foo !bar
matches lines that contain foo and do not contain bar.
My intent is not that you should use it. I only wanted to show how something so unconventional as fish can be used for some serious, non-trivial, project. Even if you don’t install it or use it, pick a couple of the animated Gifs – you might find some unique features you never saw before. At the very least you can enjoy the surreal aspect of fish+fzf making a file manager :-)
All that said, it is my daily driver now. So there’s at least one user :-)
Exiting :-)
In honor of the countless jokes about exiting vim, it is :q then ENTER :-)
Unlike vim, Ctrl-C also works!
Oh and a lot of its behaviour is geared to vim users. If someone actually asks for some other editor I’ll do something about it (and probably ask for help!).
Features
There’re a couple of quick demos here.
Single-pane
Let’s get this out of the way: since fzf provides the UI, this cannot be a
dual-pane file manager. It still does everything I used to do in vifm by
using the notion of an “alternate directory” (ALT_DIR), and mapping F5 to
copy and F6 to move selected files from the current directory to ALT_DIR,
but, as they say, YMMV.
Features at a glance
Here are some top level features:
-
Standard fzf features: fuzzy matching, especially negative matching, TAB to select multiple items, etc.
-
Select files and run any command you like on them.
- Either one at a time (using
%c), or all at once (%f) - Comes with a dozen or so sample commands
- See Running commands section for more
- Either one at a time (using
-
Use the output of any command to change the “view” (i.e., the file list in the main window).
- Again, comes with a dozen or so example commands that change the view
- E.g.,
fd -tf --newer 1hourchanges the view to show files modified in the last hour - See Changing the view section for more
-
Add a hotkey to any command you use often
- Or assign a short “colon-command” (similar to vim)
- See Hotkeys vs colon-commands for when to use which
- See Quick access to commands and views section for all the details
-
Learns my most frequently used commands and views as I use it
- (note: no AI bullshit here; just standard Unix commands)
- Fuzzy select these commands from their respective history files.
The “view” command
The “view” feature is similar to vifm’s %U modifier, or midnight commander’s
“External panelise”, but it goes beyond those. First, it is this feature that
provides even the default/initial file list you see when you run fzfm. Also,
the command is re-applied every time the UI refreshes. (In contrast, mc and
vifm recognise when files are deleted and remove them from the view, but they
don’t notice newly added files unless you explicitly run the command again.)
What this means is that, if you, say, run :s<ENTER> to show the list sorted
by du -sk size, then that is the mode it is in until you hit Escape. Edit
a file and make it (much) larger and it will move up. Navigate to another
directory, and it will recalculate for that. Same for :m (sort by
descending mtime) or :c (ctime).
The “preview” command
We’re used to file managers’ preview being a view of the file. But you can
have anything in the preview – a git log of the file is very useful when
exploring a new project, for instance (and is just as useful when your cursor
is on a directory). In “git” mode, files with changes can show a git diff --color in the preview. Or, in the “vimgrep” mode, the preview shows matched
lines with context.
“Modes”
Increasingly, I find that fzfm’s ability to create custom modes for specific
purposes is becoming very useful. For example, if you are in a git directory,
type g and hit alt-m to get into fzfm’s git mode.
Almost all modes have builtin help, accessed by typing h then hitting
alt-m while within that mode. I strongly suggest using this at least the
first time you try a mode.
Documentation for modes is included in the demos page. Since “mode” and “demo” are anagrams, why not? :-)
In decreasing order of how often I use them, the current modes are:
-
t(ortodotxt) – has jumped to top spot within a few hours of being coded!-
I use it so much I got tired of typing “t” then “alt-m”; I added this to my
cmd_histfile: key=alt-t todotxt mode ; fzfm_mode_handler t
-
-
dd(ordirdiff) – differences between 2 directories -
g(orgit) – obvious- why is this lower? Because lazygit exists ;-)
-
ps– runs the ps command. Pretty powerful; check out the help at least! -
vg(vimgrep)
Install and run
Pre-requisites and installation
Pre-requisites:
- absolutely required:
fzfandfish - the
:helpfeature:batandless - filetype helper: 7z, atool (aunpack, apack), bat, iconv, lynx, mediainfo, pdftotext, unoconv, unrar, and zip/unzip
- default/sample external commands: vim, git, perl, fd, etc., but those commands are yours to add/change/delete anyway.
Clone the repo and run ./install-fzfm. It copies the defaults and the
modules. The modules get overwritten, but the defaults may contain your own
customisations, so it pops up a vimdiff window for you to selectively copy
anything the new version has, without blindly overwriting all your stuff.
Then we copy the main script. By default we put it in $HOME/bin and call it a day!
I’m too lazy to write a system-wide install, and mess with sudo, etc. It’s just a single script, so you can do that yourself if that is what you want.
Command line
First, the special option -., if specified, is processed. This makes fzfm show hidden files by default. This option must come first due to a technical limitation1. What I do is alias fm to fzfm and fmd to fzfm -.2.
Next, there are 3 possible options:
-m modeto directly go into a mode; e.g.,fzfm -m git-c 'some view command'to set the view command directly.-p 'some preview command'to set the preview command. This becomes especially useful in some cases; see the examples below.
Some examples:
fzfm -c 'du -sk *|sort -rn|cut -f2' # sort descending by disk usage fzfm -c 'fd -e pl -e pm' # only look at perl programs in the tree fzfm -c 'rg -l pattern' -p 'rg -C5 --color=always pattern {}' # only look at files containing "pattern", then in the preview show the matched lines with context
TIP: Setting up useful combinations of view+preview is the whole point of “modes”, as well as some of the non-mode commands (:c – sort by descending ctime, :cr – same, recursively, and :rg pattern, which does essentially what the 3rd example above does, with the added twist that if you TAB-select some entries it’ll only search those. For frequently used combinations, you can setup your own in cmd_hist
Notes:
- The
-moption cannot be used with the other two. - The view and preview commands need to be in fish syntax. For simple commands and redirection/pipes there is no difference, as you can see in the examples; it’s only if you get into if/then/else and such that it becomes a problem.
Finally, the arguments, if any, are resolved to real paths (especially, relative paths are made absolute).
fzfm: Sets PWD, START_DIR and ALT_DIR all the same.fzfm dir: Does acdto dir, then same as previous usage.fzfm dir1 dir2: Does acdto dir1, sets PWD and START_DIR to dir1, sets ALT_DIR to dir2fzfm dir1 file: (experimental) modifies usage #2 as follows:- “file” should be a file at the top level of “dir1”
- The cursor will be placed at that file on startup
- Otherwise it’s the same as usage #2
fzfm dir1 file dir2: ditto, for usage #3.
Tip: here is how to enable hidden files.
Directories
fzfm has 3 directories it always know about.
- CWD: (no explanation needed)
- START_DIR: the directory it was started from.
- ALT_DIR: an alternate directory that helps us do some things that a single pane file manager normally cannot.
fzfm will always show you the names of these three at the top so there’s no
confusion.
Builtin (internal) hotkeys
There are very few builtin actions, i.e., whose hotkeys cannot be changed.
-
fzfstandard keys: TAB to multi-select, Ctrl-C to exit, ctrl-a to toggle “select all”. -
alt-s to shell out (uses
$SHELL). -
ENTER on a directory to
cdto it. -
ENTER on a file, or after multi-selecting several files, to “open” all of them. Specifically,
fzfm:- Opens each non-text file using
xdg-open(one at a time) - Then opens all text files in one go using
$EDITOR
- Opens each non-text file using
-
ctrl-r and ctrl-v are special feature keys which need their own sections: “Running commands” and “Changing the view”.
Basic/important navigation keys
These used to be “builtin” (see previous section), but are now not. You can change the key assignments if you wish, and even the behaviour (details below):
-
alt-c to pick a directory to cd to
-
ctrl-t to select several files across the tree (i.e., can be from different subdirectories), and show only those in the view.
-
alt-f: pick a file, and cd to its directory. Useful if you remember the filename (or parts of it) better than the directory it lives in. Or the filename is easier to type uniquely.
Notes on alt-c, ctrl-t, and alt-f
- alt-c and ctrl-t perform the same function you’re familiar with if
you use fzf’s standard keybinds in your shell
- Think of alt-f as a cross between those two :)
- By default, they use
fd -tdandfd -tf, respectively. Pick an item and hit enter, and the “cd” happens. - If the query field is non-empty, it is used to pre-filter the list, which somehow seems “cooler” than typing in the query after the list loads!
- If the query starts with a “
.”, then- the
.is stripped, and - the
fdbecomesfd -H --no-ignore-vcs, so hidden and vcs-ignored files also show up, but you can modify the code quite easily to change that to, say-HIinstead.
- the
- Next, if the query starts with a “-a”, “-m”, “-c”, or “-s”, then this is
stripped, and the output of the previous command is sorted by atime,
mtime, ctime, and size, respectively. This is very useful if you want to
see all the PDFs in a directory, but newest first.
- This only applies to ctrl-t (doesn’t make sense for the other two).
-
Fish’s
argparsedoesn’t like non-alphanumeric options, so we have to get that out of the way :-) ↩ -
Well, I say “alias” but in fish it’s an “abbreviation”. ↩
Running commands on files
Like any decent file manager, fzfm can run any commands you like. It’s easier to describe using examples.
IMPORTANT NOTE: fzfm is written in the fish shell language. It doesn’t have to be your default shell, so alt-s will invoke your shell, not fish. But the commands you run within fzfm (i.e., almost everything described below) will have to be in fish syntax. For simple commands and sequences there is not much difference; only complex commands (if-then-else, tests, etc.), will need care. Or you could just wrap your bash commands in a bash -c '...' :-)
Simple ad hoc commands.
-
:!ncdu -eThis is a command that does not require any file/dir arguments, and so it just runs in the current directory.
-
:!vim %fYou need to select at least one file (using the fzf-standard TAB key) before you type this. Vim will run once, with all your selected files as arguments.
-
:!vim %cAgain, you have to select at least one file using TAB. But this time, vim will run once per file; i.e., if you selected 3 files, vim will run 3 times.
While vim may not be a great example for this, some commands require
%cand can’t be used with%f. For example,tar -tvf %c | lesswould never work if you used%fand you selected multiple files. -
:!mv -v %f %d2; pauseApart from
%cand%f, there are 2 other macros.%d1is replaced by theSTART_DIR, and%d2byALT_DIR. This command moves selected files/dirs to the ALT_DIR, pausing after it is done so you can visually see what it did. (The defaultF6hotkey does exactly this, by the way).Note:
pauseis a fish function provided byfzfmfor convenience. See the Tips, tricks, and traps section for more on this.
You can’t have both %c and %f in the same command, so there’s no
ambiguity.
Command history
But you don’t have to keep re-typing these commands. fzfm has a command
history file ~/.config/fzfm/cmd_hist, which already contains many
commands, and you can add more yourself.
-
ctrl-r, pick command, hit Enter
When you hit ctrl-r, your current file/dir selections are saved, and you see a new fzf window with the contents of the command history file.
Pick one and hit enter. Done
-
ctrl-r, type new command, hit alt-n
This is what you do if the command you want doesn’t exist, and you would like to run it, but also add it to the command history file for later reuse.
If there is already a command that is almost what you want, but not quite, you don’t have to type the whole thing out. Filter or arrow down to the “almost there” command, and hit ctrl-g to “get” that line up to the query area, then edit it as needed before hitting alt-n.
Notes on the command history file:
- Commands are shown in decreasing order of frequency of use (no AI here;
just plain unix commands
sortanduniq). %c,%f,%d1, and%d2work the same as before.- On startup, if the history file does not exist, it is created and populated with a default set of commands. You are free to edit the file to add/change/remove commands, as well as edit the hotkeys and “colon-commands”; see later for more on this.
Changing the filelist
The default list of files and directories shown comes from running ls -AN --color=always in the current directory. But pretty much any command that
produces a list of files/dirs can be used.
Unlike the examples so far (i.e., commands that run on selected files), this
cannot be done using the :!some-command method.
To change the view, you must first hit ctrl-v, which will show you the
view history file ~/.config/fzfm/view_hist. At this point, you can:
- pick one of the commands and hit enter
- type an entirely new command and hit alt-n
- ctrl-g (see previous section) also works
Some examples I use frequently are:
fd -tf --newer 1hourto show files modified in the last 1 hourgit ls-files -mto show changed and deleted files in the repo
Everything else is similar to the previous section (“Running commands”). This
includes hotkeys, and the behaviour of %c, %f, %d1, and %d2, with one
exception: regardless of what macros you use, the command only runs once. For
example, this view command produces all the files under CWD which have the
same extension as the current selected file:
fd -e (path extension %c)
# runs "fd -e txt" if you're on a text file
But if you selected more than one file, this will still run just once, with the first file you selected, and ignore the others.
In contrast, here’s one where you select several files, and it finds files matching any of their extensions:
eval fd (path extension %f | sort -u | sed -e 's/^/-e /')
# runs "fd -e txt -e md -e pdf" if you selected one or more txt, md, and pdf files.
Hotkeys and “:” commands
Quick access to commands and views
TLDR: you can assign hotkeys to commands and views. Hotkeys are keys that
start with ctrl-, alt-, shift-, or function keys f1 to f12, or the
special key del. See the “AVAILABLE KEYS” section in the fzf man page for
more on this. You can also assign a colon-command short form for each
command.
Quick examples (details in the next 3 sections):
: key=alt-8 files modified in last 8 hours ; fd -H -tf --newer 8hours
You press the hotkey alt-8, and the magic happens :) alt-8 is recognised by fzf so it works directly. No need to hit ctrl-v, pick this line, and hit enter.
: key=:mr recursive, mtime descending, files only ; find -type f -printf '%TF.%TT\t%p\n' | sort -k1r | cut -f2
You type the colon-command :mr and hit enter.
But both hotkey and colon-command mechanisms first require that you know how to add a description to a command. So here goes…
Adding a description to a command
When you have too many commands that may look similar, it may be easier to select using a description. For example, take this command:
git log --name-only --format=%n | CDUP=(git rev-parse --show-cdup) perl -lne 'print if /./ and s(^)($ENV{CDUP}) and -f and not $seen{$_}++'
Offhand, it’s not obvious that this shows you the files in “most recently committed” order.
But add a description:
: git - files by most recent commit ; git log --name-only --format=%n | CDUP=(git rev-parse --show-cdup) perl -lne 'print if /./ and s(^)($ENV{CDUP}) and -f and not $seen{$_}++'
and now it’s not only easier to remember, it’s much easier to search using fzf (on my set of commands, “rec com” gets me this command straight away).
There’s no special trick here. All 3 major shells accept “:” as a kind of
no-op command that does nothing, even if it has arguments, so we just use
that. The “;” at the end of the description leads to the command that
actually gets executed. (You can’t use a “#” instead of the :; that would
make the entire line a comment).
Here’s another one: take each tar file and extract its contents into its own
directory. This examples also shows multiple commands chained, including
setting local variables. The %c ensures that it runs once per tar file
selected.
: unpack tar file into its own directory; set -l b (path basename -E %c); mkdir $b; tar -C $b -xvf %c
Adding a hotkey
So now you can see how we specify the hotkey:
: key=alt-8 files modified in last 8 hours ; fd -H -tf --newer 8hours
To the (fish) shell executing the command, the key=alt-8 is just part of our
“description”. But fzfm parses it as an instruction to assign that hotkey
to that command.
You can add hotkeys to either of the two history files, but you can’t add the same hotkey to both files.
Other examples from the defaults that come with fzfm:
# run (ctrl-r) examples
: key=del delete files ; ls -ald %f; pause "remove these files?" && rm -rf %f
: key=alt-up cd up ; cd ..
: key=alt-left cd prev dir ; prevd
: key=alt-right cd next dir ; nextd
Notice how you get a chance to eyeball the files/dirs being deleted before you hit enter. (Or hit n and enter to abort the rm).
And I particularly like how the alt-left and alt-right mimic the fish shell’s own shortcuts (and using the same keys too!)
The next section is about adding colon-commands, but you should also see hotkeys vs colon-commands for when to use which.
Adding a “colon-command” shortcut
Hotkeys are fine, and there’s no shortage of them; the fzf documentation indicates there are over a hundred key-combinations available, even if you don’t count common navigation keys and ctrl-c and so on.
But if you add more than a dozen hotkeys, you may find it difficult to remember them.
So, time to borrow something from vim and create our own :foo commands!
# view (ctrl-v) example
: key=:s size descending ; fd -d 1 -0 | xargs -0 du -sk | sort -rn | cut -f2
It is hard to say if this is faster than hitting ctrl-r/ctrl-v and picking the correct command to run. That would depend on how many similar commands existed, and how often you used them. For me, this is more deterministic, but YMMV, as they say.
WARNING: The one thing you must remember is
that in this mode, if the command expects a file (i.e., has %c or %f in
it), you have to use TAB to select the file even if you’re just selecting
one file. (If you know how fzf works, you know why this is).
Hotkeys vs colon-commands
When to assign a hotkey, and when to make do with a “colon-command”?
First, reread the warning at the end of the previous section: if the command expects arguments, you need to TAB-select if using a colon-command.
So the general guideline is: if it’s a frequently used command, or the
command expects arguments (i.e., contains %c or %f), give it a hotkey. If
it’s not frequently used, or the command does not expect arguments, give
it a colon command. Example:
# Works like vim's "yy" or "dd", then "p", but note the uppercase.
# You have to hit alt-shift-y/d not just alt-y/d.
: key=alt-Y ; set -U -- fzfm_yyddbuf cp -ai -- (path resolve -- %f)
: key=alt-D ; set -U -- fzfm_yyddbuf mv -i -- (path resolve -- %f)
: key=:p ; test -n "$fzfm_yyddbuf" && ls -ald -- $fzfm_yyddbuf[4..] && pause "$fzfm_yyddbuf[1..2] these files to PWD ?" && $fzfm_yyddbuf .
They’re modelled after vim’s yy/dd/p normal mode commands. The first two (copy/cut) need arguments, so they get hotkeys. The third one (paste) does not, so it gets a colon command, albeit a very short one.
But it’s not a hard and fast rule, and it’s entirely up to you. For example,
I deem chmod to be rare enough that even though it uses arguments, I didn’t
give it a hotkey. But cd .. is frequent enough to get one (and there are
many more such expections if you look in the sample files that get installed
when you first run fzfm).
And finally, commands that are even rarer, don’t get either; you have to hit ctrl-r or ctrl-v and pick them from the list.
Default Commands that come with fzfm
Several commands come with the code, as examples of how to use them. You’ve
seen some of them earlier, with explanations. You can see all of them in the
files cmd_hist and view_hist in the source repo, or, with your own
customisations and modifications, in the same files in ~/.config/fzfm.
Tips, tricks, and traps
Changing the view and the preview
You’ve already seen these, in this section:
fzfm -c 'du -sk *|sort -rn|cut -f2' # sort descending by disk usage
# ...OR...
fzfm -c 'rg -l pattern' -p 'rg -C5 --color=always pattern {}'
If you use commands like these often, you may have added them to your bashrc or similar. Another way is to add them to your cmd_hist, because then you can invoke them from within fzfm. The equivalents for those are:
: key=:sddu sort desc by disk usage ; set view_cmd "du -sk *|sort -rn|cut -f2" # ^^^^ ----------------------- : key=:myrg run rg with given pattern ; set view_cmd "rg -l $query"; set preview_cmd "rg -C5 --color=always $query {}" # ^^^^ -------------------------
Notes:
-
These are run by typing
:sddu, or:myrg some_pattern, and hitting enter. -
The parts marked by
^^^^(i.e., “sddu”, “myrg”) are your choice; use whatever mnemonic you like. -
The parts marked by the dashes are optional (as you already know, from here).
-
The first example is no different from adding
: key=:sddu sort desc by disk usage ; du -sk *|sort -rn|cut -f2
to the view history file, which is probably a better way to do this when you don’t also need to set a preview command.
Traversing the directory history
Just hit alt-h and it will show you a directory history. (This comes straight from fish; fzfm is not doing anything special here!)
If you first type something in the query field before hitting alt-h, it is used to pre-filter the list,
Pause after running a command
To pause after running a command, just end the command with ; pause.
You can add an optional message, e.g., pause "remove these files?". The
actual message will have [Y/n] appended. By convention, this means that
hitting enter implies y (the capitalised option). If you want the
opposite, hit n then enter.
You can reverse this by adding [y/N] in your message, e.g., pause "update source archive? [y/N]". Now hitting enter implies n and y
needs to be explicitly typed.
If you don’t want every occurrence of the command to pause for you to eyeball the output, you can use this trick to check it only when needed:
- Hit
alt-sto spawn a shell. You will see the output of the previous command just above the prompt. If your terminal has scollback, you should be able to use that also. - When done, hit
ctrl-dto get back tofzfm.
Colon commands don’t have to start with a “:”
(but please do start them with a “:”!)
All the colon commands we showed so far, and most of them in the shipped
default, are like key=alt-up (fzf recognises them as “expect” keys), or like
key=:cp, which means you make your selection using TAB, then type in :cp
and hit enter.
But, strictly speaking, you don’t need to start these “colon commands” with a “:”. For example, the command list that comes with fzfm has these:
: key=... cd up 2 levels ; cd ../..
: key=.... cd up 3 levels ; cd ../../..
However, for your sanity I recommend you start them consistently with a special character that is unlikely to be part of most file names.
(Why? Here’s a hypothetical scenario: You have a command called “md” for
“mkdir” but you rarely use it so you forgot you had it. One day you filter on
your markdown files (‘md’), and hit enter. Instead of opening that markdown
file in your editor, it tries to run mkdir).
Internal variables used
The command history file as well as the config file (~/.config/fzfm/cmd_hist
and ~/.config/fzfm/config, respectively) make use of some “internal”
variables to do certain things, which are not adequately documented right now.
goto_item
query
view_cmd
preview_cmd
FZFM_DEF_VIEW_CMD
FZFM_DEF_PREVIEW_CMD
selection
fzfm_mode
fzfm_mode_keys
fzfm_yyddbuf
Their values can be seen by hitting ctrl-d. Note that if no files are currently in the copy/cut buffer, the last variable will not show up.
$query is a particularly interesting one; see next item. fzfm_yyddbuf is
for copy/cut-paste; see that section for more.
Using $query in a command
Some commands include the word “$query”. These commands cannot be run by
hitting ctrl-r or ctrl-v (as the case may be) and picking the command to run.
They must be run directly from the main window, because the word $query
is replaced by the arguments you give to the command in the fzf query
window.
(This is one of the quirks of using fzf!)
-
This example is from the “view” list:
: key=:/ run locate ; locate -b -e -i $queryThe way you run it is you just type
:/ foo, and it will locate all the files whose basenames containfooand will show them in the main window. Thefoois the argument you are supplying, which can only come from the fzf query field. -
This example is from the “commands” list:
: key=:sa SET alt dir ; test -d "$query" && set ALT_DIR $query || set ALT_DIR $PWDThis is interesting because the argument is optional. If you type just
:saand hit enter, it sets ALT_DIR to PWD. If you give it an argument, e.g.,:sa /tmp, then it will make that the ALT_DIR.
using your own (fish) functions
Fzfm’s command and view history files, and ad hoc commands, can only be one line. Of course, you can srill do complex things, such as this:
: key=f2 rename file/s ; if test $FZFM_SEL_COUNT -gt 1; vidir %f; else; echo "rename: %f"; mv -vi %f (read -P "to....: " -c %f); pause; end
But it does get hard to maintain.
Luckily, you can add any number of functions to ~/.config/fzfm/config.
Those are examples that work for me; no doubt you’ll have your own.
A note about the language: These functions must be written in fish; indeed the whole file is “source”-d into the fzfm script.
However, if a given function has no need to touch fzfm’s internal variables, then it can be written as an external command, in any language you like.
Enabling hidden files
If the first argument is -., then the default view (file list) command (ls -N --color=always) gets a -A added so that hidden files become visible.
Curiously, this is not part of the main fzfm script; it happens in the
user-customisable ~/.config/fzfm/config file :-)
Shipped command and features
The cmd_hist and view_hist files come with a lot of commands. Most of them are self-explanatory, so you really should go through them to see what is available.
But there are some shipped features that are complex enough to need extra documentation. This is a loose collection of those.
copy/cut and paste files
To copy/cut then paste files:
- Select one or more files.
- Hit alt-shift-y to copy, alt-shift-d to cut.
- Notice the 4th line of the header changes to something like
cp: 3 filesormv: 4 files.
- Notice the 4th line of the header changes to something like
- Go to the target directory and type
:pand hit enter.- This works across invocations of fzfm
- Answer “y” or just hit enter on the prompt that comes up
- If you realised this is not the right destination, answer “n”, go to
the right directory, and try
:pagain.
Some other notes:
- The yy/dd buffer will be cleared if any fzfm instance (of the same
$USER) hitsEscape. - The buffer will be cleared after a successful paste operation.
- It will not be cleared if you answered “n” to the prompt asking to confirm the paste.
- The buffer will persist until one of the above two events happen.
You can even quit and re-run fzfm, and the 4th line will still show the
cp: ...ormv: ...message as a reminder. - To see the current contents, hit ctrl-d. See the internal variables section for more on that.
The example we used in the Hotkeys vs colon-commands section showed the code for this, by the way.
Bookmarking a directory
Setting a bookmark: Just cd to the right place and press alt-b. A bookmark will get added.
Actually, it bookmarks the directory and the file currently selected. You’d be surprised how nice it is to jump to a directory and be positioned at a specific file than at the top! (If you select multiple files and hit alt-b, only the last one gets added.)
Jumping to a bookmark: alt-j will show a list of existing bookmarks to pick from.
If you type a string of at least two characters before hitting that hotkey, it is used to pre-filter the list. This is usually the same as typing the string after you hit alt-j, but there’s a subtle difference if you type two words before hitting alt-j: that second word is used to find the cursor target within the directory you’re jumping to.
E.g., I type lite ins, then hit Alt-J. The lite would match
/home/sitaram/code/gitolite in my bookmarks file (the ins would be ignored
for the purposes of finding the entry in the bookmark file). After that it
would look for a file within that directory that matched ins, which is
“install”, and the cursor would land on that file.
If, instead, I typed lite con then hit alt-j, it would show me an fzf picker
to choose between contrib (a directory) and CONTRIBUTING (a file), to land
the cursor on.
(Note that none of this happens if you type Alt-J first, then type lite ins; it’s just a normal fzf query then).
Editing bookmarks: The colon-command :be (for bookmark edit) will open
up the bookmark file in your $EDITOR. You can use this to delete bookmarks
if needed.
Jumping to a Vim mark: Remember up above we said “If you type a string of
at least two characters”? If you type just one character and hit Alt-J, it
will convert that to uppercase and open up vim on that bookmark. It expects
the EDITOR environment variable is set to vim or gvim (I prefer gvim for
this specific use case).
:rg
:rg pattern will set the view command to rg -l, and the preview command to rg -C5 so that you can see matching lines, with 5 lines of context, on the right side.
:cds
The “:cds” command is “stolen” from vifm, my other favourite file manager, but with a couple of twists.
The main purpose is to go from, say, fin/2024/tax, to fin/2025/tax, with a “ctrl-right” key (that’s the key mapped to _fzfm_cds :next in cmd_hist). This is done by incrementing the last sequence of digits in the directory name. Similarly, decrement instead of increment (mapped to ctrl-left in cmd_hist)
The other way to use it, is to replace “old” with “new” in the directory name to change directories. Fzfm has a few more tricks here though:
- allow partial names
- be relaxed about the exact new path being available
This works by replacing the last occurrence of “old”, and making any adjustments needed to get to the deepest directory that satisfies the change.
Example: if you have the following directories (notice the asymmetries; fin has 2022 but no 2025, and med/2023 has “reports” instead of “PDFs”):
fin/2022/PDFs/
fin/2023/PDFs/
fin/2024/PDFs/
medical/2023/reports/
medical/2024/PDFs/
medical/2025/PDFs/
Then, for each of them, here’s the result of running the command.
PWD COMMAND NEW PWD
fin/2022/PDFs/ :cds fin med medical
fin/2023/PDFs/ :cds fin med medical
fin/2024/PDFs/ :cds fin med medical/2024/PDFs
medical/2023/reports/ :cds med fin fin
medical/2024/PDFs/ :cds med fin fin/2024/PDFs
medical/2025/PDFs/ :cds med fin fin
Also notice we used “med” not “medical”, but even a partial name works.
Toggle reversing a list with alt-r
Alt-r will toggle between the list as your view_cmd presents it, and the reverse. This is achieved by the simple expedient of appending | tac to the command, if it is not already present, and removing it if it is.
(2026-10-08) Directory specific view/preview commands
As you know by now, Fzfm has any number of ways to view a file list. Sometimes you want a specific view always applied to a specific directory. Automatically, without having to type in any commands.
My actual use cases for this:
- For some directories, I’d like to see the most recently edited files at the top.
- Some directories (e.g., “fin”, “med”, …) contain subdirectories 2024, 2025, 2026, and so on. I’d like the listing to show up in reverse (i.e., 2026 at the top, because that’s where all the action is and it’s boring to scroll). (Also see footnote “year first” below).
- My student-project reviews directory has files named after the year, and apart from showing 2026 first, as above, I also want the preview to show a sorted list of second level markdown headings. Those are the names of the projects, and that’s more interesting in a preview than seeing several lines about whatever happens to be the first project in the file!
(You could argue that the second example is a special case of the first. But it’s not exactly the same. You could also argue that this is sheer laziness, to avoid typing two characters and hitting enter. I’d ask you to lookup “three great virtues of a programmer”, and remember I’m a perl guy at heart.)
Using this feature is simple. Just set whatever view and preview you want in the current directory, then type in :svp and hit Enter. That’s it. Next time you cd to this directory, the view will change.
Specifically, using the 3 examples above:
-
Type
:m, hit enter, type:svp, hit enter. (:mis pre-defined in the shippedview_histfile asls -t --color=always) -
Type
:r, hit enter, type:svp, hit enter. (alt-ris pre-defined in the shippedcmd_histfile; look it up) -
This is a bit more complicated. Type
:!set preview_cmd "rg --color=always '^##' {} | LANG=C sort -f"hit enter, check that the output is what you wanted, then type
:svpand hit enter.
Notes:
- You can do this any number of times for a given directory. The last setting is what will be in effect.
- There is no built in way to delete dir-specific view/preview for a given directory. You’d have to edit
~/.config/fzfm/dband manually delete three lines. (It’s a simple text file in the git-config format.) - If you would like to see the default view and/or preview, you can hit Escape. Escape toggles between the default view/preview and the directory-specific view/preview.
Important
One caveat: Directory specific view and preview commands will take effect only if the current view and preview commands are both set at the default. If the current view or preview are not defaults and you navigate to a directory which has directory specific view and preview commands defined for it, they will not take effect. The idea is that the “temporary/ad hoc” custom view you specify at runtime takes precedence over a “permanent” directory specific view/preview.
Footnotes
“year first”
What I actually do, for this, is that I have a simple fish function called year_first (this is not shipped with fzfm; it’s part of my personal fzfm config):
function year_first begin ls | rg '^20\d\d' | tac ls | rg -v '^20\d\d' end | xargs_ls end
and my view command for those directories is just years_first.
The xargs_ls function is shipped with fzfm, and it’s definition is
function xargs_ls # This is useful to pipe to at the end of any custom view command, and it # became tedious to apply it everywhere. xargs -r -d\n ls -U -d -N --color=always # Note especially the "-U" end
Demos and documentation for modes
Demos
These two show (show off?) what makes fzfm really unique. They don’t have a lot of how to use it; they’re more like teaser demos on what you can do if you decide to start using fzfm.
- The dynamic filelist
- The preview window
- This one shows the “collapse” function, triggered by
alt-shift-_; it was easier to make that little demo than to explain without it :)
Modes
Just go through each of these one by one, in that order.
Or at least just the first one.
- Modes – intro using the “dirdiff” mode
- “todo.txt” mode – this is the part where we take a sharp turn away from being a file manager :)
- “git” mode
- Drilling down into “git log -G” – this is something that works only within git mode, but it is significant enough that I gave it its own page and demo.
The Dynamic file list
This is the first of my “not documentation but just show the features” articles about fzfm.
Most file managers show a list of files in the current directory. Some file managers can show files produced by a command. But these commands run once, and the list does not change as you operate.
In fzfm, even the initial file list that you see by default, is generated by
an external program (in this case ls -N --color=always). In fact fzfm
doesn’t do any file/dir operations.
The file list can be built by any command! And every time fzfm renders the
main window, the command is re-run. (This makes it very different from
vifm’s %U and mc’s “external panelise”, for those who know those tools).
And since fzfm is just showing the output of a command, does the output have to be actual files?. For some mindblowing examples, see:
- managing a “todo.txt” file
- drilling down into “git log -G”
- (one more coming soon…)
Example 1 – working on TODOs left in files
I tend to pepper my files with TODO items, so when I find time I try to go over them and resolve as many as I can.
In the demo below, you can see the list dwindle as I take care of each item.
Example 2 – largest files
I use gdu frequently, but it is limited in what you can do with the files it
shows. You can’t move them elsewhere, zip them up, or so many other things
that a file manager could do.
The Preview window
The preview window here is what FZF provides; there’s not a lot of magic added
to it except to use it imaginatively. You can use pretty much any code that
uses {} etc.
Best example is this, but you will perhaps not appreciate it unless you are somewhat familiar with the todo.txt format.
A simple example of a trivial addition to the preview mode is when you type
# and hit enter, you get to see the xxh128sum of the file also:
The code for that is one line in the command history file:
: key=# see file hash also ; toggle_xsum
and this function in the config file:
function toggle_xsum set -l p $preview_cmd set -l x "test -f {} && xxh128sum {}|rg --color=always -o '^.{ 32}' ; " set p (string replace $x '' $p) || set p "$x$p" set preview_cmd $p true end
Another example is git mode, which uses it to show git diff, git log, etc.
Collapsing a list
Let’s say we’re looking into all the pieces of gitolite related to “perms”. Running rg -l perms, or anything similar, will get you a flat list of files. We’d like to see those collapsed into a tree.
In effect, it’s like looking at a subset of the tree.
The demo below shows this for the “perms” example, but any “view” that results in showing files from all over the directory can be useful to collapse for easier understanding and navigation. For example, if you’re in your mp3 directory, you can focus on just the longest tracks by using :!set view_cmd "fd -tf -S +50M" (or whatever), and then collapse that list as shown in the demo.
And the code for this is, other than boilerplate/housekeeping, is basically 3 lines:
eval $_fm_coll_source_view_cmd | path filter -f | sed -e 's/^\.\///' | rg / | cut -f1 -d/ | sort -u | xargs_ls
echo
# top level files
eval $_fm_coll_source_view_cmd | path filter -f | sed -e 's/^\.\///' | rg -v / | xargs_ls
echo
# rest of the files
eval $_fm_coll_source_view_cmd | path filter -f | sed -e 's/^\.\///' | rg / | xargs_ls
Fzfm’s “modes”
Fzfm has “modes”; I’ll explain what and how using the “dirdiff” mode as an example.
The “dirdiff” mode is essentially a TUI wrapper around the output of diff -qr dir1 dir2
General intro to modes
Fzfm modes are useful to provide specific functionality that you may not need all the time.
- You enter a mode by typing in the mode name or short name (e.g.,
dirdiff, orddfor short), and hittingalt-m. This performs some sort of initialisation, and then the display changes.- You can also directly enter a mode while starting fzfm. E.g., the Gif
below uses
fzfm -m dd dir1 dir2.
- You can also directly enter a mode while starting fzfm. E.g., the Gif
below uses
- When you enter a mode, a mode indicator appears on the fifth line. In
addition, some other information might appear. For example, in the
dirdiff mode you will see three numbers prefixed by
d,l, andr.- They are, respectively, the number of files which are
different, unique to theleft side, and unique to theright side.
- They are, respectively, the number of files which are
- Most modes have their own commands and hotkeys which only work within that
mode. A command must be followed by
alt-m. Hotkeys stand on their own.- Hotkeys example: in “dirdiff” mode,
alt-mruns vimdiff of the left file and the right file. In “git” mode,alt-ptoggles between showing and not showing--patchin the git log output. - Commands example: in “dirdiff” mode, typing
d, thenalt-mwill show you only the files that are different on both sides. In “git” mode,mrc+alt-mshows all files in most recently committed order.
- Hotkeys example: in “dirdiff” mode,
- Most modes also respond to
helporh(+alt-m) to show you a brief help. - Some modes may also respond to
F5to refresh the mode. This reruns the initialisation (in “dirdiff”, this would rerun thediff -qrcommand), and also resets the display to the default. - Finally, you exit a mode by hitting Escape (not shown in Gif below).
Here’s a brief Gif that shows off all this.
Fzfm’s “todo.txt” mode
This is another of those “why are we still calling it a file manager?” things :-)
A lot more detail is in my blogpost at https://hode.gitolite.com/blog/2026-09-12-fzfm-todotxt-mode.html so I won’t repeat it here.
Here’s the main window when you run fzfm -m t (or fzfm -m todotxt), and
then hit the down-arrow key a few times:
Notice the mode indicator on the fifth line.
The extra numbers at the beginning of each entry are:
- number of days before the task is due (negative means overdue); and this is what it is sorted by. I find this easier than mentally computing date differences from today :)
- the line number of the entry within the file, so when you hit
alt-eit opens vim at that line
The preview window is where things get interesting. It shows you all the lines in the file which contain either
- the same project as the current line (e.g.,
@tech,@birthdays) - the same context as the current line (e.g.,
+amrita) - or any of the words in the current line which start with an uppercase letter (e.g. “Prof” in line 6, “July” in line 19)
It’s pretty nice to see all other birthdays when you focus on a line with the
@birthday context, for instance.
And the code…?
This is the code for initialising the mode:
case "init" set view_cmd "todo" set preview_cmd "echo {} | rg -o -w -e 'due:20..-..-..' -e '@\S+' -e '\S*\+\w\S+' -e '[A-Z]\w+' | rg -v 'rec:\+' | rg -n -w --color=always -F -f - $TODO_FILE" set preview_window "bottom" set -g fzfm_mode_keys alt-e,alt-S
This is what runs when you hit alt-e on a line, to edit the todo file at that line:
case "alt-e" # edit current or first selection echo $selection[1]|read dummy1 n dummy2 vim $TODO_FILE +$n
Oh and I forgot to mention that there’s also a quick-and-dirty “sort the file in-place by due date” feature:
case "alt-S" vim $TODO_FILE -c 'so '(printf %s\n "sort i" "sort /due:/" "g/due:/move 0" "g/due:/move 0" wq | psub) # I challenge you to do this easier in any other language :-)
That’s it. Other than boilerplate, there’re basically 9 lines of code!
The “todo” script
This is not part of fzfm. It’s my replacement for https://github.com/todotxt/todo.txt-cli/blob/master/todo.sh, which I consider useless because it can’t even sort by due date!
Here’s my “todo” script, which I only use for display. Everything else I do within vim.
#!/bin/fish function sort_by_days_left perl -MTime::Piece -e ' my $today = localtime; open(STDOUT, "|-", "env LANG=C sort -f -k1,1n -k3 | sed -e \"s/^9999/ -/\""); while (<>) { $ln = sprintf "%3d", $.; unless (/due:(20..-..-..)/) { print "9999 $ln $_"; next; } my $d2 = Time::Piece->strptime($1, "%Y-%m-%d"); my $days = int($d2->epoch/86400) - int($today->epoch/86400); printf "%4d $ln %s", $days, $_; } ' end set rgn --passthru --color=always --colors=match:style:nobold set rgb --passthru --color=always --colors=match:style:bold set l (string length (wc -l < $TODO_FILE)) # set perlexp '$x=$.++; printf "%0'$l'd ", $x' set to cat test -t 1 && set to less # perl -pe $perlexp < $TODO_FILE | sort_by_days_left < $TODO_FILE | rg $rgn --colors=match:fg:160,160,160 -e '^ *\d+ +\d+ ' | rg $rgb --colors=match:fg:red -e '\(A\).*' | rg $rgb --colors=match:fg:yellow -e '\(B\).*' | rg $rgb --colors=match:fg:green -e '\(C\).*' | rg $rgn --colors=match:fg:115,155,235 -e '\(W\).*' | rg $rgn --colors=match:fg:155,0,235 -e '\(Z\).*' | rg $rgb --colors=match:fg:cyan -e '\S+:\S+' | rg $rgn --colors=match:fg:160,0,160 -e '@\S+' | rg $rgn --colors=match:fg:225,105,0 -e '\+\S+' | $to
“git” mode
The Gif below (just over a minute long) shows fzfm’s “git” mode. However, it’s probably useful to have some notes as a refresher, so you don’t have to watch the gif if you don’t want to.
Basics
cdto a directory that contains a git repo, and typeg+alt-mto get into git mode.- A “mode” indicator appears, showing
mode: git, on the 5th line.- After that you get 4 numbers in colors; the count of deleted files is
in. red, modified in yellow, new (untracked) in green, and ignored in
blue.
- Sadly, at the moment, this does not show staged file counts, only un-staged.
- After this is the branch name.
- After that you get 4 numbers in colors; the count of deleted files is
in. red, modified in yellow, new (untracked) in green, and ignored in
blue.
- The preview changes to a
git log --statfor the file or directory the cursor is on. Toggle between that and the normal view usingalt+m.- You can hit
alt-pto toggle--patchin the preview window.
- You can hit
- If the cursor is on a file that has un-staged changes, then the preview
window shows a
git diffbefore thegit log --stat.- Scrolling the previews depends on how you’ve set your
FZF_DEFAULT_OPTIONS
- Scrolling the previews depends on how you’ve set your
Changing the view
- You can change the view by typing a command and then hitting
alt+m.alt-pstill works to toggle--patchin any of these views.
icommand shows you ignored files- Similarly
mfor modified,dfor deleted, andnfor new allshows all files.mrcshows all files in most recently committed order.- I find myself using this mode quite often; I may add a hot key for it.
- See below for how
F5resets the view to the default
Further customisation (not shown in the Gif)
-
To add a hotkey for a frequently used sub-command (e.g., in my case,
mrc), add something like this to~/.config/fzfm/cmd_hist: key=alt-R ; set fzfm_mode; fzfm_mode_handler g; fzfm_mode_handler mrcThen, pressing
alt-shift-rwill switch to git mode and get you the “most recently committed files” view.At present it’s not possible to add a hotkey only for a mode without touching the source code for the mode.
Drilling down into “git log -G”
When you run git log -G <some pattern> -i, you get a list of refs where this
pattern was found in a change (delete or insert).
A slightly more user-friendly way is to use gitk -G<pattern> -i, but even
then, it doesn’t quite do the job – it always behaves as if --pickaxe-all
was specified.
Here’s fzfm’s take on this:
Type g queryrc when you’re already in git mode, and
hit alt_m, you get the first frame of this GIF:
- The first commit (b89…) has 3 files changed, of which 2 are affected, which is not too bad.
- You can see the diff for a particular file by scrolling to it.
- The next commit (60e…) has 21 files changed, of which only 2 files have a change that includes the pattern you’re interested in. (Also notice the 2752 at top right; this is the full log for the commit. Way too much info when you’re looking for a single pattern!)
- But again, you can scroll to an individual file to see just that file’s diff.
There are a couple of other things you can do here. First of all, each file
you see there, if it still exists in the currently checked out branch, can be
edited, moved, renamed, whatever. More importantly, if you hit tab on a
commit line, then tab on a file under that commit, and then alt-v, you can
see the file as it was at that commit.
I may add more features later, but so far, this is good enough for me. None of the open source TUI tools I looked at would do this. (Among the GUIs, I am told some of the proprietary ones can also do this, but I wouldn’t know about those).