Welcome to Vifm Q&A, where you can ask questions about using Vifm. Registration is optional, anonymous posts are moderated. E-mail and GitHub logins are enabled.
0 votes
in vifm by
edited by

I need help with vifm vimscript coding: I've enabled fzf multiselection and want to integrate some sort of:

a) if statement to check if selection is multiple
b) if multiple files are selected start a loop where each file selected is then executed with its default application (specified in the filextype section)

replacing the goto code line doing so.

command! fzff : set noquickview
  \| let $FZF_PICK = system('fd --hidden -t f | fzf --multi --height 10 2>/dev/tty')
  \| if $FZF_PICK != ''
  \|   execute 'goto' fnameescape($FZF_PICK)
  \| endif

Is that feasible within vifm? Thank you in advance

1 Answer

0 votes
by
selected by
 
Best answer

Try this:

command! fzff : set noquickview
    \| let $FZF_PICK = system('fd --hidden -t f | fzf --multi --height 10 2>/dev/tty')
    \| let $FZF_COUNT = system('echo "$FZF_PICK" | wc -l')
    \| if $FZF_COUNT == 1
    \|   execute 'goto' fnameescape($FZF_PICK)
    \| elseif $FZF_COUNT > 1
    \|   execute 'select! !echo "$FZF_PICK"'
    \| keepsel endif
by

thank you for the code!
If I select only one file it bring me to the file location as usual but it gives me "invalid command name" in the command line.
If I select multiple files it exit fzf and nothing changes, not even an error, the cursor stay were it was.

by

Updated, it should be elseif, not elif.

by

ok, here's the updated beheaviour:
- On single selection it brings me to the file but gives me 'Unmatched if-else-endif' error
- On multiple selection it correctly select the files but only if the selection is in the same path, I tried to multiselect files that are in current directory and file that are in a subdir but it cuts the selection to files in current directory

by

'Unmatched if-else-endif' error

I was hoping keepsel will do and I won't have to do this ugly workaround for selection:

command! test : set noquickview
    \| let $FZF_PICK = system("echo -e 'hdd\nssd'")
    \| let $FZF_COUNT = system('echo "$FZF_PICK" | wc -l')
    \| if $FZF_COUNT == 1
    \|   execute 'nnoremap waround :nunmap waround<cr>'
    \|   execute 'goto' fnameescape($FZF_PICK)
    \| elseif $FZF_COUNT > 1
    \|   execute 'nnoremap waround :nunmap waround<cr>gs'
    \|   execute 'select! !echo "$FZF_PICK"'
    \| endif
    \| normal waround

it cuts the selection to files in current directory

Well, it can't select files which aren't visible. You can construct a custom view instead:

command! test : set noquickview
    \| let $FZF_PICK = system("echo -e 'hdd\nssd'")
    \| let $FZF_COUNT = system('echo "$FZF_PICK" | wc -l')
    \| if $FZF_COUNT == 1
    \|   execute 'goto' fnameescape($FZF_PICK)
    \| elseif $FZF_COUNT > 1
    \|   execute '!echo "$FZF_PICK" %%n%%u'
    \| endif
by

thank you so much, the custom view is really lovely!
There's a way to set the custom view tab name? because it names it after the command:

...FZF_PICK" %n%u] @ /etc/systemd/system

and forgive me if I hijack this question to add another similar one:
when I use key y for example, it popups the command view and the custom commands I added do not have a proper description like "yank files" and "to last column", how do I add it?

key: d       :!if [[ -n '$XDG_CURRENT_DESKTOP' ]]...
key: f       :!echo -n %c:p | wl-copy %i<cr>:echo...
key: y       yank files
sel: $       to last column
by

Tab name can be set with :tabname, but you might be talking about view title which isn't customizable.

User-defined keys don't have description. Probably will be added in the future.

by

can't wait to see how this awesome project evolves! keep it up <3

by
edited by

one last thing, I would like to use xdg-open on a multiple files selection so I need a loop command. How do I adapt this already working command?

filextype {*.bmp,*.jpg,*.jpeg,*.png,*.gif,*.xpm},<image/*> xdg-open %f

should be something like

filextype {*.bmp,*.jpg,*.jpeg,*.png,*.gif,*.xpm},<image/*> for element in %f; do xdg-open $element; done

but what vifm variable I have to use instead of "%f"? I tried to make it up from the vifm manual but I failed for now

by

for element in %f; do xdg-open $element; done loop should work fine. You might want to add echo -n && in front because for is a builtin and Vifm won't find it in $PATH which is a prerequisite for running the command.

You should also be able to just use xdg-open %c and Vifm will run the command for each file individually when you open multiple files at the same time.

by

unfortunately xdg-open %c did not work, but the for loop did! thank you for the support

by

I would like to add the header argument to fzf, I got it working in a shell function like this

fd --hidden -t f | fzf --multi --height 10 --header "
This is the first line of the header
This is the second line of the header"
[...]

and that correctly prints

This is the first line of the header
This is the second line of the header

in the fzf header. But when I try to replicate it in vifmrc

command! fzff : set noquickview
    \| let $FZF_PICK = system('fd --hidden -t f | fzf --multi --height 10 --header"
    \This is the first line of the header
    \This is the second line of the header" 2>/dev/tty')
    \| let $FZF_COUNT = system('echo "$FZF_PICK" | wc -l')
    [...]

it prints

This is the first line of the headerThis is the second line of the header

without the new line. I've tried to insert \n but can't find a way to escape that

by

Double quotes handle sequences like \n, but to keep using single quotes you can insert newline via $'string' syntax of the shell:

command! fzff : set noquickview
    \| let $FZF_PICK = system('fd --hidden -t f | fzf --multi --height 10 --header $''
    \This is the first line of the header
    \This is the second line of the header'' 2>/dev/tty')
    \| let $FZF_COUNT = system('echo "$FZF_PICK" | wc -l')
    [...]

' got doubled to escape it inside of single quotes (so 'ab''c' means ab'c).

by
edited by

unfortunately I cannot get this to work, even with ' doubled those lines are printed one after the other in the fzf window

result:

This is the first line of the headerThis is the second line of the header

edit: even using double quote do not work, it literally prints \n between the two line

command! fzff : set noquickview
    \| let $FZF_PICK = system('fd --hidden -t f | fzf --multi --height 10 --header "
    \This is the first line of the header\n
    \This is the second line of the header" 2>/dev/tty')
    \| let $FZF_COUNT = system('echo "$FZF_PICK" | wc -l')
        [...]

result:

This is the first line of the header\nThis is the second line of the header
by

Damn, I meant to add \n between the lines:

command! fzff : set noquickview
    \| let $FZF_PICK = system('fd --hidden -t f | fzf --multi --height 10 --header $''
    \This is the first line of the header\n
    \This is the second line of the header'' 2>/dev/tty')
    \| let $FZF_COUNT = system('echo "$FZF_PICK" | wc -l')
    [...]

You didn't use use double quotes correctly, the outer ones make the difference:

command! fzff : set noquickview
    \| let $FZF_PICK = system("fd --hidden -t f | fzf --multi --height 10 --header '
    \This is the first line of the header\n
    \This is the second line of the header' 2>/dev/tty")
    \| let $FZF_COUNT = system('echo "$FZF_PICK" | wc -l')
    [...]
by

perfection! works both ways now, thank you

by

I tried to edit the command so it search on selected elements (rather than be fixed on current dir)

command! fzff : set noquickview
\| let $FZF_PICK = system("fd . --hidden -tf %f | fzf --multi --height 10 --header '
\This is the first line of the header\n
\This is the second line of the header' 2>/dev/tty")
\| let $FZF_COUNT = system('echo "$FZF_PICK" | wc -l')
[...]

but it does not support elements with whitespaces in name.

I tried to solve that by using \'%f\' and it worked when selecting single elements with whitespaces, but now multiselection is not working anymore

by

Use

command! fzff : set noquickview
\| let $FZF_PICK = system(expand("fd . --hidden -tf %%f | fzf --multi --height 10 --header '))
...

to postpone expansion.

by
edited by

what special keyword expand() triggers on? because this solution brokes the current, modified, version I use of that snippet:

\| let $FZF_COUNT = system(expand("fd . --hidden -tf %%f | fzf-tmux -p 95%% --ansi --multi --preview 'bat --theme=ansi --color=always --style=numbers {}' --preview-window='right,50%%,border-left' --prompt='Files > ' 2>/dev/tty  | tee \"$FZF_FILE\" | wc -l"))

edit: it's the two other %% (obviously). There's a way to escape them?

edit 2: even if I temporarely remove those two arguments containing %%, the solution goes back working (even with whitespaces) but it will search only the elements within the current cursor position (no multiselection), where the previous %f supported multi selection (but not whitespaces)

by

expand() processes the same %-macros which are expanded without it, but does it later. Double those percent signs again to get 95%%%% and 50%%%%.

%f depends on selection and many commands reset it. You can move expand() part to be the first line and then just reuse its result in system() invocations.

by

thank you for the %%%% suggestion, it fixed the command but unfortunately I cannot multiselect anymore after adding expand. What do you mean by reset?

by

Expansion of %f depends on selection and many commands drop selection which is why you only get the current file in commands other than the first one. You want something like:

command! fzff : let $VIFM_SELECTION = expand('%%f')
             \| set noquickview
             \| let $FZF_PICK = system("fd . --hidden -tf $VIFM_SELECTION | fzf --multi --height 10 --header '
             \This is the first line of the header\n
             \This is the second line of the header' 2>/dev/tty")
             \| let $FZF_COUNT = system('echo "$FZF_PICK" | wc -l')

In this case you can drop expand() inside system() and keep %% instead of %%%%.

by
edited by

thank you, that's really a neat syntax! unfortunately it does break both multiselection and folder with whitespaces selection, but how assigning expand to a variable cause that?

here's the complete command current in use

command! fzff : set noquickview
  \| let $FZF_SELECTION = expand('%%f')
  \| let $FZF_FILE = system('mktemp /tmp/vifm-fzf-XXX')
  \| let $FZF_COUNT = system("fd . --hidden -tf $FZF_SELECTION | fzf-tmux -p 95%% --ansi --multi --preview 'bat --theme=ansi --color=always --style=numbers {}' --preview-window='right,50%%,border-left' 2>/dev/tty  | tee \"$FZF_FILE\" | wc -l")
  \| if $FZF_COUNT == 1
  \|   execute 'goto' fnameescape(system('cat "$FZF_FILE"'))
  \| elseif $FZF_COUNT > 1
  \|   execute '!cat "$FZF_FILE" %%n%%u'
  \| endif
  \| execute '!rm -f "$FZF_FILE" %%i'

edit: if I use expand("%f") instead, withespace selection works again but not multiselection. That because double quotes will consider whitespace as a string right? in that case could the problem be the second part of the command?

by

You didn't make it the first command (put it after set noquickview). I thought the expansion in shell will work fine, but looks like environment variables are expanded after escapes are processed. Replaced that with expansion by Vifm (".$FZF_SELECTION.") which should work the same as %%f.

command! fzff : let $FZF_SELECTION = expand('%%f')
  \| set noquickview
  \| let $FZF_FILE = system('mktemp /tmp/vifm-fzf-XXX')
  \| let $FZF_COUNT = system("fd . --hidden -tf ".$FZF_SELECTION." | fzf-tmux -p 95%% --ansi --multi --preview 'bat --theme=ansi --color=always --style=numbers {}' --preview-window='right,50%%,border-left' 2>/dev/tty  | tee \"$FZF_FILE\" | wc -l")
  \| if $FZF_COUNT == 1
  \|   execute 'goto' fnameescape($FZF_FILE)
  \| elseif $FZF_COUNT > 1
  \|   execute '!cat "$FZF_FILE" %%n%%u'
  \| endif
  \| execute '!rm -f "$FZF_FILE" %%i'

let $FZF_SELECTION = '%f' would work better if there were no single quotes in file names, althought that could be addressed with %f:gs/'/''/ but it's yet another way of doing this (this one doesn't depend on order though).

by

that's working FLAWLESSLY, I'm about to cry ahahah, it's so beautiful and I cannot thank you enough <3

by

That:

command! fzff : set noquickview
    \| let $FZF_PICK = system('fd --hidden -t f | fzf --multi --height 10 2>/dev/tty')
    \| let $FZF_COUNT = system('echo "$FZF_PICK" | wc -l')
    \| if $FZF_COUNT == 1
    \|   execute 'goto' fnameescape($FZF_PICK)
    \| elseif $FZF_COUNT > 1
    \|   execute 'select! !echo "$FZF_PICK"'
    \| keepsel endif

works, but I am getting the error:

Unmatched if-else-endif

if I have just one file selected.

by

works, but I am getting the error:

Yes, it was pointed out in a comment. keepsel "hides" endif, the code should probably ignore keepsel when looking for parts of conditionals.

by

Sorry, the comments were somehow collapsed. ;/

by

I would like to add this condition

if system("grep -iqE 'debian|ubuntu' /etc/os-release") != ''
  let $BAT = "batcat"
  let $FD = "fdfind"
else
  let $BAT = "bat"
  let $FD = "fd"
endif

how do I reference the $BAT and $FD variables inside this part?

command! fzff : let $FZF_SELECTION = expand('%%f')
  \| set noquickview
  \| let $FZF_FILE = system('mktemp /tmp/vifm-fzf-XXX')
  \| let $FZF_COUNT = system("fd . --hidden -tf ".$FZF_SELECTION." | fzf-tmux -p 95%% --ansi --multi --preview 'bat --theme=ansi --color=always --style=numbers {}' --preview-window='right,50%%,border-left' 2>/dev/tty  | tee \"$FZF_FILE\" | wc -l")
  \| if $FZF_COUNT == 1
  \|   execute 'goto' fnameescape($FZF_FILE)
  \| elseif $FZF_COUNT > 1
  \|   execute '!cat "$FZF_FILE" %%n%%u'
  \| endif
  \| execute '!rm -f "$FZF_FILE" %%i'

I think they need to be escaped? because replacing system("fd . with system("$FD . do not seems to work

by

replacing system("fd . with system("$FD . do not seems to work.

It should actually work. You can also do system($FD." . instead to expand it in Vifm instead of letting a shell handle it.

by
edited by

both solution does not give any kind of error but the output is empty, if I explicit use fdfind it works properly

edit: tried in another system where fd is in use and it works over there, there's something wrong with the if statement, if I replace it with variables only it works

let $BAT = "batcat"
let $FD = "fdfind"

found out the solution, I wrongly thought that the if statement would check the exit code of that shell command, instead it checks the output and using the argument -q was suppressing it

by

I'm currently running into an exeption this 99% working snippet

command! fzfc : set noquickview
  \| let $FZF_PICK = system("rga -uu --color=always --line-number --no-heading --smart-case \"${*:-}\" \"%f\" | fzf-tmux -p 95%% --ansi --preview \"$BAT --theme=gruvbox-dark --color=always {1} --highlight-line {2}\" --preview-window=\"up,75%%,border-bottom,+{2}+3/3,~3\" --delimiter : --prompt=\"Content > \" 2>/dev/tty")
  \| if $FZF_PICK != ''
  \|   let $FZFC_PATH = system('echo $FZF_PICK | cut -f1 -d:')
  \|   let $FZFC_LINENUM = system('echo $FZF_PICK | cut -f2 -d":"')
  \|   execute 'edit +$FZFC_LINENUM' fnameescape($FZFC_PATH)
  \| endif

where I get this error if select a result before fzf finish to list all matches

:fzfccat: write error: Broken pipe

what part of the snippet is causing this and how could I prevent it?

by

Maybe rga or fzf-tmux is using cat inside. You can try adding 2>/dev/null. In case of fzf-tmux, 2>/dev/tty may need to change to >/dev/tty or </dev/tty but I don't remember whether it cares which file descriptor is the terminal.

by

lately I had to change fzf-tmux to fzf because fzf-tmux was failing rendering images in the fzf preview.
So under fzf now both >/dev/tty and </dev/tty alternative fails even after fzf finish to list all matches; while both 2>/dev/null and original 2>/dev/tty seems to correctly open the matched file and line, even while fzf is still listing matches, but only after I ctrl-c out of a hanging terminal window that appears over vifm as soon as I select the match

by

The behaviour is as if rga keeps running after fzf has exited. This normally doesn't happen because attemping to write into a closed pipe kills the process left of | (e.g., find / | echo exits immediately). By the way, the first system() should really be term() for managing terminal's state better, although this may not be the cause.

by

unfortunately didn't solve the specific case. I tried to look for a workaround to kill rga but apparently it seems to ignore SIGPIPE?
anyway this is a problem that is not regarding vifm, thanks for the heads-up about term(), I replaced system() for every interactive command

If you would like to make a bug report or feature request consider using GitHub, SourceForge or e-mail. Posting such things here is acceptable, but this is not a perfect place for them.
...