mirror of
https://github.com/Llewellynvdm/fzf.git
synced 2024-12-23 03:19:01 +00:00
Add --listen=HTTP_PORT option to receive actions
Supersedes #2019 See also: * #1728 * https://github.com/junegunn/fzf.vim/pull/1044
This commit is contained in:
parent
51c518da1e
commit
1ba7484d60
@ -3,6 +3,14 @@ CHANGELOG
|
|||||||
|
|
||||||
0.36.0
|
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
|
- Added `next-selected` and `prev-selected` actions to move between selected
|
||||||
items
|
items
|
||||||
```sh
|
```sh
|
||||||
|
@ -721,6 +721,19 @@ ncurses finder only after the input stream is complete.
|
|||||||
e.g. \fBfzf --multi | fzf --sync\fR
|
e.g. \fBfzf --multi | fzf --sync\fR
|
||||||
.RE
|
.RE
|
||||||
.TP
|
.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"
|
.B "--version"
|
||||||
Display version information and exit
|
Display version information and exit
|
||||||
|
|
||||||
|
465
src/options.go
465
src/options.go
@ -113,6 +113,7 @@ const usage = `usage: fzf [options]
|
|||||||
--read0 Read input delimited by ASCII NUL characters
|
--read0 Read input delimited by ASCII NUL characters
|
||||||
--print0 Print output delimited by ASCII NUL characters
|
--print0 Print output delimited by ASCII NUL characters
|
||||||
--sync Synchronous search for multi-staged filtering
|
--sync Synchronous search for multi-staged filtering
|
||||||
|
--listen=HTTP_PORT Start HTTP server to receive actions (POST /)
|
||||||
--version Display version information and exit
|
--version Display version information and exit
|
||||||
|
|
||||||
Environment variables
|
Environment variables
|
||||||
@ -296,6 +297,7 @@ type Options struct {
|
|||||||
PreviewLabel labelOpts
|
PreviewLabel labelOpts
|
||||||
Unicode bool
|
Unicode bool
|
||||||
Tabstop int
|
Tabstop int
|
||||||
|
ListenPort int
|
||||||
ClearOnExit bool
|
ClearOnExit bool
|
||||||
Version bool
|
Version bool
|
||||||
}
|
}
|
||||||
@ -868,8 +870,9 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
executeRegexp *regexp.Regexp
|
executeRegexp *regexp.Regexp
|
||||||
splitRegexp *regexp.Regexp
|
splitRegexp *regexp.Regexp
|
||||||
|
actionNameRegexp *regexp.Regexp
|
||||||
)
|
)
|
||||||
|
|
||||||
func firstKey(keymap map[tui.Event]string) tui.Event {
|
func firstKey(keymap map[tui.Event]string) tui.Event {
|
||||||
@ -891,45 +894,216 @@ func init() {
|
|||||||
executeRegexp = regexp.MustCompile(
|
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)(\([^)]*\)|\[[^\]]*\]|~[^~]*~|![^!]*!|@[^@]*@|\#[^\#]*\#|\$[^\$]*\$|%[^%]*%|\^[^\^]*\^|&[^&]*&|\*[^\*]*\*|;[^;]*;|/[^/]*/|\|[^\|]*\|)`)
|
`(?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("[,:]+")
|
splitRegexp = regexp.MustCompile("[,:]+")
|
||||||
|
actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+")
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseKeymap(keymap map[tui.Event][]*action, str string) {
|
func maskActionContents(action string) string {
|
||||||
masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string {
|
masked := executeRegexp.ReplaceAllStringFunc(action, func(src string) string {
|
||||||
symbol := ":"
|
prefix := src[:1] + actionNameRegexp.FindString(src[1:])
|
||||||
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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return prefix + "(" + strings.Repeat(" ", len(src)-len(prefix)-2) + ")"
|
return prefix + "(" + strings.Repeat(" ", len(src)-len(prefix)-2) + ")"
|
||||||
})
|
})
|
||||||
masked = strings.Replace(masked, "::", string([]rune{escapedColon, ':'}), -1)
|
masked = strings.Replace(masked, "::", string([]rune{escapedColon, ':'}), -1)
|
||||||
masked = strings.Replace(masked, ",:", string([]rune{escapedComma, ':'}), -1)
|
masked = strings.Replace(masked, ",:", string([]rune{escapedComma, ':'}), -1)
|
||||||
masked = strings.Replace(masked, "+:", string([]rune{escapedPlus, ':'}), -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
|
idx := 0
|
||||||
for _, pairStr := range strings.Split(masked, ",") {
|
for _, pairStr := range strings.Split(masked, ",") {
|
||||||
origPairStr := str[idx : idx+len(pairStr)]
|
origPairStr := str[idx : idx+len(pairStr)]
|
||||||
@ -937,7 +1111,7 @@ func parseKeymap(keymap map[tui.Event][]*action, str string) {
|
|||||||
|
|
||||||
pair := strings.SplitN(pairStr, ":", 2)
|
pair := strings.SplitN(pairStr, ":", 2)
|
||||||
if len(pair) < 2 {
|
if len(pair) < 2 {
|
||||||
errorExit("bind action not specified: " + origPairStr)
|
exit("bind action not specified: " + origPairStr)
|
||||||
}
|
}
|
||||||
var key tui.Event
|
var key tui.Event
|
||||||
if len(pair[0]) == 1 && pair[0][0] == escapedColon {
|
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")
|
keys := parseKeyChords(pair[0], "key name required")
|
||||||
key = firstKey(keys)
|
key = firstKey(keys)
|
||||||
}
|
}
|
||||||
|
putAllowed := key.Type == tui.Rune && unicode.IsGraphic(key.Char)
|
||||||
idx2 := len(pair[0]) + 1
|
keymap[key] = parseActionList(pair[1], origPairStr[len(pair[0])+1:], keymap[key], putAllowed, exit)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1455,7 +1424,7 @@ func parseOptions(opts *Options, allArgs []string) {
|
|||||||
case "--tiebreak":
|
case "--tiebreak":
|
||||||
opts.Criteria = parseTiebreak(nextString(allArgs, &i, "sort criterion required"))
|
opts.Criteria = parseTiebreak(nextString(allArgs, &i, "sort criterion required"))
|
||||||
case "--bind":
|
case "--bind":
|
||||||
parseKeymap(opts.Keymap, nextString(allArgs, &i, "bind expression required"))
|
parseKeymap(opts.Keymap, nextString(allArgs, &i, "bind expression required"), errorExit)
|
||||||
case "--color":
|
case "--color":
|
||||||
_, spec := optionalNextString(allArgs, &i)
|
_, spec := optionalNextString(allArgs, &i)
|
||||||
if len(spec) == 0 {
|
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)"))
|
nextString(allArgs, &i, "padding required (TRBL / TB,RL / T,RL,B / T,R,B,L)"))
|
||||||
case "--tabstop":
|
case "--tabstop":
|
||||||
opts.Tabstop = nextInt(allArgs, &i, "tab stop required")
|
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":
|
case "--clear":
|
||||||
opts.ClearOnExit = true
|
opts.ClearOnExit = true
|
||||||
case "--no-clear":
|
case "--no-clear":
|
||||||
@ -1723,7 +1696,7 @@ func parseOptions(opts *Options, allArgs []string) {
|
|||||||
} else if match, value := optString(arg, "--color="); match {
|
} else if match, value := optString(arg, "--color="); match {
|
||||||
opts.Theme = parseTheme(opts.Theme, value)
|
opts.Theme = parseTheme(opts.Theme, value)
|
||||||
} else if match, value := optString(arg, "--bind="); match {
|
} 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 {
|
} else if match, value := optString(arg, "--history="); match {
|
||||||
setHistory(value)
|
setHistory(value)
|
||||||
} else if match, value := optString(arg, "--history-size="); match {
|
} else if match, value := optString(arg, "--history-size="); match {
|
||||||
@ -1744,6 +1717,8 @@ func parseOptions(opts *Options, allArgs []string) {
|
|||||||
opts.Padding = parseMargin("padding", value)
|
opts.Padding = parseMargin("padding", value)
|
||||||
} else if match, value := optString(arg, "--tabstop="); match {
|
} else if match, value := optString(arg, "--tabstop="); match {
|
||||||
opts.Tabstop = atoi(value)
|
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 {
|
} else if match, value := optString(arg, "--hscroll-off="); match {
|
||||||
opts.HscrollOff = atoi(value)
|
opts.HscrollOff = atoi(value)
|
||||||
} else if match, value := optString(arg, "--scroll-off="); match {
|
} 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")
|
errorExit("tab stop must be a positive integer")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if opts.ListenPort < 0 || opts.ListenPort > 65535 {
|
||||||
|
errorExit("invalid listen port")
|
||||||
|
}
|
||||||
|
|
||||||
if len(opts.JumpLabels) == 0 {
|
if len(opts.JumpLabels) == 0 {
|
||||||
errorExit("empty jump labels")
|
errorExit("empty jump labels")
|
||||||
}
|
}
|
||||||
|
@ -262,13 +262,17 @@ func TestBind(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
check(tui.CtrlA.AsEvent(), "", actBeginningOfLine)
|
check(tui.CtrlA.AsEvent(), "", actBeginningOfLine)
|
||||||
|
errorString := ""
|
||||||
|
errorFn := func(e string) {
|
||||||
|
errorString = e
|
||||||
|
}
|
||||||
parseKeymap(keymap,
|
parseKeymap(keymap,
|
||||||
"ctrl-a:kill-line,ctrl-b:toggle-sort+up+down,c:page-up,alt-z:page-down,"+
|
"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 {};,"+
|
"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 (,),[,],/,:,@,%,{};,"+
|
"alt-a:execute-Multi@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};,"+
|
||||||
"x:Execute(foo+bar),X:execute/bar+baz/"+
|
"x:Execute(foo+bar),X:execute/bar+baz/"+
|
||||||
",f1:+first,f1:+top"+
|
",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.CtrlA.AsEvent(), "", actKillLine)
|
||||||
check(tui.CtrlB.AsEvent(), "", actToggleSort, actUp, actDown)
|
check(tui.CtrlB.AsEvent(), "", actToggleSort, actUp, actDown)
|
||||||
check(tui.Key('c'), "", actPageUp)
|
check(tui.Key('c'), "", actPageUp)
|
||||||
@ -286,12 +290,15 @@ func TestBind(t *testing.T) {
|
|||||||
check(tui.Key('+'), "++\nfoobar,Y:execute(baz)+up", actExecute)
|
check(tui.Key('+'), "++\nfoobar,Y:execute(baz)+up", actExecute)
|
||||||
|
|
||||||
for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} {
|
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)
|
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)
|
check(tui.F1.AsEvent(), "", actAbort)
|
||||||
|
if len(errorString) > 0 {
|
||||||
|
t.Errorf("error parsing keymap: %s", errorString)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestColorSpec(t *testing.T) {
|
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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -3,8 +3,10 @@ package fzf
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"math"
|
"math"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"regexp"
|
"regexp"
|
||||||
@ -167,6 +169,7 @@ type Terminal struct {
|
|||||||
padding [4]sizeSpec
|
padding [4]sizeSpec
|
||||||
strong tui.Attr
|
strong tui.Attr
|
||||||
unicode bool
|
unicode bool
|
||||||
|
listenPort int
|
||||||
borderShape tui.BorderShape
|
borderShape tui.BorderShape
|
||||||
cleanExit bool
|
cleanExit bool
|
||||||
paused bool
|
paused bool
|
||||||
@ -200,6 +203,7 @@ type Terminal struct {
|
|||||||
sigstop bool
|
sigstop bool
|
||||||
startChan chan fitpad
|
startChan chan fitpad
|
||||||
killChan chan int
|
killChan chan int
|
||||||
|
serverChan chan []*action
|
||||||
slab *util.Slab
|
slab *util.Slab
|
||||||
theme *tui.ColorTheme
|
theme *tui.ColorTheme
|
||||||
tui tui.Renderer
|
tui tui.Renderer
|
||||||
@ -481,7 +485,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
|||||||
}
|
}
|
||||||
var previewBox *util.EventBox
|
var previewBox *util.EventBox
|
||||||
showPreviewWindow := len(opts.Preview.command) > 0 && !opts.Preview.hidden
|
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()
|
previewBox = util.NewEventBox()
|
||||||
}
|
}
|
||||||
strongAttr := tui.Bold
|
strongAttr := tui.Bold
|
||||||
@ -556,6 +561,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
|||||||
margin: opts.Margin,
|
margin: opts.Margin,
|
||||||
padding: opts.Padding,
|
padding: opts.Padding,
|
||||||
unicode: opts.Unicode,
|
unicode: opts.Unicode,
|
||||||
|
listenPort: opts.ListenPort,
|
||||||
borderShape: opts.BorderShape,
|
borderShape: opts.BorderShape,
|
||||||
borderLabel: nil,
|
borderLabel: nil,
|
||||||
borderLabelOpts: opts.BorderLabel,
|
borderLabelOpts: opts.BorderLabel,
|
||||||
@ -595,6 +601,7 @@ 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),
|
||||||
tui: renderer,
|
tui: renderer,
|
||||||
initFunc: func() { renderer.Init() },
|
initFunc: func() { renderer.Init() },
|
||||||
executing: util.NewAtomicBool(false)}
|
executing: util.NewAtomicBool(false)}
|
||||||
@ -619,6 +626,39 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
|||||||
return &t
|
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 {
|
func borderLines(shape tui.BorderShape) int {
|
||||||
switch shape {
|
switch shape {
|
||||||
case tui.BorderHorizontal, tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderDouble:
|
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, "FZF_PREVIEW_"+lines)
|
||||||
env = append(env, columns)
|
env = append(env, columns)
|
||||||
env = append(env, "FZF_PREVIEW_"+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
|
cmd.Env = env
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2492,6 +2535,16 @@ func (t *Terminal) Loop() {
|
|||||||
looping := true
|
looping := true
|
||||||
_, startEvent := t.keymap[tui.Start.AsEvent()]
|
_, 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 {
|
for looping {
|
||||||
var newCommand *string
|
var newCommand *string
|
||||||
changed := false
|
changed := false
|
||||||
@ -2499,11 +2552,21 @@ func (t *Terminal) Loop() {
|
|||||||
queryChanged := false
|
queryChanged := false
|
||||||
|
|
||||||
var event tui.Event
|
var event tui.Event
|
||||||
|
actions := []*action{}
|
||||||
if startEvent {
|
if startEvent {
|
||||||
event = tui.Start.AsEvent()
|
event = tui.Start.AsEvent()
|
||||||
startEvent = false
|
startEvent = false
|
||||||
} else {
|
} 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()
|
t.mutex.Lock()
|
||||||
@ -3043,7 +3106,9 @@ func (t *Terminal) Loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if t.jumping == jumpDisabled {
|
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 {
|
if len(actions) == 0 && event.Type == tui.Rune {
|
||||||
doAction(&action{t: actRune})
|
doAction(&action{t: actRune})
|
||||||
} else if !doActions(actions) {
|
} else if !doActions(actions) {
|
||||||
|
Loading…
Reference in New Issue
Block a user