diff options
| author | Junegunn Choi <junegunn.c@gmail.com> | 2025-06-16 00:39:11 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-06-16 00:39:11 +0900 |
| commit | 0c00b203e61bffbadbc499cbf68af6f89a5a3e29 (patch) | |
| tree | effd2636b35640cede4f2e8e362f18ee91836a5b /src/terminal.go | |
| parent | 3b68dcdd81394f1ac9f743e1f74ff754f95eef9e (diff) | |
| download | fzf-0c00b203e61bffbadbc499cbf68af6f89a5a3e29.tar.gz | |
Implement asynchronous transform actions (#4419)
Close #4418
Example:
fzf --bind 'focus:bg-transform-header(sleep 2; date; echo {})'
Diffstat (limited to 'src/terminal.go')
| -rw-r--r-- | src/terminal.go | 465 |
1 files changed, 309 insertions, 156 deletions
diff --git a/src/terminal.go b/src/terminal.go index 898d34c7..85a51112 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -388,6 +388,10 @@ type Terminal struct { startChan chan fitpad killChan chan bool serverInputChan chan []*action + callbackChan chan func() + bgQueue map[action][]func() + bgSemaphore chan struct{} + bgSemaphores map[action]chan struct{} keyChan chan tui.Event eventChan chan tui.Event slab *util.Slab @@ -489,6 +493,7 @@ const ( actBackwardDeleteCharEof actBackwardWord actCancel + actChangeBorderLabel actChangeGhost actChangeHeader @@ -505,6 +510,7 @@ const ( actChangePreviewWindow actChangePrompt actChangeQuery + actClearScreen actClearQuery actClearSelection @@ -561,6 +567,7 @@ const ( actHidePreview actTogglePreview actTogglePreviewWrap + actTransform actTransformBorderLabel actTransformGhost @@ -576,6 +583,23 @@ const ( actTransformPrompt actTransformQuery actTransformSearch + + actBgTransform + actBgTransformBorderLabel + actBgTransformGhost + actBgTransformHeader + actBgTransformFooter + actBgTransformHeaderLabel + actBgTransformFooterLabel + actBgTransformInputLabel + actBgTransformListLabel + actBgTransformNth + actBgTransformPointer + actBgTransformPreviewLabel + actBgTransformPrompt + actBgTransformQuery + actBgTransformSearch + actSearch actPreview actPreviewTop @@ -613,6 +637,7 @@ const ( actBell actExclude actExcludeMulti + actAsync ) func (a actionType) Name() string { @@ -623,10 +648,34 @@ func processExecution(action actionType) bool { switch action { case actTransform, actTransformBorderLabel, + actTransformGhost, actTransformHeader, + actTransformFooter, + actTransformHeaderLabel, + actTransformFooterLabel, + actTransformInputLabel, + actTransformListLabel, + actTransformNth, + actTransformPointer, actTransformPreviewLabel, actTransformPrompt, actTransformQuery, + actTransformSearch, + actBgTransform, + actBgTransformBorderLabel, + actBgTransformGhost, + actBgTransformHeader, + actBgTransformFooter, + actBgTransformHeaderLabel, + actBgTransformFooterLabel, + actBgTransformInputLabel, + actBgTransformListLabel, + actBgTransformNth, + actBgTransformPointer, + actBgTransformPreviewLabel, + actBgTransformPrompt, + actBgTransformQuery, + actBgTransformSearch, actPreview, actChangePreview, actRefreshPreview, @@ -773,7 +822,7 @@ func mayTriggerPreview(opts *Options) bool { for _, actions := range opts.Keymap { for _, action := range actions { switch action.t { - case actPreview, actChangePreview, actTransform: + case actPreview, actChangePreview, actTransform, actBgTransform: return true } } @@ -987,6 +1036,10 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor startChan: make(chan fitpad, 1), killChan: make(chan bool), serverInputChan: make(chan []*action, 100), + callbackChan: make(chan func(), maxBgProcesses), + bgQueue: make(map[action][]func()), + bgSemaphore: make(chan struct{}, maxBgProcesses), + bgSemaphores: make(map[action]chan struct{}), keyChan: make(chan tui.Event), eventChan: make(chan tui.Event, 6), // start | (load + result + zero|one) | (focus) | (resize) tui: renderer, @@ -2578,7 +2631,9 @@ func (t *Terminal) printPrompt() { before, after := t.updatePromptOffset() if len(before) == 0 && len(after) == 0 && len(t.ghost) > 0 { - w.CPrint(tui.ColGhost, t.ghost) + maxWidth := util.Max(1, w.Width()-t.promptLen-1) + runes, _ := t.trimRight([]rune(t.ghost), maxWidth) + w.CPrint(tui.ColGhost, string(runes)) return } @@ -4291,6 +4346,75 @@ func (t *Terminal) captureLines(template string) string { return t.executeCommand(template, false, true, true, false, "") } +func (t *Terminal) captureAsync(a action, firstLineOnly bool, callback func(string)) { + _, list := t.buildPlusList(a.a, false) + command, tempFiles := t.replacePlaceholder(a.a, false, string(t.input), list) + item := func() { + cmd := t.executor.ExecCommand(command, false) + cmd.Env = t.environ() + + out, _ := cmd.StdoutPipe() + reader := bufio.NewReader(out) + var output string + if err := cmd.Start(); err == nil { + if firstLineOnly { + output, _ = reader.ReadString('\n') + output = strings.TrimRight(output, "\r\n") + } else { + bytes, _ := io.ReadAll(reader) + output = string(bytes) + } + cmd.Wait() + } + removeFiles(tempFiles) + + t.callbackChan <- func() { callback(output) } + } + queue, prs := t.bgQueue[a] + if !prs { + queue = []func(){} + } + queue = append(queue, item) + t.bgQueue[a] = queue +} + +func (t *Terminal) dispatchAsync() { +Loop: + for a, queue := range t.bgQueue { + delete(t.bgQueue, a) + if len(queue) == 0 { + continue + } + + semaphore, prs := t.bgSemaphores[a] + if !prs { + semaphore = make(chan struct{}, maxBgProcessesPerAction) + t.bgSemaphores[a] = semaphore + } + for _, item := range queue { + select { + // Acquire local semaphore + case semaphore <- struct{}{}: + default: + // Failed to acquire local semaphore, putting only the last one back to the queue + t.bgQueue[a] = queue[len(queue)-1:] + continue Loop + } + todo := item + go func() { + // Acquire global semaphore + t.bgSemaphore <- struct{}{} + + todo() + // Release local semaphore + <-semaphore + // Release global semaphore + <-t.bgSemaphore + }() + } + } +} + func (t *Terminal) executeCommand(template string, forcePlus bool, background bool, capture bool, firstLineOnly bool, info string) string { line := "" valid, list := t.buildPlusList(template, forcePlus) @@ -5089,11 +5213,27 @@ func (t *Terminal) Loop() error { barrier <- true needBarrier = false } + + // These variables are defined outside the loop to be accessible from closures + events := []util.EventType{} + changed := false + var newNth *[]Range + req := func(evts ...util.EventType) { + for _, event := range evts { + events = append(events, event) + if event == reqClose || event == reqQuit { + looping = false + } + } + } + + // The main event loop for loopIndex := int64(0); looping; loopIndex++ { var newCommand *commandSpec - var newNth *[]Range var reloadSync bool - changed := false + events = []util.EventType{} + changed = false + newNth = nil beof := false queryChanged := false denylist := []int32{} @@ -5110,6 +5250,7 @@ func (t *Terminal) Loop() error { var event tui.Event actions := []*action{} + callbacks := []func(){} select { case event = <-t.keyChan: needBarrier = true @@ -5141,6 +5282,20 @@ func (t *Terminal) Loop() error { } } } + case callback := <-t.callbackChan: + event = tui.Invalid.AsEvent() + actions = append(actions, &action{t: actAsync}) + callbacks = append(callbacks, callback) + DrainCallback: + for { + select { + case callback = <-t.callbackChan: + callbacks = append(callbacks, callback) + continue DrainCallback + default: + break DrainCallback + } + } } t.mutex.Lock() @@ -5155,15 +5310,6 @@ func (t *Terminal) Loop() error { previousInput := t.input previousCx := t.cx t.lastKey = event.KeyName() - events := []util.EventType{} - req := func(evts ...util.EventType) { - for _, event := range evts { - events = append(events, event) - if event == reqClose || event == reqQuit { - looping = false - } - } - } updatePreviewWindow := func(forcePreview bool) { t.resizeWindows(forcePreview, false) req(reqPrompt, reqList, reqInfo, reqHeader, reqFooter) @@ -5238,9 +5384,29 @@ func (t *Terminal) Loop() error { // actions to allow changing the query even when the input is hidden // e.g. fzf --no-input --bind 'space:show-input+change-query(foo)+hide-input' currentInput := t.input + capture := func(firstLineOnly bool, callback func(string)) { + if a.t >= actBgTransform { + // bg-transform-* + t.captureAsync(*a, firstLineOnly, callback) + } else if a.t >= actTransform { + // transform-* + if firstLineOnly { + callback(t.captureLine(a.a)) + } else { + callback(t.captureLines(a.a)) + } + } else { + // change-* + callback(a.a) + } + } Action: switch a.t { case actIgnore, actStart, actClick: + case actAsync: + for _, callback := range callbacks { + callback() + } case actBecome: valid, list := t.buildPlusList(a.a, false) if valid { @@ -5333,15 +5499,17 @@ func (t *Terminal) Loop() error { t.previewed.version = 0 req(reqPreviewRefresh) } - case actTransformPrompt: - prompt := t.captureLine(a.a) - t.promptString = prompt - t.prompt, t.promptLen = t.parsePrompt(prompt) - req(reqPrompt) - case actTransformQuery: - query := t.captureLine(a.a) - t.input = []rune(query) - t.cx = len(t.input) + case actTransformPrompt, actBgTransformPrompt: + capture(true, func(prompt string) { + t.promptString = prompt + t.prompt, t.promptLen = t.parsePrompt(prompt) + req(reqPrompt) + }) + case actTransformQuery, actBgTransformQuery: + capture(true, func(query string) { + t.input = []rune(query) + t.cx = len(t.input) + }) case actToggleSort: t.sort = !t.sort changed = true @@ -5399,119 +5567,102 @@ func (t *Terminal) Loop() error { } t.multi = multi req(reqList, reqInfo) - case actChangeNth, actTransformNth: - expr := a.a - if a.t == actTransformNth { - expr = t.captureLine(a.a) - } - - // Split nth expression - tokens := strings.Split(expr, "|") - if nth, err := splitNth(tokens[0]); err == nil { - // Changed - newNth = &nth - } else { - // The default - newNth = &t.nth - } - // Cycle - if len(tokens) > 1 { - a.a = strings.Join(append(tokens[1:], tokens[0]), "|") - } - if !compareRanges(t.nthCurrent, *newNth) { - changed = true - t.nthCurrent = *newNth - t.forceRerenderList() - } + case actChangeNth, actTransformNth, actBgTransformNth: + capture(true, func(expr string) { + // Split nth expression + tokens := strings.Split(expr, "|") + if nth, err := splitNth(tokens[0]); err == nil { + // Changed + newNth = &nth + } else { + // The default + newNth = &t.nth + } + // Cycle + if len(tokens) > 1 { + a.a = strings.Join(append(tokens[1:], tokens[0]), "|") + } + if !compareRanges(t.nthCurrent, *newNth) { + changed = true + t.nthCurrent = *newNth + t.forceRerenderList() + } + }) case actChangeQuery: t.input = []rune(a.a) t.cx = len(t.input) - case actChangeHeader, actTransformHeader: - header := a.a - if a.t == actTransformHeader { - header = t.captureLines(a.a) - } - if t.changeHeader(header) { - if t.headerWindow != nil { - // Need to resize header window + case actChangeHeader, actTransformHeader, actBgTransformHeader: + capture(false, func(header string) { + if t.changeHeader(header) { + if t.headerWindow != nil { + // Need to resize header window + req(reqFullRedraw) + } else { + req(reqHeader, reqList, reqPrompt, reqInfo) + } + } else { + req(reqHeader) + } + }) + case actChangeFooter, actTransformFooter, actBgTransformFooter: + capture(false, func(footer string) { + if t.changeFooter(footer) { req(reqFullRedraw) } else { - req(reqHeader, reqList, reqPrompt, reqInfo) + req(reqFooter) } - } else { - req(reqHeader) - } - case actChangeFooter, actTransformFooter: - footer := a.a - if a.t == actTransformFooter { - footer = t.captureLines(a.a) - } - if t.changeFooter(footer) { - req(reqFullRedraw) - } else { - req(reqFooter) - } - case actChangeHeaderLabel, actTransformHeaderLabel: - label := a.a - if a.t == actTransformHeaderLabel { - label = t.captureLine(a.a) - } - t.headerLabelOpts.label = label - t.headerLabel, t.headerLabelLen = t.ansiLabelPrinter(label, &tui.ColHeaderLabel, false) - req(reqRedrawHeaderLabel) - case actChangeFooterLabel, actTransformFooterLabel: - label := a.a - if a.t == actTransformFooterLabel { - label = t.captureLine(a.a) - } - t.footerLabelOpts.label = label - t.footerLabel, t.footerLabelLen = t.ansiLabelPrinter(label, &tui.ColFooterLabel, false) - req(reqRedrawFooterLabel) - case actChangeInputLabel, actTransformInputLabel: - label := a.a - if a.t == actTransformInputLabel { - label = t.captureLine(a.a) - } - t.inputLabelOpts.label = label - if t.inputBorder != nil { - t.inputLabel, t.inputLabelLen = t.ansiLabelPrinter(label, &tui.ColInputLabel, false) - req(reqRedrawInputLabel) - } - case actChangeListLabel, actTransformListLabel: - label := a.a - if a.t == actTransformListLabel { - label = t.captureLine(a.a) - } - t.listLabelOpts.label = label - if t.wborder != nil { - t.listLabel, t.listLabelLen = t.ansiLabelPrinter(label, &tui.ColListLabel, false) - req(reqRedrawListLabel) - } - case actChangeBorderLabel, actTransformBorderLabel: - label := a.a - if a.t == actTransformBorderLabel { - label = t.captureLine(a.a) - } - t.borderLabelOpts.label = label - if t.border != nil { - t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(label, &tui.ColBorderLabel, false) - req(reqRedrawBorderLabel) - } - case actChangePreviewLabel, actTransformPreviewLabel: - label := a.a - if a.t == actTransformPreviewLabel { - label = t.captureLine(a.a) - } - t.previewLabelOpts.label = label - if t.pborder != nil { - t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(label, &tui.ColPreviewLabel, false) - req(reqRedrawPreviewLabel) - } - case actTransform: - body := t.captureLines(a.a) - if actions, err := parseSingleActionList(strings.Trim(body, "\r\n")); err == nil { - return doActions(actions) - } + }) + case actChangeHeaderLabel, actTransformHeaderLabel, actBgTransformHeaderLabel: + capture(true, func(label string) { + t.headerLabelOpts.label = label + t.headerLabel, t.headerLabelLen = t.ansiLabelPrinter(label, &tui.ColHeaderLabel, false) + req(reqRedrawHeaderLabel) + }) + case actChangeFooterLabel, actTransformFooterLabel, actBgTransformFooterLabel: + capture(true, func(label string) { + t.footerLabelOpts.label = label + t.footerLabel, t.footerLabelLen = t.ansiLabelPrinter(label, &tui.ColFooterLabel, false) + req(reqRedrawFooterLabel) + }) + case actChangeInputLabel, actTransformInputLabel, actBgTransformInputLabel: + capture(true, func(label string) { + t.inputLabelOpts.label = label + if t.inputBorder != nil { + t.inputLabel, t.inputLabelLen = t.ansiLabelPrinter(label, &tui.ColInputLabel, false) + req(reqRedrawInputLabel) + } + }) + case actChangeListLabel, actTransformListLabel, actBgTransformListLabel: + capture(true, func(label string) { + t.listLabelOpts.label = label + if t.wborder != nil { + t.listLabel, t.listLabelLen = t.ansiLabelPrinter(label, &tui.ColListLabel, false) + req(reqRedrawListLabel) + } + }) + case actChangeBorderLabel, actTransformBorderLabel, actBgTransformBorderLabel: + capture(true, func(label string) { + t.borderLabelOpts.label = label + if t.border != nil { + t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(label, &tui.ColBorderLabel, false) + req(reqRedrawBorderLabel) + } + }) + case actChangePreviewLabel, actTransformPreviewLabel, actBgTransformPreviewLabel: + capture(true, func(label string) { + t.previewLabelOpts.label = label + if t.pborder != nil { + t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(label, &tui.ColPreviewLabel, false) + req(reqRedrawPreviewLabel) + } + }) + case actTransform, actBgTransform: + capture(false, func(body string) { + if actions, err := parseSingleActionList(strings.Trim(body, "\r\n")); err == nil { + // NOTE: We're not properly passing the return value here + doActions(actions) + } + }) case actChangePrompt: t.promptString = a.a t.prompt, t.promptLen = t.parsePrompt(a.a) @@ -5933,10 +6084,12 @@ func (t *Terminal) Loop() error { override := []rune(a.a) t.inputOverride = &override changed = true - case actTransformSearch: - override := []rune(t.captureLine(a.a)) - t.inputOverride = &override - changed = true + case actTransformSearch, actBgTransformSearch: + capture(true, func(query string) { + override := []rune(query) + t.inputOverride = &override + changed = true + }) case actEnableSearch: t.paused = false changed = true @@ -6276,30 +6429,26 @@ func (t *Terminal) Loop() error { } } } - case actChangeGhost, actTransformGhost: - ghost := a.a - if a.t == actTransformGhost { - ghost = t.captureLine(a.a) - } - t.ghost = ghost - if len(t.input) == 0 { - req(reqPrompt) - } - case actChangePointer, actTransformPointer: - pointer := a.a - if a.t == actTransformPointer { - pointer = t.captureLine(a.a) - } - length := uniseg.StringWidth(pointer) - if length <= 2 { - if length != t.pointerLen { - t.forceRerenderList() + case actChangeGhost, actTransformGhost, actBgTransformGhost: + capture(true, func(ghost string) { + t.ghost = ghost + if len(t.input) == 0 { + req(reqPrompt) } - t.pointer = pointer - t.pointerLen = length - t.pointerEmpty = strings.Repeat(" ", t.pointerLen) - req(reqList) - } + }) + case actChangePointer, actTransformPointer, actBgTransformPointer: + capture(true, func(pointer string) { + length := uniseg.StringWidth(pointer) + if length <= 2 { + if length != t.pointerLen { + t.forceRerenderList() + } + t.pointer = pointer + t.pointerLen = length + t.pointerEmpty = strings.Repeat(" ", t.pointerLen) + req(reqList) + } + }) case actChangePreview: if t.previewOpts.command != a.a { t.previewOpts.command = a.a @@ -6451,6 +6600,10 @@ func (t *Terminal) Loop() error { if reload { reloadRequest = &searchRequest{sort: t.sort, sync: reloadSync, nth: newNth, command: newCommand, environ: t.environ(), changed: changed, denylist: denylist, revision: t.merger.Revision()} } + + // Dispatch queued background requests + t.dispatchAsync() + t.mutex.Unlock() // Must be unlocked before touching reqBox if reload { |
