From 0f50dc848e58b47408713c2c31f2be73287c030a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 3 Sep 2023 16:30:35 +0900 Subject: Add 'GET /' endpoint for getting the program state (experimental) Related #3372 --- src/server.go | 34 +++++++++++++++++-------- src/terminal.go | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 97 insertions(+), 16 deletions(-) (limited to 'src') diff --git a/src/server.go b/src/server.go index c583400f..36980157 100644 --- a/src/server.go +++ b/src/server.go @@ -23,11 +23,12 @@ const ( ) type httpServer struct { - apiKey []byte - channel chan []*action + apiKey []byte + 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 { return nil, port } @@ -50,8 +51,9 @@ func startHttpServer(port int, channel chan []*action) (error, int) { } server := httpServer{ - apiKey: []byte(os.Getenv("FZF_API_KEY")), - channel: channel, + apiKey: []byte(os.Getenv("FZF_API_KEY")), + actionChannel: actionChannel, + responseChannel: responseChannel, } go func() { @@ -83,13 +85,18 @@ func (server *httpServer) handleHttpRequest(conn net.Conn) string { contentLength := 0 apiKey := "" body := "" - unauthorized := func(message string) string { + answer := func(code string, message string) string { 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 { - message += "\n" - return httpBadRequest + fmt.Sprintf("Content-Length: %d%s", len(message), crlf+crlf+message) + return answer(httpBadRequest, message) + } + good := func(message string) string { + return answer(httpOk+"Content-Type: application/json"+crlf, message) } conn.SetReadDeadline(time.Now().Add(httpReadTimeout)) scanner := bufio.NewScanner(conn) @@ -110,7 +117,12 @@ func (server *httpServer) handleHttpRequest(conn net.Conn) string { text := scanner.Text() switch section { 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") } section++ @@ -160,6 +172,6 @@ func (server *httpServer) handleHttpRequest(conn net.Conn) string { return bad("no action specified") } - server.channel <- actions + server.actionChannel <- actions return httpOk } diff --git a/src/terminal.go b/src/terminal.go index 6418a5df..1d8b55ba 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -2,6 +2,7 @@ package fzf import ( "bufio" + "encoding/json" "fmt" "io" "math" @@ -144,6 +145,23 @@ var emptyLine = itemLine{} 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 type Terminal struct { initDelay time.Duration @@ -245,7 +263,8 @@ type Terminal struct { sigstop bool startChan chan fitpad killChan chan int - serverChan chan []*action + serverInputChan chan []*action + serverOutputChan chan string eventChan chan tui.Event slab *util.Slab theme *tui.ColorTheme @@ -398,6 +417,7 @@ const ( actUnbind actRebind actBecome + actResponse ) type placeholderFlags struct { @@ -657,7 +677,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, 10), + serverInputChan: make(chan []*action, 10), + serverOutputChan: make(chan string), eventChan: make(chan tui.Event, 1), tui: renderer, initFunc: func() { renderer.Init() }, @@ -703,7 +724,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { _, t.hasLoadActions = t.keymap[tui.Load.AsEvent()] if t.listenPort != nil { - err, port := startHttpServer(*t.listenPort, t.serverChan) + err, port := startHttpServer(*t.listenPort, t.serverInputChan, t.serverOutputChan) if err != nil { errorExit(err.Error()) } @@ -2813,7 +2834,7 @@ func (t *Terminal) Loop() { t.printInfo() } if onFocus, prs := t.keymap[tui.Focus.AsEvent()]; prs && focusChanged { - t.serverChan <- onFocus + t.serverInputChan <- onFocus } if focusChanged || version != t.version { version = t.version @@ -2925,7 +2946,7 @@ func (t *Terminal) Loop() { select { case event = <-t.eventChan: needBarrier = !event.Is(tui.Load, tui.One, tui.Zero) - case actions = <-t.serverChan: + case actions = <-t.serverInputChan: event = tui.Invalid.AsEvent() needBarrier = false } @@ -3000,6 +3021,8 @@ func (t *Terminal) Loop() { doAction = func(a *action) bool { switch a.t { case actIgnore: + case actResponse: + t.serverOutputChan <- t.dumpStatus() case actBecome: valid, list := t.buildPlusList(a.a, false) if valid { @@ -3768,3 +3791,49 @@ func (t *Terminal) maxItems() int { } 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) +} -- cgit v1.2.3