Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for using lf as a multi file & folder selection dialog #89

Closed
cstrahan opened this issue Oct 31, 2017 · 9 comments
Closed

Support for using lf as a multi file & folder selection dialog #89

cstrahan opened this issue Oct 31, 2017 · 9 comments

Comments

@cstrahan
Copy link

cstrahan commented Oct 31, 2017

It would be awesome if lf could be used for selecting multiple files (and directories)!

As an example, ranger has a --choosefiles option that writes out the selected files and/or folders. With this option, I can write a key binding for zsh so that ctrl+o will allow me select multiple files and directories, hit enter to exit, and then all the paths are inserted directly into the command line at the location of the cursor. FWIW, here's what my zsh binding (and zle widget) look like:

function _browse-with-ranger() {
  local space=""
  local file
  local outfile=$(mktemp "${TMPDIR:-/var/tmp}"/ranger-chosen-file.XXXX)

  exec </dev/tty

  ranger --choosefiles="${outfile}" --cmd 'map <enter> open_with 0'

  if [[ -e "${outfile}" ]]; then
    while IFS='' read -r file || [[ -n "${line}" ]]; do
      # use either a relative path or fullpath, whichever is shorter.
      local abs="${file}"
      local rel=$(realpath --relative-to="${PWD}" "${abs}")
      if [[ "${#abs}" -gt "${#rel}" ]]; then
        file="${rel}"
      else
        file="${abs}"
      fi

      LBUFFER="${LBUFFER}${space}${(q)file}"
      space=" "
    done < "${outfile}"
  fi

  zle reset-prompt
}

zle -N _browse-with-ranger

# Choose file with Ranger
bindkey '^o' _browse-with-ranger 

Code ugliness aside, hopefully that gives a sense of what I'm going for.

@gokcehan
Copy link
Owner

@cstrahan I'm confused about a few things. You mention you want to select multiple files with --chosefiles option but your example works for a single file using --chosefile option. This example by the way already seems to work with lf when you only select a single file. In order to improve these options, what kind of interface and behavior do you exactly want to see implemented?

By the way, is there a reason you don't want to run your shell commands within lf instead of trying to construct the command line outside of it? lf should be particularly suited for the former type of workflow. Not to disregard this feature request but I'm just curious.

@cstrahan
Copy link
Author

@gokcehan

@cstrahan I'm confused about a few things. You mention you want to select multiple files with --chosefiles option but your example works for a single file using --chosefile option.

Sorry, I should have given you a heads up about me updating the example zsh code. I thought I had updated my personal binding to support multiple files via --choosefiles a while back, but I was mistaken (to complicate matters, I stopped using my binding a while ago because it was practically blocked on ranger/ranger#667). After I realized my example binding code didn't actually do what I claimed it did, I fixed it up (and greatly simplified things compared to the first example I posted). The code as it stands now in my comment works as described.

This example by the way already seems to work with lf when you only select a single file. In order to improve these options, what kind of interface and behavior do you exactly want to see implemented?

I'd like to be able to select multiple files/directories (e.g. with <space>) and then have that list of selection either

  1. Written to a file (similar to lf -last-dir-path/lf -selection-path), or
  2. Written out to stdout (though on POSIX-like systems No. 1 could be used, e.g. something like lf -multi-selection-path /dev/stdout, so this may not need to be a separate option/flag)

Maybe the flag could be -multi-selection-path <PATH>, and the exiting flags could be kept to stay backwards compatible (and to streamline the case where someone just wants to select one file or directory).

An additional option to use a null char ('\0') for the seperator would also be ideal (maybe a new flag like -print0, or something similar).

By the way, is there a reason you don't want to run your shell commands within lf instead of trying to construct the command line outside of it? lf should be particularly suited for the former type of workflow. Not to disregard this feature request but I'm just curious.

I spend most of my time in zsh, and that's where all of my muscle memory is. I'm mostly interested in having a more convenient way to select files within zsh, though I might investigate a more lf-centric workflow.

If the feature sounds good, I could try implementing it.

(Thanks for writing and sharing lf, btw!)

@cstrahan
Copy link
Author

I should also add a couple more details, based on how I (now) have things set up with my zsh+ranger binding:

  • There should (ideally, IMO) be some way (from the CLI) to map <enter> to :open, which will just write selections out and exit (with ranger that can be done with --cmd 'map <enter> open_with 0'). Being able to set the mapping from the command line would allow for using something like <enter> for this purpose, while obviating the need for the user to make the map global.
  • If no files have been selected via <space> (or whatever binding the user has set), then <enter> calls :open, and :open takes the file argument (i.e. the file under the cursor) and just writes that out. That way a single file/folder selection can be made quickly with just <enter>, instead of having to do <space><enter>.

@gokcehan
Copy link
Owner

@cstrahan -selection-path already works with multiple files/directories and we don't have a separate option for the single file case. I think ranger implemented the multiple file version later on and then kept the single file version for backward compatibility. In any case, I don't see the point for having two separate options since the current file is already written to the output file when no file is selected. Can you clarify this point before we can discuss the rest?

@cstrahan
Copy link
Author

cstrahan commented Oct 31, 2017

@cstrahan -selection-path already works with multiple files/directories and we don't have a separate option for the single file case.

Oh, I see: from experimenting, I thought it only allowed for selecting files, but now I see that if you toggle some directories and files, and then :open a file, lf will write out the expected list of paths. However, and this is the thing that threw me off, if you :open a folder it will merely cd you into that directory (e.g. while keeping you in lf), so if all you've chosen are a couple of folders, you have to find an arbitrary file to :open on. The relevant logic seems to be here:

lf/eval.go

Lines 251 to 289 in 3f7bd0a

if curr.IsDir() {
app.nav.open()
if err != nil {
msg := fmt.Sprintf("opening directory: %s", err)
app.ui.message = msg
log.Print(msg)
return
}
app.ui.loadFile(app.nav)
app.ui.loadFileInfo(app.nav)
return
}
if gSelectionPath != "" {
out, err := os.Create(gSelectionPath)
if err != nil {
log.Printf("opening selection file: %s", err)
return
}
defer out.Close()
var path string
if len(app.nav.marks) != 0 {
marks := app.nav.currMarks()
path = strings.Join(marks, "\n")
} else if curr, err := app.nav.currFile(); err == nil {
path = curr.Path
} else {
return
}
_, err = out.WriteString(path)
if err != nil {
log.Printf("writing selection file: %s", err)
}
app.quit <- true
return

So -selection-path is 99% of what I want; the last thing I desire is a convenient way to exit lf and write out the selected paths, regardless of whether the thing under cursor is a file or directory. From what I see, it looks like lf doesn't distinguish between "entering" a directory (say, via the l mapping) and "opening" the directory, and that might need to change with this suggestion. For comparison, you can enter the directory under the cursor in ranger with l, and you can also "open" the directory (passing the directory's path to whatever program you have configured for directories) by using map <enter> open_with 0, that effectively makes <enter> a no-op that just exits ranger and dumps the list of selected paths. For completeness, you can specify these flags to open_with:

 f   Fork the process.  (Run in background)
 c   Run the current file only, instead of the selection
 r   Run application with root privilege (requires sudo)
 t   Run application in a new terminal window

What lf calls open is two separate operations in ranger: move vs open_with. These are (a portion of) the default bindings:

map <UP>       move up=1
map <DOWN>     move down=1
map <LEFT>     move left=1
map <RIGHT>    move right=1

copymap <UP>       k
copymap <DOWN>     j
copymap <LEFT>     h
copymap <RIGHT>    l

It would be awesome if I could have l remain as the "enter the folder under the cursor", while being able to add a new mapping for <enter> that was something like "do a no-op open and exit" (or if we don't want to overload "open" as ranger has, it could be a new command to specifically "accept toggled selections and exit (after saving to disk, of course)").

In any case, I don't see the point for having two separate options since the current file is already written to the output file when no file is selected. Can you clarify this point before we can discuss the rest?

You're absolutely right! I didn't realize that lf already does that. No need for another CLI option/flag, then.

@gokcehan
Copy link
Owner

gokcehan commented Nov 1, 2017

@cstrahan I would say directories are meant to be opened by a file manager anyway so I don't see much point making it configurable especially if that will make things more complicated than it is right now. If we can simplify the current design somehow by making directory opening configurable, that would be fine, but I can't think of any way to do that.

As you say, our best option is to add another builtin command to exit with the current selection or the current file. I was thinking maybe we could call it write (with a default w key next to q) and implement it even without --selection-path option to output file selection to stdout. But then things can get messy if a user calls a shell command that also outputs to stdout while selecting files. Maybe we can implement it in such a way that when --selection-path is given it outputs to the file and when it is not given it can output to stdout.

I'm getting warmer to this idea of using lf for file selection but I'm not sure about some of the details yet.

@ismay
Copy link

ismay commented Mar 11, 2022

I was wondering if this is still on the table.

add another builtin command to exit with the current selection or the current file. I was thinking maybe we could call it write (with a default w key next to q) and implement it even without --selection-path option to output file selection to stdout. But then things can get messy if a user calls a shell command that also outputs to stdout while selecting files. Maybe we can implement it in such a way that when --selection-path is given it outputs to the file and when it is not given it can output to stdout.

Writing to stdout if selection-path is not given would be perfect. That’s what nnn does as well for example (https://github.com/jarun/nnn/wiki/Usage#program-options). That way it’d be easier to integrate lf as a file picker for kakoune.

To illustrate my usecase, it’s so I can use lf in commands like this:

define-command nnn-open -docstring 'Pick a file with nnn' %{
    terminal sh -c %{
        # Parse parameters to local variables
        kak_buffile=$1 kak_session=$2 kak_client=$3

        # Get working directory of current buffer
        kak_pwd=$(dirname "${kak_buffile}")

        # Pick a file with nnn
        filename=$(nnn -p - "${kak_pwd}")

        # Construct the command to pass to kakoune
        kak_cmd="evaluate-commands -client $kak_client edit $filename"

        # Echo the command back to the parent session
        echo $kak_cmd | kak -p $kak_session
    } -- %val{buffile} %val{session} %val{client}
}

@gokcehan
Copy link
Owner

@ismay This is an old issue and there are many things that have changed but I think you can already use /dev/stdout for this purpose:

filename=$(lf -selection-path /dev/stdout)

But it does not work well if you use stdout while running lf (e.g. running a shell command). Also, we are currently using tcell for the ui and it uses /dev/tty but there was a time it used stdin and stdout instead which do not work well in subshells as far as I know. So in general temporary files are a safer choice for this purpose.

@ismay
Copy link

ismay commented Mar 14, 2022

Ah I see. I’ll try implementing it that way, thanks for the explanation!

edit: for those looking to do the same, this is what I've settled on in terms of integrating lf with kakoune, works like a charm:

define-command lf-open -docstring 'Pick a file with lf' %{
  terminal sh -c %{
    # Local variables
    kak_buffile=$1 kak_session=$2 kak_client=$3

    # Create temporary file for selection
    lf_tmp=$(mktemp "${TMPDIR:-/tmp}"/lf-open.XXXXXXXXXX)

    # Get working directory of current buffer
    kak_pwd=$(dirname "${kak_buffile}")

    # Pick a file with lf
    $(lf -selection-path "${lf_tmp}" "${kak_pwd}")

    # Get first line of selection file (ignoring multiple selections)
    filename=$(head -n 1 "${lf_tmp}")

    # Only echo a command back if there was a selection
    if [ -n "$filename" ]; then
      # Construct the command to pass to kakoune
      kak_cmd="evaluate-commands -client $kak_client edit $filename"

      # Echo the command back to the parent session
      echo $kak_cmd | kak -p $kak_session
    fi
  } -- %val{buffile} %val{session} %val{client}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants