diff --git a/CHANGELOG.md b/CHANGELOG.md index 48c2c96..ba10cd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 6d6d49b..a538a91 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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" diff --git a/src/options.go b/src/options.go index 9be2910..f2de1d7 100644 --- a/src/options.go +++ b/src/options.go @@ -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") } diff --git a/src/server.go b/src/server.go index 2912b1a..89a7938 100644 --- a/src/server.go +++ b/src/server.go @@ -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 diff --git a/src/terminal.go b/src/terminal.go index 0310a9f..dfc21a3 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -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 diff --git a/test/test_go.rb b/test/test_go.rb index 07a7c19..de81f6e 100755 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -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