Skip to content

Make sixel output flicker free #1943

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
3 changes: 3 additions & 0 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,9 @@ func (app *app) loop() {
if err == nil {
if r.path == curr.path {
app.ui.regPrev = r
if gOpts.sixel {
app.ui.sxScreen.forceClear = true
}
}
}

Expand Down
12 changes: 7 additions & 5 deletions eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,6 @@ func (e *setExpr) eval(app *app, args []string) {
if app.nav.height != app.ui.wins[0].h {
app.nav.height = app.ui.wins[0].h
clear(app.nav.regCache)
if gOpts.sixel {
app.ui.sxScreen.lastFile = ""
}
}
app.ui.loadFile(app, true)
}
Expand Down Expand Up @@ -157,6 +154,9 @@ func (e *setExpr) eval(app *app, args []string) {
err = errors.New("preview: 'ratios' should consist of at least two numbers before enabling 'preview'")
}
if err == nil {
if gOpts.sixel {
app.ui.sxScreen.forceClear = true
}
gOpts.preview = preview
app.ui.loadFile(app, true)
}
Expand All @@ -174,6 +174,9 @@ func (e *setExpr) eval(app *app, args []string) {
err = applyBoolOpt(&gOpts.showbinds, e)
case "sixel", "nosixel", "sixel!":
err = applyBoolOpt(&gOpts.sixel, e)
clear(app.nav.regCache)
app.ui.sxScreen.forceClear = true
app.ui.loadFile(app, true)
case "smartcase", "nosmartcase", "smartcase!":
err = applyBoolOpt(&gOpts.smartcase, e)
if err == nil {
Expand Down Expand Up @@ -350,7 +353,6 @@ func (e *setExpr) eval(app *app, args []string) {
app.ui.wins = getWins(app.ui.screen)
if gOpts.sixel {
clear(app.nav.regCache)
app.ui.sxScreen.lastFile = ""
}
app.ui.loadFile(app, true)
case "scrolloff":
Expand Down Expand Up @@ -1433,7 +1435,7 @@ func (e *callExpr) eval(app *app, args []string) {
}
if gOpts.sixel {
clear(app.nav.regCache)
app.ui.sxScreen.lastFile = ""
app.ui.sxScreen.forceClear = true
}
for _, dir := range app.nav.dirs {
dir.boundPos(app.nav.height)
Expand Down
Binary file added lf-31
Binary file not shown.
5 changes: 3 additions & 2 deletions misc.go
Original file line number Diff line number Diff line change
Expand Up @@ -325,8 +325,9 @@ func getFileExtension(file fs.FileInfo) string {
}

var (
reModKey = regexp.MustCompile(`<(c|s|a)-(.+)>`)
reRulerSub = regexp.MustCompile(`%[apmcsfithd]|%\{[^}]+\}`)
reModKey = regexp.MustCompile(`<(c|s|a)-(.+)>`)
reRulerSub = regexp.MustCompile(`%[apmcsfithd]|%\{[^}]+\}`)
reSixelSize = regexp.MustCompile(`"1;1;(\d+);(\d+)`)
)

var (
Expand Down
88 changes: 46 additions & 42 deletions sixel.go
Original file line number Diff line number Diff line change
@@ -1,69 +1,73 @@
package main

import (
"fmt"
"log"
"os"
"strings"
"strconv"

"github.com/gdamore/tcell/v2"
)

const (
gSixelBegin = "\033P"

// The filler character should be:
// - rarely used: the filler is used to trick tcell into redrawing, if the
// filler character appears in the user's preview, that cell might not
// be cleaned up properly
// - ideally renders as empty space: the filler alternates between bold
// and regular, using a non-space would look weird to the user.
gSixelFiller = '\u2000'
)
const gSixelBegin = "\033P"

type sixelScreen struct {
xprev, yprev int
sixel *string
altFill bool
lastFile string // TODO maybe use hash of sixels instead to flip altFill
lastFile string
lastWin win
forceClear bool
}

func (sxs *sixelScreen) fillerStyle(filePath string) tcell.Style {
if sxs.lastFile != filePath {
sxs.altFill = !sxs.altFill
func (sxs *sixelScreen) clearSixel(win *win, screen tcell.Screen, filePath string) {
if sxs.lastFile != "" && (filePath != sxs.lastFile || *win != sxs.lastWin || sxs.forceClear) {
screen.LockRegion(sxs.lastWin.x, sxs.lastWin.y, sxs.lastWin.w, sxs.lastWin.h, false)
}
}

if sxs.altFill {
return tcell.StyleDefault.Bold(true)
func (sxs *sixelScreen) printSixel(win *win, screen tcell.Screen, reg *reg) {
if reg.path == sxs.lastFile && *win == sxs.lastWin && !sxs.forceClear {
return
}
return tcell.StyleDefault
}

func (sxs *sixelScreen) showSixels() {
if sxs.sixel == nil {
if reg.sixel == nil {
sxs.lastFile = ""
return
}

// XXX: workaround for bug where quitting lf might leave the terminal in bold
fmt.Fprint(os.Stderr, "\033[0m")
ti, err := tcell.LookupTerminfo(os.Getenv("TERM"))
if err != nil {
log.Printf("sixel: failed to look up term into %s", err)
return
}

fmt.Fprint(os.Stderr, "\0337") // Save cursor position
fmt.Fprintf(os.Stderr, "\033[%d;%dH", sxs.yprev, sxs.xprev) // Move cursor to position
fmt.Fprint(os.Stderr, *sxs.sixel) //
fmt.Fprint(os.Stderr, "\0338") // Restore cursor position
}
tty, ok := screen.Tty()
if !ok {
log.Printf("sixel: failed to get tty")
return
}

func (sxs *sixelScreen) printSixel(win *win, screen tcell.Screen, reg *reg) {
if reg.sixel == nil {
ws, err := tty.WindowSize()
if err != nil {
log.Printf("sixel: failed to get window size %s", err)
return
}
cw, ch := ws.CellDimensions()
if cw <= 0 || ch <= 0 {
log.Printf("sixel: cell dimensions should not be 0")
return
}

// HACK: fillers are used to control when tcell redraws the region where a sixel image is drawn.
// alternating between bold and regular is to clear the image before drawing a new one.
st := sxs.fillerStyle(reg.path)
for y := range win.h {
st = win.print(screen, 0, y, st, strings.Repeat(string(gSixelFiller), win.w))
matches := reSixelSize.FindStringSubmatch(*reg.sixel)
if matches == nil {
log.Printf("sixel: failed to get image size")
return
}
iw, _ := strconv.Atoi(matches[1])
ih, _ := strconv.Atoi(matches[2])

screen.LockRegion(win.x, win.y, iw/cw, ih/ch, true)
ti.TPuts(tty, ti.TGoto(win.x, win.y))
ti.TPuts(tty, *reg.sixel)

sxs.xprev, sxs.yprev = win.x+1, win.y+1
sxs.sixel = reg.sixel
sxs.lastFile = reg.path
sxs.lastWin = *win
sxs.forceClear = false
}
17 changes: 7 additions & 10 deletions ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -1020,7 +1020,6 @@ func (ui *ui) draw(nav *nav) {
context := dirContext{selections: nav.selections, saves: nav.saves, tags: nav.tags}

ui.screen.Clear()
ui.sxScreen.sixel = nil

ui.drawPromptLine(nav)

Expand Down Expand Up @@ -1058,14 +1057,15 @@ func (ui *ui) draw(nav *nav) {
ui.screen.ShowCursor(ui.msgWin.x+runeSliceWidth(prefix)+runeSliceWidth(left), ui.msgWin.y)
}

if gOpts.preview {
curr, err := nav.currFile()
if err == nil {
preview := ui.wins[len(ui.wins)-1]

curr, err := nav.currFile()
if err == nil {
preview := ui.wins[len(ui.wins)-1]
ui.sxScreen.clearSixel(preview, ui.screen, curr.path)
if gOpts.preview {
if curr.Mode().IsRegular() || (curr.IsDir() && gOpts.dirpreviews) {
preview.printReg(ui.screen, ui.regPrev, nav.previewLoading, &ui.sxScreen)
} else if curr.IsDir() {
ui.sxScreen.lastFile = ""
preview.printDir(ui, ui.dirPrev, &context,
&dirStyle{colors: ui.styles, icons: ui.icons, role: Preview})
}
Expand Down Expand Up @@ -1097,10 +1097,6 @@ func (ui *ui) draw(nav *nav) {
}

ui.screen.Show()
if ui.menu == "" && ui.cmdPrefix == "" && ui.sxScreen.sixel != nil {
ui.sxScreen.lastFile = ui.regPrev.path
ui.sxScreen.showSixels()
}
}

func findBinds(keys map[string]expr, prefix string) (binds map[string]expr, ok bool) {
Expand Down Expand Up @@ -1522,6 +1518,7 @@ func (ui *ui) readExpr() {
}

func (ui *ui) suspend() error {
ui.sxScreen.forceClear = true
return ui.screen.Suspend()
}

Expand Down