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 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 0.38.0
------ ------
- New actions - New actions

View File

@ -738,9 +738,12 @@ ncurses finder only after the input stream is complete.
e.g. \fBfzf --multi | fzf --sync\fR e.g. \fBfzf --multi | fzf --sync\fR
.RE .RE
.TP .TP
.B "--listen=HTTP_PORT" .B "--listen[=HTTP_PORT]"
Start HTTP server on the given port. It allows external processes to send 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. e.g.
\fB# Start HTTP server on port 6266 \fB# Start HTTP server on port 6266
@ -748,6 +751,9 @@ e.g.
# Send action to the server # Send action to the server
curl -XPOST localhost:6266 -d 'reload(seq 100)+change-prompt(hundred> )' 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 \fR
.TP .TP
.B "--version" .B "--version"

View File

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

View File

@ -19,14 +19,26 @@ const (
maxContentLength = 1024 * 1024 maxContentLength = 1024 * 1024
) )
func startHttpServer(port int, channel chan []*action) error { func startHttpServer(port int, channel chan []*action) (error, int) {
if port == 0 { if port < 0 {
return nil return nil, port
} }
listener, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", port)) listener, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", port))
if err != nil { 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() { go func() {
@ -45,7 +57,7 @@ func startHttpServer(port int, channel chan []*action) error {
listener.Close() listener.Close()
}() }()
return nil return nil, port
} }
// Here we are writing a simplistic HTTP server without using net/http // Here we are writing a simplistic HTTP server without using net/http

View File

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

View File

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