From a84b101d262c4799756908008dd8bcef393a157a Mon Sep 17 00:00:00 2001 From: Mariusz Kuchta Date: Wed, 2 Apr 2025 15:46:43 +0300 Subject: [PATCH 1/9] Make sixel output flicker free --- eval.go | 3 --- sixel.go | 67 ++++++++++++++++++++++---------------------------------- ui.go | 11 ++++------ 3 files changed, 30 insertions(+), 51 deletions(-) diff --git a/eval.go b/eval.go index 42e0488b..3ee74881 100644 --- a/eval.go +++ b/eval.go @@ -92,7 +92,6 @@ func (e *setExpr) eval(app *app, args []string) { app.nav.height = app.ui.wins[0].h clear(app.nav.regCache) if gOpts.sixel { - app.ui.sxScreen.lastFile = "" } } app.ui.loadFile(app, true) @@ -350,7 +349,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": @@ -1420,7 +1418,6 @@ func (e *callExpr) eval(app *app, args []string) { } if gOpts.sixel { clear(app.nav.regCache) - app.ui.sxScreen.lastFile = "" } for _, dir := range app.nav.dirs { dir.boundPos(app.nav.height) diff --git a/sixel.go b/sixel.go index d559cccd..5cbd1c76 100644 --- a/sixel.go +++ b/sixel.go @@ -1,69 +1,54 @@ package main import ( - "fmt" "os" - "strings" "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' ) type sixelScreen struct { - xprev, yprev int - sixel *string - altFill bool - lastFile string // TODO maybe use hash of sixels instead to flip altFill + lastFile string + lastWinW int + lastWinH int } -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 filePath != sxs.lastFile || win.w != sxs.lastWinW || win.h != sxs.lastWinH { + screen.LockRegion(win.x, win.y, win.w, win.h, false) } - if sxs.altFill { - return tcell.StyleDefault.Bold(true) - } - return tcell.StyleDefault } +func (sxs *sixelScreen) printSixel(win *win, screen tcell.Screen, reg *reg) { -func (sxs *sixelScreen) showSixels() { - if sxs.sixel == nil { + if reg.path == sxs.lastFile && win.w == sxs.lastWinW && win.h == sxs.lastWinH { return } - - // XXX: workaround for bug where quitting lf might leave the terminal in bold - fmt.Fprint(os.Stderr, "\033[0m") - - 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 -} - -func (sxs *sixelScreen) printSixel(win *win, screen tcell.Screen, reg *reg) { if reg.sixel == nil { + sxs.lastFile = "" 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)) + tty, ok := screen.Tty() + if !ok { + screen.Fini() + } + + screen.LockRegion(win.x, win.y, win.w, win.h, true) + + // Get the terminfo for our current terminal + ti, err := tcell.LookupTerminfo(os.Getenv("TERM")) + if err != nil { + screen.Fini() } - sxs.xprev, sxs.yprev = win.x+1, win.y+1 - sxs.sixel = reg.sixel + // Move the cursor to our draw position + ti.TPuts(tty, ti.TGoto(win.x, win.y)) + ti.TPuts(tty, *reg.sixel) + sxs.lastFile = reg.path + sxs.lastWinW = win.w + sxs.lastWinH = win.h } diff --git a/ui.go b/ui.go index 54dd1037..802243be 100644 --- a/ui.go +++ b/ui.go @@ -660,7 +660,7 @@ func newUI(screen tcell.Screen) *ui { styles: parseStyles(), icons: parseIcons(), currentFile: "", - sxScreen: sixelScreen{}, + sxScreen: sixelScreen{"", 0, 0}, } go ui.pollEvents() @@ -1037,7 +1037,6 @@ func (ui *ui) draw(nav *nav) { ui.screen.SetContent(i, j, ' ', nil, st) } } - ui.sxScreen.sixel = nil ui.drawPromptLine(nav) @@ -1080,8 +1079,9 @@ func (ui *ui) draw(nav *nav) { curr, err := nav.currFile() if err == nil { preview := ui.wins[len(ui.wins)-1] - + ui.sxScreen.clearSixel(preview, ui.screen, curr.path) if curr.IsDir() { + ui.sxScreen.lastFile = "" preview.printDir(ui, ui.dirPrev, &context, &dirStyle{colors: ui.styles, icons: ui.icons, role: Preview}, nav.previewLoading) @@ -1116,10 +1116,6 @@ func (ui *ui) draw(nav *nav) { } ui.screen.Show() - if ui.menuBuf == nil && 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) { @@ -1523,6 +1519,7 @@ func (ui *ui) readExpr() { } func (ui *ui) suspend() error { + ui.sxScreen.lastFile = "" return ui.screen.Suspend() } From 0738551785500d0b5a8374733d2a88a8ea94f6c1 Mon Sep 17 00:00:00 2001 From: Mariusz Kuchta Date: Thu, 3 Apr 2025 13:19:36 +0300 Subject: [PATCH 2/9] Address pr comments --- sixel.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/sixel.go b/sixel.go index 5cbd1c76..b0cec2bc 100644 --- a/sixel.go +++ b/sixel.go @@ -1,6 +1,7 @@ package main import ( + "log" "os" "github.com/gdamore/tcell/v2" @@ -12,8 +13,8 @@ const ( type sixelScreen struct { lastFile string - lastWinW int - lastWinH int + lastWinW int + lastWinH int } func (sxs *sixelScreen) clearSixel(win *win, screen tcell.Screen, filePath string) { @@ -34,16 +35,16 @@ func (sxs *sixelScreen) printSixel(win *win, screen tcell.Screen, reg *reg) { tty, ok := screen.Tty() if !ok { - screen.Fini() + log.Printf("returning underlying tty failed during sixel render") + return } - screen.LockRegion(win.x, win.y, win.w, win.h, true) - // Get the terminfo for our current terminal ti, err := tcell.LookupTerminfo(os.Getenv("TERM")) if err != nil { - screen.Fini() + log.Printf("terminal lookup failed during sixel render %s", err) } + screen.LockRegion(win.x, win.y, win.w, win.h, true) // Move the cursor to our draw position ti.TPuts(tty, ti.TGoto(win.x, win.y)) From b485d75794fd746b3f6918dd9bb86b6c97aa37ed Mon Sep 17 00:00:00 2001 From: Mariusz Kuchta Date: Sun, 6 Apr 2025 20:47:05 +0300 Subject: [PATCH 3/9] Lock regions based on sixel dimensions This fixes issues related to clearing the previous sixel data --- eval.go | 2 -- misc.go | 5 +++-- sixel.go | 30 ++++++++++++++++++++++-------- ui.go | 4 ++-- 4 files changed, 27 insertions(+), 14 deletions(-) diff --git a/eval.go b/eval.go index 3ee74881..0c02070a 100644 --- a/eval.go +++ b/eval.go @@ -91,8 +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.loadFile(app, true) } diff --git a/misc.go b/misc.go index 7dcc74c8..d08bfb75 100644 --- a/misc.go +++ b/misc.go @@ -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 ( diff --git a/sixel.go b/sixel.go index b0cec2bc..21318eb2 100644 --- a/sixel.go +++ b/sixel.go @@ -3,6 +3,7 @@ package main import ( "log" "os" + "strconv" "github.com/gdamore/tcell/v2" ) @@ -13,16 +14,20 @@ const ( type sixelScreen struct { lastFile string + lastSxW int + lastSxH int lastWinW int lastWinH int } -func (sxs *sixelScreen) clearSixel(win *win, screen tcell.Screen, filePath string) { - if filePath != sxs.lastFile || win.w != sxs.lastWinW || win.h != sxs.lastWinH { - screen.LockRegion(win.x, win.y, win.w, win.h, false) +func (sxs *sixelScreen) unlockSixel(win *win, screen tcell.Screen, filePath string) { + if filePath != "" && (filePath != sxs.lastFile || win.w != sxs.lastWinW || win.h != sxs.lastWinH) { + screen.LockRegion(win.x, win.y, sxs.lastSxW, sxs.lastSxH, false) + } } + func (sxs *sixelScreen) printSixel(win *win, screen tcell.Screen, reg *reg) { if reg.path == sxs.lastFile && win.w == sxs.lastWinW && win.h == sxs.lastWinH { @@ -32,24 +37,33 @@ func (sxs *sixelScreen) printSixel(win *win, screen tcell.Screen, reg *reg) { sxs.lastFile = "" return } - tty, ok := screen.Tty() if !ok { log.Printf("returning underlying tty failed during sixel render") return } - - // Get the terminfo for our current terminal ti, err := tcell.LookupTerminfo(os.Getenv("TERM")) if err != nil { log.Printf("terminal lookup failed during sixel render %s", err) + return + } + v, _ := tty.WindowSize() + w, h := v.CellDimensions() + matches := reSixelSize.FindStringSubmatch(*reg.sixel) + if matches == nil { + log.Printf("sixel dimensions cannot be looked up") + return } - screen.LockRegion(win.x, win.y, win.w, win.h, true) + iw, _ := strconv.Atoi(matches[1]) + ih, _ := strconv.Atoi(matches[2]) - // Move the cursor to our draw position + // width and height are -1 to avoid showing half filled sixels + screen.LockRegion(win.x, win.y, iw/w-1, ih/h-1, true) ti.TPuts(tty, ti.TGoto(win.x, win.y)) ti.TPuts(tty, *reg.sixel) sxs.lastFile = reg.path + sxs.lastSxW = iw + sxs.lastSxH = ih sxs.lastWinW = win.w sxs.lastWinH = win.h } diff --git a/ui.go b/ui.go index 802243be..f7694d0e 100644 --- a/ui.go +++ b/ui.go @@ -660,7 +660,7 @@ func newUI(screen tcell.Screen) *ui { styles: parseStyles(), icons: parseIcons(), currentFile: "", - sxScreen: sixelScreen{"", 0, 0}, + sxScreen: sixelScreen{"", 0, 0, 0, 0}, } go ui.pollEvents() @@ -1079,7 +1079,7 @@ func (ui *ui) draw(nav *nav) { curr, err := nav.currFile() if err == nil { preview := ui.wins[len(ui.wins)-1] - ui.sxScreen.clearSixel(preview, ui.screen, curr.path) + ui.sxScreen.unlockSixel(preview, ui.screen, curr.path) if curr.IsDir() { ui.sxScreen.lastFile = "" preview.printDir(ui, ui.dirPrev, &context, From 49f160576feed8632ff3dd5f010b8b06c64fb29f Mon Sep 17 00:00:00 2001 From: Mariusz Kuchta Date: Thu, 10 Apr 2025 18:51:54 +0300 Subject: [PATCH 4/9] name changes and extra error handeling --- sixel.go | 20 ++++++++++---------- ui.go | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/sixel.go b/sixel.go index 21318eb2..f533edf5 100644 --- a/sixel.go +++ b/sixel.go @@ -14,15 +14,13 @@ const ( type sixelScreen struct { lastFile string - lastSxW int - lastSxH int lastWinW int lastWinH int } -func (sxs *sixelScreen) unlockSixel(win *win, screen tcell.Screen, filePath string) { - if filePath != "" && (filePath != sxs.lastFile || win.w != sxs.lastWinW || win.h != sxs.lastWinH) { - screen.LockRegion(win.x, win.y, sxs.lastSxW, sxs.lastSxH, false) +func (sxs *sixelScreen) clearSixel(win *win, screen tcell.Screen, filePath string) { + if sxs.lastFile != "" && (filePath != sxs.lastFile || win.w != sxs.lastWinW || win.h != sxs.lastWinH) { + screen.LockRegion(win.x, win.y, win.w, win.h, false) } @@ -47,8 +45,12 @@ func (sxs *sixelScreen) printSixel(win *win, screen tcell.Screen, reg *reg) { log.Printf("terminal lookup failed during sixel render %s", err) return } - v, _ := tty.WindowSize() - w, h := v.CellDimensions() + ws, err := tty.WindowSize() + if err != nil { + log.Printf("window size lookup failed during sixel render %s", err) + return + } + cw, ch := ws.CellDimensions() matches := reSixelSize.FindStringSubmatch(*reg.sixel) if matches == nil { log.Printf("sixel dimensions cannot be looked up") @@ -58,12 +60,10 @@ func (sxs *sixelScreen) printSixel(win *win, screen tcell.Screen, reg *reg) { ih, _ := strconv.Atoi(matches[2]) // width and height are -1 to avoid showing half filled sixels - screen.LockRegion(win.x, win.y, iw/w-1, ih/h-1, true) + screen.LockRegion(win.x, win.y, iw/cw-1, ih/ch-1, true) ti.TPuts(tty, ti.TGoto(win.x, win.y)) ti.TPuts(tty, *reg.sixel) sxs.lastFile = reg.path - sxs.lastSxW = iw - sxs.lastSxH = ih sxs.lastWinW = win.w sxs.lastWinH = win.h } diff --git a/ui.go b/ui.go index f7694d0e..802243be 100644 --- a/ui.go +++ b/ui.go @@ -660,7 +660,7 @@ func newUI(screen tcell.Screen) *ui { styles: parseStyles(), icons: parseIcons(), currentFile: "", - sxScreen: sixelScreen{"", 0, 0, 0, 0}, + sxScreen: sixelScreen{"", 0, 0}, } go ui.pollEvents() @@ -1079,7 +1079,7 @@ func (ui *ui) draw(nav *nav) { curr, err := nav.currFile() if err == nil { preview := ui.wins[len(ui.wins)-1] - ui.sxScreen.unlockSixel(preview, ui.screen, curr.path) + ui.sxScreen.clearSixel(preview, ui.screen, curr.path) if curr.IsDir() { ui.sxScreen.lastFile = "" preview.printDir(ui, ui.dirPrev, &context, From 17eb77af67957bca9ecbe8653748d5c9208b17ea Mon Sep 17 00:00:00 2001 From: Mariusz Kuchta Date: Sat, 12 Apr 2025 01:50:45 +0300 Subject: [PATCH 5/9] Apply force clearing changes and change log messages --- eval.go | 7 + sixel.go | 50 +- ui-k.go | 1649 ------------------------------------------------------ 3 files changed, 33 insertions(+), 1673 deletions(-) delete mode 100644 ui-k.go diff --git a/eval.go b/eval.go index 6e82e04d..8af2d8a1 100644 --- a/eval.go +++ b/eval.go @@ -154,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) } @@ -171,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 { @@ -1416,6 +1422,7 @@ func (e *callExpr) eval(app *app, args []string) { } if gOpts.sixel { clear(app.nav.regCache) + app.ui.sxScreen.forceClear = true } for _, dir := range app.nav.dirs { dir.boundPos(app.nav.height) diff --git a/sixel.go b/sixel.go index f533edf5..ab300c09 100644 --- a/sixel.go +++ b/sixel.go @@ -8,62 +8,64 @@ import ( "github.com/gdamore/tcell/v2" ) -const ( - gSixelBegin = "\033P" -) +const gSixelBegin = "\033P" type sixelScreen struct { - lastFile string - lastWinW int - lastWinH int + lastFile string + lastWin win + forceClear bool } func (sxs *sixelScreen) clearSixel(win *win, screen tcell.Screen, filePath string) { - if sxs.lastFile != "" && (filePath != sxs.lastFile || win.w != sxs.lastWinW || win.h != sxs.lastWinH) { - screen.LockRegion(win.x, win.y, win.w, win.h, false) - + 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) } - } func (sxs *sixelScreen) printSixel(win *win, screen tcell.Screen, reg *reg) { - - if reg.path == sxs.lastFile && win.w == sxs.lastWinW && win.h == sxs.lastWinH { + if reg.path == sxs.lastFile && *win == sxs.lastWin && !sxs.forceClear { return } + if reg.sixel == nil { sxs.lastFile = "" return } - tty, ok := screen.Tty() - if !ok { - log.Printf("returning underlying tty failed during sixel render") - return - } + ti, err := tcell.LookupTerminfo(os.Getenv("TERM")) if err != nil { - log.Printf("terminal lookup failed during sixel render %s", err) + log.Printf("sixel: failed to look up term into %s", err) return } + + tty, ok := screen.Tty() + if !ok { + log.Printf("sixel: failed to get tty") + return + } + ws, err := tty.WindowSize() if err != nil { - log.Printf("window size lookup failed during sixel render %s", err) + log.Printf("sixel: failed to get window size %s", err) return } cw, ch := ws.CellDimensions() + matches := reSixelSize.FindStringSubmatch(*reg.sixel) if matches == nil { - log.Printf("sixel dimensions cannot be looked up") + log.Printf("sixel: failed to get image size") return } iw, _ := strconv.Atoi(matches[1]) ih, _ := strconv.Atoi(matches[2]) - // width and height are -1 to avoid showing half filled sixels - screen.LockRegion(win.x, win.y, iw/cw-1, ih/ch-1, true) + // clear sixel area first before drawing the image to prevent residue from + // the previous preview + 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.lastFile = reg.path - sxs.lastWinW = win.w - sxs.lastWinH = win.h + sxs.lastWin = *win + sxs.forceClear = false } diff --git a/ui-k.go b/ui-k.go deleted file mode 100644 index 1e61b6dc..00000000 --- a/ui-k.go +++ /dev/null @@ -1,1649 +0,0 @@ -package main - -import ( - "bytes" - "fmt" - "log" - "os" - "path/filepath" - "sort" - "strconv" - "strings" - "text/tabwriter" - "time" - "unicode" - "unicode/utf8" - - "github.com/gdamore/tcell/v2" - "github.com/mattn/go-runewidth" - "golang.org/x/term" -) - -const gEscapeCode = 27 - -var gKeyVal = map[tcell.Key]string{ - tcell.KeyEnter: "", - tcell.KeyBackspace: "", - tcell.KeyTab: "", - tcell.KeyBacktab: "", - tcell.KeyEsc: "", - tcell.KeyBackspace2: "", - tcell.KeyDelete: "", - tcell.KeyInsert: "", - tcell.KeyUp: "", - tcell.KeyDown: "", - tcell.KeyLeft: "", - tcell.KeyRight: "", - tcell.KeyHome: "", - tcell.KeyEnd: "", - tcell.KeyUpLeft: "", - tcell.KeyUpRight: "", - tcell.KeyDownLeft: "", - tcell.KeyDownRight: "", - tcell.KeyCenter: "
", - tcell.KeyPgDn: "", - tcell.KeyPgUp: "", - tcell.KeyClear: "", - tcell.KeyExit: "", - tcell.KeyCancel: "", - tcell.KeyPause: "", - tcell.KeyPrint: "", - tcell.KeyF1: "", - tcell.KeyF2: "", - tcell.KeyF3: "", - tcell.KeyF4: "", - tcell.KeyF5: "", - tcell.KeyF6: "", - tcell.KeyF7: "", - tcell.KeyF8: "", - tcell.KeyF9: "", - tcell.KeyF10: "", - tcell.KeyF11: "", - tcell.KeyF12: "", - tcell.KeyF13: "", - tcell.KeyF14: "", - tcell.KeyF15: "", - tcell.KeyF16: "", - tcell.KeyF17: "", - tcell.KeyF18: "", - tcell.KeyF19: "", - tcell.KeyF20: "", - tcell.KeyF21: "", - tcell.KeyF22: "", - tcell.KeyF23: "", - tcell.KeyF24: "", - tcell.KeyF25: "", - tcell.KeyF26: "", - tcell.KeyF27: "", - tcell.KeyF28: "", - tcell.KeyF29: "", - tcell.KeyF30: "", - tcell.KeyF31: "", - tcell.KeyF32: "", - tcell.KeyF33: "", - tcell.KeyF34: "", - tcell.KeyF35: "", - tcell.KeyF36: "", - tcell.KeyF37: "", - tcell.KeyF38: "", - tcell.KeyF39: "", - tcell.KeyF40: "", - tcell.KeyF41: "", - tcell.KeyF42: "", - tcell.KeyF43: "", - tcell.KeyF44: "", - tcell.KeyF45: "", - tcell.KeyF46: "", - tcell.KeyF47: "", - tcell.KeyF48: "", - tcell.KeyF49: "", - tcell.KeyF50: "", - tcell.KeyF51: "", - tcell.KeyF52: "", - tcell.KeyF53: "", - tcell.KeyF54: "", - tcell.KeyF55: "", - tcell.KeyF56: "", - tcell.KeyF57: "", - tcell.KeyF58: "", - tcell.KeyF59: "", - tcell.KeyF60: "", - tcell.KeyF61: "", - tcell.KeyF62: "", - tcell.KeyF63: "", - tcell.KeyF64: "", - tcell.KeyCtrlA: "", - tcell.KeyCtrlB: "", - tcell.KeyCtrlC: "", - tcell.KeyCtrlD: "", - tcell.KeyCtrlE: "", - tcell.KeyCtrlF: "", - tcell.KeyCtrlG: "", - tcell.KeyCtrlJ: "", - tcell.KeyCtrlK: "", - tcell.KeyCtrlL: "", - tcell.KeyCtrlN: "", - tcell.KeyCtrlO: "", - tcell.KeyCtrlP: "", - tcell.KeyCtrlQ: "", - tcell.KeyCtrlR: "", - tcell.KeyCtrlS: "", - tcell.KeyCtrlT: "", - tcell.KeyCtrlU: "", - tcell.KeyCtrlV: "", - tcell.KeyCtrlW: "", - tcell.KeyCtrlX: "", - tcell.KeyCtrlY: "", - tcell.KeyCtrlZ: "", - tcell.KeyCtrlSpace: "", - tcell.KeyCtrlUnderscore: "", - tcell.KeyCtrlRightSq: "", - tcell.KeyCtrlBackslash: "", - tcell.KeyCtrlCarat: "", -} - -var gValKey map[string]tcell.Key - -func init() { - gValKey = make(map[string]tcell.Key, len(gKeyVal)) - for k, v := range gKeyVal { - gValKey[v] = k - } -} - -type win struct { - w, h, x, y int -} - -func newWin(w, h, x, y int) *win { - return &win{w, h, x, y} -} - -func (win *win) renew(w, h, x, y int) { - win.w, win.h, win.x, win.y = w, h, x, y -} - -func printLength(s string) int { - ind := 0 - off := 0 - slen := len(s) - for i := 0; i < slen; i++ { - r, w := utf8.DecodeRuneInString(s[i:]) - - if r == gEscapeCode && i+1 < slen && s[i+1] == '[' { - j := strings.IndexAny(s[i:min(slen, i+64)], "mK") - if j == -1 { - continue - } - - i += j - continue - } - - i += w - 1 - - if r == '\t' { - ind += gOpts.tabstop - (ind-off)%gOpts.tabstop - } else { - ind += runewidth.RuneWidth(r) - } - } - - return ind -} - -func (win *win) print(screen tcell.Screen, x, y int, st tcell.Style, s string) tcell.Style { - off := x - var comb []rune - slen := len(s) - for i := 0; i < slen; i++ { - r, w := utf8.DecodeRuneInString(s[i:]) - - if r == gEscapeCode && i+1 < slen && s[i+1] == '[' { - j := strings.IndexAny(s[i:min(slen, i+64)], "mK") - if j == -1 { - continue - } - if s[i+j] == 'm' { - st = applyAnsiCodes(s[i+2:i+j], st) - } - - i += j - continue - } - - for { - rc, wc := utf8.DecodeRuneInString(s[i+w:]) - if !unicode.Is(unicode.Mn, rc) { - break - } - comb = append(comb, rc) - i += wc - } - - if x < win.w { - screen.SetContent(win.x+x, win.y+y, r, comb, st) - comb = nil - } - - i += w - 1 - - if r == '\t' { - ind := gOpts.tabstop - (x-off)%gOpts.tabstop - for i := 0; i < ind && x+i < win.w; i++ { - screen.SetContent(win.x+x+i, win.y+y, ' ', nil, st) - } - x += ind - } else { - x += runewidth.RuneWidth(r) - } - } - - return st -} - -func (win *win) printf(screen tcell.Screen, x, y int, st tcell.Style, format string, a ...any) { - win.print(screen, x, y, st, fmt.Sprintf(format, a...)) -} - -func (win *win) printLine(screen tcell.Screen, x, y int, st tcell.Style, s string) { - win.printf(screen, x, y, st, "%s%*s", s, win.w-printLength(s), "") -} - -func (win *win) printRight(screen tcell.Screen, y int, st tcell.Style, s string) { - win.print(screen, win.w-printLength(s), y, st, s) -} - -func (win *win) printReg(screen tcell.Screen, reg *reg, previewLoading bool, sxs *sixelScreen) { - if reg == nil { - return - } - - st := tcell.StyleDefault - - if reg.loading { - if previewLoading { - st = st.Reverse(true) - win.print(screen, 2, 0, st, "loading...") - } - return - } - - for i, l := range reg.lines { - if i > win.h-1 { - break - } - - st = win.print(screen, 2, i, st, l) - } - - sxs.printSixel(win, screen, reg) -} - -var gThisYear = time.Now().Year() - -func infotimefmt(t time.Time) string { - if t.Year() == gThisYear { - return t.Format(gOpts.infotimefmtnew) - } - return t.Format(gOpts.infotimefmtold) -} - -func fileInfo(f *file, d *dir, userWidth int, groupWidth int) string { - var info strings.Builder - - for _, s := range getInfo(d.path) { - switch s { - case "size": - if f.IsDir() && getDirCounts(d.path) { - switch { - case f.dirCount < -1: - info.WriteString(" !") - case f.dirCount < 0: - info.WriteString(" ?") - case f.dirCount < 1000: - fmt.Fprintf(&info, " %4d", f.dirCount) - default: - info.WriteString(" 999+") - } - continue - } - - var sz string - if f.IsDir() && f.dirSize < 0 { - sz = "-" - } else { - sz = humanize(f.TotalSize()) - } - fmt.Fprintf(&info, " %4s", sz) - case "time": - fmt.Fprintf(&info, " %*s", max(len(gOpts.infotimefmtnew), len(gOpts.infotimefmtold)), infotimefmt(f.ModTime())) - case "atime": - fmt.Fprintf(&info, " %*s", max(len(gOpts.infotimefmtnew), len(gOpts.infotimefmtold)), infotimefmt(f.accessTime)) - case "ctime": - fmt.Fprintf(&info, " %*s", max(len(gOpts.infotimefmtnew), len(gOpts.infotimefmtold)), infotimefmt(f.changeTime)) - case "perm": - info.WriteString(" " + f.FileInfo.Mode().String()) - case "user": - fmt.Fprintf(&info, " %-*s", userWidth, userName(f.FileInfo)) - case "group": - fmt.Fprintf(&info, " %-*s", groupWidth, groupName(f.FileInfo)) - default: - log.Printf("unknown info type: %s", s) - } - } - - return info.String() -} - -type dirContext struct { - selections map[string]int - saves map[string]bool - tags map[string]string -} - -type dirRole byte - -const ( - Active dirRole = iota - Parent - Preview -) - -type dirStyle struct { - colors styleMap - icons iconMap - role dirRole -} - -func (win *win) printDir(ui *ui, dir *dir, context *dirContext, dirStyle *dirStyle) { - if win.w < 5 || dir == nil { - return - } - - messageStyle := tcell.StyleDefault.Reverse(true) - - if dir.noPerm { - win.print(ui.screen, 2, 0, messageStyle, "permission denied") - return - } - fileslen := len(dir.files) - if dir.loading && fileslen == 0 { - win.print(ui.screen, 2, 0, messageStyle, "loading...") - return - } - - if fileslen == 0 { - win.print(ui.screen, 2, 0, messageStyle, "empty") - return - } - - beg := max(dir.ind-dir.pos, 0) - end := min(beg+win.h, fileslen) - - if beg > end { - return - } - - var lnwidth int - - if dirStyle.role == Active && (gOpts.number || gOpts.relativenumber) { - lnwidth = 1 - if gOpts.number && gOpts.relativenumber { - lnwidth++ - } - for j := 10; j <= fileslen; j *= 10 { - lnwidth++ - } - } - - var userWidth int - var groupWidth int - - // Only fetch user/group widths if configured to display them - for _, s := range getInfo(dir.path) { - switch s { - case "user": - userWidth = getUserWidth(dir, beg, end) - case "group": - groupWidth = getGroupWidth(dir, beg, end) - } - - if userWidth > 0 && groupWidth > 0 { - break - } - } - - for i, f := range dir.files[beg:end] { - st := dirStyle.colors.get(f) - - if lnwidth > 0 { - var ln string - - if gOpts.number && (!gOpts.relativenumber) { - ln = fmt.Sprintf("%*d", lnwidth, i+1+beg) - } else if gOpts.relativenumber { - switch { - case i < dir.pos: - ln = fmt.Sprintf("%*d", lnwidth, dir.pos-i) - case i > dir.pos: - ln = fmt.Sprintf("%*d", lnwidth, i-dir.pos) - case gOpts.number: - ln = fmt.Sprintf("%*d ", lnwidth-1, i+1+beg) - default: - ln = fmt.Sprintf("%*d", lnwidth, 0) - } - } - - win.print(ui.screen, 0, i, tcell.StyleDefault, fmt.Sprintf(optionToFmtstr(gOpts.numberfmt), ln)) - } - - path := filepath.Join(dir.path, f.Name()) - - if _, ok := context.selections[path]; ok { - win.print(ui.screen, lnwidth, i, parseEscapeSequence(gOpts.selectfmt), " ") - } else if cp, ok := context.saves[path]; ok { - if cp { - win.print(ui.screen, lnwidth, i, parseEscapeSequence(gOpts.copyfmt), " ") - } else { - win.print(ui.screen, lnwidth, i, parseEscapeSequence(gOpts.cutfmt), " ") - } - } - - // make space for select marker, and leave another space at the end - maxWidth := win.w - lnwidth - 2 - // make extra space to separate windows if drawbox is not enabled - if !gOpts.drawbox { - maxWidth -= 1 - } - - tag := " " - if val, ok := context.tags[path]; ok && len(val) > 0 { - tag = val - } - - var icon []rune - var iconDef iconDef - if gOpts.icons { - iconDef = dirStyle.icons.get(f) - icon = append(icon, []rune(iconDef.icon)...) - icon = append(icon, ' ') - } - - // subtract space for tag and icon - maxFilenameWidth := maxWidth - 1 - runeSliceWidth(icon) - - info := fileInfo(f, dir, userWidth, groupWidth) - infolen := len(info) - showInfo := infolen > 0 && 2*infolen < maxWidth - if showInfo { - maxFilenameWidth -= infolen - } - - filename := []rune(f.Name()) - if runeSliceWidth(filename) > maxFilenameWidth { - truncatePos := (maxFilenameWidth - 1) * gOpts.truncatepct / 100 - lastPart := runeSliceWidthLastRange(filename, maxFilenameWidth-truncatePos-1) - filename = runeSliceWidthRange(filename, 0, truncatePos) - filename = append(filename, []rune(gOpts.truncatechar)...) - filename = append(filename, lastPart...) - } - for j := runeSliceWidth(filename); j < maxFilenameWidth; j++ { - filename = append(filename, ' ') - } - - if showInfo { - filename = append(filename, []rune(info)...) - } - - if i == dir.pos { - var cursorFmt string - switch dirStyle.role { - case Active: - cursorFmt = optionToFmtstr(gOpts.cursoractivefmt) - case Parent: - cursorFmt = optionToFmtstr(gOpts.cursorparentfmt) - case Preview: - cursorFmt = optionToFmtstr(gOpts.cursorpreviewfmt) - } - - // print tag separately as it can contain color escape sequences - win.print(ui.screen, lnwidth+1, i, st, fmt.Sprintf(cursorFmt, tag)) - - line := append(icon, filename...) - line = append(line, ' ') - win.print(ui.screen, lnwidth+2, i, st, fmt.Sprintf(cursorFmt, string(line))) - } else { - if tag == " " { - win.print(ui.screen, lnwidth+1, i, st, " ") - } else { - tagStr := fmt.Sprintf(optionToFmtstr(gOpts.tagfmt), tag) - win.print(ui.screen, lnwidth+1, i, tcell.StyleDefault, tagStr) - } - - if len(icon) > 0 { - iconStyle := st - if iconDef.hasStyle { - iconStyle = iconDef.style - } - win.print(ui.screen, lnwidth+2, i, iconStyle, string(icon)) - } - - line := append(filename, ' ') - win.print(ui.screen, lnwidth+2+runeSliceWidth(icon), i, st, string(line)) - } - } -} - -func getUserWidth(dir *dir, beg int, end int) int { - maxw := 0 - - for _, f := range dir.files[beg:end] { - maxw = max(len(userName(f.FileInfo)), maxw) - } - - return maxw -} - -func getGroupWidth(dir *dir, beg int, end int) int { - maxw := 0 - - for _, f := range dir.files[beg:end] { - maxw = max(len(groupName(f.FileInfo)), maxw) - } - - return maxw -} - -func getWidths(wtot int) []int { - rsum := 0 - for _, r := range gOpts.ratios { - rsum += r - } - - wlen := len(gOpts.ratios) - widths := make([]int, wlen) - - if gOpts.drawbox { - wtot -= (wlen + 1) - } - - wsum := 0 - for i := range wlen - 1 { - widths[i] = gOpts.ratios[i] * wtot / rsum - wsum += widths[i] - } - widths[wlen-1] = wtot - wsum - - return widths -} - -func getWins(screen tcell.Screen) []*win { - wtot, htot := screen.Size() - - widths := getWidths(wtot) - - wacc := 0 - wlen := len(widths) - wins := make([]*win, 0, wlen) - for i := range wlen { - if gOpts.drawbox { - wacc++ - wins = append(wins, newWin(widths[i], htot-4, wacc, 2)) - } else { - wins = append(wins, newWin(widths[i], htot-2, wacc, 1)) - } - wacc += widths[i] - } - - return wins -} - -type ui struct { - screen tcell.Screen - sxScreen sixelScreen - polling bool - wins []*win - promptWin *win - msgWin *win - menuWin *win - msg string - msgIsStat bool - regPrev *reg - dirPrev *dir - exprChan chan expr - keyChan chan string - tevChan chan tcell.Event - evChan chan tcell.Event - menu string - cmdPrefix string - cmdAccLeft []rune - cmdAccRight []rune - cmdYankBuf []rune - cmdTmp []rune - keyAcc []rune - keyCount []rune - styles styleMap - icons iconMap - currentFile string -} - -func newUI(screen tcell.Screen) *ui { - wtot, htot := screen.Size() - - ui := &ui{ - screen: screen, - polling: true, - wins: getWins(screen), - promptWin: newWin(wtot, 1, 0, 0), - msgWin: newWin(wtot, 1, 0, htot-1), - menuWin: newWin(wtot, 1, 0, htot-2), - msgIsStat: true, - exprChan: make(chan expr, 1000), - keyChan: make(chan string, 1000), - tevChan: make(chan tcell.Event, 1000), - evChan: make(chan tcell.Event, 1000), - styles: parseStyles(), - icons: parseIcons(), - currentFile: "", - sxScreen: sixelScreen{"", 0, 0}, - } - - go ui.pollEvents() - - return ui -} - -func (ui *ui) winAt(x, y int) (int, *win) { - for i := len(ui.wins) - 1; i >= 0; i-- { - w := ui.wins[i] - if x >= w.x && y >= w.y && y < w.y+w.h { - return i, w - } - } - return -1, nil -} - -func (ui *ui) pollEvents() { - var ev tcell.Event - for { - ev = ui.screen.PollEvent() - if ev == nil { - ui.polling = false - return - } - ui.tevChan <- ev - } -} - -func (ui *ui) renew() { - ui.wins = getWins(ui.screen) - - wtot, htot := ui.screen.Size() - ui.promptWin.renew(wtot, 1, 0, 0) - ui.msgWin.renew(wtot, 1, 0, htot-1) - ui.menuWin.renew(wtot, 1, 0, htot-2) -} - -func (ui *ui) sort() { - if ui.dirPrev == nil { - return - } - name := ui.dirPrev.name() - ui.dirPrev.sort() - ui.dirPrev.sel(name, ui.wins[0].h) -} - -func (ui *ui) echo(msg string) { - ui.msg = msg - ui.msgIsStat = false -} - -func (ui *ui) echomsg(msg string) { - ui.echo(msg) - log.Print(msg) -} - -func optionToFmtstr(optstr string) string { - if !strings.Contains(optstr, "%s") { - return optstr + "%s\033[0m" - } else { - return optstr - } -} - -func (ui *ui) echoerr(msg string) { - ui.echo(fmt.Sprintf(optionToFmtstr(gOpts.errorfmt), msg)) - log.Printf("error: %s", msg) -} - -func (ui *ui) echoerrf(format string, a ...any) { - ui.echoerr(fmt.Sprintf(format, a...)) -} - -// This represents the preview for a regular file. -// This can also be used to represent the preview of a directory if -// `dirpreviews` is enabled. -type reg struct { - loading bool - volatile bool - loadTime time.Time - path string - lines []string - sixel *string -} - -func (ui *ui) loadFile(app *app, volatile bool) { - if !app.nav.init { - return - } - - curr, err := app.nav.currFile() - if err != nil { - return - } - - if curr.path != ui.currentFile { - ui.currentFile = curr.path - onSelect(app) - } - - if volatile { - app.nav.previewChan <- "" - } - - if !gOpts.preview { - return - } - - if curr.Mode().IsRegular() || (curr.IsDir() && gOpts.dirpreviews) { - ui.regPrev = app.nav.loadReg(curr.path, volatile) - } else if curr.IsDir() { - ui.dirPrev = app.nav.loadDir(curr.path) - } -} - -func (ui *ui) loadFileInfo(nav *nav) { - if !nav.init { - return - } - - ui.msg = "" - ui.msgIsStat = true - - curr, err := nav.currFile() - if err != nil { - return - } - - if curr.err != nil { - ui.echoerrf("stat: %s", curr.err) - return - } - - statfmt := strings.ReplaceAll(gOpts.statfmt, "|", "\x1f") - replace := func(s string, val string) { - if val == "" { - val = "\x00" - } - statfmt = strings.ReplaceAll(statfmt, s, val) - } - replace("%p", curr.Mode().String()) - replace("%c", linkCount(curr)) - replace("%u", userName(curr)) - replace("%g", groupName(curr)) - replace("%s", humanize(curr.Size())) - replace("%S", fmt.Sprintf("%4s", humanize(curr.Size()))) - replace("%t", curr.ModTime().Format(gOpts.timefmt)) - replace("%l", curr.linkTarget) - - var fileInfo strings.Builder - for _, section := range strings.Split(statfmt, "\x1f") { - if !strings.Contains(section, "\x00") { - fileInfo.WriteString(section) - } - } - - ui.msg = fileInfo.String() -} - -func (ui *ui) drawPromptLine(nav *nav) { - st := tcell.StyleDefault - - dir := nav.currDir() - pwd := dir.path - - if strings.HasPrefix(pwd, gUser.HomeDir) { - pwd = filepath.Join("~", strings.TrimPrefix(pwd, gUser.HomeDir)) - } - - sep := string(filepath.Separator) - - var fname string - curr, err := nav.currFile() - if err == nil { - fname = filepath.Base(curr.path) - } - - var prompt string - - prompt = strings.ReplaceAll(gOpts.promptfmt, "%u", gUser.Username) - prompt = strings.ReplaceAll(prompt, "%h", gHostname) - prompt = strings.ReplaceAll(prompt, "%f", fname) - - if printLength(strings.ReplaceAll(strings.ReplaceAll(prompt, "%w", pwd), "%d", pwd)) > ui.promptWin.w { - names := strings.Split(pwd, sep) - for i := range names { - if names[i] == "" { - continue - } - r, _ := utf8.DecodeRuneInString(names[i]) - names[i] = string(r) - if printLength(strings.ReplaceAll(strings.ReplaceAll(prompt, "%w", strings.Join(names, sep)), "%d", strings.Join(names, sep))) <= ui.promptWin.w { - break - } - } - pwd = strings.Join(names, sep) - } - - prompt = strings.ReplaceAll(prompt, "%w", pwd) - if !strings.HasSuffix(pwd, sep) { - pwd += sep - } - prompt = strings.ReplaceAll(prompt, "%d", pwd) - - if len(dir.filter) != 0 { - prompt = strings.ReplaceAll(prompt, "%F", fmt.Sprint(dir.filter)) - } else { - prompt = strings.ReplaceAll(prompt, "%F", "") - } - - // spacer - avail := ui.promptWin.w - printLength(prompt) + 2 - if avail > 0 { - prompt = strings.Replace(prompt, "%S", strings.Repeat(" ", avail), 1) - } - prompt = strings.ReplaceAll(prompt, "%S", "") - - ui.promptWin.print(ui.screen, 0, 0, st, prompt) -} - -func formatRulerOpt(name string, val string) string { - // handle escape character so it doesn't mess up the ruler - val = strings.ReplaceAll(val, "\033", "\033[7m\\033\033[0m") - - // display name of builtin options for clarity - if !strings.HasPrefix(name, "lf_user_") { - return fmt.Sprintf("%s=%s", strings.TrimPrefix(name, "lf_"), val) - } - - return val -} - -func (ui *ui) drawRuler(nav *nav) { - st := tcell.StyleDefault - - dir := nav.currDir() - - ui.msgWin.print(ui.screen, 0, 0, st, ui.msg) - - tot := len(dir.files) - ind := min(dir.ind+1, tot) - hid := len(dir.allFiles) - tot - acc := string(ui.keyCount) + string(ui.keyAcc) - - copy := 0 - move := 0 - for _, cp := range nav.saves { - if cp { - copy++ - } else { - move++ - } - } - - currSelections := nav.currSelections() - - progress := []string{} - - if nav.copyTotal > 0 { - percentage := int((100 * float64(nav.copyBytes)) / float64(nav.copyTotal)) - progress = append(progress, fmt.Sprintf("[%d%%]", percentage)) - } - - if nav.moveTotal > 0 { - progress = append(progress, fmt.Sprintf("[%d/%d]", nav.moveCount, nav.moveTotal)) - } - - if nav.deleteTotal > 0 { - progress = append(progress, fmt.Sprintf("[%d/%d]", nav.deleteCount, nav.deleteTotal)) - } - - opts := getOptsMap() - - rulerfmt := strings.ReplaceAll(gOpts.rulerfmt, "|", "\x1f") - rulerfmt = reRulerSub.ReplaceAllStringFunc(rulerfmt, func(s string) string { - var result string - switch s { - case "%a": - result = acc - case "%p": - result = strings.Join(progress, " ") - case "%m": - result = fmt.Sprintf("%.d", move) - case "%c": - result = fmt.Sprintf("%.d", copy) - case "%s": - result = fmt.Sprintf("%.d", len(currSelections)) - case "%f": - result = strings.Join(dir.filter, " ") - case "%i": - result = strconv.Itoa(ind) - case "%t": - result = strconv.Itoa(tot) - case "%h": - result = strconv.Itoa(hid) - case "%d": - result = diskFree(dir.path) - default: - s = strings.TrimSuffix(strings.TrimPrefix(s, "%{"), "}") - if val, ok := opts[s]; ok { - result = formatRulerOpt(s, val) - } - } - if result == "" { - return "\x00" - } - return result - }) - var ruler strings.Builder - for _, section := range strings.Split(rulerfmt, "\x1f") { - if !strings.Contains(section, "\x00") { - ruler.WriteString(section) - } - } - ui.msgWin.printRight(ui.screen, 0, st, ruler.String()) -} - -func (ui *ui) drawBox() { - st := parseEscapeSequence(gOpts.borderfmt) - - w, h := ui.screen.Size() - - for i := 1; i < w-1; i++ { - ui.screen.SetContent(i, 1, tcell.RuneHLine, nil, st) - ui.screen.SetContent(i, h-2, tcell.RuneHLine, nil, st) - } - - for i := 2; i < h-2; i++ { - ui.screen.SetContent(0, i, tcell.RuneVLine, nil, st) - ui.screen.SetContent(w-1, i, tcell.RuneVLine, nil, st) - } - - if gOpts.roundbox { - ui.screen.SetContent(0, 1, '╭', nil, st) - ui.screen.SetContent(w-1, 1, '╮', nil, st) - ui.screen.SetContent(0, h-2, '╰', nil, st) - ui.screen.SetContent(w-1, h-2, '╯', nil, st) - } else { - ui.screen.SetContent(0, 1, tcell.RuneULCorner, nil, st) - ui.screen.SetContent(w-1, 1, tcell.RuneURCorner, nil, st) - ui.screen.SetContent(0, h-2, tcell.RuneLLCorner, nil, st) - ui.screen.SetContent(w-1, h-2, tcell.RuneLRCorner, nil, st) - } - - wacc := 0 - for wind := range len(ui.wins) - 1 { - wacc += ui.wins[wind].w + 1 - ui.screen.SetContent(wacc, 1, tcell.RuneTTee, nil, st) - for i := 2; i < h-2; i++ { - ui.screen.SetContent(wacc, i, tcell.RuneVLine, nil, st) - } - ui.screen.SetContent(wacc, h-2, tcell.RuneBTee, nil, st) - } -} - -func (ui *ui) dirOfWin(nav *nav, wind int) *dir { - wins := len(ui.wins) - if gOpts.preview { - wins-- - } - ind := len(nav.dirs) - wins + wind - if ind < 0 { - return nil - } - return nav.dirs[ind] -} - -func (ui *ui) draw(nav *nav) { - st := tcell.StyleDefault - context := dirContext{selections: nav.selections, saves: nav.saves, tags: nav.tags} - -<<<<<<< HEAD - // XXX: manual clean without flush to avoid flicker on Windows - wtot, htot := ui.screen.Size() - for i := range wtot { - for j := range htot { - ui.screen.SetContent(i, j, ' ', nil, st) - } - } -======= - ui.screen.Clear() - ui.sxScreen.sixel = nil ->>>>>>> origin/master - - ui.drawPromptLine(nav) - - wins := len(ui.wins) - if gOpts.preview { - wins-- - } - for i := range wins { - role := Parent - if i == wins-1 { - role = Active - } - if dir := ui.dirOfWin(nav, i); dir != nil { - ui.wins[i].printDir(ui, dir, &context, - &dirStyle{colors: ui.styles, icons: ui.icons, role: role}) - } - } - - switch ui.cmdPrefix { - case "": - ui.drawRuler(nav) - ui.screen.HideCursor() - case ">": - maxWidth := ui.msgWin.w - 1 // leave space for cursor at the end - prefix := runeSliceWidthRange([]rune(ui.cmdPrefix), 0, maxWidth) - left := runeSliceWidthLastRange(ui.cmdAccLeft, maxWidth-runeSliceWidth(prefix)-printLength(ui.msg)) - ui.msgWin.printLine(ui.screen, 0, 0, st, string(prefix)+ui.msg) - ui.msgWin.print(ui.screen, runeSliceWidth(prefix)+printLength(ui.msg), 0, st, string(left)+string(ui.cmdAccRight)) - ui.screen.ShowCursor(ui.msgWin.x+runeSliceWidth(prefix)+printLength(ui.msg)+runeSliceWidth(left), ui.msgWin.y) - default: - maxWidth := ui.msgWin.w - 1 // leave space for cursor at the end - prefix := runeSliceWidthRange([]rune(ui.cmdPrefix), 0, maxWidth) - left := runeSliceWidthLastRange(ui.cmdAccLeft, maxWidth-runeSliceWidth(prefix)) - ui.msgWin.printLine(ui.screen, 0, 0, st, string(prefix)+string(left)+string(ui.cmdAccRight)) - 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] -<<<<<<< HEAD - ui.sxScreen.clearSixel(preview, ui.screen, curr.path) - if curr.IsDir() { - ui.sxScreen.lastFile = "" - preview.printDir(ui, ui.dirPrev, &context, - &dirStyle{colors: ui.styles, icons: ui.icons, role: Preview}, - nav.previewLoading) - } else if curr.Mode().IsRegular() { -======= - - if curr.Mode().IsRegular() || (curr.IsDir() && gOpts.dirpreviews) { ->>>>>>> origin/master - preview.printReg(ui.screen, ui.regPrev, nav.previewLoading, &ui.sxScreen) - } else if curr.IsDir() { - preview.printDir(ui, ui.dirPrev, &context, - &dirStyle{colors: ui.styles, icons: ui.icons, role: Preview}) - } - } - } - - if gOpts.drawbox { - ui.drawBox() - } - - if ui.menu != "" { - lines := strings.Split(ui.menu, "\n") - - lines = lines[:len(lines)-1] - - ui.menuWin.h = len(lines) - 1 - ui.menuWin.y = ui.wins[0].h - ui.menuWin.h - - if gOpts.drawbox { - ui.menuWin.y += 2 - } - - ui.menuWin.printLine(ui.screen, 0, 0, st.Bold(true), lines[0]) - - for i, line := range lines[1:] { - ui.menuWin.printLine(ui.screen, 0, i+1, st, "") - ui.menuWin.print(ui.screen, 0, i+1, st, line) - } - } - - ui.screen.Show() -<<<<<<< HEAD -======= - if ui.menu == "" && ui.cmdPrefix == "" && ui.sxScreen.sixel != nil { - ui.sxScreen.lastFile = ui.regPrev.path - ui.sxScreen.showSixels() - } ->>>>>>> origin/master -} - -func findBinds(keys map[string]expr, prefix string) (binds map[string]expr, ok bool) { - binds = make(map[string]expr) - for key, expr := range keys { - if !strings.HasPrefix(key, prefix) { - continue - } - binds[key] = expr - if key == prefix { - ok = true - } - } - return -} - -func listExprMap(binds map[string]expr, title string) string { - t := new(tabwriter.Writer) - b := new(bytes.Buffer) - - keys := make([]string, 0, len(binds)) - for k := range binds { - keys = append(keys, k) - } - sort.Strings(keys) - - t.Init(b, 0, gOpts.tabstop, 2, '\t', 0) - fmt.Fprintf(t, "%s\tcommand\n", title) - for _, k := range keys { - fmt.Fprintf(t, "%s\t%v\n", k, binds[k]) - } - t.Flush() - - return b.String() -} - -func listBinds(binds map[string]expr) string { - return listExprMap(binds, "keys") -} - -func listCmds() string { - return listExprMap(gOpts.cmds, "name") -} - -func listJumps(jumps []string, ind int) string { - t := new(tabwriter.Writer) - b := new(bytes.Buffer) - - maxlength := len(strconv.Itoa(max(ind, len(jumps)-1-ind))) - - t.Init(b, 0, gOpts.tabstop, 2, '\t', 0) - fmt.Fprintln(t, " jump\tpath") - // print jumps in order of most recent, Vim uses the opposite order - for i := len(jumps) - 1; i >= 0; i-- { - switch { - case i < ind: - fmt.Fprintf(t, " %*d\t%s\n", maxlength, ind-i, jumps[i]) - case i > ind: - fmt.Fprintf(t, " %*d\t%s\n", maxlength, i-ind, jumps[i]) - default: - fmt.Fprintf(t, "> %*d\t%s\n", maxlength, 0, jumps[i]) - } - } - t.Flush() - - return b.String() -} - -func listHistory(history []cmdItem) string { - t := new(tabwriter.Writer) - b := new(bytes.Buffer) - - maxlength := len(strconv.Itoa(len(history))) - - t.Init(b, 0, gOpts.tabstop, 2, '\t', 0) - fmt.Fprintln(t, "number\tcommand") - for i, cmd := range history { - fmt.Fprintf(t, "%*d\t%s%s\n", maxlength, i+1, cmd.prefix, cmd.value) - } - t.Flush() - - return b.String() -} - -func listMarks(marks map[string]string) string { - t := new(tabwriter.Writer) - b := new(bytes.Buffer) - - keys := make([]string, 0, len(marks)) - for k := range marks { - keys = append(keys, k) - } - sort.Strings(keys) - - t.Init(b, 0, gOpts.tabstop, 2, '\t', 0) - fmt.Fprintln(t, "mark\tpath") - for _, k := range keys { - fmt.Fprintf(t, "%s\t%s\n", k, marks[k]) - } - t.Flush() - - return b.String() -} - -func listFilesInCurrDir(nav *nav) string { - if !nav.init { - return "" - } - dir := nav.currDir() - if dir.loading { - log.Printf("listFilesInCurrDir(): %s is still loading, `files` isn't ready for remote query", dir.path) - return "" - } - - b := new(strings.Builder) - for _, file := range dir.files { - fmt.Fprintln(b, file.path) - } - - return b.String() -} - -func (ui *ui) pollEvent() tcell.Event { - select { - case val := <-ui.keyChan: - var ch rune - var mod tcell.ModMask - k := tcell.KeyRune - - if key, ok := gValKey[val]; ok { - return tcell.NewEventKey(key, ch, mod) - } - - switch { - case utf8.RuneCountInString(val) == 1: - ch, _ = utf8.DecodeRuneInString(val) - case val == "": - ch = '<' - case val == "": - ch = '>' - case val == "": - ch = ' ' - case reModKey.MatchString(val): - matches := reModKey.FindStringSubmatch(val) - switch matches[1] { - case "c": - mod = tcell.ModCtrl - case "s": - mod = tcell.ModShift - case "a": - mod = tcell.ModAlt - } - val = matches[2] - if utf8.RuneCountInString(val) == 1 { - ch, _ = utf8.DecodeRuneInString(val) - break - } else if key, ok := gValKey["<"+val+">"]; ok { - k = key - break - } - fallthrough - default: - k = tcell.KeyESC - ui.echoerrf("unknown key: %s", val) - } - - return tcell.NewEventKey(k, ch, mod) - case ev := <-ui.tevChan: - return ev - } -} - -func addSpecialKeyModifier(val string, mod tcell.ModMask) string { - switch { - case !strings.HasPrefix(val, "<"): - return val - case mod == tcell.ModCtrl && !strings.HasPrefix(val, "")...) - case tev.Rune() == '>': - ui.keyAcc = append(ui.keyAcc, []rune("")...) - case tev.Rune() == ' ': - ui.keyAcc = append(ui.keyAcc, []rune("")...) - case tev.Modifiers() == tcell.ModAlt: - ui.keyAcc = append(ui.keyAcc, '<', 'a', '-', tev.Rune(), '>') - case unicode.IsDigit(tev.Rune()) && len(ui.keyAcc) == 0: - ui.keyCount = append(ui.keyCount, tev.Rune()) - default: - ui.keyAcc = append(ui.keyAcc, tev.Rune()) - } - } else { - val := gKeyVal[tev.Key()] - val = addSpecialKeyModifier(val, tev.Modifiers()) - if val == "" && len(ui.keyAcc) != 0 { - ui.keyAcc = nil - ui.keyCount = nil - ui.menu = "" - return draw - } - ui.keyAcc = append(ui.keyAcc, []rune(val)...) - } - - if len(ui.keyAcc) == 0 { - return draw - } - - binds, ok := findBinds(gOpts.keys, string(ui.keyAcc)) - - switch len(binds) { - case 0: - ui.echoerrf("unknown mapping: %s", string(ui.keyAcc)) - ui.keyAcc = nil - ui.keyCount = nil - ui.menu = "" - return draw - default: - if ok { - if len(ui.keyCount) > 0 { - c, err := strconv.Atoi(string(ui.keyCount)) - if err != nil { - log.Printf("converting command count: %s", err) - } - count = c - } - expr := gOpts.keys[string(ui.keyAcc)] - - if count != 0 { - switch e := expr.(type) { - case *callExpr: - expr = &callExpr{name: e.name, args: e.args, count: count} - case *listExpr: - expr = &listExpr{exprs: e.exprs, count: count} - } - } - - ui.keyAcc = nil - ui.keyCount = nil - ui.menu = "" - return expr - } - if gOpts.showbinds { - ui.menu = listBinds(binds) - } - return draw - } - case *tcell.EventMouse: - if ui.cmdPrefix != "" { - return nil - } - - var button string - - switch tev.Buttons() { - case tcell.Button1: - button = "" - case tcell.Button2: - button = "" - case tcell.Button3: - button = "" - case tcell.Button4: - button = "" - case tcell.Button5: - button = "" - case tcell.Button6: - button = "" - case tcell.Button7: - button = "" - case tcell.Button8: - button = "" - case tcell.WheelUp: - button = "" - case tcell.WheelDown: - button = "" - case tcell.WheelLeft: - button = "" - case tcell.WheelRight: - button = "" - case tcell.ButtonNone: - return nil - } - if tev.Modifiers() == tcell.ModCtrl { - button = "'}) - if expr, ok := gOpts.cmdkeys[val]; ok { - return expr - } - } else { - return &callExpr{"cmd-insert", []string{string(tev.Rune())}, 1} - } - } else { - val := gKeyVal[tev.Key()] - val = addSpecialKeyModifier(val, tev.Modifiers()) - if expr, ok := gOpts.cmdkeys[val]; ok { - return expr - } - } - } - return nil -} - -func (ui *ui) readEvent(ev tcell.Event, nav *nav) expr { - if ev == nil { - return nil - } - - if _, ok := ev.(*tcell.EventKey); ok && ui.cmdPrefix != "" { - return readCmdEvent(ev) - } - - return ui.readNormalEvent(ev, nav) -} - -func (ui *ui) readExpr() { - go func() { - for { - ui.evChan <- ui.pollEvent() - } - }() -} - -func (ui *ui) suspend() error { - ui.sxScreen.lastFile = "" - return ui.screen.Suspend() -} - -func (ui *ui) resume() error { - err := ui.screen.Resume() - if !ui.polling { - go ui.pollEvents() - ui.polling = true - } - return err -} - -func (ui *ui) exportMode() { - getMode := func() string { - if strings.HasPrefix(ui.cmdPrefix, "delete") { - return "delete" - } - - if strings.HasPrefix(ui.cmdPrefix, "replace") || strings.HasPrefix(ui.cmdPrefix, "create") { - return "rename" - } - - switch ui.cmdPrefix { - case "filter: ": - return "filter" - case "find: ", "find-back: ": - return "find" - case "mark-save: ", "mark-load: ", "mark-remove: ": - return "mark" - case "rename: ": - return "rename" - case "/", "?": - return "search" - case ":": - return "command" - case "$", "%", "!", "&": - return "shell" - case ">": - return "pipe" - case "": - return "normal" - default: - return "unknown" - } - } - - os.Setenv("lf_mode", getMode()) -} - -func (ui *ui) exportSizes() { - w, h := ui.screen.Size() - os.Setenv("lf_width", strconv.Itoa(w)) - os.Setenv("lf_height", strconv.Itoa(h)) -} - -func anyKey() { - fmt.Fprint(os.Stderr, gOpts.waitmsg) - defer fmt.Fprint(os.Stderr, "\n") - oldState, err := term.MakeRaw(int(os.Stdin.Fd())) - if err != nil { - panic(err) - } - defer term.Restore(int(os.Stdin.Fd()), oldState) - - b := make([]byte, 8) - os.Stdin.Read(b) -} - -func listMatches(screen tcell.Screen, matches []string, selectedInd int) string { - mlen := len(matches) - if mlen < 2 { - return "" - } - - var b strings.Builder - - wtot, _ := screen.Size() - wcol := 0 - for _, m := range matches { - wcol = max(wcol, len(m)) - } - wcol += gOpts.tabstop - wcol%gOpts.tabstop - ncol := max(wtot/wcol, 1) - - b.WriteString("possible matches\n") - - for i := 0; i < mlen; { - for j := 0; j < ncol && i < mlen; i, j = i+1, j+1 { - target := matches[i] - - if selectedInd == i { - fmt.Fprintf(&b, "\033[7m%s\033[0m%*s", target, wcol-len(target), "") - } else { - fmt.Fprintf(&b, "%s%*s", target, wcol-len(target), "") - } - } - b.WriteByte('\n') - } - - return b.String() -} From 8ebb1d7e87a2e02f04dc91c08bdc32073fe935b0 Mon Sep 17 00:00:00 2001 From: Mariusz Kuchta Date: Sun, 13 Apr 2025 20:15:25 +0300 Subject: [PATCH 6/9] Delete missleading comment --- sixel.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/sixel.go b/sixel.go index ab300c09..79ecde84 100644 --- a/sixel.go +++ b/sixel.go @@ -59,8 +59,6 @@ func (sxs *sixelScreen) printSixel(win *win, screen tcell.Screen, reg *reg) { iw, _ := strconv.Atoi(matches[1]) ih, _ := strconv.Atoi(matches[2]) - // clear sixel area first before drawing the image to prevent residue from - // the previous preview 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) From ae40ab73223dd916f3eeccc84ad5186a741d4565 Mon Sep 17 00:00:00 2001 From: Mariusz Kuchta Date: Sun, 13 Apr 2025 21:41:53 +0300 Subject: [PATCH 7/9] check pixel dimensions --- sixel.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sixel.go b/sixel.go index 79ecde84..33760eca 100644 --- a/sixel.go +++ b/sixel.go @@ -50,6 +50,10 @@ func (sxs *sixelScreen) printSixel(win *win, screen tcell.Screen, reg *reg) { return } cw, ch := ws.CellDimensions() + if cw < 0 || ch < 0 { + log.Printf("sixel: cell dimensions should not be 0") + return + } matches := reSixelSize.FindStringSubmatch(*reg.sixel) if matches == nil { From 3b0bcb60e146101f180c7379270b7ec789a353c0 Mon Sep 17 00:00:00 2001 From: Mariusz Kuchta Date: Mon, 14 Apr 2025 10:59:50 +0300 Subject: [PATCH 8/9] correct cell dimensions checking --- sixel.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sixel.go b/sixel.go index 33760eca..e29c0d27 100644 --- a/sixel.go +++ b/sixel.go @@ -50,7 +50,7 @@ func (sxs *sixelScreen) printSixel(win *win, screen tcell.Screen, reg *reg) { return } cw, ch := ws.CellDimensions() - if cw < 0 || ch < 0 { + if cw <= 0 || ch <= 0 { log.Printf("sixel: cell dimensions should not be 0") return } From 04eb37c13f5d926047e44d5e5b37609fc2fec1b7 Mon Sep 17 00:00:00 2001 From: Mariusz Kuchta Date: Mon, 14 Apr 2025 11:27:34 +0300 Subject: [PATCH 9/9] Add force clearing on image update --- app.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app.go b/app.go index e03c07e5..3ca76116 100644 --- a/app.go +++ b/app.go @@ -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 + } } }