summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/chunklist.go7
-rw-r--r--src/core.go50
-rw-r--r--src/matcher.go15
-rw-r--r--src/options.go74
-rw-r--r--src/reader.go75
-rw-r--r--src/reader_test.go11
-rw-r--r--src/terminal.go138
-rw-r--r--src/tui/light.go8
-rw-r--r--src/tui/tcell.go6
-rw-r--r--src/tui/tui.go19
-rw-r--r--src/util/chars.go5
11 files changed, 312 insertions, 96 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 2db5b3ae..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++
@@ -135,10 +136,11 @@ 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)
+ }, eventBox, opts.ReadZero, opts.Filter == nil)
go reader.ReadSource()
}
@@ -182,7 +184,7 @@ func Run(opts *Options, revision string) {
}
}
return false
- }, eventBox, opts.ReadZero)
+ }, eventBox, opts.ReadZero, false)
reader.ReadSource()
} else {
eventBox.Unwatch(EvtReadNew)
@@ -223,10 +225,23 @@ func Run(opts *Options, revision string) {
// Event coordination
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
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)
@@ -235,21 +250,38 @@ func Run(opts *Options, revision string) {
switch evt {
case EvtReadNew, EvtReadFin:
- reading = reading && evt == EvtReadNew
+ clearCache := false
+ if evt == EvtReadFin && nextCommand != nil {
+ clearCache = true
+ 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, terminal.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 {
+ restart(*command)
+ }
}
snapshot, _ := chunkList.Snapshot()
- matcher.Reset(snapshot, terminal.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 2ab3a896..9b20e55a 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)
@@ -56,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
@@ -141,12 +142,21 @@ const (
layoutReverseList
)
+type infoStyle int
+
+const (
+ infoDefault infoStyle = iota
+ infoInline
+ infoHidden
+)
+
type previewOpts struct {
command string
position windowPosition
size sizeSpec
hidden bool
wrap bool
+ border bool
}
// Options stores the values of command-line options
@@ -154,6 +164,7 @@ type Options struct {
Fuzzy bool
FuzzyAlgo algo.Algo
Extended bool
+ Phony bool
Case Case
Normalize bool
Nth []Range
@@ -175,7 +186,7 @@ type Options struct {
Hscroll bool
HscrollOff int
FileWord bool
- InlineInfo bool
+ InfoStyle infoStyle
JumpLabels string
Prompt string
Query string
@@ -207,6 +218,7 @@ func defaultOptions() *Options {
Fuzzy: true,
FuzzyAlgo: algo.FuzzyMatchV2,
Extended: true,
+ Phony: false,
Case: CaseSmart,
Normalize: true,
Nth: make([]Range, 0),
@@ -227,7 +239,7 @@ func defaultOptions() *Options {
Hscroll: true,
HscrollOff: 10,
FileWord: false,
- InlineInfo: false,
+ InfoStyle: infoDefault,
JumpLabels: defaultJumpLabels,
Prompt: "> ",
Query: "",
@@ -237,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) },
@@ -414,6 +426,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":
@@ -628,13 +648,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"
@@ -787,6 +809,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:
@@ -822,6 +846,8 @@ func isExecuteAction(str string) actionType {
prefix = matches[0][2]
}
switch prefix {
+ case "reload":
+ return actReload
case "execute":
return actExecute
case "execute-silent":
@@ -887,6 +913,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
@@ -898,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":
@@ -910,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")
@@ -1014,6 +1059,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":
@@ -1088,10 +1137,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
@@ -1146,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":
@@ -1199,6 +1253,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/reader.go b/src/reader.go
index b418f549..b388411b 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,17 @@ type Reader struct {
eventBox *util.EventBox
delimNil bool
event int32
+ finChan chan bool
+ mutex sync.Mutex
+ 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)}
+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() {
@@ -29,9 +37,12 @@ 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) {
+ if r.wait {
+ r.finChan <- true
+ }
return
} else {
pollInterval += readerPollIntervalStep
@@ -46,7 +57,37 @@ func (r *Reader) startEventPoller() {
func (r *Reader) fin(success bool) {
atomic.StoreInt32(&r.event, int32(EvtReadFin))
- r.eventBox.Set(EvtReadFin, success)
+ if r.wait {
+ <-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 +95,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 +144,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..8bbb488e 100644
--- a/src/reader_test.go
+++ b/src/reader_test.go
@@ -10,10 +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 },
- eventBox: eb,
- event: int32(EvtReady)}
+ reader := NewReader(
+ func(s []byte) bool { strs = append(strs, string(s)); return true },
+ eb, false, true)
reader.startEventPoller()
@@ -23,7 +22,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 +47,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..4cd507a3 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
@@ -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 {
@@ -355,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 {
@@ -374,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,
@@ -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,
@@ -432,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()
@@ -440,11 +450,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 {
@@ -614,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 := 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
// 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
@@ -666,7 +680,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 {
@@ -719,7 +733,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
@@ -731,18 +755,13 @@ 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
}
- 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 +779,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() {
@@ -780,7 +798,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 {
@@ -809,7 +827,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 {
@@ -1383,7 +1401,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
@@ -1422,7 +1440,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}
@@ -1508,11 +1526,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 +1550,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()
@@ -1584,9 +1601,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 {
@@ -1607,7 +1621,7 @@ func (t *Terminal) Loop() {
switch req {
case reqPrompt:
t.printPrompt()
- if t.inlineInfo {
+ if t.noInfoLine() {
t.printInfo()
}
case reqInfo:
@@ -1673,6 +1687,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 +1772,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)
@@ -1987,7 +2003,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()
@@ -2025,10 +2041,25 @@ 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
+ t.selected = make(map[int32]selectedItem)
+ }
}
return true
}
- changed := false
mapkey := event.Type
if t.jumping == jumpDisabled {
actions := t.keymap[mapkey]
@@ -2042,8 +2073,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 +2093,7 @@ func (t *Terminal) Loop() {
req(reqList)
}
- if changed {
+ if queryChanged {
if t.isPreviewEnabled() {
_, q := hasPreviewFlags(t.preview.command)
if q {
@@ -2070,14 +2102,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)
@@ -2127,7 +2159,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/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..5a2e8d1b 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
@@ -215,6 +221,8 @@ type BorderStyle struct {
bottomRight rune
}
+type BorderCharacter int
+
func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
if unicode {
return BorderStyle{
@@ -238,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)
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)