Skip to content
Leenuus edited this page Aug 20, 2024 · 84 revisions

Below are some shell tools that can be integrated into lf using regular or remote commands. Feel free to add more tools to this list as you like.

A new cd command that helps you navigate faster by learning your habits.

cmd z-jump ${{
    lf -remote "send $id cd \"$(/path/to/z.lua -e "$@" | sed 's/\\/\\\\/g;s/"/\\"/g')\""
}}
map Z push :z-jump<space>-I<space>
map zb push :z-jump<space>-b<space>
map zz push :z-jump<space>

zoxide is a smarter cd command that helps you jump to any directory in just a few keystrokes. Integrating zoxide with lf is simple:

# bash/any POSIX shell

cmd z %{{
    result="$(zoxide query --exclude "$PWD" "$@" | sed 's/\\/\\\\/g;s/"/\\"/g')"
    lf -remote "send $id cd \"$result\""
}}

cmd zi ${{
    result="$(zoxide query -i | sed 's/\\/\\\\/g;s/"/\\"/g')"
    lf -remote "send $id cd \"$result\""
}}

cmd on-cd &{{
    zoxide add "$PWD"
}}
# Powershell > 7.4

set shellflag "-cwa"

cmd z ${{
    [Console]::OutputEncoding = [System.Text.Encoding]::GetEncoding("UTF-8")
    $result = ((zoxide query --exclude $PWD $args[0]) -replace "/", "//")
    lf -remote "send $env:id cd '$result'"
}}

cmd zi ${{
    [Console]::OutputEncoding = [System.Text.Encoding]::GetEncoding("UTF-8")
    $result=((zoxide query -i) -replace "/", "//")
    lf -remote "send $id cd '$result'"
}}

cmd on-cd &{{
    zoxide add "$PWD"
}}

trash-cli can be used as command line interface for FreeDesktop.org Trash specification. trash-cli already provides separate commands for trash operations (i.e. trash-put, trash-empty, trash-list, trash-restore, trash-rm) so you can simply map these commands to a key:

cmd trash %{{
	mapfile -t files <<< "$fx"
	trash-put "${files[@]}"
}}

Note that trash-cli uses the same trashcan used by KDE, GNOME, and XFCE.

gio trash

On systems that use GIO (Gnome Input/Output), such as Ubuntu, this will use the gio trash command to move the currently selected items (files and dirs) to the trashcan. If GIO is not available, it falls back to mv.

cmd trash ${{
    set -f
    if gio trash 2>/dev/null; then
        gio trash $fx
    else
        mkdir -p ~/.trash
        mv -- $fx ~/.trash
    fi
}}

Basic use of mv to implement a trashcan located in the user's home dir, as in the fallback option here, has some noteable disadvantages:

  • If the items being moved to trash are not on the same filesystem as the user's home dir, they are moved between filesystems with potentially lengthy copy+delete operations.

  • Items in trash take up storage in the filesystem holding the user's home dir instead of the filesystem they were initially in.

  • Since items with the same name will overwrite each other if they're moved to the same dir, items with common names easily become overwritten and unrecoverable.

  • The OS does not know that files moved to trash are probably less important to the user than the user's other files, so will not suggest them for deleting when running low on disk space and may included them in automated backups, etc.

  • The OS does not provide any functionality that helps finding and restoring accidentally deleted files to their original locations.

gio trash, however, implements a trashcan without any of the disadvantages listed above. Instead of moving items across filesystems, it creates trash dirs as required on the filesystems where the items alread are. Items are stored with metadata containing original names and locations, preventing overwrites. The OS suggests deleting items from the trash when running low on space. Functionality for finding files that were moved to trash and restoring them to their original locations is available.

Assuming gio trash is in use the next command provides a way to restore selected files when browsing the Trash folder, located at ~/.local/share/Trash/files/:

cmd trash-restore %{{
    set -f
    ft="$(basename -a -- $fx | sed 's|^|trash:///|')"
    gio trash --restore $ft
    printf 'restored'
    printf ' %s' $(basename -a -- $fx)
}}

autojump can be used to jump to a directory in lf that contains a given string:

cmd aj %lf -remote "send $id cd \"$(autojump "$1" | sed 's/\\/\\\\/g;s/"/\\"/g')\""
map a push :aj<space>

Note that autojump relies on shell prompts to build and update its database, so it will only be updated when you run commands outside of lf or exit from lfcd function.

It is possible to write a handler to open lf in response to "Open in folder" requests from GUI applications like browsers or text editors. Below is a sample C program:

#include <stdio.h>
#include <stdlib.h>
#include <dbus/dbus.h>

static void show_items(DBusMessage* message) {
    // set TERMINAL to configure the terminal in which lf is opened
    const char *term = getenv("TERMINAL");
    DBusMessageIter iter;
    dbus_message_iter_init(message, &iter);
    DBusMessageIter array;
    dbus_message_iter_recurse(&iter, &array);
    while (dbus_message_iter_get_arg_type(&array) != DBUS_TYPE_INVALID) {
        const char* item;
        dbus_message_iter_get_basic(&array, &item);
        item += 7; // remove 'file://' prefix
        char* cmd;
        asprintf(&cmd, "%s lf '%s' &", term, item);
        system(cmd);
        free(cmd);
        dbus_message_iter_next(&array);
    }
}

static DBusHandlerResult message_handler(DBusConnection* connection, DBusMessage* message, void* user_data) {
    if (dbus_message_is_method_call(message, "org.freedesktop.FileManager1", "ShowItems")) {
        DBusMessage* reply = dbus_message_new_method_return(message);
        if (reply != NULL) {
            show_items(message);
            dbus_connection_send(connection, reply, NULL);
            dbus_message_unref(reply);
        } else {
            fprintf(stderr, "Error creating reply message\n");
            return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
        }
    }

    return DBUS_HANDLER_RESULT_HANDLED;
}

int main() {
    DBusConnection* connection = dbus_bus_get(DBUS_BUS_SESSION, NULL);
    if (connection == NULL) {
        fprintf(stderr, "Failed to connect to the D-Bus session bus\n");
        return 1;
    }

    dbus_bus_request_name(connection, "org.freedesktop.FileManager1", DBUS_NAME_FLAG_REPLACE_EXISTING, NULL);

    dbus_connection_add_filter(connection, message_handler, NULL, NULL);
    while (dbus_connection_read_write_dispatch(connection, -1))
        ;

    return 0;
}

Compile using the following command:

gcc -o file-handler file-handler.c $(pkg-config --cflags --libs dbus-1)

Then set the TERMINAL environment variable to configure which terminal is used to open lf. Flags can be added if required, e.g. use a value of alacritty -e for Alacritty.

eza can be used to provide the file information shown in the bottom left corner:

cmd on-select &{{
    lf -remote "send $id set statfmt \"$(eza -ld --color=always "$f" | sed 's/\\/\\\\/g;s/"/\\"/g')\""
}}

fasd can be used to navigate directories:

cmd fasd_dir ${{
    res="$(fasd -dl | grep -iv cache | fzf 2>/dev/tty)"
    if [ -n "$res" ]; then
        if [ -d "$res" ]; then
            cmd="cd"
        else
            cmd="select"
        fi
        res="$(printf '%s' "$res" | sed 's/\\/\\\\/g;s/"/\\"/g')"
        lf -remote "send $id $cmd \"$res\""
    fi
}}

map go :fasd_dir

A couple of useful Git commands that can be run directly from LF if you're in a git project.

cmd git_branch ${{
    git branch | fzf | xargs git checkout
    pwd_shell="$(pwd | sed 's/\\/\\\\/g;s/"/\\"/g')"
    lf -remote "send $id updir; cd \"$pwd_shell\""
}}
map gb :git_branch
map gp $clear; git pull --rebase || true; echo "press ENTER"; read ENTER
map gs $clear; git status; echo "press ENTER"; read ENTER
map gl $clear; git log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit

An example on-cd command to show some git related information:

cmd on-cd &{{
    # display git repository status in your prompt
    source /usr/share/git/completion/git-prompt.sh
    GIT_PS1_SHOWDIRTYSTATE=auto
    GIT_PS1_SHOWSTASHSTATE=auto
    GIT_PS1_SHOWUNTRACKEDFILES=auto
    GIT_PS1_SHOWUPSTREAM=auto
    GIT_PS1_COMPRESSSPARSESTATE=auto
    git="$(__git_ps1 " [GIT BRANCH:> %s]")" || true
    fmt="\033[32;1m%u@%h\033[0m:\033[34;1m%w\033[0m\033[33;1m$git\033[0m"
    lf -remote "send $id set promptfmt \"$fmt\""
}}

Another example on-cd command to show some git, mercury and subversion repository information only on parent directory. This will clear prompt when outside of parent directory of a git repository.

cmd on-cd &{{
    # display repository status in your prompt
    if [ -d .git ] || [ -f .git ]; then
        branch="$(git branch --show-current 2>/dev/null)" || true
        remote="$(git config --get "branch.$branch.remote" 2>/dev/null)" || true
        url="$(git remote get-url "$remote" 2>/dev/null)" || true
        fmt="\033[32;1m%u@%h\033[0m:\033[34;1m%w\033[0m\033[33;1m [GIT BRANCH:> $branch >> $url]\033[0m"
    elif [ -d .hg ]; then
        hg="$(hg branch 2>/dev/null)" || true
        fmt="\033[32;1m%u@%h\033[0m:\033[34;1m%w\033[0m\033[33;1m [HG BRANCH:> $hg]\033[0m"
    elif [ -d .svn ]; then
        svn="$(svn info 2>/dev/null | awk '/^URL: /{print $2}')" || true
        fmt="\033[32;1m%u@%h\033[0m:\033[34;1m%w\033[0m\033[33;1m [SVN URL:> $svn]\033[0m"
    else
        fmt="\033[32;1m%u@%h\033[0m:\033[34;1m%d\033[0m\033[1m%f\033[0m"
    fi
    lf -remote "send $id set promptfmt \"$fmt\""
}}

You can bind keys in lf to your usual fzf commands:

map f $$EDITOR $(fzf)

It is also possible to define commands with arguments to use with fzf:

cmd fzf $$EDITOR $(find . -name "$1" | fzf)
map f push :fzf<space>

If you want to jump to a file or directory in lf using fuzzy matching, you can utilize fzf for this purpose:

cmd fzf_jump ${{
    res="$(find . -maxdepth 1 | fzf --reverse --header="Jump to location")"
    if [ -n "$res" ]; then
        if [ -d "$res" ]; then
            cmd="cd"
        else
            cmd="select"
        fi
        res="$(printf '%s' "$res" | sed 's/\\/\\\\/g;s/"/\\"/g')"
        lf -remote "send $id $cmd \"$res\""
    fi
}}
map <c-f> :fzf_jump

Combining fzf with ripgrep, you can interactively search in the contents of files under the current directory and select a file from the results:

cmd fzf_search ${{
    cmd="rg --column --line-number --no-heading --color=always --smart-case"
    fzf --ansi --disabled --layout=reverse --header="Search in files" --delimiter=: \
        --bind="start:reload([ -n {q} ] && $cmd -- {q} || true)" \
        --bind="change:reload([ -n {q} ] && $cmd -- {q} || true)" \
        --bind='enter:become(lf -remote "send $id select \"$(printf "%s" {1} | sed '\''s/\\/\\\\/g;s/"/\\"/g'\'')\"")' \
        --preview='cat -- {1}' # Use your favorite previewer here (bat, source-highlight, etc.), for example:
        #--preview-window='+{2}-/2' \
        #--preview='bat --color=always --highlight-line={2} -- {1}'
        # Alternatively you can even use the same previewer you've configured for lf
        #--preview='~/.config/lf/cleaner; ~/.config/lf/previewer {1} "$FZF_PREVIEW_COLUMNS" "$FZF_PREVIEW_LINES" "$FZF_PREVIEW_LEFT" "$FZF_PREVIEW_TOP"')"
}}
map gs :fzf_search

You can use sshfs to mount remote filesystems and then browse them in lf.

To prevent operation not permitted errors when attempting to move files across devices on the same mount point, pass the -o workaround=renamexdev argument to sshfs.

The bundled Vim script plugin etc/lf.vim only works in terminal vim and provides only a basic :LF command. There are a number of vim plugins providing more features which can be used in gvim and neovim as well:

Here are some that aren't specific to lf:

Yet another way to copy and move showing progress but using only cp, mv, and cp-p magic. This also shows the speed and the ETA.

cmd paste $cp-p --lf-paste "$id"

archivemount can be used to browse archives like directories. To integrate archivemount with lf, define a mapping to invoke it on a selected archive file:

map am ${{
    mntdir="$f.mnt"
    mkdir -p -- "$mntdir"
    archivemount "$f" "$mntdir"
    lf -remote "send $id cd \"$(printf '%s' "$mntdir" | sed 's/\\/\\\\/g;s/"/\\"/g')\""
}}

To automatically unmount archives after exiting, it is necessary to invoke lf as a wrapper script. This is because archives cannot be unmounted while the user is currently inside them, which means that unmounting has to be performed after lf exits.

alias lf=lfwrapper

lfwrapper() {
    command lf "$@"

    # cleanup
    awk '$1 == "archivemount" { print $2 }' /etc/mtab | while read -r mntdir; do
        sanitized_input="$(printf "$mntdir")" # /etc/mtab uses octal representation of spaces (possible other symbols too), printf would convert octal representation, so that it can be used in the umount & rmdir commands.
        umount "$sanitized_input"
        rmdir "$sanitized_input"
    done
}

It is possible to integrate with lfcd as well. There is the possibility that the user might exit lf while inside an archive, in which case the destination can be set to the first existing parent directory.

lfcd() {
    dir="$(lf -print-last-dir "$@")"
    while ! cd "$dir" 2>/dev/null; do
        dir="$(dirname -- "$dir")"
    done
}

zsh file picker / directory changer

This snippet adds a zsh key binding Alt-k that opens lf in a tmux split. Pressing a in lf adds the selected file(s) to the zsh command line as relative paths, A adds absolute paths. . changes the zsh directory.

Add this to your .zshrc:

_zlf() {
    emulate -L zsh
    local d=$(mktemp -d) || return 1
    {
        mkfifo -m 600 $d/fifo || return 1
        tmux split -bf zsh -c "exec {ZLE_FIFO}>$d/fifo; export ZLE_FIFO; exec lf" || return 1
        local fd
        exec {fd}<$d/fifo
        zle -Fw $fd _zlf_handler
    } always {
        rm -rf $d
    }
}
zle -N _zlf
bindkey '\ek' _zlf

_zlf_handler() {
    emulate -L zsh
    local line
    if ! read -r line <&$1; then
        zle -F $1
        exec {1}<&-
        return 1
    fi
    eval $line
    zle -R
}
zle -N _zlf_handler

If you don't use tmux, you can modify the command to open a terminal emulator window instead, but if it runs synchronously you need to add &! at the end to fork and disown the process.

Finally, add this to lfrc:

cmd zle-cd %printf 'cd %q && zle reset-prompt\n' "$PWD" >&$ZLE_FIFO

cmd zle-insert-relative %{{
    for f in $fx; do
        printf 'LBUFFER+="${LBUFFER:+ }${(q)$(realpath %q --relative-to=$PWD)}"\n' "$f" >&$ZLE_FIFO
    done
}}

cmd zle-insert-absolute %{{
    for f in $fx; do
        printf 'LBUFFER+="${LBUFFER:+ }%q"\n' "$f" >&$ZLE_FIFO
    done
}}

cmd zle-init :{{
    map . zle-cd
    map a zle-insert-relative
    map A zle-insert-absolute
}}

&[[ -n "$ZLE_FIFO" ]] && lf -remote "send $id zle-init"

An alternative to this available as a plugin is located here: https://github.com/chmouel/zsh-select-with-lf

YouTube

The following script allows you to use lf as a means to search, preview, and play videos hosted on YouTube:

https://github.com/slavistan/lf-gadgets/tree/master/lf-yt

Keep in mind, it requires a YouTube API key.

On Windows, you can use QuickLook with lf to preview files just like with other file managers. Simply add

map <space> $your/path/to/QuickLook.exe %f%

If using WSL on windows, you can convert file paths using wslpath -w and execute them with powershell

map <space> ${{
    QL_EXE='C:\PATH_TO\QuickLook.exe'
    QL_FILE=$(wslpath -w $f)
    powershell.exe "$QL_EXE $QL_FILE"
}}

then you can use space to preview any files. Notice that this mapping replaces the original function of space.

Convert audio files to mp3s using lame:

# convert to mp3 files using lame
cmd mp3 ${{
    set -f
    outname="$(printf '%s' "$f" | cut -d. -f1)"
    lame -V --preset standard $f "$outname.mp3"
}}

Run node scripts in a directory

cmd node_script ${{
    script="$(jq -r '.scripts | keys[]' <package.json | sort | fzf --height 20%)"
    npm run "$script"
}}

In Windows, you can easily integrate 7zip with lf as an archive extractor.

In lfrc:

cmd extract $%LOCALAPPDATA%/lf/extract.cmd %f%
map x extract

Create a file named extract.cmd available to lf in your lf user settings:

@ECHO OFF
REM
REM LF Archive Extract script
REM
REM Use 7zip for extractor
REM
REM Extract archive contents into destination folder
REM with the same name as the archive file
REM

7z x %1 -o%~n1 -y 1> %LOCALAPPDATA%\lf\extract.log 2>&1

ffmpeg can be used to convert videos to .mp3, compress videos to save space, and more.

The following script converts (selected) files of type webm,mkv,mp4 to mp3, then moves them to the ~/Music folder.

If it's not webm,mkv,mp4, the selected file is ignored.

It also checks for videos without an audio layer (via ffprobe, so if you don't need it, remove that if check) and notifies the user upon completion via notify-send (also optional, can be removed alongside the 4 variables used)

If an argument passes into this command/function, it also deletes the original video files.

cmd stripvideolayer ${{
    clear
    set -f

    # Variables for notify-send
    converted_filenames=""
    converted_files_count=0
    videos_without_audio_streams=""
    videos_without_audio_streams_count=0

    for pickedFilepath in $fx; do
        case $pickedFilepath in
	    *.mp4 | *.webm | *.mkv) ;;
	    *) echo "Skipping $pickedFilepath" && continue 1;;
        esac

        parsed_MP3="$(printf '%s' "$pickedFilepath" | sed 's/\(.mp4\|.webm\|.mkv\)/.mp3/' | sed 's|.*\/||')"
        parsed_MP3="$HOME/Music/$parsed_MP3"

        # Using ffprobe because videos without audiostream result in exit code 1 which stops this entire loop of many files
        # Remove (alongside its 2 variables) if you don't record videos without audio (which are admittedly rare)
        if [[ $(ffprobe -loglevel error -show_entries stream=codec_type -of csv=p=0 "$pickedFilepath") != *"audio"* ]]; then
            ((videos_without_audio_streams_count=videos_without_audio_streams_count+1))
            videos_without_audio_streams="$videos_without_audio_streams"$'\n'"$pickedFilepath"
            continue 1
        fi

        ffmpeg -i "$pickedFilepath" "$parsed_MP3"

        ((converted_files_count=converted_files_count+1))
        converted_filenames="$converted_filenames"$'\n'"$pickedFilepath"

        if [[ $# -eq 1 ]]; then
            rm -f -- "$pickedFilepath"
        fi
    done

    # Notify the results to the user
    if [[ $converted_files_count -gt 0 ]]; then
        converted_filenames=$(echo "$converted_filenames" | sed 's|.*\/||')
        notify-send "Converted MP3 Files($converted_files_count):" "$converted_filenames"
    fi

    if [[ $videos_without_audio_streams_count -gt 0 ]]; then
        videos_without_audio_streams=$(echo "$videos_without_audio_streams" | sed 's|.*\/||')
        notify-send "Videos without audio stream($videos_without_audio_streams_count):" "$videos_without_audio_streams"
    fi

    # Uncomment the below line if you want to automatically unselect the original converted video files
    #lf -remote "send $id unselect"
}}

map u stripvideolayer
map <a-u> stripvideolayer delete_after_encoding

The following script compresses the (selected) files of type webm,mkv,mp4

It is essentially a glorified ffmpeg -i input.video -vcodec libx265 -crf "$compressionRatio" output.mp4;

Recommended for videos downloaded off websites (especially .webm/.mkv) and for mass-selecting a 10GB+ video folder and just letting it run in the background for hours, saving many gigabytes. And whenever it's finished, it notifies the user.

The compression ratio is determined by the user input (30 being default, which even at crystal-clear videos is hard to see difference, but 30 is rarely worth it on mp4 videos)

cmd compressvideo ${{
    clear
    set -f

    converted_filenames="" # notify-send variable
    converted_files_count=0 # notify-send variable

    echo "Compression Rate? (default: 31, maximum: 50)"
    read compressionRate

    # If not a number (e.g. empty), give default 31 value
    if ! [[ $cr =~ ^[0-5][0-9]$ ]]; then
        compressionRate="31"
    fi

    for pickedFilepath in $fx; do
        # could instead use ffprobe but would get more complicated as the filetype suffix becomes unknown
        case "$pickedFilepath" in
            *.mp4)
                tempFilepath="$(printf '%s' "$pickedFilepath" | sed 's|.mp4|(CONVERTING).mp4|')"
                mv -f "$pickedFilepath" "$tempFilepath"

                ffmpeg -i "$tempFilepath" -vcodec libx265 -crf "$compressionRate" "$pickedFilepath"
                rm -f -- "$tempFilepath"
                ;;
            *.webm | *.mkv)
                newFilepath="$(printf '%s' "$pickedFilepath" | sed 's/\(.webm\|.mkv\)/.mp4/')"
                ffmpeg -i "$pickedFilepath" -vcodec libx265 -crf "$compressionRate" "$newFilepath"
                rm -f -- "$pickedFilepath"
                ;;
            *) continue 1;;
        esac

        ((converted_files_count=converted_files_count+1))
        converted_filenames="$converted_filenames"$'\n'"$pickedFilepath"

    done

    # Notify the user of the results
    if [[ $converted_files_count -gt 0 ]]; then
        converted_filenames="$(printf '%s' "$converted_filenames" | sed 's|.*\/||')"
        notify-send "Compressed Videos($converted_files_count):" "$converted_filenames"
    fi
}}

Atool works as a simple frontend for various compression tools and can be used as a simple extract solution for many formats:

cmd extract ${{
    set -f
    atool -x $fx
}}

Since file extraction tools are occasionally affected by vulnerabilities that may allow overwriting files anywhere in the filesystem, it may be a good idea to sandbox the extraction process using this script

The lfcd function for bash, that makes it so that when quitting lf, the working dir will be the one you were in in lf. Add this to your config.fish file:

https://github.com/gokcehan/lf/blob/master/etc/lfcd.fish

Starship is a cross-shell prompt displaying info, like the git branch or go version of the current working directory.

cmd on-cd &{{
    fmt="$(STARSHIP_SHELL= starship prompt | sed 's/\\/\\\\/g;s/"/\\"/g')"
    lf -remote "send $id set promptfmt \"$fmt\""
}}