From 11962dabba69e706246bfcd54fa42b1e1c6bee8b Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 9 Nov 2019 22:54:48 +0900 Subject: [PATCH 01/14] Add --phony option for disabling search With --phony, fzf becomes a simply selector interface without its own search functionality. The query string is only used for building the command for preview or execute action. Close #1723 --- src/core.go | 10 ++++++++-- src/options.go | 7 +++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/core.go b/src/core.go index 2db5b3ae..ae8c8ebe 100644 --- a/src/core.go +++ b/src/core.go @@ -227,6 +227,12 @@ func Run(opts *Options, revision string) { for { delay := true ticks++ + input := func() []rune { + if opts.Phony { + return []rune{} + } + return []rune(terminal.Input()) + } eventBox.Wait(func(events *util.Events) { if _, fin := (*events)[EvtReadFin]; fin { delete(*events, EvtReadNew) @@ -241,7 +247,7 @@ func Run(opts *Options, revision string) { if opts.Sync { terminal.UpdateList(PassMerger(&snapshot, opts.Tac)) } - matcher.Reset(snapshot, terminal.Input(), false, !reading, sort) + matcher.Reset(snapshot, input(), false, !reading, sort) case EvtSearchNew: switch val := value.(type) { @@ -249,7 +255,7 @@ func Run(opts *Options, revision string) { sort = val } snapshot, _ := chunkList.Snapshot() - matcher.Reset(snapshot, terminal.Input(), true, !reading, sort) + matcher.Reset(snapshot, input(), true, !reading, sort) delay = false case EvtSearchProgress: diff --git a/src/options.go b/src/options.go index 2ab3a896..9e093797 100644 --- a/src/options.go +++ b/src/options.go @@ -33,6 +33,7 @@ const usage = `usage: fzf [options] -d, --delimiter=STR Field delimiter regex (default: AWK-style) +s, --no-sort Do not sort the result --tac Reverse the order of the input + --phony Do not perform search --tiebreak=CRI[,..] Comma-separated list of sort criteria to apply when the scores are tied [length|begin|end|index] (default: length) @@ -154,6 +155,7 @@ type Options struct { Fuzzy bool FuzzyAlgo algo.Algo Extended bool + Phony bool Case Case Normalize bool Nth []Range @@ -207,6 +209,7 @@ func defaultOptions() *Options { Fuzzy: true, FuzzyAlgo: algo.FuzzyMatchV2, Extended: true, + Phony: false, Case: CaseSmart, Normalize: true, Nth: make([]Range, 0), @@ -1014,6 +1017,10 @@ func parseOptions(opts *Options, allArgs []string) { } case "--no-expect": opts.Expect = make(map[int]string) + case "--no-phony": + opts.Phony = false + case "--phony": + opts.Phony = true case "--tiebreak": opts.Criteria = parseTiebreak(nextString(allArgs, &i, "sort criterion required")) case "--bind": From 78da9287272a0bfa183498c5b2e9fde10a3663a0 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 10 Nov 2019 11:36:22 +0900 Subject: [PATCH 02/14] Experimental implementation of "reload" action # Reload input list with different sources seq 10 | fzf --bind 'ctrl-a:reload(seq 100),ctrl-b:reload(seq 1000)' # Reload as you type seq 10 | fzf --bind 'change:reload:seq {q}' --phony # Integration with ripgrep RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case " INITIAL_QUERY="" FZF_DEFAULT_COMMAND="$RG_PREFIX '$INITIAL_QUERY'" \ fzf --bind "change:reload:$RG_PREFIX {q} || true" \ --ansi --phony --query "$INITIAL_QUERY" Close #751 Close #965 Close #974 Close #1736 Related #1723 --- src/chunklist.go | 7 ++++ src/core.go | 36 +++++++++++++++++---- src/matcher.go | 15 +++++---- src/options.go | 10 ++++-- src/reader.go | 68 +++++++++++++++++++++++++++++++------- src/reader_test.go | 5 +-- src/terminal.go | 81 +++++++++++++++++++++++++++++----------------- 7 files changed, 164 insertions(+), 58 deletions(-) diff --git a/src/chunklist.go b/src/chunklist.go index 510cd734..cd635c25 100644 --- a/src/chunklist.go +++ b/src/chunklist.go @@ -64,6 +64,13 @@ func (cl *ChunkList) Push(data []byte) bool { return ret } +// Clear clears the data +func (cl *ChunkList) Clear() { + cl.mutex.Lock() + cl.chunks = nil + cl.mutex.Unlock() +} + // Snapshot returns immutable snapshot of the ChunkList func (cl *ChunkList) Snapshot() ([]*Chunk, int) { cl.mutex.Lock() diff --git a/src/core.go b/src/core.go index ae8c8ebe..d9d98d18 100644 --- a/src/core.go +++ b/src/core.go @@ -135,8 +135,9 @@ func Run(opts *Options, revision string) { // Reader streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync + var reader *Reader if !streamingFilter { - reader := NewReader(func(data []byte) bool { + reader = NewReader(func(data []byte) bool { return chunkList.Push(data) }, eventBox, opts.ReadZero) go reader.ReadSource() @@ -223,6 +224,7 @@ func Run(opts *Options, revision string) { // Event coordination reading := true ticks := 0 + var nextCommand *string eventBox.Watch(EvtReadNew) for { delay := true @@ -241,21 +243,41 @@ func Run(opts *Options, revision string) { switch evt { case EvtReadNew, EvtReadFin: - reading = reading && evt == EvtReadNew + clearCache := false + if evt == EvtReadFin && nextCommand != nil { + chunkList.Clear() + clearCache = true + go reader.restart(*nextCommand) + nextCommand = nil + } else { + reading = reading && evt == EvtReadNew + } snapshot, count := chunkList.Snapshot() - terminal.UpdateCount(count, !reading, value.(bool)) + terminal.UpdateCount(count, !reading, value.(*string)) if opts.Sync { terminal.UpdateList(PassMerger(&snapshot, opts.Tac)) } - matcher.Reset(snapshot, input(), false, !reading, sort) + matcher.Reset(snapshot, input(), false, !reading, sort, clearCache) case EvtSearchNew: + var command *string switch val := value.(type) { - case bool: - sort = val + case searchRequest: + sort = val.sort + command = val.command + } + if command != nil { + if reading { + reader.terminate() + nextCommand = command + } else { + reading = true + chunkList.Clear() + go reader.restart(*command) + } } snapshot, _ := chunkList.Snapshot() - matcher.Reset(snapshot, input(), true, !reading, sort) + matcher.Reset(snapshot, input(), true, !reading, sort, command != nil) delay = false case EvtSearchProgress: diff --git a/src/matcher.go b/src/matcher.go index 69250873..22aa819c 100644 --- a/src/matcher.go +++ b/src/matcher.go @@ -12,10 +12,11 @@ import ( // MatchRequest represents a search request type MatchRequest struct { - chunks []*Chunk - pattern *Pattern - final bool - sort bool + chunks []*Chunk + pattern *Pattern + final bool + sort bool + clearCache bool } // Matcher is responsible for performing search @@ -69,7 +70,7 @@ func (m *Matcher) Loop() { events.Clear() }) - if request.sort != m.sort { + if request.sort != m.sort || request.clearCache { m.sort = request.sort m.mergerCache = make(map[string]*Merger) clearChunkCache() @@ -221,7 +222,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) { } // Reset is called to interrupt/signal the ongoing search -func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool) { +func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool, clearCache bool) { pattern := m.patternBuilder(patternRunes) var event util.EventType @@ -230,5 +231,5 @@ func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final } else { event = reqRetry } - m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort && pattern.sortable}) + m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort && pattern.sortable, clearCache}) } diff --git a/src/options.go b/src/options.go index 9e093797..848ce3d9 100644 --- a/src/options.go +++ b/src/options.go @@ -631,13 +631,15 @@ func init() { // Backreferences are not supported. // "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|') executeRegexp = regexp.MustCompile( - `(?si):(execute(?:-multi|-silent)?):.+|:(execute(?:-multi|-silent)?)(\([^)]*\)|\[[^\]]*\]|~[^~]*~|![^!]*!|@[^@]*@|\#[^\#]*\#|\$[^\$]*\$|%[^%]*%|\^[^\^]*\^|&[^&]*&|\*[^\*]*\*|;[^;]*;|/[^/]*/|\|[^\|]*\|)`) + `(?si):(execute(?:-multi|-silent)?|reload):.+|:(execute(?:-multi|-silent)?|reload)(\([^)]*\)|\[[^\]]*\]|~[^~]*~|![^!]*!|@[^@]*@|\#[^\#]*\#|\$[^\$]*\$|%[^%]*%|\^[^\^]*\^|&[^&]*&|\*[^\*]*\*|;[^;]*;|/[^/]*/|\|[^\|]*\|)`) } func parseKeymap(keymap map[int][]action, str string) { masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string { prefix := ":execute" - if src[len(prefix)] == '-' { + if strings.HasPrefix(src, ":reload") { + prefix = ":reload" + } else if src[len(prefix)] == '-' { c := src[len(prefix)+1] if c == 's' || c == 'S' { prefix += "-silent" @@ -790,6 +792,8 @@ func parseKeymap(keymap map[int][]action, str string) { } else { var offset int switch t { + case actReload: + offset = len("reload") case actExecuteSilent: offset = len("execute-silent") case actExecuteMulti: @@ -825,6 +829,8 @@ func isExecuteAction(str string) actionType { prefix = matches[0][2] } switch prefix { + case "reload": + return actReload case "execute": return actExecute case "execute-silent": diff --git a/src/reader.go b/src/reader.go index b418f549..dd68486c 100644 --- a/src/reader.go +++ b/src/reader.go @@ -4,6 +4,8 @@ import ( "bufio" "io" "os" + "os/exec" + "sync" "sync/atomic" "time" @@ -16,11 +18,16 @@ type Reader struct { eventBox *util.EventBox delimNil bool event int32 + finChan chan bool + mutex sync.Mutex + exec *exec.Cmd + command *string + killed bool } // NewReader returns new Reader object func NewReader(pusher func([]byte) bool, eventBox *util.EventBox, delimNil bool) *Reader { - return &Reader{pusher, eventBox, delimNil, int32(EvtReady)} + return &Reader{pusher, eventBox, delimNil, int32(EvtReady), make(chan bool, 1), sync.Mutex{}, nil, nil, false} } func (r *Reader) startEventPoller() { @@ -29,9 +36,10 @@ func (r *Reader) startEventPoller() { pollInterval := readerPollIntervalMin for { if atomic.CompareAndSwapInt32(ptr, int32(EvtReadNew), int32(EvtReady)) { - r.eventBox.Set(EvtReadNew, true) + r.eventBox.Set(EvtReadNew, (*string)(nil)) pollInterval = readerPollIntervalMin } else if atomic.LoadInt32(ptr) == int32(EvtReadFin) { + r.finChan <- true return } else { pollInterval += readerPollIntervalStep @@ -46,7 +54,35 @@ func (r *Reader) startEventPoller() { func (r *Reader) fin(success bool) { atomic.StoreInt32(&r.event, int32(EvtReadFin)) - r.eventBox.Set(EvtReadFin, success) + <-r.finChan + + r.mutex.Lock() + ret := r.command + if success || r.killed { + ret = nil + } + r.mutex.Unlock() + + r.eventBox.Set(EvtReadFin, ret) +} + +func (r *Reader) terminate() { + r.mutex.Lock() + defer func() { r.mutex.Unlock() }() + + r.killed = true + if r.exec != nil && r.exec.Process != nil { + util.KillCommand(r.exec) + } else { + os.Stdin.Close() + } +} + +func (r *Reader) restart(command string) { + r.event = int32(EvtReady) + r.startEventPoller() + success := r.readFromCommand(nil, command) + r.fin(success) } // ReadSource reads data from the default command or from standard input @@ -54,12 +90,13 @@ func (r *Reader) ReadSource() { r.startEventPoller() var success bool if util.IsTty() { + // The default command for *nix requires bash + shell := "bash" cmd := os.Getenv("FZF_DEFAULT_COMMAND") if len(cmd) == 0 { - // The default command for *nix requires bash - success = r.readFromCommand("bash", defaultCommand) + success = r.readFromCommand(&shell, defaultCommand) } else { - success = r.readFromCommand("sh", cmd) + success = r.readFromCommand(nil, cmd) } } else { success = r.readFromStdin() @@ -102,16 +139,25 @@ func (r *Reader) readFromStdin() bool { return true } -func (r *Reader) readFromCommand(shell string, cmd string) bool { - listCommand := util.ExecCommandWith(shell, cmd, false) - out, err := listCommand.StdoutPipe() +func (r *Reader) readFromCommand(shell *string, command string) bool { + r.mutex.Lock() + r.killed = false + r.command = &command + if shell != nil { + r.exec = util.ExecCommandWith(*shell, command, true) + } else { + r.exec = util.ExecCommand(command, true) + } + out, err := r.exec.StdoutPipe() if err != nil { + r.mutex.Unlock() return false } - err = listCommand.Start() + err = r.exec.Start() + r.mutex.Unlock() if err != nil { return false } r.feed(out) - return listCommand.Wait() == nil + return r.exec.Wait() == nil } diff --git a/src/reader_test.go b/src/reader_test.go index c29936ce..b0610688 100644 --- a/src/reader_test.go +++ b/src/reader_test.go @@ -12,6 +12,7 @@ func TestReadFromCommand(t *testing.T) { eb := util.NewEventBox() reader := Reader{ pusher: func(s []byte) bool { strs = append(strs, string(s)); return true }, + finChan: make(chan bool, 1), eventBox: eb, event: int32(EvtReady)} @@ -23,7 +24,7 @@ func TestReadFromCommand(t *testing.T) { } // Normal command - reader.fin(reader.readFromCommand("sh", `echo abc && echo def`)) + reader.fin(reader.readFromCommand(nil, `echo abc && echo def`)) if len(strs) != 2 || strs[0] != "abc" || strs[1] != "def" { t.Errorf("%s", strs) } @@ -48,7 +49,7 @@ func TestReadFromCommand(t *testing.T) { reader.startEventPoller() // Failing command - reader.fin(reader.readFromCommand("sh", `no-such-command`)) + reader.fin(reader.readFromCommand(nil, `no-such-command`)) strs = []string{} if len(strs) > 0 { t.Errorf("%s", strs) diff --git a/src/terminal.go b/src/terminal.go index 3c656cce..2db811c6 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -102,7 +102,7 @@ type Terminal struct { count int progress int reading bool - success bool + failed *string jumping jumpMode jumpLabels string printer func(string) @@ -228,6 +228,7 @@ const ( actExecuteMulti // Deprecated actSigStop actTop + actReload ) type placeholderFlags struct { @@ -238,6 +239,11 @@ type placeholderFlags struct { file bool } +type searchRequest struct { + sort bool + command *string +} + func toActions(types ...actionType) []action { actions := make([]action, len(types)) for idx, t := range types { @@ -408,7 +414,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { ansi: opts.Ansi, tabstop: opts.Tabstop, reading: true, - success: true, + failed: nil, jumping: jumpDisabled, jumpLabels: opts.JumpLabels, printer: opts.Printer, @@ -440,11 +446,11 @@ func (t *Terminal) Input() []rune { } // UpdateCount updates the count information -func (t *Terminal) UpdateCount(cnt int, final bool, success bool) { +func (t *Terminal) UpdateCount(cnt int, final bool, failedCommand *string) { t.mutex.Lock() t.count = cnt t.reading = !final - t.success = success + t.failed = failedCommand t.mutex.Unlock() t.reqBox.Set(reqInfo, nil) if final { @@ -742,7 +748,9 @@ func (t *Terminal) printInfo() { pos = 2 } - output := fmt.Sprintf("%d/%d", t.merger.Length(), t.count) + found := t.merger.Length() + total := util.Max(found, t.count) + output := fmt.Sprintf("%d/%d", found, total) if t.toggleSort { if t.sort { output += " +S" @@ -760,16 +768,15 @@ func (t *Terminal) printInfo() { if t.progress > 0 && t.progress < 100 { output += fmt.Sprintf(" (%d%%)", t.progress) } - if !t.success && t.count == 0 { - if len(os.Getenv("FZF_DEFAULT_COMMAND")) > 0 { - output = "[$FZF_DEFAULT_COMMAND failed]" - } else { - output = "[default command failed - $FZF_DEFAULT_COMMAND required]" - } + if t.failed != nil && t.count == 0 { + output = fmt.Sprintf("[Command failed: %s]", *t.failed) } - if pos+len(output) <= t.window.Width() { - t.window.CPrint(tui.ColInfo, 0, output) + maxWidth := t.window.Width() - pos + if len(output) > maxWidth { + outputRunes, _ := t.trimRight([]rune(output), maxWidth-2) + output = string(outputRunes) + ".." } + t.window.CPrint(tui.ColInfo, 0, output) } func (t *Terminal) printHeader() { @@ -1383,7 +1390,7 @@ func (t *Terminal) hasPreviewWindow() bool { func (t *Terminal) currentItem() *Item { cnt := t.merger.Length() - if cnt > 0 && cnt > t.cy { + if t.cy >= 0 && cnt > 0 && cnt > t.cy { return t.merger.Get(t.cy).item } return nil @@ -1508,11 +1515,10 @@ func (t *Terminal) Loop() { t.mutex.Lock() reading := t.reading t.mutex.Unlock() - if !reading { - break - } time.Sleep(spinnerDuration) - t.reqBox.Set(reqInfo, nil) + if reading { + t.reqBox.Set(reqInfo, nil) + } } }() } @@ -1533,7 +1539,7 @@ func (t *Terminal) Loop() { // We don't display preview window if no match if request[0] != nil { command := replacePlaceholder(t.preview.command, - t.ansi, t.delimiter, t.printsep, false, string(t.input), request) + t.ansi, t.delimiter, t.printsep, false, string(t.Input()), request) cmd := util.ExecCommand(command, true) if t.pwindow != nil { env := os.Environ() @@ -1673,6 +1679,10 @@ func (t *Terminal) Loop() { looping := true for looping { + var newCommand *string + changed := false + queryChanged := false + event := t.tui.GetChar() t.mutex.Lock() @@ -1754,9 +1764,7 @@ func (t *Terminal) Loop() { } case actToggleSort: t.sort = !t.sort - t.eventBox.Set(EvtSearchNew, t.sort) - t.mutex.Unlock() - return false + changed = true case actPreviewUp: if t.hasPreviewWindow() { scrollPreview(-1) @@ -2025,10 +2033,24 @@ func (t *Terminal) Loop() { } } } + case actReload: + t.failed = nil + + valid, list := t.buildPlusList(a.a, false) + // If the command template has {q}, we run the command even when the + // query string is empty. + if !valid { + _, query := hasPreviewFlags(a.a) + valid = query + } + if valid { + command := replacePlaceholder(a.a, + t.ansi, t.delimiter, t.printsep, false, string(t.input), list) + newCommand = &command + } } return true } - changed := false mapkey := event.Type if t.jumping == jumpDisabled { actions := t.keymap[mapkey] @@ -2042,8 +2064,9 @@ func (t *Terminal) Loop() { continue } t.truncateQuery() - changed = string(previousInput) != string(t.input) - if onChanges, prs := t.keymap[tui.Change]; changed && prs { + queryChanged = string(previousInput) != string(t.input) + changed = changed || queryChanged + if onChanges, prs := t.keymap[tui.Change]; queryChanged && prs { if !doActions(onChanges, tui.Change) { continue } @@ -2061,7 +2084,7 @@ func (t *Terminal) Loop() { req(reqList) } - if changed { + if queryChanged { if t.isPreviewEnabled() { _, q := hasPreviewFlags(t.preview.command) if q { @@ -2070,14 +2093,14 @@ func (t *Terminal) Loop() { } } - if changed || t.cx != previousCx { + if queryChanged || t.cx != previousCx { req(reqPrompt) } t.mutex.Unlock() // Must be unlocked before touching reqBox - if changed { - t.eventBox.Set(EvtSearchNew, t.sort) + if changed || newCommand != nil { + t.eventBox.Set(EvtSearchNew, searchRequest{sort: t.sort, command: newCommand}) } for _, event := range events { t.reqBox.Set(event, nil) From e975bd0c8da25b51c0713d951af10cbd44cc3e15 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 10 Nov 2019 13:13:45 +0900 Subject: [PATCH 03/14] Add test cases for --phony and reload action --- test/test_go.rb | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/test/test_go.rb b/test/test_go.rb index 9282f9ee..1dbbf5ce 100755 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -277,7 +277,7 @@ class TestGoFZF < TestBase def test_fzf_default_command_failure tmux.send_keys fzf.sub('FZF_DEFAULT_COMMAND=', 'FZF_DEFAULT_COMMAND=false'), :Enter - tmux.until { |lines| lines[-2].include?('FZF_DEFAULT_COMMAND failed') } + tmux.until { |lines| lines[-2].include?('Command failed: false') } tmux.send_keys :Enter end @@ -1612,6 +1612,26 @@ class TestGoFZF < TestBase tmux.send_keys "#{FZF} --preview 'cat #{tempname}'", :Enter tmux.until { |lines| lines[1].include?('+ green') } end + + def test_phony + tmux.send_keys %(seq 1000 | #{FZF} --query 333 --phony --preview 'echo {} {q}'), :Enter + tmux.until { |lines| lines.match_count == 1000 } + tmux.until { |lines| lines[1].include?('1 333') } + tmux.send_keys 'foo' + tmux.until { |lines| lines.match_count == 1000 } + tmux.until { |lines| lines[1].include?('1 333foo') } + end + + def test_reload + tmux.send_keys %(seq 1000 | #{FZF} --bind 'change:reload(seq {q}),a:reload(seq 100),b:reload:seq 200'), :Enter + tmux.until { |lines| lines.match_count == 1000 } + tmux.send_keys 'a' + tmux.until { |lines| lines.item_count == 100 && lines.match_count == 100 } + tmux.send_keys 'b' + tmux.until { |lines| lines.item_count == 200 && lines.match_count == 200 } + tmux.send_keys '555' + tmux.until { |lines| lines.item_count == 555 && lines.match_count == 1 } + end end module TestShell From 73c0a645e0fd80acb5ddb755142834a245745888 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 11 Nov 2019 12:53:03 +0900 Subject: [PATCH 04/14] Remove unnecessary reader barrier on --filter mode --- src/core.go | 4 ++-- src/reader.go | 13 +++++++++---- src/reader_test.go | 8 +++----- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/core.go b/src/core.go index d9d98d18..c5ad2353 100644 --- a/src/core.go +++ b/src/core.go @@ -139,7 +139,7 @@ func Run(opts *Options, revision string) { if !streamingFilter { reader = NewReader(func(data []byte) bool { return chunkList.Push(data) - }, eventBox, opts.ReadZero) + }, eventBox, opts.ReadZero, opts.Filter == nil) go reader.ReadSource() } @@ -183,7 +183,7 @@ func Run(opts *Options, revision string) { } } return false - }, eventBox, opts.ReadZero) + }, eventBox, opts.ReadZero, false) reader.ReadSource() } else { eventBox.Unwatch(EvtReadNew) diff --git a/src/reader.go b/src/reader.go index dd68486c..b388411b 100644 --- a/src/reader.go +++ b/src/reader.go @@ -23,11 +23,12 @@ type Reader struct { exec *exec.Cmd command *string killed bool + wait bool } // NewReader returns new Reader object -func NewReader(pusher func([]byte) bool, eventBox *util.EventBox, delimNil bool) *Reader { - return &Reader{pusher, eventBox, delimNil, int32(EvtReady), make(chan bool, 1), sync.Mutex{}, nil, nil, false} +func NewReader(pusher func([]byte) bool, eventBox *util.EventBox, delimNil bool, wait bool) *Reader { + return &Reader{pusher, eventBox, delimNil, int32(EvtReady), make(chan bool, 1), sync.Mutex{}, nil, nil, false, wait} } func (r *Reader) startEventPoller() { @@ -39,7 +40,9 @@ func (r *Reader) startEventPoller() { r.eventBox.Set(EvtReadNew, (*string)(nil)) pollInterval = readerPollIntervalMin } else if atomic.LoadInt32(ptr) == int32(EvtReadFin) { - r.finChan <- true + if r.wait { + r.finChan <- true + } return } else { pollInterval += readerPollIntervalStep @@ -54,7 +57,9 @@ func (r *Reader) startEventPoller() { func (r *Reader) fin(success bool) { atomic.StoreInt32(&r.event, int32(EvtReadFin)) - <-r.finChan + if r.wait { + <-r.finChan + } r.mutex.Lock() ret := r.command diff --git a/src/reader_test.go b/src/reader_test.go index b0610688..8bbb488e 100644 --- a/src/reader_test.go +++ b/src/reader_test.go @@ -10,11 +10,9 @@ import ( func TestReadFromCommand(t *testing.T) { strs := []string{} eb := util.NewEventBox() - reader := Reader{ - pusher: func(s []byte) bool { strs = append(strs, string(s)); return true }, - finChan: make(chan bool, 1), - eventBox: eb, - event: int32(EvtReady)} + reader := NewReader( + func(s []byte) bool { strs = append(strs, string(s)); return true }, + eb, false, true) reader.startEventPoller() From deccf20a359af9f7420be9dc6aad4c50d6b329e7 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 11 Nov 2019 23:31:31 +0900 Subject: [PATCH 05/14] Fix regression of select-all --- src/terminal.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/terminal.go b/src/terminal.go index 2db811c6..d1fbe40d 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -1429,7 +1429,7 @@ func (t *Terminal) selectItem(item *Item) bool { return false } if _, found := t.selected[item.Index()]; found { - return false + return true } t.selected[item.Index()] = selectedItem{time.Now(), item} From 7e1c0f39e76864ad0aa4d5c88d074b1055a617a6 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 12 Nov 2019 00:10:24 +0900 Subject: [PATCH 06/14] 'reload' action should reset --header-lines --- src/core.go | 13 ++++++++----- test/test_go.rb | 10 +++++----- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/core.go b/src/core.go index c5ad2353..087c7df9 100644 --- a/src/core.go +++ b/src/core.go @@ -225,6 +225,12 @@ func Run(opts *Options, revision string) { reading := true ticks := 0 var nextCommand *string + restart := func(command string) { + reading = true + chunkList.Clear() + header = make([]string, 0, opts.HeaderLines) + go reader.restart(command) + } eventBox.Watch(EvtReadNew) for { delay := true @@ -245,9 +251,8 @@ func Run(opts *Options, revision string) { case EvtReadNew, EvtReadFin: clearCache := false if evt == EvtReadFin && nextCommand != nil { - chunkList.Clear() clearCache = true - go reader.restart(*nextCommand) + restart(*nextCommand) nextCommand = nil } else { reading = reading && evt == EvtReadNew @@ -271,9 +276,7 @@ func Run(opts *Options, revision string) { reader.terminate() nextCommand = command } else { - reading = true - chunkList.Clear() - go reader.restart(*command) + restart(*command) } } snapshot, _ := chunkList.Snapshot() diff --git a/test/test_go.rb b/test/test_go.rb index 1dbbf5ce..a441e5ee 100755 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1623,14 +1623,14 @@ class TestGoFZF < TestBase end def test_reload - tmux.send_keys %(seq 1000 | #{FZF} --bind 'change:reload(seq {q}),a:reload(seq 100),b:reload:seq 200'), :Enter - tmux.until { |lines| lines.match_count == 1000 } + tmux.send_keys %(seq 1000 | #{FZF} --bind 'change:reload(seq {q}),a:reload(seq 100),b:reload:seq 200' --header-lines 2), :Enter + tmux.until { |lines| lines.match_count == 998 } tmux.send_keys 'a' - tmux.until { |lines| lines.item_count == 100 && lines.match_count == 100 } + tmux.until { |lines| lines.item_count == 98 && lines.match_count == 98 } tmux.send_keys 'b' - tmux.until { |lines| lines.item_count == 200 && lines.match_count == 200 } + tmux.until { |lines| lines.item_count == 198 && lines.match_count == 198 } tmux.send_keys '555' - tmux.until { |lines| lines.item_count == 555 && lines.match_count == 1 } + tmux.until { |lines| lines.item_count == 553 && lines.match_count == 1 } end end From 05b5f3f845a88e9b034f3302bb50672fc5b7f8c7 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 12 Nov 2019 00:57:19 +0900 Subject: [PATCH 07/14] 'reload' action should reset multi-selection --- src/terminal.go | 1 + test/test_go.rb | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/terminal.go b/src/terminal.go index d1fbe40d..c5ff3025 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -2047,6 +2047,7 @@ func (t *Terminal) Loop() { command := replacePlaceholder(a.a, t.ansi, t.delimiter, t.printsep, false, string(t.input), list) newCommand = &command + t.selected = make(map[int32]selectedItem) } } return true diff --git a/test/test_go.rb b/test/test_go.rb index a441e5ee..210f8fec 100755 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1623,14 +1623,17 @@ class TestGoFZF < TestBase end def test_reload - tmux.send_keys %(seq 1000 | #{FZF} --bind 'change:reload(seq {q}),a:reload(seq 100),b:reload:seq 200' --header-lines 2), :Enter + tmux.send_keys %(seq 1000 | #{FZF} --bind 'change:reload(seq {q}),a:reload(seq 100),b:reload:seq 200' --header-lines 2 --multi 2), :Enter tmux.until { |lines| lines.match_count == 998 } tmux.send_keys 'a' tmux.until { |lines| lines.item_count == 98 && lines.match_count == 98 } tmux.send_keys 'b' tmux.until { |lines| lines.item_count == 198 && lines.match_count == 198 } + tmux.send_keys :Tab + tmux.until { |lines| lines[-2].include?('(1/2)') } tmux.send_keys '555' tmux.until { |lines| lines.item_count == 553 && lines.match_count == 1 } + tmux.until { |lines| !lines[-2].include?('(1/2)') } end end From 751aa1944ae7fd1d8d1a12cc622f77cde160bb52 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 12 Nov 2019 22:45:25 +0900 Subject: [PATCH 08/14] Remove trailing whitespaces when using --with-nth --- src/core.go | 1 + src/util/chars.go | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/src/core.go b/src/core.go index 087c7df9..9d118a49 100644 --- a/src/core.go +++ b/src/core.go @@ -126,6 +126,7 @@ func Run(opts *Options, revision string) { return false } item.text, item.colors = ansiProcessor([]byte(transformed)) + item.text.TrimTrailingWhitespaces() item.text.Index = itemIndex item.origText = &data itemIndex++ diff --git a/src/util/chars.go b/src/util/chars.go index e36ab769..a57ba4bb 100644 --- a/src/util/chars.go +++ b/src/util/chars.go @@ -142,6 +142,11 @@ func (chars *Chars) TrailingWhitespaces() int { return whitespaces } +func (chars *Chars) TrimTrailingWhitespaces() { + whitespaces := chars.TrailingWhitespaces() + chars.slice = chars.slice[0 : len(chars.slice)-whitespaces] +} + func (chars *Chars) ToString() string { if runes := chars.optionalRunes(); runes != nil { return string(runes) From 23a06d63ac63352206b7f6d1ca6973cb1cc5dac3 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 13 Nov 2019 01:27:39 +0900 Subject: [PATCH 09/14] Update CHANGELOG and man pages --- CHANGELOG.md | 44 ++++++++ man/man1/fzf-tmux.1 | 4 +- man/man1/fzf.1 | 242 +++++++++++++++++++++++++++++++------------- 3 files changed, 218 insertions(+), 72 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70293d82..c18a6eae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,50 @@ CHANGELOG ========= +0.19.0 +------ + +- Added "reload" action for dynamically updating the input list without + restarting fzf. See https://github.com/junegunn/fzf/issues/1750 to learn + more about it. + ```sh + # Using fzf as the selector interface for ripgrep + RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case " + INITIAL_QUERY="foo" + FZF_DEFAULT_COMMAND="$RG_PREFIX '$INITIAL_QUERY'" \ + fzf --bind "change:reload:$RG_PREFIX {q} || true" \ + --ansi --phony --query "$INITIAL_QUERY" + ``` +- `--multi` now takes an optional integer argument which indicates the maximum + number of items that can be selected + ```sh + seq 100 | fzf --multi 3 --reverse --height 50% + ``` +- If a placeholder expression for `--preview` and `execute` action (and the + new `reload` action) contains `f` flag, it is replaced to the + path of a temporary file that holds the evaluated list. This is useful + when you multi-select a large number of items and the length of the + evaluated string may exceed [`ARG_MAX`][argmax]. + ```sh + # Press CTRL-A to select 100K items and see the sum of all the numbers + seq 100000 | fzf --multi --bind ctrl-a:select-all \ + --preview "awk '{sum+=\$1} END {print sum}' {+f}" + ``` +- `deselect-all` no longer deselects unmatched items. It is now consistent + with `select-all` and `toggle-all` in that it only affects matched items. +- Due to the limitation of bash, fuzzy completion is enabled by default for + a fixed set of commands. A helper function for easily setting up fuzzy + completion for any command is now provided. + ```sh + # usage: _fzf_setup_completion path|dir COMMANDS... + _fzf_setup_completion path git kubectl + ``` +- When you transform the input with `--with-nth`, the trailing white spaces + are removed. +- See https://github.com/junegunn/fzf/milestone/15?closed=1 for more details + +[argmax]: https://unix.stackexchange.com/questions/120642/what-defines-the-maximum-size-for-a-command-single-argument + 0.18.0 ------ diff --git a/man/man1/fzf-tmux.1 b/man/man1/fzf-tmux.1 index 2bad0241..6a5ee467 100644 --- a/man/man1/fzf-tmux.1 +++ b/man/man1/fzf-tmux.1 @@ -1,7 +1,7 @@ .ig The MIT License (MIT) -Copyright (c) 2017 Junegunn Choi +Copyright (c) 2019 Junegunn Choi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. .. -.TH fzf-tmux 1 "Mar 2019" "fzf 0.18.0" "fzf-tmux - open fzf in tmux split pane" +.TH fzf-tmux 1 "Nov 2019" "fzf 0.19.0" "fzf-tmux - open fzf in tmux split pane" .SH NAME fzf-tmux - open fzf in tmux split pane diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 3a994f65..906fbe61 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -1,7 +1,7 @@ .ig The MIT License (MIT) -Copyright (c) 2017 Junegunn Choi +Copyright (c) 2019 Junegunn Choi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. .. -.TH fzf 1 "Mar 2019" "fzf 0.18.0" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Nov 2019" "fzf 0.19.0" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder @@ -70,6 +70,10 @@ Transform the presentation of each line using field index expressions .TP .BI "-d, --delimiter=" "STR" Field delimiter regex for \fB--nth\fR and \fB--with-nth\fR (default: AWK-style) +.TP +.BI "--phony" +Do not perform search. With this option, fzf becomes a simple selector +interface rather than a "fuzzy finder". .SS Search result .TP .B "+s, --no-sort" @@ -79,7 +83,8 @@ Do not sort the result Reverse the order of the input .RS -e.g. \fBhistory | fzf --tac --no-sort\fR +e.g. + \fBhistory | fzf --tac --no-sort\fR .RE .TP .BI "--tiebreak=" "CRI[,..]" @@ -109,7 +114,8 @@ Comma-separated list of sort criteria to apply when the scores are tied. .SS Interface .TP .B "-m, --multi" -Enable multi-select with tab/shift-tab +Enable multi-select with tab/shift-tab. It optionally takes an integer argument +which denotes the maximum number of items that can be selected. .TP .B "+m, --no-multi" Disable multi-select @@ -118,8 +124,8 @@ Disable multi-select Disable mouse .TP .BI "--bind=" "KEYBINDS" -Comma-separated list of custom key bindings. See \fBKEY BINDINGS\fR for the -details. +Comma-separated list of custom key bindings. See \fBKEY/EVENT BINDINGS\fR for +the details. .TP .B "--cycle" Enable cyclic scroll @@ -201,8 +207,9 @@ terminal size with \fB%\fR suffix. .br .br -e.g. \fBfzf --margin 10%\fR - \fBfzf --margin 1,5%\fR +e.g. + \fBfzf --margin 10% + fzf --margin 1,5%\fR .RE .TP .B "--inline-info" @@ -235,11 +242,6 @@ color mappings. Ansi color code of -1 denotes terminal default foreground/background color. You can also specify 24-bit color in \fB#rrggbb\fR format. -.RS -e.g. \fBfzf --color=bg+:24\fR - \fBfzf --color=light,fg:232,bg:255,bg+:116,info:27\fR -.RE - .RS .B BASE SCHEME: (default: dark on 256-color terminal, otherwise 16) @@ -264,6 +266,19 @@ e.g. \fBfzf --color=bg+:24\fR \fBmarker \fRMulti-select marker \fBspinner \fRStreaming input indicator \fBheader \fRHeader + +.B EXAMPLES: + + \fB# Seoul256 theme with 8-bit colors + # (https://github.com/junegunn/seoul256.vim) + fzf --color='bg:237,bg+:236,info:143,border:240,spinner:108' \\ + --color='hl:65,fg:252,header:65,fg+:252' \\ + --color='pointer:161,marker:168,prompt:110,hl+:108' + + # Seoul256 theme with 24-bit colors + fzf --color='bg:#4B4B4B,bg+:#3F3F3F,info:#BDBB72,border:#6B6B6B,spinner:#98BC99' \\ + --color='hl:#719872,fg:#D9D9D9,header:#719872,fg+:#D9D9D9' \\ + --color='pointer:#E12672,marker:#E17899,prompt:#98BEDE,hl+:#98BC99'\fR .RE .TP .B "--no-bold" @@ -291,8 +306,9 @@ string, specify field index expressions between the braces (See \fBFIELD INDEX EXPRESSION\fR for the details). .RS -e.g. \fBfzf --preview='head -$LINES {}'\fR - \fBls -l | fzf --preview="echo user={3} when={-4..-2}; cat {-1}" --header-lines=1\fR +e.g. + \fBfzf --preview='head -$LINES {}' + ls -l | fzf --preview="echo user={3} when={-4..-2}; cat {-1}" --header-lines=1\fR fzf exports \fB$FZF_PREVIEW_LINES\fR and \fB$FZF_PREVIEW_COLUMNS\fR so that they represent the exact size of the preview window. (It also overrides @@ -304,8 +320,9 @@ A placeholder expression starting with \fB+\fR flag will be replaced to the space-separated list of the selected lines (or the current line if no selection was made) individually quoted. -e.g. \fBfzf --multi --preview='head -10 {+}'\fR - \fBgit log --oneline | fzf --multi --preview 'git show {+1}'\fR +e.g. + \fBfzf --multi --preview='head -10 {+}' + git log --oneline | fzf --multi --preview 'git show {+1}'\fR When using a field index expression, leading and trailing whitespace is stripped from the replacement string. To preserve the whitespace, use the \fBs\fR flag. @@ -314,6 +331,17 @@ Also, \fB{q}\fR is replaced to the current query string, and \fB{n}\fR is replaced to zero-based ordinal index of the line. Use \fB{+n}\fR if you want all index numbers when multiple lines are selected. +A placeholder expression with \fBf\fR flag is replaced to the path of +a temporary file that holds the evaluated list. This is useful when you +multi-select a large number of items and the length of the evaluated string may +exceed \fBARG_MAX\fR. + +e.g. + \fB# Press CTRL-A to select 100K items and see the sum of all the numbers. + # This won't work properly without 'f' flag due to ARG_MAX limit. + seq 100000 | fzf --multi --bind ctrl-a:select-all \\ + --preview "awk '{sum+=\$1} END {print sum}' {+f}"\fR + Note that you can escape a placeholder pattern by prepending a backslash. Preview window will be updated even when there is no match for the current @@ -338,8 +366,9 @@ execute the command in the background. .RE .RS -e.g. \fBfzf --preview="head {}" --preview-window=up:30%\fR - \fBfzf --preview="file {}" --preview-window=down:1\fR +e.g. + \fBfzf --preview="head {}" --preview-window=up:30% + fzf --preview="file {}" --preview-window=down:1\fR .RE .SS Scripting .TP @@ -369,7 +398,8 @@ times, fzf will expect the union of the keys. \fB--no-expect\fR will clear the list. .RS -e.g. \fBfzf --expect=ctrl-v,ctrl-t,alt-s --expect=f1,f2,~,@\fR +e.g. + \fBfzf --expect=ctrl-v,ctrl-t,alt-s --expect=f1,f2,~,@\fR .RE .TP .B "--read0" @@ -475,56 +505,102 @@ query matches entries that start with \fBcore\fR and end with either \fBgo\fR, e.g. \fB^core go$ | rb$ | py$\fR -.SH KEY BINDINGS -You can customize key bindings of fzf with \fB--bind\fR option which takes -a comma-separated list of key binding expressions. Each key binding expression -follows the following format: \fBKEY:ACTION\fR +.SH KEY/EVENT BINDINGS +\fB--bind\fR option allows you to bind \fBa key\fR or \fBan event\fR to one or +more \fBactions\fR. You can use it to customize key bindings or implement +dynamic behaviors. -e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR +\fB--bind\fR takes a comma-separated list of binding expressions. Each binding +expression is \fBKEY:ACTION\fR or \fBEVENT:ACTION\fR. -.B AVAILABLE KEYS: (SYNONYMS) - \fIctrl-[a-z]\fR - \fIctrl-space\fR - \fIctrl-alt-[a-z]\fR - \fIalt-[a-z]\fR - \fIalt-[0-9]\fR - \fIf[1-12]\fR - \fIenter\fR (\fIreturn\fR \fIctrl-m\fR) - \fIspace\fR - \fIbspace\fR (\fIbs\fR) - \fIalt-up\fR - \fIalt-down\fR - \fIalt-left\fR - \fIalt-right\fR - \fIalt-enter\fR - \fIalt-space\fR - \fIalt-bspace\fR (\fIalt-bs\fR) - \fIalt-/\fR - \fItab\fR - \fIbtab\fR (\fIshift-tab\fR) - \fIesc\fR - \fIdel\fR - \fIup\fR - \fIdown\fR - \fIleft\fR - \fIright\fR - \fIhome\fR - \fIend\fR - \fIpgup\fR (\fIpage-up\fR) - \fIpgdn\fR (\fIpage-down\fR) - \fIshift-up\fR - \fIshift-down\fR - \fIshift-left\fR - \fIshift-right\fR - \fIleft-click\fR - \fIright-click\fR - \fIdouble-click\fR - or any single character +e.g. + \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR -Additionally, a special event named \fIchange\fR is available which is -triggered whenever the query string is changed. +.SS AVAILABLE KEYS: (SYNONYMS) +\fIctrl-[a-z]\fR +.br +\fIctrl-space\fR +.br +\fIctrl-alt-[a-z]\fR +.br +\fIalt-[a-z]\fR +.br +\fIalt-[0-9]\fR +.br +\fIf[1-12]\fR +.br +\fIenter\fR (\fIreturn\fR \fIctrl-m\fR) +.br +\fIspace\fR +.br +\fIbspace\fR (\fIbs\fR) +.br +\fIalt-up\fR +.br +\fIalt-down\fR +.br +\fIalt-left\fR +.br +\fIalt-right\fR +.br +\fIalt-enter\fR +.br +\fIalt-space\fR +.br +\fIalt-bspace\fR (\fIalt-bs\fR) +.br +\fIalt-/\fR +.br +\fItab\fR +.br +\fIbtab\fR (\fIshift-tab\fR) +.br +\fIesc\fR +.br +\fIdel\fR +.br +\fIup\fR +.br +\fIdown\fR +.br +\fIleft\fR +.br +\fIright\fR +.br +\fIhome\fR +.br +\fIend\fR +.br +\fIpgup\fR (\fIpage-up\fR) +.br +\fIpgdn\fR (\fIpage-down\fR) +.br +\fIshift-up\fR +.br +\fIshift-down\fR +.br +\fIshift-left\fR +.br +\fIshift-right\fR +.br +\fIleft-click\fR +.br +\fIright-click\fR +.br +\fIdouble-click\fR +.br +or any single character - e.g. \fBfzf --bind change:top\fR +.SS AVAILABLE EVENTS: +\fIchange\fR (triggered whenever the query string is changed) +.br + + e.g. + \fB# Moves cursor to the top (or bottom depending on --layout) whenever the query is changed + fzf --bind change:top\fR + +.SS AVAILABLE ACTIONS: +A key or an event can be bound to one or more of the following actions. \fBACTION: DEFAULT BINDINGS (NOTES): \fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR @@ -563,6 +639,7 @@ triggered whenever the query string is changed. \fBpreview-page-up\fR \fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR) \fBprint-query\fR (print query and exit) + \fBreload(...)\fR (see below for the details) \fBreplace-query\fR (replace query string with the current selection) \fBselect-all\fR \fBtoggle\fR (\fIright-click\fR) @@ -580,9 +657,14 @@ triggered whenever the query string is changed. \fBup\fR \fIctrl-k ctrl-p up\fR \fByank\fR \fIctrl-y\fR +.SS ACTION COMPOSITION + Multiple actions can be chained using \fB+\fR separator. - \fBfzf --bind 'ctrl-a:select-all+accept'\fR +e.g. + \fBfzf --bind 'ctrl-a:select-all+accept'\fR + +.SS COMMAND EXECUTION With \fBexecute(...)\fR action, you can execute arbitrary commands without leaving fzf. For example, you can turn fzf into a simple file browser by @@ -611,9 +693,9 @@ parse errors. \fBexecute|...|\fR \fBexecute:...\fR .RS -This is the special form that frees you from parse errors as it does not expect -the closing character. The catch is that it should be the last one in the -comma-separated list of key-action pairs. +The last one is the special form that frees you from parse errors as it does +not expect the closing character. The catch is that it should be the last one +in the comma-separated list of key-action pairs. .RE fzf switches to the alternate screen when executing a command. However, if the @@ -623,6 +705,26 @@ executes the command without the switching. Note that fzf will not be responsive until the command is complete. For asynchronous execution, start your command as a background process (i.e. appending \fB&\fR). +.SS RELOAD INPUT + +\fBreload(...)\fR action is used to dynamically update the input list +without restarting fzf. It takes the same command template with placeholder +expressions as \fBexecute(...)\fR. + +See \fIhttps://github.com/junegunn/fzf/issues/1750\fR for more info. + +e.g. + \fB# Update the list of processes by pressing CTRL-R + ps -ef | fzf --bind 'ctrl-r:reload(ps -ef)' --header 'Press CTRL-R to reload' \\ + --header-lines=1 --layout=reverse + + # Integration with ripgrep + RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case " + INITIAL_QUERY="foobar" + FZF_DEFAULT_COMMAND="$RG_PREFIX '$INITIAL_QUERY'" \\ + fzf --bind "change:reload:$RG_PREFIX {q} || true" \\ + --ansi --phony --query "$INITIAL_QUERY"\fR + .SH AUTHOR Junegunn Choi (\fIjunegunn.c@gmail.com\fR) From 168453da71af199a76279b9d4017ad1f72ba1f26 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 14 Nov 2019 22:39:25 +0900 Subject: [PATCH 10/14] More key chords for --bind Close #1752 --- CHANGELOG.md | 1 + man/man1/fzf.1 | 8 ++++++++ src/options.go | 8 ++++++++ src/tui/light.go | 8 ++++++++ src/tui/tcell.go | 6 ++++++ src/tui/tui.go | 6 ++++++ 6 files changed, 37 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c18a6eae..d33a181b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ CHANGELOG ``` - When you transform the input with `--with-nth`, the trailing white spaces are removed. +- `ctrl-\`, `ctrl-]`, `ctrl-^`, and `ctrl-/` can now be used with `--bind` - See https://github.com/junegunn/fzf/milestone/15?closed=1 for more details [argmax]: https://unix.stackexchange.com/questions/120642/what-defines-the-maximum-size-for-a-command-single-argument diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 906fbe61..bd18fb81 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -521,6 +521,14 @@ e.g. .br \fIctrl-space\fR .br +\fIctrl-\\\fR +.br +\fIctrl-]\fR +.br +\fIctrl-^\fR (\fIctrl-6\fR) +.br +\fIctrl-/\fR (\fIctrl-_\fR) +.br \fIctrl-alt-[a-z]\fR .br \fIalt-[a-z]\fR diff --git a/src/options.go b/src/options.go index 848ce3d9..fb432a6f 100644 --- a/src/options.go +++ b/src/options.go @@ -417,6 +417,14 @@ func parseKeyChords(str string, message string) map[int]string { chord = tui.BSpace case "ctrl-space": chord = tui.CtrlSpace + case "ctrl-^", "ctrl-6": + chord = tui.CtrlCaret + case "ctrl-/", "ctrl-_": + chord = tui.CtrlSlash + case "ctrl-\\": + chord = tui.CtrlBackSlash + case "ctrl-]": + chord = tui.CtrlRightBracket case "change": chord = tui.Change case "alt-enter", "alt-return": diff --git a/src/tui/light.go b/src/tui/light.go index 43d7efee..d1020a99 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -345,6 +345,14 @@ func (r *LightRenderer) GetChar() Event { return Event{BSpace, 0, nil} case 0: return Event{CtrlSpace, 0, nil} + case 28: + return Event{CtrlBackSlash, 0, nil} + case 29: + return Event{CtrlRightBracket, 0, nil} + case 30: + return Event{CtrlCaret, 0, nil} + case 31: + return Event{CtrlSlash, 0, nil} case ESC: ev := r.escSequence(&sz) // Second chance diff --git a/src/tui/tcell.go b/src/tui/tcell.go index 098e8a18..4bd7c812 100644 --- a/src/tui/tcell.go +++ b/src/tui/tcell.go @@ -284,6 +284,12 @@ func (r *FullscreenRenderer) GetChar() Event { return Event{keyfn('z'), 0, nil} case tcell.KeyCtrlSpace: return Event{CtrlSpace, 0, nil} + case tcell.KeyCtrlBackslash: + return Event{CtrlBackSlash, 0, nil} + case tcell.KeyCtrlRightSq: + return Event{CtrlRightBracket, 0, nil} + case tcell.KeyCtrlUnderscore: + return Event{CtrlSlash, 0, nil} case tcell.KeyBackspace2: if alt { return Event{AltBS, 0, nil} diff --git a/src/tui/tui.go b/src/tui/tui.go index 9b821940..de1d4b56 100644 --- a/src/tui/tui.go +++ b/src/tui/tui.go @@ -40,6 +40,12 @@ const ( ESC CtrlSpace + // https://apple.stackexchange.com/questions/24261/how-do-i-send-c-that-is-control-slash-to-the-terminal + CtrlBackSlash + CtrlRightBracket + CtrlCaret + CtrlSlash + Invalid Resize Mouse From d2fa4701651a1228ab7aea67eb195fc446f4a2a0 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 15 Nov 2019 00:39:29 +0900 Subject: [PATCH 11/14] Add --info=STYLE [default|inline|hidden] Close #1738 --- CHANGELOG.md | 4 ++++ man/man1/fzf.1 | 17 +++++++++++++++-- src/options.go | 39 ++++++++++++++++++++++++++++++++++----- src/terminal.go | 48 ++++++++++++++++++++++++++---------------------- test/test_go.rb | 5 +++++ 5 files changed, 84 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d33a181b..0a1a8a6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,10 @@ CHANGELOG # usage: _fzf_setup_completion path|dir COMMANDS... _fzf_setup_completion path git kubectl ``` +- Info line style can be changed by `--info=STYLE` + - `--info=default` + - `--info=inline` (same as old `--inline-info`) + - `--info=hidden` - When you transform the input with `--with-nth`, the trailing white spaces are removed. - `ctrl-\`, `ctrl-]`, `ctrl-^`, and `ctrl-/` can now be used with `--bind` diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index bd18fb81..f90a0571 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -212,8 +212,21 @@ e.g. fzf --margin 1,5%\fR .RE .TP -.B "--inline-info" -Display finder info inline with the query +.BI "--info=" "STYLE" +Determines the display style of finder info. + +.br +.BR default " Display on the next line to the prompt" +.br +.BR inline " Display on the same line" +.br +.BR hidden " Do not display finder info" +.br + +.TP +.B "--no-info" +A synonym for \fB--info=hidden\fB + .TP .BI "--prompt=" "STR" Input prompt (default: '> ') diff --git a/src/options.go b/src/options.go index fb432a6f..fe0d06d3 100644 --- a/src/options.go +++ b/src/options.go @@ -57,7 +57,7 @@ const usage = `usage: fzf [options] --layout=LAYOUT Choose layout: [default|reverse|reverse-list] --border Draw border above and below the finder --margin=MARGIN Screen margin (TRBL / TB,RL / T,RL,B / T,R,B,L) - --inline-info Display finder info inline with the query + --info=STYLE Finder info style [default|inline|hidden] --prompt=STR Input prompt (default: '> ') --header=STR String to print as header --header-lines=N The first N lines of the input are treated as header @@ -142,6 +142,14 @@ const ( layoutReverseList ) +type infoStyle int + +const ( + infoDefault infoStyle = iota + infoInline + infoHidden +) + type previewOpts struct { command string position windowPosition @@ -177,7 +185,7 @@ type Options struct { Hscroll bool HscrollOff int FileWord bool - InlineInfo bool + InfoStyle infoStyle JumpLabels string Prompt string Query string @@ -230,7 +238,7 @@ func defaultOptions() *Options { Hscroll: true, HscrollOff: 10, FileWord: false, - InlineInfo: false, + InfoStyle: infoDefault, JumpLabels: defaultJumpLabels, Prompt: "> ", Query: "", @@ -904,6 +912,20 @@ func parseLayout(str string) layoutType { return layoutDefault } +func parseInfoStyle(str string) infoStyle { + switch str { + case "default": + return infoDefault + case "inline": + return infoInline + case "hidden": + return infoHidden + default: + errorExit("invalid info style (expected: default / inline / hidden)") + } + return infoDefault +} + func parsePreviewWindow(opts *previewOpts, input string) { // Default opts.position = posRight @@ -1109,10 +1131,15 @@ func parseOptions(opts *Options, allArgs []string) { opts.FileWord = true case "--no-filepath-word": opts.FileWord = false + case "--info": + opts.InfoStyle = parseInfoStyle( + nextString(allArgs, &i, "info style required")) + case "--no-info": + opts.InfoStyle = infoHidden case "--inline-info": - opts.InlineInfo = true + opts.InfoStyle = infoInline case "--no-inline-info": - opts.InlineInfo = false + opts.InfoStyle = infoDefault case "--jump-labels": opts.JumpLabels = nextString(allArgs, &i, "label characters required") validateJumpLabels = true @@ -1220,6 +1247,8 @@ func parseOptions(opts *Options, allArgs []string) { opts.MinHeight = atoi(value) } else if match, value := optString(arg, "--layout="); match { opts.Layout = parseLayout(value) + } else if match, value := optString(arg, "--info="); match { + opts.InfoStyle = parseInfoStyle(value) } else if match, value := optString(arg, "--toggle-sort="); match { parseToggleSort(opts.Keymap, value) } else if match, value := optString(arg, "--expect="); match { diff --git a/src/terminal.go b/src/terminal.go index c5ff3025..0b812ccc 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -60,7 +60,7 @@ var emptyLine = itemLine{} // Terminal represents terminal input/output type Terminal struct { initDelay time.Duration - inlineInfo bool + infoStyle infoStyle prompt string promptLen int queryLen [2]int @@ -361,7 +361,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { if previewBox != nil && (opts.Preview.position == posUp || opts.Preview.position == posDown) { effectiveMinHeight *= 2 } - if opts.InlineInfo { + if opts.InfoStyle != infoDefault { effectiveMinHeight -= 1 } if opts.Bordered { @@ -380,7 +380,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { } t := Terminal{ initDelay: delay, - inlineInfo: opts.InlineInfo, + infoStyle: opts.InfoStyle, queryLen: [2]int{0, 0}, layout: opts.Layout, fullscreen: fullscreen, @@ -438,6 +438,10 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { return &t } +func (t *Terminal) noInfoLine() bool { + return t.infoStyle != infoDefault +} + // Input returns current query string func (t *Terminal) Input() []rune { t.mutex.Lock() @@ -672,7 +676,7 @@ func (t *Terminal) move(y int, x int, clear bool) { y = h - y - 1 case layoutReverseList: n := 2 + len(t.header) - if t.inlineInfo { + if t.noInfoLine() { n-- } if y < n { @@ -725,7 +729,17 @@ func (t *Terminal) printPrompt() { func (t *Terminal) printInfo() { pos := 0 - if t.inlineInfo { + switch t.infoStyle { + case infoDefault: + t.move(1, 0, true) + if t.reading { + duration := int64(spinnerDuration) + idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration + t.window.CPrint(tui.ColSpinner, t.strong, _spinner[idx]) + } + t.move(1, 2, false) + pos = 2 + case infoInline: pos = t.promptLen + t.queryLen[0] + t.queryLen[1] + 1 if pos+len(" < ") > t.window.Width() { return @@ -737,15 +751,8 @@ func (t *Terminal) printInfo() { t.window.CPrint(tui.ColPrompt, t.strong, " < ") } pos += len(" < ") - } else { - t.move(1, 0, true) - if t.reading { - duration := int64(spinnerDuration) - idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration - t.window.CPrint(tui.ColSpinner, t.strong, _spinner[idx]) - } - t.move(1, 2, false) - pos = 2 + case infoHidden: + return } found := t.merger.Length() @@ -787,7 +794,7 @@ func (t *Terminal) printHeader() { var state *ansiState for idx, lineStr := range t.header { line := idx + 2 - if t.inlineInfo { + if t.noInfoLine() { line-- } if line >= max { @@ -816,7 +823,7 @@ func (t *Terminal) printList() { i = maxy - 1 - j } line := i + 2 + len(t.header) - if t.inlineInfo { + if t.noInfoLine() { line-- } if i < count { @@ -1590,9 +1597,6 @@ func (t *Terminal) Loop() { } exit := func(getCode func() int) { - if !t.cleanExit && t.fullscreen && t.inlineInfo { - t.placeCursor() - } t.tui.Close() code := getCode() if code <= exitNoMatch && t.history != nil { @@ -1613,7 +1617,7 @@ func (t *Terminal) Loop() { switch req { case reqPrompt: t.printPrompt() - if t.inlineInfo { + if t.noInfoLine() { t.printInfo() } case reqInfo: @@ -1995,7 +1999,7 @@ func (t *Terminal) Loop() { my -= t.window.Top() mx = util.Constrain(mx-t.promptLen, 0, len(t.input)) min := 2 + len(t.header) - if t.inlineInfo { + if t.noInfoLine() { min-- } h := t.window.Height() @@ -2151,7 +2155,7 @@ func (t *Terminal) vset(o int) bool { func (t *Terminal) maxItems() int { max := t.window.Height() - 2 - len(t.header) - if t.inlineInfo { + if t.noInfoLine() { max++ } return util.Max(max, 0) diff --git a/test/test_go.rb b/test/test_go.rb index 210f8fec..501641e2 100755 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1516,6 +1516,11 @@ class TestGoFZF < TestBase tmux.until { |lines| lines[-1] == prompt } end + def test_info_hidden + tmux.send_keys 'seq 10 | fzf --info=hidden', :Enter + tmux.until { |lines| lines[-2] == '> 1' } + end + def test_change_top tmux.send_keys %(seq 1000 | #{FZF} --bind change:top), :Enter tmux.until { |lines| lines.match_count == 1000 } From e24299239ee2b38ab93b7c57932c3ac765f596b3 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 15 Nov 2019 11:37:52 +0900 Subject: [PATCH 12/14] Add `--preview-window noborder` option to disable preview border Close #1699 --- CHANGELOG.md | 2 ++ man/man1/fzf.1 | 4 ++-- src/options.go | 8 +++++++- src/terminal.go | 6 +++++- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a1a8a6d..b8eb3f5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,8 @@ CHANGELOG - `--info=default` - `--info=inline` (same as old `--inline-info`) - `--info=hidden` +- Preview window border can be disabled by adding `noborder` to + `--preview-window`. - When you transform the input with `--with-nth`, the trailing white spaces are removed. - `ctrl-\`, `ctrl-]`, `ctrl-^`, and `ctrl-/` can now be used with `--bind` diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index f90a0571..877330cd 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -361,8 +361,8 @@ Preview window will be updated even when there is no match for the current query if any of the placeholder expressions evaluates to a non-empty string. .RE .TP -.BI "--preview-window=" "[POSITION][:SIZE[%]][:wrap][:hidden]" -Determine the layout of the preview window. If the argument ends with +.BI "--preview-window=" "[POSITION][:SIZE[%]][:noborder][:wrap][:hidden]" +Determines the layout of the preview window. If the argument contains \fB:hidden\fR, the preview window will be hidden by default until \fBtoggle-preview\fR action is triggered. Long lines are truncated by default. Line wrap can be enabled with \fB:wrap\fR flag. diff --git a/src/options.go b/src/options.go index fe0d06d3..26c67e8e 100644 --- a/src/options.go +++ b/src/options.go @@ -156,6 +156,7 @@ type previewOpts struct { size sizeSpec hidden bool wrap bool + border bool } // Options stores the values of command-line options @@ -248,7 +249,7 @@ func defaultOptions() *Options { ToggleSort: false, Expect: make(map[int]string), Keymap: make(map[int][]action), - Preview: previewOpts{"", posRight, sizeSpec{50, true}, false, false}, + Preview: previewOpts{"", posRight, sizeSpec{50, true}, false, false, true}, PrintQuery: false, ReadZero: false, Printer: func(str string) { fmt.Println(str) }, @@ -937,6 +938,7 @@ func parsePreviewWindow(opts *previewOpts, input string) { sizeRegex := regexp.MustCompile("^[0-9]+%?$") for _, token := range tokens { switch token { + case "": case "hidden": opts.hidden = true case "wrap": @@ -949,6 +951,10 @@ func parsePreviewWindow(opts *previewOpts, input string) { opts.position = posLeft case "right": opts.position = posRight + case "border": + opts.border = true + case "noborder": + opts.border = false default: if sizeRegex.MatchString(token) { opts.size = parseSize(token, 99, "window size") diff --git a/src/terminal.go b/src/terminal.go index 0b812ccc..13b932e5 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -624,7 +624,11 @@ func (t *Terminal) resizeWindows() { noBorder := tui.MakeBorderStyle(tui.BorderNone, t.unicode) if previewVisible { createPreviewWindow := func(y int, x int, w int, h int) { - t.pborder = t.tui.NewWindow(y, x, w, h, tui.MakeBorderStyle(tui.BorderAround, t.unicode)) + previewBorder := noBorder + if t.preview.border { + previewBorder = tui.MakeBorderStyle(tui.BorderAround, t.unicode) + } + t.pborder = t.tui.NewWindow(y, x, w, h, previewBorder) pwidth := w - 4 // ncurses auto-wraps the line when the cursor reaches the right-end of // the window. To prevent unintended line-wraps, we use the width one From d630484eeb41097788d62aededb6ec6f188a46ee Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 15 Nov 2019 17:20:47 +0900 Subject: [PATCH 13/14] Update error message for --preview-window --- src/options.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/options.go b/src/options.go index 26c67e8e..9b20e55a 100644 --- a/src/options.go +++ b/src/options.go @@ -1200,7 +1200,7 @@ func parseOptions(opts *Options, allArgs []string) { opts.Preview.command = "" case "--preview-window": parsePreviewWindow(&opts.Preview, - nextString(allArgs, &i, "preview window layout required: [up|down|left|right][:SIZE[%]][:wrap][:hidden]")) + nextString(allArgs, &i, "preview window layout required: [up|down|left|right][:SIZE[%]][:noborder][:wrap][:hidden]")) case "--height": opts.Height = parseHeight(nextString(allArgs, &i, "height required: HEIGHT[%]")) case "--min-height": From 2886f06977579276d0c1a8273d29d5bdad23d628 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 15 Nov 2019 18:27:08 +0900 Subject: [PATCH 14/14] Fix --preview-window noborder with non-default background color --- src/terminal.go | 6 +++--- src/tui/tui.go | 13 +++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/terminal.go b/src/terminal.go index 13b932e5..4cd507a3 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -624,9 +624,9 @@ func (t *Terminal) resizeWindows() { noBorder := tui.MakeBorderStyle(tui.BorderNone, t.unicode) if previewVisible { createPreviewWindow := func(y int, x int, w int, h int) { - previewBorder := noBorder - if t.preview.border { - previewBorder = tui.MakeBorderStyle(tui.BorderAround, t.unicode) + previewBorder := tui.MakeBorderStyle(tui.BorderAround, t.unicode) + if !t.preview.border { + previewBorder = tui.MakeTransparentBorder() } t.pborder = t.tui.NewWindow(y, x, w, h, previewBorder) pwidth := w - 4 diff --git a/src/tui/tui.go b/src/tui/tui.go index de1d4b56..5a2e8d1b 100644 --- a/src/tui/tui.go +++ b/src/tui/tui.go @@ -221,6 +221,8 @@ type BorderStyle struct { bottomRight rune } +type BorderCharacter int + func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle { if unicode { return BorderStyle{ @@ -244,6 +246,17 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle { } } +func MakeTransparentBorder() BorderStyle { + return BorderStyle{ + shape: BorderAround, + horizontal: ' ', + vertical: ' ', + topLeft: ' ', + topRight: ' ', + bottomLeft: ' ', + bottomRight: ' '} +} + type Renderer interface { Init() Pause(clear bool)