mirror of
https://github.com/Llewellynvdm/fzf.git
synced 2024-10-31 19:22:30 +00:00
Add 'GET /' endpoint for getting the program state (experimental)
Related #3372
This commit is contained in:
parent
c5e4b83de3
commit
0f50dc848e
@ -14,6 +14,14 @@ CHANGELOG
|
|||||||
# Client
|
# Client
|
||||||
curl localhost:6266 -H "x-api-key: $FZF_API_KEY" -d 'change-query(yo)'
|
curl localhost:6266 -H "x-api-key: $FZF_API_KEY" -d 'change-query(yo)'
|
||||||
```
|
```
|
||||||
|
- `--listen` server can report program state in JSON format (`GET /`)
|
||||||
|
```sh
|
||||||
|
# fzf server started in "headless" mode
|
||||||
|
fzf --listen 6266 2> /dev/null
|
||||||
|
|
||||||
|
# Get program state
|
||||||
|
curl localhost:6266 | jq .
|
||||||
|
```
|
||||||
- Added `toggle-header` action
|
- Added `toggle-header` action
|
||||||
- Bug fixes
|
- Bug fixes
|
||||||
|
|
||||||
|
@ -795,6 +795,9 @@ e.g.
|
|||||||
\fB# Start HTTP server on port 6266
|
\fB# Start HTTP server on port 6266
|
||||||
fzf --listen 6266
|
fzf --listen 6266
|
||||||
|
|
||||||
|
# Get program state in JSON format (experimental)
|
||||||
|
curl localhost:6266
|
||||||
|
|
||||||
# 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> )'
|
||||||
|
|
||||||
|
@ -23,11 +23,12 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type httpServer struct {
|
type httpServer struct {
|
||||||
apiKey []byte
|
apiKey []byte
|
||||||
channel chan []*action
|
actionChannel chan []*action
|
||||||
|
responseChannel chan string
|
||||||
}
|
}
|
||||||
|
|
||||||
func startHttpServer(port int, channel chan []*action) (error, int) {
|
func startHttpServer(port int, actionChannel chan []*action, responseChannel chan string) (error, int) {
|
||||||
if port < 0 {
|
if port < 0 {
|
||||||
return nil, port
|
return nil, port
|
||||||
}
|
}
|
||||||
@ -50,8 +51,9 @@ func startHttpServer(port int, channel chan []*action) (error, int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
server := httpServer{
|
server := httpServer{
|
||||||
apiKey: []byte(os.Getenv("FZF_API_KEY")),
|
apiKey: []byte(os.Getenv("FZF_API_KEY")),
|
||||||
channel: channel,
|
actionChannel: actionChannel,
|
||||||
|
responseChannel: responseChannel,
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
@ -83,13 +85,18 @@ func (server *httpServer) handleHttpRequest(conn net.Conn) string {
|
|||||||
contentLength := 0
|
contentLength := 0
|
||||||
apiKey := ""
|
apiKey := ""
|
||||||
body := ""
|
body := ""
|
||||||
unauthorized := func(message string) string {
|
answer := func(code string, message string) string {
|
||||||
message += "\n"
|
message += "\n"
|
||||||
return httpUnauthorized + fmt.Sprintf("Content-Length: %d%s", len(message), crlf+crlf+message)
|
return code + fmt.Sprintf("Content-Length: %d%s", len(message), crlf+crlf+message)
|
||||||
|
}
|
||||||
|
unauthorized := func(message string) string {
|
||||||
|
return answer(httpUnauthorized, message)
|
||||||
}
|
}
|
||||||
bad := func(message string) string {
|
bad := func(message string) string {
|
||||||
message += "\n"
|
return answer(httpBadRequest, message)
|
||||||
return httpBadRequest + fmt.Sprintf("Content-Length: %d%s", len(message), crlf+crlf+message)
|
}
|
||||||
|
good := func(message string) string {
|
||||||
|
return answer(httpOk+"Content-Type: application/json"+crlf, message)
|
||||||
}
|
}
|
||||||
conn.SetReadDeadline(time.Now().Add(httpReadTimeout))
|
conn.SetReadDeadline(time.Now().Add(httpReadTimeout))
|
||||||
scanner := bufio.NewScanner(conn)
|
scanner := bufio.NewScanner(conn)
|
||||||
@ -110,7 +117,12 @@ func (server *httpServer) handleHttpRequest(conn net.Conn) string {
|
|||||||
text := scanner.Text()
|
text := scanner.Text()
|
||||||
switch section {
|
switch section {
|
||||||
case 0:
|
case 0:
|
||||||
if !strings.HasPrefix(text, "POST / HTTP") {
|
// TODO: Parameter support e.g. "GET /?limit=100 HTTP"
|
||||||
|
if strings.HasPrefix(text, "GET / HTTP") {
|
||||||
|
server.actionChannel <- []*action{{t: actResponse}}
|
||||||
|
response := <-server.responseChannel
|
||||||
|
return good(response)
|
||||||
|
} else if !strings.HasPrefix(text, "POST / HTTP") {
|
||||||
return bad("invalid request method")
|
return bad("invalid request method")
|
||||||
}
|
}
|
||||||
section++
|
section++
|
||||||
@ -160,6 +172,6 @@ func (server *httpServer) handleHttpRequest(conn net.Conn) string {
|
|||||||
return bad("no action specified")
|
return bad("no action specified")
|
||||||
}
|
}
|
||||||
|
|
||||||
server.channel <- actions
|
server.actionChannel <- actions
|
||||||
return httpOk
|
return httpOk
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package fzf
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
@ -144,6 +145,23 @@ var emptyLine = itemLine{}
|
|||||||
|
|
||||||
type labelPrinter func(tui.Window, int)
|
type labelPrinter func(tui.Window, int)
|
||||||
|
|
||||||
|
type StatusItem struct {
|
||||||
|
Index int `json:"index"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Status struct {
|
||||||
|
Reading bool `json:"reading"`
|
||||||
|
Query string `json:"query"`
|
||||||
|
Position int `json:"position"`
|
||||||
|
Sort bool `json:"sort"`
|
||||||
|
TotalCount int `json:"totalCount"`
|
||||||
|
MatchCount int `json:"matchCount"`
|
||||||
|
Current *StatusItem `json:"current"`
|
||||||
|
Matches []StatusItem `json:"matches"`
|
||||||
|
Selected []StatusItem `json:"selected"`
|
||||||
|
}
|
||||||
|
|
||||||
// Terminal represents terminal input/output
|
// Terminal represents terminal input/output
|
||||||
type Terminal struct {
|
type Terminal struct {
|
||||||
initDelay time.Duration
|
initDelay time.Duration
|
||||||
@ -245,7 +263,8 @@ type Terminal struct {
|
|||||||
sigstop bool
|
sigstop bool
|
||||||
startChan chan fitpad
|
startChan chan fitpad
|
||||||
killChan chan int
|
killChan chan int
|
||||||
serverChan chan []*action
|
serverInputChan chan []*action
|
||||||
|
serverOutputChan chan string
|
||||||
eventChan chan tui.Event
|
eventChan chan tui.Event
|
||||||
slab *util.Slab
|
slab *util.Slab
|
||||||
theme *tui.ColorTheme
|
theme *tui.ColorTheme
|
||||||
@ -398,6 +417,7 @@ const (
|
|||||||
actUnbind
|
actUnbind
|
||||||
actRebind
|
actRebind
|
||||||
actBecome
|
actBecome
|
||||||
|
actResponse
|
||||||
)
|
)
|
||||||
|
|
||||||
type placeholderFlags struct {
|
type placeholderFlags struct {
|
||||||
@ -657,7 +677,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
|||||||
theme: opts.Theme,
|
theme: opts.Theme,
|
||||||
startChan: make(chan fitpad, 1),
|
startChan: make(chan fitpad, 1),
|
||||||
killChan: make(chan int),
|
killChan: make(chan int),
|
||||||
serverChan: make(chan []*action, 10),
|
serverInputChan: make(chan []*action, 10),
|
||||||
|
serverOutputChan: make(chan string),
|
||||||
eventChan: make(chan tui.Event, 1),
|
eventChan: make(chan tui.Event, 1),
|
||||||
tui: renderer,
|
tui: renderer,
|
||||||
initFunc: func() { renderer.Init() },
|
initFunc: func() { renderer.Init() },
|
||||||
@ -703,7 +724,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
|||||||
_, t.hasLoadActions = t.keymap[tui.Load.AsEvent()]
|
_, t.hasLoadActions = t.keymap[tui.Load.AsEvent()]
|
||||||
|
|
||||||
if t.listenPort != nil {
|
if t.listenPort != nil {
|
||||||
err, port := startHttpServer(*t.listenPort, t.serverChan)
|
err, port := startHttpServer(*t.listenPort, t.serverInputChan, t.serverOutputChan)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorExit(err.Error())
|
errorExit(err.Error())
|
||||||
}
|
}
|
||||||
@ -2813,7 +2834,7 @@ func (t *Terminal) Loop() {
|
|||||||
t.printInfo()
|
t.printInfo()
|
||||||
}
|
}
|
||||||
if onFocus, prs := t.keymap[tui.Focus.AsEvent()]; prs && focusChanged {
|
if onFocus, prs := t.keymap[tui.Focus.AsEvent()]; prs && focusChanged {
|
||||||
t.serverChan <- onFocus
|
t.serverInputChan <- onFocus
|
||||||
}
|
}
|
||||||
if focusChanged || version != t.version {
|
if focusChanged || version != t.version {
|
||||||
version = t.version
|
version = t.version
|
||||||
@ -2925,7 +2946,7 @@ func (t *Terminal) Loop() {
|
|||||||
select {
|
select {
|
||||||
case event = <-t.eventChan:
|
case event = <-t.eventChan:
|
||||||
needBarrier = !event.Is(tui.Load, tui.One, tui.Zero)
|
needBarrier = !event.Is(tui.Load, tui.One, tui.Zero)
|
||||||
case actions = <-t.serverChan:
|
case actions = <-t.serverInputChan:
|
||||||
event = tui.Invalid.AsEvent()
|
event = tui.Invalid.AsEvent()
|
||||||
needBarrier = false
|
needBarrier = false
|
||||||
}
|
}
|
||||||
@ -3000,6 +3021,8 @@ func (t *Terminal) Loop() {
|
|||||||
doAction = func(a *action) bool {
|
doAction = func(a *action) bool {
|
||||||
switch a.t {
|
switch a.t {
|
||||||
case actIgnore:
|
case actIgnore:
|
||||||
|
case actResponse:
|
||||||
|
t.serverOutputChan <- t.dumpStatus()
|
||||||
case actBecome:
|
case actBecome:
|
||||||
valid, list := t.buildPlusList(a.a, false)
|
valid, list := t.buildPlusList(a.a, false)
|
||||||
if valid {
|
if valid {
|
||||||
@ -3768,3 +3791,49 @@ func (t *Terminal) maxItems() int {
|
|||||||
}
|
}
|
||||||
return util.Max(max, 0)
|
return util.Max(max, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Terminal) dumpItem(i *Item) StatusItem {
|
||||||
|
if i == nil {
|
||||||
|
return StatusItem{}
|
||||||
|
}
|
||||||
|
return StatusItem{
|
||||||
|
Index: int(i.Index()),
|
||||||
|
Text: i.AsString(t.ansi),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dumpItemLimit = 100 // TODO: Make configurable via GET parameter
|
||||||
|
|
||||||
|
func (t *Terminal) dumpStatus() string {
|
||||||
|
selectedItems := t.sortSelected()
|
||||||
|
selected := make([]StatusItem, util.Min(dumpItemLimit, len(selectedItems)))
|
||||||
|
for i, selectedItem := range selectedItems[:len(selected)] {
|
||||||
|
selected[i] = t.dumpItem(selectedItem.item)
|
||||||
|
}
|
||||||
|
|
||||||
|
matches := make([]StatusItem, util.Min(dumpItemLimit, t.merger.Length()))
|
||||||
|
for i := range matches {
|
||||||
|
matches[i] = t.dumpItem(t.merger.Get(i).item)
|
||||||
|
}
|
||||||
|
|
||||||
|
var current *StatusItem
|
||||||
|
currentItem := t.currentItem()
|
||||||
|
if currentItem != nil {
|
||||||
|
item := t.dumpItem(currentItem)
|
||||||
|
current = &item
|
||||||
|
}
|
||||||
|
|
||||||
|
dump := Status{
|
||||||
|
Reading: t.reading,
|
||||||
|
Query: string(t.input),
|
||||||
|
Position: t.cy,
|
||||||
|
Sort: t.sort,
|
||||||
|
TotalCount: t.count,
|
||||||
|
MatchCount: t.merger.Length(),
|
||||||
|
Current: current,
|
||||||
|
Matches: matches,
|
||||||
|
Selected: selected,
|
||||||
|
}
|
||||||
|
bytes, _ := json.Marshal(&dump) // TODO: Errors?
|
||||||
|
return string(bytes)
|
||||||
|
}
|
||||||
|
@ -8,6 +8,7 @@ require 'shellwords'
|
|||||||
require 'erb'
|
require 'erb'
|
||||||
require 'tempfile'
|
require 'tempfile'
|
||||||
require 'net/http'
|
require 'net/http'
|
||||||
|
require 'json'
|
||||||
|
|
||||||
TEMPLATE = DATA.read
|
TEMPLATE = DATA.read
|
||||||
UNSETS = %w[
|
UNSETS = %w[
|
||||||
@ -2776,9 +2777,21 @@ class TestGoFZF < TestBase
|
|||||||
-> { URI("http://localhost:#{File.read('/tmp/fzf-port').chomp}") } }.each do |opts, fn|
|
-> { URI("http://localhost:#{File.read('/tmp/fzf-port').chomp}") } }.each do |opts, fn|
|
||||||
tmux.send_keys "seq 10 | fzf #{opts}", :Enter
|
tmux.send_keys "seq 10 | fzf #{opts}", :Enter
|
||||||
tmux.until { |lines| assert_equal 10, lines.item_count }
|
tmux.until { |lines| assert_equal 10, lines.item_count }
|
||||||
|
state = JSON.parse(Net::HTTP.get(fn.call), symbolize_names: true)
|
||||||
|
assert_equal 10, state[:totalCount]
|
||||||
|
assert_equal 10, state[:matchCount]
|
||||||
|
assert_empty state[:query]
|
||||||
|
assert_equal({ index: 0, text: '1' }, state[:current])
|
||||||
|
|
||||||
Net::HTTP.post(fn.call, 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ')
|
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 100, lines.item_count }
|
||||||
tmux.until { |lines| assert_equal 'hundred> yo', lines[-1] }
|
tmux.until { |lines| assert_equal 'hundred> yo', lines[-1] }
|
||||||
|
state = JSON.parse(Net::HTTP.get(fn.call), symbolize_names: true)
|
||||||
|
assert_equal 100, state[:totalCount]
|
||||||
|
assert_equal 0, state[:matchCount]
|
||||||
|
assert_equal 'yo', state[:query]
|
||||||
|
assert_nil state[:current]
|
||||||
|
|
||||||
teardown
|
teardown
|
||||||
setup
|
setup
|
||||||
end
|
end
|
||||||
|
Loading…
Reference in New Issue
Block a user