Skip to content
SeerLite edited this page Jun 22, 2024 · 74 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 ${{
  # ZLUA_SCRIPT="/path/to/z.lua"  # Usually unnecessary
  lf -remote "send ${id} cd \"$($ZLUA_SCRIPT -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 "$@")"
	lf -remote "send $id cd '$result'"
}}

cmd zi ${{
	result="$(zoxide query -i)"
	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 %trash-put $fx

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

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.

Note

exa is no longer maintained. Consider using eza instead.

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

cmd on-select &{{
    lf -remote "send $id set statfmt \"$(exa -ld --color=always "$f")\""
}}

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"
    lf -remote "send $id 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 $vi $(fzf)

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

cmd fzf $vi $(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 ${{
    RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
    res="$(
        FZF_DEFAULT_COMMAND="$RG_PREFIX ''" \
            fzf --bind "change:reload:$RG_PREFIX {q} || true" \
            --ansi --layout=reverse --header 'Search in files' \
            | cut -d':' -f1 | sed 's/\\/\\\\/g;s/"/\\"/g'
    )"
    [ -n "$res" ] && lf -remote "send $id select \"$res\""
}}
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.

Vim script in etc/lf.vim only works in terminal vim and provides only a basic :LF command. There is a separate vim plugin lf.vim that provides more features and can be used in gvim and neovim as well.

vim-floaterm also provides a lf wrapper which allows to use lf in the nvim's floating window.

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 '$mntdir'"
}}

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=$(echo "$f" | cut -f 1 -d '.')
    lame -V --preset standard $f "${outname}.mp3"
}}

Run node scripts in a directory

cmd node_script ${{
    script=$(cat package.json | jq -r '.scripts | keys[] ' | sort | fzf --height 20%) && npm run $(echo "$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=$(echo "$pickedFilepath" | sed 's/\(.mp4\|.webm\|.mkv\)/.mp3/' | sed 's|.*\/||');
      parsed_MP3="~/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=$(echo "$pickedFilepath" | sed 's|.mp4|(CONVERTING).mp4|');
		mv -f "$pickedFilepath" "$tempFilepath";

		ffmpeg -i "$tempFilepath" -vcodec libx265 -crf "$compressionRate" "$pickedFilepath";
		rm -f -- "$tempFilepath";
		;;
         *.webm | *.mkv)
		newFilepath=$(echo "$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=$(echo "$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 $f
}}

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 &{{
    export STARSHIP_SHELL=
    fmt="$(starship prompt)"
    lf -remote "send $id set promptfmt \"$fmt\""
}}