Skip to content
neeshy edited this page Nov 6, 2024 · 200 revisions

Note

The following tips assume that the ifs option is set to "\n", the shell option is set to a POSIX compatible shell, and the XDG_CONFIG_HOME and XDG_DATA_HOME environment variables are either unset or set to their default values (according to the XDG Base Directory Specification). You may need to adjust these to your setup accordingly.

Note

Because of the nature of lf -remote, in many of the examples below sed is used as a means of escaping special characters. This is necessary for arguments which contain single or double quotes, a backslash, or even a newline. If you find yourself with repeated uses of sed in your configuration, consider using the following script instead:

#!/bin/sh
exec sed -z 's/\\/\\\\/g;s/"/\\"/g;s/\n/\\n/g;s/^/"/;s/$/"/'

Name the script lf-escape and use it like so:

lf -remote "send $id select $(printf '%s' "$file" | lf-escape)"

This has the advantage (over the command used in many of the examples) of correctly escaping newlines and automatically adding quotes around the given argument. This was omitted from the sed commands below for the sake of brevity. If, for whatever reason, you have filenames containing newlines on your system (unlikely but possible), consider using the above script.

Tip

For shell scripting, you can use https://shellcheck.net/ to check for common errors.

  • Use a shebang of #!/bin/sh for POSIX sh scripts.
  • Use a shebang of #!/bin/bash for bash scripts.

Configuration

Edit and reload config file

It is possible to assign a keybinding for editing and reloading the config file. This is convenient especially when you are making config changes as a new user:

cmd edit-config ${{
    $EDITOR ~/.config/lf/lfrc
    lf -remote "send $id source ~/.config/lf/lfrc"
}}

map C edit-config

Start with default settings (ignore user configuration)

It is possible to start lf with the default settings by passing a dummy blank file for the -config option, which will prevent the user config file lfrc from being loaded:

lf -config /dev/null

Use environment variables in command or mapping

You can't use environment variables directly in mappings and internal commands. In order to utilize environment variables, lf -remote must be called. For example, here's a configuration line which maps gG to cd $GOPATH:

map gG $lf -remote "send $id cd $GOPATH"

It is possible to wrap this in a utility command which uses eval to expand the environment variables and then invoke lf -remote to send the result back to lf:

cmd eval &{{
    cmd="send $id"
    for arg; do
        cmd="$cmd $(eval "printf '%s' \"$arg\"" | sed 's/\\/\\\\/g;s/ /\\ /g;s/"/\\"/g')"
    done
    lf -remote "$cmd"
}}

This makes it possible to use environment variables in settings:

eval set previewer "$XDG_CONFIG_HOME/lf/previewer"
eval set cleaner "$XDG_CONFIG_HOME/lf/cleaner"

Environment variables can also be used in commands:

eval cd "$HOME/Documents"

Command substitution works too:

eval echo "$(date)"

Support for custom profiles/themes

Although lf doesn't directly support profiles/themes, it is possible to emulate this by placing the profile settings inside separate files and loading them using the source command. The profiles can be placed in a dedicated directory such as ~/.config/lf/profiles.

~/.config/lf/profiles/foo:

set info size
set promptfmt "\033[32m%u\033[34m%d\033[0m%f"
set ratios 1:1:1

~/.config/lf/profiles/bar:

set info time
set promptfmt "\033[36m%d\033[33m%f"
set ratios 1:1:2

lfrc file:

cmd profile &lf -remote "send $id source ~/.config/lf/profiles/$1"

With this, it should be possible to switch profiles using profile foo and profile bar. You may want to additionally create a profile with the default settings and source it before switching profiles, so that settings from the previous profile are not retained.

Configuration dependent on version

You might want to check your .lfrc file into source control, or otherwise share it between machines with different versions of lf installed. If you do this, you might run into a situation where you want to set options differently dependent on the version of lf being used.

For example, set cursorpreviewfmt is not supported on lf prior to version 29. You can work around this with some code in your .lfrc like this:

${{
    if [ "$(lf --version)" -ge 29 ]; then
        # See https://github.com/gokcehan/lf/issues/1258
        lf -remote "send $id set cursorpreviewfmt \"\\033[7m\""
    fi
}}

This will create a custom command which checks the version of lf, runs logic conditionally using remote commands, and then runs that command immediately.

Configuration dependent on OS

Similarly to the tip above, you can also set configuration parameters based upon the operating system you are running on. As an example, this code snippet sets a mapping that yanks the current path differently, depending on whether you are using Linux or MacOS:

${{
    if [ "$(uname -s)" = Darwin ]; then
        lf -remote "send $id map Y \$printf '%s' \"\$fx\" | pbcopy"
    else
        lf -remote "send $id map Y \$printf '%s' \"\$fx\" | xclip -selection clipboard"
    fi
}}

Since $id seems unset on Windows, a work-around for Windows could be:

source ~/.config/lf/win32.lfrc
$lf -remote "send $id source ~/.config/lf/unix.lfrc"

Note

On UNIX OSes both files are sourced. Therefore, it is advised to override the relevant options in unix.lfrc.

Appearance

Enable Borders

If you want to have borders around the columns:

set drawbox

Dual pane single column mode

If you are used to file managers with single column directory views (e.g. midnight commander or far manager), there are a few settings you can use to get a similar feeling:

set nopreview
set ratios 1
set info size:time

If you also want to have dual panes, you need to utilize a terminal multiplexer or your window manager. For tmux you may consider using a shell alias to automatically create dual panes:

alias mc='tmux split -h lf; lf'

Alternatively, you can accomplish the same thing by dynamically defining lf's startup configuration like so:

mc() {
    cmd='lf -config <(cat ~/.config/lf/lfrc; printf "set %s\n" nopreview "ratios 1" "info size:time")'
    tmux split -h "$cmd"; eval "$cmd"
}

This has the advantage of not needing to modify your main lf configuration.

Dynamically set number of columns

You can configure the on-redraw hook command to set the number of columns based on the current terminal width:

cmd on-redraw %{{
    if [ "$lf_width" -le 80 ]; then
        lf -remote "send $id set ratios 1:2"
    elif [ "$lf_width" -le 160 ]; then
        lf -remote "send $id set ratios 1:2:3"
    else
        lf -remote "send $id set ratios 1:2:3:5"
    fi
}}

Toggling the preview column

Although previews can be toggled using the preview option, the number of columns displayed remains the same, which results in the directories being shifted to a different column. To prevent the directories from being shifted, the number of columns also adjusted when toggling the preview:

cmd toggle-preview %{{
    if [ "$lf_preview" = true ]; then
        lf -remote "send $id :set preview false; set ratios 1:5"
    else
        lf -remote "send $id :set preview true; set ratios 1:2:3"
    fi
}}

map zp toggle-preview

Note

The order of commands can matter. For example, preview cannot be enabled if ratios contains only a single number, so if ratios is currently set to 1, then you must use set ratios 1:1; set preview true instead of set preview true; set ratios 1:1.

Changing the preview directory cursor

Since release r29, lf defaults to using an underline for the cursor in the preview pane, in order to distinguish it from the active cursor. This is controlled by the cursorpreviewfmt option.

Here are a few examples for different values of the option:

underline (default) pre-r29 behavior grey cursor no cursor
set cursorpreviewfmt "\033[4m" set cursorpreviewfmt "\033[7m" set cursorpreviewfmt "\033[7;90m" set cursorpreviewfmt ""
image image image image

This uses ANSI codes, see for example this reference.

Navigation

Move up/down in the parent directory

Although there is no native support for moving up/down in the parent directory, this can be achieved by chaining commands:

map J :updir; down; open
map K :updir; up; open

There is a caveat where if the next entry in the parent directory is a file, then it will be selected and opened instead. However, the dironly option can be used to prevent a file from being selected:

cmd move-parent &{{
    dironly="setlocal \"$(dirname -- "$PWD" | sed 's/\\/\\\\/g;s/"/\\"/g')\" dironly"
    lf -remote "send $id :updir; $dironly true; $1; $dironly false; open"
}}

map J move-parent down
map K move-parent up

Follow symbolic links

You can define a command to follow symbolic links:

cmd follow-link %{{
  lf -remote "send $id select \"$(readlink -- "$f" | sed 's/\\/\\\\/g;s/"/\\"/g')\""
}}

map gL follow-link

Select all files or directories in the current directory

cmd select-type &{{
    set -f
    [ "$#" -eq 0 ] && exit
    files="$(
        find "$PWD" -mindepth 1 -maxdepth 1 -type "$1" $([ "$lf_hidden" = false ] && printf '%s\n' -not -name '.*') -print0 |
        sed -z 's/\\/\\\\/g;s/"/\\"/g;s/\n/\\n/g;s/^/"/;s/$/"/' |
        tr '\0' ' ')"

    lf -remote "send $id :unselect; toggle $files"
}}

cmd select-dirs select-type d
cmd select-files select-type f

Go to the first file in the current directory

cmd goto-file &{{
    if [ -n "$(find -mindepth 1 -maxdepth 1 -type d -print -quit)" ]; then
        lf -remote "send $id :set dironly; bottom; set nodironly; down"
    else
        lf -remote "send $id top"
    fi
}}

map M goto-file

This is useful on its own, however, it can also be combined with invert-below to accomplish something similar to the above section:

map V invert-below

Selecting all files can be accomplished by chaining M together with V. Then, you may invert the selection with v to select directories instead.

Using incfilter to select and open files

The incfilter option is normally used for providing a preview of filtered files for the filter modal command. However this can also be used as a file selector (similar to fzf) for opening files.

set incfilter

map f filter

cmap <enter> &{{
    # select and open file
    if [ "$lf_mode" = filter ]; then
        lf -remote "send $id :cmd-enter; setfilter; open"
    else
        lf -remote "send $id cmd-enter"
    fi
}}

cmap <a-n> &{{
    # go to next file
    if [ "$lf_mode" = filter ]; then
        lf -remote "send $id down"
    fi
}}

cmap <a-p> &{{
    # go to previous file
    if [ "$lf_mode" = filter ]; then
        lf -remote "send $id up"
    fi
}}

Copy/paste

Clear selected files after copying and pasting

By default, the list of selected files is cleared after executing cut followed by paste, but the list is retained after copy followed by paste. This matches the behavior of a number of other file managers. To ensure the selected files are cleared after copy/paste, you can add the clear command:

map p :paste; clear

Copy or move files on the client instead of the server

GNU cp has the default option: --reflink=auto, which performs a lightweight copy when possible. Hence, these mappings (and some of the ones below) have the advantage of utilizing CoW when this feature is available on the filesystem.

cmd paste %{{
    set -- $(cat ~/.local/share/lf/files)
    mode="$1"
    shift
    case "$mode" in
        copy) cp -ri -- "$@" .;;
        move)
            mv -i -- "$@" .
            lf -remote "send clear"
            ;;
    esac
}}

The following snippet has the additional advantage of handling name collisions, and gracefully falling back onto lf's native :paste when CoW isn't available (so that the progress printout is kept).

cmd paste-cow &{{
    set -- $(cat ~/.local/share/lf/files)
    mode="$1"
    shift
    case "$mode" in
        copy)
            if [ "$#" -eq 0 ]; then
                lf -remote "send $id echoerr paste-cow: no file in copy/cut buffer"
                exit
            fi

            first="true"
            for file in "$@"; do
                name="$(basename -- "$file")"
                orig="$name"

                count="0"
                while [ -e "$name" ]; do
                    name="$orig.~$((count += 1))~"
                done

                # Use -n instead of --update=none-fail for now (in case of TOCTOU)
                # Upstream bug: https://bugs.gnu.org/70727
                # The fix is unreleased as of writing
                if ! out="$(cp -an --reflink=always -- "$file" "./$name" 2>&1)"; then
                    if [ -n "$first" ]; then
                        # It's only safe to fallback to lf's own paste on the first iteration
                        lf -remote "send $id paste"
                    elif [ -n "$out" ]; then
                        lf -remote "send $id echoerr \"$(printf '%s' "$out" | sed 's/\\/\\\\/g;s/"/\\"/g')\""
                    else
                        lf -remote "send $id echoerr cp: failed"
                    fi
                    exit
                fi
                first=""
            done

            lf -remote "send $id echo \"\\033[0;32mCopied successfully (reflinked)\\033[0m\""
            ;;
        move) lf -remote "send $id paste";;
    esac
}}

# use a different command name to avoid shadowing :paste
map p paste-cow

Here's yet another version which will selectively fallback to lf's native :paste command on a file-by-file basis:

cmd paste-cow &{{
    set -- $(cat ~/.local/share/lf/files)
    mode="$1"
    shift
    case "$mode" in
        copy)
            if [ "$#" -eq 0 ]; then
                lf -remote "send $id echoerr paste-cow: no file in copy/cut buffer"
                exit
            fi

            fallback=""
            printf 'copy\n' > ~/.local/share/lf/files
            for file in "$@"; do
                name="$(basename -- "$file")"
                orig="$name"

                count="0"
                while [ -e "$name" ]; do
                    name="$orig.~$((count += 1))~"
                done

                if ! out="$(cp -an --reflink=always -- "$file" "./$name" 2>&1)"; then
                    fallback="true"
                    printf '%s\n' "$file" >> ~/.local/share/lf/files
                    # this line may spam your screen, if so comment it out
                    [ -n "$out" ] && lf -remote "send $id echoerr \"$(printf '%s' "$out" | sed 's/\\/\\\\/g;s/"/\\"/g')\""
                fi
            done

            if [ -n "$fallback" ]; then
                lf -remote "send $id :sync; paste; clear"
            else
                lf -remote "send $id :clear; echo \"\\033[0;32mCopied successfully (reflinked)\\033[0m\""
            fi
            ;;
        move) lf -remote "send $id paste";;
    esac
}}

Make backups when copying or moving

Unfortunately POSIX cp and mv commands do not define an option to backup existing files, but if you have the GNU implementation, you can define a custom paste command to use --backup option with these commands:

cmd paste %{{
    set -- $(cat ~/.local/share/lf/files)
    mode="$1"
    shift
    case "$mode" in
        copy) cp -r --backup=numbered -- "$@" .;;
        move)
            mv --backup=numbered -- "$@" .
            lf -remote "send clear"
            ;;
    esac
}}

See man cp or man mv for more information.

Copy or move files asynchronously

You can define an asynchronous paste command to do file copying and moving asynchronously:

cmd paste &{{
    set -- $(cat ~/.local/share/lf/files)
    mode="$1"
    shift
    case "$mode" in
        copy) cp -rn -- "$@" .;;
        move)
            mv -n -- "$@" .
            lf -remote "send clear"
            ;;
    esac
}}

Tip

You can also define this command with a different name (e.g. cmd paste-async &{{ .. }}) and then bind it to a different key (e.g. map P paste-async) to use it selectively.

Show progress when copying or moving

You can use an alternative file copying program that provides progress information such as rsync and feed this information to lf to display progress while coping files:

cmd paste &{{
    set -- $(cat ~/.local/share/lf/files)
    mode="$1"
    shift
    case "$mode" in
        copy)
            rsync -av --ignore-existing --progress -- "$@" . |
            stdbuf -i0 -o0 -e0 tr '\r' '\n' |
            while IFS= read -r line; do
                line="$(printf '%s' "$line" | sed 's/\\/\\\\/g;s/"/\\"/g')"
                lf -remote "send $id echo \"$line\""
            done
            ;;
        move)
            mv -n -- "$@" .
            lf -remote "send clear"
            ;;
    esac
}}

Information is shown at the bottom of the screen every second but it is overwritten for each action that also uses this part of the screen.

Use the selection instead of the copy/cut buffer when when copying or moving

By default the lf server saves the names of files to be copied or moved so that you can copy files in one client and paste them in another. If you don't need such functionality then you may consider remapping existing keys to operate on the file selection instead of the copy/cut buffer:

map y %cp -ri -- $fs .
map d %mv -i -- $fs .
map p

Or, if you prefer to use the p key:

map p %cp -ri -- $fs .
map P %mv -i -- $fs .
map y
map d

This results in a two-step method where you select the files, then either move or copy the files to the destination.

If you prefer to use lf's builtin paste mechanism, consider the bindings listed below. Do, however, take into consideration that using cp may be more performant.

cmd alt-paste &{{
    [ -n "$fs" ] && lf -remote "send $id $1"
    lf -remote "send $id paste"
}}

map p alt-paste copy
map P alt-paste cut

If you want the file selection to be synced between multiple instances of lf a more complex configuration can be used:

cmd load-select &{{
    # skip if triggered via save-select from itself
    if [ $# -eq 1 ] && [ "$1" = "$id" ]; then
        exit
    fi

    lf -remote "send $id unselect"
    if [ -s ~/.local/share/lf/select ]; then
        files="$(sed 's/\\/\\\\/g;s/"/\\"/g;s/^/"/;s/$/"/' ~/.local/share/lf/select | tr '\n' ' ')"
        lf -remote "send $id toggle $files"
    fi
}}

cmd save-select &{{
    printf '%s\n' "$fs" > ~/.local/share/lf/select
    lf -remote "send load-select $id"
}}

# redefine existing maps to invoke save-select afterwards
map <space> :toggle; down; save-select
map u :unselect; save-select
map v :invert; save-select

# define wrapper command for glob-select since it needs to forward an argument
cmd globsel &{{
    glob="$(printf '%s' "$1" | sed 's/\\/\\\\/g;s/"/\\"/g')"
    lf -remote "send $id :glob-select \"$glob\"; save-select"
}}

cmd alt-paste &{{
    [ -n "$fs" ] && lf -remote "send $id :$1; save-select"
    lf -remote "send $id paste"
}}

map p alt-paste copy
map P alt-paste cut

# load selection on startup
load-select

Add to cut/copied files

Like ranger's da, dr, and dt. Implementing this for copying files is mostly the same, simply rename move to copy where applicable.

Note

This can also be used to switch a file selection from move to copy or vice versa.

cmd cut-add %{{
    sed -i '1s/.*/move/' ~/.local/share/lf/files
    printf '%s\n' "$fx" >> ~/.local/share/lf/files
    lf -remote "send $id :unselect; sync"
}}

cmd cut-remove %{{
    sed -i '1s/.*/move/' ~/.local/share/lf/files
    printf '%s\n' "$fx" | while IFS= read -r file; do
        sed -i "\|$file|d" ~/.local/share/lf/files
    done
    lf -remote "send $id :unselect; sync"
}}

cmd cut-toggle %{{
    files="$(comm --output-delimiter= -3 \
        <(tail -n+2 ~/.local/share/lf/files | sort) \
        <(printf '%s\n' "$fx" | sort) | tr -d '\0')"
    printf 'move\n%s\n' "$files" > ~/.local/share/lf/files
    lf -remote "send $id :unselect; sync"
}}

Select files in the copy/cut buffer

The following command will set the file selection to the files specified in the copy/cut buffer.

cmd select-buffer &{{
    files="$(sed -n 's/\\/\\\\/g;s/"/\\"/g;s/^/"/;s/$/"/;2,$p' ~/.local/share/lf/files | tr '\n' ' ')"
    if [ -z "$files" ]; then
        lf -remote "send $id echoerr select-buffer: no file in copy/cut buffer"
        exit
    fi
    lf -remote "send $id :unselect; toggle $files"
}}

Use this version if you want to add to the current selection instead of entirely replacing it:

cmd select-buffer &{{
    set -- $(cat ~/.local/share/lf/files)
    shift
    if [ "$#" -eq 0 ]; then
        lf -remote "send $id echoerr select-buffer: no file in copy/cut buffer"
        exit
    fi
    files="$(printf '%s\n' "$@" $fs | sort -u | sed 's/\\/\\\\/g;s/"/\\"/g;s/^/"/;s/$/"/' | tr '\n' ' ')"
    lf -remote "send $id :unselect; toggle $files"
}}

After running this command, you can modify the selection the usual way, and finally issue either d or y again. This can be used as an alternative to ranger's da, dr, and dt keybinds (and the section above). You can also this to switch from move to copy or vice versa.

File operations

Creating new directories

You can use the underlying mkdir command to create new directories. It is possible to define a push mapping for such commands for easier typing as follows:

map a push %mkdir<space>

You can also create a custom command for this purpose

cmd mkdir %mkdir "$@"
map a push :mkdir<space>

This command creates a directory for each argument passed by lf. For example, :mkdir foo 'bar baz' creates two directories named foo and bar baz.

You can also join arguments with space characters to avoid the need to quote arguments as such:

cmd mkdir %IFS=" "; mkdir -- "$*"
map a push :mkdir<space>

This command creates a single directory with the given name. For example, :mkdir foo bar creates a single directory named foo bar.

You can also consider passing -p option to mkdir command to be able to create nested directories (e.g. mkdir -p foo/bar to create a directory named foo and then a directory named bar inside foo).

If you want to select the new directory afterwards, you can call a remote select command as such:

cmd mkdir %{{
    IFS=" "
    file="$*"
    mkdir -p -- "$file"
    lf -remote "send $id select \"$(printf '%s' "$file" | sed 's/\\/\\\\/g;s/"/\\"/g')\""
}}

Alternatively, if you want to enter the newly created directory automatically:

cmd mkdir %{{
    IFS=" "
    file="$*"
    mkdir -p -- "$file"
    lf -remote "send $id cd \"$(printf '%s' "$file" | sed 's/\\/\\\\/g;s/"/\\"/g')\""
}}

If you want to move selected items into the newly created directory:

cmd mkdir %{{
    IFS=" "
    file="$*"
    mkdir -p -- "$file"
    if [ -n "$fs" ]; then
        mv -- $fs "$file"
        lf -remote "send $id unselect"
    fi
}}

Here's an example that uses raku:

lf-mkdir

#!/usr/bin/env raku
my $dir = prompt "Directory Name: ";
mkdir $dir;
run "lf", "-remote",
"send {%*ENV<id>} select \"{$dir.match(/^<-[/]>+/).subst: '"', '\"', :g}\"";

lfrc

map Md %lf-mkdir

Creating new files

You can use the underlying touch command to create new files. Simply replace occurrences of mkdir in the above section with touch.

map a push %touch<space>

As a custom command:

cmd touch %touch "$@"
map a push :touch<space>

With all arguments joined into a single one:

cmd touch %IFS=" "; touch -- "$*"
map a push :touch<space>

With subsequent file selection:

cmd touch %{{
    IFS=" "
    file="$*"
    touch -- "$file"
    lf -remote "send $id select \"$(printf '%s' "$file" | sed 's/\\/\\\\/g;s/"/\\"/g')\""
}}

To create a new file, select it, and open it in your editor:

cmd touch %{{
    IFS=" "
    file="$*"
    touch -- "$file"
    file="$(printf '%s' "$file" | sed 's/\\/\\\\/g;s/"/\\"/g')"
    lf -remote "send $id :select \"$file\"; \$\$EDITOR \"$file\""
}}

Here's an example that uses raku:

lf-mkfile

#!/usr/bin/env raku
my $file = prompt "Filename: ";
open($file, :r, :create).close;
run "lf", "-remote", "send {%*ENV<id>} select \"{$file.subst: '"', '\"', :g}\"";

lfrc

map Mf %lf-mkfile

Creating links

Here's a config snippet that adds a symbolic and hard linking command and mapping:

# y (select for copy) and P to paste symlink
# d (select for cut) and P to paste hard link
cmd link %{{
    set -- $(cat ~/.local/share/lf/files)
    mode="$1"
    shift
    if [ "$#" -lt 1 ]; then
        lf -remote "send $id echoerr no files to link"
        exit
    fi
    case "$mode" in
        # 'copy' mode indicates a symlink
        copy) ln -sr -t . -- "$@";;
        # while 'move' mode indicates a hard link
        move)
            ln -t . -- "$@"
            lf -remote "send clear"
            ;;
    esac
}}

map P :link

P is used for both symbolic and hard linking. The "cut" mode of files denotes a hard link, while a "copy" mode denotes a symlink.

Enhancing the rename command

Since #1162, lf by default places the cursor right before the extension when renaming a file. However you can add additional bindings for other renaming schemes with the following config:

# unmap the default rename keybinding
map r

map i rename
map I :rename; cmd-home
map A :rename; cmd-end
map c :rename; cmd-delete-home
map C :rename; cmd-end; cmd-delete-home

This will give you the following behavior for the following keybindings:

Keybinding Description Effect of renaming foo.txt
i rename with the cursor placed at the extension (original behavior) rename: foo|.txt
I rename with the cursor placed at the beginning rename: |foo.txt
A rename with the cursor placed at the end rename: foo.txt|
c rename with the portion before the extension deleted rename: |.txt
C rename with the entire filename deleted rename: |

Bulk rename multiple files

You can define a command to rename multiple files at the same time using your text editor to change the names.

cmd bulk-rename ${{
    old="$(mktemp)"
    new="$(mktemp)"
    if [ -n "$fs" ]; then
        fs="$(basename -a -- $fs)"
    else
        fs="$(ls)"
    fi
    printf '%s\n' "$fs" > "$old"
    printf '%s\n' "$fs" > "$new"
    $EDITOR "$new"
    [ "$(wc -l < "$new")" -ne "$(wc -l < "$old")" ] && exit
    paste "$old" "$new" | while IFS= read -r names; do
        src="$(printf '%s' "$names" | cut -f1)"
        dst="$(printf '%s' "$names" | cut -f2)"
        if [ "$src" = "$dst" ] || [ -e "$dst" ]; then
            continue
        fi
        mv -- "$src" "$dst"
    done
    rm -- "$old" "$new"
    lf -remote "send $id unselect"
}}

map R bulk-rename

This command either works on selected files or non-hidden files in the current directory if you don't have any selection.

Another very compact possibility which renames the selected items only with vidir is:

cmd bulk-rename $printf '%s\n' "$fx" | vidir -

Or, if you want more features, such as support for cyclic renames (A -> B, B -> A,...), for creating missing folders, git mv, and with safeguards against deleting / overwriting files, and without annoying numbers before filenames - consider using dmulholl/vimv.

(same name as another vimv, but this one is more feature rich and written in rust, don't forget to read its --help).

cmd bulk-rename ${{
    vimv -- $(basename -a -- $fx)
    lf -remote "send $id :load; unselect"
}}

Another option is qmv:

cmd bulk-rename ${{
    clear
    qmv -d -- $fx
    lf -remote "send $id unselect"
}}

Yet another option is File::Name::Editor written in the raku language.

cmd bulk-rename ${{
    clear
    file-name-editor --confirm $(basename -a -- $fx)
    lf -remote "send $id unselect"
}}

Executing commands

Open with a specific application

cmd open-with-gui &"$@" $fx # opens asynchronously, intended for GUI applications
cmd open-with-tui $"$@" $fx # opens synchronously, intended for TUI applications
cmd open-with-cli %"$@" $fx # opens synchronously and prints to the bottom statline, intended for CLI applications
map O push :open-with-gui<space>
map o push :open-with-tui<space>
map <c-o> push :open-with-cli<space>

Run a command with spaces escaped

cmd run-escaped %{{
  IFS=" "
  cmd="$1"
  shift
  "$cmd" "$*"
}}

map \\ push :run-escaped<space>

Command line

Repeating the previous command

Press . to repeat the last command typed on the command line:

map . :read; cmd-history-prev; cmd-enter

Automatically complete partially typed commands

When using the command line, it is possible to use cmd-complete to automatically complete the typed command (if unambiguous) just before running it. This allows for shorthands. For example, use :del as a shorthand for :delete, and :setf foo as a shorthand for :setfilter foo.

cmap <enter> &{{
    if [ "$lf_mode" = command ]; then
        lf -remote "send $id complete-cmd"
    else
        lf -remote "send $id cmd-enter"
    fi
}}

cmd complete-cmd :{{
    # position cursor at the end of the first word
    cmd-home
    cmd-word

    # perform tab completion before running the command
    cmd-complete
    cmd-enter
}}

Integrations

More elaborate examples can be found here.

Yank paths into your clipboard

The following commands each use xclip to copy file paths/names into your clipboard. These commands strip the trailing line break.

cmd yank-file $printf '%s' "$f" | xclip -i -selection clipboard
cmd yank-paths $printf '%s' "$fx" | xclip -i -selection clipboard
cmd yank-dirname &printf '%s' "$PWD" | xclip -i -selection clipboard
cmd yank-basename &basename -a -- $fx | head -c-1 | xclip -i -selection clipboard
cmd yank-basename-without-extension &basename -a -- $fx | sed -E 's/\.[^.]+$//' | head -c-1 | xclip -i -selection clipboard

Here's an alternative implementation of yank-basename-without-extension which treats any character after the first "." as part of the extension.

cmd yank-basename-without-extension &basename -a -- $fx | cut -d. -f1 | head -c-1 | xclip -i -selection clipboard

Tip

Substitute xclip for your clipboard manager of choice (e.g. xsel -ib, wl-copy or even pbcoby for Mac OSX).

cd to the current directory on quit

lf provides a function lfcd to cd into lf current directory on quit.

This implies you must execute either lf or lfcd and cannot change your mind about whether to cd to the current directory on quit after running one or the other.

Following allows to quit lf normally using q hotkey or cd to current directory on quit using Q hotkey.

Create script lf.bash in your ~/.config/lf directory with following contents:

lf() {
    export LF_CD_FILE="/var/tmp/.lfcd-$$"
    command lf "$@"
    if [ -s "$LF_CD_FILE" ]; then
        local DIR="$(realpath -- "$(cat -- "$LF_CD_FILE")")"
        if [ "$DIR" != "$PWD" ]; then
            printf 'cd to %s\n' "$DIR"
            cd "$DIR"
        fi
        rm "$LF_CD_FILE"
    fi
    unset LF_CD_FILE
}

Source this file in your .bashrc:

source ~/.config/lf/lf.bash

Then add following to your lfrc config file:

cmd quit-and-cd &{{
    pwd > "$LF_CD_FILE"
    lf -remote "send $id quit"
}}

map Q quit-and-cd

Then you just run lf normally and quit with q or Q.

Start your shell in the last directory lf visited

Place this in your lfrc:

cmd on-cd &{{
    printf '%s\n' "$PWD" >> ~/.local/share/lf/dir_history
}}

on-cd

And this in your shell's *rc file:

lastdir="$(tail -n1 ~/.local/share/lf/dir_history)"
[ -d "$lastdir" ] && cd "$lastdir"

Warning

This will always enter the last directory provided by lf, so changing the directory with anything other than lf (e.g. cd) will not affect your shell's CWD.

Show the current directory in the window title

This is useful if you e.g. want to use rofi to search for specific instance of lf.

Operating System Commands (OSC) offer an escape sequence for changing the title of the terminal. The following changes the title to current directory:

printf '\033]0;%s\007' "$PWD"

However, it's not a standard, so it doesn't work in some terminals. It is confirmed to be working in alacritty, st, kitty and rxvt-unicode. KDE's konsole uses slightly a different escape sequence:

printf '\033]30;%s\007' "$PWD"

We can use this in lf via special on-cd command, which runs when directory is changed.

# '&' commands run silently in background (which is what we want here),
# but are not connected to stdout.
# To make sure our escape sequence still reaches stdout we pipe it to /dev/tty
cmd on-cd &printf '\033]0;%s\007' "$PWD" > /dev/tty

# also run at startup
on-cd

If you want to show ~ instead of /home/username, change the printf line to

printf '\033]0;%s\007' "${PWD/#$HOME/\~}" > /dev/tty

Or to strictly match POSIX standard (compatible with more shells):

printf '\033]0;%s\007' "$(pwd | sed "s|^$HOME|~|")" > /dev/tty

It might also be useful to show a program name.

printf '\033]0;lf - %s\007' "${PWD/#$HOME/\~}" > /dev/tty
demo of the first version

demo gif

(it actually changes instantly, I just pull updates only once per second)

Reporting the current directory to the terminal

lf can be configured to send an OSC 7 terminal sequence whenever it changes directories, which advises the terminal on what the current directory is supposed to be:

# set pane_path when changing directory
cmd on-cd &printf '\033]7;file://%s\033\\' "$PWD" > /dev/tty

# unset pane_path when quitting
cmd on-quit &printf '\033]7;\033\\' > /dev/tty

# trigger on-cd upon startup
on-cd

This is useful when trying to open a new pane/tab from the current directory shown in lf. In addition this also works with tmux, which uses the OSC 7 terminal sequence to set the variable pane_path. Below is a configuration which opens panes at the location of pane_path, falling back to pane_current_path if not set:

# use pane_path, falling back to pane_current_path
bind v split-window -h -c '#{?pane_path,#{pane_path},#{pane_current_path}}'
bind s split-window -v -c '#{?pane_path,#{pane_path},#{pane_current_path}}'

Put lf into the background

You might be missing the possibility to put the lf job into the background with the default ctrl and z keys.

map <c-z> $kill -STOP "$PPID"

Toggle preview with the i key (with the less pager)

In lf the i key is the default mapping for previewing files. It is convenient to make i also quit the less pager. Hence you can use the same key to open a file and to close it.

$ echo "i quit" >> ~/.config/lesskey

This will create a lesskey config file for less.

Warning

Make sure your PAGER is set to less.

Custom pager command

cmd pager ${{
    if [ -f "$f" ]; then
        $PAGER "$f"
    elif [ -d "$f" ]; then
        tree "$f" | $PAGER
    fi
}}

This will run the $PAGER when the cursor is on the file or run tree in $PAGER if it's a directory.

Warn about nested instances

Add the following to your lfrc to show a warning on startup if lf is running as a nested instance:

&[ "$LF_LEVEL" -eq 1 ] || lf -remote "send $id echoerr \"Warning: You're in a nested lf instance!\""

Split words by default in zsh

zsh does not split words by default as described here, which makes it difficult to work with $fs and $fx variables, but a compatibility option named shwordsplit (-y or --sh-word-split) is provided for this purpose. You can set this option for all commands as such:

set shell zsh
set shellopts '-euy'
set ifs "\n"
set filesep "\n"  # default already

Share any file w/ a 256MiB limit

The returned URL will be appended to the clipboard (Linux). Use pbcopy instead for OSX.

cmd share $curl -F"file=@$fx" https://0x0.st | xclip -selection c

Directory specific configuration

Persistent options (e.g. sorting) for some directories

Let's say you generally prefer your sorting to be

# latest modified first
set sortby time
set reverse

but for some selected directories (e.g. for movies / tv series) you would prefer natural sort.

Hack for r30 and lower

lf doesn't remember options changed at runtime, but luckily we do have on-cd and user_{key} options.

Here is how the said example could be implemented using it:

cmd on-cd &{{
    case "$PWD" in
        /mnt/movies*)
            lf -remote "send $id set user_prev_sortby $lf_sortby"
            lf -remote "send $id set sortby natural"
            lf -remote "send $id set noreverse"

            lf -remote "send $id echomsg changed sort to natural"
            ;;
        *)
            # restore sorting on directory exit
            if [ -n "$lf_user_prev_sortby" ]; then
                lf -remote "send $id set sortby $lf_user_prev_sortby"
                lf -remote "send $id set reverse"

                lf -remote "send $id echomsg restored sort to $lf_user_prev_sortby"
                lf -remote "send $id set user_prev_sortby ''"
            fi
            ;;
    esac
}}

# run on startup too
on-cd

But this is much better served by the setlocal option introduced in r31

e.g. to recreate the example above you would need to just add the following lines to your lfrc:

setlocal /mnt/movies/ sortby natural
setlocal /mnt/movies/ noreverse

Note

The / at the end of the path makes the option recursive, see lf -doc for more information.

Searchable bookmarks

First, set an environment variable called LF_BOOKMARK_PATH to an empty folder which will contain your bookmarks, then add the following to your lfrc.

cmd bookmark-jump ${{
    res="$(cat -- "$LF_BOOKMARK_PATH/$(ls -- "$LF_BOOKMARK_PATH" | fzf)" | sed 's/\\/\\\\/g;s/"/\\"/g')"
    lf -remote "send $id cd \"$res\""
}}

cmd bookmark-create ${{
    read -r ans
    printf '%s\n' "$PWD" > "$LF_BOOKMARK_PATH/$ans"
}}
Clone this wiki locally