Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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
  • 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 1hour changes 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

  • 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 (or todotxt) – 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_hist file

      : key=alt-t todotxt mode          ; fzfm_mode_handler t
      
  • dd (or dirdiff) – differences between 2 directories

  • g (or git) – 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: fzf and fish
  • the :help feature: bat and less
  • 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 mode to 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 -m option 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).

  1. fzfm: Sets PWD, START_DIR and ALT_DIR all the same.
  2. fzfm dir: Does a cd to dir, then same as previous usage.
  3. fzfm dir1 dir2: Does a cd to dir1, sets PWD and START_DIR to dir1, sets ALT_DIR to dir2
  4. fzfm 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
  5. 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.

  • fzf standard 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 cd to 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
  • 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 -td and fd -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 fd becomes fd -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 -HI instead.
  • 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).


  1. Fish’s argparse doesn’t like non-alphanumeric options, so we have to get that out of the way :-) ↩

  2. 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 -e

    This is a command that does not require any file/dir arguments, and so it just runs in the current directory.

  • :!vim %f

    You 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 %c

    Again, 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 %c and can’t be used with %f. For example, tar -tvf %c | less would never work if you used %f and you selected multiple files.

  • :!mv -v %f %d2; pause

    Apart from %c and %f, there are 2 other macros. %d1 is replaced by the START_DIR, and %d2 by ALT_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 default F6 hotkey does exactly this, by the way).

    Note: pause is a fish function provided by fzfm for 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 sort and uniq).
  • %c, %f, %d1, and %d2 work 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 1hour to show files modified in the last 1 hour
  • git ls-files -m to 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-s to 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-d to get back to fzfm.

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 $query
    

    The way you run it is you just type :/ foo, and it will locate all the files whose basenames contain foo and will show them in the main window. The foo is 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 $PWD
    

    This is interesting because the argument is optional. If you type just :sa and 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 files or mv: 4 files.
  • Go to the target directory and type :p and 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 :p again.

Some other notes:

  • The yy/dd buffer will be cleared if any fzfm instance (of the same $USER) hits Escape.
  • 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: ... or mv: ... 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:

  1. For some directories, I’d like to see the most recently edited files at the top.
  2. 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).
  3. 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:

  1. Type :m, hit enter, type :svp, hit enter. (:m is pre-defined in the shipped view_hist file as ls -t --color=always)

  2. Type :r, hit enter, type :svp, hit enter. (alt-r is pre-defined in the shipped cmd_hist file; look it up)

  3. 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 :svp and 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/db and 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.

Modes

Just go through each of these one by one, in that order.

Or at least just the first one.

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:


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, or dd for short), and hitting alt-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.
  • 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, and r.
    • They are, respectively, the number of files which are different, unique to the left side, and unique to the right side.
  • 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-m runs vimdiff of the left file and the right file. In “git” mode, alt-p toggles between showing and not showing --patch in the git log output.
    • Commands example: in “dirdiff” mode, typing d, then alt-m will show you only the files that are different on both sides. In “git” mode, mrc + alt-m shows all files in most recently committed order.
  • Most modes also respond to help or h (+ alt-m) to show you a brief help.
  • Some modes may also respond to F5 to refresh the mode. This reruns the initialisation (in “dirdiff”, this would rerun the diff -qr command), 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-e it 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

  • cd to a directory that contains a git repo, and type g + alt-m to 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.
  • The preview changes to a git log --stat for the file or directory the cursor is on. Toggle between that and the normal view using alt+m.
    • You can hit alt-p to toggle --patch in the preview window.
  • If the cursor is on a file that has un-staged changes, then the preview window shows a git diff before the git log --stat.
    • Scrolling the previews depends on how you’ve set your FZF_DEFAULT_OPTIONS

Changing the view

  • You can change the view by typing a command and then hitting alt+m.
    • alt-p still works to toggle --patch in any of these views.
  • i command shows you ignored files
  • Similarly m for modified, d for deleted, and n for new
  • all shows all files.
  • mrc shows 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
  • F5 resets 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 mrc
    

    Then, pressing alt-shift-r will 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).