Add --header-lines option
This commit is contained in:
parent
18469b6954
commit
f469c25730
@ -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"
|
||||||
|
@ -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
|
||||||
|
@ -44,5 +44,6 @@ const (
|
|||||||
EvtSearchNew
|
EvtSearchNew
|
||||||
EvtSearchProgress
|
EvtSearchProgress
|
||||||
EvtSearchFin
|
EvtSearchFin
|
||||||
|
EvtHeader
|
||||||
EvtClose
|
EvtClose
|
||||||
)
|
)
|
||||||
|
24
src/core.go
24
src/core.go
@ -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:
|
||||||
|
144
src/options.go
144
src/options.go
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
Loading…
Reference in New Issue
Block a user