3

THE QUESTION

I've been trying to use fzf to bind a shortcut (preferebly Ctrl + o) to interactively search a file and pipe it to xdg-open in zsh. Many shortcuts are already part of the fzf utility, as long as you source the file located in fzf/shell/key-bindings.zsh. There, you'll find snippets for binding Ctrl + r to fuzzy search your command history and Ctrl + t to fuzzy complete your current command line. Here's what the first shortcut looks like in fzf's source code:

# CTRL-R - Paste the selected command from history into the command line
fzf-history-widget() {
  local selected num
  setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases 2> /dev/null
  selected=( $(fc -rl 1 | perl -ne 'print if !$seen{(/^\s*[0-9]+\**\s+(.*)/, $1)}++' |
    FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort,ctrl-z:ignore $FZF_CTRL_R_OPTS --query=${(qqq)LBUFFER} +m" $(__fzfcmd)) )
  local ret=$?
  if [ -n "$selected" ]; then
    num=$selected[1]
    if [ -n "$num" ]; then
      zle vi-fetch-history -n $num
    fi
  fi
  zle reset-prompt
  return $ret
}
zle     -N   fzf-history-widget
bindkey '^R' fzf-history-widget

Of course, its behavior is a bit different from the behavior I want since it pastes the selected result to the command line instead of piping it directly to a command. The following bash script, though, has precisely the behavior I expected and running it does a much better job at explaining it than myself.

bind -x '"\C-o": file="$(fzf --height 40% --reverse)" && [ -f "$file" ] && xdg-open "$file"'

I've also found this fish function with the same behavior, but fish's syntax is very different from POSIX compliant shells like zsh and bash so I don't know how helpful of reference that can be.

Now then, does anyone know to bind Ctrl + o to an interactive fzf search that pipes its result directly to xdg-open in zsh?

Thanks in advance!


THE ANSWER

Here's a working script, which I wrote based on this Stack Overflow answer.

fuzzy-xdg-open() {
  local output
  output=$(fzf --height 40% --reverse </dev/tty) && xdg-open ${(q-)output}
  zle reset-prompt
}

zle -N fuzzy-xdg-open
bindkey '^o' fuzzy-xdg-open
2
  • Are you asking for the zsh equivalent of bind? Or do you have another issue? The current question is extremely narrow and doesn't clearly state what the end goal is. Aug 13 at 18:41
  • Hi @Ginnungagap! I really wasn't clear in my question so I rewrote it and included some references to better explain myself. Thanks for the call out!
    – pesader
    Aug 15 at 18:43
1

In zsh, you bind a key or key combination (with bindkey) to a widget, widgets performing special actions in the line editor.

You can define a custom widget from a function with the zle builtin.

In that custom widget, you can run arbitrary commands, but you'll also likely want to consider what to do with the numeric argument.

If the user types Alt+3 Ctrl+o for instance, do you want to run the command three times as is customary for most widgets?

You'll also want to consider whether the line editor buffer (what you typed so far) or prompt should be redrawn after running the command or stowed away before in case the command you run happens to write something to the terminal. Also, should the exit status of the command determine whether the widget succeeded or not (resulting in a visual or audible notification for instance).

If the tools you run within the widget needs to get input from the user, you'll also want to restore stdin on the tty as ZLE redirects it on /dev/null so as to prevent them from messing up with the normal input flow in the line editor. You'll want to make sure your command leaves the terminal in the same state as it found it though (which should be the case for fzf).

So for instance, that could be:

my-open-widget() {
  zle -I # invalidate current zle display
  local file ret=0
  repeat ${NUMERIC-1} {
    file="$(fzf --height 40% --reverse)" &&
      [ -f "$file" ] &&
      xdg-open "$file" ||
      ret=$?
  } < /dev/tty
  return $ret
}

zle -N my-open-widget
bindkey '^o' my-open-widget

To run your code as many times as requested and return failure if any failed, invalidating the display beforehand to make sure things output by that code if any doesn't mess the prompt.

Alternatively, you may want to abort as soon as one instance fails (replacing || ret=$? with || return). Or if you know the code doesn't mess-up the display and more generally doesn't interact with the tty, skip the zle -I. Or you could just ignore the numeric argument and run the code unconditionally once and only once, so in the end, it could be as simple as putting your code in the body of that widget function.

5
  • Hi, thank you for your answer! When I ran this script I got the following error message: my-open-widget:zle:1: too many arguments for -I
    – pesader
    Aug 15 at 18:39
  • @pesader, I suppose you pasted it in an interactive shell where interactivecomments is off. Either remove that comment or instead put it in a file to source or your ~/.zshrc. Aug 15 at 19:14
  • Thanks, that was indeed causing the error! However the script does not seem to work, since I get an empty promp and no fzf suggestions appear. Even if I type an exact filename it doesn't get piped to xdg-open.
    – pesader
    Aug 15 at 23:37
  • 1
    @pesader, see edit and Using fzf in a zle key binding Aug 16 at 12:35
  • thanks, that helped me figure it out. I'll update the answer with the script!
    – pesader
    Aug 16 at 17:34
0

That kind of keybindings are not directly possible in vanilla . However, if you install the zsh-edit plugin, you can then use the following:

bind '^O' 'file="$(fzf --height 40% --reverse)" && [ -f "$file" ] && xdg-open "$file"'

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.