From 750b2a63130fc6b67aaa64c59d42cff428c26b4a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 25 Dec 2022 16:27:02 +0900 Subject: [PATCH] Add GET endpoints for getting the state of the finder * GET / (or GET /current) * GET /query --- CHANGELOG.md | 14 +++++++++++--- man/man1/fzf.1 | 14 +++++++++++--- src/options.go | 2 +- src/server.go | 31 ++++++++++++++++++++++--------- src/terminal.go | 20 ++++++++++++++++---- test/test_go.rb | 7 +++++-- 6 files changed, 66 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 163b41b0..802f14ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 39264545..1bcc3922 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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. diff --git a/src/options.go b/src/options.go index 1fb649f2..f5fa24a3 100644 --- a/src/options.go +++ b/src/options.go @@ -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 diff --git a/src/server.go b/src/server.go index 421bc20b..cc0a55d9 100644 --- a/src/server.go +++ b/src/server.go @@ -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 } diff --git a/src/terminal.go b/src/terminal.go index d0c0a9de..3ba189f3 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -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 diff --git a/test/test_go.rb b/test/test_go.rb index 4c424396..5dcad12b 100755 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -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