Add GET endpoints for getting the state of the finder

* GET / (or GET /current)
* GET /query
pull/3097/head^2
Junegunn Choi 1 year ago
parent de0da86bd7
commit 750b2a6313
No known key found for this signature in database
GPG Key ID: 254BC280FEF9C627

@ -3,13 +3,21 @@ CHANGELOG
0.36.0
------
- Added `--listen=HTTP_PORT` option to receive actions from external processes
- Added `--listen=HTTP_PORT` option to start HTTP server. It allows external
processes to send actions to perform via POST method, or retrieve the
current state of the finder.
```sh
# Start HTTP server on port 6266
fzf --listen 6266
# Send actions to the server
curl -XPOST localhost:6266 -d 'reload(seq 100)+change-prompt(hundred> )'
# Send action to the server via POST method
curl localhost:6266 -d 'reload(seq 100)+change-prompt(hundred> )'
# Retrieve the current item
curl localhost:6266
# Retrieve the query string
curl localhost:6266/query
```
- Added `next-selected` and `prev-selected` actions to move between selected
items

@ -722,14 +722,22 @@ e.g. \fBfzf --multi | fzf --sync\fR
.RE
.TP
.B "--listen=HTTP_PORT"
Start HTTP server on the given port to receive actions via POST requests.
Start HTTP server on the given port. It allows external processes to send
actions to perform via POST method, or retrieve the current state of the
finder.
e.g.
\fB# Start HTTP server on port 6266
fzf --listen 6266
# Send action to the server
curl -XPOST localhost:6266 -d 'reload(seq 100)+change-prompt(hundred> )'
# Send action to the server via POST method
curl localhost:6266 -d 'reload(seq 100)+change-prompt(hundred> )'
# Retrieve the current item
curl localhost:6266
# Retrieve the query string
curl localhost:6266/query
\fR
The port number is exported as \fB$FZF_LISTEN_PORT\fR on the child processes.

@ -113,7 +113,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
--version Display version information and exit
Environment variables

@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"net"
"regexp"
"strconv"
"strings"
"time"
@ -13,13 +14,18 @@ import (
const (
crlf = "\r\n"
httpPattern = "^(GET|POST) (/[^ ]*) HTTP"
httpOk = "HTTP/1.1 200 OK" + crlf
httpBadRequest = "HTTP/1.1 400 Bad Request" + crlf
httpReadTimeout = 10 * time.Second
maxContentLength = 1024 * 1024
)
func startHttpServer(port int, channel chan []*action) error {
var (
httpRegexp *regexp.Regexp
)
func startHttpServer(port int, requestChan chan []*action, responseChan chan string) error {
if port == 0 {
return nil
}
@ -29,6 +35,7 @@ func startHttpServer(port int, channel chan []*action) error {
return fmt.Errorf("port not available: %d", port)
}
httpRegexp = regexp.MustCompile(httpPattern)
go func() {
for {
conn, err := listener.Accept()
@ -39,7 +46,7 @@ func startHttpServer(port int, channel chan []*action) error {
continue
}
}
conn.Write([]byte(handleHttpRequest(conn, channel)))
conn.Write([]byte(handleHttpRequest(conn, requestChan, responseChan)))
conn.Close()
}
listener.Close()
@ -54,12 +61,14 @@ func startHttpServer(port int, channel chan []*action) error {
// * No --listen: 2.8MB
// * --listen with net/http: 5.7MB
// * --listen w/o net/http: 3.3MB
func handleHttpRequest(conn net.Conn, channel chan []*action) string {
func handleHttpRequest(conn net.Conn, requestChan chan []*action, responseChan chan string) string {
contentLength := 0
body := ""
response := func(header string, message string) string {
return header + fmt.Sprintf("Content-Length: %d%s", len(message), crlf+crlf+message)
}
bad := func(message string) string {
message += "\n"
return httpBadRequest + fmt.Sprintf("Content-Length: %d%s", len(message), crlf+crlf+message)
return response(httpBadRequest, strings.TrimSpace(message)+"\n")
}
conn.SetReadDeadline(time.Now().Add(httpReadTimeout))
scanner := bufio.NewScanner(conn)
@ -80,8 +89,13 @@ func handleHttpRequest(conn net.Conn, channel chan []*action) string {
text := scanner.Text()
switch section {
case 0:
if !strings.HasPrefix(text, "POST / HTTP") {
return bad("invalid request method")
httpMatch := httpRegexp.FindStringSubmatch(text)
if len(httpMatch) != 3 {
return bad("invalid HTTP request: " + text)
}
if httpMatch[1] == "GET" {
requestChan <- []*action{{t: actEvaluate, a: httpMatch[2][1:]}}
return response(httpOk, <-responseChan)
}
section++
case 1:
@ -120,7 +134,6 @@ func handleHttpRequest(conn net.Conn, channel chan []*action) string {
if len(actions) == 0 {
return bad("no action specified")
}
channel <- actions
requestChan <- actions
return httpOk
}

@ -201,7 +201,8 @@ type Terminal struct {
sigstop bool
startChan chan fitpad
killChan chan int
serverChan chan []*action
serverRequestChan chan []*action
serverResponseChan chan string
slab *util.Slab
theme *tui.ColorTheme
tui tui.Renderer
@ -276,6 +277,7 @@ const (
actDeleteChar
actDeleteCharEOF
actEndOfLine
actEvaluate
actForwardChar
actForwardWord
actKillLine
@ -599,7 +601,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
theme: opts.Theme,
startChan: make(chan fitpad, 1),
killChan: make(chan int),
serverChan: make(chan []*action),
serverRequestChan: make(chan []*action),
serverResponseChan: make(chan string),
tui: renderer,
initFunc: func() { renderer.Init() },
executing: util.NewAtomicBool(false)}
@ -621,7 +624,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
t.separator, t.separatorLen = t.ansiLabelPrinter(bar, &tui.ColSeparator, true)
}
if err := startHttpServer(t.listenPort, t.serverChan); err != nil {
if err := startHttpServer(t.listenPort, t.serverRequestChan, t.serverResponseChan); err != nil {
errorExit(err.Error())
}
@ -2531,7 +2534,7 @@ func (t *Terminal) Loop() {
select {
case event = <-eventChan:
needBarrier = true
case actions = <-t.serverChan:
case actions = <-t.serverRequestChan:
event = tui.Invalid.AsEvent()
needBarrier = false
}
@ -2614,6 +2617,15 @@ func (t *Terminal) Loop() {
t.executeCommand(a.a, false, a.t == actExecuteSilent)
case actExecuteMulti:
t.executeCommand(a.a, true, false)
case actEvaluate:
response := ""
switch a.a {
case "", "current":
response = t.currentItem().AsString(t.ansi)
case "query":
response = string(t.input)
}
t.serverResponseChan <- response
case actInvalid:
t.mutex.Unlock()
return false

@ -2440,9 +2440,12 @@ class TestGoFZF < TestBase
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> ')
Net::HTTP.post(URI('http://localhost:6266'), 'change-query(00)+reload(seq 100)+change-prompt:hundred> ')
tmux.until { |lines| assert_equal 100, lines.item_count }
tmux.until { |lines| assert_equal 'hundred> yo', lines[-1] }
tmux.until { |lines| assert_equal 'hundred> 00', lines[-1] }
assert_equal '100', Net::HTTP.get(URI('http://localhost:6266'))
assert_equal '100', Net::HTTP.get(URI('http://localhost:6266/current'))
assert_equal '00', Net::HTTP.get(URI('http://localhost:6266/query'))
end
end

Loading…
Cancel
Save