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:
Junegunn Choi 2020-10-18 17:03:33 +09:00 committed by GitHub
parent 305896fcb3
commit faf68dbc5c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 251 additions and 120 deletions

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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})
} }
} }
} }

View File

@ -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) {}

View File

@ -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,

View File

@ -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()

View File

@ -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
} }