Add --header-lines option

This commit is contained in:
Junegunn Choi 2015-07-22 03:21:20 +09:00
parent 18469b6954
commit f469c25730
8 changed files with 143 additions and 77 deletions

View File

@ -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. automatically truncated when the number of the lines exceeds the value.
.TP .TP
.BI "--header-file=" "FILE" .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. 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 .SS Scripting
.TP .TP
.BI "-q, --query=" "STR" .BI "-q, --query=" "STR"

View File

@ -26,8 +26,13 @@ func NewChunkList(trans ItemBuilder) *ChunkList {
trans: trans} trans: trans}
} }
func (c *Chunk) push(trans ItemBuilder, data *string, index int) { func (c *Chunk) push(trans ItemBuilder, data *string, index int) bool {
*c = append(*c, trans(data, index)) item := trans(data, index)
if item != nil {
*c = append(*c, item)
return true
}
return false
} }
// IsFull returns true if the Chunk is full // IsFull returns true if the Chunk is full
@ -48,7 +53,7 @@ func CountItems(cs []*Chunk) int {
} }
// Push adds the item to the list // Push adds the item to the list
func (cl *ChunkList) Push(data string) { func (cl *ChunkList) Push(data string) bool {
cl.mutex.Lock() cl.mutex.Lock()
defer cl.mutex.Unlock() defer cl.mutex.Unlock()
@ -57,8 +62,11 @@ func (cl *ChunkList) Push(data string) {
cl.chunks = append(cl.chunks, &newChunk) cl.chunks = append(cl.chunks, &newChunk)
} }
cl.lastChunk().push(cl.trans, &data, cl.count) if cl.lastChunk().push(cl.trans, &data, cl.count) {
cl.count++ cl.count++
return true
}
return false
} }
// Snapshot returns immutable snapshot of the ChunkList // Snapshot returns immutable snapshot of the ChunkList

View File

@ -44,5 +44,6 @@ const (
EvtSearchNew EvtSearchNew
EvtSearchProgress EvtSearchProgress
EvtSearchFin EvtSearchFin
EvtHeader
EvtClose EvtClose
) )

View File

@ -44,6 +44,7 @@ Reader -> EvtReadNew -> Matcher (restart)
Terminal -> EvtSearchNew:bool -> Matcher (restart) Terminal -> EvtSearchNew:bool -> Matcher (restart)
Matcher -> EvtSearchProgress -> Terminal (update info) Matcher -> EvtSearchProgress -> Terminal (update info)
Matcher -> EvtSearchFin -> Terminal (update list) Matcher -> EvtSearchFin -> Terminal (update list)
Matcher -> EvtHeader -> Terminal (update header)
*/ */
// Run starts fzf // Run starts fzf
@ -83,8 +84,14 @@ func Run(opts *Options) {
// Chunk list // Chunk list
var chunkList *ChunkList var chunkList *ChunkList
header := make([]string, 0, opts.HeaderLines)
if len(opts.WithNth) == 0 { if len(opts.WithNth) == 0 {
chunkList = NewChunkList(func(data *string, index int) *Item { 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) data, colors := ansiProcessor(data)
return &Item{ return &Item{
text: data, text: data,
@ -94,6 +101,11 @@ func Run(opts *Options) {
}) })
} else { } else {
chunkList = NewChunkList(func(data *string, index int) *Item { 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) tokens := Tokenize(data, opts.Delimiter)
trans := Transform(tokens, opts.WithNth) trans := Transform(tokens, opts.WithNth)
item := Item{ item := Item{
@ -113,7 +125,9 @@ func Run(opts *Options) {
// Reader // Reader
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
if !streamingFilter { 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() go reader.ReadSource()
} }
@ -134,11 +148,12 @@ func Run(opts *Options) {
if streamingFilter { if streamingFilter {
reader := Reader{ reader := Reader{
func(str string) { func(str string) bool {
item := chunkList.trans(&str, 0) item := chunkList.trans(&str, 0)
if pattern.MatchItem(item) { if item != nil && pattern.MatchItem(item) {
fmt.Println(*item.text) fmt.Println(*item.text)
} }
return false
}, eventBox, opts.ReadZero} }, eventBox, opts.ReadZero}
reader.ReadSource() reader.ReadSource()
} else { } else {
@ -206,6 +221,9 @@ func Run(opts *Options) {
terminal.UpdateProgress(val) terminal.UpdateProgress(val)
} }
case EvtHeader:
terminal.UpdateHeader(value.([]string), opts.HeaderLines)
case EvtSearchFin: case EvtSearchFin:
switch val := value.(type) { switch val := value.(type) {
case *Merger: case *Merger:

View File

@ -46,6 +46,7 @@ const usage = `usage: fzf [options]
--history=FILE History file --history=FILE History file
--history-size=N Maximum number of history entries (default: 1000) --history-size=N Maximum number of history entries (default: 1000)
--header-file=FILE The file whose content to be printed as header --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 Scripting
-q, --query=STR Start the finder with the given query -q, --query=STR Start the finder with the given query
@ -94,38 +95,39 @@ const (
// Options stores the values of command-line options // Options stores the values of command-line options
type Options struct { type Options struct {
Mode Mode Mode Mode
Case Case Case Case
Nth []Range Nth []Range
WithNth []Range WithNth []Range
Delimiter *regexp.Regexp Delimiter *regexp.Regexp
Sort int Sort int
Tac bool Tac bool
Tiebreak tiebreak Tiebreak tiebreak
Multi bool Multi bool
Ansi bool Ansi bool
Mouse bool Mouse bool
Theme *curses.ColorTheme Theme *curses.ColorTheme
Black bool Black bool
Reverse bool Reverse bool
Cycle bool Cycle bool
Hscroll bool Hscroll bool
InlineInfo bool InlineInfo bool
Prompt string Prompt string
Query string Query string
Select1 bool Select1 bool
Exit0 bool Exit0 bool
Filter *string Filter *string
ToggleSort bool ToggleSort bool
Expect map[int]string Expect map[int]string
Keymap map[int]actionType Keymap map[int]actionType
Execmap map[int]string Execmap map[int]string
PrintQuery bool PrintQuery bool
ReadZero bool ReadZero bool
Sync bool Sync bool
History *History History *History
Header []string Header []string
Version bool HeaderLines int
Version bool
} }
func defaultTheme() *curses.ColorTheme { func defaultTheme() *curses.ColorTheme {
@ -137,38 +139,39 @@ func defaultTheme() *curses.ColorTheme {
func defaultOptions() *Options { func defaultOptions() *Options {
return &Options{ return &Options{
Mode: ModeFuzzy, Mode: ModeFuzzy,
Case: CaseSmart, Case: CaseSmart,
Nth: make([]Range, 0), Nth: make([]Range, 0),
WithNth: make([]Range, 0), WithNth: make([]Range, 0),
Delimiter: nil, Delimiter: nil,
Sort: 1000, Sort: 1000,
Tac: false, Tac: false,
Tiebreak: byLength, Tiebreak: byLength,
Multi: false, Multi: false,
Ansi: false, Ansi: false,
Mouse: true, Mouse: true,
Theme: defaultTheme(), Theme: defaultTheme(),
Black: false, Black: false,
Reverse: false, Reverse: false,
Cycle: false, Cycle: false,
Hscroll: true, Hscroll: true,
InlineInfo: false, InlineInfo: false,
Prompt: "> ", Prompt: "> ",
Query: "", Query: "",
Select1: false, Select1: false,
Exit0: false, Exit0: false,
Filter: nil, Filter: nil,
ToggleSort: false, ToggleSort: false,
Expect: make(map[int]string), Expect: make(map[int]string),
Keymap: defaultKeymap(), Keymap: defaultKeymap(),
Execmap: make(map[int]string), Execmap: make(map[int]string),
PrintQuery: false, PrintQuery: false,
ReadZero: false, ReadZero: false,
Sync: false, Sync: false,
History: nil, History: nil,
Header: make([]string, 0), Header: make([]string, 0),
Version: false} HeaderLines: 0,
Version: false}
} }
func help(ok int) { func help(ok int) {
@ -724,9 +727,18 @@ func parseOptions(opts *Options, allArgs []string) {
setHistory(nextString(allArgs, &i, "history file path required")) setHistory(nextString(allArgs, &i, "history file path required"))
case "--history-size": case "--history-size":
setHistoryMax(nextInt(allArgs, &i, "history max size required")) 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": case "--header-file":
opts.Header = readHeaderFile( opts.Header = readHeaderFile(
nextString(allArgs, &i, "header file name required")) 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": case "--version":
opts.Version = true opts.Version = true
default: default:
@ -762,6 +774,10 @@ func parseOptions(opts *Options, allArgs []string) {
setHistoryMax(atoi(value)) setHistoryMax(atoi(value))
} else if match, value := optString(arg, "--header-file="); match { } else if match, value := optString(arg, "--header-file="); match {
opts.Header = readHeaderFile(value) opts.Header = readHeaderFile(value)
opts.HeaderLines = 0
} else if match, value := optString(arg, "--header-lines="); match {
opts.Header = []string{}
opts.HeaderLines = atoi(value)
} else { } else {
errorExit("unknown option: " + arg) errorExit("unknown option: " + arg)
} }

View File

@ -11,7 +11,7 @@ import (
// Reader reads from command or standard input // Reader reads from command or standard input
type Reader struct { type Reader struct {
pusher func(string) pusher func(string) bool
eventBox *util.EventBox eventBox *util.EventBox
delimNil bool delimNil bool
} }
@ -43,8 +43,9 @@ func (r *Reader) feed(src io.Reader) {
if err == nil { if err == nil {
line = line[:len(line)-1] line = line[:len(line)-1]
} }
r.pusher(line) if r.pusher(line) {
r.eventBox.Set(EvtReadNew, nil) r.eventBox.Set(EvtReadNew, nil)
}
} }
if err != nil { if err != nil {
break break

View File

@ -10,7 +10,7 @@ func TestReadFromCommand(t *testing.T) {
strs := []string{} strs := []string{}
eb := util.NewEventBox() eb := util.NewEventBox()
reader := Reader{ reader := Reader{
pusher: func(s string) { strs = append(strs, s) }, pusher: func(s string) bool { strs = append(strs, s); return true },
eventBox: eb} eventBox: eb}
// Check EventBox // Check EventBox

View File

@ -79,6 +79,7 @@ var _runeWidths = make(map[rune]int)
const ( const (
reqPrompt util.EventType = iota reqPrompt util.EventType = iota
reqInfo reqInfo
reqHeader
reqList reqList
reqRefresh reqRefresh
reqRedraw 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 // UpdateProgress updates the search progress
func (t *Terminal) UpdateProgress(progress float32) { func (t *Terminal) UpdateProgress(progress float32) {
t.mutex.Lock() t.mutex.Lock()
@ -686,6 +703,8 @@ func (t *Terminal) Loop() {
t.printInfo() t.printInfo()
case reqList: case reqList:
t.printList() t.printList()
case reqHeader:
t.printHeader()
case reqRefresh: case reqRefresh:
t.suppress = false t.suppress = false
case reqRedraw: case reqRedraw: