Omit port number in --listen for automatic port assignment

Close #3200
This commit is contained in:
Junegunn Choi 2023-03-19 15:42:47 +09:00
parent 3c34dd8275
commit fcd7e8768d
No known key found for this signature in database
GPG Key ID: 254BC280FEF9C627
6 changed files with 77 additions and 24 deletions

View File

@ -1,6 +1,20 @@
CHANGELOG
=========
0.39.0
------
- If you use `--listen` option without a port number fzf will automatically
allocate an available port and export it as `$FZF_PORT` environment
variable.
```sh
# Automatic port assignment
fzf --listen --bind 'start:execute-silent:echo $FZF_PORT > /tmp/fzf-port'
# Say hello
curl "localhost:$(cat /tmp/fzf-port)" -d 'preview:echo Hello, fzf is listening on $FZF_PORT.'
```
- Bug fixes
0.38.0
------
- New actions

View File

@ -738,9 +738,12 @@ ncurses finder only after the input stream is complete.
e.g. \fBfzf --multi | fzf --sync\fR
.RE
.TP
.B "--listen=HTTP_PORT"
.B "--listen[=HTTP_PORT]"
Start HTTP server on the given port. It allows external processes to send
actions to perform via POST method.
actions to perform via POST method. If the port number is omitted or given as
0, fzf will choose the port automatically and export it as \fBFZF_PORT\fR
environment variable to the child processes started via \fBexecute\fR and
\fBexecute-silent\fR actions.
e.g.
\fB# Start HTTP server on port 6266
@ -748,6 +751,9 @@ e.g.
# Send action to the server
curl -XPOST localhost:6266 -d 'reload(seq 100)+change-prompt(hundred> )'
# Choose port automatically and export it as $FZF_PORT to the child process
fzf --listen --bind 'start:execute-silent:echo $FZF_PORT > /tmp/fzf-port'
\fR
.TP
.B "--version"

View File

@ -116,7 +116,7 @@ const usage = `usage: fzf [options]
--read0 Read input delimited by ASCII NUL characters
--print0 Print output delimited by ASCII NUL characters
--sync Synchronous search for multi-staged filtering
--listen=HTTP_PORT Start HTTP server to receive actions (POST /)
--listen[=HTTP_PORT] Start HTTP server to receive actions (POST /)
--version Display version information and exit
Environment variables
@ -316,7 +316,7 @@ type Options struct {
PreviewLabel labelOpts
Unicode bool
Tabstop int
ListenPort int
ListenPort *int
ClearOnExit bool
Version bool
}
@ -1756,9 +1756,10 @@ func parseOptions(opts *Options, allArgs []string) {
case "--tabstop":
opts.Tabstop = nextInt(allArgs, &i, "tab stop required")
case "--listen":
opts.ListenPort = nextInt(allArgs, &i, "listen port required")
port := optionalNumeric(allArgs, &i, 0)
opts.ListenPort = &port
case "--no-listen":
opts.ListenPort = 0
opts.ListenPort = nil
case "--clear":
opts.ClearOnExit = true
case "--no-clear":
@ -1849,7 +1850,8 @@ func parseOptions(opts *Options, allArgs []string) {
} else if match, value := optString(arg, "--tabstop="); match {
opts.Tabstop = atoi(value)
} else if match, value := optString(arg, "--listen="); match {
opts.ListenPort = atoi(value)
port := atoi(value)
opts.ListenPort = &port
} else if match, value := optString(arg, "--hscroll-off="); match {
opts.HscrollOff = atoi(value)
} else if match, value := optString(arg, "--scroll-off="); match {
@ -1879,7 +1881,7 @@ func parseOptions(opts *Options, allArgs []string) {
errorExit("tab stop must be a positive integer")
}
if opts.ListenPort < 0 || opts.ListenPort > 65535 {
if opts.ListenPort != nil && (*opts.ListenPort < 0 || *opts.ListenPort > 65535) {
errorExit("invalid listen port")
}

View File

@ -19,14 +19,26 @@ const (
maxContentLength = 1024 * 1024
)
func startHttpServer(port int, channel chan []*action) error {
if port == 0 {
return nil
func startHttpServer(port int, channel chan []*action) (error, int) {
if port < 0 {
return nil, port
}
listener, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", port))
if err != nil {
return fmt.Errorf("port not available: %d", port)
return fmt.Errorf("port not available: %d", port), port
}
if port == 0 {
addr := listener.Addr().String()
parts := strings.SplitN(addr, ":", 2)
if len(parts) < 2 {
return fmt.Errorf("cannot extract port: %s", addr), port
}
var err error
port, err = strconv.Atoi(parts[1])
if err != nil {
return err, port
}
}
go func() {
@ -45,7 +57,7 @@ func startHttpServer(port int, channel chan []*action) error {
listener.Close()
}()
return nil
return nil, port
}
// Here we are writing a simplistic HTTP server without using net/http

View File

@ -203,7 +203,7 @@ type Terminal struct {
padding [4]sizeSpec
strong tui.Attr
unicode bool
listenPort int
listenPort *int
borderShape tui.BorderShape
cleanExit bool
paused bool
@ -538,7 +538,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
}
var previewBox *util.EventBox
// We need to start previewer if HTTP server is enabled even when --preview option is not specified
if len(opts.Preview.command) > 0 || hasPreviewAction(opts) || opts.ListenPort > 0 {
if len(opts.Preview.command) > 0 || hasPreviewAction(opts) || opts.ListenPort != nil {
previewBox = util.NewEventBox()
}
strongAttr := tui.Bold
@ -694,13 +694,25 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
_, t.hasLoadActions = t.keymap[tui.Load.AsEvent()]
if err := startHttpServer(t.listenPort, t.serverChan); err != nil {
errorExit(err.Error())
if t.listenPort != nil {
err, port := startHttpServer(*t.listenPort, t.serverChan)
if err != nil {
errorExit(err.Error())
}
t.listenPort = &port
}
return &t
}
func (t *Terminal) environ() []string {
env := os.Environ()
if t.listenPort != nil {
env = append(env, fmt.Sprintf("FZF_PORT=%d", *t.listenPort))
}
return env
}
func borderLines(shape tui.BorderShape) int {
switch shape {
case tui.BorderHorizontal, tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderDouble:
@ -2248,6 +2260,7 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
}
command := t.replacePlaceholder(template, forcePlus, string(t.input), list)
cmd := util.ExecCommand(command, false)
cmd.Env = t.environ()
t.executing.Set(true)
if !background {
cmd.Stdin = tui.TtyIn()
@ -2494,17 +2507,17 @@ func (t *Terminal) Loop() {
_, query := t.Input()
command := t.replacePlaceholder(commandTemplate, false, string(query), items)
cmd := util.ExecCommand(command, true)
env := t.environ()
if pwindow != nil {
height := pwindow.Height()
env := os.Environ()
lines := fmt.Sprintf("LINES=%d", height)
columns := fmt.Sprintf("COLUMNS=%d", pwindow.Width())
env = append(env, lines)
env = append(env, "FZF_PREVIEW_"+lines)
env = append(env, columns)
env = append(env, "FZF_PREVIEW_"+columns)
cmd.Env = env
}
cmd.Env = env
out, _ := cmd.StdoutPipe()
cmd.Stderr = cmd.Stdout

View File

@ -2629,11 +2629,17 @@ class TestGoFZF < TestBase
end
def test_listen
tmux.send_keys 'seq 10 | fzf --listen 6266', :Enter
tmux.until { |lines| assert_equal 10, lines.item_count }
Net::HTTP.post(URI('http://localhost:6266'), 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ')
tmux.until { |lines| assert_equal 100, lines.item_count }
tmux.until { |lines| assert_equal 'hundred> yo', lines[-1] }
{ '--listen 6266' => lambda { URI('http://localhost:6266') },
"--listen --sync --bind 'start:execute-silent:echo $FZF_PORT > /tmp/fzf-port'" =>
lambda { URI("http://localhost:#{File.read('/tmp/fzf-port').chomp}") } }.each do |opts, fn|
tmux.send_keys "seq 10 | fzf #{opts}", :Enter
tmux.until { |lines| assert_equal 10, lines.item_count }
Net::HTTP.post(fn.call, 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ')
tmux.until { |lines| assert_equal 100, lines.item_count }
tmux.until { |lines| assert_equal 'hundred> yo', lines[-1] }
teardown
setup
end
end
def test_toggle_alternative_preview_window