From f469c25730aaa711b8327be068514c944074cce4 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 22 Jul 2015 03:21:20 +0900 Subject: [PATCH] Add --header-lines option --- man/man1/fzf.1 | 5 +- src/chunklist.go | 18 ++++-- src/constants.go | 1 + src/core.go | 24 +++++++- src/options.go | 144 +++++++++++++++++++++++++-------------------- src/reader.go | 7 ++- src/reader_test.go | 2 +- src/terminal.go | 19 ++++++ 8 files changed, 143 insertions(+), 77 deletions(-) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 8d10d389..c42fe668 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -259,8 +259,11 @@ Maximum number of entries in the history file (default: 1000). The file is automatically truncated when the number of the lines exceeds the value. .TP .BI "--header-file=" "FILE" -The content of the file will be printed as the "sticky" header. The file can +The content of the file will be printed as the sticky header. The file can span multiple lines and can contain ANSI color codes. +.TP +.BI "--header-lines=" "N" +The first N lines of the input are treated as the sticky header. .SS Scripting .TP .BI "-q, --query=" "STR" diff --git a/src/chunklist.go b/src/chunklist.go index 52084f2f..ee52d321 100644 --- a/src/chunklist.go +++ b/src/chunklist.go @@ -26,8 +26,13 @@ func NewChunkList(trans ItemBuilder) *ChunkList { trans: trans} } -func (c *Chunk) push(trans ItemBuilder, data *string, index int) { - *c = append(*c, trans(data, index)) +func (c *Chunk) push(trans ItemBuilder, data *string, index int) bool { + item := trans(data, index) + if item != nil { + *c = append(*c, item) + return true + } + return false } // IsFull returns true if the Chunk is full @@ -48,7 +53,7 @@ func CountItems(cs []*Chunk) int { } // Push adds the item to the list -func (cl *ChunkList) Push(data string) { +func (cl *ChunkList) Push(data string) bool { cl.mutex.Lock() defer cl.mutex.Unlock() @@ -57,8 +62,11 @@ func (cl *ChunkList) Push(data string) { cl.chunks = append(cl.chunks, &newChunk) } - cl.lastChunk().push(cl.trans, &data, cl.count) - cl.count++ + if cl.lastChunk().push(cl.trans, &data, cl.count) { + cl.count++ + return true + } + return false } // Snapshot returns immutable snapshot of the ChunkList diff --git a/src/constants.go b/src/constants.go index 73ba451e..26929079 100644 --- a/src/constants.go +++ b/src/constants.go @@ -44,5 +44,6 @@ const ( EvtSearchNew EvtSearchProgress EvtSearchFin + EvtHeader EvtClose ) diff --git a/src/core.go b/src/core.go index c7277080..e38908a8 100644 --- a/src/core.go +++ b/src/core.go @@ -44,6 +44,7 @@ Reader -> EvtReadNew -> Matcher (restart) Terminal -> EvtSearchNew:bool -> Matcher (restart) Matcher -> EvtSearchProgress -> Terminal (update info) Matcher -> EvtSearchFin -> Terminal (update list) +Matcher -> EvtHeader -> Terminal (update header) */ // Run starts fzf @@ -83,8 +84,14 @@ func Run(opts *Options) { // Chunk list var chunkList *ChunkList + header := make([]string, 0, opts.HeaderLines) if len(opts.WithNth) == 0 { chunkList = NewChunkList(func(data *string, index int) *Item { + if len(header) < opts.HeaderLines { + header = append(header, *data) + eventBox.Set(EvtHeader, header) + return nil + } data, colors := ansiProcessor(data) return &Item{ text: data, @@ -94,6 +101,11 @@ func Run(opts *Options) { }) } else { chunkList = NewChunkList(func(data *string, index int) *Item { + if len(header) < opts.HeaderLines { + header = append(header, *data) + eventBox.Set(EvtHeader, header) + return nil + } tokens := Tokenize(data, opts.Delimiter) trans := Transform(tokens, opts.WithNth) item := Item{ @@ -113,7 +125,9 @@ func Run(opts *Options) { // Reader streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync if !streamingFilter { - reader := Reader{func(str string) { chunkList.Push(str) }, eventBox, opts.ReadZero} + reader := Reader{func(str string) bool { + return chunkList.Push(str) + }, eventBox, opts.ReadZero} go reader.ReadSource() } @@ -134,11 +148,12 @@ func Run(opts *Options) { if streamingFilter { reader := Reader{ - func(str string) { + func(str string) bool { item := chunkList.trans(&str, 0) - if pattern.MatchItem(item) { + if item != nil && pattern.MatchItem(item) { fmt.Println(*item.text) } + return false }, eventBox, opts.ReadZero} reader.ReadSource() } else { @@ -206,6 +221,9 @@ func Run(opts *Options) { terminal.UpdateProgress(val) } + case EvtHeader: + terminal.UpdateHeader(value.([]string), opts.HeaderLines) + case EvtSearchFin: switch val := value.(type) { case *Merger: diff --git a/src/options.go b/src/options.go index e789d4e4..d8b2bd87 100644 --- a/src/options.go +++ b/src/options.go @@ -46,6 +46,7 @@ const usage = `usage: fzf [options] --history=FILE History file --history-size=N Maximum number of history entries (default: 1000) --header-file=FILE The file whose content to be printed as header + --header-lines=N The first N lines of the input are treated as header Scripting -q, --query=STR Start the finder with the given query @@ -94,38 +95,39 @@ const ( // Options stores the values of command-line options type Options struct { - Mode Mode - Case Case - Nth []Range - WithNth []Range - Delimiter *regexp.Regexp - Sort int - Tac bool - Tiebreak tiebreak - Multi bool - Ansi bool - Mouse bool - Theme *curses.ColorTheme - Black bool - Reverse bool - Cycle bool - Hscroll bool - InlineInfo bool - Prompt string - Query string - Select1 bool - Exit0 bool - Filter *string - ToggleSort bool - Expect map[int]string - Keymap map[int]actionType - Execmap map[int]string - PrintQuery bool - ReadZero bool - Sync bool - History *History - Header []string - Version bool + Mode Mode + Case Case + Nth []Range + WithNth []Range + Delimiter *regexp.Regexp + Sort int + Tac bool + Tiebreak tiebreak + Multi bool + Ansi bool + Mouse bool + Theme *curses.ColorTheme + Black bool + Reverse bool + Cycle bool + Hscroll bool + InlineInfo bool + Prompt string + Query string + Select1 bool + Exit0 bool + Filter *string + ToggleSort bool + Expect map[int]string + Keymap map[int]actionType + Execmap map[int]string + PrintQuery bool + ReadZero bool + Sync bool + History *History + Header []string + HeaderLines int + Version bool } func defaultTheme() *curses.ColorTheme { @@ -137,38 +139,39 @@ func defaultTheme() *curses.ColorTheme { func defaultOptions() *Options { return &Options{ - Mode: ModeFuzzy, - Case: CaseSmart, - Nth: make([]Range, 0), - WithNth: make([]Range, 0), - Delimiter: nil, - Sort: 1000, - Tac: false, - Tiebreak: byLength, - Multi: false, - Ansi: false, - Mouse: true, - Theme: defaultTheme(), - Black: false, - Reverse: false, - Cycle: false, - Hscroll: true, - InlineInfo: false, - Prompt: "> ", - Query: "", - Select1: false, - Exit0: false, - Filter: nil, - ToggleSort: false, - Expect: make(map[int]string), - Keymap: defaultKeymap(), - Execmap: make(map[int]string), - PrintQuery: false, - ReadZero: false, - Sync: false, - History: nil, - Header: make([]string, 0), - Version: false} + Mode: ModeFuzzy, + Case: CaseSmart, + Nth: make([]Range, 0), + WithNth: make([]Range, 0), + Delimiter: nil, + Sort: 1000, + Tac: false, + Tiebreak: byLength, + Multi: false, + Ansi: false, + Mouse: true, + Theme: defaultTheme(), + Black: false, + Reverse: false, + Cycle: false, + Hscroll: true, + InlineInfo: false, + Prompt: "> ", + Query: "", + Select1: false, + Exit0: false, + Filter: nil, + ToggleSort: false, + Expect: make(map[int]string), + Keymap: defaultKeymap(), + Execmap: make(map[int]string), + PrintQuery: false, + ReadZero: false, + Sync: false, + History: nil, + Header: make([]string, 0), + HeaderLines: 0, + Version: false} } func help(ok int) { @@ -724,9 +727,18 @@ func parseOptions(opts *Options, allArgs []string) { setHistory(nextString(allArgs, &i, "history file path required")) case "--history-size": setHistoryMax(nextInt(allArgs, &i, "history max size required")) + case "--no-header-file": + opts.Header = []string{} + case "--no-header-lines": + opts.HeaderLines = 0 case "--header-file": opts.Header = readHeaderFile( nextString(allArgs, &i, "header file name required")) + opts.HeaderLines = 0 + case "--header-lines": + opts.Header = []string{} + opts.HeaderLines = atoi( + nextString(allArgs, &i, "number of header lines required")) case "--version": opts.Version = true default: @@ -762,6 +774,10 @@ func parseOptions(opts *Options, allArgs []string) { setHistoryMax(atoi(value)) } else if match, value := optString(arg, "--header-file="); match { opts.Header = readHeaderFile(value) + opts.HeaderLines = 0 + } else if match, value := optString(arg, "--header-lines="); match { + opts.Header = []string{} + opts.HeaderLines = atoi(value) } else { errorExit("unknown option: " + arg) } diff --git a/src/reader.go b/src/reader.go index 356c2db8..aab8b02a 100644 --- a/src/reader.go +++ b/src/reader.go @@ -11,7 +11,7 @@ import ( // Reader reads from command or standard input type Reader struct { - pusher func(string) + pusher func(string) bool eventBox *util.EventBox delimNil bool } @@ -43,8 +43,9 @@ func (r *Reader) feed(src io.Reader) { if err == nil { line = line[:len(line)-1] } - r.pusher(line) - r.eventBox.Set(EvtReadNew, nil) + if r.pusher(line) { + r.eventBox.Set(EvtReadNew, nil) + } } if err != nil { break diff --git a/src/reader_test.go b/src/reader_test.go index 144a3ff1..00b9e337 100644 --- a/src/reader_test.go +++ b/src/reader_test.go @@ -10,7 +10,7 @@ func TestReadFromCommand(t *testing.T) { strs := []string{} eb := util.NewEventBox() reader := Reader{ - pusher: func(s string) { strs = append(strs, s) }, + pusher: func(s string) bool { strs = append(strs, s); return true }, eventBox: eb} // Check EventBox diff --git a/src/terminal.go b/src/terminal.go index 43d2d8c0..844574a1 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -79,6 +79,7 @@ var _runeWidths = make(map[rune]int) const ( reqPrompt util.EventType = iota reqInfo + reqHeader reqList reqRefresh reqRedraw @@ -231,6 +232,22 @@ func (t *Terminal) UpdateCount(cnt int, final bool) { } } +// UpdateHeader updates the header +func (t *Terminal) UpdateHeader(header []string, lines int) { + t.mutex.Lock() + t.header = make([]string, lines) + copy(t.header, header) + if !t.reverse { + reversed := make([]string, lines) + for idx, str := range t.header { + reversed[lines-idx-1] = str + } + t.header = reversed + } + t.mutex.Unlock() + t.reqBox.Set(reqHeader, nil) +} + // UpdateProgress updates the search progress func (t *Terminal) UpdateProgress(progress float32) { t.mutex.Lock() @@ -686,6 +703,8 @@ func (t *Terminal) Loop() { t.printInfo() case reqList: t.printList() + case reqHeader: + t.printHeader() case reqRefresh: t.suppress = false case reqRedraw: