mirror of
https://github.com/Llewellynvdm/fzf.git
synced 2024-06-03 07:50:49 +00:00
Implement streaming preview window (#2215)
Fix #2212 # Will start rendering after 200ms, update every 100ms fzf --preview 'for i in $(seq 100); do echo $i; sleep 0.01; done' # Should print "Loading .." message after 500ms fzf --preview 'sleep 1; for i in $(seq 100); do echo $i; sleep 0.01; done' # The first line should appear after 200ms fzf --preview 'date; sleep 2; date' # Should not render before enough lines for the scroll offset are ready rg --line-number --no-heading --color=always ^ | fzf --delimiter : --ansi --preview-window '+{2}-/2' \ --preview 'sleep 1; bat --style=numbers --color=always --pager=never --highlight-line={2} {1}'
This commit is contained in:
parent
305896fcb3
commit
faf68dbc5c
|
@ -1,6 +1,13 @@
|
||||||
CHANGELOG
|
CHANGELOG
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
0.24.0
|
||||||
|
------
|
||||||
|
- fzf can render preview window before the command completes
|
||||||
|
```sh
|
||||||
|
fzf --preview 'sleep 1; for i in $(seq 100); do echo $i; sleep 0.01; done'
|
||||||
|
```
|
||||||
|
|
||||||
0.23.1
|
0.23.1
|
||||||
------
|
------
|
||||||
- Added `--preview-window` options for disabling flags
|
- Added `--preview-window` options for disabling flags
|
||||||
|
|
16
README.md
16
README.md
|
@ -582,9 +582,9 @@ and fzf will warn you about it. To suppress the warning message, we added
|
||||||
|
|
||||||
### Preview window
|
### Preview window
|
||||||
|
|
||||||
When the `--preview` option is set, fzf automatically starts an external process
|
When the `--preview` option is set, fzf automatically starts an external process
|
||||||
with the current line as the argument and shows the result in the split window.
|
with the current line as the argument and shows the result in the split window.
|
||||||
Your `$SHELL` is used to execute the command with `$SHELL -c COMMAND`.
|
Your `$SHELL` is used to execute the command with `$SHELL -c COMMAND`.
|
||||||
The window can be scrolled using the mouse or custom key bindings.
|
The window can be scrolled using the mouse or custom key bindings.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
@ -592,16 +592,8 @@ The window can be scrolled using the mouse or custom key bindings.
|
||||||
fzf --preview 'cat {}'
|
fzf --preview 'cat {}'
|
||||||
```
|
```
|
||||||
|
|
||||||
Since the preview window is updated only after the process is complete, it's
|
|
||||||
important that the command finishes quickly.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Use head instead of cat so that the command doesn't take too long to finish
|
|
||||||
fzf --preview 'head -100 {}'
|
|
||||||
```
|
|
||||||
|
|
||||||
Preview window supports ANSI colors, so you can use any program that
|
Preview window supports ANSI colors, so you can use any program that
|
||||||
syntax-highlights the content of a file, such as
|
syntax-highlights the content of a file, such as
|
||||||
[Bat](https://github.com/sharkdp/bat) or
|
[Bat](https://github.com/sharkdp/bat) or
|
||||||
[Highlight](http://www.andre-simon.de/doku/highlight/en/highlight.php):
|
[Highlight](http://www.andre-simon.de/doku/highlight/en/highlight.php):
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,8 @@ const (
|
||||||
initialDelayTac = 100 * time.Millisecond
|
initialDelayTac = 100 * time.Millisecond
|
||||||
spinnerDuration = 100 * time.Millisecond
|
spinnerDuration = 100 * time.Millisecond
|
||||||
previewCancelWait = 500 * time.Millisecond
|
previewCancelWait = 500 * time.Millisecond
|
||||||
|
previewChunkDelay = 100 * time.Millisecond
|
||||||
|
previewDelayed = 500 * time.Millisecond
|
||||||
maxPatternLength = 300
|
maxPatternLength = 300
|
||||||
maxMulti = math.MaxInt32
|
maxMulti = math.MaxInt32
|
||||||
|
|
||||||
|
|
330
src/terminal.go
330
src/terminal.go
|
@ -4,7 +4,6 @@ import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
@ -43,11 +42,25 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
type previewer struct {
|
type previewer struct {
|
||||||
text string
|
version int
|
||||||
lines int
|
lines []string
|
||||||
offset int
|
offset int
|
||||||
enabled bool
|
enabled bool
|
||||||
more bool
|
scrollable bool
|
||||||
|
final bool
|
||||||
|
spinner string
|
||||||
|
}
|
||||||
|
|
||||||
|
type previewed struct {
|
||||||
|
version int
|
||||||
|
numLines int
|
||||||
|
offset int
|
||||||
|
filled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type eachLine struct {
|
||||||
|
line string
|
||||||
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
type itemLine struct {
|
type itemLine struct {
|
||||||
|
@ -125,6 +138,7 @@ type Terminal struct {
|
||||||
reqBox *util.EventBox
|
reqBox *util.EventBox
|
||||||
preview previewOpts
|
preview previewOpts
|
||||||
previewer previewer
|
previewer previewer
|
||||||
|
previewed previewed
|
||||||
previewBox *util.EventBox
|
previewBox *util.EventBox
|
||||||
eventBox *util.EventBox
|
eventBox *util.EventBox
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
|
@ -171,6 +185,7 @@ const (
|
||||||
reqPreviewEnqueue
|
reqPreviewEnqueue
|
||||||
reqPreviewDisplay
|
reqPreviewDisplay
|
||||||
reqPreviewRefresh
|
reqPreviewRefresh
|
||||||
|
reqPreviewDelayed
|
||||||
reqQuit
|
reqQuit
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -263,12 +278,15 @@ type searchRequest struct {
|
||||||
|
|
||||||
type previewRequest struct {
|
type previewRequest struct {
|
||||||
template string
|
template string
|
||||||
|
pwindow tui.Window
|
||||||
list []*Item
|
list []*Item
|
||||||
}
|
}
|
||||||
|
|
||||||
type previewResult struct {
|
type previewResult struct {
|
||||||
content string
|
version int
|
||||||
|
lines []string
|
||||||
offset int
|
offset int
|
||||||
|
spinner string
|
||||||
}
|
}
|
||||||
|
|
||||||
func toActions(types ...actionType) []action {
|
func toActions(types ...actionType) []action {
|
||||||
|
@ -353,6 +371,13 @@ func hasPreviewAction(opts *Options) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func makeSpinner(unicode bool) []string {
|
||||||
|
if unicode {
|
||||||
|
return []string{`⠋`, `⠙`, `⠹`, `⠸`, `⠼`, `⠴`, `⠦`, `⠧`, `⠇`, `⠏`}
|
||||||
|
}
|
||||||
|
return []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`}
|
||||||
|
}
|
||||||
|
|
||||||
// NewTerminal returns new Terminal object
|
// NewTerminal returns new Terminal object
|
||||||
func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
||||||
input := trimQuery(opts.Query)
|
input := trimQuery(opts.Query)
|
||||||
|
@ -416,14 +441,10 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
||||||
wordRubout = fmt.Sprintf("%s[^%s]", sep, sep)
|
wordRubout = fmt.Sprintf("%s[^%s]", sep, sep)
|
||||||
wordNext = fmt.Sprintf("[^%s]%s|(.$)", sep, sep)
|
wordNext = fmt.Sprintf("[^%s]%s|(.$)", sep, sep)
|
||||||
}
|
}
|
||||||
spinner := []string{`⠋`, `⠙`, `⠹`, `⠸`, `⠼`, `⠴`, `⠦`, `⠧`, `⠇`, `⠏`}
|
|
||||||
if !opts.Unicode {
|
|
||||||
spinner = []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`}
|
|
||||||
}
|
|
||||||
t := Terminal{
|
t := Terminal{
|
||||||
initDelay: delay,
|
initDelay: delay,
|
||||||
infoStyle: opts.InfoStyle,
|
infoStyle: opts.InfoStyle,
|
||||||
spinner: spinner,
|
spinner: makeSpinner(opts.Unicode),
|
||||||
queryLen: [2]int{0, 0},
|
queryLen: [2]int{0, 0},
|
||||||
layout: opts.Layout,
|
layout: opts.Layout,
|
||||||
fullscreen: fullscreen,
|
fullscreen: fullscreen,
|
||||||
|
@ -467,7 +488,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
||||||
selected: make(map[int32]selectedItem),
|
selected: make(map[int32]selectedItem),
|
||||||
reqBox: util.NewEventBox(),
|
reqBox: util.NewEventBox(),
|
||||||
preview: opts.Preview,
|
preview: opts.Preview,
|
||||||
previewer: previewer{"", 0, 0, previewBox != nil && !opts.Preview.hidden, false},
|
previewer: previewer{0, []string{}, 0, previewBox != nil && !opts.Preview.hidden, false, true, ""},
|
||||||
|
previewed: previewed{0, 0, 0, false},
|
||||||
previewBox: previewBox,
|
previewBox: previewBox,
|
||||||
eventBox: eventBox,
|
eventBox: eventBox,
|
||||||
mutex: sync.Mutex{},
|
mutex: sync.Mutex{},
|
||||||
|
@ -682,6 +704,8 @@ func (t *Terminal) resizeWindows() {
|
||||||
if t.pwindow != nil {
|
if t.pwindow != nil {
|
||||||
t.pwindow.Close()
|
t.pwindow.Close()
|
||||||
}
|
}
|
||||||
|
// Reset preview version so that full redraw occurs
|
||||||
|
t.previewed.version = 0
|
||||||
|
|
||||||
width := screenWidth - marginInt[1] - marginInt[3]
|
width := screenWidth - marginInt[1] - marginInt[3]
|
||||||
height := screenHeight - marginInt[0] - marginInt[2]
|
height := screenHeight - marginInt[0] - marginInt[2]
|
||||||
|
@ -719,12 +743,6 @@ func (t *Terminal) resizeWindows() {
|
||||||
pwidth -= 4
|
pwidth -= 4
|
||||||
x += 2
|
x += 2
|
||||||
}
|
}
|
||||||
// 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
|
|
||||||
// column larger than the desired value.
|
|
||||||
if !t.preview.wrap && t.tui.DoesAutoWrap() {
|
|
||||||
pwidth += 1
|
|
||||||
}
|
|
||||||
t.pwindow = t.tui.NewWindow(y, x, pwidth, pheight, true, noBorder)
|
t.pwindow = t.tui.NewWindow(y, x, pwidth, pheight, true, noBorder)
|
||||||
}
|
}
|
||||||
verticalPad := 2
|
verticalPad := 2
|
||||||
|
@ -824,6 +842,14 @@ func (t *Terminal) printPrompt() {
|
||||||
t.window.CPrint(tui.ColNormal, t.strong, string(after))
|
t.window.CPrint(tui.ColNormal, t.strong, string(after))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Terminal) trimMessage(message string, maxWidth int) string {
|
||||||
|
if len(message) <= maxWidth {
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
runes, _ := t.trimRight([]rune(message), maxWidth-2)
|
||||||
|
return string(runes) + strings.Repeat(".", util.Constrain(maxWidth, 0, 2))
|
||||||
|
}
|
||||||
|
|
||||||
func (t *Terminal) printInfo() {
|
func (t *Terminal) printInfo() {
|
||||||
pos := 0
|
pos := 0
|
||||||
switch t.infoStyle {
|
switch t.infoStyle {
|
||||||
|
@ -875,11 +901,7 @@ func (t *Terminal) printInfo() {
|
||||||
if t.failed != nil && t.count == 0 {
|
if t.failed != nil && t.count == 0 {
|
||||||
output = fmt.Sprintf("[Command failed: %s]", *t.failed)
|
output = fmt.Sprintf("[Command failed: %s]", *t.failed)
|
||||||
}
|
}
|
||||||
maxWidth := t.window.Width() - pos
|
output = t.trimMessage(output, t.window.Width()-pos)
|
||||||
if len(output) > maxWidth {
|
|
||||||
outputRunes, _ := t.trimRight([]rune(output), maxWidth-2)
|
|
||||||
output = string(outputRunes) + strings.Repeat(".", util.Constrain(maxWidth, 0, 2))
|
|
||||||
}
|
|
||||||
t.window.CPrint(tui.ColInfo, 0, output)
|
t.window.CPrint(tui.ColInfo, 0, output)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1130,28 +1152,47 @@ func (t *Terminal) printHighlighted(result Result, attr tui.Attr, col1 tui.Color
|
||||||
return displayWidth
|
return displayWidth
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Terminal) printPreview() {
|
func (t *Terminal) renderPreviewSpinner() {
|
||||||
if !t.hasPreviewWindow() {
|
numLines := len(t.previewer.lines)
|
||||||
return
|
spin := t.previewer.spinner
|
||||||
|
if len(spin) > 0 || t.previewer.scrollable {
|
||||||
|
maxWidth := t.pwindow.Width()
|
||||||
|
if !t.previewer.scrollable {
|
||||||
|
if maxWidth > 0 {
|
||||||
|
t.pwindow.Move(0, maxWidth-1)
|
||||||
|
t.pwindow.CPrint(tui.ColSpinner, t.strong, spin)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
offsetString := fmt.Sprintf("%d/%d", t.previewer.offset+1, numLines)
|
||||||
|
if len(spin) > 0 {
|
||||||
|
spin += " "
|
||||||
|
maxWidth -= 2
|
||||||
|
}
|
||||||
|
offsetRunes, _ := t.trimRight([]rune(offsetString), maxWidth)
|
||||||
|
pos := maxWidth - t.displayWidth(offsetRunes)
|
||||||
|
t.pwindow.Move(0, pos)
|
||||||
|
if maxWidth > 0 {
|
||||||
|
t.pwindow.CPrint(tui.ColSpinner, t.strong, spin)
|
||||||
|
t.pwindow.CPrint(tui.ColInfo, tui.Reverse, string(offsetRunes))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
t.pwindow.Erase()
|
}
|
||||||
|
|
||||||
|
func (t *Terminal) renderPreviewText(unchanged bool) {
|
||||||
maxWidth := t.pwindow.Width()
|
maxWidth := t.pwindow.Width()
|
||||||
if t.tui.DoesAutoWrap() {
|
|
||||||
maxWidth -= 1
|
|
||||||
}
|
|
||||||
reader := bufio.NewReader(strings.NewReader(t.previewer.text))
|
|
||||||
lineNo := -t.previewer.offset
|
lineNo := -t.previewer.offset
|
||||||
height := t.pwindow.Height()
|
height := t.pwindow.Height()
|
||||||
t.previewer.more = t.previewer.offset > 0
|
if unchanged {
|
||||||
|
t.pwindow.Move(0, 0)
|
||||||
|
} else {
|
||||||
|
t.previewed.filled = false
|
||||||
|
t.pwindow.Erase()
|
||||||
|
}
|
||||||
var ansi *ansiState
|
var ansi *ansiState
|
||||||
for ; ; lineNo++ {
|
for _, line := range t.previewer.lines {
|
||||||
line, err := reader.ReadString('\n')
|
|
||||||
eof := err == io.EOF
|
|
||||||
if !eof {
|
|
||||||
line = line[:len(line)-1]
|
|
||||||
}
|
|
||||||
if lineNo >= height || t.pwindow.Y() == height-1 && t.pwindow.X() > 0 {
|
if lineNo >= height || t.pwindow.Y() == height-1 && t.pwindow.X() > 0 {
|
||||||
|
t.previewed.filled = true
|
||||||
break
|
break
|
||||||
} else if lineNo >= 0 {
|
} else if lineNo >= 0 {
|
||||||
var fillRet tui.FillReturn
|
var fillRet tui.FillReturn
|
||||||
|
@ -1170,31 +1211,55 @@ func (t *Terminal) printPreview() {
|
||||||
}
|
}
|
||||||
return fillRet == tui.FillContinue
|
return fillRet == tui.FillContinue
|
||||||
})
|
})
|
||||||
t.previewer.more = t.previewer.more || t.pwindow.Y() == height-1 && t.pwindow.X() == t.pwindow.Width()
|
t.previewer.scrollable = t.previewer.scrollable || t.pwindow.Y() == height-1 && t.pwindow.X() == t.pwindow.Width()
|
||||||
if fillRet == tui.FillNextLine {
|
if fillRet == tui.FillNextLine {
|
||||||
continue
|
continue
|
||||||
} else if fillRet == tui.FillSuspend {
|
} else if fillRet == tui.FillSuspend {
|
||||||
|
t.previewed.filled = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if unchanged && lineNo == 0 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
t.pwindow.Fill("\n")
|
|
||||||
}
|
|
||||||
if eof {
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
lineNo++
|
||||||
}
|
}
|
||||||
t.pwindow.FinishFill()
|
if !unchanged {
|
||||||
if t.previewer.lines > height {
|
t.pwindow.FinishFill()
|
||||||
t.previewer.more = true
|
|
||||||
offset := fmt.Sprintf("%d/%d", t.previewer.offset+1, t.previewer.lines)
|
|
||||||
pos := t.pwindow.Width() - len(offset)
|
|
||||||
if t.tui.DoesAutoWrap() {
|
|
||||||
pos -= 1
|
|
||||||
}
|
|
||||||
t.pwindow.Move(0, pos)
|
|
||||||
t.pwindow.CPrint(tui.ColInfo, tui.Reverse, offset)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Terminal) printPreview() {
|
||||||
|
if !t.hasPreviewWindow() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
numLines := len(t.previewer.lines)
|
||||||
|
height := t.pwindow.Height()
|
||||||
|
unchanged := (t.previewed.filled || numLines == t.previewed.numLines) &&
|
||||||
|
t.previewer.version == t.previewed.version &&
|
||||||
|
t.previewer.offset == t.previewed.offset
|
||||||
|
t.previewer.scrollable = t.previewer.offset > 0 || numLines > height
|
||||||
|
t.renderPreviewText(unchanged)
|
||||||
|
t.renderPreviewSpinner()
|
||||||
|
t.previewed.numLines = numLines
|
||||||
|
t.previewed.version = t.previewer.version
|
||||||
|
t.previewed.offset = t.previewer.offset
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Terminal) printPreviewDelayed() {
|
||||||
|
if !t.hasPreviewWindow() || len(t.previewer.lines) > 0 && t.previewed.version == t.previewer.version {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.previewer.scrollable = false
|
||||||
|
t.renderPreviewText(true)
|
||||||
|
|
||||||
|
message := t.trimMessage("Loading ..", t.pwindow.Width())
|
||||||
|
pos := t.pwindow.Width() - len(message)
|
||||||
|
t.pwindow.Move(0, pos)
|
||||||
|
t.pwindow.CPrint(tui.ColInfo, tui.Reverse, message)
|
||||||
|
}
|
||||||
|
|
||||||
func (t *Terminal) processTabs(runes []rune, prefixWidth int) (string, int) {
|
func (t *Terminal) processTabs(runes []rune, prefixWidth int) (string, int) {
|
||||||
var strbuf bytes.Buffer
|
var strbuf bytes.Buffer
|
||||||
l := prefixWidth
|
l := prefixWidth
|
||||||
|
@ -1686,9 +1751,11 @@ func (t *Terminal) Loop() {
|
||||||
|
|
||||||
if t.hasPreviewer() {
|
if t.hasPreviewer() {
|
||||||
go func() {
|
go func() {
|
||||||
|
version := 0
|
||||||
for {
|
for {
|
||||||
var items []*Item
|
var items []*Item
|
||||||
var commandTemplate string
|
var commandTemplate string
|
||||||
|
var pwindow tui.Window
|
||||||
t.previewBox.Wait(func(events *util.Events) {
|
t.previewBox.Wait(func(events *util.Events) {
|
||||||
for req, value := range *events {
|
for req, value := range *events {
|
||||||
switch req {
|
switch req {
|
||||||
|
@ -1696,63 +1763,129 @@ func (t *Terminal) Loop() {
|
||||||
request := value.(previewRequest)
|
request := value.(previewRequest)
|
||||||
commandTemplate = request.template
|
commandTemplate = request.template
|
||||||
items = request.list
|
items = request.list
|
||||||
|
pwindow = request.pwindow
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
events.Clear()
|
events.Clear()
|
||||||
})
|
})
|
||||||
|
version++
|
||||||
// We don't display preview window if no match
|
// We don't display preview window if no match
|
||||||
if items[0] != nil {
|
if items[0] != nil {
|
||||||
command := t.replacePlaceholder(commandTemplate, false, string(t.Input()), items)
|
command := t.replacePlaceholder(commandTemplate, false, string(t.Input()), items)
|
||||||
offset := 0
|
initialOffset := 0
|
||||||
cmd := util.ExecCommand(command, true)
|
cmd := util.ExecCommand(command, true)
|
||||||
if t.pwindow != nil {
|
if pwindow != nil {
|
||||||
height := t.pwindow.Height()
|
height := pwindow.Height()
|
||||||
offset = t.evaluateScrollOffset(items, height)
|
initialOffset = util.Max(0, t.evaluateScrollOffset(items, height))
|
||||||
env := os.Environ()
|
env := os.Environ()
|
||||||
lines := fmt.Sprintf("LINES=%d", height)
|
lines := fmt.Sprintf("LINES=%d", height)
|
||||||
columns := fmt.Sprintf("COLUMNS=%d", t.pwindow.Width())
|
columns := fmt.Sprintf("COLUMNS=%d", pwindow.Width())
|
||||||
env = append(env, lines)
|
env = append(env, lines)
|
||||||
env = append(env, "FZF_PREVIEW_"+lines)
|
env = append(env, "FZF_PREVIEW_"+lines)
|
||||||
env = append(env, columns)
|
env = append(env, columns)
|
||||||
env = append(env, "FZF_PREVIEW_"+columns)
|
env = append(env, "FZF_PREVIEW_"+columns)
|
||||||
cmd.Env = env
|
cmd.Env = env
|
||||||
}
|
}
|
||||||
var out bytes.Buffer
|
|
||||||
cmd.Stdout = &out
|
out, _ := cmd.StdoutPipe()
|
||||||
cmd.Stderr = &out
|
cmd.Stderr = cmd.Stdout
|
||||||
err := cmd.Start()
|
reader := bufio.NewReader(out)
|
||||||
if err != nil {
|
eofChan := make(chan bool)
|
||||||
out.Write([]byte(err.Error()))
|
|
||||||
}
|
|
||||||
finishChan := make(chan bool, 1)
|
finishChan := make(chan bool, 1)
|
||||||
updateChan := make(chan bool)
|
reapChan := make(chan bool)
|
||||||
go func() {
|
err := cmd.Start()
|
||||||
select {
|
reaps := 0
|
||||||
case code := <-t.killChan:
|
if err != nil {
|
||||||
if code != exitCancel {
|
t.reqBox.Set(reqPreviewDisplay, previewResult{version, []string{err.Error()}, 0, ""})
|
||||||
util.KillCommand(cmd)
|
} else {
|
||||||
os.Exit(code)
|
reaps = 2
|
||||||
} else {
|
lineChan := make(chan eachLine)
|
||||||
select {
|
// Goroutine 1 reads process output
|
||||||
case <-time.After(previewCancelWait):
|
go func() {
|
||||||
util.KillCommand(cmd)
|
for {
|
||||||
updateChan <- true
|
line, err := reader.ReadString('\n')
|
||||||
case <-finishChan:
|
lineChan <- eachLine{line, err}
|
||||||
updateChan <- false
|
if err != nil {
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case <-finishChan:
|
eofChan <- true
|
||||||
updateChan <- false
|
}()
|
||||||
|
// Goroutine 2 periodically requests rendering
|
||||||
|
go func(version int) {
|
||||||
|
lines := []string{}
|
||||||
|
spinner := makeSpinner(t.unicode)
|
||||||
|
spinnerIndex := -1 // Delay initial rendering by an extra tick
|
||||||
|
ticker := time.NewTicker(previewChunkDelay)
|
||||||
|
offset := initialOffset
|
||||||
|
Loop:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
if len(lines) > 0 && len(lines) >= initialOffset {
|
||||||
|
if spinnerIndex >= 0 {
|
||||||
|
spin := spinner[spinnerIndex%len(spinner)]
|
||||||
|
t.reqBox.Set(reqPreviewDisplay, previewResult{version, lines, offset, spin})
|
||||||
|
offset = -1
|
||||||
|
}
|
||||||
|
spinnerIndex++
|
||||||
|
}
|
||||||
|
case eachLine := <-lineChan:
|
||||||
|
line := eachLine.line
|
||||||
|
err := eachLine.err
|
||||||
|
if len(line) > 0 {
|
||||||
|
lines = append(lines, line)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
if len(lines) > 0 {
|
||||||
|
t.reqBox.Set(reqPreviewDisplay, previewResult{version, lines, offset, ""})
|
||||||
|
}
|
||||||
|
break Loop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ticker.Stop()
|
||||||
|
reapChan <- true
|
||||||
|
}(version)
|
||||||
|
}
|
||||||
|
// Goroutine 3 is responsible for cancelling running preview command
|
||||||
|
go func(version int) {
|
||||||
|
timer := time.NewTimer(previewDelayed)
|
||||||
|
Loop:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-timer.C:
|
||||||
|
t.reqBox.Set(reqPreviewDelayed, version)
|
||||||
|
case code := <-t.killChan:
|
||||||
|
if code != exitCancel {
|
||||||
|
util.KillCommand(cmd)
|
||||||
|
os.Exit(code)
|
||||||
|
} else {
|
||||||
|
timer := time.NewTimer(previewCancelWait)
|
||||||
|
select {
|
||||||
|
case <-timer.C:
|
||||||
|
util.KillCommand(cmd)
|
||||||
|
case <-finishChan:
|
||||||
|
}
|
||||||
|
timer.Stop()
|
||||||
|
}
|
||||||
|
break Loop
|
||||||
|
case <-finishChan:
|
||||||
|
break Loop
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}()
|
timer.Stop()
|
||||||
cmd.Wait()
|
reapChan <- true
|
||||||
|
}(version)
|
||||||
|
<-eofChan
|
||||||
|
cmd.Wait() // NOTE: We should not call Wait before EOF
|
||||||
finishChan <- true
|
finishChan <- true
|
||||||
if out.Len() > 0 || !<-updateChan {
|
for i := 0; i < reaps; i++ {
|
||||||
t.reqBox.Set(reqPreviewDisplay, previewResult{out.String(), offset})
|
<-reapChan
|
||||||
}
|
}
|
||||||
cleanTemporaryFiles()
|
cleanTemporaryFiles()
|
||||||
} else {
|
} else {
|
||||||
t.reqBox.Set(reqPreviewDisplay, previewResult{"", 0})
|
t.reqBox.Set(reqPreviewDisplay, previewResult{version, nil, 0, ""})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
@ -1772,7 +1905,7 @@ func (t *Terminal) Loop() {
|
||||||
if len(command) > 0 && t.isPreviewEnabled() {
|
if len(command) > 0 && t.isPreviewEnabled() {
|
||||||
_, list := t.buildPlusList(command, false)
|
_, list := t.buildPlusList(command, false)
|
||||||
t.cancelPreview()
|
t.cancelPreview()
|
||||||
t.previewBox.Set(reqPreviewEnqueue, previewRequest{command, list})
|
t.previewBox.Set(reqPreviewEnqueue, previewRequest{command, t.pwindow, list})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1827,12 +1960,18 @@ func (t *Terminal) Loop() {
|
||||||
})
|
})
|
||||||
case reqPreviewDisplay:
|
case reqPreviewDisplay:
|
||||||
result := value.(previewResult)
|
result := value.(previewResult)
|
||||||
t.previewer.text = result.content
|
t.previewer.version = result.version
|
||||||
t.previewer.lines = strings.Count(t.previewer.text, "\n")
|
t.previewer.lines = result.lines
|
||||||
t.previewer.offset = util.Constrain(result.offset, 0, t.previewer.lines-1)
|
t.previewer.spinner = result.spinner
|
||||||
|
if result.offset >= 0 {
|
||||||
|
t.previewer.offset = util.Constrain(result.offset, 0, len(t.previewer.lines)-1)
|
||||||
|
}
|
||||||
t.printPreview()
|
t.printPreview()
|
||||||
case reqPreviewRefresh:
|
case reqPreviewRefresh:
|
||||||
t.printPreview()
|
t.printPreview()
|
||||||
|
case reqPreviewDelayed:
|
||||||
|
t.previewer.version = value.(int)
|
||||||
|
t.printPreviewDelayed()
|
||||||
case reqPrintQuery:
|
case reqPrintQuery:
|
||||||
exit(func() int {
|
exit(func() int {
|
||||||
t.printer(string(t.input))
|
t.printer(string(t.input))
|
||||||
|
@ -1885,14 +2024,15 @@ func (t *Terminal) Loop() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
scrollPreview := func(amount int) {
|
scrollPreview := func(amount int) {
|
||||||
if !t.previewer.more {
|
if !t.previewer.scrollable {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
newOffset := t.previewer.offset + amount
|
newOffset := t.previewer.offset + amount
|
||||||
|
numLines := len(t.previewer.lines)
|
||||||
if t.preview.cycle {
|
if t.preview.cycle {
|
||||||
newOffset = (newOffset + t.previewer.lines) % t.previewer.lines
|
newOffset = (newOffset + numLines) % numLines
|
||||||
}
|
}
|
||||||
newOffset = util.Constrain(newOffset, 0, t.previewer.lines-1)
|
newOffset = util.Constrain(newOffset, 0, numLines-1)
|
||||||
if t.previewer.offset != newOffset {
|
if t.previewer.offset != newOffset {
|
||||||
t.previewer.offset = newOffset
|
t.previewer.offset = newOffset
|
||||||
req(reqPreviewRefresh)
|
req(reqPreviewRefresh)
|
||||||
|
@ -1934,7 +2074,7 @@ func (t *Terminal) Loop() {
|
||||||
if valid {
|
if valid {
|
||||||
t.cancelPreview()
|
t.cancelPreview()
|
||||||
t.previewBox.Set(reqPreviewEnqueue,
|
t.previewBox.Set(reqPreviewEnqueue,
|
||||||
previewRequest{t.preview.command, list})
|
previewRequest{t.preview.command, t.pwindow, list})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,10 +32,9 @@ func (r *FullscreenRenderer) Clear() {}
|
||||||
func (r *FullscreenRenderer) Refresh() {}
|
func (r *FullscreenRenderer) Refresh() {}
|
||||||
func (r *FullscreenRenderer) Close() {}
|
func (r *FullscreenRenderer) Close() {}
|
||||||
|
|
||||||
func (r *FullscreenRenderer) DoesAutoWrap() bool { return false }
|
func (r *FullscreenRenderer) GetChar() Event { return Event{} }
|
||||||
func (r *FullscreenRenderer) GetChar() Event { return Event{} }
|
func (r *FullscreenRenderer) MaxX() int { return 0 }
|
||||||
func (r *FullscreenRenderer) MaxX() int { return 0 }
|
func (r *FullscreenRenderer) MaxY() int { return 0 }
|
||||||
func (r *FullscreenRenderer) MaxY() int { return 0 }
|
|
||||||
|
|
||||||
func (r *FullscreenRenderer) RefreshWindows(windows []Window) {}
|
func (r *FullscreenRenderer) RefreshWindows(windows []Window) {}
|
||||||
|
|
||||||
|
|
|
@ -624,10 +624,6 @@ func (r *LightRenderer) MaxY() int {
|
||||||
return r.height
|
return r.height
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *LightRenderer) DoesAutoWrap() bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *LightRenderer) NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window {
|
func (r *LightRenderer) NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window {
|
||||||
w := &LightWindow{
|
w := &LightWindow{
|
||||||
renderer: r,
|
renderer: r,
|
||||||
|
|
|
@ -166,10 +166,6 @@ func (w *TcellWindow) Y() int {
|
||||||
return w.lastY
|
return w.lastY
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *FullscreenRenderer) DoesAutoWrap() bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *FullscreenRenderer) Clear() {
|
func (r *FullscreenRenderer) Clear() {
|
||||||
_screen.Sync()
|
_screen.Sync()
|
||||||
_screen.Clear()
|
_screen.Clear()
|
||||||
|
|
|
@ -286,7 +286,6 @@ type Renderer interface {
|
||||||
|
|
||||||
MaxX() int
|
MaxX() int
|
||||||
MaxY() int
|
MaxY() int
|
||||||
DoesAutoWrap() bool
|
|
||||||
|
|
||||||
NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window
|
NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user