// NewTerminal returns new Terminal object func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { input := []rune(opts.Query) return &Terminal{ inlineInfo: opts.InlineInfo, prompt: opts.Prompt, reverse: opts.Reverse, hscroll: opts.Hscroll, cx: len(input), cy: 0, offset: 0, yanked: []rune{}, input: input, multi: opts.Multi, sort: opts.Sort > 0, toggleSort: opts.ToggleSort, expect: opts.Expect, keymap: opts.Keymap, execmap: opts.Execmap, pressed: "", printQuery: opts.PrintQuery, history: opts.History, cycle: opts.Cycle, reading: true, merger: EmptyMerger, selected: make(map[uint32]selectedItem), reqBox: util.NewEventBox(), eventBox: eventBox, mutex: sync.Mutex{}, suppress: true, startChan: make(chan bool, 1), initFunc: func() { C.Init(opts.Theme, opts.Black, opts.Mouse) }} }
// NewMatcher returns a new Matcher func NewMatcher(patternBuilder func([]rune) *Pattern, sort bool, tac bool, eventBox *util.EventBox) *Matcher { return &Matcher{ patternBuilder: patternBuilder, sort: sort, tac: tac, eventBox: eventBox, reqBox: util.NewEventBox(), partitions: runtime.NumCPU(), mergerCache: make(map[string]*Merger)} }
// NewTerminal returns new Terminal object func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { input := []rune(opts.Query) var header []string if opts.Reverse { header = opts.Header } else { header = reverseStringArray(opts.Header) } _tabStop = opts.Tabstop var delay time.Duration if opts.Tac { delay = initialDelayTac } else { delay = initialDelay } return &Terminal{ initDelay: delay, inlineInfo: opts.InlineInfo, prompt: opts.Prompt, reverse: opts.Reverse, hscroll: opts.Hscroll, hscrollOff: opts.HscrollOff, cx: len(input), cy: 0, offset: 0, yanked: []rune{}, input: input, multi: opts.Multi, sort: opts.Sort > 0, toggleSort: opts.ToggleSort, expect: opts.Expect, keymap: opts.Keymap, execmap: opts.Execmap, pressed: "", printQuery: opts.PrintQuery, history: opts.History, margin: opts.Margin, marginInt: [4]int{0, 0, 0, 0}, cycle: opts.Cycle, header: header, header0: header, ansi: opts.Ansi, reading: true, merger: EmptyMerger, selected: make(map[int32]selectedItem), reqBox: util.NewEventBox(), eventBox: eventBox, mutex: sync.Mutex{}, suppress: true, startChan: make(chan bool, 1), initFunc: func() { C.Init(opts.Theme, opts.Black, opts.Mouse) }} }
// NewMatcher returns a new Matcher func NewMatcher(patternBuilder func([]rune) *Pattern, sort bool, tac bool, eventBox *util.EventBox) *Matcher { partitions := util.Min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions) return &Matcher{ patternBuilder: patternBuilder, sort: sort, tac: tac, eventBox: eventBox, reqBox: util.NewEventBox(), partitions: partitions, slab: make([]*util.Slab, partitions), mergerCache: make(map[string]*Merger)} }
func TestReadFromCommand(t *testing.T) { strs := []string{} eb := util.NewEventBox() reader := Reader{ pusher: func(s string) { strs = append(strs, s) }, eventBox: eb} // Check EventBox if eb.Peek(EvtReadNew) { t.Error("EvtReadNew should not be set yet") } // Normal command reader.readFromCommand(`echo abc && echo def`) if len(strs) != 2 || strs[0] != "abc" || strs[1] != "def" { t.Errorf("%s", strs) } // Check EventBox again if !eb.Peek(EvtReadNew) { t.Error("EvtReadNew should be set yet") } // Wait should return immediately eb.Wait(func(events *util.Events) { if _, found := (*events)[EvtReadNew]; !found { t.Errorf("%s", events) } events.Clear() }) // EventBox is cleared if eb.Peek(EvtReadNew) { t.Error("EvtReadNew should not be set yet") } // Failing command reader.readFromCommand(`no-such-command`) strs = []string{} if len(strs) > 0 { t.Errorf("%s", strs) } // Check EventBox again if eb.Peek(EvtReadNew) { t.Error("Command failed. EvtReadNew should be set") } }
// NewTerminal returns new Terminal object func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { input := []rune(opts.Query) return &Terminal{ prompt: opts.Prompt, reverse: opts.Reverse, cx: len(input), cy: 0, offset: 0, yanked: []rune{}, input: input, multi: opts.Multi, printQuery: opts.PrintQuery, merger: EmptyMerger, selected: make(map[*string]selectedItem), reqBox: util.NewEventBox(), eventBox: eventBox, mutex: sync.Mutex{}, suppress: true, startChan: make(chan bool, 1), initFunc: func() { C.Init(opts.Color, opts.Color256, opts.Black, opts.Mouse) }} }
// Run starts fzf func Run(opts *Options) { initProcs() sort := opts.Sort > 0 rankTiebreak = opts.Tiebreak if opts.Version { fmt.Println(version) os.Exit(exitOk) } // Event channel eventBox := util.NewEventBox() // ANSI code processor ansiProcessor := func(data []byte) ([]rune, []ansiOffset) { return util.BytesToRunes(data), nil } ansiProcessorRunes := func(data []rune) ([]rune, []ansiOffset) { return data, nil } if opts.Ansi { if opts.Theme != nil { var state *ansiState ansiProcessor = func(data []byte) ([]rune, []ansiOffset) { trimmed, offsets, newState := extractColor(string(data), state) state = newState return []rune(trimmed), offsets } } else { // When color is disabled but ansi option is given, // we simply strip out ANSI codes from the input ansiProcessor = func(data []byte) ([]rune, []ansiOffset) { trimmed, _, _ := extractColor(string(data), nil) return []rune(trimmed), nil } } ansiProcessorRunes = func(data []rune) ([]rune, []ansiOffset) { return ansiProcessor([]byte(string(data))) } } // Chunk list var chunkList *ChunkList header := make([]string, 0, opts.HeaderLines) if len(opts.WithNth) == 0 { chunkList = NewChunkList(func(data []byte, index int) *Item { if len(header) < opts.HeaderLines { header = append(header, string(data)) eventBox.Set(EvtHeader, header) return nil } runes, colors := ansiProcessor(data) return &Item{ text: runes, index: uint32(index), colors: colors, rank: Rank{0, 0, uint32(index)}} }) } else { chunkList = NewChunkList(func(data []byte, index int) *Item { runes := util.BytesToRunes(data) tokens := Tokenize(runes, opts.Delimiter) trans := Transform(tokens, opts.WithNth) if len(header) < opts.HeaderLines { header = append(header, string(joinTokens(trans))) eventBox.Set(EvtHeader, header) return nil } item := Item{ text: joinTokens(trans), origText: &runes, index: uint32(index), colors: nil, rank: Rank{0, 0, uint32(index)}} trimmed, colors := ansiProcessorRunes(item.text) item.text = trimmed item.colors = colors return &item }) } // Reader streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync if !streamingFilter { reader := Reader{func(data []byte) bool { return chunkList.Push(data) }, eventBox, opts.ReadZero} go reader.ReadSource() } // Matcher patternBuilder := func(runes []rune) *Pattern { return BuildPattern( opts.Fuzzy, opts.Extended, opts.Case, opts.Tiebreak != byEnd, opts.Nth, opts.Delimiter, runes) } matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox) // Filtering mode if opts.Filter != nil { if opts.PrintQuery { fmt.Println(*opts.Filter) } pattern := patternBuilder([]rune(*opts.Filter)) found := false if streamingFilter { reader := Reader{ func(runes []byte) bool { item := chunkList.trans(runes, 0) if item != nil && pattern.MatchItem(item) { fmt.Println(string(item.text)) found = true } return false }, eventBox, opts.ReadZero} reader.ReadSource() } else { eventBox.Unwatch(EvtReadNew) eventBox.WaitFor(EvtReadFin) snapshot, _ := chunkList.Snapshot() merger, _ := matcher.scan(MatchRequest{ chunks: snapshot, pattern: pattern}) for i := 0; i < merger.Length(); i++ { fmt.Println(merger.Get(i).AsString(opts.Ansi)) found = true } } if found { os.Exit(exitOk) } os.Exit(exitNoMatch) } // Synchronous search if opts.Sync { eventBox.Unwatch(EvtReadNew) eventBox.WaitFor(EvtReadFin) } // Go interactive go matcher.Loop() // Terminal I/O terminal := NewTerminal(opts, eventBox) deferred := opts.Select1 || opts.Exit0 go terminal.Loop() if !deferred { terminal.startChan <- true } // Event coordination reading := true ticks := 0 eventBox.Watch(EvtReadNew) for { delay := true ticks++ eventBox.Wait(func(events *util.Events) { defer events.Clear() for evt, value := range *events { switch evt { case EvtReadNew, EvtReadFin: reading = reading && evt == EvtReadNew snapshot, count := chunkList.Snapshot() terminal.UpdateCount(count, !reading) matcher.Reset(snapshot, terminal.Input(), false, !reading, sort) case EvtSearchNew: switch val := value.(type) { case bool: sort = val } snapshot, _ := chunkList.Snapshot() matcher.Reset(snapshot, terminal.Input(), true, !reading, sort) delay = false case EvtSearchProgress: switch val := value.(type) { case float32: terminal.UpdateProgress(val) } case EvtHeader: terminal.UpdateHeader(value.([]string)) case EvtSearchFin: switch val := value.(type) { case *Merger: if deferred { count := val.Length() if opts.Select1 && count > 1 || opts.Exit0 && !opts.Select1 && count > 0 { deferred = false terminal.startChan <- true } else if val.final { if opts.Exit0 && count == 0 || opts.Select1 && count == 1 { if opts.PrintQuery { fmt.Println(opts.Query) } if len(opts.Expect) > 0 { fmt.Println() } for i := 0; i < count; i++ { fmt.Println(val.Get(i).AsString(opts.Ansi)) } if count > 0 { os.Exit(exitOk) } os.Exit(exitNoMatch) } deferred = false terminal.startChan <- true } } terminal.UpdateList(val) } } } }) if delay && reading { dur := util.DurWithin( time.Duration(ticks)*coordinatorDelayStep, 0, coordinatorDelayMax) time.Sleep(dur) } } }
// NewTerminal returns new Terminal object func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { input := []rune(opts.Query) var header []string if opts.Reverse { header = opts.Header } else { header = reverseStringArray(opts.Header) } _tabStop = opts.Tabstop var delay time.Duration if opts.Tac { delay = initialDelayTac } else { delay = initialDelay } var previewBox *util.EventBox if len(opts.Preview.command) > 0 { previewBox = util.NewEventBox() } strongAttr := tui.Bold if !opts.Bold { strongAttr = tui.AttrRegular } return &Terminal{ initDelay: delay, inlineInfo: opts.InlineInfo, prompt: opts.Prompt, reverse: opts.Reverse, hscroll: opts.Hscroll, hscrollOff: opts.HscrollOff, cx: len(input), cy: 0, offset: 0, yanked: []rune{}, input: input, multi: opts.Multi, sort: opts.Sort > 0, toggleSort: opts.ToggleSort, delimiter: opts.Delimiter, expect: opts.Expect, keymap: opts.Keymap, execmap: opts.Execmap, pressed: "", printQuery: opts.PrintQuery, history: opts.History, margin: opts.Margin, strong: strongAttr, cycle: opts.Cycle, header: header, header0: header, ansi: opts.Ansi, reading: true, jumping: jumpDisabled, jumpLabels: opts.JumpLabels, printer: opts.Printer, merger: EmptyMerger, selected: make(map[int32]selectedItem), reqBox: util.NewEventBox(), preview: opts.Preview, previewer: previewer{"", 0, 0, previewBox != nil && !opts.Preview.hidden}, previewBox: previewBox, eventBox: eventBox, mutex: sync.Mutex{}, suppress: true, slab: util.MakeSlab(slab16Size, slab32Size), theme: opts.Theme, startChan: make(chan bool, 1), initFunc: func() { tui.Init(opts.Theme, opts.Black, opts.Mouse) }} }
// NewTerminal returns new Terminal object func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { input := []rune(opts.Query) var header []string if opts.Reverse { header = opts.Header } else { header = reverseStringArray(opts.Header) } var delay time.Duration if opts.Tac { delay = initialDelayTac } else { delay = initialDelay } var previewBox *util.EventBox if len(opts.Preview.command) > 0 { previewBox = util.NewEventBox() } strongAttr := tui.Bold if !opts.Bold { strongAttr = tui.AttrRegular } var renderer tui.Renderer if opts.Height.size > 0 { maxHeightFunc := func(termHeight int) int { var maxHeight int if opts.Height.percent { maxHeight = util.Min(termHeight, util.Max(int(opts.Height.size*float64(termHeight)/100.0), opts.MinHeight)) } else { maxHeight = util.Min(termHeight, int(opts.Height.size)) } if opts.InlineInfo { return util.Max(maxHeight, minHeight-1) } return util.Max(maxHeight, minHeight) } renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, maxHeightFunc) } else { renderer = tui.NewFullscreenRenderer(opts.Theme, opts.Black, opts.Mouse) } wordRubout := "[^[:alnum:]][[:alnum:]]" wordNext := "[[:alnum:]][^[:alnum:]]|(.$)" if opts.FileWord { sep := regexp.QuoteMeta(string(os.PathSeparator)) wordRubout = fmt.Sprintf("%s[^%s]", sep, sep) wordNext = fmt.Sprintf("[^%s]%s|(.$)", sep, sep) } return &Terminal{ initDelay: delay, inlineInfo: opts.InlineInfo, prompt: opts.Prompt, reverse: opts.Reverse, hscroll: opts.Hscroll, hscrollOff: opts.HscrollOff, wordRubout: wordRubout, wordNext: wordNext, cx: len(input), cy: 0, offset: 0, yanked: []rune{}, input: input, multi: opts.Multi, sort: opts.Sort > 0, toggleSort: opts.ToggleSort, delimiter: opts.Delimiter, expect: opts.Expect, keymap: opts.Keymap, execmap: opts.Execmap, pressed: "", printQuery: opts.PrintQuery, history: opts.History, margin: opts.Margin, strong: strongAttr, cycle: opts.Cycle, header: header, header0: header, ansi: opts.Ansi, tabstop: opts.Tabstop, reading: true, jumping: jumpDisabled, jumpLabels: opts.JumpLabels, printer: opts.Printer, merger: EmptyMerger, selected: make(map[int32]selectedItem), reqBox: util.NewEventBox(), preview: opts.Preview, previewer: previewer{"", 0, 0, previewBox != nil && !opts.Preview.hidden}, previewBox: previewBox, eventBox: eventBox, mutex: sync.Mutex{}, suppress: true, slab: util.MakeSlab(slab16Size, slab32Size), theme: opts.Theme, startChan: make(chan bool, 1), tui: renderer, initFunc: func() { renderer.Init() }} }
// Run starts fzf func Run(options *Options) { initProcs() opts := ParseOptions() if opts.Version { fmt.Println(Version) os.Exit(0) } // Event channel eventBox := util.NewEventBox() // ANSI code processor ansiProcessor := func(data *string) (*string, []ansiOffset) { // By default, we do nothing return data, nil } if opts.Ansi { if opts.Color { ansiProcessor = func(data *string) (*string, []ansiOffset) { return extractColor(data) } } else { // When color is disabled but ansi option is given, // we simply strip out ANSI codes from the input ansiProcessor = func(data *string) (*string, []ansiOffset) { trimmed, _ := extractColor(data) return trimmed, nil } } } // Chunk list var chunkList *ChunkList if len(opts.WithNth) == 0 { chunkList = NewChunkList(func(data *string, index int) *Item { data, colors := ansiProcessor(data) return &Item{ text: data, index: uint32(index), colors: colors, rank: Rank{0, 0, uint32(index)}} }) } else { chunkList = NewChunkList(func(data *string, index int) *Item { tokens := Tokenize(data, opts.Delimiter) item := Item{ text: Transform(tokens, opts.WithNth).whole, origText: data, index: uint32(index), colors: nil, rank: Rank{0, 0, uint32(index)}} trimmed, colors := ansiProcessor(item.text) item.text = trimmed item.colors = colors return &item }) } // Reader streamingFilter := opts.Filter != nil && opts.Sort == 0 && !opts.Tac && !opts.Sync if !streamingFilter { reader := Reader{func(str string) { chunkList.Push(str) }, eventBox} go reader.ReadSource() } // Matcher patternBuilder := func(runes []rune) *Pattern { return BuildPattern( opts.Mode, opts.Case, opts.Nth, opts.Delimiter, runes) } matcher := NewMatcher(patternBuilder, opts.Sort > 0, opts.Tac, eventBox) // Filtering mode if opts.Filter != nil { if opts.PrintQuery { fmt.Println(*opts.Filter) } pattern := patternBuilder([]rune(*opts.Filter)) if streamingFilter { reader := Reader{ func(str string) { item := chunkList.trans(&str, 0) if pattern.MatchItem(item) { fmt.Println(*item.text) } }, eventBox} reader.ReadSource() } else { eventBox.Unwatch(EvtReadNew) eventBox.WaitFor(EvtReadFin) snapshot, _ := chunkList.Snapshot() merger, _ := matcher.scan(MatchRequest{ chunks: snapshot, pattern: pattern}) for i := 0; i < merger.Length(); i++ { fmt.Println(merger.Get(i).AsString()) } } os.Exit(0) } // Synchronous search if opts.Sync { eventBox.Unwatch(EvtReadNew) eventBox.WaitFor(EvtReadFin) } // Go interactive go matcher.Loop() // Terminal I/O terminal := NewTerminal(opts, eventBox) deferred := opts.Select1 || opts.Exit0 go terminal.Loop() if !deferred { terminal.startChan <- true } // Event coordination reading := true ticks := 0 eventBox.Watch(EvtReadNew) for { delay := true ticks++ eventBox.Wait(func(events *util.Events) { defer events.Clear() for evt, value := range *events { switch evt { case EvtReadNew, EvtReadFin: reading = reading && evt == EvtReadNew snapshot, count := chunkList.Snapshot() terminal.UpdateCount(count, !reading) matcher.Reset(snapshot, terminal.Input(), false, !reading) case EvtSearchNew: snapshot, _ := chunkList.Snapshot() matcher.Reset(snapshot, terminal.Input(), true, !reading) delay = false case EvtSearchProgress: switch val := value.(type) { case float32: terminal.UpdateProgress(val) } case EvtSearchFin: switch val := value.(type) { case *Merger: if deferred { count := val.Length() if opts.Select1 && count > 1 || opts.Exit0 && !opts.Select1 && count > 0 { deferred = false terminal.startChan <- true } else if val.final { if opts.Exit0 && count == 0 || opts.Select1 && count == 1 { if opts.PrintQuery { fmt.Println(opts.Query) } for i := 0; i < count; i++ { fmt.Println(val.Get(i).AsString()) } os.Exit(0) } deferred = false terminal.startChan <- true } } terminal.UpdateList(val) } } } }) if delay && reading { dur := util.DurWithin( time.Duration(ticks)*coordinatorDelayStep, 0, coordinatorDelayMax) time.Sleep(dur) } } }