junegunn/fzf

Add support for an action that replaces fzf

jlebon opened this issue · 6 comments

  • I have read through the manual page (man fzf)
  • I have the latest version of fzf
  • I have searched through the existing issues

Info

  • OS
    • Linux
    • Mac OS X
    • Windows
    • Etc.
  • Shell
    • bash
    • zsh
    • fish

Problem / Steps to reproduce

Right now, we have the execute() action, which is useful for spawning programs, but the original fzf process is still running so that we can go back to it. In some use cases, a better fit would be to replace the fzf process with the target invocation. For example:

  • a pattern I've started using recently in my fzf-powered tools is to have it recursively call itself on a slightly modified version of the command (for example, a file browser with a binding to go up by one directory is implemented by respawning itself passing the parent directory as argument)
  • some bindings are intended to spawn executables that "take ownership" of the fzf results; the user in that case has no intention to return to fzf since its job is done; a common example is spawning a text editor

My current approach is to do execute(...)+abort but it keeps fzf running unnecessarily until the program exits. In the case of the first use case above, recursively calling back into fzf leads to a continuously growing process stack. Another approach is to wrap fzf and call the program on the results that it returns (e.g. vim $(my-fzf-powered-file-finder)), but (1) this doesn't work if you want to be able to bind multiple hotkeys to different executables, and (2) since stdout is being captured, it breaks anything fzf spawns that needs raw terminal access (e.g. less).

Proposal

Introduce a new exec() action which causes fzf to exec(3) into the invocation rather than fork and exec.

And now searching closed issues, I see #816 which is similar and mentions --expect which I missed before. I think --expect could be a cleaner workaround for this, but requires support from the wrapping script. It seems like #816 lead to the creation of the +abort pattern, but I think what we really want there is to just call exec(3) to not have to wait around just to exit once the invocation returns.

@jlebon +1 for this, I had this problem just yesterday where I want an exec-and-exit. "run()" would be a good name for it.
I'm not sure this workaround fits your use case but I'm using a shell function wrapper. This doesn't help your 1) and 2) though.

function hi(){ echo "Your word is: $(cat /usr/share/dict/words | fzf -q "$1" --prompt "hi> ")";};

Example:
https://github.com/jknight/package-json-fzf/blob/main/README.md

I think --expect could be a cleaner workaround for this, but requires support from the wrapping script.

So the suggested feature is essentially a shortcut? execute provides a functionality that is not otherwise possible, but what we're discussing here is already achievable with the existing options. fzf was designed to become a good citizen in the Unix toolchain and it's more natural to use it in a shell script with other tools rather than to use it alone.

Introduce a new exec() action which causes fzf to exec(3) into the invocation rather than fork and exec.

While we know that exec is the right name for the feature, not everyone should be aware of the system call and the name's too similar to execute that it can be misleading. I think something like become(...) should be more appropriate.

  • have it recursively call itself on a slightly modified version of the command

Aren't you bothered by the flicking of the screen in between the processes? Using reload bindings can be a good alternative although you'll have to manage the state in an external file.

FZF_DEFAULT_COMMAND="pwd | tee /tmp/pwd; ls --color=always" \
  fzf --reverse --ansi --header-lines 1 \
  --bind 'backward-eof:reload:cd "$(cat /tmp/pwd)/.."; eval "$FZF_DEFAULT_COMMAND"' \
  --bind 'enter:reload(cd "$(cat /tmp/pwd)"/{} || cd "$(cat /tmp/pwd)"; eval "$FZF_DEFAULT_COMMAND")+top'

Hi @junegunn, first I want to say thank you for fzf. It's rare that I come across a tool that so significantly changes my development experience. The ability to create with just a tiny shell script an interactive interface tailor-made for your needs is incredibly powerful. Thank you.

Introduce a new exec() action which causes fzf to exec(3) into the invocation rather than fork and exec.

While we know that exec is the right name for the feature, not everyone should be aware of the system call and the name's too similar to execute that it can be misleading. I think something like become(...) should be more appropriate.

SGTM!

I think --expect could be a cleaner workaround for this, but requires support from the wrapping script.

So the suggested feature is essentially a shortcut? execute provides a functionality that is not otherwise possible, but what we're discussing here is already achievable with the existing options. fzf was designed to become a good citizen in the Unix toolchain and it's more natural to use it in a shell script with other tools rather than to use it alone.

I don't think --expect fixes this part though:

(2) since stdout is being captured, it breaks anything fzf spawns that needs raw terminal access (e.g. less).

I think what happens there is that less senses that stdout is not a terminal and goes into cat mode.

I guess another approach is adding an --output FILE switch to have fzf write its result to a separate file instead of stdout. And then the wrapping script wouldn't have to redirect fzf output.

But also I suspect that a new become(...) would simplify a lot of scripts that currently use --expect.

  • have it recursively call itself on a slightly modified version of the command

Aren't you bothered by the flicking of the screen in between the processes? Using reload bindings can be a good alternative although you'll have to manage the state in an external file.

FZF_DEFAULT_COMMAND="pwd | tee /tmp/pwd; ls --color=always" \
  fzf --reverse --ansi --header-lines 1 \
  --bind 'backward-eof:reload:cd "$(cat /tmp/pwd)/.."; eval "$FZF_DEFAULT_COMMAND"' \
  --bind 'enter:reload(cd "$(cat /tmp/pwd)"/{} || cd "$(cat /tmp/pwd)"; eval "$FZF_DEFAULT_COMMAND")+top'

Yeah, changing the original invocation is the tricky part, and using state files is an interesting approach though a bit awkward. I actually had some ideas around that, but I'll keep that for a separate RFE. :)

I don't think --expect fixes this part though:

(2) since stdout is being captured, it breaks anything fzf spawns that needs raw terminal access (e.g. less).

FYI, you can workaround the issue by redirecting the stdout to /dev/tty

lines=$(fzf --bind 'alt-l:execute:less {} > /dev/tty' --expect=ctrl-l)

Thank you for the quick turnaround on this! It's working great.