From 1ba7484d606bf3797b3936651051bb4113dbcad2 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 18 Dec 2022 00:22:15 +0900 Subject: [PATCH] Add --listen=HTTP_PORT option to receive actions Supersedes #2019 See also: * #1728 * https://github.com/junegunn/fzf.vim/pull/1044 --- CHANGELOG.md | 8 + man/man1/fzf.1 | 13 ++ src/options.go | 465 +++++++++++++++++++++----------------------- src/options_test.go | 29 ++- src/terminal.go | 71 ++++++- 5 files changed, 337 insertions(+), 249 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e239f1..163b41b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,14 @@ CHANGELOG 0.36.0 ------ +- Added `--listen=HTTP_PORT` option to receive actions from external processes + ```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> )' + ``` - Added `next-selected` and `prev-selected` actions to move between selected items ```sh diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index a407232..876f28f 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -721,6 +721,19 @@ ncurses finder only after the input stream is complete. 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. + +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> )' + \fR + +The port number is exported as \fB$FZF_LISTEN_PORT\fR on the child processes. +.TP .B "--version" Display version information and exit diff --git a/src/options.go b/src/options.go index 64cf560..1ef8bca 100644 --- a/src/options.go +++ b/src/options.go @@ -113,6 +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 /) --version Display version information and exit Environment variables @@ -296,6 +297,7 @@ type Options struct { PreviewLabel labelOpts Unicode bool Tabstop int + ListenPort int ClearOnExit bool Version bool } @@ -868,8 +870,9 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme { } var ( - executeRegexp *regexp.Regexp - splitRegexp *regexp.Regexp + executeRegexp *regexp.Regexp + splitRegexp *regexp.Regexp + actionNameRegexp *regexp.Regexp ) func firstKey(keymap map[tui.Event]string) tui.Event { @@ -891,45 +894,216 @@ func init() { executeRegexp = regexp.MustCompile( `(?si)[:+](execute(?:-multi|-silent)?|reload|preview|change-query|change-prompt|change-preview-window|change-preview|(?:re|un)bind):.+|[:+](execute(?:-multi|-silent)?|reload|preview|change-query|change-prompt|change-preview-window|change-preview|(?:re|un)bind)(\([^)]*\)|\[[^\]]*\]|~[^~]*~|![^!]*!|@[^@]*@|\#[^\#]*\#|\$[^\$]*\$|%[^%]*%|\^[^\^]*\^|&[^&]*&|\*[^\*]*\*|;[^;]*;|/[^/]*/|\|[^\|]*\|)`) splitRegexp = regexp.MustCompile("[,:]+") + actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+") } -func parseKeymap(keymap map[tui.Event][]*action, str string) { - masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string { - symbol := ":" - if strings.HasPrefix(src, "+") { - symbol = "+" - } - prefix := symbol + "execute" - if strings.HasPrefix(src[1:], "reload") { - prefix = symbol + "reload" - } else if strings.HasPrefix(src[1:], "change-preview-window") { - prefix = symbol + "change-preview-window" - } else if strings.HasPrefix(src[1:], "change-preview") { - prefix = symbol + "change-preview" - } else if strings.HasPrefix(src[1:], "preview") { - prefix = symbol + "preview" - } else if strings.HasPrefix(src[1:], "unbind") { - prefix = symbol + "unbind" - } else if strings.HasPrefix(src[1:], "rebind") { - prefix = symbol + "rebind" - } else if strings.HasPrefix(src[1:], "change-query") { - prefix = symbol + "change-query" - } else if strings.HasPrefix(src[1:], "change-prompt") { - prefix = symbol + "change-prompt" - } else if src[len(prefix)] == '-' { - c := src[len(prefix)+1] - if c == 's' || c == 'S' { - prefix += "-silent" - } else { - prefix += "-multi" - } - } +func maskActionContents(action string) string { + masked := executeRegexp.ReplaceAllStringFunc(action, func(src string) string { + prefix := src[:1] + actionNameRegexp.FindString(src[1:]) return prefix + "(" + strings.Repeat(" ", len(src)-len(prefix)-2) + ")" }) masked = strings.Replace(masked, "::", string([]rune{escapedColon, ':'}), -1) masked = strings.Replace(masked, ",:", string([]rune{escapedComma, ':'}), -1) masked = strings.Replace(masked, "+:", string([]rune{escapedPlus, ':'}), -1) + return masked +} +func parseSingleActionList(str string, exit func(string)) []*action { + // We prepend a colon to satisfy executeRegexp and remove it later + masked := maskActionContents(":" + str)[1:] + return parseActionList(masked, str, []*action{}, false, exit) +} + +func parseActionList(masked string, original string, prevActions []*action, putAllowed bool, exit func(string)) []*action { + maskedStrings := strings.Split(masked, "+") + originalStrings := make([]string, len(maskedStrings)) + idx := 0 + for i, maskedString := range maskedStrings { + originalStrings[i] = original[idx : idx+len(maskedString)] + idx += len(maskedString) + 1 + } + actions := make([]*action, 0, len(maskedStrings)) + appendAction := func(types ...actionType) { + actions = append(actions, toActions(types...)...) + } + prevSpec := "" + for specIndex, spec := range originalStrings { + spec = prevSpec + spec + specLower := strings.ToLower(spec) + switch specLower { + case "ignore": + appendAction(actIgnore) + case "beginning-of-line": + appendAction(actBeginningOfLine) + case "abort": + appendAction(actAbort) + case "accept": + appendAction(actAccept) + case "accept-non-empty": + appendAction(actAcceptNonEmpty) + case "print-query": + appendAction(actPrintQuery) + case "refresh-preview": + appendAction(actRefreshPreview) + case "replace-query": + appendAction(actReplaceQuery) + case "backward-char": + appendAction(actBackwardChar) + case "backward-delete-char": + appendAction(actBackwardDeleteChar) + case "backward-delete-char/eof": + appendAction(actBackwardDeleteCharEOF) + case "backward-word": + appendAction(actBackwardWord) + case "clear-screen": + appendAction(actClearScreen) + case "delete-char": + appendAction(actDeleteChar) + case "delete-char/eof": + appendAction(actDeleteCharEOF) + case "deselect": + appendAction(actDeselect) + case "end-of-line": + appendAction(actEndOfLine) + case "cancel": + appendAction(actCancel) + case "clear-query": + appendAction(actClearQuery) + case "clear-selection": + appendAction(actClearSelection) + case "forward-char": + appendAction(actForwardChar) + case "forward-word": + appendAction(actForwardWord) + case "jump": + appendAction(actJump) + case "jump-accept": + appendAction(actJumpAccept) + case "kill-line": + appendAction(actKillLine) + case "kill-word": + appendAction(actKillWord) + case "unix-line-discard", "line-discard": + appendAction(actUnixLineDiscard) + case "unix-word-rubout", "word-rubout": + appendAction(actUnixWordRubout) + case "yank": + appendAction(actYank) + case "backward-kill-word": + appendAction(actBackwardKillWord) + case "toggle-down": + appendAction(actToggle, actDown) + case "toggle-up": + appendAction(actToggle, actUp) + case "toggle-in": + appendAction(actToggleIn) + case "toggle-out": + appendAction(actToggleOut) + case "toggle-all": + appendAction(actToggleAll) + case "toggle-search": + appendAction(actToggleSearch) + case "select": + appendAction(actSelect) + case "select-all": + appendAction(actSelectAll) + case "deselect-all": + appendAction(actDeselectAll) + case "close": + appendAction(actClose) + case "toggle": + appendAction(actToggle) + case "down": + appendAction(actDown) + case "up": + appendAction(actUp) + case "first", "top": + appendAction(actFirst) + case "last": + appendAction(actLast) + case "page-up": + appendAction(actPageUp) + case "page-down": + appendAction(actPageDown) + case "half-page-up": + appendAction(actHalfPageUp) + case "half-page-down": + appendAction(actHalfPageDown) + case "prev-history", "previous-history": + appendAction(actPrevHistory) + case "next-history": + appendAction(actNextHistory) + case "prev-selected": + appendAction(actPrevSelected) + case "next-selected": + appendAction(actNextSelected) + case "toggle-preview": + appendAction(actTogglePreview) + case "toggle-preview-wrap": + appendAction(actTogglePreviewWrap) + case "toggle-sort": + appendAction(actToggleSort) + case "preview-top": + appendAction(actPreviewTop) + case "preview-bottom": + appendAction(actPreviewBottom) + case "preview-up": + appendAction(actPreviewUp) + case "preview-down": + appendAction(actPreviewDown) + case "preview-page-up": + appendAction(actPreviewPageUp) + case "preview-page-down": + appendAction(actPreviewPageDown) + case "preview-half-page-up": + appendAction(actPreviewHalfPageUp) + case "preview-half-page-down": + appendAction(actPreviewHalfPageDown) + case "enable-search": + appendAction(actEnableSearch) + case "disable-search": + appendAction(actDisableSearch) + case "put": + if putAllowed { + appendAction(actRune) + } else { + exit("unable to put non-printable character") + } + default: + t := isExecuteAction(specLower) + if t == actIgnore { + if specIndex == 0 && specLower == "" { + actions = append(prevActions, actions...) + } else { + exit("unknown action: " + spec) + } + } else { + offset := len(actionNameRegexp.FindString(spec)) + var actionArg string + if spec[offset] == ':' { + if specIndex == len(originalStrings)-1 { + actionArg = spec[offset+1:] + actions = append(actions, &action{t: t, a: actionArg}) + } else { + prevSpec = spec + "+" + continue + } + } else { + actionArg = spec[offset+1 : len(spec)-1] + actions = append(actions, &action{t: t, a: actionArg}) + } + if t == actUnbind || t == actRebind { + parseKeyChords(actionArg, spec[0:offset]+" target required") + } + } + } + prevSpec = "" + } + return actions +} + +func parseKeymap(keymap map[tui.Event][]*action, str string, exit func(string)) { + masked := maskActionContents(str) idx := 0 for _, pairStr := range strings.Split(masked, ",") { origPairStr := str[idx : idx+len(pairStr)] @@ -937,7 +1111,7 @@ func parseKeymap(keymap map[tui.Event][]*action, str string) { pair := strings.SplitN(pairStr, ":", 2) if len(pair) < 2 { - errorExit("bind action not specified: " + origPairStr) + exit("bind action not specified: " + origPairStr) } var key tui.Event if len(pair[0]) == 1 && pair[0][0] == escapedColon { @@ -950,213 +1124,8 @@ func parseKeymap(keymap map[tui.Event][]*action, str string) { keys := parseKeyChords(pair[0], "key name required") key = firstKey(keys) } - - idx2 := len(pair[0]) + 1 - specs := strings.Split(pair[1], "+") - actions := make([]*action, 0, len(specs)) - appendAction := func(types ...actionType) { - actions = append(actions, toActions(types...)...) - } - prevSpec := "" - for specIndex, maskedSpec := range specs { - spec := origPairStr[idx2 : idx2+len(maskedSpec)] - idx2 += len(maskedSpec) + 1 - spec = prevSpec + spec - specLower := strings.ToLower(spec) - switch specLower { - case "ignore": - appendAction(actIgnore) - case "beginning-of-line": - appendAction(actBeginningOfLine) - case "abort": - appendAction(actAbort) - case "accept": - appendAction(actAccept) - case "accept-non-empty": - appendAction(actAcceptNonEmpty) - case "print-query": - appendAction(actPrintQuery) - case "refresh-preview": - appendAction(actRefreshPreview) - case "replace-query": - appendAction(actReplaceQuery) - case "backward-char": - appendAction(actBackwardChar) - case "backward-delete-char": - appendAction(actBackwardDeleteChar) - case "backward-delete-char/eof": - appendAction(actBackwardDeleteCharEOF) - case "backward-word": - appendAction(actBackwardWord) - case "clear-screen": - appendAction(actClearScreen) - case "delete-char": - appendAction(actDeleteChar) - case "delete-char/eof": - appendAction(actDeleteCharEOF) - case "deselect": - appendAction(actDeselect) - case "end-of-line": - appendAction(actEndOfLine) - case "cancel": - appendAction(actCancel) - case "clear-query": - appendAction(actClearQuery) - case "clear-selection": - appendAction(actClearSelection) - case "forward-char": - appendAction(actForwardChar) - case "forward-word": - appendAction(actForwardWord) - case "jump": - appendAction(actJump) - case "jump-accept": - appendAction(actJumpAccept) - case "kill-line": - appendAction(actKillLine) - case "kill-word": - appendAction(actKillWord) - case "unix-line-discard", "line-discard": - appendAction(actUnixLineDiscard) - case "unix-word-rubout", "word-rubout": - appendAction(actUnixWordRubout) - case "yank": - appendAction(actYank) - case "backward-kill-word": - appendAction(actBackwardKillWord) - case "toggle-down": - appendAction(actToggle, actDown) - case "toggle-up": - appendAction(actToggle, actUp) - case "toggle-in": - appendAction(actToggleIn) - case "toggle-out": - appendAction(actToggleOut) - case "toggle-all": - appendAction(actToggleAll) - case "toggle-search": - appendAction(actToggleSearch) - case "select": - appendAction(actSelect) - case "select-all": - appendAction(actSelectAll) - case "deselect-all": - appendAction(actDeselectAll) - case "close": - appendAction(actClose) - case "toggle": - appendAction(actToggle) - case "down": - appendAction(actDown) - case "up": - appendAction(actUp) - case "first", "top": - appendAction(actFirst) - case "last": - appendAction(actLast) - case "page-up": - appendAction(actPageUp) - case "page-down": - appendAction(actPageDown) - case "half-page-up": - appendAction(actHalfPageUp) - case "half-page-down": - appendAction(actHalfPageDown) - case "prev-history", "previous-history": - appendAction(actPrevHistory) - case "next-history": - appendAction(actNextHistory) - case "prev-selected": - appendAction(actPrevSelected) - case "next-selected": - appendAction(actNextSelected) - case "toggle-preview": - appendAction(actTogglePreview) - case "toggle-preview-wrap": - appendAction(actTogglePreviewWrap) - case "toggle-sort": - appendAction(actToggleSort) - case "preview-top": - appendAction(actPreviewTop) - case "preview-bottom": - appendAction(actPreviewBottom) - case "preview-up": - appendAction(actPreviewUp) - case "preview-down": - appendAction(actPreviewDown) - case "preview-page-up": - appendAction(actPreviewPageUp) - case "preview-page-down": - appendAction(actPreviewPageDown) - case "preview-half-page-up": - appendAction(actPreviewHalfPageUp) - case "preview-half-page-down": - appendAction(actPreviewHalfPageDown) - case "enable-search": - appendAction(actEnableSearch) - case "disable-search": - appendAction(actDisableSearch) - case "put": - if key.Type == tui.Rune && unicode.IsGraphic(key.Char) { - appendAction(actRune) - } else { - errorExit("unable to put non-printable character: " + pair[0]) - } - default: - t := isExecuteAction(specLower) - if t == actIgnore { - if specIndex == 0 && specLower == "" { - actions = append(keymap[key], actions...) - } else { - errorExit("unknown action: " + spec) - } - } else { - var offset int - switch t { - case actReload: - offset = len("reload") - case actPreview: - offset = len("preview") - case actChangePreviewWindow: - offset = len("change-preview-window") - case actChangePreview: - offset = len("change-preview") - case actChangePrompt: - offset = len("change-prompt") - case actChangeQuery: - offset = len("change-query") - case actUnbind: - offset = len("unbind") - case actRebind: - offset = len("rebind") - case actExecuteSilent: - offset = len("execute-silent") - case actExecuteMulti: - offset = len("execute-multi") - default: - offset = len("execute") - } - var actionArg string - if spec[offset] == ':' { - if specIndex == len(specs)-1 { - actionArg = spec[offset+1:] - actions = append(actions, &action{t: t, a: actionArg}) - } else { - prevSpec = spec + "+" - continue - } - } else { - actionArg = spec[offset+1 : len(spec)-1] - actions = append(actions, &action{t: t, a: actionArg}) - } - if t == actUnbind || t == actRebind { - parseKeyChords(actionArg, spec[0:offset]+" target required") - } - } - } - prevSpec = "" - } - keymap[key] = actions + putAllowed := key.Type == tui.Rune && unicode.IsGraphic(key.Char) + keymap[key] = parseActionList(pair[1], origPairStr[len(pair[0])+1:], keymap[key], putAllowed, exit) } } @@ -1455,7 +1424,7 @@ func parseOptions(opts *Options, allArgs []string) { case "--tiebreak": opts.Criteria = parseTiebreak(nextString(allArgs, &i, "sort criterion required")) case "--bind": - parseKeymap(opts.Keymap, nextString(allArgs, &i, "bind expression required")) + parseKeymap(opts.Keymap, nextString(allArgs, &i, "bind expression required"), errorExit) case "--color": _, spec := optionalNextString(allArgs, &i) if len(spec) == 0 { @@ -1657,6 +1626,10 @@ func parseOptions(opts *Options, allArgs []string) { nextString(allArgs, &i, "padding required (TRBL / TB,RL / T,RL,B / T,R,B,L)")) case "--tabstop": opts.Tabstop = nextInt(allArgs, &i, "tab stop required") + case "--listen": + opts.ListenPort = nextInt(allArgs, &i, "listen port required") + case "--no-listen": + opts.ListenPort = 0 case "--clear": opts.ClearOnExit = true case "--no-clear": @@ -1723,7 +1696,7 @@ func parseOptions(opts *Options, allArgs []string) { } else if match, value := optString(arg, "--color="); match { opts.Theme = parseTheme(opts.Theme, value) } else if match, value := optString(arg, "--bind="); match { - parseKeymap(opts.Keymap, value) + parseKeymap(opts.Keymap, value, errorExit) } else if match, value := optString(arg, "--history="); match { setHistory(value) } else if match, value := optString(arg, "--history-size="); match { @@ -1744,6 +1717,8 @@ func parseOptions(opts *Options, allArgs []string) { opts.Padding = parseMargin("padding", value) } else if match, value := optString(arg, "--tabstop="); match { opts.Tabstop = atoi(value) + } else if match, value := optString(arg, "--listen="); match { + opts.ListenPort = atoi(value) } else if match, value := optString(arg, "--hscroll-off="); match { opts.HscrollOff = atoi(value) } else if match, value := optString(arg, "--scroll-off="); match { @@ -1773,6 +1748,10 @@ func parseOptions(opts *Options, allArgs []string) { errorExit("tab stop must be a positive integer") } + if opts.ListenPort < 0 || opts.ListenPort > 65535 { + errorExit("invalid listen port") + } + if len(opts.JumpLabels) == 0 { errorExit("empty jump labels") } diff --git a/src/options_test.go b/src/options_test.go index 0fb569f..14ede09 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -262,13 +262,17 @@ func TestBind(t *testing.T) { } } check(tui.CtrlA.AsEvent(), "", actBeginningOfLine) + errorString := "" + errorFn := func(e string) { + errorString = e + } parseKeymap(keymap, "ctrl-a:kill-line,ctrl-b:toggle-sort+up+down,c:page-up,alt-z:page-down,"+ "f1:execute(ls {+})+abort+execute(echo {+})+select-all,f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+ "alt-a:execute-Multi@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};,"+ "x:Execute(foo+bar),X:execute/bar+baz/"+ ",f1:+first,f1:+top"+ - ",,:abort,::accept,+:execute:++\nfoobar,Y:execute(baz)+up") + ",,:abort,::accept,+:execute:++\nfoobar,Y:execute(baz)+up", errorFn) check(tui.CtrlA.AsEvent(), "", actKillLine) check(tui.CtrlB.AsEvent(), "", actToggleSort, actUp, actDown) check(tui.Key('c'), "", actPageUp) @@ -286,12 +290,15 @@ func TestBind(t *testing.T) { check(tui.Key('+'), "++\nfoobar,Y:execute(baz)+up", actExecute) for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} { - parseKeymap(keymap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char)) + parseKeymap(keymap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char), errorFn) check(tui.Key([]rune(fmt.Sprintf("%d", idx%10))[0]), "foobar", actExecute) } - parseKeymap(keymap, "f1:abort") + parseKeymap(keymap, "f1:abort", errorFn) check(tui.F1.AsEvent(), "", actAbort) + if len(errorString) > 0 { + t.Errorf("error parsing keymap: %s", errorString) + } } func TestColorSpec(t *testing.T) { @@ -466,3 +473,19 @@ func TestValidateSign(t *testing.T) { } } } + +func TestParseSingleActionList(t *testing.T) { + actions := parseSingleActionList("Execute@foo+bar,baz@+up+up+reload:down+down", func(string) {}) + if len(actions) != 4 { + t.Errorf("Invalid number of actions parsed:%d", len(actions)) + } + if actions[0].t != actExecute || actions[0].a != "foo+bar,baz" { + t.Errorf("Invalid action parsed: %v", actions[0]) + } + if actions[1].t != actUp || actions[2].t != actUp { + t.Errorf("Invalid action parsed: %v / %v", actions[1], actions[2]) + } + if actions[3].t != actReload || actions[3].a != "down+down" { + t.Errorf("Invalid action parsed: %v", actions[3]) + } +} diff --git a/src/terminal.go b/src/terminal.go index e0ff5b2..47cc34c 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -3,8 +3,10 @@ package fzf import ( "bufio" "fmt" + "io" "io/ioutil" "math" + "net/http" "os" "os/signal" "regexp" @@ -167,6 +169,7 @@ type Terminal struct { padding [4]sizeSpec strong tui.Attr unicode bool + listenPort int borderShape tui.BorderShape cleanExit bool paused bool @@ -200,6 +203,7 @@ type Terminal struct { sigstop bool startChan chan fitpad killChan chan int + serverChan chan []*action slab *util.Slab theme *tui.ColorTheme tui tui.Renderer @@ -481,7 +485,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { } var previewBox *util.EventBox showPreviewWindow := len(opts.Preview.command) > 0 && !opts.Preview.hidden - if len(opts.Preview.command) > 0 || hasPreviewAction(opts) { + // 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 { previewBox = util.NewEventBox() } strongAttr := tui.Bold @@ -556,6 +561,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { margin: opts.Margin, padding: opts.Padding, unicode: opts.Unicode, + listenPort: opts.ListenPort, borderShape: opts.BorderShape, borderLabel: nil, borderLabelOpts: opts.BorderLabel, @@ -595,6 +601,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { theme: opts.Theme, startChan: make(chan fitpad, 1), killChan: make(chan int), + serverChan: make(chan []*action), tui: renderer, initFunc: func() { renderer.Init() }, executing: util.NewAtomicBool(false)} @@ -619,6 +626,39 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { return &t } +func (t *Terminal) startServer() { + if t.listenPort == 0 { + return + } + + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + w.WriteHeader(http.StatusBadRequest) + return + } + body, err := io.ReadAll(r.Body) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + response := "" + actions := parseSingleActionList(string(body), func(message string) { + response = message + }) + + if len(response) > 0 { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintln(w, response) + return + } + t.serverChan <- actions + }) + go func() { + http.ListenAndServe(fmt.Sprintf(":%d", t.listenPort), nil) + }() +} + func borderLines(shape tui.BorderShape) int { switch shape { case tui.BorderHorizontal, tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderDouble: @@ -2256,6 +2296,9 @@ func (t *Terminal) Loop() { env = append(env, "FZF_PREVIEW_"+lines) env = append(env, columns) env = append(env, "FZF_PREVIEW_"+columns) + if t.listenPort > 0 { + env = append(env, fmt.Sprintf("FZF_LISTEN_PORT=%d", t.listenPort)) + } cmd.Env = env } @@ -2492,6 +2535,16 @@ func (t *Terminal) Loop() { looping := true _, startEvent := t.keymap[tui.Start.AsEvent()] + t.startServer() + eventChan := make(chan tui.Event) + needBarrier := true + barrier := make(chan bool) + go func() { + for { + <-barrier + eventChan <- t.tui.GetChar() + } + }() for looping { var newCommand *string changed := false @@ -2499,11 +2552,21 @@ func (t *Terminal) Loop() { queryChanged := false var event tui.Event + actions := []*action{} if startEvent { event = tui.Start.AsEvent() startEvent = false } else { - event = t.tui.GetChar() + if needBarrier { + barrier <- true + } + select { + case event = <-eventChan: + needBarrier = true + case actions = <-t.serverChan: + event = tui.Invalid.AsEvent() + needBarrier = false + } } t.mutex.Lock() @@ -3043,7 +3106,9 @@ func (t *Terminal) Loop() { } if t.jumping == jumpDisabled { - actions := t.keymap[event.Comparable()] + if len(actions) == 0 { + actions = t.keymap[event.Comparable()] + } if len(actions) == 0 && event.Type == tui.Rune { doAction(&action{t: actRune}) } else if !doActions(actions) {